Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ReactPy ASGI Middleware and standalone ReactPy ASGI App #1113

Merged
merged 88 commits into from
Feb 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
252489e
everything besides component_dispatch_app
Archmonger Jul 18, 2023
2546cfa
Unfinished component dispatcher
Archmonger Jul 18, 2023
16ada33
threading for disk calls
Archmonger Jul 18, 2023
bde42aa
More robust route handling
Archmonger Jul 19, 2023
fa34c31
fix carrier
Archmonger Jul 19, 2023
a936c66
Plan ahead for new websocket URL pattern
Archmonger Jul 19, 2023
a936c86
better path stuff
Archmonger Jul 19, 2023
727f3f0
use etag instead of modified header
Archmonger Jul 19, 2023
750a821
Merge remote-tracking branch 'upstream/main' into asgi-middleware
Archmonger Jul 19, 2023
daaa235
prepare recv queue for potential threading
Archmonger Jul 19, 2023
3d97430
refactoring
Archmonger Jul 19, 2023
f16e413
add backhaul thread
Archmonger Jul 19, 2023
291b538
remove icon from default head
Archmonger Jul 19, 2023
0b5ba46
small bug fixes
Archmonger Jul 19, 2023
fc99a65
fix another typo
Archmonger Jul 19, 2023
139ba98
Update asgi.py
Archmonger Jul 19, 2023
b350590
Update asgi.py
Archmonger Jul 19, 2023
473cdfd
customizable block size
Archmonger Jul 20, 2023
32b367c
Merge branch 'asgi-middleware' of https://github.com/Archmonger/react…
Archmonger Jul 20, 2023
5707fba
refactor init
Archmonger Jul 20, 2023
93d7c03
format
Archmonger Jul 20, 2023
24fb817
use starlette for static files
Archmonger Jul 20, 2023
5df567e
local ws connection
Archmonger Jul 23, 2023
57d47da
more refactoring
Archmonger Jul 23, 2023
765a6a4
fix path check
Archmonger Jul 27, 2023
3dac6bf
Merge remote-tracking branch 'upstream/main' into asgi-middleware
Archmonger Jan 22, 2025
a91ca99
reduce LOC changes
Archmonger Jan 22, 2025
ae242ba
rewrite `@reactpy/client`
Archmonger Jan 24, 2025
28e0119
New build method of `@reactpy/app`
Archmonger Jan 24, 2025
732780e
Remove legacy backend code
Archmonger Jan 24, 2025
d1fe8c7
allow extension matching in copy dir
Archmonger Jan 25, 2025
ba899d6
functional standalone app and static apps, broken dispatcher
Archmonger Jan 25, 2025
ef4938a
move utils
Archmonger Jan 25, 2025
b2a429f
Functional standalone mode
Archmonger Jan 25, 2025
4d539fc
refactoring
Archmonger Jan 25, 2025
1304126
Add Jinja to deps
Archmonger Jan 25, 2025
b23f6b6
refactoring in preparation for functional middleware
Archmonger Jan 27, 2025
3480475
Force user to add slashes to tail and head of `path_prefix`, and crea…
Archmonger Jan 27, 2025
459d3bc
more refactoring to standalone mode
Archmonger Jan 27, 2025
278790d
functional template tag
Archmonger Jan 27, 2025
a14d455
Rename location.search to location.query_string
Archmonger Jan 27, 2025
318f1fb
Add jinja to deps
Archmonger Jan 27, 2025
f98881e
Merge remote-tracking branch 'upstream/main' into asgi-middleware
Archmonger Jan 27, 2025
df3c0f7
Move some project files around
Archmonger Jan 27, 2025
bb63511
Remove `module_from_template`
Archmonger Jan 27, 2025
353185a
Remove unused import
Archmonger Jan 27, 2025
3209467
Expose new APIs
Archmonger Jan 27, 2025
31b4045
add changelog
Archmonger Jan 27, 2025
dbdfa9b
Remove accidentally commited test files
Archmonger Jan 27, 2025
130bc00
Implement ReactPyConfig(TypedDict)
Archmonger Jan 27, 2025
097bbc0
Rename pathname to path
Archmonger Jan 27, 2025
594e217
Fix typo
Archmonger Jan 27, 2025
b0822d0
Add missing gitignore
Archmonger Jan 27, 2025
ca88394
Revert changes to copy_dir.py
Archmonger Jan 27, 2025
451c651
remove useless parameter in client
Archmonger Jan 27, 2025
38e22be
Remove reactpy.core.types
Archmonger Jan 27, 2025
7cd7388
self review
Archmonger Jan 27, 2025
7650f13
Add missing docstrings
Archmonger Jan 27, 2025
1778d1c
render_reactpy_template func
Archmonger Jan 27, 2025
101a23b
if-modified-since matching
Archmonger Jan 27, 2025
ebd2676
rename to render_mount_template
Archmonger Jan 27, 2025
fbda6c5
Remove fluff from ReactPyTemplateTag
Archmonger Jan 27, 2025
df03c9d
Remove obsolete attributes from ReactPyApp
Archmonger Jan 27, 2025
ff26158
Simplier send func for ComponentDispatchApp
Archmonger Jan 27, 2025
3fe68d2
Bump to 2.0.0a0
Archmonger Jan 28, 2025
3d2941e
fix query string parsing
Archmonger Jan 29, 2025
f7b15f4
Fix backend fixture
Archmonger Jan 29, 2025
c92079a
test_standalone
Archmonger Jan 29, 2025
fd3adae
fix old tests
Archmonger Jan 29, 2025
2fe399b
fix hatch fmt error
Archmonger Jan 29, 2025
9520bf1
Fix type hints
Archmonger Jan 30, 2025
dc479c1
remove pip upgrade from workflow
Archmonger Jan 30, 2025
1a4571d
Fix test issues
Archmonger Jan 30, 2025
16edba0
Remove usage of deprecated co_lnotab
Archmonger Jan 30, 2025
65d2469
fix test_automatic_reconnect test
Archmonger Jan 30, 2025
2959d9b
fix type error
Archmonger Jan 30, 2025
5182c83
Fix flakey test_script_re_run_on_content_change
Archmonger Jan 30, 2025
a058403
Fix a few coverage hits
Archmonger Jan 31, 2025
a8c19b8
tests for middleware
Archmonger Jan 31, 2025
8581391
100% test coverage
Archmonger Jan 31, 2025
cc2836b
Remove Python 3.9 support
Archmonger Jan 31, 2025
9820c86
Add wait method to test_unregistered_root_component
Archmonger Jan 31, 2025
285f2e2
reverse sleep position
Archmonger Jan 31, 2025
71520cd
longer server shutdown timeout
Archmonger Jan 31, 2025
122c00b
self review
Archmonger Jan 31, 2025
1a9dfa1
temporarily disable docs tests
Archmonger Jan 31, 2025
0c027c6
Even higher shutdown timeout - GH sometimes lags really badly
Archmonger Jan 31, 2025
4aeb365
Allow CI to run on all PRs
Archmonger Jan 31, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/.hatch-run.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ jobs:
with:
python-version: ${{ matrix.python-version }}
- name: Install Python Dependencies
run: pip install --upgrade pip hatch uv
run: pip install --upgrade hatch uv
- name: Run Scripts
env:
NPM_CONFIG_TOKEN: ${{ secrets.node-auth-token }}
Expand Down
6 changes: 4 additions & 2 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
- main
pull_request:
branches:
- main
- "*"
schedule:
- cron: "0 0 * * 0"

