summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris McDonough <chrism@agendaless.com>2010-04-14 02:49:19 +0000
committerChris McDonough <chrism@agendaless.com>2010-04-14 02:49:19 +0000
commitff1213e8f2aed987108ba57aed517c033491b1aa (patch)
treef531544c3373ae7d5b51746987cb373326277a9c
parent2b6bc8adfa294f7133680f64df411251afb67dfc (diff)
downloadpyramid-ff1213e8f2aed987108ba57aed517c033491b1aa.tar.gz
pyramid-ff1213e8f2aed987108ba57aed517c033491b1aa.tar.bz2
pyramid-ff1213e8f2aed987108ba57aed517c033491b1aa.zip
Add "exception views" work contributed primarily by Andrey Popp by merging the "phash" branch.
-rw-r--r--CHANGES.txt1035
-rw-r--r--HISTORY.txt996
-rw-r--r--TODO.txt37
-rw-r--r--docs/narr/configuration.rst5
-rw-r--r--docs/narr/hooks.rst107
-rw-r--r--docs/narr/urldispatch.rst3
-rw-r--r--docs/narr/views.rst91
-rw-r--r--docs/tutorials/bfgwiki/authorization.rst2
-rw-r--r--docs/tutorials/bfgwiki/src/authorization/tutorial/configure.zcml5
-rw-r--r--docs/tutorials/bfgwiki2/authorization.rst6
-rw-r--r--docs/tutorials/bfgwiki2/src/authorization/tutorial/configure.zcml5
-rw-r--r--docs/zcml/forbidden.rst16
-rw-r--r--docs/zcml/notfound.rst16
-rw-r--r--repoze/bfg/compat/__init__.py6
-rw-r--r--repoze/bfg/configuration.py324
-rw-r--r--repoze/bfg/interfaces.py13
-rw-r--r--repoze/bfg/request.py6
-rw-r--r--repoze/bfg/router.py41
-rw-r--r--repoze/bfg/security.py3
-rw-r--r--repoze/bfg/testing.py2
-rw-r--r--repoze/bfg/tests/exceptionviewapp/__init__.py1
-rw-r--r--repoze/bfg/tests/exceptionviewapp/configure.zcml44
-rw-r--r--repoze/bfg/tests/exceptionviewapp/models.py18
-rw-r--r--repoze/bfg/tests/exceptionviewapp/views.py17
-rw-r--r--repoze/bfg/tests/fixtureapp/configure.zcml15
-rw-r--r--repoze/bfg/tests/fixtureapp/views.py14
-rw-r--r--repoze/bfg/tests/hybridapp/configure.zcml56
-rw-r--r--repoze/bfg/tests/hybridapp/views.py22
-rw-r--r--repoze/bfg/tests/test_configuration.py743
-rw-r--r--repoze/bfg/tests/test_integration.py59
-rw-r--r--repoze/bfg/tests/test_request.py2
-rw-r--r--repoze/bfg/tests/test_router.py578
-rw-r--r--repoze/bfg/tests/test_security.py3
-rw-r--r--repoze/bfg/tests/test_view.py3
-rw-r--r--repoze/bfg/tests/test_zcml.py193
-rw-r--r--repoze/bfg/view.py3
-rw-r--r--repoze/bfg/zcml.py65
37 files changed, 3046 insertions, 1509 deletions
diff --git a/CHANGES.txt b/CHANGES.txt
index cb9453b43..4f125d33d 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,1016 +1,73 @@
Next release
============
-Bug Fixes
----------
-
-- Defer conditional import of IPython to avoid breakage under mod_wsgi.
- http://bugs.repoze.org/issue138
-
-- The ``__name__`` value assigned to the returned object in the
- ``bfg_alchemy`` application template's ``MyApp`` model was an
- integer. This was incorrect. It is now a string.
-
-Internal
---------
-
-- Replace the statement ``path = path.rstrip('/').lstrip('/')`` with
- the simpler ``path = path.strip('/')`` in the
- ``repoze.bfg.traversal.traversal_path`` function.
-
-1.2 (2010-02-10)
-================
-
-- No changes from 1.2b6.
-
-1.2b6 (2010-02-06)
-==================
-
-Backwards Incompatibilities
----------------------------
-
-- Remove magical feature of ``repoze.bfg.url.model_url`` which
- prepended a fully-expanded urldispatch route URL before a the
- model's path if it was noticed that the request had matched a route.
- This feature was ill-conceived, and didn't work in all scenarios.
-
-Bug Fixes
----------
-
-- More correct conversion of provided ``renderer`` values to resource
- specification values (internal).
-
-1.2b5 (2010-02-04)
-==================
-
-Bug Fixes
----------
-
-- 1.2b4 introduced a bug whereby views added via a route configuration
- that named a view callable and also a ``view_attr`` became broken.
- Symptom: ``MyViewClass is not callable`` or the ``__call__`` of a
- class was being called instead of the method named via
- ``view_attr``.
-
-- Fix a bug whereby a ``renderer`` argument to the ``@bfg_view``
- decorator that provided a package-relative template filename might
- not have been resolved properly. Symptom: inappropriate ``Missing
- template resource`` errors.
-
-1.2b4 (2010-02-03)
-==================
-
-Documentation
--------------
-
-- Update GAE tutorial to use Chameleon instead of Jinja2 (now that
- it's possible).
-
-Bug Fixes
----------
-
-- Ensure that ``secure`` flag for AuthTktAuthenticationPolicy
- constructor does what it's documented to do (merge Daniel Holth's
- fancy-cookies-2 branch).
-
-Features
---------
-
-- Add ``path`` and ``http_only`` options to
- AuthTktAuthenticationPolicy constructor (merge Daniel Holth's
- fancy-cookies-2 branch).
-
-Backwards Incompatibilities
----------------------------
-
-- Remove ``view_header``, ``view_accept``, ``view_xhr``,
- ``view_path_info``, ``view_request_method``, ``view_request_param``,
- and ``view_containment`` predicate arguments from the
- ``Configurator.add_route`` argument list. These arguments were
- speculative. If you need the features exposed by these arguments,
- add a view associated with a route using the ``route_name`` argument
- to the ``add_view`` method instead.
-
-- Remove ``view_header``, ``view_accept``, ``view_xhr``,
- ``view_path_info``, ``view_request_method``, ``view_request_param``,
- and ``view_containment`` predicate arguments from the ``route`` ZCML
- directive attribute set. These attributes were speculative. If you
- need the features exposed by these attributes, add a view associated
- with a route using the ``route_name`` attribute of the ``view`` ZCML
- directive instead.
-
-Dependencies
-------------
-
-- Remove dependency on ``sourcecodegen`` (not depended upon by
- Chameleon 1.1.1+).
-
-1.2b3 (2010-01-24)
-==================
-
-Bug Fixes
----------
-
-- When "hybrid mode" (both traversal and urldispatch) is in use,
- default to finding route-related views even if a non-route-related
- view registration has been made with a more specific context. The
- default used to be to find views with a more specific context first.
- Use the new ``use_global_views`` argument to the route definition to
- get back the older behavior.
-
-Features
---------
-
-- Add ``use_global_views`` argument to ``add_route`` method of
- Configurator. When this argument is true, views registered for *no*
- route will be found if no more specific view related to the route is
- found.
-
-- Add ``use_global_views`` attribute to ZCML ``<route>`` directive
- (see above).
-
-Internal
---------
-
-- When registering a view, register the view adapter with the
- "requires" interfaces as ``(request_type, context_type)`` rather
- than ``(context_type, request_type)``. This provides for saner
- lookup, because the registration will always be made with a specific
- request interface, but registration may not be made with a specific
- context interface. In general, when creating multiadapters, you
- want to order the requires interfaces so that the the elements which
- are more likely to be registered using specific interfaces are
- ordered before those which are less likely.
-
-1.2b2 (2010-01-21)
-==================
-
-Bug Fixes
----------
-
-- When the ``Configurator`` is passed an instance of
- ``zope.component.registry.Components`` as a ``registry`` constructor
- argument, fix the instance up to have the attributes we expect of an
- instance of ``repoze.bfg.registry.Registry`` when ``setup_registry``
- is called. This makes it possible to use the global Zope component
- registry as a BFG application registry.
-
-- When WebOb 0.9.7.1 was used, a deprecation warning was issued for
- the class attribute named ``charset`` within
- ``repoze.bfg.request.Request``. BFG now *requires* WebOb >= 0.9.7,
- and code was added so that this deprecation warning has disappeared.
-
-- Fix a view lookup ordering bug whereby a view with a larger number
- of predicates registered first (literally first, not "earlier") for
- a triad would lose during view lookup to one registered with fewer.
-
-- Make sure views with exactly N custom predicates are always called
- before views with exactly N non-custom predicates given all else is
- equal in the view configuration.
-
-Documentation
--------------
-
-- Change renderings of ZCML directive documentation.
-
-- Add a narrative documentation chapter: "Using the Zope Component
- Architecture in repoze.bfg".
-
-Dependencies
-------------
-
-- Require WebOb >= 0.9.7
-
-1.2b1 (2010-01-18)
-==================
-
-Bug Fixes
----------
-
-- In ``bfg_routesalchemy``, ``bfg_alchemy`` paster templates and the
- ``bfgwiki2`` tutorial, clean up the SQLAlchemy connection by
- registering a ``repoze.tm.after_end`` callback instead of relying on
- a ``__del__`` method of a ``Cleanup`` class added to the WSGI
- environment. The ``__del__`` strategy was fragile and caused
- problems in the wild. Thanks to Daniel Holth for testing.
-
-Features
---------
-
-- Read logging configuration from PasteDeploy config file ``loggers``
- section (and related) when ``paster bfgshell`` is invoked.
-
-Documentation
--------------
-
-- Major rework in preparation for book publication.
-
-1.2a11 (2010-01-05)
-===================
-
-Bug Fixes
----------
-
-- Make ``paster bfgshell`` and ``paster create -t bfg_xxx`` work on
- Jython (fix minor incompatibility with treatment of ``__doc__`` at
- the class level).
-
-- Updated dependency on ``WebOb`` to require a version which supports
- features now used in tests.
-
-Features
---------
-
-- Jython compatibility (at least when repoze.bfg.jinja2 is used as the
- templating engine; Chameleon does not work under Jython).
-
-- Show the derived abspath of template resource specifications in the
- traceback when a renderer template cannot be found.
-
-- Show the original traceback when a Chameleon template cannot be
- rendered due to a platform incompatibility.
-
-1.2a10 (2010-01-04)
-===================
-
-Features
---------
-
-- The ``Configurator.add_view`` method now accepts an argument named
- ``context``. This is an alias for the older argument named
- ``for_``; it is preferred over ``for_``, but ``for_`` will continue
- to be supported "forever".
-
-- The ``view`` ZCML directive now accepts an attribute named
- ``context``. This is an alias for the older attribute named
- ``for``; it is preferred over ``for``, but ``for`` will continue to
- be supported "forever".
-
-- The ``Configurator.add_route`` method now accepts an argument named
- ``view_context``. This is an alias for the older argument named
- ``view_for``; it is preferred over ``view_for``, but ``view_for``
- will continue to be supported "forever".
-
-- The ``route`` ZCML directive now accepts an attribute named
- ``view_context``. This is an alias for the older attribute named
- ``view_for``; it is preferred over ``view_for``, but ``view_for``
- will continue to be supported "forever".
-
-Documentation and Paster Templates
-----------------------------------
-
-- LaTeX rendering tweaks.
-
-- All uses of the ``Configurator.add_view`` method that used its
- ``for_`` argument now use the ``context`` argument instead.
-
-- All uses of the ``Configurator.add_route`` method that used its
- ``view_for`` argument now use the ``view_context`` argument instead.
-
-- All uses of the ``view`` ZCML directive that used its ``for``
- attribute now use the ``context`` attribute instead.
-
-- All uses of the ``route`` ZCML directive that used its ``view_for``
- attribute now use the ``view_context`` attribute instead.
-
-- Add a (minimal) tutorial dealing with use of ``repoze.catalog`` in a
- ``repoze.bfg`` application.
-
-Documentation Licensing
------------------------
-
-- Loosen the documentation licensing to allow derivative works: it is
- now offered under the `Creative Commons
- Attribution-Noncommercial-Share Alike 3.0 United States License
- <http://creativecommons.org/licenses/by-nc-sa/3.0/us/>`_. This is
- only a documentation licensing change; the ``repoze.bfg`` software
- continues to be offered under the Repoze Public License at
- http://repoze.org/license.html (BSD-like).
-
-1.2a9 (2009-12-27)
-==================
-
-Documentation Licensing
------------------------
-
-- The *documentation* (the result of ``make <html|latex|htmlhelp>``
- within the ``docs`` directory) in this release is now offered under
- the Creative Commons Attribution-Noncommercial-No Derivative Works
- 3.0 United States License as described by
- http://creativecommons.org/licenses/by-nc-nd/3.0/us/ . This is only
- a licensing change for the documentation; the ``repoze.bfg``
- software continues to be offered under the Repoze Public License
- at http://repoze.org/license.html (BSD-like).
-
-Documentation
--------------
-
-- Added manual index entries to generated index.
-
-- Document the previously existing (but non-API)
- ``repoze.bfg.configuration.Configurator.setup_registry`` method as
- an official API of a ``Configurator``.
-
-- Fix syntax errors in various documentation code blocks.
-
-- Created new top-level documentation section: "ZCML Directives".
- This section contains detailed ZCML directive information, some of
- which was removed from various narrative chapters.
-
-- The LaTeX rendering of the documentation has been improved.
-
-- Added a "Fore-Matter" section with author, copyright, and licensing
- information.
-
-1.2a8 (2009-12-24)
-==================
-
Features
--------
-- Add a ``**kw`` arg to the ``Configurator.add_settings`` API.
+- Added "exception views". When you use an exception (anything than
+ inherits from the Python ``Exception`` builtin) as view context
+ argument, e.g.::
-- Add ``hook_zca`` and ``unhook_zca`` methods to the ``Configurator``
- API.
+ from repoze.bfg.view import bfg_view
+ from repoze.bfg.exceptions import NotFound
+ from webob.exc import HTTPNotFound
-- The ``repoze.bfg.testing.setUp`` method now returns a
- ``Configurator`` instance which can be used to do further
- configuration during unit tests.
+ @bfg_view(context=NotFound)
+ def notfound_view(request):
+ return HTTPNotFound()
-Bug Fixes
----------
+ For the above example, when the ``repoze.bfg.exceptions.NotFound``
+ exception is raised by any view or any root factory, the
+ ``notfound_view`` view callable wil be invoked and its response
+ returned.
-- The ``json`` renderer failed to set the response content type to
- ``application/json``. It now does, by setting
- ``request.response_content_type`` unless this attribute is already
- set.
+ Other normal view predicates can also be used in combination with an
+ exception view registration:
-- The ``string`` renderer failed to set the response content type to
- ``text/plain``. It now does, by setting
- ``request.response_content_type`` unless this attribute is already
- set.
-
-Documentation
--------------
+ from repoze.bfg.view import bfg_view
+ from repoze.bfg.exceptions import NotFound
+ from webob.exc import HTTPNotFound
-- General documentation improvements by using better Sphinx roles such
- as "class", "func", "meth", and so on. This means that there are
- many more hyperlinks pointing to API documentation for API
- definitions in all narrative, tutorial, and API documentation
- elements.
+ @bfg_view(context=NotFound, route_name='home')
+ def notfound_view(request):
+ return HTTPNotFound()
-- Added a description of imperative configuration in various places
- which only described ZCML configuration.
+ The above exception view names the ``route_name`` of ``home``,
+ meaning that it will only be called when the route matched has a
+ name of ``home``. You can therefore have more than one exception
+ view for any given exception in the system: the "most specific" one
+ will be called when the set of request circumstances which match the
+ view registration. The only predicate that cannot be not be used
+ successfully is ``name``. The name used to look up an exception
+ view is always the empty string.
-- A syntactical refreshing of various tutorials.
+ Existing (pre-1.3) normal views registered against objects
+ inheriting from ``Exception`` will continue to work. Exception
+ views used for user-defined exceptions and system exceptions used as
+ contexts will also work.
-- Added the ``repoze.bfg.authentication``,
- ``repoze.bfg.authorization``, and ``repoze.bfg.interfaces`` modules
- to API documentation.
+ The feature can be used with any view registration mechanism
+ (``@bfg_view`` decorator, ZCML, or imperative ``add_view`` styles).
Deprecations
------------
-- The ``repoze.bfg.testing.registerRoutesMapper`` API (added in an
- early 1.2 alpha) was deprecated. Its import now generates a
- deprecation warning.
-
-1.2a7 (2009-12-20)
-==================
-
-Features
---------
-
-- Add four new testing-related APIs to the
- ``repoze.bfg.configuration.Configurator`` class:
- ``testing_securitypolicy``, ``testing_models``,
- ``testing_add_subscriber``, and ``testing_add_template``. These
- were added in order to provide more direct access to the
- functionality of the ``repoze.bfg.testing`` APIs named
- ``registerDummySecurityPolicy``, ``registerModels``,
- ``registerEventListener``, and ``registerTemplateRenderer`` when a
- configurator is used. The ``testing`` APIs named are nominally
- deprecated (although they will likely remain around "forever", as
- they are in heavy use in the wild).
-
-- Add a new API to the ``repoze.bfg.configuration.Configurator``
- class: ``add_settings``. This API can be used to add "settings"
- (information returned within via the
- ``repoze.bfg.settings.get_settings`` API) after the configurator has
- been initially set up. This is most useful for testing purposes.
-
-- Add a ``custom_predicates`` argument to the ``Configurator``
- ``add_view`` method, the ``bfg_view`` decorator and the attribute
- list of the ZCML ``view`` directive. If ``custom_predicates`` is
- specified, it must be a sequence of predicate callables (a predicate
- callable accepts two arguments: ``context`` and ``request`` and
- returns ``True`` or ``False``). The associated view callable will
- only be invoked if all custom predicates return ``True``. Use one
- or more custom predicates when no existing predefined predicate is
- useful. Predefined and custom predicates can be mixed freely.
-
-- Add a ``custom_predicates`` argument to the ``Configurator``
- ``add_route`` and the attribute list of the ZCML ``route``
- directive. If ``custom_predicates`` is specified, it must be a
- sequence of predicate callables (a predicate callable accepts two
- arguments: ``context`` and ``request`` and returns ``True`` or
- ``False``). The associated route will match will only be invoked if
- all custom predicates return ``True``, else route matching
- continues. Note that the value ``context`` will always be ``None``
- when passed to a custom route predicate. Use one or more custom
- predicates when no existing predefined predicate is useful.
- Predefined and custom predicates can be mixed freely.
+- The exception views feature replaces the need for the
+ ``set_notfound_view`` and ``set_forbidden_view`` methods of the
+ ``Configurator`` as well as the ``notfound`` and ``forbidden`` ZCML
+ directives. Those methods and directives will continue to work for
+ the foreseeable future, but they are deprecated in the
+ documentation.
Internal
--------
-- Remove the ``repoze.bfg.testing.registerTraverser`` function. This
- function was never an API.
-
-Documenation
-------------
-
-- Doc-deprecated most helper functions in the ``repoze.bfg.testing``
- module. These helper functions likely won't be removed any time
- soon, nor will they generate a warning any time soon, due to their
- heavy use in the wild, but equivalent behavior exists in methods of
- a Configurator.
-
-1.2a6 (2009-12-18)
-==================
-
-Features
---------
-
-- The ``Configurator`` object now has two new methods: ``begin`` and
- ``end``. The ``begin`` method is meant to be called before any
- "configuration" begins (e.g. before ``add_view``, et. al are
- called). The ``end`` method is meant to be called after all
- "configuration" is complete.
-
- Previously, before there was imperative configuration at all (1.1
- and prior), configuration begin and end was invariably implied by
- the process of loading a ZCML file. When a ZCML load happened, the
- threadlocal data structure containing the request and registry was
- modified before the load, and torn down after the load, making sure
- that all framework code that needed ``get_current_registry`` for the
- duration of the ZCML load was satisfied.
-
- Some API methods called during imperative configuration, (such as
- ``Configurator.add_view`` when a renderer is involved) end up for
- historical reasons calling ``get_current_registry``. However, in
- 1.2a5 and below, the Configurator supplied no functionality that
- allowed people to make sure that ``get_current_registry`` returned
- the registry implied by the configurator being used. ``begin`` now
- serves this purpose. Inversely, ``end`` pops the thread local
- stack, undoing the actions of ``begin``.
-
- We make this boundary explicit to reduce the potential for confusion
- when the configurator is used in different circumstances (e.g. in
- unit tests and app code vs. just in initial app setup).
-
- Existing code written for 1.2a1-1.2a5 which does not call ``begin``
- or ``end`` continues to work in the same manner it did before. It
- is however suggested that this code be changed to call ``begin`` and
- ``end`` to reduce the potential for confusion in the future.
-
-- All ``paster`` templates which generate an application skeleton now
- make use of the new ``begin`` and ``end`` methods of the
- Configurator they use in their respective copies of ``run.py`` and
- ``tests.py``.
+- View registrations and lookups are now done with three "requires"
+ arguments instead of two to accomodate orthogonality of exception
+ views.
Documentation
-------------
-- All documentation that makes use of a ``Configurator`` object to do
- application setup and test setup now makes use of the new ``begin``
- and ``end`` methods of the configurator.
-
-Bug Fixes
----------
-
-- When a ``repoze.bfg.exceptions.NotFound`` or
- ``repoze.bfg.exceptions.Forbidden`` *class* (as opposed to instance)
- was raised as an exception within a root factory (or route root
- factory), the exception would not be caught properly by the
- ``repoze.bfg.`` Router and it would propagate to up the call stack,
- as opposed to rendering the not found view or the forbidden view as
- would have been expected.
-
-- When Chameleon page or text templates used as renderers were added
- imperatively (via ``Configurator.add_view`` or some derivative),
- they too-eagerly attempted to look up the ``reload_templates``
- setting via ``get_settings``, meaning they were always registered in
- non-auto-reload-mode (the default). Each now waits until its
- respective ``template`` attribute is accessed to look up the value.
-
-- When a route with the same name as a previously registered route was
- added, the old route was not removed from the mapper's routelist.
- Symptom: the old registered route would be used (and possibly
- matched) during route lookup when it should not have had a chance to
- ever be used.
-
-1.2a5 (2009-12-10)
-==================
-
-Features
---------
-
-- When the ``repoze.bfg.exceptions.NotFound`` or
- ``repoze.bfg.exceptions.Forbidden`` error is raised from within a
- custom root factory or the ``factory`` of a route, the appropriate
- response is now sent to the requesting user agent (the result of the
- notfound view or the forbidden view, respectively). When these
- errors are raised from within a root factory, the ``context`` passed
- to the notfound or forbidden view will be ``None``. Also, the
- request will not be decorated with ``view_name``, ``subpath``,
- ``context``, etc. as would normally be the case if traversal had
- been allowed to take place.
-
-Internals
----------
-
-- The exception class representing the error raised by various methods
- of a ``Configurator`` is now importable as
- ``repoze.bfg.exceptions.ConfigurationError``.
-
-Documentation
--------------
-
-- General documentation freshening which takes imperative
- configuration into account in more places and uses glossary
- references more liberally.
-
-- Remove explanation of changing the request type in a new request
- event subscriber, as other predicates are now usually an easier way
- to get this done.
-
-- Added "Thread Locals" narrative chapter to documentation, and added
- a API chapter documenting the ``repoze.bfg.threadlocals`` module.
-
-- Added a "Special Exceptions" section to the "Views" narrative
- documentation chapter explaining the effect of raising
- ``repoze.bfg.exceptions.NotFound`` and
- ``repoze.bfg.exceptions.Forbidden`` from within view code.
-
-Dependencies
-------------
-
-- A new dependency on the ``twill`` package was added to the
- ``setup.py`` ``tests_require`` argument (Twill will only be
- downloaded when ``repoze.bfg`` ``setup.py test`` or ``setup.py
- nosetests`` is invoked).
-
-1.2a4 (2009-12-07)
-==================
-
-Features
---------
-
-- ``repoze.bfg.testing.DummyModel`` now accepts a new constructor
- keyword argument: ``__provides__``. If this constructor argument is
- provided, it should be an interface or a tuple of interfaces. The
- resulting model will then provide these interfaces (they will be
- attached to the constructed model via
- ``zope.interface.alsoProvides``).
-
-Bug Fixes
----------
-
-- Operation on GAE was broken, presumably because the
- ``repoze.bfg.configuration`` module began to attempt to import the
- ``repoze.bfg.chameleon_zpt`` and ``repoze.bfg.chameleon_text``
- modules, and these cannot be used on non-CPython platforms. It now
- tolerates startup time import failures for these modules, and only
- raise an import error when a template from one of these packages is
- actually used.
-
-1.2a3 (2009-12-02)
-==================
-
-Bug Fixes
----------
-
-- The ``repoze.bfg.url.route_url`` function inappropriately passed
- along ``_query`` and/or ``_anchor`` arguments to the
- ``mapper.generate`` function, resulting in blowups.
-
-- When two views were registered with differering ``for`` interfaces
- or classes, and the ``for`` of first view registered was a
- superclass of the second, the ``repoze.bfg`` view machinery would
- incorrectly associate the two views with the same "multiview".
- Multiviews are meant to be collections of views that have *exactly*
- the same for/request/viewname values, without taking inheritance
- into account. Symptom: wrong view callable found even when you had
- correctly specified a ``for_`` interface/class during view
- configuration for one or both view configurations.
-
-Backwards Incompatibilities
----------------------------
-
-- The ``repoze.bfg.templating`` module has been removed; it had been
- deprecated in 1.1 and never actually had any APIs in it.
-
-1.2a2 (2009-11-29)
-==================
-
-Bug Fixes
----------
-
-- The the long description of this package (as shown on PyPI) was not
- valid reStructuredText, and so was not renderable.
-
-- Trying to use an HTTP method name string such as ``GET`` as a
- ``request_type`` predicate argument caused a startup time failure
- when it was encountered in imperative configuration or in a
- decorator (symptom: ``Type Error: Required specification must be a
- specification``). This now works again, although ``request_method``
- is now the preferred predicate argument for associating a view
- configuration with an HTTP request method.
-
-Documentation
--------------
-
-- Fixed "Startup" narrative documentation chapter; it was explaining
- "the old way" an application constructor worked.
-
-1.2a1 (2009-11-28)
-==================
-
-Features
---------
-
-- An imperative configuration mode.
-
- A ``repoze.bfg`` application can now begin its life as a single
- Python file. Later, the application might evolve into a set of
- Python files in a package. Even later, it might start making use of
- other configuration features, such as ``ZCML``. But neither the use
- of a package nor the use of non-imperative configuration is required
- to create a simple ``repoze.bfg`` application any longer.
-
- Imperative configuration makes ``repoze.bfg`` competetive with
- "microframeworks" such as `Bottle <http://bottle.paws.de/>`_ and
- `Tornado <http://www.tornadoweb.org/>`_. ``repoze.bfg`` has a good
- deal of functionality that most microframeworks lack, so this is
- hopefully a "best of both worlds" feature.
-
- The simplest possible ``repoze.bfg`` application is now::
-
- from webob import Response
- from wsgiref import simple_server
- from repoze.bfg.configuration import Configurator
-
- def hello_world(request):
- return Response('Hello world!')
-
- if __name__ == '__main__':
- config = Configurator()
- config.add_view(hello_world)
- app = config.make_wsgi_app()
- simple_server.make_server('', 8080, app).serve_forever()
-
-- A new class now exists: ``repoze.bfg.configuration.Configurator``.
- This class forms the basis for sharing machinery between
- "imperatively" configured applications and traditional
- declaratively-configured applications.
-
-- The ``repoze.bfg.testing.setUp`` function now accepts three extra
- optional keyword arguments: ``registry``, ``request`` and
- ``hook_zca``.
-
- If the ``registry`` argument is not ``None``, the argument will be
- treated as the registry that is set as the "current registry" (it
- will be returned by ``repoze.bfg.threadlocal.get_current_registry``)
- for the duration of the test. If the ``registry`` argument is
- ``None`` (the default), a new registry is created and used for the
- duration of the test.
-
- The value of the ``request`` argument is used as the "current
- request" (it will be returned by
- ``repoze.bfg.threadlocal.get_current_request``) for the duration of
- the test; it defaults to ``None``.
-
- If ``hook_zca`` is ``True`` (the default), the
- ``zope.component.getSiteManager`` function will be hooked with a
- function that returns the value of ``registry`` (or the
- default-created registry if ``registry`` is ``None``) instead of the
- registry returned by ``zope.component.getGlobalSiteManager``,
- causing the Zope Component Architecture API (``getSiteManager``,
- ``getAdapter``, ``getUtility``, and so on) to use the testing
- registry instead of the global ZCA registry.
-
-- The ``repoze.bfg.testing.tearDown`` function now accepts an
- ``unhook_zca`` argument. If this argument is ``True`` (the
- default), ``zope.component.getSiteManager.reset()`` will be called.
- This will cause the result of the ``zope.component.getSiteManager``
- function to be the global ZCA registry (the result of
- ``zope.component.getGlobalSiteManager``) once again.
-
-- The ``run.py`` module in various ``repoze.bfg`` ``paster`` templates
- now use a ``repoze.bfg.configuration.Configurator`` class instead of
- the (now-legacy) ``repoze.bfg.router.make_app`` function to produce
- a WSGI application.
-
-Documentation
--------------
-
-- The documentation now uses the "request-only" view calling
- convention in most examples (as opposed to the ``context, request``
- convention). This is a documentation-only change; the ``context,
- request`` convention is also supported and documented, and will be
- "forever".
-
-- ``repoze.bfg.configuration`` API documentation has been added.
-
-- A narrative documentation chapter entitled "Creating Your First
- ``repoze.bfg`` Application" has been added. This chapter details
- usage of the new ``repoze.bfg.configuration.Configurator`` class,
- and demonstrates a simplified "imperative-mode" configuration; doing
- ``repoze.bfg`` application configuration imperatively was previously
- much more difficult.
-
-- A narrative documentation chapter entitled "Configuration,
- Decorations and Code Scanning" explaining ZCML- vs. imperative-
- vs. decorator-based configuration equivalence.
-
-- The "ZCML Hooks" chapter has been renamed to "Hooks"; it documents
- how to override hooks now via imperative configuration and ZCML.
-
-- The explanation about how to supply an alternate "response factory"
- has been removed from the "Hooks" chapter. This feature may be
- removed in a later release (it still works now, it's just not
- documented).
-
-- Add a section entitled "Test Set Up and Tear Down" to the
- unittesting chapter.
-
-Bug Fixes
-----------
-
-- The ACL authorization policy debugging output when
- ``debug_authorization`` console debugging output was turned on
- wasn't as clear as it could have been when a view execution was
- denied due to an authorization failure resulting from the set of
- principals passed never having matched any ACE in any ACL in the
- lineage. Now in this case, we report ``<default deny>`` as the ACE
- value and either the root ACL or ``<No ACL found on any object in
- model lineage>`` if no ACL was found.
-
-- When two views were registered with the same ``accept`` argument,
- but were otherwise registered with the same arguments, if a request
- entered the application which had an ``Accept`` header that accepted
- *either* of the media types defined by the set of views registered
- with predicates that otherwise matched, a more or less "random" one
- view would "win". Now, we try harder to use the view callable
- associated with the view configuration that has the most specific
- ``accept`` argument. Thanks to Alberto Valverde for an initial
- patch.
-
-Internals
----------
-
-- The routes mapper is no longer a root factory wrapper. It is now
- consulted directly by the router.
-
-- The ``repoze.bfg.registry.make_registry`` callable has been removed.
-
-- The ``repoze.bfg.view.map_view`` callable has been removed.
-
-- The ``repoze.bfg.view.owrap_view`` callable has been removed.
-
-- The ``repoze.bfg.view.predicate_wrap`` callable has been removed.
-
-- The ``repoze.bfg.view.secure_view`` callable has been removed.
-
-- The ``repoze.bfg.view.authdebug_view`` callable has been removed.
-
-- The ``repoze.bfg.view.renderer_from_name`` callable has been
- removed. Use ``repoze.bfg.configuration.Configurator.renderer_from_name``
- instead (still not an API, however).
-
-- The ``repoze.bfg.view.derive_view`` callable has been removed. Use
- ``repoze.bfg.configuration.Configurator.derive_view`` instead (still
- not an API, however).
-
-- The ``repoze.bfg.settings.get_options`` callable has been removed.
- Its job has been subsumed by the ``repoze.bfg.settings.Settings``
- class constructor.
-
-- The ``repoze.bfg.view.requestonly`` function has been moved to
- ``repoze.bfg.configuration.requestonly``.
-
-- The ``repoze.bfg.view.rendered_response`` function has been moved to
- ``repoze.bfg.configuration.rendered_response``.
-
-- The ``repoze.bfg.view.decorate_view`` function has been moved to
- ``repoze.bfg.configuration.decorate_view``.
-
-- The ``repoze.bfg.view.MultiView`` class has been moved to
- ``repoze.bfg.configuration.MultiView``.
-
-- The ``repoze.bfg.zcml.Uncacheable`` class has been removed.
-
-- The ``repoze.bfg.resource.resource_spec`` function has been removed.
-
-- All ZCML directives which deal with attributes which are paths now
- use the ``path`` method of the ZCML context to resolve a relative
- name to an absolute one (imperative configuration requirement).
-
-- The ``repoze.bfg.scripting.get_root`` API now uses a 'real' WebOb
- request rather than a FakeRequest when it sets up the request as a
- threadlocal.
-
-- The ``repoze.bfg.traversal.traverse`` API now uses a 'real' WebOb
- request rather than a FakeRequest when it calls the traverser.
-
-- The ``repoze.bfg.request.FakeRequest`` class has been removed.
-
-- Most uses of the ZCA threadlocal API (the ``getSiteManager``,
- ``getUtility``, ``getAdapter``, ``getMultiAdapter`` threadlocal API)
- have been removed from the core. Instead, when a threadlocal is
- necessary, the core uses the
- ``repoze.bfg.threadlocal.get_current_registry`` API to obtain the
- registry.
-
-- The internal ILogger utility named ``repoze.bfg.debug`` is now just
- an IDebugLogger unnamed utility. A named utility with the old name
- is registered for b/w compat.
-
-- The ``repoze.bfg.interfaces.ITemplateRendererFactory`` interface was
- removed; it has become unused.
-
-- Instead of depending on the ``martian`` package to do code scanning,
- we now just use our own scanning routines.
-
-- We now no longer have a dependency on ``repoze.zcml`` package;
- instead, the ``repoze.bfg`` package includes implementations of the
- ``adapter``, ``subscriber`` and ``utility`` directives.
-
-- Relating to the following functions:
-
- ``repoze.bfg.view.render_view``
-
- ``repoze.bfg.view.render_view_to_iterable``
-
- ``repoze.bfg.view.render_view_to_response``
-
- ``repoze.bfg.view.append_slash_notfound_view``
-
- ``repoze.bfg.view.default_notfound_view``
-
- ``repoze.bfg.view.default_forbidden_view``
-
- ``repoze.bfg.configuration.rendered_response``
-
- ``repoze.bfg.security.has_permission``
-
- ``repoze.bfg.security.authenticated_userid``
-
- ``repoze.bfg.security.effective_principals``
-
- ``repoze.bfg.security.view_execution_permitted``
-
- ``repoze.bfg.security.remember``
-
- ``repoze.bfg.security.forget``
-
- ``repoze.bfg.url.route_url``
-
- ``repoze.bfg.url.model_url``
-
- ``repoze.bfg.url.static_url``
-
- ``repoze.bfg.traversal.virtual_root``
-
- Each of these functions now expects to be called with a request
- object that has a ``registry`` attribute which represents the
- current ``repoze.bfg`` registry. They fall back to obtaining the
- registry from the threadlocal API.
-
-Backwards Incompatibilites
---------------------------
-
-- Unit tests which use ``zope.testing.cleanup.cleanUp`` for the
- purpose of isolating tests from one another may now begin to fail
- due to lack of isolation between tests.
-
- Here's why: In repoze.bfg 1.1 and prior, the registry returned by
- ``repoze.bfg.threadlocal.get_current_registry`` when no other
- registry had been pushed on to the threadlocal stack was the
- ``zope.component.globalregistry.base`` global registry (aka the
- result of ``zope.component.getGlobalSiteManager()``). In repoze.bfg
- 1.2+, however, the registry returned in this situation is the new
- module-scope ``repoze.bfg.registry.global_registry`` object. The
- ``zope.testing.cleanup.cleanUp`` function clears the
- ``zope.component.globalregistry.base`` global registry
- unconditionally. However, it does not know about the
- ``repoze.bfg.registry.global_registry`` object, so it does not clear
- it.
-
- If you use the ``zope.testing.cleanup.cleanUp`` function in the
- ``setUp`` of test cases in your unit test suite instead of using the
- (more correct as of 1.1) ``repoze.bfg.testing.setUp``, you will need
- to replace all calls to ``zope.testing.cleanup.cleanUp`` with a call
- to ``repoze.bfg.testing.setUp``.
-
- If replacing all calls to ``zope.testing.cleanup.cleanUp`` with a
- call to ``repoze.bfg.testing.setUp`` is infeasible, you can put this
- bit of code somewhere that is executed exactly **once** (*not* for
- each test in a test suite; in the `` __init__.py`` of your package
- or your package's ``tests`` subpackage would be a reasonable
- place)::
-
- import zope.testing.cleanup
- from repoze.bfg.testing import setUp
- zope.testing.cleanup.addCleanUp(setUp)
-
-- When there is no "current registry" in the
- ``repoze.bfg.threadlocal.manager`` threadlocal data structure (this
- is the case when there is no "current request" or we're not in the
- midst of a ``r.b.testing.setUp``-bounded unit test), the ``.get``
- method of the manager returns a data structure containing a *global*
- registry. In previous releases, this function returned the global
- Zope "base" registry: the result of
- ``zope.component.getGlobalSiteManager``, which is an instance of the
- ``zope.component.registry.Component`` class. In this release,
- however, the global registry returns a globally importable instance
- of the ``repoze.bfg.registry.Registry`` class. This registry
- instance can always be imported as
- ``repoze.bfg.registry.global_registry``.
-
- Effectively, this means that when you call
- ``repoze.bfg.threadlocal.get_current_registry`` when no request or
- ``setUp`` bounded unit test is in effect, you will always get back
- the global registry that lives in
- ``repoze.bfg.registry.global_registry``. It also means that
- ``repoze.bfg`` APIs that *call* ``get_current_registry`` will use
- this registry.
-
- This change was made because ``repoze.bfg`` now expects the registry
- it uses to have a slightly different API than a bare instance of
- ``zope.component.registry.Components``.
-
-- View registration no longer registers a
- ``repoze.bfg.interfaces.IViewPermission`` adapter (it is no longer
- checked by the framework; since 1.1, views have been responsible for
- providing their own security).
-
-- The ``repoze.bfg.router.make_app`` callable no longer accepts the
- ``authentication_policy`` nor the ``authorization_policy``
- arguments. This feature was deprecated in version 1.0 and has been
- removed.
-
-- Obscure: the machinery which configured views with a
- ``request_type`` *and* a ``route_name`` would ignore the request
- interface implied by ``route_name`` registering a view only for the
- interface implied by ``request_type``. In the unlikely event that
- you were trying to use these two features together, the symptom
- would have been that views that named a ``request_type`` but which
- were also associated with routes were not found when the route
- matched. Now if a view is configured with both a ``request_type``
- and a ``route_name``, an error is raised.
-
-- The ``route`` ZCML directive now no longer accepts the
- ``request_type`` or ``view_request_type`` attributes. These
- attributes didn't actually work in any useful way (see entry above
- this one).
-
-- Because the ``repoze.bfg`` package now includes implementations of
- the ``adapter``, ``subscriber`` and ``utility`` ZCML directives, it
- is now an error to have ``<include package="repoze.zcml"
- file="meta.zcml"/>`` in the ZCML of a ``repoze.bfg`` application. A
- ZCML conflict error will be raised if your ZCML does so. This
- shouldn't be an issue for "normal" installations; it has always been
- the responsibility of the ``repoze.bfg.includes`` ZCML to include
- this file in the past; it now just doesn't.
-
-- The ``repoze.bfg.testing.zcml_configure`` API was removed. Use
- the ``Configurator.load_zcml`` API instead.
-
-Deprecations
-------------
-
-- The ``repoze.bfg.router.make_app`` function is now nominally
- deprecated. Its import and usage does not throw a warning, nor will
- it probably ever disappear. However, using a
- ``repoze.bfg.configuration.Configurator`` class is now the preferred
- way to generate a WSGI application.
-
- Note that ``make_app`` calls
- ``zope.component.getSiteManager.sethook(
- repoze.bfg.threadlocal.get_current_registry)`` on the caller's
- behalf, hooking ZCA global API lookups, for backwards compatibility
- purposes. If you disuse ``make_app``, your calling code will need
- to perform this call itself, at least if your application uses the
- ZCA global API (``getSiteManager``, ``getAdapter``, etc).
-
-Dependencies
-------------
-
-- A dependency on the ``martian`` package has been removed (its
- functionality is replaced internally).
-
-- A dependency on the ``repoze.zcml`` package has been removed (its
- functionality is replaced internally).
-
+- Exception view documentation was added to the ``Hooks`` narrative
+ chapter.
diff --git a/HISTORY.txt b/HISTORY.txt
index 93de7e90c..bc69dbeb2 100644
--- a/HISTORY.txt
+++ b/HISTORY.txt
@@ -1,3 +1,999 @@
+1.2 (2010-02-10)
+================
+
+- No changes from 1.2b6.
+
+1.2b6 (2010-02-06)
+==================
+
+Backwards Incompatibilities
+---------------------------
+
+- Remove magical feature of ``repoze.bfg.url.model_url`` which
+ prepended a fully-expanded urldispatch route URL before a the
+ model's path if it was noticed that the request had matched a route.
+ This feature was ill-conceived, and didn't work in all scenarios.
+
+Bug Fixes
+---------
+
+- More correct conversion of provided ``renderer`` values to resource
+ specification values (internal).
+
+1.2b5 (2010-02-04)
+==================
+
+Bug Fixes
+---------
+
+- 1.2b4 introduced a bug whereby views added via a route configuration
+ that named a view callable and also a ``view_attr`` became broken.
+ Symptom: ``MyViewClass is not callable`` or the ``__call__`` of a
+ class was being called instead of the method named via
+ ``view_attr``.
+
+- Fix a bug whereby a ``renderer`` argument to the ``@bfg_view``
+ decorator that provided a package-relative template filename might
+ not have been resolved properly. Symptom: inappropriate ``Missing
+ template resource`` errors.
+
+1.2b4 (2010-02-03)
+==================
+
+Documentation
+-------------
+
+- Update GAE tutorial to use Chameleon instead of Jinja2 (now that
+ it's possible).
+
+Bug Fixes
+---------
+
+- Ensure that ``secure`` flag for AuthTktAuthenticationPolicy
+ constructor does what it's documented to do (merge Daniel Holth's
+ fancy-cookies-2 branch).
+
+Features
+--------
+
+- Add ``path`` and ``http_only`` options to
+ AuthTktAuthenticationPolicy constructor (merge Daniel Holth's
+ fancy-cookies-2 branch).
+
+Backwards Incompatibilities
+---------------------------
+
+- Remove ``view_header``, ``view_accept``, ``view_xhr``,
+ ``view_path_info``, ``view_request_method``, ``view_request_param``,
+ and ``view_containment`` predicate arguments from the
+ ``Configurator.add_route`` argument list. These arguments were
+ speculative. If you need the features exposed by these arguments,
+ add a view associated with a route using the ``route_name`` argument
+ to the ``add_view`` method instead.
+
+- Remove ``view_header``, ``view_accept``, ``view_xhr``,
+ ``view_path_info``, ``view_request_method``, ``view_request_param``,
+ and ``view_containment`` predicate arguments from the ``route`` ZCML
+ directive attribute set. These attributes were speculative. If you
+ need the features exposed by these attributes, add a view associated
+ with a route using the ``route_name`` attribute of the ``view`` ZCML
+ directive instead.
+
+Dependencies
+------------
+
+- Remove dependency on ``sourcecodegen`` (not depended upon by
+ Chameleon 1.1.1+).
+
+1.2b3 (2010-01-24)
+==================
+
+Bug Fixes
+---------
+
+- When "hybrid mode" (both traversal and urldispatch) is in use,
+ default to finding route-related views even if a non-route-related
+ view registration has been made with a more specific context. The
+ default used to be to find views with a more specific context first.
+ Use the new ``use_global_views`` argument to the route definition to
+ get back the older behavior.
+
+Features
+--------
+
+- Add ``use_global_views`` argument to ``add_route`` method of
+ Configurator. When this argument is true, views registered for *no*
+ route will be found if no more specific view related to the route is
+ found.
+
+- Add ``use_global_views`` attribute to ZCML ``<route>`` directive
+ (see above).
+
+Internal
+--------
+
+- When registering a view, register the view adapter with the
+ "requires" interfaces as ``(request_type, context_type)`` rather
+ than ``(context_type, request_type)``. This provides for saner
+ lookup, because the registration will always be made with a specific
+ request interface, but registration may not be made with a specific
+ context interface. In general, when creating multiadapters, you
+ want to order the requires interfaces so that the the elements which
+ are more likely to be registered using specific interfaces are
+ ordered before those which are less likely.
+
+1.2b2 (2010-01-21)
+==================
+
+Bug Fixes
+---------
+
+- When the ``Configurator`` is passed an instance of
+ ``zope.component.registry.Components`` as a ``registry`` constructor
+ argument, fix the instance up to have the attributes we expect of an
+ instance of ``repoze.bfg.registry.Registry`` when ``setup_registry``
+ is called. This makes it possible to use the global Zope component
+ registry as a BFG application registry.
+
+- When WebOb 0.9.7.1 was used, a deprecation warning was issued for
+ the class attribute named ``charset`` within
+ ``repoze.bfg.request.Request``. BFG now *requires* WebOb >= 0.9.7,
+ and code was added so that this deprecation warning has disappeared.
+
+- Fix a view lookup ordering bug whereby a view with a larger number
+ of predicates registered first (literally first, not "earlier") for
+ a triad would lose during view lookup to one registered with fewer.
+
+- Make sure views with exactly N custom predicates are always called
+ before views with exactly N non-custom predicates given all else is
+ equal in the view configuration.
+
+Documentation
+-------------
+
+- Change renderings of ZCML directive documentation.
+
+- Add a narrative documentation chapter: "Using the Zope Component
+ Architecture in repoze.bfg".
+
+Dependencies
+------------
+
+- Require WebOb >= 0.9.7
+
+1.2b1 (2010-01-18)
+==================
+
+Bug Fixes
+---------
+
+- In ``bfg_routesalchemy``, ``bfg_alchemy`` paster templates and the
+ ``bfgwiki2`` tutorial, clean up the SQLAlchemy connection by
+ registering a ``repoze.tm.after_end`` callback instead of relying on
+ a ``__del__`` method of a ``Cleanup`` class added to the WSGI
+ environment. The ``__del__`` strategy was fragile and caused
+ problems in the wild. Thanks to Daniel Holth for testing.
+
+Features
+--------
+
+- Read logging configuration from PasteDeploy config file ``loggers``
+ section (and related) when ``paster bfgshell`` is invoked.
+
+Documentation
+-------------
+
+- Major rework in preparation for book publication.
+
+1.2a11 (2010-01-05)
+===================
+
+Bug Fixes
+---------
+
+- Make ``paster bfgshell`` and ``paster create -t bfg_xxx`` work on
+ Jython (fix minor incompatibility with treatment of ``__doc__`` at
+ the class level).
+
+- Updated dependency on ``WebOb`` to require a version which supports
+ features now used in tests.
+
+Features
+--------
+
+- Jython compatibility (at least when repoze.bfg.jinja2 is used as the
+ templating engine; Chameleon does not work under Jython).
+
+- Show the derived abspath of template resource specifications in the
+ traceback when a renderer template cannot be found.
+
+- Show the original traceback when a Chameleon template cannot be
+ rendered due to a platform incompatibility.
+
+1.2a10 (2010-01-04)
+===================
+
+Features
+--------
+
+- The ``Configurator.add_view`` method now accepts an argument named
+ ``context``. This is an alias for the older argument named
+ ``for_``; it is preferred over ``for_``, but ``for_`` will continue
+ to be supported "forever".
+
+- The ``view`` ZCML directive now accepts an attribute named
+ ``context``. This is an alias for the older attribute named
+ ``for``; it is preferred over ``for``, but ``for`` will continue to
+ be supported "forever".
+
+- The ``Configurator.add_route`` method now accepts an argument named
+ ``view_context``. This is an alias for the older argument named
+ ``view_for``; it is preferred over ``view_for``, but ``view_for``
+ will continue to be supported "forever".
+
+- The ``route`` ZCML directive now accepts an attribute named
+ ``view_context``. This is an alias for the older attribute named
+ ``view_for``; it is preferred over ``view_for``, but ``view_for``
+ will continue to be supported "forever".
+
+Documentation and Paster Templates
+----------------------------------
+
+- LaTeX rendering tweaks.
+
+- All uses of the ``Configurator.add_view`` method that used its
+ ``for_`` argument now use the ``context`` argument instead.
+
+- All uses of the ``Configurator.add_route`` method that used its
+ ``view_for`` argument now use the ``view_context`` argument instead.
+
+- All uses of the ``view`` ZCML directive that used its ``for``
+ attribute now use the ``context`` attribute instead.
+
+- All uses of the ``route`` ZCML directive that used its ``view_for``
+ attribute now use the ``view_context`` attribute instead.
+
+- Add a (minimal) tutorial dealing with use of ``repoze.catalog`` in a
+ ``repoze.bfg`` application.
+
+Documentation Licensing
+-----------------------
+
+- Loosen the documentation licensing to allow derivative works: it is
+ now offered under the `Creative Commons
+ Attribution-Noncommercial-Share Alike 3.0 United States License
+ <http://creativecommons.org/licenses/by-nc-sa/3.0/us/>`_. This is
+ only a documentation licensing change; the ``repoze.bfg`` software
+ continues to be offered under the Repoze Public License at
+ http://repoze.org/license.html (BSD-like).
+
+1.2a9 (2009-12-27)
+==================
+
+Documentation Licensing
+-----------------------
+
+- The *documentation* (the result of ``make <html|latex|htmlhelp>``
+ within the ``docs`` directory) in this release is now offered under
+ the Creative Commons Attribution-Noncommercial-No Derivative Works
+ 3.0 United States License as described by
+ http://creativecommons.org/licenses/by-nc-nd/3.0/us/ . This is only
+ a licensing change for the documentation; the ``repoze.bfg``
+ software continues to be offered under the Repoze Public License
+ at http://repoze.org/license.html (BSD-like).
+
+Documentation
+-------------
+
+- Added manual index entries to generated index.
+
+- Document the previously existing (but non-API)
+ ``repoze.bfg.configuration.Configurator.setup_registry`` method as
+ an official API of a ``Configurator``.
+
+- Fix syntax errors in various documentation code blocks.
+
+- Created new top-level documentation section: "ZCML Directives".
+ This section contains detailed ZCML directive information, some of
+ which was removed from various narrative chapters.
+
+- The LaTeX rendering of the documentation has been improved.
+
+- Added a "Fore-Matter" section with author, copyright, and licensing
+ information.
+
+1.2a8 (2009-12-24)
+==================
+
+Features
+--------
+
+- Add a ``**kw`` arg to the ``Configurator.add_settings`` API.
+
+- Add ``hook_zca`` and ``unhook_zca`` methods to the ``Configurator``
+ API.
+
+- The ``repoze.bfg.testing.setUp`` method now returns a
+ ``Configurator`` instance which can be used to do further
+ configuration during unit tests.
+
+Bug Fixes
+---------
+
+- The ``json`` renderer failed to set the response content type to
+ ``application/json``. It now does, by setting
+ ``request.response_content_type`` unless this attribute is already
+ set.
+
+- The ``string`` renderer failed to set the response content type to
+ ``text/plain``. It now does, by setting
+ ``request.response_content_type`` unless this attribute is already
+ set.
+
+Documentation
+-------------
+
+- General documentation improvements by using better Sphinx roles such
+ as "class", "func", "meth", and so on. This means that there are
+ many more hyperlinks pointing to API documentation for API
+ definitions in all narrative, tutorial, and API documentation
+ elements.
+
+- Added a description of imperative configuration in various places
+ which only described ZCML configuration.
+
+- A syntactical refreshing of various tutorials.
+
+- Added the ``repoze.bfg.authentication``,
+ ``repoze.bfg.authorization``, and ``repoze.bfg.interfaces`` modules
+ to API documentation.
+
+Deprecations
+------------
+
+- The ``repoze.bfg.testing.registerRoutesMapper`` API (added in an
+ early 1.2 alpha) was deprecated. Its import now generates a
+ deprecation warning.
+
+1.2a7 (2009-12-20)
+==================
+
+Features
+--------
+
+- Add four new testing-related APIs to the
+ ``repoze.bfg.configuration.Configurator`` class:
+ ``testing_securitypolicy``, ``testing_models``,
+ ``testing_add_subscriber``, and ``testing_add_template``. These
+ were added in order to provide more direct access to the
+ functionality of the ``repoze.bfg.testing`` APIs named
+ ``registerDummySecurityPolicy``, ``registerModels``,
+ ``registerEventListener``, and ``registerTemplateRenderer`` when a
+ configurator is used. The ``testing`` APIs named are nominally
+ deprecated (although they will likely remain around "forever", as
+ they are in heavy use in the wild).
+
+- Add a new API to the ``repoze.bfg.configuration.Configurator``
+ class: ``add_settings``. This API can be used to add "settings"
+ (information returned within via the
+ ``repoze.bfg.settings.get_settings`` API) after the configurator has
+ been initially set up. This is most useful for testing purposes.
+
+- Add a ``custom_predicates`` argument to the ``Configurator``
+ ``add_view`` method, the ``bfg_view`` decorator and the attribute
+ list of the ZCML ``view`` directive. If ``custom_predicates`` is
+ specified, it must be a sequence of predicate callables (a predicate
+ callable accepts two arguments: ``context`` and ``request`` and
+ returns ``True`` or ``False``). The associated view callable will
+ only be invoked if all custom predicates return ``True``. Use one
+ or more custom predicates when no existing predefined predicate is
+ useful. Predefined and custom predicates can be mixed freely.
+
+- Add a ``custom_predicates`` argument to the ``Configurator``
+ ``add_route`` and the attribute list of the ZCML ``route``
+ directive. If ``custom_predicates`` is specified, it must be a
+ sequence of predicate callables (a predicate callable accepts two
+ arguments: ``context`` and ``request`` and returns ``True`` or
+ ``False``). The associated route will match will only be invoked if
+ all custom predicates return ``True``, else route matching
+ continues. Note that the value ``context`` will always be ``None``
+ when passed to a custom route predicate. Use one or more custom
+ predicates when no existing predefined predicate is useful.
+ Predefined and custom predicates can be mixed freely.
+
+Internal
+--------
+
+- Remove the ``repoze.bfg.testing.registerTraverser`` function. This
+ function was never an API.
+
+Documenation
+------------
+
+- Doc-deprecated most helper functions in the ``repoze.bfg.testing``
+ module. These helper functions likely won't be removed any time
+ soon, nor will they generate a warning any time soon, due to their
+ heavy use in the wild, but equivalent behavior exists in methods of
+ a Configurator.
+
+1.2a6 (2009-12-18)
+==================
+
+Features
+--------
+
+- The ``Configurator`` object now has two new methods: ``begin`` and
+ ``end``. The ``begin`` method is meant to be called before any
+ "configuration" begins (e.g. before ``add_view``, et. al are
+ called). The ``end`` method is meant to be called after all
+ "configuration" is complete.
+
+ Previously, before there was imperative configuration at all (1.1
+ and prior), configuration begin and end was invariably implied by
+ the process of loading a ZCML file. When a ZCML load happened, the
+ threadlocal data structure containing the request and registry was
+ modified before the load, and torn down after the load, making sure
+ that all framework code that needed ``get_current_registry`` for the
+ duration of the ZCML load was satisfied.
+
+ Some API methods called during imperative configuration, (such as
+ ``Configurator.add_view`` when a renderer is involved) end up for
+ historical reasons calling ``get_current_registry``. However, in
+ 1.2a5 and below, the Configurator supplied no functionality that
+ allowed people to make sure that ``get_current_registry`` returned
+ the registry implied by the configurator being used. ``begin`` now
+ serves this purpose. Inversely, ``end`` pops the thread local
+ stack, undoing the actions of ``begin``.
+
+ We make this boundary explicit to reduce the potential for confusion
+ when the configurator is used in different circumstances (e.g. in
+ unit tests and app code vs. just in initial app setup).
+
+ Existing code written for 1.2a1-1.2a5 which does not call ``begin``
+ or ``end`` continues to work in the same manner it did before. It
+ is however suggested that this code be changed to call ``begin`` and
+ ``end`` to reduce the potential for confusion in the future.
+
+- All ``paster`` templates which generate an application skeleton now
+ make use of the new ``begin`` and ``end`` methods of the
+ Configurator they use in their respective copies of ``run.py`` and
+ ``tests.py``.
+
+Documentation
+-------------
+
+- All documentation that makes use of a ``Configurator`` object to do
+ application setup and test setup now makes use of the new ``begin``
+ and ``end`` methods of the configurator.
+
+Bug Fixes
+---------
+
+- When a ``repoze.bfg.exceptions.NotFound`` or
+ ``repoze.bfg.exceptions.Forbidden`` *class* (as opposed to instance)
+ was raised as an exception within a root factory (or route root
+ factory), the exception would not be caught properly by the
+ ``repoze.bfg.`` Router and it would propagate to up the call stack,
+ as opposed to rendering the not found view or the forbidden view as
+ would have been expected.
+
+- When Chameleon page or text templates used as renderers were added
+ imperatively (via ``Configurator.add_view`` or some derivative),
+ they too-eagerly attempted to look up the ``reload_templates``
+ setting via ``get_settings``, meaning they were always registered in
+ non-auto-reload-mode (the default). Each now waits until its
+ respective ``template`` attribute is accessed to look up the value.
+
+- When a route with the same name as a previously registered route was
+ added, the old route was not removed from the mapper's routelist.
+ Symptom: the old registered route would be used (and possibly
+ matched) during route lookup when it should not have had a chance to
+ ever be used.
+
+1.2a5 (2009-12-10)
+==================
+
+Features
+--------
+
+- When the ``repoze.bfg.exceptions.NotFound`` or
+ ``repoze.bfg.exceptions.Forbidden`` error is raised from within a
+ custom root factory or the ``factory`` of a route, the appropriate
+ response is now sent to the requesting user agent (the result of the
+ notfound view or the forbidden view, respectively). When these
+ errors are raised from within a root factory, the ``context`` passed
+ to the notfound or forbidden view will be ``None``. Also, the
+ request will not be decorated with ``view_name``, ``subpath``,
+ ``context``, etc. as would normally be the case if traversal had
+ been allowed to take place.
+
+Internals
+---------
+
+- The exception class representing the error raised by various methods
+ of a ``Configurator`` is now importable as
+ ``repoze.bfg.exceptions.ConfigurationError``.
+
+Documentation
+-------------
+
+- General documentation freshening which takes imperative
+ configuration into account in more places and uses glossary
+ references more liberally.
+
+- Remove explanation of changing the request type in a new request
+ event subscriber, as other predicates are now usually an easier way
+ to get this done.
+
+- Added "Thread Locals" narrative chapter to documentation, and added
+ a API chapter documenting the ``repoze.bfg.threadlocals`` module.
+
+- Added a "Special Exceptions" section to the "Views" narrative
+ documentation chapter explaining the effect of raising
+ ``repoze.bfg.exceptions.NotFound`` and
+ ``repoze.bfg.exceptions.Forbidden`` from within view code.
+
+Dependencies
+------------
+
+- A new dependency on the ``twill`` package was added to the
+ ``setup.py`` ``tests_require`` argument (Twill will only be
+ downloaded when ``repoze.bfg`` ``setup.py test`` or ``setup.py
+ nosetests`` is invoked).
+
+1.2a4 (2009-12-07)
+==================
+
+Features
+--------
+
+- ``repoze.bfg.testing.DummyModel`` now accepts a new constructor
+ keyword argument: ``__provides__``. If this constructor argument is
+ provided, it should be an interface or a tuple of interfaces. The
+ resulting model will then provide these interfaces (they will be
+ attached to the constructed model via
+ ``zope.interface.alsoProvides``).
+
+Bug Fixes
+---------
+
+- Operation on GAE was broken, presumably because the
+ ``repoze.bfg.configuration`` module began to attempt to import the
+ ``repoze.bfg.chameleon_zpt`` and ``repoze.bfg.chameleon_text``
+ modules, and these cannot be used on non-CPython platforms. It now
+ tolerates startup time import failures for these modules, and only
+ raise an import error when a template from one of these packages is
+ actually used.
+
+1.2a3 (2009-12-02)
+==================
+
+Bug Fixes
+---------
+
+- The ``repoze.bfg.url.route_url`` function inappropriately passed
+ along ``_query`` and/or ``_anchor`` arguments to the
+ ``mapper.generate`` function, resulting in blowups.
+
+- When two views were registered with differering ``for`` interfaces
+ or classes, and the ``for`` of first view registered was a
+ superclass of the second, the ``repoze.bfg`` view machinery would
+ incorrectly associate the two views with the same "multiview".
+ Multiviews are meant to be collections of views that have *exactly*
+ the same for/request/viewname values, without taking inheritance
+ into account. Symptom: wrong view callable found even when you had
+ correctly specified a ``for_`` interface/class during view
+ configuration for one or both view configurations.
+
+Backwards Incompatibilities
+---------------------------
+
+- The ``repoze.bfg.templating`` module has been removed; it had been
+ deprecated in 1.1 and never actually had any APIs in it.
+
+1.2a2 (2009-11-29)
+==================
+
+Bug Fixes
+---------
+
+- The the long description of this package (as shown on PyPI) was not
+ valid reStructuredText, and so was not renderable.
+
+- Trying to use an HTTP method name string such as ``GET`` as a
+ ``request_type`` predicate argument caused a startup time failure
+ when it was encountered in imperative configuration or in a
+ decorator (symptom: ``Type Error: Required specification must be a
+ specification``). This now works again, although ``request_method``
+ is now the preferred predicate argument for associating a view
+ configuration with an HTTP request method.
+
+Documentation
+-------------
+
+- Fixed "Startup" narrative documentation chapter; it was explaining
+ "the old way" an application constructor worked.
+
+1.2a1 (2009-11-28)
+==================
+
+Features
+--------
+
+- An imperative configuration mode.
+
+ A ``repoze.bfg`` application can now begin its life as a single
+ Python file. Later, the application might evolve into a set of
+ Python files in a package. Even later, it might start making use of
+ other configuration features, such as ``ZCML``. But neither the use
+ of a package nor the use of non-imperative configuration is required
+ to create a simple ``repoze.bfg`` application any longer.
+
+ Imperative configuration makes ``repoze.bfg`` competetive with
+ "microframeworks" such as `Bottle <http://bottle.paws.de/>`_ and
+ `Tornado <http://www.tornadoweb.org/>`_. ``repoze.bfg`` has a good
+ deal of functionality that most microframeworks lack, so this is
+ hopefully a "best of both worlds" feature.
+
+ The simplest possible ``repoze.bfg`` application is now::
+
+ from webob import Response
+ from wsgiref import simple_server
+ from repoze.bfg.configuration import Configurator
+
+ def hello_world(request):
+ return Response('Hello world!')
+
+ if __name__ == '__main__':
+ config = Configurator()
+ config.add_view(hello_world)
+ app = config.make_wsgi_app()
+ simple_server.make_server('', 8080, app).serve_forever()
+
+- A new class now exists: ``repoze.bfg.configuration.Configurator``.
+ This class forms the basis for sharing machinery between
+ "imperatively" configured applications and traditional
+ declaratively-configured applications.
+
+- The ``repoze.bfg.testing.setUp`` function now accepts three extra
+ optional keyword arguments: ``registry``, ``request`` and
+ ``hook_zca``.
+
+ If the ``registry`` argument is not ``None``, the argument will be
+ treated as the registry that is set as the "current registry" (it
+ will be returned by ``repoze.bfg.threadlocal.get_current_registry``)
+ for the duration of the test. If the ``registry`` argument is
+ ``None`` (the default), a new registry is created and used for the
+ duration of the test.
+
+ The value of the ``request`` argument is used as the "current
+ request" (it will be returned by
+ ``repoze.bfg.threadlocal.get_current_request``) for the duration of
+ the test; it defaults to ``None``.
+
+ If ``hook_zca`` is ``True`` (the default), the
+ ``zope.component.getSiteManager`` function will be hooked with a
+ function that returns the value of ``registry`` (or the
+ default-created registry if ``registry`` is ``None``) instead of the
+ registry returned by ``zope.component.getGlobalSiteManager``,
+ causing the Zope Component Architecture API (``getSiteManager``,
+ ``getAdapter``, ``getUtility``, and so on) to use the testing
+ registry instead of the global ZCA registry.
+
+- The ``repoze.bfg.testing.tearDown`` function now accepts an
+ ``unhook_zca`` argument. If this argument is ``True`` (the
+ default), ``zope.component.getSiteManager.reset()`` will be called.
+ This will cause the result of the ``zope.component.getSiteManager``
+ function to be the global ZCA registry (the result of
+ ``zope.component.getGlobalSiteManager``) once again.
+
+- The ``run.py`` module in various ``repoze.bfg`` ``paster`` templates
+ now use a ``repoze.bfg.configuration.Configurator`` class instead of
+ the (now-legacy) ``repoze.bfg.router.make_app`` function to produce
+ a WSGI application.
+
+Documentation
+-------------
+
+- The documentation now uses the "request-only" view calling
+ convention in most examples (as opposed to the ``context, request``
+ convention). This is a documentation-only change; the ``context,
+ request`` convention is also supported and documented, and will be
+ "forever".
+
+- ``repoze.bfg.configuration`` API documentation has been added.
+
+- A narrative documentation chapter entitled "Creating Your First
+ ``repoze.bfg`` Application" has been added. This chapter details
+ usage of the new ``repoze.bfg.configuration.Configurator`` class,
+ and demonstrates a simplified "imperative-mode" configuration; doing
+ ``repoze.bfg`` application configuration imperatively was previously
+ much more difficult.
+
+- A narrative documentation chapter entitled "Configuration,
+ Decorations and Code Scanning" explaining ZCML- vs. imperative-
+ vs. decorator-based configuration equivalence.
+
+- The "ZCML Hooks" chapter has been renamed to "Hooks"; it documents
+ how to override hooks now via imperative configuration and ZCML.
+
+- The explanation about how to supply an alternate "response factory"
+ has been removed from the "Hooks" chapter. This feature may be
+ removed in a later release (it still works now, it's just not
+ documented).
+
+- Add a section entitled "Test Set Up and Tear Down" to the
+ unittesting chapter.
+
+Bug Fixes
+----------
+
+- The ACL authorization policy debugging output when
+ ``debug_authorization`` console debugging output was turned on
+ wasn't as clear as it could have been when a view execution was
+ denied due to an authorization failure resulting from the set of
+ principals passed never having matched any ACE in any ACL in the
+ lineage. Now in this case, we report ``<default deny>`` as the ACE
+ value and either the root ACL or ``<No ACL found on any object in
+ model lineage>`` if no ACL was found.
+
+- When two views were registered with the same ``accept`` argument,
+ but were otherwise registered with the same arguments, if a request
+ entered the application which had an ``Accept`` header that accepted
+ *either* of the media types defined by the set of views registered
+ with predicates that otherwise matched, a more or less "random" one
+ view would "win". Now, we try harder to use the view callable
+ associated with the view configuration that has the most specific
+ ``accept`` argument. Thanks to Alberto Valverde for an initial
+ patch.
+
+Internals
+---------
+
+- The routes mapper is no longer a root factory wrapper. It is now
+ consulted directly by the router.
+
+- The ``repoze.bfg.registry.make_registry`` callable has been removed.
+
+- The ``repoze.bfg.view.map_view`` callable has been removed.
+
+- The ``repoze.bfg.view.owrap_view`` callable has been removed.
+
+- The ``repoze.bfg.view.predicate_wrap`` callable has been removed.
+
+- The ``repoze.bfg.view.secure_view`` callable has been removed.
+
+- The ``repoze.bfg.view.authdebug_view`` callable has been removed.
+
+- The ``repoze.bfg.view.renderer_from_name`` callable has been
+ removed. Use ``repoze.bfg.configuration.Configurator.renderer_from_name``
+ instead (still not an API, however).
+
+- The ``repoze.bfg.view.derive_view`` callable has been removed. Use
+ ``repoze.bfg.configuration.Configurator.derive_view`` instead (still
+ not an API, however).
+
+- The ``repoze.bfg.settings.get_options`` callable has been removed.
+ Its job has been subsumed by the ``repoze.bfg.settings.Settings``
+ class constructor.
+
+- The ``repoze.bfg.view.requestonly`` function has been moved to
+ ``repoze.bfg.configuration.requestonly``.
+
+- The ``repoze.bfg.view.rendered_response`` function has been moved to
+ ``repoze.bfg.configuration.rendered_response``.
+
+- The ``repoze.bfg.view.decorate_view`` function has been moved to
+ ``repoze.bfg.configuration.decorate_view``.
+
+- The ``repoze.bfg.view.MultiView`` class has been moved to
+ ``repoze.bfg.configuration.MultiView``.
+
+- The ``repoze.bfg.zcml.Uncacheable`` class has been removed.
+
+- The ``repoze.bfg.resource.resource_spec`` function has been removed.
+
+- All ZCML directives which deal with attributes which are paths now
+ use the ``path`` method of the ZCML context to resolve a relative
+ name to an absolute one (imperative configuration requirement).
+
+- The ``repoze.bfg.scripting.get_root`` API now uses a 'real' WebOb
+ request rather than a FakeRequest when it sets up the request as a
+ threadlocal.
+
+- The ``repoze.bfg.traversal.traverse`` API now uses a 'real' WebOb
+ request rather than a FakeRequest when it calls the traverser.
+
+- The ``repoze.bfg.request.FakeRequest`` class has been removed.
+
+- Most uses of the ZCA threadlocal API (the ``getSiteManager``,
+ ``getUtility``, ``getAdapter``, ``getMultiAdapter`` threadlocal API)
+ have been removed from the core. Instead, when a threadlocal is
+ necessary, the core uses the
+ ``repoze.bfg.threadlocal.get_current_registry`` API to obtain the
+ registry.
+
+- The internal ILogger utility named ``repoze.bfg.debug`` is now just
+ an IDebugLogger unnamed utility. A named utility with the old name
+ is registered for b/w compat.
+
+- The ``repoze.bfg.interfaces.ITemplateRendererFactory`` interface was
+ removed; it has become unused.
+
+- Instead of depending on the ``martian`` package to do code scanning,
+ we now just use our own scanning routines.
+
+- We now no longer have a dependency on ``repoze.zcml`` package;
+ instead, the ``repoze.bfg`` package includes implementations of the
+ ``adapter``, ``subscriber`` and ``utility`` directives.
+
+- Relating to the following functions:
+
+ ``repoze.bfg.view.render_view``
+
+ ``repoze.bfg.view.render_view_to_iterable``
+
+ ``repoze.bfg.view.render_view_to_response``
+
+ ``repoze.bfg.view.append_slash_notfound_view``
+
+ ``repoze.bfg.view.default_notfound_view``
+
+ ``repoze.bfg.view.default_forbidden_view``
+
+ ``repoze.bfg.configuration.rendered_response``
+
+ ``repoze.bfg.security.has_permission``
+
+ ``repoze.bfg.security.authenticated_userid``
+
+ ``repoze.bfg.security.effective_principals``
+
+ ``repoze.bfg.security.view_execution_permitted``
+
+ ``repoze.bfg.security.remember``
+
+ ``repoze.bfg.security.forget``
+
+ ``repoze.bfg.url.route_url``
+
+ ``repoze.bfg.url.model_url``
+
+ ``repoze.bfg.url.static_url``
+
+ ``repoze.bfg.traversal.virtual_root``
+
+ Each of these functions now expects to be called with a request
+ object that has a ``registry`` attribute which represents the
+ current ``repoze.bfg`` registry. They fall back to obtaining the
+ registry from the threadlocal API.
+
+Backwards Incompatibilites
+--------------------------
+
+- Unit tests which use ``zope.testing.cleanup.cleanUp`` for the
+ purpose of isolating tests from one another may now begin to fail
+ due to lack of isolation between tests.
+
+ Here's why: In repoze.bfg 1.1 and prior, the registry returned by
+ ``repoze.bfg.threadlocal.get_current_registry`` when no other
+ registry had been pushed on to the threadlocal stack was the
+ ``zope.component.globalregistry.base`` global registry (aka the
+ result of ``zope.component.getGlobalSiteManager()``). In repoze.bfg
+ 1.2+, however, the registry returned in this situation is the new
+ module-scope ``repoze.bfg.registry.global_registry`` object. The
+ ``zope.testing.cleanup.cleanUp`` function clears the
+ ``zope.component.globalregistry.base`` global registry
+ unconditionally. However, it does not know about the
+ ``repoze.bfg.registry.global_registry`` object, so it does not clear
+ it.
+
+ If you use the ``zope.testing.cleanup.cleanUp`` function in the
+ ``setUp`` of test cases in your unit test suite instead of using the
+ (more correct as of 1.1) ``repoze.bfg.testing.setUp``, you will need
+ to replace all calls to ``zope.testing.cleanup.cleanUp`` with a call
+ to ``repoze.bfg.testing.setUp``.
+
+ If replacing all calls to ``zope.testing.cleanup.cleanUp`` with a
+ call to ``repoze.bfg.testing.setUp`` is infeasible, you can put this
+ bit of code somewhere that is executed exactly **once** (*not* for
+ each test in a test suite; in the `` __init__.py`` of your package
+ or your package's ``tests`` subpackage would be a reasonable
+ place)::
+
+ import zope.testing.cleanup
+ from repoze.bfg.testing import setUp
+ zope.testing.cleanup.addCleanUp(setUp)
+
+- When there is no "current registry" in the
+ ``repoze.bfg.threadlocal.manager`` threadlocal data structure (this
+ is the case when there is no "current request" or we're not in the
+ midst of a ``r.b.testing.setUp``-bounded unit test), the ``.get``
+ method of the manager returns a data structure containing a *global*
+ registry. In previous releases, this function returned the global
+ Zope "base" registry: the result of
+ ``zope.component.getGlobalSiteManager``, which is an instance of the
+ ``zope.component.registry.Component`` class. In this release,
+ however, the global registry returns a globally importable instance
+ of the ``repoze.bfg.registry.Registry`` class. This registry
+ instance can always be imported as
+ ``repoze.bfg.registry.global_registry``.
+
+ Effectively, this means that when you call
+ ``repoze.bfg.threadlocal.get_current_registry`` when no request or
+ ``setUp`` bounded unit test is in effect, you will always get back
+ the global registry that lives in
+ ``repoze.bfg.registry.global_registry``. It also means that
+ ``repoze.bfg`` APIs that *call* ``get_current_registry`` will use
+ this registry.
+
+ This change was made because ``repoze.bfg`` now expects the registry
+ it uses to have a slightly different API than a bare instance of
+ ``zope.component.registry.Components``.
+
+- View registration no longer registers a
+ ``repoze.bfg.interfaces.IViewPermission`` adapter (it is no longer
+ checked by the framework; since 1.1, views have been responsible for
+ providing their own security).
+
+- The ``repoze.bfg.router.make_app`` callable no longer accepts the
+ ``authentication_policy`` nor the ``authorization_policy``
+ arguments. This feature was deprecated in version 1.0 and has been
+ removed.
+
+- Obscure: the machinery which configured views with a
+ ``request_type`` *and* a ``route_name`` would ignore the request
+ interface implied by ``route_name`` registering a view only for the
+ interface implied by ``request_type``. In the unlikely event that
+ you were trying to use these two features together, the symptom
+ would have been that views that named a ``request_type`` but which
+ were also associated with routes were not found when the route
+ matched. Now if a view is configured with both a ``request_type``
+ and a ``route_name``, an error is raised.
+
+- The ``route`` ZCML directive now no longer accepts the
+ ``request_type`` or ``view_request_type`` attributes. These
+ attributes didn't actually work in any useful way (see entry above
+ this one).
+
+- Because the ``repoze.bfg`` package now includes implementations of
+ the ``adapter``, ``subscriber`` and ``utility`` ZCML directives, it
+ is now an error to have ``<include package="repoze.zcml"
+ file="meta.zcml"/>`` in the ZCML of a ``repoze.bfg`` application. A
+ ZCML conflict error will be raised if your ZCML does so. This
+ shouldn't be an issue for "normal" installations; it has always been
+ the responsibility of the ``repoze.bfg.includes`` ZCML to include
+ this file in the past; it now just doesn't.
+
+- The ``repoze.bfg.testing.zcml_configure`` API was removed. Use
+ the ``Configurator.load_zcml`` API instead.
+
+Deprecations
+------------
+
+- The ``repoze.bfg.router.make_app`` function is now nominally
+ deprecated. Its import and usage does not throw a warning, nor will
+ it probably ever disappear. However, using a
+ ``repoze.bfg.configuration.Configurator`` class is now the preferred
+ way to generate a WSGI application.
+
+ Note that ``make_app`` calls
+ ``zope.component.getSiteManager.sethook(
+ repoze.bfg.threadlocal.get_current_registry)`` on the caller's
+ behalf, hooking ZCA global API lookups, for backwards compatibility
+ purposes. If you disuse ``make_app``, your calling code will need
+ to perform this call itself, at least if your application uses the
+ ZCA global API (``getSiteManager``, ``getAdapter``, etc).
+
+Dependencies
+------------
+
+- A dependency on the ``martian`` package has been removed (its
+ functionality is replaced internally).
+
+- A dependency on the ``repoze.zcml`` package has been removed (its
+ functionality is replaced internally).
+
1.1.1 (2009-11-21)
==================
diff --git a/TODO.txt b/TODO.txt
index 777445fd1..e46de2eea 100644
--- a/TODO.txt
+++ b/TODO.txt
@@ -1,12 +1,41 @@
:mod:`repoze.bfg` TODOs
=======================
-- Decide on ``unhook_zca`` argument to ``tearDown``.
-
-- Named notfound views.
-
- Supply ``X-Vhm-Host`` support.
- Review tutorials.
- Basic WSGI documentation (pipeline / app / server).
+
+- Decide on INotFoundView and IForbidden interface, which are obsolete now.
+
+- Document exception view lookup machinery:
+
+ - Lookup proceeds by request interface first and then by interface provided
+ by exception.
+
+ - If lookup fails with more special request interface (read as request
+ interface related to some route) it will fallback to lookup by IRequest.
+
+ - Current order of interfaces used for exception view lookup leads to the
+ following statement: view with more special request interface and more
+ common context interfaces always matched first, event if we have view
+ with IRequest, but more special context interfaces (see integration tests
+ with hybridapp for route9).
+
+- Exception view backwards compat / features:
+
+ - Add an "exception" attr to the request before calling an exception
+ view.
+
+ - Register wrapper views within set_notfound_view and
+ set_forbidden_view (and ZCML if it doesn't call those) so that
+ "context" is either the "real" context or None.
+
+- Use Venusian for decorator scanning (fix Venusian to have scan
+ categories first).
+
+- Allow a translator to be supplied for template rendering.
+
+- Figure out a way to expose some of the functionality of
+ ``Configurator._derive_view`` as an API.
diff --git a/docs/narr/configuration.rst b/docs/narr/configuration.rst
index 66ecd486c..a3336e735 100644
--- a/docs/narr/configuration.rst
+++ b/docs/narr/configuration.rst
@@ -198,9 +198,8 @@ effectively a "macro" which calls the
behalf.
The ``<view>`` tag is an example of a :mod:`repoze.bfg` declaration
-tag. Other such tags include ``<route>``, ``<scan>``, ``<notfound>``,
-``<forbidden>``, and others. Each of these tags is effectively a
-"macro" which calls methods of a
+tag. Other such tags include ``<route>`` and ``<scan>``. Each of
+these tags is effectively a "macro" which calls methods of a
:class:`repoze.bfg.configuration.Configurator` object on your behalf.
Essentially, using a :term:`ZCML` file and loading it from the
diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst
index 9410f3b79..678b9dbc3 100644
--- a/docs/narr/hooks.rst
+++ b/docs/narr/hooks.rst
@@ -3,8 +3,8 @@
Using Hooks
===========
-"Hooks" can be used to influence the behavior of the
-:mod:`repoze.bfg` framework in various ways.
+"Hooks" can be used to influence the behavior of the :mod:`repoze.bfg`
+framework in various ways.
.. index::
single: not found view
@@ -15,23 +15,29 @@ Changing the Not Found View
---------------------------
When :mod:`repoze.bfg` can't map a URL to view code, it invokes a
-:term:`not found view`, which is a :term:`view callable`. The view it
-invokes can be customized through application configuration. This
-view can be configured via :term:`imperative configuration` or
-:term:`ZCML`.
+:term:`not found view`, which is a :term:`view callable`. A default
+notfound view exists. The default not found view can be overridden
+through application configuration. This override can be done via
+:term:`imperative configuration` or :term:`ZCML`.
+
+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:`repoze.bfg.exceptions.NotFound`
+class as the ``context`` of the view configuration.
.. topic:: Using Imperative Configuration
If your application uses :term:`imperative configuration`, you can
replace the Not Found view by using the
- :meth:`repoze.bfg.configuration.Configurator.set_notfound_view`
- method:
+ :meth:`repoze.bfg.configuration.Configurator.add_view` method to
+ register an "exception view":
.. code-block:: python
:linenos:
- import helloworld.views
- config.set_notfound_view(helloworld.views.notfound_view)
+ from repoze.bfg.exceptions import NotFound
+ from helloworld.views import notfound_view
+ config.add_view(notfound_view, context=NotFound)
Replace ``helloworld.views.notfound_view`` with a reference to the
Python :term:`view callable` you want to use to represent the Not
@@ -46,16 +52,22 @@ view can be configured via :term:`imperative configuration` or
.. code-block:: xml
:linenos:
- <notfound
- view="helloworld.views.notfound_view"/>
+ <view
+ view="helloworld.views.notfound_view"
+ context="repoze.bfg.exceptions.NotFound"/>
Replace ``helloworld.views.notfound_view`` with the Python dotted name
to the notfound view you want to use.
- Other attributes of the ``notfound`` directive are documented at
- :ref:`notfound_directive`.
+Like any other view, the notfound view must accept at least a
+``request`` parameter, or both ``context`` and ``request``. The
+``request`` is the current :term:`request` representing the denied
+action. The ``context`` (if used in the call signature) will be the
+instance of the :exc:`repoze.bfg.exceptions.NotFound` exception that
+caused the view to be called.
-Here's some sample code that implements a minimal NotFound view:
+Here's some sample code that implements a minimal NotFound view
+callable:
.. code-block:: python
:linenos:
@@ -65,13 +77,21 @@ Here's some sample code that implements a minimal NotFound view:
def notfound_view(request):
return HTTPNotFound()
-.. note:: When a NotFound view is invoked, it is passed a
- :term:`request`. The ``environ`` attribute of the request is the
- WSGI environment. Within the WSGI environ will be a key named
- ``repoze.bfg.message`` that has a value explaining why the not
- found error was raised. This error will be different when the
- ``debug_notfound`` environment setting is true than it is when it
- is false.
+.. note:: When a NotFound view callable is invoked, it is passed a
+ :term:`request`. The ``exception`` attribute of the request will
+ be an instance of the :exc:`repoze.bfg.exceptions.NotFound`
+ exception that caused the not found view to be called. The value
+ of ``request.exception.args[0]`` will be a value explaining why the
+ not found error was raised. This message will be different when
+ the ``debug_notfound`` environment setting is true than it is when
+ it is false.
+
+.. warning:: When a NotFound view callable accepts an argument list as
+ described in :ref:`request_and_context_view_definitions`, the
+ ``context`` passed as the first argument to the view callable will
+ be the :exc:`repoze.bfg.exceptions.NotFound` exception instance.
+ If available, the *model* context will still be available as
+ ``request.context``.
.. index::
single: forbidden view
@@ -84,21 +104,28 @@ Changing the Forbidden View
When :mod:`repoze.bfg` can't authorize execution of a view based on
the :term:`authorization policy` in use, it invokes a :term:`forbidden
view`. The default forbidden response has a 401 status code and is
-very plain, but it can be overridden as necessary using either
-:term:`imperative configuration` or :term:`ZCML`:
+very plain, but the view which generates it can be overridden as
+necessary using either :term:`imperative configuration` or
+:term:`ZCML`:
+
+The :term:`forbidden 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:`repoze.bfg.exceptions.Forbidden`
+class as the ``context`` of the view configuration.
.. topic:: Using Imperative Configuration
If your application uses :term:`imperative configuration`, you can
replace the Forbidden view by using the
- :meth:`repoze.bfg.configuration.Configurator.set_forbidden_view`
- method:
+ :meth:`repoze.bfg.configuration.Configurator.add_view` method to
+ register an "exception view":
.. code-block:: python
:linenos:
- import helloworld.views
- config.set_forbiddden_view(helloworld.views.forbidden_view)
+ from helloworld.views import forbidden_view
+ from repoze.bfg.exceptions import Forbidden
+ config.add_view(forbidden_view, context=Forbidden)
Replace ``helloworld.views.forbidden_view`` with a reference to the
Python :term:`view callable` you want to use to represent the
@@ -113,16 +140,13 @@ very plain, but it can be overridden as necessary using either
.. code-block:: xml
:linenos:
- <forbidden
- view="helloworld.views.forbidden_view"/>
-
+ <view
+ view="helloworld.views.notfound_view"
+ context="repoze.bfg.exceptions.Forbidden"/>
Replace ``helloworld.views.forbidden_view`` with the Python
dotted name to the forbidden view you want to use.
- Other attributes of the ``forbidden`` directive are documented at
- :ref:`forbidden_directive`.
-
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
@@ -140,13 +164,14 @@ Here's some sample code that implements a minimal forbidden view:
def forbidden_view(request):
return render_template_to_response('templates/login_form.pt')
-.. note:: When a forbidden view is invoked, it is passed the
- :term:`request` as the second argument. An attribute of the
- request is ``environ``, which is the WSGI environment. Within the
- WSGI environ will be a key named ``repoze.bfg.message`` that has a
- value explaining why the current view invocation was forbidden.
- This error will be different when the ``debug_authorization``
- environment setting is true than it is when it is false.
+.. note:: When a forbidden view callable is invoked, it is passed a
+ :term:`request`. The ``exception`` attribute of the request will
+ be an instance of the :exc:`repoze.bfg.exceptions.Forbidden`
+ exception that caused the forbidden view to be called. The value
+ of ``request.exception.args[0]`` will be a value explaining why the
+ forbidden was raised. This message will be different when the
+ ``debug_authorization`` environment setting is true than it is when
+ it is false.
.. warning:: the default forbidden view sends a response with a ``401
Unauthorized`` status code for backwards compatibility reasons.
diff --git a/docs/narr/urldispatch.rst b/docs/narr/urldispatch.rst
index 0c6b6cfe6..9e478ef2d 100644
--- a/docs/narr/urldispatch.rst
+++ b/docs/narr/urldispatch.rst
@@ -936,7 +936,8 @@ stanza:
.. code-block:: xml
:linenos:
- <notfound
+ <view
+ context="repoze.bfg.exceptions.NotFound"
view="repoze.bfg.views.append_slash_notfound_view"
/>
diff --git a/docs/narr/views.rst b/docs/narr/views.rst
index 030d19052..a24e4b7b5 100644
--- a/docs/narr/views.rst
+++ b/docs/narr/views.rst
@@ -133,7 +133,7 @@ represent the method expected to return a response, you can use an
.. _request_and_context_view_definitions:
-Request-And-Context View Callable Definitions
+Context-And-Request View Callable Definitions
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Usually, view callables are defined to accept only a single argument:
@@ -813,6 +813,8 @@ See also :ref:`renderer_directive` and
.. index::
single: view exceptions
+.. _special_exceptions_in_callables:
+
Using Special Exceptions In View Callables
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -836,7 +838,92 @@ agent which performed the request.
In all cases, the message provided to the exception constructor is
made available to the view which :mod:`repoze.bfg` invokes as
-``request.environ['repoze.bfg.message']``.
+``request.exception.args[0]``.
+
+Exception Views
+~~~~~~~~~~~~~~~~
+
+The machinery which allows the special
+:exc:`repoze.bfg.exceptions.NotFound` and
+:exc:`repoze.bfg.exceptions.Forbidden` exceptions to be caught by
+specialized views as described in
+:ref:`special_exceptions_in_callables` can also be used by application
+developers to convert arbitrary exceptions to responses.
+
+To register a view that should be called whenever a particular
+exception is raised from with :mod:`repoze.bfg` view code, use the
+exception class or one of its superclasses as the ``context`` of a
+view configuration which points at a view callable you'd like to
+generate a response.
+
+For example, given the following exception class in a module named
+``helloworld.exceptions``:
+
+.. code-block:: python
+ :linenos:
+
+ class ValidationFailure(Exception):
+ def __init__(self, msg):
+ self.msg = msg
+
+
+You can wire a view callable to be called whenever any of your *other*
+code raises a ``hellworld.exceptions.ValidationFailure`` exception:
+
+.. code-block:: python
+ :linenos:
+
+ from helloworld.exceptions import ValidationFailure
+
+ @bfg_view(context=ValidationFailure)
+ def failed_validation(exc, request):
+ response = Response('Failed validation: %s' % exc.msg)
+ response.status_int = 500
+ return response
+
+Assuming that a :term:`scan` was run to pick up this view
+registration, this view callable will be invoked whenever a
+``helloworld.exceptions.ValidationError`` is raised by your
+application's view code. The same exception raised by a custom root
+factory or a custom traverser is also caught and hooked.
+
+Other normal view predicates can also be used in combination with an
+exception view registration:
+
+.. code-block:: python
+ :linenos:
+
+ from repoze.bfg.view import bfg_view
+ from repoze.bfg.exceptions import NotFound
+ from webob.exc import HTTPNotFound
+
+ @bfg_view(context=NotFound, route_name='home')
+ def notfound_view(request):
+ return HTTPNotFound()
+
+The above exception view names the ``route_name`` of ``home``, meaning
+that it will only be called when the route matched has a name of
+``home``. You can therefore have more than one exception view for any
+given exception in the system: the "most specific" one will be called
+when the set of request circumstances which match the view
+registration.
+
+The only view predicate that cannot be not be used successfully when
+creating an exception view configuration is ``name``. The name used
+to look up an exception view is always the empty string. Views
+registered as exception views which have a name will be ignored.
+
+.. note::
+
+ Normal (non-exception) views registered against a context which
+ inherits from :exc:`Exception` will work normally. When an
+ exception view configuraton is processed, *two* exceptions are
+ registered. One as a "normal" view, the other as an "exception"
+ view. This means that you can use an exception as ``context`` for a
+ normal view.
+
+The feature can be used with any view registration mechanism
+(``@bfg_view`` decorator, ZCML, or imperative ``add_view`` styles).
.. index::
single: unicode, views, and forms
diff --git a/docs/tutorials/bfgwiki/authorization.rst b/docs/tutorials/bfgwiki/authorization.rst
index 1b83d3651..8c2ab1df9 100644
--- a/docs/tutorials/bfgwiki/authorization.rst
+++ b/docs/tutorials/bfgwiki/authorization.rst
@@ -27,7 +27,7 @@ Changing ``configure.zcml``
We'll change our ``configure.zcml`` file to enable an
``AuthTktAuthenticationPolicy`` and an ``ACLAuthorizationPolicy`` to
-enable declarative security checking. We'll also add a ``forbidden``
+enable declarative security checking. We'll also add a new view
stanza, which species a :term:`forbidden view`. This configures our
login view to show up when :mod:`repoze.bfg` detects that a view
invocation can not be authorized. When you're done, your
diff --git a/docs/tutorials/bfgwiki/src/authorization/tutorial/configure.zcml b/docs/tutorials/bfgwiki/src/authorization/tutorial/configure.zcml
index 837c04089..5297b9ee3 100644
--- a/docs/tutorials/bfgwiki/src/authorization/tutorial/configure.zcml
+++ b/docs/tutorials/bfgwiki/src/authorization/tutorial/configure.zcml
@@ -5,9 +5,10 @@
<scan package="."/>
- <forbidden
+ <view
view=".login.login"
- renderer="templates/login.pt"/>
+ renderer="templates/login.pt"
+ context="repoze.bfg.exceptions.Forbidden"/>
<authtktauthenticationpolicy
secret="sosecret"
diff --git a/docs/tutorials/bfgwiki2/authorization.rst b/docs/tutorials/bfgwiki2/authorization.rst
index 9a37760f1..2f1a9e082 100644
--- a/docs/tutorials/bfgwiki2/authorization.rst
+++ b/docs/tutorials/bfgwiki2/authorization.rst
@@ -84,9 +84,9 @@ Changing ``configure.zcml``
We'll change our ``configure.zcml`` file to enable an
``AuthTktAuthenticationPolicy`` and an ``ACLAuthorizationPolicy`` to
enable declarative security checking. We'll also change
-``configure.zcml`` to add a ``forbidden`` stanza which points at our
-``login`` :term:`view callable`, also known as a :term:`forbidden
-view`. This configures our newly created login view to show up when
+``configure.zcml`` to add a view stanza which points at our ``login``
+:term:`view callable`, also known as a :term:`forbidden view`. This
+configures our newly created login view to show up when
:mod:`repoze.bfg` detects that a view invocation can not be
authorized. Also, we'll add ``view_permission`` attributes with the
value ``edit`` to the ``edit_page`` and ``add_page`` route
diff --git a/docs/tutorials/bfgwiki2/src/authorization/tutorial/configure.zcml b/docs/tutorials/bfgwiki2/src/authorization/tutorial/configure.zcml
index 564cb7443..018892cd1 100644
--- a/docs/tutorials/bfgwiki2/src/authorization/tutorial/configure.zcml
+++ b/docs/tutorials/bfgwiki2/src/authorization/tutorial/configure.zcml
@@ -53,9 +53,10 @@
view_permission="edit"
/>
- <forbidden
+ <view
view=".login.login"
- renderer="templates/login.pt"/>
+ renderer="templates/login.pt"
+ for="repoze.bfg.exceptions.Forbidden"/>
<authtktauthenticationpolicy
secret="sosecret"
diff --git a/docs/zcml/forbidden.rst b/docs/zcml/forbidden.rst
index bd2235ccf..5a52a05ab 100644
--- a/docs/zcml/forbidden.rst
+++ b/docs/zcml/forbidden.rst
@@ -9,6 +9,14 @@ view`. The default forbidden response has a 401 status code and is
very plain, but it can be overridden as necessary using the
``forbidden`` ZCML directive.
+.. warning::
+
+ The ``forbidden`` ZCML directive is deprecated in :mod:`repoze.bfg`
+ version 1.3. Instead, you should use the :ref:`view_directive`
+ directive with a ``context`` that names the
+ :exc:`repoze.bfg.exceptions.Forbidden` class. See
+ :ref:`changing_the_forbidden_view` form more information.
+
Attributes
~~~~~~~~~~
@@ -63,8 +71,12 @@ Example
Alternatives
~~~~~~~~~~~~
-The :meth:`repoze.bfg.configuration.Configurator.set_forbidden_view`
-method performs the same job as the ``forbidden`` ZCML directive.
+Use the :ref:`view_directive` directive with a ``context`` that names
+the :exc:`repoze.bfg.exceptions.Forbidden` class.
+
+Use the :meth:`repoze.bfg.configuration.Configurator.add_view` method,
+passing it a ``context`` which is the
+:exc:`repoze.bfg.exceptions.Forbidden` class.
See Also
~~~~~~~~
diff --git a/docs/zcml/notfound.rst b/docs/zcml/notfound.rst
index 141cac6f9..3fe9900d4 100644
--- a/docs/zcml/notfound.rst
+++ b/docs/zcml/notfound.rst
@@ -3,6 +3,14 @@
``notfound``
------------
+.. warning::
+
+ The ``notfound`` ZCML directive is deprecated in :mod:`repoze.bfg`
+ version 1.3. Instead, you should use the :ref:`view_directive`
+ directive with a ``context`` that names the
+ :exc:`repoze.bfg.exceptions.NotFound` class. See
+ :ref:`changing_the_notfound_view` form more information.
+
When :mod:`repoze.bfg` can't map a URL to view code, it invokes a
:term:`not found view`. The default not found view is very plain, but
the view callable used can be configured via the ``notfound`` ZCML
@@ -62,8 +70,12 @@ Example
Alternatives
~~~~~~~~~~~~
-The :meth:`repoze.bfg.configuration.Configurator.set_notfound_view`
-method performs the same job as the ``notfound`` ZCML directive.
+Use the :ref:`view_directive` directive with a ``context`` that names
+the :exc:`repoze.bfg.exceptions.NotFound` class.
+
+Use the :meth:`repoze.bfg.configuration.Configurator.add_view` method,
+passing it a ``context`` which is the
+:exc:`repoze.bfg.exceptions.NotFound` class.
See Also
~~~~~~~~
diff --git a/repoze/bfg/compat/__init__.py b/repoze/bfg/compat/__init__.py
index c83bc4aec..b3585eac2 100644
--- a/repoze/bfg/compat/__init__.py
+++ b/repoze/bfg/compat/__init__.py
@@ -140,3 +140,9 @@ try:
except ImportError: #pragma: no cover
from pkgutil_26 import walk_packages
+try:
+ from hashlib import md5
+except ImportError: # pragma: no cover
+ import md5
+ md5 = md5.new
+
diff --git a/repoze/bfg/configuration.py b/repoze/bfg/configuration.py
index 025c6e048..3d1c0214d 100644
--- a/repoze/bfg/configuration.py
+++ b/repoze/bfg/configuration.py
@@ -2,6 +2,7 @@ import os
import re
import sys
import threading
+import types
import inspect
from webob import Response
@@ -17,9 +18,8 @@ from repoze.bfg.interfaces import IAuthenticationPolicy
from repoze.bfg.interfaces import IAuthorizationPolicy
from repoze.bfg.interfaces import IDebugLogger
from repoze.bfg.interfaces import IDefaultRootFactory
-from repoze.bfg.interfaces import IForbiddenView
+from repoze.bfg.interfaces import IExceptionViewClassifier
from repoze.bfg.interfaces import IMultiView
-from repoze.bfg.interfaces import INotFoundView
from repoze.bfg.interfaces import IPackageOverrides
from repoze.bfg.interfaces import IRendererFactory
from repoze.bfg.interfaces import IRequest
@@ -32,12 +32,14 @@ from repoze.bfg.interfaces import ISettings
from repoze.bfg.interfaces import ITemplateRenderer
from repoze.bfg.interfaces import ITraverser
from repoze.bfg.interfaces import IView
+from repoze.bfg.interfaces import IViewClassifier
from repoze.bfg import chameleon_text
from repoze.bfg import chameleon_zpt
from repoze.bfg import renderers
from repoze.bfg.authorization import ACLAuthorizationPolicy
from repoze.bfg.compat import all
+from repoze.bfg.compat import md5
from repoze.bfg.compat import walk_packages
from repoze.bfg.events import WSGIApplicationCreatedEvent
from repoze.bfg.exceptions import Forbidden
@@ -59,8 +61,11 @@ from repoze.bfg.traversal import find_interface
from repoze.bfg.urldispatch import RoutesMapper
from repoze.bfg.view import render_view_to_response
from repoze.bfg.view import static
+from repoze.bfg.view import default_notfound_view
+from repoze.bfg.view import default_forbidden_view
-MAX_WEIGHT = 10000
+MAX_ORDER = 1 << 30
+DEFAULT_PHASH = md5().hexdigest()
DEFAULT_RENDERERS = (
('.pt', chameleon_zpt.renderer_factory),
@@ -206,7 +211,8 @@ class Configurator(object):
def _derive_view(self, view, permission=None, predicates=(),
attr=None, renderer_name=None, wrapper_viewname=None,
- viewname=None, accept=None, score=MAX_WEIGHT):
+ viewname=None, accept=None, order=MAX_ORDER,
+ phash=DEFAULT_PHASH):
renderer = self._renderer_from_name(renderer_name)
authn_policy = self.registry.queryUtility(IAuthenticationPolicy)
authz_policy = self.registry.queryUtility(IAuthorizationPolicy)
@@ -220,7 +226,7 @@ class Configurator(object):
authn_policy, authz_policy, settings,
logger)
predicated_view = _predicate_wrap(debug_view, predicates)
- derived_view = _attr_wrap(predicated_view, accept, score)
+ derived_view = _attr_wrap(predicated_view, accept, order, phash)
return derived_view
def _override(self, package, path, override_package, override_prefix,
@@ -235,23 +241,6 @@ class Configurator(object):
name=pkg_name, info=_info)
override.insert(path, override_pkg_name, override_prefix)
-
- def _system_view(self, iface, view=None, attr=None, renderer=None,
- wrapper=None, _info=u''):
- if not view:
- if renderer:
- def view(context, request):
- return {}
- else:
- raise ConfigurationError(
- '"view" argument was not specified and no renderer '
- 'specified')
-
- derived_view = self._derive_view(view, attr=attr,
- renderer_name=renderer,
- wrapper_viewname=wrapper)
- self.registry.registerUtility(derived_view, iface, '', info=_info)
-
def _set_security_policies(self, authentication, authorization=None):
if authorization is None:
authorization = ACLAuthorizationPolicy() # default
@@ -312,6 +301,8 @@ class Configurator(object):
authorization_policy)
for name, renderer in renderers:
self.add_renderer(name, renderer)
+ self.set_notfound_view(default_notfound_view)
+ self.set_forbidden_view(default_forbidden_view)
# getSiteManager is a unit testing dep injection
def hook_zca(self, getSiteManager=None):
@@ -723,14 +714,15 @@ class Configurator(object):
view_info.append(info)
return
- score, predicates = _make_predicates(
+ order, predicates, phash = _make_predicates(
xhr=xhr, request_method=request_method, path_info=path_info,
request_param=request_param, header=header, accept=accept,
containment=containment, request_type=request_type,
custom=custom_predicates)
derived_view = self._derive_view(view, permission, predicates, attr,
- renderer, wrapper, name, accept, score)
+ renderer, wrapper, name, accept, order,
+ phash)
if context is None:
context = for_
@@ -764,41 +756,76 @@ class Configurator(object):
old_view = None
for view_type in (IView, ISecuredView, IMultiView):
- old_view = registered((request_iface, r_context), view_type, name)
+ old_view = registered((IViewClassifier, request_iface, r_context),
+ view_type, name)
if old_view is not None:
break
-
- if old_view is None:
- # No component was registered for any of our I*View
- # interfaces exactly; this is the first view for this
- # triad. We don't need a multiview.
- if hasattr(derived_view, '__call_permissive__'):
+
+ is_exception_view = isexception(context)
+
+ def regclosure():
+ if hasattr(view, '__call_permissive__'):
view_iface = ISecuredView
else:
view_iface = IView
- self.registry.registerAdapter(derived_view,
- (request_iface, context),
- view_iface, name, info=_info)
+ self.registry.registerAdapter(
+ derived_view, (IViewClassifier, request_iface, context),
+ view_iface, name, info=_info)
+ if is_exception_view:
+ self.registry.registerAdapter(
+ derived_view,
+ (IExceptionViewClassifier, request_iface, context),
+ view_iface, name, info=_info)
+
+ is_multiview = IMultiView.providedBy(old_view)
+ old_phash = getattr(old_view, '__phash__', DEFAULT_PHASH)
+
+ if old_view is None:
+ # - No component was yet registered for any of our I*View
+ # interfaces exactly; this is the first view for this
+ # triad.
+ regclosure()
+
+ elif (not is_multiview) and (old_phash == phash):
+ # - A single view component was previously registered with
+ # the same predicate hash as this view; this registration
+ # is therefore an override.
+ regclosure()
+
else:
+ # - A view or multiview was already registered for this
+ # triad, and the new view is not an override.
+
# XXX we could try to be more efficient here and register
# a non-secured view for a multiview if none of the
# multiview's consituent views have a permission
# associated with them, but this code is getting pretty
# rough already
- if IMultiView.providedBy(old_view):
+ if is_multiview:
multiview = old_view
else:
multiview = MultiView(name)
old_accept = getattr(old_view, '__accept__', None)
- old_score = getattr(old_view, '__score__', MAX_WEIGHT)
- multiview.add(old_view, old_score, old_accept)
- multiview.add(derived_view, score, accept)
+ old_order = getattr(old_view, '__order__', MAX_ORDER)
+ multiview.add(old_view, old_order, old_accept, old_phash)
+ multiview.add(derived_view, order, accept, phash)
for view_type in (IView, ISecuredView):
# unregister any existing views
self.registry.adapters.unregister(
- (request_iface, r_context), view_type, name=name)
- self.registry.registerAdapter(multiview, (request_iface, context),
- IMultiView, name, info=_info)
+ (IViewClassifier, request_iface, r_context),
+ view_type, name=name)
+ if is_exception_view:
+ self.registry.adapters.unregister(
+ (IExceptionViewClassifier, request_iface, r_context),
+ view_type, name=name)
+ self.registry.registerAdapter(
+ multiview, (IViewClassifier, request_iface, context),
+ IMultiView, name, info=_info)
+ if is_exception_view:
+ self.registry.registerAdapter(
+ multiview,
+ (IExceptionViewClassifier, request_iface, context),
+ IMultiView, name, info=_info)
def add_route(self,
name,
@@ -1030,13 +1057,15 @@ class Configurator(object):
"""
# these are route predicates; if they do not match, the next route
# in the routelist will be tried
- _, predicates = _make_predicates(xhr=xhr,
- request_method=request_method,
- path_info=path_info,
- request_param=request_param,
- header=header,
- accept=accept,
- custom=custom_predicates)
+ ignored, predicates, ignored = _make_predicates(
+ xhr=xhr,
+ request_method=request_method,
+ path_info=path_info,
+ request_param=request_param,
+ header=header,
+ accept=accept,
+ custom=custom_predicates
+ )
request_iface = self.registry.queryUtility(IRouteRequest, name=name)
@@ -1172,10 +1201,32 @@ class Configurator(object):
override(package, path, override_package, override_prefix,
_info=_info)
- def set_forbidden_view(self, *arg, **kw):
+ def set_forbidden_view(self, view=None, attr=None, renderer=None,
+ wrapper=None, _info=u''):
""" Add a default forbidden view to the current configuration
state.
+ .. warning:: This method has been deprecated in
+ :mod:`repoze.bfg` 1.3. *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.
+
+ ..note:: For backwards compatibility with :mod:`repoze.bfg`
+ 1.2, unlike an 'exception view' as described in
+ :ref:`exception_views`, a ``context, request`` view
+ callable registered using this API should not expect to
+ receive the exception as its first (``context``) argument.
+ Instead it should expect to receive the 'real' context as
+ found via context-finding or ``None`` if no context could
+ be found. The exception causing the registered view to be
+ called is however still available as ``request.exception``.
+ .. warning:: This method has been deprecated in
+ :mod:`repoze.bfg` 1.3. See
+ :ref:`changing_the_forbidden_view` to see how a not found
+ view should be registered in :mod:`repoze.bfg` 1.3+.
+
The ``view`` argument should be a :term:`view callable`.
The ``attr`` argument should be the attribute of the view
@@ -1191,16 +1242,36 @@ class Configurator(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).
-
- See :ref:`changing_the_forbidden_view` for more
- information."""
- return self._system_view(IForbiddenView, *arg, **kw)
-
- def set_notfound_view(self, *arg, **kw):
+ method's ``wrapper`` argument for a description)."""
+ view = self._derive_view(view, attr=attr, renderer_name=renderer)
+ def bwcompat_view(context, request):
+ ctx = getattr(request, 'context', None)
+ return view(ctx, request)
+ return self.add_view(bwcompat_view, context=Forbidden,
+ wrapper=wrapper, _info=_info)
+
+ def set_notfound_view(self, view=None, attr=None, renderer=None,
+ wrapper=None, _info=u''):
""" Add a default not found view to the current configuration
state.
+ .. warning:: This method has been deprecated in
+ :mod:`repoze.bfg` 1.3. *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.
+
+ ..note:: For backwards compatibility with :mod:`repoze.bfg`
+ 1.2, unlike an 'exception view' as described in
+ :ref:`exception_views`, a ``context, request`` view
+ callable registered using this API should not expect to
+ receive the exception as its first (``context``) argument.
+ Instead it should expect to receive the 'real' context as
+ found via context-finding or ``None`` if no context could
+ be found. The exception causing the registered view to be
+ called is however still available as ``request.exception``.
+
The ``view`` argument should be a :term:`view callable`.
The ``attr`` argument should be the attribute of the view
@@ -1217,11 +1288,13 @@ class Configurator(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).
-
- See :ref:`changing_the_notfound_view` for more
- information.
"""
- return self._system_view(INotFoundView, *arg, **kw)
+ view = self._derive_view(view, attr=attr, renderer_name=renderer)
+ def bwcompat_view(context, request):
+ ctx = getattr(request, 'context', None)
+ return view(ctx, request)
+ return self.add_view(bwcompat_view, context=NotFound,
+ wrapper=wrapper, _info=_info)
def add_static_view(self, name, path, cache_max_age=3600, _info=u''):
""" Add a view used to render static resources to the current
@@ -1353,46 +1426,68 @@ class Configurator(object):
def _make_predicates(xhr=None, request_method=None, path_info=None,
request_param=None, header=None, accept=None,
containment=None, request_type=None, custom=()):
- # Predicates are added to the predicate list in (presumed)
+
+ # PREDICATES
+ # ----------
+ #
+ # Given an argument list, a predicate list is computed.
+ # Predicates are added to a predicate list in (presumed)
# computation expense order. All predicates associated with a
- # view must evaluate true for the view to "match" a request.
- # Elsewhere in the code, we evaluate them using a generator
- # expression. The fastest predicate should be evaluated first,
- # then the next fastest, and so on, as if one returns false, the
- # remainder of the predicates won't need to be evaluated.
-
- # Each predicate is associated with a weight value. The weight
- # symbolizes the relative potential "importance" of the predicate
- # to all other predicates. A larger weight indicates greater
- # importance. These weights are subtracted from an aggregate
- # 'weight' variable. The aggregate weight is then divided by the
- # length of the predicate list to compute a "score" for this view.
- # The score represents the ordering in which a "multiview" ( a
+ # view or route must evaluate true for the view or route to
+ # "match" during a request. Elsewhere in the code, we evaluate
+ # predicates using a generator expression. The fastest predicate
+ # should be evaluated first, then the next fastest, and so on, as
+ # if one returns false, the remainder of the predicates won't need
+ # to be evaluated.
+ #
+ # While we compute predicates, we also compute a predicate hash
+ # (aka phash) that can be used by a caller to identify identical
+ # predicate lists.
+ #
+ # ORDERING
+ # --------
+ #
+ # A "order" is computed for the predicate list. An order is
+ # a scoring.
+ #
+ # Each predicate is associated with a weight value, which is a
+ # multiple of 2. The weight of a predicate symbolizes the
+ # relative potential "importance" of the predicate to all other
+ # predicates. A larger weight indicates greater importance.
+ #
+ # All weights for a given predicate list are bitwise ORed together
+ # to create a "score"; this score is then subtracted from
+ # MAX_ORDER and divided by an integer representing the number of
+ # predicates+1 to determine the order.
+ #
+ # The order represents the ordering in which a "multiview" ( a
# collection of views that share the same context/request/name
# triad but differ in other ways via predicates) will attempt to
- # call its set of views. Views with lower scores will be tried
+ # call its set of views. Views with lower orders will be tried
# first. The intent is to a) ensure that views with more
# predicates are always evaluated before views with fewer
# predicates and b) to ensure a stable call ordering of views that
- # share the same number of predicates.
-
- # Views which do not have any predicates get a score of
- # MAX_WEIGHT, meaning that they will be tried very last.
+ # share the same number of predicates. Views which do not have
+ # any predicates get an order of MAX_ORDER, meaning that they will
+ # be tried very last.
predicates = []
- weight = MAX_WEIGHT
+ weights = []
+ h = md5()
if xhr:
def xhr_predicate(context, request):
return request.is_xhr
- weight = weight - 20
+ weights.append(1 << 0)
predicates.append(xhr_predicate)
+ h.update('xhr:%r' % bool(xhr))
if request_method is not None:
def request_method_predicate(context, request):
return request.method == request_method
- weight = weight - 30
+ weights.append(1 << 2)
predicates.append(request_method_predicate)
+ h.update('request_method:%r' % request_method)
if path_info is not None:
try:
@@ -1401,8 +1496,9 @@ def _make_predicates(xhr=None, request_method=None, path_info=None,
raise ConfigurationError(why[0])
def path_info_predicate(context, request):
return path_info_val.match(request.path_info) is not None
- weight = weight - 40
+ weights.append(1 << 3)
predicates.append(path_info_predicate)
+ h.update('path_info:%r' % path_info)
if request_param is not None:
request_param_val = None
@@ -1412,8 +1508,9 @@ def _make_predicates(xhr=None, request_method=None, path_info=None,
if request_param_val is None:
return request_param in request.params
return request.params.get(request_param) == request_param_val
- weight = weight - 50
+ weights.append(1 << 4)
predicates.append(request_param_predicate)
+ h.update('request_param:%r=%r' % (request_param, request_param_val))
if header is not None:
header_name = header
@@ -1429,35 +1526,43 @@ def _make_predicates(xhr=None, request_method=None, path_info=None,
return header_name in request.headers
val = request.headers.get(header_name)
return header_val.match(val) is not None
- weight = weight - 60
+ weights.append(1 << 5)
predicates.append(header_predicate)
+ h.update('header:%r=%r' % (header_name, header_val))
if accept is not None:
def accept_predicate(context, request):
return accept in request.accept
- weight = weight - 70
+ weights.append(1 << 6)
predicates.append(accept_predicate)
+ h.update('accept:%r' % accept)
if containment is not None:
def containment_predicate(context, request):
return find_interface(context, containment) is not None
- weight = weight - 80
+ weights.append(1 << 7)
predicates.append(containment_predicate)
+ h.update('containment:%r' % id(containment))
if request_type is not None:
def request_type_predicate(context, request):
return request_type.providedBy(request)
- weight = weight - 90
+ weights.append(1 << 8)
predicates.append(request_type_predicate)
+ h.update('request_type:%r' % id(request_type))
if custom:
- for predicate in custom:
- weight = weight - 100
+ for num, predicate in enumerate(custom):
predicates.append(predicate)
+ h.update('custom%s:%r' % (num, id(predicate)))
+ weights.append(1 << 9)
- # this will be == MAX_WEIGHT if no predicates
- score = weight / (len(predicates) + 1)
- return score, predicates
+ score = 0
+ for bit in weights:
+ score = score | bit
+ order = (MAX_ORDER - score) / (len(predicates) + 1)
+ phash = h.hexdigest()
+ return order, predicates, phash
class MultiView(object):
implements(IMultiView)
@@ -1468,13 +1573,19 @@ class MultiView(object):
self.views = []
self.accepts = []
- def add(self, view, score, accept=None):
+ def add(self, view, order, accept=None, phash=None):
+ if phash is not None:
+ for i, (s, v, h) in enumerate(list(self.views)):
+ if phash == h:
+ self.views[i] = (order, view, phash)
+ return
+
if accept is None or '*' in accept:
- self.views.append((score, view))
+ self.views.append((order, view, phash))
self.views.sort()
else:
subset = self.media_views.setdefault(accept, [])
- subset.append((score, view))
+ subset.append((order, view, phash))
subset.sort()
accepts = set(self.accepts)
accepts.add(accept)
@@ -1496,7 +1607,7 @@ class MultiView(object):
return self.views
def match(self, context, request):
- for score, view in self.get_views(request):
+ for order, view, phash in self.get_views(request):
if not hasattr(view, '__predicated__'):
return view
if view.__predicated__(context, request):
@@ -1515,7 +1626,7 @@ class MultiView(object):
return view(context, request)
def __call__(self, context, request):
- for score, view in self.get_views(request):
+ for order, view, phash in self.get_views(request):
try:
return view(context, request)
except NotFound:
@@ -1548,7 +1659,7 @@ def decorate_view(wrapped_view, original_view):
except AttributeError:
pass
try:
- wrapped_view.__score__ = original_view.__score__
+ wrapped_view.__order__ = original_view.__order__
except AttributeError:
pass
return True
@@ -1795,19 +1906,26 @@ def _authdebug_view(view, permission, authn_policy, authz_policy, settings,
return wrapped_view
-def _attr_wrap(view, accept, score):
+def _attr_wrap(view, accept, order, phash):
# this is a little silly but we don't want to decorate the original
- # function with attributes that indicate accept and score,
+ # function with attributes that indicate accept, order, and phash,
# so we use a wrapper
- if (accept is None) and (score == MAX_WEIGHT):
+ if (accept is None) and (order == MAX_ORDER) and (phash == DEFAULT_PHASH):
return view # defaults
def attr_view(context, request):
return view(context, request)
attr_view.__accept__ = accept
- attr_view.__score__ = score
+ attr_view.__order__ = order
+ attr_view.__phash__ = phash
decorate_view(attr_view, view)
return attr_view
+def isclass(o):
+ return isinstance(o, (type, types.ClassType))
+
+def isexception(o):
+ return isinstance(o, Exception) or isclass(o) and issubclass(o, Exception)
+
# note that ``options`` is a b/w compat alias for ``settings`` and
# ``Configurator`` is a testing dep inj
def make_app(root_factory, package=None, filename='configure.zcml',
diff --git a/repoze/bfg/interfaces.py b/repoze/bfg/interfaces.py
index c322de2ff..53a905972 100644
--- a/repoze/bfg/interfaces.py
+++ b/repoze/bfg/interfaces.py
@@ -29,6 +29,9 @@ class IWSGIApplicationCreatedEvent(Interface):
class IRequest(Interface):
""" Request type interface attached to all request objects """
+# for exception view lookups
+IRequest.combined = IRequest
+
class IRouteRequest(Interface):
""" *internal only* interface used as in a utility lookup to find
route-specific interfaces. Not an API."""
@@ -78,6 +81,12 @@ class IResponseFactory(Interface):
should accept all the arguments that the webob.Response class
accepts)"""
+class IViewClassifier(Interface):
+ """ *Internal only* marker interface for views."""
+
+class IExceptionViewClassifier(Interface):
+ """ *Internal only* marker interface for exception views."""
+
class IView(Interface):
def __call__(context, request):
""" Must return an object that implements IResponse. May
@@ -99,7 +108,7 @@ class IMultiView(ISecuredView):
""" *internal only*. A multiview is a secured view that is a
collection of other views. Each of the views is associated with
zero or more predicates. Not an API."""
- def add(view, predicates, score):
+ def add(view, predicates, order, accept=None, phash=None):
""" Add a view to the multiview. """
class IRootFactory(Interface):
@@ -242,4 +251,4 @@ class IPackageOverrides(Interface):
# VH_ROOT_KEY is an interface; its imported from other packages (e.g.
# traversalwrapper)
-VH_ROOT_KEY = 'HTTP_X_VHM_ROOT'
+VH_ROOT_KEY = 'HTTP_X_VHM_ROOT'
diff --git a/repoze/bfg/request.py b/repoze/bfg/request.py
index 4dbbbb0df..8939e7b88 100644
--- a/repoze/bfg/request.py
+++ b/repoze/bfg/request.py
@@ -80,7 +80,11 @@ class Request(WebobRequest):
return self.environ.values()
def route_request_iface(name, bases=()):
- return InterfaceClass('%s_IRequest' % name, bases=bases)
+ iface = InterfaceClass('%s_IRequest' % name, bases=bases)
+ # for exception view lookups
+ iface.combined = InterfaceClass('%s_combined_IRequest' % name,
+ bases=(iface, IRequest))
+ return iface
def add_global_response_headers(request, headerlist):
attrs = request.__dict__
diff --git a/repoze/bfg/router.py b/repoze/bfg/router.py
index 13da06266..d63eceb32 100644
--- a/repoze/bfg/router.py
+++ b/repoze/bfg/router.py
@@ -2,8 +2,7 @@ from zope.interface import implements
from zope.interface import providedBy
from repoze.bfg.interfaces import IDebugLogger
-from repoze.bfg.interfaces import IForbiddenView
-from repoze.bfg.interfaces import INotFoundView
+from repoze.bfg.interfaces import IExceptionViewClassifier
from repoze.bfg.interfaces import IRequest
from repoze.bfg.interfaces import IRootFactory
from repoze.bfg.interfaces import IRouteRequest
@@ -12,19 +11,17 @@ from repoze.bfg.interfaces import IRoutesMapper
from repoze.bfg.interfaces import ISettings
from repoze.bfg.interfaces import ITraverser
from repoze.bfg.interfaces import IView
+from repoze.bfg.interfaces import IViewClassifier
from repoze.bfg.configuration import make_app # b/c import
from repoze.bfg.events import AfterTraversal
from repoze.bfg.events import NewRequest
from repoze.bfg.events import NewResponse
-from repoze.bfg.exceptions import Forbidden
from repoze.bfg.exceptions import NotFound
from repoze.bfg.request import Request
from repoze.bfg.threadlocal import manager
from repoze.bfg.traversal import DefaultRootFactory
from repoze.bfg.traversal import ModelGraphTraverser
-from repoze.bfg.view import default_forbidden_view
-from repoze.bfg.view import default_notfound_view
make_app # prevent pyflakes from complaining
@@ -37,8 +34,6 @@ class Router(object):
def __init__(self, registry):
q = registry.queryUtility
self.logger = q(IDebugLogger)
- self.notfound_view = q(INotFoundView, default=default_notfound_view)
- self.forbidden_view = q(IForbiddenView, default=default_forbidden_view)
self.root_factory = q(IRootFactory, default=DefaultRootFactory)
self.routes_mapper = q(IRoutesMapper)
self.root_policy = self.root_factory # b/w compat
@@ -56,6 +51,7 @@ class Router(object):
return an iterable.
"""
registry = self.registry
+ adapters = registry.adapters
has_listeners = registry.has_listeners
logger = self.logger
manager = self.threadlocal_manager
@@ -72,7 +68,7 @@ class Router(object):
has_listeners and registry.notify(NewRequest(request))
request_iface = IRequest
-
+
try:
# find the root
root_factory = self.root_factory
@@ -94,7 +90,6 @@ class Router(object):
attrs['root'] = root
# find a view callable
- adapters = registry.adapters
traverser = adapters.queryAdapter(root, ITraverser)
if traverser is None:
traverser = ModelGraphTraverser(root)
@@ -107,7 +102,7 @@ class Router(object):
has_listeners and registry.notify(AfterTraversal(request))
context_iface = providedBy(context)
view_callable = adapters.lookup(
- (request_iface, context_iface),
+ (IViewClassifier, request_iface, context_iface),
IView, name=view_name, default=None)
# invoke the view callable
@@ -125,25 +120,27 @@ class Router(object):
else:
msg = request.path_info
environ['repoze.bfg.message'] = msg
- response = self.notfound_view(context, request)
+ raise NotFound(msg)
else:
response = view_callable(context, request)
# handle exceptions raised during root finding and view lookup
- except Forbidden, why:
- try:
- msg = why[0]
- except (IndexError, TypeError):
- msg = ''
- environ['repoze.bfg.message'] = msg
- response = self.forbidden_view(context, request)
- except NotFound, why:
+ except Exception, why:
+ for_ = (IExceptionViewClassifier,
+ request_iface.combined,
+ providedBy(why))
+ view_callable = adapters.lookup(for_, IView, default=None)
+ if view_callable is None:
+ raise
+
try:
msg = why[0]
- except (IndexError, TypeError):
+ except Exception:
msg = ''
environ['repoze.bfg.message'] = msg
- response = self.notfound_view(context, request)
+
+ attrs['exception'] = why
+ response = view_callable(why, request)
# process the response
has_listeners and registry.notify(NewResponse(response))
@@ -159,7 +156,7 @@ class Router(object):
if 'global_response_headers' in attrs:
headers = list(headers)
headers.extend(attrs['global_response_headers'])
-
+
start_response(status, headers)
return app_iter
diff --git a/repoze/bfg/security.py b/repoze/bfg/security.py
index 822fd9ee7..cd1bae9a5 100644
--- a/repoze/bfg/security.py
+++ b/repoze/bfg/security.py
@@ -5,6 +5,7 @@ from zope.deprecation import deprecated
from repoze.bfg.interfaces import IAuthenticationPolicy
from repoze.bfg.interfaces import IAuthorizationPolicy
from repoze.bfg.interfaces import ISecuredView
+from repoze.bfg.interfaces import IViewClassifier
from repoze.bfg.exceptions import Forbidden as Unauthorized # b/c import
from repoze.bfg.threadlocal import get_current_registry
@@ -122,7 +123,7 @@ def view_execution_permitted(context, request, name=''):
reg = request.registry
except AttributeError:
reg = get_current_registry() # b/c
- provides = map(providedBy, (request, context))
+ provides = [IViewClassifier] + map(providedBy, (request, context))
view = reg.adapters.lookup(provides, ISecuredView, name=name)
if view is None:
return Allowed(
diff --git a/repoze/bfg/testing.py b/repoze/bfg/testing.py
index 4e9abdae0..b68539f43 100644
--- a/repoze/bfg/testing.py
+++ b/repoze/bfg/testing.py
@@ -14,6 +14,7 @@ from repoze.bfg.interfaces import IRequest
from repoze.bfg.interfaces import IRoutesMapper
from repoze.bfg.interfaces import ISecuredView
from repoze.bfg.interfaces import IView
+from repoze.bfg.interfaces import IViewClassifier
from repoze.bfg.interfaces import IViewPermission
from repoze.bfg.configuration import Configurator
@@ -165,6 +166,7 @@ def registerView(name, result='', view=None, for_=(Interface, Interface),
:meth:`repoze.bfg.configuration.Configurator.add_view``
method in your unit and integration tests.
"""
+ for_ = (IViewClassifier, ) + for_
if view is None:
def view(context, request):
return Response(result)
diff --git a/repoze/bfg/tests/exceptionviewapp/__init__.py b/repoze/bfg/tests/exceptionviewapp/__init__.py
new file mode 100644
index 000000000..ef5fe8b12
--- /dev/null
+++ b/repoze/bfg/tests/exceptionviewapp/__init__.py
@@ -0,0 +1 @@
+# a package
diff --git a/repoze/bfg/tests/exceptionviewapp/configure.zcml b/repoze/bfg/tests/exceptionviewapp/configure.zcml
new file mode 100644
index 000000000..680e065a6
--- /dev/null
+++ b/repoze/bfg/tests/exceptionviewapp/configure.zcml
@@ -0,0 +1,44 @@
+<configure xmlns="http://namespaces.repoze.org/bfg">
+
+ <include package="repoze.bfg.includes" />
+
+ <view view=".views.maybe"/>
+
+ <view context=".models.NotAnException"
+ view=".views.no"/>
+
+ <view context=".models.AnException"
+ view=".views.yes"/>
+
+ <view name="raise_exception"
+ view=".views.raise_exception"/>
+
+ <route name="route_raise_exception"
+ path="route_raise_exception"
+ view=".views.raise_exception"/>
+
+ <route name="route_raise_exception2"
+ path="route_raise_exception2"
+ view=".views.raise_exception"
+ factory=".models.route_factory"/>
+
+ <route name="route_raise_exception3"
+ path="route_raise_exception3"
+ view=".views.raise_exception"
+ factory=".models.route_factory2"/>
+
+ <view context=".models.AnException"
+ route_name="route_raise_exception3"
+ view=".views.whoa"/>
+
+ <route name="route_raise_exception4"
+ path="route_raise_exception4"
+ view=".views.raise_exception"/>
+
+ <view context=".models.AnException"
+ route_name="route_raise_exception4"
+ view=".views.whoa"/>
+
+</configure>
+
+
diff --git a/repoze/bfg/tests/exceptionviewapp/models.py b/repoze/bfg/tests/exceptionviewapp/models.py
new file mode 100644
index 000000000..fe407badc
--- /dev/null
+++ b/repoze/bfg/tests/exceptionviewapp/models.py
@@ -0,0 +1,18 @@
+
+class NotAnException(object):
+ pass
+
+class AnException(Exception):
+ pass
+
+class RouteContext(object):
+ pass
+
+class RouteContext2(object):
+ pass
+
+def route_factory(*arg):
+ return RouteContext()
+
+def route_factory2(*arg):
+ return RouteContext2()
diff --git a/repoze/bfg/tests/exceptionviewapp/views.py b/repoze/bfg/tests/exceptionviewapp/views.py
new file mode 100644
index 000000000..1432618cf
--- /dev/null
+++ b/repoze/bfg/tests/exceptionviewapp/views.py
@@ -0,0 +1,17 @@
+from webob import Response
+from models import AnException
+
+def no(request):
+ return Response('no')
+
+def yes(request):
+ return Response('yes')
+
+def maybe(request):
+ return Response('maybe')
+
+def whoa(request):
+ return Response('whoa')
+
+def raise_exception(request):
+ raise AnException()
diff --git a/repoze/bfg/tests/fixtureapp/configure.zcml b/repoze/bfg/tests/fixtureapp/configure.zcml
index b936b158e..e3470d47a 100644
--- a/repoze/bfg/tests/fixtureapp/configure.zcml
+++ b/repoze/bfg/tests/fixtureapp/configure.zcml
@@ -7,6 +7,21 @@
/>
<view
+ view=".views.exception_view"
+ for="RuntimeError"
+ />
+
+ <view
+ view=".views.protected_view"
+ name="protected.html"
+ />
+
+ <view
+ view=".views.erroneous_view"
+ name="error.html"
+ />
+
+ <view
view=".views.fixture_view"
name="dummyskin.html"
request_type=".views.IDummy"
diff --git a/repoze/bfg/tests/fixtureapp/views.py b/repoze/bfg/tests/fixtureapp/views.py
index d9bc0bb6e..862046d43 100644
--- a/repoze/bfg/tests/fixtureapp/views.py
+++ b/repoze/bfg/tests/fixtureapp/views.py
@@ -1,10 +1,22 @@
from zope.interface import Interface
from webob import Response
+from repoze.bfg.exceptions import Forbidden
def fixture_view(context, request):
""" """
return Response('fixture')
+def erroneous_view(context, request):
+ """ """
+ raise RuntimeError()
+
+def exception_view(context, request):
+ """ """
+ return Response('supressed')
+
+def protected_view(context, request):
+ """ """
+ raise Forbidden()
+
class IDummy(Interface):
pass
-
diff --git a/repoze/bfg/tests/hybridapp/configure.zcml b/repoze/bfg/tests/hybridapp/configure.zcml
index 56c6ea8db..a94409e26 100644
--- a/repoze/bfg/tests/hybridapp/configure.zcml
+++ b/repoze/bfg/tests/hybridapp/configure.zcml
@@ -58,4 +58,60 @@
use_global_views="True"
/>
+ <route
+ path="error"
+ name="route7"
+ />
+
+ <view
+ route_name="route7"
+ view=".views.erroneous_view"
+ />
+
+ <route
+ path="error2"
+ name="route8"
+ />
+
+ <view
+ route_name="route8"
+ view=".views.erroneous_view"
+ />
+
+ <!-- we want this view to "win" for route7 as exception view -->
+ <view
+ view=".views.exception_view"
+ for="RuntimeError"
+ />
+
+ <!-- we want this view to "win" for route8 as exception view-->
+ <view
+ route_name="route8"
+ view=".views.exception2_view"
+ for="RuntimeError"
+ />
+
+ <route
+ path="error_sub"
+ name="route9"
+ />
+
+ <view
+ route_name="route9"
+ view=".views.erroneous_sub_view"
+ />
+
+ <!-- we want this view to "win" for route9 as exception view... -->
+ <view
+ route_name="route9"
+ view=".views.exception2_view"
+ for=".views.SuperException"
+ />
+
+ <!-- ...even if we have more context-specialized view for raised exception -->
+ <view
+ view=".views.exception_view"
+ for=".views.SubException"
+ />
+
</configure>
diff --git a/repoze/bfg/tests/hybridapp/views.py b/repoze/bfg/tests/hybridapp/views.py
index 7f60ddbfe..135ef8290 100644
--- a/repoze/bfg/tests/hybridapp/views.py
+++ b/repoze/bfg/tests/hybridapp/views.py
@@ -15,3 +15,25 @@ def global2_view(request):
def route2_view(request):
""" """
return Response('route2')
+
+def exception_view(request):
+ """ """
+ return Response('supressed')
+
+def exception2_view(request):
+ """ """
+ return Response('supressed2')
+
+def erroneous_view(request):
+ """ """
+ raise RuntimeError()
+
+def erroneous_sub_view(request):
+ """ """
+ raise SubException()
+
+class SuperException(Exception):
+ """ """
+
+class SubException(SuperException):
+ """ """
diff --git a/repoze/bfg/tests/test_configuration.py b/repoze/bfg/tests/test_configuration.py
index 90413f1d6..99d564b91 100644
--- a/repoze/bfg/tests/test_configuration.py
+++ b/repoze/bfg/tests/test_configuration.py
@@ -21,16 +21,22 @@ class ConfiguratorTests(unittest.TestCase):
return Renderer
def _getViewCallable(self, config, ctx_iface=None, request_iface=None,
- name=''):
+ name='', exception_view=False):
from zope.interface import Interface
from repoze.bfg.interfaces import IRequest
from repoze.bfg.interfaces import IView
+ from repoze.bfg.interfaces import IViewClassifier
+ from repoze.bfg.interfaces import IExceptionViewClassifier
+ if exception_view:
+ classifier = IExceptionViewClassifier
+ else:
+ classifier = IViewClassifier
if ctx_iface is None:
ctx_iface = Interface
if request_iface is None:
request_iface = IRequest
return config.registry.adapters.lookup(
- (request_iface, ctx_iface), IView, name=name,
+ (classifier, request_iface, ctx_iface), IView, name=name,
default=None)
def _getRouteRequestIface(self, config, name):
@@ -182,6 +188,8 @@ class ConfiguratorTests(unittest.TestCase):
pass
reg = DummyRegistry()
config = self._makeOne(reg)
+ config.set_notfound_view = lambda *arg, **kw: None
+ config.set_forbidden_view = lambda *arg, **kw: None
config.setup_registry()
self.assertEqual(reg.has_listeners, True)
self.assertEqual(reg.notify(1), None)
@@ -558,54 +566,260 @@ class ConfiguratorTests(unittest.TestCase):
from zope.interface import Interface
from repoze.bfg.interfaces import IRequest
from repoze.bfg.interfaces import ISecuredView
+ from repoze.bfg.interfaces import IViewClassifier
view = lambda *arg: 'OK'
view.__call_permissive__ = view
config = self._makeOne()
config.add_view(view=view)
wrapper = config.registry.adapters.lookup(
- (IRequest, Interface), ISecuredView, name='', default=None)
+ (IViewClassifier, IRequest, Interface),
+ ISecuredView, name='', default=None)
self.assertEqual(wrapper, view)
+ def test_add_view_exception_register_secured_view(self):
+ from zope.interface import implementedBy
+ from repoze.bfg.interfaces import IRequest
+ from repoze.bfg.interfaces import IView
+ from repoze.bfg.interfaces import IExceptionViewClassifier
+ view = lambda *arg: 'OK'
+ view.__call_permissive__ = view
+ config = self._makeOne()
+ config.add_view(view=view, context=RuntimeError)
+ wrapper = config.registry.adapters.lookup(
+ (IExceptionViewClassifier, IRequest, implementedBy(RuntimeError)),
+ IView, name='', default=None)
+ self.assertEqual(wrapper, view)
+
+ def test_add_view_same_phash_overrides_existing_single_view(self):
+ from repoze.bfg.compat import md5
+ from zope.interface import Interface
+ from repoze.bfg.interfaces import IRequest
+ from repoze.bfg.interfaces import IView
+ from repoze.bfg.interfaces import IViewClassifier
+ from repoze.bfg.interfaces import IMultiView
+ phash = md5()
+ phash.update('xhr:True')
+ view = lambda *arg: 'NOT OK'
+ view.__phash__ = phash.hexdigest()
+ config = self._makeOne()
+ config.registry.registerAdapter(
+ view, (IViewClassifier, IRequest, Interface), IView, name='')
+ def newview(context, request):
+ return 'OK'
+ config.add_view(view=newview, xhr=True)
+ wrapper = self._getViewCallable(config)
+ self.failIf(IMultiView.providedBy(wrapper))
+ request = DummyRequest()
+ request.is_xhr = True
+ self.assertEqual(wrapper(None, request), 'OK')
+
+ def test_add_view_exc_same_phash_overrides_existing_single_view(self):
+ from repoze.bfg.compat import md5
+ from zope.interface import implementedBy
+ from repoze.bfg.interfaces import IRequest
+ from repoze.bfg.interfaces import IView
+ from repoze.bfg.interfaces import IExceptionViewClassifier
+ from repoze.bfg.interfaces import IMultiView
+ phash = md5()
+ phash.update('xhr:True')
+ view = lambda *arg: 'NOT OK'
+ view.__phash__ = phash.hexdigest()
+ config = self._makeOne()
+ config.registry.registerAdapter(
+ view,
+ (IExceptionViewClassifier, IRequest, implementedBy(RuntimeError)),
+ IView, name='')
+ def newview(context, request):
+ return 'OK'
+ config.add_view(view=newview, xhr=True,
+ context=RuntimeError)
+ wrapper = self._getViewCallable(
+ config, ctx_iface=implementedBy(RuntimeError), exception_view=True)
+ self.failIf(IMultiView.providedBy(wrapper))
+ request = DummyRequest()
+ request.is_xhr = True
+ self.assertEqual(wrapper(None, request), 'OK')
+
+ def test_add_view_default_phash_overrides_no_phash(self):
+ from zope.interface import Interface
+ from repoze.bfg.interfaces import IRequest
+ from repoze.bfg.interfaces import IView
+ from repoze.bfg.interfaces import IViewClassifier
+ from repoze.bfg.interfaces import IMultiView
+ view = lambda *arg: 'NOT OK'
+ config = self._makeOne()
+ config.registry.registerAdapter(
+ view, (IViewClassifier, IRequest, Interface), IView, name='')
+ def newview(context, request):
+ return 'OK'
+ config.add_view(view=newview)
+ wrapper = self._getViewCallable(config)
+ self.failIf(IMultiView.providedBy(wrapper))
+ request = DummyRequest()
+ request.is_xhr = True
+ self.assertEqual(wrapper(None, request), 'OK')
+
+ def test_add_view_exc_default_phash_overrides_no_phash(self):
+ from zope.interface import implementedBy
+ from repoze.bfg.interfaces import IRequest
+ from repoze.bfg.interfaces import IView
+ from repoze.bfg.interfaces import IExceptionViewClassifier
+ from repoze.bfg.interfaces import IMultiView
+ view = lambda *arg: 'NOT OK'
+ config = self._makeOne()
+ config.registry.registerAdapter(
+ view,
+ (IExceptionViewClassifier, IRequest, implementedBy(RuntimeError)),
+ IView, name='')
+ def newview(context, request):
+ return 'OK'
+ config.add_view(view=newview, context=RuntimeError)
+ wrapper = self._getViewCallable(
+ config, ctx_iface=implementedBy(RuntimeError), exception_view=True)
+ self.failIf(IMultiView.providedBy(wrapper))
+ request = DummyRequest()
+ request.is_xhr = True
+ self.assertEqual(wrapper(None, request), 'OK')
+
+ def test_add_view_default_phash_overrides_default_phash(self):
+ from repoze.bfg.configuration import DEFAULT_PHASH
+ from zope.interface import Interface
+ from repoze.bfg.interfaces import IRequest
+ from repoze.bfg.interfaces import IView
+ from repoze.bfg.interfaces import IViewClassifier
+ from repoze.bfg.interfaces import IMultiView
+ view = lambda *arg: 'NOT OK'
+ view.__phash__ = DEFAULT_PHASH
+ config = self._makeOne()
+ config.registry.registerAdapter(
+ view, (IViewClassifier, IRequest, Interface), IView, name='')
+ def newview(context, request):
+ return 'OK'
+ config.add_view(view=newview)
+ wrapper = self._getViewCallable(config)
+ self.failIf(IMultiView.providedBy(wrapper))
+ request = DummyRequest()
+ request.is_xhr = True
+ self.assertEqual(wrapper(None, request), 'OK')
+
+ def test_add_view_exc_default_phash_overrides_default_phash(self):
+ from repoze.bfg.configuration import DEFAULT_PHASH
+ from zope.interface import implementedBy
+ from repoze.bfg.interfaces import IRequest
+ from repoze.bfg.interfaces import IView
+ from repoze.bfg.interfaces import IExceptionViewClassifier
+ from repoze.bfg.interfaces import IMultiView
+ view = lambda *arg: 'NOT OK'
+ view.__phash__ = DEFAULT_PHASH
+ config = self._makeOne()
+ config.registry.registerAdapter(
+ view,
+ (IExceptionViewClassifier, IRequest, implementedBy(RuntimeError)),
+ IView, name='')
+ def newview(context, request):
+ return 'OK'
+ config.add_view(view=newview, context=RuntimeError)
+ wrapper = self._getViewCallable(
+ config, ctx_iface=implementedBy(RuntimeError), exception_view=True)
+ self.failIf(IMultiView.providedBy(wrapper))
+ request = DummyRequest()
+ request.is_xhr = True
+ self.assertEqual(wrapper(None, request), 'OK')
+
def test_add_view_multiview_replaces_existing_view(self):
from zope.interface import Interface
from repoze.bfg.interfaces import IRequest
from repoze.bfg.interfaces import IView
+ from repoze.bfg.interfaces import IViewClassifier
from repoze.bfg.interfaces import IMultiView
view = lambda *arg: 'OK'
+ view.__phash__ = 'abc'
config = self._makeOne()
config.registry.registerAdapter(
- view, (IRequest, Interface), IView, name='')
+ view, (IViewClassifier, IRequest, Interface), IView, name='')
config.add_view(view=view)
wrapper = self._getViewCallable(config)
self.failUnless(IMultiView.providedBy(wrapper))
self.assertEqual(wrapper(None, None), 'OK')
+ def test_add_view_exc_multiview_replaces_existing_view(self):
+ from zope.interface import implementedBy
+ from repoze.bfg.interfaces import IRequest
+ from repoze.bfg.interfaces import IView
+ from repoze.bfg.interfaces import IExceptionViewClassifier
+ from repoze.bfg.interfaces import IViewClassifier
+ from repoze.bfg.interfaces import IMultiView
+ view = lambda *arg: 'OK'
+ view.__phash__ = 'abc'
+ config = self._makeOne()
+ config.registry.registerAdapter(
+ view,
+ (IViewClassifier, IRequest, implementedBy(RuntimeError)),
+ IView, name='')
+ config.registry.registerAdapter(
+ view,
+ (IExceptionViewClassifier, IRequest, implementedBy(RuntimeError)),
+ IView, name='')
+ config.add_view(view=view, context=RuntimeError)
+ wrapper = self._getViewCallable(
+ config, ctx_iface=implementedBy(RuntimeError), exception_view=True)
+ self.failUnless(IMultiView.providedBy(wrapper))
+ self.assertEqual(wrapper(None, None), 'OK')
+
def test_add_view_multiview_replaces_existing_securedview(self):
from zope.interface import Interface
from repoze.bfg.interfaces import IRequest
from repoze.bfg.interfaces import ISecuredView
from repoze.bfg.interfaces import IMultiView
+ from repoze.bfg.interfaces import IViewClassifier
view = lambda *arg: 'OK'
+ view.__phash__ = 'abc'
config = self._makeOne()
config.registry.registerAdapter(
- view, (IRequest, Interface), ISecuredView, name='')
+ view, (IViewClassifier, IRequest, Interface),
+ ISecuredView, name='')
config.add_view(view=view)
wrapper = self._getViewCallable(config)
self.failUnless(IMultiView.providedBy(wrapper))
self.assertEqual(wrapper(None, None), 'OK')
+ def test_add_view_exc_multiview_replaces_existing_securedview(self):
+ from zope.interface import implementedBy
+ from repoze.bfg.interfaces import IRequest
+ from repoze.bfg.interfaces import ISecuredView
+ from repoze.bfg.interfaces import IMultiView
+ from repoze.bfg.interfaces import IViewClassifier
+ from repoze.bfg.interfaces import IExceptionViewClassifier
+ view = lambda *arg: 'OK'
+ view.__phash__ = 'abc'
+ config = self._makeOne()
+ config.registry.registerAdapter(
+ view,
+ (IViewClassifier, IRequest, implementedBy(RuntimeError)),
+ ISecuredView, name='')
+ config.registry.registerAdapter(
+ view,
+ (IExceptionViewClassifier, IRequest, implementedBy(RuntimeError)),
+ ISecuredView, name='')
+ config.add_view(view=view, context=RuntimeError)
+ wrapper = self._getViewCallable(
+ config, ctx_iface=implementedBy(RuntimeError), exception_view=True)
+ self.failUnless(IMultiView.providedBy(wrapper))
+ self.assertEqual(wrapper(None, None), 'OK')
+
def test_add_view_with_accept_multiview_replaces_existing_view(self):
from zope.interface import Interface
from repoze.bfg.interfaces import IRequest
from repoze.bfg.interfaces import IView
from repoze.bfg.interfaces import IMultiView
+ from repoze.bfg.interfaces import IViewClassifier
def view(context, request):
return 'OK'
def view2(context, request):
return 'OK2'
config = self._makeOne()
config.registry.registerAdapter(
- view, (IRequest, Interface), IView, name='')
+ view, (IViewClassifier, IRequest, Interface), IView, name='')
config.add_view(view=view2, accept='text/html')
wrapper = self._getViewCallable(config)
self.failUnless(IMultiView.providedBy(wrapper))
@@ -616,19 +830,52 @@ class ConfiguratorTests(unittest.TestCase):
request.accept = DummyAccept('text/html', 'text/html')
self.assertEqual(wrapper(None, request), 'OK2')
+ def test_add_view_exc_with_accept_multiview_replaces_existing_view(self):
+ from zope.interface import implementedBy
+ from repoze.bfg.interfaces import IRequest
+ from repoze.bfg.interfaces import IView
+ from repoze.bfg.interfaces import IMultiView
+ from repoze.bfg.interfaces import IViewClassifier
+ from repoze.bfg.interfaces import IExceptionViewClassifier
+ def view(context, request):
+ return 'OK'
+ def view2(context, request):
+ return 'OK2'
+ config = self._makeOne()
+ config.registry.registerAdapter(
+ view,
+ (IViewClassifier, IRequest, implementedBy(RuntimeError)),
+ IView, name='')
+ config.registry.registerAdapter(
+ view,
+ (IExceptionViewClassifier, IRequest, implementedBy(RuntimeError)),
+ IView, name='')
+ config.add_view(view=view2, accept='text/html', context=RuntimeError)
+ wrapper = self._getViewCallable(
+ config, ctx_iface=implementedBy(RuntimeError), exception_view=True)
+ self.failUnless(IMultiView.providedBy(wrapper))
+ self.assertEqual(len(wrapper.views), 1)
+ self.assertEqual(len(wrapper.media_views), 1)
+ self.assertEqual(wrapper(None, None), 'OK')
+ request = DummyRequest()
+ request.accept = DummyAccept('text/html', 'text/html')
+ self.assertEqual(wrapper(None, request), 'OK2')
+
def test_add_view_multiview_replaces_existing_view_with___accept__(self):
from zope.interface import Interface
from repoze.bfg.interfaces import IRequest
from repoze.bfg.interfaces import IView
from repoze.bfg.interfaces import IMultiView
+ from repoze.bfg.interfaces import IViewClassifier
def view(context, request):
return 'OK'
def view2(context, request):
return 'OK2'
view.__accept__ = 'text/html'
+ view.__phash__ = 'abc'
config = self._makeOne()
config.registry.registerAdapter(
- view, (IRequest, Interface), IView, name='')
+ view, (IViewClassifier, IRequest, Interface), IView, name='')
config.add_view(view=view2)
wrapper = self._getViewCallable(config)
self.failUnless(IMultiView.providedBy(wrapper))
@@ -639,19 +886,78 @@ class ConfiguratorTests(unittest.TestCase):
request.accept = DummyAccept('text/html')
self.assertEqual(wrapper(None, request), 'OK')
+ def test_add_view_exc_mulview_replaces_existing_view_with___accept__(self):
+ from zope.interface import implementedBy
+ from repoze.bfg.interfaces import IRequest
+ from repoze.bfg.interfaces import IView
+ from repoze.bfg.interfaces import IMultiView
+ from repoze.bfg.interfaces import IViewClassifier
+ from repoze.bfg.interfaces import IExceptionViewClassifier
+ def view(context, request):
+ return 'OK'
+ def view2(context, request):
+ return 'OK2'
+ view.__accept__ = 'text/html'
+ view.__phash__ = 'abc'
+ config = self._makeOne()
+ config.registry.registerAdapter(
+ view,
+ (IViewClassifier, IRequest, implementedBy(RuntimeError)),
+ IView, name='')
+ config.registry.registerAdapter(
+ view,
+ (IExceptionViewClassifier, IRequest, implementedBy(RuntimeError)),
+ IView, name='')
+ config.add_view(view=view2, context=RuntimeError)
+ wrapper = self._getViewCallable(
+ config, ctx_iface=implementedBy(RuntimeError), exception_view=True)
+ self.failUnless(IMultiView.providedBy(wrapper))
+ self.assertEqual(len(wrapper.views), 1)
+ self.assertEqual(len(wrapper.media_views), 1)
+ self.assertEqual(wrapper(None, None), 'OK2')
+ request = DummyRequest()
+ request.accept = DummyAccept('text/html')
+ self.assertEqual(wrapper(None, request), 'OK')
+
def test_add_view_multiview_replaces_multiview(self):
from zope.interface import Interface
from repoze.bfg.interfaces import IRequest
from repoze.bfg.interfaces import IMultiView
+ from repoze.bfg.interfaces import IViewClassifier
view = DummyMultiView()
config = self._makeOne()
- config.registry.registerAdapter(view, (IRequest, Interface),
- IMultiView, name='')
+ config.registry.registerAdapter(
+ view, (IViewClassifier, IRequest, Interface),
+ IMultiView, name='')
view2 = lambda *arg: 'OK2'
config.add_view(view=view2)
wrapper = self._getViewCallable(config)
self.failUnless(IMultiView.providedBy(wrapper))
- self.assertEqual(wrapper.views, [(view2, None)])
+ self.assertEqual([x[:2] for x in wrapper.views], [(view2, None)])
+ self.assertEqual(wrapper(None, None), 'OK1')
+
+ def test_add_view_exc_multiview_replaces_multiview(self):
+ from zope.interface import implementedBy
+ from repoze.bfg.interfaces import IRequest
+ from repoze.bfg.interfaces import IMultiView
+ from repoze.bfg.interfaces import IViewClassifier
+ from repoze.bfg.interfaces import IExceptionViewClassifier
+ view = DummyMultiView()
+ config = self._makeOne()
+ config.registry.registerAdapter(
+ view,
+ (IViewClassifier, IRequest, implementedBy(RuntimeError)),
+ IMultiView, name='')
+ config.registry.registerAdapter(
+ view,
+ (IExceptionViewClassifier, IRequest, implementedBy(RuntimeError)),
+ IMultiView, name='')
+ view2 = lambda *arg: 'OK2'
+ config.add_view(view=view2, context=RuntimeError)
+ wrapper = self._getViewCallable(
+ config, ctx_iface=implementedBy(RuntimeError), exception_view=True)
+ self.failUnless(IMultiView.providedBy(wrapper))
+ self.assertEqual([x[:2] for x in wrapper.views], [(view2, None)])
self.assertEqual(wrapper(None, None), 'OK1')
def test_add_view_multiview_context_superclass_then_subclass(self):
@@ -659,6 +965,7 @@ class ConfiguratorTests(unittest.TestCase):
from repoze.bfg.interfaces import IRequest
from repoze.bfg.interfaces import IView
from repoze.bfg.interfaces import IMultiView
+ from repoze.bfg.interfaces import IViewClassifier
class ISuper(Interface):
pass
class ISub(ISuper):
@@ -667,7 +974,7 @@ class ConfiguratorTests(unittest.TestCase):
view2 = lambda *arg: 'OK2'
config = self._makeOne()
config.registry.registerAdapter(
- view, (IRequest, ISuper), IView, name='')
+ view, (IViewClassifier, IRequest, ISuper), IView, name='')
config.add_view(view=view2, for_=ISub)
wrapper = self._getViewCallable(config, ISuper, IRequest)
self.failIf(IMultiView.providedBy(wrapper))
@@ -676,6 +983,40 @@ class ConfiguratorTests(unittest.TestCase):
self.failIf(IMultiView.providedBy(wrapper))
self.assertEqual(wrapper(None, None), 'OK2')
+ def test_add_view_multiview_exception_superclass_then_subclass(self):
+ from zope.interface import implementedBy
+ from repoze.bfg.interfaces import IRequest
+ from repoze.bfg.interfaces import IView
+ from repoze.bfg.interfaces import IMultiView
+ from repoze.bfg.interfaces import IViewClassifier
+ from repoze.bfg.interfaces import IExceptionViewClassifier
+ class Super(Exception):
+ pass
+ class Sub(Super):
+ pass
+ view = lambda *arg: 'OK'
+ view2 = lambda *arg: 'OK2'
+ config = self._makeOne()
+ config.registry.registerAdapter(
+ view, (IViewClassifier, IRequest, Super), IView, name='')
+ config.registry.registerAdapter(
+ view, (IExceptionViewClassifier, IRequest, Super), IView, name='')
+ config.add_view(view=view2, for_=Sub)
+ wrapper = self._getViewCallable(
+ config, implementedBy(Super), IRequest)
+ wrapper_exc_view = self._getViewCallable(
+ config, implementedBy(Super), IRequest, exception_view=True)
+ self.assertEqual(wrapper_exc_view, wrapper)
+ self.failIf(IMultiView.providedBy(wrapper_exc_view))
+ self.assertEqual(wrapper_exc_view(None, None), 'OK')
+ wrapper = self._getViewCallable(
+ config, implementedBy(Sub), IRequest)
+ wrapper_exc_view = self._getViewCallable(
+ config, implementedBy(Sub), IRequest, exception_view=True)
+ self.assertEqual(wrapper_exc_view, wrapper)
+ self.failIf(IMultiView.providedBy(wrapper_exc_view))
+ self.assertEqual(wrapper_exc_view(None, None), 'OK2')
+
def test_add_view_multiview_call_ordering(self):
from zope.interface import directlyProvides
def view1(context, request): return 'view1'
@@ -821,6 +1162,37 @@ class ConfiguratorTests(unittest.TestCase):
self.failIfEqual(wrapper, None)
self.assertEqual(wrapper(None, None), 'OK')
+ def test_add_view_with_route_name_exception(self):
+ from zope.interface import implementedBy
+ from zope.component import ComponentLookupError
+ view = lambda *arg: 'OK'
+ config = self._makeOne()
+ config.add_view(view=view, route_name='foo', context=RuntimeError)
+ self.assertEqual(len(config.registry.deferred_route_views), 1)
+ infos = config.registry.deferred_route_views['foo']
+ self.assertEqual(len(infos), 1)
+ info = infos[0]
+ self.assertEqual(info['route_name'], 'foo')
+ self.assertEqual(info['view'], view)
+ self.assertRaises(ComponentLookupError,
+ self._getRouteRequestIface, config, 'foo')
+ wrapper_exc_view = self._getViewCallable(
+ config, ctx_iface=implementedBy(RuntimeError),
+ exception_view=True)
+ self.assertEqual(wrapper_exc_view, None)
+ config.add_route('foo', '/a/b')
+ request_iface = self._getRouteRequestIface(config, 'foo')
+ self.failIfEqual(request_iface, None)
+ wrapper_exc_view = self._getViewCallable(
+ config, ctx_iface=implementedBy(RuntimeError),
+ request_iface=request_iface, exception_view=True)
+ self.failIfEqual(wrapper_exc_view, None)
+ wrapper = self._getViewCallable(
+ config, ctx_iface=implementedBy(RuntimeError),
+ request_iface=request_iface)
+ self.assertEqual(wrapper_exc_view, wrapper)
+ self.assertEqual(wrapper_exc_view(None, None), 'OK')
+
def test_add_view_with_request_method_true(self):
view = lambda *arg: 'OK'
config = self._makeOne()
@@ -1048,6 +1420,16 @@ class ConfiguratorTests(unittest.TestCase):
request.is_xhr = True
self.assertEqual(wrapper(None, request), 'OK')
+ def test_add_view_same_predicates(self):
+ view2 = lambda *arg: 'second'
+ view1 = lambda *arg: 'first'
+ config = self._makeOne()
+ config.add_view(view=view1)
+ config.add_view(view=view2)
+ view = self._getViewCallable(config)
+ request = self._makeRequest(config)
+ self.assertEqual(view(None, request), 'second')
+
def _assertRoute(self, config, name, path, num_predicates=0):
from repoze.bfg.interfaces import IRoutesMapper
mapper = config.registry.getUtility(IRoutesMapper)
@@ -1164,6 +1546,22 @@ class ConfiguratorTests(unittest.TestCase):
wrapper = self._getViewCallable(config, IOther, request_type)
self.assertEqual(wrapper, None)
+ def test_add_route_with_view_exception(self):
+ from zope.interface import implementedBy
+ config = self._makeOne()
+ view = lambda *arg: 'OK'
+ config.add_route('name', 'path', view=view, view_context=RuntimeError)
+ request_type = self._getRouteRequestIface(config, 'name')
+ wrapper = self._getViewCallable(
+ config, ctx_iface=implementedBy(RuntimeError),
+ request_iface=request_type, exception_view=True)
+ self.assertEqual(wrapper(None, None), 'OK')
+ self._assertRoute(config, 'name', 'path')
+ wrapper = self._getViewCallable(
+ config, ctx_iface=IOther,
+ request_iface=request_type, exception_view=True)
+ self.assertEqual(wrapper, None)
+
def test_add_route_with_view_for(self):
config = self._makeOne()
view = lambda *arg: 'OK'
@@ -1280,6 +1678,7 @@ class ConfiguratorTests(unittest.TestCase):
from zope.interface import implementedBy
from repoze.bfg.static import StaticRootFactory
from repoze.bfg.interfaces import IView
+ from repoze.bfg.interfaces import IViewClassifier
config = self._makeOne()
config.add_static_view('static', 'fixtures/static')
request_type = self._getRouteRequestIface(config, 'static')
@@ -1287,7 +1686,7 @@ class ConfiguratorTests(unittest.TestCase):
self.assertEqual(route.factory.__class__, StaticRootFactory)
iface = implementedBy(StaticRootFactory)
wrapped = config.registry.adapters.lookup(
- (request_type, iface), IView, name='')
+ (IViewClassifier, request_type, iface), IView, name='')
request = self._makeRequest(config)
self.assertEqual(wrapped(None, request).__class__, PackageURLParser)
@@ -1296,6 +1695,7 @@ class ConfiguratorTests(unittest.TestCase):
from zope.interface import implementedBy
from repoze.bfg.static import StaticRootFactory
from repoze.bfg.interfaces import IView
+ from repoze.bfg.interfaces import IViewClassifier
config = self._makeOne()
config.add_static_view('static', 'repoze.bfg.tests:fixtures/static')
request_type = self._getRouteRequestIface(config, 'static')
@@ -1303,7 +1703,7 @@ class ConfiguratorTests(unittest.TestCase):
self.assertEqual(route.factory.__class__, StaticRootFactory)
iface = implementedBy(StaticRootFactory)
wrapped = config.registry.adapters.lookup(
- (request_type, iface), IView, name='')
+ (IViewClassifier, request_type, iface), IView, name='')
request = self._makeRequest(config)
self.assertEqual(wrapped(None, request).__class__, PackageURLParser)
@@ -1313,6 +1713,7 @@ class ConfiguratorTests(unittest.TestCase):
from zope.interface import implementedBy
from repoze.bfg.static import StaticRootFactory
from repoze.bfg.interfaces import IView
+ from repoze.bfg.interfaces import IViewClassifier
config = self._makeOne()
here = os.path.dirname(__file__)
static_path = os.path.join(here, 'fixtures', 'static')
@@ -1322,76 +1723,63 @@ class ConfiguratorTests(unittest.TestCase):
self.assertEqual(route.factory.__class__, StaticRootFactory)
iface = implementedBy(StaticRootFactory)
wrapped = config.registry.adapters.lookup(
- (request_type, iface), IView, name='')
+ (IViewClassifier, request_type, iface), IView, name='')
request = self._makeRequest(config)
self.assertEqual(wrapped(None, request).__class__, StaticURLParser)
- def test__system_view_no_view_no_renderer(self):
- from repoze.bfg.exceptions import ConfigurationError
- config = self._makeOne()
- self.assertRaises(ConfigurationError, config._system_view, IDummy)
-
- def test__system_view_no_view_with_renderer(self):
+ def test_set_notfound_view(self):
+ from zope.interface import implementedBy
+ from repoze.bfg.interfaces import IRequest
+ from repoze.bfg.exceptions import NotFound
config = self._makeOne()
- self._registerRenderer(config, name='.pt')
- config._system_view(IDummy,
- renderer='repoze.bfg.tests:fixtures/minimal.pt')
+ view = lambda *arg: arg
+ config.set_notfound_view(view)
request = self._makeRequest(config)
- view = config.registry.getUtility(IDummy)
+ view = self._getViewCallable(config, ctx_iface=implementedBy(NotFound),
+ request_iface=IRequest)
result = view(None, request)
- self.assertEqual(result.body, 'Hello!')
+ self.assertEqual(result, (None, request))
- def test__system_view_with_attr(self):
+ def test_set_notfound_view_request_has_context(self):
+ from zope.interface import implementedBy
+ from repoze.bfg.interfaces import IRequest
+ from repoze.bfg.exceptions import NotFound
config = self._makeOne()
- class view(object):
- def __init__(self, context, request):
- pass
- def index(self):
- return 'OK'
- config._system_view(IDummy, view=view, attr='index')
- view = config.registry.getUtility(IDummy)
+ view = lambda *arg: arg
+ config.set_notfound_view(view)
request = self._makeRequest(config)
+ request.context = 'abc'
+ view = self._getViewCallable(config, ctx_iface=implementedBy(NotFound),
+ request_iface=IRequest)
result = view(None, request)
- self.assertEqual(result, 'OK')
+ self.assertEqual(result, ('abc', request))
- def test__system_view_with_wrapper(self):
- from zope.interface import Interface
- from zope.interface import directlyProvides
+ def test_set_forbidden_view(self):
+ from zope.interface import implementedBy
from repoze.bfg.interfaces import IRequest
- from repoze.bfg.interfaces import IView
- config = self._makeOne()
- view = lambda *arg: DummyResponse()
- wrapper = lambda *arg: 'OK2'
- config.registry.registerAdapter(wrapper, (Interface, Interface),
- IView, name='wrapper')
- config._system_view(IDummy, view=view, wrapper='wrapper')
- view = config.registry.getUtility(IDummy)
- request = self._makeRequest(config)
- directlyProvides(request, IRequest)
- request.registry = config.registry
- context = DummyContext()
- result = view(context, request)
- self.assertEqual(result, 'OK2')
-
- def test_set_notfound_view(self):
- from repoze.bfg.interfaces import INotFoundView
+ from repoze.bfg.exceptions import Forbidden
config = self._makeOne()
view = lambda *arg: 'OK'
- config.set_notfound_view(view)
+ config.set_forbidden_view(view)
request = self._makeRequest(config)
- view = config.registry.getUtility(INotFoundView)
+ view = self._getViewCallable(config, ctx_iface=implementedBy(Forbidden),
+ request_iface=IRequest)
result = view(None, request)
self.assertEqual(result, 'OK')
- def test_set_forbidden_view(self):
- from repoze.bfg.interfaces import IForbiddenView
+ def test_set_forbidden_view_request_has_context(self):
+ from zope.interface import implementedBy
+ from repoze.bfg.interfaces import IRequest
+ from repoze.bfg.exceptions import Forbidden
config = self._makeOne()
- view = lambda *arg: 'OK'
+ view = lambda *arg: arg
config.set_forbidden_view(view)
request = self._makeRequest(config)
- view = config.registry.getUtility(IForbiddenView)
+ request.context = 'abc'
+ view = self._getViewCallable(config, ctx_iface=implementedBy(Forbidden),
+ request_iface=IRequest)
result = view(None, request)
- self.assertEqual(result, 'OK')
+ self.assertEqual(result, ('abc', request))
def test__set_authentication_policy(self):
from repoze.bfg.interfaces import IAuthenticationPolicy
@@ -1680,6 +2068,7 @@ class ConfiguratorTests(unittest.TestCase):
def test__derive_view_with_wrapper_viewname(self):
from webob import Response
from repoze.bfg.interfaces import IView
+ from repoze.bfg.interfaces import IViewClassifier
inner_response = Response('OK')
def inner_view(context, request):
return inner_response
@@ -1690,7 +2079,7 @@ class ConfiguratorTests(unittest.TestCase):
return Response('outer ' + request.wrapped_body)
config = self._makeOne()
config.registry.registerAdapter(
- outer_view, (None, None), IView, 'owrap')
+ outer_view, (IViewClassifier, None, None), IView, 'owrap')
result = config._derive_view(inner_view, viewname='inner',
wrapper_viewname='owrap')
self.failIf(result is inner_view)
@@ -2396,6 +2785,183 @@ class Test_decorate_view(unittest.TestCase):
self.failUnless(view1.__predicated__.im_func is
view2.__predicated__.im_func)
+class Test__make_predicates(unittest.TestCase):
+ def _callFUT(self, **kw):
+ from repoze.bfg.configuration import _make_predicates
+ return _make_predicates(**kw)
+
+ def test_ordering_xhr_and_request_method_trump_only_containment(self):
+ order1, _, _ = self._callFUT(xhr=True, request_method='GET')
+ order2, _, _ = self._callFUT(containment=True)
+ self.failUnless(order1 < order2)
+
+ def test_ordering_number_of_predicates(self):
+ order1, _, _ = self._callFUT(
+ xhr='xhr',
+ request_method='request_method',
+ path_info='path_info',
+ request_param='param',
+ header='header',
+ accept='accept',
+ containment='containment',
+ request_type='request_type',
+ custom=('a',)
+ )
+ order2, _, _ = self._callFUT(
+ xhr='xhr',
+ request_method='request_method',
+ path_info='path_info',
+ request_param='param',
+ header='header',
+ accept='accept',
+ containment='containment',
+ request_type='request_type',
+ custom=('a',)
+ )
+ order3, _, _ = self._callFUT(
+ xhr='xhr',
+ request_method='request_method',
+ path_info='path_info',
+ request_param='param',
+ header='header',
+ accept='accept',
+ containment='containment',
+ request_type='request_type',
+ )
+ order4, _, _ = self._callFUT(
+ xhr='xhr',
+ request_method='request_method',
+ path_info='path_info',
+ request_param='param',
+ header='header',
+ accept='accept',
+ containment='containment',
+ )
+ order5, _, _ = self._callFUT(
+ xhr='xhr',
+ request_method='request_method',
+ path_info='path_info',
+ request_param='param',
+ header='header',
+ accept='accept',
+ )
+ order6, _, _ = self._callFUT(
+ xhr='xhr',
+ request_method='request_method',
+ path_info='path_info',
+ request_param='param',
+ header='header',
+ )
+ order7, _, _ = self._callFUT(
+ xhr='xhr',
+ request_method='request_method',
+ path_info='path_info',
+ request_param='param',
+ )
+ order8, _, _ = self._callFUT(
+ xhr='xhr',
+ request_method='request_method',
+ path_info='path_info',
+ )
+ order9, _, _ = self._callFUT(
+ xhr='xhr',
+ request_method='request_method',
+ )
+ order10, _, _ = self._callFUT(
+ xhr='xhr',
+ )
+ order11, _, _ = self._callFUT(
+ )
+ self.assertEqual(order1, order2)
+ self.failUnless(order3 > order2)
+ self.failUnless(order4 > order3)
+ self.failUnless(order5 > order4)
+ self.failUnless(order6 > order5)
+ self.failUnless(order7 > order6)
+ self.failUnless(order8 > order7)
+ self.failUnless(order9 > order8)
+ self.failUnless(order10 > order9)
+ self.failUnless(order11 > order10)
+
+ def test_ordering_importance_of_predicates(self):
+ order1, _, _ = self._callFUT(
+ xhr='xhr',
+ )
+ order2, _, _ = self._callFUT(
+ request_method='request_method',
+ )
+ order3, _, _ = self._callFUT(
+ path_info='path_info',
+ )
+ order4, _, _ = self._callFUT(
+ request_param='param',
+ )
+ order5, _, _ = self._callFUT(
+ header='header',
+ )
+ order6, _, _ = self._callFUT(
+ accept='accept',
+ )
+ order7, _, _ = self._callFUT(
+ containment='containment',
+ )
+ order8, _, _ = self._callFUT(
+ request_type='request_type',
+ )
+ order9, _, _ = self._callFUT(
+ custom=('a',),
+ )
+ self.failUnless(order1 > order2)
+ self.failUnless(order2 > order3)
+ self.failUnless(order3 > order4)
+ self.failUnless(order4 > order5)
+ self.failUnless(order5 > order6)
+ self.failUnless(order6 > order7)
+ self.failUnless(order7 > order8)
+ self.failUnless(order8 > order9)
+
+ def test_ordering_importance_and_number(self):
+ order1, _, _ = self._callFUT(
+ xhr='xhr',
+ request_method='request_method',
+ )
+ order2, _, _ = self._callFUT(
+ custom=('a',),
+ )
+ self.failUnless(order1 < order2)
+
+ order1, _, _ = self._callFUT(
+ xhr='xhr',
+ request_method='request_method',
+ )
+ order2, _, _ = self._callFUT(
+ request_method='request_method',
+ custom=('a',),
+ )
+ self.failUnless(order1 > order2)
+
+ order1, _, _ = self._callFUT(
+ xhr='xhr',
+ request_method='request_method',
+ path_info='path_info',
+ )
+ order2, _, _ = self._callFUT(
+ request_method='request_method',
+ custom=('a',),
+ )
+ self.failUnless(order1 < order2)
+
+ order1, _, _ = self._callFUT(
+ xhr='xhr',
+ request_method='request_method',
+ path_info='path_info',
+ )
+ order2, _, _ = self._callFUT(
+ xhr='xhr',
+ request_method='request_method',
+ custom=('a',),
+ )
+ self.failUnless(order1 > order2)
class TestMultiView(unittest.TestCase):
def _getTargetClass(self):
@@ -2418,20 +2984,33 @@ class TestMultiView(unittest.TestCase):
def test_add(self):
mv = self._makeOne()
mv.add('view', 100)
- self.assertEqual(mv.views, [(100, 'view')])
+ self.assertEqual(mv.views, [(100, 'view', None)])
mv.add('view2', 99)
- self.assertEqual(mv.views, [(99, 'view2'), (100, 'view')])
+ self.assertEqual(mv.views, [(99, 'view2', None), (100, 'view', None)])
mv.add('view3', 100, 'text/html')
- self.assertEqual(mv.media_views['text/html'], [(100, 'view3')])
+ self.assertEqual(mv.media_views['text/html'], [(100, 'view3', None)])
mv.add('view4', 99, 'text/html')
self.assertEqual(mv.media_views['text/html'],
- [(99, 'view4'), (100, 'view3')])
+ [(99, 'view4', None), (100, 'view3', None)])
mv.add('view5', 100, 'text/xml')
- self.assertEqual(mv.media_views['text/xml'], [(100, 'view5')])
+ self.assertEqual(mv.media_views['text/xml'], [(100, 'view5', None)])
self.assertEqual(set(mv.accepts), set(['text/xml', 'text/html']))
- self.assertEqual(mv.views, [(99, 'view2'), (100, 'view')])
+ self.assertEqual(mv.views, [(99, 'view2', None), (100, 'view', None)])
mv.add('view6', 98, 'text/*')
- self.assertEqual(mv.views, [(98, 'view6'),(99, 'view2'), (100, 'view')])
+ self.assertEqual(mv.views, [(98, 'view6', None),
+ (99, 'view2', None),
+ (100, 'view', None)])
+
+ def test_add_with_phash(self):
+ mv = self._makeOne()
+ mv.add('view', 100, phash='abc')
+ self.assertEqual(mv.views, [(100, 'view', 'abc')])
+ mv.add('view', 100, phash='abc')
+ self.assertEqual(mv.views, [(100, 'view', 'abc')])
+ mv.add('view', 100, phash='def')
+ self.assertEqual(mv.views, [(100, 'view', 'abc'), (100, 'view', 'def')])
+ mv.add('view', 100, phash='abc')
+ self.assertEqual(mv.views, [(100, 'view', 'abc'), (100, 'view', 'def')])
def test_get_views_request_has_no_accept(self):
request = DummyRequest()
@@ -2478,7 +3057,7 @@ class TestMultiView(unittest.TestCase):
def view(context, request):
""" """
view.__predicated__ = lambda *arg: False
- mv.views = [(100, view)]
+ mv.views = [(100, view, None)]
context = DummyContext()
request = DummyRequest()
self.assertRaises(NotFound, mv.match, context, request)
@@ -2488,7 +3067,7 @@ class TestMultiView(unittest.TestCase):
def view(context, request):
""" """
view.__predicated__ = lambda *arg: True
- mv.views = [(100, view)]
+ mv.views = [(100, view, None)]
context = DummyContext()
request = DummyRequest()
result = mv.match(context, request)
@@ -2505,7 +3084,7 @@ class TestMultiView(unittest.TestCase):
mv = self._makeOne()
def view(context, request):
""" """
- mv.views = [(100, view)]
+ mv.views = [(100, view, None)]
self.assertEqual(mv.__permitted__(None, None), True)
def test_permitted(self):
@@ -2515,7 +3094,7 @@ class TestMultiView(unittest.TestCase):
def permitted(context, request):
return False
view.__permitted__ = permitted
- mv.views = [(100, view)]
+ mv.views = [(100, view, None)]
context = DummyContext()
request = DummyRequest()
result = mv.__permitted__(context, request)
@@ -2539,7 +3118,7 @@ class TestMultiView(unittest.TestCase):
raise NotFound
def view2(context, request):
return expected_response
- mv.views = [(100, view1), (99, view2)]
+ mv.views = [(100, view1, None), (99, view2, None)]
response = mv(context, request)
self.assertEqual(response, expected_response)
@@ -2551,7 +3130,7 @@ class TestMultiView(unittest.TestCase):
expected_response = DummyResponse()
def view(context, request):
return expected_response
- mv.views = [(100, view)]
+ mv.views = [(100, view, None)]
response = mv(context, request)
self.assertEqual(response, expected_response)
@@ -2573,7 +3152,7 @@ class TestMultiView(unittest.TestCase):
def permissive(context, request):
return expected_response
view.__call_permissive__ = permissive
- mv.views = [(100, view)]
+ mv.views = [(100, view, None)]
response = mv.__call_permissive__(context, request)
self.assertEqual(response, expected_response)
@@ -2585,7 +3164,7 @@ class TestMultiView(unittest.TestCase):
expected_response = DummyResponse()
def view(context, request):
return expected_response
- mv.views = [(100, view)]
+ mv.views = [(100, view, None)]
response = mv.__call_permissive__(context, request)
self.assertEqual(response, expected_response)
@@ -2598,7 +3177,7 @@ class TestMultiView(unittest.TestCase):
def view(context, request):
return expected_response
mv.views = [(100, None)]
- mv.media_views['text/xml'] = [(100, view)]
+ mv.media_views['text/xml'] = [(100, view, None)]
mv.accepts = ['text/xml']
response = mv(context, request)
self.assertEqual(response, expected_response)
@@ -2611,8 +3190,8 @@ class TestMultiView(unittest.TestCase):
expected_response = DummyResponse()
def view(context, request):
return expected_response
- mv.views = [(100, view)]
- mv.media_views['text/xml'] = [(100, None)]
+ mv.views = [(100, view, None)]
+ mv.media_views['text/xml'] = [(100, None, None)]
mv.accepts = ['text/xml']
response = mv(context, request)
self.assertEqual(response, expected_response)
@@ -2932,8 +3511,8 @@ class DummyMultiView:
def __init__(self):
self.views = []
self.name = 'name'
- def add(self, view, score, accept=None):
- self.views.append((view, accept))
+ def add(self, view, order, accept=None, phash=None):
+ self.views.append((view, accept, phash))
def __call__(self, context, request):
return 'OK1'
def __permitted__(self, context, request):
diff --git a/repoze/bfg/tests/test_integration.py b/repoze/bfg/tests/test_integration.py
index 41144f7c3..c54509378 100644
--- a/repoze/bfg/tests/test_integration.py
+++ b/repoze/bfg/tests/test_integration.py
@@ -31,12 +31,14 @@ class WGSIAppPlusBFGViewTests(unittest.TestCase):
def test_scanned(self):
from repoze.bfg.interfaces import IRequest
from repoze.bfg.interfaces import IView
+ from repoze.bfg.interfaces import IViewClassifier
from repoze.bfg.configuration import Configurator
from repoze.bfg.tests import test_integration
config = Configurator()
config.scan(test_integration)
reg = config.registry
- view = reg.adapters.lookup((IRequest, INothing), IView, name='')
+ view = reg.adapters.lookup(
+ (IViewClassifier, IRequest, INothing), IView, name='')
self.assertEqual(view, wsgiapptest)
here = os.path.dirname(__file__)
@@ -63,11 +65,12 @@ class TestStaticApp(unittest.TestCase):
open(os.path.join(here, 'fixtures/minimal.pt'), 'r').read())
class TwillBase(unittest.TestCase):
+ root_factory = None
def setUp(self):
import sys
import twill
from repoze.bfg.configuration import Configurator
- config = Configurator()
+ config = Configurator(root_factory=self.root_factory)
config.load_zcml(self.config)
twill.add_wsgi_intercept('localhost', 6543, config.make_wsgi_app)
if sys.platform is 'win32': # pragma: no cover
@@ -98,6 +101,11 @@ class TestFixtureApp(TwillBase):
self.assertEqual(browser.get_html(), 'fixture')
browser.go('http://localhost:6543/dummyskin.html')
self.assertEqual(browser.get_code(), 404)
+ browser.go('http://localhost:6543/error.html')
+ self.assertEqual(browser.get_code(), 200)
+ self.assertEqual(browser.get_html(), 'supressed')
+ browser.go('http://localhost:6543/protected.html')
+ self.assertEqual(browser.get_code(), 401)
class TestCCBug(TwillBase):
# "unordered" as reported in IRC by author of
@@ -140,6 +148,15 @@ class TestHybridApp(TwillBase):
browser.go('http://localhost:6543/pqr/global2')
self.assertEqual(browser.get_code(), 200)
self.assertEqual(browser.get_html(), 'global2')
+ browser.go('http://localhost:6543/error')
+ self.assertEqual(browser.get_code(), 200)
+ self.assertEqual(browser.get_html(), 'supressed')
+ browser.go('http://localhost:6543/error2')
+ self.assertEqual(browser.get_code(), 200)
+ self.assertEqual(browser.get_html(), 'supressed2')
+ browser.go('http://localhost:6543/error_sub')
+ self.assertEqual(browser.get_code(), 200)
+ self.assertEqual(browser.get_html(), 'supressed2')
class TestRestBugApp(TwillBase):
# test bug reported by delijati 2010/2/3 (http://pastebin.com/d4cc15515)
@@ -168,6 +185,44 @@ class TestViewDecoratorApp(TwillBase):
self.assertEqual(browser.get_code(), 200)
self.failUnless('OK3' in browser.get_html())
+from repoze.bfg.tests.exceptionviewapp.models import AnException, NotAnException
+excroot = {'anexception':AnException(),
+ 'notanexception':NotAnException()}
+
+class TestExceptionViewsApp(TwillBase):
+ config = 'repoze.bfg.tests.exceptionviewapp:configure.zcml'
+ root_factory = lambda *arg: excroot
+ def test_it(self):
+ import twill.commands
+ browser = twill.commands.get_browser()
+ browser.go('http://localhost:6543/')
+ self.assertEqual(browser.get_code(), 200)
+ self.failUnless('maybe' in browser.get_html())
+
+ browser.go('http://localhost:6543/notanexception')
+ self.assertEqual(browser.get_code(), 200)
+ self.failUnless('no' in browser.get_html())
+
+ browser.go('http://localhost:6543/anexception')
+ self.assertEqual(browser.get_code(), 200)
+ self.failUnless('yes' in browser.get_html())
+
+ browser.go('http://localhost:6543/route_raise_exception')
+ self.assertEqual(browser.get_code(), 200)
+ self.failUnless('yes' in browser.get_html())
+
+ browser.go('http://localhost:6543/route_raise_exception2')
+ self.assertEqual(browser.get_code(), 200)
+ self.failUnless('yes' in browser.get_html())
+
+ browser.go('http://localhost:6543/route_raise_exception3')
+ self.assertEqual(browser.get_code(), 200)
+ self.failUnless('whoa' in browser.get_html())
+
+ browser.go('http://localhost:6543/route_raise_exception4')
+ self.assertEqual(browser.get_code(), 200)
+ self.failUnless('whoa' in browser.get_html())
+
class DummyContext(object):
pass
diff --git a/repoze/bfg/tests/test_request.py b/repoze/bfg/tests/test_request.py
index 06255721e..7b3d0ce7b 100644
--- a/repoze/bfg/tests/test_request.py
+++ b/repoze/bfg/tests/test_request.py
@@ -165,6 +165,8 @@ class Test_route_request_iface(unittest.TestCase):
def test_it(self):
iface = self._callFUT('routename')
self.assertEqual(iface.__name__, 'routename_IRequest')
+ self.assertTrue(hasattr(iface, 'combined'))
+ self.assertEqual(iface.combined.__name__, 'routename_combined_IRequest')
class Test_add_global_response_headers(unittest.TestCase):
def _callFUT(self, request, headerlist):
diff --git a/repoze/bfg/tests/test_router.py b/repoze/bfg/tests/test_router.py
index 0d7bee720..8702b9317 100644
--- a/repoze/bfg/tests/test_router.py
+++ b/repoze/bfg/tests/test_router.py
@@ -13,11 +13,10 @@ class TestRouter(unittest.TestCase):
def _registerRouteRequest(self, name):
from repoze.bfg.interfaces import IRouteRequest
- from zope.interface import Interface
- class IRequest(Interface):
- """ """
- self.registry.registerUtility(IRequest, IRouteRequest, name=name)
- return IRequest
+ from repoze.bfg.request import route_request_iface
+ iface = route_request_iface(name)
+ self.registry.registerUtility(iface, IRouteRequest, name=name)
+ return iface
def _connectRoute(self, path, name, factory=None):
from repoze.bfg.interfaces import IRoutesMapper
@@ -75,9 +74,10 @@ class TestRouter(unittest.TestCase):
self.registry.registerAdapter(DummyTraverserFactory, (None,),
ITraverser, name='')
- def _registerView(self, app, name, *for_):
+ def _registerView(self, app, name, classifier, req_iface, ctx_iface):
from repoze.bfg.interfaces import IView
- self.registry.registerAdapter(app, for_, IView, name)
+ self.registry.registerAdapter(
+ app, (classifier, req_iface, ctx_iface), IView, name)
def _registerEventListener(self, iface):
L = []
@@ -118,44 +118,15 @@ class TestRouter(unittest.TestCase):
router = self._makeOne()
self.assertEqual(router.root_policy, rootfactory)
- def test_iforbiddenview_override(self):
- from repoze.bfg.interfaces import IForbiddenView
- def app():
- """ """
- self.registry.registerUtility(app, IForbiddenView)
- router = self._makeOne()
- self.assertEqual(router.forbidden_view, app)
-
- def test_iforbiddenview_nooverride(self):
- router = self._makeOne()
- from repoze.bfg.view import default_forbidden_view
- self.assertEqual(router.forbidden_view, default_forbidden_view)
-
- def test_inotfoundview_override(self):
- from repoze.bfg.interfaces import INotFoundView
- def app():
- """ """
- self.registry.registerUtility(app, INotFoundView)
- router = self._makeOne()
- self.assertEqual(router.notfound_view, app)
-
- def test_inotfoundview_nooverride(self):
- router = self._makeOne()
- from repoze.bfg.view import default_notfound_view
- self.assertEqual(router.notfound_view, default_notfound_view)
-
def test_call_traverser_default(self):
+ from repoze.bfg.exceptions import NotFound
environ = self._makeEnviron()
logger = self._registerLogger()
router = self._makeOne()
start_response = DummyStartResponse()
- result = router(environ, start_response)
- headers = start_response.headers
- self.assertEqual(len(headers), 2)
- status = start_response.status
- self.assertEqual(status, '404 Not Found')
- self.failUnless('<code>/</code>' in result[0], result)
- self.failIf('debug_notfound' in result[0])
+ why = exc_raised(NotFound, router, environ, start_response)
+ self.failUnless('/' in why[0], why)
+ self.failIf('debug_notfound' in why[0])
self.assertEqual(len(logger.messages), 0)
def test_traverser_raises_notfound_class(self):
@@ -165,12 +136,7 @@ class TestRouter(unittest.TestCase):
self._registerTraverserFactory(context, raise_error=NotFound)
router = self._makeOne()
start_response = DummyStartResponse()
- result = router(environ, start_response)
- headers = start_response.headers
- self.assertEqual(len(headers), 2)
- status = start_response.status
- self.assertEqual(status, '404 Not Found')
- self.failUnless('<code></code>' in result[0], result)
+ self.assertRaises(NotFound, router, environ, start_response)
def test_traverser_raises_notfound_instance(self):
from repoze.bfg.exceptions import NotFound
@@ -179,12 +145,8 @@ class TestRouter(unittest.TestCase):
self._registerTraverserFactory(context, raise_error=NotFound('foo'))
router = self._makeOne()
start_response = DummyStartResponse()
- result = router(environ, start_response)
- headers = start_response.headers
- self.assertEqual(len(headers), 2)
- status = start_response.status
- self.assertEqual(status, '404 Not Found')
- self.failUnless('<code>foo</code>' in result[0], result)
+ why = exc_raised(NotFound, router, environ, start_response)
+ self.failUnless('foo' in why[0], why)
def test_traverser_raises_forbidden_class(self):
from repoze.bfg.exceptions import Forbidden
@@ -193,12 +155,7 @@ class TestRouter(unittest.TestCase):
self._registerTraverserFactory(context, raise_error=Forbidden)
router = self._makeOne()
start_response = DummyStartResponse()
- result = router(environ, start_response)
- headers = start_response.headers
- self.assertEqual(len(headers), 2)
- status = start_response.status
- self.assertEqual(status, '401 Unauthorized')
- self.failUnless('<code></code>' in result[0], result)
+ self.assertRaises(Forbidden, router, environ, start_response)
def test_traverser_raises_forbidden_instance(self):
from repoze.bfg.exceptions import Forbidden
@@ -207,30 +164,24 @@ class TestRouter(unittest.TestCase):
self._registerTraverserFactory(context, raise_error=Forbidden('foo'))
router = self._makeOne()
start_response = DummyStartResponse()
- result = router(environ, start_response)
- headers = start_response.headers
- self.assertEqual(len(headers), 2)
- status = start_response.status
- self.assertEqual(status, '401 Unauthorized')
- self.failUnless('<code>foo</code>' in result[0], result)
+ why = exc_raised(Forbidden, router, environ, start_response)
+ self.failUnless('foo' in why[0], why)
def test_call_no_view_registered_no_isettings(self):
+ from repoze.bfg.exceptions import NotFound
environ = self._makeEnviron()
context = DummyContext()
self._registerTraverserFactory(context)
logger = self._registerLogger()
router = self._makeOne()
start_response = DummyStartResponse()
- result = router(environ, start_response)
- headers = start_response.headers
- self.assertEqual(len(headers), 2)
- status = start_response.status
- self.assertEqual(status, '404 Not Found')
- self.failUnless('<code>/</code>' in result[0], result)
- self.failIf('debug_notfound' in result[0])
+ why = exc_raised(NotFound, router, environ, start_response)
+ self.failUnless('/' in why[0], why)
+ self.failIf('debug_notfound' in why[0])
self.assertEqual(len(logger.messages), 0)
def test_call_no_view_registered_debug_notfound_false(self):
+ from repoze.bfg.exceptions import NotFound
environ = self._makeEnviron()
context = DummyContext()
self._registerTraverserFactory(context)
@@ -238,16 +189,13 @@ class TestRouter(unittest.TestCase):
self._registerSettings(debug_notfound=False)
router = self._makeOne()
start_response = DummyStartResponse()
- result = router(environ, start_response)
- headers = start_response.headers
- self.assertEqual(len(headers), 2)
- status = start_response.status
- self.assertEqual(status, '404 Not Found')
- self.failUnless('<code>/</code>' in result[0], result)
- self.failIf('debug_notfound' in result[0])
+ why = exc_raised(NotFound, router, environ, start_response)
+ self.failUnless('/' in why[0], why)
+ self.failIf('debug_notfound' in why[0])
self.assertEqual(len(logger.messages), 0)
def test_call_no_view_registered_debug_notfound_true(self):
+ from repoze.bfg.exceptions import NotFound
environ = self._makeEnviron()
context = DummyContext()
self._registerTraverserFactory(context)
@@ -255,17 +203,13 @@ class TestRouter(unittest.TestCase):
logger = self._registerLogger()
router = self._makeOne()
start_response = DummyStartResponse()
- result = router(environ, start_response)
- headers = start_response.headers
- self.assertEqual(len(headers), 2)
- status = start_response.status
- self.assertEqual(status, '404 Not Found')
+ why = exc_raised(NotFound, router, environ, start_response)
self.failUnless(
"debug_notfound of url http://localhost:8080/; path_info: '/', "
- "context:" in result[0])
- self.failUnless(
- "view_name: '', subpath: []" in result[0])
- self.failUnless('http://localhost:8080' in result[0], result)
+ "context:" in why[0])
+ self.failUnless("view_name: '', subpath: []" in why[0])
+ self.failUnless('http://localhost:8080' in why[0], why)
+
self.assertEqual(len(logger.messages), 1)
message = logger.messages[0]
self.failUnless('of url http://localhost:8080' in message)
@@ -275,35 +219,25 @@ class TestRouter(unittest.TestCase):
self.failUnless("subpath: []" in message)
def test_call_view_returns_nonresponse(self):
+ from repoze.bfg.interfaces import IViewClassifier
context = DummyContext()
self._registerTraverserFactory(context)
environ = self._makeEnviron()
view = DummyView('abc')
- self._registerView(view, '', None, None)
- router = self._makeOne()
- start_response = DummyStartResponse()
- self.assertRaises(ValueError, router, environ, start_response)
-
- def test_inotfoundview_returns_nonresponse(self):
- from repoze.bfg.interfaces import INotFoundView
- context = DummyContext()
- environ = self._makeEnviron()
- self._registerTraverserFactory(context)
- def app(context, request):
- """ """
- self.registry.registerUtility(app, INotFoundView)
+ self._registerView(view, '', IViewClassifier, None, None)
router = self._makeOne()
start_response = DummyStartResponse()
self.assertRaises(ValueError, router, environ, start_response)
def test_call_view_registered_nonspecific_default_path(self):
+ from repoze.bfg.interfaces import IViewClassifier
context = DummyContext()
self._registerTraverserFactory(context)
response = DummyResponse()
response.app_iter = ['Hello world']
view = DummyView(response)
environ = self._makeEnviron()
- self._registerView(view, '', None, None)
+ self._registerView(view, '', IViewClassifier, None, None)
self._registerRootFactory(context)
router = self._makeOne()
start_response = DummyStartResponse()
@@ -318,6 +252,7 @@ class TestRouter(unittest.TestCase):
self.assertEqual(request.root, context)
def test_call_view_registered_nonspecific_nondefault_path_and_subpath(self):
+ from repoze.bfg.interfaces import IViewClassifier
context = DummyContext()
self._registerTraverserFactory(context, view_name='foo',
subpath=['bar'],
@@ -327,7 +262,7 @@ class TestRouter(unittest.TestCase):
response.app_iter = ['Hello world']
view = DummyView(response)
environ = self._makeEnviron()
- self._registerView(view, 'foo', None, None)
+ self._registerView(view, 'foo', IViewClassifier, None, None)
router = self._makeOne()
start_response = DummyStartResponse()
result = router(environ, start_response)
@@ -346,6 +281,7 @@ class TestRouter(unittest.TestCase):
class IContext(Interface):
pass
from repoze.bfg.interfaces import IRequest
+ from repoze.bfg.interfaces import IViewClassifier
context = DummyContext()
directlyProvides(context, IContext)
self._registerTraverserFactory(context)
@@ -354,7 +290,7 @@ class TestRouter(unittest.TestCase):
response.app_iter = ['Hello world']
view = DummyView(response)
environ = self._makeEnviron()
- self._registerView(view, '', IRequest, IContext)
+ self._registerView(view, '', IViewClassifier, IRequest, IContext)
router = self._makeOne()
start_response = DummyStartResponse()
result = router(environ, start_response)
@@ -370,6 +306,8 @@ class TestRouter(unittest.TestCase):
def test_call_view_registered_specific_fail(self):
from zope.interface import Interface
from zope.interface import directlyProvides
+ from repoze.bfg.exceptions import NotFound
+ from repoze.bfg.interfaces import IViewClassifier
class IContext(Interface):
pass
class INotContext(Interface):
@@ -381,31 +319,30 @@ class TestRouter(unittest.TestCase):
response = DummyResponse()
view = DummyView(response)
environ = self._makeEnviron()
- self._registerView(view, '', IRequest, IContext)
+ self._registerView(view, '', IViewClassifier, IRequest, IContext)
router = self._makeOne()
start_response = DummyStartResponse()
- result = router(environ, start_response)
- self.assertEqual(start_response.status, '404 Not Found')
- self.failUnless('404' in result[0])
+ self.assertRaises(NotFound, router, environ, start_response)
def test_call_view_raises_forbidden(self):
from zope.interface import Interface
from zope.interface import directlyProvides
+ from repoze.bfg.exceptions import Forbidden
class IContext(Interface):
pass
from repoze.bfg.interfaces import IRequest
+ from repoze.bfg.interfaces import IViewClassifier
context = DummyContext()
directlyProvides(context, IContext)
self._registerTraverserFactory(context, subpath=[''])
response = DummyResponse()
- view = DummyView(response, raise_unauthorized=True)
+ view = DummyView(response, raise_exception=Forbidden("unauthorized"))
environ = self._makeEnviron()
- self._registerView(view, '', IRequest, IContext)
+ self._registerView(view, '', IViewClassifier, IRequest, IContext)
router = self._makeOne()
start_response = DummyStartResponse()
- response = router(environ, start_response)
- self.assertEqual(start_response.status, '401 Unauthorized')
- self.assertEqual(environ['repoze.bfg.message'], 'unauthorized')
+ why = exc_raised(Forbidden, router, environ, start_response)
+ self.assertEqual(why[0], 'unauthorized')
def test_call_view_raises_notfound(self):
from zope.interface import Interface
@@ -413,18 +350,19 @@ class TestRouter(unittest.TestCase):
class IContext(Interface):
pass
from repoze.bfg.interfaces import IRequest
+ from repoze.bfg.interfaces import IViewClassifier
+ from repoze.bfg.exceptions import NotFound
context = DummyContext()
directlyProvides(context, IContext)
self._registerTraverserFactory(context, subpath=[''])
response = DummyResponse()
- view = DummyView(response, raise_notfound=True)
+ view = DummyView(response, raise_exception=NotFound("notfound"))
environ = self._makeEnviron()
- self._registerView(view, '', IRequest, IContext)
+ self._registerView(view, '', IViewClassifier, IRequest, IContext)
router = self._makeOne()
start_response = DummyStartResponse()
- response = router(environ, start_response)
- self.assertEqual(start_response.status, '404 Not Found')
- self.assertEqual(environ['repoze.bfg.message'], 'notfound')
+ why = exc_raised(NotFound, router, environ, start_response)
+ self.assertEqual(why[0], 'notfound')
def test_call_request_has_global_response_headers(self):
from zope.interface import Interface
@@ -432,6 +370,7 @@ class TestRouter(unittest.TestCase):
class IContext(Interface):
pass
from repoze.bfg.interfaces import IRequest
+ from repoze.bfg.interfaces import IViewClassifier
context = DummyContext()
directlyProvides(context, IContext)
self._registerTraverserFactory(context, subpath=[''])
@@ -441,7 +380,7 @@ class TestRouter(unittest.TestCase):
request.global_response_headers = [('b', 2)]
return response
environ = self._makeEnviron()
- self._registerView(view, '', IRequest, IContext)
+ self._registerView(view, '', IViewClassifier, IRequest, IContext)
router = self._makeOne()
start_response = DummyStartResponse()
router(environ, start_response)
@@ -452,13 +391,14 @@ class TestRouter(unittest.TestCase):
from repoze.bfg.interfaces import INewRequest
from repoze.bfg.interfaces import INewResponse
from repoze.bfg.interfaces import IAfterTraversal
+ from repoze.bfg.interfaces import IViewClassifier
context = DummyContext()
self._registerTraverserFactory(context)
response = DummyResponse()
response.app_iter = ['Hello world']
view = DummyView(response)
environ = self._makeEnviron()
- self._registerView(view, '', None, None)
+ self._registerView(view, '', IViewClassifier, None, None)
request_events = self._registerEventListener(INewRequest)
aftertraversal_events = self._registerEventListener(IAfterTraversal)
response_events = self._registerEventListener(INewResponse)
@@ -474,13 +414,14 @@ class TestRouter(unittest.TestCase):
self.assertEqual(result, response.app_iter)
def test_call_pushes_and_pops_threadlocal_manager(self):
+ from repoze.bfg.interfaces import IViewClassifier
context = DummyContext()
self._registerTraverserFactory(context)
response = DummyResponse()
response.app_iter = ['Hello world']
view = DummyView(response)
environ = self._makeEnviron()
- self._registerView(view, '', None, None)
+ self._registerView(view, '', IViewClassifier, None, None)
router = self._makeOne()
start_response = DummyStartResponse()
router.threadlocal_manager = DummyThreadLocalManager()
@@ -489,7 +430,8 @@ class TestRouter(unittest.TestCase):
self.assertEqual(len(router.threadlocal_manager.popped), 1)
def test_call_route_matches_and_has_factory(self):
- req_iface = self._registerRouteRequest('foo')
+ from repoze.bfg.interfaces import IViewClassifier
+ self._registerRouteRequest('foo')
root = object()
def factory(request):
return root
@@ -500,7 +442,7 @@ class TestRouter(unittest.TestCase):
response.app_iter = ['Hello world']
view = DummyView(response)
environ = self._makeEnviron(PATH_INFO='/archives/action1/article1')
- self._registerView(view, '', None, None)
+ self._registerView(view, '', IViewClassifier, None, None)
self._registerRootFactory(context)
router = self._makeOne()
start_response = DummyStartResponse()
@@ -522,9 +464,10 @@ class TestRouter(unittest.TestCase):
def test_call_route_matches_doesnt_overwrite_subscriber_iface(self):
from repoze.bfg.interfaces import INewRequest
+ from repoze.bfg.interfaces import IViewClassifier
from zope.interface import alsoProvides
from zope.interface import Interface
- req_iface = self._registerRouteRequest('foo')
+ self._registerRouteRequest('foo')
class IFoo(Interface):
pass
def listener(event):
@@ -540,7 +483,7 @@ class TestRouter(unittest.TestCase):
response.app_iter = ['Hello world']
view = DummyView(response)
environ = self._makeEnviron(PATH_INFO='/archives/action1/article1')
- self._registerView(view, '', None, None)
+ self._registerView(view, '', IViewClassifier, None, None)
self._registerRootFactory(context)
router = self._makeOne()
start_response = DummyStartResponse()
@@ -576,9 +519,8 @@ class TestRouter(unittest.TestCase):
environ = self._makeEnviron()
router = self._makeOne()
start_response = DummyStartResponse()
- app_iter = router(environ, start_response)
- self.assertEqual(start_response.status, '404 Not Found')
- self.failUnless('from root factory' in app_iter[0])
+ why = exc_raised(NotFound, router, environ, start_response)
+ self.failUnless('from root factory' in why[0])
def test_root_factory_raises_forbidden(self):
from repoze.bfg.interfaces import IRootFactory
@@ -595,29 +537,378 @@ class TestRouter(unittest.TestCase):
environ = self._makeEnviron()
router = self._makeOne()
start_response = DummyStartResponse()
+ why = exc_raised(Forbidden, router, environ, start_response)
+ self.failUnless('from root factory' in why[0])
+
+ def test_root_factory_exception_propagating(self):
+ from repoze.bfg.interfaces import IRootFactory
+ from zope.interface import Interface
+ from zope.interface import directlyProvides
+ def rootfactory(request):
+ raise RuntimeError()
+ self.registry.registerUtility(rootfactory, IRootFactory)
+ class IContext(Interface):
+ pass
+ context = DummyContext()
+ directlyProvides(context, IContext)
+ environ = self._makeEnviron()
+ router = self._makeOne()
+ start_response = DummyStartResponse()
+ self.assertRaises(RuntimeError, router, environ, start_response)
+
+ def test_traverser_exception_propagating(self):
+ environ = self._makeEnviron()
+ context = DummyContext()
+ self._registerTraverserFactory(context, raise_error=RuntimeError())
+ router = self._makeOne()
+ start_response = DummyStartResponse()
+ self.assertRaises(RuntimeError, router, environ, start_response)
+
+ def test_call_view_exception_propagating(self):
+ from zope.interface import Interface
+ from zope.interface import directlyProvides
+ class IContext(Interface):
+ pass
+ from repoze.bfg.interfaces import IRequest
+ from repoze.bfg.interfaces import IViewClassifier
+ context = DummyContext()
+ directlyProvides(context, IContext)
+ self._registerTraverserFactory(context, subpath=[''])
+ response = DummyResponse()
+ view = DummyView(response, raise_exception=RuntimeError)
+ environ = self._makeEnviron()
+ self._registerView(view, '', IViewClassifier, IRequest, IContext)
+ router = self._makeOne()
+ start_response = DummyStartResponse()
+ self.assertRaises(RuntimeError, router, environ, start_response)
+
+ def test_call_view_raises_exception_view(self):
+ from repoze.bfg.interfaces import IViewClassifier
+ from repoze.bfg.interfaces import IExceptionViewClassifier
+ from repoze.bfg.interfaces import IRequest
+ response = DummyResponse()
+ exception_response = DummyResponse()
+ exception_response.app_iter = ["Hello, world"]
+ view = DummyView(response, raise_exception=RuntimeError)
+ exception_view = DummyView(exception_response)
+ environ = self._makeEnviron()
+ self._registerView(view, '', IViewClassifier, IRequest, None)
+ self._registerView(exception_view, '', IExceptionViewClassifier,
+ IRequest, RuntimeError)
+ router = self._makeOne()
+ start_response = DummyStartResponse()
+ result = router(environ, start_response)
+ self.assertEqual(result, ["Hello, world"])
+ self.assertEqual(view.request.exception.__class__, RuntimeError)
+
+ def test_call_view_raises_super_exception_sub_exception_view(self):
+ from repoze.bfg.interfaces import IViewClassifier
+ from repoze.bfg.interfaces import IExceptionViewClassifier
+ from repoze.bfg.interfaces import IRequest
+ class SuperException(Exception):
+ pass
+ class SubException(SuperException):
+ pass
+ response = DummyResponse()
+ exception_response = DummyResponse()
+ exception_response.app_iter = ["Hello, world"]
+ view = DummyView(response, raise_exception=SuperException)
+ exception_view = DummyView(exception_response)
+ environ = self._makeEnviron()
+ self._registerView(view, '', IViewClassifier, IRequest, None)
+ self._registerView(exception_view, '', IExceptionViewClassifier,
+ IRequest, SubException)
+ router = self._makeOne()
+ start_response = DummyStartResponse()
+ self.assertRaises(SuperException, router, environ, start_response)
+
+ def test_call_view_raises_sub_exception_super_exception_view(self):
+ from repoze.bfg.interfaces import IViewClassifier
+ from repoze.bfg.interfaces import IExceptionViewClassifier
+ from repoze.bfg.interfaces import IRequest
+ class SuperException(Exception):
+ pass
+ class SubException(SuperException):
+ pass
+ response = DummyResponse()
+ exception_response = DummyResponse()
+ exception_response.app_iter = ["Hello, world"]
+ view = DummyView(response, raise_exception=SubException)
+ exception_view = DummyView(exception_response)
+ environ = self._makeEnviron()
+ self._registerView(view, '', IViewClassifier, IRequest, None)
+ self._registerView(exception_view, '', IExceptionViewClassifier,
+ IRequest, SuperException)
+ router = self._makeOne()
+ start_response = DummyStartResponse()
+ result = router(environ, start_response)
+ self.assertEqual(result, ["Hello, world"])
+
+ def test_call_view_raises_exception_another_exception_view(self):
+ from repoze.bfg.interfaces import IViewClassifier
+ from repoze.bfg.interfaces import IExceptionViewClassifier
+ from repoze.bfg.interfaces import IRequest
+ class MyException(Exception):
+ pass
+ class AnotherException(Exception):
+ pass
+ response = DummyResponse()
+ exception_response = DummyResponse()
+ exception_response.app_iter = ["Hello, world"]
+ view = DummyView(response, raise_exception=MyException)
+ exception_view = DummyView(exception_response)
+ environ = self._makeEnviron()
+ self._registerView(view, '', IViewClassifier, IRequest, None)
+ self._registerView(exception_view, '', IExceptionViewClassifier,
+ IRequest, AnotherException)
+ router = self._makeOne()
+ start_response = DummyStartResponse()
+ self.assertRaises(MyException, router, environ, start_response)
+
+ def test_root_factory_raises_exception_view(self):
+ from repoze.bfg.interfaces import IRootFactory
+ from repoze.bfg.interfaces import IRequest
+ from repoze.bfg.interfaces import IExceptionViewClassifier
+ def rootfactory(request):
+ raise RuntimeError()
+ self.registry.registerUtility(rootfactory, IRootFactory)
+ exception_response = DummyResponse()
+ exception_response.app_iter = ["Hello, world"]
+ exception_view = DummyView(exception_response)
+ self._registerView(exception_view, '', IExceptionViewClassifier,
+ IRequest, RuntimeError)
+ environ = self._makeEnviron()
+ router = self._makeOne()
+ start_response = DummyStartResponse()
app_iter = router(environ, start_response)
- self.assertEqual(start_response.status, '401 Unauthorized')
- self.failUnless('from root factory' in app_iter[0])
+ self.assertEqual(app_iter, ["Hello, world"])
+
+ def test_traverser_raises_exception_view(self):
+ from repoze.bfg.interfaces import IRequest
+ from repoze.bfg.interfaces import IExceptionViewClassifier
+ environ = self._makeEnviron()
+ context = DummyContext()
+ self._registerTraverserFactory(context, raise_error=RuntimeError())
+ exception_response = DummyResponse()
+ exception_response.app_iter = ["Hello, world"]
+ exception_view = DummyView(exception_response)
+ self._registerView(exception_view, '', IExceptionViewClassifier,
+ IRequest, RuntimeError)
+ router = self._makeOne()
+ start_response = DummyStartResponse()
+ result = router(environ, start_response)
+ self.assertEqual(result, ["Hello, world"])
+
+ def test_exception_view_returns_non_response(self):
+ from repoze.bfg.interfaces import IRequest
+ from repoze.bfg.interfaces import IViewClassifier
+ from repoze.bfg.interfaces import IExceptionViewClassifier
+ environ = self._makeEnviron()
+ response = DummyResponse()
+ view = DummyView(response, raise_exception=RuntimeError)
+ self._registerView(view, '', IViewClassifier, IRequest, None)
+ exception_view = DummyView(None)
+ self._registerView(exception_view, '', IExceptionViewClassifier,
+ IRequest, RuntimeError)
+ router = self._makeOne()
+ start_response = DummyStartResponse()
+ self.assertRaises(ValueError, router, environ, start_response)
+
+ def test_call_route_raises_route_exception_view(self):
+ from repoze.bfg.interfaces import IViewClassifier
+ from repoze.bfg.interfaces import IExceptionViewClassifier
+ req_iface = self._registerRouteRequest('foo')
+ self._connectRoute('archives/:action/:article', 'foo', None)
+ view = DummyView(DummyResponse(), raise_exception=RuntimeError)
+ self._registerView(view, '', IViewClassifier, req_iface, None)
+ response = DummyResponse()
+ response.app_iter = ["Hello, world"]
+ exception_view = DummyView(response)
+ self._registerView(exception_view, '', IExceptionViewClassifier,
+ req_iface, RuntimeError)
+ environ = self._makeEnviron(PATH_INFO='/archives/action1/article1')
+ start_response = DummyStartResponse()
+ router = self._makeOne()
+ result = router(environ, start_response)
+ self.assertEqual(result, ["Hello, world"])
+
+ def test_call_view_raises_exception_route_view(self):
+ from repoze.bfg.interfaces import IViewClassifier
+ from repoze.bfg.interfaces import IExceptionViewClassifier
+ from repoze.bfg.interfaces import IRequest
+ req_iface = self._registerRouteRequest('foo')
+ self._connectRoute('archives/:action/:article', 'foo', None)
+ view = DummyView(DummyResponse(), raise_exception=RuntimeError)
+ self._registerView(view, '', IViewClassifier, IRequest, None)
+ response = DummyResponse()
+ response.app_iter = ["Hello, world"]
+ exception_view = DummyView(response)
+ self._registerView(exception_view, '', IExceptionViewClassifier,
+ req_iface, RuntimeError)
+ environ = self._makeEnviron()
+ start_response = DummyStartResponse()
+ router = self._makeOne()
+ self.assertRaises(RuntimeError, router, environ, start_response)
+
+ def test_call_route_raises_exception_view(self):
+ from repoze.bfg.interfaces import IViewClassifier
+ from repoze.bfg.interfaces import IExceptionViewClassifier
+ from repoze.bfg.interfaces import IRequest
+ req_iface = self._registerRouteRequest('foo')
+ self._connectRoute('archives/:action/:article', 'foo', None)
+ view = DummyView(DummyResponse(), raise_exception=RuntimeError)
+ self._registerView(view, '', IViewClassifier, req_iface, None)
+ response = DummyResponse()
+ response.app_iter = ["Hello, world"]
+ exception_view = DummyView(response)
+ self._registerView(exception_view, '', IExceptionViewClassifier,
+ IRequest, RuntimeError)
+ environ = self._makeEnviron(PATH_INFO='/archives/action1/article1')
+ start_response = DummyStartResponse()
+ router = self._makeOne()
+ result = router(environ, start_response)
+ self.assertEqual(result, ["Hello, world"])
+
+ def test_call_route_raises_super_exception_sub_exception_view(self):
+ from repoze.bfg.interfaces import IViewClassifier
+ from repoze.bfg.interfaces import IExceptionViewClassifier
+ from repoze.bfg.interfaces import IRequest
+ class SuperException(Exception):
+ pass
+ class SubException(SuperException):
+ pass
+ req_iface = self._registerRouteRequest('foo')
+ self._connectRoute('archives/:action/:article', 'foo', None)
+ view = DummyView(DummyResponse(), raise_exception=SuperException)
+ self._registerView(view, '', IViewClassifier, req_iface, None)
+ response = DummyResponse()
+ response.app_iter = ["Hello, world"]
+ exception_view = DummyView(response)
+ self._registerView(exception_view, '', IExceptionViewClassifier,
+ IRequest, SubException)
+ environ = self._makeEnviron(PATH_INFO='/archives/action1/article1')
+ start_response = DummyStartResponse()
+ router = self._makeOne()
+ self.assertRaises(SuperException, router, environ, start_response)
+
+ def test_call_route_raises_sub_exception_super_exception_view(self):
+ from repoze.bfg.interfaces import IViewClassifier
+ from repoze.bfg.interfaces import IExceptionViewClassifier
+ from repoze.bfg.interfaces import IRequest
+ class SuperException(Exception):
+ pass
+ class SubException(SuperException):
+ pass
+ req_iface = self._registerRouteRequest('foo')
+ self._connectRoute('archives/:action/:article', 'foo', None)
+ view = DummyView(DummyResponse(), raise_exception=SubException)
+ self._registerView(view, '', IViewClassifier, req_iface, None)
+ response = DummyResponse()
+ response.app_iter = ["Hello, world"]
+ exception_view = DummyView(response)
+ self._registerView(exception_view, '', IExceptionViewClassifier,
+ IRequest, SuperException)
+ environ = self._makeEnviron(PATH_INFO='/archives/action1/article1')
+ start_response = DummyStartResponse()
+ router = self._makeOne()
+ result = router(environ, start_response)
+ self.assertEqual(result, ["Hello, world"])
+
+ def test_call_route_raises_exception_another_exception_view(self):
+ from repoze.bfg.interfaces import IViewClassifier
+ from repoze.bfg.interfaces import IExceptionViewClassifier
+ from repoze.bfg.interfaces import IRequest
+ class MyException(Exception):
+ pass
+ class AnotherException(Exception):
+ pass
+ req_iface = self._registerRouteRequest('foo')
+ self._connectRoute('archives/:action/:article', 'foo', None)
+ view = DummyView(DummyResponse(), raise_exception=MyException)
+ self._registerView(view, '', IViewClassifier, req_iface, None)
+ response = DummyResponse()
+ response.app_iter = ["Hello, world"]
+ exception_view = DummyView(response)
+ self._registerView(exception_view, '', IExceptionViewClassifier,
+ IRequest, AnotherException)
+ environ = self._makeEnviron(PATH_INFO='/archives/action1/article1')
+ start_response = DummyStartResponse()
+ router = self._makeOne()
+ self.assertRaises(MyException, router, environ, start_response)
+
+ def test_call_route_raises_exception_view_specializing(self):
+ from repoze.bfg.interfaces import IViewClassifier
+ from repoze.bfg.interfaces import IExceptionViewClassifier
+ from repoze.bfg.interfaces import IRequest
+ req_iface = self._registerRouteRequest('foo')
+ self._connectRoute('archives/:action/:article', 'foo', None)
+ view = DummyView(DummyResponse(), raise_exception=RuntimeError)
+ self._registerView(view, '', IViewClassifier, req_iface, None)
+ response = DummyResponse()
+ response.app_iter = ["Hello, world"]
+ exception_view = DummyView(response)
+ self._registerView(exception_view, '', IExceptionViewClassifier,
+ IRequest, RuntimeError)
+ response_spec = DummyResponse()
+ response_spec.app_iter = ["Hello, special world"]
+ exception_view_spec = DummyView(response_spec)
+ self._registerView(exception_view_spec, '', IExceptionViewClassifier,
+ req_iface, RuntimeError)
+ environ = self._makeEnviron(PATH_INFO='/archives/action1/article1')
+ start_response = DummyStartResponse()
+ router = self._makeOne()
+ result = router(environ, start_response)
+ self.assertEqual(result, ["Hello, special world"])
+
+ def test_call_route_raises_exception_view_another_route(self):
+ from repoze.bfg.interfaces import IViewClassifier
+ from repoze.bfg.interfaces import IExceptionViewClassifier
+ req_iface = self._registerRouteRequest('foo')
+ another_req_iface = self._registerRouteRequest('bar')
+ self._connectRoute('archives/:action/:article', 'foo', None)
+ view = DummyView(DummyResponse(), raise_exception=RuntimeError)
+ self._registerView(view, '', IViewClassifier, req_iface, None)
+ response = DummyResponse()
+ response.app_iter = ["Hello, world"]
+ exception_view = DummyView(response)
+ self._registerView(exception_view, '', IExceptionViewClassifier,
+ another_req_iface, RuntimeError)
+ environ = self._makeEnviron(PATH_INFO='/archives/action1/article1')
+ start_response = DummyStartResponse()
+ router = self._makeOne()
+ self.assertRaises(RuntimeError, router, environ, start_response)
+
+ def test_call_view_raises_exception_view_route(self):
+ from repoze.bfg.interfaces import IRequest
+ from repoze.bfg.interfaces import IViewClassifier
+ from repoze.bfg.interfaces import IExceptionViewClassifier
+ req_iface = self._registerRouteRequest('foo')
+ response = DummyResponse()
+ exception_response = DummyResponse()
+ exception_response.app_iter = ["Hello, world"]
+ view = DummyView(response, raise_exception=RuntimeError)
+ exception_view = DummyView(exception_response)
+ environ = self._makeEnviron()
+ self._registerView(view, '', IViewClassifier, IRequest, None)
+ self._registerView(exception_view, '', IExceptionViewClassifier,
+ req_iface, RuntimeError)
+ router = self._makeOne()
+ start_response = DummyStartResponse()
+ self.assertRaises(RuntimeError, router, environ, start_response)
class DummyContext:
pass
class DummyView:
- def __init__(self, response, raise_unauthorized=False,
- raise_notfound=False):
+ def __init__(self, response, raise_exception=None):
self.response = response
- self.raise_unauthorized = raise_unauthorized
- self.raise_notfound = raise_notfound
+ self.raise_exception = raise_exception
def __call__(self, context, request):
self.context = context
self.request = request
- if self.raise_unauthorized:
- from repoze.bfg.exceptions import Forbidden
- raise Forbidden('unauthorized')
- if self.raise_notfound:
- from repoze.bfg.exceptions import NotFound
- raise NotFound('notfound')
+ if not self.raise_exception is None:
+ raise self.raise_exception
return self.response
class DummyRootFactory:
@@ -662,3 +953,12 @@ class DummyLogger:
warn = info
debug = info
+def exc_raised(exc, func, *arg, **kw):
+ try:
+ func(*arg, **kw)
+ except exc, e:
+ return e
+ else:
+ raise AssertionError('%s not raised' % exc) # pragma: no cover
+
+
diff --git a/repoze/bfg/tests/test_security.py b/repoze/bfg/tests/test_security.py
index 0a15831b7..13a0e2d9b 100644
--- a/repoze/bfg/tests/test_security.py
+++ b/repoze/bfg/tests/test_security.py
@@ -118,6 +118,7 @@ class TestViewExecutionPermitted(unittest.TestCase):
from repoze.bfg.threadlocal import get_current_registry
from zope.interface import Interface
from repoze.bfg.interfaces import ISecuredView
+ from repoze.bfg.interfaces import IViewClassifier
class Checker(object):
def __permitted__(self, context, request):
self.context = context
@@ -125,7 +126,7 @@ class TestViewExecutionPermitted(unittest.TestCase):
return allow
checker = Checker()
reg = get_current_registry()
- reg.registerAdapter(checker, (Interface, Interface),
+ reg.registerAdapter(checker, (IViewClassifier, Interface, Interface),
ISecuredView, view_name)
return checker
diff --git a/repoze/bfg/tests/test_view.py b/repoze/bfg/tests/test_view.py
index 5f053d94d..bcfa45e91 100644
--- a/repoze/bfg/tests/test_view.py
+++ b/repoze/bfg/tests/test_view.py
@@ -11,7 +11,8 @@ class BaseTest(object):
def _registerView(self, reg, app, name):
from repoze.bfg.interfaces import IRequest
- for_ = (IRequest, IContext)
+ from repoze.bfg.interfaces import IViewClassifier
+ for_ = (IViewClassifier, IRequest, IContext)
from repoze.bfg.interfaces import IView
reg.registerAdapter(app, for_, IView, name)
diff --git a/repoze/bfg/tests/test_zcml.py b/repoze/bfg/tests/test_zcml.py
index 426e6e24d..452769de8 100644
--- a/repoze/bfg/tests/test_zcml.py
+++ b/repoze/bfg/tests/test_zcml.py
@@ -23,6 +23,7 @@ class TestViewDirective(unittest.TestCase):
def test_request_type_ashttpmethod(self):
from repoze.bfg.threadlocal import get_current_registry
from repoze.bfg.interfaces import IView
+ from repoze.bfg.interfaces import IViewClassifier
from repoze.bfg.interfaces import IRequest
context = DummyContext()
view = lambda *arg: None
@@ -37,7 +38,8 @@ class TestViewDirective(unittest.TestCase):
register = action['callable']
register()
reg = get_current_registry()
- wrapper = reg.adapters.lookup((IRequest, IDummy), IView, name='')
+ wrapper = reg.adapters.lookup(
+ (IViewClassifier, IRequest, IDummy), IView, name='')
request = DummyRequest()
request.method = 'GET'
self.assertEqual(wrapper.__predicated__(None, request), True)
@@ -48,6 +50,7 @@ class TestViewDirective(unittest.TestCase):
from zope.interface import directlyProvides
from repoze.bfg.threadlocal import get_current_registry
from repoze.bfg.interfaces import IView
+ from repoze.bfg.interfaces import IViewClassifier
from repoze.bfg.interfaces import IRequest
context = DummyContext(IDummy)
view = lambda *arg: 'OK'
@@ -61,7 +64,8 @@ class TestViewDirective(unittest.TestCase):
register = actions[0]['callable']
register()
reg = get_current_registry()
- regview = reg.adapters.lookup((IRequest, IDummy), IView, name='')
+ regview = reg.adapters.lookup(
+ (IViewClassifier, IRequest, IDummy), IView, name='')
self.assertNotEqual(view, regview)
request = DummyRequest()
directlyProvides(request, IDummy)
@@ -81,6 +85,7 @@ class TestViewDirective(unittest.TestCase):
def test_with_dotted_renderer(self):
from repoze.bfg.threadlocal import get_current_registry
from repoze.bfg.interfaces import IView
+ from repoze.bfg.interfaces import IViewClassifier
from repoze.bfg.interfaces import IRendererFactory
from repoze.bfg.interfaces import IRequest
context = DummyContext()
@@ -100,12 +105,14 @@ class TestViewDirective(unittest.TestCase):
self.assertEqual(actions[0]['discriminator'], discrim)
register = actions[0]['callable']
register()
- regview = reg.adapters.lookup((IRequest, IDummy), IView, name='')
+ regview = reg.adapters.lookup(
+ (IViewClassifier, IRequest, IDummy), IView, name='')
self.assertEqual(regview(None, None).body, 'OK')
def test_with_custom_predicates(self):
from repoze.bfg.threadlocal import get_current_registry
from repoze.bfg.interfaces import IView
+ from repoze.bfg.interfaces import IViewClassifier
from repoze.bfg.interfaces import IRequest
context = DummyContext()
reg = get_current_registry()
@@ -125,12 +132,14 @@ class TestViewDirective(unittest.TestCase):
self.assertEqual(actions[0]['discriminator'], discrim)
register = actions[0]['callable']
register()
- regview = reg.adapters.lookup((IRequest, IDummy), IView, name='')
+ regview = reg.adapters.lookup(
+ (IViewClassifier, IRequest, IDummy), IView, name='')
self.assertEqual(regview(None, None), 'OK')
def test_context_trumps_for(self):
from repoze.bfg.threadlocal import get_current_registry
from repoze.bfg.interfaces import IView
+ from repoze.bfg.interfaces import IViewClassifier
from repoze.bfg.interfaces import IRequest
context = DummyContext()
reg = get_current_registry()
@@ -146,12 +155,14 @@ class TestViewDirective(unittest.TestCase):
self.assertEqual(actions[0]['discriminator'], discrim)
register = actions[0]['callable']
register()
- regview = reg.adapters.lookup((IRequest, IDummy), IView, name='')
+ regview = reg.adapters.lookup(
+ (IViewClassifier, IRequest, IDummy), IView, name='')
self.assertEqual(regview(None, None), 'OK')
def test_with_for(self):
from repoze.bfg.threadlocal import get_current_registry
from repoze.bfg.interfaces import IView
+ from repoze.bfg.interfaces import IViewClassifier
from repoze.bfg.interfaces import IRequest
context = DummyContext()
reg = get_current_registry()
@@ -166,7 +177,8 @@ class TestViewDirective(unittest.TestCase):
self.assertEqual(actions[0]['discriminator'], discrim)
register = actions[0]['callable']
register()
- regview = reg.adapters.lookup((IRequest, IDummy), IView, name='')
+ regview = reg.adapters.lookup(
+ (IViewClassifier, IRequest, IDummy), IView, name='')
self.assertEqual(regview(None, None), 'OK')
class TestNotFoundDirective(unittest.TestCase):
@@ -176,13 +188,17 @@ class TestNotFoundDirective(unittest.TestCase):
def tearDown(self):
testing.tearDown()
- def _callFUT(self, context, view):
+ def _callFUT(self, context, view, **kw):
from repoze.bfg.zcml import notfound
- return notfound(context, view)
+ return notfound(context, view, **kw)
def test_it(self):
+ from zope.interface import implementedBy
from repoze.bfg.threadlocal import get_current_registry
- from repoze.bfg.interfaces import INotFoundView
+ from repoze.bfg.interfaces import IRequest
+ from repoze.bfg.interfaces import IView
+ from repoze.bfg.interfaces import IViewClassifier
+ from repoze.bfg.exceptions import NotFound
context = DummyContext()
def view(request):
@@ -191,14 +207,48 @@ class TestNotFoundDirective(unittest.TestCase):
actions = context.actions
self.assertEqual(len(actions), 1)
+ discrim = ('view', NotFound, '', None, IView, None, None, None, None,
+ None, False, None, None, None)
regadapt = actions[0]
- self.assertEqual(regadapt['discriminator'], INotFoundView)
+ self.assertEqual(regadapt['discriminator'], discrim)
register = regadapt['callable']
register()
reg = get_current_registry()
- derived_view = reg.getUtility(INotFoundView)
+ derived_view = reg.adapters.lookup(
+ (IViewClassifier, IRequest, implementedBy(NotFound)),
+ IView, default=None)
+
+ self.assertNotEqual(derived_view, None)
self.assertEqual(derived_view(None, None), 'OK')
- self.assertEqual(derived_view.__name__, view.__name__)
+ self.assertEqual(derived_view.__name__, 'bwcompat_view')
+
+ def test_it_with_dotted_renderer(self):
+ from zope.interface import implementedBy
+ from repoze.bfg.threadlocal import get_current_registry
+ from repoze.bfg.interfaces import IRequest
+ from repoze.bfg.interfaces import IView
+ from repoze.bfg.interfaces import IViewClassifier
+ from repoze.bfg.exceptions import NotFound
+ from repoze.bfg.configuration import Configurator
+ context = DummyContext()
+ reg = get_current_registry()
+ config = Configurator(reg)
+ def dummy_renderer_factory(*arg, **kw):
+ return lambda *arg, **kw: 'OK'
+ config.add_renderer('.pt', dummy_renderer_factory)
+ def view(request):
+ return {}
+ self._callFUT(context, view, renderer='fake.pt')
+ actions = context.actions
+ regadapt = actions[0]
+ register = regadapt['callable']
+ register()
+ derived_view = reg.adapters.lookup(
+ (IViewClassifier, IRequest, implementedBy(NotFound)),
+ IView, default=None)
+ self.assertNotEqual(derived_view, None)
+ self.assertEqual(derived_view(None, None).body, 'OK')
+ self.assertEqual(derived_view.__name__, 'bwcompat_view')
class TestForbiddenDirective(unittest.TestCase):
def setUp(self):
@@ -207,92 +257,67 @@ class TestForbiddenDirective(unittest.TestCase):
def tearDown(self):
testing.tearDown()
- def _callFUT(self, context, view):
+ def _callFUT(self, context, view, **kw):
from repoze.bfg.zcml import forbidden
- return forbidden(context, view)
+ return forbidden(context, view, **kw)
def test_it(self):
+ from zope.interface import implementedBy
from repoze.bfg.threadlocal import get_current_registry
+ from repoze.bfg.interfaces import IRequest
+ from repoze.bfg.interfaces import IView
+ from repoze.bfg.interfaces import IViewClassifier
+ from repoze.bfg.exceptions import Forbidden
context = DummyContext()
def view(request):
return 'OK'
self._callFUT(context, view)
actions = context.actions
- from repoze.bfg.interfaces import IForbiddenView
self.assertEqual(len(actions), 1)
+ discrim = ('view', Forbidden, '', None, IView, None, None, None, None,
+ None, False, None, None, None)
regadapt = actions[0]
- self.assertEqual(regadapt['discriminator'], IForbiddenView)
+ self.assertEqual(regadapt['discriminator'], discrim)
register = regadapt['callable']
register()
reg = get_current_registry()
- derived_view = reg.getUtility(IForbiddenView)
- self.assertEqual(derived_view(None, None), 'OK')
- self.assertEqual(derived_view.__name__, view.__name__)
-
-class TestSystemViewHandler(unittest.TestCase):
- def setUp(self):
- testing.setUp()
-
- def tearDown(self):
- testing.tearDown()
+ derived_view = reg.adapters.lookup(
+ (IViewClassifier, IRequest, implementedBy(Forbidden)),
+ IView, default=None)
- def _makeOne(self, iface):
- from repoze.bfg.zcml import SystemViewHandler
- return SystemViewHandler(iface)
+ self.assertNotEqual(derived_view, None)
+ self.assertEqual(derived_view(None, None), 'OK')
+ self.assertEqual(derived_view.__name__, 'bwcompat_view')
- def test_no_view_no_renderer(self):
- handler = self._makeOne(IDummy)
- from repoze.bfg.exceptions import ConfigurationError
- context = DummyContext()
- handler(context)
- actions = context.actions
- self.assertEqual(len(actions), 1)
- regadapt = actions[0]
- self.assertEqual(regadapt['discriminator'], IDummy)
- register = regadapt['callable']
- self.assertRaises(ConfigurationError, register)
-
- def test_no_view_with_renderer(self):
+ def test_it_with_dotted_renderer(self):
+ from zope.interface import implementedBy
from repoze.bfg.threadlocal import get_current_registry
- from repoze.bfg.interfaces import IRendererFactory
- reg = get_current_registry()
- def renderer(path):
- return lambda *arg: 'OK'
- reg.registerUtility(renderer, IRendererFactory, name='dummy')
+ from repoze.bfg.interfaces import IRequest
+ from repoze.bfg.interfaces import IView
+ from repoze.bfg.interfaces import IViewClassifier
+ from repoze.bfg.exceptions import Forbidden
+ from repoze.bfg.configuration import Configurator
context = DummyContext()
- handler = self._makeOne(IDummy)
- handler(context, renderer='dummy')
- actions = context.actions
- self.assertEqual(len(actions), 1)
- regadapt = actions[0]
- self.assertEqual(regadapt['discriminator'], IDummy)
- register = regadapt['callable']
- register()
- derived_view = reg.getUtility(IDummy)
- request = DummyRequest()
- self.assertEqual(derived_view(None, request).body, 'OK')
-
- def test_template_renderer(self):
- from repoze.bfg.threadlocal import get_current_registry
- from repoze.bfg.interfaces import IRendererFactory
reg = get_current_registry()
- def renderer(path):
- return lambda *arg: 'OK'
- reg.registerUtility(renderer, IRendererFactory, name='.pt')
- context = DummyContext()
- handler = self._makeOne(IDummy)
- handler(context, renderer='fixtures/minimal.pt')
+ config = Configurator(reg)
+ def dummy_renderer_factory(*arg, **kw):
+ return lambda *arg, **kw: 'OK'
+ config.add_renderer('.pt', dummy_renderer_factory)
+ def view(request):
+ return {}
+ self._callFUT(context, view, renderer='fake.pt')
actions = context.actions
- self.assertEqual(len(actions), 1)
regadapt = actions[0]
- self.assertEqual(regadapt['discriminator'], IDummy)
register = regadapt['callable']
register()
- derived_view = reg.getUtility(IDummy)
- request = DummyRequest()
- self.assertEqual(derived_view(None, request).body, 'OK')
+ derived_view = reg.adapters.lookup(
+ (IViewClassifier, IRequest, implementedBy(Forbidden)),
+ IView, default=None)
+ self.assertNotEqual(derived_view, None)
+ self.assertEqual(derived_view(None, None).body, 'OK')
+ self.assertEqual(derived_view.__name__, 'bwcompat_view')
class TestRepozeWho1AuthenticationPolicyDirective(unittest.TestCase):
def setUp(self):
@@ -506,6 +531,7 @@ class TestRouteDirective(unittest.TestCase):
from repoze.bfg.threadlocal import get_current_registry
from zope.interface import Interface
from repoze.bfg.interfaces import IView
+ from repoze.bfg.interfaces import IViewClassifier
from repoze.bfg.interfaces import IRouteRequest
context = DummyContext()
view = lambda *arg: 'OK'
@@ -526,12 +552,14 @@ class TestRouteDirective(unittest.TestCase):
view_discriminator = view_action['discriminator']
discrim = ('view', None, '', None, IView, 'name', None)
self.assertEqual(view_discriminator, discrim)
- wrapped = reg.adapters.lookup((request_type, Interface), IView, name='')
+ wrapped = reg.adapters.lookup(
+ (IViewClassifier, request_type, Interface), IView, name='')
self.failUnless(wrapped)
def test_with_view_and_view_context(self):
from repoze.bfg.threadlocal import get_current_registry
from repoze.bfg.interfaces import IView
+ from repoze.bfg.interfaces import IViewClassifier
from repoze.bfg.interfaces import IRouteRequest
context = DummyContext()
view = lambda *arg: 'OK'
@@ -552,12 +580,14 @@ class TestRouteDirective(unittest.TestCase):
view_discriminator = view_action['discriminator']
discrim = ('view', IDummy, '', None, IView, 'name', None)
self.assertEqual(view_discriminator, discrim)
- wrapped = reg.adapters.lookup((request_type, IDummy), IView, name='')
+ wrapped = reg.adapters.lookup(
+ (IViewClassifier, request_type, IDummy), IView, name='')
self.failUnless(wrapped)
def test_with_view_context_trumps_view_for(self):
from repoze.bfg.threadlocal import get_current_registry
from repoze.bfg.interfaces import IView
+ from repoze.bfg.interfaces import IViewClassifier
from repoze.bfg.interfaces import IRouteRequest
context = DummyContext()
view = lambda *arg: 'OK'
@@ -581,7 +611,8 @@ class TestRouteDirective(unittest.TestCase):
view_discriminator = view_action['discriminator']
discrim = ('view', IDummy, '', None, IView, 'name', None)
self.assertEqual(view_discriminator, discrim)
- wrapped = reg.adapters.lookup((request_type, IDummy), IView, name='')
+ wrapped = reg.adapters.lookup(
+ (IViewClassifier, request_type, IDummy), IView, name='')
self.failUnless(wrapped)
def test_with_dotted_renderer(self):
@@ -589,9 +620,8 @@ class TestRouteDirective(unittest.TestCase):
from repoze.bfg.threadlocal import get_current_registry
from zope.interface import Interface
from repoze.bfg.interfaces import IView
+ from repoze.bfg.interfaces import IViewClassifier
from repoze.bfg.interfaces import IRouteRequest
-
-
from repoze.bfg.interfaces import IRendererFactory
reg = get_current_registry()
def renderer(path):
@@ -617,7 +647,8 @@ class TestRouteDirective(unittest.TestCase):
view_discriminator = view_action['discriminator']
discrim = ('view', None, '', None, IView, 'name', None)
self.assertEqual(view_discriminator, discrim)
- wrapped = reg.adapters.lookup((request_type, Interface), IView, name='')
+ wrapped = reg.adapters.lookup(
+ (IViewClassifier, request_type, Interface), IView, name='')
self.failUnless(wrapped)
request = DummyRequest()
result = wrapped(None, request)
@@ -658,6 +689,7 @@ class TestStaticDirective(unittest.TestCase):
from zope.interface import implementedBy
from repoze.bfg.static import StaticRootFactory
from repoze.bfg.interfaces import IView
+ from repoze.bfg.interfaces import IViewClassifier
from repoze.bfg.interfaces import IRouteRequest
from repoze.bfg.interfaces import IRoutesMapper
context = DummyContext()
@@ -684,7 +716,8 @@ class TestStaticDirective(unittest.TestCase):
self.assertEqual(discriminator[4], IView)
iface = implementedBy(StaticRootFactory)
request_type = reg.getUtility(IRouteRequest, 'name')
- view = reg.adapters.lookup((request_type, iface), IView, name='')
+ view = reg.adapters.lookup(
+ (IViewClassifier, request_type, iface), IView, name='')
request = DummyRequest()
self.assertEqual(view(None, request).__class__, PackageURLParser)
diff --git a/repoze/bfg/view.py b/repoze/bfg/view.py
index 8c1430654..1f83faa5b 100644
--- a/repoze/bfg/view.py
+++ b/repoze/bfg/view.py
@@ -25,6 +25,7 @@ from zope.interface.advice import getFrameInfo
from repoze.bfg.interfaces import IResponseFactory
from repoze.bfg.interfaces import IRoutesMapper
from repoze.bfg.interfaces import IView
+from repoze.bfg.interfaces import IViewClassifier
from repoze.bfg.path import caller_package
from repoze.bfg.path import package_path
@@ -70,7 +71,7 @@ def render_view_to_response(context, request, name='', secure=True):
was disallowed.
If ``secure`` is ``False``, no permission checking is done."""
- provides = map(providedBy, (request, context))
+ provides = [IViewClassifier] + map(providedBy, (request, context))
try:
reg = request.registry
except AttributeError:
diff --git a/repoze/bfg/zcml.py b/repoze/bfg/zcml.py
index 4ae04387f..3935303c5 100644
--- a/repoze/bfg/zcml.py
+++ b/repoze/bfg/zcml.py
@@ -18,8 +18,6 @@ from zope.schema import TextLine
from repoze.bfg.interfaces import IAuthenticationPolicy
from repoze.bfg.interfaces import IAuthorizationPolicy
-from repoze.bfg.interfaces import IForbiddenView
-from repoze.bfg.interfaces import INotFoundView
from repoze.bfg.interfaces import IRendererFactory
from repoze.bfg.interfaces import IRouteRequest
from repoze.bfg.interfaces import IView
@@ -30,6 +28,8 @@ from repoze.bfg.authentication import RepozeWho1AuthenticationPolicy
from repoze.bfg.authorization import ACLAuthorizationPolicy
from repoze.bfg.configuration import Configurator
from repoze.bfg.exceptions import ConfigurationError
+from repoze.bfg.exceptions import NotFound
+from repoze.bfg.exceptions import Forbidden
from repoze.bfg.request import route_request_iface
from repoze.bfg.resource import resource_spec_from_abspath
from repoze.bfg.static import StaticRootFactory
@@ -356,29 +356,52 @@ class ISystemViewDirective(Interface):
description = u'',
required=False)
-class SystemViewHandler(object):
- def __init__(self, iface):
- self.iface = iface
+def notfound(_context,
+ view=None,
+ attr=None,
+ renderer=None,
+ wrapper=None):
- def __call__(self, _context, view=None, attr=None, renderer=None,
- wrapper=None):
- if renderer and '.' in renderer:
- renderer = path_spec(_context, renderer)
+ if renderer and '.' in renderer:
+ renderer = path_spec(_context, renderer)
- def register(iface=self.iface):
- reg = get_current_registry()
- config = Configurator(reg, package=_context.package)
- config._system_view(iface, view=view, attr=attr, renderer=renderer,
- wrapper=wrapper, _info=_context.info)
+ def register():
+ reg = get_current_registry()
+ config = Configurator(reg, package=_context.package)
+ config.set_notfound_view(view=view, attr=attr, renderer=renderer,
+ wrapper=wrapper, _info=_context.info)
- _context.action(
- discriminator = self.iface,
- callable = register,
- )
-
-notfound = SystemViewHandler(INotFoundView)
-forbidden = SystemViewHandler(IForbiddenView)
+ discriminator = ('view', NotFound, '', None, IView, None, None, None,
+ None, attr, False, None, None, None)
+
+ _context.action(
+ discriminator = discriminator,
+ callable = register,
+ )
+
+def forbidden(_context,
+ view=None,
+ attr=None,
+ renderer=None,
+ wrapper=None):
+ if renderer and '.' in renderer:
+ renderer = path_spec(_context, renderer)
+
+ def register():
+ reg = get_current_registry()
+ config = Configurator(reg, package=_context.package)
+ config.set_forbidden_view(view=view, attr=attr, renderer=renderer,
+ wrapper=wrapper, _info=_context.info)
+
+ discriminator = ('view', Forbidden, '', None, IView, None, None, None,
+ None, attr, False, None, None, None)
+
+ _context.action(
+ discriminator = discriminator,
+ callable = register,
+ )
+
class IResourceDirective(Interface):
"""
Directive for specifying that one package may override resources from