From 844ed90133f051d013330cb0ed4c95dbb29eecc1 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 20 Feb 2012 12:41:47 -0500 Subject: Features -------- - Add an ``introspection`` boolean to the Configurator constructor. If this is ``True``, actions registered using the Configurator will be registered with the introspector. If it is ``False``, they won't. The default is ``True``. Setting it to ``False`` during action processing will prevent introspection for any following registration statements, and setting it to ``True`` will start them up again. This addition is to service a requirement that the debug toolbar's own views and methods not show up in the introspector. Backwards Incompatibilities --------------------------- - Remove ``pyramid.config.Configurator.with_context`` class method. It was never an API, it is only used by ``pyramid_zcml`` and its functionality has been moved to that package's latest release. This means that you'll need to use the latest release of ``pyramid_zcml`` with this release of Pyramid. - The ``introspector`` argument to the ``pyramid.config.Configurator`` constructor API has been removed. It has been replaced by the boolean ``introspection`` flag. - The ``pyramid.registry.noop_introspector`` API object has been removed. --- CHANGES.txt | 29 +++++++++++++++++++ docs/api/registry.rst | 7 ----- docs/narr/introspector.rst | 15 ++++------ pyramid/config/__init__.py | 51 ++++++++++++---------------------- pyramid/registry.py | 22 --------------- pyramid/tests/test_config/test_init.py | 48 ++++---------------------------- pyramid/tests/test_registry.py | 46 ------------------------------ 7 files changed, 59 insertions(+), 159 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 1df924b4c..239e1326f 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,32 @@ +Next release +============ + +Features +-------- + +- Add an ``introspection`` boolean to the Configurator constructor. If this + is ``True``, actions registered using the Configurator will be registered + with the introspector. If it is ``False``, they won't. The default is + ``True``. Setting it to ``False`` during action processing will prevent + introspection for any following registration statements, and setting it to + ``True`` will start them up again. This addition is to service a + requirement that the debug toolbar's own views and methods not show up in + the introspector. + +Backwards Incompatibilities +--------------------------- + +- Remove ``pyramid.config.Configurator.with_context`` class method. It was + never an API, it is only used by ``pyramid_zcml`` and its functionality has + been moved to that package's latest release. This means that you'll need + to use the latest release of ``pyramid_zcml`` with this release of Pyramid. + +- The ``introspector`` argument to the ``pyramid.config.Configurator`` + constructor API has been removed. It has been replaced by the boolean + ``introspection`` flag. + +- The ``pyramid.registry.noop_introspector`` API object has been removed. + 1.3a8 (2012-02-19) ================== diff --git a/docs/api/registry.rst b/docs/api/registry.rst index e18d1b6c2..e62e2ba6f 100644 --- a/docs/api/registry.rst +++ b/docs/api/registry.rst @@ -38,10 +38,3 @@ This class is new as of :app:`Pyramid` 1.3. -.. class:: noop_introspector - - An introspector which throws away all registrations, useful for disabling - introspection altogether (pass as ``introspector`` to the - :term:`Configurator` constructor). - - This class is new as of :app:`Pyramid` 1.3. diff --git a/docs/narr/introspector.rst b/docs/narr/introspector.rst index d465c47d9..74595cac8 100644 --- a/docs/narr/introspector.rst +++ b/docs/narr/introspector.rst @@ -576,17 +576,14 @@ relationships. It looks something like this: Disabling Introspection ----------------------- -You can disable Pyramid introspection by passing the object -:attr:`pyramid.registry.noop_introspector` to the :term:`Configurator` -constructor in your application setup: +You can disable Pyramid introspection by passing the flag +``introspection=False`` to the :term:`Configurator` constructor in your +application setup: .. code-block:: python from pyramid.config import Configurator - from pyramid.registry import noop_introspector - config = Configurator(..., introspector=noop_introspector) + config = Configurator(..., introspection=False) -When the noop introspector is active, all introspectables generated by -configuration directives are thrown away. A noop introspector behaves just -like a "real" introspector, but the methods of a noop introspector do nothing -and return null values. +When ``introspection`` is ``False``, all introspectables generated by +configuration directives are thrown away. diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index 06d3c6abf..52d7aca83 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -239,11 +239,11 @@ class Configurator( :meth:`pyramid.config.Configurator.add_route` will have the specified path prepended to their pattern. This parameter is new in Pyramid 1.2. - If ``introspector`` is passed, it must be an instance implementing the - attributes and methods of :class:`pyramid.interfaces.IIntrospector`. If - ``introspector`` is not passed (or is passed as ``None``), the default - introspector implementation will be used. This parameter is new in - Pyramid 1.3. + If ``introspection`` is passed, it must be a boolean value. If it's + ``True``, introspection values during actions will be kept for for use + for tools like the debug toolbar. If it's ``False``, introspection + values provided by registrations will be ignored. By default, it is + ``True``. This parameter is new as of Pyramid 1.3. """ manager = manager # for testing injection venusian = venusian # for testing injection @@ -273,7 +273,7 @@ class Configurator( autocommit=False, exceptionresponse_view=default_exceptionresponse_view, route_prefix=None, - introspector=None, + introspection=True, ): if package is None: package = caller_package() @@ -284,6 +284,7 @@ class Configurator( self.registry = registry self.autocommit = autocommit self.route_prefix = route_prefix + self.introspection = introspection if registry is None: registry = Registry(self.package_name) self.registry = registry @@ -301,7 +302,6 @@ class Configurator( session_factory=session_factory, default_view_mapper=default_view_mapper, exceptionresponse_view=exceptionresponse_view, - introspector=introspector, ) def setup_registry(self, @@ -318,7 +318,7 @@ class Configurator( session_factory=None, default_view_mapper=None, exceptionresponse_view=default_exceptionresponse_view, - introspector=None): + ): """ When you pass a non-``None`` ``registry`` argument to the :term:`Configurator` constructor, no initial setup is performed against the registry. This is because the registry you pass in may @@ -339,10 +339,6 @@ class Configurator( self._fix_registry() - if introspector is not None: - # use nondefault introspector - self.introspector = introspector - self._set_settings(settings) self._register_response_adapters() @@ -529,7 +525,8 @@ class Configurator( ``introspectables`` is a sequence of :term:`introspectable` objects (or the empty sequence if no introspectable objects are associated - with this action). + with this action). If this configurator's ``introspection`` + attribute is ``False``, these introspectables will be ignored. ``extra`` provides a facility for inserting extra keys and values into an action dictionary. @@ -543,14 +540,17 @@ class Configurator( autocommit = self.autocommit action_info = self.action_info - introspector = self.introspector + + if not self.introspection: + # if we're not introspecting, ignore any introspectables passed + # to us + introspectables = () if autocommit: if callable is not None: callable(*args, **kw) - if introspector is not None: - for introspectable in introspectables: - introspectable.register(introspector, action_info) + for introspectable in introspectables: + introspectable.register(self.introspector, action_info) else: action = extra @@ -782,22 +782,6 @@ class Configurator( m = types.MethodType(c, self, self.__class__) return m - @classmethod - def with_context(cls, context): - """A classmethod used by ``pyramid_zcml`` directives to obtain a - configurator with 'the right' context. Returns a new Configurator - instance.""" - configurator = cls( - registry=context.registry, - package=context.package, - autocommit=context.autocommit, - route_prefix=context.route_prefix - ) - configurator.basepath = context.basepath - configurator.includepath = context.includepath - configurator.info = context.info - return configurator - def with_package(self, package): """ Return a new Configurator instance with the same registry as this configurator using the package supplied as the @@ -809,6 +793,7 @@ class Configurator( package=package, autocommit=self.autocommit, route_prefix=self.route_prefix, + introspection=self.introspection, ) configurator.basepath = self.basepath configurator.includepath = self.includepath diff --git a/pyramid/registry.py b/pyramid/registry.py index 7e373b58a..f0f9c83ea 100644 --- a/pyramid/registry.py +++ b/pyramid/registry.py @@ -172,28 +172,6 @@ class Introspector(object): raise KeyError((category_name, discriminator)) return self._refs.get(intr, []) -@implementer(IIntrospector) -class _NoopIntrospector(object): - def add(self, intr): - pass - def get(self, category_name, discriminator, default=None): - return default - def get_category(self, category_name, default=None, sort_key=None): - return default - def categorized(self, sort_key=None): - return [] - def categories(self): - return [] - def remove(self, category_name, discriminator): - return - def relate(self, *pairs): - return - unrelate = relate - def related(self, intr): - return [] - -noop_introspector = _NoopIntrospector() - @implementer(IIntrospectable) class Introspectable(dict): diff --git a/pyramid/tests/test_config/test_init.py b/pyramid/tests/test_config/test_init.py index 283800e1e..f872b9df9 100644 --- a/pyramid/tests/test_config/test_init.py +++ b/pyramid/tests/test_config/test_init.py @@ -228,11 +228,9 @@ class ConfiguratorTests(unittest.TestCase): request_iface=IRequest) self.assertTrue(view.__wraps__ is exceptionresponse_view) - def test_ctor_with_introspector(self): - introspector = DummyIntrospector() - config = self._makeOne(introspector=introspector) - self.assertEqual(config.introspector, introspector) - self.assertEqual(config.registry.introspector, introspector) + def test_ctor_with_introspection(self): + config = self._makeOne(introspection=False) + self.assertEqual(config.introspection, False) def test_with_package_module(self): from pyramid.tests.test_config import test_init @@ -648,7 +646,7 @@ pyramid.tests.test_config.dummy_include2""", default = inst.introspector self.assertTrue(hasattr(default, 'add')) self.assertEqual(inst.introspector, inst.registry.introspector) - introspector = DummyIntrospector() + introspector = object() inst.introspector = introspector new = inst.introspector self.assertTrue(new is introspector) @@ -759,25 +757,6 @@ pyramid.tests.test_config.dummy_include2""", else: # pragma: no cover raise AssertionError - def test_with_context(self): - config = self._makeOne() - context = DummyZCMLContext() - context.basepath = 'basepath' - context.includepath = ('spec',) - context.package = 'pyramid' - context.autocommit = True - context.registry = 'abc' - context.route_prefix = 'buz' - context.info = 'info' - newconfig = config.with_context(context) - self.assertEqual(newconfig.package_name, 'pyramid') - self.assertEqual(newconfig.autocommit, True) - self.assertEqual(newconfig.registry, 'abc') - self.assertEqual(newconfig.route_prefix, 'buz') - self.assertEqual(newconfig.basepath, 'basepath') - self.assertEqual(newconfig.includepath, ('spec',)) - self.assertEqual(newconfig.info, 'info') - def test_action_branching_kw_is_None(self): config = self._makeOne(autocommit=True) self.assertEqual(config.action('discrim'), None) @@ -1638,7 +1617,7 @@ class TestActionState(unittest.TestCase): 'order':0, 'includepath':(), 'info':None, 'introspectables':(intr,)}, ] - introspector = DummyIntrospector() + introspector = object() c.execute_actions(introspector=introspector) self.assertEqual(output, [((1,), {})]) self.assertEqual(intr.registered, [(introspector, None)]) @@ -1651,7 +1630,7 @@ class TestActionState(unittest.TestCase): 'order':0, 'includepath':(), 'info':None, 'introspectables':(intr,)}, ] - introspector = DummyIntrospector() + introspector = object() c.execute_actions(introspector=introspector) self.assertEqual(intr.registered, [(introspector, None)]) @@ -1982,21 +1961,6 @@ class DummyActionState(object): def action(self, *arg, **kw): self.actions.append((arg, kw)) -class DummyZCMLContext(object): - package = None - registry = None - autocommit = False - route_prefix = None - basepath = None - includepath = () - info = '' - -class DummyIntrospector(object): - def __init__(self): - self.intrs = [] - def add(self, intr): - self.intrs.append(intr) - class DummyIntrospectable(object): def __init__(self): self.registered = [] diff --git a/pyramid/tests/test_registry.py b/pyramid/tests/test_registry.py index 29803346a..11019b852 100644 --- a/pyramid/tests/test_registry.py +++ b/pyramid/tests/test_registry.py @@ -254,52 +254,6 @@ class TestIntrospector(unittest.TestCase): del inst._categories['category'] self.assertRaises(KeyError, inst.related, intr) -class Test_noop_introspector(unittest.TestCase): - def _makeOne(self): - from pyramid.registry import noop_introspector - return noop_introspector - - def test_conformance(self): - from zope.interface.verify import verifyObject - from pyramid.interfaces import IIntrospector - verifyObject(IIntrospector, self._makeOne()) - - def test_add(self): - inst = self._makeOne() - self.assertEqual(inst.add('a'), None) - - def test_get(self): - inst = self._makeOne() - self.assertEqual(inst.get('category', 'd', default='123'), '123') - - def test_get_category(self): - inst = self._makeOne() - self.assertEqual(inst.get_category('category', default='123'), '123') - - def test_categorized(self): - inst = self._makeOne() - self.assertEqual(inst.categorized(), []) - - def test_categories(self): - inst = self._makeOne() - self.assertEqual(inst.categories(), []) - - def test_remove(self): - inst = self._makeOne() - self.assertEqual(inst.remove('cat', 'discrim'), None) - - def test_relate(self): - inst = self._makeOne() - self.assertEqual(inst.relate(), None) - - def test_unrelate(self): - inst = self._makeOne() - self.assertEqual(inst.unrelate(), None) - - def test_related(self): - inst = self._makeOne() - self.assertEqual(inst.related('a'), []) - class TestIntrospectable(unittest.TestCase): def _getTargetClass(slf): from pyramid.registry import Introspectable -- cgit v1.2.3 From 0192b52ded4dd1fd788ff572c9dfc54ac01e62d6 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 20 Feb 2012 13:24:19 -0500 Subject: add tests for introspection switch --- pyramid/tests/test_config/test_init.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/pyramid/tests/test_config/test_init.py b/pyramid/tests/test_config/test_init.py index f872b9df9..da331e5ee 100644 --- a/pyramid/tests/test_config/test_init.py +++ b/pyramid/tests/test_config/test_init.py @@ -774,6 +774,13 @@ pyramid.tests.test_config.dummy_include2""", self.assertEqual(intr.registered[0][0], config.introspector) self.assertEqual(intr.registered[0][1].__class__, ActionInfo) + def test_action_autocommit_with_introspectables_introspection_off(self): + config = self._makeOne(autocommit=True) + config.introspection = False + intr = DummyIntrospectable() + config.action('discrim', introspectables=(intr,)) + self.assertEqual(len(intr.registered), 0) + def test_action_branching_nonautocommit_with_config_info(self): config = self._makeOne(autocommit=False) config.info = 'abc' @@ -825,6 +832,19 @@ pyramid.tests.test_config.dummy_include2""", self.assertEqual( state.actions[0][1]['introspectables'], (intr,)) + def test_action_nonautocommit_with_introspectables_introspection_off(self): + config = self._makeOne(autocommit=False) + config.info = '' + config._ainfo = [] + config.introspection = False + state = DummyActionState() + config.action_state = state + state.autocommit = False + intr = DummyIntrospectable() + config.action('discrim', introspectables=(intr,)) + self.assertEqual( + state.actions[0][1]['introspectables'], ()) + def test_scan_integration(self): from zope.interface import alsoProvides from pyramid.interfaces import IRequest -- cgit v1.2.3 From 0d0b5bf8c8369cda8f26f6d12068c500ca900d7c Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 20 Feb 2012 13:27:15 -0500 Subject: modify whatsnew --- docs/whatsnew-1.3.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/whatsnew-1.3.rst b/docs/whatsnew-1.3.rst index d2df88093..0a8205d61 100644 --- a/docs/whatsnew-1.3.rst +++ b/docs/whatsnew-1.3.rst @@ -128,7 +128,6 @@ application developer. New APIs were added to support introspection :attr:`pyramid.registry.Introspectable`, -:attr:`pyramid.registry.noop_introspector`, :attr:`pyramid.config.Configurator.introspector`, :attr:`pyramid.config.Configurator.introspectable`, :attr:`pyramid.registry.Registry.introspector`. -- cgit v1.2.3 From 9ee05af8a4dca28907cd5d6e89799eff01767ee1 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 20 Feb 2012 13:27:53 -0500 Subject: garden --- TODO.txt | 3 --- 1 file changed, 3 deletions(-) diff --git a/TODO.txt b/TODO.txt index 90f5f8547..78c1740d0 100644 --- a/TODO.txt +++ b/TODO.txt @@ -27,9 +27,6 @@ Nice-to-Have * get rid of "tweens" category (can't sort properly?) - * Introspection hiding for directives for purposes of omitting toolbar - registrations. Maybe toolbar can just use a null introspector? - - Fix deployment recipes in cookbook (discourage proxying without changing server). -- cgit v1.2.3 From 5ca6a9541aa995414e55443503f0f2148c8eb4ba Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 20 Feb 2012 13:36:35 -0500 Subject: provide version number --- CHANGES.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index 239e1326f..076541221 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -19,7 +19,8 @@ Backwards Incompatibilities - Remove ``pyramid.config.Configurator.with_context`` class method. It was never an API, it is only used by ``pyramid_zcml`` and its functionality has been moved to that package's latest release. This means that you'll need - to use the latest release of ``pyramid_zcml`` with this release of Pyramid. + to use the latest release of ``pyramid_zcml`` (0.9.2+) with this release of + Pyramid. - The ``introspector`` argument to the ``pyramid.config.Configurator`` constructor API has been removed. It has been replaced by the boolean -- cgit v1.2.3 From 65d4a671283f9162afd4c0f8a61009d1ac0b904f Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 20 Feb 2012 13:49:10 -0500 Subject: remove untruth --- docs/api/config.rst | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/api/config.rst b/docs/api/config.rst index b76fed9cb..ca9351db0 100644 --- a/docs/api/config.rst +++ b/docs/api/config.rst @@ -112,9 +112,7 @@ The :term:`introspector` related to this configuration. It is an instance implementing the :class:`pyramid.interfaces.IIntrospector` - interface. If the Configurator constructor was supplied with an - ``introspector`` argument, this attribute will be that value. - Otherwise, it will be an instance of a default introspector type. + interface. .. note:: -- cgit v1.2.3 From 225a7b88ced6d2c4453a67b03d3c3592caf5141a Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 20 Feb 2012 13:53:22 -0500 Subject: recategorize a couple of methods --- docs/api/config.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api/config.rst b/docs/api/config.rst index ca9351db0..6b4ed7b1b 100644 --- a/docs/api/config.rst +++ b/docs/api/config.rst @@ -76,18 +76,18 @@ .. automethod:: action .. automethod:: add_directive .. automethod:: with_package + .. automethod:: derive_view :methodcategory:`Utility Methods` .. automethod:: absolute_asset_spec - .. automethod:: derive_view .. automethod:: maybe_dotted - .. automethod:: setup_registry :methodcategory:`ZCA-Related APIs` .. automethod:: hook_zca .. automethod:: unhook_zca + .. automethod:: setup_registry :methodcategory:`Testing Helper APIs` -- cgit v1.2.3 From 56a10086cda6abdd33f5d50f4090c843eadb808d Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 20 Feb 2012 13:55:09 -0500 Subject: garden --- TODO.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/TODO.txt b/TODO.txt index 78c1740d0..2d811288d 100644 --- a/TODO.txt +++ b/TODO.txt @@ -15,6 +15,12 @@ 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(...) + - Decorator for append_slash_notfound_view_factory. - Introspection: -- cgit v1.2.3 From 8fc93b93f21485f0c303ad461ac8070e2bb6dae7 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 20 Feb 2012 14:01:38 -0500 Subject: garden --- TODO.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/TODO.txt b/TODO.txt index 2d811288d..0143d275e 100644 --- a/TODO.txt +++ b/TODO.txt @@ -21,6 +21,11 @@ Nice-to-Have with config.partial(package='bar') as c: c.add_view(...) + or:: + + with config.partial(introspection=False) as c: + c.add_view(..) + - Decorator for append_slash_notfound_view_factory. - Introspection: -- cgit v1.2.3 From 40301d8132a3f633dc0b5e941e8bd82a29f0fb36 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 20 Feb 2012 14:13:59 -0500 Subject: garden --- CHANGES.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index 076541221..e8b2326e4 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -19,7 +19,7 @@ Backwards Incompatibilities - Remove ``pyramid.config.Configurator.with_context`` class method. It was never an API, it is only used by ``pyramid_zcml`` and its functionality has been moved to that package's latest release. This means that you'll need - to use the latest release of ``pyramid_zcml`` (0.9.2+) with this release of + to use the 0.9.2 or later release of ``pyramid_zcml`` with this release of Pyramid. - The ``introspector`` argument to the ``pyramid.config.Configurator`` -- cgit v1.2.3 From 22bae974cf49a80fd60bfe51b110ee00e91e729b Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 20 Feb 2012 21:27:19 -0500 Subject: - The static file response object used by ``config.add_static_view`` opened the static file twice, when it only needed to open it once. --- CHANGES.txt | 6 ++++++ pyramid/static.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index e8b2326e4..8595e726e 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -28,6 +28,12 @@ Backwards Incompatibilities - The ``pyramid.registry.noop_introspector`` API object has been removed. +Bug Fixes +--------- + +- The static file response object used by ``config.add_static_view`` opened + the static file twice, when it only needed to open it once. + 1.3a8 (2012-02-19) ================== diff --git a/pyramid/static.py b/pyramid/static.py index 8788d016d..e91485fad 100644 --- a/pyramid/static.py +++ b/pyramid/static.py @@ -68,7 +68,7 @@ class _FileResponse(Response): if 'wsgi.file_wrapper' in environ: app_iter = environ['wsgi.file_wrapper'](f, _BLOCK_SIZE) else: - app_iter = _FileIter(open(path, 'rb'), _BLOCK_SIZE) + app_iter = _FileIter(f, _BLOCK_SIZE) self.app_iter = app_iter # assignment of content_length must come after assignment of app_iter self.content_length = content_length -- cgit v1.2.3 From fee3811d2995683b8b319e3283aeedc75e42f5a2 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 20 Feb 2012 21:58:34 -0500 Subject: squash some resource warnings under py3 --- pyramid/scripts/pserve.py | 5 +++++ pyramid/tests/test_path.py | 7 +++++-- pyramid/tests/test_scripts/test_pserve.py | 2 ++ pyramid/tests/test_static.py | 2 ++ 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/pyramid/scripts/pserve.py b/pyramid/scripts/pserve.py index 087549cd2..c2df7162f 100644 --- a/pyramid/scripts/pserve.py +++ b/pyramid/scripts/pserve.py @@ -581,6 +581,11 @@ class LazyWriter(object): self.lock.release() return self.fileobj + def __del__(self): + fileobj = self.fileobj + if fileobj is not None: + fileobj.close() + def write(self, text): fileobj = self.open() fileobj.write(text) diff --git a/pyramid/tests/test_path.py b/pyramid/tests/test_path.py index 304afad7c..42b38d785 100644 --- a/pyramid/tests/test_path.py +++ b/pyramid/tests/test_path.py @@ -283,7 +283,8 @@ class TestPkgResourcesAssetDescriptor(unittest.TestCase): inst = self._makeOne() inst.pkg_resources = DummyPkgResource() inst.pkg_resources.resource_stream = lambda x, y: '%s:%s' % (x, y) - self.assertEqual(inst.stream(), + s = inst.stream() + self.assertEqual(s, '%s:%s' % ('pyramid.tests', 'test_asset.py')) def test_isdir(self): @@ -337,7 +338,9 @@ class TestFSAssetDescriptor(unittest.TestCase): def test_stream(self): inst = self._makeOne() - val = inst.stream().read() + s = inst.stream() + val = s.read() + s.close() self.assertTrue(b'asset' in val) def test_isdir_False(self): diff --git a/pyramid/tests/test_scripts/test_pserve.py b/pyramid/tests/test_scripts/test_pserve.py index fe489aa66..d19eb6901 100644 --- a/pyramid/tests/test_scripts/test_pserve.py +++ b/pyramid/tests/test_scripts/test_pserve.py @@ -87,6 +87,7 @@ class TestLazyWriter(unittest.TestCase): inst = self._makeOne(filename) fp = inst.open() self.assertEqual(fp.name, filename) + fp.close() finally: os.remove(filename) @@ -122,6 +123,7 @@ class TestLazyWriter(unittest.TestCase): inst.flush() fp = inst.fileobj self.assertEqual(fp.name, filename) + fp.close() finally: os.remove(filename) diff --git a/pyramid/tests/test_static.py b/pyramid/tests/test_static.py index 3d6fbe893..109f82cbf 100644 --- a/pyramid/tests/test_static.py +++ b/pyramid/tests/test_static.py @@ -185,6 +185,7 @@ class Test_static_view_use_subpath_False(unittest.TestCase): self.assertEqual(response.status, '200 OK') self.assertEqual(response.content_type, 'application/x-tar') self.assertEqual(response.content_encoding, 'gzip') + response.app_iter.close() def test_resource_no_content_encoding(self): inst = self._makeOne('pyramid.tests:fixtures/static') @@ -194,6 +195,7 @@ class Test_static_view_use_subpath_False(unittest.TestCase): self.assertEqual(response.status, '200 OK') self.assertEqual(response.content_type, 'text/html') self.assertEqual(response.content_encoding, None) + response.app_iter.close() class Test_static_view_use_subpath_True(unittest.TestCase): def _getTargetClass(self): -- cgit v1.2.3 From 3f7681efc96f815008abc30e152cd906851b00b0 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 20 Feb 2012 22:32:33 -0500 Subject: squash the last resource warning --- pyramid/tests/test_static.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyramid/tests/test_static.py b/pyramid/tests/test_static.py index 109f82cbf..02cd49430 100644 --- a/pyramid/tests/test_static.py +++ b/pyramid/tests/test_static.py @@ -128,6 +128,7 @@ class Test_static_view_use_subpath_False(unittest.TestCase): self.assertTrue(isinstance(app_iter, _Wrapper)) self.assertTrue(b'static' in app_iter.file.read()) self.assertEqual(app_iter.block_size, _BLOCK_SIZE) + app_iter.file.close() def test_resource_is_file_with_cache_max_age(self): inst = self._makeOne('pyramid.tests:fixtures/static', cache_max_age=600) -- cgit v1.2.3 From 0db4a157083d51251b4d3f574a1699fc76359c9d Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 22 Feb 2012 15:37:50 -0500 Subject: - New API: ``pyramid.config.Configurator.add_notfound_view``. This is a wrapper for ``pyramid.Config.configurator.add_view`` which provides easy append_slash support. It should be preferred over calling ``add_view`` directly with ``context=HTTPNotFound`` as was previously recommended. - New API: ``pyramid.view.notfound_view_config``. This is a decorator constructor like ``pyramid.view.view_config`` that calls ``pyramid.config.Configurator.add_notfound_view`` when scanned. It should be preferred over using ``pyramid.view.view_config`` with ``context=HTTPNotFound`` as was previously recommended. - The older deprecated ``set_notfound_view`` Configurator method is now an alias for the new ``add_notfound_view`` Configurator method. This has the following impact: the ``context`` sent to views with a ``(context, request)`` call signature registered via the deprecated ``add_notfound_view``/``set_notfound_view`` will now be the HTTPNotFound exception object instead of the actual resource context found. Use ``request.context`` to get the actual resource context. It's also recommended to disuse ``set_notfound_view`` in favor of ``add_notfound_view``, despite the aliasing. - The API documentation for ``pyramid.view.append_slash_notfound_view`` and ``pyramid.view.AppendSlashNotFoundViewFactory`` was removed. These names still exist and are still importable, but they are no longer APIs. Use ``pyramid.config.Configurator.add_notfound_view(append_slash=True)`` or ``pyramid.view.notfound_view_config(append_slash=True)`` to get the same behavior. - The ``set_forbidden_view`` method of the Configurator was removed from the documentation. It has been deprecated since Pyramid 1.1. - The AppendSlashNotFoundViewFactory used request.path to match routes. This was wrong because request.path contains the script name, and this would cause it to fail in circumstances where the script name was not empty. It should have used request.path_info, and now does. - Updated the "Registering a Not Found View" section of the "Hooks" chapter, replacing explanations of registering a view using ``add_view`` or ``view_config`` with ones using ``add_notfound_view`` or ``notfound_view_config``. - Updated the "Redirecting to Slash-Appended Routes" section of the "URL Dispatch" chapter, replacing explanations of registering a view using ``add_view`` or ``view_config`` with ones using ``add_notfound_view`` or ``notfound_view_config`` --- CHANGES.txt | 52 ++++++++++ TODO.txt | 10 +- docs/api/config.rst | 3 +- docs/api/view.rst | 6 +- docs/narr/hooks.rst | 81 +++++++++++++--- docs/narr/renderers.rst | 2 +- docs/narr/urldispatch.rst | 141 +++++++++++++++------------- pyramid/config/util.py | 3 +- pyramid/config/views.py | 105 ++++++++++++--------- pyramid/tests/pkgs/notfoundview/__init__.py | 30 ++++++ pyramid/tests/test_config/test_views.py | 28 +++--- pyramid/tests/test_integration.py | 17 ++++ pyramid/tests/test_view.py | 48 +++++++++- pyramid/view.py | 91 ++++++++++++++++-- 14 files changed, 464 insertions(+), 153 deletions(-) create mode 100644 pyramid/tests/pkgs/notfoundview/__init__.py diff --git a/CHANGES.txt b/CHANGES.txt index 8595e726e..efeba0447 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -13,6 +13,17 @@ Features requirement that the debug toolbar's own views and methods not show up in the introspector. +- New API: ``pyramid.config.Configurator.add_notfound_view``. This is a + wrapper for ``pyramid.Config.configurator.add_view`` which provides easy + append_slash support. It should be preferred over calling ``add_view`` + directly with ``context=HTTPNotFound`` as was previously recommended. + +- New API: ``pyramid.view.notfound_view_config``. This is a decorator + constructor like ``pyramid.view.view_config`` that calls + ``pyramid.config.Configurator.add_notfound_view`` when scanned. It should + be preferred over using ``pyramid.view.view_config`` with + ``context=HTTPNotFound`` as was previously recommended. + Backwards Incompatibilities --------------------------- @@ -28,12 +39,53 @@ Backwards Incompatibilities - The ``pyramid.registry.noop_introspector`` API object has been removed. +- The older deprecated ``set_notfound_view`` Configurator method is now an + alias for the new ``add_notfound_view`` Configurator method. This has the + following impact: the ``context`` sent to views with a ``(context, + request)`` call signature registered via the deprecated + ``add_notfound_view``/``set_notfound_view`` will now be the HTTPNotFound + exception object instead of the actual resource context found. Use + ``request.context`` to get the actual resource context. It's also + recommended to disuse ``set_notfound_view`` in favor of + ``add_notfound_view``, despite the aliasing. + +Deprecations +------------ + +- The API documentation for ``pyramid.view.append_slash_notfound_view`` and + ``pyramid.view.AppendSlashNotFoundViewFactory`` was removed. These names + still exist and are still importable, but they are no longer APIs. Use + ``pyramid.config.Configurator.add_notfound_view(append_slash=True)`` or + ``pyramid.view.notfound_view_config(append_slash=True)`` to get the same + behavior. + +- The ``set_forbidden_view`` method of the Configurator was removed from the + documentation. It has been deprecated since Pyramid 1.1. + Bug Fixes --------- - The static file response object used by ``config.add_static_view`` opened the static file twice, when it only needed to open it once. +- The AppendSlashNotFoundViewFactory used request.path to match routes. This + was wrong because request.path contains the script name, and this would + cause it to fail in circumstances where the script name was not empty. It + should have used request.path_info, and now does. + +Documentation +------------- + +- Updated the "Registering a Not Found View" section of the "Hooks" chapter, + replacing explanations of registering a view using ``add_view`` or + ``view_config`` with ones using ``add_notfound_view`` or + ``notfound_view_config``. + +- Updated the "Redirecting to Slash-Appended Routes" section of the "URL + Dispatch" chapter, replacing explanations of registering a view using + ``add_view`` or ``view_config`` with ones using ``add_notfound_view`` or + ``notfound_view_config`` + 1.3a8 (2012-02-19) ================== diff --git a/TODO.txt b/TODO.txt index 0143d275e..20a677bea 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,9 +1,17 @@ Pyramid TODOs ============= +Must-Have +--------- + +- Fix scaffolds and tutorials to use notfound_view_config rather than + view_config. + Nice-to-Have ------------ +- Add forbidden_view_config? + - Add docs about upgrading between Pyramid versions (e.g. how to see deprecation warnings). @@ -26,8 +34,6 @@ Nice-to-Have with config.partial(introspection=False) as c: c.add_view(..) -- Decorator for append_slash_notfound_view_factory. - - Introspection: * ``default root factory`` category (prevent folks from needing to searh diff --git a/docs/api/config.rst b/docs/api/config.rst index 6b4ed7b1b..bf5fdbb7c 100644 --- a/docs/api/config.rst +++ b/docs/api/config.rst @@ -24,8 +24,7 @@ .. automethod:: add_route .. automethod:: add_static_view(name, path, cache_max_age=3600, permission=NO_PERMISSION_REQUIRED) .. automethod:: add_view - .. automethod:: set_forbidden_view - .. automethod:: set_notfound_view + .. automethod:: add_notfound_view :methodcategory:`Adding an Event Subscriber` diff --git a/docs/api/view.rst b/docs/api/view.rst index 9f59ddae7..cb269e48e 100644 --- a/docs/api/view.rst +++ b/docs/api/view.rst @@ -19,11 +19,11 @@ .. autoclass:: view_defaults :members: + .. autoclass:: notfound_view_config + :members: + .. autoclass:: static :members: :inherited-members: - .. autofunction:: append_slash_notfound_view(context, request) - - .. autoclass:: AppendSlashNotFoundViewFactory diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index eaccc14a3..cbc40ceee 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -19,24 +19,66 @@ found view`, which is a :term:`view callable`. A default notfound view exists. The default not found view can be overridden through application configuration. -The :term:`not found view` callable is a view callable like any other. The -:term:`view configuration` which causes it to be a "not found" view consists -only of naming the :exc:`pyramid.httpexceptions.HTTPNotFound` class as the -``context`` of the view configuration. - If your application uses :term:`imperative configuration`, you can replace -the Not Found view by using the :meth:`pyramid.config.Configurator.add_view` -method to register an "exception view": +the Not Found view by using the +:meth:`pyramid.config.Configurator.add_notfound_view` method: .. code-block:: python :linenos: - from pyramid.httpexceptions import HTTPNotFound - from helloworld.views import notfound_view - config.add_view(notfound_view, context=HTTPNotFound) + from helloworld.views import notfound + config.add_notfound_view(notfound) + +Replace ``helloworld.views.notfound`` with a reference to the :term:`view +callable` you want to use to represent the Not Found view. The :term:`not +found view` callable is a view callable like any other. + +If your application instead uses :class:`pyramid.view.view_config` decorators +and a :term:`scan`, you can replace the Not Found view by using the +:class:`pyramid.view.notfound_view_config` decorator: + +.. code-block:: python + :linenos: + + from pyramid.view import notfound_view_config + + notfound_view_config() + def notfound(request): + return Response('Not Found, dude', status='404 Not Found') + + def main(globals, **settings): + config = Configurator() + config.scan() + +This does exactly what the imperative example above showed. -Replace ``helloworld.views.notfound_view`` with a reference to the -:term:`view callable` you want to use to represent the Not Found view. +Your application can define *multiple* not found views if necessary. Both +:meth:`pyramid.config.Configurator.add_notfound_view` and +:class:`pyramid.view.notfound_view_config` take most of the same arguments as +:class:`pyramid.config.Configurator.add_view` and +:class:`pyramid.view.view_config`, respectively. This means that not found +views can carry predicates limiting their applicability. For example: + +.. code-block:: python + :linenos: + + from pyramid.view import notfound_view_config + + notfound_view_config(request_method='GET') + def notfound_get(request): + return Response('Not Found during GET, dude', status='404 Not Found') + + notfound_view_config(request_method='POST') + def notfound_post(request): + return Response('Not Found during POST, dude', status='404 Not Found') + + def main(globals, **settings): + config = Configurator() + config.scan() + +The ``notfound_get`` view will be called when a view could not be found and +the request method was ``GET``. The ``notfound_post`` view will be called +when a view could not be found and the request method was ``POST``. Like any other view, the notfound view must accept at least a ``request`` parameter, or both ``context`` and ``request``. The ``request`` is the @@ -45,6 +87,11 @@ used in the call signature) will be the instance of the :exc:`~pyramid.httpexceptions.HTTPNotFound` exception that caused the view to be called. +Both :meth:`pyramid.config.Configurator.add_notfound_view` and +:class:`pyramid.view.notfound_view_config` can be used to automatically +redirect requests to slash-appended routes. See +:ref:`redirecting_to_slash_appended_routes` for examples. + Here's some sample code that implements a minimal NotFound view callable: .. code-block:: python @@ -52,7 +99,7 @@ Here's some sample code that implements a minimal NotFound view callable: from pyramid.httpexceptions import HTTPNotFound - def notfound_view(request): + def notfound(request): return HTTPNotFound() .. note:: @@ -66,6 +113,14 @@ Here's some sample code that implements a minimal NotFound view callable: ``pyramid.debug_notfound`` environment setting is true than it is when it is false. +.. note:: + + Both :meth:`pyramid.config.Configurator.add_notfound_view` and + :class:`pyramid.view.notfound_view_config` are new as of Pyramid 1.3. + Older Pyramid documentation instructed users to use ``add_view`` instead, + with a ``context`` of ``HTTPNotFound``. This still works; the convenience + method and decorator are just wrappers around this functionality. + .. warning:: When a NotFound view callable accepts an argument list as diff --git a/docs/narr/renderers.rst b/docs/narr/renderers.rst index 1f1b1943b..76035cbdf 100644 --- a/docs/narr/renderers.rst +++ b/docs/narr/renderers.rst @@ -103,7 +103,7 @@ Likewise for an :term:`HTTP exception` response: .. code-block:: python :linenos: - from pyramid.httpexceptions import HTTPNotFound + from pyramid.httpexceptions import HTTPFound from pyramid.view import view_config @view_config(renderer='json') diff --git a/docs/narr/urldispatch.rst b/docs/narr/urldispatch.rst index a7bf74786..7c0b437c1 100644 --- a/docs/narr/urldispatch.rst +++ b/docs/narr/urldispatch.rst @@ -772,95 +772,102 @@ ignored when ``static`` is ``True``. Redirecting to Slash-Appended Routes ------------------------------------ -For behavior like Django's ``APPEND_SLASH=True``, use the -:func:`~pyramid.view.append_slash_notfound_view` view as the :term:`Not Found -view` in your application. Defining this view as the :term:`Not Found view` -is a way to automatically redirect requests where the URL lacks a trailing -slash, but requires one to match the proper route. When configured, along -with at least one other route in your application, this view will be invoked -if the value of ``PATH_INFO`` does not already end in a slash, and if the -value of ``PATH_INFO`` *plus* a slash matches any route's pattern. In this -case it does an HTTP redirect to the slash-appended ``PATH_INFO``. - -Let's use an example, because this behavior is a bit magical. If the -``append_slash_notfound_view`` is configured in your application and your -route configuration looks like so: +For behavior like Django's ``APPEND_SLASH=True``, use the ``append_slash`` +argument to :meth:`pyramid.config.Configurator.add_notfound_view` or the +equivalent ``append_slash`` argument to the +:class:`pyramid.view.notfound_view_config` decorator. + +Adding ``append_slash=True`` is a way to automatically redirect requests +where the URL lacks a trailing slash, but requires one to match the proper +route. When configured, along with at least one other route in your +application, this view will be invoked if the value of ``PATH_INFO`` does not +already end in a slash, and if the value of ``PATH_INFO`` *plus* a slash +matches any route's pattern. In this case it does an HTTP redirect to the +slash-appended ``PATH_INFO``. + +To configure the slash-appending not found view in your application, change +the application's startup configuration, adding the following stanza: .. code-block:: python :linenos: - config.add_route('noslash', 'no_slash') - config.add_route('hasslash', 'has_slash/') +Let's use an example. If the following routes are configured in your +application: + +.. code-block:: python + :linenos: + + from pyramid.httpexceptions import HTTPNotFound + + def notfound(request): + return HTTPNotFound('Not found, bro.') + + def no_slash(request): + return Response('No slash') - config.add_view('myproject.views.no_slash', route_name='noslash') - config.add_view('myproject.views.has_slash', route_name='hasslash') + def has_slash(request): + return Response('Has slash') + + def main(g, **settings): + config = Configurator() + config.add_route('noslash', 'no_slash') + config.add_route('hasslash', 'has_slash/') + config.add_view(no_slash, route_name='noslash') + config.add_view(has_slash, route_name='hasslash') + config.add_notfound_view(notfound, append_slash=True) + +If a request enters the application with the ``PATH_INFO`` value of +``/no_slash``, the first route will match and the browser will show "No +slash". However, if a request enters the application with the ``PATH_INFO`` +value of ``/no_slash/``, *no* route will match, and the slash-appending not +found view will not find a matching route with an appended slash. As a +result, the ``notfound`` view will be called and it will return a "Not found, +bro." body. If a request enters the application with the ``PATH_INFO`` value of ``/has_slash/``, the second route will match. If a request enters the application with the ``PATH_INFO`` value of ``/has_slash``, a route *will* be found by the slash-appending not found view. An HTTP redirect to -``/has_slash/`` will be returned to the user's browser. +``/has_slash/`` will be returned to the user's browser. As a result, the +``notfound`` view will never actually be called. -If a request enters the application with the ``PATH_INFO`` value of -``/no_slash``, the first route will match. However, if a request enters the -application with the ``PATH_INFO`` value of ``/no_slash/``, *no* route will -match, and the slash-appending not found view will *not* find a matching -route with an appended slash. - -.. warning:: - - You **should not** rely on this mechanism to redirect ``POST`` requests. - The redirect of the slash-appending not found view will turn a ``POST`` - request into a ``GET``, losing any ``POST`` data in the original - request. - -To configure the slash-appending not found view in your application, change -the application's startup configuration, adding the following stanza: +The following application uses the :class:`pyramid.view.notfound_view_config` +and :class:`pyramid.view.view_config` decorators and a :term:`scan` to do +exactly the same job: .. code-block:: python :linenos: - config.add_view('pyramid.view.append_slash_notfound_view', - context='pyramid.httpexceptions.HTTPNotFound') - -See :ref:`view_module` and :ref:`changing_the_notfound_view` for more -information about the slash-appending not found view and for a more general -description of how to configure a not found view. + from pyramid.httpexceptions import HTTPNotFound + from pyramid.view import notfound_view_config, view_config -Custom Not Found View With Slash Appended Routes -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + @notfound_view_config(append_slash=True) + def notfound(request): + return HTTPNotFound('Not found, bro.') -There can only be one :term:`Not Found view` in any :app:`Pyramid` -application. Even if you use :func:`~pyramid.view.append_slash_notfound_view` -as the Not Found view, :app:`Pyramid` still must generate a ``404 Not Found`` -response when it cannot redirect to a slash-appended URL; this not found -response will be visible to site users. + @view_config(route_name='noslash') + def no_slash(request): + return Response('No slash') -If you don't care what this 404 response looks like, and only you need -redirections to slash-appended route URLs, you may use the -:func:`~pyramid.view.append_slash_notfound_view` object as the Not Found view -as described above. However, if you wish to use a *custom* notfound view -callable when a URL cannot be redirected to a slash-appended URL, you may -wish to use an instance of the -:class:`~pyramid.view.AppendSlashNotFoundViewFactory` class as the Not Found -view, supplying a :term:`view callable` to be used as the custom notfound -view as the first argument to its constructor. For instance: + @view_config(route_name='hasslash') + def has_slash(request): + return Response('Has slash') -.. code-block:: python - :linenos: - - from pyramid.httpexceptions import HTTPNotFound - from pyramid.view import AppendSlashNotFoundViewFactory + def main(g, **settings): + config = Configurator() + config.add_route('noslash', 'no_slash') + config.add_route('hasslash', 'has_slash/') + config.scan() - def notfound_view(context, request): - return HTTPNotFound('It aint there, stop trying!') +.. warning:: - custom_append_slash = AppendSlashNotFoundViewFactory(notfound_view) - config.add_view(custom_append_slash, context=HTTPNotFound) + You **should not** rely on this mechanism to redirect ``POST`` requests. + The redirect of the slash-appending not found view will turn a ``POST`` + request into a ``GET``, losing any ``POST`` data in the original + request. -The ``notfound_view`` supplied must adhere to the two-argument view callable -calling convention of ``(context, request)`` (``context`` will be the -exception object). +See :ref:`view_module` and :ref:`changing_the_notfound_view` for for a more +general description of how to configure a view and/or a not found view. .. index:: pair: debugging; route matching diff --git a/pyramid/config/util.py b/pyramid/config/util.py index 4c7ecd359..b8d0f2319 100644 --- a/pyramid/config/util.py +++ b/pyramid/config/util.py @@ -204,7 +204,8 @@ def make_predicates(xhr=None, request_method=None, path_info=None, if containment is not None: def containment_predicate(context, request): - return find_interface(context, containment) is not None + ctx = getattr(request, 'context', context) + return find_interface(ctx, containment) is not None containment_predicate.__text__ = "containment = %s" % containment weights.append(1 << 7) predicates.append(containment_predicate) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 9d2e15537..7f6a37cc4 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -54,7 +54,12 @@ from pyramid.httpexceptions import ( from pyramid.security import NO_PERMISSION_REQUIRED from pyramid.static import static_view from pyramid.threadlocal import get_current_registry -from pyramid.view import render_view_to_response + +from pyramid.view import ( + render_view_to_response, + AppendSlashNotFoundViewFactory, + ) + from pyramid.util import object_description from pyramid.config.util import ( @@ -1353,10 +1358,6 @@ class ViewsConfiguratorMixin(object): The ``wrapper`` argument should be the name of another view which will wrap this view when rendered (see the ``add_view`` method's ``wrapper`` argument for a description).""" - if isinstance(renderer, string_types): - renderer = renderers.RendererHelper( - name=renderer, package=self.package, - registry = self.registry) view = self._derive_view(view, attr=attr, renderer=renderer) def bwcompat_view(context, request): context = getattr(request, 'context', None) @@ -1365,46 +1366,66 @@ class ViewsConfiguratorMixin(object): wrapper=wrapper, renderer=renderer) @action_method - def set_notfound_view(self, view=None, attr=None, renderer=None, - wrapper=None): - """ Add a default not found view to the current configuration - state. - - .. warning:: - - This method has been deprecated in :app:`Pyramid` 1.0. *Do not use - it for new development; it should only be used to support older code - bases which depend upon it.* See :ref:`changing_the_notfound_view` to - see how a not found view should be registered in new projects. - - The ``view`` argument should be a :term:`view callable` or a - :term:`dotted Python name` which refers to a view callable. - - The ``attr`` argument should be the attribute of the view - callable used to retrieve the response (see the ``add_view`` - method's ``attr`` argument for a description). + def add_notfound_view( + self, view=None, attr=None, renderer=None, wrapper=None, + route_name=None, request_type=None, request_method=None, + request_param=None, containment=None, xhr=None, accept=None, + header=None, path_info=None, custom_predicates=(), decorator=None, + mapper=None, match_param=None, append_slash=False): + """ Add a default notfound view to the current configuration state. + The view will be called when a view cannot otherwise be found for the + set of circumstances implied by the predicates provided. The + simplest example is: + + .. code-block:: python + + config.add_notfound_view(someview) + + All arguments except ``append_slash`` have the same meaning as + :meth:`pyramid.config.Configurator.add_view` and each predicate + argument restricts the set of circumstances under which this notfound + view will be invoked. + + If ``append_slash`` is ``True``, when this notfound view is invoked, + and the current path info does not end in a slash, the notfound logic + will attempt to find a :term:`route` that matches the request's path + info suffixed with a slash. If such a route exists, Pyramid will + issue a redirect to the URL implied by the route; if it does not, + Pyramid will return the result of the view callable provided as + ``view``, as normal. - The ``renderer`` argument should be the name of (or path to) a - :term:`renderer` used to generate a response for this view - (see the - :meth:`pyramid.config.Configurator.add_view` - method's ``renderer`` argument for information about how a - configurator relates to a renderer). + .. note:: - The ``wrapper`` argument should be the name of another view - which will wrap this view when rendered (see the ``add_view`` - method's ``wrapper`` argument for a description). + This method is new as of Pyramid 1.3. """ - if isinstance(renderer, string_types): - renderer = renderers.RendererHelper( - name=renderer, package=self.package, - registry=self.registry) - view = self._derive_view(view, attr=attr, renderer=renderer) - def bwcompat_view(context, request): - context = getattr(request, 'context', None) - return view(context, request) - return self.add_view(bwcompat_view, context=HTTPNotFound, - wrapper=wrapper, renderer=renderer) + settings = dict( + view=view, + context=HTTPNotFound, + wrapper=wrapper, + request_type=request_type, + request_method=request_method, + request_param=request_param, + containment=containment, + xhr=xhr, + accept=accept, + header=header, + path_info=path_info, + custom_predicates=custom_predicates, + decorator=decorator, + mapper=mapper, + match_param=match_param, + route_name=route_name + ) + if append_slash: + view = self._derive_view(view, attr=attr, renderer=renderer) + view = AppendSlashNotFoundViewFactory(view) + settings['view'] = view + else: + settings['attr'] = attr + settings['renderer'] = renderer + return self.add_view(**settings) + + set_notfound_view = add_notfound_view # deprecated sorta-bw-compat alias @action_method def set_view_mapper(self, mapper): diff --git a/pyramid/tests/pkgs/notfoundview/__init__.py b/pyramid/tests/pkgs/notfoundview/__init__.py new file mode 100644 index 000000000..ae148ea8c --- /dev/null +++ b/pyramid/tests/pkgs/notfoundview/__init__.py @@ -0,0 +1,30 @@ +from pyramid.view import notfound_view_config, view_config +from pyramid.response import Response + +@notfound_view_config(route_name='foo', append_slash=True) +def foo_notfound(request): # pragma: no cover + return Response('foo_notfound') + +@notfound_view_config(route_name='baz') +def baz_notfound(request): + return Response('baz_notfound') + +@notfound_view_config(append_slash=True) +def notfound(request): + return Response('generic_notfound') + +@view_config(route_name='bar') +def bar(request): + return Response('OK bar') + +@view_config(route_name='foo2') +def foo2(request): + return Response('OK foo2') + +def includeme(config): + config.add_route('foo', '/foo') + config.add_route('foo2', '/foo/') + config.add_route('bar', '/bar/') + config.add_route('baz', '/baz') + config.scan('pyramid.tests.pkgs.notfoundview') + diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index eb18d5c84..668fd7185 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -1682,14 +1682,14 @@ class TestViewsConfigurationMixin(unittest.TestCase): - def test_set_notfound_view(self): + def test_add_notfound_view(self): from pyramid.renderers import null_renderer from zope.interface import implementedBy from pyramid.interfaces import IRequest from pyramid.httpexceptions import HTTPNotFound config = self._makeOne(autocommit=True) view = lambda *arg: arg - config.set_notfound_view(view, renderer=null_renderer) + config.add_notfound_view(view, renderer=null_renderer) request = self._makeRequest(config) view = self._getViewCallable(config, ctx_iface=implementedBy(HTTPNotFound), @@ -1697,30 +1697,33 @@ class TestViewsConfigurationMixin(unittest.TestCase): result = view(None, request) self.assertEqual(result, (None, request)) - def test_set_notfound_view_request_has_context(self): + def test_add_notfound_view_append_slash(self): + from pyramid.response import Response from pyramid.renderers import null_renderer from zope.interface import implementedBy from pyramid.interfaces import IRequest from pyramid.httpexceptions import HTTPNotFound config = self._makeOne(autocommit=True) - view = lambda *arg: arg - config.set_notfound_view(view, renderer=null_renderer) + config.add_route('foo', '/foo/') + def view(request): return Response('OK') + config.add_notfound_view(view, renderer=null_renderer,append_slash=True) request = self._makeRequest(config) - request.context = 'abc' + request.environ['PATH_INFO'] = '/foo' + request.query_string = 'a=1&b=2' + request.path = '/scriptname/foo' view = self._getViewCallable(config, ctx_iface=implementedBy(HTTPNotFound), request_iface=IRequest) result = view(None, request) - self.assertEqual(result, ('abc', request)) - - @testing.skip_on('java') - def test_set_notfound_view_with_renderer(self): + self.assertEqual(result.location, '/scriptname/foo/?a=1&b=2') + + def test_add_notfound_view_with_renderer(self): from zope.interface import implementedBy from pyramid.interfaces import IRequest from pyramid.httpexceptions import HTTPNotFound config = self._makeOne(autocommit=True) view = lambda *arg: {} - config.set_notfound_view( + config.add_notfound_view( view, renderer='pyramid.tests.test_config:files/minimal.pt') config.begin() @@ -1734,8 +1737,7 @@ class TestViewsConfigurationMixin(unittest.TestCase): config.end() self.assertTrue(b'div' in result.body) - @testing.skip_on('java') - def test_set_forbidden_view_with_renderer(self): + def test_add_forbidden_view_with_renderer(self): from zope.interface import implementedBy from pyramid.interfaces import IRequest from pyramid.httpexceptions import HTTPForbidden diff --git a/pyramid/tests/test_integration.py b/pyramid/tests/test_integration.py index 86cd73910..57b7e40b2 100644 --- a/pyramid/tests/test_integration.py +++ b/pyramid/tests/test_integration.py @@ -357,6 +357,23 @@ class TestViewDecoratorApp(IntegrationBase, unittest.TestCase): res = self.testapp.get('/second', status=200) self.assertTrue(b'OK2' in res.body) +class TestNotFoundView(IntegrationBase, unittest.TestCase): + package = 'pyramid.tests.pkgs.notfoundview' + + def test_it(self): + res = self.testapp.get('/wontbefound', status=200) + self.assertTrue(b'generic_notfound' in res.body) + res = self.testapp.get('/bar', status=302) + self.assertEqual(res.location, 'http://localhost/bar/') + res = self.testapp.get('/bar/', status=200) + self.assertTrue(b'OK bar' in res.body) + res = self.testapp.get('/foo', status=302) + self.assertEqual(res.location, 'http://localhost/foo/') + res = self.testapp.get('/foo/', status=200) + self.assertTrue(b'OK foo2' in res.body) + res = self.testapp.get('/baz', status=200) + self.assertTrue(b'baz_notfound' in res.body) + class TestViewPermissionBug(IntegrationBase, unittest.TestCase): # view_execution_permitted bug as reported by Shane at http://lists.repoze.org/pipermail/repoze-dev/2010-October/003603.html package = 'pyramid.tests.pkgs.permbugapp' diff --git a/pyramid/tests/test_view.py b/pyramid/tests/test_view.py index 03a111828..f092f281b 100644 --- a/pyramid/tests/test_view.py +++ b/pyramid/tests/test_view.py @@ -48,7 +48,51 @@ class BaseTest(object): context = DummyContext() directlyProvides(context, IContext) return context - + +class Test_notfound_view_config(BaseTest, unittest.TestCase): + def _makeOne(self, **kw): + from pyramid.view import notfound_view_config + return notfound_view_config(**kw) + + def test_ctor(self): + inst = self._makeOne(attr='attr', path_info='path_info', + append_slash=True) + self.assertEqual(inst.__dict__, + {'attr':'attr', 'path_info':'path_info', + 'append_slash':True}) + + def test_it_function(self): + def view(request): pass + decorator = self._makeOne(attr='attr', renderer='renderer', + append_slash=True) + venusian = DummyVenusian() + decorator.venusian = venusian + wrapped = decorator(view) + self.assertTrue(wrapped is view) + config = call_venusian(venusian) + settings = config.settings + self.assertEqual( + settings, + [{'attr': 'attr', 'venusian': venusian, 'append_slash': True, + 'renderer': 'renderer', '_info': 'codeinfo', 'view': None}] + ) + + def test_it_class(self): + decorator = self._makeOne() + venusian = DummyVenusian() + decorator.venusian = venusian + decorator.venusian.info.scope = 'class' + class view(object): pass + wrapped = decorator(view) + self.assertTrue(wrapped is view) + config = call_venusian(venusian) + settings = config.settings + self.assertEqual(len(settings), 1) + self.assertEqual(len(settings[0]), 5) + self.assertEqual(settings[0]['venusian'], venusian) + self.assertEqual(settings[0]['view'], None) # comes from call_venusian + self.assertEqual(settings[0]['attr'], 'view') + self.assertEqual(settings[0]['_info'], 'codeinfo') class RenderViewToResponseTests(BaseTest, unittest.TestCase): def _callFUT(self, *arg, **kw): @@ -672,6 +716,8 @@ class DummyConfig(object): def add_view(self, **kw): self.settings.append(kw) + add_notfound_view = add_view + def with_package(self, pkg): self.pkg = pkg return self diff --git a/pyramid/view.py b/pyramid/view.py index a68f9ad8a..9f049bf09 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -9,11 +9,16 @@ from pyramid.interfaces import ( IViewClassifier, ) -from pyramid.compat import map_ +from pyramid.compat import ( + map_, + decode_path_info, + ) + from pyramid.httpexceptions import ( HTTPFound, default_exceptionresponse_view, ) + from pyramid.path import caller_package from pyramid.static import static_view from pyramid.threadlocal import get_current_registry @@ -274,11 +279,7 @@ class AppendSlashNotFoundViewFactory(object): self.notfound_view = notfound_view def __call__(self, context, request): - if not isinstance(context, Exception): - # backwards compat for an append_notslash_view registered via - # config.set_notfound_view instead of as a proper exception view - context = getattr(request, 'exception', None) or context - path = request.path + path = decode_path_info(request.environ['PATH_INFO'] or '/') registry = request.registry mapper = registry.queryUtility(IRoutesMapper) if mapper is not None and not path.endswith('/'): @@ -287,8 +288,8 @@ class AppendSlashNotFoundViewFactory(object): if route.match(slashpath) is not None: qs = request.query_string if qs: - slashpath += '?' + qs - return HTTPFound(location=slashpath) + qs = '?' + qs + return HTTPFound(location=request.path+'/'+qs) return self.notfound_view(context, request) append_slash_notfound_view = AppendSlashNotFoundViewFactory() @@ -316,6 +317,80 @@ See also :ref:`changing_the_notfound_view`. """ +class notfound_view_config(object): + """ + + An analogue of :class:`pyramid.view.view_config` which registers a + :term:`not found view`. + + The notfound_view_config constructor accepts most of the same arguments + as the constructor of :class:`pyramid.view.view_config`. It can be used + in the same places, and behaves in largely the same way, except it always + registers a not found exception view instead of a "normal" view. + + Example: + + .. code-block:: python + + from pyramid.view import notfound_view_config + from pyramid.response import Response + + notfound_view_config() + def notfound(request): + return Response('Not found, dude!', status='404 Not Found') + + All arguments except ``append_slash`` have the same meaning as + :meth:`pyramid.view.view_config` and each predicate + argument restricts the set of circumstances under which this notfound + view will be invoked. + + If ``append_slash`` is ``True``, when the notfound view is invoked, and + the current path info does not end in a slash, the notfound logic will + attempt to find a :term:`route` that matches the request's path info + suffixed with a slash. If such a route exists, Pyramid will issue a + redirect to the URL implied by the route; if it does not, Pyramid will + return the result of the view callable provided as ``view``, as normal. + + See :ref:`changing_the_notfound_view` for detailed usage information. + + .. note:: + + This class is new as of Pyramid 1.3. + """ + + venusian = venusian + + def __init__(self, request_type=default, request_method=default, + route_name=default, request_param=default, attr=default, + renderer=default, containment=default, wrapper=default, + xhr=default, accept=default, header=default, + path_info=default, custom_predicates=default, + decorator=default, mapper=default, match_param=default, + append_slash=False): + L = locals() + for k, v in L.items(): + if k not in ('self', 'L') and v is not default: + self.__dict__[k] = v + + def __call__(self, wrapped): + settings = self.__dict__.copy() + + def callback(context, name, ob): + config = context.config.with_package(info.module) + config.add_notfound_view(view=ob, **settings) + + info = self.venusian.attach(wrapped, callback, category='pyramid') + + if info.scope == 'class': + # if the decorator was attached to a method in a class, or + # otherwise executed at class scope, we need to set an + # 'attr' into the settings if one isn't already in there + if settings.get('attr') is None: + settings['attr'] = wrapped.__name__ + + settings['_info'] = info.codeinfo # fbo "action_method" + return wrapped + def is_response(ob): """ Return ``True`` if ``ob`` implements the interface implied by :ref:`the_response`. ``False`` if not. -- cgit v1.2.3 From 59fa4a30da200b2302545be024d72ee38f8a35d9 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 22 Feb 2012 15:59:57 -0500 Subject: whoops --- TODO.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/TODO.txt b/TODO.txt index 20a677bea..56ddc384c 100644 --- a/TODO.txt +++ b/TODO.txt @@ -7,6 +7,8 @@ Must-Have - Fix scaffolds and tutorials to use notfound_view_config rather than view_config. +- Add __no_permission_required__ to see notfound views. + Nice-to-Have ------------ -- cgit v1.2.3 From 90a4588329fe1fc7a9a1d6f1ae32544499bff0cf Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 22 Feb 2012 17:20:36 -0500 Subject: register notfound views with __no_permission_required__ --- TODO.txt | 2 -- pyramid/config/views.py | 3 ++- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/TODO.txt b/TODO.txt index 56ddc384c..20a677bea 100644 --- a/TODO.txt +++ b/TODO.txt @@ -7,8 +7,6 @@ Must-Have - Fix scaffolds and tutorials to use notfound_view_config rather than view_config. -- Add __no_permission_required__ to see notfound views. - Nice-to-Have ------------ diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 7f6a37cc4..9bd7b4b50 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1414,7 +1414,8 @@ class ViewsConfiguratorMixin(object): decorator=decorator, mapper=mapper, match_param=match_param, - route_name=route_name + route_name=route_name, + permission=NO_PERMISSION_REQUIRED, ) if append_slash: view = self._derive_view(view, attr=attr, renderer=renderer) -- cgit v1.2.3 From b20214d2ad88bfa2864bf39efbef5a5d5b15d872 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 22 Feb 2012 17:23:44 -0500 Subject: not required --- TODO.txt | 6 ------ 1 file changed, 6 deletions(-) diff --git a/TODO.txt b/TODO.txt index 20a677bea..db1daa2c4 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,12 +1,6 @@ Pyramid TODOs ============= -Must-Have ---------- - -- Fix scaffolds and tutorials to use notfound_view_config rather than - view_config. - Nice-to-Have ------------ -- cgit v1.2.3 From 2d045891789c58856831dc676d06c0b86fdd84c5 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 22 Feb 2012 18:04:23 -0500 Subject: maintain whatsnew --- docs/whatsnew-1.3.rst | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/docs/whatsnew-1.3.rst b/docs/whatsnew-1.3.rst index 0a8205d61..7f6c3d7cb 100644 --- a/docs/whatsnew-1.3.rst +++ b/docs/whatsnew-1.3.rst @@ -211,6 +211,22 @@ added, as well, but the configurator method should be preferred as it provides conflict detection and consistency in the lifetime of the properties. +Not Found View Helpers +~~~~~~~~~~~~~~~~~~~~~~ + +- New API: :meth:`pyramid.config.Configurator.add_notfound_view`. This is a + wrapper for :meth:`pyramid.Config.configurator.add_view` which provides + support for an "append_slash" feature as well as doing the right thing when + it comes to permissions (a not found view should always be public). It + should be preferred over calling ``add_view`` directly with + ``context=HTTPNotFound`` as was previously recommended. + +- New API: :class:`pyramid.view.notfound_view_config``. This is a decorator + constructor like :class:`pyramid.view.view_config` that calls + :meth:`pyramid.config.Configurator.add_notfound_view` when scanned. It + should be preferred over using ``pyramid.view.view_config`` with + ``context=HTTPNotFound`` as was previously recommended. + Minor Feature Additions ----------------------- @@ -408,6 +424,35 @@ Backwards Incompatibilities ``pyramid.interfaces.IContextURL`` adapter is found when :meth:`pyramid.request.Request.resource_url` is called. +- Remove ``pyramid.config.Configurator.with_context`` class method. It was + never an API, it is only used by ``pyramid_zcml`` and its functionality has + been moved to that package's latest release. This means that you'll need + to use the 0.9.2 or later release of ``pyramid_zcml`` with this release of + Pyramid. + +- The older deprecated ``set_notfound_view`` Configurator method is now an + alias for the new :meth:`pyramid.config.Configurator.add_notfound_view` + method. This has the following impact: the ``context`` sent to views with + a ``(context, request)`` call signature registered via the deprecated + ``add_notfound_view`` / ``set_notfound_view`` will now be the HTTPNotFound + exception object instead of the actual resource context found. Use + ``request.context`` to get the actual resource context. It's also + recommended to disuse ``set_notfound_view`` in favor of + ``add_notfound_view``, despite the aliasing. + +Deprecations +------------ + +- The API documentation for ``pyramid.view.append_slash_notfound_view`` and + ``pyramid.view.AppendSlashNotFoundViewFactory`` was removed. These names + still exist and are still importable, but they are no longer APIs. Use + ``pyramid.config.Configurator.add_notfound_view(append_slash=True)`` or + ``pyramid.view.notfound_view_config(append_slash=True)`` to get the same + behavior. + +- The ``set_forbidden_view`` method of the Configurator was removed from the + documentation. It has been deprecated since Pyramid 1.1. + Documentation Enhancements -------------------------- @@ -440,6 +485,14 @@ Documentation Enhancements Rationale: it provides the correct info for the Python 2.5 version of GAE only, and this version of Pyramid does not support Python 2.5. +- Updated the :ref:`changing_the_notfound_view` section, replacing + explanations of registering a view using ``add_view`` or ``view_config`` + with ones using ``add_notfound_view`` or ``notfound_view_config``. + +- Updated the :ref:`redirecting_to_slash_appended_routes` section, replacing + explanations of registering a view using ``add_view`` or ``view_config`` + with ones using ``add_notfound_view`` or ``notfound_view_config`` + Dependency Changes ------------------ -- cgit v1.2.3 From a7fe30f0eabd6c6fd3bcc910faa41720a75056de Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 22 Feb 2012 19:24:09 -0500 Subject: - New API: ``pyramid.config.Configurator.add_forbidden_view``. This is a wrapper for ``pyramid.Config.configurator.add_view`` which does the right thing about permissions. It should be preferred over calling ``add_view`` directly with ``context=HTTPForbidden`` as was previously recommended. - New API: ``pyramid.view.forbidden_view_config``. This is a decorator constructor like ``pyramid.view.view_config`` that calls ``pyramid.config.Configurator.add_forbidden_view`` when scanned. It should be preferred over using ``pyramid.view.view_config`` with ``context=HTTPForbidden`` as was previously recommended. - Updated the "Creating a Not Forbidden View" section of the "Hooks" chapter, replacing explanations of registering a view using ``add_view`` or ``view_config`` with ones using ``add_forbidden_view`` or ``forbidden_view_config``. - Updated all tutorials to use ``pyramid.view.forbidden_view_config`` rather than ``pyramid.view.view_config`` with an HTTPForbidden context. --- CHANGES.txt | 45 ++++++++--- TODO.txt | 2 - docs/api/config.rst | 1 + docs/api/view.rst | 3 + docs/narr/hooks.rst | 29 +++++-- docs/tutorials/wiki/authorization.rst | 28 +++---- .../wiki/src/authorization/tutorial/views.py | 8 +- docs/tutorials/wiki/src/tests/tutorial/views.py | 8 +- docs/tutorials/wiki2/authorization.rst | 26 +++--- .../wiki2/src/authorization/tutorial/views.py | 8 +- docs/tutorials/wiki2/src/tests/tutorial/views.py | 8 +- docs/whatsnew-1.3.rst | 46 ++++++++--- pyramid/config/views.py | 94 +++++++++++++--------- pyramid/tests/pkgs/forbiddenview/__init__.py | 31 +++++++ pyramid/tests/test_config/test_views.py | 24 +----- pyramid/tests/test_integration.py | 9 +++ pyramid/tests/test_view.py | 44 +++++++++- pyramid/view.py | 65 +++++++++++++++ 18 files changed, 353 insertions(+), 126 deletions(-) create mode 100644 pyramid/tests/pkgs/forbiddenview/__init__.py diff --git a/CHANGES.txt b/CHANGES.txt index efeba0447..39bf59210 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -15,8 +15,9 @@ Features - New API: ``pyramid.config.Configurator.add_notfound_view``. This is a wrapper for ``pyramid.Config.configurator.add_view`` which provides easy - append_slash support. It should be preferred over calling ``add_view`` - directly with ``context=HTTPNotFound`` as was previously recommended. + append_slash support and does the right thing about permissions. It should + be preferred over calling ``add_view`` directly with + ``context=HTTPNotFound`` as was previously recommended. - New API: ``pyramid.view.notfound_view_config``. This is a decorator constructor like ``pyramid.view.view_config`` that calls @@ -24,6 +25,17 @@ Features be preferred over using ``pyramid.view.view_config`` with ``context=HTTPNotFound`` as was previously recommended. +- New API: ``pyramid.config.Configurator.add_forbidden_view``. This is a + wrapper for ``pyramid.Config.configurator.add_view`` which does the right + thing about permissions. It should be preferred over calling ``add_view`` + directly with ``context=HTTPForbidden`` as was previously recommended. + +- New API: ``pyramid.view.forbidden_view_config``. This is a decorator + constructor like ``pyramid.view.view_config`` that calls + ``pyramid.config.Configurator.add_forbidden_view`` when scanned. It should + be preferred over using ``pyramid.view.view_config`` with + ``context=HTTPForbidden`` as was previously recommended. + Backwards Incompatibilities --------------------------- @@ -40,14 +52,16 @@ Backwards Incompatibilities - The ``pyramid.registry.noop_introspector`` API object has been removed. - The older deprecated ``set_notfound_view`` Configurator method is now an - alias for the new ``add_notfound_view`` Configurator method. This has the - following impact: the ``context`` sent to views with a ``(context, - request)`` call signature registered via the deprecated - ``add_notfound_view``/``set_notfound_view`` will now be the HTTPNotFound - exception object instead of the actual resource context found. Use + alias for the new ``add_notfound_view`` Configurator method. Likewise, the + older deprecated ``set_forbidden_view`` is now an alias for the new + ``add_forbidden_view``. This has the following impact: the ``context`` sent + to views with a ``(context, request)`` call signature registered via the + ``set_notfound_view`` or ``set_forbidden_view`` will now be an exception + object instead of the actual resource context found. Use ``request.context`` to get the actual resource context. It's also recommended to disuse ``set_notfound_view`` in favor of - ``add_notfound_view``, despite the aliasing. + ``add_notfound_view``, and disuse ``set_forbidden_view`` in favor of + ``add_forbidden_view`` despite the aliasing. Deprecations ------------ @@ -59,8 +73,9 @@ Deprecations ``pyramid.view.notfound_view_config(append_slash=True)`` to get the same behavior. -- The ``set_forbidden_view`` method of the Configurator was removed from the - documentation. It has been deprecated since Pyramid 1.1. +- The ``set_forbidden_view`` and ``set_notfound_view`` methods of the + Configurator were removed from the documentation. They have been + deprecated since Pyramid 1.1. Bug Fixes --------- @@ -76,16 +91,24 @@ Bug Fixes Documentation ------------- -- Updated the "Registering a Not Found View" section of the "Hooks" chapter, +- Updated the "Creating a Not Found View" section of the "Hooks" chapter, replacing explanations of registering a view using ``add_view`` or ``view_config`` with ones using ``add_notfound_view`` or ``notfound_view_config``. +- Updated the "Creating a Not Forbidden View" section of the "Hooks" chapter, + replacing explanations of registering a view using ``add_view`` or + ``view_config`` with ones using ``add_forbidden_view`` or + ``forbidden_view_config``. + - Updated the "Redirecting to Slash-Appended Routes" section of the "URL Dispatch" chapter, replacing explanations of registering a view using ``add_view`` or ``view_config`` with ones using ``add_notfound_view`` or ``notfound_view_config`` +- Updated all tutorials to use ``pyramid.view.forbidden_view_config`` rather + than ``pyramid.view.view_config`` with an HTTPForbidden context. + 1.3a8 (2012-02-19) ================== diff --git a/TODO.txt b/TODO.txt index db1daa2c4..5d96f7c7d 100644 --- a/TODO.txt +++ b/TODO.txt @@ -4,8 +4,6 @@ Pyramid TODOs Nice-to-Have ------------ -- Add forbidden_view_config? - - Add docs about upgrading between Pyramid versions (e.g. how to see deprecation warnings). diff --git a/docs/api/config.rst b/docs/api/config.rst index bf5fdbb7c..cd58e74d3 100644 --- a/docs/api/config.rst +++ b/docs/api/config.rst @@ -25,6 +25,7 @@ .. automethod:: add_static_view(name, path, cache_max_age=3600, permission=NO_PERMISSION_REQUIRED) .. automethod:: add_view .. automethod:: add_notfound_view + .. automethod:: add_forbidden_view :methodcategory:`Adding an Event Subscriber` diff --git a/docs/api/view.rst b/docs/api/view.rst index cb269e48e..21d2bb90d 100644 --- a/docs/api/view.rst +++ b/docs/api/view.rst @@ -22,6 +22,9 @@ .. autoclass:: notfound_view_config :members: + .. autoclass:: forbidden_view_config + :members: + .. autoclass:: static :members: :inherited-members: diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index cbc40ceee..b7f052b00 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -145,23 +145,40 @@ the view which generates it can be overridden as necessary. The :term:`forbidden view` callable is a view callable like any other. The :term:`view configuration` which causes it to be a "forbidden" view consists -only of naming the :exc:`pyramid.httpexceptions.HTTPForbidden` class as the -``context`` of the view configuration. +of using the meth:`pyramid.config.Configurator.add_forbidden_view` API or the +:class:`pyramid.view.forbidden_view_config` decorator. -You can replace the forbidden view by using the -:meth:`pyramid.config.Configurator.add_view` method to register an "exception -view": +For example, you can add a forbidden view by using the +:meth:`pyramid.config.Configurator.add_forbidden_view` method to register a +forbidden view: .. code-block:: python :linenos: from helloworld.views import forbidden_view from pyramid.httpexceptions import HTTPForbidden - config.add_view(forbidden_view, context=HTTPForbidden) + config.add_forbidden_view(forbidden_view) Replace ``helloworld.views.forbidden_view`` with a reference to the Python :term:`view callable` you want to use to represent the Forbidden view. +If instead you prefer to use decorators and a :term:`scan`, you can use the +:class:`pyramid.view.forbidden_view_config` decorator to mark a view callable +as a forbidden view: + +.. code-block:: python + :linenos: + + from pyramid.view import forbidden_view_config + + forbidden_view_config() + def forbidden(request): + return Response('forbidden') + + def main(globals, **settings): + config = Configurator() + config.scan() + Like any other view, the forbidden view must accept at least a ``request`` parameter, or both ``context`` and ``request``. The ``context`` (available as ``request.context`` if you're using the request-only view argument diff --git a/docs/tutorials/wiki/authorization.rst b/docs/tutorials/wiki/authorization.rst index 8f583ece7..c1be2cc72 100644 --- a/docs/tutorials/wiki/authorization.rst +++ b/docs/tutorials/wiki/authorization.rst @@ -132,14 +132,14 @@ We'll add these views to the existing ``views.py`` file we have in our project. Here's what the ``login`` view callable will look like: .. literalinclude:: src/authorization/tutorial/views.py - :lines: 83-111 + :lines: 86-113 :linenos: :language: python Here's what the ``logout`` view callable will look like: .. literalinclude:: src/authorization/tutorial/views.py - :lines: 113-117 + :lines: 115-119 :linenos: :language: python @@ -149,18 +149,18 @@ different :term:`view configuration` for the ``login`` view callable. The first view configuration decorator configures the ``login`` view callable so it will be invoked when someone visits ``/login`` (when the context is a -Wiki and the view name is ``login``). The second decorator (with context of -``pyramid.httpexceptions.HTTPForbidden``) specifies a :term:`forbidden view`. -This configures our login view to be presented to the user when -:app:`Pyramid` detects that a view invocation can not be authorized. Because -we've configured a forbidden view, the ``login`` view callable will be -invoked whenever one of our users tries to execute a view callable that they -are not allowed to invoke as determined by the :term:`authorization policy` -in use. In our application, for example, this means that if a user has not -logged in, and he tries to add or edit a Wiki page, he will be shown the -login form. Before being allowed to continue on to the add or edit form, he -will have to provide credentials that give him permission to add or edit via -this login form. +Wiki and the view name is ``login``). The second decorator, named +``forbidden_view_config`` specifies a :term:`forbidden view`. This +configures our login view to be presented to the user when :app:`Pyramid` +detects that a view invocation can not be authorized. Because we've +configured a forbidden view, the ``login`` view callable will be invoked +whenever one of our users tries to execute a view callable that they are not +allowed to invoke as determined by the :term:`authorization policy` in use. +In our application, for example, this means that if a user has not logged in, +and he tries to add or edit a Wiki page, he will be shown the login form. +Before being allowed to continue on to the add or edit form, he will have to +provide credentials that give him permission to add or edit via this login +form. Note that we're relying on some additional imports within the bodies of these views (e.g. ``remember`` and ``forget``). We'll see a rendering of the diff --git a/docs/tutorials/wiki/src/authorization/tutorial/views.py b/docs/tutorials/wiki/src/authorization/tutorial/views.py index 2f0502c17..fcbe6fe25 100644 --- a/docs/tutorials/wiki/src/authorization/tutorial/views.py +++ b/docs/tutorials/wiki/src/authorization/tutorial/views.py @@ -3,7 +3,10 @@ import re from pyramid.httpexceptions import HTTPFound -from pyramid.view import view_config +from pyramid.view import ( + view_config, + forbidden_view_config, + ) from pyramid.security import ( authenticated_userid, @@ -82,8 +85,7 @@ def edit_page(context, request): @view_config(context='.models.Wiki', name='login', renderer='templates/login.pt') -@view_config(context='pyramid.httpexceptions.HTTPForbidden', - renderer='templates/login.pt') +@forbidden_view_config(renderer='templates/login.pt') def login(request): login_url = request.resource_url(request.context, 'login') referrer = request.url diff --git a/docs/tutorials/wiki/src/tests/tutorial/views.py b/docs/tutorials/wiki/src/tests/tutorial/views.py index 2f0502c17..fcbe6fe25 100644 --- a/docs/tutorials/wiki/src/tests/tutorial/views.py +++ b/docs/tutorials/wiki/src/tests/tutorial/views.py @@ -3,7 +3,10 @@ import re from pyramid.httpexceptions import HTTPFound -from pyramid.view import view_config +from pyramid.view import ( + view_config, + forbidden_view_config, + ) from pyramid.security import ( authenticated_userid, @@ -82,8 +85,7 @@ def edit_page(context, request): @view_config(context='.models.Wiki', name='login', renderer='templates/login.pt') -@view_config(context='pyramid.httpexceptions.HTTPForbidden', - renderer='templates/login.pt') +@forbidden_view_config(renderer='templates/login.pt') def login(request): login_url = request.resource_url(request.context, 'login') referrer = request.url diff --git a/docs/tutorials/wiki2/authorization.rst b/docs/tutorials/wiki2/authorization.rst index b1d0bf37c..900bf0975 100644 --- a/docs/tutorials/wiki2/authorization.rst +++ b/docs/tutorials/wiki2/authorization.rst @@ -159,33 +159,35 @@ logged in user and redirect back to the front page. The ``login`` view callable will look something like this: .. literalinclude:: src/authorization/tutorial/views.py - :lines: 87-113 + :lines: 89-115 :linenos: :language: python The ``logout`` view callable will look something like this: .. literalinclude:: src/authorization/tutorial/views.py - :lines: 115-119 + :lines: 117-121 :linenos: :language: python -The ``login`` view callable is decorated with two ``@view_config`` -decorators, one which associates it with the ``login`` route, the other which -associates it with the ``HTTPForbidden`` context. The one which associates -it with the ``login`` route makes it visible when we visit ``/login``. The -one which associates it with the ``HTTPForbidden`` context makes it the -:term:`forbidden view`. The forbidden view is displayed whenever Pyramid or -your application raises an HTTPForbidden exception. In this case, we'll be -relying on the forbidden view to show the login form whenver someone attempts -to execute an action which they're not yet authorized to perform. +The ``login`` view callable is decorated with two decorators, a +``@view_config`` decorators, which associates it with the ``login`` route, +the other a ``@forbidden_view_config`` decorator which turns it in to an +:term:`exception view` when Pyramid raises a +:class:`pyramid.httpexceptions.HTTPForbidden` exception. The one which +associates it with the ``login`` route makes it visible when we visit +``/login``. The other one makes it a :term:`forbidden view`. The forbidden +view is displayed whenever Pyramid or your application raises an +HTTPForbidden exception. In this case, we'll be relying on the forbidden +view to show the login form whenver someone attempts to execute an action +which they're not yet authorized to perform. The ``logout`` view callable is decorated with a ``@view_config`` decorator which associates it with the ``logout`` route. This makes it visible when we visit ``/login``. We'll need to import some stuff to service the needs of these two functions: -the ``HTTPForbidden`` exception, a number of values from the +the ``pyramid.view.forbidden_view_config`` class, a number of values from the ``pyramid.security`` module, and a value from our newly added ``tutorial.security`` package. diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views.py b/docs/tutorials/wiki2/src/authorization/tutorial/views.py index 087e6076b..1453cd2e6 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/views.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/views.py @@ -4,10 +4,12 @@ from docutils.core import publish_parts from pyramid.httpexceptions import ( HTTPFound, HTTPNotFound, - HTTPForbidden, ) -from pyramid.view import view_config +from pyramid.view import ( + view_config, + forbidden_view_config, + ) from pyramid.security import ( remember, @@ -85,7 +87,7 @@ def edit_page(request): ) @view_config(route_name='login', renderer='templates/login.pt') -@view_config(context=HTTPForbidden, renderer='templates/login.pt') +@forbidden_view_config(renderer='templates/login.pt') def login(request): login_url = request.route_url('login') referrer = request.url diff --git a/docs/tutorials/wiki2/src/tests/tutorial/views.py b/docs/tutorials/wiki2/src/tests/tutorial/views.py index 375f1f5a5..465d98ae1 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/views.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/views.py @@ -4,10 +4,12 @@ from docutils.core import publish_parts from pyramid.httpexceptions import ( HTTPFound, HTTPNotFound, - HTTPForbidden, ) -from pyramid.view import view_config +from pyramid.view import ( + view_config, + forbidden_view_config, + ) from pyramid.security import ( remember, @@ -88,7 +90,7 @@ def edit_page(request): ) @view_config(route_name='login', renderer='templates/login.pt') -@view_config(context=HTTPForbidden, renderer='templates/login.pt') +@forbidden_view_config(renderer='templates/login.pt') def login(request): login_url = request.route_url('login') referrer = request.url diff --git a/docs/whatsnew-1.3.rst b/docs/whatsnew-1.3.rst index 7f6c3d7cb..101caed94 100644 --- a/docs/whatsnew-1.3.rst +++ b/docs/whatsnew-1.3.rst @@ -211,8 +211,10 @@ added, as well, but the configurator method should be preferred as it provides conflict detection and consistency in the lifetime of the properties. -Not Found View Helpers -~~~~~~~~~~~~~~~~~~~~~~ +Not Found and Forbidden View Helpers +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Not Found helpers: - New API: :meth:`pyramid.config.Configurator.add_notfound_view`. This is a wrapper for :meth:`pyramid.Config.configurator.add_view` which provides @@ -227,6 +229,20 @@ Not Found View Helpers should be preferred over using ``pyramid.view.view_config`` with ``context=HTTPNotFound`` as was previously recommended. +Forbidden helpers: + +- New API: :meth:`pyramid.config.Configurator.add_forbidden_view`. This is a + wrapper for :meth:`pyramid.Config.configurator.add_view` which does the + right thing about permissions. It should be preferred over calling + ``add_view`` directly with ``context=HTTPForbidden`` as was previously + recommended. + +- New API: :class:`pyramid.view.forbidden_view_config`. This is a decorator + constructor like :class:`pyramid.view.view_config` that calls + :meth:`pyramid.config.Configurator.add_forbidden_view` when scanned. It + should be preferred over using ``pyramid.view.view_config`` with + ``context=HTTPForbidden`` as was previously recommended. + Minor Feature Additions ----------------------- @@ -431,14 +447,16 @@ Backwards Incompatibilities Pyramid. - The older deprecated ``set_notfound_view`` Configurator method is now an - alias for the new :meth:`pyramid.config.Configurator.add_notfound_view` - method. This has the following impact: the ``context`` sent to views with - a ``(context, request)`` call signature registered via the deprecated - ``add_notfound_view`` / ``set_notfound_view`` will now be the HTTPNotFound - exception object instead of the actual resource context found. Use + alias for the new ``add_notfound_view`` Configurator method. Likewise, the + older deprecated ``set_forbidden_view`` is now an alias for the new + ``add_forbidden_view`` Configurator method. This has the following impact: + the ``context`` sent to views with a ``(context, request)`` call signature + registered via the ``set_notfound_view`` or ``set_forbidden_view`` will now + be an exception object instead of the actual resource context found. Use ``request.context`` to get the actual resource context. It's also recommended to disuse ``set_notfound_view`` in favor of - ``add_notfound_view``, despite the aliasing. + ``add_notfound_view``, and disuse ``set_forbidden_view`` in favor of + ``add_forbidden_view`` despite the aliasing. Deprecations ------------ @@ -450,8 +468,9 @@ Deprecations ``pyramid.view.notfound_view_config(append_slash=True)`` to get the same behavior. -- The ``set_forbidden_view`` method of the Configurator was removed from the - documentation. It has been deprecated since Pyramid 1.1. +- The ``set_forbidden_view`` and ``set_notfound_view`` methods of the + Configurator were removed from the documentation. They have been + deprecated since Pyramid 1.1. Documentation Enhancements -------------------------- @@ -485,6 +504,10 @@ Documentation Enhancements Rationale: it provides the correct info for the Python 2.5 version of GAE only, and this version of Pyramid does not support Python 2.5. +- Updated the :ref:`changing_the_forbidden_view` section, replacing + explanations of registering a view using ``add_view`` or ``view_config`` + with ones using ``add_forbidden_view`` or ``forbidden_view_config``. + - Updated the :ref:`changing_the_notfound_view` section, replacing explanations of registering a view using ``add_view`` or ``view_config`` with ones using ``add_notfound_view`` or ``notfound_view_config``. @@ -493,6 +516,9 @@ Documentation Enhancements explanations of registering a view using ``add_view`` or ``view_config`` with ones using ``add_notfound_view`` or ``notfound_view_config`` +- Updated all tutorials to use ``pyramid.view.forbidden_view_config`` rather + than ``pyramid.view.view_config`` with an HTTPForbidden context. + Dependency Changes ------------------ diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 9bd7b4b50..b4216c220 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1329,42 +1329,59 @@ class ViewsConfiguratorMixin(object): return deriver(view) @action_method - def set_forbidden_view(self, view=None, attr=None, renderer=None, - wrapper=None): - """ Add a default forbidden view to the current configuration - state. + def add_forbidden_view( + self, view=None, attr=None, renderer=None, wrapper=None, + route_name=None, request_type=None, request_method=None, + request_param=None, containment=None, xhr=None, accept=None, + header=None, path_info=None, custom_predicates=(), decorator=None, + mapper=None, match_param=None): + """ Add a forbidden view to the current configuration state. The + view will be called when Pyramid or application code raises a + :exc:`pyramid.httpexceptions.HTTPForbidden` exception and the set of + circumstances implied by the predicates provided are matched. The + simplest example is: - .. warning:: + .. code-block:: python - This method has been deprecated in :app:`Pyramid` 1.0. *Do not use - it for new development; it should only be used to support older code - bases which depend upon it.* See :ref:`changing_the_forbidden_view` - to see how a forbidden view should be registered in new projects. - - The ``view`` argument should be a :term:`view callable` or a - :term:`dotted Python name` which refers to a view callable. - - The ``attr`` argument should be the attribute of the view - callable used to retrieve the response (see the ``add_view`` - method's ``attr`` argument for a description). - - The ``renderer`` argument should be the name of (or path to) a - :term:`renderer` used to generate a response for this view - (see the - :meth:`pyramid.config.Configurator.add_view` - method's ``renderer`` argument for information about how a - configurator relates to a renderer). - - The ``wrapper`` argument should be the name of another view - which will wrap this view when rendered (see the ``add_view`` - method's ``wrapper`` argument for a description).""" - view = self._derive_view(view, attr=attr, renderer=renderer) - def bwcompat_view(context, request): - context = getattr(request, 'context', None) - return view(context, request) - return self.add_view(bwcompat_view, context=HTTPForbidden, - wrapper=wrapper, renderer=renderer) + def forbidden(request): + return Response('Forbidden', status='403 Forbidden') + + config.add_forbidden_view(forbidden) + + All arguments have the same meaning as + :meth:`pyramid.config.Configurator.add_view` and each predicate + argument restricts the set of circumstances under which this notfound + view will be invoked. + + .. note:: + This method is new as of Pyramid 1.3. + """ + settings = dict( + view=view, + context=HTTPForbidden, + wrapper=wrapper, + request_type=request_type, + request_method=request_method, + request_param=request_param, + containment=containment, + xhr=xhr, + accept=accept, + header=header, + path_info=path_info, + custom_predicates=custom_predicates, + decorator=decorator, + mapper=mapper, + match_param=match_param, + route_name=route_name, + permission=NO_PERMISSION_REQUIRED, + attr=attr, + renderer=renderer, + ) + return self.add_view(**settings) + + set_forbidden_view = add_forbidden_view # deprecated sorta-bw-compat alias + @action_method def add_notfound_view( self, view=None, attr=None, renderer=None, wrapper=None, @@ -1373,13 +1390,16 @@ class ViewsConfiguratorMixin(object): header=None, path_info=None, custom_predicates=(), decorator=None, mapper=None, match_param=None, append_slash=False): """ Add a default notfound view to the current configuration state. - The view will be called when a view cannot otherwise be found for the - set of circumstances implied by the predicates provided. The - simplest example is: + The view will be called when Pyramid or application code raises an + :exc:`pyramid.httpexceptions.HTTPForbidden` exception (e.g. when a + view cannot be found for the request). The simplest example is: .. code-block:: python - config.add_notfound_view(someview) + def notfound(request): + return Response('Not Found', status='404 Not Found') + + config.add_notfound_view(notfound) All arguments except ``append_slash`` have the same meaning as :meth:`pyramid.config.Configurator.add_view` and each predicate diff --git a/pyramid/tests/pkgs/forbiddenview/__init__.py b/pyramid/tests/pkgs/forbiddenview/__init__.py new file mode 100644 index 000000000..631a442d2 --- /dev/null +++ b/pyramid/tests/pkgs/forbiddenview/__init__.py @@ -0,0 +1,31 @@ +from pyramid.view import forbidden_view_config, view_config +from pyramid.response import Response +from pyramid.authentication import AuthTktAuthenticationPolicy +from pyramid.authorization import ACLAuthorizationPolicy + +@forbidden_view_config(route_name='foo') +def foo_forbidden(request): # pragma: no cover + return Response('foo_forbidden') + +@forbidden_view_config() +def forbidden(request): + return Response('generic_forbidden') + +@view_config(route_name='foo') +def foo(request): # pragma: no cover + return Response('OK foo') + +@view_config(route_name='bar') +def bar(request): # pragma: no cover + return Response('OK bar') + +def includeme(config): + authn_policy = AuthTktAuthenticationPolicy('seekri1') + authz_policy = ACLAuthorizationPolicy() + config.set_authentication_policy(authn_policy) + config.set_authorization_policy(authz_policy) + config.set_default_permission('a') + config.add_route('foo', '/foo') + config.add_route('bar', '/bar') + config.scan('pyramid.tests.pkgs.forbiddenview') + diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index 668fd7185..7bfe174b7 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -1649,14 +1649,14 @@ class TestViewsConfigurationMixin(unittest.TestCase): self.assertEqual(info.added, [(config, 'static', static_path, {})]) - def test_set_forbidden_view(self): + def test_add_forbidden_view(self): from pyramid.renderers import null_renderer from zope.interface import implementedBy from pyramid.interfaces import IRequest from pyramid.httpexceptions import HTTPForbidden config = self._makeOne(autocommit=True) view = lambda *arg: 'OK' - config.set_forbidden_view(view, renderer=null_renderer) + config.add_forbidden_view(view, renderer=null_renderer) request = self._makeRequest(config) view = self._getViewCallable(config, ctx_iface=implementedBy(HTTPForbidden), @@ -1664,24 +1664,6 @@ class TestViewsConfigurationMixin(unittest.TestCase): result = view(None, request) self.assertEqual(result, 'OK') - def test_set_forbidden_view_request_has_context(self): - from pyramid.renderers import null_renderer - from zope.interface import implementedBy - from pyramid.interfaces import IRequest - from pyramid.httpexceptions import HTTPForbidden - config = self._makeOne(autocommit=True) - view = lambda *arg: arg - config.set_forbidden_view(view, renderer=null_renderer) - request = self._makeRequest(config) - request.context = 'abc' - view = self._getViewCallable(config, - ctx_iface=implementedBy(HTTPForbidden), - request_iface=IRequest) - result = view(None, request) - self.assertEqual(result, ('abc', request)) - - - def test_add_notfound_view(self): from pyramid.renderers import null_renderer from zope.interface import implementedBy @@ -1743,7 +1725,7 @@ class TestViewsConfigurationMixin(unittest.TestCase): from pyramid.httpexceptions import HTTPForbidden config = self._makeOne(autocommit=True) view = lambda *arg: {} - config.set_forbidden_view( + config.add_forbidden_view( view, renderer='pyramid.tests.test_config:files/minimal.pt') config.begin() diff --git a/pyramid/tests/test_integration.py b/pyramid/tests/test_integration.py index 57b7e40b2..bf3bafc09 100644 --- a/pyramid/tests/test_integration.py +++ b/pyramid/tests/test_integration.py @@ -373,6 +373,15 @@ class TestNotFoundView(IntegrationBase, unittest.TestCase): self.assertTrue(b'OK foo2' in res.body) res = self.testapp.get('/baz', status=200) self.assertTrue(b'baz_notfound' in res.body) + +class TestForbiddenView(IntegrationBase, unittest.TestCase): + package = 'pyramid.tests.pkgs.forbiddenview' + + def test_it(self): + res = self.testapp.get('/foo', status=200) + self.assertTrue(b'foo_forbidden' in res.body) + res = self.testapp.get('/bar', status=200) + self.assertTrue(b'generic_forbidden' in res.body) class TestViewPermissionBug(IntegrationBase, unittest.TestCase): # view_execution_permitted bug as reported by Shane at http://lists.repoze.org/pipermail/repoze-dev/2010-October/003603.html diff --git a/pyramid/tests/test_view.py b/pyramid/tests/test_view.py index f092f281b..a775e7bc9 100644 --- a/pyramid/tests/test_view.py +++ b/pyramid/tests/test_view.py @@ -94,6 +94,48 @@ class Test_notfound_view_config(BaseTest, unittest.TestCase): self.assertEqual(settings[0]['attr'], 'view') self.assertEqual(settings[0]['_info'], 'codeinfo') +class Test_forbidden_view_config(BaseTest, unittest.TestCase): + def _makeOne(self, **kw): + from pyramid.view import forbidden_view_config + return forbidden_view_config(**kw) + + def test_ctor(self): + inst = self._makeOne(attr='attr', path_info='path_info') + self.assertEqual(inst.__dict__, + {'attr':'attr', 'path_info':'path_info'}) + + def test_it_function(self): + def view(request): pass + decorator = self._makeOne(attr='attr', renderer='renderer') + venusian = DummyVenusian() + decorator.venusian = venusian + wrapped = decorator(view) + self.assertTrue(wrapped is view) + config = call_venusian(venusian) + settings = config.settings + self.assertEqual( + settings, + [{'attr': 'attr', 'venusian': venusian, + 'renderer': 'renderer', '_info': 'codeinfo', 'view': None}] + ) + + def test_it_class(self): + decorator = self._makeOne() + venusian = DummyVenusian() + decorator.venusian = venusian + decorator.venusian.info.scope = 'class' + class view(object): pass + wrapped = decorator(view) + self.assertTrue(wrapped is view) + config = call_venusian(venusian) + settings = config.settings + self.assertEqual(len(settings), 1) + self.assertEqual(len(settings[0]), 4) + self.assertEqual(settings[0]['venusian'], venusian) + self.assertEqual(settings[0]['view'], None) # comes from call_venusian + self.assertEqual(settings[0]['attr'], 'view') + self.assertEqual(settings[0]['_info'], 'codeinfo') + class RenderViewToResponseTests(BaseTest, unittest.TestCase): def _callFUT(self, *arg, **kw): from pyramid.view import render_view_to_response @@ -716,7 +758,7 @@ class DummyConfig(object): def add_view(self, **kw): self.settings.append(kw) - add_notfound_view = add_view + add_notfound_view = add_forbidden_view = add_view def with_package(self, pkg): self.pkg = pkg diff --git a/pyramid/view.py b/pyramid/view.py index 9f049bf09..d722c0cbb 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -391,6 +391,71 @@ class notfound_view_config(object): settings['_info'] = info.codeinfo # fbo "action_method" return wrapped +class forbidden_view_config(object): + """ + + An analogue of :class:`pyramid.view.view_config` which registers a + :term:`forbidden view`. + + The forbidden_view_config constructor accepts most of the same arguments + as the constructor of :class:`pyramid.view.view_config`. It can be used + in the same places, and behaves in largely the same way, except it always + registers a forbidden exception view instead of a "normal" view. + + Example: + + .. code-block:: python + + from pyramid.view import forbidden_view_config + from pyramid.response import Response + + forbidden_view_config() + def notfound(request): + return Response('You are not allowed', status='401 Unauthorized') + + All have the same meaning as :meth:`pyramid.view.view_config` and each + predicate argument restricts the set of circumstances under which this + notfound view will be invoked. + + See :ref:`changing_the_forbidden_view` for detailed usage information. + + .. note:: + + This class is new as of Pyramid 1.3. + """ + + venusian = venusian + + def __init__(self, request_type=default, request_method=default, + route_name=default, request_param=default, attr=default, + renderer=default, containment=default, wrapper=default, + xhr=default, accept=default, header=default, + path_info=default, custom_predicates=default, + decorator=default, mapper=default, match_param=default): + L = locals() + for k, v in L.items(): + if k not in ('self', 'L') and v is not default: + self.__dict__[k] = v + + def __call__(self, wrapped): + settings = self.__dict__.copy() + + def callback(context, name, ob): + config = context.config.with_package(info.module) + config.add_forbidden_view(view=ob, **settings) + + info = self.venusian.attach(wrapped, callback, category='pyramid') + + if info.scope == 'class': + # if the decorator was attached to a method in a class, or + # otherwise executed at class scope, we need to set an + # 'attr' into the settings if one isn't already in there + if settings.get('attr') is None: + settings['attr'] = wrapped.__name__ + + settings['_info'] = info.codeinfo # fbo "action_method" + return wrapped + def is_response(ob): """ Return ``True`` if ``ob`` implements the interface implied by :ref:`the_response`. ``False`` if not. -- cgit v1.2.3 From eb64fca9efa08045133b7b35415ae5d95a8caba1 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 22 Feb 2012 19:28:29 -0500 Subject: gardeng --- TODO.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/TODO.txt b/TODO.txt index 5d96f7c7d..a58260d1f 100644 --- a/TODO.txt +++ b/TODO.txt @@ -4,6 +4,9 @@ Pyramid TODOs Nice-to-Have ------------ +- Expose _FileIter and _FileResponse somehow fbo of + manual-static-view-creators. + - Add docs about upgrading between Pyramid versions (e.g. how to see deprecation warnings). -- cgit v1.2.3