From f915b3fe93e6bdbbe89acb938270509cd720f8e0 Mon Sep 17 00:00:00 2001
From: Archmonger <16909269+Archmonger@users.noreply.github.com>
Date: Wed, 19 Oct 2022 00:05:10 -0700
Subject: [PATCH 01/20] v2.0.2
---
CHANGELOG.md | 24 +++++++--
docs/src/features/components.md | 94 ++++++++++++++++++++-------------
docs/src/installation/index.md | 4 +-
requirements/pkg-deps.txt | 2 +-
src/django_idom/__init__.py | 2 +-
src/django_idom/components.py | 2 +-
tests/test_app/settings.py | 1 +
7 files changed, 82 insertions(+), 47 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1a579201..359c221f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -24,13 +24,28 @@ Using the following categories, list your changes in this order:
- Nothing (yet)
-## [2.0.1]- 2022-10-18
+## [2.0.2] - 2022-10-19
+
+## Changed
+
+- Minimum `channels` version is now `4.0.0`.
+
+### Fixed
+
+- Change type hint on `view_to_component` callable to have `request` argument be optional.
+- Have docs demonstrate how to use Django-IDOM with `channels>=4.0.0`.
+
+## Security
+
+- Add note to docs about potential information exposure via `view_to_component` when using `compatibility=True`.
+
+## [2.0.1] - 2022-10-18
### Fixed
-- Ability to use `key=...` parameter on all prefabricated components
+- Ability to use `key=...` parameter on all prefabricated components.
-## [2.0.0]- 2022-10-17
+## [2.0.0] - 2022-10-17
### Added
@@ -155,7 +170,8 @@ Using the following categories, list your changes in this order:
- Support for IDOM within the Django
-[unreleased]: https://github.com/idom-team/django-idom/compare/2.0.1...HEAD
+[unreleased]: https://github.com/idom-team/django-idom/compare/2.0.2...HEAD
+[2.0.2]: https://github.com/idom-team/django-idom/compare/2.0.1...2.0.2
[2.0.1]: https://github.com/idom-team/django-idom/compare/2.0.0...2.0.1
[2.0.0]: https://github.com/idom-team/django-idom/compare/1.2.0...2.0.0
[1.2.0]: https://github.com/idom-team/django-idom/compare/1.1.0...1.2.0
diff --git a/docs/src/features/components.md b/docs/src/features/components.md
index c8a110f9..4370a115 100644
--- a/docs/src/features/components.md
+++ b/docs/src/features/components.md
@@ -41,7 +41,13 @@ Convert any Django view into a IDOM component by usng this decorator. Compatible
| --- | --- |
| `_ViewComponentConstructor` | A function that takes `request: HttpRequest | None, *args: Any, key: Key | None, **kwargs: Any` and returns an IDOM component. |
-??? warning "Existing limitations"
+??? Warning "Potential information exposure when using `compatibility = True`"
+
+ When using `compatibility` mode, IDOM automatically exposes a URL to your view.
+
+ It is your responsibility to ensure priveledged information is not leaked via this method. This can be done via directly writing conditionals into your view, or by adding decorators such as [user_passes_test](https://docs.djangoproject.com/en/dev/topics/auth/default/#django.contrib.auth.decorators.user_passes_test) to your views prior to using `view_to_component`.
+
+??? info "Existing limitations"
There are currently several limitations of using `view_to_component` that may be resolved in a future version of `django_idom`.
@@ -77,87 +83,95 @@ Convert any Django view into a IDOM component by usng this decorator. Compatible
??? question "How do I transform views from external libraries?"
- === "components.py"
-
In order to convert external views, you can utilize `view_to_component` as a function, rather than a decorator.
+ === "components.py"
+
```python linenums="1"
from idom import component, html
from django.http import HttpResponse
from django_idom.components import view_to_component
from some_library.views import example_view
- converted_view = view_to_component(example_view)
+ example_vtc = view_to_component(example_view)
@component
def my_component():
return html.div(
- converted_view(),
+ example_vtc(),
)
```
-??? question "How do I provide `args` and `kwargs` to a view?"
+??? question "How do I provide `request`, `args`, and `kwargs` to a view?"
- You can use the `args` and `kwargs` parameters to provide positional and keyworded arguments to a view.
+ **`Request`**
+
+ You can use the `request` parameter to provide the view a custom request object.
=== "components.py"
```python linenums="1"
from idom import component, html
- from django.http import HttpResponse
+ from django.http import HttpResponse, HttpRequest
from django_idom.components import view_to_component
+ example_request = HttpRequest()
+ example_request.method = "PUT"
+
@view_to_component
- def hello_world_view(request, arg1, arg2, key1=None, key2=None):
- return HttpResponse(f"Hello World! {arg1} {arg2} {key1} {key2}")
+ def hello_world_view(request):
+ return HttpResponse(f"Hello World! {request.method}")
@component
def my_component():
return html.div(
hello_world_view(
- None, # Your request object (optional)
- "value_1",
- "value_2",
- key1="abc",
- key2="123",
+ example_request,
),
)
```
-??? question "How do I provide a custom `request` object to a view?"
+ ---
- You can use the `request` parameter to provide the view a custom request object.
+ **`args` and `kwargs`**
+
+ You can use the `args` and `kwargs` parameters to provide positional and keyworded arguments to a view.
=== "components.py"
```python linenums="1"
from idom import component, html
- from django.http import HttpResponse, HttpRequest
+ from django.http import HttpResponse
from django_idom.components import view_to_component
- example_request = HttpRequest()
- example_request.method = "PUT"
-
@view_to_component
- def hello_world_view(request):
- return HttpResponse(f"Hello World! {request.method}")
+ def hello_world_view(request, arg1, arg2, key1=None, key2=None):
+ return HttpResponse(f"Hello World! {arg1} {arg2} {key1} {key2}")
@component
def my_component():
return html.div(
hello_world_view(
- example_request,
+ None, # Your request object (optional)
+ "value_1",
+ "value_2",
+ key1="abc",
+ key2="123",
),
)
```
-??? question "What is `compatibility` mode?"
+??? question "How do I use `strict_parseing`, `compatibility`, and `transforms`?"
- For views that rely on HTTP responses other than `GET` (such as `PUT`, `POST`, `PATCH`, etc), you should consider using compatibility mode to render your view within an iframe.
+ **`strict_parsing`**
- Any view can be rendered within compatibility mode. However, the `transforms`, `strict_parsing`, `request`, `args`, and `kwargs` arguments do not apply to compatibility mode.
+ By default, an exception will be generated if your view's HTML does not perfectly adhere to HTML5.
- Please note that by default the iframe is unstyled, and thus won't look pretty until you add some CSS.
+ However, there are some circumstances where you may not have control over the original HTML, so you may be unable to fix it. Or you may be relying on non-standard HTML tags such as `#!html Hello World `.
+
+ In these scenarios, you may want to rely on best-fit parsing by setting the `strict_parsing` parameter to `False`.
+
+ Note that best-fit parsing is designed to be similar to how web browsers would handle non-standard or broken HTML.
=== "components.py"
@@ -166,9 +180,9 @@ Convert any Django view into a IDOM component by usng this decorator. Compatible
from django.http import HttpResponse
from django_idom.components import view_to_component
- @view_to_component(compatibility=True)
+ @view_to_component(strict_parsing=False)
def hello_world_view(request):
- return HttpResponse("Hello World!")
+ return HttpResponse(" Hello World ")
@component
def my_component():
@@ -177,13 +191,17 @@ Convert any Django view into a IDOM component by usng this decorator. Compatible
)
```
-??? question "What is `strict_parsing`?"
- By default, an exception will be generated if your view's HTML does not perfectly adhere to HTML5.
- However, there are some circumstances where you may not have control over the original HTML, so you may be unable to fix it. Or you may be relying on non-standard HTML tags such as `#!html Hello World `.
+ ---
- In these scenarios, you may want to rely on best-fit parsing by setting the `strict_parsing` parameter to `False`.
+ **`compatibility`**
+
+ For views that rely on HTTP responses other than `GET` (such as `PUT`, `POST`, `PATCH`, etc), you should consider using compatibility mode to render your view within an iframe.
+
+ Any view can be rendered within compatibility mode. However, the `transforms`, `strict_parsing`, `request`, `args`, and `kwargs` arguments do not apply to compatibility mode.
+
+ Please note that by default the iframe is unstyled, and thus won't look pretty until you add some CSS.
=== "components.py"
@@ -192,9 +210,9 @@ Convert any Django view into a IDOM component by usng this decorator. Compatible
from django.http import HttpResponse
from django_idom.components import view_to_component
- @view_to_component(strict_parsing=False)
+ @view_to_component(compatibility=True)
def hello_world_view(request):
- return HttpResponse(" Hello World ")
+ return HttpResponse("Hello World!")
@component
def my_component():
@@ -203,9 +221,9 @@ Convert any Django view into a IDOM component by usng this decorator. Compatible
)
```
- Note that best-fit parsing is very similar to how web browsers will handle broken HTML.
+ ---
-??? question "What is `transforms`?"
+ **`transforms`**
After your view has been turned into [VDOM](https://idom-docs.herokuapp.com/docs/reference/specifications.html#vdom) (python dictionaries), `view_to_component` will call your `transforms` functions on every VDOM node.
diff --git a/docs/src/installation/index.md b/docs/src/installation/index.md
index 61782278..a59816fc 100644
--- a/docs/src/installation/index.md
+++ b/docs/src/installation/index.md
@@ -29,7 +29,7 @@ In your settings you'll need to add `django_idom` to [`INSTALLED_APPS`](https://
Django-IDOM requires ASGI in order to use Websockets.
- If you haven't enabled ASGI on your **Django project** yet, you'll need to add `channels` to `INSTALLED_APPS` and set your `ASGI_APPLICATION` variable.
+ If you haven't enabled ASGI on your **Django project** yet, you'll need to install `channels[daphne]`, add `daphne` to `INSTALLED_APPS`, then set your `ASGI_APPLICATION` variable.
Read the [Django Channels Docs](https://channels.readthedocs.io/en/stable/installation.html) for more info.
@@ -37,7 +37,7 @@ In your settings you'll need to add `django_idom` to [`INSTALLED_APPS`](https://
```python linenums="1"
INSTALLED_APPS = [
- "channels",
+ "daphne",
...
]
ASGI_APPLICATION = "example_project.asgi.application"
diff --git a/requirements/pkg-deps.txt b/requirements/pkg-deps.txt
index 8fb71a85..d2329eb6 100644
--- a/requirements/pkg-deps.txt
+++ b/requirements/pkg-deps.txt
@@ -1,4 +1,4 @@
-channels >=3.0.0
+channels >=4.0.0
idom >=0.40.2, <0.41.0
aiofile >=3.0
typing_extensions
diff --git a/src/django_idom/__init__.py b/src/django_idom/__init__.py
index 04bb0519..d1247b15 100644
--- a/src/django_idom/__init__.py
+++ b/src/django_idom/__init__.py
@@ -3,7 +3,7 @@
from django_idom.websocket.paths import IDOM_WEBSOCKET_PATH
-__version__ = "2.0.1"
+__version__ = "2.0.2"
__all__ = [
"IDOM_WEBSOCKET_PATH",
"IdomWebsocket",
diff --git a/src/django_idom/components.py b/src/django_idom/components.py
index 8c9ff3fa..edac06d6 100644
--- a/src/django_idom/components.py
+++ b/src/django_idom/components.py
@@ -20,7 +20,7 @@
class _ViewComponentConstructor(Protocol):
def __call__(
- self, request: HttpRequest | None, *args: Any, **kwargs: Any
+ self, request: HttpRequest | None = None, *args: Any, **kwargs: Any
) -> ComponentType:
...
diff --git a/tests/test_app/settings.py b/tests/test_app/settings.py
index 8f5ff109..6cb4dc82 100644
--- a/tests/test_app/settings.py
+++ b/tests/test_app/settings.py
@@ -30,6 +30,7 @@
# Application definition
INSTALLED_APPS = [
+ "daphne", # Overrides `runserver` command with an ASGI server
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
From 0d05e6ed883952ff32f370220e6d4a7564191e5b Mon Sep 17 00:00:00 2001
From: Archmonger <16909269+Archmonger@users.noreply.github.com>
Date: Thu, 20 Oct 2022 01:36:43 -0700
Subject: [PATCH 02/20] more strict type hint checking
---
pyproject.toml | 1 +
requirements/check-types.txt | 1 -
src/django_idom/components.py | 28 ++++++++++++++-------------
src/django_idom/decorators.py | 2 +-
src/django_idom/utils.py | 2 +-
src/django_idom/websocket/consumer.py | 2 +-
tests/test_app/admin.py | 2 +-
tests/test_app/components.py | 24 +++++++++++------------
tests/test_app/models.py | 4 ++--
tests/test_app/urls.py | 2 +-
tests/test_app/views.py | 27 +++++++++++++++-----------
11 files changed, 51 insertions(+), 44 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index 0103531f..6bf3a9fc 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -16,3 +16,4 @@ ignore_missing_imports = true
warn_unused_configs = true
warn_redundant_casts = true
warn_unused_ignores = true
+check_untyped_defs = true
diff --git a/requirements/check-types.txt b/requirements/check-types.txt
index c176075a..f0aa93ac 100644
--- a/requirements/check-types.txt
+++ b/requirements/check-types.txt
@@ -1,2 +1 @@
mypy
-django-stubs[compatible-mypy]
diff --git a/src/django_idom/components.py b/src/django_idom/components.py
index edac06d6..8a68a1d3 100644
--- a/src/django_idom/components.py
+++ b/src/django_idom/components.py
@@ -3,7 +3,7 @@
import json
import os
from inspect import iscoroutinefunction
-from typing import Any, Callable, Dict, Protocol, Sequence
+from typing import Any, Callable, Protocol, Sequence
from channels.db import database_sync_to_async
from django.contrib.staticfiles.finders import find
@@ -33,11 +33,11 @@ def _view_to_component(
strict_parsing: bool,
request: HttpRequest | None,
args: Sequence | None,
- kwargs: Dict | None,
+ kwargs: dict | None,
):
converted_view, set_converted_view = hooks.use_state(None)
- args = args or ()
- kwargs = kwargs or {}
+ _args: Sequence = args or ()
+ _kwargs: dict = kwargs or {}
request_obj = request
if not request_obj:
request_obj = HttpRequest()
@@ -46,7 +46,7 @@ def _view_to_component(
dotted_path = f"{view.__module__}.{view.__name__}" # type: ignore[union-attr]
dotted_path = dotted_path.replace("<", "").replace(">", "")
IDOM_VIEW_COMPONENT_IFRAMES[dotted_path] = ViewComponentIframe(
- view, args, kwargs
+ view, _args, _kwargs
)
else:
dotted_path = None
@@ -55,7 +55,7 @@ def _view_to_component(
@hooks.use_effect(
dependencies=[
json.dumps(vars(request_obj), default=lambda x: _generate_obj_name(x)),
- json.dumps([args, kwargs], default=lambda x: _generate_obj_name(x)),
+ json.dumps([_args, _kwargs], default=lambda x: _generate_obj_name(x)),
]
)
async def async_render():
@@ -68,17 +68,17 @@ async def async_render():
"src": reverse("idom:view_to_component", args=[dotted_path]),
"loading": "lazy",
}
- )
+ ) # type: ignore
)
return
# Render Check 2: Async function view
elif iscoroutinefunction(view):
- view_html = await view(request_obj, *args, **kwargs)
+ view_html = await view(request_obj, *_args, **_kwargs) # type: ignore
# Render Check 3: Async class view
elif getattr(view, "view_is_async", False):
- view_or_template_view = await view.as_view()(request_obj, *args, **kwargs)
+ view_or_template_view = await view.as_view()(request_obj, *_args, **_kwargs) # type: ignore
if getattr(view_or_template_view, "render", None): # TemplateView
view_html = await view_or_template_view.render()
else: # View
@@ -86,8 +86,8 @@ async def async_render():
# Render Check 4: Sync class view
elif getattr(view, "as_view", None):
- async_cbv = database_sync_to_async(view.as_view())
- view_or_template_view = await async_cbv(request_obj, *args, **kwargs)
+ async_cbv = database_sync_to_async(view.as_view()) # type: ignore
+ view_or_template_view = await async_cbv(request_obj, *_args, **_kwargs)
if getattr(view_or_template_view, "render", None): # TemplateView
view_html = await database_sync_to_async(view_or_template_view.render)()
else: # View
@@ -95,7 +95,9 @@ async def async_render():
# Render Check 5: Sync function view
else:
- view_html = await database_sync_to_async(view)(request_obj, *args, **kwargs)
+ view_html = await database_sync_to_async(view)(
+ request_obj, *_args, **_kwargs
+ )
# Signal that the view has been rendered
set_converted_view(
@@ -103,7 +105,7 @@ async def async_render():
view_html.content.decode("utf-8").strip(),
*transforms,
strict=strict_parsing,
- )
+ ) # type: ignore
)
# Return the view if it's been rendered via the `async_render` hook
diff --git a/src/django_idom/decorators.py b/src/django_idom/decorators.py
index 5800d220..6d01c990 100644
--- a/src/django_idom/decorators.py
+++ b/src/django_idom/decorators.py
@@ -11,7 +11,7 @@
def auth_required(
component: Callable | None = None,
auth_attribute: str = "is_active",
- fallback: ComponentType | VdomDict | None = None,
+ fallback: ComponentType | Callable | VdomDict | None = None,
) -> Callable:
"""If the user passes authentication criteria, the decorated component will be rendered.
Otherwise, the fallback component will be rendered.
diff --git a/src/django_idom/utils.py b/src/django_idom/utils.py
index 6b34145d..5744622b 100644
--- a/src/django_idom/utils.py
+++ b/src/django_idom/utils.py
@@ -70,7 +70,7 @@ def _get_loaders(self):
for e in engines.all():
if hasattr(e, "engine"):
template_source_loaders.extend(
- e.engine.get_template_loaders(e.engine.loaders)
+ e.engine.get_template_loaders(e.engine.loaders) # type: ignore
)
loaders = []
for loader in template_source_loaders:
diff --git a/src/django_idom/websocket/consumer.py b/src/django_idom/websocket/consumer.py
index cdc3eb9f..c4c78971 100644
--- a/src/django_idom/websocket/consumer.py
+++ b/src/django_idom/websocket/consumer.py
@@ -71,7 +71,7 @@ async def _run_dispatch_loop(self):
)
return
- self._idom_recv_queue = recv_queue = asyncio.Queue()
+ self._idom_recv_queue = recv_queue = asyncio.Queue() # type: ignore
try:
await serve_json_patch(
Layout(WebsocketContext(component_instance, value=socket)),
diff --git a/tests/test_app/admin.py b/tests/test_app/admin.py
index aa897829..b77adbb6 100644
--- a/tests/test_app/admin.py
+++ b/tests/test_app/admin.py
@@ -1,6 +1,6 @@
from django.contrib import admin
-from .models import TodoItem
+from test_app.models import TodoItem
@admin.register(TodoItem)
diff --git a/tests/test_app/components.py b/tests/test_app/components.py
index f0a405f0..01e0abd3 100644
--- a/tests/test_app/components.py
+++ b/tests/test_app/components.py
@@ -200,7 +200,7 @@ def todo_list():
elif add_item.error:
mutation_status = html.h2(f"Error when adding - {add_item.error}")
else:
- mutation_status = ""
+ mutation_status = "" # type: ignore
def on_submit(event):
if event["key"] == "Enter":
@@ -279,7 +279,7 @@ def _render_items(items, toggle_item):
@component
def view_to_component_sync_func_compatibility():
return html.div(
- {"id": inspect.currentframe().f_code.co_name},
+ {"id": inspect.currentframe().f_code.co_name}, # type: ignore
_view_to_component_sync_func_compatibility(key="test"),
html.hr(),
)
@@ -288,7 +288,7 @@ def view_to_component_sync_func_compatibility():
@component
def view_to_component_async_func_compatibility():
return html.div(
- {"id": inspect.currentframe().f_code.co_name},
+ {"id": inspect.currentframe().f_code.co_name}, # type: ignore
_view_to_component_async_func_compatibility(),
html.hr(),
)
@@ -297,7 +297,7 @@ def view_to_component_async_func_compatibility():
@component
def view_to_component_sync_class_compatibility():
return html.div(
- {"id": inspect.currentframe().f_code.co_name},
+ {"id": inspect.currentframe().f_code.co_name}, # type: ignore
_view_to_component_sync_class_compatibility(),
html.hr(),
)
@@ -306,7 +306,7 @@ def view_to_component_sync_class_compatibility():
@component
def view_to_component_async_class_compatibility():
return html.div(
- {"id": inspect.currentframe().f_code.co_name},
+ {"id": inspect.currentframe().f_code.co_name}, # type: ignore
_view_to_component_async_class_compatibility(),
html.hr(),
)
@@ -315,7 +315,7 @@ def view_to_component_async_class_compatibility():
@component
def view_to_component_template_view_class_compatibility():
return html.div(
- {"id": inspect.currentframe().f_code.co_name},
+ {"id": inspect.currentframe().f_code.co_name}, # type: ignore
_view_to_component_template_view_class_compatibility(),
html.hr(),
)
@@ -328,11 +328,11 @@ def view_to_component_request():
def on_click(_):
post_request = HttpRequest()
post_request.method = "POST"
- set_request(post_request)
+ set_request(post_request) # type: ignore
return html._(
html.button(
- {"id": f"{inspect.currentframe().f_code.co_name}_btn", "onClick": on_click},
+ {"id": f"{inspect.currentframe().f_code.co_name}_btn", "onClick": on_click}, # type: ignore
"Click me",
),
_view_to_component_request(request=request),
@@ -348,7 +348,7 @@ def on_click(_):
return html._(
html.button(
- {"id": f"{inspect.currentframe().f_code.co_name}_btn", "onClick": on_click},
+ {"id": f"{inspect.currentframe().f_code.co_name}_btn", "onClick": on_click}, # type: ignore
"Click me",
),
_view_to_component_args(None, success),
@@ -364,7 +364,7 @@ def on_click(_):
return html._(
html.button(
- {"id": f"{inspect.currentframe().f_code.co_name}_btn", "onClick": on_click},
+ {"id": f"{inspect.currentframe().f_code.co_name}_btn", "onClick": on_click}, # type: ignore
"Click me",
),
_view_to_component_kwargs(success=success),
@@ -376,7 +376,7 @@ def view_to_component_decorator(request):
return render(
request,
"view_to_component.html",
- {"test_name": inspect.currentframe().f_code.co_name},
+ {"test_name": inspect.currentframe().f_code.co_name}, # type: ignore
)
@@ -385,5 +385,5 @@ def view_to_component_decorator_args(request):
return render(
request,
"view_to_component.html",
- {"test_name": inspect.currentframe().f_code.co_name},
+ {"test_name": inspect.currentframe().f_code.co_name}, # type: ignore
)
diff --git a/tests/test_app/models.py b/tests/test_app/models.py
index 93d3efd2..7b6fc0f8 100644
--- a/tests/test_app/models.py
+++ b/tests/test_app/models.py
@@ -2,5 +2,5 @@
class TodoItem(models.Model):
- done = models.BooleanField()
- text = models.CharField(max_length=1000)
+ done = models.BooleanField() # type: ignore
+ text = models.CharField(max_length=1000) # type: ignore
diff --git a/tests/test_app/urls.py b/tests/test_app/urls.py
index 3038eb46..0c996ac4 100644
--- a/tests/test_app/urls.py
+++ b/tests/test_app/urls.py
@@ -27,7 +27,7 @@ class AccessUser:
has_module_perms = has_perm = __getattr__ = lambda s, *a, **kw: True
-admin.site.has_permission = lambda r: setattr(r, "user", AccessUser()) or True
+admin.site.has_permission = lambda r: setattr(r, "user", AccessUser()) or True # type: ignore
urlpatterns = [
path("", base_template),
diff --git a/tests/test_app/views.py b/tests/test_app/views.py
index cec726f5..2d16ae97 100644
--- a/tests/test_app/views.py
+++ b/tests/test_app/views.py
@@ -6,15 +6,14 @@
def base_template(request):
- context = {}
- return render(request, "base.html", context)
+ return render(request, "base.html", {})
def view_to_component_sync_func(request):
return render(
request,
"view_to_component.html",
- {"test_name": inspect.currentframe().f_code.co_name},
+ {"test_name": inspect.currentframe().f_code.co_name}, # type:ignore
)
@@ -22,7 +21,7 @@ async def view_to_component_async_func(request):
return render(
request,
"view_to_component.html",
- {"test_name": inspect.currentframe().f_code.co_name},
+ {"test_name": inspect.currentframe().f_code.co_name}, # type:ignore
)
@@ -55,7 +54,7 @@ def view_to_component_sync_func_compatibility(request):
return render(
request,
"view_to_component.html",
- {"test_name": inspect.currentframe().f_code.co_name},
+ {"test_name": inspect.currentframe().f_code.co_name}, # type:ignore
)
@@ -63,7 +62,7 @@ async def view_to_component_async_func_compatibility(request):
return await database_sync_to_async(render)(
request,
"view_to_component.html",
- {"test_name": inspect.currentframe().f_code.co_name},
+ {"test_name": inspect.currentframe().f_code.co_name}, # type:ignore
)
@@ -97,7 +96,7 @@ def view_to_component_script(request):
request,
"view_to_component_script.html",
{
- "test_name": inspect.currentframe().f_code.co_name,
+ "test_name": inspect.currentframe().f_code.co_name, # type:ignore
"status": "false",
},
)
@@ -108,14 +107,14 @@ def view_to_component_request(request):
return render(
request,
"view_to_component.html",
- {"test_name": inspect.currentframe().f_code.co_name},
+ {"test_name": inspect.currentframe().f_code.co_name}, # type:ignore
)
return render(
request,
"view_to_component.html",
{
- "test_name": inspect.currentframe().f_code.co_name,
+ "test_name": inspect.currentframe().f_code.co_name, # type:ignore
"status": "false",
"success": "false",
},
@@ -126,7 +125,10 @@ def view_to_component_args(request, success):
return render(
request,
"view_to_component.html",
- {"test_name": inspect.currentframe().f_code.co_name, "status": success},
+ {
+ "test_name": inspect.currentframe().f_code.co_name, # type:ignore
+ "status": success,
+ },
)
@@ -134,5 +136,8 @@ def view_to_component_kwargs(request, success="false"):
return render(
request,
"view_to_component.html",
- {"test_name": inspect.currentframe().f_code.co_name, "status": success},
+ {
+ "test_name": inspect.currentframe().f_code.co_name, # type:ignore
+ "status": success,
+ },
)
From 8e2cefd8e3278d9cc50d37cb82c38edbc83ba683 Mon Sep 17 00:00:00 2001
From: Archmonger <16909269+Archmonger@users.noreply.github.com>
Date: Thu, 20 Oct 2022 01:40:42 -0700
Subject: [PATCH 03/20] formatting
---
tests/test_app/admin.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/tests/test_app/admin.py b/tests/test_app/admin.py
index b77adbb6..da45013d 100644
--- a/tests/test_app/admin.py
+++ b/tests/test_app/admin.py
@@ -1,5 +1,4 @@
from django.contrib import admin
-
from test_app.models import TodoItem
From e62a725b4c80dbaed568488a41938e225d71cf64 Mon Sep 17 00:00:00 2001
From: Archmonger <16909269+Archmonger@users.noreply.github.com>
Date: Thu, 20 Oct 2022 01:44:00 -0700
Subject: [PATCH 04/20] warn_unused_ignores = false
---
pyproject.toml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pyproject.toml b/pyproject.toml
index 6bf3a9fc..ae6203d2 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -15,5 +15,5 @@ lines_after_imports = 2
ignore_missing_imports = true
warn_unused_configs = true
warn_redundant_casts = true
-warn_unused_ignores = true
+warn_unused_ignores = false
check_untyped_defs = true
From 76ebaa01dabdc7925388d5e98340d6a40347e00a Mon Sep 17 00:00:00 2001
From: Archmonger <16909269+Archmonger@users.noreply.github.com>
Date: Thu, 20 Oct 2022 15:29:58 -0700
Subject: [PATCH 05/20] minor docs changes
---
docs/src/features/components.md | 1 +
docs/src/features/hooks.md | 12 ++++++------
docs/src/getting-started/learn-more.md | 2 +-
3 files changed, 8 insertions(+), 7 deletions(-)
diff --git a/docs/src/features/components.md b/docs/src/features/components.md
index 4370a115..69cb6fea 100644
--- a/docs/src/features/components.md
+++ b/docs/src/features/components.md
@@ -52,6 +52,7 @@ Convert any Django view into a IDOM component by usng this decorator. Compatible
There are currently several limitations of using `view_to_component` that may be resolved in a future version of `django_idom`.
- Requires manual intervention to change request methods beyond `GET`.
+ - IDOM events cannot conveniently be attached to converted view HTML.
- Does not currently load any HTML contained with a `
` tag
- Has no option to automatically intercept local anchor link (ex. `#!html `) click events
diff --git a/docs/src/features/hooks.md b/docs/src/features/hooks.md
index e9e67e39..b35f43ae 100644
--- a/docs/src/features/hooks.md
+++ b/docs/src/features/hooks.md
@@ -324,12 +324,6 @@ You can expect this hook to provide strings such as `/idom/my_path`.
return html.div(my_location)
```
-??? info "This hook's behavior will be changed in a future update"
-
- This hook will be updated to return the browser's currently active path. This change will come in alongside IDOM URL routing support.
-
- Check out [idom-team/idom-router#2](https://github.com/idom-team/idom-router/issues/2) for more information.
-
??? example "See Interface"
**Parameters**
@@ -342,6 +336,12 @@ You can expect this hook to provide strings such as `/idom/my_path`.
| --- | --- |
| `Location` | A object containing the current URL's `pathname` and `search` query. |
+??? info "This hook's behavior will be changed in a future update"
+
+ This hook will be updated to return the browser's currently active path. This change will come in alongside IDOM URL routing support.
+
+ Check out [idom-team/idom-router#2](https://github.com/idom-team/idom-router/issues/2) for more information.
+
## Use Origin
This is a shortcut that returns the Websocket's `origin`.
diff --git a/docs/src/getting-started/learn-more.md b/docs/src/getting-started/learn-more.md
index e9aa7419..c2d98e1b 100644
--- a/docs/src/getting-started/learn-more.md
+++ b/docs/src/getting-started/learn-more.md
@@ -8,4 +8,4 @@ Additionally, the vast majority of tutorials/guides you find for React can be ap
| Learn More |
| --- |
-| [Django-IDOM Advanced Usage](../features/components.md){ .md-button } [IDOM Hooks, Events, and More](https://idom-docs.herokuapp.com/docs/guides/creating-interfaces/index.html){ .md-button } [Ask Questions on GitHub Discussions](https://github.com/idom-team/idom/discussions){ .md-button .md-button--primary } |
+| [Django-IDOM Advanced Usage](../features/components.md){ .md-button } [IDOM Core Documentation](https://idom-docs.herokuapp.com/docs/guides/creating-interfaces/index.html){ .md-button } [Ask Questions on GitHub Discussions](https://github.com/idom-team/idom/discussions){ .md-button .md-button--primary } |
From a9ceb75fb6b2c0e4737418b2bd1adb7d2ad8280c Mon Sep 17 00:00:00 2001
From: Archmonger <16909269+Archmonger@users.noreply.github.com>
Date: Sat, 22 Oct 2022 06:03:14 -0700
Subject: [PATCH 06/20] fix vtc type hints
---
CHANGELOG.md | 2 +-
src/django_idom/components.py | 29 ++++++++++++++++++++++++++---
2 files changed, 27 insertions(+), 4 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 359c221f..9ebdd471 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -33,7 +33,7 @@ Using the following categories, list your changes in this order:
### Fixed
- Change type hint on `view_to_component` callable to have `request` argument be optional.
-- Have docs demonstrate how to use Django-IDOM with `channels>=4.0.0`.
+- Change type hint on `view_to_component` to represent it as a decorator with paranthesis (ex `@view_to_component(compatibility=True)`)
## Security
diff --git a/src/django_idom/components.py b/src/django_idom/components.py
index 8a68a1d3..2389d849 100644
--- a/src/django_idom/components.py
+++ b/src/django_idom/components.py
@@ -3,7 +3,7 @@
import json
import os
from inspect import iscoroutinefunction
-from typing import Any, Callable, Protocol, Sequence
+from typing import Any, Callable, Protocol, Sequence, overload
from channels.db import database_sync_to_async
from django.contrib.staticfiles.finders import find
@@ -112,14 +112,37 @@ async def async_render():
return converted_view
+# Function definition of using view_to_component as a argless decorator,
+# or as a plain function call
+@overload
+def view_to_component(
+ view: Callable | View,
+ compatibility: bool = False,
+ transforms: Sequence[Callable[[VdomDict], Any]] = (),
+ strict_parsing: bool = True,
+) -> _ViewComponentConstructor:
+ ...
+
+
+# Function definition when used as decorator with paranthesis arguments
+@overload
+def view_to_component(
+ view: None = ...,
+ compatibility: bool = False,
+ transforms: Sequence[Callable[[VdomDict], Any]] = (),
+ strict_parsing: bool = True,
+) -> Callable[[Callable], _ViewComponentConstructor]:
+ ...
+
+
# TODO: Might want to intercept href clicks and form submit events.
# Form events will probably be accomplished through the upcoming DjangoForm.
def view_to_component(
- view: Callable | View = None, # type: ignore[assignment]
+ view: Callable | View | None = None,
compatibility: bool = False,
transforms: Sequence[Callable[[VdomDict], Any]] = (),
strict_parsing: bool = True,
-) -> _ViewComponentConstructor:
+) -> _ViewComponentConstructor | Callable[[Callable], _ViewComponentConstructor]:
"""Converts a Django view to an IDOM component.
Keyword Args:
From afa084e9dfd283cd76ec6628e89b56267fb4644d Mon Sep 17 00:00:00 2001
From: Archmonger <16909269+Archmonger@users.noreply.github.com>
Date: Sat, 22 Oct 2022 06:10:11 -0700
Subject: [PATCH 07/20] better comments for vtc overloads
---
src/django_idom/components.py | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/src/django_idom/components.py b/src/django_idom/components.py
index 2389d849..f0bf9858 100644
--- a/src/django_idom/components.py
+++ b/src/django_idom/components.py
@@ -112,8 +112,9 @@ async def async_render():
return converted_view
-# Function definition of using view_to_component as a argless decorator,
-# or as a plain function call
+# Type hints for:
+# example = view_to_component(my_view, ...)
+# @view_to_component
@overload
def view_to_component(
view: Callable | View,
@@ -124,7 +125,8 @@ def view_to_component(
...
-# Function definition when used as decorator with paranthesis arguments
+# Type hints for:
+# @view_to_component(...)
@overload
def view_to_component(
view: None = ...,
From da8b3f141d24d003ec0416112e027bc8836aa69b Mon Sep 17 00:00:00 2001
From: Archmonger <16909269+Archmonger@users.noreply.github.com>
Date: Thu, 27 Oct 2022 15:53:32 -0700
Subject: [PATCH 08/20] re-add django stubs
---
requirements/check-types.txt | 1 +
1 file changed, 1 insertion(+)
diff --git a/requirements/check-types.txt b/requirements/check-types.txt
index f0aa93ac..c176075a 100644
--- a/requirements/check-types.txt
+++ b/requirements/check-types.txt
@@ -1 +1,2 @@
mypy
+django-stubs[compatible-mypy]
From 1763fbe7c7ac36148ee515842a73280e1448bf6f Mon Sep 17 00:00:00 2001
From: Archmonger <16909269+Archmonger@users.noreply.github.com>
Date: Thu, 27 Oct 2022 15:59:00 -0700
Subject: [PATCH 09/20] change version
---
CHANGELOG.md | 6 +++---
src/django_idom/__init__.py | 2 +-
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9ebdd471..00f4654b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -24,7 +24,7 @@ Using the following categories, list your changes in this order:
- Nothing (yet)
-## [2.0.2] - 2022-10-19
+## [2.1.0] - 2022-10-27
## Changed
@@ -170,8 +170,8 @@ Using the following categories, list your changes in this order:
- Support for IDOM within the Django
-[unreleased]: https://github.com/idom-team/django-idom/compare/2.0.2...HEAD
-[2.0.2]: https://github.com/idom-team/django-idom/compare/2.0.1...2.0.2
+[unreleased]: https://github.com/idom-team/django-idom/compare/2.1.0...HEAD
+[2.0.2]: https://github.com/idom-team/django-idom/compare/2.0.1...2.1.0
[2.0.1]: https://github.com/idom-team/django-idom/compare/2.0.0...2.0.1
[2.0.0]: https://github.com/idom-team/django-idom/compare/1.2.0...2.0.0
[1.2.0]: https://github.com/idom-team/django-idom/compare/1.1.0...1.2.0
diff --git a/src/django_idom/__init__.py b/src/django_idom/__init__.py
index d1247b15..406f04db 100644
--- a/src/django_idom/__init__.py
+++ b/src/django_idom/__init__.py
@@ -3,7 +3,7 @@
from django_idom.websocket.paths import IDOM_WEBSOCKET_PATH
-__version__ = "2.0.2"
+__version__ = "2.1.0"
__all__ = [
"IDOM_WEBSOCKET_PATH",
"IdomWebsocket",
From 68936c7e09dded489ff099a4abac894169b7e3d5 Mon Sep 17 00:00:00 2001
From: Archmonger <16909269+Archmonger@users.noreply.github.com>
Date: Mon, 31 Oct 2022 03:04:39 -0700
Subject: [PATCH 10/20] more type hint stuff
---
pyproject.toml | 2 +-
src/django_idom/components.py | 16 +++++++++++-----
2 files changed, 12 insertions(+), 6 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index ae6203d2..6bf3a9fc 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -15,5 +15,5 @@ lines_after_imports = 2
ignore_missing_imports = true
warn_unused_configs = true
warn_redundant_casts = true
-warn_unused_ignores = false
+warn_unused_ignores = true
check_untyped_defs = true
diff --git a/src/django_idom/components.py b/src/django_idom/components.py
index f0bf9858..1ff5a14b 100644
--- a/src/django_idom/components.py
+++ b/src/django_idom/components.py
@@ -3,7 +3,7 @@
import json
import os
from inspect import iscoroutinefunction
-from typing import Any, Callable, Protocol, Sequence, overload
+from typing import Any, Callable, Protocol, Sequence, Union, cast, overload
from channels.db import database_sync_to_async
from django.contrib.staticfiles.finders import find
@@ -35,7 +35,10 @@ def _view_to_component(
args: Sequence | None,
kwargs: dict | None,
):
- converted_view, set_converted_view = hooks.use_state(None)
+ converted_view, set_converted_view = hooks.use_state(
+ cast(Union[VdomDict, None], None)
+ )
+
_args: Sequence = args or ()
_kwargs: dict = kwargs or {}
request_obj = request
@@ -68,16 +71,17 @@ async def async_render():
"src": reverse("idom:view_to_component", args=[dotted_path]),
"loading": "lazy",
}
- ) # type: ignore
+ )
)
return
# Render Check 2: Async function view
elif iscoroutinefunction(view):
- view_html = await view(request_obj, *_args, **_kwargs) # type: ignore
+ view_html = await view(request_obj, *_args, **_kwargs)
# Render Check 3: Async class view
elif getattr(view, "view_is_async", False):
+ # django-stubs does not support async views yet, so we have to ignore types here
view_or_template_view = await view.as_view()(request_obj, *_args, **_kwargs) # type: ignore
if getattr(view_or_template_view, "render", None): # TemplateView
view_html = await view_or_template_view.render()
@@ -86,6 +90,8 @@ async def async_render():
# Render Check 4: Sync class view
elif getattr(view, "as_view", None):
+ # MyPy does not know how to properly interpret this as a `View` type
+ # And `isinstance(view, View)` does not work due to some weird Django internal shenanigans
async_cbv = database_sync_to_async(view.as_view()) # type: ignore
view_or_template_view = await async_cbv(request_obj, *_args, **_kwargs)
if getattr(view_or_template_view, "render", None): # TemplateView
@@ -105,7 +111,7 @@ async def async_render():
view_html.content.decode("utf-8").strip(),
*transforms,
strict=strict_parsing,
- ) # type: ignore
+ )
)
# Return the view if it's been rendered via the `async_render` hook
From fa460a1232223479f6119778abb2e37a505729c2 Mon Sep 17 00:00:00 2001
From: Archmonger <16909269+Archmonger@users.noreply.github.com>
Date: Mon, 31 Oct 2022 03:10:54 -0700
Subject: [PATCH 11/20] fix some stragglers
---
src/django_idom/components.py | 2 +-
src/django_idom/http/views.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/django_idom/components.py b/src/django_idom/components.py
index 1ff5a14b..bd725b86 100644
--- a/src/django_idom/components.py
+++ b/src/django_idom/components.py
@@ -76,7 +76,7 @@ async def async_render():
return
# Render Check 2: Async function view
- elif iscoroutinefunction(view):
+ elif iscoroutinefunction(view) and callable(view):
view_html = await view(request_obj, *_args, **_kwargs)
# Render Check 3: Async class view
diff --git a/src/django_idom/http/views.py b/src/django_idom/http/views.py
index 033e9ffd..ceb24f21 100644
--- a/src/django_idom/http/views.py
+++ b/src/django_idom/http/views.py
@@ -48,7 +48,7 @@ async def view_to_component_iframe(
# Render Check 1: Async function view
if iscoroutinefunction(iframe.view):
- response = await iframe.view(request, *iframe.args, **iframe.kwargs) # type: ignore[operator]
+ response = await iframe.view(request, *iframe.args, **iframe.kwargs)
# Render Check 2: Async class view
elif getattr(iframe.view, "view_is_async", False):
From 9446341c26a8e56041d0bb735cc90c5cfa27343c Mon Sep 17 00:00:00 2001
From: Archmonger <16909269+Archmonger@users.noreply.github.com>
Date: Mon, 31 Oct 2022 03:20:02 -0700
Subject: [PATCH 12/20] cries in type hints
---
src/django_idom/http/views.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/django_idom/http/views.py b/src/django_idom/http/views.py
index ceb24f21..41fd8ce3 100644
--- a/src/django_idom/http/views.py
+++ b/src/django_idom/http/views.py
@@ -47,7 +47,7 @@ async def view_to_component_iframe(
raise ValueError(f"No view registered for component {view_path}.")
# Render Check 1: Async function view
- if iscoroutinefunction(iframe.view):
+ if iscoroutinefunction(iframe.view) and callable(iframe.view):
response = await iframe.view(request, *iframe.args, **iframe.kwargs)
# Render Check 2: Async class view
From 841fa3b9fb2547e050784998339d2039a29870b5 Mon Sep 17 00:00:00 2001
From: Archmonger <16909269+Archmonger@users.noreply.github.com>
Date: Mon, 31 Oct 2022 16:17:10 -0700
Subject: [PATCH 13/20] run mypy on tests
---
noxfile.py | 1 +
tests/test_app/migrations/0001_initial.py | 19 +++++++++++++------
2 files changed, 14 insertions(+), 6 deletions(-)
diff --git a/noxfile.py b/noxfile.py
index c0c63855..abfb1c3e 100644
--- a/noxfile.py
+++ b/noxfile.py
@@ -64,6 +64,7 @@ def test_types(session: Session) -> None:
install_requirements_file(session, "check-types")
install_requirements_file(session, "pkg-deps")
session.run("mypy", "--show-error-codes", "src/django_idom")
+ session.run("mypy", "--show-error-codes", "tests/test_app")
@nox.session
diff --git a/tests/test_app/migrations/0001_initial.py b/tests/test_app/migrations/0001_initial.py
index e05fedd6..69498ba5 100644
--- a/tests/test_app/migrations/0001_initial.py
+++ b/tests/test_app/migrations/0001_initial.py
@@ -7,16 +7,23 @@ class Migration(migrations.Migration):
initial = True
- dependencies = [
- ]
+ dependencies = [] # type: ignore
operations = [
migrations.CreateModel(
- name='TodoItem',
+ name="TodoItem",
fields=[
- ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('done', models.BooleanField()),
- ('text', models.CharField(max_length=1000)),
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("done", models.BooleanField()),
+ ("text", models.CharField(max_length=1000)),
],
),
]
From 504f7c8ea6e9f897ba8f02e461a6e1bd0d926252 Mon Sep 17 00:00:00 2001
From: Archmonger <16909269+Archmonger@users.noreply.github.com>
Date: Mon, 31 Oct 2022 17:00:17 -0700
Subject: [PATCH 14/20] DRY view rendering
---
CHANGELOG.md | 4 +-
src/django_idom/components.py | 86 +++++++++++------------------------
src/django_idom/http/views.py | 28 ++----------
src/django_idom/utils.py | 44 +++++++++++++++++-
4 files changed, 76 insertions(+), 86 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 00f4654b..8aac3bfb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -24,7 +24,7 @@ Using the following categories, list your changes in this order:
- Nothing (yet)
-## [2.1.0] - 2022-10-27
+## [2.1.0] - 2022-10-31
## Changed
@@ -171,7 +171,7 @@ Using the following categories, list your changes in this order:
- Support for IDOM within the Django
[unreleased]: https://github.com/idom-team/django-idom/compare/2.1.0...HEAD
-[2.0.2]: https://github.com/idom-team/django-idom/compare/2.0.1...2.1.0
+[2.1.0]: https://github.com/idom-team/django-idom/compare/2.0.1...2.1.0
[2.0.1]: https://github.com/idom-team/django-idom/compare/2.0.0...2.0.1
[2.0.0]: https://github.com/idom-team/django-idom/compare/1.2.0...2.0.0
[1.2.0]: https://github.com/idom-team/django-idom/compare/1.1.0...1.2.0
diff --git a/src/django_idom/components.py b/src/django_idom/components.py
index bd725b86..163eb47b 100644
--- a/src/django_idom/components.py
+++ b/src/django_idom/components.py
@@ -2,10 +2,8 @@
import json
import os
-from inspect import iscoroutinefunction
from typing import Any, Callable, Protocol, Sequence, Union, cast, overload
-from channels.db import database_sync_to_async
from django.contrib.staticfiles.finders import find
from django.http import HttpRequest
from django.urls import reverse
@@ -15,7 +13,7 @@
from django_idom.config import IDOM_CACHE, IDOM_VIEW_COMPONENT_IFRAMES
from django_idom.types import ViewComponentIframe
-from django_idom.utils import _generate_obj_name
+from django_idom.utils import _generate_obj_name, render_view
class _ViewComponentConstructor(Protocol):
@@ -38,89 +36,57 @@ def _view_to_component(
converted_view, set_converted_view = hooks.use_state(
cast(Union[VdomDict, None], None)
)
-
_args: Sequence = args or ()
_kwargs: dict = kwargs or {}
- request_obj = request
- if not request_obj:
- request_obj = HttpRequest()
- request_obj.method = "GET"
- if compatibility:
- dotted_path = f"{view.__module__}.{view.__name__}" # type: ignore[union-attr]
- dotted_path = dotted_path.replace("<", "").replace(">", "")
- IDOM_VIEW_COMPONENT_IFRAMES[dotted_path] = ViewComponentIframe(
- view, _args, _kwargs
- )
+ if request:
+ _request: HttpRequest = request
else:
- dotted_path = None
+ _request = HttpRequest()
+ _request.method = "GET"
# Render the view render within a hook
@hooks.use_effect(
dependencies=[
- json.dumps(vars(request_obj), default=lambda x: _generate_obj_name(x)),
+ json.dumps(vars(_request), default=lambda x: _generate_obj_name(x)),
json.dumps([_args, _kwargs], default=lambda x: _generate_obj_name(x)),
]
)
async def async_render():
"""Render the view in an async hook to avoid blocking the main thread."""
- # Render Check 1: Compatibility mode
+ # Compatibility mode doesn't require a traditional render
if compatibility:
- set_converted_view(
- html.iframe(
- {
- "src": reverse("idom:view_to_component", args=[dotted_path]),
- "loading": "lazy",
- }
- )
- )
return
- # Render Check 2: Async function view
- elif iscoroutinefunction(view) and callable(view):
- view_html = await view(request_obj, *_args, **_kwargs)
-
- # Render Check 3: Async class view
- elif getattr(view, "view_is_async", False):
- # django-stubs does not support async views yet, so we have to ignore types here
- view_or_template_view = await view.as_view()(request_obj, *_args, **_kwargs) # type: ignore
- if getattr(view_or_template_view, "render", None): # TemplateView
- view_html = await view_or_template_view.render()
- else: # View
- view_html = view_or_template_view
-
- # Render Check 4: Sync class view
- elif getattr(view, "as_view", None):
- # MyPy does not know how to properly interpret this as a `View` type
- # And `isinstance(view, View)` does not work due to some weird Django internal shenanigans
- async_cbv = database_sync_to_async(view.as_view()) # type: ignore
- view_or_template_view = await async_cbv(request_obj, *_args, **_kwargs)
- if getattr(view_or_template_view, "render", None): # TemplateView
- view_html = await database_sync_to_async(view_or_template_view.render)()
- else: # View
- view_html = view_or_template_view
-
- # Render Check 5: Sync function view
- else:
- view_html = await database_sync_to_async(view)(
- request_obj, *_args, **_kwargs
- )
-
- # Signal that the view has been rendered
+ # Render the view
+ response = await render_view(view, _request, _args, _kwargs)
set_converted_view(
utils.html_to_vdom(
- view_html.content.decode("utf-8").strip(),
+ response.content.decode("utf-8").strip(),
*transforms,
strict=strict_parsing,
)
)
+ # Render in compatibility mode, if needed
+ if compatibility:
+ dotted_path = f"{view.__module__}.{view.__name__}".replace("<", "").replace(">", "") # type: ignore
+ IDOM_VIEW_COMPONENT_IFRAMES[dotted_path] = ViewComponentIframe(
+ view, _args, _kwargs
+ )
+ return html.iframe(
+ {
+ "src": reverse("idom:view_to_component", args=[dotted_path]),
+ "loading": "lazy",
+ }
+ )
+
# Return the view if it's been rendered via the `async_render` hook
return converted_view
# Type hints for:
-# example = view_to_component(my_view, ...)
-# @view_to_component
+# 1. example = view_to_component(my_view, ...)
+# 2. @view_to_component
@overload
def view_to_component(
view: Callable | View,
@@ -132,7 +98,7 @@ def view_to_component(
# Type hints for:
-# @view_to_component(...)
+# 1. @view_to_component(...)
@overload
def view_to_component(
view: None = ...,
diff --git a/src/django_idom/http/views.py b/src/django_idom/http/views.py
index 41fd8ce3..262634f9 100644
--- a/src/django_idom/http/views.py
+++ b/src/django_idom/http/views.py
@@ -1,13 +1,12 @@
import os
-from inspect import iscoroutinefunction
from aiofile import async_open
-from channels.db import database_sync_to_async
from django.core.exceptions import SuspiciousOperation
-from django.http import HttpRequest, HttpResponse
+from django.http import HttpRequest, HttpResponse, HttpResponseNotFound
from idom.config import IDOM_WED_MODULES_DIR
from django_idom.config import IDOM_CACHE, IDOM_VIEW_COMPONENT_IFRAMES
+from django_idom.utils import render_view
async def web_modules_file(request: HttpRequest, file: str) -> HttpResponse:
@@ -44,27 +43,10 @@ async def view_to_component_iframe(
# Get the view from IDOM_REGISTERED_IFRAMES
iframe = IDOM_VIEW_COMPONENT_IFRAMES.get(view_path)
if not iframe:
- raise ValueError(f"No view registered for component {view_path}.")
+ return HttpResponseNotFound()
- # Render Check 1: Async function view
- if iscoroutinefunction(iframe.view) and callable(iframe.view):
- response = await iframe.view(request, *iframe.args, **iframe.kwargs)
-
- # Render Check 2: Async class view
- elif getattr(iframe.view, "view_is_async", False):
- response = await iframe.view.as_view()(request, *iframe.args, **iframe.kwargs) # type: ignore[misc, union-attr]
-
- # Render Check 3: Sync class view
- elif getattr(iframe.view, "as_view", None):
- response = await database_sync_to_async(iframe.view.as_view())( # type: ignore[union-attr]
- request, *iframe.args, **iframe.kwargs
- )
-
- # Render Check 4: Sync function view
- else:
- response = await database_sync_to_async(iframe.view)(
- request, *iframe.args, **iframe.kwargs
- )
+ # Render the view
+ response = await render_view(iframe.view, request, iframe.args, iframe.kwargs)
# Ensure page can be rendered as an iframe
response["X-Frame-Options"] = "SAMEORIGIN"
diff --git a/src/django_idom/utils.py b/src/django_idom/utils.py
index 5744622b..8394589e 100644
--- a/src/django_idom/utils.py
+++ b/src/django_idom/utils.py
@@ -6,10 +6,14 @@
import re
from fnmatch import fnmatch
from importlib import import_module
-from typing import Any, Callable
+from inspect import iscoroutinefunction
+from typing import Any, Callable, Sequence
+from channels.db import database_sync_to_async
+from django.http import HttpRequest, HttpResponse
from django.template import engines
from django.utils.encoding import smart_str
+from django.views import View
from django_idom.config import IDOM_REGISTERED_COMPONENTS
@@ -30,6 +34,44 @@
)
+async def render_view(
+ view: Callable | View,
+ request: HttpRequest,
+ args: Sequence,
+ kwargs: dict,
+) -> HttpResponse:
+ """Ingests a Django view (class or function) and returns an HTTP response object."""
+ # Render Check 1: Async function view
+ if iscoroutinefunction(view) and callable(view):
+ response = await view(request, *args, **kwargs)
+
+ # Render Check 2: Async class view
+ elif getattr(view, "view_is_async", False):
+ # django-stubs does not support async views yet, so we have to ignore types here
+ view_or_template_view = await view.as_view()(request, *args, **kwargs) # type: ignore
+ if getattr(view_or_template_view, "render", None): # TemplateView
+ response = await view_or_template_view.render()
+ else: # View
+ response = view_or_template_view
+
+ # Render Check 3: Sync class view
+ elif getattr(view, "as_view", None):
+ # MyPy does not know how to properly interpret this as a `View` type
+ # And `isinstance(view, View)` does not work due to some weird Django internal shenanigans
+ async_cbv = database_sync_to_async(view.as_view()) # type: ignore
+ view_or_template_view = await async_cbv(request, *args, **kwargs)
+ if getattr(view_or_template_view, "render", None): # TemplateView
+ response = await database_sync_to_async(view_or_template_view.render)()
+ else: # View
+ response = view_or_template_view
+
+ # Render Check 4: Sync function view
+ else:
+ response = await database_sync_to_async(view)(request, *args, **kwargs)
+
+ return response
+
+
def _register_component(dotted_path: str) -> None:
if dotted_path in IDOM_REGISTERED_COMPONENTS:
return
From 3209276816fdcf7bb37e18605b0deda74e901703 Mon Sep 17 00:00:00 2001
From: Archmonger <16909269+Archmonger@users.noreply.github.com>
Date: Mon, 31 Oct 2022 17:15:38 -0700
Subject: [PATCH 15/20] add examples of potential info exposure
---
docs/src/features/components.md | 26 +++++++++++++++++++++++++-
1 file changed, 25 insertions(+), 1 deletion(-)
diff --git a/docs/src/features/components.md b/docs/src/features/components.md
index 69cb6fea..d2ae54c9 100644
--- a/docs/src/features/components.md
+++ b/docs/src/features/components.md
@@ -45,7 +45,31 @@ Convert any Django view into a IDOM component by usng this decorator. Compatible
When using `compatibility` mode, IDOM automatically exposes a URL to your view.
- It is your responsibility to ensure priveledged information is not leaked via this method. This can be done via directly writing conditionals into your view, or by adding decorators such as [user_passes_test](https://docs.djangoproject.com/en/dev/topics/auth/default/#django.contrib.auth.decorators.user_passes_test) to your views prior to using `view_to_component`.
+ It is your responsibility to ensure priveledged information is not leaked via this method.
+
+ This can be done via directly writing conditionals into your view, or by adding decorators such as [user_passes_test](https://docs.djangoproject.com/en/dev/topics/auth/default/#django.contrib.auth.decorators.user_passes_test) to your views prior to using `view_to_component`.
+
+ === "Function Based View"
+
+ ```python
+ ...
+
+ @view_to_component(compatibility=True)
+ @user_passes_test(lambda u: u.is_superuser)
+ def example_view(request):
+ ...
+ ```
+
+ === "Class Based View"
+
+ ```python
+ ...
+
+ @view_to_component(compatibility=True)
+ @method_decorator(user_passes_test(lambda u: u.is_superuser), name="dispatch")
+ class ExampleView(TemplateView):
+ ...
+ ```
??? info "Existing limitations"
From 2ad9607ec666c22beff1768bc229f904181212df Mon Sep 17 00:00:00 2001
From: Archmonger <16909269+Archmonger@users.noreply.github.com>
Date: Mon, 31 Oct 2022 17:18:36 -0700
Subject: [PATCH 16/20] clarify sync/async support
---
docs/src/features/components.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/src/features/components.md b/docs/src/features/components.md
index d2ae54c9..0b6db68f 100644
--- a/docs/src/features/components.md
+++ b/docs/src/features/components.md
@@ -4,7 +4,7 @@
## View To Component
-Convert any Django view into a IDOM component by usng this decorator. Compatible with sync/async [Function Based Views](https://docs.djangoproject.com/en/dev/topics/http/views/) and [Class Based Views](https://docs.djangoproject.com/en/dev/topics/class-based-views/).
+Convert any Django view into a IDOM component by usng this decorator. Compatible with [Function Based Views](https://docs.djangoproject.com/en/dev/topics/http/views/) and [Class Based Views](https://docs.djangoproject.com/en/dev/topics/class-based-views/). Views can be sync or async.
=== "components.py"
From 87a535a4d082a52c992f7aaeefd355463159baca Mon Sep 17 00:00:00 2001
From: Archmonger <16909269+Archmonger@users.noreply.github.com>
Date: Mon, 31 Oct 2022 19:35:33 -0700
Subject: [PATCH 17/20] fix spacing
---
docs/src/features/components.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/src/features/components.md b/docs/src/features/components.md
index 0b6db68f..eca86f66 100644
--- a/docs/src/features/components.md
+++ b/docs/src/features/components.md
@@ -108,7 +108,7 @@ Convert any Django view into a IDOM component by usng this decorator. Compatible
??? question "How do I transform views from external libraries?"
- In order to convert external views, you can utilize `view_to_component` as a function, rather than a decorator.
+ In order to convert external views, you can utilize `view_to_component` as a function, rather than a decorator.
=== "components.py"
From 7d687e5c7efd8ba2ef60718379d3a902bd8d3c6a Mon Sep 17 00:00:00 2001
From: Archmonger <16909269+Archmonger@users.noreply.github.com>
Date: Mon, 31 Oct 2022 19:53:08 -0700
Subject: [PATCH 18/20] fix running tests docs
---
docs/src/contribute/running-tests.md | 23 +++++++++++++++++------
1 file changed, 17 insertions(+), 6 deletions(-)
diff --git a/docs/src/contribute/running-tests.md b/docs/src/contribute/running-tests.md
index ece8d1a7..f1ff6cc1 100644
--- a/docs/src/contribute/running-tests.md
+++ b/docs/src/contribute/running-tests.md
@@ -1,15 +1,26 @@
-This repo uses [Nox](https://nox.thea.codes/en/stable/) to run scripts which can be found in `noxfile.py`. For a full test of available scripts run `nox -l`. To run the full test suite simple execute:
+This repo uses [Nox](https://nox.thea.codes/en/stable/) to run tests. For a full test of available scripts run `nox -l`.
+
+If you plan to run tests, you'll need to install the following dependencies first:
+
+- [Python 3.8+](https://www.python.org/downloads/)
+- [Git](https://git-scm.com/downloads)
+
+Once done, you should clone this repository:
+
+```bash
+git clone https://github.com/idom-team/django-idom.git
+cd django-idom
+pip install -r ./requirements/test-run.txt --upgrade
+```
+
+Then, by running the command below you can run the full test suite:
```
nox -s test
```
-If you do not want to run the tests in the background:
+Or, if you want to run the tests in the foreground:
```
nox -s test -- --headed
```
-
-!!! warning "Most tests will not run on Windows"
-
- Due to [bugs within Django Channels](https://github.com/django/channels/issues/1207), functional tests are not run on Windows. In order for Windows users to test Django-IDOM functionality, you will need to run tests via [Windows Subsystem for Linux](https://code.visualstudio.com/docs/remote/wsl).
From 6accbfa9d1144f9bf9a7e82cbd573be3234f6958 Mon Sep 17 00:00:00 2001
From: Ryan Morshead
Date: Tue, 1 Nov 2022 20:09:07 -0700
Subject: [PATCH 19/20] consolidate session.run calls
---
noxfile.py | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/noxfile.py b/noxfile.py
index abfb1c3e..1e55dabf 100644
--- a/noxfile.py
+++ b/noxfile.py
@@ -63,8 +63,7 @@ def test_suite(session: Session) -> None:
def test_types(session: Session) -> None:
install_requirements_file(session, "check-types")
install_requirements_file(session, "pkg-deps")
- session.run("mypy", "--show-error-codes", "src/django_idom")
- session.run("mypy", "--show-error-codes", "tests/test_app")
+ session.run("mypy", "--show-error-codes", "src/django_idom", "tests/test_app")
@nox.session
From 16a6383ac70229076055546f1355e69785ecd208 Mon Sep 17 00:00:00 2001
From: Archmonger <16909269+Archmonger@users.noreply.github.com>
Date: Tue, 1 Nov 2022 23:11:57 -0700
Subject: [PATCH 20/20] bump version release date
---
CHANGELOG.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8aac3bfb..cab02d0e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -24,7 +24,7 @@ Using the following categories, list your changes in this order:
- Nothing (yet)
-## [2.1.0] - 2022-10-31
+## [2.1.0] - 2022-11-01
## Changed