-
-
Save mick88/a29f3dd675c2c7a4ea6e549b707189a6 to your computer and use it in GitHub Desktop.
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 |
Thanks so much for creating this! It has been super useful. I have two changes that I'd like to share that have improved its usage:
- Allow lazy loading to be disabled with a setting (eg
settings.LAZY_LOAD_VIEWS
). I set this toFalse
in production, andTrue
in development environments. This makes the view load immediately upon server startup in production so I don't impact performance there, but I can still get the runserver startup improvement given by lazily loading views. Note that I also refactored the module importing logic to a separate method, since it's used in multiple places. - I had an issue with the
csrf_exempt
method decorator not being honored when using LazyView. So, I refactored theview_func
to be an enclosed function instead of a method, and I manually set theview_func.csrf_exempt = True
attribute on it where it's needed.
Here's the version I'm using:
"""Taken from: https://gist.github.com/mick88/a29f3dd675c2c7a4ea6e549b707189a6"""
from typing import Callable
from django.conf import settings
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:
path(
"view/",
LazyView("path.to.ViewClass").as_view(),
name="url-name"
),
If the URL needs to be csrf_exempt, you'll need to inform the LazyView that it's
csrf_exempt when calling the LazyView.as_view method:
path(
"view/",
LazyView("path.to.ViewClass").as_view(csrf_exempt=True),
name="url-name"
),
NOTE: Your CBV will still need to inherit from CSRFExemptMixin.
"""
def __init__(self, view_cls_path: str) -> None:
super().__init__()
self.view_cls_path = view_cls_path
def _import_view(self):
"""Imports the module at self.view_cls_path and sets it to the self.view
attribute.
"""
view_cls: type[View] = import_string(self.view_cls_path)
self.view: Callable = view_cls.as_view(**self.initkwargs)
def as_view(self, csrf_exempt: bool = False, **initkwargs):
def view_func(*args, **kwargs):
"""Wrapper of the self.view function that will import the view first."""
if not hasattr(self, "view"):
self._import_view()
return self.view(*args, **kwargs)
# The CsrfViewMiddleware.process_view method checks for the csrf_exempt
# attribute on the view_func *before* the view_func is called. Even if lazy
# loading is disabled, as it is in production, importing the view module
# immediately will not set the csrf_exempt attribute on the view_func here, so
# we have to set it manually here.
if csrf_exempt:
view_func.csrf_exempt = True
self.initkwargs = initkwargs
# If LAZY_LOAD_VIEWS is False, view is cached at import time. 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:
self._import_view()
# Instead of returning the 'view_func' wrapper, just return the view
# function directly. This will hopefully also reduce the likelihood of bugs,
# since we won't be referencing the view_func wrapper.
return self.view
else:
# Return the view_func wrapper that
return view_func
For URL patterns that need csrf exempt, here's what it looks like:
path(
"",
LazyView("myproject.app.views.MyView").as_view(csrf_exempt=True),
),
If you have any suggestions for how to improve this implementation, or any questions, please let me know!
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.
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
Uh oh!
There was an error while loading. Please reload this page.