From ea067e0f59466db288a0adb59559062880b6b077 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sun, 24 Sep 2023 05:10:59 -0700 Subject: [PATCH] Use .as_view() for CBVs --- docs/python/vtc-cbv.py | 2 +- docs/python/vti-cbv.py | 2 +- docs/src/reference/components.md | 8 +++- src/reactpy_django/components.py | 2 + src/reactpy_django/utils.py | 64 +++++++++++++------------------- tests/test_app/components.py | 16 +++++--- 6 files changed, 45 insertions(+), 49 deletions(-) diff --git a/docs/python/vtc-cbv.py b/docs/python/vtc-cbv.py index 244ad026..47509b75 100644 --- a/docs/python/vtc-cbv.py +++ b/docs/python/vtc-cbv.py @@ -3,7 +3,7 @@ from . import views -hello_world_component = view_to_component(views.HelloWorld) +hello_world_component = view_to_component(views.HelloWorld.as_view()) @component diff --git a/docs/python/vti-cbv.py b/docs/python/vti-cbv.py index ff2ce963..4e1f1b44 100644 --- a/docs/python/vti-cbv.py +++ b/docs/python/vti-cbv.py @@ -3,7 +3,7 @@ from . import views -hello_world_iframe = view_to_iframe(views.HelloWorld) +hello_world_iframe = view_to_iframe(views.HelloWorld.as_view()) @component diff --git a/docs/src/reference/components.md b/docs/src/reference/components.md index ecdf540c..65b94923 100644 --- a/docs/src/reference/components.md +++ b/docs/src/reference/components.md @@ -54,7 +54,9 @@ Compatible with sync or async [Function Based Views](https://docs.djangoproject. ??? question "How do I use this for Class Based Views?" - Class Based Views are accepted by `#!python view_to_component` as an argument. For example... + Class Based Views are accepted by `#!python view_to_component` as an argument. + + Calling `#!python as_view()` is optional, but recommended. === "components.py" @@ -195,7 +197,9 @@ Compatible with sync or async [Function Based Views](https://docs.djangoproject. ??? question "How do I use this for Class Based Views?" - Class Based Views are accepted by `#!python view_to_iframe` as an argument. For example... + Class Based Views are accepted by `#!python view_to_iframe` as an argument. + + Calling `#!python as_view()` is optional, but recommended. === "components.py" diff --git a/src/reactpy_django/components.py b/src/reactpy_django/components.py index f01b1858..ab02e996 100644 --- a/src/reactpy_django/components.py +++ b/src/reactpy_django/components.py @@ -220,6 +220,8 @@ def _view_to_iframe( """The actual component. Used to prevent pollution of acceptable kwargs keys.""" from reactpy_django.config import REACTPY_REGISTERED_IFRAME_VIEWS + if hasattr(view, "view_class"): + view = view.view_class dotted_path = view if isinstance(view, str) else generate_obj_name(view) registered_view = REACTPY_REGISTERED_IFRAME_VIEWS.get(dotted_path) diff --git a/src/reactpy_django/utils.py b/src/reactpy_django/utils.py index e2f15af0..922ce22a 100644 --- a/src/reactpy_django/utils.py +++ b/src/reactpy_django/utils.py @@ -5,10 +5,10 @@ import logging import os import re +from asyncio import iscoroutinefunction from datetime import timedelta from fnmatch import fnmatch from importlib import import_module -from inspect import iscoroutinefunction from typing import Any, Callable, Sequence from asgiref.sync import async_to_sync @@ -54,37 +54,21 @@ async def render_view( 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): + # Convert class-based view to function-based view + if getattr(view, "as_view", None): + view = view.as_view() # type: ignore[union-attr] + + # Async function view + if iscoroutinefunction(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(), thread_sensitive=False) # 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, thread_sensitive=False - )() - else: # View - response = view_or_template_view - - # Render Check 4: Sync function view + # Sync function view else: - response = await database_sync_to_async(view, thread_sensitive=False)( - request, *args, **kwargs - ) + response = await database_sync_to_async(view)(request, *args, **kwargs) + + # TemplateView + if getattr(response, "render", None): + response = await database_sync_to_async(response.render)() return response @@ -121,6 +105,8 @@ def register_iframe(view: Callable | View | str): """ from reactpy_django.config import REACTPY_REGISTERED_IFRAME_VIEWS + if hasattr(view, "view_class"): + view = view.view_class dotted_path = view if isinstance(view, str) else generate_obj_name(view) try: REACTPY_REGISTERED_IFRAME_VIEWS[dotted_path] = import_dotted_path(dotted_path) @@ -247,22 +233,22 @@ def register_components(self, components: set[str]) -> None: ) -def generate_obj_name(object: Any) -> str: +def generate_obj_name(obj: Any) -> str: """Makes a best effort to create a name for an object. Useful for JSON serialization of Python objects.""" - # Attempt to use dunder methods to create a name - if hasattr(object, "__module__"): - if hasattr(object, "__name__"): - return f"{object.__module__}.{object.__name__}" - if hasattr(object, "__class__"): - return f"{object.__module__}.{object.__class__.__name__}" + # First attempt: Dunder methods + if hasattr(obj, "__module__"): + if hasattr(obj, "__name__"): + return f"{obj.__module__}.{obj.__name__}" + if hasattr(obj, "__class__") and hasattr(obj.__class__, "__name__"): + return f"{obj.__module__}.{obj.__class__.__name__}" - # First fallback: String representation + # Second attempt: String representation with contextlib.suppress(Exception): - return str(object) + return str(obj) - # Last fallback: Empty string + # Fallback: Empty string return "" diff --git a/tests/test_app/components.py b/tests/test_app/components.py index 473719b2..f7569307 100644 --- a/tests/test_app/components.py +++ b/tests/test_app/components.py @@ -448,10 +448,14 @@ async def on_change(event): view_to_component_sync_func = view_to_component(views.view_to_component_sync_func) view_to_component_async_func = view_to_component(views.view_to_component_async_func) -view_to_component_sync_class = view_to_component(views.ViewToComponentSyncClass) -view_to_component_async_class = view_to_component(views.ViewToComponentAsyncClass) +view_to_component_sync_class = view_to_component( + views.ViewToComponentSyncClass.as_view() +) +view_to_component_async_class = view_to_component( + views.ViewToComponentAsyncClass.as_view() +) view_to_component_template_view_class = view_to_component( - views.ViewToComponentTemplateViewClass + views.ViewToComponentTemplateViewClass.as_view() ) _view_to_component_sync_func_compatibility = view_to_component( views.view_to_component_sync_func_compatibility, compatibility=True @@ -460,13 +464,13 @@ async def on_change(event): views.view_to_component_async_func_compatibility, compatibility=True ) _view_to_component_sync_class_compatibility = view_to_component( - views.ViewToComponentSyncClassCompatibility, compatibility=True + views.ViewToComponentSyncClassCompatibility.as_view(), compatibility=True ) _view_to_component_async_class_compatibility = view_to_component( - views.ViewToComponentAsyncClassCompatibility, compatibility=True + views.ViewToComponentAsyncClassCompatibility.as_view(), compatibility=True ) _view_to_component_template_view_class_compatibility = view_to_component( - views.ViewToComponentTemplateViewClassCompatibility, compatibility=True + views.ViewToComponentTemplateViewClassCompatibility.as_view(), compatibility=True ) _view_to_iframe_args = view_to_iframe(views.view_to_iframe_args) _view_to_iframe_not_registered = view_to_iframe("view_does_not_exist")