Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/main' into router
Browse files Browse the repository at this point in the history
  • Loading branch information
Archmonger committed Sep 16, 2023
2 parents df3907e + 1fbdfbb commit 9791a49
Show file tree
Hide file tree
Showing 33 changed files with 447 additions and 102 deletions.
11 changes: 6 additions & 5 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
*By submitting this pull request you agree that all contributions to this project are made under the MIT license.*

## Description

A summary of the changes.
Expand All @@ -8,6 +6,9 @@ A summary of the changes.

Please update this checklist as you complete each item:

- [ ] Tests have been included for all bug fixes or added functionality.
- [ ] The changelog has been updated with any significant changes, if necessary.
- [ ] GitHub Issues which may be closed by this PR have been linked.
- [ ] Tests have been developed for bug fixes or new functionality.
- [ ] The changelog has been updated, if necessary.
- [ ] Documentation has been updated, if necessary.
- [ ] GitHub Issues closed by this PR have been linked.

<sub>By submitting this pull request you agree that all contributions comply with this project's open source license(s).</sub>
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,18 @@ Using the following categories, list your changes in this order:

<!--changelog-start-->

## [Unreleased]

### Added

- ReactPy components can now use SEO compatible rendering!
- `settings.py:REACTPY_PRERENDER` can be set to `True` to enable this behavior by default
- Or, you can enable it on individual components via the template tag: `{% component "..." prerender="True" %}`

### Changed

- Renamed undocumented utility function `reactpy_django.utils.ComponentPreloader` to `reactpy_django.utils.RootComponentFinder`.

## [3.5.1] - 2023-09-07

### Added
Expand Down
2 changes: 1 addition & 1 deletion docs/python/template-tag-bad-view.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@


def example_view(request):
context_vars = {"dont_do_this": "example_project.my_app.components.hello_world"}
context_vars = {"my_variable": "example_project.my_app.components.hello_world"}
return render(request, "my-template.html", context_vars)
2 changes: 1 addition & 1 deletion docs/src/about/code.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Then, by running the command below you can:
- Download, build, and install Javascript dependencies

```bash linenums="0"
pip install -e . -r requirements.txt
pip install -e . -r requirements.txt --verbose --upgrade
```

!!! warning "Pitfall"
Expand Down
2 changes: 1 addition & 1 deletion docs/src/about/docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Then, by running the command below you can:
- Self-host a test server for the documentation

```bash linenums="0"
pip install -e . -r requirements.txt --upgrade
pip install -r requirements.txt --upgrade
```

