Skip to content

Instantly share code, notes, and snippets.

@mick88
Last active February 27, 2025 20:11
Show Gist options
  • Save mick88/a29f3dd675c2c7a4ea6e549b707189a6 to your computer and use it in GitHub Desktop.
Save mick88/a29f3dd675c2c7a4ea6e549b707189a6 to your computer and use it in GitHub Desktop.
Django: Lazy view
from typing import Callable
from django.utils.module_loading import import_string
from django.views import View
class LazyView:
"""
Wrapper for view class that can be used for urls to avoid loading the view class at the import time.
The view is imported at first access and cached.
To use in urls.py, just instantiate `LazyView` with class path argument and use as a normal view:
```python
url(r'^/view$', LazyView('path.to.ViewClass').as_view(), name="url-name"),
```
"""
def __init__(self, view_cls_path: str) -> None:
super().__init__()
self.view_cls_path = view_cls_path
def view_func(self, *args, **kwargs):
if not hasattr(self, 'view'):
view_cls: type[View] = import_string(self.view_cls_path)
self.view: Callable = view_cls.as_view(**self.initkwargs)
return self.view(*args, **kwargs)
def as_view(self, **initkwargs):
self.initkwargs = initkwargs
return self.view_func
@jonesnc
Copy link

jonesnc commented Feb 20, 2025

I also use decorators on the methods of the class based view, but the problem is the view_func doesn't have the csrf_exempt attribute set to True, which gets evaluated before the view is imported.

I updated my version to just return the view function itself if lazy loading isn't enabled, so the csrf_exempt attribute is only needed when lazy loading is enabled.

@jonesnc
Copy link

jonesnc commented Feb 27, 2025

Here's another update that fixes the csrf_exempt issue for me, without needing to manually configure the LazyView class.

This version lazily copies the view's dispatch method attributes over to the LazyView's function wrapper's attributes. The dispatch method attributes are updated by the from django.views.decorators.csrf.csrf_exempt decorator on the view's dispatch method, for example.

class LazyView:
    """
    Wrapper for view class that can be used for urls to avoid loading the view class
    at the import time.
    The view is imported at first access and cached.
    To use in urls.py, just instantiate `LazyView` with class path argument and use
    as a normal view:

    path(
        "view/",
        LazyView("path.to.ViewClass").as_view(),
        name="url-name"
    ),

    """

    view_cls_path = None
    view_entry_point = None
    view_cls = None
    is_loaded = False

    def __init__(self, view_cls_path: str) -> None:
        super().__init__()
        self.view_cls_path = view_cls_path

    def _import_view(self):
        """Returns the view.as_view() function at self.view_cls_path."""
        self.view_cls: type[View] = import_string(self.view_cls_path)
        self.view_entry_point = self.view_cls.as_view(**self.initkwargs)
        self.is_loaded = True

    def as_view(self, **initkwargs):

        self.initkwargs = initkwargs

        # If LAZY_LOAD_VIEWS is False, view is imported at server start. This is done to
        # keep response times low in production, as importing views at request-time
        # can be expensive.
        if not settings.LAZY_LOAD_VIEWS:
            # Instead of returning the lazy wrapper here, just return the result
            # of calling view_cls.as_view(), which is set to self.view_entry_point.
            self._import_view()
            return self.view_entry_point

        else:

            class ViewFunctionWrapper:
                """Lazily copy the attributes set by decorators like @csrf_exempt.

                Since we want to preserve laziness, the attributes are only copied
                when the __getattr__ function is called, which is during the request,
                instead of on server start.
                """

                def __init__(wrapper_self, func):
                    wrapper_self.func = func

                    # We only need to copy the attributes once, which this attribute
                    # tracks.
                    wrapper_self.has_loaded_dispatch_attrs = False

                def __call__(wrapper_self, *args, **kwargs):
                    """Forward the call to the original function."""
                    return wrapper_self.func(*args, **kwargs)

                def __getattr__(wrapper_self, name):
                    """Copies the dispatch attrs to the wrapped func's attrs."""

                    # Ignore __ attributes to keep things lightweight, since the
                    # assumption is that no dispatch decorator will set an important
                    # attribute with __ in the name.
                    if not wrapper_self.has_loaded_dispatch_attrs and "__" not in name:
                        wrapper_self.has_loaded_dispatch_attrs = True

                        if not self.is_loaded:
                            self._import_view()

                        # Copy possible attributes set by decorators,
                        # e.g. @csrf_exempt, from the dispatch method.
                        wrapper_self.func.__dict__.update(
                            self.view_cls.dispatch.__dict__
                        )

                    # Return the actual attr value here.
                    return getattr(wrapper_self.func, name)

            def view_func(*args, **kwargs):
                """Wrapper of the self.view function that will lazily import view."""

                # The ViewFunctionWrapper *probably* already imported the view, but
                # just to be safe, we'll call it here as well.
                if not self.is_loaded:
                    self._import_view()

                return self.view_entry_point(*args, **kwargs)

            wrapped_view_func = ViewFunctionWrapper(view_func)

            return wrapped_view_func

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment