From 6c15971a0006ed944e19a8f93b5897dccda028c0 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 20 Mar 2012 00:35:17 -0400 Subject: - The fix for issue https://github.com/Pylons/pyramid/issues/461 (which made it possible for instance methods to be used as view callables) introduced a backwards incompatibility when methods that declared only a request argument were used. See https://github.com/Pylons/pyramid/issues/503 Fixes #503 --- CHANGES.txt | 5 +++++ pyramid/compat.py | 7 +++++++ pyramid/config/views.py | 27 ++++++++++++--------------- pyramid/tests/test_config/test_views.py | 12 ++++++++++++ 4 files changed, 36 insertions(+), 15 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 4befd2ecc..916deb009 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -11,6 +11,11 @@ Bug Fixes because it mistakenly detects that a route was matched when, in fact, it was not. +- The fix for issue https://github.com/Pylons/pyramid/issues/461 (which made + it possible for instance methods to be used as view callables) introduced a + backwards incompatibility when methods that declared only a request + argument were used. See https://github.com/Pylons/pyramid/issues/503 + 1.3b3 (2012-03-17) ================== diff --git a/pyramid/compat.py b/pyramid/compat.py index 3459e0b02..222810b3b 100644 --- a/pyramid/compat.py +++ b/pyramid/compat.py @@ -1,3 +1,4 @@ +import inspect import platform import sys import types @@ -185,8 +186,10 @@ else: if PY3: # pragma: no cover im_func = '__func__' + im_self = '__self__' else: im_func = 'im_func' + im_self = 'im_self' try: # pragma: no cover import configparser @@ -237,3 +240,7 @@ else: from urlparse import unquote as unquote_to_bytes def unquote_bytes_to_wsgi(bytestring): return unquote_to_bytes(bytestring) + +def is_bound_method(ob): + return inspect.ismethod(ob) and getattr(ob, im_self, None) is not None + diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 432db79cf..a79d20d7d 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -41,6 +41,7 @@ from pyramid.compat import ( im_func, url_quote, WIN, + is_bound_method, ) from pyramid.exceptions import ( @@ -140,18 +141,7 @@ class ViewDeriver(object): self.decorated_view( self.rendered_view( self.mapped_view( - self.text_wrapped_view( - view)))))))))) - - @wraps_view - def text_wrapped_view(self, view): - # if the method is an instance method, we need to wrap it in order - # to be able to assign a __text__ value to it later. see #461. - if inspect.ismethod(view): - def text_wrapper(context, request): - return view(context, request) - return text_wrapper - return view + view))))))))) @wraps_view def mapped_view(self, view): @@ -428,9 +418,16 @@ class DefaultViewMapper(object): elif self.attr: mapped_view = self.map_nonclass_attr(view) if inspect.isroutine(mapped_view): - # we potentially mutate an unwrapped view here if it's a function; - # we do this to avoid function call overhead of injecting another - # wrapper + # This branch will be true if the view is a function or a method. + # We potentially mutate an unwrapped object here if it's a + # function. We do this to avoid function call overhead of + # injecting another wrapper. However, we must wrap if the + # function is a bound method because we can't set attributes on a + # bound method. + if is_bound_method(view): + _mapped_view = mapped_view + def mapped_view(context, request): + return _mapped_view(context, request) if self.attr is not None: mapped_view.__text__ = 'attr %s of %s' % ( self.attr, object_description(view)) diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index 8877c011e..d44bec691 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -230,6 +230,18 @@ class TestViewsConfigurationMixin(unittest.TestCase): result = wrapper(None, None) self.assertEqual(result, 'OK') + def test_add_view_as_instancemethod_requestonly(self): + from pyramid.renderers import null_renderer + class View: + def index(self, request): + return 'OK' + view = View() + config=self._makeOne(autocommit=True) + config.add_view(view=view.index, renderer=null_renderer) + wrapper = self._getViewCallable(config) + result = wrapper(None, None) + self.assertEqual(result, 'OK') + def test_add_view_as_instance_requestonly(self): from pyramid.renderers import null_renderer class AView: -- cgit v1.2.3