Skip to content

Commit

Permalink
Python 3.12 compatibility (#236)
Browse files Browse the repository at this point in the history
Add compatibility for Python 3.12 and some misc code cleanup
  • Loading branch information
Archmonger committed May 7, 2024
1 parent c80f94d commit 4dc81d0
Show file tree
Hide file tree
Showing 20 changed files with 73 additions and 60 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test-src.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11"]
python-version: ["3.9", "3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- name: Use Python ${{ matrix.python-version }}
Expand Down
6 changes: 4 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ Using the following categories, list your changes in this order:

## [Unreleased]

- Nothing (yet)!
### Added

- Python 3.12 compatibility

## [3.8.0] - 2024-02-20

Expand All @@ -50,7 +52,7 @@ Using the following categories, list your changes in this order:

### Changed

- Simplified code for cascading deletion of UserData.
- Simplified code for cascading deletion of user data.

## [3.7.0] - 2024-01-30

Expand Down
6 changes: 1 addition & 5 deletions docs/examples/python/configure-asgi-middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,6 @@
application = ProtocolTypeRouter(
{
"http": django_asgi_app,
"websocket": AuthMiddlewareStack(
URLRouter(
[REACTPY_WEBSOCKET_ROUTE],
)
),
"websocket": AuthMiddlewareStack(URLRouter([REACTPY_WEBSOCKET_ROUTE])),
}
)
28 changes: 11 additions & 17 deletions docs/examples/python/use-channel-layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,20 @@


@component
def my_sender_component():
sender = use_channel_layer("my-channel-name")
def my_component():
async def receive_message(message):
set_message(message["text"])

async def submit_event(event):
async def send_message(event):
if event["key"] == "Enter":
await sender({"text": event["target"]["value"]})

return html.div(
"Message Sender: ",
html.input({"type": "text", "onKeyDown": submit_event}),
)


@component
def my_receiver_component():
message, set_message = hooks.use_state("")
sender = use_channel_layer("my-channel-name", receiver=receive_message)

async def receive_event(message):
set_message(message["text"])

use_channel_layer("my-channel-name", receiver=receive_event)

return html.div(f"Message Receiver: {message}")
return html.div(
f"Received: {message}",
html.br(),
"Send: ",
html.input({"type": "text", "onKeyDown": send_message}),
)
4 changes: 2 additions & 2 deletions docs/examples/python/user-passes-test-component-fallback.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ def my_component_fallback():
return html.div("I am NOT logged in!")


def auth_check(user):
def is_authenticated(user):
return user.is_authenticated


@user_passes_test(auth_check, fallback=my_component_fallback)
@user_passes_test(is_authenticated, fallback=my_component_fallback)
@component
def my_component():
return html.div("I am logged in!")
4 changes: 2 additions & 2 deletions docs/examples/python/user-passes-test-vdom-fallback.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
from reactpy_django.decorators import user_passes_test


def auth_check(user):
def is_authenticated(user):
return user.is_authenticated


@user_passes_test(auth_check, fallback=html.div("I am NOT logged in!"))
@user_passes_test(is_authenticated, fallback=html.div("I am NOT logged in!"))
@component
def my_component():
return html.div("I am logged in!")
4 changes: 2 additions & 2 deletions docs/examples/python/user-passes-test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
from reactpy_django.decorators import user_passes_test


def auth_check(user):
def is_authenticated(user):
return user.is_authenticated


@user_passes_test(auth_check)
@user_passes_test(is_authenticated)
@component
def my_component():
return html.div("I am logged in!")
12 changes: 6 additions & 6 deletions docs/src/reference/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -536,15 +536,15 @@ This is useful when used in combination with [`#!python use_channel_layer`](#use

??? example "See Interface"

<font size="4">**Parameters**</font>
<font size="4">**Parameters**</font>

`#!python None`
`#!python None`

<font size="4">**Returns**</font>
<font size="4">**Returns**</font>

| Type | Description |
| --- | --- |
| `#!python str` | A string containing the root component's `#!python id`. |
| Type | Description |
| --- | --- |
| `#!python str` | A string containing the root component's `#!python id`. |

---

Expand Down
2 changes: 1 addition & 1 deletion docs/src/reference/management-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ ReactPy exposes Django management commands that can be used to perform various R

Command used to manually clean ReactPy data.

When using this command without arguments, it will perform all cleaning operations. You can specify only performing specific cleaning operations through arguments such as `--sessions`.
When using this command without arguments, it will perform all cleaning operations. You can limit cleaning to specific operations through arguments such as `--sessions`.

!!! example "Terminal"

Expand Down
5 changes: 3 additions & 2 deletions docs/src/reference/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ This setting is incompatible with [`daphne`](https://github.com/django/daphne).

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.
ReactPy will use these hosts in a round-robin fashion, allowing for easy distributed computing. This is typically useful for self-hosted applications.

You can use the `#!python host` argument in your [template tag](../reference/template-tag.md#component) to manually override this default.

Expand All @@ -147,9 +147,10 @@ Configures whether to pre-render your components via HTTP, which enables SEO com

During pre-rendering, there are some key differences in behavior:

1. Only the component's first render is pre-rendered.
1. Only the component's first paint is pre-rendered.
2. All [`connection` hooks](https://reactive-python.github.io/reactpy-django/latest/reference/hooks/#connection-hooks) will provide HTTP variants.
3. The component will be non-interactive until a WebSocket connection is formed.
4. The component is re-rendered once a WebSocket connection is formed.

<!-- TODO: The comment below will become true when ReactPy no longer strips scripts from the DOM -->
<!-- 4. `#!python html.script` elements are executed twice (pre-render and post-render). -->
Expand Down
6 changes: 4 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ warn_redundant_casts = true
warn_unused_ignores = true
check_untyped_defs = true

[tool.ruff.isort]
[tool.ruff.lint.isort]
known-first-party = ["src", "tests"]

[tool.ruff]
[tool.ruff.lint]
ignore = ["E501"]

[tool.ruff]
extend-exclude = ["*/migrations/*", ".venv/*", ".eggs/*", ".nox/*", "build/*"]
line-length = 120
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Operating System :: OS Independent",
"Intended Audience :: Developers",
"Intended Audience :: Science/Research",
Expand Down
8 changes: 4 additions & 4 deletions src/reactpy_django/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
)
from uuid import uuid4

import orjson as pickle
import orjson
from channels import DEFAULT_CHANNEL_LAYER
from channels.db import database_sync_to_async
from channels.layers import InMemoryChannelLayer, get_channel_layer
Expand Down Expand Up @@ -351,7 +351,7 @@ async def _set_user_data(data: dict):

pk = get_pk(user)
model, _ = await UserDataModel.objects.aget_or_create(user_pk=pk)
model.data = pickle.dumps(data)
model.data = orjson.dumps(data)
await model.asave()

query: Query[dict | None] = use_query(
Expand Down Expand Up @@ -471,7 +471,7 @@ async def _get_user_data(

pk = get_pk(user)
model, _ = await UserDataModel.objects.aget_or_create(user_pk=pk)
data = pickle.loads(model.data) if model.data else {}
data = orjson.loads(model.data) if model.data else {}

if not isinstance(data, dict):
raise TypeError(f"Expected dict while loading user data, got {type(data)}")
Expand All @@ -489,7 +489,7 @@ async def _get_user_data(
data[key] = new_value
changed = True
if changed:
model.data = pickle.dumps(data)
model.data = orjson.dumps(data)
if save_default_data:
await model.asave()

Expand Down
4 changes: 2 additions & 2 deletions src/reactpy_django/http/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@
urlpatterns = [
path(
"web_module/<path:file>",
views.web_modules_file, # type: ignore[arg-type]
views.web_modules_file,
name="web_modules",
),
path(
"iframe/<str:dotted_path>",
views.view_to_iframe, # type: ignore[arg-type]
views.view_to_iframe,
name="view_to_iframe",
),
]
4 changes: 3 additions & 1 deletion src/reactpy_django/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
from django.db.models.signals import pre_delete
from django.dispatch import receiver

from reactpy_django.utils import get_pk


class ComponentSession(models.Model):
"""A model for storing component sessions."""
Expand Down Expand Up @@ -41,6 +43,6 @@ class UserDataModel(models.Model):
@receiver(pre_delete, sender=get_user_model(), dispatch_uid="reactpy_delete_user_data")
def delete_user_data(sender, instance, **kwargs):
"""Delete ReactPy's `UserDataModel` when a Django `User` is deleted."""
pk = getattr(instance, instance._meta.pk.name)
pk = get_pk(instance)

UserDataModel.objects.filter(user_pk=pk).delete()
2 changes: 1 addition & 1 deletion src/reactpy_django/router/resolvers.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def parse_path(path: str) -> tuple[re.Pattern[str], ConverterMapping]:
pattern += f"{re.escape(path[last_match_end:])}$"

# Replace literal `*` with "match anything" regex pattern, if it's at the end of the path
if pattern.endswith("\*$"):
if pattern.endswith(r"\*$"):
pattern = f"{pattern[:-3]}.*$"

return re.compile(pattern), converters
3 changes: 1 addition & 2 deletions src/reactpy_django/templatetags/reactpy.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from __future__ import annotations

from distutils.util import strtobool
from logging import getLogger
from uuid import uuid4

Expand All @@ -22,7 +21,7 @@
OfflineComponentMissing,
)
from reactpy_django.types import ComponentParams
from reactpy_django.utils import SyncLayout, validate_component_args
from reactpy_django.utils import SyncLayout, strtobool, validate_component_args

try:
RESOLVED_WEB_MODULES_PATH = reverse("reactpy:web_modules", args=["/"]).strip("/")
Expand Down
16 changes: 16 additions & 0 deletions src/reactpy_django/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -366,3 +366,19 @@ def render(self):
def get_pk(model):
"""Returns the value of the primary key for a Django model."""
return getattr(model, model._meta.pk.name)


def strtobool(val):
"""Convert a string representation of truth to true (1) or false (0).
True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values
are 'n', 'no', 'f', 'false', 'off', and '0'. Raises ValueError if
'val' is anything else.
"""
val = val.lower()
if val in ("y", "yes", "t", "true", "on", "1"):
return 1
elif val in ("n", "no", "f", "false", "off", "0"):
return 0
else:
raise ValueError("invalid truth value %r" % (val,))
4 changes: 2 additions & 2 deletions tests/test_app/tests/test_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import os
import socket
import sys
from distutils.util import strtobool
from functools import partial
from time import sleep

Expand All @@ -14,6 +13,7 @@
from django.test.utils import modify_settings
from playwright.sync_api import TimeoutError, sync_playwright
from reactpy_django.models import ComponentSession
from reactpy_django.utils import strtobool

GITHUB_ACTIONS = os.getenv("GITHUB_ACTIONS", "False")
CLICK_DELAY = 250 if strtobool(GITHUB_ACTIONS) else 25 # Delay in miliseconds.
Expand Down Expand Up @@ -628,7 +628,7 @@ def test_url_router(self):
path.get_attribute("data-path"),
)
string = new_page.query_selector("#router-string")
self.assertEquals("Path 12", string.text_content())
self.assertEqual("Path 12", string.text_content())

finally:
new_page.close()
Expand Down
12 changes: 6 additions & 6 deletions tests/test_app/tests/test_regex.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,25 +99,25 @@ def test_comment_regex(self):
self.assertNotRegex(r'{% component "my.component" %}', COMMENT_REGEX)

# Components surrounded by comments
self.assertEquals(
self.assertEqual(
COMMENT_REGEX.sub(
"", r'{% component "my.component" %} <!-- comment -->'
).strip(),
'{% component "my.component" %}',
)
self.assertEquals(
self.assertEqual(
COMMENT_REGEX.sub(
"", r'<!-- comment --> {% component "my.component" %}'
).strip(),
'{% component "my.component" %}',
)
self.assertEquals(
self.assertEqual(
COMMENT_REGEX.sub(
"", r'<!-- comment --> {% component "my.component" %} <!-- comment -->'
).strip(),
'{% component "my.component" %}',
)
self.assertEquals(
self.assertEqual(
COMMENT_REGEX.sub(
"",
r"""<!-- comment
Expand All @@ -130,11 +130,11 @@ def test_comment_regex(self):
)

# Components surrounded by comments
self.assertEquals(
self.assertEqual(
COMMENT_REGEX.sub("", r'<!-- {% component "my.component" %} -->'),
"",
)
self.assertEquals(
self.assertEqual(
COMMENT_REGEX.sub(
"",
r"""<!--
Expand Down

0 comments on commit 4dc81d0

Please sign in to comment.