From 1d21b0f7b336f5761e570023d64d413748b07cc1 Mon Sep 17 00:00:00 2001 From: Stephen Burrows Date: Wed, 1 Jun 2011 11:53:53 -0400 Subject: [PATCH] Refactored RequestNodeMiddleware to use SimpleLazyObject - comparable to django r16297/r16305. Resolves issue #144. --- philo/contrib/penfield/models.py | 2 +- philo/middleware.py | 73 +++++++++++------------- philo/models/nodes.py | 2 +- philo/utils/lazycompat.py | 97 ++++++++++++++++++++++++++++++++ philo/views.py | 4 +- 5 files changed, 133 insertions(+), 45 deletions(-) create mode 100644 philo/utils/lazycompat.py diff --git a/philo/contrib/penfield/models.py b/philo/contrib/penfield/models.py index b8ca610..7a3eaa8 100644 --- a/philo/contrib/penfield/models.py +++ b/philo/contrib/penfield/models.py @@ -201,7 +201,7 @@ class FeedView(MultiView): language = settings.LANGUAGE_CODE.decode(), feed_url = add_domain( current_site.domain, - self.__get_dynamic_attr('feed_url', obj) or node.construct_url(node.subpath, with_domain=True, request=request, secure=request.is_secure()), + self.__get_dynamic_attr('feed_url', obj) or node.construct_url(node._subpath, with_domain=True, request=request, secure=request.is_secure()), request.is_secure() ), author_name = self.__get_dynamic_attr('author_name', obj), diff --git a/philo/middleware.py b/philo/middleware.py index b90067a..037fdc8 100644 --- a/philo/middleware.py +++ b/philo/middleware.py @@ -3,56 +3,47 @@ from django.contrib.sites.models import Site from django.http import Http404 from philo.models import Node, View +from philo.utils.lazycompat import SimpleLazyObject -class LazyNode(object): - def __get__(self, request, obj_type=None): - if not hasattr(request, '_cached_node_path'): - return None - - if not hasattr(request, '_found_node'): - try: - current_site = Site.objects.get_current() - except Site.DoesNotExist: - current_site = None - - path = request._cached_node_path - trailing_slash = False - if path[-1] == '/': - trailing_slash = True - - try: - node, subpath = Node.objects.get_with_path(path, root=getattr(current_site, 'root_node', None), absolute_result=False) - except Node.DoesNotExist: - node = None - else: - if subpath is None: - subpath = "" - subpath = "/" + subpath - - if not node.handles_subpath(subpath): - node = None - else: - if trailing_slash and subpath[-1] != "/": - subpath += "/" - - node.subpath = subpath - - request._found_node = node - - return request._found_node +def get_node(path): + """Returns a :class:`Node` instance at ``path`` (relative to the current site) or ``None``.""" + try: + current_site = Site.objects.get_current() + except Site.DoesNotExist: + current_site = None + + trailing_slash = False + if path[-1] == '/': + trailing_slash = True + + try: + node, subpath = Node.objects.get_with_path(path, root=getattr(current_site, 'root_node', None), absolute_result=False) + except Node.DoesNotExist: + return None + + if subpath is None: + subpath = "" + subpath = "/" + subpath + + if trailing_slash and subpath[-1] != "/": + subpath += "/" + + node._path = path + node._subpath = subpath + + return node class RequestNodeMiddleware(object): """Adds a ``node`` attribute, representing the currently-viewed node, to every incoming :class:`HttpRequest` object. This is required by :func:`philo.views.node_view`.""" - def process_request(self, request): - request.__class__.node = LazyNode() - def process_view(self, request, view_func, view_args, view_kwargs): try: - request._cached_node_path = view_kwargs['path'] + path = view_kwargs['path'] except KeyError: - pass + request.node = None + else: + request.node = SimpleLazyObject(lambda: get_node(path)) def process_exception(self, request, exception): if settings.DEBUG or not hasattr(request, 'node') or not request.node: diff --git a/philo/models/nodes.py b/philo/models/nodes.py index 87ccb40..0e041f2 100644 --- a/philo/models/nodes.py +++ b/philo/models/nodes.py @@ -235,7 +235,7 @@ class MultiView(View): """ clear_url_caches() - subpath = request.node.subpath + subpath = request.node._subpath view, args, kwargs = resolve(subpath, urlconf=self) view_args = getargspec(view) if extra_context is not None and ('extra_context' in view_args[0] or view_args[2] is not None): diff --git a/philo/utils/lazycompat.py b/philo/utils/lazycompat.py new file mode 100644 index 0000000..3876562 --- /dev/null +++ b/philo/utils/lazycompat.py @@ -0,0 +1,97 @@ +try: + from django.utils.functional import empty, LazyObject, SimpleLazyObject +except ImportError: + # Supply LazyObject and SimpleLazyObject for django < r16308 + import operator + + + empty = object() + def new_method_proxy(func): + def inner(self, *args): + if self._wrapped is empty: + self._setup() + return func(self._wrapped, *args) + return inner + + class LazyObject(object): + """ + A wrapper for another class that can be used to delay instantiation of the + wrapped class. + + By subclassing, you have the opportunity to intercept and alter the + instantiation. If you don't need to do that, use SimpleLazyObject. + """ + def __init__(self): + self._wrapped = empty + + __getattr__ = new_method_proxy(getattr) + + def __setattr__(self, name, value): + if name == "_wrapped": + # Assign to __dict__ to avoid infinite __setattr__ loops. + self.__dict__["_wrapped"] = value + else: + if self._wrapped is empty: + self._setup() + setattr(self._wrapped, name, value) + + def __delattr__(self, name): + if name == "_wrapped": + raise TypeError("can't delete _wrapped.") + if self._wrapped is empty: + self._setup() + delattr(self._wrapped, name) + + def _setup(self): + """ + Must be implemented by subclasses to initialise the wrapped object. + """ + raise NotImplementedError + + # introspection support: + __members__ = property(lambda self: self.__dir__()) + __dir__ = new_method_proxy(dir) + + + class SimpleLazyObject(LazyObject): + """ + A lazy object initialised from any function. + + Designed for compound objects of unknown type. For builtins or objects of + known type, use django.utils.functional.lazy. + """ + def __init__(self, func): + """ + Pass in a callable that returns the object to be wrapped. + + If copies are made of the resulting SimpleLazyObject, which can happen + in various circumstances within Django, then you must ensure that the + callable can be safely run more than once and will return the same + value. + """ + self.__dict__['_setupfunc'] = func + super(SimpleLazyObject, self).__init__() + + def _setup(self): + self._wrapped = self._setupfunc() + + __str__ = new_method_proxy(str) + __unicode__ = new_method_proxy(unicode) + + def __deepcopy__(self, memo): + if self._wrapped is empty: + # We have to use SimpleLazyObject, not self.__class__, because the + # latter is proxied. + result = SimpleLazyObject(self._setupfunc) + memo[id(self)] = result + return result + else: + import copy + return copy.deepcopy(self._wrapped, memo) + + # Need to pretend to be the wrapped class, for the sake of objects that care + # about this (especially in equality tests) + __class__ = property(new_method_proxy(operator.attrgetter("__class__"))) + __eq__ = new_method_proxy(operator.eq) + __hash__ = new_method_proxy(hash) + __nonzero__ = new_method_proxy(bool) \ No newline at end of file diff --git a/philo/views.py b/philo/views.py index d3054b9..2c2a952 100644 --- a/philo/views.py +++ b/philo/views.py @@ -37,10 +37,10 @@ def node_view(request, path=None, **kwargs): raise Http404 node = request.node - subpath = request.node.subpath + subpath = request.node._subpath # Explicitly disallow trailing slashes if we are otherwise at a node's url. - if request._cached_node_path != "/" and request._cached_node_path[-1] == "/" and subpath == "/": + if node._path != "/" and node._path[-1] == "/" and subpath == "/": return HttpResponseRedirect(node.get_absolute_url()) if not node.handles_subpath(subpath): -- 2.20.1