diff options
| author | Chris McDonough <chrism@agendaless.com> | 2010-04-14 02:49:19 +0000 |
|---|---|---|
| committer | Chris McDonough <chrism@agendaless.com> | 2010-04-14 02:49:19 +0000 |
| commit | ff1213e8f2aed987108ba57aed517c033491b1aa (patch) | |
| tree | f531544c3373ae7d5b51746987cb373326277a9c | |
| parent | 2b6bc8adfa294f7133680f64df411251afb67dfc (diff) | |
| download | pyramid-ff1213e8f2aed987108ba57aed517c033491b1aa.tar.gz pyramid-ff1213e8f2aed987108ba57aed517c033491b1aa.tar.bz2 pyramid-ff1213e8f2aed987108ba57aed517c033491b1aa.zip | |
Add "exception views" work contributed primarily by Andrey Popp by merging the "phash" branch.
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) ================== @@ -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 |
