diff options
| author | Chris McDonough <chrism@plope.com> | 2012-02-14 04:15:09 -0500 |
|---|---|---|
| committer | Chris McDonough <chrism@plope.com> | 2012-02-14 04:15:09 -0500 |
| commit | 0dd383d5c160460a66cef5fce46a4d4e7c6fe167 (patch) | |
| tree | fe7c2a0f7516e1c8b59adfa747441e0d8db30061 | |
| parent | ad3c25bac043e9d14ce8ffe1b03ae6d0e92b3b0e (diff) | |
| parent | 9ed1e0ba957c36f6ae29c25ffeaa6c2c02f716a9 (diff) | |
| download | pyramid-0dd383d5c160460a66cef5fce46a4d4e7c6fe167.tar.gz pyramid-0dd383d5c160460a66cef5fce46a4d4e7c6fe167.tar.bz2 pyramid-0dd383d5c160460a66cef5fce46a4d4e7c6fe167.zip | |
Merge branch 'master' into 1.3-branch
| -rw-r--r-- | CHANGES.txt | 6 | ||||
| -rw-r--r-- | docs/designdefense.rst | 4 | ||||
| -rw-r--r-- | docs/narr/views.rst | 2 | ||||
| -rw-r--r-- | pyramid/config/util.py | 5 | ||||
| -rw-r--r-- | pyramid/config/views.py | 32 | ||||
| -rw-r--r-- | pyramid/tests/test_config/test_views.py | 125 | ||||
| -rw-r--r-- | pyramid/urldispatch.py | 2 |
7 files changed, 165 insertions, 11 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index fcd54217f..411681d81 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -11,6 +11,12 @@ Features http://readthedocs.org/docs/venusian/en/latest/#ignore-scan-argument for more information about how to use the ``ignore`` argument to ``scan``. +- Better error messages when a view callable returns a value that cannot be + converted to a response (for example, when a view callable returns a + dictionary without a renderer defined, or doesn't return any value at all). + The error message now contains information about the view callable itself + as well as the result of calling it. + Dependencies ------------ diff --git a/docs/designdefense.rst b/docs/designdefense.rst index 59b0e5a2d..84b6be907 100644 --- a/docs/designdefense.rst +++ b/docs/designdefense.rst @@ -219,7 +219,7 @@ is this: Using such wrappers, we strive to always hide the ZCA API from application developers. Application developers should just never know about the ZCA API: they should call a Python function with some object germane to the domain as -an argument, and it should returns a result. A corollary that follows is +an argument, and it should return a result. A corollary that follows is that any reader of an application that has been written using :app:`Pyramid` needn't understand the ZCA API either. @@ -720,7 +720,7 @@ microframeworks and Django boast. The :mod:`zope.component`, package on which :app:`Pyramid` depends has transitive dependencies on several other packages (:mod:`zope.event`, and :mod:`zope.interface`). :app:`Pyramid` also has its own direct dependencies, -such as :term:`PasteDeploy`, :term:`Chameleon`, :term:`Mako` :term:`WebOb`, +such as :term:`PasteDeploy`, :term:`Chameleon`, :term:`Mako`, :term:`WebOb`, :mod:`zope.deprecation` and some of these in turn have their own transitive dependencies. diff --git a/docs/narr/views.rst b/docs/narr/views.rst index fa34cca61..dbc702de8 100644 --- a/docs/narr/views.rst +++ b/docs/narr/views.rst @@ -30,7 +30,7 @@ View Callables View callables are, at the risk of sounding obvious, callable Python objects. Specifically, view callables can be functions, classes, or instances -that implement an ``__call__`` method (making the instance callable). +that implement a ``__call__`` method (making the instance callable). View callables must, at a minimum, accept a single argument named ``request``. This argument represents a :app:`Pyramid` :term:`Request` diff --git a/pyramid/config/util.py b/pyramid/config/util.py index b39fb8ee0..4c7ecd359 100644 --- a/pyramid/config/util.py +++ b/pyramid/config/util.py @@ -6,7 +6,6 @@ from zope.interface import implementer from pyramid.interfaces import IActionInfo from pyramid.compat import ( - string_types, bytes_, is_nonstr_iter, ) @@ -44,7 +43,7 @@ def action_method(wrapped): self._ainfo = [] info = kw.pop('_info', None) # backframes for outer decorators to actionmethods - backframes = kw.pop('_backframes', 2) + backframes = kw.pop('_backframes', 2) if is_nonstr_iter(info) and len(info) == 4: # _info permitted as extract_stack tuple info = ActionInfo(*info) @@ -132,7 +131,7 @@ def make_predicates(xhr=None, request_method=None, path_info=None, request_method = sorted(request_method) def request_method_predicate(context, request): return request.method in request_method - text = "request method = %s" % repr(request_method) + text = "request method = %r" % request_method request_method_predicate.__text__ = text weights.append(1 << 2) predicates.append(request_method_predicate) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index a87ab54c7..1988b532b 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -55,6 +55,7 @@ from pyramid.security import NO_PERMISSION_REQUIRED from pyramid.static import static_view from pyramid.threadlocal import get_current_registry from pyramid.view import render_view_to_response +from pyramid.util import object_description from pyramid.config.util import ( DEFAULT_PHASH, @@ -67,6 +68,12 @@ from pyramid.config.util import ( urljoin = urlparse.urljoin url_parse = urlparse.urlparse +def view_description(view): + try: + return view.__text__ + except AttributeError: + return object_description(view) + def wraps_view(wrapper): def inner(self, view): wrapper_view = wrapper(self, view) @@ -99,7 +106,7 @@ def preserve_view_attrs(view, wrapper): # "wrapped view" for attr in ('__permitted__', '__call_permissive__', '__permission__', '__predicated__', '__predicates__', '__accept__', - '__order__'): + '__order__', '__text__'): try: setattr(wrapper, attr, getattr(view, attr)) except AttributeError: @@ -343,9 +350,19 @@ class ViewDeriver(object): result = view(context, request) response = registry.queryAdapterOrSelf(result, IResponse) if response is None: - raise ValueError( - 'Could not convert view return value "%s" into a ' - 'response object' % (result,)) + if result is None: + append = (' You may have forgotten to return a value from ' + 'the view callable.') + elif isinstance(result, dict): + append = (' You may have forgotten to define a renderer in ' + 'the view configuration.') + else: + append = '' + msg = ('Could not convert return value of the view callable %s ' + 'into a response object. ' + 'The value returned was %r.' + append) + + raise ValueError(msg % (view_description(view), result)) return response return viewresult_to_response @@ -376,6 +393,8 @@ class DefaultViewMapper(object): mapped_view = self.map_class_requestonly(view) else: mapped_view = self.map_class_native(view) + mapped_view.__text__ = 'method %s of %s' % ( + self.attr or '__call__', object_description(view)) return mapped_view def map_nonclass(self, view): @@ -388,6 +407,11 @@ class DefaultViewMapper(object): mapped_view = self.map_nonclass_requestonly(view) elif self.attr: mapped_view = self.map_nonclass_attr(view) + if self.attr is not None: + mapped_view.__text__ = 'attr %s of %s' % ( + self.attr, object_description(view)) + else: + mapped_view.__text__ = object_description(view) return mapped_view def map_class_requestonly(self, view): diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index 4af29325a..eb18d5c84 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -2282,6 +2282,113 @@ class TestViewDeriver(unittest.TestCase): self.config.registry.registerUtility(policy, IAuthenticationPolicy) self.config.registry.registerUtility(policy, IAuthorizationPolicy) + def test_function_returns_non_adaptable(self): + def view(request): + return None + deriver = self._makeOne() + result = deriver(view) + self.assertFalse(result is view) + try: + result(None, None) + except ValueError as e: + self.assertEqual( + e.args[0], + 'Could not convert return value of the view callable function ' + 'pyramid.tests.test_config.test_views.view into a response ' + 'object. The value returned was None. You may have forgotten ' + 'to return a value from the view callable.' + ) + else: # pragma: no cover + raise AssertionError + + def test_function_returns_non_adaptable_dict(self): + def view(request): + return {'a':1} + deriver = self._makeOne() + result = deriver(view) + self.assertFalse(result is view) + try: + result(None, None) + except ValueError as e: + self.assertEqual( + e.args[0], + "Could not convert return value of the view callable function " + "pyramid.tests.test_config.test_views.view into a response " + "object. The value returned was {'a': 1}. You may have " + "forgotten to define a renderer in the view configuration." + ) + else: # pragma: no cover + raise AssertionError + + def test_instance_returns_non_adaptable(self): + class AView(object): + def __call__(self, request): + return None + view = AView() + deriver = self._makeOne() + result = deriver(view) + self.assertFalse(result is view) + try: + result(None, None) + except ValueError as e: + msg = e.args[0] + self.assertTrue(msg.startswith( + 'Could not convert return value of the view callable object ' + '<pyramid.tests.test_config.test_views.AView object at')) + self.assertTrue(msg.endswith( + '> into a response object. The value returned was None. You ' + 'may have forgotten to return a value from the view callable.')) + else: # pragma: no cover + raise AssertionError + + def test_requestonly_default_method_returns_non_adaptable(self): + request = DummyRequest() + class AView(object): + def __init__(self, request): + pass + def __call__(self): + return None + deriver = self._makeOne() + result = deriver(AView) + self.assertFalse(result is AView) + try: + result(None, request) + except ValueError as e: + self.assertEqual( + e.args[0], + 'Could not convert return value of the view callable ' + 'method __call__ of ' + 'class pyramid.tests.test_config.test_views.AView into a ' + 'response object. The value returned was None. You may have ' + 'forgotten to return a value from the view callable.' + ) + else: # pragma: no cover + raise AssertionError + + def test_requestonly_nondefault_method_returns_non_adaptable(self): + request = DummyRequest() + class AView(object): + def __init__(self, request): + pass + def theviewmethod(self): + return None + deriver = self._makeOne(attr='theviewmethod') + result = deriver(AView) + self.assertFalse(result is AView) + try: + result(None, request) + except ValueError as e: + self.assertEqual( + e.args[0], + 'Could not convert return value of the view callable ' + 'method theviewmethod of ' + 'class pyramid.tests.test_config.test_views.AView into a ' + 'response object. The value returned was None. You may have ' + 'forgotten to return a value from the view callable.' + ) + else: # pragma: no cover + raise AssertionError + def test_requestonly_function(self): response = DummyResponse() def view(request): @@ -3689,6 +3796,24 @@ class TestStaticURLInfo(unittest.TestCase): view_attr='attr') self.assertEqual(config.view_kw['attr'], 'attr') +class Test_view_description(unittest.TestCase): + def _callFUT(self, view): + from pyramid.config.views import view_description + return view_description(view) + + def test_with_text(self): + def view(): pass + view.__text__ = 'some text' + result = self._callFUT(view) + self.assertEqual(result, 'some text') + + def test_without_text(self): + def view(): pass + result = self._callFUT(view) + self.assertEqual(result, + 'function pyramid.tests.test_config.test_views.view') + + class DummyRegistry: pass diff --git a/pyramid/urldispatch.py b/pyramid/urldispatch.py index bd1da8f71..009804280 100644 --- a/pyramid/urldispatch.py +++ b/pyramid/urldispatch.py @@ -14,13 +14,13 @@ from pyramid.compat import ( string_types, binary_type, is_nonstr_iter, + decode_path_info, ) from pyramid.exceptions import URLDecodeError from pyramid.traversal import ( quote_path_segment, - decode_path_info, split_path_info, ) |