Expand All @@ -27,8 +27,10 @@ jobs:
job-name: "python-{0} {1}"
run-cmd: "hatch test"
runs-on: '["ubuntu-latest", "macos-latest", "windows-latest"]'
python-version: '["3.9", "3.10", "3.11"]'
python-version: '["3.10", "3.11", "3.12", "3.13"]'
test-documentation:
# Temporarily disabled
if: 0
uses: ./.github/workflows/.hatch-run.yml
with:
job-name: "python-{0}"
Expand Down
6 changes: 3 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# --- Build Artifacts ---
src/reactpy/static/**/index.js*
src/reactpy/static/*

# --- Jupyter ---
*.ipynb_checkpoints
Expand All @@ -15,8 +15,8 @@ src/reactpy/static/**/index.js*

# --- Python ---
.hatch
.venv
venv
.venv*
venv*
MANIFEST
build
dist
Expand Down
2 changes: 1 addition & 1 deletion docs/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,6 @@ RUN sphinx-build -v -W -b html source build
# Define Entrypoint
# -----------------
ENV PORT=5000
ENV REACTPY_DEBUG_MODE=1
ENV REACTPY_DEBUG=1
ENV REACTPY_CHECK_VDOM_SPEC=0
CMD ["python", "main.py"]
2 changes: 1 addition & 1 deletion docs/docs_app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from docs_app.examples import get_normalized_example_name, load_examples
from reactpy import component
from reactpy.backend.sanic import Options, configure, use_request
from reactpy.core.types import ComponentConstructor
from reactpy.types import ComponentConstructor

