Last active
February 27, 2025 20:11
-
-
Save mick88/a29f3dd675c2c7a4ea6e549b707189a6 to your computer and use it in GitHub Desktop.
Django: Lazy view
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
Author
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.
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
For views that need decorators, i tend to use decorators within the class itself (by decorating dispatch() method) rather than urls.py. If this method works for you, it'd be a better workaround than passing boolean arguments.