From d8afa3b5cb5e896700e654c3b8d90de54d54269c Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sun, 29 Jan 2012 01:06:46 -0800 Subject: Clarify Python 2 and 3 compatibility for tl;dr users. --- docs/whatsnew-1.3.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/whatsnew-1.3.rst b/docs/whatsnew-1.3.rst index ee4e2ccb5..cdb7a3b4f 100644 --- a/docs/whatsnew-1.3.rst +++ b/docs/whatsnew-1.3.rst @@ -15,7 +15,9 @@ The major feature additions in Pyramid 1.3 follow. Python 3 Compatibility ~~~~~~~~~~~~~~~~~~~~~~ -Pyramid is now Python 3 compatible. Python 3.2 or better is required. +In addition to running on Python 2 (version 2.6 or 2.7 required), Pyramid is +now Python 3 compatible. For Python 3 compatibility, Python 3.2 or better +is required. .. warning:: -- cgit v1.2.3 From d01ae960c9e278bd361e2517e0fef7173549f642 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 5 Feb 2012 23:56:29 -0500 Subject: fix extra paren --- docs/narr/urldispatch.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/narr/urldispatch.rst b/docs/narr/urldispatch.rst index 6d9dfdd92..dfa4e629d 100644 --- a/docs/narr/urldispatch.rst +++ b/docs/narr/urldispatch.rst @@ -341,7 +341,7 @@ The above pattern will match these URLs, generating the following matchdicts: .. code-block:: text foo/1/2/ -> {'baz':u'1', 'bar':u'2', 'fizzle':()} - foo/abc/def/a/b/c -> {'baz':u'abc', 'bar':u'def', 'fizzle': u'a/b/c')} + foo/abc/def/a/b/c -> {'baz':u'abc', 'bar':u'def', 'fizzle': u'a/b/c'} This occurs because the default regular expression for a marker is ``[^/]+`` which will match everything up to the first ``/``, while ``{fizzle:.*}`` will -- cgit v1.2.3 From 9399c12f561a74cdba7a5758c78173083625e6a9 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 6 Feb 2012 10:37:14 -0600 Subject: fix #424 --- docs/narr/project.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/narr/project.rst b/docs/narr/project.rst index ea0045ca7..d69f0cf13 100644 --- a/docs/narr/project.rst +++ b/docs/narr/project.rst @@ -98,7 +98,7 @@ Or on Windows: .. code-block:: text - $ Scripts\pcreate alchemy MyProject + $ Scripts\pcreate -s alchemy MyProject Here's sample output from a run of ``pcreate`` on UNIX for a project we name ``MyProject``: -- cgit v1.2.3 From 22e0aae7ebb0963b1322af146c52830226941f60 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 7 Feb 2012 01:11:19 -0500 Subject: - Internal: catch unhashable discriminators early (raise an error instead of allowing them to find their way into resolveConflicts). --- CHANGES.txt | 3 +++ pyramid/config/__init__.py | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 9edcbdbe8..7d20796e1 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -7,6 +7,9 @@ Features - More informative error message when a ``config.include`` cannot find an ``includeme``. See https://github.com/Pylons/pyramid/pull/392. +- Internal: catch unhashable discriminators early (raise an error instead of + allowing them to find their way into resolveConflicts). + Bug Fixes --------- diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index 57ab7e13a..bd3a80ad3 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -533,6 +533,10 @@ class Configurator( ``extra`` provides a facility for inserting extra keys and values into an action dictionary. """ + # catch nonhashable discriminators here; most unit tests use + # autocommit=False, which won't catch unhashable discriminators + assert hash(discriminator) + if kw is None: kw = {} -- cgit v1.2.3 From ac0ecec4c851c7d76418c1520b27619b93a808eb Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 6 Feb 2012 23:04:32 -0600 Subject: Added a test to reproduce #425. --- pyramid/tests/test_config/test_util.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pyramid/tests/test_config/test_util.py b/pyramid/tests/test_config/test_util.py index ebf308929..43e56af23 100644 --- a/pyramid/tests/test_config/test_util.py +++ b/pyramid/tests/test_config/test_util.py @@ -328,6 +328,16 @@ class Test__make_predicates(unittest.TestCase): hash2, _, __= self._callFUT(request_method='GET') self.assertEqual(hash1, hash2) + def test_match_param_hashable(self): + # https://github.com/Pylons/pyramid/issues/425 + import pyramid.testing + def view(request): pass + config = pyramid.testing.setUp(autocommit=False) + config.add_route('foo', '/foo/{bar}') + config.add_view(view, route_name='foo', match_param='bar=barf') + config.add_view(view, route_name='foo', match_param={'bar': 'baz'}) + config.commit() + class TestActionInfo(unittest.TestCase): def _getTargetClass(self): from pyramid.config.util import ActionInfo -- cgit v1.2.3 From 835d4812d3b5e37c54325b992f66ba45714d56cb Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 6 Feb 2012 23:37:37 -0600 Subject: Modified match_param to accept a tuple. Fixes #425. --- CHANGES.txt | 11 +++++++++++ pyramid/config/util.py | 14 ++++++++------ pyramid/config/views.py | 8 ++++---- pyramid/tests/test_config/test_util.py | 12 ++++++------ 4 files changed, 29 insertions(+), 16 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 7d20796e1..8bfdc6d66 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -10,6 +10,10 @@ Features - Internal: catch unhashable discriminators early (raise an error instead of allowing them to find their way into resolveConflicts). +- The `match_param` view predicate now accepts a string or a tuple. + This replaces the broken behavior of accepting a dict. See + https://github.com/Pylons/pyramid/issues/425 for more information. + Bug Fixes --------- @@ -21,6 +25,13 @@ Bug Fixes - The ``prequest`` script would fail when used against URLs which did not return HTML or text. See https://github.com/Pylons/pyramid/issues/381 +Backwards Incompatibilities +--------------------------- + +- The `match_param` view predicate no longer accepts a dict. This will + have no negative affect because the implementation was broken for + dict-based arguments. + Documentation ------------- diff --git a/pyramid/config/util.py b/pyramid/config/util.py index 6c1bb8368..b39fb8ee0 100644 --- a/pyramid/config/util.py +++ b/pyramid/config/util.py @@ -221,19 +221,21 @@ def make_predicates(xhr=None, request_method=None, path_info=None, h.update(bytes_('request_type:%r' % hash(request_type))) if match_param is not None: - if isinstance(match_param, string_types): - match_param, match_param_val = match_param.split('=', 1) - match_param = {match_param: match_param_val} - text = "match_param %s" % match_param + if not is_nonstr_iter(match_param): + match_param = (match_param,) + match_param = sorted(match_param) + text = "match_param %s" % repr(match_param) + reqs = [p.split('=', 1) for p in match_param] def match_param_predicate(context, request): - for k, v in match_param.items(): + for k, v in reqs: if request.matchdict.get(k) != v: return False return True match_param_predicate.__text__ = text weights.append(1 << 9) predicates.append(match_param_predicate) - h.update(bytes_('match_param:%r' % match_param)) + for p in match_param: + h.update(bytes_('match_param:%r' % p)) if custom: for num, predicate in enumerate(custom): diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 0359c46f7..a87ab54c7 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -843,18 +843,18 @@ class ViewsConfiguratorMixin(object): .. note:: This feature is new as of :app:`Pyramid` 1.2. - This param may be either a single string of the format "key=value" - or a dict of key/value pairs. + This value can be a string of the format "key=value" or a tuple + containing one or more of these strings. A view declaration with this argument ensures that the view will only be called when the :term:`request` has key/value pairs in its :term:`matchdict` that equal those supplied in the predicate. e.g. ``match_param="action=edit" would require the ``action`` - parameter in the :term:`matchdict` match the right hande side of + parameter in the :term:`matchdict` match the right hand side of the expression (``edit``) for the view to "match" the current request. - If the ``match_param`` is a dict, every key/value pair must match + If the ``match_param`` is a tuple, every key/value pair must match for the predicate to pass. containment diff --git a/pyramid/tests/test_config/test_util.py b/pyramid/tests/test_config/test_util.py index 43e56af23..1ad1fb3c1 100644 --- a/pyramid/tests/test_config/test_util.py +++ b/pyramid/tests/test_config/test_util.py @@ -281,7 +281,7 @@ class Test__make_predicates(unittest.TestCase): self.assertEqual(predicates[5].__text__, 'accept = accept') self.assertEqual(predicates[6].__text__, 'containment = containment') self.assertEqual(predicates[7].__text__, 'request_type = request_type') - self.assertEqual(predicates[8].__text__, "match_param {'foo': 'bar'}") + self.assertEqual(predicates[8].__text__, "match_param ['foo=bar']") self.assertEqual(predicates[9].__text__, 'custom predicate') self.assertEqual(predicates[10].__text__, 'classmethod predicate') self.assertEqual(predicates[11].__text__, '') @@ -299,13 +299,13 @@ class Test__make_predicates(unittest.TestCase): self.assertFalse(predicates[0](Dummy(), request)) def test_match_param_from_dict(self): - _, predicates, _ = self._callFUT(match_param={'foo':'bar','baz':'bum'}) + _, predicates, _ = self._callFUT(match_param=('foo=bar','baz=bum')) request = DummyRequest() request.matchdict = {'foo':'bar', 'baz':'bum'} self.assertTrue(predicates[0](Dummy(), request)) def test_match_param_from_dict_fails(self): - _, predicates, _ = self._callFUT(match_param={'foo':'bar','baz':'bum'}) + _, predicates, _ = self._callFUT(match_param=('foo=bar','baz=bum')) request = DummyRequest() request.matchdict = {'foo':'bar', 'baz':'foo'} self.assertFalse(predicates[0](Dummy(), request)) @@ -333,9 +333,9 @@ class Test__make_predicates(unittest.TestCase): import pyramid.testing def view(request): pass config = pyramid.testing.setUp(autocommit=False) - config.add_route('foo', '/foo/{bar}') - config.add_view(view, route_name='foo', match_param='bar=barf') - config.add_view(view, route_name='foo', match_param={'bar': 'baz'}) + config.add_route('foo', '/foo/{a}/{b}') + config.add_view(view, route_name='foo', match_param='a=bar') + config.add_view(view, route_name='foo', match_param=('a=bar', 'b=baz')) config.commit() class TestActionInfo(unittest.TestCase): -- cgit v1.2.3 From 1ceae4adac7c050e955778c71b3680edb9a3cb8c Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 7 Feb 2012 10:06:52 -0600 Subject: bug in url dispatch docs --- docs/narr/urldispatch.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/narr/urldispatch.rst b/docs/narr/urldispatch.rst index dfa4e629d..a7bf74786 100644 --- a/docs/narr/urldispatch.rst +++ b/docs/narr/urldispatch.rst @@ -340,7 +340,7 @@ The above pattern will match these URLs, generating the following matchdicts: .. code-block:: text - foo/1/2/ -> {'baz':u'1', 'bar':u'2', 'fizzle':()} + foo/1/2/ -> {'baz':u'1', 'bar':u'2', 'fizzle':u''} foo/abc/def/a/b/c -> {'baz':u'abc', 'bar':u'def', 'fizzle': u'a/b/c'} This occurs because the default regular expression for a marker is ``[^/]+`` -- cgit v1.2.3 From f4952ee0d30eeb67976684d6422f5db05d9f5264 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 7 Feb 2012 20:18:39 -0500 Subject: add asset descriptor interface --- docs/api/interfaces.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/api/interfaces.rst b/docs/api/interfaces.rst index 5b190b53b..11cd8cf7e 100644 --- a/docs/api/interfaces.rst +++ b/docs/api/interfaces.rst @@ -76,3 +76,6 @@ Other Interfaces .. autointerface:: IActionInfo :members: + + .. autointerface:: IAssetDescriptor + :members: -- cgit v1.2.3 From 18659cb5ecd5b281ed7fa0353677c7792069cbc4 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 7 Feb 2012 22:19:34 -0500 Subject: link to the interface --- pyramid/path.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/path.py b/pyramid/path.py index 8a8898174..d1c3b6d31 100644 --- a/pyramid/path.py +++ b/pyramid/path.py @@ -165,7 +165,7 @@ class AssetResolver(Resolver): """ Resolve the asset spec named as ``spec`` to an object that has the attributes and methods described in - `pyramid.interfaces.IAssetDescriptor`. + :class:`pyramid.interfaces.IAssetDescriptor`. If ``spec`` is an absolute filename (e.g. ``/path/to/myproject/templates/foo.pt``) or an absolute asset -- cgit v1.2.3 From e3f9d0e6ea3c98699de7b60bc3900b1a40fcba19 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 8 Feb 2012 00:05:31 -0500 Subject: prep for 1.3a7 --- CHANGES.txt | 4 ++-- docs/conf.py | 2 +- docs/whatsnew-1.3.rst | 4 ++++ setup.py | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 8bfdc6d66..d76f7087a 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,5 +1,5 @@ -Next release -============ +1.3a7 (2012-02-07) +================== Features -------- diff --git a/docs/conf.py b/docs/conf.py index 3496bd38c..0c56f56e7 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -80,7 +80,7 @@ copyright = '%s, Agendaless Consulting' % datetime.datetime.now().year # other places throughout the built documents. # # The short X.Y version. -version = '1.3a6' +version = '1.3a7' # The full version, including alpha/beta/rc tags. release = version diff --git a/docs/whatsnew-1.3.rst b/docs/whatsnew-1.3.rst index 2793471a3..231421262 100644 --- a/docs/whatsnew-1.3.rst +++ b/docs/whatsnew-1.3.rst @@ -317,6 +317,10 @@ Backwards Incompatibilities (indeterminate value based on Python 3 vs. Python 2). This has to be done to normalize matching on Python 2 and Python 3. +- The ``match_param`` view predicate no longer accepts a dict. This will have + no negative affect because the implementation was broken for dict-based + arguments. + Documentation Enhancements -------------------------- diff --git a/setup.py b/setup.py index 64c0ad419..c719bd9fe 100644 --- a/setup.py +++ b/setup.py @@ -56,7 +56,7 @@ if not PY3: ]) setup(name='pyramid', - version='1.3a6', + version='1.3a7', description=('The Pyramid web application development framework, a ' 'Pylons project'), long_description=README + '\n\n' + CHANGES, -- cgit v1.2.3 From e4b8fa632c5d2b020e168f4efe3d7c00049a279f Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 9 Feb 2012 00:12:26 -0500 Subject: Features -------- - The ``scan`` method of a ``Configurator`` can be passed an ``ignore`` argument, which can be a string, a callable, or a list consisting of strings and/or callables. This feature allows submodules, subpackages, and global objects from being scanned. See http://readthedocs.org/docs/venusian/en/latest/#ignore-scan-argument for more information about how to use the ``ignore`` argument to ``scan``. Dependencies ------------ - Depend on ``venusian`` >= 1.0a3 to provide scan ``ignore`` support. --- CHANGES.txt | 18 ++++++++++++++++++ docs/whatsnew-1.3.rst | 7 +++++++ pyramid/config/__init__.py | 21 +++++++++++++++++++-- pyramid/tests/test_config/test_init.py | 22 ++++++++++++++++++++++ setup.py | 2 +- tox.ini | 3 +++ 6 files changed, 70 insertions(+), 3 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index d76f7087a..fcd54217f 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,21 @@ +Next release +============ + +Features +-------- + +- The ``scan`` method of a ``Configurator`` can be passed an ``ignore`` + argument, which can be a string, a callable, or a list consisting of + strings and/or callables. This feature allows submodules, subpackages, and + global objects from being scanned. See + http://readthedocs.org/docs/venusian/en/latest/#ignore-scan-argument for + more information about how to use the ``ignore`` argument to ``scan``. + +Dependencies +------------ + +- Depend on ``venusian`` >= 1.0a3 to provide scan ``ignore`` support. + 1.3a7 (2012-02-07) ================== diff --git a/docs/whatsnew-1.3.rst b/docs/whatsnew-1.3.rst index 231421262..b6cfde039 100644 --- a/docs/whatsnew-1.3.rst +++ b/docs/whatsnew-1.3.rst @@ -252,6 +252,13 @@ Minor Feature Additions http://www.python.org/dev/peps/pep-0333/#optional-platform-specific-file-handling) when one is provided by the web server. +- The :meth:`pyramid.config.Configurator.scan` method can be passed an + ``ignore`` argument, which can be a string, a callable, or a list + consisting of strings and/or callables. This feature allows submodules, + subpackages, and global objects from being scanned. See + http://readthedocs.org/docs/venusian/en/latest/#ignore-scan-argument for + more information about how to use the ``ignore`` argument to ``scan``. + Backwards Incompatibilities --------------------------- diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index bd3a80ad3..1656b5410 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -851,7 +851,8 @@ class Configurator( return self.manager.pop() # this is *not* an action method (uses caller_package) - def scan(self, package=None, categories=None, onerror=None, **kw): + def scan(self, package=None, categories=None, onerror=None, ignore=None, + **kw): """Scan a Python package and any of its subpackages for objects marked with :term:`configuration decoration` such as :class:`pyramid.view.view_config`. Any decorated object found will @@ -879,6 +880,20 @@ class Configurator( :term:`Venusian` documentation for more information about ``onerror`` callbacks. + The ``ignore`` argument, if provided, should be a Venusian ``ignore`` + value. Providing an ``ignore`` argument allows the scan to ignore + particular modules, packages, or global objects during a scan. + ``ignore`` can be a string or a callable, or a list containing + strings or callables. The simplest usage of ``ignore`` is to provide + a module or package by providing a full path to its dotted name. For + example: ``config.scan(ignore='my.module.subpackage')`` would ignore + the ``my.module.subpackage`` package during a scan, which would + prevent the subpackage and any of its submodules from being imported + and scanned. See the :term:`Venusian` documentation for more + information about the ``ignore`` argument. + + .. note:: the ``ignore`` argument is new in Pyramid 1.3. + To perform a ``scan``, Pyramid creates a Venusian ``Scanner`` object. The ``kw`` argument represents a set of keyword arguments to pass to the Venusian ``Scanner`` object's constructor. See the @@ -900,7 +915,9 @@ class Configurator( ctorkw.update(kw) scanner = self.venusian.Scanner(**ctorkw) - scanner.scan(package, categories=categories, onerror=onerror) + + scanner.scan(package, categories=categories, onerror=onerror, + ignore=ignore) def make_wsgi_app(self): """ Commits any pending configuration statements, sends a diff --git a/pyramid/tests/test_config/test_init.py b/pyramid/tests/test_config/test_init.py index a866bed55..d237b3fe8 100644 --- a/pyramid/tests/test_config/test_init.py +++ b/pyramid/tests/test_config/test_init.py @@ -929,6 +929,28 @@ pyramid.tests.test_config.dummy_include2""", result = render_view_to_response(ctx, req, 'pod_notinit') self.assertEqual(result, None) + def test_scan_integration_with_ignore(self): + from zope.interface import alsoProvides + from pyramid.interfaces import IRequest + from pyramid.view import render_view_to_response + import pyramid.tests.test_config.pkgs.scannable as package + config = self._makeOne(autocommit=True) + config.scan(package, + ignore='pyramid.tests.test_config.pkgs.scannable.another') + + ctx = DummyContext() + req = DummyRequest() + alsoProvides(req, IRequest) + req.registry = config.registry + + req.method = 'GET' + result = render_view_to_response(ctx, req, '') + self.assertEqual(result, 'grokked') + + # ignored + v = render_view_to_response(ctx, req, 'another_stacked_class2') + self.assertEqual(v, None) + def test_scan_integration_dottedname_package(self): from zope.interface import alsoProvides from pyramid.interfaces import IRequest diff --git a/setup.py b/setup.py index c719bd9fe..a6cfa1480 100644 --- a/setup.py +++ b/setup.py @@ -37,7 +37,7 @@ install_requires=[ 'repoze.lru >= 0.4', # py3 compat 'zope.interface >= 3.8.0', # has zope.interface.registry 'zope.deprecation >= 3.5.0', # py3 compat - 'venusian >= 1.0a1', # ``onerror`` + 'venusian >= 1.0a3', # ``ignore`` 'translationstring >= 0.4', # py3 compat 'PasteDeploy >= 1.5.0', # py3 compat ] diff --git a/tox.ini b/tox.ini index 79728bc18..1e7223886 100644 --- a/tox.ini +++ b/tox.ini @@ -11,6 +11,7 @@ deps = repoze.sphinx.autointerface WebTest virtualenv + venusian>=1.0a3 [testenv:py32] commands = @@ -18,6 +19,7 @@ commands = deps = WebTest virtualenv + venusian>=1.0a3 [testenv:cover] basepython = @@ -30,6 +32,7 @@ deps = WebTest repoze.sphinx.autointerface virtualenv + venusian>=1.0a3 nose coverage==3.4 nosexcover -- cgit v1.2.3 From b791a714758835e58604a431030cf4be976554fe Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Mon, 13 Feb 2012 18:22:03 -0500 Subject: [#415] Making the response is None exception more informative. --- pyramid/config/views.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 0359c46f7..51d0c0b36 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -343,9 +343,10 @@ class ViewDeriver(object): result = view(context, request) response = registry.queryAdapterOrSelf(result, IResponse) if response is None: + view_name = '.'.join((view.__module__,view.__name__)) raise ValueError( - 'Could not convert view return value "%s" into a ' - 'response object' % (result,)) + 'Could not convert %s return value "%s" into a ' + 'response object' % (view_name,result,)) return response return viewresult_to_response -- cgit v1.2.3 From 4b200157ee96a55140ff4bf2e0ca504bf268d579 Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Tue, 14 Feb 2012 01:31:16 -0500 Subject: [#415] Using repr of view based on @mcdonc comments --- pyramid/config/views.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 51d0c0b36..831b8b20d 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -343,10 +343,9 @@ class ViewDeriver(object): result = view(context, request) response = registry.queryAdapterOrSelf(result, IResponse) if response is None: - view_name = '.'.join((view.__module__,view.__name__)) raise ValueError( - 'Could not convert %s return value "%s" into a ' - 'response object' % (view_name,result,)) + 'Could not convert %r return value "%s" into a ' + 'response object' % (view,result,)) return response return viewresult_to_response -- cgit v1.2.3 From af24f7d5f69a74f9887ca6df622ef67c69075988 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 14 Feb 2012 04:13:41 -0500 Subject: - 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. --- CHANGES.txt | 6 ++ pyramid/config/util.py | 1 - pyramid/config/views.py | 32 +++++++- pyramid/tests/test_config/test_views.py | 125 ++++++++++++++++++++++++++++++++ 4 files changed, 159 insertions(+), 5 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/pyramid/config/util.py b/pyramid/config/util.py index 6b7aa2fa1..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, ) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 86b139e3e..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 %r return value "%s" into a ' - 'response object' % (view,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 ' + ' 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 -- cgit v1.2.3