From cf46a12292cf15303d68d27c6ba4155ecc2b4586 Mon Sep 17 00:00:00 2001 From: Ian Wilson Date: Sun, 3 Jun 2012 13:01:15 -0700 Subject: Expose signed_serialize and signed_deserialize in UnencryptedCookieSessionFactoryConfig. --- pyramid/session.py | 106 +++++++++++++++++++++++------------------- pyramid/tests/test_session.py | 42 +++++++++++++++++ 2 files changed, 100 insertions(+), 48 deletions(-) diff --git a/pyramid/session.py b/pyramid/session.py index 76b2b30b1..40e21ddbc 100644 --- a/pyramid/session.py +++ b/pyramid/session.py @@ -33,6 +33,53 @@ def manage_accessed(wrapped): accessed.__doc__ = wrapped.__doc__ return accessed +def signed_serialize(data, secret): + """ Serialize any pickleable structure (``data``) and sign it + using the ``secret`` (must be a string). Return the + serialization, which includes the signature as its first 40 bytes. + The ``signed_deserialize`` method will deserialize such a value. + + This function is useful for creating signed cookies. For example: + + .. code-block:: python + + cookieval = signed_serialize({'a':1}, 'secret') + response.set_cookie('signed_cookie', cookieval) + """ + pickled = pickle.dumps(data, pickle.HIGHEST_PROTOCOL) + sig = hmac.new(bytes_(secret), pickled, sha1).hexdigest() + return sig + native_(base64.b64encode(pickled)) + +def signed_deserialize(serialized, secret, hmac=hmac): + """ Deserialize the value returned from ``signed_serialize``. If + the value cannot be deserialized for any reason, a + :exc:`ValueError` exception will be raised. + + This function is useful for deserializing a signed cookie value + created by ``signed_serialize``. For example: + + .. code-block:: python + + cookieval = request.cookies['signed_cookie'] + data = signed_deserialize(cookieval, 'secret') + """ + # hmac parameterized only for unit tests + try: + input_sig, pickled = (serialized[:40], + base64.b64decode(bytes_(serialized[40:]))) + except (binascii.Error, TypeError) as e: + # Badly formed data can make base64 die + raise ValueError('Badly formed base64 data: %s' % e) + + sig = hmac.new(bytes_(secret), pickled, sha1).hexdigest() + + # Avoid timing attacks (see + # http://seb.dbzteam.org/crypto/python-oauth-timing-hmac.pdf) + if strings_differ(sig, input_sig): + raise ValueError('Invalid signature') + + return pickle.loads(pickled) + def UnencryptedCookieSessionFactoryConfig( secret, timeout=1200, @@ -43,6 +90,8 @@ def UnencryptedCookieSessionFactoryConfig( cookie_secure=False, cookie_httponly=False, cookie_on_exception=True, + signed_serialize=signed_serialize, + signed_deserialize=signed_deserialize, ): """ Configure a :term:`session factory` which will provide unencrypted @@ -89,6 +138,15 @@ def UnencryptedCookieSessionFactoryConfig( If ``True``, set a session cookie even if an exception occurs while rendering a view. Default: ``True``. + ``signed_serialize`` + A callable which takes more or less arbitrary python data structure and + a secret and returns a signed serialization in bytes. + Default: ``signed_serialize`` (using pickle). + + ``signed_deserialize`` + A callable which takes a signed and serialized data structure in bytes + and a secret and returns the original data structure if the signature + is valid. Default: ``signed_deserialize`` (using pickle). """ @implementer(ISession) @@ -225,51 +283,3 @@ def UnencryptedCookieSessionFactoryConfig( return True return UnencryptedCookieSessionFactory - -def signed_serialize(data, secret): - """ Serialize any pickleable structure (``data``) and sign it - using the ``secret`` (must be a string). Return the - serialization, which includes the signature as its first 40 bytes. - The ``signed_deserialize`` method will deserialize such a value. - - This function is useful for creating signed cookies. For example: - - .. code-block:: python - - cookieval = signed_serialize({'a':1}, 'secret') - response.set_cookie('signed_cookie', cookieval) - """ - pickled = pickle.dumps(data, pickle.HIGHEST_PROTOCOL) - sig = hmac.new(bytes_(secret), pickled, sha1).hexdigest() - return sig + native_(base64.b64encode(pickled)) - -def signed_deserialize(serialized, secret, hmac=hmac): - """ Deserialize the value returned from ``signed_serialize``. If - the value cannot be deserialized for any reason, a - :exc:`ValueError` exception will be raised. - - This function is useful for deserializing a signed cookie value - created by ``signed_serialize``. For example: - - .. code-block:: python - - cookieval = request.cookies['signed_cookie'] - data = signed_deserialize(cookieval, 'secret') - """ - # hmac parameterized only for unit tests - try: - input_sig, pickled = (serialized[:40], - base64.b64decode(bytes_(serialized[40:]))) - except (binascii.Error, TypeError) as e: - # Badly formed data can make base64 die - raise ValueError('Badly formed base64 data: %s' % e) - - sig = hmac.new(bytes_(secret), pickled, sha1).hexdigest() - - # Avoid timing attacks (see - # http://seb.dbzteam.org/crypto/python-oauth-timing-hmac.pdf) - if strings_differ(sig, input_sig): - raise ValueError('Invalid signature') - - return pickle.loads(pickled) - diff --git a/pyramid/tests/test_session.py b/pyramid/tests/test_session.py index 6d75c7950..5143b7a95 100644 --- a/pyramid/tests/test_session.py +++ b/pyramid/tests/test_session.py @@ -205,6 +205,48 @@ class TestUnencryptedCookieSession(unittest.TestCase): self.assertTrue(token) self.assertTrue('_csrft_' in session) + def test_serialize_option(self): + from pyramid.response import Response + secret = 'secret' + request = testing.DummyRequest() + session = self._makeOne(request, + signed_serialize=dummy_signed_serialize) + session['key'] = 'value' + response = Response() + self.assertEqual(session._set_cookie(response), True) + cookie = response.headerlist[-1][1] + expected_cookieval = dummy_signed_serialize( + (session.accessed, session.created, {'key': 'value'}), secret) + response = Response() + response.set_cookie('session', expected_cookieval) + expected_cookie = response.headerlist[-1][1] + self.assertEqual(cookie, expected_cookie) + + def test_deserialize_option(self): + import time + secret = 'secret' + request = testing.DummyRequest() + accessed = time.time() + state = {'key': 'value'} + cookieval = dummy_signed_serialize((accessed, accessed, state), secret) + request.cookies['session'] = cookieval + session = self._makeOne(request, + signed_deserialize=dummy_signed_deserialize) + self.assertEqual(dict(session), state) + +def dummy_signed_serialize(data, secret): + import base64 + from pyramid.compat import pickle, bytes_ + pickled = pickle.dumps(data) + return base64.b64encode(bytes_(secret)) + base64.b64encode(pickled) + +def dummy_signed_deserialize(serialized, secret): + import base64 + from pyramid.compat import pickle, bytes_ + serialized_data = base64.b64decode( + serialized[len(base64.b64encode(bytes_(secret))):]) + return pickle.loads(serialized_data) + class Test_manage_accessed(unittest.TestCase): def _makeOne(self, wrapped): from pyramid.session import manage_accessed -- cgit v1.2.3 From 34f00aede8634810164baaac0fd9214b683ff181 Mon Sep 17 00:00:00 2001 From: "Lorenzo M. Catucci" Date: Fri, 8 Jun 2012 12:31:50 +0200 Subject: RFC 2616 sec. 3.7 case insensitive match test Since "... The type, subtype, and parameter attribute names are case- insensitive..." the type and subtype in the accept parameter in add_view should be treated in a case insensitive way. --- pyramid/tests/test_config/test_views.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index 9b46f83c9..4e1be2190 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -645,6 +645,26 @@ class TestViewsConfigurationMixin(unittest.TestCase): request.accept = DummyAccept('text/html', 'text/html') self.assertEqual(wrapper(None, request), 'OK2') + def test_add_view_mixed_case_replaces_existing_view(self): + from pyramid.renderers import null_renderer + def view(context, request): return 'OK' + def view2(context, request): return 'OK2' + def view3(context, request): return 'OK3' + def get_val(obj, key): + return obj.get(key) + config = self._makeOne(autocommit=True) + config.add_view(view=view, renderer=null_renderer) + config.add_view(view=view2, accept='text/html', renderer=null_renderer) + config.add_view(view=view3, accept='text/HTML', renderer=null_renderer) + wrapper = self._getViewCallable(config) + self.assertTrue(IMultiView.providedBy(wrapper)) + self.assertEqual(len(wrapper.media_views.items()),1) + self.assertFalse(wrapper.media_views.has_key('text/HTML')) + self.assertEqual(wrapper(None, None), 'OK') + request = DummyRequest() + request.accept = DummyAccept('text/html', 'text/html') + self.assertEqual(wrapper(None, request), 'OK3') + def test_add_views_with_accept_multiview_replaces_existing(self): from pyramid.renderers import null_renderer def view(context, request): return 'OK' -- cgit v1.2.3 From ed9663c861b5da5a684a3ebd26700e52e49ce39f Mon Sep 17 00:00:00 2001 From: "Lorenzo M. Catucci" Date: Fri, 8 Jun 2012 13:32:55 +0200 Subject: Lowercase the accept parameter in add_view Fix the RFC 2616 sec. 3.7 compliance by storing a canonical cased version of the parameter. --- pyramid/config/views.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 9e9b5321b..d54976988 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1001,6 +1001,9 @@ class ViewsConfiguratorMixin(object): # GET implies HEAD too request_method = as_sorted_tuple(request_method + ('HEAD',)) + if accept is not None: + accept = accept.lower() + order, predicates, phash = make_predicates(xhr=xhr, request_method=request_method, path_info=path_info, request_param=request_param, header=header, accept=accept, -- cgit v1.2.3 From ce5b5e4b842fe0e88d9ba5055b419b756723e8ec Mon Sep 17 00:00:00 2001 From: "Lorenzo M. Catucci" Date: Wed, 13 Jun 2012 16:25:14 +0200 Subject: Accept the Contributor Agreement. as suggested by "Contributing Source Code and Documentation" document. --- CONTRIBUTORS.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 98f73d5f9..dacc48765 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -172,3 +172,5 @@ Contributors - Wayne Witzel III, 2012/03/27 - Marin Rukavina, 2012/05/03 + +- Lorenzo M. Catucci, 2012/06/08 -- cgit v1.2.3 From 2cc0710ecf9b47e5a6f41caf6b1c56b29bab2db0 Mon Sep 17 00:00:00 2001 From: Ian Joseph Wilson Date: Sun, 17 Jun 2012 10:40:51 -0700 Subject: Sign contributors agreement. --- CONTRIBUTORS.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index c3995aaba..5ccac50d3 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -176,3 +176,5 @@ Contributors - Marc Abramowitz, 2012/06/13 - Jeff Cook, 2012/06/16 + +- Ian Wilson, 2012/06/17 -- cgit v1.2.3 From 0d8ff5b39aefaf2a9b3fdd79894d855e59ed9a2e Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Fri, 3 Aug 2012 12:07:10 -0700 Subject: Tests for fixing issue #621 --- pyramid/tests/test_config/__init__.py | 8 +++++++ pyramid/tests/test_config/test_init.py | 26 ++++++++++++++++++++++ .../test_init.py:TestConfigurator_add_directive | 0 3 files changed, 34 insertions(+) create mode 100644 pyramid/tests/test_config/test_init.py:TestConfigurator_add_directive diff --git a/pyramid/tests/test_config/__init__.py b/pyramid/tests/test_config/__init__.py index 5b40a8c09..81d9f4965 100644 --- a/pyramid/tests/test_config/__init__.py +++ b/pyramid/tests/test_config/__init__.py @@ -43,3 +43,11 @@ def dummy_extend(config, discrim): def dummy_extend2(config, discrim): config.action(discrim, None, config.registry) +from functools import partial +dummy_partial = partial(dummy_extend, discrim='partial') + +class DummyCallable(object): + def __call__(self, config, discrim): + config.action(discrim, None, config.package) +dummy_callable = DummyCallable() + diff --git a/pyramid/tests/test_config/test_init.py b/pyramid/tests/test_config/test_init.py index 37c3de275..7a483355c 100644 --- a/pyramid/tests/test_config/test_init.py +++ b/pyramid/tests/test_config/test_init.py @@ -1414,6 +1414,32 @@ class TestConfigurator_add_directive(unittest.TestCase): self.assertEqual(action['callable'], None) self.assertEqual(action['args'], test_config) + def test_add_directive_with_partial(self): + from pyramid.tests import test_config + config = self.config + config.add_directive( + 'dummy_partial', 'pyramid.tests.test_config.dummy_partial') + self.assertTrue(hasattr(config, 'dummy_partial')) + config.dummy_partial() + after = config.action_state + action = after.actions[-1] + self.assertEqual(action['discriminator'], 'partial') + self.assertEqual(action['callable'], None) + self.assertEqual(action['args'], test_config) + + def test_add_directive_with_custom_callable(self): + from pyramid.tests import test_config + config = self.config + config.add_directive( + 'dummy_callable', 'pyramid.tests.test_config.dummy_callable') + self.assertTrue(hasattr(config, 'dummy_callable')) + config.dummy_callable('discrim') + after = config.action_state + action = after.actions[-1] + self.assertEqual(action['discriminator'], 'discrim') + self.assertEqual(action['callable'], None) + self.assertEqual(action['args'], test_config) + def test_extend_with_python_callable(self): from pyramid.tests import test_config config = self.config diff --git a/pyramid/tests/test_config/test_init.py:TestConfigurator_add_directive b/pyramid/tests/test_config/test_init.py:TestConfigurator_add_directive new file mode 100644 index 000000000..e69de29bb -- cgit v1.2.3 From 0f0629faebdd1683bce1b69a5d7b099afc794a75 Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Fri, 3 Aug 2012 12:07:24 -0700 Subject: Fix for issue #621 --- pyramid/config/util.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pyramid/config/util.py b/pyramid/config/util.py index 4e4c93be3..ade02e87f 100644 --- a/pyramid/config/util.py +++ b/pyramid/config/util.py @@ -1,6 +1,8 @@ import re import traceback +from functools import update_wrapper + from zope.interface import implementer from pyramid.interfaces import IActionInfo @@ -59,9 +61,10 @@ def action_method(wrapped): finally: self._ainfo.pop() return result - wrapper.__name__ = wrapped.__name__ - wrapper.__doc__ = wrapped.__doc__ - wrapper.__docobj__ = wrapped # for sphinx + + if hasattr(wrapped, '__name__'): + update_wrapper(wrapper, wrapped) + wrapper.__docobj__ = wrapped return wrapper def make_predicates(xhr=None, request_method=None, path_info=None, -- cgit v1.2.3 From 4aeb44f1ea912c3c3611bc677f654602488fedb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Fidosz?= Date: Fri, 3 Aug 2012 22:56:49 +0200 Subject: fixed indentation in narr/helloworld.py --- docs/narr/helloworld.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/narr/helloworld.py b/docs/narr/helloworld.py index 7c26c8cdc..c01329af9 100644 --- a/docs/narr/helloworld.py +++ b/docs/narr/helloworld.py @@ -2,14 +2,15 @@ from wsgiref.simple_server import make_server from pyramid.config import Configurator from pyramid.response import Response + def hello_world(request): - return Response('Hello %(name)s!' % request.matchdict) + return Response('Hello %(name)s!' % request.matchdict) if __name__ == '__main__': - config = Configurator() - config.add_route('hello', '/hello/{name}') - config.add_view(hello_world, route_name='hello') - app = config.make_wsgi_app() - server = make_server('0.0.0.0', 8080, app) - server.serve_forever() + config = Configurator() + config.add_route('hello', '/hello/{name}') + config.add_view(hello_world, route_name='hello') + app = config.make_wsgi_app() + server = make_server('0.0.0.0', 8080, app) + server.serve_forever() -- cgit v1.2.3 From a0243a5264371a3e69fd188cee17c6fbfd8341a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Fidosz?= Date: Sun, 5 Aug 2012 10:33:07 +0200 Subject: fixed line numbers in firstapp --- docs/narr/firstapp.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/narr/firstapp.rst b/docs/narr/firstapp.rst index a86826d86..dbdc48549 100644 --- a/docs/narr/firstapp.rst +++ b/docs/narr/firstapp.rst @@ -127,7 +127,7 @@ defined imports and function definitions, placed within the confines of an .. literalinclude:: helloworld.py :linenos: - :lines: 8-13 + :lines: 9-15 Let's break this down piece-by-piece. @@ -136,7 +136,7 @@ Configurator Construction .. literalinclude:: helloworld.py :linenos: - :lines: 8-9 + :lines: 9-10 The ``if __name__ == '__main__':`` line in the code sample above represents a Python idiom: the code inside this if clause is not invoked unless the script @@ -169,7 +169,7 @@ Adding Configuration .. ignore-next-block .. literalinclude:: helloworld.py :linenos: - :lines: 10-11 + :lines: 11-12 First line above calls the :meth:`pyramid.config.Configurator.add_route` method, which registers a :term:`route` to match any URL path that begins @@ -189,7 +189,7 @@ WSGI Application Creation .. ignore-next-block .. literalinclude:: helloworld.py :linenos: - :lines: 12 + :lines: 13 After configuring views and ending configuration, the script creates a WSGI *application* via the :meth:`pyramid.config.Configurator.make_wsgi_app` @@ -218,7 +218,7 @@ WSGI Application Serving .. ignore-next-block .. literalinclude:: helloworld.py :linenos: - :lines: 13 + :lines: 14 Finally, we actually serve the application to requestors by starting up a WSGI server. We happen to use the :mod:`wsgiref` ``make_server`` server -- cgit v1.2.3 From 84362da3c531ea8bbdebff8bf82df5bfee77e6dd Mon Sep 17 00:00:00 2001 From: Karl Johan Kleist Date: Sun, 19 Aug 2012 13:22:14 +0300 Subject: add option "--stop-daemon" only if appropriate --- pyramid/scripts/pserve.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pyramid/scripts/pserve.py b/pyramid/scripts/pserve.py index ea2a4ae09..151aa34b6 100644 --- a/pyramid/scripts/pserve.py +++ b/pyramid/scripts/pserve.py @@ -136,12 +136,13 @@ class PServeCommand(object): metavar="GROUP", help="Set the group (usually only possible when run as root)") - parser.add_option( - '--stop-daemon', - dest='stop_daemon', - action='store_true', - help=('Stop a daemonized server (given a PID file, or default ' - 'pyramid.pid file)')) + if hasattr(os, 'fork'): + parser.add_option( + '--stop-daemon', + dest='stop_daemon', + action='store_true', + help=('Stop a daemonized server (given a PID file, or default ' + 'pyramid.pid file)')) _scheme_re = re.compile(r'^[a-z][a-z]+:', re.I) -- cgit v1.2.3 From dbc792498c1c8a3390c1babc1bf742def40e2b46 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 19 Aug 2012 11:26:55 -0400 Subject: Garden. --- CHANGES.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index a9dd18985..abd30c950 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -17,12 +17,15 @@ Bug Fixes during iteration`` exception. It no longer does. See https://github.com/Pylons/pyramid/issues/635 for more information. -- In Mako Templates lookup, check if the uri is already adjusted and bring +- In Mako Templates lookup, cyheck if the uri is already adjusted and bring it back to an asset spec. Normally occurs with inherited templates or included components. https://github.com/Pylons/pyramid/issues/606 https://github.com/Pylons/pyramid/issues/607 +- ``pserve``: don't show --stop-daemon option on Windows. + https://github.com/Pylons/pyramid/pull/661 + Features -------- -- cgit v1.2.3 From b9b4657e79e7e1ed1f0c79bf82c3fe19a0ab5687 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 19 Aug 2012 12:15:27 -0400 Subject: - Undo effects of merging pull #661 because tests wont pass on windows https://github.com/Pylons/pyramid/pull/661 --- CHANGES.txt | 3 --- pyramid/scripts/pserve.py | 13 ++++++------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index abd30c950..0291d924a 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -23,9 +23,6 @@ Bug Fixes https://github.com/Pylons/pyramid/issues/606 https://github.com/Pylons/pyramid/issues/607 -- ``pserve``: don't show --stop-daemon option on Windows. - https://github.com/Pylons/pyramid/pull/661 - Features -------- diff --git a/pyramid/scripts/pserve.py b/pyramid/scripts/pserve.py index 151aa34b6..ea2a4ae09 100644 --- a/pyramid/scripts/pserve.py +++ b/pyramid/scripts/pserve.py @@ -136,13 +136,12 @@ class PServeCommand(object): metavar="GROUP", help="Set the group (usually only possible when run as root)") - if hasattr(os, 'fork'): - parser.add_option( - '--stop-daemon', - dest='stop_daemon', - action='store_true', - help=('Stop a daemonized server (given a PID file, or default ' - 'pyramid.pid file)')) + parser.add_option( + '--stop-daemon', + dest='stop_daemon', + action='store_true', + help=('Stop a daemonized server (given a PID file, or default ' + 'pyramid.pid file)')) _scheme_re = re.compile(r'^[a-z][a-z]+:', re.I) -- cgit v1.2.3 From 909dfee23c43709f2aa07942fb633cc740a9a6b3 Mon Sep 17 00:00:00 2001 From: Blaise Laflamme Date: Wed, 22 Aug 2012 10:23:40 -0400 Subject: fixed bug with mixing up asset spec and absolute uri in mako template inheritance --- pyramid/mako_templating.py | 2 ++ pyramid/tests/test_mako_templating.py | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/pyramid/mako_templating.py b/pyramid/mako_templating.py index 489c1f11a..2b09e8d45 100644 --- a/pyramid/mako_templating.py +++ b/pyramid/mako_templating.py @@ -43,6 +43,8 @@ class PkgResourceTemplateLookup(TemplateLookup): if relativeto is not None: relativeto = relativeto.replace('$', ':') if not(':' in uri) and (':' in relativeto): + if uri.startswith('/'): + return uri pkg, relto = relativeto.split(':') _uri = posixpath.join(posixpath.dirname(relto), uri) return '{0}:{1}'.format(pkg, _uri) diff --git a/pyramid/tests/test_mako_templating.py b/pyramid/tests/test_mako_templating.py index aced6c586..97b2c679b 100644 --- a/pyramid/tests/test_mako_templating.py +++ b/pyramid/tests/test_mako_templating.py @@ -499,6 +499,16 @@ class TestPkgResourceTemplateLookup(unittest.TestCase): result = inst.adjust_uri('b', '../a') self.assertEqual(result, '../b') + def test_adjust_uri_not_asset_spec_abs_with_relativeto_asset_spec(self): + inst = self._makeOne() + result = inst.adjust_uri('/c', 'a:b') + self.assertEqual(result, '/c') + + def test_adjust_uri_asset_spec_with_relativeto_not_asset_spec_abs(self): + inst = self._makeOne() + result = inst.adjust_uri('a:b', '/c') + self.assertEqual(result, 'a:b') + def test_get_template_not_asset_spec(self): fixturedir = self.get_fixturedir() inst = self._makeOne(directories=[fixturedir]) -- cgit v1.2.3 From 7853bc001283db8c5b87d026dcc130d1bece8dcc Mon Sep 17 00:00:00 2001 From: Blaise Laflamme Date: Wed, 22 Aug 2012 10:49:18 -0400 Subject: garden --- CHANGES.txt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index 0291d924a..30df788b3 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -17,12 +17,16 @@ Bug Fixes during iteration`` exception. It no longer does. See https://github.com/Pylons/pyramid/issues/635 for more information. -- In Mako Templates lookup, cyheck if the uri is already adjusted and bring +- In Mako Templates lookup, check if the uri is already adjusted and bring it back to an asset spec. Normally occurs with inherited templates or included components. https://github.com/Pylons/pyramid/issues/606 https://github.com/Pylons/pyramid/issues/607 +- In Mako Templates lookup, check for absolute uri (using mako directories) + when mixing up inheritance with asset specs. + https://github.com/Pylons/pyramid/issues/662 + Features -------- -- cgit v1.2.3 From 75a5885afc250db7f19a4c80d947b2bef5826356 Mon Sep 17 00:00:00 2001 From: Ronan Amicel Date: Thu, 23 Aug 2012 17:02:22 +0300 Subject: Fixed typo in docs/narr/views.rst --- docs/narr/views.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/narr/views.rst b/docs/narr/views.rst index f6ee9a8d5..9e41464a6 100644 --- a/docs/narr/views.rst +++ b/docs/narr/views.rst @@ -177,7 +177,7 @@ HTTP Exceptions ~~~~~~~~~~~~~~~ All classes documented in the :mod:`pyramid.httpexceptions` module documented -as inheriting from the :class:`pryamid.httpexceptions.HTTPException` are +as inheriting from the :class:`pyramid.httpexceptions.HTTPException` are :term:`http exception` objects. Instances of an HTTP exception object may either be *returned* or *raised* from within view code. In either case (return or raise) the instance will be used as as the view's response. -- cgit v1.2.3 From eb403ffe22501ae0546c02c7c24b54b5e3d1eb83 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 23 Aug 2012 09:57:35 -0500 Subject: exposed the serve_forever line to the helloworld narrative --- docs/narr/firstapp.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/narr/firstapp.rst b/docs/narr/firstapp.rst index dbdc48549..ccaa6e9e2 100644 --- a/docs/narr/firstapp.rst +++ b/docs/narr/firstapp.rst @@ -218,7 +218,7 @@ WSGI Application Serving .. ignore-next-block .. literalinclude:: helloworld.py :linenos: - :lines: 14 + :lines: 14-15 Finally, we actually serve the application to requestors by starting up a WSGI server. We happen to use the :mod:`wsgiref` ``make_server`` server -- cgit v1.2.3 From 45b6e192c44ebee124317a90a9a6dcc044407a2a Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 23 Aug 2012 10:31:38 -0500 Subject: added cookie session changes to CHANGES.txt --- CHANGES.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 30df788b3..b8747f06c 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -109,6 +109,11 @@ Features config = Configurator() config.add_permission('view') +- The ``UnencryptedCookieSessionFactoryConfig`` now accepts + ``signed_serialize`` and ``signed_deserialize`` hooks which may be used + to influence how the sessions are marshalled (by default this is done + with HMAC+pickle). + Deprecations ------------ -- cgit v1.2.3 From 9822a18731616e53bd77045ad8a7879d1a969c23 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 23 Aug 2012 10:52:31 -0500 Subject: fixed an incompatibility with accept header test in py3 --- pyramid/tests/test_config/test_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index 1a59b3c5c..72a0d8ebd 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -658,7 +658,7 @@ class TestViewsConfigurationMixin(unittest.TestCase): wrapper = self._getViewCallable(config) self.assertTrue(IMultiView.providedBy(wrapper)) self.assertEqual(len(wrapper.media_views.items()),1) - self.assertFalse(wrapper.media_views.has_key('text/HTML')) + self.assertFalse('text/HTML' in wrapper.media_views) self.assertEqual(wrapper(None, None), 'OK') request = DummyRequest() request.accept = DummyAccept('text/html', 'text/html') -- cgit v1.2.3 From 75a8ff4ad0ba8eed13592eb5e7a211da3035e209 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 23 Aug 2012 10:53:04 -0500 Subject: updated CHANGES with accept-header bug fix --- CHANGES.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index b8747f06c..e799c8f00 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -27,6 +27,11 @@ Bug Fixes when mixing up inheritance with asset specs. https://github.com/Pylons/pyramid/issues/662 +- HTTP Accept headers were not being normalized causing potentially + conflicting view registrations to go unnoticed. Two views that only + differ in the case ('text/html' vs. 'text/HTML') will now raise an error. + https://github.com/Pylons/pyramid/pull/620 + Features -------- -- cgit v1.2.3 From 20f2de54356741c1e682118b58ffcb9324599294 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 23 Aug 2012 13:15:15 -0400 Subject: sorting differences made this test fail every so often on py33 --- pyramid/tests/test_config/test_factories.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyramid/tests/test_config/test_factories.py b/pyramid/tests/test_config/test_factories.py index a95326772..38e80416f 100644 --- a/pyramid/tests/test_config/test_factories.py +++ b/pyramid/tests/test_config/test_factories.py @@ -125,9 +125,9 @@ class TestFactoriesMixin(unittest.TestCase): request = DummyRequest(config.registry) event = Event() config.registry.notify(event) - exts = event.request.extensions - self.assertTrue('foo' in exts[0]) - self.assertTrue('bar' in exts[1]) + exts = list(sorted(event.request.extensions)) + self.assertEqual('bar', exts[0]) + self.assertEqual('foo', exts[1]) def test_set_request_method_subscriber(self): from zope.interface import implementer -- cgit v1.2.3 From c9a0331f5f867a1ba3f13565f8c61cfa433bc8fc Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 24 Aug 2012 14:03:04 -0400 Subject: garden --- CHANGES.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index e799c8f00..d20257679 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -32,6 +32,11 @@ Bug Fixes differ in the case ('text/html' vs. 'text/HTML') will now raise an error. https://github.com/Pylons/pyramid/pull/620 +- Configurator.add_directive now accepts arbitrary callables like partials or + objects implementing ``__call__`` which dont have ``__name__`` and + ``__doc__`` attributes. See https://github.com/Pylons/pyramid/issues/621 + and https://github.com/Pylons/pyramid/pull/647. + Features -------- -- cgit v1.2.3 From 95f766bc7c380797c569da464f1f41d12b05bdbe Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 25 Aug 2012 00:10:46 -0400 Subject: Subscriber predicates: - Add ``add_subscriber_predicate`` method to Configurator. - Allow ``add_subscriber`` and ``subscriber`` venusian decorator to accept ``**predicates`` arguments. - Document subscriber predicate feature. - Share more code between view, route, and subscriber related method wrt predicates. --- CHANGES.txt | 13 +-- docs/glossary.rst | 3 +- docs/narr/hooks.rst | 122 ++++++++++++++++++++++++--- pyramid/config/__init__.py | 29 +++++++ pyramid/config/adapters.py | 94 +++++++++++++++++---- pyramid/config/routes.py | 41 +++------- pyramid/config/views.py | 39 +++------ pyramid/events.py | 17 ++-- pyramid/tests/test_config/test_adapters.py | 127 ++++++++++++++++++++++++++++- pyramid/tests/test_events.py | 21 ++++- 10 files changed, 405 insertions(+), 101 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index d20257679..369e9d74d 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -40,9 +40,11 @@ Bug Fixes Features -------- -- Third-party custom view and route predicates can now be added for use by - view authors via ``pyramid.config.Configurator.add_view_predicate`` and - ``pyramid.config.Configurator.add_route_predicate``. So, for example, +- Third-party custom view, route, and subscriber predicates can now be added + for use by view authors via + ``pyramid.config.Configurator.add_view_predicate``, + ``pyramid.config.Configurator.add_route_predicate`` and + ``pyramid.config.Configurator.add_subscriber_predicate``. So, for example, doing this:: config.add_view_predicate('abc', my.package.ABCPredicate) @@ -52,8 +54,9 @@ Features @view_config(abc=1) - See "Adding A Third Party View or Route Predicate" in the Hooks chapter for - more information. + Similar features exist for ``add_route``, and ``add_subscriber``. See + "Adding A Third Party View, Route, or Subscriber Predicate" in the Hooks + chapter for more information. Note that changes made to support the above feature now means that only actions registered using the same "order" can conflict with one another. diff --git a/docs/glossary.rst b/docs/glossary.rst index ba3203f89..34cf1b078 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -997,6 +997,7 @@ Glossary predicate factory A callable which is used by a third party during the registration of a - route or view predicates to extend the view and route configuration + route, view, or subscriber predicates to extend the configuration system. See :ref:`registering_thirdparty_predicates` for more information. + diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index 2c15cd690..96fa77a07 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -1235,17 +1235,23 @@ implict and explicit tween chains used by an application. See .. _registering_thirdparty_predicates: -Adding A Third Party View or Route Predicate --------------------------------------------- +Adding A Third Party View, Route, or Subscriber Predicate +--------------------------------------------------------- .. note:: - Third-party predicates are a feature new as of Pyramid 1.4. + Third-party view, route, and subscriber predicates are a feature new as of + Pyramid 1.4. -View and route predicates used during view configuration allow you to narrow -the set of circumstances under which a view or route will match. For -example, the ``request_method`` view predicate can be used to ensure a view -callable is only invoked when the request's method is ``POST``: +.. _view_and_route_predicates: + +View and Route Predicates +~~~~~~~~~~~~~~~~~~~~~~~~~ + +View and route predicates used during configuration allow you to narrow the +set of circumstances under which a view or route will match. For example, +the ``request_method`` view predicate can be used to ensure a view callable +is only invoked when the request's method is ``POST``: .. code-block:: python @@ -1286,9 +1292,9 @@ The first argument to :meth:`pyramid.config.Configurator.add_view_predicate`, the name, is a string representing the name that is expected to be passed to ``view_config`` (or its imperative analogue ``add_view``). -The second argument is a predicate factory. A predicate factory is most -often a class with a constructor (``__init__``), a ``text`` method, a -``phash`` method and a ``__call__`` method. For example: +The second argument is a view or route predicate factory. A view or route +predicate factory is most often a class with a constructor (``__init__``), a +``text`` method, a ``phash`` method and a ``__call__`` method. For example: .. code-block:: python :linenos: @@ -1330,3 +1336,99 @@ You can use the same predicate factory as both a view predicate and as a route predicate, but you'll need to call ``add_view_predicate`` and ``add_route_predicate`` separately with the same factory. +.. _subscriber_predicates: + +Subscriber Predicates +~~~~~~~~~~~~~~~~~~~~~ + +Subscriber predicates work almost exactly like view and route predicates. +They narrow the set of circumstances in which a subscriber will be called. +There are several minor differences between a subscriber predicate and a +view/route predicate: + +- There are no default subscriber predicates. You must register one to use + one. + +- The ``__call__`` method of a subscriber predicate accepts a single + ``event`` object instead of a ``context`` and a ``request``. + +- Not every subscriber predicate can be used with every event type. Some + subscriber predicates will assume a certain event type. + +Here's an example of a subscriber predicate that can be used in conjunction +with a subscriber that subscribes to the :class:`pyramid.events.NewReqest` +event type. + +.. code-block:: python + :linenos: + + class RequestPathStartsWith(object): + def __init__(self, val, config): + self.val = val + + def text(self): + return 'path_startswith = %s' % (self.val,) + + phash = text + + def __call__(self, event): + return event.request.path.startswith(self.val) + +Once you've created a subscriber predicate, it may registered via +:meth:`pyramid.config.Configurator.add_subscriber_predicate`. For example: + +.. code-block:: python + + config.add_subscriber_predicate( + 'request_path_startswith', RequestPathStartsWith) + +Once a subscriber predicate is registered, you can use it in a call to +:meth:`pyramid.config.Configurator.add_subscriber` or to +:class:`pyramid.events.subscriber`. Here's an example of using the +previously registered ``request_path_startswith`` predicate in a call to +:meth:`~pyramid.config.Configurator.add_subscriber`: + +.. code-block:: python + :linenos: + + # define a subscriber in your code + + def yosubscriber(event): + event.request.yo = 'YO!' + + # and at configuration time + + config.add_subscriber(yosubscriber, NewRequest, + request_path_startswith='/add_yo') + +Here's the same subscriber/predicate/event-type combination used via +:class:`~pyramid.events.subscriber`. + +.. code-block:: python + :linenos: + + from pyramid.events import subscriber + + @subscriber(NewRequest, request_path_startswith='/add_yo') + def yosubscriber(event): + event.request.yo = 'YO!' + +In either of the above configurations, the ``yosubscriber`` callable will +only be called if the request path starts with ``/add_yo``. Otherwise the +event subscriber will not be called. + +Note that the ``request_path_startswith`` subscriber you defined can be used +with events that have a ``request`` attribute, but not ones that do not. So, +for example, the predicate can be used with subscribers registered for +:class:`pyramid.events.NewRequest` and :class:`pyramid.events.ContextFound` +events, but it cannot be used with subscribers registered for +:class:`pyramid.events.ApplicationCreated` because the latter type of event +has no ``request`` attribute. The point being: unlike route and view +predicates, not every type of subscriber predicate will necessarily be +applicable for use in every subscriber registration. It is not the +responsibility of the predicate author to make every predicate make sense for +every event type; it is the responsibility of the predicate consumer to use +predicates that make sense for a particular event type registration. + + + diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index 6010740ca..de09cfbcf 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -12,6 +12,8 @@ from webob.exc import WSGIHTTPException as WebobWSGIHTTPException from pyramid.interfaces import ( IDebugLogger, IExceptionResponse, + IPredicateList, + PHASE1_CONFIG, ) from pyramid.asset import resolve_asset_spec @@ -71,6 +73,7 @@ from pyramid.config.tweens import TweensConfiguratorMixin from pyramid.config.util import ( action_method, ActionInfo, + PredicateList, ) from pyramid.config.views import ViewsConfiguratorMixin from pyramid.config.zca import ZCAConfiguratorMixin @@ -489,6 +492,32 @@ class Configurator( _get_introspector, _set_introspector, _del_introspector ) + def _get_predlist(self, name): + predlist = self.registry.queryUtility(IPredicateList, name=name) + if predlist is None: + predlist = PredicateList() + self.registry.registerUtility(predlist, IPredicateList, name=name) + return predlist + + def _add_predicate(self, type, name, factory, weighs_more_than=None, + weighs_less_than=None): + discriminator = ('%s predicate' % type, name) + intr = self.introspectable( + '%s predicates' % type, + discriminator, + '%s predicate named %s' % (type, name), + '%s predicate' % type) + intr['name'] = name + intr['factory'] = factory + intr['weighs_more_than'] = weighs_more_than + intr['weighs_less_than'] = weighs_less_than + def register(): + predlist = self._get_predlist(type) + predlist.add(name, factory, weighs_more_than=weighs_more_than, + weighs_less_than=weighs_less_than) + self.action(discriminator, register, introspectables=(intr,), + order=PHASE1_CONFIG) # must be registered early + @property def action_info(self): info = self.info # usually a ZCML action (ParserInfo) if self.info diff --git a/pyramid/config/adapters.py b/pyramid/config/adapters.py index 5f15f2e46..3af810990 100644 --- a/pyramid/config/adapters.py +++ b/pyramid/config/adapters.py @@ -1,3 +1,5 @@ +from functools import update_wrapper + from zope.interface import Interface from pyramid.interfaces import ( @@ -6,40 +8,103 @@ from pyramid.interfaces import ( IResourceURL, ) -from pyramid.config.util import action_method +from pyramid.config.util import ( + action_method, + ) + class AdaptersConfiguratorMixin(object): @action_method - def add_subscriber(self, subscriber, iface=None): + def add_subscriber(self, subscriber, iface=None, **predicates): """Add an event :term:`subscriber` for the event stream - implied by the supplied ``iface`` interface. The - ``subscriber`` argument represents a callable object (or a - :term:`dotted Python name` which identifies a callable); it - will be called with a single object ``event`` whenever - :app:`Pyramid` emits an :term:`event` associated with the - ``iface``, which may be an :term:`interface` or a class or a - :term:`dotted Python name` to a global object representing an - interface or a class. Using the default ``iface`` value, - ``None`` will cause the subscriber to be registered for all - event types. See :ref:`events_chapter` for more information - about events and subscribers.""" + implied by the supplied ``iface`` interface. + + The ``subscriber`` argument represents a callable object (or a + :term:`dotted Python name` which identifies a callable); it will be + called with a single object ``event`` whenever :app:`Pyramid` emits + an :term:`event` associated with the ``iface``, which may be an + :term:`interface` or a class or a :term:`dotted Python name` to a + global object representing an interface or a class. + + Using the default ``iface`` value, ``None`` will cause the subscriber + to be registered for all event types. See :ref:`events_chapter` for + more information about events and subscribers. + + Any number of predicate keyword arguments may be passed in + ``**predicates``. Each predicate named will narrow the set of + circumstances that the subscriber will be invoked. Each named + predicate must have been registered via + :meth:`pyramid.config.Configurator.add_subscriber_predicate` before it + can be used. See :ref:`subscriber_predicates` for more information. + + .. note:: + + THe ``**predicates`` argument is new as of Pyramid 1.4. + """ dotted = self.maybe_dotted subscriber, iface = dotted(subscriber), dotted(iface) if iface is None: iface = (Interface,) if not isinstance(iface, (tuple, list)): iface = (iface,) + def register(): - self.registry.registerHandler(subscriber, iface) + predlist = self._get_predlist('subscriber') + order, preds, phash = predlist.make(self, **predicates) + intr.update({'phash':phash, 'order':order, 'predicates':preds}) + derived_subscriber = self._derive_subscriber(subscriber, preds) + self.registry.registerHandler(derived_subscriber, iface) + intr = self.introspectable('subscribers', id(subscriber), self.object_description(subscriber), 'subscriber') + intr['subscriber'] = subscriber intr['interfaces'] = iface + self.action(None, register, introspectables=(intr,)) return subscriber + def _derive_subscriber(self, subscriber, predicates): + if not predicates: + return subscriber + def subscriber_wrapper(event): + if all((predicate(event) for predicate in predicates)): + subscriber(event) + if hasattr(subscriber, '__name__'): + update_wrapper(subscriber_wrapper, subscriber) + return subscriber_wrapper + + @action_method + def add_subscriber_predicate(self, name, factory, weighs_more_than=None, + weighs_less_than=None): + """ + Adds a subscriber predicate factory. The associated subscriber + predicate can later be named as a keyword argument to + :meth:`pyramid.config.Configurator.add_subscriber` in the + ``**predicates`` anonyous keyword argument dictionary. + + ``name`` should be the name of the predicate. It must be a valid + Python identifier (it will be used as a ``**predicates`` keyword + argument to :meth:`~pyramid.config.Configurator.add_subscriber`). + + ``factory`` should be a :term:`predicate factory`. + + See :ref:`subscriber_predicates` for more information. + + .. note:: + + This method is new as of Pyramid 1.4. + """ + self._add_predicate( + 'subscriber', + name, + factory, + weighs_more_than=weighs_more_than, + weighs_less_than=weighs_less_than + ) + @action_method def add_response_adapter(self, adapter, type_or_iface): """ When an object of type (or interface) ``type_or_iface`` is @@ -203,4 +268,3 @@ class AdaptersConfiguratorMixin(object): intr['resource_iface'] = resource_iface self.action(discriminator, register, introspectables=(intr,)) - diff --git a/pyramid/config/routes.py b/pyramid/config/routes.py index 18fe39e45..6796a1c5d 100644 --- a/pyramid/config/routes.py +++ b/pyramid/config/routes.py @@ -1,7 +1,6 @@ import warnings from pyramid.interfaces import ( - IPredicateList, IRequest, IRouteRequest, IRoutesMapper, @@ -17,7 +16,6 @@ from pyramid.urldispatch import RoutesMapper from pyramid.config.util import ( action_method, as_sorted_tuple, - PredicateList, ) import pyramid.config.predicates @@ -265,7 +263,7 @@ class RoutesConfiguratorMixin(object): registered via :meth:`pyramid.config.Configurator.add_view_predicate`. More than one key/value pair can be used at the same time. See - :ref:`registering_thirdparty_predicates` for more information about + :ref:`view_and_route_predicates` for more information about third-party predicates. This argument is new as of Pyramid 1.4. View-Related Arguments @@ -434,7 +432,7 @@ class RoutesConfiguratorMixin(object): ) ) - predlist = self.route_predlist + predlist = self._get_predlist('route') _, preds, _ = predlist.make(self, **pvals) route = mapper.connect( name, pattern, factory, predicates=preds, @@ -466,15 +464,6 @@ class RoutesConfiguratorMixin(object): attr=view_attr, ) - @property - def route_predlist(self): - predlist = self.registry.queryUtility(IPredicateList, name='route') - if predlist is None: - predlist = PredicateList() - self.registry.registerUtility(predlist, IPredicateList, - name='route') - return predlist - @action_method def add_route_predicate(self, name, factory, weighs_more_than=None, weighs_less_than=None): @@ -488,29 +477,19 @@ class RoutesConfiguratorMixin(object): ``factory`` should be a :term:`predicate factory`. - See :ref:`registering_thirdparty_predicates` for more information. + See :ref:`view_and_route_predicates` for more information. .. note:: This method is new as of Pyramid 1.4. """ - discriminator = ('route predicate', name) - intr = self.introspectable( - 'route predicates', - discriminator, - 'route predicate named %s' % name, - 'route predicate') - intr['name'] = name - intr['factory'] = factory - intr['weighs_more_than'] = weighs_more_than - intr['weighs_less_than'] = weighs_less_than - def register(): - predlist = self.route_predlist - predlist.add(name, factory, weighs_more_than=weighs_more_than, - weighs_less_than=weighs_less_than) - # must be registered before routes connected - self.action(discriminator, register, introspectables=(intr,), - order=PHASE1_CONFIG) + self._add_predicate( + 'route', + name, + factory, + weighs_more_than=weighs_more_than, + weighs_less_than=weighs_less_than + ) def add_default_route_predicates(self): p = pyramid.config.predicates diff --git a/pyramid/config/views.py b/pyramid/config/views.py index b61a71914..eaaaebdaf 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -20,7 +20,6 @@ from pyramid.interfaces import ( IException, IExceptionViewClassifier, IMultiView, - IPredicateList, IRendererFactory, IRequest, IResponse, @@ -79,7 +78,6 @@ from pyramid.config.util import ( DEFAULT_PHASH, MAX_ORDER, action_method, - PredicateList, ) urljoin = urlparse.urljoin @@ -1008,7 +1006,7 @@ class ViewsConfiguratorMixin(object): registered via :meth:`pyramid.config.Configurator.add_view_predicate`. More than one key/value pair can be used at the same time. See - :ref:`registering_thirdparty_predicates` for more information about + :ref:`view_and_route_predicates` for more information about third-party predicates. This argument is new as of Pyramid 1.4. """ @@ -1107,7 +1105,7 @@ class ViewsConfiguratorMixin(object): ) view_intr.update(**predicates) introspectables.append(view_intr) - predlist = self.view_predlist + predlist = self._get_predlist('view') def register(permission=permission, renderer=renderer): # the discrim_func above is guaranteed to have been called already @@ -1302,14 +1300,6 @@ class ViewsConfiguratorMixin(object): introspectables.append(perm_intr) self.action(discriminator, register, introspectables=introspectables) - @property - def view_predlist(self): - predlist = self.registry.queryUtility(IPredicateList, name='view') - if predlist is None: - predlist = PredicateList() - self.registry.registerUtility(predlist, IPredicateList, name='view') - return predlist - @action_method def add_view_predicate(self, name, factory, weighs_more_than=None, weighs_less_than=None): @@ -1324,28 +1314,19 @@ class ViewsConfiguratorMixin(object): ``factory`` should be a :term:`predicate factory`. - See :ref:`registering_thirdparty_predicates` for more information. + See :ref:`view_and_route_predicates` for more information. .. note:: This method is new as of Pyramid 1.4. """ - discriminator = ('view predicate', name) - intr = self.introspectable( - 'view predicates', - discriminator, - 'view predicate named %s' % name, - 'view predicate') - intr['name'] = name - intr['factory'] = factory - intr['weighs_more_than'] = weighs_more_than - intr['weighs_less_than'] = weighs_less_than - def register(): - predlist = self.view_predlist - predlist.add(name, factory, weighs_more_than=weighs_more_than, - weighs_less_than=weighs_less_than) - self.action(discriminator, register, introspectables=(intr,), - order=PHASE1_CONFIG) # must be registered before views added + self._add_predicate( + 'view', + name, + factory, + weighs_more_than=weighs_more_than, + weighs_less_than=weighs_less_than + ) def add_default_view_predicates(self): p = pyramid.config.predicates diff --git a/pyramid/events.py b/pyramid/events.py index db274823c..836466ba2 100644 --- a/pyramid/events.py +++ b/pyramid/events.py @@ -14,9 +14,10 @@ from pyramid.interfaces import ( ) class subscriber(object): - """ Decorator activated via a :term:`scan` which treats the - function being decorated as an event subscriber for the set of - interfaces passed as ``*ifaces`` to the decorator constructor. + """ Decorator activated via a :term:`scan` which treats the function + being decorated as an event subscriber for the set of interfaces passed + as ``*ifaces`` and the set of predicate terms passed as ``**predicates`` + to the decorator constructor. For example: @@ -61,16 +62,22 @@ class subscriber(object): config = Configurator() config.scan('somepackage_containing_subscribers') + Any ``**predicate`` arguments will be passed along to + :meth:`pyramid.config.Configurator.add_subscriber`. See + :ref:`subscriber_predicates` for a description of how predicates can + narrow the set of circumstances in which a subscriber will be called. + """ venusian = venusian # for unit testing - def __init__(self, *ifaces): + def __init__(self, *ifaces, **predicates): self.ifaces = ifaces + self.predicates = predicates def register(self, scanner, name, wrapped): config = scanner.config for iface in self.ifaces or (Interface,): - config.add_subscriber(wrapped, iface) + config.add_subscriber(wrapped, iface, **self.predicates) def __call__(self, wrapped): self.venusian.attach(wrapped, self.register, category='pyramid') diff --git a/pyramid/tests/test_config/test_adapters.py b/pyramid/tests/test_config/test_adapters.py index 83ea0f05b..56d2acc2f 100644 --- a/pyramid/tests/test_config/test_adapters.py +++ b/pyramid/tests/test_config/test_adapters.py @@ -81,6 +81,121 @@ class AdaptersConfiguratorMixinTests(unittest.TestCase): config.registry.subscribers((event.object, IDummy), None) self.assertEqual(len(L), 1) + def test_add_subscriber_with_specific_type_and_predicates_True(self): + from zope.interface import implementer + from zope.interface import Interface + class IEvent(Interface): + pass + @implementer(IEvent) + class Event: + pass + L = [] + def subscriber(event): + L.append(event) + config = self._makeOne(autocommit=True) + predlist = config._get_predlist('subscriber') + jam_predicate = predicate_maker('jam') + jim_predicate = predicate_maker('jim') + predlist.add('jam', jam_predicate) + predlist.add('jim', jim_predicate) + config.add_subscriber(subscriber, IEvent, jam=True, jim=True) + event = Event() + event.jam = True + event.jim = True + config.registry.notify(event) + self.assertEqual(len(L), 1) + self.assertEqual(L[0], event) + config.registry.notify(object()) + self.assertEqual(len(L), 1) + + def test_add_subscriber_with_default_type_predicates_True(self): + from zope.interface import implementer + from zope.interface import Interface + class IEvent(Interface): + pass + @implementer(IEvent) + class Event: + pass + L = [] + def subscriber(event): + L.append(event) + config = self._makeOne(autocommit=True) + predlist = config._get_predlist('subscriber') + jam_predicate = predicate_maker('jam') + jim_predicate = predicate_maker('jim') + predlist.add('jam', jam_predicate) + predlist.add('jim', jim_predicate) + config.add_subscriber(subscriber, jam=True, jim=True) + event = Event() + event.jam = True + event.jim = True + config.registry.notify(event) + self.assertEqual(len(L), 1) + self.assertEqual(L[0], event) + config.registry.notify(object()) + self.assertEqual(len(L), 1) + + def test_add_subscriber_with_specific_type_and_predicates_False(self): + from zope.interface import implementer + from zope.interface import Interface + class IEvent(Interface): + pass + @implementer(IEvent) + class Event: + pass + L = [] + def subscriber(event): L.append(event) + config = self._makeOne(autocommit=True) + predlist = config._get_predlist('subscriber') + jam_predicate = predicate_maker('jam') + jim_predicate = predicate_maker('jim') + predlist.add('jam', jam_predicate) + predlist.add('jim', jim_predicate) + config.add_subscriber(subscriber, IEvent, jam=True, jim=True) + event = Event() + event.jam = True + event.jim = False + config.registry.notify(event) + self.assertEqual(len(L), 0) + + def test_add_subscriber_with_default_type_predicates_False(self): + from zope.interface import implementer + from zope.interface import Interface + class IEvent(Interface): + pass + @implementer(IEvent) + class Event: + pass + L = [] + def subscriber(event): L.append(event) + config = self._makeOne(autocommit=True) + predlist = config._get_predlist('subscriber') + jam_predicate = predicate_maker('jam') + jim_predicate = predicate_maker('jim') + predlist.add('jam', jam_predicate) + predlist.add('jim', jim_predicate) + config.add_subscriber(subscriber, jam=True, jim=True) + event = Event() + event.jam = False + event.jim = True + config.registry.notify(event) + self.assertEqual(len(L), 0) + + def test_add_subscriber_predicate(self): + config = self._makeOne() + L = [] + def add_predicate(type, name, factory, weighs_less_than=None, + weighs_more_than=None): + self.assertEqual(type, 'subscriber') + self.assertEqual(name, 'name') + self.assertEqual(factory, 'factory') + self.assertEqual(weighs_more_than, 1) + self.assertEqual(weighs_less_than, 2) + L.append(1) + config._add_predicate = add_predicate + config.add_subscriber_predicate('name', 'factory', 1, 2) + self.assertTrue(L) + def test_add_response_adapter(self): from pyramid.interfaces import IResponse config = self._makeOne(autocommit=True) @@ -228,4 +343,14 @@ class DummyResourceURL(object): self.resource = resource self.request = request - +def predicate_maker(name): + class Predicate(object): + def __init__(self, val, config): + self.val = val + def phash(self): + return 'phash' + text = phash + def __call__(self, event): + return getattr(event, name, None) == self.val + return Predicate + diff --git a/pyramid/tests/test_events.py b/pyramid/tests/test_events.py index 3e9c959d9..2c72c07e8 100644 --- a/pyramid/tests/test_events.py +++ b/pyramid/tests/test_events.py @@ -131,9 +131,9 @@ class TestSubscriber(unittest.TestCase): def tearDown(self): testing.tearDown() - def _makeOne(self, *ifaces): + def _makeOne(self, *ifaces, **predicates): from pyramid.events import subscriber - return subscriber(*ifaces) + return subscriber(*ifaces, **predicates) def test_register_single(self): from zope.interface import Interface @@ -190,6 +190,16 @@ class TestSubscriber(unittest.TestCase): self.assertEqual(dummy_venusian.attached, [(foo, dec.register, 'pyramid')]) + def test_regsister_with_predicates(self): + from zope.interface import Interface + dec = self._makeOne(a=1) + def foo(): pass + config = DummyConfigurator() + scanner = Dummy() + scanner.config = config + dec.register(scanner, None, foo) + self.assertEqual(config.subscribed, [(foo, Interface, {'a':1})]) + class TestBeforeRender(unittest.TestCase): def _makeOne(self, system, val=None): from pyramid.events import BeforeRender @@ -264,8 +274,11 @@ class DummyConfigurator(object): def __init__(self): self.subscribed = [] - def add_subscriber(self, wrapped, ifaces): - self.subscribed.append((wrapped, ifaces)) + def add_subscriber(self, wrapped, ifaces, **predicates): + if not predicates: + self.subscribed.append((wrapped, ifaces)) + else: + self.subscribed.append((wrapped, ifaces, predicates)) class DummyRegistry(object): pass -- cgit v1.2.3 From 94ce94a8ea8c299549b9d71899f0386bfeb32f92 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 25 Aug 2012 00:24:12 -0400 Subject: return value from subscriber (even though technically nothing can rely on it) --- pyramid/config/adapters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/config/adapters.py b/pyramid/config/adapters.py index 3af810990..f0ed40e4c 100644 --- a/pyramid/config/adapters.py +++ b/pyramid/config/adapters.py @@ -71,7 +71,7 @@ class AdaptersConfiguratorMixin(object): return subscriber def subscriber_wrapper(event): if all((predicate(event) for predicate in predicates)): - subscriber(event) + return subscriber(event) if hasattr(subscriber, '__name__'): update_wrapper(subscriber_wrapper, subscriber) return subscriber_wrapper -- cgit v1.2.3 From a0547ef9d05fd6ed724d1c64a57a0063b7f1f87a Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 25 Aug 2012 00:30:32 -0400 Subject: garden --- TODO.txt | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/TODO.txt b/TODO.txt index 4b4f48499..d1209f325 100644 --- a/TODO.txt +++ b/TODO.txt @@ -24,17 +24,6 @@ Nice-to-Have - Modify the urldispatch chapter examples to assume a scan rather than ``add_view``. -- Context manager for creating a new configurator (replacing - ``with_package``). E.g.:: - - with config.partial(package='bar') as c: - c.add_view(...) - - or:: - - with config.partial(introspection=False) as c: - c.add_view(..) - - Introspection: * ``default root factory`` category (prevent folks from needing to searh @@ -114,7 +103,7 @@ Future - 1.4: Remove ``chameleon_text`` / ``chameleon_zpt`` deprecated functions (render_*) -- 1.4: Remove ``pyramid.configuration.ConfigurationError`` (deprecated). +- 1.4: Remove ``pyramid.configuration`` (deprecated). - 1.4: Remove ``pyramid.paster.PyramidTemplate`` (deprecated). @@ -128,7 +117,7 @@ Future - 1.5: Remove ``pyramid.requests.DeprecatedRequestMethodsMixin``. -- 1.5: Maybe? deprecate set_request_property in favor of pointing people at +- 1.6: Maybe? deprecate set_request_property in favor of pointing people at set_request_method. - 1.6: Remove IContextURL and TraversalContextURL. @@ -152,3 +141,15 @@ Probably Bad Ideas - http://pythonguy.wordpress.com/2011/06/22/dynamic-variables-revisited/ instead of thread locals + +- Context manager for creating a new configurator (replacing + ``with_package``). E.g.:: + + with config.partial(package='bar') as c: + c.add_view(...) + + or:: + + with config.partial(introspection=False) as c: + c.add_view(..) + -- cgit v1.2.3 From cc33a518a124883a41cadba13fb3dc4d07dfd43d Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 25 Aug 2012 02:11:31 -0400 Subject: subscribers registered for more than one interface receive all objects, which is fucking stupid, but true --- pyramid/config/adapters.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyramid/config/adapters.py b/pyramid/config/adapters.py index f0ed40e4c..01438fd30 100644 --- a/pyramid/config/adapters.py +++ b/pyramid/config/adapters.py @@ -69,9 +69,9 @@ class AdaptersConfiguratorMixin(object): def _derive_subscriber(self, subscriber, predicates): if not predicates: return subscriber - def subscriber_wrapper(event): - if all((predicate(event) for predicate in predicates)): - return subscriber(event) + def subscriber_wrapper(*arg): + if all((predicate(*arg) for predicate in predicates)): + return subscriber(*arg) if hasattr(subscriber, '__name__'): update_wrapper(subscriber_wrapper, subscriber) return subscriber_wrapper -- cgit v1.2.3 From 3c820fcfbf80621ea07cd9599b02cf95e5faf3c0 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 25 Aug 2012 02:33:27 -0400 Subject: rant --- pyramid/config/adapters.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pyramid/config/adapters.py b/pyramid/config/adapters.py index 01438fd30..e9b0df614 100644 --- a/pyramid/config/adapters.py +++ b/pyramid/config/adapters.py @@ -70,6 +70,17 @@ class AdaptersConfiguratorMixin(object): if not predicates: return subscriber def subscriber_wrapper(*arg): + # We need to accept *arg and pass it along because zope + # subscribers are designed poorly. Notification will always call + # an associated subscriber with all of the objects involved in + # the subscription lookup, despite the fact that the event sender + # always has the option to attach those objects to the event + # object itself (and usually does). It would be much saner if the + # registry just used extra args passed to notify to do the lookup + # but only called event subscribers with the actual event object, + # or if we had been smart enough early on to always wrap + # subscribers in something that threw away the extra args, but + # c'est la vie. if all((predicate(*arg) for predicate in predicates)): return subscriber(*arg) if hasattr(subscriber, '__name__'): -- cgit v1.2.3 From a31280b563d40aaa3ab102811df53c33c6d06a70 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 25 Aug 2012 02:41:09 -0400 Subject: indent like a man --- pyramid/config/adapters.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pyramid/config/adapters.py b/pyramid/config/adapters.py index e9b0df614..865e66a4a 100644 --- a/pyramid/config/adapters.py +++ b/pyramid/config/adapters.py @@ -55,10 +55,12 @@ class AdaptersConfiguratorMixin(object): derived_subscriber = self._derive_subscriber(subscriber, preds) self.registry.registerHandler(derived_subscriber, iface) - intr = self.introspectable('subscribers', - id(subscriber), - self.object_description(subscriber), - 'subscriber') + intr = self.introspectable( + 'subscribers', + id(subscriber), + self.object_description(subscriber), + 'subscriber' + ) intr['subscriber'] = subscriber intr['interfaces'] = iface -- cgit v1.2.3 From 405213d0ee1f60de38e7ed49f50df3b8480b7c66 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 25 Aug 2012 03:19:37 -0400 Subject: _get_predlist -> get_predlist --- pyramid/config/__init__.py | 4 ++-- pyramid/config/adapters.py | 2 +- pyramid/config/routes.py | 2 +- pyramid/config/views.py | 2 +- pyramid/tests/test_config/test_adapters.py | 8 ++++---- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index de09cfbcf..1dc438597 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -492,7 +492,7 @@ class Configurator( _get_introspector, _set_introspector, _del_introspector ) - def _get_predlist(self, name): + def get_predlist(self, name): predlist = self.registry.queryUtility(IPredicateList, name=name) if predlist is None: predlist = PredicateList() @@ -512,7 +512,7 @@ class Configurator( intr['weighs_more_than'] = weighs_more_than intr['weighs_less_than'] = weighs_less_than def register(): - predlist = self._get_predlist(type) + predlist = self.get_predlist(type) predlist.add(name, factory, weighs_more_than=weighs_more_than, weighs_less_than=weighs_less_than) self.action(discriminator, register, introspectables=(intr,), diff --git a/pyramid/config/adapters.py b/pyramid/config/adapters.py index 865e66a4a..12c4de660 100644 --- a/pyramid/config/adapters.py +++ b/pyramid/config/adapters.py @@ -49,7 +49,7 @@ class AdaptersConfiguratorMixin(object): iface = (iface,) def register(): - predlist = self._get_predlist('subscriber') + predlist = self.get_predlist('subscriber') order, preds, phash = predlist.make(self, **predicates) intr.update({'phash':phash, 'order':order, 'predicates':preds}) derived_subscriber = self._derive_subscriber(subscriber, preds) diff --git a/pyramid/config/routes.py b/pyramid/config/routes.py index 6796a1c5d..1a7fdfac9 100644 --- a/pyramid/config/routes.py +++ b/pyramid/config/routes.py @@ -432,7 +432,7 @@ class RoutesConfiguratorMixin(object): ) ) - predlist = self._get_predlist('route') + predlist = self.get_predlist('route') _, preds, _ = predlist.make(self, **pvals) route = mapper.connect( name, pattern, factory, predicates=preds, diff --git a/pyramid/config/views.py b/pyramid/config/views.py index eaaaebdaf..19e4acbcb 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1105,7 +1105,7 @@ class ViewsConfiguratorMixin(object): ) view_intr.update(**predicates) introspectables.append(view_intr) - predlist = self._get_predlist('view') + predlist = self.get_predlist('view') def register(permission=permission, renderer=renderer): # the discrim_func above is guaranteed to have been called already diff --git a/pyramid/tests/test_config/test_adapters.py b/pyramid/tests/test_config/test_adapters.py index 56d2acc2f..d47e012dc 100644 --- a/pyramid/tests/test_config/test_adapters.py +++ b/pyramid/tests/test_config/test_adapters.py @@ -93,7 +93,7 @@ class AdaptersConfiguratorMixinTests(unittest.TestCase): def subscriber(event): L.append(event) config = self._makeOne(autocommit=True) - predlist = config._get_predlist('subscriber') + predlist = config.get_predlist('subscriber') jam_predicate = predicate_maker('jam') jim_predicate = predicate_maker('jim') predlist.add('jam', jam_predicate) @@ -120,7 +120,7 @@ class AdaptersConfiguratorMixinTests(unittest.TestCase): def subscriber(event): L.append(event) config = self._makeOne(autocommit=True) - predlist = config._get_predlist('subscriber') + predlist = config.get_predlist('subscriber') jam_predicate = predicate_maker('jam') jim_predicate = predicate_maker('jim') predlist.add('jam', jam_predicate) @@ -146,7 +146,7 @@ class AdaptersConfiguratorMixinTests(unittest.TestCase): L = [] def subscriber(event): L.append(event) config = self._makeOne(autocommit=True) - predlist = config._get_predlist('subscriber') + predlist = config.get_predlist('subscriber') jam_predicate = predicate_maker('jam') jim_predicate = predicate_maker('jim') predlist.add('jam', jam_predicate) @@ -169,7 +169,7 @@ class AdaptersConfiguratorMixinTests(unittest.TestCase): L = [] def subscriber(event): L.append(event) config = self._makeOne(autocommit=True) - predlist = config._get_predlist('subscriber') + predlist = config.get_predlist('subscriber') jam_predicate = predicate_maker('jam') jim_predicate = predicate_maker('jim') predlist.add('jam', jam_predicate) -- cgit v1.2.3 From a9289d95036eb23e973815e529d3db3fea235046 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 26 Aug 2012 15:50:34 -0400 Subject: - Forward-port from 1.3 branch: when registering multiple views with an ``accept`` predicate in a Pyramid application runing under Python 3, you might have received a ``TypeError: unorderable types: function() < function()`` exception. --- CHANGES.txt | 5 +++++ pyramid/config/views.py | 2 +- pyramid/tests/test_config/test_views.py | 25 +++++++++++++++++++++---- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 369e9d74d..c414ab74c 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -37,6 +37,11 @@ Bug Fixes ``__doc__`` attributes. See https://github.com/Pylons/pyramid/issues/621 and https://github.com/Pylons/pyramid/pull/647. +- Forward-port from 1.3 branch: when registering multiple views with an + ``accept`` predicate in a Pyramid application runing under Python 3, you + might have received a ``TypeError: unorderable types: function() < + function()`` exception. + Features -------- diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 19e4acbcb..36896a17e 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -573,7 +573,7 @@ class MultiView(object): return else: subset.append((order, view, phash)) - subset.sort() + subset.sort(key=operator.itemgetter(0)) accepts = set(self.accepts) accepts.add(accept) self.accepts = list(accepts) # dedupe diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index 72a0d8ebd..575d8c738 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -2140,11 +2140,28 @@ class TestMultiView(unittest.TestCase): def test_add_with_phash_override_accept(self): mv = self._makeOne() - mv.add('view2', 100, accept='text/html', phash='abc') - mv.add('view3', 100, accept='text/html', phash='abc') - mv.add('view4', 99, accept='text/html', phash='def') + def view1(): pass + def view2(): pass + def view3(): pass + mv.add(view1, 100, accept='text/html', phash='abc') + mv.add(view2, 100, accept='text/html', phash='abc') + mv.add(view3, 99, accept='text/html', phash='def') self.assertEqual(mv.media_views['text/html'], - [(99, 'view4', 'def'), (100, 'view3', 'abc')]) + [(99, view3, 'def'), (100, view2, 'abc')]) + + def test_add_with_phash_override_accept2(self): + mv = self._makeOne() + def view1(): pass + def view2(): pass + def view3(): pass + mv.add(view1, 100, accept='text/html', phash='abc') + mv.add(view2, 100, accept='text/html', phash='def') + mv.add(view3, 99, accept='text/html', phash='ghi') + self.assertEqual(mv.media_views['text/html'], + [(99, view3, 'ghi'), + (100, view1, 'abc'), + (100, view2, 'def')] + ) def test_multiple_with_functions_as_views(self): # this failed on py3 at one point, because functions aren't orderable -- cgit v1.2.3