Chris McDonough
2012-02-14 9ed1e0ba957c36f6ae29c25ffeaa6c2c02f716a9
Merge branch 'wwitzel3-ww/415'
15 files modified
316 ■■■■■ changed files
CHANGES.txt 38 ●●●●● patch | view | raw | blame | history
docs/api/interfaces.rst 3 ●●●●● patch | view | raw | blame | history
docs/conf.py 2 ●●● patch | view | raw | blame | history
docs/narr/project.rst 2 ●●● patch | view | raw | blame | history
docs/narr/urldispatch.rst 4 ●●●● patch | view | raw | blame | history
docs/whatsnew-1.3.rst 15 ●●●●● patch | view | raw | blame | history
pyramid/config/__init__.py 25 ●●●●● patch | view | raw | blame | history
pyramid/config/util.py 15 ●●●● patch | view | raw | blame | history
pyramid/config/views.py 40 ●●●● patch | view | raw | blame | history
pyramid/path.py 2 ●●● patch | view | raw | blame | history
pyramid/tests/test_config/test_init.py 22 ●●●●● patch | view | raw | blame | history
pyramid/tests/test_config/test_util.py 16 ●●●● patch | view | raw | blame | history
pyramid/tests/test_config/test_views.py 125 ●●●●● patch | view | raw | blame | history
setup.py 4 ●●●● patch | view | raw | blame | history
tox.ini 3 ●●●●● patch | view | raw | blame | history
CHANGES.txt
@@ -4,8 +4,39 @@
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 @@
- 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
-------------
docs/api/interfaces.rst
@@ -76,3 +76,6 @@
  .. autointerface:: IActionInfo
     :members:
  .. autointerface:: IAssetDescriptor
     :members:
docs/conf.py
@@ -80,7 +80,7 @@
# 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
docs/narr/project.rst
@@ -98,7 +98,7 @@
.. 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``:
docs/narr/urldispatch.rst
@@ -340,8 +340,8 @@
.. 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
docs/whatsnew-1.3.rst
@@ -15,7 +15,9 @@
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 @@
  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 @@
  (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
--------------------------
pyramid/config/__init__.py
@@ -533,6 +533,10 @@
        ``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 @@
        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 @@
        :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 @@
        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
pyramid/config/util.py
@@ -6,7 +6,6 @@
from pyramid.interfaces import IActionInfo
from pyramid.compat import (
    string_types,
    bytes_,
    is_nonstr_iter,
    )
@@ -221,19 +220,21 @@
        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):
pyramid/config/views.py
@@ -55,6 +55,7 @@
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,
@@ -66,6 +67,12 @@
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):
@@ -99,7 +106,7 @@
    # "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 @@
            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 @@
            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 @@
            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 @@
          .. 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
pyramid/path.py
@@ -165,7 +165,7 @@
        """
        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
pyramid/tests/test_config/test_init.py
@@ -929,6 +929,28 @@
        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
pyramid/tests/test_config/test_util.py
@@ -281,7 +281,7 @@
        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 @@
        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 @@
        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
pyramid/tests/test_config/test_views.py
@@ -2282,6 +2282,113 @@
        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 @@
                 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
setup.py
@@ -37,7 +37,7 @@
    '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 @@
        ])
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,
tox.ini
@@ -11,6 +11,7 @@
    repoze.sphinx.autointerface
    WebTest
    virtualenv
    venusian>=1.0a3
[testenv:py32]
commands = 
@@ -18,6 +19,7 @@
deps =
    WebTest
    virtualenv
    venusian>=1.0a3
[testenv:cover]
basepython =
@@ -30,6 +32,7 @@
    WebTest
    repoze.sphinx.autointerface
    virtualenv
    venusian>=1.0a3
    nose
    coverage==3.4
    nosexcover