THIS_DIR = Path(__file__).parent
DOCS_DIR = THIS_DIR.parent
Expand Down
16 changes: 16 additions & 0 deletions docs/source/about/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,36 @@ Changelog
Unreleased
----------

**Added**
- :pull:`1113` - Added ``reactpy.ReactPy`` that can be used to run ReactPy in standalone mode.
- :pull:`1113` - Added ``reactpy.ReactPyMiddleware`` that can be used to run ReactPy with any ASGI compatible framework.
- :pull:`1113` - Added ``reactpy.jinja.Component`` that can be used alongside ``ReactPyMiddleware`` to embed several ReactPy components into your existing application.
- :pull:`1113` - Added ``standard``, ``uvicorn``, ``jinja`` installation extras (for example ``pip install reactpy[standard]``).
- :pull:`1113` - Added support for Python 3.12 and 3.13.

**Changed**

- :pull:`1251` - Substitute client-side usage of ``react`` with ``preact``.
- :pull:`1239` - Script elements no longer support behaving like effects. They now strictly behave like plain HTML script elements.
- :pull:`1255` - The ``reactpy.html`` module has been modified to allow for auto-creation of any HTML nodes. For example, you can create a ``<data-table>`` element by calling ``html.data_table()``.
- :pull:`1256` - Change ``set_state`` comparison method to check equality with ``==`` more consistently.
- :pull:`1257` - Add support for rendering ``@component`` children within ``vdom_to_html``.
- :pull:`1113` - Renamed the ``use_location`` hook's ``search`` attribute to ``query_string``.
- :pull:`1113` - Renamed the ``use_location`` hook's ``pathname`` attribute to ``path``.
- :pull:`1113` - Renamed ``reactpy.config.REACTPY_DEBUG_MODE`` to ``reactpy.config.REACTPY_DEBUG``.

**Removed**

- :pull:`1255` - Removed the ability to import ``reactpy.html.*`` elements directly. You must now call ``html.*`` to access the elements.
- :pull:`1255` - Removed ``reactpy.sample`` module.
- :pull:`1255` - Removed ``reactpy.svg`` module. Contents previously within ``reactpy.svg.*`` can now be accessed via ``html.svg.*``.
- :pull:`1255` - Removed ``reactpy.html._`` function. Use ``html.fragment`` instead.
- :pull:`1113` - Removed ``reactpy.run``. See the documentation for the new method to run ReactPy applications.
- :pull:`1113` - Removed ``reactpy.backend.*``. See the documentation for the new method to run ReactPy applications.
- :pull:`1113` - Removed ``reactpy.core.types`` module. Use ``reactpy.types`` instead.
- :pull:`1113` - All backend related installation extras (such as ``pip install reactpy[starlette]``) have been removed.
- :pull:`1113` - Removed deprecated function ``module_from_template``.
- :pull:`1113` - Removed support for Python 3.9.

**Fixed**

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ loaded with :func:`~reactpy.web.module.export`.

.. note::

When :data:`reactpy.config.REACTPY_DEBUG_MODE` is active, named exports will be validated.
When :data:`reactpy.config.REACTPY_DEBUG` is active, named exports will be validated.

The remaining files that we need to create are concerned with creating a Python package.
We won't cover all the details here, so refer to the Setuptools_ documentation for
Expand Down
8 changes: 4 additions & 4 deletions docs/source/guides/getting-started/running-reactpy.rst
Original file line number Diff line number Diff line change
Expand Up @@ -103,29 +103,29 @@ Running ReactPy in Debug Mode
-----------------------------

