From bc26debd9ed2a46fca1b0931c78b4054bd37841d Mon Sep 17 00:00:00 2001 From: John Anderson Date: Thu, 25 Dec 2014 23:42:05 -0800 Subject: Add support for passing unbound class methods to `add_view` --- pyramid/config/views.py | 13 ++++++++++++- pyramid/tests/test_config/test_views.py | 21 +++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index c01b72e12..3e305055f 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -42,7 +42,8 @@ from pyramid.compat import ( url_quote, WIN, is_bound_method, - is_nonstr_iter + is_nonstr_iter, + im_self, ) from pyramid.exceptions import ( @@ -418,6 +419,16 @@ class DefaultViewMapper(object): self.attr = kw.get('attr') def __call__(self, view): + # Map the attr directly if the passed in view is method and a + # constructor is defined and must be unbound (for backwards + # compatibility) + if inspect.ismethod(view): + is_bound = getattr(view, im_self, None) is not None + + if not is_bound: + self.attr = view.__name__ + view = view.im_class + if inspect.isclass(view): view = self.map_class(view) else: diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index b0d03fb72..664208fad 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -1666,6 +1666,27 @@ class TestViewsConfigurationMixin(unittest.TestCase): renderer=null_renderer) self.assertRaises(ConfigurationConflictError, config.commit) + def test_add_view_class_method_no_attr(self): + from pyramid.renderers import null_renderer + from zope.interface import directlyProvides + + class ViewClass(object): + def __init__(self, request): + self.request = request + + def run(self): + return 'OK' + + config = self._makeOne(autocommit=True) + config.add_view(view=ViewClass.run, renderer=null_renderer) + + wrapper = self._getViewCallable(config) + context = DummyContext() + directlyProvides(context, IDummy) + request = self._makeRequest(config) + result = wrapper(context, request) + self.assertEqual(result, 'OK') + def test_derive_view_function(self): from pyramid.renderers import null_renderer def view(request): -- cgit v1.2.3 From 4a7029f6b313b65ba94d0726042ea3adbad38e81 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Fri, 26 Dec 2014 22:48:41 -0800 Subject: Raise errors if unbound methods are passed in --- pyramid/compat.py | 17 ++++++++++++ pyramid/config/views.py | 15 +++++------ pyramid/tests/test_compat.py | 46 +++++++++++++++++++++++++++++++++ pyramid/tests/test_config/test_views.py | 21 +++++---------- 4 files changed, 76 insertions(+), 23 deletions(-) create mode 100644 pyramid/tests/test_compat.py diff --git a/pyramid/compat.py b/pyramid/compat.py index bfa345b88..749435ebc 100644 --- a/pyramid/compat.py +++ b/pyramid/compat.py @@ -244,3 +244,20 @@ else: def is_bound_method(ob): return inspect.ismethod(ob) and getattr(ob, im_self, None) is not None +def is_unbound_method(fn): + """ + This consistently verifies that the callable is bound to a + class. + """ + is_bound = is_bound_method(fn) + + if not is_bound and inspect.isroutine(fn): + spec = inspect.getargspec(fn) + has_self = len(spec.args) > 0 and spec.args[0] == 'self' + + if PY3 and inspect.isfunction(fn) and has_self: # pragma: no cover + return True + elif inspect.ismethod(fn): + return True + + return False diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 3e305055f..d498395e1 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -42,6 +42,7 @@ from pyramid.compat import ( url_quote, WIN, is_bound_method, + is_unbound_method, is_nonstr_iter, im_self, ) @@ -419,15 +420,11 @@ class DefaultViewMapper(object): self.attr = kw.get('attr') def __call__(self, view): - # Map the attr directly if the passed in view is method and a - # constructor is defined and must be unbound (for backwards - # compatibility) - if inspect.ismethod(view): - is_bound = getattr(view, im_self, None) is not None - - if not is_bound: - self.attr = view.__name__ - view = view.im_class + if is_unbound_method(view) and self.attr is None: + raise ConfigurationError(( + 'Unbound method calls are not supported, please set the class ' + 'as your `view` and the method as your `attr`' + )) if inspect.isclass(view): view = self.map_class(view) diff --git a/pyramid/tests/test_compat.py b/pyramid/tests/test_compat.py new file mode 100644 index 000000000..2f80100dd --- /dev/null +++ b/pyramid/tests/test_compat.py @@ -0,0 +1,46 @@ +import unittest + +class TestUnboundMethods(unittest.TestCase): + def test_old_style_bound(self): + from pyramid.compat import is_unbound_method + + class OldStyle: + def run(self): + return 'OK' + + self.assertFalse(is_unbound_method(OldStyle().run)) + + def test_new_style_bound(self): + from pyramid.compat import is_unbound_method + + class NewStyle(object): + def run(self): + return 'OK' + + self.assertFalse(is_unbound_method(NewStyle().run)) + + def test_old_style_unbound(self): + from pyramid.compat import is_unbound_method + + class OldStyle: + def run(self): + return 'OK' + + self.assertTrue(is_unbound_method(OldStyle.run)) + + def test_new_style_unbound(self): + from pyramid.compat import is_unbound_method + + class NewStyle(object): + def run(self): + return 'OK' + + self.assertTrue(is_unbound_method(NewStyle.run)) + + def test_normal_func_unbound(self): + from pyramid.compat import is_unbound_method + + def func(): + return 'OK' + + self.assertFalse(is_unbound_method(func)) diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index 664208fad..d1eb1ed3c 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -1669,23 +1669,16 @@ class TestViewsConfigurationMixin(unittest.TestCase): def test_add_view_class_method_no_attr(self): from pyramid.renderers import null_renderer from zope.interface import directlyProvides - - class ViewClass(object): - def __init__(self, request): - self.request = request - - def run(self): - return 'OK' + from pyramid.exceptions import ConfigurationError config = self._makeOne(autocommit=True) - config.add_view(view=ViewClass.run, renderer=null_renderer) + class DummyViewClass(object): + def run(self): pass - wrapper = self._getViewCallable(config) - context = DummyContext() - directlyProvides(context, IDummy) - request = self._makeRequest(config) - result = wrapper(context, request) - self.assertEqual(result, 'OK') + def configure_view(): + config.add_view(view=DummyViewClass.run, renderer=null_renderer) + + self.assertRaises(ConfigurationError, configure_view) def test_derive_view_function(self): from pyramid.renderers import null_renderer -- cgit v1.2.3 From 6d4676137885f63f364a2b2ae6205c6931a57220 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Fri, 26 Dec 2014 23:04:56 -0800 Subject: Don't need im_self --- pyramid/config/views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index d498395e1..afacc1e0b 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -44,7 +44,6 @@ from pyramid.compat import ( is_bound_method, is_unbound_method, is_nonstr_iter, - im_self, ) from pyramid.exceptions import ( -- cgit v1.2.3 From 03a0d79306b2846313df1983a721d5cccf4ec3ce Mon Sep 17 00:00:00 2001 From: John Anderson Date: Fri, 26 Dec 2014 23:19:32 -0800 Subject: Clean up compat tests --- pyramid/tests/test_compat.py | 36 ++++++++---------------------------- 1 file changed, 8 insertions(+), 28 deletions(-) diff --git a/pyramid/tests/test_compat.py b/pyramid/tests/test_compat.py index 2f80100dd..23ccce82e 100644 --- a/pyramid/tests/test_compat.py +++ b/pyramid/tests/test_compat.py @@ -1,46 +1,26 @@ import unittest +from pyramid.compat import is_unbound_method class TestUnboundMethods(unittest.TestCase): def test_old_style_bound(self): - from pyramid.compat import is_unbound_method - - class OldStyle: - def run(self): - return 'OK' - self.assertFalse(is_unbound_method(OldStyle().run)) def test_new_style_bound(self): - from pyramid.compat import is_unbound_method - - class NewStyle(object): - def run(self): - return 'OK' - self.assertFalse(is_unbound_method(NewStyle().run)) def test_old_style_unbound(self): - from pyramid.compat import is_unbound_method - - class OldStyle: - def run(self): - return 'OK' - self.assertTrue(is_unbound_method(OldStyle.run)) def test_new_style_unbound(self): - from pyramid.compat import is_unbound_method - - class NewStyle(object): - def run(self): - return 'OK' - self.assertTrue(is_unbound_method(NewStyle.run)) def test_normal_func_unbound(self): - from pyramid.compat import is_unbound_method - - def func(): - return 'OK' + def func(): return 'OK' self.assertFalse(is_unbound_method(func)) + +class OldStyle: + def run(self): return 'OK' + +class NewStyle(object): + def run(self): return 'OK' -- cgit v1.2.3