diff options
| author | Chris McDonough <chrism@plope.com> | 2012-02-14 04:13:58 -0500 |
|---|---|---|
| committer | Chris McDonough <chrism@plope.com> | 2012-02-14 04:13:58 -0500 |
| commit | 9ed1e0ba957c36f6ae29c25ffeaa6c2c02f716a9 (patch) | |
| tree | d5df37904835cfaac437acaf8d886fdb7c2072ce | |
| parent | c08f7656c1427cf5ebacf18356a5f55bd20ecfbd (diff) | |
| parent | af24f7d5f69a74f9887ca6df622ef67c69075988 (diff) | |
| download | pyramid-9ed1e0ba957c36f6ae29c25ffeaa6c2c02f716a9.tar.gz pyramid-9ed1e0ba957c36f6ae29c25ffeaa6c2c02f716a9.tar.bz2 pyramid-9ed1e0ba957c36f6ae29c25ffeaa6c2c02f716a9.zip | |
Merge branch 'wwitzel3-ww/415'
| -rw-r--r-- | CHANGES.txt | 38 | ||||
| -rw-r--r-- | docs/api/interfaces.rst | 3 | ||||
| -rw-r--r-- | docs/conf.py | 2 | ||||
| -rw-r--r-- | docs/narr/project.rst | 2 | ||||
| -rw-r--r-- | docs/narr/urldispatch.rst | 4 | ||||
| -rw-r--r-- | docs/whatsnew-1.3.rst | 15 | ||||
| -rw-r--r-- | pyramid/config/__init__.py | 25 | ||||
| -rw-r--r-- | pyramid/config/util.py | 15 | ||||
| -rw-r--r-- | pyramid/config/views.py | 40 | ||||
| -rw-r--r-- | pyramid/path.py | 2 | ||||
| -rw-r--r-- | pyramid/tests/test_config/test_init.py | 22 | ||||
| -rw-r--r-- | pyramid/tests/test_config/test_util.py | 16 | ||||
| -rw-r--r-- | pyramid/tests/test_config/test_views.py | 125 | ||||
| -rw-r--r-- | setup.py | 4 | ||||
| -rw-r--r-- | tox.ini | 3 |
15 files changed, 288 insertions, 28 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index 9edcbdbe8..411681d81 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -4,9 +4,40 @@ 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``. + +- 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 +------------ + +- Depend on ``venusian`` >= 1.0a3 to provide scan ``ignore`` support. + +1.3a7 (2012-02-07) +================== + +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). + +- 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 --------- @@ -18,6 +49,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/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: 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/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``: diff --git a/docs/narr/urldispatch.rst b/docs/narr/urldispatch.rst index 6d9dfdd92..a7bf74786 100644 --- a/docs/narr/urldispatch.rst +++ b/docs/narr/urldispatch.rst @@ -340,8 +340,8 @@ 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/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 ``[^/]+`` which will match everything up to the first ``/``, while ``{fizzle:.*}`` will diff --git a/docs/whatsnew-1.3.rst b/docs/whatsnew-1.3.rst index ed7024f62..b6cfde039 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:: @@ -250,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 --------------------------- @@ -315,6 +324,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/pyramid/config/__init__.py b/pyramid/config/__init__.py index 57ab7e13a..1656b5410 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 = {} @@ -847,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 @@ -875,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 @@ -896,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/config/util.py b/pyramid/config/util.py index 5f0dd98ac..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, ) @@ -221,19 +220,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..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): @@ -843,18 +867,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/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 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/pyramid/tests/test_config/test_util.py b/pyramid/tests/test_config/test_util.py index ebf308929..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__, '<unknown custom predicate>') @@ -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)) @@ -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/{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): def _getTargetClass(self): from pyramid.config.util import ActionInfo 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 @@ -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 ] @@ -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, @@ -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 |