Finally, to verify that everything is working properly, you can manually run the docs preview web server.
Expand Down
2 changes: 2 additions & 0 deletions docs/src/learn/add-reactpy-to-a-django-project.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ Add `#!python "reactpy_django"` to [`INSTALLED_APPS`](https://docs.djangoproject

??? note "Configure ReactPy settings (Optional)"

{% include "../reference/settings.md" start="<!--intro-start-->" end="<!--intro-end-->" %}

{% include "../reference/settings.md" start="<!--config-table-start-->" end="<!--config-table-end-->" %}

## Step 3: Configure `urls.py`
Expand Down
39 changes: 26 additions & 13 deletions docs/src/reference/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

<p class="intro" markdown>

Your **Django project's** `settings.py` can modify the behavior of ReactPy.
<!--intro-start-->

These are ReactPy-Django's default settings values. You can modify these values in your **Django project's** `settings.py` to change the behavior of ReactPy.

<!--intro-end-->

</p>

Expand All @@ -14,25 +18,34 @@ Your **Django project's** `settings.py` can modify the behavior of ReactPy.

---

## Primary Configuration

<!--config-table-start-->

These are ReactPy-Django's default settings values. You can modify these values in your **Django project's** `settings.py` to change the behavior of ReactPy.
## General Settings

| Setting | Default Value | Example Value(s) | Description |
| --- | --- | --- | --- |
| `#!python REACTPY_CACHE` | `#!python "default"` | `#!python "my-reactpy-cache"` | Cache used to store ReactPy web modules. ReactPy benefits from a fast, well indexed cache. We recommend installing [`redis`](https://redis.io/) or [`python-diskcache`](https://grantjenks.com/docs/diskcache/tutorial.html#djangocache). |
| `#!python REACTPY_DATABASE` | `#!python "default"` | `#!python "my-reactpy-database"` | Database used to store ReactPy session data. ReactPy requires a multiprocessing-safe and thread-safe database. If configuring `#!python REACTPY_DATABASE`, it is mandatory to enable our database router like such:<br/>`#!python DATABASE_ROUTERS = ["reactpy_django.database.Router", ...]` |
| `#!python REACTPY_SESSION_MAX_AGE` | `#!python 259200` | `#!python 0`, `#!python 60`, `#!python 96000` | Maximum seconds to store ReactPy session data, such as `#!python args` and `#!python kwargs` passed into your component template tag. Use `#!python 0` to not store any session data. |
| `#!python REACTPY_URL_PREFIX` | `#!python "reactpy/"` | `#!python "rp/"`, `#!python "render/reactpy/"` | The prefix to be used for all ReactPy WebSocket and HTTP URLs. |
| `#!python REACTPY_DEFAULT_QUERY_POSTPROCESSOR` | `#!python "reactpy_django.utils.django_query_postprocessor"` | `#!python None`, `#!python "example_project.my_query_postprocessor"` | Dotted path to the default `#!python reactpy_django.hooks.use_query` postprocessor function. Postprocessor functions can be async or sync, and the parameters must contain the arg `#!python data`. Set `#!python REACTPY_DEFAULT_QUERY_POSTPROCESSOR` to `#!python None` to globally disable the default postprocessor. |
| `#!python REACTPY_URL_PREFIX` | `#!python "reactpy/"` | `#!python "rp/"`, `#!python "render/reactpy/"` | The prefix used for all ReactPy WebSocket and HTTP URLs. |
| `#!python REACTPY_DEFAULT_QUERY_POSTPROCESSOR` | `#!python "reactpy_django.utils.django_query_postprocessor"` | `#!python "example_project.postprocessor"`, `#!python None` | Dotted path to the default `#!python reactpy_django.hooks.use_query` postprocessor function. Postprocessor functions can be async or sync, and the function must contain a `#!python data` parameter. Set `#!python REACTPY_DEFAULT_QUERY_POSTPROCESSOR` to `#!python None` to globally disable the default postprocessor. |
| `#!python REACTPY_AUTH_BACKEND` | `#!python "django.contrib.auth.backends.ModelBackend"` | `#!python "example_project.auth.MyModelBackend"` | Dotted path to the Django authentication backend to use for ReactPy components. This is only needed if:<br/> 1. You are using `#!python AuthMiddlewareStack` and...<br/> 2. You are using Django's `#!python AUTHENTICATION_BACKENDS` setting and...<br/> 3. Your Django user model does not define a `#!python backend` attribute. |
| `#!python REACTPY_BACKHAUL_THREAD` | `#!python False` | `#!python True` | Whether to render ReactPy components in a dedicated thread. This allows the web server to process traffic during ReactPy rendering. Vastly improves throughput with web servers such as [`hypercorn`](https://pgjones.gitlab.io/hypercorn/) and [`uvicorn`](https://www.uvicorn.org/). |
| `#!python REACTPY_DEFAULT_HOSTS` | `#!python None` | `#!python ["localhost:8000", "localhost:8001", "localhost:8002/subdir" ]` | The default host(s) that can render your ReactPy components. ReactPy will use these hosts in a round-robin fashion, allowing for easy distributed computing. You can use the `#!python host` argument in your [template tag](../reference/template-tag.md#component) as a manual override. |
| `#!python REACTPY_RECONNECT_INTERVAL` | `#!python 750` | `#!python 100`, `#!python 2500`, `#!python 6000` | Milliseconds between client reconnection attempts. This value will gradually increase if `#!python REACTPY_RECONNECT_BACKOFF_MULTIPLIER` is greater than `#!python 1`. |

## Performance Settings

| Setting | Default Value | Example Value(s) | Description |
| --- | --- | --- | --- |
| `#!python REACTPY_DATABASE` | `#!python "default"` | `#!python "my-reactpy-database"` | Multiprocessing-safe database used to store ReactPy session data. If configuring `#!python REACTPY_DATABASE`, it is mandatory to enable our database router like such:<br/>`#!python DATABASE_ROUTERS = ["reactpy_django.database.Router", ...]` |
| `#!python REACTPY_CACHE` | `#!python "default"` | `#!python "my-reactpy-cache"` | Cache used for ReactPy JavaScript modules. We recommend installing [`redis`](https://redis.io/) or [`python-diskcache`](https://grantjenks.com/docs/diskcache/tutorial.html#djangocache). |
| `#!python REACTPY_BACKHAUL_THREAD` | `#!python False` | `#!python True` | Configures whether ReactPy components are rendered in a dedicated thread. This allows the web server to process traffic during ReactPy rendering. Vastly improves throughput with web servers such as [`hypercorn`](https://pgjones.gitlab.io/hypercorn/) and [`uvicorn`](https://www.uvicorn.org/). |
| `#!python REACTPY_DEFAULT_HOSTS` | `#!python None` | `#!python ["localhost:8000", "localhost:8001", "localhost:8002/subdir"]` | The default host(s) that can render your ReactPy components. ReactPy will use these hosts in a round-robin fashion, allowing for easy distributed computing. You can use the `#!python host` argument in your [template tag](../reference/template-tag.md#component) as a manual override. |
| `#!python REACTPY_PRERENDER` | `#!python False` | `#!python True` | Configures whether to pre-render your components, which enables SEO compatibility and increases perceived responsiveness. You can use the `#!python prerender` argument in your [template tag](../reference/template-tag.md#component) as a manual override. During pre-rendering, there are some key differences in behavior:<br/> 1. Only the component's first render is pre-rendered.<br/> 2. All `#!python connection` related hooks use HTTP.<br/> 3. `#!python html.script` is executed during both pre-render and render.<br/> 4. Component is non-interactive until a WebSocket connection is formed. |

## Stability Settings

| Setting | Default Value | Example Value(s) | Description |
| --- | --- | --- | --- |
| `#!python REACTPY_RECONNECT_INTERVAL` | `#!python 750` | `#!python 100`, `#!python 2500`, `#!python 6000` | Milliseconds between client reconnection attempts. |
| `#!python REACTPY_RECONNECT_BACKOFF_MULTIPLIER` | `#!python 1.25` | `#!python 1`, `#!python 1.5`, `#!python 3` | On each reconnection attempt, the `#!python REACTPY_RECONNECT_INTERVAL` will be multiplied by this value to increase the time between attempts. You can keep time between each reconnection the same by setting this to `#!python 1`. |
| `#!python REACTPY_RECONNECT_MAX_INTERVAL` | `#!python 60000` | `#!python 10000`, `#!python 25000`, `#!python 900000` | Maximum milliseconds between client reconnection attempts. This allows setting an upper bound on how high `#!python REACTPY_RECONNECT_BACKOFF_MULTIPLIER` can increase the time between reconnection attempts. |
| `#!python REACTPY_RECONNECT_MAX_RETRIES` | `#!python 150` | `#!python 0`, `#!python 5`, `#!python 300` | Maximum number of reconnection attempts before the client gives up. |
| `#!python REACTPY_RECONNECT_BACKOFF_MULTIPLIER` | `#!python 1.25` | `#!python 1`, `#!python 1.5`, `#!python 3` | Multiplier for the time between client reconnection attempts. On each reconnection attempt, the `#!python REACTPY_RECONNECT_INTERVAL` will be multiplied by this to increase the time between attempts. You can keep time between each reconnection the same by setting this to `#!python 1`. |
| `#!python REACTPY_SESSION_MAX_AGE` | `#!python 259200` | `#!python 0`, `#!python 60`, `#!python 96000` | Maximum seconds to store ReactPy component sessions. This includes data such as `#!python *args` and `#!python **kwargs` passed into your component template tag. Use `#!python 0` to not store any session data. |

<!--config-table-end-->
15 changes: 8 additions & 7 deletions docs/src/reference/template-tag.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ This template tag can be used to insert any number of ReactPy components onto yo
| `#!python dotted_path` | `#!python str` | The dotted path to the component to render. | N/A |
| `#!python *args` | `#!python Any` | The positional arguments to provide to the component. | N/A |
| `#!python class` | `#!python str | None` | The HTML class to apply to the top-level component div. | `#!python None` |
| `#!python key` | `#!python str | None` | Force the component's root node to use a [specific key value](https://reactpy.dev/docs/guides/creating-interfaces/rendering-data/index.html#organizing-items-with-keys). Using `#!python key` within a template tag is effectively useless. | `#!python None` |
| `#!python host` | `#!python str | None` | The host to use for the ReactPy connections. If set to `#!python None`, the host will be automatically configured.<br/>Example values include: `localhost:8000`, `example.com`, `example.com/subdir` | `#!python None` |
| `#!python key` | `#!python Any` | Force the component's root node to use a [specific key value](https://reactpy.dev/docs/guides/creating-interfaces/rendering-data/index.html#organizing-items-with-keys). Using `#!python key` within a template tag is effectively useless. | `#!python None` |
| `#!python host` | `#!python str | None` | The host to use for the ReactPy connections. If unset, the host will be automatically configured.<br/>Example values include: `localhost:8000`, `example.com`, `example.com/subdir` | `#!python None` |
| `#!python prerender` | `#!python str` | If `#!python "True"`, the component will pre-rendered, which enables SEO compatibility and increases perceived responsiveness. | `#!python "False"` |
| `#!python **kwargs` | `#!python Any` | The keyword arguments to provide to the component. | N/A |

<font size="4">**Returns**</font>
Expand All @@ -37,11 +38,11 @@ This template tag can be used to insert any number of ReactPy components onto yo

<!--context-start-->

??? warning "Do not use context variables for the ReactPy component name"
??? warning "Do not use context variables for the component path"

Our preprocessor relies on the template tag containing a string.
The ReactPy component finder (`#!python reactpy_django.utils.RootComponentFinder`) requires that your component path is a string.

**Do not** use Django template/context variables for the component path. Failure to follow this warning can result in unexpected behavior.
**Do not** use Django template/context variables for the component path. Failure to follow this warning can result in unexpected behavior, such as components that will not render.

For example, **do not** do the following:

Expand All @@ -52,7 +53,7 @@ This template tag can be used to insert any number of ReactPy components onto yo
{% component "example_project.my_app.components.hello_world" recipient="World" %}

<!-- This is bad -->
{% component dont_do_this recipient="World" %}
{% component my_variable recipient="World" %}
```

=== "views.py"
Expand Down Expand Up @@ -81,7 +82,7 @@ This template tag can be used to insert any number of ReactPy components onto yo

1. If your host address are completely separate ( `origin1.com != origin2.com` ) you will need to [configure CORS headers](https://pypi.org/project/django-cors-headers/) on your main application during deployment.
2. You will not need to register ReactPy HTTP or WebSocket paths on any applications that do not perform any component rendering.
3. Your component will only be able to access `#!python *args`/`#!python **kwargs` you provide to the template tag if your applications share a common database.
3. Your component will only be able to access your template tag's `#!python *args`/`#!python **kwargs` if your applications share a common database.

<!--multiple-components-start-->

Expand Down
4 changes: 2 additions & 2 deletions docs/src/reference/utils.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@ This function is used manually register a root component with ReactPy.
{% include "../../python/register-component.py" %}
```

??? warning "Only use this within `#!python AppConfig.ready()`"
??? warning "Only use this within `#!python MyAppConfig.ready()`"

You should always call `#!python register_component` within a Django [`#!python AppConfig.ready()` method](https://docs.djangoproject.com/en/4.2/ref/applications/#django.apps.AppConfig.ready). This ensures you will retain multiprocessing compatibility, such as with ASGI web server workers.
You should always call `#!python register_component` within a Django [`#!python MyAppConfig.ready()` method](https://docs.djangoproject.com/en/dev/ref/applications/#django.apps.AppConfig.ready). This ensures you will retain multiprocessing compatibility, such as with ASGI web server workers.

??? question "Do I need to use this?"

Expand Down
33 changes: 20 additions & 13 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from logging import StreamHandler, getLogger
from pathlib import Path

from setuptools import find_packages, setup
from setuptools import find_namespace_packages, setup
from setuptools.command.develop import develop
from setuptools.command.sdist import sdist

Expand Down Expand Up @@ -47,27 +47,35 @@ def list2cmdline(cmd_list):
package = {
"name": name,
"python_requires": ">=3.9",
"packages": find_packages(str(src_dir)),
"packages": find_namespace_packages(str(src_dir)),
"package_dir": {"": "src"},
"description": "Control the web with Python",
"author": "Ryan Morshead",
"author_email": "ryan.morshead@gmail.com",
"description": "It's React, but in Python. Now with Django integration.",
"author": "Mark Bakhit",
"author_email": "archiethemonger@gmail.com",
"url": "https://github.com/reactive-python/reactpy-django",
"license": "MIT",
"platforms": "Linux, Mac OS X, Windows",
"keywords": ["interactive", "widgets", "DOM", "React"],
"keywords": [
"interactive",
"reactive",
"widgets",
"DOM",
"React",
"ReactJS",
"ReactPy",
],
"include_package_data": True,
"zip_safe": False,
"classifiers": [
"Framework :: Django",
"Framework :: Django :: 4.0",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Operating System :: OS Independent",
"Intended Audience :: Developers",
"Intended Audience :: Science/Research",
"Topic :: Multimedia :: Graphics",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Environment :: Web Environment",
],
}
Expand Down Expand Up @@ -129,18 +137,17 @@ def run(self):
log.info(f"> {list2cmdline(args_list)}")
subprocess.run(args_list, cwd=js_dir, check=True)
except Exception:
log.error("Failed to update NPM")
log.error(traceback.format_exc())
raise
log.error("Failed to update NPM, continuing anyway...")

log.info("Installing Javascript...")
try:
args_list = [npm, "install"]
log.info(f"> {list2cmdline(args_list)}")
subprocess.run(args_list, cwd=js_dir, check=True)
except Exception:
log.error("Failed to install Javascript")
log.error(traceback.format_exc())
log.error("Failed to install Javascript")
raise

log.info("Building Javascript...")
Expand All @@ -149,8 +156,8 @@ def run(self):
log.info(f"> {list2cmdline(args_list)}")
subprocess.run(args_list, cwd=js_dir, check=True)
except Exception:
log.error("Failed to build Javascript")
log.error(traceback.format_exc())
log.error("Failed to build Javascript")
raise

log.info("Successfully built Javascript")
Expand Down
Loading

0 comments on commit 9791a49

Please sign in to comment.