Skip to content

Commit

Permalink
Use .as_view() for CBVs
Browse files Browse the repository at this point in the history
  • Loading branch information
Archmonger committed Sep 24, 2023
1 parent b52a400 commit ea067e0
Show file tree
Hide file tree
Showing 6 changed files with 45 additions and 49 deletions.
2 changes: 1 addition & 1 deletion docs/python/vtc-cbv.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/python/vti-cbv.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 6 additions & 2 deletions docs/src/reference/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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"

Expand Down
2 changes: 2 additions & 0 deletions src/reactpy_django/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
64 changes: 25 additions & 39 deletions src/reactpy_django/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 ""


Expand Down
16 changes: 10 additions & 6 deletions tests/test_app/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")
Expand Down

0 comments on commit ea067e0

Please sign in to comment.