ReactPy provides a debug mode that is turned off by default. This can be enabled when you
run your application by setting the ``REACTPY_DEBUG_MODE`` environment variable.
run your application by setting the ``REACTPY_DEBUG`` environment variable.

.. tab-set::

.. tab-item:: Unix Shell

.. code-block::

export REACTPY_DEBUG_MODE=1
export REACTPY_DEBUG=1
python my_reactpy_app.py

.. tab-item:: Command Prompt

.. code-block:: text

set REACTPY_DEBUG_MODE=1
set REACTPY_DEBUG=1
python my_reactpy_app.py

.. tab-item:: PowerShell

.. code-block:: powershell

$env:REACTPY_DEBUG_MODE = "1"
$env:REACTPY_DEBUG = "1"
python my_reactpy_app.py

.. danger::
Expand Down
73 changes: 27 additions & 46 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,15 @@ classifiers = [
dependencies = [
"exceptiongroup >=1.0",
"typing-extensions >=3.10",
"mypy-extensions >=0.4.3",
"anyio >=3",
"jsonpatch >=1.32",
"fastjsonschema >=2.14.5",
"requests >=2",
"colorlog >=6",
"asgiref >=3",
"lxml >=4",
"servestatic >=3.0.0",
"orjson >=3",
]
dynamic = ["version"]
urls.Changelog = "https://reactpy.dev/docs/about/changelog.html"
Expand Down Expand Up @@ -69,24 +70,15 @@ commands = [
'bun run --cwd "src/js/packages/@reactpy/client" build',
'bun install --cwd "src/js/packages/@reactpy/app"',
'bun run --cwd "src/js/packages/@reactpy/app" build',
'python "src/build_scripts/copy_dir.py" "src/js/packages/@reactpy/app/dist" "src/reactpy/static/assets"',
'python "src/build_scripts/copy_dir.py" "src/js/packages/@reactpy/app/dist" "src/reactpy/static"',
]
artifacts = []

[project.optional-dependencies]
# TODO: Nuke backends from the optional deps
all = ["reactpy[starlette,sanic,fastapi,flask,tornado,testing]"]
starlette = ["starlette >=0.13.6", "uvicorn[standard] >=0.19.0"]
sanic = [
"sanic>=21",
"sanic-cors",
"tracerite>=1.1.1",
"setuptools",
"uvicorn[standard]>=0.19.0",
]
fastapi = ["fastapi >=0.63.0", "uvicorn[standard] >=0.19.0"]
flask = ["flask", "markupsafe>=1.1.1,<2.1", "flask-cors", "flask-sock"]
tornado = ["tornado"]
all = ["reactpy[jinja,uvicorn,testing]"]
standard = ["reactpy[jinja,uvicorn]"]
jinja = ["jinja2-simple-tags", "jinja2 >=3"]
uvicorn = ["uvicorn[standard]"]
testing = ["playwright"]


Expand All @@ -103,45 +95,31 @@ extra-dependencies = [
"responses",
"playwright",
"jsonpointer",
# TODO: Nuke everything past this point after removing backends from deps
"starlette >=0.13.6",
"uvicorn[standard] >=0.19.0",
"sanic>=21",
"sanic-cors",
"sanic-testing",
"tracerite>=1.1.1",
"setuptools",
"uvicorn[standard]>=0.19.0",
"fastapi >=0.63.0",
"uvicorn[standard] >=0.19.0",
"flask",
"markupsafe>=1.1.1,<2.1",
"flask-cors",
"flask-sock",
"tornado",
"uvicorn[standard]",
"jinja2-simple-tags",
"jinja2 >=3",
"starlette",
]

[[tool.hatch.envs.hatch-test.matrix]]
python = ["3.9", "3.10", "3.11", "3.12"]
python = ["3.10", "3.11", "3.12", "3.13"]

[tool.pytest.ini_options]
addopts = """\
--strict-config
--strict-markers
"""
--strict-config
--strict-markers
"""
filterwarnings = """
ignore::DeprecationWarning:uvicorn.*
ignore::DeprecationWarning:websockets.*
ignore::UserWarning:tests.test_core.test_vdom
"""
testpaths = "tests"
xfail_strict = true
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"
log_cli_level = "INFO"

[tool.hatch.envs.default.scripts]
test-cov = "playwright install && coverage run -m pytest {args:tests}"
cov-report = ["coverage report"]
cov = ["test-cov {args}", "cov-report"]

[tool.hatch.envs.default.env-vars]
REACTPY_DEBUG_MODE = "1"

#######################################
# >>> Hatch Documentation Scripts <<< #
#######################################
Expand Down Expand Up @@ -256,10 +234,14 @@ warn_unused_ignores = true
source_pkgs = ["reactpy"]
branch = false
parallel = false
omit = ["reactpy/__init__.py"]
omit = [
"src/reactpy/__init__.py",
"src/reactpy/_console/*",
"src/reactpy/__main__.py",
]

[tool.coverage.report]
fail_under = 98
fail_under = 100
show_missing = true
skip_covered = true
sort = "Name"
Expand All @@ -269,7 +251,6 @@ exclude_also = [
"if __name__ == .__main__.:",
"if TYPE_CHECKING:",
]
omit = ["**/reactpy/__main__.py"]

[tool.ruff]
target-version = "py39"
Expand Down
2 changes: 1 addition & 1 deletion src/js/packages/@reactpy/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"typescript": "^5.7.3"
},
"scripts": {
"build": "bun build \"src/index.ts\" --outdir \"dist\" --minify --sourcemap=linked",
"build": "bun build \"src/index.ts\" --outdir=\"dist\" --minify --sourcemap=\"linked\"",
"checkTypes": "tsc --noEmit"
}
}
20 changes: 1 addition & 19 deletions src/js/packages/@reactpy/app/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1 @@
import { mount, SimpleReactPyClient } from "@reactpy/client";

function app(element: HTMLElement) {
const client = new SimpleReactPyClient({
serverLocation: {
url: document.location.origin,
route: document.location.pathname,
query: document.location.search,
},
});
mount(element, client);
}

const element = document.getElementById("app");
if (element) {
app(element);
} else {
console.error("Element with id 'app' not found");
}
export { mountReactPy } from "@reactpy/client";
83 changes: 83 additions & 0 deletions src/js/packages/@reactpy/client/src/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import logger from "./logger";
import {
ReactPyClientInterface,
ReactPyModule,
GenericReactPyClientProps,
ReactPyUrls,
} from "./types";
import { createReconnectingWebSocket } from "./websocket";

export abstract class BaseReactPyClient implements ReactPyClientInterface {
private readonly handlers: { [key: string]: ((message: any) => void)[] } = {};
protected readonly ready: Promise<void>;
private resolveReady: (value: undefined) => void;

constructor() {
this.resolveReady = () => {};
this.ready = new Promise((resolve) => (this.resolveReady = resolve));
}

onMessage(type: string, handler: (message: any) => void): () => void {
(this.handlers[type] || (this.handlers[type] = [])).push(handler);
this.resolveReady(undefined);
return () => {
this.handlers[type] = this.handlers[type].filter((h) => h !== handler);
};
}

abstract sendMessage(message: any): void;
abstract loadModule(moduleName: string): Promise<ReactPyModule>;

/**
* Handle an incoming message.
*
* This should be called by subclasses when a message is received.
*
* @param message The message to handle. The message must have a `type` property.
*/
protected handleIncoming(message: any): void {
if (!message.type) {
logger.warn("Received message without type", message);
return;
}

const messageHandlers: ((m: any) => void)[] | undefined =
this.handlers[message.type];
if (!messageHandlers) {
logger.warn("Received message without handler", message);
return;
}

messageHandlers.forEach((h) => h(message));
}
}

export class ReactPyClient
extends BaseReactPyClient
implements ReactPyClientInterface
{
urls: ReactPyUrls;
socket: { current?: WebSocket };
mountElement: HTMLElement;

constructor(props: GenericReactPyClientProps) {
super();

this.urls = props.urls;
this.mountElement = props.mountElement;
this.socket = createReconnectingWebSocket({
url: this.urls.componentUrl,
readyPromise: this.ready,
...props.reconnectOptions,
onMessage: (event) => this.handleIncoming(JSON.parse(event.data)),
});
}

sendMessage(message: any): void {
this.socket.current?.send(JSON.stringify(message));
}

loadModule(moduleName: string): Promise<ReactPyModule> {
return import(`${this.urls.jsModulesPath}${moduleName}`);
}
}
Loading