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