diff options
| author | Rob Miller <rob@mochimedia.com> | 2010-12-29 14:12:30 -0800 |
|---|---|---|
| committer | Rob Miller <rob@mochimedia.com> | 2010-12-29 14:12:30 -0800 |
| commit | 9c1d34a3de11286c98dce52592ef05a3c05046a5 (patch) | |
| tree | 3b9d60fabfa876bdaba9debd477f85cd9cf926bf | |
| parent | 88231cc1b16f1f5a0983dba1dab9b401bbde0c00 (diff) | |
| parent | 8739f576ed84bb48cec9c2d4b60e92878a273b1f (diff) | |
| download | pyramid-9c1d34a3de11286c98dce52592ef05a3c05046a5.tar.gz pyramid-9c1d34a3de11286c98dce52592ef05a3c05046a5.tar.bz2 pyramid-9c1d34a3de11286c98dce52592ef05a3c05046a5.zip | |
Merge remote branch 'refs/remotes/upstream/viewderiver' into ra_view_decoration
Conflicts:
CHANGES.txt
pyramid/config.py
| -rw-r--r-- | CHANGES.txt | 20 | ||||
| -rw-r--r-- | TODO.txt | 9 | ||||
| -rw-r--r-- | docs/api/interfaces.rst | 4 | ||||
| -rw-r--r-- | docs/index.rst | 1 | ||||
| -rw-r--r-- | docs/latexindex.rst | 3 | ||||
| -rw-r--r-- | docs/narr/assets.rst | 4 | ||||
| -rw-r--r-- | docs/narr/csrf.rst | 2 | ||||
| -rw-r--r-- | docs/narr/flash.rst | 14 | ||||
| -rw-r--r-- | docs/narr/introduction.rst | 2 | ||||
| -rw-r--r-- | docs/narr/renderers.rst | 1 | ||||
| -rw-r--r-- | docs/narr/resources.rst | 8 | ||||
| -rw-r--r-- | docs/narr/urldispatch.rst | 6 | ||||
| -rw-r--r-- | docs/tutorials/cmf/actions.rst | 28 | ||||
| -rw-r--r-- | docs/tutorials/cmf/catalog.rst | 73 | ||||
| -rw-r--r-- | docs/tutorials/cmf/content.rst | 67 | ||||
| -rw-r--r-- | docs/tutorials/cmf/index.rst | 38 | ||||
| -rw-r--r-- | docs/tutorials/cmf/missing.rst | 22 | ||||
| -rw-r--r-- | docs/tutorials/cmf/skins.rst | 23 | ||||
| -rw-r--r-- | docs/tutorials/cmf/workflow.rst | 14 | ||||
| -rw-r--r-- | pyramid/config.py | 654 | ||||
| -rw-r--r-- | pyramid/interfaces.py | 21 | ||||
| -rw-r--r-- | pyramid/paster.py | 19 | ||||
| -rw-r--r-- | pyramid/tests/test_config.py | 20 | ||||
| -rw-r--r-- | pyramid/tests/test_paster.py | 35 | ||||
| -rw-r--r-- | setup.py | 2 |
25 files changed, 499 insertions, 591 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index c86594933..fabb882f7 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,14 @@ -1.0a9 (unreleased) +======= +Next release +============ + +Bug Fixes +--------- + +- The ``proutes`` command tried too hard to resolve the view for printing, + resulting in exceptions when an exceptional root factory was encountered. + Instead of trying to resolve the view, if it cannot, it will now just print + ``<unknown>``. Features -------- @@ -10,6 +20,14 @@ Features - If a handler class provides an _action_decorator classmethod, use that as the decorator for each view registration for that handler. +Documentation +------------- + +- The (weak) "Converting a CMF Application to Pyramid" tutorial has been + removed from the tutorials section. It was moved to the + ``pyramid_tutorials`` Github repository. + + 1.0a8 (2010-12-27) ================== @@ -8,6 +8,12 @@ Must-Have (before 1.0) - Consider deprecations for ``model`` and ``resource`` APIs. +- Re-make testing.setUp() and testing.tearDown() the canonical APIs for test + configuration. + +- ``decorator=`` parameter to view_config. This would replace the existing + _map_view "decorator" if it existed (Rob needs). + Should-Have ----------- @@ -26,9 +32,6 @@ Should-Have - Change "Cleaning up After a Request" in the urldispatch chapter to use ``request.add_response_callback``. -- ``decorator=`` parameter to view_config. This would replace the existing - _map_view "decorator" if it existed. - - Provide a response_set_cookie method on the request for rendered responses that can be used as input to response.set_cookie? diff --git a/docs/api/interfaces.rst b/docs/api/interfaces.rst index b3c14e5f7..3ce926230 100644 --- a/docs/api/interfaces.rst +++ b/docs/api/interfaces.rst @@ -35,3 +35,7 @@ Other Interfaces .. autointerface:: ITemplateRenderer + .. autointerface:: IViewMapperFactory + + .. autointerface:: IViewMapper + diff --git a/docs/index.rst b/docs/index.rst index 23ffb3b1b..d35eb2325 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -78,7 +78,6 @@ applications to various platforms. tutorials/wiki/index.rst tutorials/wiki2/index.rst tutorials/bfg/index.rst - tutorials/cmf/index.rst tutorials/gae/index.rst tutorials/modwsgi/index.rst tutorials/zeo/index.rst diff --git a/docs/latexindex.rst b/docs/latexindex.rst index 25e6791d0..058835937 100644 --- a/docs/latexindex.rst +++ b/docs/latexindex.rst @@ -125,7 +125,9 @@ ZCML Directive Reference zcml/configure zcml/default_permission zcml/forbidden + zcml/handler zcml/include + zcml/localenegotiator zcml/notfound zcml/remoteuserauthenticationpolicy zcml/renderer @@ -134,6 +136,7 @@ ZCML Directive Reference zcml/scan zcml/static zcml/subscriber + zcml/translationdir zcml/utility zcml/view diff --git a/docs/narr/assets.rst b/docs/narr/assets.rst index f147426ce..f73ff231a 100644 --- a/docs/narr/assets.rst +++ b/docs/narr/assets.rst @@ -98,7 +98,7 @@ directory on a filesystem to an application user's browser. Use the mechanism makes a directory of static files available at a name relative to the application root URL, e.g. ``/static`` or as an external URL. -.. note:: `~pyramid.config.Configurator.add_static_view` cannot serve a +.. note:: :meth:`~pyramid.config.Configurator.add_static_view` cannot serve a single file, nor can it serve a directory of static files directly relative to the root URL of a :app:`Pyramid` application. For these features, see :ref:`advanced_static`. @@ -282,7 +282,7 @@ create such a circumstance, we suggest using the in the application ``.ini`` file named ``media_location``. Then set the value of ``media_location`` to either a prefix or a URL depending on whether the application is being run in development or in production (use a different -`.ini`` file for production than you do for development). This is just a +``.ini`` file for production than you do for development). This is just a suggestion for a pattern; any setting name other than ``media_location`` could be used. diff --git a/docs/narr/csrf.rst b/docs/narr/csrf.rst index 7586b0ed7..2f545fb4f 100644 --- a/docs/narr/csrf.rst +++ b/docs/narr/csrf.rst @@ -9,7 +9,7 @@ phenomenon whereby a user with an identity on your website might click on a URL or button on another website which unwittingly redirects the user to your application to perform some command that requires elevated privileges. -You can avoid most of these attacks by making sure that a the correct *CSRF +You can avoid most of these attacks by making sure that the correct *CSRF token* has been set in an :app:`Pyramid` session object before performing any actions in code which requires elevated privileges and is invoked via a form post. To use CSRF token support, you must enable a :term:`session factory` diff --git a/docs/narr/flash.rst b/docs/narr/flash.rst index d41c2cdaf..037bfc416 100644 --- a/docs/narr/flash.rst +++ b/docs/narr/flash.rst @@ -38,7 +38,7 @@ provide is not modified in any way. The ``queue`` argument allows you to choose a queue to which to append the message you provide. This can be used to push different kinds of messages -into flash storage for later display in different places on a page. You cam +into flash storage for later display in different places on a page. You can pass any name for your queue, but it must be a string. The default value is the empty string, which chooses the default queue. Each queue is independent, and can be popped by ``pop_flash`` or examined via ``peek_flash`` separately. @@ -49,20 +49,22 @@ default flash message queue. request.session.flash(msg, 'myappsqueue') -The ``allow_duplicate`` argument, which defaults to ``True``. If this is +The ``allow_duplicate`` argument defaults to ``True``. If this is ``False``, if you attempt to add a message to a queue which is already present in the queue, it will not be added. Using the ``session.pop_flash`` Method -------------------------------------- -Once one or more messages has been added to a flash queue by the +Once one or more messages have been added to a flash queue by the ``session.flash`` API, the ``session.pop_flash`` API can be used to pop that queue and return it for use. To pop a particular queue of messages from the flash object, use the session object's ``pop_flash`` method. +.. method:: pop_flash(queue='') + .. code-block:: python :linenos: @@ -85,14 +87,16 @@ been popped. The object returned from ``pop_flash`` is a list. -Using the ``session.pop_flash`` Method --------------------------------------- +Using the ``session.peek_flash`` Method +--------------------------------------- Once one or more messages has been added to a flash queue by the ``session.flash`` API, the ``session.peek_flash`` API can be used to "peek" at that queue. Unlike ``session.pop_flash``, the queue is not popped from flash storage. +.. method:: peek_flash(queue='') + .. code-block:: python :linenos: diff --git a/docs/narr/introduction.rst b/docs/narr/introduction.rst index 3ade3726c..c61ef21d4 100644 --- a/docs/narr/introduction.rst +++ b/docs/narr/introduction.rst @@ -63,7 +63,7 @@ A Sense of Fun Minimalism :app:`Pyramid` provides only the very basics: *URL to code - mapping*, *templating*, *security*, and *resources*. There is not + mapping*, *templating*, *security*, and *assets*. There is not much more to the framework than these pieces: you are expected to provide the rest. diff --git a/docs/narr/renderers.rst b/docs/narr/renderers.rst index 3804fcf42..76e9562fa 100644 --- a/docs/narr/renderers.rst +++ b/docs/narr/renderers.rst @@ -22,6 +22,7 @@ response. For example: from pyramid.response import Response from pyramid.view import view_config + @view_config(renderer='json') def hello_world(request): return {'content':'Hello!'} diff --git a/docs/narr/resources.rst b/docs/narr/resources.rst index ac88afdfd..8cf2cead2 100644 --- a/docs/narr/resources.rst +++ b/docs/narr/resources.rst @@ -404,7 +404,7 @@ Obtaining the Lineage of a Resource ----------------------------------- :func:`pyramid.location.lineage` returns a generator representing the -:term:`lineage` of the :term:`location` aware:term:`resource` object. +:term:`lineage` of the :term:`location` aware :term:`resource` object. The :func:`~pyramid.location.lineage` function returns the resource it is passed, then each parent of the resource, in order. For example, if the @@ -533,7 +533,7 @@ declares that the blog entry implements an :term:`interface`. implements(IBlogEntry) def __init__(self, title, body, author): self.title = title - self.body = body + self.body = body self.author = author self.created = datetime.datetime.now() @@ -568,7 +568,7 @@ To do so, use the :func:`zope.interface.directlyProvides` function: class BlogEntry(object): def __init__(self, title, body, author): self.title = title - self.body = body + self.body = body self.author = author self.created = datetime.datetime.now() @@ -596,7 +596,7 @@ the :func:`zope.interface.alsoProvides` function: class BlogEntry(object): def __init__(self, title, body, author): self.title = title - self.body = body + self.body = body self.author = author self.created = datetime.datetime.now() diff --git a/docs/narr/urldispatch.rst b/docs/narr/urldispatch.rst index 4c601340f..0d28a0e96 100644 --- a/docs/narr/urldispatch.rst +++ b/docs/narr/urldispatch.rst @@ -533,12 +533,12 @@ neither predicates nor view configuration information. callables. Use custom predicates when no set of predefined predicates does what you need. Custom predicates can be combined with predefined predicates as necessary. Each custom predicate callable should accept two - arguments: ``context`` and ``request`` and should return either ``True`` or + arguments: ``info`` and ``request`` and should return either ``True`` or ``False`` after doing arbitrary evaluation of the context resource and/or the request. If all callables return ``True``, the associated route will be considered viable for a given request. If any custom predicate returns - ``False``, route matching continues. Note that the value ``context`` will - always be ``None`` when passed to a custom route predicate. + ``False``, route matching continues. See :ref:`custom_route_predicates` + for more information. **View-Related Arguments** diff --git a/docs/tutorials/cmf/actions.rst b/docs/tutorials/cmf/actions.rst deleted file mode 100644 index a6e33fa59..000000000 --- a/docs/tutorials/cmf/actions.rst +++ /dev/null @@ -1,28 +0,0 @@ -.. _actions_chapter: - -======= -Actions -======= - -In CMF, the "actions tool" along with "action providers" create an extensible -mechanism to show links in the CMF management UI that invoke a particular -behavior or which show a particular template. - -:app:`Pyramid` itself has no such concept, and no package provides a direct -replacement. Actions are such a generic concept that it's simple to -reimplement action-like navigation in a different way within any given -application. For example, a module-scope global dictionary which has keys -that are action names, and values which are tuples of (permission, link). -Take that concept and expand on it, and you'll have some passable actions -tool replacement within a single application. - -The `pyramid_viewgroup <https://github.com/Pylons/pyramid_viewgroup/>`_ -package provides some functionality for creating "view groups". Each view in -a viewgroup can provide some snippet of HTML (e.g. a single "tab"), and -individual views (tabs) within the group which cannot be displayed to the -user due to the user's lack of permissions will be omitted from the rendered -output. - -The :term:`repoze.lemonade` package provides "list item" support that -may be used to construct action lists. - diff --git a/docs/tutorials/cmf/catalog.rst b/docs/tutorials/cmf/catalog.rst deleted file mode 100644 index d5e9534ae..000000000 --- a/docs/tutorials/cmf/catalog.rst +++ /dev/null @@ -1,73 +0,0 @@ -.. _catalog_chapter: - -======= -Catalog -======= - -The main feature of the CMF catalog is that it filters search results -from the Zope 2 "catalog" based on the requesting user's ability to -view a particular cataloged object. - -:app:`Pyramid` itself has no cataloging facility, but an addon -package named :term:`repoze.catalog` offers similar functionality. - -Creating an Allowed Index -------------------------- - -In CMF, a catalog index named ``getAllowedRolesAndUsers`` along with -application indexing code allows for filtered search results. It's -reasonably easy to reproduce this pattern using some custom code. - -Creating The ``allowed`` Index -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Here's some code which creates an ``allowed`` index for use in a -``repoze.catalog`` catalog:: - - from pyramid.security import principals_allowed_by_permission - from repoze.catalog.indexes.keyword import CatalogKeywordIndex - from repoze.catalog.catalog import Catalog - - class Allowed: - def __init__(self, permission): - self.permission = permission - - def __call__(self, context, default): - principals = principals_allowed_by_permission(context, - self.permission) - return principals - - def make_allowed_index(permission='View'): - index = CatalogKeywordIndex(Allowed(permission)) - return index - - index = make_allowed_index() - catalog = Catalog() - catalog['allowed'] = index - -When you index an item, the allowed index will be populated with all -the principal ids which have the 'View' permission. - -Using the ``allowed`` Index -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Here's how you might use the ``allowed`` index within a query:: - - from pyramid.security import effective_principals - principals = effective_principals(request) - catalog.searchResults(allowed={'operator':'or', 'query':principals}) - -The above query will return all document ids that the current user has -the 'View' permission against. Add other indexes to the query to get -a useful result. - -See the `repoze.catalog package -<http://svn.repoze.org/repoze.catalog/trunk>`_ for more information. - - - - - - - - diff --git a/docs/tutorials/cmf/content.rst b/docs/tutorials/cmf/content.rst deleted file mode 100644 index 85e5b5fbc..000000000 --- a/docs/tutorials/cmf/content.rst +++ /dev/null @@ -1,67 +0,0 @@ -.. _content_types_chapter: - -============= -Content Types -============= - -In CMF, a content type is defined as a bag of settings (the type -information, controlled within the "types tool"), as well as factory -code which generates an instance of that content. It is possible to -construct and enumerate content types using APIs defined on the types -tool. - -:app:`Pyramid` itself has no such concept, but an addon package named -:term:`repoze.lemonade` has a barebones replacement. - -Factory Type Information ------------------------- - -A factory type information object in CMF allows you to associate a -title, a description, an internationalization domain, an icon, an -initial view name, a factory, and a number of security settings with a -type name. Each type information object knows how to manufacture -content objects that match its type. - -:app:`Pyramid` certainly enforces none of these concepts in any -particular way, but :term:`repoze.lemonade` does. - -``repoze.lemonade`` Content -+++++++++++++++++++++++++++ - -:term:`repoze.lemonade` provides a reasonably handy directive and set -of helper functions which allow you to: - -#. Associate a interface with a factory function, making it into a - "content type". - -#. Enumerate all interfaces associated with factory functions. - -.. note:: Using this pattern is often plain silly, as it's usually - just as easy to actually import a class implementation and - create an instance directly using its constructor. But it - can be useful in cases where you want to address some set of - constructors uniformly without doing direct imports in the - code which performs the construction, or if you need to make - content construction uniform across a diverse set of model - types, or if you need to enumerate some set of information - about "content" types. It's left as an exercise to the - reader to determine under which circumstances using this - pattern is an appropriate thing to do. Hint: not very - often, unless you're doing the indirection solely to aid - content-agnostic unit tests or if you need to get an - enumerated subset of content type information to aid in UI - generation. That said, this *is* a tutorial about how to - get CMF-like features in :app:`Pyramid`, so we'll assume - the pattern is useful to readers. - -See the `repoze.lemonade package -<http://svn.repoze.org/repoze.lemonade/trunk>`_ for more information, -particularly its documentation for "content". - - - - - - - - diff --git a/docs/tutorials/cmf/index.rst b/docs/tutorials/cmf/index.rst deleted file mode 100644 index 26aa336a9..000000000 --- a/docs/tutorials/cmf/index.rst +++ /dev/null @@ -1,38 +0,0 @@ -Converting an Existing Zope/CMF Application to :app:`Pyramid` -================================================================ - -The Zope `Content Management Framework -<http://www.zope.org/Products/CMF/>`_ (aka CMF) is a layer on top of -:term:`Zope` 2 that provides facilities for creating content-driven -websites. It's reasonably easy to convert a modern Zope/CMF -application to :app:`Pyramid`. - -The main difference between CMF and :app:`Pyramid` is that :app:`Pyramid` -does not advertise itself as a system into which you can plug arbitrary -"packages" that extend a system-supplied management user interface. You -*could* build a CMF-like layer on top of :app:`Pyramid` but none currently -exists. For those sorts of high-extensibility, highly-regularized-UI -systems, CMF is still the better choice. - -:app:`Pyramid` (and other more lightweight systems) is often a -better choice when you're building the a user interface from scratch, -which often happens when the paradigms of some CMF-provided user -interface don't match the requirements of an application very closely. -Even so, a good number of developers tend to use CMF even when they do -start an application for which they need to build a UI from scratch, -because CMF happens to provide other helpful services, such as types, -skins, and workflow; this tutorial is for those sorts of developers -and projects. - -.. toctree:: - :maxdepth: 2 - - content.rst - catalog.rst - skins.rst - actions.rst - workflow.rst - missing.rst - - - diff --git a/docs/tutorials/cmf/missing.rst b/docs/tutorials/cmf/missing.rst deleted file mode 100644 index 964e0ab04..000000000 --- a/docs/tutorials/cmf/missing.rst +++ /dev/null @@ -1,22 +0,0 @@ -Missing Comparisons -=================== - -We currently don't have any comparative Pyramid-vs-CMF information -about the following concepts within this tutorial: - -- Templates - -- Forms - -- Membership - -- Discussions - -- Syndication - -- Dublincore - -Please ask on the `Pylons-devel maillist -<http://groups.google.com/group/pylons-devel>`_ or on the `#pylons IRC -channel <http://irc.freenode.net#pylons>`_ about these topics. - diff --git a/docs/tutorials/cmf/skins.rst b/docs/tutorials/cmf/skins.rst deleted file mode 100644 index 676a076b3..000000000 --- a/docs/tutorials/cmf/skins.rst +++ /dev/null @@ -1,23 +0,0 @@ -.. _skins_chapter: - -===== -Skins -===== - -In CMF, a "skin layer" is defined as a collection of templates and -code (Python scripts, DTML methods, etc) that can be activated and -deactivated within a particular setup. A collection of active "skin -layers" grouped in a particular order forms a "skin". "Add-on" CMF -products often provide skin layers that are activated within a -particular skin to provide the site with additional features. - -To override static resources using a "search path" much like a set of -skin layers, :app:`Pyramid` provides the concept of -:term:`resource` overrides. See :ref:`overriding_resources_section` -for more information about resource overrides. - -While there is no analogue to a skin layer search path for locating -Python code (as opposed to resources), :term:`view` code combined with -differing :term:`predicate` arguments can provide a good deal of -the same sort of behavior. - diff --git a/docs/tutorials/cmf/workflow.rst b/docs/tutorials/cmf/workflow.rst deleted file mode 100644 index cc70d771a..000000000 --- a/docs/tutorials/cmf/workflow.rst +++ /dev/null @@ -1,14 +0,0 @@ -.. _workflow_chapter: - -======== -Workflow -======== - -In CMF, the "workflow tool" allows developers to design state machines -which imply transition between content states. - -:app:`Pyramid` itself has no such concept, but the -:term:`repoze.workflow` package provides a simple state machine -implementation that can act as a barebones workflow tool. See its -documentation for more information. - diff --git a/pyramid/config.py b/pyramid/config.py index 274938225..e1005102b 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -4,6 +4,7 @@ import re import sys import threading import traceback +from types import FunctionType import venusian @@ -45,6 +46,7 @@ from pyramid.interfaces import ITranslationDirectories from pyramid.interfaces import ITraverser from pyramid.interfaces import IView from pyramid.interfaces import IViewClassifier +from pyramid.interfaces import IViewMapperFactory try: from pyramid import chameleon_text @@ -338,30 +340,25 @@ class Configurator(object): def _split_spec(self, path_or_spec): return resolve_asset_spec(path_or_spec, self.package_name) + # b/w compat def _derive_view(self, view, permission=None, predicates=(), attr=None, renderer=None, wrapper_viewname=None, viewname=None, accept=None, order=MAX_ORDER, phash=DEFAULT_PHASH): - if renderer is None: # use default renderer if one exists - default_renderer_factory = self.registry.queryUtility( - IRendererFactory) - if default_renderer_factory is not None: - renderer = {'name':None, 'package':self.package} view = self.maybe_dotted(view) - authn_policy = self.registry.queryUtility(IAuthenticationPolicy) - authz_policy = self.registry.queryUtility(IAuthorizationPolicy) - settings = self.registry.settings - logger = self.registry.queryUtility(IDebugLogger) - mapped_view = _map_view(view, self.registry, attr, renderer) - owrapped_view = _owrap_view(mapped_view, viewname, wrapper_viewname) - secured_view = _secure_view(owrapped_view, permission, - authn_policy, authz_policy) - debug_view = _authdebug_view(secured_view, permission, - authn_policy, authz_policy, settings, - logger) - predicated_view = _predicate_wrap(debug_view, predicates) - derived_view = _attr_wrap(predicated_view, accept, order, phash) - return derived_view + deriver = ViewDeriver( + registry=self.registry, + permission=permission, + predicates=predicates, + attr=attr, + renderer=renderer, + wrapper_viewname=wrapper_viewname, + viewname=viewname, + accept=accept, + order=order, + phash=phash, + package=self.package) + return deriver(view) def _override(self, package, path, override_package, override_prefix, PackageOverrides=PackageOverrides): @@ -941,11 +938,16 @@ class Configurator(object): pattern = route.pattern - action_decorator = getattr(handler, '_action_decorator', None) - if action_decorator is not None and action_decorator.im_self is None: - raise ConfigurationError( - 'The "_action_decorator" method on a handler class MUST be ' - 'defined as a classmethod.') + action_decorator = getattr(handler, '__action_decorator__', None) + if action_decorator is not None: + class_or_static = getattr(action_decorator, 'im_self', + None) is not None + if not class_or_static: + class_or_static = isinstance(action_decorator, FunctionType) + if not class_or_static: + raise ConfigurationError( + 'The "__action_decorator__" callable on a handler class ' + 'MUST be defined as a classmethod or a staticmethod.') path_has_action = ':action' in pattern or '{action}' in pattern @@ -1016,9 +1018,9 @@ class Configurator(object): def add_view(self, view=None, name="", for_=None, permission=None, request_type=None, route_name=None, request_method=None, request_param=None, containment=None, attr=None, - renderer=None, wrapper=None, decorator=None, xhr=False, - accept=None, header=None, path_info=None, - custom_predicates=(), context=None): + renderer=None, wrapper=None, xhr=False, accept=None, + header=None, path_info=None, custom_predicates=(), + context=None, decorator=None, view_mapper=None): """ Add a :term:`view configuration` to the current configuration state. Arguments to ``add_view`` are broken down below into *predicate* arguments and *non-predicate* @@ -1272,6 +1274,18 @@ class Configurator(object): the context and/or the request. If all callables return ``True``, the associated view callable will be considered viable for a given request. + + view_mapper + + A class implementing the + :class:`pyramid.interfaces.IViewMapperFactory` interface, which + performs view argument and response mapping. By default it is + ``None``, which indicates that the view should use the default view + mapper. This plug-point is useful for Pyramid extension + developers, but it's not very useful for' + 'civilians' who are just developing stock Pyramid applications. + Pay no attention to the man behind the curtain. + """ view = self.maybe_dotted(view) context = self.maybe_dotted(context) @@ -1310,6 +1324,7 @@ class Configurator(object): renderer=renderer, wrapper=wrapper, xhr=xhr, accept=accept, header=header, path_info=path_info, custom_predicates=custom_predicates, context=context, + view_mapper = view_mapper, ) view_info = deferred_views.setdefault(route_name, []) view_info.append(info) @@ -1340,13 +1355,18 @@ class Configurator(object): permission = self.registry.queryUtility(IDefaultPermission) # NO_PERMISSION_REQUIRED handled by _secure_view - derived_view = self._derive_view(view, permission, predicates, attr, - renderer, wrapper, name, accept, - order, phash) - if decorator is not None: - wrapped_view = decorator(derived_view) - decorate_view(wrapped_view, derived_view) - derived_view = wrapped_view + derived_view = ViewDeriver(registry=self.registry, + permission=permission, + predicates=predicates, + attr=attr, + renderer=renderer, + wrapper_viewname=wrapper, + viewname=name, + accept=accept, + order=order, + phash=phash, + decorator=decorator, + view_mapper=view_mapper)(view) registered = self.registry.adapters.registered @@ -2654,149 +2674,313 @@ class MultiView(object): continue raise PredicateMismatch(self.name) -def decorate_view(wrapped_view, original_view): - if wrapped_view is original_view: - return False - wrapped_view.__module__ = original_view.__module__ - wrapped_view.__doc__ = original_view.__doc__ - try: - wrapped_view.__name__ = original_view.__name__ - except AttributeError: - wrapped_view.__name__ = repr(original_view) - try: - wrapped_view.__permitted__ = original_view.__permitted__ - except AttributeError: - pass - try: - wrapped_view.__call_permissive__ = original_view.__call_permissive__ - except AttributeError: - pass - try: - wrapped_view.__predicated__ = original_view.__predicated__ - except AttributeError: - pass - try: - wrapped_view.__accept__ = original_view.__accept__ - except AttributeError: - pass - try: - wrapped_view.__order__ = original_view.__order__ - except AttributeError: - pass - return True - -def requestonly(class_or_callable, attr=None): - """ Return true of the class or callable accepts only a request argument, - as opposed to something that accepts context, request """ - if attr is None: - attr = '__call__' - if inspect.isfunction(class_or_callable): - fn = class_or_callable - elif inspect.isclass(class_or_callable): +def preserve_attrs(wrapped): + def inner(self, view): + wrapped_view = wrapped(self, view) + if wrapped_view is view: + return view + wrapped_view.__module__ = view.__module__ + wrapped_view.__doc__ = view.__doc__ try: - fn = class_or_callable.__init__ + wrapped_view.__name__ = view.__name__ except AttributeError: - return False - else: + wrapped_view.__name__ = repr(view) try: - fn = getattr(class_or_callable, attr) + wrapped_view.__permitted__ = view.__permitted__ except AttributeError: - return False + pass + try: + wrapped_view.__call_permissive__ = view.__call_permissive__ + except AttributeError: + pass + try: + wrapped_view.__predicated__ = view.__predicated__ + except AttributeError: + pass + try: + wrapped_view.__accept__ = view.__accept__ + except AttributeError: + pass + try: + wrapped_view.__order__ = view.__order__ + except AttributeError: + pass + return wrapped_view + return inner + +class ViewDeriver(object): + def __init__(self, **kw): + self.kw = kw + self.registry = kw['registry'] + self.authn_policy = self.registry.queryUtility( + IAuthenticationPolicy) + self.authz_policy = self.registry.queryUtility( + IAuthorizationPolicy) + self.logger = self.registry.queryUtility(IDebugLogger) + + def __call__(self, view): + mapper = self.kw.get('view_mapper') + if mapper is None: + mapper = DefaultViewMapper + view = mapper(**self.kw)(view) + return self.attr_wrapped_view( + self.predicated_view( + self.authdebug_view( + self.secured_view( + self.owrap_view( + view))))) + + @preserve_attrs + def owrap_view(self, view): + wrapper_viewname = self.kw.get('wrapper_viewname') + viewname = self.kw.get('viewname') + if not wrapper_viewname: + return view + def _owrapped_view(context, request): + response = view(context, request) + request.wrapped_response = response + request.wrapped_body = response.body + request.wrapped_view = view + wrapped_response = render_view_to_response(context, request, + wrapper_viewname) + if wrapped_response is None: + raise ValueError( + 'No wrapper view named %r found when executing view ' + 'named %r' % (wrapper_viewname, viewname)) + return wrapped_response + return _owrapped_view + + @preserve_attrs + def secured_view(self, view): + permission = self.kw.get('permission') + if permission == '__no_permission_required__': + # allow views registered within configurations that have a + # default permission to explicitly override the default + # permission, replacing it with no permission at all + permission = None + + wrapped_view = view + if self.authn_policy and self.authz_policy and (permission is not None): + def _secured_view(context, request): + principals = self.authn_policy.effective_principals(request) + if self.authz_policy.permits(context, principals, permission): + return view(context, request) + msg = getattr(request, 'authdebug_message', + 'Unauthorized: %s failed permission check' % view) + raise Forbidden(msg) + _secured_view.__call_permissive__ = view + def _permitted(context, request): + principals = self.authn_policy.effective_principals(request) + return self.authz_policy.permits(context, principals, + permission) + _secured_view.__permitted__ = _permitted + wrapped_view = _secured_view + + return wrapped_view + + @preserve_attrs + def authdebug_view(self, view): + wrapped_view = view + settings = self.registry.settings + permission = self.kw.get('permission') + if settings and settings.get('debug_authorization', False): + def _authdebug_view(context, request): + view_name = getattr(request, 'view_name', None) + + if self.authn_policy and self.authz_policy: + if permission is None: + msg = 'Allowed (no permission registered)' + else: + principals = self.authn_policy.effective_principals( + request) + msg = str(self.authz_policy.permits(context, principals, + permission)) + else: + msg = 'Allowed (no authorization policy in use)' + + view_name = getattr(request, 'view_name', None) + url = getattr(request, 'url', None) + msg = ('debug_authorization of url %s (view name %r against ' + 'context %r): %s' % (url, view_name, context, msg)) + self.logger and self.logger.debug(msg) + if request is not None: + request.authdebug_message = msg + return view(context, request) - try: - argspec = inspect.getargspec(fn) - except TypeError: - return False + wrapped_view = _authdebug_view - args = argspec[0] - defaults = argspec[3] + return wrapped_view - if hasattr(fn, 'im_func'): - # it's an instance method - if not args: + @preserve_attrs + def predicated_view(self, view): + predicates = self.kw.get('predicates', ()) + if not predicates: + return view + def predicate_wrapper(context, request): + if all((predicate(context, request) for predicate in predicates)): + return view(context, request) + raise PredicateMismatch('predicate mismatch for view %s' % view) + def checker(context, request): + return all((predicate(context, request) for predicate in + predicates)) + predicate_wrapper.__predicated__ = checker + return predicate_wrapper + + @preserve_attrs + def attr_wrapped_view(self, view): + kw = self.kw + accept, order, phash = (kw.get('accept', None), + kw.get('order', MAX_ORDER), + kw.get('phash', DEFAULT_PHASH)) + # this is a little silly but we don't want to decorate the original + # function with attributes that indicate accept, order, and phash, + # so we use a wrapper + 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.__order__ = order + attr_view.__phash__ = phash + return attr_view + +class DefaultViewMapper(object): + implements(IViewMapperFactory) + def __init__(self, **kw): + self.kw = kw + self.helper = None + self.renderer = kw.get('renderer') + self.registry = kw.get('registry') + if self.renderer is None and self.registry is not None: + # use default renderer if one exists + default_renderer_factory = self.registry.queryUtility( + IRendererFactory) + if default_renderer_factory is not None: + self.renderer = {'name':None, 'package':kw.get('package')} + if self.renderer is not None: + self.helper = RendererHelper(self.renderer['name'], + package=self.renderer['package'], + registry=self.registry) + + def requestonly(self, view): + attr = self.kw.get('attr') + if attr is None: + attr = '__call__' + if inspect.isfunction(view): + fn = view + elif inspect.isclass(view): + try: + fn = view.__init__ + except AttributeError: + return False + else: + try: + fn = getattr(view, attr) + except AttributeError: + return False + + try: + argspec = inspect.getargspec(fn) + except TypeError: return False - args = args[1:] - if not args: - return False - if len(args) == 1: - return True + args = argspec[0] + defaults = argspec[3] - elif args[0] == 'request': - if len(args) - len(defaults) == 1: + if hasattr(fn, 'im_func'): + # it's an instance method + if not args: + return False + args = args[1:] + if not args: + return False + + if len(args) == 1: return True - return False + elif args[0] == 'request': + if len(args) - len(defaults) == 1: + return True -def is_response(ob): - if ( hasattr(ob, 'app_iter') and hasattr(ob, 'headerlist') and - hasattr(ob, 'status') ): - return True - return False + return False + + @preserve_attrs + def __call__(self, view): + attr = self.kw.get('attr') + decorator = self.kw.get('decorator') + isclass = inspect.isclass(view) + ronly = self.requestonly(view) + if isclass and ronly: + view = self.map_requestonly_class(view) + elif isclass: + view = self.map_class(view) + elif ronly: + view = self.map_requestonly_func(view) + elif attr: + view = self.map_attr(view) + elif self.helper is not None: + view = self.map_rendered(view) + if decorator is not None: + view = decorator(view) + return view -def _map_view(view, registry, attr=None, renderer=None): - wrapped_view = view - - helper = None - - if renderer is not None: - helper = RendererHelper(renderer['name'], - package=renderer['package'], - registry=registry) - - if inspect.isclass(view): - # If the object we've located is a class, turn it into a - # function that operates like a Zope view (when it's invoked, - # construct an instance using 'context' and 'request' as - # position arguments, then immediately invoke the __call__ - # method of the instance with no arguments; __call__ should - # return an IResponse). - if requestonly(view, attr): - # its __init__ accepts only a single request argument, - # instead of both context and request - def _class_requestonly_view(context, request): - inst = view(request) - if attr is None: - response = inst() - else: - response = getattr(inst, attr)() - if helper is not None: - if not is_response(response): - system = { - 'view':inst, - 'renderer_name':renderer['name'], # b/c - 'renderer_info':renderer, - 'context':context, - 'request':request - } - response = helper.render_to_response(response, system, - request=request) - return response - wrapped_view = _class_requestonly_view - else: - # its __init__ accepts both context and request - def _class_view(context, request): - inst = view(context, request) - if attr is None: - response = inst() - else: - response = getattr(inst, attr)() - if helper is not None: - if not is_response(response): - system = {'view':inst, - 'renderer_name':renderer['name'], # b/c - 'renderer_info':renderer, - 'context':context, - 'request':request - } - response = helper.render_to_response(response, system, - request=request) - return response - wrapped_view = _class_view - - elif requestonly(view, attr): - # its __call__ accepts only a single request argument, - # instead of both context and request + def map_requestonly_class(self, view): + # its a class that has an __init__ which only accepts request + attr = self.kw.get('attr') + helper = self.helper + renderer = self.renderer + def _class_requestonly_view(context, request): + inst = view(request) + if attr is None: + response = inst() + else: + response = getattr(inst, attr)() + if helper is not None: + if not is_response(response): + system = { + 'view':inst, + 'renderer_name':renderer['name'], # b/c + 'renderer_info':renderer, + 'context':context, + 'request':request + } + response = helper.render_to_response(response, system, + request=request) + return response + return _class_requestonly_view + + def map_class(self, view): + # its a class that has an __init__ which accepts both context and + # request + attr = self.kw.get('attr') + helper = self.helper + renderer = self.renderer + def _class_view(context, request): + inst = view(context, request) + if attr is None: + response = inst() + else: + response = getattr(inst, attr)() + if helper is not None: + if not is_response(response): + system = {'view':inst, + 'renderer_name':renderer['name'], # b/c + 'renderer_info':renderer, + 'context':context, + 'request':request + } + response = helper.render_to_response(response, system, + request=request) + return response + return _class_view + + def map_requestonly_func(self, view): + # its a function or instance that has a __call__ accepts only a + # single request argument + attr = self.kw.get('attr') + helper = self.helper + renderer = self.renderer def _requestonly_view(context, request): if attr is None: response = view(request) @@ -2815,9 +2999,14 @@ def _map_view(view, registry, attr=None, renderer=None): response = helper.render_to_response(response, system, request=request) return response - wrapped_view = _requestonly_view - - elif attr: + return _requestonly_view + + def map_attr(self, view): + # its a function that has a __call__ which accepts both context and + # request, but still has an attr + attr = self.kw.get('attr') + helper = self.helper + renderer = self.renderer def _attr_view(context, request): response = getattr(view, attr)(context, request) if helper is not None: @@ -2832,9 +3021,13 @@ def _map_view(view, registry, attr=None, renderer=None): response = helper.render_to_response(response, system, request=request) return response - wrapped_view = _attr_view + return _attr_view - elif helper is not None: + def map_rendered(self, view): + # it's a function that has a __call__ that accepts both context and + # request, but requires rendering + helper = self.helper + renderer = self.renderer def _rendered_view(context, request): response = view(context, request) if not is_response(response): @@ -2848,113 +3041,7 @@ def _map_view(view, registry, attr=None, renderer=None): response = helper.render_to_response(response, system, request=request) return response - wrapped_view = _rendered_view - - decorate_view(wrapped_view, view) - return wrapped_view - -def _owrap_view(view, viewname, wrapper_viewname): - if not wrapper_viewname: - return view - def _owrapped_view(context, request): - response = view(context, request) - request.wrapped_response = response - request.wrapped_body = response.body - request.wrapped_view = view - wrapped_response = render_view_to_response(context, request, - wrapper_viewname) - if wrapped_response is None: - raise ValueError( - 'No wrapper view named %r found when executing view ' - 'named %r' % (wrapper_viewname, viewname)) - return wrapped_response - decorate_view(_owrapped_view, view) - return _owrapped_view - -def _predicate_wrap(view, predicates): - if not predicates: - return view - def predicate_wrapper(context, request): - if all((predicate(context, request) for predicate in predicates)): - return view(context, request) - raise PredicateMismatch('predicate mismatch for view %s' % view) - def checker(context, request): - return all((predicate(context, request) for predicate in - predicates)) - predicate_wrapper.__predicated__ = checker - decorate_view(predicate_wrapper, view) - return predicate_wrapper - -def _secure_view(view, permission, authn_policy, authz_policy): - if permission == '__no_permission_required__': - # allow views registered within configurations that have a - # default permission to explicitly override the default - # permission, replacing it with no permission at all - permission = None - - wrapped_view = view - if authn_policy and authz_policy and (permission is not None): - def _secured_view(context, request): - principals = authn_policy.effective_principals(request) - if authz_policy.permits(context, principals, permission): - return view(context, request) - msg = getattr(request, 'authdebug_message', - 'Unauthorized: %s failed permission check' % view) - raise Forbidden(msg) - _secured_view.__call_permissive__ = view - def _permitted(context, request): - principals = authn_policy.effective_principals(request) - return authz_policy.permits(context, principals, permission) - _secured_view.__permitted__ = _permitted - wrapped_view = _secured_view - decorate_view(wrapped_view, view) - - return wrapped_view - -def _authdebug_view(view, permission, authn_policy, authz_policy, settings, - logger): - wrapped_view = view - if settings and settings.get('debug_authorization', False): - def _authdebug_view(context, request): - view_name = getattr(request, 'view_name', None) - - if authn_policy and authz_policy: - if permission is None: - msg = 'Allowed (no permission registered)' - else: - principals = authn_policy.effective_principals(request) - msg = str(authz_policy.permits(context, principals, - permission)) - else: - msg = 'Allowed (no authorization policy in use)' - - view_name = getattr(request, 'view_name', None) - url = getattr(request, 'url', None) - msg = ('debug_authorization of url %s (view name %r against ' - 'context %r): %s' % (url, view_name, context, msg)) - logger and logger.debug(msg) - if request is not None: - request.authdebug_message = msg - return view(context, request) - - wrapped_view = _authdebug_view - decorate_view(wrapped_view, view) - - return wrapped_view - -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, order, and phash, - # so we use a wrapper - 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.__order__ = order - attr_view.__phash__ = phash - decorate_view(attr_view, view) - return attr_view + return _rendered_view def isexception(o): if IInterface.providedBy(o): @@ -3005,3 +3092,20 @@ class PyramidConfigurationMachine(ConfigurationMachine): self._seen_files.add(spec) return True +def is_response(ob): + if ( hasattr(ob, 'app_iter') and hasattr(ob, 'headerlist') and + hasattr(ob, 'status') ): + return True + return False + +# b/c +def _map_view(view, registry, attr=None, renderer=None): + return DefaultViewMapper(registry=registry, attr=attr, + renderer=renderer)(view) + +# b/c +def requestonly(view, attr=None): + """ Return true of the class or callable accepts only a request argument, + as opposed to something that accepts context, request """ + return DefaultViewMapper(attr=attr).requestonly(view) + diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index 32359ca94..10a324b28 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -120,6 +120,27 @@ class ITemplateRenderer(IRenderer): accepts arbitrary keyword arguments and returns a string or unicode object """ +class IViewMapper(Interface): + def __call__(self, object): + """ Provided with an arbitrary object (a function, class, or + instance), returns a callable with the call signature ``(context, + request)``. The callable returned should itself return a Response + object. An IViewMapper is returned by + :class:`pyramid.interfaces.IViewMapperFactory`.""" + +class IViewMapperFactory(Interface): + def __call__(self, **kw): + """ + Return an object which implements + :class:`pyramid.interfaces.IViewMapper`. ``kw`` will be a dictionary + containing view-specific arguments, such as ``permission``, + ``predicates``, ``attr``, ``renderer``, and other items. An + IViewMapperFactory is used by + :meth:`pyramid.config.Configurator.add_view` to provide a plugpoint + to extension developers who want to modify potential view callable + invocation signatures and response values. + """ + # internal interfaces class IRequest(Interface): diff --git a/pyramid/paster.py b/pyramid/paster.py index 57dac6b32..5efbae51c 100644 --- a/pyramid/paster.py +++ b/pyramid/paster.py @@ -201,19 +201,10 @@ class PRoutesCommand(PCommand): request_iface = registry.queryUtility(IRouteRequest, name=route.name) view_callable = None - if request_iface is not None: - if route.factory is None: - context_iface = Interface - else: - request = Request.blank('/') - inst = route.factory(request) - context_iface = providedBy(inst) - # try with factory instance as context; views registered for - # a more general interface will be found if the context - # iface is very specific + if (request_iface is None) or (route.factory is not None): + self.out(fmt % (route.name, route.pattern, '<unknown>')) + else: view_callable = registry.adapters.lookup( - (IViewClassifier, request_iface, context_iface), + (IViewClassifier, request_iface, Interface), IView, name='', default=None) - self.out(fmt % (route.name, route.pattern, view_callable)) - - + self.out(fmt % (route.name, route.pattern, view_callable)) diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py index 0a87f4d7f..b094caae9 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -3868,16 +3868,18 @@ class Test__map_view(unittest.TestCase): request = self._makeRequest() self.assertEqual(result(None, request).body, 'Hello!') -class Test_decorate_view(unittest.TestCase): - def _callFUT(self, wrapped, original): - from pyramid.config import decorate_view - return decorate_view(wrapped, original) +class Test_preserve_attrs(unittest.TestCase): + def _callFUT(self, fn, view): + from pyramid.config import preserve_attrs + return preserve_attrs(fn)(None, view) def test_it_same(self): def view(context, request): """ """ - result = self._callFUT(view, view) - self.assertEqual(result, False) + def afunc(self, view): + return view + result = self._callFUT(afunc, view) + self.failUnless(result is view) def test_it_different(self): class DummyView1: @@ -3906,8 +3908,10 @@ class Test_decorate_view(unittest.TestCase): """ """ view1 = DummyView1() view2 = DummyView2() - result = self._callFUT(view1, view2) - self.assertEqual(result, True) + def afunc(self, view): + return view1 + result = self._callFUT(afunc, view2) + self.assertEqual(result, view1) self.failUnless(view1.__doc__ is view2.__doc__) self.failUnless(view1.__module__ is view2.__module__) self.failUnless(view1.__name__ is view2.__name__) diff --git a/pyramid/tests/test_paster.py b/pyramid/tests/test_paster.py index a5f613f8e..35349b7c7 100644 --- a/pyramid/tests/test_paster.py +++ b/pyramid/tests/test_paster.py @@ -133,7 +133,31 @@ class TestPRoutesCommand(unittest.TestCase): self.assertEqual(result, None) self.assertEqual(L, []) + def test_single_route_no_route_registered(self): + command = self._makeOne() + route = DummyRoute('a', '/a') + mapper = DummyMapper(route) + command._get_mapper = lambda *arg: mapper + L = [] + command.out = L.append + app = DummyApp() + loadapp = DummyLoadApp(app) + command.loadapp = (loadapp,) + command.args = ('/foo/bar/myapp.ini', 'myapp') + result = command.command() + self.assertEqual(result, None) + self.assertEqual(len(L), 3) + self.assertEqual(L[-1].split(), ['a', '/a', '<unknown>']) + def test_single_route_no_views_registered(self): + from zope.interface import Interface + from pyramid.registry import Registry + from pyramid.interfaces import IRouteRequest + registry = Registry() + def view():pass + class IMyRoute(Interface): + pass + registry.registerUtility(IMyRoute, IRouteRequest, name='a') command = self._makeOne() route = DummyRoute('a', '/a') mapper = DummyMapper(route) @@ -141,13 +165,14 @@ class TestPRoutesCommand(unittest.TestCase): L = [] command.out = L.append app = DummyApp() + app.registry = registry loadapp = DummyLoadApp(app) command.loadapp = (loadapp,) command.args = ('/foo/bar/myapp.ini', 'myapp') result = command.command() self.assertEqual(result, None) self.assertEqual(len(L), 3) - self.assertEqual(L[-1].split(), ['a', '/a', 'None']) + self.assertEqual(L[-1].split()[:3], ['a', '/a', 'None']) def test_single_route_one_view_registered(self): from zope.interface import Interface @@ -181,7 +206,6 @@ class TestPRoutesCommand(unittest.TestCase): def test_single_route_one_view_registered_with_factory(self): from zope.interface import Interface - from zope.interface import implements from pyramid.registry import Registry from pyramid.interfaces import IRouteRequest from pyramid.interfaces import IViewClassifier @@ -190,8 +214,6 @@ class TestPRoutesCommand(unittest.TestCase): def view():pass class IMyRoot(Interface): pass - class Root(object): - implements(IMyRoot) class IMyRoute(Interface): pass registry.registerAdapter(view, @@ -199,8 +221,7 @@ class TestPRoutesCommand(unittest.TestCase): IView, '') registry.registerUtility(IMyRoute, IRouteRequest, name='a') command = self._makeOne() - def factory(request): - return Root() + def factory(request): pass route = DummyRoute('a', '/a', factory=factory) mapper = DummyMapper(route) command._get_mapper = lambda *arg: mapper @@ -214,7 +235,7 @@ class TestPRoutesCommand(unittest.TestCase): result = command.command() self.assertEqual(result, None) self.assertEqual(len(L), 3) - self.assertEqual(L[-1].split()[:4], ['a', '/a', '<function', 'view']) + self.assertEqual(L[-1].split()[:3], ['a', '/a', '<unknown>']) def test__get_mapper(self): from pyramid.registry import Registry @@ -12,7 +12,7 @@ # ############################################################################## -__version__ = '1.0a8' +__version__ = '0.0' import os import platform |
