From afa1cb6352fd39d8b0788c7708768aaca2974385 Mon Sep 17 00:00:00 2001 From: Casey Duncan Date: Mon, 20 Dec 2010 23:33:08 -0700 Subject: promote parenthetical to an actual sentence, minor reword --- docs/narr/resources.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/narr/resources.rst b/docs/narr/resources.rst index fe1f760bb..4c2897ac8 100644 --- a/docs/narr/resources.rst +++ b/docs/narr/resources.rst @@ -3,8 +3,8 @@ Resources A :term:`resource` is an object that represents a "place" in a tree related to your application. Every :app:`Pyramid` application has at least one -resource object: the :term:`root` resource (even if you don't define one -manually, a default root resource is created for you). The root resource is +resource object: the :term:`root` resource. Even if you don't define a +root resource manually, a default one is created for you. The root resource is the root of a :term:`resource tree`. A resource tree is a set of nested dictionary-like objects which you can use to represent your website's structure. -- cgit v1.2.3 From 14575f52880028537594eb06b35f2b39dd9818c9 Mon Sep 17 00:00:00 2001 From: Casey Duncan Date: Mon, 20 Dec 2010 23:37:39 -0700 Subject: reword, reorder paragraph for clarity --- docs/narr/resources.rst | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/narr/resources.rst b/docs/narr/resources.rst index 4c2897ac8..7b31a8260 100644 --- a/docs/narr/resources.rst +++ b/docs/narr/resources.rst @@ -1,21 +1,21 @@ Resources ========= -A :term:`resource` is an object that represents a "place" in a tree related -to your application. Every :app:`Pyramid` application has at least one -resource object: the :term:`root` resource. Even if you don't define a -root resource manually, a default one is created for you. The root resource is -the root of a :term:`resource tree`. A resource tree is a set of nested -dictionary-like objects which you can use to represent your website's -structure. +A :term:`resource` is an object that represents a "place" in a tree +related to your application. Every :app:`Pyramid` application has at +least one resource object: the :term:`root` resource. Even if you don't +define a root resource manually, a default one is created for you. The +root resource is the root of a :term:`resource tree`. A resource tree +is a set of nested dictionary-like objects which you can use to +represent your website's structure. In an application which uses :term:`traversal` to map URLs to code, the -resource tree structure is used heavily to map a URL to a :term:`view -callable`. :app:`Pyramid` will walk "up" the resource tree by traversing -through the nested dictionary structure of the tree when :term:`traversal` is -used in order to find a :term:`context` resource. Once a context resource is -found, the context resource and data in the request will be used to find a -:term:`view callable`. +resource tree structure is used heavily to map each URL to a :term:`view +callable`. When :term:`traversal` is used, :app:`Pyramid` will walk +through the resource tree by traversing through its nested dictionary +structure in order to find a :term:`context` resource. Once a context +resource is found, the context resource and data in the request will be +used to find a :term:`view callable`. In an application which uses :term:`URL dispatch`, the resource tree is only used indirectly, and is often "invisible" to the developer. In URL dispatch -- cgit v1.2.3 From 5f0a81a0fd40afcabd4e8f3185136f829193d4bc Mon Sep 17 00:00:00 2001 From: Casey Duncan Date: Mon, 20 Dec 2010 23:46:30 -0700 Subject: make it clear that leaf resource's __getitem__ must always raise KeyError --- docs/narr/resources.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/narr/resources.rst b/docs/narr/resources.rst index 7b31a8260..6350f01ec 100644 --- a/docs/narr/resources.rst +++ b/docs/narr/resources.rst @@ -26,7 +26,7 @@ much less important in applications that use URL dispatch than applications that use traversal. In "Zope-like" :app:`Pyramid` applications, resource objects also often store -data persistently and offer methods related to mutating that persistent data. +data persistently, and offer methods related to mutating that persistent data. In these kinds of applications, resources not only represent the site structure of your website, but they become the :term:`domain model` of the application. @@ -72,8 +72,8 @@ tree: the container's ``__getitem__`` should return the sub-resource. - Leaf resources, which do not contain other resources, must not implement a - ``__getitem__``, or if they do, their ``__getitem__`` method must raise a - :exc:`KeyError`. + ``__getitem__``, or if they do, their ``__getitem__`` method must always + raise a :exc:`KeyError`. See :ref:`traversal_chapter` for more information about how traversal works against resource instances. -- cgit v1.2.3 From c661f30d1ad82baedb2847d5ba65b8f85311e9e5 Mon Sep 17 00:00:00 2001 From: Casey Duncan Date: Tue, 21 Dec 2010 00:01:07 -0700 Subject: remove word typically to improve flow --- docs/narr/resources.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/narr/resources.rst b/docs/narr/resources.rst index 6350f01ec..2485689a4 100644 --- a/docs/narr/resources.rst +++ b/docs/narr/resources.rst @@ -239,7 +239,7 @@ A slash is appended to all resource URLs when :func:`~pyramid.url.resource_url` is used to generate them in this simple manner, because resources are "places" in the hierarchy, and URLs are meant to be clicked on to be visited. Relative URLs that you include on HTML pages -rendered as the result of the default view of a resource are typically more +rendered as the result of the default view of a resource are more apt to be relative to these resources than relative to their parent. You can also pass extra elements to :func:`~pyramid.url.resource_url`: -- cgit v1.2.3 From 4a10119ecd49316b38c556ef720b515a0c5f7a22 Mon Sep 17 00:00:00 2001 From: Casey Duncan Date: Tue, 21 Dec 2010 00:07:36 -0700 Subject: add missing space --- docs/narr/resources.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/narr/resources.rst b/docs/narr/resources.rst index 2485689a4..69dbc0af6 100644 --- a/docs/narr/resources.rst +++ b/docs/narr/resources.rst @@ -399,7 +399,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 -- cgit v1.2.3 From e8f26928bf5c8fb8490a72436718cedf8fe19281 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 22 Dec 2010 11:28:58 -0500 Subject: factor all view wrapper funcs into a ViewDeriver class --- pyramid/config.py | 589 ++++++++++++++++++++++++------------------- pyramid/tests/test_config.py | 20 +- 2 files changed, 344 insertions(+), 265 deletions(-) diff --git a/pyramid/config.py b/pyramid/config.py index 93123c119..f6746a20e 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -266,10 +266,14 @@ class Configurator(object): renderer_globals_factory=None, default_permission=None, session_factory=None, + view_deriver=None, autocommit=False, ): if package is None: package = caller_package() + if view_deriver is None: + view_deriver = DefaultViewDeriver + self.view_deriver = view_deriver name_resolver = DottedNameResolver(package) self.name_resolver = name_resolver self.package_name = name_resolver.package_name @@ -342,26 +346,20 @@ class Configurator(object): 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 = self.view_deriver(view, + 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() def _override(self, package, path, override_package, override_prefix, PackageOverrides=PackageOverrides): @@ -2619,149 +2617,312 @@ 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): +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 """ - if attr is None: - attr = '__call__' - if inspect.isfunction(class_or_callable): - fn = class_or_callable - elif inspect.isclass(class_or_callable): + return DefaultViewDeriver(view, attr=attr).requestonly() + +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 DefaultViewDeriver(object): + def __init__(self, view, **kw): + self.kw = kw + self.view = view + self.registry = kw.get('registry') + self.helper = None + self.renderer = kw.get('renderer') + 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) + if self.registry is not None: + self.authn_policy = self.registry.queryUtility( + IAuthenticationPolicy) + self.authz_policy = self.registry.queryUtility( + IAuthorizationPolicy) + self.logger = self.registry.queryUtility(IDebugLogger) + + def __call__(self): + return self.attr_wrapped_view( + self.predicated_view( + self.authdebug_view( + self.secured_view( + self.owrap_view( + self.map_view(self.view)))))) + + def requestonly(self): + view = 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 + try: + argspec = inspect.getargspec(fn) + except TypeError: + return False - args = argspec[0] - defaults = argspec[3] + args = argspec[0] + defaults = argspec[3] - if hasattr(fn, 'im_func'): - # it's an instance method + if hasattr(fn, 'im_func'): + # it's an instance method + if not args: + return False + args = args[1:] if not args: return False - args = args[1:] - if not args: - return False - - if len(args) == 1: - return True - elif args[0] == 'request': - if len(args) - len(defaults) == 1: + 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 + -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() + @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: - 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 + 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) + + wrapped_view = _authdebug_view + + return wrapped_view + + @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 + + @preserve_attrs + def map_view(self, view): + attr = self.kw.get('attr') + isclass = inspect.isclass(view) + ronly = self.requestonly() + if isclass and ronly: + view = self.adapt_requestonly_class() + elif isclass: + view = self.adapt_class() + elif ronly: + view = self.adapt_requestonly_func() + elif attr: + view = self.adapt_attr() + elif self.helper is not None: + view = self.adapt_rendered() + return view + + def adapt_requestonly_class(self): + # its a class that has an __init__ which only accepts request + view = self.view + 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 adapt_class(self): + # its a class that has an __init__ which accepts both context and + # request + view = self.view + 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 adapt_requestonly_func(self): + # its a function or instance that has a __call__ accepts only a + # single request argument + view = self.view + attr = self.kw.get('attr') + helper = self.helper + renderer = self.renderer def _requestonly_view(context, request): if attr is None: response = view(request) @@ -2780,9 +2941,15 @@ 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 adapt_attr(self): + # its a function that has a __call__ which accepts both context and + # request, but still has an attr + view = self.view + 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: @@ -2797,9 +2964,14 @@ def _map_view(view, registry, attr=None, renderer=None): response = helper.render_to_response(response, system, request=request) return response - wrapped_view = _attr_view - - elif helper is not None: + return _attr_view + + def adapt_rendered(self): + # it's a function that has a __call__ that accepts both context and + # request, but requires rendering + view = self.view + helper = self.helper + renderer = self.renderer def _rendered_view(context, request): response = view(context, request) if not is_response(response): @@ -2813,113 +2985,11 @@ 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 _rendered_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 +def _map_view(view, registry, attr=None, renderer=None): + return DefaultViewDeriver(view, registry=registry, attr=attr, + renderer=renderer).map_view(view) def isexception(o): if IInterface.providedBy(o): @@ -2970,3 +3040,8 @@ 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 diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py index e4ab9a867..812bfffa3 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -3788,16 +3788,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: @@ -3826,8 +3828,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__) -- cgit v1.2.3 From 98b0afb7bbf4e58cfeaa7a2e970b35069751284a Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 23 Dec 2010 12:54:46 -0500 Subject: garden --- TODO.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/TODO.txt b/TODO.txt index 3a32322f8..0ee458254 100644 --- a/TODO.txt +++ b/TODO.txt @@ -24,6 +24,8 @@ Should-Have - ``decorator=`` parameter to view_config. This would replace the existing _map_view "decorator" if it existed. +- Static (URL-generation only) routes. + Nice-to-Have ------------ -- cgit v1.2.3 From a827e04ac308f8005f674bef01d70a842be708cf Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 23 Dec 2010 13:20:39 -0500 Subject: note __resource_url__ requirements --- docs/narr/resources.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/narr/resources.rst b/docs/narr/resources.rst index f90b1eb12..ac88afdfd 100644 --- a/docs/narr/resources.rst +++ b/docs/narr/resources.rst @@ -324,6 +324,11 @@ would have been what was returned anyway, but your code can perform arbitrary logic as necessary. For example, your code may wish to override the hostname or port number of the generated URL. +Note that the URL generated by ``__resource_url__`` should be fully +qualified, should end in a slash, and should not contain any query string or +anchor elements (only path elements) to work best with +:func:`pyramid.url.resource_url`. + Generating the Path To a Resource --------------------------------- -- cgit v1.2.3 From c4d40135fdcd4125f36c3dd5d0e3b555b539f69a Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 23 Dec 2010 13:22:17 -0500 Subject: clarify --- pyramid/url.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyramid/url.py b/pyramid/url.py index 4c2f5d393..bebd73104 100644 --- a/pyramid/url.py +++ b/pyramid/url.py @@ -256,7 +256,8 @@ def resource_url(resource, request, *elements, **kw): e.g. ``http://example.com?foo=1#bar``. If the ``resource`` passed in has a ``__resource_url__`` method, it will - be used to generate the URL that is returned by this function. See also + be used to generate the URL (scheme, host, port, path) that for the base + resource which is operated upon by this function. See also :ref:`overriding_resource_url_generation`. .. note:: If the :term:`resource` used is the result of a -- cgit v1.2.3 From 169711bd49e862c27279ec60ef841ed132d24ac1 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 23 Dec 2010 13:26:40 -0500 Subject: correct missed bfg->pyramid --- pyramid/url.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/url.py b/pyramid/url.py index bebd73104..e1eaaaa1e 100644 --- a/pyramid/url.py +++ b/pyramid/url.py @@ -227,7 +227,7 @@ def resource_url(resource, request, *elements, **kw): a data structure with an ``.items()`` method that returns a sequence of two-tuples (presumably a dictionary). This data structure will be turned into a query string per the documentation - of ``repoze.url.urlencode`` function. After the query data is + of ``pyramid.url.urlencode`` function. After the query data is turned into a query string, a leading ``?`` is prepended, and the resulting string is appended to the generated URL. -- cgit v1.2.3 From e6f45b05eccc86f0f3a390748df26e34b8fe06c6 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 23 Dec 2010 18:09:03 -0500 Subject: - The name ``registry`` was not available in the ``paster pshell`` environment under IPython. --- CHANGES.txt | 6 ++++++ pyramid/paster.py | 3 ++- pyramid/tests/test_paster.py | 3 ++- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 8532ebec8..eef001f73 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,6 +1,12 @@ Next release ============ +Bug Fixes +--------- + +- The name ``registry`` was not available in the ``paster pshell`` + environment under IPython. + Features -------- diff --git a/pyramid/paster.py b/pyramid/paster.py index bd0e13413..0a0b65fe0 100644 --- a/pyramid/paster.py +++ b/pyramid/paster.py @@ -123,7 +123,8 @@ class PShellCommand(Command): root, closer = self.get_root(app) if IPShell is not None and not self.options.disable_ipython: try: - shell = IPShell(argv=[], user_ns={'root':root}) + shell = IPShell(argv=[], user_ns={'root':root, + 'registry':app.registry}) shell.IP.BANNER = shell.IP.BANNER + '\n\n' + banner shell.mainloop() finally: diff --git a/pyramid/tests/test_paster.py b/pyramid/tests/test_paster.py index 41e6dc441..e2478ac4f 100644 --- a/pyramid/tests/test_paster.py +++ b/pyramid/tests/test_paster.py @@ -50,7 +50,8 @@ class TestPShellCommand(unittest.TestCase): pushed = app.threadlocal_manager.pushed[0] self.assertEqual(pushed['registry'], dummy_registry) self.assertEqual(pushed['request'].registry, dummy_registry) - self.assertEqual(dummy_shell_factory.shell.local_ns,{'root':dummy_root}) + self.assertEqual(dummy_shell_factory.shell.local_ns, + {'root':dummy_root, 'registry':dummy_registry}) self.assertEqual(dummy_shell_factory.shell.global_ns, {}) self.failUnless('\n\n' in dummy_shell_factory.shell.IP.BANNER) self.assertEqual(len(app.threadlocal_manager.popped), 1) -- cgit v1.2.3 From 99cfc081fa0498c1fa229124efb1669793e20227 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 23 Dec 2010 18:52:59 -0500 Subject: use WebOb docstring rather than copying it --- pyramid/httpexceptions.py | 66 +---------------------------------------------- 1 file changed, 1 insertion(+), 65 deletions(-) diff --git a/pyramid/httpexceptions.py b/pyramid/httpexceptions.py index 130c3ee8d..6d05f9475 100644 --- a/pyramid/httpexceptions.py +++ b/pyramid/httpexceptions.py @@ -1,68 +1,4 @@ -"""HTTP Exceptions. - -HTTP Exceptions can be returned from handlers and views (they are -valid :term:`Response` objects). - -All HTTP exceptions are sub-classes of HTTPException, with additional -sub-classes for each of the major types of HTTP response. For example, -all 200-class HTTP exceptions sub-class HTTPOk, which sub-classes -HTTPException. - -A status_map dict is also provided, which allows for key based access -to exception objects by the HTTP status code. - -The exceptions are ordered into a class hierarchy based on status code -divisions to allow for capturing of various types of HTTP exceptions -as well:: - - Exception - HTTPException - HTTPOk - * 200 - HTTPOk - * 201 - HTTPCreated - * 202 - HTTPAccepted - * 203 - HTTPNonAuthoritativeInformation - * 204 - HTTPNoContent - * 205 - HTTPResetContent - * 206 - HTTPPartialContent - HTTPRedirection - * 300 - HTTPMultipleChoices - * 301 - HTTPMovedPermanently - * 302 - HTTPFound - * 303 - HTTPSeeOther - * 304 - HTTPNotModified - * 305 - HTTPUseProxy - * 306 - Unused (not implemented, obviously) - * 307 - HTTPTemporaryRedirect - HTTPError - HTTPClientError - * 400 - HTTPBadRequest - * 401 - HTTPUnauthorized - * 402 - HTTPPaymentRequired - * 403 - HTTPForbidden - * 404 - HTTPNotFound - * 405 - HTTPMethodNotAllowed - * 406 - HTTPNotAcceptable - * 407 - HTTPProxyAuthenticationRequired - * 408 - HTTPRequestTimeout - * 409 - HTTPConflict - * 410 - HTTPGone - * 411 - HTTPLengthRequired - * 412 - HTTPPreconditionFailed - * 413 - HTTPRequestEntityTooLarge - * 414 - HTTPRequestURITooLong - * 415 - HTTPUnsupportedMediaType - * 416 - HTTPRequestRangeNotSatisfiable - * 417 - HTTPExpectationFailed - HTTPServerError - * 500 - HTTPInternalServerError - * 501 - HTTPNotImplemented - * 502 - HTTPBadGateway - * 503 - HTTPServiceUnavailable - * 504 - HTTPGatewayTimeout - * 505 - HTTPVersionNotSupported - -""" +from webob.exc import __doc__ from webob.exc import status_map # Parent classes -- cgit v1.2.3 From 53901dbad3cc176c86d8ab570a2d18759fb01160 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 23 Dec 2010 18:53:08 -0500 Subject: fix references --- docs/glossary.rst | 6 ++++++ docs/tutorials/wiki/definingviews.rst | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/glossary.rst b/docs/glossary.rst index a3aacebce..ed96b8afe 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -845,4 +845,10 @@ Glossary `WebTest `_ is a package which can help you write functional tests for your WSGI application. + WebError + WSGI middleware which can display debuggable traceback information in + the browser when an exception is raised by a Pyramid application. See + http://pypi.python.org/pypi/WebError . + + diff --git a/docs/tutorials/wiki/definingviews.rst b/docs/tutorials/wiki/definingviews.rst index 53f5ff994..c051d6db7 100644 --- a/docs/tutorials/wiki/definingviews.rst +++ b/docs/tutorials/wiki/definingviews.rst @@ -118,7 +118,7 @@ The ``add_page`` view function ------------------------------ The ``add_page`` function will be configured to respond when the context is a -Wiki and the :term:`view_name` is ``add_page``. We'll provide it with a +Wiki and the :term:`view name` is ``add_page``. We'll provide it with a ``@view_config`` decorator which names the string ``add_page`` as its :term:`view name` (via name=), the class ``tutorial.models.Wiki`` as its context, and the renderer named ``templates/edit.pt``. This means that when @@ -165,7 +165,7 @@ The ``edit_page`` view function ------------------------------- The ``edit_page`` function will be configured to respond when the context is -a Page and the :term:`view_name` is ``edit_page``. We'll provide it with a +a Page and the :term:`view name` is ``edit_page``. We'll provide it with a ``@view_config`` decorator which names the string ``edit_page`` as its :term:`view name` (via name=), the class ``tutorial.models.Page`` as its context, and the renderer named ``templates/edit.pt``. This means that when -- cgit v1.2.3 From 52a4aabd679a9e9e91a579d1546abd5dfbe2931d Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 23 Dec 2010 19:01:22 -0500 Subject: share shell globals between interact and IPShell --- pyramid/paster.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pyramid/paster.py b/pyramid/paster.py index 0a0b65fe0..c8bc36e80 100644 --- a/pyramid/paster.py +++ b/pyramid/paster.py @@ -121,18 +121,17 @@ class PShellCommand(Command): self.logging_file_config(config_file) app = self.get_app(config_file, section_name, loadapp=self.loadapp[0]) root, closer = self.get_root(app) + shell_globals = {'root':root, 'registry':app.registry} if IPShell is not None and not self.options.disable_ipython: try: - shell = IPShell(argv=[], user_ns={'root':root, - 'registry':app.registry}) + shell = IPShell(argv=[], user_ns=shell_globals) shell.IP.BANNER = shell.IP.BANNER + '\n\n' + banner shell.mainloop() finally: closer() else: try: - self.interact[0](banner, - local={'root':root,'registry':app.registry}) + self.interact[0](banner, local=shell_globals) finally: closer() -- cgit v1.2.3 From d1138fdd4fe55358dcb583c5ddee3f45043d1fb5 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 23 Dec 2010 19:32:05 -0500 Subject: fix awkward wording --- docs/tutorials/wiki/definingviews.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/tutorials/wiki/definingviews.rst b/docs/tutorials/wiki/definingviews.rst index c051d6db7..2c016b373 100644 --- a/docs/tutorials/wiki/definingviews.rst +++ b/docs/tutorials/wiki/definingviews.rst @@ -7,9 +7,10 @@ Conventionally, :term:`view callable` objects are defined within a automagically special about the filename ``views.py``. Files implementing views often have ``view`` in their filenames (or may live in a Python subpackage of your application package named ``views``), but this is only by -convention. However, a project may have many views throughout its codebase -in arbitrarily-named files. In this application, we'll be continuing to use -the ``views.py`` module, because there's no reason to break convention. +convention. A project may have many views throughout its codebase in +arbitrarily-named files. In this application, however, we'll be continuing +to use the ``views.py`` module, because there's no reason to break +convention. A :term:`view callable` in a :app:`Pyramid` application is typically a simple Python function that accepts a single parameter: :term:`request`. A view -- cgit v1.2.3 From b743bb4da42198f223ec756936dc0c581b08b534 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 23 Dec 2010 20:01:42 -0500 Subject: tutorial accuracy and wording improvements --- docs/tutorials/wiki/authorization.rst | 174 +++++++++++---------- docs/tutorials/wiki/basiclayout.rst | 12 +- docs/tutorials/wiki/definingmodels.rst | 44 +++--- docs/tutorials/wiki/definingviews.rst | 150 +++++++++--------- .../wiki/src/authorization/tutorial/login.py | 4 +- 5 files changed, 194 insertions(+), 190 deletions(-) diff --git a/docs/tutorials/wiki/authorization.rst b/docs/tutorials/wiki/authorization.rst index 34cde288f..57c4a3ce5 100644 --- a/docs/tutorials/wiki/authorization.rst +++ b/docs/tutorials/wiki/authorization.rst @@ -23,15 +23,13 @@ For any :app:`Pyramid` application to perform authorization, we need to add a registry` to add an :term:`authentication policy` and a :term:`authorization policy`. -Changing ``__init__.py`` -~~~~~~~~~~~~~~~~~~~~~~~~ +Adding Authentication and Authorization Policies +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -We'll change our ``__init__.py`` file to enable an +We'll change our package's ``__init__.py`` file to enable an ``AuthTktAuthenticationPolicy`` and an ``ACLAuthorizationPolicy`` to enable -declarative security checking. We'll also add a new view stanza, which -specifies a :term:`forbidden view`. This configures our login view to show -up when :app:`Pyramid` detects that a view invocation can not be authorized. -When you're done, your ``__init__.py`` will look like so: +declarative security checking. When you're done, your ``__init__.py`` will +look like so: .. literalinclude:: src/authorization/tutorial/__init__.py :linenos: @@ -84,6 +82,22 @@ and logout views. Add a file named ``login.py`` to your application :linenos: :language: python +Note that the ``login`` view callable in the ``login.py`` file has *two* view +configuration decorators. In the first view configuration decorator, we +configured the ``login`` view callable so it will be invoked when someone +visits ``/login`` (when the context is a Wiki and the view name is +``login``). The second decorator (with context of +``pyramid.exceptions.Forbidden``) specifies a :term:`forbidden view`. This +configures our login view to show up when :app:`Pyramid` detects that a view +invocation can not be authorized. Because we've configured a forbidden view, +the ``login`` view callable will be invoked whenever one of our users tries +to execute a view callable that they are disallowed from invoking based on +the :term:`authorization policy` in use. In our application, for example, +this means that if a user has not logged in, and he tries to add or edit a +Wiki page, he will be shown the login form. Before being allowed to continue +on to the add or edit form, he will have to provide credentials that give him +permission to add or edit via this login form. + Changing Existing Views ~~~~~~~~~~~~~~~~~~~~~~~ @@ -138,17 +152,15 @@ class="main_content">`` div: Logout -Giving Our Root Model Object an ACL ------------------------------------ +Giving Our Root Resource an ACL +------------------------------- -We need to give our root model object an :term:`ACL`. This ACL will -be sufficient to provide enough information to the :app:`Pyramid` -security machinery to challenge a user who doesn't have appropriate -credentials when he attempts to invoke the ``add_page`` or -``edit_page`` views. +We need to give our root resource object an :term:`ACL`. This ACL will be +sufficient to provide enough information to the :app:`Pyramid` security +machinery to challenge a user who doesn't have appropriate credentials when +he attempts to invoke the ``add_page`` or ``edit_page`` views. -We need to perform some imports at module scope in our ``models.py`` -file: +We need to perform some imports at module scope in our ``models.py`` file: .. code-block:: python :linenos: @@ -156,8 +168,8 @@ file: from pyramid.security import Allow from pyramid.security import Everyone -Our root model is a ``Wiki`` object. We'll add the following line at -class scope to our ``Wiki`` class: +Our root resource object is a ``Wiki`` instance. We'll add the following +line at class scope to our ``Wiki`` class: .. code-block:: python :linenos: @@ -165,12 +177,11 @@ class scope to our ``Wiki`` class: __acl__ = [ (Allow, Everyone, 'view'), (Allow, 'group:editors', 'edit') ] -It's only happenstance that we're assigning this ACL at class scope. -An ACL can be attached to an object *instance* too; this is how "row -level security" can be achieved in :app:`Pyramid` applications. We -actually only need *one* ACL for the entire system, however, because -our security requirements are simple, so this feature is not -demonstrated. +It's only happenstance that we're assigning this ACL at class scope. An ACL +can be attached to an object *instance* too; this is how "row level security" +can be achieved in :app:`Pyramid` applications. We actually only need *one* +ACL for the entire system, however, because our security requirements are +simple, so this feature is not demonstrated. Our resulting ``models.py`` file will now look like so: @@ -181,76 +192,71 @@ Our resulting ``models.py`` file will now look like so: Adding ``permission`` Declarations to our ``view_config`` Decorators -------------------------------------------------------------------- -To protect each of our views with a particular permission, we need to -pass a ``permission`` argument to each of our -:class:`pyramid.view.view_config` decorators. To do so, within -``views.py``: +To protect each of our views with a particular permission, we need to pass a +``permission`` argument to each of our :class:`pyramid.view.view_config` +decorators. To do so, within ``views.py``: -- We add ``permission='view'`` to the decorator attached to the - ``view_wiki`` view function. This makes the assertion that only - users who possess the effective ``view`` permission at the time of - the request may invoke this view. We've granted - :data:`pyramid.security.Everyone` the view permission at the root - model via its ACL, so everyone will be able to invoke the - ``view_wiki`` view. +- We add ``permission='view'`` to the decorator attached to the ``view_wiki`` + view function. This makes the assertion that only users who possess the + ``view`` permission against the context resource at the time of the request + may invoke this view. We've granted :data:`pyramid.security.Everyone` the + view permission at the root model via its ACL, so everyone will be able to + invoke the ``view_wiki`` view. -- We add ``permission='view'`` to the decorator attached to the - ``view_page`` view function. This makes the assertion that only - users who possess the effective ``view`` permission at the time of +- We add ``permission='view'`` to the decorator attached to the ``view_page`` + view function. This makes the assertion that only users who possess the + effective ``view`` permission against the context resource at the time of the request may invoke this view. We've granted - :data:`pyramid.security.Everyone` the view permission at the root - model via its ACL, so everyone will be able to invoke the - ``view_page`` view. - -- We add ``permission='edit'`` to the decorator attached to the - ``add_page`` view function. This makes the assertion that only - users who possess the effective ``edit`` permission at the time of - the request may invoke this view. We've granted the - ``group:editors`` principal the ``edit`` permission at the root - model via its ACL, so only the a user whom is a member of the group - named ``group:editors`` will able to invoke the ``add_page`` view. - We've likewise given the ``editor`` user membership to this group - via thes ``security.py`` file by mapping him to the - ``group:editors`` group in the ``GROUPS`` data structure (``GROUPS = - {'editor':['group:editors']}``); the ``groupfinder`` function - consults the ``GROUPS`` data structure. This means that the - ``editor`` user can add pages. - -- We add ``permission='edit'`` to the decorator attached to the - ``edit_page`` view function. This makes the assertion that only - users who possess the effective ``edit`` permission at the time of - the request may invoke this view. We've granted the - ``group:editors`` principal the ``edit`` permission at the root - model via its ACL, so only the a user whom is a member of the group - named ``group:editors`` will able to invoke the ``edit_page`` view. - We've likewise given the ``editor`` user membership to this group - via thes ``security.py`` file by mapping him to the - ``group:editors`` group in the ``GROUPS`` data structure (``GROUPS = - {'editor':['group:editors']}``); the ``groupfinder`` function - consults the ``GROUPS`` data structure. This means that the - ``editor`` user can edit pages. + :data:`pyramid.security.Everyone` the view permission at the root model via + its ACL, so everyone will be able to invoke the ``view_page`` view. + +- We add ``permission='edit'`` to the decorator attached to the ``add_page`` + view function. This makes the assertion that only users who possess the + effective ``edit`` permission against the context resource at the time of + the request may invoke this view. We've granted the ``group:editors`` + principal the ``edit`` permission at the root model via its ACL, so only + the a user whom is a member of the group named ``group:editors`` will able + to invoke the ``add_page`` view. We've likewise given the ``editor`` user + membership to this group via thes ``security.py`` file by mapping him to + the ``group:editors`` group in the ``GROUPS`` data structure (``GROUPS = + {'editor':['group:editors']}``); the ``groupfinder`` function consults the + ``GROUPS`` data structure. This means that the ``editor`` user can add + pages. + +- We add ``permission='edit'`` to the decorator attached to the ``edit_page`` + view function. This makes the assertion that only users who possess the + effective ``edit`` permission against the context resource at the time of + the request may invoke this view. We've granted the ``group:editors`` + principal the ``edit`` permission at the root model via its ACL, so only + the a user whom is a member of the group named ``group:editors`` will able + to invoke the ``edit_page`` view. We've likewise given the ``editor`` user + membership to this group via thes ``security.py`` file by mapping him to + the ``group:editors`` group in the ``GROUPS`` data structure (``GROUPS = + {'editor':['group:editors']}``); the ``groupfinder`` function consults the + ``GROUPS`` data structure. This means that the ``editor`` user can edit + pages. Viewing the Application in a Browser ------------------------------------ -We can finally examine our application in a browser. The views we'll -try are as follows: +We can finally examine our application in a browser. The views we'll try are +as follows: -- Visiting ``http://localhost:6543/`` in a browser invokes the - ``view_wiki`` view. This always redirects to the ``view_page`` view - of the FrontPage page object. It is executable by any user. +- Visiting ``http://localhost:6543/`` in a browser invokes the ``view_wiki`` + view. This always redirects to the ``view_page`` view of the ``FrontPage`` + page resource. It is executable by any user. -- Visiting ``http://localhost:6543/FrontPage/`` in a browser invokes - the ``view_page`` view of the front page page object. This is - because it's the :term:`default view` (a view without a ``name``) - for ``Page`` objects. It is executable by any user. +- Visiting ``http://localhost:6543/FrontPage/`` in a browser invokes the + ``view_page`` view of the ``FrontPage`` Page resource. This is because + it's the :term:`default view` (a view without a ``name``) for ``Page`` + resources. It is executable by any user. -- Visiting ``http://localhost:6543/FrontPage/edit_page`` in a browser - invokes the edit view for the front page object. It is executable - by only the ``editor`` user. If a different user (or the anonymous - user) invokes it, a login form will be displayed. Supplying the - credentials with the username ``editor``, password ``editor`` will - show the edit page form being displayed. +- Visiting ``http://localhost:6543/FrontPage/edit_page`` in a browser invokes + the edit view for the ``FrontPage`` Page resource. It is executable by + only the ``editor`` user. If a different user (or the anonymous user) + invokes it, a login form will be displayed. Supplying the credentials with + the username ``editor``, password ``editor`` will show the edit page form + being displayed. - Visiting ``http://localhost:6543/add_page/SomePageName`` in a browser invokes the add view for a page. It is executable by only diff --git a/docs/tutorials/wiki/basiclayout.rst b/docs/tutorials/wiki/basiclayout.rst index 8e6e89e57..f6e1f800a 100644 --- a/docs/tutorials/wiki/basiclayout.rst +++ b/docs/tutorials/wiki/basiclayout.rst @@ -2,10 +2,9 @@ Basic Layout ============ -The starter files generated by the ``pyramid_zodb`` template are basic, -but they provide a good orientation for the high-level patterns common -to most :term:`traversal` -based :app:`Pyramid` (and :term:`ZODB` -based) projects. +The starter files generated by the ``pyramid_zodb`` template are basic, but +they provide a good orientation for the high-level patterns common to most +:term:`traversal` -based :app:`Pyramid` (and :term:`ZODB` based) projects. The source code for this tutorial stage can be browsed via `http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki/src/basiclayout/ @@ -78,11 +77,10 @@ Resources and Models with ``models.py`` hierarchically in a :term:`resource tree`. This tree is consulted by :term:`traversal` to map URLs to code. In this application, the resource tree represents the site structure, but it *also* represents the -:term:`domain model` of the application, because eeach resource is a node +:term:`domain model` of the application, because each resource is a node stored persistently in a :term:`ZODB` database. The ``models.py`` file is where the ``pyramid_zodb`` Paster template put the classes that implement our -resource objects, each of which happens also to be a domain model -object. +resource objects, each of which happens also to be a domain model object. Here is the source for ``models.py``: diff --git a/docs/tutorials/wiki/definingmodels.rst b/docs/tutorials/wiki/definingmodels.rst index 078a8e014..f201f6dc7 100644 --- a/docs/tutorials/wiki/definingmodels.rst +++ b/docs/tutorials/wiki/definingmodels.rst @@ -38,12 +38,11 @@ we're not going to use it. .. note:: - There is nothing automagically special about the filename - ``models.py``. A project may have many models throughout its - codebase in arbitrarily-named files. Files implementing models - often have ``model`` in their filenames (or they may live in a - Python subpackage of your application package named ``models``) , - but this is only by convention. + There is nothing automagically special about the filename ``models.py``. A + project may have many models throughout its codebase in arbitrarily-named + files. Files implementing models often have ``model`` in their filenames, + or they may live in a Python subpackage of your application package named + ``models``, but this is only by convention. Then, we'll add a ``Wiki`` class. Because this is a ZODB application, this class should inherit from :class:`persistent.mapping.PersistentMapping`. We @@ -131,6 +130,22 @@ When we're done changing ``tests.py``, it will look something like so: :linenos: :language: python +Declaring Dependencies in Our ``setup.py`` File +----------------------------------------------- + +Our application now depends on packages which are not dependencies of the +original "tutorial" application as it was generated by the ``paster create`` +command. We'll add these dependencies to our ``tutorial`` package's +``setup.py`` file by assigning these dependencies to both the +``install_requires`` and the ``tests_require`` parameters to the ``setup`` +function. In particular, we require the ``docutils`` package. + +Our resulting ``setup.py`` should look like so: + +.. literalinclude:: src/models/setup.py + :linenos: + :language: python + Running the Tests ----------------- @@ -160,20 +175,3 @@ The expected output is something like this: OK -Declaring Dependencies in Our ``setup.py`` File ------------------------------------------------ - -Our application depends on packages which are not dependencies of the -original "tutorial" application as it was generated by the ``paster -create`` command. We'll add these dependencies to our ``tutorial`` -package's ``setup.py`` file by assigning these dependencies to both -the ``install_requires`` and the ``tests_require`` parameters to the -``setup`` function. In particular, we require the ``docutils`` -package. - -Our resulting ``setup.py`` should look like so: - -.. literalinclude:: src/models/setup.py - :linenos: - :language: python - diff --git a/docs/tutorials/wiki/definingviews.rst b/docs/tutorials/wiki/definingviews.rst index 2c016b373..5b0e5dca1 100644 --- a/docs/tutorials/wiki/definingviews.rst +++ b/docs/tutorials/wiki/definingviews.rst @@ -18,19 +18,22 @@ callable is assumed to return a :term:`response` object. However, a :app:`Pyramid` view can also be defined as callable which accepts *two* arguments: a :term:`context` and a :term:`request`. In :term:`url -dispatch` based applications, the context object is rarely used in the view +dispatch` based applications, the context resource is rarely used in the view body itself, so within code that uses URL-dispatch-only, it's common to -define views as callables that accept only a request to avoid the visual -"noise". This application, however, uses :term:`traversal` to map URLs to -resources, so we're often interested in the context; it's not "noise" to use. +define views as callables that accept only a ``request`` to avoid the visual +"noise" of a ``context`` argument. This application, however, uses +:term:`traversal` to map URLs to a context :term:`resource`, and since our +:term:`resource tree` also represents our application's "domain model", we're +often interested in the context, because it represents the persistent storage +of our application. For this reason, having ``context`` in the callable +argument list is not "noise" to us; instead it's actually rather important +within the view code we'll define in this application. The single-arg (``request`` -only) or two-arg (``context`` and ``request``) calling conventions will work in any :app:`Pyramid` application for any view; -they can be used interchangeably as necessary. In :term:`traversal` based -applications, such as the application we're building in this tutorial, the -context is used frequently within the body of a view method, so we'll be -using the two-argument ``(context, request)`` syntax in this application for -views that we add. +they can be used interchangeably as necessary. We'll be using the +two-argument ``(context, request)`` view callable argument list syntax in +this application. We're going to define several :term:`view callable` functions then wire them into :app:`Pyramid` using some :term:`view configuration`. @@ -43,7 +46,7 @@ Adding View Functions ===================== We're going to add four :term:`view callable` functions to our ``views.py`` -module. One view (named ``view_wiki``) will display the wiki itself (it will +module. One view named ``view_wiki`` will display the wiki itself (it will answer on the root URL), another named ``view_page`` will display an individual page, another named ``add_page`` will allow a page to be added, and a final view named ``edit_page`` will allow a page to be edited. @@ -52,36 +55,35 @@ The ``view_wiki`` view function ------------------------------- The ``view_wiki`` function will be configured to respond as the default view -callable for a ``Wiki`` resource object. We'll provide it with a -``@view_config`` decorator which names the class ``tutorial.models.Wiki`` as -its context. This means that when a Wiki object is the context, and no -:term:`view name` exists in the request, this view will be used. The view -configuration associated with ``view_wiki`` does not use a ``renderer`` -because the view callable always returns a :term:`response` object rather -than a dictionary. No renderer is necessary when a view returns a response -object. - -The view callable always redirects to the ``Page`` object named "FrontPage". -It returns an instance of the :class:`pyramid.httpexceptions.HTTPFound` class -(instances of which implement the WebOb :term:`response` interface), and the -:func:`pyramid.url.resource_url` API. :func:`pyramid.url.resource_url` -constructs a URL to the ``FrontPage`` page resource -(e.g. ``http://localhost:6543/FrontPage``), and uses it as the "location" of -the HTTPFound response, forming an HTTP redirect. +callable for a Wiki resource. We'll provide it with a ``@view_config`` +decorator which names the class ``tutorial.models.Wiki`` as its context. +This means that when a Wiki resource is the context, and no :term:`view name` +exists in the request, this view will be used. The view configuration +associated with ``view_wiki`` does not use a ``renderer`` because the view +callable always returns a :term:`response` object rather than a dictionary. +No renderer is necessary when a view returns a response object. + +The ``view_wiki`` view callable always redirects to the URL of a Page +resource named "FrontPage". To do so, it returns an instance of the +:class:`pyramid.httpexceptions.HTTPFound` class (instances of which implement +the WebOb :term:`response` interface). The :func:`pyramid.url.resource_url` +API. :func:`pyramid.url.resource_url` constructs a URL to the ``FrontPage`` +page resource (e.g. ``http://localhost:6543/FrontPage``), and uses it as the +"location" of the HTTPFound response, forming an HTTP redirect. The ``view_page`` view function ------------------------------- The ``view_page`` function will be configured to respond as the default view -of a ``Page`` resource. We'll provide it with a ``@view_config`` decorator -which names the class ``tutorial.models.Wiki`` as its context. This means -that when a Page object is the context, and no :term:`view name` exists in -the request, this view will be used. We inform :app:`Pyramid` this view will -use the ``templates/view.pt`` template file as a ``renderer``. +of a Page resource. We'll provide it with a ``@view_config`` decorator which +names the class ``tutorial.models.Wiki`` as its context. This means that +when a Page resource is the context, and no :term:`view name` exists in the +request, this view will be used. We inform :app:`Pyramid` this view will use +the ``templates/view.pt`` template file as a ``renderer``. The ``view_page`` function generates the :term:`ReStructuredText` body of a page (stored as the ``data`` attribute of the context passed to the view; the -context will be a Page object) as HTML. Then it substitutes an HTML anchor +context will be a Page resource) as HTML. Then it substitutes an HTML anchor for each *WikiWord* reference in the rendered HTML using a compiled regular expression. @@ -96,7 +98,7 @@ substitution value and returns it. As a result, the ``content`` variable is now a fully formed bit of HTML containing various view and add links for WikiWords based on the content of -our current page object. +our current page resource. We then generate an edit URL (because it's easier to do here than in the template), and we wrap up a number of arguments in a dictionary and return @@ -118,13 +120,13 @@ callable. In the ``view_wiki`` view callable, we unconditionally return a The ``add_page`` view function ------------------------------ -The ``add_page`` function will be configured to respond when the context is a -Wiki and the :term:`view name` is ``add_page``. We'll provide it with a -``@view_config`` decorator which names the string ``add_page`` as its -:term:`view name` (via name=), the class ``tutorial.models.Wiki`` as its +The ``add_page`` function will be configured to respond when the context +resource is a Wiki and the :term:`view name` is ``add_page``. We'll provide +it with a ``@view_config`` decorator which names the string ``add_page`` as +its :term:`view name` (via name=), the class ``tutorial.models.Wiki`` as its context, and the renderer named ``templates/edit.pt``. This means that when -a Wiki object is the context, and a :term:`view name` exists as the result of -traverasal named ``add_page``, this view will be used. We inform +a Wiki resource is the context, and a :term:`view name` named ``add_page`` +exists as the result of traversal, this view will be used. We inform :app:`Pyramid` this view will use the ``templates/edit.pt`` template file as a ``renderer``. We share the same template between add and edit views, thus ``edit.pt`` instead of ``add.pt``. @@ -132,9 +134,9 @@ a ``renderer``. We share the same template between add and edit views, thus The ``add_page`` function will be invoked when a user clicks on a WikiWord which isn't yet represented as a page in the system. The ``check`` function within the ``view_page`` view generates URLs to this view. It also acts as a -handler for the form that is generated when we want to add a page object. -The ``context`` of the ``add_page`` view is always a Wiki object (*not* a -Page object). +handler for the form that is generated when we want to add a page resource. +The ``context`` of the ``add_page`` view is always a Wiki resource (*not* a +Page resource). The request :term:`subpath` in :app:`Pyramid` is the sequence of names that are found *after* the :term:`view name` in the URL segments given in the @@ -151,14 +153,14 @@ expression ``'form.submitted' in request.params`` is ``False``), the view renders a template. To do so, it generates a "save url" which the template use as the form post URL during rendering. We're lazy here, so we're trying to use the same template (``templates/edit.pt``) for the add view as well as -the page edit view. To do so, we create a dummy Page object in order to -satisfy the edit form's desire to have *some* page object exposed as +the page edit view. To do so, we create a dummy Page resource object in +order to satisfy the edit form's desire to have *some* page object exposed as ``page``, and we'll render the template to a response. If the view rendering *is* a result of a form submission (if the expression ``'form.submitted' in request.params`` is ``True``), we scrape the page body from the form data, create a Page object using the name in the subpath and -the page body, and save it into "our context" (the wiki) using the +the page body, and save it into "our context" (the Wiki) using the ``__setitem__`` method of the context. We then redirect back to the ``view_page`` view (the default view for a page) for the newly created page. @@ -166,23 +168,23 @@ The ``edit_page`` view function ------------------------------- The ``edit_page`` function will be configured to respond when the context is -a Page and the :term:`view name` is ``edit_page``. We'll provide it with a -``@view_config`` decorator which names the string ``edit_page`` as its -:term:`view name` (via name=), the class ``tutorial.models.Page`` as its +a Page resource and the :term:`view name` is ``edit_page``. We'll provide it +with a ``@view_config`` decorator which names the string ``edit_page`` as its +:term:`view name` (via ``name=``), the class ``tutorial.models.Page`` as its context, and the renderer named ``templates/edit.pt``. This means that when -a Page object is the context, and a :term:`view name` exists as the result of -traverasal named ``edit_page``, this view will be used. We inform +a Page resource is the context, and a :term:`view name` exists as the result +of traverasal named ``edit_page``, this view will be used. We inform :app:`Pyramid` this view will use the ``templates/edit.pt`` template file as a ``renderer``. The ``edit_page`` function will be invoked when a user clicks the "Edit this Page" button on the view form. It renders an edit form but it also acts as -the handler for the form it renders. The ``context`` of the ``edit_page`` -view will *always* be a Page object (never a Wiki object). +the form post view callable for the form it renders. The ``context`` of the +``edit_page`` view will *always* be a Page resource (never a Wiki resource). If the view execution is *not* a result of a form submission (if the expression ``'form.submitted' in request.params`` is ``False``), the view -simply renders the edit form, passing the request, the page object, and a +simply renders the edit form, passing the request, the page resource, and a save_url which will be used as the action of the generated form. If the view execution *is* a result of a form submission (if the expression @@ -208,8 +210,8 @@ Most view callables we've added expected to be rendered via a :term:`template`. The default templating systems in :app:`Pyramid` are :term:`Chameleon` and :term:`Mako`. Chameleon is a variant of :term:`ZPT`, which is an XML-based templating language. Mako is a non-XML-based -templating language. Because we have to pick one, we'll use Chameleon for -this tutorial. +templating language. Because we had to pick one, we chose Chameleon for this +tutorial. The templates we create will live in the ``templates`` directory of our tutorial package. Chameleon templates must have a ``.pt`` extension to be @@ -218,8 +220,8 @@ recognized as such. The ``view.pt`` Template ------------------------ -The ``view.pt`` template is used for viewing a single wiki page. It is used -by the ``view_page`` view function. It should have a div that is "structure +The ``view.pt`` template is used for viewing a single Page. It is used by +the ``view_page`` view function. It should have a div that is "structure replaced" with the ``content`` value provided by the view. It should also have a link on the rendered page that points at the "edit" URL (the URL which invokes the ``edit_page`` view for the page being viewed). @@ -244,12 +246,12 @@ the below: The ``edit.pt`` Template ------------------------ -The ``edit.pt`` template is used for adding and editing a wiki page. It is -used by the ``add_page`` and ``edit_page`` view functions. It should display -a page containing a form that POSTs back to the "save_url" argument supplied -by the view. The form should have a "body" textarea field (the page data), -and a submit button that has the name "form.submitted". The textarea in the -form should be filled with any existing page data when it is rendered. +The ``edit.pt`` template is used for adding and editing a Page. It is used +by the ``add_page`` and ``edit_page`` view functions. It should display a +page containing a form that POSTs back to the "save_url" argument supplied by +the view. The form should have a "body" textarea field (the page data), and +a submit button that has the name "form.submitted". The textarea in the form +should be filled with any existing page data when it is rendered. Once we're done with the ``edit.pt`` template, it will look a lot like the below: @@ -258,10 +260,10 @@ below: :linenos: :language: xml -Static Resources ----------------- +Static Assets +------------- -Our templates name a single static resource named ``style.css``. We need to +Our templates name a single static asset named ``style.css``. We need to create this and place it in a file named ``style.css`` within our package's ``static`` directory. This file is a little too long to replicate within the body of this guide, however it is available `online @@ -270,7 +272,7 @@ body of this guide, however it is available `online This CSS file will be accessed via e.g. ``http://localhost:6543/static/style.css`` by virtue of the call to ``add_static_view`` directive we've made in the ``__init__`` file. Any -number and type of static resources can be placed in this directory (or +number and type of static assets can be placed in this directory (or subdirectories) and are just referred to by URL within templates. Testing the Views @@ -324,20 +326,20 @@ Viewing the Application in a Browser Once we've completed our edits, we can finally examine our application in a browser. The views we'll try are as follows: -- Visiting ``http://localhost:6543/`` in a browser invokes the - ``view_wiki`` view. This always redirects to the ``view_page`` view - of the FrontPage page object. +- Visiting ``http://localhost:6543/`` in a browser invokes the ``view_wiki`` + view. This always redirects to the ``view_page`` view of the ``FrontPage`` + Page resource. - Visiting ``http://localhost:6543/FrontPage/`` in a browser invokes - the ``view_page`` view of the front page page object. This is + the ``view_page`` view of the front page resource. This is because it's the *default view* (a view without a ``name``) for Page - objects. + resources. - Visiting ``http://localhost:6543/FrontPage/edit_page`` in a browser - invokes the edit view for the front page object. + invokes the edit view for the ``FrontPage`` Page resource. - Visiting ``http://localhost:6543/add_page/SomePageName`` in a - browser invokes the add view for a page. + browser invokes the add view for a Page. - To generate an error, visit ``http://localhost:6543/add_page`` which will generate an ``IndexError`` for the expression diff --git a/docs/tutorials/wiki/src/authorization/tutorial/login.py b/docs/tutorials/wiki/src/authorization/tutorial/login.py index 59e71a1d9..463db71a6 100644 --- a/docs/tutorials/wiki/src/authorization/tutorial/login.py +++ b/docs/tutorials/wiki/src/authorization/tutorial/login.py @@ -7,10 +7,10 @@ from pyramid.url import resource_url from tutorial.security import USERS -@view_config(context='pyramid.exceptions.Forbidden', - renderer='templates/login.pt') @view_config(context='tutorial.models.Wiki', name='login', renderer='templates/login.pt') +@view_config(context='pyramid.exceptions.Forbidden', + renderer='templates/login.pt') def login(request): login_url = resource_url(request.context, request, 'login') referrer = request.url -- cgit v1.2.3 From c57b0660fef15be7e8c5e235ae899372a19b2c91 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 23 Dec 2010 20:06:28 -0500 Subject: specify ordering unimportance --- docs/tutorials/wiki/authorization.rst | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/docs/tutorials/wiki/authorization.rst b/docs/tutorials/wiki/authorization.rst index 57c4a3ce5..1b66ace96 100644 --- a/docs/tutorials/wiki/authorization.rst +++ b/docs/tutorials/wiki/authorization.rst @@ -83,20 +83,24 @@ and logout views. Add a file named ``login.py`` to your application :language: python Note that the ``login`` view callable in the ``login.py`` file has *two* view -configuration decorators. In the first view configuration decorator, we -configured the ``login`` view callable so it will be invoked when someone -visits ``/login`` (when the context is a Wiki and the view name is -``login``). The second decorator (with context of +configuration decorators. The order of these decorators is unimportant. +Each just adds a different :term:`view configuration` for the ``login`` view +callable. + +The first view configuration decorator configures the ``login`` view callable +so it will be invoked when someone visits ``/login`` (when the context is a +Wiki and the view name is ``login``). The second decorator (with context of ``pyramid.exceptions.Forbidden``) specifies a :term:`forbidden view`. This -configures our login view to show up when :app:`Pyramid` detects that a view -invocation can not be authorized. Because we've configured a forbidden view, -the ``login`` view callable will be invoked whenever one of our users tries -to execute a view callable that they are disallowed from invoking based on -the :term:`authorization policy` in use. In our application, for example, -this means that if a user has not logged in, and he tries to add or edit a -Wiki page, he will be shown the login form. Before being allowed to continue -on to the add or edit form, he will have to provide credentials that give him -permission to add or edit via this login form. +configures our login view to be presented to the user when :app:`Pyramid` +detects that a view invocation can not be authorized. Because we've +configured a forbidden view, the ``login`` view callable will be invoked +whenever one of our users tries to execute a view callable that they are not +allowed to invoke as determined by the :term:`authorization policy` in use. +In our application, for example, this means that if a user has not logged in, +and he tries to add or edit a Wiki page, he will be shown the login form. +Before being allowed to continue on to the add or edit form, he will have to +provide credentials that give him permission to add or edit via this login +form. Changing Existing Views ~~~~~~~~~~~~~~~~~~~~~~~ -- cgit v1.2.3 From 8a93b2c92240cd77c32d5ad725b4db7a17693e65 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 24 Dec 2010 15:24:19 -0500 Subject: problems identified by Mike --- TODO.txt | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/TODO.txt b/TODO.txt index 0ee458254..d5524c8af 100644 --- a/TODO.txt +++ b/TODO.txt @@ -8,6 +8,15 @@ Must-Have (before 1.0) - Consider deprecations for ``model`` and ``resource`` APIs. +- Fix add_static_view docs to mention: static views cannot be served from the + root, that "name" really means "prefix", and that a non-URL prefix can be a + path with slashes in it. + +- Add an example of serving a static asset from the root using a view. + +- Add an example of using a cascade to serve static assets from the root. + + Should-Have ----------- @@ -26,6 +35,9 @@ Should-Have - Static (URL-generation only) routes. +- Provide a response_set_cookie method on the request for rendered responses + that can be used as input to response.set_cookie? + Nice-to-Have ------------ @@ -82,9 +94,6 @@ Nice-to-Have action = '^foo$' mypackage.views.MyView.foo_GET -- Provide a response_cookies attribute on the request for rendered - responses that can be used as input to response.set_cookie. - - Raise an exception when a value in response_headerlist is not a string or decide to encode. -- cgit v1.2.3 From c9320849bfe10b07a294033c99f0b45733d6f79b Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 24 Dec 2010 15:31:30 -0500 Subject: garden --- TODO.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/TODO.txt b/TODO.txt index d5524c8af..d73583cab 100644 --- a/TODO.txt +++ b/TODO.txt @@ -16,6 +16,7 @@ Must-Have (before 1.0) - Add an example of using a cascade to serve static assets from the root. +- Explore static file return from handler action using wsgiapp2 + fileapp. Should-Have ----------- -- cgit v1.2.3 From c5acd0807c9cda79ed5ee36ec98474d607922e05 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 24 Dec 2010 15:39:07 -0500 Subject: garden --- TODO.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/TODO.txt b/TODO.txt index d73583cab..8771cdd42 100644 --- a/TODO.txt +++ b/TODO.txt @@ -14,6 +14,9 @@ Must-Have (before 1.0) - Add an example of serving a static asset from the root using a view. +- Add an example of serving a static asset using a view by setting + response.app_iter and the response content-type/content-disposition. + - Add an example of using a cascade to serve static assets from the root. - Explore static file return from handler action using wsgiapp2 + fileapp. -- cgit v1.2.3 From 83efb21867545b0e2084be1fa7aa33f02b211cd3 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 24 Dec 2010 16:20:35 -0500 Subject: garden --- TODO.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/TODO.txt b/TODO.txt index 8771cdd42..546ae8f1b 100644 --- a/TODO.txt +++ b/TODO.txt @@ -21,6 +21,9 @@ Must-Have (before 1.0) - Explore static file return from handler action using wsgiapp2 + fileapp. +- Document ``config.add_route('catchall', '/*subpath', + view=static_view('some:asset/spec'))`` + Should-Have ----------- -- cgit v1.2.3 From b33dcac47dc0e759cd77a1548d8b38663a977df0 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 24 Dec 2010 16:22:55 -0500 Subject: - Fix API documentation rendering for ``pyramid.view.static`` --- CHANGES.txt | 2 ++ docs/api/view.rst | 1 + pyramid/tests/test_static.py | 2 +- pyramid/view.py | 6 ++++-- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index eef001f73..6932a0c25 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -59,6 +59,8 @@ Documentation - Changed the "ZODB + Traversal Wiki Tutorial" based on changes to ``pyramid_zodb`` Paster template. +- Fix API documentation rendering for ``pyramid.view.static`` + 1.0a7 (2010-12-20) ================== diff --git a/docs/api/view.rst b/docs/api/view.rst index 0057cca4a..4dddea25f 100644 --- a/docs/api/view.rst +++ b/docs/api/view.rst @@ -18,6 +18,7 @@ .. autoclass:: static :members: + :inherited-members: .. autofunction:: append_slash_notfound_view(context, request) diff --git a/pyramid/tests/test_static.py b/pyramid/tests/test_static.py index f7ae6e1a5..33bd51d31 100644 --- a/pyramid/tests/test_static.py +++ b/pyramid/tests/test_static.py @@ -153,7 +153,7 @@ class TestPackageURLParser(unittest.TestCase): self.failUnless('404 Not Found' in body) self.assertEqual(sr.status, '404 Not Found') -class TestStaticView(unittest.TestCase): +class Test_static_view(unittest.TestCase): def setUp(self): cleanUp() diff --git a/pyramid/view.py b/pyramid/view.py index 67329c363..3dc110863 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -19,10 +19,12 @@ from pyramid.interfaces import IView from pyramid.interfaces import IViewClassifier from pyramid.httpexceptions import HTTPFound -from pyramid.static import static_view as static # B/C +from pyramid.static import static_view from pyramid.threadlocal import get_current_registry -static = static # dont yet deprecate this (ever?) +# Nast BW compat hack: dont yet deprecate this (ever?) +class static(static_view): # only subclass for purposes of autodoc + __doc__ = static_view.__doc__ _marker = object() -- cgit v1.2.3 From 8fdcf54bee2cf9609bf4a6a2a0e13a9632ad295b Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 24 Dec 2010 16:23:54 -0500 Subject: garden --- TODO.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/TODO.txt b/TODO.txt index 546ae8f1b..e3c2e5f6c 100644 --- a/TODO.txt +++ b/TODO.txt @@ -22,7 +22,8 @@ Must-Have (before 1.0) - Explore static file return from handler action using wsgiapp2 + fileapp. - Document ``config.add_route('catchall', '/*subpath', - view=static_view('some:asset/spec'))`` + view=pyramid.views.static('some:asset/spec'))``, and explain that + ``static`` requires ``subpath``. Should-Have ----------- -- cgit v1.2.3 From 10fd8fe6bc26a7241542353032540cd4415ee9cc Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 24 Dec 2010 17:09:32 -0500 Subject: - Added "Advanced Configuration" narrative chapter which documents how to deal with configuration conflicts, two-phase configuration, ``include`` and ``commit``. --- CHANGES.txt | 4 + TODO.txt | 3 +- docs/index.rst | 1 + docs/latexindex.rst | 1 + docs/narr/advconfig.rst | 373 ++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 381 insertions(+), 1 deletion(-) create mode 100644 docs/narr/advconfig.rst diff --git a/CHANGES.txt b/CHANGES.txt index 6932a0c25..e28ac9792 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -59,6 +59,10 @@ Documentation - Changed the "ZODB + Traversal Wiki Tutorial" based on changes to ``pyramid_zodb`` Paster template. +- Added "Advanced Configuration" narrative chapter which documents how to + deal with configuration conflicts, two-phase configuration, ``include`` and + ``commit``. + - Fix API documentation rendering for ``pyramid.view.static`` 1.0a7 (2010-12-20) diff --git a/TODO.txt b/TODO.txt index e3c2e5f6c..077e23012 100644 --- a/TODO.txt +++ b/TODO.txt @@ -4,7 +4,8 @@ Pyramid TODOs Must-Have (before 1.0) ---------------------- -- Narrative docs for ``Configurator.include`` and ``Configurator.commit``. +- Reconcile "extending an existing application" chapter with existence of + "advanced configuration" chapter. - Consider deprecations for ``model`` and ``resource`` APIs. diff --git a/docs/index.rst b/docs/index.rst index 343fb28ba..3830a83f9 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -58,6 +58,7 @@ Narrative documentation in chapter form explaining how to use narr/environment narr/testing narr/hooks + narr/advconfig narr/declarative narr/extending narr/assets diff --git a/docs/latexindex.rst b/docs/latexindex.rst index 713c9841f..2d4644416 100644 --- a/docs/latexindex.rst +++ b/docs/latexindex.rst @@ -51,6 +51,7 @@ Narrative Documentation narr/environment narr/testing narr/hooks + narr/advconfig narr/declarative narr/extending narr/assets diff --git a/docs/narr/advconfig.rst b/docs/narr/advconfig.rst new file mode 100644 index 000000000..a4692e4c6 --- /dev/null +++ b/docs/narr/advconfig.rst @@ -0,0 +1,373 @@ +.. index:: + pair: advanced; configuration + +.. _advconfig_narr: + +Advanced Configuration +====================== + +To support application extensibility, the :app:`Pyramid` +:term:`Configurator`, by default, detects configuration conflicts and allows +you to include configuration imperatively from other packages or modules. It +also, by default, performs configuration in two separate phases. This allows +you to ignore relative configuration statement ordering in some +circumstances. + +.. index:: + single: imperative configuration + +.. _conflict_detection: + +Conflict Detection +------------------ + +Here's a familiar example of one of the simplest :app:`Pyramid` applications, +configured imperatively: + +.. code-block:: python + :linenos: + + from paste.httpserver import serve + from pyramid.config import Configurator + from pyramid.response import Response + + def hello_world(request): + return Response('Hello world!') + + if __name__ == '__main__': + config = Configurator() + config.add_view(hello_world) + app = config.make_wsgi_app() + serve(app, host='0.0.0.0') + +When you start this application, all will be OK. However, what happens if we +try to add another view to the configuration with the same set of +:term:`predicate` arguments as one we've already added? + +.. code-block:: python + :linenos: + + from paste.httpserver import serve + from pyramid.config import Configurator + from pyramid.response import Response + + def hello_world(request): + return Response('Hello world!') + + def goodbye_world(request): + return Response('Goodbye world!') + + if __name__ == '__main__': + config = Configurator() + + config.add_view(hello_world, name='hello') + + # conflicting view configuration + config.add_view(goodbye_world, name='hello') + + app = config.make_wsgi_app() + serve(app, host='0.0.0.0') + +The application now has two conflicting view configuration statements. When +we try to start it again, it won't start. Instead, we'll receive a traceback +that ends something like this: + +.. code-block:: guess + :linenos: + + Traceback (most recent call last): + File "app.py", line 12, in + app = config.make_wsgi_app() + File "pyramid/config.py", line 839, in make_wsgi_app + self.commit() + File "pyramid/pyramid/config.py", line 473, in commit + self._ctx.execute_actions() + File "zope/configuration/config.py", line 600, in execute_actions + for action in resolveConflicts(self.actions): + File "zope/configuration/config.py", line 1507, in resolveConflicts + raise ConfigurationConflictError(conflicts) + zope.configuration.config.ConfigurationConflictError: + Conflicting configuration actions + For: ('view', None, '', None, , + None, None, None, None, None, False, None, None, None) + ('app.py', 14, '', 'config.add_view(hello_world)') + ('app.py', 17, '', 'config.add_view(hello_world)') + +This traceback is trying to tell us: + +- We've got conflicting information for a set of view configuration + statements (The ``For:`` line). + +- There are two statements which conflict, shown beneath the ``For:`` line: + ``config.add_view(hello_world. 'hello')`` on line 14 of ``app.py``, and + ``config.add_view(goodbye_world, 'hello')`` on line 17 of ``app.py``. + +These two configuration statements are in conflict because we've tried to +tell the system that the set of :term:`predicate` values for both view +configurations are exactly the same. Both the ``hello_world`` and +``goodbye_world`` views are configured to respond under the same set of +circumstances. This circumstance: the :term:`view name` (represented by the +``name=`` predicate) is ``hello``. + +This presents an ambiguity that :app:`Pyramid` cannot resolve. Rather than +allowing the circumstance to go unreported, by default Pyramid raises a +:exc:`ConfigurationConflictError` error and prevents the application from +running. + +Conflict detection happens for any kind of configuration: imperative +configuration, :term:`ZCML` configuration, or configuration that results from +the execution of a :term:`scan`. + +Manually Resolving Conflicts +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +There are a number of ways to manually resolve conflicts: the "right" way, by +strategically using :meth:`pyramid.config.Configurator.commit`, or by using +an "autocommitting" configurator. + +The Right Thing ++++++++++++++++ + +The most correct way to resolve conflicts is to "do the needful": change your +configuration code to not have conflicting configuration statements. The +details of how this is done depends entirely on the configuration statements +made by your application. Use the detail provided in the +:exc:`ConfigurationConflictError` to track down the offending conflicts and +modify your configuration code accordingly. + +If you're getting a conflict while trying to extend an existing application, +and that application has a function which performs configuration like this +one: + +.. code-block:: python + :linenos: + + def add_routes(config): + config.add_route(...) + +Don't call this function directly with ``config`` as an argument. Instead, +use :meth:`pyramid.config.Configuration.include`: + +.. code-block:: python + :linenos: + + config.include(add_routes) + +Using :meth:`~pyramid.config.Configuration.include` instead of calling the +function directly provides a modicum of automated conflict resolution, with +the configuration statements you define in the calling code overriding those +of the included function. See also :ref:`automatic_conflict_resolution` and +:ref:`including_configuration`. + +Using ``config.commit()`` ++++++++++++++++++++++++++ + +You can manually commit a configuration by using the +:meth:`pyramid.config.Configurator.commit` method between configuration +calls. For example, we can make the application we examined previously which +generated a conflict exception + +.. code-block:: python + :linenos: + + from paste.httpserver import serve + from pyramid.config import Configurator + from pyramid.response import Response + + def hello_world(request): + return Response('Hello world!') + + def goodbye_world(request): + return Response('Goodbye world!') + + if __name__ == '__main__': + config = Configurator() + + config.add_view(hello_world, name='hello') + + # conflicting view configuration + config.add_view(goodbye_world, name='hello') + + app = config.make_wsgi_app() + serve(app, host='0.0.0.0') + +We can prevent the two ``add_view`` calls from conflicting by issuing a call +to :meth:`~pyramid.config.Configurator.commit` between them: + +.. code-block:: python + :linenos: + + from paste.httpserver import serve + from pyramid.config import Configurator + from pyramid.response import Response + + def hello_world(request): + return Response('Hello world!') + + def goodbye_world(request): + return Response('Goodbye world!') + + if __name__ == '__main__': + config = Configurator() + + config.add_view(hello_world, name='hello') + + config.commit() # commit any pending configuration actions + + # no-longer-conflicting view configuration + config.add_view(goodbye_world, name='hello') + + app = config.make_wsgi_app() + serve(app, host='0.0.0.0') + +In the above example we've issued a call to +:meth:`~pyramid.config.Configurator.commit` between the two ``add_view`` +calls. :meth:`~pyramid.config.Configurator.commit` will cause any pending +configuration statements. + +Calling :meth:`~pyramid.config.Configurator.commit` is safe at any time. It +executes all pending configuration actions and leaves the configuration +action list "clean". + +.. _autocommitting_configurator: + +Using An Autocommitting Configurator +++++++++++++++++++++++++++++++++++++ + +You can also use a heavy hammer to circumvent conflict detection by using a +configurator constructor parameter: ``autocommit=True``. For example: + +.. code-block:: python + :linenos: + + from pyramid.config import Configurator + + if __name__ == '__main__': + config = Configurator(autocommit=True) + +When the ``autocommit`` parameter passed to the Configurator is ``True``, +conflict detection (and :ref:`twophase_config`) is disabled. +:meth:`pyramid.config.Configurator.commit` has no effect when ``autocommit`` +is ``True`` either. + +If you use a Configurator in code that performs unit testing, it's usually a +good idea to use an autocommitting Configurator, because you are usually +unconcerned about conflict detection or two-phase configuration in test code. + +.. _automatic_conflict_resolution: + +Automatic Conflict Resolution +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If your code uses the :meth:`pyramid.config.Configurator.include` method to +include external configuration, some conflicts are automatically resolved. +Configuration statements that are made as the result of an "include" will be +overridden by configuration statements that happen within the caller of +the "include" method. See also + +Automatic conflict resolution supports this goal: if a user wants to reuse a +Pyramid application, and they want to customize the configuration of this +application without hacking its code "from outside", they can "include" the +package and override only some of its configuration statements within the +code that does the include. No conflicts will be generated by configuration +statements within the code which does the including, even if configuration +statements in the included code would conflict if it was moved "up" to the +calling code. + +Methods Which Provide Conflict Detection +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +These are the methods of the configurator which provide conflict detection: + +:meth:`~pyramid.config.Configurator.add_view`, +:meth:`~pyramid.config.Configurator.add_route`, +:meth:`~pyramid.config.Configurator.add_renderer`, +:meth:`~pyramid.config.Configurator.set_request_factory`, +:meth:`~pyramid.config.Configurator.set_renderer_globals_factory` +:meth:`~pyramid.config.Configurator.set_locale_negotiator` and +:meth:`~pyramid.config.Configurator.set_default_permission`. + +Some other methods of the configurator also indirectly provide conflict +detection, because they're implemented in terms of conflict-aware methods: + +- :meth:`~pyramid.config.Configurator.add_handler`, a frontend for + ``add_route`` and ``add_view``. + +- :meth:`~pyramid.config.Configurator.add_route` does a second type of + conflict detection when a ``view`` parameter is passed (it calls + ``add_view``). + +- :meth:`~pyramid.config.Configurator.static_view`, a frontend for + ``add_route`` and ``add_view``. + +.. _including_configuration: + +Including Configuration from External Sources +--------------------------------------------- + +Some application programmers will factor their configuration code in such a +way that it is easy to reuse and override configuration statements. For +example, such a developer might factor out a function used to add routes to +his application: + +.. code-block:: python + :linenos: + + def add_routes(config): + config.add_route(...) + +Rather than calling this function directly with ``config`` as an argument. +Instead, use :meth:`pyramid.config.Configuration.include`: + +.. code-block:: python + :linenos: + + config.include(add_routes) + +Using ``include`` rather than calling the function directly will allow +:ref:`automatic_conflict_resolution` to work. + +.. _twophase_config: + +Two-Phase Configuration +----------------------- + +When a non-autocommitting :term:`Configurator` is used to do configuration +(the default), configuration execution happens in two phases. In the first +phase, "eager" configuration actions (actions that must happen before all +others, such as registering a renderer) are executed, and *discriminators* +are computed for each of the actions that depend on the result of the eager +actions. In the second phase, the discriminators of all actions are compared +to do conflict detection. + +Due to this, for configuration methods that have no internal ordering +constraints, execution order of configuration method calls is not important. +For example, the relative ordering of +:meth:`pyramid.config.Configurator.add_view` and +:meth:`pyramid.config.Configurator.add_renderer` is unimportant when a +non-autocommitting configurator is used. This code snippet: + +.. code-block:: python + :linenos: + + config.add_view('some.view', renderer='path_to_custom/renderer.rn') + config.add_renderer('.rn', SomeCustomRendererFactory) + +Has the same result as: + +.. code-block:: python + :linenos: + + config.add_renderer('.rn', SomeCustomRendererFactory) + config.add_view('some.view', renderer='path_to_custom/renderer.rn') + +Even though the view statement depends on the registration of a custom +renderer, due to two-phase configuration, the order in which the +configuration statements are issued is not important. + +The same is untrue when you use an *autocommitting* configurator (see +:ref:`autocommitting_configurator`). When an autocommitting configurator is +used, two-phase configuration is disabled, and configuration statements must +be ordered in dependency order. + + -- cgit v1.2.3 From ef492de9cc24c573faaf12d654267480202fe645 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 24 Dec 2010 17:20:00 -0500 Subject: fix --- docs/narr/advconfig.rst | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/docs/narr/advconfig.rst b/docs/narr/advconfig.rst index a4692e4c6..2ed0a96ab 100644 --- a/docs/narr/advconfig.rst +++ b/docs/narr/advconfig.rst @@ -164,8 +164,9 @@ Using ``config.commit()`` You can manually commit a configuration by using the :meth:`pyramid.config.Configurator.commit` method between configuration -calls. For example, we can make the application we examined previously which -generated a conflict exception +calls. For example, we prevent conflicts from occurring in the application +we examined previously as the result of adding a ``commit``. Here's the +application that generates conflicts: .. code-block:: python :linenos: @@ -229,6 +230,10 @@ Calling :meth:`~pyramid.config.Configurator.commit` is safe at any time. It executes all pending configuration actions and leaves the configuration action list "clean". +Note that :meth:`~pyramid.config.Configurator.commit` has no effect when +you're using an *autocommitting* configurator (see +:ref:`autocommitting_configurator`). + .. _autocommitting_configurator: Using An Autocommitting Configurator @@ -246,9 +251,12 @@ configurator constructor parameter: ``autocommit=True``. For example: config = Configurator(autocommit=True) When the ``autocommit`` parameter passed to the Configurator is ``True``, -conflict detection (and :ref:`twophase_config`) is disabled. +conflict detection (and :ref:`twophase_config`) is disabled. Configuration +statements will be executed immediately, and succeeding statements will +override preceding ones. + :meth:`pyramid.config.Configurator.commit` has no effect when ``autocommit`` -is ``True`` either. +is ``True``. If you use a Configurator in code that performs unit testing, it's usually a good idea to use an autocommitting Configurator, because you are usually @@ -267,12 +275,12 @@ the "include" method. See also Automatic conflict resolution supports this goal: if a user wants to reuse a Pyramid application, and they want to customize the configuration of this -application without hacking its code "from outside", they can "include" the -package and override only some of its configuration statements within the -code that does the include. No conflicts will be generated by configuration -statements within the code which does the including, even if configuration -statements in the included code would conflict if it was moved "up" to the -calling code. +application without hacking its code "from outside", they can "include" a +configuration function from the package and override only some of its +configuration statements within the code that does the include. No conflicts +will be generated by configuration statements within the code which does the +including, even if configuration statements in the included code would +conflict if it was moved "up" to the calling code. Methods Which Provide Conflict Detection ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -- cgit v1.2.3 From c1b032a5a31a23d28767d2fce66d6a0f5b835791 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 24 Dec 2010 17:31:58 -0500 Subject: internal ordering --- docs/narr/advconfig.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/narr/advconfig.rst b/docs/narr/advconfig.rst index 2ed0a96ab..68e3796fe 100644 --- a/docs/narr/advconfig.rst +++ b/docs/narr/advconfig.rst @@ -378,4 +378,10 @@ The same is untrue when you use an *autocommitting* configurator (see used, two-phase configuration is disabled, and configuration statements must be ordered in dependency order. +Some configuration methods, such as +:meth:`pyramid.config.Configurator.add_route` and +:meth:`pyramid.config.Configurator.add_handler` have internal ordering +constraints: they routes they imply require relative ordering. Such ordering +constraints are not absolved by two-phase configuration. Routes are still +added in configuration execution order. -- cgit v1.2.3 From ec0834c7554010f064e836588211f8c671b950be Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 24 Dec 2010 17:33:42 -0500 Subject: internal ordering --- docs/narr/advconfig.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/narr/advconfig.rst b/docs/narr/advconfig.rst index 68e3796fe..e096ef863 100644 --- a/docs/narr/advconfig.rst +++ b/docs/narr/advconfig.rst @@ -371,7 +371,9 @@ Has the same result as: Even though the view statement depends on the registration of a custom renderer, due to two-phase configuration, the order in which the -configuration statements are issued is not important. +configuration statements are issued is not important. ``add_view`` will be +able to find the ``.rn`` renderer even if ``add_renderer`` is called after +``add_view``. The same is untrue when you use an *autocommitting* configurator (see :ref:`autocommitting_configurator`). When an autocommitting configurator is -- cgit v1.2.3 From dc475bf58829bae6e377c95a0e457c491923b60f Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 24 Dec 2010 18:08:56 -0500 Subject: garden --- TODO.txt | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/TODO.txt b/TODO.txt index 077e23012..f0b1e8b58 100644 --- a/TODO.txt +++ b/TODO.txt @@ -4,11 +4,16 @@ Pyramid TODOs Must-Have (before 1.0) ---------------------- +- Write a "Whats New" (delta from BFG 1.3) + - Reconcile "extending an existing application" chapter with existence of "advanced configuration" chapter. - Consider deprecations for ``model`` and ``resource`` APIs. +Should-Have +----------- + - Fix add_static_view docs to mention: static views cannot be served from the root, that "name" really means "prefix", and that a non-URL prefix can be a path with slashes in it. @@ -26,14 +31,10 @@ Must-Have (before 1.0) view=pyramid.views.static('some:asset/spec'))``, and explain that ``static`` requires ``subpath``. -Should-Have ------------ +- Static (URL-generation only) routes. - Add narrative docs for wsgiapp and wsgiapp2. -- Add docs for httpexceptions module for each webob.exc class that inherits - from WSGIHTTPException. - - translationdir ZCML directive use of ``path_spec`` should maybe die. - Change "Cleaning up After a Request" in the urldispatch chapter to @@ -42,8 +43,6 @@ Should-Have - ``decorator=`` parameter to view_config. This would replace the existing _map_view "decorator" if it existed. -- Static (URL-generation only) routes. - - Provide a response_set_cookie method on the request for rendered responses that can be used as input to response.set_cookie? -- cgit v1.2.3 From 8cdb1b7dac2c771348bde7afe8a91abe2862c613 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 24 Dec 2010 19:39:24 -0500 Subject: reorganize --- docs/designdefense.rst | 1576 ++++++++++++++++++++++-------------------------- 1 file changed, 728 insertions(+), 848 deletions(-) diff --git a/docs/designdefense.rst b/docs/designdefense.rst index b92d8810d..2761d181f 100644 --- a/docs/designdefense.rst +++ b/docs/designdefense.rst @@ -3,133 +3,34 @@ Defending Pyramid's Design ========================== -From time to time, challenges to various aspects of :app:`Pyramid` -design are lodged. To give context to discussions that follow, we -detail some of the design decisions and trade-offs here. In some -cases, we acknowledge that the framework can be made better and we -describe future steps which will be taken to improve it; in some cases -we just file the challenge as "noted", as obviously you can't please -everyone all of the time. - -Pyramid Has Zope Things In It, So It's Too Complex --------------------------------------------------- - -On occasion, someone will feel compelled to post a mailing -list message that reads something like this: - -.. code-block:: text - - had a quick look at pyramid ... too complex to me and not really - understand for which benefits.. I feel should consider whether it's time - for me to step back to django .. I always hated zope (useless ?) - complexity and I love simple way of thinking - -(Paraphrased from a real email, actually.) - -Let's take this criticism point-by point. - -Too Complex -+++++++++++ - -- If you can understand this hello world program, you can use Pyramid: - -.. code-block:: python - :linenos: - - from paste.httpserver import serve - from pyramid.config import Configurator - from pyramid.response import Response - - def hello_world(request): - return Response('Hello world!') - - if __name__ == '__main__': - config = Configurator() - config.add_view(hello_world) - app = config.make_wsgi_app() - serve(app) - -- Pyramid is 5,000 lines of runtime code. Pylons 1.0 has about 3,000 lines - of runtime code. Django has about 60,000 lines of runtime code. You'd - practically need to bend the laws of space and time for Django to be - simpler than Pyramid. - -- It has 600 or more pages of documentation (printed), covering topics from - the very basic to the most advanced. *Nothing* is left undocumented, quite - literally. - -- It has an *awesome*, very helpful community. Visit the #repoze and/or - #pylons IRC channels on freenode.net and see. - -Hate Zope -+++++++++ - -I'm sorry you feel that way. The Zope brand has certainly taken its share of -lumps over the years, and has a reputation for being insular and mysterious. -But the word "Zope" is literally quite meaningless without qualification. -What *part* of Zope do you hate? "Zope" is a brand, not a technology. - -If it's Zope2-the-web-framework, Pyramid is not that. The primary designers -and developers of Pyramid, if anyone, should know. We wrote Pyramid's -predecessor (:mod:`repoze.bfg`), in part, because *we* knew that Zope 2 had -usability issues and limitations. :mod:`repoze.bfg` (and now :app:`Pyramid`) -was written to address these issues. - -If it's Zope3-the-web-framework, Pyramid is *definitely* not that. Making -use of lots of Zope 3 technologies is territory already staked out by the -:term:`Grok` project. Save for the obvious fact that they're both web -frameworks, :mod:`Pyramid` is very, very different than Grok. Grok exposes -lots of Zope technologies to end users. On the other hand, if you need to -understand a Zope-only concept while using Pyramid, then we've failed on some -very basic axis. - -If it's just the word Zope: it's, charitably, only guilt by association. You -need to understand that just because a piece of software internally uses some -package named ``zope.foo``, it doesn't turn the piece of software that uses -it into "Zope". There is a lot of *great* software written that has the word -Zope in its name. Zope is not some sort of monolithic thing, and a lot of -its software is usable externally. - -Zope Is Useless -+++++++++++++++ - -It's not really the job of this document to defend Zope. But Zope has been -around for over 10 years and has an incredibly large, active community. If -you don't believe this, http://taichino.appspot.com/pypi_ranking/authors is -an eye-opening reality check. - -Love Simplicity -+++++++++++++++ - -Years of effort have gone into honing this package and its documentation to -make it as simple as humanly possible for developers to use. Everything is a -tradeoff, of course, and people have their own ideas about what "simple" is. -You may have a style difference if you believe Pyramid is complex. Its -developers obviously disagree. +From time to time, challenges to various aspects of :app:`Pyramid` design are +lodged. To give context to discussions that follow, we detail some of the +design decisions and trade-offs here. In some cases, we acknowledge that the +framework can be made better and we describe future steps which will be taken +to improve it; in some cases we just file the challenge as "noted", as +obviously you can't please everyone all of the time. Pyramid Uses A Zope Component Architecture ("ZCA") Registry ----------------------------------------------------------- -:app:`Pyramid` uses a :term:`Zope Component Architecture` (ZCA) -"component registry" as its :term:`application registry` under the -hood. This is a point of some contention. :app:`Pyramid` is of a -:term:`Zope` pedigree, so it was natural for its developers to use a -ZCA registry at its inception. However, we understand that using a -ZCA registry has issues and consequences, which we've attempted to -address as best we can. Here's an introspection about -:app:`Pyramid` use of a ZCA registry, and the trade-offs its usage +:app:`Pyramid` uses a :term:`Zope Component Architecture` (ZCA) "component +registry" as its :term:`application registry` under the hood. This is a +point of some contention. :app:`Pyramid` is of a :term:`Zope` pedigree, so +it was natural for its developers to use a ZCA registry at its inception. +However, we understand that using a ZCA registry has issues and consequences, +which we've attempted to address as best we can. Here's an introspection +about :app:`Pyramid` use of a ZCA registry, and the trade-offs its usage involves. Problems ++++++++ The "global" API that may be used to access data in a ZCA "component -registry" is not particularly pretty or intuitive, and sometimes it's -just plain obtuse. Likewise, the conceptual load on a casual source -code reader of code that uses the ZCA global API is somewhat high. -Consider a ZCA neophyte reading the code that performs a typical -"unnamed utility" lookup using the :func:`zope.component.getUtility` -global API: +registry" is not particularly pretty or intuitive, and sometimes it's just +plain obtuse. Likewise, the conceptual load on a casual source code reader +of code that uses the ZCA global API is somewhat high. Consider a ZCA +neophyte reading the code that performs a typical "unnamed utility" lookup +using the :func:`zope.component.getUtility` global API: .. ignore-next-block .. code-block:: python @@ -139,74 +40,68 @@ global API: from zope.component import getUtility settings = getUtility(ISettings) -After this code runs, ``settings`` will be a Python dictionary. But -it's unlikely that any "civilian" would know that just by reading the -code. There are a number of comprehension issues with the bit of code -above that are obvious. - -First, what's a "utility"? Well, for the purposes of this discussion, -and for the purpose of the code above, it's just not very important. -If you really want to know, you can read `this -`_. However, still, -readers of such code need to understand the concept in order to parse -it. This is problem number one. - -Second, what's this ``ISettings`` thing? It's an :term:`interface`. -Is that important here? Not really, we're just using it as a "key" -for some lookup based on its identity as a marker: it represents an -object that has the dictionary API, but that's not very important in -this context. That's problem number two. - -Third of all, what does the ``getUtility`` function do? It's -performing a lookup for the ``ISettings`` "utility" that should -return.. well, a utility. Note how we've already built up a -dependency on the understanding of an :term:`interface` and the -concept of "utility" to answer this question: a bad sign so far. Note -also that the answer is circular, a *really* bad sign. - -Fourth, where does ``getUtility`` look to get the data? Well, the -"component registry" of course. What's a component registry? Problem -number four. - -Fifth, assuming you buy that there's some magical registry hanging -around, where *is* this registry? *Homina homina*... "around"? -That's sort of the best answer in this context (a more specific answer -would require knowledge of internals). Can there be more than one -registry? Yes. So *which* registry does it find the registration in? -Well, the "current" registry of course. In terms of -:app:`Pyramid`, the current registry is a thread local variable. -Using an API that consults a thread local makes understanding how it -works non-local. +After this code runs, ``settings`` will be a Python dictionary. But it's +unlikely that any "civilian" would know that just by reading the code. There +are a number of comprehension issues with the bit of code above that are +obvious. + +First, what's a "utility"? Well, for the purposes of this discussion, and +for the purpose of the code above, it's just not very important. If you +really want to know, you can read `this +`_. However, still, readers +of such code need to understand the concept in order to parse it. This is +problem number one. + +Second, what's this ``ISettings`` thing? It's an :term:`interface`. Is that +important here? Not really, we're just using it as a "key" for some lookup +based on its identity as a marker: it represents an object that has the +dictionary API, but that's not very important in this context. That's +problem number two. + +Third of all, what does the ``getUtility`` function do? It's performing a +lookup for the ``ISettings`` "utility" that should return.. well, a utility. +Note how we've already built up a dependency on the understanding of an +:term:`interface` and the concept of "utility" to answer this question: a bad +sign so far. Note also that the answer is circular, a *really* bad sign. + +Fourth, where does ``getUtility`` look to get the data? Well, the "component +registry" of course. What's a component registry? Problem number four. + +Fifth, assuming you buy that there's some magical registry hanging around, +where *is* this registry? *Homina homina*... "around"? That's sort of the +best answer in this context (a more specific answer would require knowledge +of internals). Can there be more than one registry? Yes. So *which* +registry does it find the registration in? Well, the "current" registry of +course. In terms of :app:`Pyramid`, the current registry is a thread local +variable. Using an API that consults a thread local makes understanding how +it works non-local. You've now bought in to the fact that there's a registry that is just "hanging around". But how does the registry get populated? Why, :term:`ZCML` of course. Sometimes. Or via imperative code. In this -particular case, however, the registration of ``ISettings`` is made by -the framework itself "under the hood": it's not present in any ZCML -nor was it performed imperatively. This is extremely hard to -comprehend. Problem number six. +particular case, however, the registration of ``ISettings`` is made by the +framework itself "under the hood": it's not present in any ZCML nor was it +performed imperatively. This is extremely hard to comprehend. Problem +number six. -Clearly there's some amount of cognitive load here that needs to be -borne by a reader of code that extends the :app:`Pyramid` framework -due to its use of the ZCA, even if he or she is already an expert -Python programmer and whom is an expert in the domain of web -applications. This is suboptimal. +Clearly there's some amount of cognitive load here that needs to be borne by +a reader of code that extends the :app:`Pyramid` framework due to its use of +the ZCA, even if he or she is already an expert Python programmer and whom is +an expert in the domain of web applications. This is suboptimal. Ameliorations +++++++++++++ -First, the primary amelioration: :app:`Pyramid` *does not expect -application developers to understand ZCA concepts or any of its APIs*. -If an *application* developer needs to understand a ZCA concept or API -during the creation of a :app:`Pyramid` application, we've failed -on some axis. +First, the primary amelioration: :app:`Pyramid` *does not expect application +developers to understand ZCA concepts or any of its APIs*. If an +*application* developer needs to understand a ZCA concept or API during the +creation of a :app:`Pyramid` application, we've failed on some axis. Instead, the framework hides the presence of the ZCA registry behind -special-purpose API functions that *do* use ZCA APIs. Take for -example the ``pyramid.security.authenticated_userid`` function, -which returns the userid present in the current request or ``None`` if -no userid is present in the current request. The application -developer calls it like so: +special-purpose API functions that *do* use ZCA APIs. Take for example the +``pyramid.security.authenticated_userid`` function, which returns the userid +present in the current request or ``None`` if no userid is present in the +current request. The application developer calls it like so: .. ignore-next-block .. code-block:: python @@ -234,38 +129,34 @@ is this: return None return policy.authenticated_userid(request) -Using such wrappers, we strive to always hide the ZCA API from -application developers. Application developers should just never know -about the ZCA API: they should call a Python function with some object -germane to the domain as an argument, and it should returns a result. -A corollary that follows is that any reader of an application that has -been written using :app:`Pyramid` needn't understand the ZCA API -either. - -Hiding the ZCA API from application developers and code readers is a -form of enhancing "domain specificity". No application developer -wants to need to understand the minutiae of the mechanics of how a web -framework does its thing. People want to deal in concepts that are -closer to the domain they're working in: for example, web developers -want to know about *users*, not *utilities*. :app:`Pyramid` uses -the ZCA as an implementation detail, not as a feature which is exposed -to end users. - -However, unlike application developers, *framework developers*, -including people who want to override :app:`Pyramid` functionality -via preordained framework plugpoints like traversal or view lookup -*must* understand the ZCA registry API. - -:app:`Pyramid` framework developers were so concerned about -conceptual load issues of the ZCA registry API for framework -developers that a `replacement registry implementation -`_ named -:mod:`repoze.component` was actually developed. Though this package -has a registry implementation which is fully functional and -well-tested, and its API is much nicer than the ZCA registry API, work -on it was largely abandoned and it is not used in :app:`Pyramid`. -We continued to use a ZCA registry within :app:`Pyramid` because it -ultimately proved a better fit. +Using such wrappers, we strive to always hide the ZCA API from application +developers. Application developers should just never know about the ZCA API: +they should call a Python function with some object germane to the domain as +an argument, and it should returns a result. A corollary that follows is +that any reader of an application that has been written using :app:`Pyramid` +needn't understand the ZCA API either. + +Hiding the ZCA API from application developers and code readers is a form of +enhancing "domain specificity". No application developer wants to need to +understand the minutiae of the mechanics of how a web framework does its +thing. People want to deal in concepts that are closer to the domain they're +working in: for example, web developers want to know about *users*, not +*utilities*. :app:`Pyramid` uses the ZCA as an implementation detail, not as +a feature which is exposed to end users. + +However, unlike application developers, *framework developers*, including +people who want to override :app:`Pyramid` functionality via preordained +framework plugpoints like traversal or view lookup *must* understand the ZCA +registry API. + +:app:`Pyramid` framework developers were so concerned about conceptual load +issues of the ZCA registry API for framework developers that a `replacement +registry implementation `_ +named :mod:`repoze.component` was actually developed. Though this package +has a registry implementation which is fully functional and well-tested, and +its API is much nicer than the ZCA registry API, work on it was largely +abandoned and it is not used in :app:`Pyramid`. We continued to use a ZCA +registry within :app:`Pyramid` because it ultimately proved a better fit. .. note:: We continued using ZCA registry rather than disusing it in favor of using the registry implementation in @@ -276,151 +167,97 @@ ultimately proved a better fit. that allowed for this functionality seemed like it was just reinventing the wheel. -Making framework developers and extenders understand the ZCA registry -API is a trade-off. We (the :app:`Pyramid` developers) like the -features that the ZCA registry gives us, and we have long-ago borne -the weight of understanding what it does and how it works. The -authors of :app:`Pyramid` understand the ZCA deeply and can read -code that uses it as easily as any other code. - -But we recognize that developers who my want to extend the framework -are not as comfortable with the ZCA registry API as the original -developers are with it. So, for the purposes of being kind to -third-party :app:`Pyramid` framework developers in, we've drawn -some lines in the sand. - -#) In all "core" code, We've made use of ZCA global API functions such - as ``zope.component.getUtility`` and ``zope.component.getAdapter`` - the exception instead of the rule. So instead of: - - .. code-block:: python - :linenos: - - from pyramid.interfaces import IAuthenticationPolicy - from zope.component import getUtility - policy = getUtility(IAuthenticationPolicy) - - :app:`Pyramid` code will usually do: - - .. code-block:: python - :linenos: - - from pyramid.interfaces import IAuthenticationPolicy - from pyramid.threadlocal import get_current_registry - registry = get_current_registry() - policy = registry.getUtility(IAuthenticationPolicy) +Making framework developers and extenders understand the ZCA registry API is +a trade-off. We (the :app:`Pyramid` developers) like the features that the +ZCA registry gives us, and we have long-ago borne the weight of understanding +what it does and how it works. The authors of :app:`Pyramid` understand the +ZCA deeply and can read code that uses it as easily as any other code. - While the latter is more verbose, it also arguably makes it more - obvious what's going on. All of the :app:`Pyramid` core code uses - this pattern rather than the ZCA global API. +But we recognize that developers who my want to extend the framework are not +as comfortable with the ZCA registry API as the original developers are with +it. So, for the purposes of being kind to third-party :app:`Pyramid` +framework developers in, we've drawn some lines in the sand. -#) We've turned the component registry used by :app:`Pyramid` into - something that is accessible using the plain old dictionary API - (like the :mod:`repoze.component` API). For example, the snippet - of code in the problem section above was: +In all "core" code, We've made use of ZCA global API functions such as +``zope.component.getUtility`` and ``zope.component.getAdapter`` the exception +instead of the rule. So instead of: - .. code-block:: python - :linenos: - - from pyramid.interfaces import ISettings - from zope.component import getUtility - settings = getUtility(ISettings) - - In a better world, we might be able to spell this as: - - .. code-block:: python - :linenos: - - from pyramid.threadlocal import get_current_registry - - registry = get_current_registry() - settings = registry['settings'] +.. code-block:: python + :linenos: - In this world, we've removed the need to understand utilities and - interfaces, because we've disused them in favor of a plain dictionary - lookup. We *haven't* removed the need to understand the concept of a - *registry*, but for the purposes of this example, it's simply a - dictionary. We haven't killed off the concept of a thread local - either. Let's kill off thread locals, pretending to want to do this - in some code that has access to the :term:`request`: + from pyramid.interfaces import IAuthenticationPolicy + from zope.component import getUtility + policy = getUtility(IAuthenticationPolicy) - .. code-block:: python - :linenos: +:app:`Pyramid` code will usually do: - registry = request.registry - settings = registry['settings'] +.. code-block:: python + :linenos: - In *this* world, we've reduced the conceptual problem to understanding - attributes and the dictionary API. Every Python programmer knows - these things, even framework programmers. + from pyramid.interfaces import IAuthenticationPolicy + from pyramid.threadlocal import get_current_registry + registry = get_current_registry() + policy = registry.getUtility(IAuthenticationPolicy) -While :app:`Pyramid` still uses some suboptimal unnamed utility -registrations, future versions of it will where possible disuse these -things in favor of straight dictionary assignments and lookups, as -demonstrated above, to be kinder to new framework developers. We'll -continue to seek ways to reduce framework developer cognitive load. +While the latter is more verbose, it also arguably makes it more obvious +what's going on. All of the :app:`Pyramid` core code uses this pattern +rather than the ZCA global API. Rationale +++++++++ -Here are the main rationales involved in the :app:`Pyramid` -decision to use the ZCA registry: - -- Pedigree. A nontrivial part of the answer to this question is - "pedigree". Much of the design of :app:`Pyramid` is stolen - directly from :term:`Zope`. Zope uses the ZCA registry to do a - number of tricks. :app:`Pyramid` mimics these tricks, and, - because the ZCA registry works well for that set of tricks, - :app:`Pyramid` uses it for the same purposes. For example, the - way that :app:`Pyramid` maps a :term:`request` to a :term:`view - callable` is lifted almost entirely from Zope. The ZCA registry - plays an important role in the particulars of how this request to - view mapping is done. - -- Features. The ZCA component registry essentially provides what can - be considered something like a "superdictionary", which allows for - more complex lookups than retrieving a value based on a single key. - Some of this lookup capability is very useful for end users, such as - being able to register a view that is only found when the context is - some class of object, or when the context implements some - :term:`interface`. - -- Singularity. There's only one "place" where "application - configuration" lives in a :app:`Pyramid` application: in a - component registry. The component registry answers questions made - to it by the framework at runtime based on the configuration of *an - application*. Note: "an application" is not the same as "a - process", multiple independently configured copies of the same - :app:`Pyramid` application are capable of running in the same +Here are the main rationales involved in the :app:`Pyramid` decision to use +the ZCA registry: + +- Pedigree. A nontrivial part of the answer to this question is "pedigree". + Much of the design of :app:`Pyramid` is stolen directly from :term:`Zope`. + Zope uses the ZCA registry to do a number of tricks. :app:`Pyramid` mimics + these tricks, and, because the ZCA registry works well for that set of + tricks, :app:`Pyramid` uses it for the same purposes. For example, the way + that :app:`Pyramid` maps a :term:`request` to a :term:`view callable` using + :term:`traversal` is lifted almost entirely from Zope. The ZCA registry + plays an important role in the particulars of how this request to view + mapping is done. + +- Features. The ZCA component registry essentially provides what can be + considered something like a "superdictionary", which allows for more + complex lookups than retrieving a value based on a single key. Some of + this lookup capability is very useful for end users, such as being able to + register a view that is only found when the context is some class of + object, or when the context implements some :term:`interface`. + +- Singularity. There's only one "place" where "application configuration" + lives in a :app:`Pyramid` application: in a component registry. The + component registry answers questions made to it by the framework at runtime + based on the configuration of *an application*. Note: "an application" is + not the same as "a process", multiple independently configured copies of + the same :app:`Pyramid` application are capable of running in the same process space. -- Composability. A ZCA component registry can be populated - imperatively, or there's an existing mechanism to populate a - registry via the use of a configuration file (ZCML). We didn't need - to write a frontend from scratch to make use of - configuration-file-driven registry population. - -- Pluggability. Use of the ZCA registry allows for framework - extensibility via a well-defined and widely understood plugin - architecture. As long as framework developers and extenders - understand the ZCA registry, it's possible to extend - :app:`Pyramid` almost arbitrarily. For example, it's relatively - easy to build a ZCML directive that registers several views "all at - once", allowing app developers to use that ZCML directive as a - "macro" in code that they write. This is somewhat of a - differentiating feature from other (non-Zope) frameworks. - -- Testability. Judicious use of the ZCA registry in framework code - makes testing that code slightly easier. Instead of using - monkeypatching or other facilities to register mock objects for - testing, we inject dependencies via ZCA registrations and then use - lookups in the code find our mock objects. - -- Speed. The ZCA registry is very fast for a specific set of complex - lookup scenarios that :app:`Pyramid` uses, having been optimized - through the years for just these purposes. The ZCA registry - contains optional C code for this purpose which demonstrably has no - (or very few) bugs. +- Composability. A ZCA component registry can be populated imperatively, or + there's an existing mechanism to populate a registry via the use of a + configuration file (ZCML). We didn't need to write a frontend from scratch + to make use of configuration-file-driven registry population. + +- Pluggability. Use of the ZCA registry allows for framework extensibility + via a well-defined and widely understood plugin architecture. As long as + framework developers and extenders understand the ZCA registry, it's + possible to extend :app:`Pyramid` almost arbitrarily. For example, it's + relatively easy to build a ZCML directive that registers several views "all + at once", allowing app developers to use that ZCML directive as a "macro" + in code that they write. This is somewhat of a differentiating feature + from other (non-Zope) frameworks. + +- Testability. Judicious use of the ZCA registry in framework code makes + testing that code slightly easier. Instead of using monkeypatching or + other facilities to register mock objects for testing, we inject + dependencies via ZCA registrations and then use lookups in the code find + our mock objects. + +- Speed. The ZCA registry is very fast for a specific set of complex lookup + scenarios that :app:`Pyramid` uses, having been optimized through the years + for just these purposes. The ZCA registry contains optional C code for + this purpose which demonstrably has no (or very few) bugs. - Ecosystem. Many existing Zope packages can be used in :app:`Pyramid` with few (or no) changes due to our use of the ZCA @@ -431,17 +268,16 @@ Conclusion If you only *develop applications* using :app:`Pyramid`, there's not much to complain about here. You just should never need to understand the ZCA -registry or even know about its presence: use documented :app:`Pyramid` APIs -instead. However, you may be an application developer who doesn't read API -documentation because it's unmanly. Instead you read the raw source code, and -because you haven't read the documentation, you don't know what functions, -classes, and methods even *form* the :app:`Pyramid` API. As a result, you've -now written code that uses internals and you've painted yourself into a -conceptual corner as a result of needing to wrestle with some ZCA-using -implementation detail. If this is you, it's extremely hard to have a lot of -sympathy for you. You'll either need to get familiar with how we're using -the ZCA registry or you'll need to use only the documented APIs; that's why -we document them as APIs. +registry API: use documented :app:`Pyramid` APIs instead. However, you may +be an application developer who doesn't read API documentation because it's +unmanly. Instead you read the raw source code, and because you haven't read +the documentation, you don't know what functions, classes, and methods even +*form* the :app:`Pyramid` API. As a result, you've now written code that +uses internals and you've painted yourself into a conceptual corner as a +result of needing to wrestle with some ZCA-using implementation detail. If +this is you, it's extremely hard to have a lot of sympathy for you. You'll +either need to get familiar with how we're using the ZCA registry or you'll +need to use only the documented APIs; that's why we document them as APIs. If you *extend* or *develop* :app:`Pyramid` (create new ZCML directives, use some of the more obscure "ZCML hooks" as described in :ref:`hooks_chapter`, @@ -458,15 +294,15 @@ In this `TOPP Engineering blog entry `_, Ian Bicking asserts that the way :mod:`repoze.bfg` used a Zope interface to represent an HTTP request method added too much indirection for not enough -gain. We agreed in general, and for this reason, :mod:`repoze.bfg` version 1.1 -(and subsequent versions including :app:`Pyramid` 1.0+) added :term:`view +gain. We agreed in general, and for this reason, :mod:`repoze.bfg` version +1.1 (and subsequent versions including :app:`Pyramid` 1.0+) added :term:`view predicate` and :term:`route predicate` modifiers to view configuration. Predicates are request-specific (or :term:`context` -specific) matching narrowers which don't use interfaces. Instead, each predicate uses a domain-specific string as a match value. -For example, to write a view configuration which matches only requests -with the ``POST`` HTTP request method, you might write a ``@view_config`` +For example, to write a view configuration which matches only requests with +the ``POST`` HTTP request method, you might write a ``@view_config`` decorator which mentioned the ``request_method`` predicate: .. code-block:: python @@ -478,8 +314,7 @@ decorator which mentioned the ``request_method`` predicate: return 'POSTed' You might further narrow the matching scenario by adding an ``accept`` -predicate that narrows matching to something that accepts a JSON -response: +predicate that narrows matching to something that accepts a JSON response: .. code-block:: python :linenos: @@ -490,8 +325,8 @@ response: def post_view(request): return 'POSTed' -Such a view would only match when the request indicated that HTTP -request method was ``POST`` and that the remote user agent passed +Such a view would only match when the request indicated that HTTP request +method was ``POST`` and that the remote user agent passed ``application/json`` (or, for that matter, ``application/*``) in its ``Accept`` request header. @@ -528,11 +363,11 @@ Pyramid "Encourages Use of ZCML" -------------------------------- :term:`ZCML` is a configuration language that can be used to configure the -:term:`Zope Component Architecture` registry that :app:`Pyramid` uses as its +:term:`Zope Component Architecture` registry that :app:`Pyramid` uses for application configuration. Often people claim that Pyramid "needs ZCML". -Quick answer: well, it doesn't. At least not anymore. In :mod:`repoze.bfg` -(the predecessor to Pyramid) versions 1.0 and and 1.1, an application needed to +Quick answer: it doesn't. At least not anymore. In :mod:`repoze.bfg` (the +predecessor to Pyramid) versions 1.0 and and 1.1, an application needed to possess a ZCML file for it to begin executing successfully. However, :mod:`repoze.bfg` 1.2 and greater (including :app:`Pyramid` 1.0) includes a completely imperative mode for all configuration. You will be able to make @@ -556,8 +391,9 @@ everything done completely imperatively. For example, the very most basic app = config.make_wsgi_app() serve(app) -In this mode, no ZCML is required at all. Hopefully this mode will allow -people who are used to doing everything imperatively feel more comfortable. +In this mode, no ZCML is required at all, nor any other sort of frameworky +frontend to application configuration. Hopefully this mode will allow people +who are used to doing everything imperatively feel more comfortable. Pyramid Uses ZCML; ZCML is XML and I Don't Like XML --------------------------------------------------- @@ -843,64 +679,59 @@ Pyramid Has Too Many Dependencies --------------------------------- This is true. At the time of this writing, the total number of Python -package distributions that :app:`Pyramid` depends upon transitively -is 18 if you use Python 2.6 or 2.7, or 16 if you use Python 2.4 or -2.5. This is a lot more than zero package distribution dependencies: -a metric which various Python microframeworks and Django boast. - -The :mod:`zope.component` and :mod:`zope.configuration` packages on -which :app:`Pyramid` depends have transitive dependencies on -several other packages (:mod:`zope.schema`, :mod:`zope.i18n`, -:mod:`zope.event`, :mod:`zope.interface`, :mod:`zope.deprecation`, -:mod:`zope.i18nmessageid`). We've been working with the Zope -community to try to collapse and untangle some of these dependencies. -We'd prefer that these packages have fewer packages as transitive -dependencies, and that much of the functionality of these packages was -moved into a smaller *number* of packages. +package distributions that :app:`Pyramid` depends upon transitively is 18 if +you use Python 2.6 or 2.7, or 16 if you use Python 2.4 or 2.5. This is a lot +more than zero package distribution dependencies: a metric which various +Python microframeworks and Django boast. + +The :mod:`zope.component` and :mod:`zope.configuration` packages on which +:app:`Pyramid` depends have transitive dependencies on several other packages +(:mod:`zope.schema`, :mod:`zope.i18n`, :mod:`zope.event`, +:mod:`zope.interface`, :mod:`zope.deprecation`, :mod:`zope.i18nmessageid`). +We've been working with the Zope community to try to collapse and untangle +some of these dependencies. We'd prefer that these packages have fewer +packages as transitive dependencies, and that much of the functionality of +these packages was moved into a smaller *number* of packages. :app:`Pyramid` also has its own direct dependencies, such as :term:`Paste`, :term:`Chameleon`, :term:`Mako` and :term:`WebOb`, and some of these in turn have their own transitive dependencies. -It should be noted that :app:`Pyramid` is positively lithe compared -to :term:`Grok`, a different Zope-based framework. As of this -writing, in its default configuration, Grok has 126 package -distribution dependencies. The number of dependencies required by -:app:`Pyramid` is many times fewer than Grok (or Zope itself, upon -which Grok is based). :app:`Pyramid` has a number of package -distribution dependencies comparable to similarly-targeted frameworks -such as Pylons 1.X. - -We try not to reinvent too many wheels (at least the ones that don't -need reinventing), and this comes at the cost of some number of -dependencies. However, "number of package distributions" is just not -a terribly great metric to measure complexity. For example, the -:mod:`zope.event` distribution on which :app:`Pyramid` depends has -a grand total of four lines of runtime code. As noted above, we're -continually trying to agitate for a collapsing of these sorts of -packages into fewer distribution files. +It should be noted that :app:`Pyramid` is positively lithe compared to +:term:`Grok`, a different Zope-based framework. As of this writing, in its +default configuration, Grok has 126 package distribution dependencies. The +number of dependencies required by :app:`Pyramid` is many times fewer than +Grok (or Zope itself, upon which Grok is based). :app:`Pyramid` has a number +of package distribution dependencies comparable to similarly-targeted +frameworks such as Pylons 1.X. + +We try not to reinvent too many wheels (at least the ones that don't need +reinventing), and this comes at the cost of some number of dependencies. +However, "number of package distributions" is just not a terribly great +metric to measure complexity. For example, the :mod:`zope.event` +distribution on which :app:`Pyramid` depends has a grand total of four lines +of runtime code. As noted above, we're continually trying to agitate for a +collapsing of these sorts of packages into fewer distribution files. Pyramid "Cheats" To Obtain Speed -------------------------------- -Complaints have been lodged by other web framework authors at various -times that :app:`Pyramid` "cheats" to gain performance. One -claimed cheating mechanism is our use (transitively) of the C -extensions provided by :mod:`zope.interface` to do fast lookups. -Another claimed cheating mechanism is the religious avoidance of -extraneous function calls. +Complaints have been lodged by other web framework authors at various times +that :app:`Pyramid` "cheats" to gain performance. One claimed cheating +mechanism is our use (transitively) of the C extensions provided by +:mod:`zope.interface` to do fast lookups. Another claimed cheating mechanism +is the religious avoidance of extraneous function calls. -If there's such a thing as cheating to get better performance, we want -to cheat as much as possible. We optimize :app:`Pyramid` -aggressively. This comes at a cost: the core code has sections that -could be expressed more readably. As an amelioration, we've commented -these sections liberally. +If there's such a thing as cheating to get better performance, we want to +cheat as much as possible. We optimize :app:`Pyramid` aggressively. This +comes at a cost: the core code has sections that could be expressed more +readably. As an amelioration, we've commented these sections liberally. Pyramid Gets Its Terminology Wrong ("MVC") ------------------------------------------ -"I'm a MVC web framework user, and I'm confused. :app:`Pyramid` -calls the controller a view! And it doesn't have any controllers." +"I'm a MVC web framework user, and I'm confused. :app:`Pyramid` calls the +controller a view! And it doesn't have any controllers." If you are in this camp, you might have come to expect things about how your existing "MVC" framework uses its terminology. For example, you probably @@ -910,12 +741,11 @@ these concepts, and each probably *works* almost exactly like your existing "MVC" web framework. We just don't use the "MVC" terminology, as we can't square its usage in the web framework space with historical reality. -People very much want to give web applications the same properties as -common desktop GUI platforms by using similar terminology, and to -provide some frame of reference for how various components in the -common web framework might hang together. But in the opinion of the -author, "MVC" doesn't match the web very well in general. Quoting from -the `Model-View-Controller Wikipedia entry +People very much want to give web applications the same properties as common +desktop GUI platforms by using similar terminology, and to provide some frame +of reference for how various components in the common web framework might +hang together. But in the opinion of the author, "MVC" doesn't match the web +very well in general. Quoting from the `Model-View-Controller Wikipedia entry `_: .. code-block:: text @@ -946,23 +776,21 @@ the `Model-View-Controller Wikipedia entry The user interface waits for further user interactions, which restarts the cycle. -To the author, it seems as if someone edited this Wikipedia -definition, tortuously couching concepts in the most generic terms -possible in order to account for the use of the term "MVC" by current -web frameworks. I doubt such a broad definition would ever be agreed -to by the original authors of the MVC pattern. But *even so*, it -seems most "MVC" web frameworks fail to meet even this falsely generic -definition. - -For example, do your templates (views) always query models directly as -is claimed in "note that the view gets its own data from the model"? -Probably not. My "controllers" tend to do this, massaging the data for -easier use by the "view" (template). What do you do when your -"controller" returns JSON? Do your controllers use a template to -generate JSON? If not, what's the "view" then? Most MVC-style GUI web -frameworks have some sort of event system hooked up that lets the view -detect when the model changes. The web just has no such facility in -its current form: it's effectively pull-only. +To the author, it seems as if someone edited this Wikipedia definition, +tortuously couching concepts in the most generic terms possible in order to +account for the use of the term "MVC" by current web frameworks. I doubt +such a broad definition would ever be agreed to by the original authors of +the MVC pattern. But *even so*, it seems most "MVC" web frameworks fail to +meet even this falsely generic definition. + +For example, do your templates (views) always query models directly as is +claimed in "note that the view gets its own data from the model"? Probably +not. My "controllers" tend to do this, massaging the data for easier use by +the "view" (template). What do you do when your "controller" returns JSON? Do +your controllers use a template to generate JSON? If not, what's the "view" +then? Most MVC-style GUI web frameworks have some sort of event system +hooked up that lets the view detect when the model changes. The web just has +no such facility in its current form: it's effectively pull-only. So, in the interest of not mistaking desire with reality, and instead of trying to jam the square peg that is the web into the round hole of "MVC", we @@ -980,11 +808,11 @@ the current constraints of the web. Pyramid Applications are Extensible; I Don't Believe In Application Extensibility --------------------------------------------------------------------------------- -Any :app:`Pyramid` application written obeying certain constraints -is *extensible*. This feature is discussed in the :app:`Pyramid` -documentation chapter named :ref:`extending_chapter`. It is made -possible by the use of the :term:`Zope Component Architecture` and -:term:`ZCML` within :app:`Pyramid`. +Any :app:`Pyramid` application written obeying certain constraints is +*extensible*. This feature is discussed in the :app:`Pyramid` documentation +chapters named :ref:`extending_chapter` and :ref:`advconf_narr`. It is made +possible by the use of the :term:`Zope Component Architecture` and within +:app:`Pyramid`. "Extensible", in this context, means: @@ -1002,108 +830,99 @@ possible by the use of the :term:`Zope Component Architecture` and ZCA, the original developer does not need to think terribly hard about the mechanics of introducing such a plugpoint. -Many developers seem to believe that creating extensible applications -is "not worth it". They instead suggest that modifying the source of -a given application for each deployment to override behavior is more -reasonable. Much discussion about version control branching and -merging typically ensues. - -It's clear that making every application extensible isn't required. -The majority of web applications only have a single deployment, and -thus needn't be extensible at all. However, some web applications -have multiple deployments, and some have *many* deployments. For -example, a generic "content management" system (CMS) may have basic -functionality that needs to be extended for a particular deployment. -That CMS system may be deployed for many organizations at many places. -Some number of deployments of this CMS may be deployed centrally by a -third party and managed as a group. It's useful to be able to extend -such a system for each deployment via preordained plugpoints than it -is to continually keep each software branch of the system in sync with -some upstream source: the upstream developers may change code in such -a way that your changes to the same codebase conflict with theirs in -fiddly, trivial ways. Merging such changes repeatedly over the -lifetime of a deployment can be difficult and time consuming, and it's -often useful to be able to modify an application for a particular -deployment in a less invasive way. - -If you don't want to think about :app:`Pyramid` application -extensibility at all, you needn't. You can ignore extensibility -entirely. However, if you follow the set of rules defined in -:ref:`extending_chapter`, you don't need to *make* your application -extensible: any application you write in the framework just *is* -automatically extensible at a basic level. The mechanisms that -deployers use to extend it will be necessarily coarse: typically, -views, routes, and resources will be capable of being overridden, -usually via :term:`ZCML`. But for most minor (and even some major) -customizations, these are often the only override plugpoints -necessary: if the application doesn't do exactly what the deployment -requires, it's often possible for a deployer to override a view, -route, or resource and quickly make it do what he or she wants it to -do in ways *not necessarily anticipated by the original developer*. -Here are some example scenarios demonstrating the benefits of such a -feature. - -- If a deployment needs a different styling, the deployer may override - the main template and the CSS in a separate Python package which - defines overrides. - -- If a deployment needs an application page to do something - differently needs it to expose more or different information, the - deployer may override the view that renders the page within a - separate Python package. - -- If a deployment needs an additional feature, the deployer may add a - view to the override package. - -As long as the fundamental design of the upstream package doesn't -change, these types of modifications often survive across many -releases of the upstream package without needing to be revisited. - -Extending an application externally is not a panacea, and carries a -set of risks similar to branching and merging: sometimes major changes -upstream will cause you to need to revisit and update some of your -modifications. But you won't regularly need to deal wth meaningless -textual merge conflicts that trivial changes to upstream packages -often entail when it comes time to update the upstream package, -because if you extend an application externally, there just is no -textual merge done. Your modifications will also, for whatever its -worth, be contained in one, canonical, well-defined place. - -Branching an application and continually merging in order to get new -features and bugfixes is clearly useful. You can do that with a -:app:`Pyramid` application just as usefully as you can do it with -any application. But deployment of an application written in -:app:`Pyramid` makes it possible to avoid the need for this even if -the application doesn't define any plugpoints ahead of time. It's -possible that promoters of competing web frameworks dismiss this -feature in favor of branching and merging because applications written -in their framework of choice aren't extensible out of the box in a +Many developers seem to believe that creating extensible applications is "not +worth it". They instead suggest that modifying the source of a given +application for each deployment to override behavior is more reasonable. +Much discussion about version control branching and merging typically ensues. + +It's clear that making every application extensible isn't required. The +majority of web applications only have a single deployment, and thus needn't +be extensible at all. However, some web applications have multiple +deployments, and some have *many* deployments. For example, a generic +"content management" system (CMS) may have basic functionality that needs to +be extended for a particular deployment. That CMS system may be deployed for +many organizations at many places. Some number of deployments of this CMS +may be deployed centrally by a third party and managed as a group. It's +useful to be able to extend such a system for each deployment via preordained +plugpoints than it is to continually keep each software branch of the system +in sync with some upstream source: the upstream developers may change code in +such a way that your changes to the same codebase conflict with theirs in +fiddly, trivial ways. Merging such changes repeatedly over the lifetime of a +deployment can be difficult and time consuming, and it's often useful to be +able to modify an application for a particular deployment in a less invasive +way. + +If you don't want to think about :app:`Pyramid` application extensibility at +all, you needn't. You can ignore extensibility entirely. However, if you +follow the set of rules defined in :ref:`extending_chapter`, you don't need +to *make* your application extensible: any application you write in the +framework just *is* automatically extensible at a basic level. The +mechanisms that deployers use to extend it will be necessarily coarse: +typically, views, routes, and resources will be capable of being +overridden. But for most minor (and even some major) customizations, these +are often the only override plugpoints necessary: if the application doesn't +do exactly what the deployment requires, it's often possible for a deployer +to override a view, route, or resource and quickly make it do what he or she +wants it to do in ways *not necessarily anticipated by the original +developer*. Here are some example scenarios demonstrating the benefits of +such a feature. + +- If a deployment needs a different styling, the deployer may override the + main template and the CSS in a separate Python package which defines + overrides. + +- If a deployment needs an application page to do something differently needs + it to expose more or different information, the deployer may override the + view that renders the page within a separate Python package. + +- If a deployment needs an additional feature, the deployer may add a view to + the override package. + +As long as the fundamental design of the upstream package doesn't change, +these types of modifications often survive across many releases of the +upstream package without needing to be revisited. + +Extending an application externally is not a panacea, and carries a set of +risks similar to branching and merging: sometimes major changes upstream will +cause you to need to revisit and update some of your modifications. But you +won't regularly need to deal wth meaningless textual merge conflicts that +trivial changes to upstream packages often entail when it comes time to +update the upstream package, because if you extend an application externally, +there just is no textual merge done. Your modifications will also, for +whatever its worth, be contained in one, canonical, well-defined place. + +Branching an application and continually merging in order to get new features +and bugfixes is clearly useful. You can do that with a :app:`Pyramid` +application just as usefully as you can do it with any application. But +deployment of an application written in :app:`Pyramid` makes it possible to +avoid the need for this even if the application doesn't define any plugpoints +ahead of time. It's possible that promoters of competing web frameworks +dismiss this feature in favor of branching and merging because applications +written in their framework of choice aren't extensible out of the box in a comparably fundamental way. -While :app:`Pyramid` application are fundamentally extensible even -if you don't write them with specific extensibility in mind, if you're -moderately adventurous, you can also take it a step further. If you -learn more about the :term:`Zope Component Architecture`, you can -optionally use it to expose other more domain-specific configuration -plugpoints while developing an application. The plugpoints you expose -needn't be as coarse as the ones provided automatically by -:app:`Pyramid` itself. For example, you might compose your own -:term:`ZCML` directive that configures a set of views for a prebaked -purpose (e.g. ``restview`` or somesuch) , allowing other people to -refer to that directive when they make declarations in the -``configure.zcml`` of their customization package. There is a cost -for this: the developer of an application that defines custom -plugpoints for its deployers will need to understand the ZCA or he -will need to develop his own similar extensibility system. - -Ultimately, any argument about whether the extensibility features lent -to applications by :app:`Pyramid` are "good" or "bad" is somewhat -pointless. You needn't take advantage of the extensibility features -provided by a particular :app:`Pyramid` application in order to -affect a modification for a particular set of its deployments. You -can ignore the application's extensibility plugpoints entirely, and -instead use version control branching and merging to manage -application deployment modifications instead, as if you were deploying +While :app:`Pyramid` application are fundamentally extensible even if you +don't write them with specific extensibility in mind, if you're moderately +adventurous, you can also take it a step further. If you learn more about +the :term:`Zope Component Architecture`, you can optionally use it to expose +other more domain-specific configuration plugpoints while developing an +application. The plugpoints you expose needn't be as coarse as the ones +provided automatically by :app:`Pyramid` itself. For example, you might +compose your own :term:`ZCML` directive that configures a set of views for a +prebaked purpose (e.g. ``restview`` or somesuch) , allowing other people to +refer to that directive when they make declarations in the ``configure.zcml`` +of their customization package. There is a cost for this: the developer of +an application that defines custom plugpoints for its deployers will need to +understand the ZCA or he will need to develop his own similar extensibility +system. + +Ultimately, any argument about whether the extensibility features lent to +applications by :app:`Pyramid` are "good" or "bad" is somewhat pointless. You +needn't take advantage of the extensibility features provided by a particular +:app:`Pyramid` application in order to affect a modification for a particular +set of its deployments. You can ignore the application's extensibility +plugpoints entirely, and instead use version control branching and merging to +manage application deployment modifications instead, as if you were deploying an application written using any other web framework. Zope 3 Enforces "TTW" Authorization Checks By Default; Pyramid Does Not @@ -1112,11 +931,11 @@ Zope 3 Enforces "TTW" Authorization Checks By Default; Pyramid Does Not Challenge +++++++++ -:app:`Pyramid` performs automatic authorization checks only at -:term:`view` execution time. Zope 3 wraps context objects with a -`security proxy `, -which causes Zope 3 to do also security checks during attribute -access. I like this, because it means: +:app:`Pyramid` performs automatic authorization checks only at :term:`view` +execution time. Zope 3 wraps context objects with a `security proxy +`, which causes Zope 3 to +do also security checks during attribute access. I like this, because it +means: #) When I use the security proxy machinery, I can have a view that conditionally displays certain HTML elements (like form fields) or @@ -1132,75 +951,69 @@ access. I like this, because it means: Defense +++++++ -:app:`Pyramid` was developed by folks familiar with Zope 2, which -has a "through the web" security model. This "TTW" security model was -the precursor to Zope 3's security proxies. Over time, as the -:app:`Pyramid` developers (working in Zope 2) created such sites, -we found authorization checks during code interpretation extremely -useful in a minority of projects. But much of the time, TTW -authorization checks usually slowed down the development velocity of -projects that had no delegation requirements. In particular, if we -weren't allowing "untrusted" users to write arbitrary Python code to -be executed by our application, the burden of "through the web" -security checks proved too costly to justify. We (collectively) -haven't written an application on top of which untrusted developers -are allowed to write code in many years, so it seemed to make sense to -drop this model by default in a new web framework. - -And since we tend to use the same toolkit for all web applications, it's -just never been a concern to be able to use the same set of -restricted-execution code under two web different frameworks. - -Justifications for disabling security proxies by default -notwithstanding, given that Zope 3 security proxies are "viral" by -nature, the only requirement to use one is to make sure you wrap a -single object in a security proxy and make sure to access that object -normally when you want proxy security checks to happen. It is -possible to override the :app:`Pyramid` "traverser" for a given -application (see :ref:`changing_the_traverser`). To get Zope3-like -behavior, it is possible to plug in a different traverser which -returns Zope3-security-proxy-wrapped objects for each traversed object -(including the :term:`context` and the :term:`root`). This would have -the effect of creating a more Zope3-like environment without much -effort. +:app:`Pyramid` was developed by folks familiar with Zope 2, which has a +"through the web" security model. This "TTW" security model was the +precursor to Zope 3's security proxies. Over time, as the :app:`Pyramid` +developers (working in Zope 2) created such sites, we found authorization +checks during code interpretation extremely useful in a minority of projects. +But much of the time, TTW authorization checks usually slowed down the +development velocity of projects that had no delegation requirements. In +particular, if we weren't allowing "untrusted" users to write arbitrary +Python code to be executed by our application, the burden of "through the +web" security checks proved too costly to justify. We (collectively) haven't +written an application on top of which untrusted developers are allowed to +write code in many years, so it seemed to make sense to drop this model by +default in a new web framework. + +And since we tend to use the same toolkit for all web applications, it's just +never been a concern to be able to use the same set of restricted-execution +code under two web different frameworks. + +Justifications for disabling security proxies by default notwithstanding, +given that Zope 3 security proxies are "viral" by nature, the only +requirement to use one is to make sure you wrap a single object in a security +proxy and make sure to access that object normally when you want proxy +security checks to happen. It is possible to override the :app:`Pyramid` +"traverser" for a given application (see :ref:`changing_the_traverser`). To +get Zope3-like behavior, it is possible to plug in a different traverser +which returns Zope3-security-proxy-wrapped objects for each traversed object +(including the :term:`context` and the :term:`root`). This would have the +effect of creating a more Zope3-like environment without much effort. .. _microframeworks_smaller_hello_world: Microframeworks Have Smaller Hello World Programs ------------------------------------------------- -Self-described "microframeworks" exist: `Bottle -`_ and `Flask `_ are -two that are becoming popular. `Bobo `_ -doesn't describe itself as a microframework, but its intended userbase -is much the same. Many others exist. We've actually even (only as a -teaching tool, not as any sort of official project) `created one using -BFG `_ (the precursor to -Pyramid). Microframeworks are small frameworks with one common -feature: each allows its users to create a fully functional -application that lives in a single Python file. - -Some developers and microframework authors point out that Pyramid's -"hello world" single-file program is longer (by about five lines) than -the equivalent program in their favorite microframework. Guilty as -charged; in a contest of "whose is shortest", Pyramid indeed loses. - -This loss isn't for lack of trying. Pyramid aims to be useful in the -same circumstance in which microframeworks claim dominance: -single-file applications. But Pyramid doesn't sacrifice its ability -to credibly support larger applications in order to achieve -hello-world LoC parity with the current crop of microframeworks. -Pyramid's design instead tries to avoid some common pitfalls -associated with naive declarative configuration schemes. The -subsections which follow explain the rationale. +Self-described "microframeworks" exist: `Bottle `_ and +`Flask `_ are two that are becoming popular. `Bobo +`_ doesn't describe itself as a microframework, +but its intended userbase is much the same. Many others exist. We've +actually even (only as a teaching tool, not as any sort of official project) +`created one using BFG `_ (the +precursor to Pyramid). Microframeworks are small frameworks with one common +feature: each allows its users to create a fully functional application that +lives in a single Python file. + +Some developers and microframework authors point out that Pyramid's "hello +world" single-file program is longer (by about five lines) than the +equivalent program in their favorite microframework. Guilty as charged; in a +contest of "whose is shortest", Pyramid indeed loses. + +This loss isn't for lack of trying. Pyramid aims to be useful in the same +circumstance in which microframeworks claim dominance: single-file +applications. But Pyramid doesn't sacrifice its ability to credibly support +larger applications in order to achieve hello-world LoC parity with the +current crop of microframeworks. Pyramid's design instead tries to avoid +some common pitfalls associated with naive declarative configuration schemes. +The subsections which follow explain the rationale. .. _you_dont_own_modulescope: Application Programmers Don't Control The Module-Scope Codepath (Import-Time Side-Effects Are Evil) +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -Please imagine a directory structure with a set of Python files in -it: +Please imagine a directory structure with a set of Python files in it: .. code-block:: text @@ -1248,13 +1061,13 @@ The contents of ``config.py``: L.append(func) return func -If we cd to the directory that holds these files and we run ``python -app.py`` given the directory structure and code above, what happens? -Presumably, our ``decorator`` decorator will be used twice, once by the -decorated function ``foo`` in ``app.py`` and once by the decorated -function ``bar`` in ``app2.py``. Since each time the decorator is -used, the list ``L`` in ``config.py`` is appended to, we'd expect a -list with two elements to be printed, right? Sadly, no: +If we cd to the directory that holds these files and we run ``python app.py`` +given the directory structure and code above, what happens? Presumably, our +``decorator`` decorator will be used twice, once by the decorated function +``foo`` in ``app.py`` and once by the decorated function ``bar`` in +``app2.py``. Since each time the decorator is used, the list ``L`` in +``config.py`` is appended to, we'd expect a list with two elements to be +printed, right? Sadly, no: .. code-block:: text @@ -1263,27 +1076,26 @@ list with two elements to be printed, right? Sadly, no: , ] -By visual inspection, that outcome (three different functions in the -list) seems impossible. We only defined two functions and we -decorated each of those functions only once, so we believe that the -``decorator`` decorator will only run twice. However, what we believe -is wrong because the code at module scope in our ``app.py`` module was -*executed twice*. The code is executed once when the script is run as -``__main__`` (via ``python app.py``), and then it is executed again -when ``app2.py`` imports the same file as ``app``. - -What does this have to do with our comparison to microframeworks? -Many microframeworks in the current crop (e.g. Bottle, Flask) -encourage you to attach configuration decorators to objects defined at -module scope. These decorators execute arbitrarily complex -registration code which populates a singleton registry that is a -global defined in external Python module. This is analogous to the -above example: the "global registry" in the above example is the list -``L``. - -Let's see what happens when we use the same pattern with the -`Groundhog `_ microframework. -Replace the contents of ``app.py`` above with this: +By visual inspection, that outcome (three different functions in the list) +seems impossible. We only defined two functions and we decorated each of +those functions only once, so we believe that the ``decorator`` decorator +will only run twice. However, what we believe is wrong because the code at +module scope in our ``app.py`` module was *executed twice*. The code is +executed once when the script is run as ``__main__`` (via ``python app.py``), +and then it is executed again when ``app2.py`` imports the same file as +``app``. + +What does this have to do with our comparison to microframeworks? Many +microframeworks in the current crop (e.g. Bottle, Flask) encourage you to +attach configuration decorators to objects defined at module scope. These +decorators execute arbitrarily complex registration code which populates a +singleton registry that is a global defined in external Python module. This +is analogous to the above example: the "global registry" in the above example +is the list ``L``. + +Let's see what happens when we use the same pattern with the `Groundhog +`_ microframework. Replace the +contents of ``app.py`` above with this: .. code-block:: python :linenos: @@ -1317,91 +1129,87 @@ Replace the contents of ``config.py`` above with this: from groundhog import Groundhog gh = Groundhog('myapp', 'seekrit') -How many routes will be registered within the routing table of the -"gh" Groundhog application? If you answered three, you are correct. -How many would a casual reader (and any sane developer) expect to be -registered? If you answered two, you are correct. Will the double -registration be a problem? With our fictional Groundhog framework's -``route`` method backing this application, not really. It will slow -the application down a little bit, because it will need to miss twice -for a route when it does not match. Will it be a problem with another -framework, another application, or another decorator? Who knows. You -need to understand the application in its totality, the framework in -its totality, and the chronology of execution to be able to predict -what the impact of unintentional code double-execution will be. - -The encouragement to use decorators which perform population of an -external registry has an unintended consequence: the application -developer now must assert ownership of every codepath that executes -Python module scope code. This code is presumed by the current crop of -decorator-based microframeworks to execute once and only once; if it -executes more than once, weird things will start to happen. It is up -to the application developer to maintain this invariant. -Unfortunately, however, in reality, this is an impossible task, -because, Python programmers *do not own the module scope codepath, and -never will*. Microframework programmers therefore will at some point -then need to start reading the tea leaves about what *might* happen if -module scope code gets executed more than once like we do in the -previous paragraph. This is a really pretty poor situation to find -yourself in as an application developer: you probably didn't even know -you signed up for the job, because the documentation offered by -decorator-based microframeworks don't warn you about it. - -Python application programmers do not control the module scope codepath. -Anyone who tries to sell you on the idea that they do is simply mistaken. -Test runners that you may want to use to run your code's tests often perform -imports of arbitrary code in strange orders that manifest bugs like the one -demonstrated above. API documentation generation tools do the same. Some -(mutant) people even think it's safe to use the Python ``reload`` command or -delete objects from ``sys.modules``, each of which has hilarious effects when -used against code that has import-time side effects. When Python programmers -assume they can use the module-scope codepath to run arbitrary code -(especially code which populates an external registry), and this assumption -is challenged by reality, the application developer is often required to -undergo a painful, meticulous debugging process to find the root cause of an -inevitably obscure symptom. The solution is often to rearrange application -import ordering or move an import statement from module-scope into a function -body. The rationale for doing so can never be expressed adequnately in the -checkin message which accompanies the fix or documented succinctly enough for -the benefit of the rest of the development team so that the problem never -happens again. It will happen again next month too, especially if you are -working on a project with other people who haven't yet internalized the -lessons you learned while you stepped through module-scope code using -``pdb``. - -Folks who have a large investment in eager decorator-based -configuration that populates an external data structure (such as -microframework authors) may argue that the set of circumstances I -outlined above is anomalous and contrived. They will argue that it -just will never happen. If you never intend your application to grow -beyond one or two or three modules, that's probably true. However, as -your codebase grows, and becomes spread across a greater number of -modules, the circumstances in which module-scope code will be executed -multiple times will become more and more likely to occur and less and -less predictable. It's not responsible to claim that double-execution -of module-scope code will never happen. It will; it's just a matter -of luck, time, and application complexity. - -If microframework authors do admit that the circumstance isn't -contrived, they might then argue that "real" damage will never happen -as the result of the double-execution (or triple-execution, etc) of -module scope code. You would be wise to disbelieve this assertion. -The potential outcomes of multiple execution are too numerous to -predict because they involve delicate relationships between -application and framework code as well as chronology of code -execution. It's literally impossible for a framework author to know -what will happen in all circumstances ("X is executed, then Y, then X -again.. a train leaves Chicago at 50 mph... "). And even if given the -gift of omniscience for some limited set of circumstances, the -framework author almost certainly does not have the double-execution -anomaly in mind when coding new features. He's thinking of adding a -feature, not protecting against problems that might be caused by the -1% multiple execution case. However, any 1% case may cause 50% of -your pain on a project, so it'd be nice if it never occured. +How many routes will be registered within the routing table of the "gh" +Groundhog application? If you answered three, you are correct. How many +would a casual reader (and any sane developer) expect to be registered? If +you answered two, you are correct. Will the double registration be a +problem? With our fictional Groundhog framework's ``route`` method backing +this application, not really. It will slow the application down a little +bit, because it will need to miss twice for a route when it does not match. +Will it be a problem with another framework, another application, or another +decorator? Who knows. You need to understand the application in its +totality, the framework in its totality, and the chronology of execution to +be able to predict what the impact of unintentional code double-execution +will be. + +The encouragement to use decorators which perform population of an external +registry has an unintended consequence: the application developer now must +assert ownership of every codepath that executes Python module scope +code. Module-scope code is presumed by the current crop of decorator-based +microframeworks to execute once and only once; if it executes more than once, +weird things will start to happen. It is up to the application developer to +maintain this invariant. Unfortunately, however, in reality, this is an +impossible task, because, Python programmers *do not own the module scope +codepath, and never will*. Anyone who tries to sell you on the idea that +they do is simply mistaken. Test runners that you may want to use to run +your code's tests often perform imports of arbitrary code in strange orders +that manifest bugs like the one demonstrated above. API documentation +generation tools do the same. Some (mutant) people even think it's safe to +use the Python ``reload`` command or delete objects from ``sys.modules``, +each of which has hilarious effects when used against code that has +import-time side effects. + +Global-registry-mutating microframework programmers therefore will at some +point need to start reading the tea leaves about what *might* happen if +module scope code gets executed more than once like we do in the previous +paragraph. When Python programmers assume they can use the module-scope +codepath to run arbitrary code (especially code which populates an external +registry), and this assumption is challenged by reality, the application +developer is often required to undergo a painful, meticulous debugging +process to find the root cause of an inevitably obscure symptom. The +solution is often to rearrange application import ordering or move an import +statement from module-scope into a function body. The rationale for doing so +can never be expressed adequately in the checkin message which accompanies +the fix and can't be documented succinctly enough for the benefit of the rest +of the development team so that the problem never happens again. It will +happen again, especially if you are working on a project with other people +who haven't yet internalized the lessons you learned while you stepped +through module-scope code using ``pdb``. This is a really pretty poor +situation to find yourself in as an application developer: you probably +didn't even know your or your team signed up for the job, because the +documentation offered by decorator-based microframeworks don't warn you about +it. + +Folks who have a large investment in eager decorator-based configuration that +populates an external data structure (such as microframework authors) may +argue that the set of circumstances I outlined above is anomalous and +contrived. They will argue that it just will never happen. If you never +intend your application to grow beyond one or two or three modules, that's +probably true. However, as your codebase grows, and becomes spread across a +greater number of modules, the circumstances in which module-scope code will +be executed multiple times will become more and more likely to occur and less +and less predictable. It's not responsible to claim that double-execution of +module-scope code will never happen. It will; it's just a matter of luck, +time, and application complexity. + +If microframework authors do admit that the circumstance isn't contrived, +they might then argue that "real" damage will never happen as the result of +the double-execution (or triple-execution, etc) of module scope code. You +would be wise to disbelieve this assertion. The potential outcomes of +multiple execution are too numerous to predict because they involve delicate +relationships between application and framework code as well as chronology of +code execution. It's literally impossible for a framework author to know +what will happen in all circumstances. But even if given the gift of +omniscience for some limited set of circumstances, the framework author +almost certainly does not have the double-execution anomaly in mind when +coding new features. He's thinking of adding a feature, not protecting +against problems that might be caused by the 1% multiple execution case. +However, any 1% case may cause 50% of your pain on a project, so it'd be nice +if it never occured. Responsible microframeworks actually offer a back-door way around the -problem. They allow you to disuse decorator based configuration -entirely. Instead of requiring you to do the following: +problem. They allow you to disuse decorator based configuration entirely. +Instead of requiring you to do the following: .. code-block:: python :linenos: @@ -1415,8 +1223,7 @@ entirely. Instead of requiring you to do the following: if __name__ == '__main__': gh.run() -They allow you to disuse the decorator syntax and go -almost-all-imperative: +They allow you to disuse the decorator syntax and go almost-all-imperative: .. code-block:: python :linenos: @@ -1431,28 +1238,27 @@ almost-all-imperative: gh.run() This is a generic mode of operation that is encouraged in the Pyramid -documentation. Some existing microframeworks (Flask, in particular) -allow for it as well. None (other than Pyramid) *encourage* it. If -you never expect your application to grow beyond two or three or four -or ten modules, it probably doesn't matter very much which mode you -use. If your application grows large, however, imperative -configuration can provide better predictability. +documentation. Some existing microframeworks (Flask, in particular) allow for +it as well. None (other than Pyramid) *encourage* it. If you never expect +your application to grow beyond two or three or four or ten modules, it +probably doesn't matter very much which mode you use. If your application +grows large, however, imperative configuration can provide better +predictability. .. note:: - Astute readers may notice that Pyramid has configuration decorators - too. Aha! Don't these decorators have the same problems? No. - These decorators do not populate an external Python module when they - are executed. They only mutate the functions (and classes and - methods) they're attached to. These mutations must later be found - during a "scan" process that has a predictable and structured import - phase. Module-localized mutation is actually the best-case - circumstance for double-imports; if a module only mutates itself and - its contents at import time, if it is imported twice, that's OK, - because each decorator invocation will always be mutating an - independent copy of the object its attached to, not a shared - resource like a registry in another module. This has the effect - that double-registrations will never be performed. + Astute readers may notice that Pyramid has configuration decorators too. + Aha! Don't these decorators have the same problems? No. These decorators + do not populate an external Python module when they are executed. They + only mutate the functions (and classes and methods) they're attached to. + These mutations must later be found during a "scan" process that has a + predictable and structured import phase. Module-localized mutation is + actually the best-case circumstance for double-imports; if a module only + mutates itself and its contents at import time, if it is imported twice, + that's OK, because each decorator invocation will always be mutating an + independent copy of the object its attached to, not a shared resource like + a registry in another module. This has the effect that + double-registrations will never be performed. Routes (Usually) Need Relative Ordering +++++++++++++++++++++++++++++++++++++++ @@ -1482,8 +1288,8 @@ Consider the following simple `Groundhog app.run() If you run this application and visit the URL ``/admin``, you will see -"admin" page. This is the intended result. However, what if you -rearrange the order of the function definitions in the file? +"admin" page. This is the intended result. However, what if you rearrange +the order of the function definitions in the file? .. code-block:: python :linenos: @@ -1506,54 +1312,50 @@ rearrange the order of the function definitions in the file? if __name__ == '__main__': app.run() -If you run this application and visit the URL ``/admin``, you will now -be returned a 404 error. This is probably not what you intended. The -reason you see a 404 error when you rearrange function definition -ordering is that routing declarations expressed via our -microframework's routing decorators have an *ordering*, and that -ordering matters. - -In the first case, where we achieved the expected result, we first -added a route with the pattern ``/admin``, then we added a route with -the pattern ``/:action`` by virtue of adding routing patterns via -decorators at module scope. When a request with a ``PATH_INFO`` of -``/admin`` enters our application, the web framework loops over each -of our application's route patterns in the order in which they were -defined in our module. As a result, the view associated with the -``/admin`` routing pattern will be invoked: it matches first. All is -right with the world. - -In the second case, where we did not achieve the expected result, we -first added a route with the pattern ``/:action``, then we added a -route with the pattern ``/admin``. When a request with a -``PATH_INFO`` of ``/admin`` enters our application, the web framework -loops over each of our application's route patterns in the order in -which they were defined in our module. As a result, the view -associated with the ``/:action`` routing pattern will be invoked: it -matches first. A 404 error is raised. This is not what we wanted; it -just happened due to the order in which we defined our view functions. - -You may be willing to maintain an ordering of your view functions -which reifies your routing policy. Your application may be small -enough where this will never cause an issue. If it becomes large -enough to matter, however, I don't envy you. Maintaining that -ordering as your application grows larger will be difficult. At some -point, you will also need to start controlling *import* ordering as -well as function definition ordering. When your application grows -beyond the size of a single file, and when decorators are used to -register views, the non-``__main__`` modules which contain -configuration decorators must be imported somehow for their -configuration to be executed. - -Does that make you a little -uncomfortable? It should, because :ref:`you_dont_own_modulescope`. +If you run this application and visit the URL ``/admin``, you will now be +returned a 404 error. This is probably not what you intended. The reason +you see a 404 error when you rearrange function definition ordering is that +routing declarations expressed via our microframework's routing decorators +have an *ordering*, and that ordering matters. + +In the first case, where we achieved the expected result, we first added a +route with the pattern ``/admin``, then we added a route with the pattern +``/:action`` by virtue of adding routing patterns via decorators at module +scope. When a request with a ``PATH_INFO`` of ``/admin`` enters our +application, the web framework loops over each of our application's route +patterns in the order in which they were defined in our module. As a result, +the view associated with the ``/admin`` routing pattern will be invoked: it +matches first. All is right with the world. + +In the second case, where we did not achieve the expected result, we first +added a route with the pattern ``/:action``, then we added a route with the +pattern ``/admin``. When a request with a ``PATH_INFO`` of ``/admin`` enters +our application, the web framework loops over each of our application's route +patterns in the order in which they were defined in our module. As a result, +the view associated with the ``/:action`` routing pattern will be invoked: it +matches first. A 404 error is raised. This is not what we wanted; it just +happened due to the order in which we defined our view functions. + +You may be willing to maintain an ordering of your view functions which +reifies your routing policy. Your application may be small enough where this +will never cause an issue. If it becomes large enough to matter, however, I +don't envy you. Maintaining that ordering as your application grows larger +will be difficult. At some point, you will also need to start controlling +*import* ordering as well as function definition ordering. When your +application grows beyond the size of a single file, and when decorators are +used to register views, the non-``__main__`` modules which contain +configuration decorators must be imported somehow for their configuration to +be executed. + +Does that make you a little uncomfortable? It should, because +:ref:`you_dont_own_modulescope`. "Stacked Object Proxies" Are Too Clever / Thread Locals Are A Nuisance ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -In another manifestation of "import fascination", some microframeworks -use the ``import`` statement to get a handle to an object which *is -not logically global*: +In another manifestation of "import fascination", some microframeworks use +the ``import`` statement to get a handle to an object which *is not logically +global*: .. code-block:: python :linenos: @@ -1573,17 +1375,16 @@ not logically global*: # credentials were invalid The `Pylons 1.X `_ web framework uses a similar -strategy. It calls these things "Stacked Object Proxies", so, for -purposes of this discussion, I'll do so as well. - -Import statements in Python (``import foo``, ``from bar import baz``) -are most frequently performed to obtain a reference to an object -defined globally within an external Python module. However, in -"normal" programs, they are never used to obtain a reference to an -object that has a lifetime measured by the scope of the body of a -function. It would be absurd to try to import, for example, a -variable named ``i`` representing a loop counter defined in the body -of a function. For example, we'd never try to import ``i`` from the +strategy. It calls these things "Stacked Object Proxies", so, for purposes +of this discussion, I'll do so as well. + +Import statements in Python (``import foo``, ``from bar import baz``) are +most frequently performed to obtain a reference to an object defined globally +within an external Python module. However, in "normal" programs, they are +never used to obtain a reference to an object that has a lifetime measured by +the scope of the body of a function. It would be absurd to try to import, +for example, a variable named ``i`` representing a loop counter defined in +the body of a function. For example, we'd never try to import ``i`` from the code below: .. code-block:: python @@ -1593,80 +1394,74 @@ code below: for i in range(10): print i -By its nature, the *request* object created as the result of a WSGI -server's call into a long-lived web framework cannot be global, -because the lifetime of a single request will be much shorter than the -lifetime of the process running the framework. A request object -created by a web framework actually has more similarity to the ``i`` -loop counter in our example above than it has to any comparable -importable object defined in the Python standard library or in -"normal" library code. - -However, systems which use stacked object proxies promote locally -scoped objects such as ``request`` out to module scope, for the -purpose of being able to offer users a "nice" spelling involving -``import``. They, for what I consider dubious reasons, would rather -present to their users the canonical way of getting at a ``request`` -as ``from framework import request`` instead of a saner ``from -myframework.threadlocals import get_request; request = get_request()`` -even though the latter is more explicit. - -It would be *most* explicit if the microframeworks did not use thread -local variables at all. Pyramid view functions are passed a request -object; many of Pyramid's APIs require that an explicit request object -be passed to them. It is *possible* to retrieve the current Pyramid -request as a threadlocal variable but it is a "in case of emergency, -break glass" type of activity. This explicitness makes Pyramid view -functions more easily unit testable, as you don't need to rely on the -framework to manufacture suitable "dummy" request (and other -similarly-scoped) objects during test setup. It also makes them more -likely to work on arbitrary systems, such as async servers that do no -monkeypatching. +By its nature, the *request* object created as the result of a WSGI server's +call into a long-lived web framework cannot be global, because the lifetime +of a single request will be much shorter than the lifetime of the process +running the framework. A request object created by a web framework actually +has more similarity to the ``i`` loop counter in our example above than it +has to any comparable importable object defined in the Python standard +library or in "normal" library code. + +However, systems which use stacked object proxies promote locally scoped +objects such as ``request`` out to module scope, for the purpose of being +able to offer users a "nice" spelling involving ``import``. They, for what I +consider dubious reasons, would rather present to their users the canonical +way of getting at a ``request`` as ``from framework import request`` instead +of a saner ``from myframework.threadlocals import get_request; request = +get_request()`` even though the latter is more explicit. + +It would be *most* explicit if the microframeworks did not use thread local +variables at all. Pyramid view functions are passed a request object; many +of Pyramid's APIs require that an explicit request object be passed to them. +It is *possible* to retrieve the current Pyramid request as a threadlocal +variable but it is a "in case of emergency, break glass" type of activity. +This explicitness makes Pyramid view functions more easily unit testable, as +you don't need to rely on the framework to manufacture suitable "dummy" +request (and other similarly-scoped) objects during test setup. It also +makes them more likely to work on arbitrary systems, such as async servers +that do no monkeypatching. Explicitly WSGI +++++++++++++++ -Some microframeworks offer a ``run()`` method of an application object -that executes a default server configuration for easy execution. - -Pyramid doesn't currently try to hide the fact that its router is a -WSGI application behind a convenience ``run()`` API. It just tells -people to import a WSGI server and use it to serve up their Pyramid -application as per the documentation of that WSGI server. - -The extra lines saved by abstracting away the serving step behind -``run()`` seem to have driven dubious second-order decisions related -to API in some microframeworks. For example, Bottle contains a -``ServerAdapter`` subclass for each type of WSGI server it supports -via its ``app.run()`` mechanism. This means that there exists code in -``bottle.py`` that depends on the following modules: ``wsgiref``, -``flup``, ``paste``, ``cherrypy``, ``fapws``, ``tornado``, -``google.appengine``, ``twisted.web``, ``diesel``, ``gevent``, -``gunicorn``, ``eventlet``, and ``rocket``. You choose the kind of -server you want to run by passing its name into the ``run`` method. -In theory, this sounds great: I can try Bottle out on ``gunicorn`` -just by passing in a name! However, to fully test Bottle, all of -these third-party systems must be installed and functional; the Bottle -developers must monitor changes to each of these packages and make -sure their code still interfaces properly with them. This expands the -packages required for testing greatly; this is a *lot* of -requirements. It is likely difficult to fully automate these tests +Some microframeworks offer a ``run()`` method of an application object that +executes a default server configuration for easy execution. + +Pyramid doesn't currently try to hide the fact that its router is a WSGI +application behind a convenience ``run()`` API. It just tells people to +import a WSGI server and use it to serve up their Pyramid application as per +the documentation of that WSGI server. + +The extra lines saved by abstracting away the serving step behind ``run()`` +seem to have driven dubious second-order decisions related to API in some +microframeworks. For example, Bottle contains a ``ServerAdapter`` subclass +for each type of WSGI server it supports via its ``app.run()`` mechanism. +This means that there exists code in ``bottle.py`` that depends on the +following modules: ``wsgiref``, ``flup``, ``paste``, ``cherrypy``, ``fapws``, +``tornado``, ``google.appengine``, ``twisted.web``, ``diesel``, ``gevent``, +``gunicorn``, ``eventlet``, and ``rocket``. You choose the kind of server +you want to run by passing its name into the ``run`` method. In theory, this +sounds great: I can try Bottle out on ``gunicorn`` just by passing in a name! +However, to fully test Bottle, all of these third-party systems must be +installed and functional; the Bottle developers must monitor changes to each +of these packages and make sure their code still interfaces properly with +them. This expands the packages required for testing greatly; this is a +*lot* of requirements. It is likely difficult to fully automate these tests due to requirements conflicts and build issues. -As a result, for single-file apps, we currently don't bother to offer -a ``run()`` shortcut; we tell folks to import their WSGI server of -choice and run it "by hand". For the people who want a server -abstraction layer, we suggest that they use PasteDeploy. In -PasteDeploy-based systems, the onus for making sure that the server -can interface with a WSGI application is placed on the server -developer, not the web framework developer, making it more likely to -be timely and correct. +As a result, for single-file apps, we currently don't bother to offer a +``run()`` shortcut; we tell folks to import their WSGI server of choice and +run it "by hand". For the people who want a server abstraction layer, we +suggest that they use PasteDeploy. In PasteDeploy-based systems, the onus +for making sure that the server can interface with a WSGI application is +placed on the server developer, not the web framework developer, making it +more likely to be timely and correct. Wrapping Up +++++++++++ -Here's a diagrammed version of the simplest pyramid application, -where comments take into account what we've discussed in the +Here's a diagrammed version of the simplest pyramid application, where +comments take into account what we've discussed in the :ref:`microframeworks_smaller_hello_world` section. .. code-block:: python @@ -1686,6 +1481,91 @@ where comments take into account what we've discussed in the app = config.make_wsgi_app() # explicitly WSGI serve(app, host='0.0.0.0') # explicitly WSGI +Pyramid Has Zope Things In It, So It's Too Complex +-------------------------------------------------- + +On occasion, someone will feel compelled to post a mailing list message that +reads something like this: + +.. code-block:: text + + had a quick look at pyramid ... too complex to me and not really + understand for which benefits.. I feel should consider whether it's time + for me to step back to django .. I always hated zope (useless ?) + complexity and I love simple way of thinking + +(Paraphrased from a real email, actually.) + +Let's take this criticism point-by point. + +Too Complex ++++++++++++ + +- If you can understand this hello world program, you can use Pyramid: + +.. code-block:: python + :linenos: + + from paste.httpserver import serve + from pyramid.config import Configurator + from pyramid.response import Response + + def hello_world(request): + return Response('Hello world!') + + if __name__ == '__main__': + config = Configurator() + config.add_view(hello_world) + app = config.make_wsgi_app() + serve(app) + +Pyramid has ~ 650 of documentation (printed), covering topics from the very +basic to the most advanced. *Nothing* is left undocumented, quite literally. +It also has an *awesome*, very helpful community. Visit the #repoze and/or +#pylons IRC channels on freenode.net and see. + +Hate Zope ++++++++++ + +I'm sorry you feel that way. The Zope brand has certainly taken its share of +lumps over the years, and has a reputation for being insular and mysterious. +But the word "Zope" is literally quite meaningless without qualification. +What *part* of Zope do you hate? "Zope" is a brand, not a technology. + +If it's Zope2-the-web-framework, Pyramid is not that. The primary designers +and developers of Pyramid, if anyone, should know. We wrote Pyramid's +predecessor (:mod:`repoze.bfg`), in part, because *we* knew that Zope 2 had +usability issues and limitations. :mod:`repoze.bfg` (and now :app:`Pyramid`) +was written to address these issues. + +If it's Zope3-the-web-framework, Pyramid is *definitely* not that. Making +use of lots of Zope 3 technologies is territory already staked out by the +:term:`Grok` project. Save for the obvious fact that they're both web +frameworks, :mod:`Pyramid` is very, very different than Grok. Grok exposes +lots of Zope technologies to end users. On the other hand, if you need to +understand a Zope-only concept while using Pyramid, then we've failed on some +very basic axis. + +If it's just the word Zope: this can only be guilt by association. Because a +piece of software internally uses some package named ``zope.foo``, it doesn't +turn the piece of software that uses it into "Zope". There is a lot of +*great* software written that has the word Zope in its name. Zope is not +some sort of monolithic thing, and a lot of its software is usable +externally. And while it's not really the job of this document to defend it, +Zope has been around for over 10 years and has an incredibly large, active +community. If you don't believe this, +http://taichino.appspot.com/pypi_ranking/authors is an eye-opening reality +check. + +Love Simplicity ++++++++++++++++ + +Years of effort have gone into honing this package and its documentation to +make it as simple as humanly possible for developers to use. Everything is a +tradeoff, of course, and people have their own ideas about what "simple" is. +You may have a style difference if you believe Pyramid is complex. Its +developers obviously disagree. + Other Challenges ---------------- -- cgit v1.2.3 From 56f24889b4614cfefff7f419920e2a2cf03f5acf Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 24 Dec 2010 19:40:55 -0500 Subject: reflow --- docs/designdefense.rst | 38 ++++++++++++++++++-------------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/docs/designdefense.rst b/docs/designdefense.rst index 2761d181f..263420635 100644 --- a/docs/designdefense.rst +++ b/docs/designdefense.rst @@ -401,8 +401,8 @@ Pyramid Uses ZCML; ZCML is XML and I Don't Like XML :term:`ZCML` is a configuration language in the XML syntax. Due to the "imperative configuration" feature (new in :mod:`repoze.bfg` 1.2), you don't need to use ZCML at all. But if you really do want to perform declarative -configuration, perhaps because you want to build an extensible application, you -will need to use and understand it. +configuration, perhaps because you want to build an extensible application, +you may need to use and understand it. :term:`ZCML` contains elements that are mostly singleton tags that are called *declarations*. For an example: @@ -418,24 +418,22 @@ called *declarations*. For an example: This declaration associates a :term:`view` with a route pattern. -All :app:`Pyramid` declarations are singleton tags, unlike many -other XML configuration systems. No XML *values* in ZCML are -meaningful; it's always just XML tags and attributes. So in the very -common case it's not really very much different than an otherwise -"flat" configuration format like ``.ini``, except a developer can -*create* a directive that requires nesting (none of these exist in -:app:`Pyramid` itself), and multiple "sections" can exist with the -same "name" (e.g. two ```` declarations) must be able to exist -simultaneously. - -You might think some other configuration file format would be better. -But all configuration formats suck in one way or another. I -personally don't think any of our lives would be markedly better if -the declarative configuration format used by :app:`Pyramid` were -YAML, JSON, or INI. It's all just plumbing that you mostly cut and -paste once you've progressed 30 minutes into your first project. -Folks who tend to agitate for another configuration file format are -folks that haven't yet spent that 30 minutes. +All :app:`Pyramid` declarations are singleton tags, unlike many other XML +configuration systems. No XML *values* in ZCML are meaningful; it's always +just XML tags and attributes. So in the very common case it's not really +very much different than an otherwise "flat" configuration format like +``.ini``, except a developer can *create* a directive that requires nesting +(none of these exist in :app:`Pyramid` itself), and multiple "sections" can +exist with the same "name" (e.g. two ```` declarations) must be able +to exist simultaneously. + +You might think some other configuration file format would be better. But +all configuration formats suck in one way or another. I personally don't +think any of our lives would be markedly better if the declarative +configuration format used by :app:`Pyramid` were YAML, JSON, or INI. It's +all just plumbing that you mostly cut and paste once you've progressed 30 +minutes into your first project. Folks who tend to agitate for another +configuration file format are folks that haven't yet spent that 30 minutes. .. _model_traversal_confusion: -- cgit v1.2.3 From 03fc303d9672b850fe66d20d5dceb62e228c3bce Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 24 Dec 2010 19:42:19 -0500 Subject: typo --- docs/designdefense.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/designdefense.rst b/docs/designdefense.rst index 263420635..1e22ae8d8 100644 --- a/docs/designdefense.rst +++ b/docs/designdefense.rst @@ -1499,7 +1499,7 @@ Let's take this criticism point-by point. Too Complex +++++++++++ -- If you can understand this hello world program, you can use Pyramid: +If you can understand this hello world program, you can use Pyramid: .. code-block:: python :linenos: -- cgit v1.2.3 From 6cdbba7c69e0794944f3f3afcd2686b0971a07d2 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 24 Dec 2010 20:35:19 -0500 Subject: add Pyramid Provides More Than One Way to Do It section --- docs/designdefense.rst | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/docs/designdefense.rst b/docs/designdefense.rst index 1e22ae8d8..78bbc7bd0 100644 --- a/docs/designdefense.rst +++ b/docs/designdefense.rst @@ -10,6 +10,86 @@ framework can be made better and we describe future steps which will be taken to improve it; in some cases we just file the challenge as "noted", as obviously you can't please everyone all of the time. +Pyramid Provides More Than One Way to Do It +------------------------------------------- + +A canon of Python popular culture is "TIOOTDI" ("there is only one way to do +it", a slighting, tongue-in-cheek reference to Perl's "TIMTOWTDI", which is +an acronym for "there is more than one way to do it"). + +:app:`Pyramid` is, for better or worse, a "TIMTOWTDI" system. For example, +it includes more than one way to resolve a URL to a :term:`view callable`: +via :term:`url dispatch` or :term:`traversal`. Multiple methods of +configuration exist: :term:`imperative configuration`, :term:`configuration +decoration`, and :term:`ZCML`. A view callable can itself be either a +function, an instance, or a class. There are multiple ways to serve static +files. It works with multiple different kinds of persistence and templating +systems. And so on. However, the existence of most of these overlapping +ways to do things are not without reason and purpose: we have a number of +audiences to serve, and we believe that TIMTOWTI at the web framework level +actually *prevents* a much more insidious and harmful set of duplication at +higher levels in the Python web community. + +:app:`Pyramid` began its life as :mod:`repoze.bfg`, written by a team of +people with many years of prior :ref:`Zope` experience. The idea of +:term:`traversal`, the usage of :term:`ZCML` and the way :term:`view lookup` +works was stolen entirely from Zope. The authorization subsystem provided by +:app:`Pyramid` is a derivative of Zope's. The idea that an application can +be *extended* without forking is also a Zope derivative. + +Implementations of these features were *required* to allow the :app:`Pyramid` +authors to build the bread-and-butter CMS-type systems for customers in the +way they were accustomed to building them. No other system save Zope itself +had such features. And Zope itself was beginning to show signs of its age, +and we were becoming hampered by consequences of early design mistakes. +Zope's lack of documentation was also difficult to work around: it was hard +to hire otherwise-smart people to work on Zope applications, because there +was no comprehensive documentation set to point them at which explained "it +all" in one consumble place. Before :mod:`repoze.bfg` went under +development, its authors obviously looked around for other frameworks that +fit the bill. But no non-Zope framework did. So we embarked on building +:mod:`repoze.bfg`. + +As the result of our research, however, it became apparent that, despite the +fact that no *one* framework had all the features we required, lots of +existing frameworks had good, and sometimes very compelling ideas. In +particular, :term:`URL dispatch` is a more direct mechanism to map URLs to +code. + +So although we couldn't find a framework save for Zope that fit our needs, +and while we incorporated a lot of Zope ideas into BFG, we also emulated the +features we found compelling in other frameworks (such as :term:`url +dispatch`). After the initial public release of BFG, as time went on, +features were added to support people allergic to various Zope-isms in the +system, such as the ability to configure the application using +:term:`imperative configuration` rather than solely using :term:`ZCML`, and +the elimination of the required use of :term:`interface` objects. It soon +became clear that we had a system that was very generic, and was beginning to +appeal to non-Zope users as well as ex-Zope users. + +As the result of this generalization, it became obvious BFG shared 90% of its +featureset with the featureset of Pylons 1, and had a very similar target +market. Because they were so similar, choosing between the two systems was +an exercise in frustration for an otherwise non-partisan developer. It was +also painful and felt wrong for Pylons and BFG development communities to +compete for users. So the Pylons and BFG teams began to work together to +form a plan to "merge". The features missing from BFG (notably :term:`view +handler` classes, flash messaging, and other minor missing bits), were added, +to provide familiarity to ex-Pylons users. The result is :app:`Pyramid`. + +We're truly hoping that the amalgamation of components in :app:`Pyramid` will +appeal to at least two currently very distinct sets of users: Pylons and BFG +(usually ex-Zope) users. By unifying the best concepts from Pylons and BFG +into a single codebase (and leaving the bad concepts from their ancestors +behind), we'll be able to consolidate our efforts better, share more code, +and promote our efforts as a unit rather than competing. We'll be able to +shortcut pointless pack mentality which results in a *much larger* +duplication of effort, represented by competing but incredibly similar +packages, each built upon a specific low level stack. We'll also shrink the +choice of credible Python web frameworks down by at least one. Some overlap +of functionality to achieve these goals is expected and unavoidable, at least +if we aim to prevent pointless duplication at higher levels. + Pyramid Uses A Zope Component Architecture ("ZCA") Registry ----------------------------------------------------------- -- cgit v1.2.3 From adf43dfd6093568fa938ed735f1db71ee22b92d8 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 24 Dec 2010 20:37:09 -0500 Subject: wording --- docs/designdefense.rst | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/docs/designdefense.rst b/docs/designdefense.rst index 78bbc7bd0..f729a3034 100644 --- a/docs/designdefense.rst +++ b/docs/designdefense.rst @@ -79,16 +79,17 @@ to provide familiarity to ex-Pylons users. The result is :app:`Pyramid`. We're truly hoping that the amalgamation of components in :app:`Pyramid` will appeal to at least two currently very distinct sets of users: Pylons and BFG -(usually ex-Zope) users. By unifying the best concepts from Pylons and BFG -into a single codebase (and leaving the bad concepts from their ancestors -behind), we'll be able to consolidate our efforts better, share more code, -and promote our efforts as a unit rather than competing. We'll be able to -shortcut pointless pack mentality which results in a *much larger* +(often ex-Zope) users. By unifying the best concepts from Pylons and BFG +into a single codebase and leaving the bad concepts from their ancestors +behind, we'll be able to consolidate our efforts better, share more code, and +promote our efforts as a unit rather than competing pointlessly. We hope to +be able to shortcut the pack mentality which results in a *much larger* duplication of effort, represented by competing but incredibly similar -packages, each built upon a specific low level stack. We'll also shrink the -choice of credible Python web frameworks down by at least one. Some overlap -of functionality to achieve these goals is expected and unavoidable, at least -if we aim to prevent pointless duplication at higher levels. +applications and libraries, each built upon a specific low level stack that +is incompatible with any other. We'll also shrink the choice of credible +Python web frameworks down by at least one. Some overlap of functionality to +achieve these goals is expected and unavoidable, at least if we aim to +prevent pointless duplication at higher levels. Pyramid Uses A Zope Component Architecture ("ZCA") Registry ----------------------------------------------------------- -- cgit v1.2.3 From 5c645f6a21a7fa11014d2c29854c0de5df36abea Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 24 Dec 2010 20:38:05 -0500 Subject: wording --- docs/designdefense.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/designdefense.rst b/docs/designdefense.rst index f729a3034..f3a29b2d1 100644 --- a/docs/designdefense.rst +++ b/docs/designdefense.rst @@ -89,7 +89,9 @@ applications and libraries, each built upon a specific low level stack that is incompatible with any other. We'll also shrink the choice of credible Python web frameworks down by at least one. Some overlap of functionality to achieve these goals is expected and unavoidable, at least if we aim to -prevent pointless duplication at higher levels. +prevent pointless duplication at higher levels. If we've done our job well +enough, the various audiences will be able to coexist and cooperate rather +than firing at each other across some imaginary web framework "DMZ". Pyramid Uses A Zope Component Architecture ("ZCA") Registry ----------------------------------------------------------- -- cgit v1.2.3 From 0bdbdfe36bcf920617953dafba198418cdcaa2fa Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 24 Dec 2010 20:42:57 -0500 Subject: wording --- docs/designdefense.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/designdefense.rst b/docs/designdefense.rst index f3a29b2d1..24fa606f8 100644 --- a/docs/designdefense.rst +++ b/docs/designdefense.rst @@ -77,7 +77,8 @@ form a plan to "merge". The features missing from BFG (notably :term:`view handler` classes, flash messaging, and other minor missing bits), were added, to provide familiarity to ex-Pylons users. The result is :app:`Pyramid`. -We're truly hoping that the amalgamation of components in :app:`Pyramid` will +The Python web framework space is currently notoriously balkanized. We're +truly hoping that the amalgamation of components in :app:`Pyramid` will appeal to at least two currently very distinct sets of users: Pylons and BFG (often ex-Zope) users. By unifying the best concepts from Pylons and BFG into a single codebase and leaving the bad concepts from their ancestors @@ -86,7 +87,7 @@ promote our efforts as a unit rather than competing pointlessly. We hope to be able to shortcut the pack mentality which results in a *much larger* duplication of effort, represented by competing but incredibly similar applications and libraries, each built upon a specific low level stack that -is incompatible with any other. We'll also shrink the choice of credible +is incompatible with the other. We'll also shrink the choice of credible Python web frameworks down by at least one. Some overlap of functionality to achieve these goals is expected and unavoidable, at least if we aim to prevent pointless duplication at higher levels. If we've done our job well -- cgit v1.2.3 From b36921fa658dd15db788adc6b03b10dd6d035dd3 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 24 Dec 2010 20:46:49 -0500 Subject: wording --- docs/designdefense.rst | 76 ++++++++++++++++++++++++++------------------------ 1 file changed, 39 insertions(+), 37 deletions(-) diff --git a/docs/designdefense.rst b/docs/designdefense.rst index 24fa606f8..86ff2d993 100644 --- a/docs/designdefense.rst +++ b/docs/designdefense.rst @@ -22,13 +22,12 @@ it includes more than one way to resolve a URL to a :term:`view callable`: via :term:`url dispatch` or :term:`traversal`. Multiple methods of configuration exist: :term:`imperative configuration`, :term:`configuration decoration`, and :term:`ZCML`. A view callable can itself be either a -function, an instance, or a class. There are multiple ways to serve static -files. It works with multiple different kinds of persistence and templating -systems. And so on. However, the existence of most of these overlapping -ways to do things are not without reason and purpose: we have a number of -audiences to serve, and we believe that TIMTOWTI at the web framework level -actually *prevents* a much more insidious and harmful set of duplication at -higher levels in the Python web community. +function, an instance, or a class. It works with multiple different kinds of +persistence and templating systems. And so on. However, the existence of +most of these overlapping ways to do things are not without reason and +purpose: we have a number of audiences to serve, and we believe that TIMTOWTI +at the web framework level actually *prevents* a much more insidious and +harmful set of duplication at higher levels in the Python web community. :app:`Pyramid` began its life as :mod:`repoze.bfg`, written by a team of people with many years of prior :ref:`Zope` experience. The idea of @@ -40,15 +39,14 @@ be *extended* without forking is also a Zope derivative. Implementations of these features were *required* to allow the :app:`Pyramid` authors to build the bread-and-butter CMS-type systems for customers in the way they were accustomed to building them. No other system save Zope itself -had such features. And Zope itself was beginning to show signs of its age, -and we were becoming hampered by consequences of early design mistakes. -Zope's lack of documentation was also difficult to work around: it was hard -to hire otherwise-smart people to work on Zope applications, because there -was no comprehensive documentation set to point them at which explained "it -all" in one consumble place. Before :mod:`repoze.bfg` went under -development, its authors obviously looked around for other frameworks that -fit the bill. But no non-Zope framework did. So we embarked on building -:mod:`repoze.bfg`. +had such features. And Zope itself was beginning to show signs of its age. +W were becoming hampered by consequences of early design mistakes. Zope's +lack of documentation was also difficult to work around: it was hard to hire +smart people to work on Zope applications, because there was no comprehensive +documentation set to point them at which explained "it all" in one consumble +place. Before :mod:`repoze.bfg` went under development, its authors +obviously looked around for other frameworks that fit the bill. But no +non-Zope framework did. So we embarked on building :mod:`repoze.bfg`. As the result of our research, however, it became apparent that, despite the fact that no *one* framework had all the features we required, lots of @@ -68,31 +66,35 @@ became clear that we had a system that was very generic, and was beginning to appeal to non-Zope users as well as ex-Zope users. As the result of this generalization, it became obvious BFG shared 90% of its -featureset with the featureset of Pylons 1, and had a very similar target -market. Because they were so similar, choosing between the two systems was -an exercise in frustration for an otherwise non-partisan developer. It was -also painful and felt wrong for Pylons and BFG development communities to -compete for users. So the Pylons and BFG teams began to work together to -form a plan to "merge". The features missing from BFG (notably :term:`view -handler` classes, flash messaging, and other minor missing bits), were added, -to provide familiarity to ex-Pylons users. The result is :app:`Pyramid`. +featureset with the featureset of Pylons 1, and thus had a very similar +target market. Because they were so similar, choosing between the two +systems was an exercise in frustration for an otherwise non-partisan +developer. It was also painful and felt wrong for Pylons and BFG development +communities to compete for users. So the Pylons and BFG teams began to work +together to form a plan to "merge". The features missing from BFG (notably +:term:`view handler` classes, flash messaging, and other minor missing bits), +were added, to provide familiarity to ex-Pylons users. The result is +:app:`Pyramid`. The Python web framework space is currently notoriously balkanized. We're truly hoping that the amalgamation of components in :app:`Pyramid` will appeal to at least two currently very distinct sets of users: Pylons and BFG -(often ex-Zope) users. By unifying the best concepts from Pylons and BFG -into a single codebase and leaving the bad concepts from their ancestors -behind, we'll be able to consolidate our efforts better, share more code, and -promote our efforts as a unit rather than competing pointlessly. We hope to -be able to shortcut the pack mentality which results in a *much larger* -duplication of effort, represented by competing but incredibly similar -applications and libraries, each built upon a specific low level stack that -is incompatible with the other. We'll also shrink the choice of credible -Python web frameworks down by at least one. Some overlap of functionality to -achieve these goals is expected and unavoidable, at least if we aim to -prevent pointless duplication at higher levels. If we've done our job well -enough, the various audiences will be able to coexist and cooperate rather -than firing at each other across some imaginary web framework "DMZ". +users. By unifying the best concepts from Pylons and BFG into a single +codebase and leaving the bad concepts from their ancestors behind, we'll be +able to consolidate our efforts better, share more code, and promote our +efforts as a unit rather than competing pointlessly. We hope to be able to +shortcut the pack mentality which results in a *much larger* duplication of +effort, represented by competing but incredibly similar applications and +libraries, each built upon a specific low level stack that is incompatible +with the other. We'll also shrink the choice of credible Python web +frameworks down by at least one. We're also hoping to attract users from +other communities (such as Zope's and TurboGears') by providing the features +they need, while allowing enough flexibility to do thing in a familiar +fashion. Some overlap of functionality to achieve these goals is expected +and unavoidable, at least if we aim to prevent pointless duplication at +higher levels. If we've done our job well enough, the various audiences will +be able to coexist and cooperate rather than firing at each other across some +imaginary web framework "DMZ". Pyramid Uses A Zope Component Architecture ("ZCA") Registry ----------------------------------------------------------- -- cgit v1.2.3 From 2ce4789b6eaf8eca65ec7f90e1dcb3de76ae155e Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 24 Dec 2010 21:11:49 -0500 Subject: add 's' --- docs/designdefense.rst | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/docs/designdefense.rst b/docs/designdefense.rst index 86ff2d993..b1e869a12 100644 --- a/docs/designdefense.rst +++ b/docs/designdefense.rst @@ -21,8 +21,7 @@ an acronym for "there is more than one way to do it"). it includes more than one way to resolve a URL to a :term:`view callable`: via :term:`url dispatch` or :term:`traversal`. Multiple methods of configuration exist: :term:`imperative configuration`, :term:`configuration -decoration`, and :term:`ZCML`. A view callable can itself be either a -function, an instance, or a class. It works with multiple different kinds of +decoration`, and :term:`ZCML`. It works with multiple different kinds of persistence and templating systems. And so on. However, the existence of most of these overlapping ways to do things are not without reason and purpose: we have a number of audiences to serve, and we believe that TIMTOWTI @@ -40,11 +39,12 @@ Implementations of these features were *required* to allow the :app:`Pyramid` authors to build the bread-and-butter CMS-type systems for customers in the way they were accustomed to building them. No other system save Zope itself had such features. And Zope itself was beginning to show signs of its age. -W were becoming hampered by consequences of early design mistakes. Zope's -lack of documentation was also difficult to work around: it was hard to hire -smart people to work on Zope applications, because there was no comprehensive -documentation set to point them at which explained "it all" in one consumble -place. Before :mod:`repoze.bfg` went under development, its authors +We were becoming hampered by consequences of its early design mistakes. +Zope's lack of documentation was also difficult to work around: it was hard +to hire smart people to work on Zope applications, because there was no +comprehensive documentation set to point them at which explained "it all" in +one consumble place, and it was too large and self-inconsistent to document +properly. Before :mod:`repoze.bfg` went under development, its authors obviously looked around for other frameworks that fit the bill. But no non-Zope framework did. So we embarked on building :mod:`repoze.bfg`. @@ -69,9 +69,10 @@ As the result of this generalization, it became obvious BFG shared 90% of its featureset with the featureset of Pylons 1, and thus had a very similar target market. Because they were so similar, choosing between the two systems was an exercise in frustration for an otherwise non-partisan -developer. It was also painful and felt wrong for Pylons and BFG development -communities to compete for users. So the Pylons and BFG teams began to work -together to form a plan to "merge". The features missing from BFG (notably +developer. It was also strange for the Pylons and BFG development +communities to be in competition for the same set of users, given how similar +the two frameworks were. So the Pylons and BFG teams began to work together +to form a plan to "merge". The features missing from BFG (notably :term:`view handler` classes, flash messaging, and other minor missing bits), were added, to provide familiarity to ex-Pylons users. The result is :app:`Pyramid`. @@ -89,7 +90,7 @@ libraries, each built upon a specific low level stack that is incompatible with the other. We'll also shrink the choice of credible Python web frameworks down by at least one. We're also hoping to attract users from other communities (such as Zope's and TurboGears') by providing the features -they need, while allowing enough flexibility to do thing in a familiar +they requre, while allowing enough flexibility to do things in a familiar fashion. Some overlap of functionality to achieve these goals is expected and unavoidable, at least if we aim to prevent pointless duplication at higher levels. If we've done our job well enough, the various audiences will -- cgit v1.2.3 From 22533dbdc0f362eb680f1053bc707e9b00e9f717 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 24 Dec 2010 21:13:07 -0500 Subject: note timtowti --- CHANGES.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index e28ac9792..cb32825d5 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -65,6 +65,9 @@ Documentation - Fix API documentation rendering for ``pyramid.view.static`` +- Add "Pyramid Provides More Than One Way to Do It" to Design Defense + documentation. + 1.0a7 (2010-12-20) ================== -- cgit v1.2.3 From 56db9ebbb2443f213cc38911c83de24a5abbb111 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 24 Dec 2010 22:26:16 -0500 Subject: - Changed "Static Assets" narrative chapter: clarify that ``name`` represents a prefix unless it's a URL, added an example of a root-relative static view fallback for URL dispatch, added an example of creating a simple view that returns the body of a file. --- CHANGES.txt | 5 + TODO.txt | 13 --- docs/narr/static.rst | 262 +++++++++++++++++++++++++++++++-------------------- 3 files changed, 163 insertions(+), 117 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index cb32825d5..db7cb991a 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -68,6 +68,11 @@ Documentation - Add "Pyramid Provides More Than One Way to Do It" to Design Defense documentation. +- Changed "Static Assets" narrative chapter: clarify that ``name`` represents + a prefix unless it's a URL, added an example of a root-relative static view + fallback for URL dispatch, added an example of creating a simple view that + returns the body of a file. + 1.0a7 (2010-12-20) ================== diff --git a/TODO.txt b/TODO.txt index f0b1e8b58..a220d78aa 100644 --- a/TODO.txt +++ b/TODO.txt @@ -14,23 +14,10 @@ Must-Have (before 1.0) Should-Have ----------- -- Fix add_static_view docs to mention: static views cannot be served from the - root, that "name" really means "prefix", and that a non-URL prefix can be a - path with slashes in it. - -- Add an example of serving a static asset from the root using a view. - -- Add an example of serving a static asset using a view by setting - response.app_iter and the response content-type/content-disposition. - - Add an example of using a cascade to serve static assets from the root. - Explore static file return from handler action using wsgiapp2 + fileapp. -- Document ``config.add_route('catchall', '/*subpath', - view=pyramid.views.static('some:asset/spec'))``, and explain that - ``static`` requires ``subpath``. - - Static (URL-generation only) routes. - Add narrative docs for wsgiapp and wsgiapp2. diff --git a/docs/narr/static.rst b/docs/narr/static.rst index 53564a632..e6be5fdff 100644 --- a/docs/narr/static.rst +++ b/docs/narr/static.rst @@ -15,18 +15,18 @@ Serving Static Assets Use the :meth:`pyramid.config.Configurator.add_static_view` to instruct :app:`Pyramid` to serve static assets such as JavaScript and CSS files. This -mechanism makes static files available at a name relative to the application -root URL, e.g. ``/static``. +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 that the ``path`` provided to -:meth:`pyramid.config.Configurator.add_static_view` may be a fully qualified -:term:`asset specification`, or an *absolute path*. +.. note:: `~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`. Here's an example of a use of -:meth:`pyramid.config.Configurator.add_static_view` that will serve -files up under the ``/static`` URL from the ``/var/www/static`` directory of -the computer which runs the :app:`Pyramid` application using an absolute -path. +:meth:`~pyramid.config.Configurator.add_static_view` that will serve files up +from the ``/var/www/static`` directory of the computer which runs the +:app:`Pyramid` application as URLs beneath the ``/static`` URL prefix. .. code-block:: python :linenos: @@ -34,10 +34,30 @@ path. # config is an instance of pyramid.config.Configurator config.add_static_view(name='static', path='/var/www/static') -Here's an example of :meth:`pyramid.config.Configurator.add_static_view` that -will serve files up under the ``/static`` URL from the ``a/b/c/static`` -directory of the Python package named ``some_package`` using a fully -qualified :term:`asset specification`. +The ``name`` prepresents a URL *prefix*. In order for files that live in the +``path`` directory to be served, a URL that requests one of them must begin +with that prefix. In the example above, ``name`` is ``static``, and ``path`` +is ``/var/www/static``. In English, this means that you wish to serve the +files that live in ``/var/www/static`` as sub-URLs of the ``/static`` URL +prefix. Therefore, the file ``/var/www/static/foo.css`` will be returned +when the user visits your application's URL ``/static/foo.css``. + +No authorization is ever required for users to visit files served by a static +view added via :meth:`~pyramid.config.Configurator.add_static_view`. If you +need "static" resources to be protected by authentication services, see +:ref:`advanced_static`. + +A static directory named at ``path`` may contain subdirectories recursively, +and any subdirectories may hold files; these will be resolved by the static +view as you would expect. The ``Content-Type`` header returned by the static +view for each particular type of file is dependent upon its file extension. + +Here's another example that uses an :term:`asset specification` instead of an +absolute path as the ``path`` argument. To convince +:meth:`pyramid.config.Configurator.add_static_view` to serve files up under +the ``/static`` URL from the ``a/b/c/static`` directory of the Python package +named ``some_package``, we can use a fully qualified :term:`asset +specification` as the ``path``: .. code-block:: python :linenos: @@ -45,22 +65,13 @@ qualified :term:`asset specification`. # config is an instance of pyramid.config.Configurator config.add_static_view(name='static', path='some_package:a/b/c/static') -Whether you use for ``path`` a fully qualified asset specification, or an -absolute path, when you place your static files on the filesystem in the -directory represented as the ``path`` of the directive, you will then be able -to view the static files in this directory via a browser at URLs prefixed -with the directive's ``name``. For instance if the ``static`` directive's -``name`` is ``static`` and the static directive's ``path`` is -``/path/to/static``, ``http://localhost:6543/static/foo.js`` will return the -file ``/path/to/static/dir/foo.js``. The static directory may contain -subdirectories recursively, and any subdirectories may hold files; these will -be resolved by the static view as you would expect. - -While the ``path`` argument can be a number of different things, the ``name`` -argument of the call to :meth:`pyramid.config.Configurator.add_static_view` -can also be one of a number of things: a *view name* or a *URL*. The above -examples have shown usage of the ``name`` argument as a view name. When -``name`` is a *URL* (or any string with a slash (``/``) in it), static assets +The ``path`` provided to :meth:`pyramid.config.Configurator.add_static_view` +may be a fully qualified :term:`asset specification`, or an *absolute path*. + +Instead of representing a URL prefix, the ``name`` argument of a call to +:meth:`pyramid.config.Configurator.add_static_view` can alternately be a +*URL*. Each of examples we've seen so far have shown usage of the ``name`` +argument as a URL prefix. However, when ``name`` is a *URL*, static assets can be served from an external webserver. In this mode, the ``name`` is used as the URL prefix when generating a URL using :func:`pyramid.url.static_url`. @@ -74,39 +85,22 @@ be fed a ``name`` argument which is ``http://example.com/images``: config.add_static_view(name='http://example.com/images', path='mypackage:images') -Because :meth:`pyramid.config.Configurator.add_static_view` is -provided with a ``name`` argument that is the URL prefix -``http://example.com/images``, subsequent calls to -:func:`pyramid.url.static_url` with paths that start with the ``path`` -argument passed to :meth:`pyramid.config.Configurator.add_static_view` -will generate a URL something like ``http://example.com/images/logo.png``. The -external webserver listening on ``example.com`` must be itself configured to -respond properly to such a request. The :func:`pyramid.url.static_url` API -is discussed in more detail later in this chapter. - -The :ref:`static_directive` ZCML directive offers an declarative equivalent -to :meth:`pyramid.config.Configurator.add_static_view`. Use of the -:ref:`static_directive` ZCML directive is completely equivalent to using -imperative configuration for the same purpose. +Because :meth:`pyramid.config.Configurator.add_static_view` is provided with +a ``name`` argument that is the URL ``http://example.com/images``, subsequent +calls to :func:`pyramid.url.static_url` with paths that start with the +``path`` argument passed to +:meth:`pyramid.config.Configurator.add_static_view` will generate a URL +something like ``http://example.com/images/logo.png``. The external +webserver listening on ``example.com`` must be itself configured to respond +properly to such a request. The :func:`pyramid.url.static_url` API is +discussed in more detail later in this chapter. .. note:: - Using :func:`pyramid.url.static_url` in conjunction with a - :meth:`pyramid.configuration.Configurator.add_static_view` makes it - possible to put static media on a separate webserver during production (if - the ``name`` argument to - :meth:`pyramid.config.Configurator.add_static_view` is a URL), - while keeping static media package-internal and served by the development - webserver during development (if the ``name`` argument to - :meth:`pyramid.config.Configurator.add_static_view` is a view - name). To create such a circumstance, we suggest using the - :attr:`pyramid.registry.Registry.settings` API in conjunction with a - setting in the application ``.ini`` file named ``media_location``. Then - set the value of ``media_location`` to either a view name 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 suggestion for a pattern; any setting name - other than ``media_location`` could be used. + The :ref:`static_directive` ZCML directive offers an declarative + equivalent to :meth:`pyramid.config.Configurator.add_static_view`. Use of + the :ref:`static_directive` ZCML directive is completely equivalent to + using imperative configuration for the same purpose. .. index:: single: generating static asset urls @@ -175,7 +169,8 @@ the the ``path`` given may be ``mypackage:images``: .. code-block:: python :linenos: - config.add_static_view(name='http://example.com/images', path='mypackage:images') + config.add_static_view(name='http://example.com/images', + path='mypackage:images') Under such a configuration, the URL generated by ``static_url`` for assets which begin with ``mypackage:images`` will be prefixed with @@ -187,24 +182,61 @@ assets which begin with ``mypackage:images`` will be prefixed with static_url('mypackage:images/logo.png', request) # -> http://example.com/images/logo.png +Using :func:`pyramid.url.static_url` in conjunction with a +:meth:`pyramid.configuration.Configurator.add_static_view` makes it possible +to put static media on a separate webserver during production (if the +``name`` argument to :meth:`pyramid.config.Configurator.add_static_view` is a +URL), while keeping static media package-internal and served by the +development webserver during development (if the ``name`` argument to +:meth:`pyramid.config.Configurator.add_static_view` is a URL prefix). To +create such a circumstance, we suggest using the +:attr:`pyramid.registry.Registry.settings` API in conjunction with a setting +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 +suggestion for a pattern; any setting name other than ``media_location`` +could be used. + .. index:: single: static assets view +.. _advanced_static: + Advanced: Serving Static Assets Using a View Callable ----------------------------------------------------- For more flexibility, static assets can be served by a :term:`view callable` -which you register manually. For example, you may want static assets to only -be available when the :term:`context` is of a particular type, or when -certain request headers are present. - -The :class:`pyramid.view.static` helper class is used to perform this -task. This class creates an object that is capable acting as a :app:`Pyramid` -view callable which serves static assets from a directory. For instance, to -serve files within a directory located on your filesystem at -``/path/to/static/dir`` from the URL path ``/static`` in your application, -create an instance of the :class:`pyramid.view.static` class inside a -``static.py`` file in your application root as below. +which you register manually. For example, if you're using :term:`URL +dispatch`, you may want static assets to only be available as a fallback if +no previous route matches. Alternately, you might like to serve a particular +static asset manually, because its download requires authentication. + +Note that you cannot use the :func:`pyramid.url.static_url` API to generate +URLs against assets made accessible by registering a custom static view. + +Root-Relative Custom Static View (URL Dispatch Only) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The :class:`pyramid.view.static` helper class generates a Pyramid view +callable. This view callable can serve static assets from a directory. An +instance of this class is actually used by the +:meth:`pyramid.config.Configurator.add_static_view` configuration method, so +its behavior is almost exactly the same once it's configured. + +.. warning:: The following example *will not work* for applications that use + :term:`traversal`, it will only work if you use :term:`URL dispatch` + exclusively. The root-relative route we'll be registering will always be + matched before traversal takes place, subverting any views registered via + ``add_view`` (at least those without a ``route_name``). A + :class:`pyramid.view.static` static view cannot be made root-relative when + you use traversal. + +To serve files within a directory located on your filesystem at +``/path/to/static/dir`` as the result of a "catchall" route hanging from the +root that exists at the end of your routing table, create an instance of the +:class:`pyramid.view.static` class inside a ``static.py`` file in your +application root as below. .. ignore-next-block .. code-block:: python @@ -213,46 +245,68 @@ create an instance of the :class:`pyramid.view.static` class inside a from pyramid.view import static static_view = static('/path/to/static/dir') -.. note:: the argument to :class:`pyramid.view.static` can also be - a "here-relative" pathname, e.g. ``my/static`` (meaning relative to the - Python package of the module in which the view is being defined). - It can also be a :term:`asset specification` - (e.g. ``anotherpackage:some/subdirectory``). - -Subsequently, you may wire this view up to be accessible as ``/static`` using -the :mod:`pyramid.config.Configurator.add_view` method in your application's -startup code against either the class or interface that represents your root -resource object. +.. note:: For better cross-system flexibility, use an :term:`asset + specification` as the argument to :class:`pyramid.view.static` instead of + a physical absolute filesystem path, e.g. ``mypackage:static`` instead of + ``/path/to/mypackage/static``. + +Subsequently, you may wire the files that are served by this view up to be +accessible as ``/`` using a configuration method in your +application's startup code. .. code-block:: python :linenos: - config.add_view('mypackage.static.static_view', name='static', - context='mypackage.resources.Root') + # .. every other add_route and/or add_handler declaration should come + # before this one, as it will, by default, catch all requests -In this case, ``mypackage.resources.Root`` refers to the class of your -:app:`Pyramid` application's resource tree. + config.add_route('catchall_static', '/*subpath', 'myapp.static.static_view') -The context argument above limits where the static view is accessible to URL -paths directly under the root object. If you omit the ``context`` argument, -then ``static`` will be accessible as the static view against any resource -object in the resource tree. This will allow ``/static/foo.js`` to work, but -it will also allow for ``/anything/static/foo.js`` too, as long as -``anything`` can be resolved. +The special name ``*subpath`` above is used by the +:class:`pyramid.view.static` view callable to signify the path of the file +relative to the directory you're serving. -Note that you cannot use the :func:`pyramid.url.static_url` API to generate -URLs against assets made accessible by registering a custom static view. +Registering A View Callable to Serve a "Static" Asset +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can register a simple view callable to serve a single static asset. To +do so, do things "by hand". First define the view callable. -.. warning:: +.. code-block:: python + :linenos: + + import os + from pyramid.view import view_config + from webob import Response + + def favicon_view(request): + here = os.path.dirname(__file__) + icon = open(os.path.join(here, 'static', 'favicon.ico')) + return Response(content_type='image/x-icon', app_iter=icon) + +The above bit of code within ``favicon_view`` computes "here", which is a +path relative to the Python file in which the function is defined. It then +uses the Python ``open`` function to obtain a file handle to a file within +"here" named ``static``, and returns a response using the open the file +handle as the response's ``app_iter``. It makes sure to set the right +content_type too. + +You might register such a view via configuration as a view callable that +should be called as the result of a traversal: + +.. code-block:: python + :linenos: + + config.add_view('myapp.views.favicon_view', name='favicon.ico') + +Or you might register it to be the view callable for a particular route: + +.. code-block:: python + :linenos: - When adding a static view to your root object, you need to be careful that - there are no resource objects contained in the root with the same key as - the view name (e.g., ``static``). Resource objects take precedence during - traversal, thus such a name collision will cause the resource to "shadow" - your static view. To avoid this issue, and ensure that your root - resource's ``__getitem__`` is never called when a static asset is - requested, you can refer to them unambiguously using the ``@@`` prefix - (goggles) in their URLs. For the above examples you could use - '/@@static/foo.js' instead of '/static/foo.js' to avoid such shadowing. - See :ref:`traversal_chapter` for information about "goggles" (``@@``). + config.add_route('favicon', '/favicon.ico', + view='myapp.views.favicon_view') +Because this is a simple view callable, it can be protected with a +:term:`permission` or can be configured to respond under different +circumstances using :term:`view predicate` arguments. -- cgit v1.2.3 From e36332bcaab8cf6062fda72f4411c8cd5cfe1e4b Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 24 Dec 2010 22:31:54 -0500 Subject: garden --- TODO.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/TODO.txt b/TODO.txt index a220d78aa..d6e10cd3c 100644 --- a/TODO.txt +++ b/TODO.txt @@ -14,6 +14,8 @@ Must-Have (before 1.0) Should-Have ----------- +- Add notes about renderer response attrs to request docs. + - Add an example of using a cascade to serve static assets from the root. - Explore static file return from handler action using wsgiapp2 + fileapp. -- cgit v1.2.3 From 781f9e02e0433a77e78f07f28e5b7d2064e73af4 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 24 Dec 2010 22:56:15 -0500 Subject: fix permission discussion --- docs/narr/static.rst | 18 +++++++--- pyramid/config.py | 96 ++++++++++++++++++++++++---------------------------- 2 files changed, 58 insertions(+), 56 deletions(-) diff --git a/docs/narr/static.rst b/docs/narr/static.rst index e6be5fdff..d4f6da76d 100644 --- a/docs/narr/static.rst +++ b/docs/narr/static.rst @@ -42,16 +42,24 @@ files that live in ``/var/www/static`` as sub-URLs of the ``/static`` URL prefix. Therefore, the file ``/var/www/static/foo.css`` will be returned when the user visits your application's URL ``/static/foo.css``. -No authorization is ever required for users to visit files served by a static -view added via :meth:`~pyramid.config.Configurator.add_static_view`. If you -need "static" resources to be protected by authentication services, see -:ref:`advanced_static`. - A static directory named at ``path`` may contain subdirectories recursively, and any subdirectories may hold files; these will be resolved by the static view as you would expect. The ``Content-Type`` header returned by the static view for each particular type of file is dependent upon its file extension. +By default, all files made available via +:meth:`~pyramid.config.Configurator.add_static_view` are accessible by +completely anonymous users. Simple authorization can be required, however. +To protect a set of static files using a permission, in addition to passing +the required ``name`` and ``path`` arguments, also pass the ``permission`` +keyword argument to :meth:`~pyramid.config.Configurator.add_static_view`. +The value of the ``permission`` argument represents the :term:`permission` +that the user must have relative to the current :term:`context` when the +static view is invoked. A user will be required to possess this permission +to view any of the files represented by ``path`` of the static view. If your +static resources must be protected by a more complex authorization scheme, +see :ref:`advanced_static`. + Here's another example that uses an :term:`asset specification` instead of an absolute path as the ``path`` argument. To convince :meth:`pyramid.config.Configurator.add_static_view` to serve files up under diff --git a/pyramid/config.py b/pyramid/config.py index 93123c119..2a18db4e9 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -2151,20 +2151,19 @@ class Configurator(object): """ Add a view used to render static assets such as images and CSS files. - The ``name`` argument is a string representing :term:`view - name` of the view which is registered. It may alternately be - a *url prefix*. + The ``name`` argument is a string representing an + application-relative local URL prefix. It may alternately be a full + URL. - The ``path`` argument is the path on disk where the static - files reside. This can be an absolute path, a - package-relative path, or a :term:`asset specification`. + The ``path`` argument is the path on disk where the static files + reside. This can be an absolute path, a package-relative path, or a + :term:`asset specification`. The ``cache_max_age`` keyword argument is input to set the - ``Expires`` and ``Cache-Control`` headers for static assets - served. Note that this argument has no effect when the - ``name`` is a *url prefix*. By default, this argument is - ``None``, meaning that no particular Expires or Cache-Control - headers are set in the response. + ``Expires`` and ``Cache-Control`` headers for static assets served. + Note that this argument has no effect when the ``name`` is a *url + prefix*. By default, this argument is ``None``, meaning that no + particular Expires or Cache-Control headers are set in the response. The ``permission`` keyword argument is used to specify the :term:`permission` required by a user to execute the static view. By @@ -2178,67 +2177,62 @@ class Configurator(object): *Usage* - The ``add_static_view`` function is typically used in - conjunction with the :func:`pyramid.url.static_url` - function. ``add_static_view`` adds a view which renders a - static asset when some URL is visited; - :func:`pyramid.url.static_url` generates a URL to that - asset. + The ``add_static_view`` function is typically used in conjunction + with the :func:`pyramid.url.static_url` function. + ``add_static_view`` adds a view which renders a static asset when + some URL is visited; :func:`pyramid.url.static_url` generates a URL + to that asset. - The ``name`` argument to ``add_static_view`` is usually a - :term:`view name`. When this is the case, the - :func:`pyramid.url.static_url` API will generate a URL - which points to a Pyramid view, which will serve up a set of - assets that live in the package itself. For example: + The ``name`` argument to ``add_static_view`` is usually a :term:`view + name`. When this is the case, the :func:`pyramid.url.static_url` API + will generate a URL which points to a Pyramid view, which will serve + up a set of assets that live in the package itself. For example: .. code-block:: python add_static_view('images', 'mypackage:images/') - Code that registers such a view can generate URLs to the view - via :func:`pyramid.url.static_url`: + Code that registers such a view can generate URLs to the view via + :func:`pyramid.url.static_url`: .. code-block:: python static_url('mypackage:images/logo.png', request) - When ``add_static_view`` is called with a ``name`` argument - that represents a simple view name, as it is above, subsequent - calls to :func:`pyramid.url.static_url` with paths that - start with the ``path`` argument passed to ``add_static_view`` - will generate a URL something like ``http:///images/logo.png``, which will cause the ``logo.png`` file - in the ``images`` subdirectory of the ``mypackage`` package to - be served. - - ``add_static_view`` can alternately be used with a ``name`` - argument which is a *URL*, causing static assets to be - served from an external webserver. This happens when the - ``name`` argument is a URL (detected as any string with a - slash in it). In this mode, the ``name`` is used as the URL - prefix when generating a URL using - :func:`pyramid.url.static_url`. For example, if - ``add_static_view`` is called like so: + When ``add_static_view`` is called with a ``name`` argument that + represents a URL prefix, as it is above, subsequent calls to + :func:`pyramid.url.static_url` with paths that start with the + ``path`` argument passed to ``add_static_view`` will generate a URL + something like ``http:///images/logo.png``, which + will cause the ``logo.png`` file in the ``images`` subdirectory of + the ``mypackage`` package to be served. + + ``add_static_view`` can alternately be used with a ``name`` argument + which is a *URL*, causing static assets to be served from an external + webserver. This happens when the ``name`` argument is a fully + qualified URL (e.g. starts with ``http://`` or similar). In this + mode, the ``name`` is used as the prefix of the full URL when + generating a URL using :func:`pyramid.url.static_url`. For example, + if ``add_static_view`` is called like so: .. code-block:: python add_static_view('http://example.com/images', 'mypackage:images/') - Subsequently, the URLs generated by - :func:`pyramid.url.static_url` for that static view will be - prefixed with ``http://example.com/images``: + Subsequently, the URLs generated by :func:`pyramid.url.static_url` + for that static view will be prefixed with + ``http://example.com/images``: .. code-block:: python static_url('mypackage:images/logo.png', request) - When ``add_static_view`` is called with a ``name`` argument - that is the URL prefix ``http://example.com/images``, - subsequent calls to :func:`pyramid.url.static_url` with - paths that start with the ``path`` argument passed to - ``add_static_view`` will generate a URL something like - ``http://example.com/logo.png``. The external webserver - listening on ``example.com`` must be itself configured to + When ``add_static_view`` is called with a ``name`` argument that is + the URL ``http://example.com/images``, subsequent calls to + :func:`pyramid.url.static_url` with paths that start with the + ``path`` argument passed to ``add_static_view`` will generate a URL + something like ``http://example.com/logo.png``. The external + webserver listening on ``example.com`` must be itself configured to respond properly to such a request. See :ref:`static_assets_section` for more information. -- cgit v1.2.3 From f5bafd1da8da405936bae032d20dba545922860e Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 25 Dec 2010 02:12:54 -0500 Subject: - Prevent misunderstanding of how the ``view`` and ``view_permission`` arguments to add_route work by raising an exception during configuration if view-related arguments exist but no ``view`` argument is passed. --- CHANGES.txt | 4 ++++ pyramid/config.py | 32 ++++++++++++++++++++++++++------ pyramid/tests/test_config.py | 40 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 6 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index db7cb991a..b544c6b49 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -21,6 +21,10 @@ Features - Added CSRF token generation, as described in the narrative chapter entitled "Preventing Cross-Site Request Forgery Attacks". +- Prevent misunderstanding of how the ``view`` and ``view_permission`` + arguments to add_route work by raising an exception during configuration if + view-related arguments exist but no ``view`` argument is passed. + Paster Templates ---------------- diff --git a/pyramid/config.py b/pyramid/config.py index 2a18db4e9..f6b4a2112 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -1748,13 +1748,14 @@ class Configurator(object): for info in view_info: self.add_view(**info) - if view: + if view_context is None: + view_context = view_for if view_context is None: - view_context = view_for - if view_context is None: - view_context = for_ - view_permission = view_permission or permission - view_renderer = view_renderer or renderer + view_context = for_ + view_permission = view_permission or permission + view_renderer = view_renderer or renderer + + if view: self.add_view( permission=view_permission, context=view_context, @@ -1764,6 +1765,25 @@ class Configurator(object): renderer=view_renderer, attr=view_attr, ) + else: + # prevent mistakes due to misunderstanding of how hybrid calls to + # add_route and add_view interact + if view_attr: + raise ConfigurationError( + 'view_attr argument not permitted without view ' + 'argument') + if view_context: + raise ConfigurationError( + 'view_context argument not permitted without view ' + 'argument') + if view_permission: + raise ConfigurationError( + 'view_permission argument not permitted without view ' + 'argument') + if view_renderer: + raise ConfigurationError( + 'view_renderer argument not permitted without ' + 'view argument') mapper = self.get_routes_mapper() diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py index e4ab9a867..84e8289be 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -2331,6 +2331,46 @@ class ConfiguratorTests(unittest.TestCase): route = config.add_route('name', 'pattern', pregenerator='123') self.assertEqual(route.pregenerator, '123') + def test_add_route_no_view_with_view_attr(self): + config = self._makeOne(autocommit=True) + from pyramid.exceptions import ConfigurationError + try: + config.add_route('name', '/pattern', view_attr='abc') + except ConfigurationError: + pass + else: + raise AssertionError + + def test_add_route_no_view_with_view_context(self): + config = self._makeOne(autocommit=True) + from pyramid.exceptions import ConfigurationError + try: + config.add_route('name', '/pattern', view_context=DummyContext) + except ConfigurationError: + pass + else: + raise AssertionError + + def test_add_route_no_view_with_view_permission(self): + config = self._makeOne(autocommit=True) + from pyramid.exceptions import ConfigurationError + try: + config.add_route('name', '/pattern', view_permission='edit') + except ConfigurationError: + pass + else: + raise AssertionError + + def test_add_route_no_view_with_view_renderer(self): + config = self._makeOne(autocommit=True) + from pyramid.exceptions import ConfigurationError + try: + config.add_route('name', '/pattern', view_renderer='json') + except ConfigurationError: + pass + else: + raise AssertionError + def test__override_not_yet_registered(self): from pyramid.interfaces import IPackageOverrides package = DummyPackage('package') -- cgit v1.2.3 From 291bcc034c59a17afa46443499ae6a670718055d Mon Sep 17 00:00:00 2001 From: Danny Navarro Date: Sat, 25 Dec 2010 12:13:18 +0100 Subject: typo --- docs/designdefense.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/designdefense.rst b/docs/designdefense.rst index b1e869a12..1b8aa1512 100644 --- a/docs/designdefense.rst +++ b/docs/designdefense.rst @@ -90,7 +90,7 @@ libraries, each built upon a specific low level stack that is incompatible with the other. We'll also shrink the choice of credible Python web frameworks down by at least one. We're also hoping to attract users from other communities (such as Zope's and TurboGears') by providing the features -they requre, while allowing enough flexibility to do things in a familiar +they require, while allowing enough flexibility to do things in a familiar fashion. Some overlap of functionality to achieve these goals is expected and unavoidable, at least if we aim to prevent pointless duplication at higher levels. If we've done our job well enough, the various audiences will -- cgit v1.2.3 From ea32d63773bec2493326c04c896f224e25c532c4 Mon Sep 17 00:00:00 2001 From: Danny Navarro Date: Sat, 25 Dec 2010 12:37:32 +0100 Subject: typo --- docs/designdefense.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/designdefense.rst b/docs/designdefense.rst index 1b8aa1512..db399fffa 100644 --- a/docs/designdefense.rst +++ b/docs/designdefense.rst @@ -260,7 +260,7 @@ ZCA registry gives us, and we have long-ago borne the weight of understanding what it does and how it works. The authors of :app:`Pyramid` understand the ZCA deeply and can read code that uses it as easily as any other code. -But we recognize that developers who my want to extend the framework are not +But we recognize that developers who might want to extend the framework are not as comfortable with the ZCA registry API as the original developers are with it. So, for the purposes of being kind to third-party :app:`Pyramid` framework developers in, we've drawn some lines in the sand. -- cgit v1.2.3 From e1a7e0679759da628676f3c73c34875e9b2b6a43 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 26 Dec 2010 01:13:26 -0500 Subject: - Move ZCML usage in Hooks chapter to Declarative Configuration chapter. --- CHANGES.txt | 2 + docs/narr/declarative.rst | 140 +++++++++++++- docs/narr/hooks.rst | 470 ++++++++++++++++++++-------------------------- 3 files changed, 340 insertions(+), 272 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index b544c6b49..127ea810e 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -77,6 +77,8 @@ Documentation fallback for URL dispatch, added an example of creating a simple view that returns the body of a file. +- Move ZCML usage in Hooks chapter to Declarative Configuration chapter. + 1.0a7 (2010-12-20) ================== diff --git a/docs/narr/declarative.rst b/docs/narr/declarative.rst index deccb6c48..12deb90e7 100644 --- a/docs/narr/declarative.rst +++ b/docs/narr/declarative.rst @@ -1266,9 +1266,143 @@ which we assume lives in a ``subscribers.py`` module within your application: See also :ref:`subscriber_directive` and :ref:`events_chapter`. +.. index:: + single: not found view + +.. _notfound_zcml: + +Configuring a Not Found View via ZCML +------------------------------------- + +If your application uses :term:`ZCML`, you can replace the Not Found view by +placing something like the following ZCML in your ``configure.zcml`` file. + +.. code-block:: xml + :linenos: + + + +Replace ``helloworld.views.notfound_view`` with the Python dotted name to the +notfound view you want to use. + +See :ref:`changing_the_notfound_view` for more information. + +.. index:: + single: forbidden view + +.. _forbidden_zcml: + +Configuring a Forbidden View via ZCML +------------------------------------- + +If your application uses :term:`ZCML`, you can replace the Forbidden view by +placing something like the following ZCML in your ``configure.zcml`` file. + +.. code-block:: xml + :linenos: + + + +Replace ``helloworld.views.forbidden_view`` with the Python dotted name to +the forbidden view you want to use. + +See :ref:`changing_the_forbidden_view` for more information. + +.. _changing_traverser_zcml: + +Configuring an Alternate Traverser via ZCML +------------------------------------------- + +Use an ``adapter`` stanza in your application's ``configure.zcml`` to +change the default traverser: + +.. code-block:: xml + :linenos: + + + +Or to register a traverser for a specific resource type: -.. Todo -.. ---- +.. code-block:: xml + :linenos: + + + +See :ref:`changing_the_traverser` for more information. + +.. index:: + single: url generator + +.. _changing_resource_url_zcml: + +Changing ``resource_url`` URL Generation via ZCML +------------------------------------------------- + +You can change how :func:`pyramid.url.resource_url` generates a URL for a +specific type of resource by adding an adapter statement to your +``configure.zcml``. + +.. code-block:: xml + :linenos: + + + +See :ref:`changing_resource_url` for more information. + +.. _changing_request_factory_zcml: + +Changing the Request Factory via ZCML +------------------------------------- + +A ``MyRequest`` class can be registered via ZCML as a request factory through +the use of the ZCML ``utility`` directive. In the below, we assume it lives +in a package named ``mypackage.mymodule``. + +.. code-block:: xml + :linenos: + + + +See :ref:`changing_request_factory` for more information. + +.. _adding_renderer_globals_zcml: + +Changing the Renderer Globals Factory via ZCML +---------------------------------------------- + +A renderer globals factory can be registered via ZCML as a through the use of +the ZCML ``utility`` directive. In the below, we assume a +``renderers_globals_factory`` function lives in a package named +``mypackage.mymodule``. + +.. code-block:: xml + :linenos: + + -.. - hooks chapter still has topics for ZCML +See :ref:`adding_renderer_globals` for more information. diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index 006f5d5cb..381da0d39 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 :app:`Pyramid` -framework in various ways. +"Hooks" can be used to influence the behavior of the :app:`Pyramid` framework +in various ways. .. index:: single: not found view @@ -14,61 +14,38 @@ framework in various ways. Changing the Not Found View --------------------------- -When :app:`Pyramid` can't map a URL to view code, it invokes a -: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`. +When :app:`Pyramid` can't map a URL to view code, it invokes a :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. -The :term:`not found view` callable is a view callable like any other. -The :term:`view configuration` which causes it to be a "not found" -view consists only of naming the :exc:`pyramid.exceptions.NotFound` -class as the ``context`` of the view configuration. +The :term:`not found view` callable is a view callable like any other. The +:term:`view configuration` which causes it to be a "not found" view consists +only of naming the :exc:`pyramid.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:`pyramid.config.Configurator.add_view` +method to register an "exception view": - If your application uses :term:`imperative configuration`, you can - replace the Not Found view by using the - :meth:`pyramid.config.Configurator.add_view` method to - register an "exception view": - - .. code-block:: python - :linenos: - - from pyramid.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 - Found view. - -.. topic:: Using ZCML - - If your application uses :term:`ZCML`, you can replace the Not Found - view by placing something like the following ZCML in your - ``configure.zcml`` file. - - .. code-block:: xml - :linenos: +.. code-block:: python + :linenos: - + from pyramid.exceptions import NotFound + from helloworld.views import notfound_view + config.add_view(notfound_view, context=NotFound) - Replace ``helloworld.views.notfound_view`` with the Python dotted name - to the notfound view you want to use. +Replace ``helloworld.views.notfound_view`` with a reference to the +:term:`view callable` you want to use to represent the Not Found view. -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:`pyramid.exceptions.NotFound` exception that -caused the view to be called. +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:`pyramid.exceptions.NotFound` exception that caused the view to be +called. -Here's some sample code that implements a minimal NotFound view -callable: +Here's some sample code that implements a minimal NotFound view callable: .. code-block:: python :linenos: @@ -93,6 +70,9 @@ callable: :exc:`pyramid.exceptions.NotFound` exception instance. If available, the resource context will still be available as ``request.context``. +For information about how to configure a not found view via :term:`ZCML`, see +:ref:`notfound_zcml`. + .. index:: single: forbidden view @@ -101,59 +81,36 @@ callable: Changing the Forbidden View --------------------------- -When :app:`Pyramid` 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 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:`pyramid.exceptions.Forbidden` -class as the ``context`` of the view configuration. +When :app:`Pyramid` 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 +the view which generates it can be overridden as necessary. -.. topic:: Using Imperative Configuration +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:`pyramid.exceptions.Forbidden` class as the +``context`` of the view configuration. - If your application uses :term:`imperative configuration`, you can - replace the Forbidden view by using the - :meth:`pyramid.config.Configurator.add_view` method to - register an "exception view": +You can replace the forbidden view by using the +:meth:`pyramid.config.Configurator.add_view` method to register an "exception +view": - .. code-block:: python - :linenos: - - from helloworld.views import forbidden_view - from pyramid.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 - Forbidden view. - -.. topic:: Using ZCML - - If your application uses :term:`ZCML`, you can replace the - Forbidden view by placing something like the following ZCML in your - ``configure.zcml`` file. - - .. code-block:: xml - :linenos: +.. code-block:: python + :linenos: - + from helloworld.views import forbidden_view + from pyramid.exceptions import Forbidden + config.add_view(forbidden_view, context=Forbidden) - Replace ``helloworld.views.forbidden_view`` with the Python - dotted name to the forbidden view you want to use. +Replace ``helloworld.views.forbidden_view`` with a reference to the Python +:term:`view callable` you want to use to represent the Forbidden view. -Like any other view, the forbidden view must accept at least a -``request`` parameter, or both ``context`` and ``request``. The -``context`` (available as ``request.context`` if you're using the -request-only view argument pattern) is the context found by the router -when the view invocation was denied. The ``request`` is the current -:term:`request` representing the denied action. +Like any other view, the forbidden view must accept at least a ``request`` +parameter, or both ``context`` and ``request``. The ``context`` (available +as ``request.context`` if you're using the request-only view argument +pattern) is the context found by the router when the view invocation was +denied. The ``request`` is the current :term:`request` representing the +denied action. Here's some sample code that implements a minimal forbidden view: @@ -161,10 +118,10 @@ Here's some sample code that implements a minimal forbidden view: :linenos: from pyramid.views import view_config + from pyramid.response import Response - @view_config(renderer='templates/login_form.pt') def forbidden_view(request): - return {} + return Response('forbidden') .. note:: When a forbidden view callable is invoked, it is passed a :term:`request`. The ``exception`` attribute of the request will @@ -181,6 +138,9 @@ Here's some sample code that implements a minimal forbidden view: an alternate forbidden view. For example, it would make sense to return a response with a ``403 Forbidden`` status code. +For information about how to configure a forbidden view via :term:`ZCML`, see +:ref:`forbidden_zcml`. + .. index:: single: traverser @@ -189,25 +149,22 @@ Here's some sample code that implements a minimal forbidden view: Changing the Traverser ---------------------- -The default :term:`traversal` algorithm that :app:`Pyramid` uses is -explained in :ref:`traversal_algorithm`. Though it is rarely -necessary, this default algorithm can be swapped out selectively for a -different traversal pattern via configuration. +The default :term:`traversal` algorithm that :app:`Pyramid` uses is explained +in :ref:`traversal_algorithm`. Though it is rarely necessary, this default +algorithm can be swapped out selectively for a different traversal pattern +via configuration. -Use an ``adapter`` stanza in your application's ``configure.zcml`` to -change the default traverser: - -.. code-block:: xml +.. code-block:: python :linenos: - + from pyramid.interfaces import ITraverser + from zope.interface import Interface + from myapp.traversal import Traverser -In the example above, ``myapp.traversal.Traverser`` is assumed to be -a class that implements the following interface: + config.registry.registerAdapter(Traverser, (Interface,), ITraverser) + +In the example above, ``myapp.traversal.Traverser`` is assumed to be a class +that implements the following interface: .. code-block:: python :linenos: @@ -241,31 +198,36 @@ a class that implements the following interface: """ More than one traversal algorithm can be active at the same time. For -instance, if your :term:`root factory` returns more than one type of -object conditionally, you could claim that an alternate traverser -adapter is ``for`` only one particular class or interface. When the -root factory returned an object that implemented that class or -interface, a custom traverser would be used. Otherwise, the default -traverser would be used. For example: - -.. code-block:: xml +instance, if your :term:`root factory` returns more than one type of object +conditionally, you could claim that an alternate traverser adapter is ``for`` +only one particular class or interface. When the root factory returned an +object that implemented that class or interface, a custom traverser would be +used. Otherwise, the default traverser would be used. For example: + +.. code-block:: python :linenos: - + from pyramid.interfaces import ITraverser + from zope.interface import Interface + from myapp.traversal import Traverser + from myapp.resources import MyRoot -If the above stanza was added to a ``configure.zcml`` file, -:app:`Pyramid` would use the ``myapp.traversal.Traverser`` only + config.registry.registerAdapter(Traverser, (MyRoot,), ITraverser) + +If the above stanza was added to a Pyramid ``__init__.py`` file's ``main`` +function, :app:`Pyramid` would use the ``myapp.traversal.Traverser`` only when the application :term:`root factory` returned an instance of the ``myapp.resources.MyRoot`` object. Otherwise it would use the default :app:`Pyramid` traverser to do traversal. +For information about how to configure an alternate traverser via +:term:`ZCML`, see :ref:`changing_traverser_zcml`. + .. index:: single: url generator +.. _changing_resource_url: + Changing How :mod:`pyramid.url.resource_url` Generates a URL ------------------------------------------------------------ @@ -276,25 +238,27 @@ generates by default may be incorrect. If you've added a traverser, you can change how :func:`pyramid.url.resource_url` generates a URL for a specific type of -resource by adding an adapter stanza for -:class:`pyramid.interfaces.IContextURL` to your application's -``configure.zcml``: +resource by adding a registerAdapter call for +:class:`pyramid.interfaces.IContextURL` to your application: -.. code-block:: xml +.. code-block:: python :linenos: - + from pyramid.interfaces import ITraverser + from zope.interface import Interface + from myapp.traversal import URLGenerator + from myapp.resources import MyRoot + + config.registry.registerAdapter(URLGenerator, (MyRoot, Interface), + IContextURL) -In the above example, the ``myapp.traversal.URLGenerator`` class will -be used to provide services to :func:`pyramid.url.resource_url` any -time the :term:`context` passed to ``resource_url`` is of class -``myapp.resources.MyRoot``. The asterisk following represents the type -of interface that must be possessed by the :term:`request` (in this -case, any interface, represented by asterisk). +In the above example, the ``myapp.traversal.URLGenerator`` class will be used +to provide services to :func:`pyramid.url.resource_url` any time the +:term:`context` passed to ``resource_url`` is of class +``myapp.resources.MyRoot``. The second argument in the ``(MyRoot, +Interface)`` tuple represents the type of interface that must be possessed by +the :term:`request` (in this case, any interface, represented by +``zope.interface.Interface``). The API that must be implemented by a class that provides :class:`pyramid.interfaces.IContextURL` is as follows: @@ -317,28 +281,25 @@ The API that must be implemented by a class that provides def __call__(self): """ Return a URL that points to the context """ -The default context URL generator is available for perusal as the -class :class:`pyramid.traversal.TraversalContextURL` in the -`traversal module -`_ of -the :term:`Pylons` GitHub Pyramid repository. +The default context URL generator is available for perusal as the class +:class:`pyramid.traversal.TraversalContextURL` in the `traversal module +`_ of the +:term:`Pylons` GitHub Pyramid repository. .. _changing_the_request_factory: Changing the Request Factory ---------------------------- -Whenever :app:`Pyramid` handles a :term:`WSGI` request, it creates -a :term:`request` object based on the WSGI environment it has been -passed. By default, an instance of the -:class:`pyramid.request.Request` class is created to represent the -request object. +Whenever :app:`Pyramid` handles a :term:`WSGI` request, it creates a +:term:`request` object based on the WSGI environment it has been passed. By +default, an instance of the :class:`pyramid.request.Request` class is created +to represent the request object. -The class (aka "factory") that :app:`Pyramid` uses to create a -request object instance can be changed by passing a -``request_factory`` argument to the constructor of the -:term:`configurator`. This argument can be either a callable or a -:term:`dotted Python name` representing a callable. +The class (aka "factory") that :app:`Pyramid` uses to create a request object +instance can be changed by passing a ``request_factory`` argument to the +constructor of the :term:`configurator`. This argument can be either a +callable or a :term:`dotted Python name` representing a callable. .. code-block:: python :linenos: @@ -350,24 +311,9 @@ request object instance can be changed by passing a config = Configurator(request_factory=MyRequest) -The same ``MyRequest`` class can alternately be registered via ZCML as -a request factory through the use of the ZCML ``utility`` directive. -In the below, we assume it lives in a package named -``mypackage.mymodule``. - -.. code-block:: xml - :linenos: - - - -Lastly, if you're doing imperative configuration, and you'd rather do -it after you've already constructed a :term:`configurator` it can also -be registered via the -:meth:`pyramid.config.Configurator.set_request_factory` -method: +If you're doing imperative configuration, and you'd rather do it after you've +already constructed a :term:`configurator` it can also be registered via the +:meth:`pyramid.config.Configurator.set_request_factory` method: .. code-block:: python :linenos: @@ -381,26 +327,26 @@ method: config = Configurator() config.set_request_factory(MyRequest) +To use ZCML for the same purpose, see :ref:`changing_request_factory_zcml`. + .. _adding_renderer_globals: Adding Renderer Globals ----------------------- -Whenever :app:`Pyramid` handles a request to perform a rendering -(after a view with a ``renderer=`` configuration attribute is invoked, -or when the any of the methods beginning with ``render`` within the -:mod:`pyramid.renderers` module are called), *renderer globals* can -be injected into the *system* values sent to the renderer. By -default, no renderer globals are injected, and the "bare" system -values (such as ``request``, ``context``, and ``renderer_name``) are -the only values present in the system dictionary passed to every -renderer. - -A callback that :app:`Pyramid` will call every time a renderer is -invoked can be added by passing a ``renderer_globals_factory`` -argument to the constructor of the :term:`configurator`. This -callback can either be a callable object or a :term:`dotted Python -name` representing such a callable. +Whenever :app:`Pyramid` handles a request to perform a rendering (after a +view with a ``renderer=`` configuration attribute is invoked, or when the any +of the methods beginning with ``render`` within the :mod:`pyramid.renderers` +module are called), *renderer globals* can be injected into the *system* +values sent to the renderer. By default, no renderer globals are injected, +and the "bare" system values (such as ``request``, ``context``, and +``renderer_name``) are the only values present in the system dictionary +passed to every renderer. + +A callback that :app:`Pyramid` will call every time a renderer is invoked can +be added by passing a ``renderer_globals_factory`` argument to the +constructor of the :term:`configurator`. This callback can either be a +callable object or a :term:`dotted Python name` representing such a callable. .. code-block:: python :linenos: @@ -411,30 +357,15 @@ name` representing such a callable. config = Configurator( renderer_globals_factory=renderer_globals_factory) -Such a callback must accept a single positional argument (notionally -named ``system``) which will contain the original system values. It -must return a dictionary of values that will be merged into the system -dictionary. See :ref:`renderer_system_values` for discription of the -values present in the system dictionary. +Such a callback must accept a single positional argument (notionally named +``system``) which will contain the original system values. It must return a +dictionary of values that will be merged into the system dictionary. See +:ref:`renderer_system_values` for discription of the values present in the +system dictionary. -A renderer globals factory can alternately be registered via ZCML as a -through the use of the ZCML ``utility`` directive. In the below, we -assume a ``renderers_globals_factory`` function lives in a package -named ``mypackage.mymodule``. - -.. code-block:: xml - :linenos: - - - -Lastly, if you're doing imperative configuration, and you'd rather do -it after you've already constructed a :term:`configurator` it can also -be registered via the -:meth:`pyramid.config.Configurator.set_renderer_globals_factory` -method: +If you're doing imperative configuration, and you'd rather do it after you've +already constructed a :term:`configurator` it can also be registered via the +:meth:`pyramid.config.Configurator.set_renderer_globals_factory` method: .. code-block:: python :linenos: @@ -450,6 +381,9 @@ method: Another mechanism which allows event subscribers to add renderer global values exists in :ref:`beforerender_event`. +If you'd rather ZCML to register a renderer globals factory, see +:ref:`adding_renderer_globals_zcml`. + .. _beforerender_event: Using The Before Render Event @@ -472,8 +406,8 @@ that can be used for this purpose. For example: An object of this type is sent as an event just before a :term:`renderer` is invoked (but *after* the application-level renderer globals factory added via -:class:`pyramid.config.Configurator.set_renderer_globals_factory`, if -any, has injected its own keys into the renderer globals dictionary). +:class:`pyramid.config.Configurator.set_renderer_globals_factory`, if any, +has injected its own keys into the renderer globals dictionary). If a subscriber attempts to add a key that already exist in the renderer globals dictionary, a :exc:`KeyError` is raised. This limitation is enforced @@ -493,16 +427,16 @@ renderer global values exists in :ref:`adding_renderer_globals`. Using Response Callbacks ------------------------ -Unlike many other web frameworks, :app:`Pyramid` does not eagerly -create a global response object. Adding a :term:`response callback` -allows an application to register an action to be performed against a -response object once it is created, usually in order to mutate it. +Unlike many other web frameworks, :app:`Pyramid` does not eagerly create a +global response object. Adding a :term:`response callback` allows an +application to register an action to be performed against a response object +once it is created, usually in order to mutate it. -The :meth:`pyramid.request.Request.add_response_callback` method is -used to register a response callback. +The :meth:`pyramid.request.Request.add_response_callback` method is used to +register a response callback. -A response callback is a callable which accepts two positional -parameters: ``request`` and ``response``. For example: +A response callback is a callable which accepts two positional parameters: +``request`` and ``response``. For example: .. code-block:: python :linenos: @@ -515,11 +449,11 @@ parameters: ``request`` and ``response``. For example: No response callback is called if an unhandled exception happens in application code, or if the response object returned by a :term:`view -callable` is invalid. Response callbacks *are*, however, invoked when -a :term:`exception view` is rendered successfully: in such a case, the -:attr:`request.exception` attribute of the request when it enters a -response callback will be an exception object instead of its default -value of ``None``. +callable` is invalid. Response callbacks *are*, however, invoked when a +:term:`exception view` is rendered successfully: in such a case, the +:attr:`request.exception` attribute of the request when it enters a response +callback will be an exception object instead of its default value of +``None``. Response callbacks are called in the order they're added (first-to-most-recently-added). All response callbacks are called *after* @@ -537,13 +471,13 @@ of a :class:`pyramid.events.NewRequest` event). Using Finished Callbacks ------------------------ -A :term:`finished callback` is a function that will be called -unconditionally by the :app:`Pyramid` :term:`router` at the very -end of request processing. A finished callback can be used to perform -an action at the end of a request unconditionally. +A :term:`finished callback` is a function that will be called unconditionally +by the :app:`Pyramid` :term:`router` at the very end of request processing. +A finished callback can be used to perform an action at the end of a request +unconditionally. -The :meth:`pyramid.request.Request.add_finished_callback` method is -used to register a finished callback. +The :meth:`pyramid.request.Request.add_finished_callback` method is used to +register a finished callback. A finished callback is a callable which accepts a single positional parameter: ``request``. For example: @@ -563,25 +497,24 @@ parameter: ``request``. For example: Finished callbacks are called in the order they're added ( first- to most-recently- added). Finished callbacks (unlike a :term:`response -callback`) are *always* called, even if an exception happens in -application code that prevents a response from being generated. - -The set of finished callbacks associated with a request are called -*very late* in the processing of that request; they are essentially -the very last thing called by the :term:`router` before a request -"ends". They are called after response processing has already occurred -in a top-level ``finally:`` block within the router request processing -code. As a result, mutations performed to the ``request`` provided to -a finished callback will have no meaningful effect, because response -processing will have already occurred, and the request's scope will -expire almost immediately after all finished callbacks have been -processed. +callback`) are *always* called, even if an exception happens in application +code that prevents a response from being generated. + +The set of finished callbacks associated with a request are called *very +late* in the processing of that request; they are essentially the very last +thing called by the :term:`router` before a request "ends". They are called +after response processing has already occurred in a top-level ``finally:`` +block within the router request processing code. As a result, mutations +performed to the ``request`` provided to a finished callback will have no +meaningful effect, because response processing will have already occurred, +and the request's scope will expire almost immediately after all finished +callbacks have been processed. It is often necessary to tell whether an exception occurred within -:term:`view callable` code from within a finished callback: in such a -case, the :attr:`request.exception` attribute of the request when it -enters a response callback will be an exception object instead of its -default value of ``None``. +:term:`view callable` code from within a finished callback: in such a case, +the :attr:`request.exception` attribute of the request when it enters a +response callback will be an exception object instead of its default value of +``None``. Errors raised by finished callbacks are not handled specially. They will be propagated to the caller of the :app:`Pyramid` router @@ -598,22 +531,21 @@ Registering Configuration Decorators ------------------------------------ Decorators such as :class:`pyramid.view.view_config` don't change the -behavior of the functions or classes they're decorating. Instead, -when a :term:`scan` is performed, a modified version of the function -or class is registered with :app:`Pyramid`. - -You may wish to have your own decorators that offer such -behaviour. This is possible by using the :term:`Venusian` package in -the same way that it is used by :app:`Pyramid`. - -By way of example, let's suppose you want to write a decorator that -registers the function it wraps with a :term:`Zope Component -Architecture` "utility" within the :term:`application registry` -provided by :app:`Pyramid`. The application registry and the -utility inside the registry is likely only to be available once your -application's configuration is at least partially completed. A normal -decorator would fail as it would be executed before the configuration -had even begun. +behavior of the functions or classes they're decorating. Instead, when a +:term:`scan` is performed, a modified version of the function or class is +registered with :app:`Pyramid`. + +You may wish to have your own decorators that offer such behaviour. This is +possible by using the :term:`Venusian` package in the same way that it is +used by :app:`Pyramid`. + +By way of example, let's suppose you want to write a decorator that registers +the function it wraps with a :term:`Zope Component Architecture` "utility" +within the :term:`application registry` provided by :app:`Pyramid`. The +application registry and the utility inside the registry is likely only to be +available once your application's configuration is at least partially +completed. A normal decorator would fail as it would be executed before the +configuration had even begun. However, using :term:`Venusian`, the decorator could be written as follows: -- cgit v1.2.3 From d29fa9b439554f135e564df5808782fbcf046122 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 26 Dec 2010 01:20:09 -0500 Subject: remove unused import --- docs/narr/static.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/narr/static.rst b/docs/narr/static.rst index d4f6da76d..bf1e57de8 100644 --- a/docs/narr/static.rst +++ b/docs/narr/static.rst @@ -284,7 +284,6 @@ do so, do things "by hand". First define the view callable. :linenos: import os - from pyramid.view import view_config from webob import Response def favicon_view(request): -- cgit v1.2.3 From 74abc0fa8c4f63db0f2fef238e9ef8af16f3c8d3 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 26 Dec 2010 01:46:33 -0500 Subject: reorder chapter --- docs/narr/hooks.rst | 304 ++++++++++++++++++++++++++++------------------------ 1 file changed, 161 insertions(+), 143 deletions(-) diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index 381da0d39..238ac8328 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -142,149 +142,7 @@ For information about how to configure a forbidden view via :term:`ZCML`, see :ref:`forbidden_zcml`. .. index:: - single: traverser - -.. _changing_the_traverser: - -Changing the Traverser ----------------------- - -The default :term:`traversal` algorithm that :app:`Pyramid` uses is explained -in :ref:`traversal_algorithm`. Though it is rarely necessary, this default -algorithm can be swapped out selectively for a different traversal pattern -via configuration. - -.. code-block:: python - :linenos: - - from pyramid.interfaces import ITraverser - from zope.interface import Interface - from myapp.traversal import Traverser - - config.registry.registerAdapter(Traverser, (Interface,), ITraverser) - -In the example above, ``myapp.traversal.Traverser`` is assumed to be a class -that implements the following interface: - -.. code-block:: python - :linenos: - - class Traverser(object): - def __init__(self, root): - """ Accept the root object returned from the root factory """ - - def __call__(self, request): - """ Return a dictionary with (at least) the keys ``root``, - ``context``, ``view_name``, ``subpath``, ``traversed``, - ``virtual_root``, and ``virtual_root_path``. These values are - typically the result of a resource tree traversal. ``root`` - is the physical root object, ``context`` will be a resource - object, ``view_name`` will be the view name used (a Unicode - name), ``subpath`` will be a sequence of Unicode names that - followed the view name but were not traversed, ``traversed`` - will be a sequence of Unicode names that were traversed - (including the virtual root path, if any) ``virtual_root`` - will be a resource object representing the virtual root (or the - physical root if traversal was not performed), and - ``virtual_root_path`` will be a sequence representing the - virtual root path (a sequence of Unicode names) or None if - traversal was not performed. - - Extra keys for special purpose functionality can be added as - necessary. - - All values returned in the dictionary will be made available - as attributes of the ``request`` object. - """ - -More than one traversal algorithm can be active at the same time. For -instance, if your :term:`root factory` returns more than one type of object -conditionally, you could claim that an alternate traverser adapter is ``for`` -only one particular class or interface. When the root factory returned an -object that implemented that class or interface, a custom traverser would be -used. Otherwise, the default traverser would be used. For example: - -.. code-block:: python - :linenos: - - from pyramid.interfaces import ITraverser - from zope.interface import Interface - from myapp.traversal import Traverser - from myapp.resources import MyRoot - - config.registry.registerAdapter(Traverser, (MyRoot,), ITraverser) - -If the above stanza was added to a Pyramid ``__init__.py`` file's ``main`` -function, :app:`Pyramid` would use the ``myapp.traversal.Traverser`` only -when the application :term:`root factory` returned an instance of the -``myapp.resources.MyRoot`` object. Otherwise it would use the default -:app:`Pyramid` traverser to do traversal. - -For information about how to configure an alternate traverser via -:term:`ZCML`, see :ref:`changing_traverser_zcml`. - -.. index:: - single: url generator - -.. _changing_resource_url: - -Changing How :mod:`pyramid.url.resource_url` Generates a URL ------------------------------------------------------------- - -When you add a traverser as described in :ref:`changing_the_traverser`, it's -often convenient to continue to use the :func:`pyramid.url.resource_url` API. -However, since the way traversal is done will have been modified, the URLs it -generates by default may be incorrect. - -If you've added a traverser, you can change how -:func:`pyramid.url.resource_url` generates a URL for a specific type of -resource by adding a registerAdapter call for -:class:`pyramid.interfaces.IContextURL` to your application: - -.. code-block:: python - :linenos: - - from pyramid.interfaces import ITraverser - from zope.interface import Interface - from myapp.traversal import URLGenerator - from myapp.resources import MyRoot - - config.registry.registerAdapter(URLGenerator, (MyRoot, Interface), - IContextURL) - -In the above example, the ``myapp.traversal.URLGenerator`` class will be used -to provide services to :func:`pyramid.url.resource_url` any time the -:term:`context` passed to ``resource_url`` is of class -``myapp.resources.MyRoot``. The second argument in the ``(MyRoot, -Interface)`` tuple represents the type of interface that must be possessed by -the :term:`request` (in this case, any interface, represented by -``zope.interface.Interface``). - -The API that must be implemented by a class that provides -:class:`pyramid.interfaces.IContextURL` is as follows: - -.. code-block:: python - :linenos: - - from zope.interface import Interface - - class IContextURL(Interface): - """ An adapter which deals with URLs related to a context. - """ - def __init__(self, context, request): - """ Accept the context and request """ - - def virtual_root(self): - """ Return the virtual root object related to a request and the - current context""" - - def __call__(self): - """ Return a URL that points to the context """ - -The default context URL generator is available for perusal as the class -:class:`pyramid.traversal.TraversalContextURL` in the `traversal module -`_ of the -:term:`Pylons` GitHub Pyramid repository. + single: request factory .. _changing_the_request_factory: @@ -329,6 +187,9 @@ already constructed a :term:`configurator` it can also be registered via the To use ZCML for the same purpose, see :ref:`changing_request_factory_zcml`. +.. index:: + single: renderer globals + .. _adding_renderer_globals: Adding Renderer Globals @@ -384,6 +245,9 @@ exists in :ref:`beforerender_event`. If you'd rather ZCML to register a renderer globals factory, see :ref:`adding_renderer_globals_zcml`. +.. index:: + single: before render event + .. _beforerender_event: Using The Before Render Event @@ -422,6 +286,9 @@ interface at :class:`pyramid.interfaces.IBeforeRender`. Another mechanism which allows event subscribers more control when adding renderer global values exists in :ref:`adding_renderer_globals`. +.. index:: + single: response callback + .. _using_response_callbacks: Using Response Callbacks @@ -466,6 +333,9 @@ response callback to happen as the result of *every* request, you must re-register the callback into every new request (perhaps within a subscriber of a :class:`pyramid.events.NewRequest` event). +.. index:: + single: finished callback + .. _using_finished_callbacks: Using Finished Callbacks @@ -525,6 +395,154 @@ finished callback to happen as the result of *every* request, you must re-register the callback into every new request (perhaps within a subscriber of a :class:`pyramid.events.NewRequest` event). +.. index:: + single: traverser + +.. _changing_the_traverser: + +Changing the Traverser +---------------------- + +The default :term:`traversal` algorithm that :app:`Pyramid` uses is explained +in :ref:`traversal_algorithm`. Though it is rarely necessary, this default +algorithm can be swapped out selectively for a different traversal pattern +via configuration. + +.. code-block:: python + :linenos: + + from pyramid.interfaces import ITraverser + from zope.interface import Interface + from myapp.traversal import Traverser + + config.registry.registerAdapter(Traverser, (Interface,), ITraverser) + +In the example above, ``myapp.traversal.Traverser`` is assumed to be a class +that implements the following interface: + +.. code-block:: python + :linenos: + + class Traverser(object): + def __init__(self, root): + """ Accept the root object returned from the root factory """ + + def __call__(self, request): + """ Return a dictionary with (at least) the keys ``root``, + ``context``, ``view_name``, ``subpath``, ``traversed``, + ``virtual_root``, and ``virtual_root_path``. These values are + typically the result of a resource tree traversal. ``root`` + is the physical root object, ``context`` will be a resource + object, ``view_name`` will be the view name used (a Unicode + name), ``subpath`` will be a sequence of Unicode names that + followed the view name but were not traversed, ``traversed`` + will be a sequence of Unicode names that were traversed + (including the virtual root path, if any) ``virtual_root`` + will be a resource object representing the virtual root (or the + physical root if traversal was not performed), and + ``virtual_root_path`` will be a sequence representing the + virtual root path (a sequence of Unicode names) or None if + traversal was not performed. + + Extra keys for special purpose functionality can be added as + necessary. + + All values returned in the dictionary will be made available + as attributes of the ``request`` object. + """ + +More than one traversal algorithm can be active at the same time. For +instance, if your :term:`root factory` returns more than one type of object +conditionally, you could claim that an alternate traverser adapter is ``for`` +only one particular class or interface. When the root factory returned an +object that implemented that class or interface, a custom traverser would be +used. Otherwise, the default traverser would be used. For example: + +.. code-block:: python + :linenos: + + from pyramid.interfaces import ITraverser + from zope.interface import Interface + from myapp.traversal import Traverser + from myapp.resources import MyRoot + + config.registry.registerAdapter(Traverser, (MyRoot,), ITraverser) + +If the above stanza was added to a Pyramid ``__init__.py`` file's ``main`` +function, :app:`Pyramid` would use the ``myapp.traversal.Traverser`` only +when the application :term:`root factory` returned an instance of the +``myapp.resources.MyRoot`` object. Otherwise it would use the default +:app:`Pyramid` traverser to do traversal. + +For information about how to configure an alternate traverser via +:term:`ZCML`, see :ref:`changing_traverser_zcml`. + +.. index:: + single: url generator + +.. _changing_resource_url: + +Changing How :mod:`pyramid.url.resource_url` Generates a URL +------------------------------------------------------------ + +When you add a traverser as described in :ref:`changing_the_traverser`, it's +often convenient to continue to use the :func:`pyramid.url.resource_url` API. +However, since the way traversal is done will have been modified, the URLs it +generates by default may be incorrect. + +If you've added a traverser, you can change how +:func:`pyramid.url.resource_url` generates a URL for a specific type of +resource by adding a registerAdapter call for +:class:`pyramid.interfaces.IContextURL` to your application: + +.. code-block:: python + :linenos: + + from pyramid.interfaces import ITraverser + from zope.interface import Interface + from myapp.traversal import URLGenerator + from myapp.resources import MyRoot + + config.registry.registerAdapter(URLGenerator, (MyRoot, Interface), + IContextURL) + +In the above example, the ``myapp.traversal.URLGenerator`` class will be used +to provide services to :func:`pyramid.url.resource_url` any time the +:term:`context` passed to ``resource_url`` is of class +``myapp.resources.MyRoot``. The second argument in the ``(MyRoot, +Interface)`` tuple represents the type of interface that must be possessed by +the :term:`request` (in this case, any interface, represented by +``zope.interface.Interface``). + +The API that must be implemented by a class that provides +:class:`pyramid.interfaces.IContextURL` is as follows: + +.. code-block:: python + :linenos: + + from zope.interface import Interface + + class IContextURL(Interface): + """ An adapter which deals with URLs related to a context. + """ + def __init__(self, context, request): + """ Accept the context and request """ + + def virtual_root(self): + """ Return the virtual root object related to a request and the + current context""" + + def __call__(self): + """ Return a URL that points to the context """ + +The default context URL generator is available for perusal as the class +:class:`pyramid.traversal.TraversalContextURL` in the `traversal module +`_ of the +:term:`Pylons` GitHub Pyramid repository. + +.. index:: + single: configuration decorator + .. _registering_configuration_decorators: Registering Configuration Decorators -- cgit v1.2.3 From 88b9ee766bf53ae1c46b8a1889674fea08053622 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 26 Dec 2010 15:57:28 -0500 Subject: - Merge "Static Assets" chapter into the "Assets" chapter. --- CHANGES.txt | 2 + docs/index.rst | 3 +- docs/latexindex.rst | 3 +- docs/narr/assets.rst | 345 ++++++++++++++++++++++++++++++++++++++++++++++++--- docs/narr/static.rst | 319 ----------------------------------------------- 5 files changed, 333 insertions(+), 339 deletions(-) delete mode 100644 docs/narr/static.rst diff --git a/CHANGES.txt b/CHANGES.txt index 127ea810e..e58a1dc76 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -79,6 +79,8 @@ Documentation - Move ZCML usage in Hooks chapter to Declarative Configuration chapter. +- Merge "Static Assets" chapter into the "Assets" chapter. + 1.0a7 (2010-12-20) ================== diff --git a/docs/index.rst b/docs/index.rst index 3830a83f9..23ffb3b1b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -45,7 +45,7 @@ Narrative documentation in chapter form explaining how to use narr/renderers narr/templates narr/resources - narr/static + narr/assets narr/webob narr/sessions narr/flash @@ -61,7 +61,6 @@ Narrative documentation in chapter form explaining how to use narr/advconfig narr/declarative narr/extending - narr/assets narr/router narr/threadlocals narr/zca diff --git a/docs/latexindex.rst b/docs/latexindex.rst index 2d4644416..25e6791d0 100644 --- a/docs/latexindex.rst +++ b/docs/latexindex.rst @@ -38,7 +38,7 @@ Narrative Documentation narr/renderers narr/templates narr/resources - narr/static + narr/assets narr/webob narr/sessions narr/flash @@ -54,7 +54,6 @@ Narrative Documentation narr/advconfig narr/declarative narr/extending - narr/assets narr/router narr/startup narr/threadlocals diff --git a/docs/narr/assets.rst b/docs/narr/assets.rst index 1932e19ff..a49b401d0 100644 --- a/docs/narr/assets.rst +++ b/docs/narr/assets.rst @@ -37,29 +37,29 @@ Understanding Assets Let's imagine you've created a :app:`Pyramid` application that uses a :term:`Chameleon` ZPT template via the -:func:`pyramid.chameleon_zpt.render_template_to_response` API. For example, -the application might address the asset named ``templates/some_template.pt`` -using that API within a ``views.py`` file inside a ``myapp`` package: +:func:`pyramid.renderers.render_to_response` API. For example, the +application might address the asset using the asset specification +``myapp:templates/some_template.pt`` using that API within a ``views.py`` +file inside a ``myapp`` package: .. ignore-next-block .. code-block:: python :linenos: - from pyramid.chameleon_zpt import render_template_to_response - render_template_to_response('templates/some_template.pt') + from pyramid.renderers import render_to_response + render_to_response('myapp:templates/some_template.pt', {}, request) -"Under the hood", when this API is called, :app:`Pyramid` attempts -to make sense out of the string ``templates/some_template.pt`` -provided by the developer. To do so, it first finds the "current" -package. The "current" package is the Python package in which the -``views.py`` module which contains this code lives. This would be the -``myapp`` package, according to our example so far. By resolving the -current package, :app:`Pyramid` has enough information to locate -the actual template file. These are the elements it needs: +"Under the hood", when this API is called, :app:`Pyramid` attempts to make +sense out of the string ``myapp:templates/some_template.pt`` provided by the +developer. This string is an :term:`asset specification`. It is composed of +two parts: - The *package name* (``myapp``) -- The *asset name* (``templates/some_template.pt``) +- The *asset name* (``templates/some_template.pt``), relative to the package + directory. + +The two parts are separated by the colon character. :app:`Pyramid` uses the :term:`pkg_resources` API to resolve the package name and asset name to an absolute (operating-system-specific) file name. It @@ -67,8 +67,321 @@ eventually passes this resolved absolute filesystem path to the Chameleon templating engine, which then uses it to load, parse, and execute the template file. -Package names often contain dots. For example, ``pyramid`` is a package. -Asset names usually look a lot like relative UNIX file paths. +.. index:: + single: add_static_view + +.. _static_assets_section: + +Serving Static Assets +--------------------- + +:app:`Pyramid` makes it possible to serve up static asset files from a +directory on a filesystem to an application user's browser. Use the +:meth:`pyramid.config.Configurator.add_static_view` to instruct +:app:`Pyramid` to serve static assets such as JavaScript and CSS files. This +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 + 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`. + +Here's an example of a use of +:meth:`~pyramid.config.Configurator.add_static_view` that will serve files up +from the ``/var/www/static`` directory of the computer which runs the +:app:`Pyramid` application as URLs beneath the ``/static`` URL prefix. + +.. code-block:: python + :linenos: + + # config is an instance of pyramid.config.Configurator + config.add_static_view(name='static', path='/var/www/static') + +The ``name`` prepresents a URL *prefix*. In order for files that live in the +``path`` directory to be served, a URL that requests one of them must begin +with that prefix. In the example above, ``name`` is ``static``, and ``path`` +is ``/var/www/static``. In English, this means that you wish to serve the +files that live in ``/var/www/static`` as sub-URLs of the ``/static`` URL +prefix. Therefore, the file ``/var/www/static/foo.css`` will be returned +when the user visits your application's URL ``/static/foo.css``. + +A static directory named at ``path`` may contain subdirectories recursively, +and any subdirectories may hold files; these will be resolved by the static +view as you would expect. The ``Content-Type`` header returned by the static +view for each particular type of file is dependent upon its file extension. + +By default, all files made available via +:meth:`~pyramid.config.Configurator.add_static_view` are accessible by +completely anonymous users. Simple authorization can be required, however. +To protect a set of static files using a permission, in addition to passing +the required ``name`` and ``path`` arguments, also pass the ``permission`` +keyword argument to :meth:`~pyramid.config.Configurator.add_static_view`. +The value of the ``permission`` argument represents the :term:`permission` +that the user must have relative to the current :term:`context` when the +static view is invoked. A user will be required to possess this permission +to view any of the files represented by ``path`` of the static view. If your +static resources must be protected by a more complex authorization scheme, +see :ref:`advanced_static`. + +Here's another example that uses an :term:`asset specification` instead of an +absolute path as the ``path`` argument. To convince +:meth:`pyramid.config.Configurator.add_static_view` to serve files up under +the ``/static`` URL from the ``a/b/c/static`` directory of the Python package +named ``some_package``, we can use a fully qualified :term:`asset +specification` as the ``path``: + +.. code-block:: python + :linenos: + + # config is an instance of pyramid.config.Configurator + config.add_static_view(name='static', path='some_package:a/b/c/static') + +The ``path`` provided to :meth:`pyramid.config.Configurator.add_static_view` +may be a fully qualified :term:`asset specification`, or an *absolute path*. + +Instead of representing a URL prefix, the ``name`` argument of a call to +:meth:`pyramid.config.Configurator.add_static_view` can alternately be a +*URL*. Each of examples we've seen so far have shown usage of the ``name`` +argument as a URL prefix. However, when ``name`` is a *URL*, static assets +can be served from an external webserver. In this mode, the ``name`` is used +as the URL prefix when generating a URL using :func:`pyramid.url.static_url`. + +For example, :meth:`pyramid.config.Configurator.add_static_view` may +be fed a ``name`` argument which is ``http://example.com/images``: + +.. code-block:: python + :linenos: + + # config is an instance of pyramid.config.Configurator + config.add_static_view(name='http://example.com/images', + path='mypackage:images') + +Because :meth:`pyramid.config.Configurator.add_static_view` is provided with +a ``name`` argument that is the URL ``http://example.com/images``, subsequent +calls to :func:`pyramid.url.static_url` with paths that start with the +``path`` argument passed to +:meth:`pyramid.config.Configurator.add_static_view` will generate a URL +something like ``http://example.com/images/logo.png``. The external +webserver listening on ``example.com`` must be itself configured to respond +properly to such a request. The :func:`pyramid.url.static_url` API is +discussed in more detail later in this chapter. + +.. note:: + + The :ref:`static_directive` ZCML directive offers an declarative + equivalent to :meth:`pyramid.config.Configurator.add_static_view`. Use of + the :ref:`static_directive` ZCML directive is completely equivalent to + using imperative configuration for the same purpose. + +.. index:: + single: generating static asset urls + single: static asset urls + +.. _generating_static_asset_urls: + +Generating Static Asset URLs +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When a :meth:`pyramid.config.Configurator.add_static_view` method is used to +register a static asset directory, a special helper API named +:func:`pyramid.url.static_url` can be used to generate the appropriate URL +for an asset that lives in one of the directories named by the static +registration ``path`` attribute. + +For example, let's assume you create a set of static declarations like so: + +.. code-block:: python + :linenos: + + config.add_static_view(name='static1', path='mypackage:assets/1') + config.add_static_view(name='static2', path='mypackage:assets/2') + +These declarations create URL-accessible directories which have URLs that +begin with ``/static1`` and ``/static2``, respectively. The assets in the +``assets/1`` directory of the ``mypackage`` package are consulted when a user +visits a URL which begins with ``/static1``, and the assets in the +``assets/2`` directory of the ``mypackage`` package are consulted when a user +visits a URL which begins with ``/static2``. + +You needn't generate the URLs to static assets "by hand" in such a +configuration. Instead, use the :func:`pyramid.url.static_url` API to +generate them for you. For example: + +.. code-block:: python + :linenos: + + from pyramid.url import static_url + from pyramid.chameleon_zpt import render_template_to_response + + def my_view(request): + css_url = static_url('mypackage:assets/1/foo.css', request) + js_url = static_url('mypackage:assets/2/foo.js', request) + return render_template_to_response('templates/my_template.pt', + css_url = css_url, + js_url = js_url) + +If the request "application URL" of the running system is +``http://example.com``, the ``css_url`` generated above would be: +``http://example.com/static1/foo.css``. The ``js_url`` generated +above would be ``http://example.com/static2/foo.js``. + +One benefit of using the :func:`pyramid.url.static_url` function rather than +constructing static URLs "by hand" is that if you need to change the ``name`` +of a static URL declaration, the generated URLs will continue to resolve +properly after the rename. + +URLs may also be generated by :func:`pyramid.url.static_url` to static assets +that live *outside* the :app:`Pyramid` application. This will happen when +the :meth:`pyramid.config.Configurator.add_static_view` API associated with +the path fed to :func:`pyramid.url.static_url` is a *URL* instead of a view +name. For example, the ``name`` argument may be ``http://example.com`` while +the the ``path`` given may be ``mypackage:images``: + +.. code-block:: python + :linenos: + + config.add_static_view(name='http://example.com/images', + path='mypackage:images') + +Under such a configuration, the URL generated by ``static_url`` for +assets which begin with ``mypackage:images`` will be prefixed with +``http://example.com/images``: + +.. code-block:: python + :linenos: + + static_url('mypackage:images/logo.png', request) + # -> http://example.com/images/logo.png + +Using :func:`pyramid.url.static_url` in conjunction with a +:meth:`pyramid.configuration.Configurator.add_static_view` makes it possible +to put static media on a separate webserver during production (if the +``name`` argument to :meth:`pyramid.config.Configurator.add_static_view` is a +URL), while keeping static media package-internal and served by the +development webserver during development (if the ``name`` argument to +:meth:`pyramid.config.Configurator.add_static_view` is a URL prefix). To +create such a circumstance, we suggest using the +:attr:`pyramid.registry.Registry.settings` API in conjunction with a setting +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 +suggestion for a pattern; any setting name other than ``media_location`` +could be used. + +.. index:: + single: static assets view + +.. _advanced_static: + +Advanced: Serving Static Assets Using a View Callable +----------------------------------------------------- + +For more flexibility, static assets can be served by a :term:`view callable` +which you register manually. For example, if you're using :term:`URL +dispatch`, you may want static assets to only be available as a fallback if +no previous route matches. Alternately, you might like to serve a particular +static asset manually, because its download requires authentication. + +Note that you cannot use the :func:`pyramid.url.static_url` API to generate +URLs against assets made accessible by registering a custom static view. + +Root-Relative Custom Static View (URL Dispatch Only) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The :class:`pyramid.view.static` helper class generates a Pyramid view +callable. This view callable can serve static assets from a directory. An +instance of this class is actually used by the +:meth:`pyramid.config.Configurator.add_static_view` configuration method, so +its behavior is almost exactly the same once it's configured. + +.. warning:: The following example *will not work* for applications that use + :term:`traversal`, it will only work if you use :term:`URL dispatch` + exclusively. The root-relative route we'll be registering will always be + matched before traversal takes place, subverting any views registered via + ``add_view`` (at least those without a ``route_name``). A + :class:`pyramid.view.static` static view cannot be made root-relative when + you use traversal. + +To serve files within a directory located on your filesystem at +``/path/to/static/dir`` as the result of a "catchall" route hanging from the +root that exists at the end of your routing table, create an instance of the +:class:`pyramid.view.static` class inside a ``static.py`` file in your +application root as below. + +.. ignore-next-block +.. code-block:: python + :linenos: + + from pyramid.view import static + static_view = static('/path/to/static/dir') + +.. note:: For better cross-system flexibility, use an :term:`asset + specification` as the argument to :class:`pyramid.view.static` instead of + a physical absolute filesystem path, e.g. ``mypackage:static`` instead of + ``/path/to/mypackage/static``. + +Subsequently, you may wire the files that are served by this view up to be +accessible as ``/`` using a configuration method in your +application's startup code. + +.. code-block:: python + :linenos: + + # .. every other add_route and/or add_handler declaration should come + # before this one, as it will, by default, catch all requests + + config.add_route('catchall_static', '/*subpath', 'myapp.static.static_view') + +The special name ``*subpath`` above is used by the +:class:`pyramid.view.static` view callable to signify the path of the file +relative to the directory you're serving. + +Registering A View Callable to Serve a "Static" Asset +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can register a simple view callable to serve a single static asset. To +do so, do things "by hand". First define the view callable. + +.. code-block:: python + :linenos: + + import os + from webob import Response + + def favicon_view(request): + here = os.path.dirname(__file__) + icon = open(os.path.join(here, 'static', 'favicon.ico')) + return Response(content_type='image/x-icon', app_iter=icon) + +The above bit of code within ``favicon_view`` computes "here", which is a +path relative to the Python file in which the function is defined. It then +uses the Python ``open`` function to obtain a file handle to a file within +"here" named ``static``, and returns a response using the open the file +handle as the response's ``app_iter``. It makes sure to set the right +content_type too. + +You might register such a view via configuration as a view callable that +should be called as the result of a traversal: + +.. code-block:: python + :linenos: + + config.add_view('myapp.views.favicon_view', name='favicon.ico') + +Or you might register it to be the view callable for a particular route: + +.. code-block:: python + :linenos: + + config.add_route('favicon', '/favicon.ico', + view='myapp.views.favicon_view') + +Because this is a simple view callable, it can be protected with a +:term:`permission` or can be configured to respond under different +circumstances using :term:`view predicate` arguments. + .. index:: pair: overriding; assets diff --git a/docs/narr/static.rst b/docs/narr/static.rst deleted file mode 100644 index bf1e57de8..000000000 --- a/docs/narr/static.rst +++ /dev/null @@ -1,319 +0,0 @@ -Static Assets -============= - -:app:`Pyramid` makes it possible to serve up static asset files from a -directory on a filesystem. This chapter describes how to configure -:app:`Pyramid` to do so. - -.. index:: - single: add_static_view - -.. _static_assets_section: - -Serving Static Assets ---------------------- - -Use the :meth:`pyramid.config.Configurator.add_static_view` to instruct -:app:`Pyramid` to serve static assets such as JavaScript and CSS files. This -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 - 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`. - -Here's an example of a use of -:meth:`~pyramid.config.Configurator.add_static_view` that will serve files up -from the ``/var/www/static`` directory of the computer which runs the -:app:`Pyramid` application as URLs beneath the ``/static`` URL prefix. - -.. code-block:: python - :linenos: - - # config is an instance of pyramid.config.Configurator - config.add_static_view(name='static', path='/var/www/static') - -The ``name`` prepresents a URL *prefix*. In order for files that live in the -``path`` directory to be served, a URL that requests one of them must begin -with that prefix. In the example above, ``name`` is ``static``, and ``path`` -is ``/var/www/static``. In English, this means that you wish to serve the -files that live in ``/var/www/static`` as sub-URLs of the ``/static`` URL -prefix. Therefore, the file ``/var/www/static/foo.css`` will be returned -when the user visits your application's URL ``/static/foo.css``. - -A static directory named at ``path`` may contain subdirectories recursively, -and any subdirectories may hold files; these will be resolved by the static -view as you would expect. The ``Content-Type`` header returned by the static -view for each particular type of file is dependent upon its file extension. - -By default, all files made available via -:meth:`~pyramid.config.Configurator.add_static_view` are accessible by -completely anonymous users. Simple authorization can be required, however. -To protect a set of static files using a permission, in addition to passing -the required ``name`` and ``path`` arguments, also pass the ``permission`` -keyword argument to :meth:`~pyramid.config.Configurator.add_static_view`. -The value of the ``permission`` argument represents the :term:`permission` -that the user must have relative to the current :term:`context` when the -static view is invoked. A user will be required to possess this permission -to view any of the files represented by ``path`` of the static view. If your -static resources must be protected by a more complex authorization scheme, -see :ref:`advanced_static`. - -Here's another example that uses an :term:`asset specification` instead of an -absolute path as the ``path`` argument. To convince -:meth:`pyramid.config.Configurator.add_static_view` to serve files up under -the ``/static`` URL from the ``a/b/c/static`` directory of the Python package -named ``some_package``, we can use a fully qualified :term:`asset -specification` as the ``path``: - -.. code-block:: python - :linenos: - - # config is an instance of pyramid.config.Configurator - config.add_static_view(name='static', path='some_package:a/b/c/static') - -The ``path`` provided to :meth:`pyramid.config.Configurator.add_static_view` -may be a fully qualified :term:`asset specification`, or an *absolute path*. - -Instead of representing a URL prefix, the ``name`` argument of a call to -:meth:`pyramid.config.Configurator.add_static_view` can alternately be a -*URL*. Each of examples we've seen so far have shown usage of the ``name`` -argument as a URL prefix. However, when ``name`` is a *URL*, static assets -can be served from an external webserver. In this mode, the ``name`` is used -as the URL prefix when generating a URL using :func:`pyramid.url.static_url`. - -For example, :meth:`pyramid.config.Configurator.add_static_view` may -be fed a ``name`` argument which is ``http://example.com/images``: - -.. code-block:: python - :linenos: - - # config is an instance of pyramid.config.Configurator - config.add_static_view(name='http://example.com/images', - path='mypackage:images') - -Because :meth:`pyramid.config.Configurator.add_static_view` is provided with -a ``name`` argument that is the URL ``http://example.com/images``, subsequent -calls to :func:`pyramid.url.static_url` with paths that start with the -``path`` argument passed to -:meth:`pyramid.config.Configurator.add_static_view` will generate a URL -something like ``http://example.com/images/logo.png``. The external -webserver listening on ``example.com`` must be itself configured to respond -properly to such a request. The :func:`pyramid.url.static_url` API is -discussed in more detail later in this chapter. - -.. note:: - - The :ref:`static_directive` ZCML directive offers an declarative - equivalent to :meth:`pyramid.config.Configurator.add_static_view`. Use of - the :ref:`static_directive` ZCML directive is completely equivalent to - using imperative configuration for the same purpose. - -.. index:: - single: generating static asset urls - single: static asset urls - -.. _generating_static_asset_urls: - -Generating Static Asset URLs -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -When a :meth:`pyramid.config.Configurator.add_static_view` method is used to -register a static asset directory, a special helper API named -:func:`pyramid.url.static_url` can be used to generate the appropriate URL -for an asset that lives in one of the directories named by the static -registration ``path`` attribute. - -For example, let's assume you create a set of static declarations like so: - -.. code-block:: python - :linenos: - - config.add_static_view(name='static1', path='mypackage:assets/1') - config.add_static_view(name='static2', path='mypackage:assets/2') - -These declarations create URL-accessible directories which have URLs that -begin with ``/static1`` and ``/static2``, respectively. The assets in the -``assets/1`` directory of the ``mypackage`` package are consulted when a user -visits a URL which begins with ``/static1``, and the assets in the -``assets/2`` directory of the ``mypackage`` package are consulted when a user -visits a URL which begins with ``/static2``. - -You needn't generate the URLs to static assets "by hand" in such a -configuration. Instead, use the :func:`pyramid.url.static_url` API to -generate them for you. For example: - -.. code-block:: python - :linenos: - - from pyramid.url import static_url - from pyramid.chameleon_zpt import render_template_to_response - - def my_view(request): - css_url = static_url('mypackage:assets/1/foo.css', request) - js_url = static_url('mypackage:assets/2/foo.js', request) - return render_template_to_response('templates/my_template.pt', - css_url = css_url, - js_url = js_url) - -If the request "application URL" of the running system is -``http://example.com``, the ``css_url`` generated above would be: -``http://example.com/static1/foo.css``. The ``js_url`` generated -above would be ``http://example.com/static2/foo.js``. - -One benefit of using the :func:`pyramid.url.static_url` function rather than -constructing static URLs "by hand" is that if you need to change the ``name`` -of a static URL declaration, the generated URLs will continue to resolve -properly after the rename. - -URLs may also be generated by :func:`pyramid.url.static_url` to static assets -that live *outside* the :app:`Pyramid` application. This will happen when -the :meth:`pyramid.config.Configurator.add_static_view` API associated with -the path fed to :func:`pyramid.url.static_url` is a *URL* instead of a view -name. For example, the ``name`` argument may be ``http://example.com`` while -the the ``path`` given may be ``mypackage:images``: - -.. code-block:: python - :linenos: - - config.add_static_view(name='http://example.com/images', - path='mypackage:images') - -Under such a configuration, the URL generated by ``static_url`` for -assets which begin with ``mypackage:images`` will be prefixed with -``http://example.com/images``: - -.. code-block:: python - :linenos: - - static_url('mypackage:images/logo.png', request) - # -> http://example.com/images/logo.png - -Using :func:`pyramid.url.static_url` in conjunction with a -:meth:`pyramid.configuration.Configurator.add_static_view` makes it possible -to put static media on a separate webserver during production (if the -``name`` argument to :meth:`pyramid.config.Configurator.add_static_view` is a -URL), while keeping static media package-internal and served by the -development webserver during development (if the ``name`` argument to -:meth:`pyramid.config.Configurator.add_static_view` is a URL prefix). To -create such a circumstance, we suggest using the -:attr:`pyramid.registry.Registry.settings` API in conjunction with a setting -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 -suggestion for a pattern; any setting name other than ``media_location`` -could be used. - -.. index:: - single: static assets view - -.. _advanced_static: - -Advanced: Serving Static Assets Using a View Callable ------------------------------------------------------ - -For more flexibility, static assets can be served by a :term:`view callable` -which you register manually. For example, if you're using :term:`URL -dispatch`, you may want static assets to only be available as a fallback if -no previous route matches. Alternately, you might like to serve a particular -static asset manually, because its download requires authentication. - -Note that you cannot use the :func:`pyramid.url.static_url` API to generate -URLs against assets made accessible by registering a custom static view. - -Root-Relative Custom Static View (URL Dispatch Only) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The :class:`pyramid.view.static` helper class generates a Pyramid view -callable. This view callable can serve static assets from a directory. An -instance of this class is actually used by the -:meth:`pyramid.config.Configurator.add_static_view` configuration method, so -its behavior is almost exactly the same once it's configured. - -.. warning:: The following example *will not work* for applications that use - :term:`traversal`, it will only work if you use :term:`URL dispatch` - exclusively. The root-relative route we'll be registering will always be - matched before traversal takes place, subverting any views registered via - ``add_view`` (at least those without a ``route_name``). A - :class:`pyramid.view.static` static view cannot be made root-relative when - you use traversal. - -To serve files within a directory located on your filesystem at -``/path/to/static/dir`` as the result of a "catchall" route hanging from the -root that exists at the end of your routing table, create an instance of the -:class:`pyramid.view.static` class inside a ``static.py`` file in your -application root as below. - -.. ignore-next-block -.. code-block:: python - :linenos: - - from pyramid.view import static - static_view = static('/path/to/static/dir') - -.. note:: For better cross-system flexibility, use an :term:`asset - specification` as the argument to :class:`pyramid.view.static` instead of - a physical absolute filesystem path, e.g. ``mypackage:static`` instead of - ``/path/to/mypackage/static``. - -Subsequently, you may wire the files that are served by this view up to be -accessible as ``/`` using a configuration method in your -application's startup code. - -.. code-block:: python - :linenos: - - # .. every other add_route and/or add_handler declaration should come - # before this one, as it will, by default, catch all requests - - config.add_route('catchall_static', '/*subpath', 'myapp.static.static_view') - -The special name ``*subpath`` above is used by the -:class:`pyramid.view.static` view callable to signify the path of the file -relative to the directory you're serving. - -Registering A View Callable to Serve a "Static" Asset -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You can register a simple view callable to serve a single static asset. To -do so, do things "by hand". First define the view callable. - -.. code-block:: python - :linenos: - - import os - from webob import Response - - def favicon_view(request): - here = os.path.dirname(__file__) - icon = open(os.path.join(here, 'static', 'favicon.ico')) - return Response(content_type='image/x-icon', app_iter=icon) - -The above bit of code within ``favicon_view`` computes "here", which is a -path relative to the Python file in which the function is defined. It then -uses the Python ``open`` function to obtain a file handle to a file within -"here" named ``static``, and returns a response using the open the file -handle as the response's ``app_iter``. It makes sure to set the right -content_type too. - -You might register such a view via configuration as a view callable that -should be called as the result of a traversal: - -.. code-block:: python - :linenos: - - config.add_view('myapp.views.favicon_view', name='favicon.ico') - -Or you might register it to be the view callable for a particular route: - -.. code-block:: python - :linenos: - - config.add_route('favicon', '/favicon.ico', - view='myapp.views.favicon_view') - -Because this is a simple view callable, it can be protected with a -:term:`permission` or can be configured to respond under different -circumstances using :term:`view predicate` arguments. -- cgit v1.2.3 From a10437f0de8636b56bc8fc85220b01494d99888b Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 26 Dec 2010 16:57:42 -0500 Subject: wording --- docs/narr/assets.rst | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/docs/narr/assets.rst b/docs/narr/assets.rst index a49b401d0..27fbfe613 100644 --- a/docs/narr/assets.rst +++ b/docs/narr/assets.rst @@ -1,10 +1,11 @@ .. index:: single: assets + single: static asssets .. _assets_chapter: -Assets -====== +Static Assets +============= An :term:`asset` is any file contained within a Python :term:`package` which is *not* a Python source code file. For example, each of the following is an @@ -26,19 +27,19 @@ example, when you create a :app:`Pyramid` application using one of the available "paster" templates, as described in :ref:`creating_a_project`, the directory representing the application contains a Python :term:`package`. Within that Python package, there are directories full of files which are -assets. For example, there is a ``templates`` directory which contains -``.pt`` files, and a ``static`` directory which contains ``.css``, ``.js``, -and ``.gif`` files. +static assets. For example, there's a ``static`` directory which contains +``.css``, ``.js``, and ``.gif`` files. These asset files are delivered when +a user visits an application URL. .. _understanding_assets: -Understanding Assets --------------------- +Understanding Asset Specifications +---------------------------------- Let's imagine you've created a :app:`Pyramid` application that uses a :term:`Chameleon` ZPT template via the :func:`pyramid.renderers.render_to_response` API. For example, the -application might address the asset using the asset specification +application might address the asset using the :term:`asset specification` ``myapp:templates/some_template.pt`` using that API within a ``views.py`` file inside a ``myapp`` package: -- cgit v1.2.3 From b33ae924870d29f1c1a4c57fde694050a535aba2 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 26 Dec 2010 17:11:57 -0500 Subject: wording --- docs/glossary.rst | 3 ++- docs/narr/assets.rst | 16 ++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/docs/glossary.rst b/docs/glossary.rst index ed96b8afe..49d273197 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -54,7 +54,8 @@ Glossary For example, the asset specification ``my.package:static/baz.css`` identifies the file named ``baz.css`` in the ``static`` subdirectory of the ``my.package`` - Python :term:`package`. + Python :term:`package`. See :ref:`asset_specifications` for more + info. package A directory on disk which contains an ``__init__.py`` file, making diff --git a/docs/narr/assets.rst b/docs/narr/assets.rst index 27fbfe613..84fe42186 100644 --- a/docs/narr/assets.rst +++ b/docs/narr/assets.rst @@ -11,17 +11,21 @@ An :term:`asset` is any file contained within a Python :term:`package` which is *not* a Python source code file. For example, each of the following is an asset: -- a :term:`Chameleon` template file contained within a Python package. +- a GIF image file contained within a Python package or contained within any + subdirectory of a Python package. -- a GIF image file contained within a Python package. +- a CSS file contained within a Python package or contained within any + subdirectory of a Python package. -- a CSS file contained within a Python package. - -- a JavaScript source file contained within a Python package. +- a JavaScript source file contained within a Python package or contained + within any subdirectory of a Python package. - A directory within a package that does not have an ``__init__.py`` in it (if it possessed an ``__init__.py`` it would *be* a package). +- a :term:`Chameleon` or :term:`Mako` template file contained within a Python + package. + The use of assets is quite common in most web development projects. For example, when you create a :app:`Pyramid` application using one of the available "paster" templates, as described in :ref:`creating_a_project`, the @@ -31,7 +35,7 @@ static assets. For example, there's a ``static`` directory which contains ``.css``, ``.js``, and ``.gif`` files. These asset files are delivered when a user visits an application URL. -.. _understanding_assets: +.. _asset_specifications: Understanding Asset Specifications ---------------------------------- -- cgit v1.2.3 From cccde7d6fa003b621383286931540203b66f01f7 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 26 Dec 2010 17:46:02 -0500 Subject: explain relative asset spec --- docs/narr/assets.rst | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/narr/assets.rst b/docs/narr/assets.rst index 84fe42186..be3325a11 100644 --- a/docs/narr/assets.rst +++ b/docs/narr/assets.rst @@ -72,6 +72,15 @@ eventually passes this resolved absolute filesystem path to the Chameleon templating engine, which then uses it to load, parse, and execute the template file. +There is a second form of asset specification: a *relative* asset +specification. Instead of using an "absolute" asset specification which +includes the package name, in certain circumstances you can omit the package +name. For example, you might be able to use ``templates/mytemplate.pt`` +instead of ``myapp:templates/some_template.pt``. Such asset specifications +are usually relative to a "current package." The "current package" is +usually the package which contains the code that *uses* the asset +specification. + .. index:: single: add_static_view @@ -143,7 +152,7 @@ specification` as the ``path``: config.add_static_view(name='static', path='some_package:a/b/c/static') The ``path`` provided to :meth:`pyramid.config.Configurator.add_static_view` -may be a fully qualified :term:`asset specification`, or an *absolute path*. +may be a fully qualified :term:`asset specification` or an *absolute path*. Instead of representing a URL prefix, the ``name`` argument of a call to :meth:`pyramid.config.Configurator.add_static_view` can alternately be a -- cgit v1.2.3 From 4062f104319f471952657947c8eaa29e0622d1e3 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 26 Dec 2010 17:50:33 -0500 Subject: wording --- docs/narr/assets.rst | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/narr/assets.rst b/docs/narr/assets.rst index be3325a11..50879ef22 100644 --- a/docs/narr/assets.rst +++ b/docs/narr/assets.rst @@ -75,11 +75,13 @@ template file. There is a second form of asset specification: a *relative* asset specification. Instead of using an "absolute" asset specification which includes the package name, in certain circumstances you can omit the package -name. For example, you might be able to use ``templates/mytemplate.pt`` -instead of ``myapp:templates/some_template.pt``. Such asset specifications -are usually relative to a "current package." The "current package" is -usually the package which contains the code that *uses* the asset -specification. +name from the specification. For example, you might be able to use +``templates/mytemplate.pt`` instead of ``myapp:templates/some_template.pt``. +Such asset specifications are usually relative to a "current package." The +"current package" is usually the package which contains the code that *uses* +the asset specification. :app:`Pyramid` APIs which accept relative asset +specifications typically describe what the asset is relative to in their +individual documentation. .. index:: single: add_static_view -- cgit v1.2.3 From 7420d3a6983a668b0f2e7ff22be30cff81e0e2b0 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 26 Dec 2010 21:57:09 -0500 Subject: wording --- docs/narr/assets.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/narr/assets.rst b/docs/narr/assets.rst index 50879ef22..f147426ce 100644 --- a/docs/narr/assets.rst +++ b/docs/narr/assets.rst @@ -66,11 +66,11 @@ two parts: The two parts are separated by the colon character. -:app:`Pyramid` uses the :term:`pkg_resources` API to resolve the package name -and asset name to an absolute (operating-system-specific) file name. It -eventually passes this resolved absolute filesystem path to the Chameleon -templating engine, which then uses it to load, parse, and execute the -template file. +:app:`Pyramid` uses the Python :term:`pkg_resources` API to resolve the +package name and asset name to an absolute (operating-system-specific) file +name. It eventually passes this resolved absolute filesystem path to the +Chameleon templating engine, which then uses it to load, parse, and execute +the template file. There is a second form of asset specification: a *relative* asset specification. Instead of using an "absolute" asset specification which -- cgit v1.2.3 From d1432f4f3c48106252b292f19442bf591c554fa5 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 27 Dec 2010 00:57:34 -0500 Subject: - Reconcile "extending an existing application" chapter with existence of "advanced configuration" chapter. --- TODO.txt | 5 +- docs/narr/advconfig.rst | 6 + docs/narr/declarative.rst | 66 ++++++-- docs/narr/extending.rst | 420 +++++++++++++++++++++++++--------------------- 4 files changed, 291 insertions(+), 206 deletions(-) diff --git a/TODO.txt b/TODO.txt index d6e10cd3c..f81c78387 100644 --- a/TODO.txt +++ b/TODO.txt @@ -6,9 +6,6 @@ Must-Have (before 1.0) - Write a "Whats New" (delta from BFG 1.3) -- Reconcile "extending an existing application" chapter with existence of - "advanced configuration" chapter. - - Consider deprecations for ``model`` and ``resource`` APIs. Should-Have @@ -38,6 +35,8 @@ Should-Have Nice-to-Have ------------ +- Better "Extending" chapter. + - Try to make test suite pass on IronPython. - Non-bwcompat use of threadlocals that need to be documented or ameliorated: diff --git a/docs/narr/advconfig.rst b/docs/narr/advconfig.rst index e096ef863..f8b3ee191 100644 --- a/docs/narr/advconfig.rst +++ b/docs/narr/advconfig.rst @@ -118,6 +118,9 @@ Conflict detection happens for any kind of configuration: imperative configuration, :term:`ZCML` configuration, or configuration that results from the execution of a :term:`scan`. +.. note:: If you use, ZCML, its conflict detection algorithm is described in + :ref:`zcml_conflict_detection`. + Manually Resolving Conflicts ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -335,6 +338,9 @@ Instead, use :meth:`pyramid.config.Configuration.include`: Using ``include`` rather than calling the function directly will allow :ref:`automatic_conflict_resolution` to work. +.. note: See :ref:`the_include_tag` for a declarative alternative to + :meth:`pyramid.config.Configurator.include`. + .. _twophase_config: Two-Phase Configuration diff --git a/docs/narr/declarative.rst b/docs/narr/declarative.rst index 12deb90e7..6654c3dcd 100644 --- a/docs/narr/declarative.rst +++ b/docs/narr/declarative.rst @@ -3,28 +3,27 @@ Declarative Configuration ========================= -The mode of configuration most comprehensively detailed by examples in -narrative chapters in this book is "imperative" configuration. This is the -configuration mode in which a developer cedes the least amount of control to -the framework; it's "imperative" because you express the configuration -directly in Python code, and you have the full power of Python at your -disposal as you issue configuration statements. However, another mode of -configuration exists within :app:`Pyramid`, which often provides better -extensibility and configuration conflict detection. +The mode of configuration detailed in the majority of examples within this +this book is "imperative" configuration. This is the configuration mode in +which a developer cedes the least amount of control to the framework; it's +"imperative" because you express the configuration directly in Python code, +and you have the full power of Python at your disposal as you issue +configuration statements. However, another mode of configuration exists +within :app:`Pyramid` named :term:`ZCML` which often provides better +opportunity for extensibility. A complete listing of ZCML directives is available within :ref:`zcml_directives`. This chapter provides an overview of how you might get started with ZCML and highlights some common tasks performed when you use -ZCML. You can get a better understanding of when it's appropriate to use -ZCML from :ref:`extending_chapter`. +ZCML. .. index:: single: declarative configuration .. _declarative_configuration: -Declarative Configuration -------------------------- +ZCML Configuration +------------------ A :app:`Pyramid` application can be configured "declaratively", if so desired. Declarative configuration relies on *declarations* made external to @@ -163,6 +162,8 @@ configure your application; instead you need to use :term:`ZCML`. .. index:: single: ZCML conflict detection +.. _zcml_conflict_detection: + ZCML Conflict Detection ~~~~~~~~~~~~~~~~~~~~~~~ @@ -345,6 +346,8 @@ contain other directives. See also :ref:`configure_directive` and :ref:`word_on_xml_namespaces`. +.. _the_include_tag: + The ```` Tag ~~~~~~~~~~~~~~~~~~~~~ @@ -478,6 +481,45 @@ declaratively. More information about this mode of configuration is available in :ref:`declarative_configuration` and within :ref:`zcml_reference`. +.. index:: + single: ZCML granularity + +ZCML Granularity +~~~~~~~~~~~~~~~~ + +It's extremely helpful to third party application "extenders" (aka +"integrators") if the :term:`ZCML` that composes the configuration for an +application is broken up into separate files which do very specific things. +These more specific ZCML files can be reintegrated within the application's +main ``configure.zcml`` via ```` +declarations. When ZCML files contain sets of specific declarations, an +integrator can avoid including any ZCML he does not want by including only +ZCML files which contain the declarations he needs. He is not forced to +"accept everything" or "use nothing". + +For example, it's often useful to put all ```` declarations in a +separate ZCML file, as ```` statements have a relative ordering that +is extremely important to the application: if an extender wants to add a +route to the "middle" of the routing table, he will always need to disuse all +the routes and cut and paste the routing configuration into his own +application. It's useful for the extender to be able to disuse just a +*single* ZCML file in this case, accepting the remainder of the configuration +from other :term:`ZCML` files in the original application. + +Granularizing ZCML is not strictly required. An extender can always disuse +*all* your ZCML, choosing instead to copy and paste it into his own package, +if necessary. However, doing so is considerate, and allows for the best +reusability. Sometimes it's possible to include only certain ZCML files from +an application that contain only the registrations you really need, omitting +others. But sometimes it's not. For brute force purposes, when you're +getting ``view`` or ``route`` registrations that you don't actually want in +your overridden application, it's always appropriate to just *not include* +any ZCML file from the overridden application. Instead, just cut and paste +the entire contents of the ``configure.zcml`` (and any ZCML file included by +the overridden application's ``configure.zcml``) into your own package and +omit the ```` ZCML declaration in the overriding +package's ``configure.zcml``. + .. _zcml_scanning: Scanning via ZCML diff --git a/docs/narr/extending.rst b/docs/narr/extending.rst index 9802a01f6..48a098374 100644 --- a/docs/narr/extending.rst +++ b/docs/narr/extending.rst @@ -3,11 +3,61 @@ Extending An Existing :app:`Pyramid` Application =================================================== -If the developer of a :app:`Pyramid` application has obeyed certain -constraints while building that application, a third party should be -able to change its behavior without needing to modify its source code. -The behavior of a :app:`Pyramid` application that obeys certain -constraints can be *overridden* or *extended* without modification. +If a :app:`Pyramid` developer has obeyed certain constraints while building +an application, a third party should be able to change the application's +behavior without needing to modify its source code. The behavior of a +:app:`Pyramid` application that obeys certain constraints can be *overridden* +or *extended* without modification. + +We'll define some jargon here for the benefit of identifying the parties +involved in such an effort. + +Developer + The original application developer. + +Integrator + Another developer who wishes to reuse the application written by the + original application developer in an unanticipated context. He may also + wish to modify the original application without changing the original + application's source code. + +The Difference Between "Extensible" and "Pluggable" Applications +---------------------------------------------------------------- + +Other web frameworks, such as :term:`Django`, advertise that they allow +developers to create "pluggable applications". They claim that if you create +an application in a certain way, it will be integratable in a sensible, +structured way into another arbitrarily-written application or project +created by a third-party developer. + +:app:`Pyramid`, as a platform, does not claim to provide such a feature. The +platform provides no guarantee that you can create an application and package +it up such that an arbitrary integrator can use it as a subcomponent in a +larger Pyramid application or project. Pyramid does not mandate the +constraints necessary for such a pattern to work satisfactorily. Because +Pyramid is not very "opinionated", developers are able to use wildly +different patterns and technologies to build an application. A given Pyramid +application may happen to be reusable by a particular third party integrator, +because the integrator and the original developer may share similar base +technology choices (such as the use of a particular relational database or +ORM). But the same application may not be reusable by a different developer, +because he has made different technology choices which are incompatible with +the original developer's. + +As a result, the concept of a "pluggable application" is left to layers built +above Pyramid, such as a "CMS" layer or "application server" layer. Such +layers are apt to provide the necessary "opinions" (such as mandating a +storage layer, a templating system, and a structured, well-documented pattern +of registering that certain URLs map to certain bits of code) which makes the +concept of a "pluggable application" possible. "Pluggable applications", +thus, should not plug in to Pyramid itself but should instead plug into a +system written atop Pyramid. + +Although it does not provide for "pluggable applications", Pyramid *does* +provide a rich set of mechanisms which allows for the extension of a single +existing application. Such features can be used by frameworks built using +Pyramid as a base. All Pyramid applications may not be *pluggable*, but all +Pyramid applications are *extensible*. .. index:: single: extensible application @@ -15,65 +65,64 @@ constraints can be *overridden* or *extended* without modification. Rules for Building An Extensible Application -------------------------------------------- -There's only one rule you need to obey if you want to build a -maximally extensible :app:`Pyramid` application: you should not use -any :term:`configuration decoration` or :term:`imperative -configuration`. This means the application developer should avoid -relying on :term:`configuration decoration` meant to be detected via -a :term:`scan`, and you mustn't configure your :app:`Pyramid` -application *imperatively* by using any code which configures the -application through methods of the :term:`Configurator` (except for -the :meth:`pyramid.config.Configurator.load_zcml` method). - -Instead, you must always use :term:`ZCML` for the equivalent -purposes. :term:`ZCML` declarations that belong to an application can be -"overridden" by integrators as necessary, but decorators and imperative code -which perform the same tasks cannot. Use only :term:`ZCML` to configure your -application if you'd like it to be extensible. See +There is only one rule you need to obey if you want to build a maximally +extensible :app:`Pyramid` application: as a developer, you should factor any +overrideable :term:`imperative configuration` you've created into functions +which can be used via :meth:`pyramid.config.Configurator.include` rather than +inlined as calls to methods of a :term:`Configurator` within the ``main`` +function in your application's ``__init__.py``. For example, rather than: + +.. code-block:: python + :linenos: + + from pyramid.config import Configurator + + if __name__ == '__main__': + config = Configurator() + config.add_view('myapp.views.view1', name='view1') + config.add_view('myapp.views.view2', name='view2') + +You should do move the calls to ``add_view`` outside of the (non-reusable) +``if __name__ == '__main__'`` block, and into a reusable function: + +.. code-block:: python + :linenos: + + from pyramid.config import Configurator + + if __name__ == '__main__': + config = Configurator() + config.include(add_views) + + def add_views(config): + config.add_view('myapp.views.view1', name='view1') + config.add_view('myapp.views.view2', name='view2') + +Doing this allows an integrator to maximally reuse the configuration +statements that relate to your application by selectively including or +disincluding the configuration functions you've created from another package. + +Alternately, you can use :term:`ZCML` for the purpose of making configuration +extensible and overrideable. :term:`ZCML` declarations that belong to an +application can be overridden and extended by integrators as necessary in a +similar fashion. If you use only :term:`ZCML` to configure your application, +it will automatically be maximally extensible without any manual effort. See :ref:`declarative_chapter` for information about using ZCML. Fundamental Plugpoints ~~~~~~~~~~~~~~~~~~~~~~ The fundamental "plug points" of an application developed using -:app:`Pyramid` are *routes*, *views*, and *resources*. Routes are -declarations made using the ZCML ```` directive. Views are -declarations made using the ZCML ```` directive (or the -``@view_config`` decorator). Resources are files that are accessed by -:app:`Pyramid` using the :term:`pkg_resources` API such as static -files and templates. - -.. index:: - single: ZCML granularity - -ZCML Granularity -~~~~~~~~~~~~~~~~ - -It's extremely helpful to third party application "extenders" (aka -"integrators") if the :term:`ZCML` that composes the configuration for -an application is broken up into separate files which do very specific -things. These more specific ZCML files can be reintegrated within the -application's main ``configure.zcml`` via ```` declarations. When ZCML files contain sets -of specific declarations, an integrator can avoid including any ZCML -he does not want by including only ZCML files which contain the -declarations he needs. He is not forced to "accept everything" or -"use nothing". - -For example, it's often useful to put all ```` declarations in -a separate ZCML file, as ```` statements have a relative -ordering that is extremely important to the application: if an -extender wants to add a route to the "middle" of the routing table, he -will always need to disuse all the routes and cut and paste the -routing configuration into his own application. It's useful for the -extender to be able to disuse just a *single* ZCML file in this case, -accepting the remainder of the configuration from other :term:`ZCML` -files in the original application. - -Granularizing ZCML is not strictly required. An extender can always -disuse *all* your ZCML, choosing instead to copy and paste it into his -own package, if necessary. However, doing so is considerate, and -allows for the best reusability. +:app:`Pyramid` are *routes*, *views*, and *assets*. Routes are declarations +made using the :meth:`pyramid.config.Configurator.add_route` method (or the +ZCML ```` directive). Views are declarations made using the +:meth:`pyramid.config.Configurator.add_view` method (or the ZCML ```` +directive). Assets are files that are accessed by :app:`Pyramid` using the +:term:`pkg_resources` API such as static files and templates via a +:term:`asset specification`. Other directives and configurator methods also +deal in routes, views, and assets. For example, +:meth:`pyramid.config.Configurator.add_handler` adds a single route, and some +number of views. .. index:: single: extending an existing application @@ -81,96 +130,88 @@ allows for the best reusability. Extending an Existing Application --------------------------------- -The steps for extending an existing application depend largely on -whether the application does or does not use configuration decorators -and/or imperative code. +The steps for extending an existing application depend largely on whether the +application does or does not use configuration decorators and/or imperative +code. + +If The Application Has Configuration Decorations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You've inherited a :app:`Pyramid` application which you'd like to extend or +override that uses :class:`pyramid.view.view_config` decorators or other +:term:`configuration decoration` decorators. -Extending an Application Which Possesses Configuration Decorators Or Which Does Configuration Imperatively -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +If you just want to *extend* the application, you can run a :term:`scan` +against the application's package, then add additional configuration that +registers more views or routes. -If you've inherited a :app:`Pyramid` application which uses -:class:`pyramid.view.view_config` decorators or which performs -configuration imperatively, one of two things may be true: +.. code-block:: python + :linenos: + + if __name__ == '__main__': + config.scan('someotherpackage') + config.add_view('mypackage.views.myview', name='myview') -- If you just want to *extend* the application, you can write - additional ZCML that registers more views or routes, loading any - existing ZCML and continuing to use any existing imperative - configuration done by the original application. +If you want to *override* configuration in the application, you *may* need to +run :meth:`pyramid.config.Configurator.commit` after performing the scan of +the original package, then add additional configuration that registers more +views or routes which performs overrides. -- If you want to *override* configuration in the application, you - *may* need to change the source code of the original application. +.. code-block:: python + :linenos: - If the only source of trouble is the existence of - :class:`pyramid.view.view_config` decorators, you can just prevent a - :term:`scan` from happening (by omitting the ```` declaration - from ZCML or omitting any call to the - :meth:`pyramid.config.Configurator.scan` method). This - will cause the decorators to do nothing. At this point, you will - need to convert all the configuration done in decorators into - equivalent :term:`ZCML` and add that ZCML to a separate Python - package as described in :ref:`extending_the_application`. + if __name__ == '__main__': + config.scan('someotherpackage') + config.commit() + config.add_view('mypackage.views.myview', name='myview' - If the source of trouble is configuration done imperatively in a - function called during application startup, you'll need to change - the code: convert imperative configuration statements into - equivalent :term:`ZCML` declarations. +Once this is done, you should be able to extend or override the application +like any other (see :ref:`extending_the_application`). -Once this is done, you should be able to extend or override the -application like any other (see :ref:`extending_the_application`). +You can alternately just prevent a :term:`scan` from happening (by omitting +any call to the :meth:`pyramid.config.Configurator.scan` method). This will +cause the decorators attached to objects in the target application to do +nothing. At this point, you will need to convert all the configuration done +in decorators into equivalent imperative configuration or ZCML and add that +configuration or ZCML to a separate Python package as described in +:ref:`extending_the_application`. .. _extending_the_application: -Extending an Application Which Does Not Possess Configuration Decorators or Imperative Configuration -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -To extend or override the behavior of an existing application, you -will need to write some :term:`ZCML`, and perhaps some implementations -of the types of things you'd like to override (such as views), which -are referred to within that ZCML. - -The general pattern for extending an existing application looks -something like this: - -- Create a new Python package. The easiest way to do this is to - create a new :app:`Pyramid` application using the "paster" - template mechanism. See :ref:`creating_a_project` for more - information. - -- Install the new package into the same Python environment as the - original application (e.g. ``python setup.py develop`` or ``python - setup.py install``). - -- Change the ``configure.zcml`` in the new package to include the - original :app:`Pyramid` application's ``configure.zcml`` via an - include statement, e.g. ````. - Alternately, if the original application writer anticipated - overriding some things and not others, instead of including the - "main" ``configure.zcml`` of the original application, include only - specific ZCML files from the original application using the ``file`` - attribute of the ```` statement, e.g. ````. - -- On a line in the new package's ``configure.zcml`` file that falls - after (XML-ordering-wise) all the ``include`` statements of the original - package ZCML, put an ``includeOverrides`` statement which identifies - *another* ZCML file within the new package (for example - ````. - -- Create an ``overrides.zcml`` file within the new package. The - statements in the ``overrides.zcml`` file will override any ZCML - statements made within the original application (such as view - declarations). - -- Create Python files containing views and other overridden elements, - such as templates and static resources as necessary, and wire these - up using ZCML registrations within the ``overrides.zcml`` file. - These registrations may extend or override the original view - registrations. See :ref:`overriding_views`, - :ref:`overriding_routes` and :ref:`overriding_resources`. +Extending the Application +~~~~~~~~~~~~~~~~~~~~~~~~~ + +To extend or override the behavior of an existing application, you will need +to create a new package which includes the configuration of the old package, +and you'll perhaps need to create implementations of the types of things +you'd like to override (such as views), which are referred to within the +original package. -- In the ``__init__.py`` of the new package, load the ``configure.zcml`` file - of the new package using the - :meth:`pyramid.config.Configurator.load_zcml` method. +The general pattern for extending an existing application looks something +like this: + +- Create a new Python package. The easiest way to do this is to create a new + :app:`Pyramid` application using the "paster" template mechanism. See + :ref:`creating_a_project` for more information. + +- In the new package, create Python files containing views and other + overridden elements, such as templates and static resources as necessary. + +- Install the new package into the same Python environment as the original + application (e.g. ``python setup.py develop`` or ``python setup.py + install``). + +- Change the ``main`` function in the new package's ``__init__py`` to include + the original :app:`Pyramid` application's configuration functions via + :meth:`pyramid.config.Configurator.include` statements or a :term:`scan`. + +- Wire the new views and assets created in the new package up using + imperative registrations within the ``main`` function of the + ``__init__.py`` file of the new application. These wiring should happen + *after* including the configuration functions of the old application. + These registrations will extend or override any registrations performed by + the original application. See :ref:`overriding_views`, + :ref:`overriding_routes` and :ref:`overriding_resources`. .. index:: pair: overriding; views @@ -180,26 +221,44 @@ something like this: Overriding Views ~~~~~~~~~~~~~~~~~ -The ZCML ```` declarations you make which *override* application -behavior will usually have the same ``context`` and ``name`` (and -:term:`predicate` attributes, if used) as the original. These -```` declarations will point at "new" view code. The new view -code itself will usually be cut-n-paste copies of view callables from -the original application with slight tweaks. For example: +The :term:`view configuration` declarations you make which *override* +application behavior will usually have the same :term:`view predicate` +attributes as the original you wish to override. These ```` +declarations will point at "new" view code, in the override package you've +created. The new view code itself will usually be cut-n-paste copies of view +callables from the original application with slight tweaks. + +For example, if the original application has the following +``configure_views`` configuration method: + +.. code-block:: python + :linenos: + + def configure_views(config): + config.add_view('theoriginalapp.views.theview', name='theview') -.. code-block:: xml +You can override the first view configuration statement made by +``configure_views`` within the override package, after loading the original +configuration function: + +.. code-block:: python :linenos: - + from pyramid.config import Configurator + from originalapp import configure_views + + if __name == '__main__': + config = Configurator() + config.include(configure_views) + config.add_view('theoverrideapp.views.theview', name='theview') + +In this case, the ``theoriginalapp.views.theview`` view will never be +executed. Instead, a new view, ``theoverrideapp.views.theview`` will be +executed instead, when request circumstances dictate. -A similar pattern can be used to *extend* the application with ```` -declarations. Just register a new view against some existing resource type -(using ``context``) and make sure the URLs it implies are available on some -other page rendering. +A similar pattern can be used to *extend* the application with ``add_view`` +declarations. Just register a new view against some other set of predicates +to make sure the URLs it implies are available on some other page rendering. .. index:: pair: overriding; routes @@ -209,48 +268,27 @@ other page rendering. Overriding Routes ~~~~~~~~~~~~~~~~~ -Route setup is currently typically performed in a sequence of ordered -ZCML ```` declarations. Because these declarations are ordered -relative to each other, and because this ordering is typically -important, you should retain the relative ordering of these -declarations when performing an override. Typically, this means -*copying* all the ```` declarations into an external ZCML file -and changing them as necessary. Then disinclude any ZCML from the -original application which contains the original declarations. +Route setup is currently typically performed in a sequence of ordered calls +to :meth:`pyramid.config.Configurator.add_route`. Because these calls are +ordered relative to each other, and because this ordering is typically +important, you should retain their relative ordering when performing an +override. Typically, this means *copying* all the ``add_route`` statements +into the override package's file and changing them as necessary. Then +disinclude any ``add_route`` statements from the original application. .. index:: pair: overriding; resources .. _overriding_resources: -Overriding Resources -~~~~~~~~~~~~~~~~~~~~ - -"Resource" files are static files on the filesystem that are -accessible within a Python *package*. An entire chapter is devoted to -resources: :ref:`resources_chapter`. Within this chapter is a section -named :ref:`overriding_resources_section`. This section of that -chapter describes in detail how to override package resources with -other resources by using :term:`ZCML` ```` declarations. Add -such ```` declarations to your override package's -``configure.zcml`` to perform overrides. - -.. index:: - single: ZCML inclusion - -Dealing With ZCML Inclusions ----------------------------- - -Sometimes it's possible to include only certain ZCML files from an -application that contain only the registrations you really need, -omitting others. But sometimes it's not. For brute force purposes, -when you're getting ``view`` or ``route`` registrations that you don't -actually want in your overridden application, it's always appropriate -to just *not include* any ZCML file from the overridden application. -Instead, just cut and paste the entire contents of the -``configure.zcml`` (and any ZCML file included by the overridden -application's ``configure.zcml``) into your own package and omit the -```` ZCML declaration in the overriding package's -``configure.zcml``. - +Overriding Assets +~~~~~~~~~~~~~~~~~ +Assets are files on the filesystem that are accessible within a Python +*package*. An entire chapter is devoted to resources: :ref:`assets_chapter`. +Within this chapter is a section named :ref:`overriding_assets_section`. +This section of that chapter describes in detail how to override package +resources with other resources by using the +:meth:`pyramid.config.Configurator.override_asset` method. Add such +``override_asset`` calls to your override package's ``__init__.py`` to +perform overrides. -- cgit v1.2.3 From 5f0398b63c9f01a4a6ad0664e117b508f9e89ade Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 27 Dec 2010 00:58:55 -0500 Subject: wording --- docs/narr/extending.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/narr/extending.rst b/docs/narr/extending.rst index 48a098374..524dcb2ac 100644 --- a/docs/narr/extending.rst +++ b/docs/narr/extending.rst @@ -99,8 +99,9 @@ You should do move the calls to ``add_view`` outside of the (non-reusable) config.add_view('myapp.views.view2', name='view2') Doing this allows an integrator to maximally reuse the configuration -statements that relate to your application by selectively including or -disincluding the configuration functions you've created from another package. +statements that relate to your application by allowing him to selectively +include or disinclude the configuration functions you've created from an +"override package". Alternately, you can use :term:`ZCML` for the purpose of making configuration extensible and overrideable. :term:`ZCML` declarations that belong to an -- cgit v1.2.3 From cfb6c56c2f4ba9fc477de0c461f2b757f6f9d9c1 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 27 Dec 2010 01:01:33 -0500 Subject: remove unnecessary use of begin/end --- docs/narr/declarative.rst | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/docs/narr/declarative.rst b/docs/narr/declarative.rst index 6654c3dcd..f36e55b29 100644 --- a/docs/narr/declarative.rst +++ b/docs/narr/declarative.rst @@ -47,9 +47,7 @@ In a file named ``helloworld.py``: if __name__ == '__main__': config = Configurator() - config.begin() config.load_zcml('configure.zcml') - config.end() app = config.make_wsgi_app() serve(app, host='0.0.0.0') @@ -82,9 +80,7 @@ the ``if __name__ == '__main__'`` section of ``helloworld.py``: if __name__ == '__main__': config = Configurator() - config.begin() config.add_view(hello_world) - config.end() app = config.make_wsgi_app() serve(app, host='0.0.0.0') @@ -98,9 +94,7 @@ it now reads as: if __name__ == '__main__': config = Configurator() - config.begin() config.load_zcml('configure.zcml') - config.end() app = config.make_wsgi_app() serve(app, host='0.0.0.0') @@ -225,9 +219,7 @@ To do so, first, create a file named ``helloworld.py``: if __name__ == '__main__': config = Configurator() - config.begin() config.load_zcml('configure.zcml') - config.end() app = config.make_wsgi_app() serve(app, host='0.0.0.0') @@ -270,10 +262,8 @@ within the ``if __name__ == '__main__'`` section of ``helloworld.py``: if __name__ == '__main__': config = Configurator() - config.begin() config.add_view(hello_world) config.add_view(goodbye_world, name='goodbye') - config.end() app = config.make_wsgi_app() serve(app, host='0.0.0.0') @@ -288,9 +278,7 @@ name='goodbye')``, so that it now reads as: if __name__ == '__main__': config = Configurator() - config.begin() config.load_zcml('configure.zcml') - config.end() app = config.make_wsgi_app() serve(app, host='0.0.0.0') @@ -545,9 +533,7 @@ file points to is scanned. if __name__ == '__main__': from pyramid.config import Configurator config = Configurator() - config.begin() config.load_zcml('configure.zcml') - config.end() app = config.make_wsgi_app() serve(app, host='0.0.0.0') -- cgit v1.2.3 From d30a83540d98bb803caca74b94ae75f88dd3154b Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 27 Dec 2010 01:05:14 -0500 Subject: remove inappropriate use of begin/end --- docs/narr/hooks.rst | 2 -- docs/narr/i18n.rst | 5 ----- 2 files changed, 7 deletions(-) diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index 238ac8328..2917b5254 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -621,10 +621,8 @@ performed, enabling you to set up the utility in advance: if __name__ == '__main__': config = Configurator() - config.begin() config.registry.registerUtility(UtilityImplementation()) config.scan() - config.end() app = config.make_wsgi_app() serve(app, host='0.0.0.0') diff --git a/docs/narr/i18n.rst b/docs/narr/i18n.rst index d8cc5cb1c..c2a5b8ce7 100644 --- a/docs/narr/i18n.rst +++ b/docs/narr/i18n.rst @@ -881,11 +881,8 @@ application startup. For example: :linenos: from pyramid.config import Configurator - config.begin() config.add_translation_dirs('my.application:locale/', 'another.application:locale/') - # ... - config.end() A message catalog in a translation directory added via :meth:`pyramid.config.Configurator.add_translation_dirs` @@ -1020,9 +1017,7 @@ For example: from pyramid.config import Configurator config = Configurator() - config.begin() config.set_locale_negotiator(my_locale_negotiator) - config.end() .. note:: You can also add a custom locale negotiator via ZCML. See :ref:`zcml_adding_a_locale_negotiator` -- cgit v1.2.3 From e8db031e8cd22affb65254539ae210f64d37f36e Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 27 Dec 2010 01:37:27 -0500 Subject: wording --- docs/designdefense.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/designdefense.rst b/docs/designdefense.rst index db399fffa..53b95b9d0 100644 --- a/docs/designdefense.rst +++ b/docs/designdefense.rst @@ -1002,7 +1002,7 @@ understand the ZCA or he will need to develop his own similar extensibility system. Ultimately, any argument about whether the extensibility features lent to -applications by :app:`Pyramid` are "good" or "bad" is somewhat pointless. You +applications by :app:`Pyramid` are "good" or "bad" is mostly pointless. You needn't take advantage of the extensibility features provided by a particular :app:`Pyramid` application in order to affect a modification for a particular set of its deployments. You can ignore the application's extensibility -- cgit v1.2.3 From ca114e4ff1a75f00699b6e7ba37052f39b5ed788 Mon Sep 17 00:00:00 2001 From: Casey Duncan Date: Mon, 27 Dec 2010 12:15:57 -0700 Subject: sign myself away --- CONTRIBUTORS.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 40234836e..b48e769a1 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -114,3 +114,6 @@ Contributors - Blaise Laflamme, 2010/11/09 - Chris Rossi, 2010/11/10 + +- Casey Duncan, 2010/12/27 + -- cgit v1.2.3 From 90a327b2cd9b9e6b27688dadcdf8125f091f242d Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 27 Dec 2010 16:25:15 -0500 Subject: - Add ``paster proute`` command which displays a summary of the routing table. See the narrative documentation section within the "URL Dispatch" chapter entitled "Displaying All Application Routes". - Added narrative documentation section within the "URL Dispatch" chapter entitled "Displaying All Application Routes" (for ``paster proutes`` command). --- CHANGES.txt | 8 +++ docs/narr/project.rst | 2 + docs/narr/urldispatch.rst | 38 ++++++++++++ pyramid/paster.py | 109 ++++++++++++++++++++++++++++++----- pyramid/tests/test_config.py | 8 +-- pyramid/tests/test_paster.py | 134 ++++++++++++++++++++++++++++++++++++++++++- setup.py | 1 + 7 files changed, 281 insertions(+), 19 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index e58a1dc76..cba3a9800 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -25,6 +25,10 @@ Features arguments to add_route work by raising an exception during configuration if view-related arguments exist but no ``view`` argument is passed. +- Add ``paster proute`` command which displays a summary of the routing + table. See the narrative documentation section within the "URL Dispatch" + chapter entitled "Displaying All Application Routes". + Paster Templates ---------------- @@ -81,6 +85,10 @@ Documentation - Merge "Static Assets" chapter into the "Assets" chapter. +- Added narrative documentation section within the "URL Dispatch" chapter + entitled "Displaying All Application Routes" (for ``paster proutes`` + command). + 1.0a7 (2010-12-20) ================== diff --git a/docs/narr/project.rst b/docs/narr/project.rst index 36f2d6975..55a2711f3 100644 --- a/docs/narr/project.rst +++ b/docs/narr/project.rst @@ -256,6 +256,8 @@ create`` -generated project. Within a project generated by the single: IPython single: paster pshell +.. _interactive_shell: + The Interactive Shell --------------------- diff --git a/docs/narr/urldispatch.rst b/docs/narr/urldispatch.rst index 76eca454d..4c601340f 100644 --- a/docs/narr/urldispatch.rst +++ b/docs/narr/urldispatch.rst @@ -1231,6 +1231,44 @@ which you started the application from. For example: See :ref:`environment_chapter` for more information about how, and where to set these values. +.. index:: + pair: routes; printing + single: paster proutes + +Displaying All Application Routes +--------------------------------- + +You can use the ``paster proutes`` command in a terminal window to print a +summary of routes related to your application. Much like the ``paster +pshell`` command (see :ref:`interactive shell`), the ``paster proutes`` +command accepts two arguments. The first argument to ``proutes`` is the path +to your application's ``.ini`` file. The second is the ``app`` section name +inside the ``.ini`` file which points to your application. + +For example: + +.. code-block:: text + :linenos: + + [chrism@thinko MyProject]$ ../bin/paster proutes development.ini MyProject + Name Pattern View + ---- ------- ---- + home / + home2 / + another /another None + static/ static/*subpath + catchall /*subpath + +``paster proutes`` generates a table. The table has three columns: a Name +name column, a Pattern column, and a View column. The items listed in the +Name column are route names, the items listen in the Pattern column are route +patterns, and the items listed in the View column are representations of the +view callable that will be invoked when a request matches the associated +route pattern. The view column may show ``None`` if no associated view +callable could be found. If no routes are configured within your +application, nothing will be printed to the console when ``paster proutes`` +is executed. + References ---------- diff --git a/pyramid/paster.py b/pyramid/paster.py index c8bc36e80..57dac6b32 100644 --- a/pyramid/paster.py +++ b/pyramid/paster.py @@ -63,7 +63,22 @@ def get_app(config_file, name, loadapp=loadapp): return app _marker = object() -class PShellCommand(Command): + +class PCommand(Command): + get_app = staticmethod(get_app) # hook point + get_root = staticmethod(get_root) # hook point + group_name = 'pyramid' + interact = (interact,) # for testing + loadapp = (loadapp,) # for testing + verbose = 3 + + def __init__(self, *arg, **kw): + # needs to be in constructor to support Jython (used to be at class + # scope as ``usage = '\n' + __doc__``. + self.usage = '\n' + self.__doc__ + Command.__init__(self, *arg, **kw) + +class PShellCommand(PCommand): """Open an interactive shell with a :app:`Pyramid` app loaded. This command accepts two positional arguments: @@ -88,7 +103,6 @@ class PShellCommand(Command): min_args = 2 max_args = 2 - group_name = 'pyramid' parser = Command.standard_parser(simulate=True) parser.add_option('-d', '--disable-ipython', @@ -96,18 +110,6 @@ class PShellCommand(Command): dest='disable_ipython', help="Don't use IPython even if it is available") - interact = (interact,) # for testing - loadapp = (loadapp,) # for testing - get_app = staticmethod(get_app) # hook point - get_root = staticmethod(get_root) # hook point - verbose = 3 - - def __init__(self, *arg, **kw): - # needs to be in constructor to support Jython (used to be at class - # scope as ``usage = '\n' + __doc__``. - self.usage = '\n' + self.__doc__ - Command.__init__(self, *arg, **kw) - def command(self, IPShell=_marker): if IPShell is _marker: try: #pragma no cover @@ -136,3 +138,82 @@ class PShellCommand(Command): closer() BFGShellCommand = PShellCommand # b/w compat forever + +class PRoutesCommand(PCommand): + """Print all URL dispatch routes used by a Pyramid application in the + order in which they are evaluated. Each route includes the name of the + route, the pattern of the route, and the view callable which will be + invoked when the route is matched. + + This command accepts two positional arguments: + + ``config_file`` -- specifies the PasteDeploy config file to use + for the interactive shell. + + ``section_name`` -- specifies the section name in the PasteDeploy + config file that represents the application. + + Example:: + + $ paster proutes myapp.ini main + + .. note:: You should use a ``section_name`` that refers to the + actual ``app`` section in the config file that points at + your Pyramid app without any middleware wrapping, or this + command will almost certainly fail. + """ + summary = "Print all URL dispatch routes related to a Pyramid application" + min_args = 2 + max_args = 2 + stdout = sys.stdout + + parser = Command.standard_parser(simulate=True) + + def _get_mapper(self, app): + from pyramid.config import Configurator + registry = app.registry + config = Configurator(registry = registry) + return config.get_routes_mapper() + + def out(self, msg): # pragma: no cover + print msg + + def command(self): + from pyramid.request import Request + from pyramid.interfaces import IRouteRequest + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IView + from zope.interface import Interface + from zope.interface import providedBy + config_file, section_name = self.args + app = self.get_app(config_file, section_name, loadapp=self.loadapp[0]) + registry = app.registry + mapper = self._get_mapper(app) + if mapper is not None: + routes = mapper.get_routes() + fmt = '%-15s %-30s %-25s' + if not routes: + return + self.out(fmt % ('Name', 'Pattern', 'View')) + self.out( + fmt % ('-'*len('Name'), '-'*len('Pattern'), '-'*len('View'))) + for route in routes: + 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 + view_callable = registry.adapters.lookup( + (IViewClassifier, request_iface, context_iface), + IView, name='', default=None) + self.out(fmt % (route.name, route.pattern, view_callable)) + + diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py index 84e8289be..c129b21ae 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -2338,7 +2338,7 @@ class ConfiguratorTests(unittest.TestCase): config.add_route('name', '/pattern', view_attr='abc') except ConfigurationError: pass - else: + else: # pragma: no cover raise AssertionError def test_add_route_no_view_with_view_context(self): @@ -2348,7 +2348,7 @@ class ConfiguratorTests(unittest.TestCase): config.add_route('name', '/pattern', view_context=DummyContext) except ConfigurationError: pass - else: + else: # pragma: no cover raise AssertionError def test_add_route_no_view_with_view_permission(self): @@ -2358,7 +2358,7 @@ class ConfiguratorTests(unittest.TestCase): config.add_route('name', '/pattern', view_permission='edit') except ConfigurationError: pass - else: + else: # pragma: no cover raise AssertionError def test_add_route_no_view_with_view_renderer(self): @@ -2368,7 +2368,7 @@ class ConfiguratorTests(unittest.TestCase): config.add_route('name', '/pattern', view_renderer='json') except ConfigurationError: pass - else: + else: # pragma: no cover raise AssertionError def test__override_not_yet_registered(self): diff --git a/pyramid/tests/test_paster.py b/pyramid/tests/test_paster.py index e2478ac4f..a5f613f8e 100644 --- a/pyramid/tests/test_paster.py +++ b/pyramid/tests/test_paster.py @@ -111,6 +111,123 @@ class TestPShellCommand(unittest.TestCase): self.failUnless(interact.banner) self.assertEqual(apps, [app]) +class TestPRoutesCommand(unittest.TestCase): + def _getTargetClass(self): + from pyramid.paster import PRoutesCommand + return PRoutesCommand + + def _makeOne(self): + return self._getTargetClass()('proutes') + + def test_no_routes(self): + command = self._makeOne() + mapper = DummyMapper() + 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(L, []) + + def test_single_route_no_views_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', 'None']) + + def test_single_route_one_view_registered(self): + from zope.interface import Interface + from pyramid.registry import Registry + from pyramid.interfaces import IRouteRequest + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IView + registry = Registry() + def view():pass + class IMyRoute(Interface): + pass + registry.registerAdapter(view, + (IViewClassifier, IMyRoute, Interface), + IView, '') + registry.registerUtility(IMyRoute, IRouteRequest, name='a') + command = self._makeOne() + route = DummyRoute('a', '/a') + mapper = DummyMapper(route) + command._get_mapper = lambda *arg: mapper + 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()[:4], ['a', '/a', ' Date: Mon, 27 Dec 2010 17:25:18 -0500 Subject: Prep for 1.0a8. --- CHANGES.txt | 4 ++-- docs/conf.py | 2 +- setup.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index cba3a9800..ebf3bf4e2 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,5 +1,5 @@ -Next release -============ +1.0a8 (2010-12-27) +================== Bug Fixes --------- diff --git a/docs/conf.py b/docs/conf.py index 7bcdf3a07..8c238cecd 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -76,7 +76,7 @@ copyright = '%s, Agendaless Consulting' % datetime.datetime.now().year # other places throughout the built documents. # # The short X.Y version. -version = '1.0a7' +version = '1.0a8' # The full version, including alpha/beta/rc tags. release = version diff --git a/setup.py b/setup.py index d58641487..b158547af 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ # ############################################################################## -__version__ = '0.0' +__version__ = '1.0a8' import os import platform -- cgit v1.2.3 From 43520cf4eb67cd24eaacbe8f3f42928abc469b2f Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 27 Dec 2010 23:02:47 -0500 Subject: include handler, translationdir, and localenegotiator zcml references in PDF --- docs/latexindex.rst | 3 +++ 1 file changed, 3 insertions(+) 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 -- cgit v1.2.3 From 096c4b3709253b642ddb985a92331141a8aba381 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 28 Dec 2010 14:29:11 -0500 Subject: back to development --- CHANGES.txt | 5 +++++ setup.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index ebf3bf4e2..af78b714d 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,8 @@ +Next release +============ + +- ... + 1.0a8 (2010-12-27) ================== diff --git a/setup.py b/setup.py index b158547af..d58641487 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ # ############################################################################## -__version__ = '1.0a8' +__version__ = '0.0' import os import platform -- cgit v1.2.3 From afbc5d57ddd848b7f2e8c8190afc2109c7563496 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 28 Dec 2010 14:30:54 -0500 Subject: - 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 ````. --- CHANGES.txt | 8 +++++++- pyramid/paster.py | 19 +++++-------------- pyramid/tests/test_paster.py | 35 ++++++++++++++++++++++++++++------- 3 files changed, 40 insertions(+), 22 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index af78b714d..643e0ed7b 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,7 +1,13 @@ 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 + ````. 1.0a8 (2010-12-27) ================== 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, '')) + 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_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', '']) + 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', '']) def test__get_mapper(self): from pyramid.registry import Registry -- cgit v1.2.3 From 17c4de891ad41c03c6e5e007f8100f02033b4555 Mon Sep 17 00:00:00 2001 From: Wichert Akkerman Date: Tue, 28 Dec 2010 21:07:19 +0100 Subject: When using the auth_tkt authentication plugin accept a ``tokens`` parameter in emember. --- CHANGES.txt | 3 +++ pyramid/authentication.py | 3 ++- pyramid/tests/test_authentication.py | 17 ++++++++++++++++- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index ebf3bf4e2..2264f50e2 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -10,6 +10,9 @@ Bug Fixes Features -------- +- When using the auth_tkt authentication plugin accept a ``tokens`` parameter + in :py:func:`pyramid.security.remember`. + - If a resource implements a ``__resource_url__`` method, it will be called as the result of invoking the ``pyramid.url.resource_url`` function to generate a URL, overriding the default logic. See the new "Generating The diff --git a/pyramid/authentication.py b/pyramid/authentication.py index 86d725bcf..b80429c72 100644 --- a/pyramid/authentication.py +++ b/pyramid/authentication.py @@ -415,7 +415,7 @@ class AuthTktCookieHelper(object): environ = request.environ return self._get_cookies(environ, '', max_age=EXPIRE) - def remember(self, request, userid, max_age=None): + def remember(self, request, userid, max_age=None, tokens=()): max_age = max_age or self.max_age environ = request.environ @@ -436,6 +436,7 @@ class AuthTktCookieHelper(object): self.secret, userid, remote_addr, + tokens=tokens, user_data=user_data, cookie_name=self.cookie_name, secure=self.secure) diff --git a/pyramid/tests/test_authentication.py b/pyramid/tests/test_authentication.py index d9d0c2c97..5f30d0d22 100644 --- a/pyramid/tests/test_authentication.py +++ b/pyramid/tests/test_authentication.py @@ -568,7 +568,22 @@ class TestAuthTktCookieHelper(unittest.TestCase): self.assertEqual(values[0]['max-age'], '500') self.failUnless(values[0]['expires']) - + + def test_remember_tokens(self): + plugin = self._makeOne('secret') + request = self._makeRequest() + result = plugin.remember(request, 'other', tokens=('foo', 'bar')) + self.assertEqual(len(result), 3) + + self.assertEqual(result[0][0], 'Set-Cookie') + self.failUnless("'tokens': ('foo', 'bar')" in result[0][1]) + + self.assertEqual(result[1][0], 'Set-Cookie') + self.failUnless("'tokens': ('foo', 'bar')" in result[1][1]) + + self.assertEqual(result[2][0], 'Set-Cookie') + self.failUnless("'tokens': ('foo', 'bar')" in result[2][1]) + def test_forget(self): plugin = self._makeOne('secret') request = self._makeRequest() -- cgit v1.2.3 From b6f7b34c95ae96cb70d583211e40a8b4300dea17 Mon Sep 17 00:00:00 2001 From: Marius Gedminas Date: Wed, 29 Dec 2010 00:26:10 +0200 Subject: Typo: cam -> can. --- docs/narr/flash.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/narr/flash.rst b/docs/narr/flash.rst index d41c2cdaf..0bffd6ddf 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. -- cgit v1.2.3 From 2c29ef9007681902575384157eea8b36d53cca30 Mon Sep 17 00:00:00 2001 From: Marius Gedminas Date: Wed, 29 Dec 2010 00:26:46 +0200 Subject: Rephrase sentence with no verb. --- docs/narr/flash.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/narr/flash.rst b/docs/narr/flash.rst index 0bffd6ddf..ad50dc82b 100644 --- a/docs/narr/flash.rst +++ b/docs/narr/flash.rst @@ -49,7 +49,7 @@ 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. -- cgit v1.2.3 From 87e5443d655b745ba7a5a675bdd52027a2240d11 Mon Sep 17 00:00:00 2001 From: Marius Gedminas Date: Wed, 29 Dec 2010 00:29:11 +0200 Subject: "one or more has" -> "one or more have". Sounds more natural to me, and Googling for "one or more: singular or plural" seems to agree. --- docs/narr/flash.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/narr/flash.rst b/docs/narr/flash.rst index ad50dc82b..d009da4fb 100644 --- a/docs/narr/flash.rst +++ b/docs/narr/flash.rst @@ -56,7 +56,7 @@ 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. -- cgit v1.2.3 From 133f1231b1288e20777b56c82d5c5b7730010eab Mon Sep 17 00:00:00 2001 From: Marius Gedminas Date: Wed, 29 Dec 2010 00:36:21 +0200 Subject: Show the method signatures of pop_flash/peek_flash. The narrative referred to popping messages from a particular queue, but the code examples didn't show how to indicate which queue you were interested in, leaving the reader a bit confused. --- docs/narr/flash.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/narr/flash.rst b/docs/narr/flash.rst index d009da4fb..71c6cf305 100644 --- a/docs/narr/flash.rst +++ b/docs/narr/flash.rst @@ -63,6 +63,8 @@ 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: @@ -93,6 +95,8 @@ Once one or more messages has been added to a flash queue by the at that queue. Unlike ``session.pop_flash``, the queue is not popped from flash storage. +.. method:: peek_flash(queue='') + .. code-block:: python :linenos: -- cgit v1.2.3 From a025f36fe9c875332d68cfcf9fe9d6398c851c57 Mon Sep 17 00:00:00 2001 From: Marius Gedminas Date: Wed, 29 Dec 2010 00:59:57 +0200 Subject: Added missing :meth:. --- docs/narr/assets.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/narr/assets.rst b/docs/narr/assets.rst index f147426ce..3d3498e26 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`. -- cgit v1.2.3 From 1e209fc65d62a86d15bf54fa2a91a6728d0ade9e Mon Sep 17 00:00:00 2001 From: Marius Gedminas Date: Wed, 29 Dec 2010 01:04:15 +0200 Subject: Fix quoting. --- docs/narr/assets.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/narr/assets.rst b/docs/narr/assets.rst index 3d3498e26..f73ff231a 100644 --- a/docs/narr/assets.rst +++ b/docs/narr/assets.rst @@ -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. -- cgit v1.2.3 From 203e848985fca05263893132896b552ccbeaa4b0 Mon Sep 17 00:00:00 2001 From: Marius Gedminas Date: Wed, 29 Dec 2010 01:36:06 +0200 Subject: Markup fix. --- docs/narr/resources.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/narr/resources.rst b/docs/narr/resources.rst index ac88afdfd..3962e9e00 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 -- cgit v1.2.3 From 5e2c481dda9c69477749693c3563255b94f25990 Mon Sep 17 00:00:00 2001 From: Marius Gedminas Date: Wed, 29 Dec 2010 01:41:55 +0200 Subject: Whitespace bigotry. --- docs/narr/resources.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/narr/resources.rst b/docs/narr/resources.rst index 3962e9e00..8cf2cead2 100644 --- a/docs/narr/resources.rst +++ b/docs/narr/resources.rst @@ -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() -- cgit v1.2.3 From de68d6666305192c6d0ffc8263f7486446a83f73 Mon Sep 17 00:00:00 2001 From: Rob Miller Date: Tue, 28 Dec 2010 18:58:34 -0800 Subject: add decorator argument to add_view method to support auto-wrapping view callables with a decorator at view registration time --- pyramid/config.py | 19 ++++++++++++++++--- pyramid/tests/test_config.py | 13 +++++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/pyramid/config.py b/pyramid/config.py index f6b4a2112..083e2a328 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -1008,9 +1008,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, xhr=False, accept=None, - header=None, path_info=None, custom_predicates=(), - context=None): + renderer=None, wrapper=None, decorator=None, xhr=False, + accept=None, header=None, path_info=None, + custom_predicates=(), context=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* @@ -1119,6 +1119,15 @@ class Configurator(object): view is the same context and request of the inner view. If this attribute is unspecified, no view wrapping is done. + decorator + + A function which will be used to decorate the registered + :term:`view callable`. The decorator function will be + called with the view callable as a single argument, and it + must return a replacement view callable which accepts the + same arguments and returns the same type of values as the + original function. + Predicate Arguments name @@ -1326,6 +1335,10 @@ class Configurator(object): 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 registered = self.registry.adapters.registered diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py index c129b21ae..a0bdf95ad 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -739,6 +739,19 @@ class ConfiguratorTests(unittest.TestCase): result = wrapper(None, None) self.assertEqual(result, 'OK') + def test_add_view_with_decorator(self): + def view(request): + return 'OK' + def view_wrapper(fn): + fn.__assert_wrapped__ = True + return fn + config = self._makeOne(autocommit=True) + config.add_view(view=view, decorator=view_wrapper) + wrapper = self._getViewCallable(config) + self.assertTrue(getattr(wrapper, '__assert_wrapped__', False)) + result = wrapper(None, None) + self.assertEqual(result, 'OK') + def test_add_view_as_instance(self): class AView: def __call__(self, context, request): -- cgit v1.2.3 From bae647592ee4913ed7e52409c89815c3ebb37ef7 Mon Sep 17 00:00:00 2001 From: Rob Miller Date: Tue, 28 Dec 2010 21:53:50 -0800 Subject: Recorded description of last change and added self to contributors list. --- CHANGES.txt | 9 +++++++++ CONTRIBUTORS.txt | 1 + 2 files changed, 10 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index ebf3bf4e2..01f4adf5b 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,12 @@ +1.0a9 (unreleased) + +Features +-------- + +- config.add_view now accepts a 'decorator' keyword argument, a + callable which will decorate the view callable before it is added to + the registry + 1.0a8 (2010-12-27) ================== diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index b48e769a1..443503914 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -117,3 +117,4 @@ Contributors - Casey Duncan, 2010/12/27 +- Rob Miller, 2010/12/28 -- cgit v1.2.3 From 73877f08d25eb77b87f0cd3aa35472165b9b50ba Mon Sep 17 00:00:00 2001 From: Rob Miller Date: Tue, 28 Dec 2010 21:54:34 -0800 Subject: Added support for an _action_decorator classmethod on handler classes. --- pyramid/config.py | 10 +++++++--- pyramid/tests/test_config.py | 16 ++++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/pyramid/config.py b/pyramid/config.py index 083e2a328..5c3bf7002 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -941,6 +941,8 @@ class Configurator(object): pattern = route.pattern + action_decorator = getattr(handler, '_action_decorator', None) + path_has_action = ':action' in pattern or '{action}' in pattern if action and path_has_action: @@ -970,7 +972,8 @@ class Configurator(object): preds.append(ActionPredicate(action)) view_args['custom_predicates'] = preds self.add_view(view=handler, attr=method_name, - route_name=route_name, **view_args) + route_name=route_name, + decorator=action_decorator, **view_args) else: method_name = action if method_name is None: @@ -993,14 +996,15 @@ class Configurator(object): view_args = expose_config.copy() del view_args['name'] self.add_view(view=handler, attr=meth_name, - route_name=route_name, **view_args) + route_name=route_name, + decorator=action_decorator, **view_args) # Now register the method itself method = getattr(handler, method_name, None) configs = getattr(method, '__exposed__', [{}]) for expose_config in configs: self.add_view(view=handler, attr=action, route_name=route_name, - **expose_config) + decorator=action_decorator, **expose_config) return route diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py index a0bdf95ad..22f491eb4 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -1977,6 +1977,22 @@ class ConfiguratorTests(unittest.TestCase): self.assertEqual(view['attr'], 'action') self.assertEqual(view['view'], MyView) + def test_add_handler_with_action_decorator(self): + config = self._makeOne(autocommit=True) + views = [] + def dummy_add_view(**kw): + views.append(kw) + config.add_view = dummy_add_view + class MyView(object): + @classmethod + def _action_decorator(cls, fn): # pragma: no cover + return fn + def action(self): # pragma: no cover + return 'response' + config.add_handler('name', '/{action}', MyView) + self.assertEqual(len(views), 1) + self.assertEqual(views[0]['decorator'], MyView._action_decorator) + def test_add_handler_doesnt_mutate_expose_dict(self): config = self._makeOne(autocommit=True) views = [] -- cgit v1.2.3 From 744d76312799f2537a200e4972f309c7670a4cef Mon Sep 17 00:00:00 2001 From: Rob Miller Date: Tue, 28 Dec 2010 22:18:34 -0800 Subject: record handler _action_decorator classmethod support --- CHANGES.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 01f4adf5b..c86594933 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -7,6 +7,9 @@ Features callable which will decorate the view callable before it is added to the registry +- If a handler class provides an _action_decorator classmethod, use that + as the decorator for each view registration for that handler. + 1.0a8 (2010-12-27) ================== -- cgit v1.2.3 From 91f9e646e86dabd68b4184aa0f48bb60856aec14 Mon Sep 17 00:00:00 2001 From: Rob Miller Date: Tue, 28 Dec 2010 22:52:41 -0800 Subject: change ``MyView`` to ``MyHandler``, since it's a handler we're mocking --- pyramid/tests/test_config.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py index 22f491eb4..2bbd2e4aa 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -1983,15 +1983,15 @@ class ConfiguratorTests(unittest.TestCase): def dummy_add_view(**kw): views.append(kw) config.add_view = dummy_add_view - class MyView(object): + class MyHandler(object): @classmethod def _action_decorator(cls, fn): # pragma: no cover return fn def action(self): # pragma: no cover return 'response' - config.add_handler('name', '/{action}', MyView) + config.add_handler('name', '/{action}', MyHandler) self.assertEqual(len(views), 1) - self.assertEqual(views[0]['decorator'], MyView._action_decorator) + self.assertEqual(views[0]['decorator'], MyHandler._action_decorator) def test_add_handler_doesnt_mutate_expose_dict(self): config = self._makeOne(autocommit=True) -- cgit v1.2.3 From 88231cc1b16f1f5a0983dba1dab9b401bbde0c00 Mon Sep 17 00:00:00 2001 From: Rob Miller Date: Tue, 28 Dec 2010 22:53:17 -0800 Subject: Enforce that _action_decorator must be a classmethod --- pyramid/config.py | 4 ++++ pyramid/tests/test_config.py | 11 +++++++++++ 2 files changed, 15 insertions(+) diff --git a/pyramid/config.py b/pyramid/config.py index 5c3bf7002..274938225 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -942,6 +942,10 @@ 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.') path_has_action = ':action' in pattern or '{action}' in pattern diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py index 2bbd2e4aa..0a87f4d7f 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -1993,6 +1993,17 @@ class ConfiguratorTests(unittest.TestCase): self.assertEqual(len(views), 1) self.assertEqual(views[0]['decorator'], MyHandler._action_decorator) + def test_add_handler_with_action_decorator_no_classmethod(self): + config = self._makeOne(autocommit=True) + class MyHandler(object): + def _action_decorator(self, fn): # pragma: no cover + return fn + def action(self): # pragma: no cover + return 'response' + from pyramid.exceptions import ConfigurationError + self.assertRaises(ConfigurationError, config.add_handler, + 'name', '/{action}', MyHandler) + def test_add_handler_doesnt_mutate_expose_dict(self): config = self._makeOne(autocommit=True) views = [] -- cgit v1.2.3 From 2a922b4cf51d3fe083b33d375282c227f90a3e4e Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 29 Dec 2010 01:59:22 -0500 Subject: fix misleading example --- docs/narr/renderers.rst | 1 + 1 file changed, 1 insertion(+) 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!'} -- cgit v1.2.3 From aca6c0df04533f98ad423fc2877276b69b1ba2e0 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 29 Dec 2010 02:01:29 -0500 Subject: resource->asset --- docs/narr/introduction.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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. -- cgit v1.2.3 From ebceb116dd201ad7058e533bf133fe6dfede16d6 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 29 Dec 2010 02:10:01 -0500 Subject: git title right --- docs/narr/flash.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/narr/flash.rst b/docs/narr/flash.rst index 71c6cf305..037bfc416 100644 --- a/docs/narr/flash.rst +++ b/docs/narr/flash.rst @@ -87,8 +87,8 @@ 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" -- cgit v1.2.3 From ae4a242a4f161f8cbc95d6e66ad67ec6d1dd2ada Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 29 Dec 2010 02:15:41 -0500 Subject: garden --- TODO.txt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/TODO.txt b/TODO.txt index f81c78387..ada3c4c2a 100644 --- a/TODO.txt +++ b/TODO.txt @@ -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? -- cgit v1.2.3 From 7e9d3c17bc2fbd4841dbf58509ddd75028971dd9 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 29 Dec 2010 02:22:50 -0500 Subject: - 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. --- CHANGES.txt | 7 ++++ docs/index.rst | 1 - docs/tutorials/cmf/actions.rst | 28 ---------------- docs/tutorials/cmf/catalog.rst | 73 ----------------------------------------- docs/tutorials/cmf/content.rst | 67 ------------------------------------- docs/tutorials/cmf/index.rst | 38 --------------------- docs/tutorials/cmf/missing.rst | 22 ------------- docs/tutorials/cmf/skins.rst | 23 ------------- docs/tutorials/cmf/workflow.rst | 14 -------- 9 files changed, 7 insertions(+), 266 deletions(-) delete mode 100644 docs/tutorials/cmf/actions.rst delete mode 100644 docs/tutorials/cmf/catalog.rst delete mode 100644 docs/tutorials/cmf/content.rst delete mode 100644 docs/tutorials/cmf/index.rst delete mode 100644 docs/tutorials/cmf/missing.rst delete mode 100644 docs/tutorials/cmf/skins.rst delete mode 100644 docs/tutorials/cmf/workflow.rst diff --git a/CHANGES.txt b/CHANGES.txt index 643e0ed7b..e7ecad31a 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -9,6 +9,13 @@ Bug Fixes Instead of trying to resolve the view, if it cannot, it will now just print ````. +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) ================== 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/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 `_ -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 -`_ 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 -`_ 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 -`_ (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 -`_ or on the `#pylons IRC -channel `_ 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. - -- cgit v1.2.3 From 0e525ce7315e9962372267966eda6bc76ff99f02 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 29 Dec 2010 03:37:34 -0500 Subject: typo --- docs/narr/csrf.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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` -- cgit v1.2.3 From 581a401c26047a6cddb6521393de4030ce0a962a Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 29 Dec 2010 03:48:51 -0500 Subject: fix from mike --- docs/narr/urldispatch.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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** -- cgit v1.2.3 From 8739f576ed84bb48cec9c2d4b60e92878a273b1f Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 29 Dec 2010 14:47:32 -0500 Subject: factor deriver from mapper --- docs/api/interfaces.rst | 4 + pyramid/config.py | 257 ++++++++++++++++++++++++++---------------------- pyramid/interfaces.py | 21 ++++ 3 files changed, 165 insertions(+), 117 deletions(-) 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/pyramid/config.py b/pyramid/config.py index d31011aa1..d3c197008 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -45,6 +45,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 @@ -266,14 +267,10 @@ class Configurator(object): renderer_globals_factory=None, default_permission=None, session_factory=None, - view_deriver=None, autocommit=False, ): if package is None: package = caller_package() - if view_deriver is None: - view_deriver = DefaultViewDeriver - self.view_deriver = view_deriver name_resolver = DottedNameResolver(package) self.name_resolver = name_resolver self.package_name = name_resolver.package_name @@ -342,24 +339,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): view = self.maybe_dotted(view) - deriver = self.view_deriver(view, - 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() + 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): @@ -1008,7 +1006,7 @@ class Configurator(object): request_param=None, containment=None, attr=None, renderer=None, wrapper=None, xhr=False, accept=None, header=None, path_info=None, custom_predicates=(), - context=None): + context=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* @@ -1253,6 +1251,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) @@ -1291,6 +1301,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) @@ -1321,10 +1332,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) - + 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, + view_mapper=view_mapper)(view) + registered = self.registry.adapters.registered # A multiviews is a set of views which are registered for @@ -2631,11 +2650,6 @@ class MultiView(object): continue raise PredicateMismatch(self.name) -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 DefaultViewDeriver(view, attr=attr).requestonly() - def preserve_attrs(wrapped): def inner(self, view): wrapped_view = wrapped(self, view) @@ -2670,81 +2684,27 @@ def preserve_attrs(wrapped): return wrapped_view return inner -class DefaultViewDeriver(object): - def __init__(self, view, **kw): +class ViewDeriver(object): + def __init__(self, **kw): self.kw = kw - self.view = view - self.registry = kw.get('registry') - self.helper = None - self.renderer = kw.get('renderer') - 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) - if self.registry is not None: - self.authn_policy = self.registry.queryUtility( - IAuthenticationPolicy) - self.authz_policy = self.registry.queryUtility( - IAuthorizationPolicy) - self.logger = self.registry.queryUtility(IDebugLogger) - - def __call__(self): + 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( - self.map_view(self.view)))))) - - def requestonly(self): - view = 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 = argspec[0] - defaults = argspec[3] - - 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 - - elif args[0] == 'request': - if len(args) - len(defaults) == 1: - return True - - return False - + view))))) @preserve_attrs def owrap_view(self, view): @@ -2861,26 +2821,85 @@ class DefaultViewDeriver(object): 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 = argspec[0] + defaults = argspec[3] + + 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 + + elif args[0] == 'request': + if len(args) - len(defaults) == 1: + return True + + return False + @preserve_attrs - def map_view(self, view): + def __call__(self, view): attr = self.kw.get('attr') isclass = inspect.isclass(view) - ronly = self.requestonly() + ronly = self.requestonly(view) if isclass and ronly: - view = self.adapt_requestonly_class() + view = self.map_requestonly_class(view) elif isclass: - view = self.adapt_class() + view = self.map_class(view) elif ronly: - view = self.adapt_requestonly_func() + view = self.map_requestonly_func(view) elif attr: - view = self.adapt_attr() + view = self.map_attr(view) elif self.helper is not None: - view = self.adapt_rendered() + view = self.map_rendered(view) return view - def adapt_requestonly_class(self): + def map_requestonly_class(self, view): # its a class that has an __init__ which only accepts request - view = self.view attr = self.kw.get('attr') helper = self.helper renderer = self.renderer @@ -2904,10 +2923,9 @@ class DefaultViewDeriver(object): return response return _class_requestonly_view - def adapt_class(self): + def map_class(self, view): # its a class that has an __init__ which accepts both context and # request - view = self.view attr = self.kw.get('attr') helper = self.helper renderer = self.renderer @@ -2930,10 +2948,9 @@ class DefaultViewDeriver(object): return response return _class_view - def adapt_requestonly_func(self): + def map_requestonly_func(self, view): # its a function or instance that has a __call__ accepts only a # single request argument - view = self.view attr = self.kw.get('attr') helper = self.helper renderer = self.renderer @@ -2957,10 +2974,9 @@ class DefaultViewDeriver(object): return response return _requestonly_view - def adapt_attr(self): + def map_attr(self, view): # its a function that has a __call__ which accepts both context and # request, but still has an attr - view = self.view attr = self.kw.get('attr') helper = self.helper renderer = self.renderer @@ -2980,10 +2996,9 @@ class DefaultViewDeriver(object): return response return _attr_view - def adapt_rendered(self): + def map_rendered(self, view): # it's a function that has a __call__ that accepts both context and # request, but requires rendering - view = self.view helper = self.helper renderer = self.renderer def _rendered_view(context, request): @@ -3001,10 +3016,6 @@ class DefaultViewDeriver(object): return response return _rendered_view -def _map_view(view, registry, attr=None, renderer=None): - return DefaultViewDeriver(view, registry=registry, attr=attr, - renderer=renderer).map_view(view) - def isexception(o): if IInterface.providedBy(o): if IException.isEqualOrExtendedBy(o): @@ -3059,3 +3070,15 @@ def is_response(ob): 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): -- cgit v1.2.3 From ac718c77c3d2b90c87f4f0b369eedff5884a2326 Mon Sep 17 00:00:00 2001 From: Joachim Krebs Date: Wed, 29 Dec 2010 20:57:00 +0000 Subject: Removed self arguments from new ISession method signatures. --- CHANGES.txt | 3 +++ pyramid/interfaces.py | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index e7ecad31a..71c55fd1f 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -9,6 +9,9 @@ Bug Fixes Instead of trying to resolve the view, if it cannot, it will now just print ````. +- The `self` argument was included in new methods of the ISession interface + signature. + Documentation ------------- diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index 32359ca94..d21976209 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -480,11 +480,11 @@ class ISession(Interface): :meth:`pyramid.interfaces.ISesssion.flash` """ - def new_csrf_token(self): + def new_csrf_token(): """ Create and set into the session a new, random cross-site request forgery protection token. Return the token. It will be a string.""" - def get_csrf_token(self): + def get_csrf_token(): """ Get the CSRF token previously added to the session via ``new_csrf_token``, and return the token. If no CSRF token exists, the value returned will be ``None``. -- cgit v1.2.3 From 613287762f6ca2f8d50651fc0b4eee2e9a5f8772 Mon Sep 17 00:00:00 2001 From: Rob Miller Date: Wed, 29 Dec 2010 14:22:34 -0800 Subject: adjust tests to work w/ latest merge and changes --- pyramid/tests/test_config.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py index b094caae9..e088ea2fc 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -1985,18 +1985,18 @@ class ConfiguratorTests(unittest.TestCase): config.add_view = dummy_add_view class MyHandler(object): @classmethod - def _action_decorator(cls, fn): # pragma: no cover + def __action_decorator__(cls, fn): # pragma: no cover return fn def action(self): # pragma: no cover return 'response' config.add_handler('name', '/{action}', MyHandler) self.assertEqual(len(views), 1) - self.assertEqual(views[0]['decorator'], MyHandler._action_decorator) + self.assertEqual(views[0]['decorator'], MyHandler.__action_decorator__) - def test_add_handler_with_action_decorator_no_classmethod(self): + def test_add_handler_with_action_decorator_fail_on_instancemethod(self): config = self._makeOne(autocommit=True) class MyHandler(object): - def _action_decorator(self, fn): # pragma: no cover + def __action_decorator__(self, fn): # pragma: no cover return fn def action(self): # pragma: no cover return 'response' -- cgit v1.2.3 From 1f8536956af7e122007da369d35924c28dd99c25 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 29 Dec 2010 17:47:35 -0500 Subject: simplify guard logic for __action_decorator__ --- pyramid/config.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/pyramid/config.py b/pyramid/config.py index e1005102b..e717556d9 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -4,7 +4,6 @@ import re import sys import threading import traceback -from types import FunctionType import venusian @@ -940,14 +939,17 @@ class Configurator(object): 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.') + if hasattr(action_decorator, 'im_self'): + # instance methods have an im_self == None + # classmethods have an im_self == cls + # staticmethods have no im_self + # instances have no im_self + if action_decorator.im_self is not handler: + raise ConfigurationError( + 'The "__action_decorator__" attribute of a handler ' + 'must not be an instance method (must be a ' + 'staticmethod, classmethod, function, or an instance ' + 'which is a callable') path_has_action = ':action' in pattern or '{action}' in pattern -- cgit v1.2.3 From 73741f6f97dce4d3f8ee340a708823db75c7aef6 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 29 Dec 2010 18:12:40 -0500 Subject: preserve_attrs->wraps_view, break out preserve_attrs bulk into preserve_view_attrs, make sure decorator preserves view attrs --- pyramid/config.py | 78 +++++++++++++++++++++++--------------------- pyramid/tests/test_config.py | 6 ++-- 2 files changed, 44 insertions(+), 40 deletions(-) diff --git a/pyramid/config.py b/pyramid/config.py index e717556d9..b8b47f686 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -2676,40 +2676,43 @@ class MultiView(object): continue raise PredicateMismatch(self.name) -def preserve_attrs(wrapped): +def wraps_view(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: - wrapped_view.__name__ = view.__name__ - except AttributeError: - wrapped_view.__name__ = repr(view) - try: - wrapped_view.__permitted__ = view.__permitted__ - except AttributeError: - 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 preserve_view_attrs(view, wrapped_view) return inner +def preserve_view_attrs(view, wrapped_view): + if wrapped_view is view: + return view + wrapped_view.__module__ = view.__module__ + wrapped_view.__doc__ = view.__doc__ + try: + wrapped_view.__name__ = view.__name__ + except AttributeError: + wrapped_view.__name__ = repr(view) + try: + wrapped_view.__permitted__ = view.__permitted__ + except AttributeError: + 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 + class ViewDeriver(object): def __init__(self, **kw): self.kw = kw @@ -2732,7 +2735,7 @@ class ViewDeriver(object): self.owrap_view( view))))) - @preserve_attrs + @wraps_view def owrap_view(self, view): wrapper_viewname = self.kw.get('wrapper_viewname') viewname = self.kw.get('viewname') @@ -2752,7 +2755,7 @@ class ViewDeriver(object): return wrapped_response return _owrapped_view - @preserve_attrs + @wraps_view def secured_view(self, view): permission = self.kw.get('permission') if permission == '__no_permission_required__': @@ -2780,7 +2783,7 @@ class ViewDeriver(object): return wrapped_view - @preserve_attrs + @wraps_view def authdebug_view(self, view): wrapped_view = view settings = self.registry.settings @@ -2813,7 +2816,7 @@ class ViewDeriver(object): return wrapped_view - @preserve_attrs + @wraps_view def predicated_view(self, view): predicates = self.kw.get('predicates', ()) if not predicates: @@ -2828,7 +2831,7 @@ class ViewDeriver(object): predicate_wrapper.__predicated__ = checker return predicate_wrapper - @preserve_attrs + @wraps_view def attr_wrapped_view(self, view): kw = self.kw accept, order, phash = (kw.get('accept', None), @@ -2907,7 +2910,7 @@ class DefaultViewMapper(object): return False - @preserve_attrs + @wraps_view def __call__(self, view): attr = self.kw.get('attr') decorator = self.kw.get('decorator') @@ -2924,7 +2927,8 @@ class DefaultViewMapper(object): elif self.helper is not None: view = self.map_rendered(view) if decorator is not None: - view = decorator(view) + decorated_view = decorator(view) + view = preserve_view_attrs(view, decorated_view) return view def map_requestonly_class(self, view): diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py index e088ea2fc..80e675623 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -3868,10 +3868,10 @@ class Test__map_view(unittest.TestCase): request = self._makeRequest() self.assertEqual(result(None, request).body, 'Hello!') -class Test_preserve_attrs(unittest.TestCase): +class Test_wraps_view(unittest.TestCase): def _callFUT(self, fn, view): - from pyramid.config import preserve_attrs - return preserve_attrs(fn)(None, view) + from pyramid.config import wraps_view + return wraps_view(fn)(None, view) def test_it_same(self): def view(context, request): -- cgit v1.2.3 From 2953baaebadfb267e1fa98d35605b88ff2274052 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 29 Dec 2010 18:22:37 -0500 Subject: make sure view returned from view mapper preserves all attrs --- pyramid/config.py | 6 +++++- pyramid/tests/test_config.py | 9 ++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/pyramid/config.py b/pyramid/config.py index b8b47f686..077db28ab 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -2910,7 +2910,6 @@ class DefaultViewMapper(object): return False - @wraps_view def __call__(self, view): attr = self.kw.get('attr') decorator = self.kw.get('decorator') @@ -2931,6 +2930,7 @@ class DefaultViewMapper(object): view = preserve_view_attrs(view, decorated_view) return view + @wraps_view def map_requestonly_class(self, view): # its a class that has an __init__ which only accepts request attr = self.kw.get('attr') @@ -2956,6 +2956,7 @@ class DefaultViewMapper(object): return response return _class_requestonly_view + @wraps_view def map_class(self, view): # its a class that has an __init__ which accepts both context and # request @@ -2981,6 +2982,7 @@ class DefaultViewMapper(object): return response return _class_view + @wraps_view def map_requestonly_func(self, view): # its a function or instance that has a __call__ accepts only a # single request argument @@ -3007,6 +3009,7 @@ class DefaultViewMapper(object): return response return _requestonly_view + @wraps_view def map_attr(self, view): # its a function that has a __call__ which accepts both context and # request, but still has an attr @@ -3029,6 +3032,7 @@ class DefaultViewMapper(object): return response return _attr_view + @wraps_view def map_rendered(self, view): # it's a function that has a __call__ that accepts both context and # request, but requires rendering diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py index 80e675623..d237592d5 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -741,14 +741,17 @@ class ConfiguratorTests(unittest.TestCase): def test_add_view_with_decorator(self): def view(request): + """ ABC """ return 'OK' def view_wrapper(fn): - fn.__assert_wrapped__ = True - return fn + def inner(context, request): + return fn(context, request) + return inner config = self._makeOne(autocommit=True) config.add_view(view=view, decorator=view_wrapper) wrapper = self._getViewCallable(config) - self.assertTrue(getattr(wrapper, '__assert_wrapped__', False)) + self.failIf(wrapper is view) + self.assertEqual(wrapper.__doc__, view.__doc__) result = wrapper(None, None) self.assertEqual(result, 'OK') -- cgit v1.2.3 From 95c9f6f331bd3294699969ae399045891c0dc6ad Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 30 Dec 2010 02:07:36 -0500 Subject: - The "view derivation" code is now factored into a set of classes rather than a large number of standalone functions (a side effect of the ``view_mapper`` refactoring). - The ``pyramid.renderer.RendererHelper`` class has grown a ``render_view`` method, which is used by the default view mapper (a side effect of the ``view_mapper`` refactoring). - The object passed as ``renderer`` to the "view deriver" is now an instance of ``pyramid.renderers.RendererHelper`` rather than a dictionary (a side effect of ``view_mapper`` refactoring). --- CHANGES.txt | 32 ++++++-- TODO.txt | 6 +- pyramid/config.py | 187 ++++++++++++++++--------------------------- pyramid/renderers.py | 12 +++ pyramid/tests/test_config.py | 59 +++++++++----- pyramid/tests/test_view.py | 11 ++- pyramid/view.py | 12 ++- pyramid/zcml.py | 5 -- 8 files changed, 163 insertions(+), 161 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index fabb882f7..018de8cf7 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -13,12 +13,19 @@ Bug Fixes Features -------- -- config.add_view now accepts a 'decorator' keyword argument, a - callable which will decorate the view callable before it is added to - the registry +- ``config.add_view`` now accepts a ``decorator`` keyword argument, a callable + which will decorate the view callable before it is added to the registry. -- If a handler class provides an _action_decorator classmethod, use that - as the decorator for each view registration for that handler. +- ``config.add_view`` now accepts a ``view_mapper`` keyword argument, which + should be a class which implements the new + ``pyramid.interfaces.IViewMapperFactory`` interface. Use of an alternate + view mapper allows objects that are meant to be used as view callables to + have an arbitrary argument list and an arbitrary result. This feature will + be used by Pyramid extension developers, not by "civilians". + +- If a handler class provides an __action_decorator__ attribute (usually a + classmethod or staticmethod), use that as the decorator for each view + registration for that handler. Documentation ------------- @@ -27,6 +34,21 @@ Documentation removed from the tutorials section. It was moved to the ``pyramid_tutorials`` Github repository. +Internals +--------- + +- The "view derivation" code is now factored into a set of classes rather + than a large number of standalone functions (a side effect of the + ``view_mapper`` refactoring). + +- The ``pyramid.renderer.RendererHelper`` class has grown a ``render_view`` + method, which is used by the default view mapper (a side effect of the + ``view_mapper`` refactoring). + +- The object passed as ``renderer`` to the "view deriver" is now an instance + of ``pyramid.renderers.RendererHelper`` rather than a dictionary (a side + effect of ``view_mapper`` refactoring). + 1.0a8 (2010-12-27) ================== diff --git a/TODO.txt b/TODO.txt index ada3c4c2a..5acc923a1 100644 --- a/TODO.txt +++ b/TODO.txt @@ -11,8 +11,10 @@ Must-Have (before 1.0) - 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). +- Document ``decorator=`` and ``view_mapper`` parameters to add_view. + +- Allow ``decorator=`` and ``view_mapper=`` to be passed via ZCML and the + ``view_config`` decorator. Should-Have ----------- diff --git a/pyramid/config.py b/pyramid/config.py index 077db28ab..a53c21b81 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -345,6 +345,15 @@ class Configurator(object): viewname=None, accept=None, order=MAX_ORDER, phash=DEFAULT_PHASH): view = self.maybe_dotted(view) + if isinstance(renderer, basestring): + renderer = RendererHelper(name=renderer, package=self.package, + registry = self.registry) + if renderer is None: + # use default renderer if one exists + if self.registry.queryUtility(IRendererFactory) is not None: + renderer = RendererHelper(name=None, + package=self.package, + registry=self.registry) deriver = ViewDeriver( registry=self.registry, permission=permission, @@ -753,9 +762,6 @@ class Configurator(object): a :term:`response` object. If a ``renderer`` argument is not supplied, the user-supplied view must itself return a :term:`response` object. """ - - if renderer is not None and not isinstance(renderer, dict): - renderer = {'name':renderer, 'package':self.package} return self._derive_view(view, attr=attr, renderer=renderer) @action_method @@ -1284,9 +1290,9 @@ class Configurator(object): 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. + 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) @@ -1338,9 +1344,6 @@ class Configurator(object): containment=containment, request_type=request_type, custom=custom_predicates) - if renderer is not None and not isinstance(renderer, dict): - renderer = {'name':renderer, 'package':self.package} - if context is None: context = for_ @@ -1350,7 +1353,17 @@ class Configurator(object): if not IInterface.providedBy(r_context): r_context = implementedBy(r_context) - def register(permission=permission): + if isinstance(renderer, basestring): + renderer = RendererHelper(name=renderer, package=self.package, + registry = self.registry) + + def register(permission=permission, renderer=renderer): + if renderer is None: + # use default renderer if one exists + if self.registry.queryUtility(IRendererFactory) is not None: + renderer = RendererHelper(name=None, + package=self.package, + registry=self.registry) if permission is None: # intent: will be None if no default permission is registered @@ -2002,8 +2015,9 @@ 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).""" - if renderer is not None and not isinstance(renderer, dict): - renderer = {'name':renderer, 'package':self.package} + if isinstance(renderer, basestring): + renderer = RendererHelper(name=renderer, package=self.package, + registry = self.registry) view = self._derive_view(view, attr=attr, renderer=renderer) def bwcompat_view(context, request): context = getattr(request, 'context', None) @@ -2041,8 +2055,9 @@ class Configurator(object): which will wrap this view when rendered (see the ``add_view`` method's ``wrapper`` argument for a description). """ - if renderer is not None and not isinstance(renderer, dict): - renderer = {'name':renderer, 'package':self.package} + if isinstance(renderer, basestring): + renderer = RendererHelper(name=renderer, package=self.package, + registry=self.registry) view = self._derive_view(view, attr=attr, renderer=renderer) def bwcompat_view(context, request): context = getattr(request, 'context', None) @@ -2853,23 +2868,12 @@ class ViewDeriver(object): 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) + self.attr = kw.get('attr') + self.decorator = kw.get('decorator') def requestonly(self, view): - attr = self.kw.get('attr') + attr = self.attr if attr is None: attr = '__call__' if inspect.isfunction(view): @@ -2911,145 +2915,90 @@ class DefaultViewMapper(object): return False def __call__(self, view): - attr = self.kw.get('attr') - decorator = self.kw.get('decorator') + attr = self.attr + decorator = self.decorator isclass = inspect.isclass(view) ronly = self.requestonly(view) if isclass and ronly: - view = self.map_requestonly_class(view) + view = preserve_view_attrs(view, self.map_requestonly_class(view)) elif isclass: - view = self.map_class(view) + view = preserve_view_attrs(view, self.map_class(view)) elif ronly: - view = self.map_requestonly_func(view) + view = preserve_view_attrs(view, self.map_requestonly_func(view)) elif attr: - view = self.map_attr(view) - elif self.helper is not None: - view = self.map_rendered(view) + view = preserve_view_attrs(view, self.map_attr(view)) + elif self.renderer is not None: + view = preserve_view_attrs(view, self.map_rendered(view)) if decorator is not None: - decorated_view = decorator(view) - view = preserve_view_attrs(view, decorated_view) + view = preserve_view_attrs(view, decorator(view)) return view - @wraps_view 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 + attr = self.attr 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) + if self.renderer is not None and not is_response(response): + response = self.renderer.render_view(request, response, view, + context) return response return _class_requestonly_view - @wraps_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 + attr = self.attr 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) + if self.renderer is not None and not is_response(response): + response = self.renderer.render_view(request, response, view, + context) return response return _class_view - @wraps_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 + # its a function that has a __call__ which accepts only a single + # request argument + attr = self.attr def _requestonly_view(context, request): if attr is None: response = view(request) else: response = getattr(view, attr)(request) - - if helper is not None: - if not is_response(response): - system = { - 'view':view, - 'renderer_name':renderer['name'], - 'renderer_info':renderer, - 'context':context, - 'request':request - } - response = helper.render_to_response(response, system, - request=request) + if self.renderer is not None and not is_response(response): + response = self.renderer.render_view(request, response, view, + context) return response return _requestonly_view - @wraps_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 + attr = self.attr def _attr_view(context, request): response = getattr(view, attr)(context, request) - if helper is not None: - if not is_response(response): - system = { - 'view':view, - 'renderer_name':renderer['name'], - 'renderer_info':renderer, - 'context':context, - 'request':request - } - response = helper.render_to_response(response, system, - request=request) + if self.renderer is not None and not is_response(response): + response = self.renderer.render_view(request, response, view, + context) return response return _attr_view - @wraps_view 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): - system = { - 'view':view, - 'renderer_name':renderer['name'], # b/c - 'renderer_info':renderer, - 'context':context, - 'request':request - } - response = helper.render_to_response(response, system, - request=request) + if self.renderer is not None and not is_response(response): + response = self.renderer.render_view(request, response, view, + context) return response return _rendered_view @@ -3102,12 +3051,6 @@ 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, @@ -3119,3 +3062,9 @@ def requestonly(view, attr=None): as opposed to something that accepts context, request """ return DefaultViewMapper(attr=attr).requestonly(view) +def is_response(ob): + if ( hasattr(ob, 'app_iter') and hasattr(ob, 'headerlist') and + hasattr(ob, 'status') ): + return True + return False + diff --git a/pyramid/renderers.py b/pyramid/renderers.py index c7fe86452..2e0514b01 100644 --- a/pyramid/renderers.py +++ b/pyramid/renderers.py @@ -282,6 +282,18 @@ class RendererHelper(object): def get_renderer(self): return self.renderer + def render_view(self, request, response, view, context): + system = { + 'view':view, + 'renderer_name':self.name, # b/c + 'renderer_info':{'name':self.name, 'package':self.package}, + 'context':context, + 'request':request + } + return self.render_to_response(response, system, + request=request) + + def render(self, value, system_values, request=None): renderer = self.renderer if system_values is None: diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py index d237592d5..b2fa0e329 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -1426,6 +1426,29 @@ class ConfiguratorTests(unittest.TestCase): self.assertEqual(result.name, fixture) self.assertEqual(result.settings, settings) + def test_add_view_with_default_renderer(self): + import pyramid.tests + from pyramid.interfaces import ISettings + class view(object): + def __init__(self, context, request): + self.request = request + self.context = context + + def __call__(self): + return {'a':'1'} + config = self._makeOne(autocommit=True) + class moo(object): + def __init__(self, *arg, **kw): + pass + def __call__(self, *arg, **kw): + return 'moo' + config.add_renderer(None, moo) + config.add_view(view=view) + wrapper = self._getViewCallable(config) + request = self._makeRequest(config) + result = wrapper(None, request) + self.assertEqual(result.body, 'moo') + def test_add_view_with_template_renderer_no_callable(self): import pyramid.tests from pyramid.interfaces import ISettings @@ -3547,16 +3570,17 @@ class Test__map_view(unittest.TestCase): def _registerRenderer(self, typ='.txt'): from pyramid.interfaces import IRendererFactory from pyramid.interfaces import ITemplateRenderer + from pyramid.renderers import RendererHelper from zope.interface import implements - class Renderer: + class DummyRenderer: implements(ITemplateRenderer) - spec = 'abc' + typ def __init__(self, path): self.__class__.path = path def __call__(self, *arg): return 'Hello!' - self.registry.registerUtility(Renderer, IRendererFactory, name=typ) - return Renderer + self.registry.registerUtility(DummyRenderer, IRendererFactory, name=typ) + renderer = RendererHelper(name='abc' + typ, registry=self.registry) + return renderer def _makeRequest(self): request = DummyRequest() @@ -3584,8 +3608,7 @@ class Test__map_view(unittest.TestCase): def test__map_view_as_function_with_attr_and_renderer(self): renderer = self._registerRenderer() view = lambda *arg: 'OK' - info = {'name':renderer.spec, 'package':None} - result = self._callFUT(view, attr='__name__', renderer=info) + result = self._callFUT(view, attr='__name__', renderer=renderer) self.failIf(result is view) self.assertRaises(TypeError, result, None, None) @@ -3643,8 +3666,7 @@ class Test__map_view(unittest.TestCase): pass def index(self): return {'a':'1'} - info = {'name':renderer.spec, 'package':None} - result = self._callFUT(view, attr='index', renderer=info) + result = self._callFUT(view, attr='index', renderer=renderer) self.failIf(result is view) self.assertEqual(view.__module__, result.__module__) self.assertEqual(view.__doc__, result.__doc__) @@ -3685,8 +3707,7 @@ class Test__map_view(unittest.TestCase): pass def index(self): return {'a':'1'} - info = {'name':renderer.spec, 'package':None} - result = self._callFUT(view, attr='index', renderer=info) + result = self._callFUT(view, attr='index', renderer=renderer) self.failIf(result is view) self.assertEqual(view.__module__, result.__module__) self.assertEqual(view.__doc__, result.__doc__) @@ -3727,8 +3748,7 @@ class Test__map_view(unittest.TestCase): pass def index(self): return {'a':'1'} - info = {'name':renderer.spec, 'package':None} - result = self._callFUT(view, attr='index', renderer=info) + result = self._callFUT(view, attr='index', renderer=renderer) self.failIf(result is view) self.assertEqual(view.__module__, result.__module__) self.assertEqual(view.__doc__, result.__doc__) @@ -3769,8 +3789,7 @@ class Test__map_view(unittest.TestCase): pass def index(self): return {'a':'1'} - info = {'name':renderer.spec, 'package':None} - result = self._callFUT(view, attr='index', renderer=info) + result = self._callFUT(view, attr='index', renderer=renderer) self.failIf(result is view) self.assertEqual(view.__module__, result.__module__) self.assertEqual(view.__doc__, result.__doc__) @@ -3802,8 +3821,7 @@ class Test__map_view(unittest.TestCase): def index(self, context, request): return {'a':'1'} view = View() - info = {'name':renderer.spec, 'package':None} - result = self._callFUT(view, attr='index', renderer=info) + result = self._callFUT(view, attr='index', renderer=renderer) self.failIf(result is view) request = self._makeRequest() self.assertEqual(result(None, request).body, 'Hello!') @@ -3838,8 +3856,7 @@ class Test__map_view(unittest.TestCase): def index(self, request): return {'a':'1'} view = View() - info = {'name':renderer.spec, 'package':None} - result = self._callFUT(view, attr='index', renderer=info) + result = self._callFUT(view, attr='index', renderer=renderer) self.failIf(result is view) self.assertEqual(view.__module__, result.__module__) self.assertEqual(view.__doc__, result.__doc__) @@ -3851,8 +3868,7 @@ class Test__map_view(unittest.TestCase): renderer = self._registerRenderer() def view(context, request): return {'a':'1'} - info = {'name':renderer.spec, 'package':None} - result = self._callFUT(view, renderer=info) + result = self._callFUT(view, renderer=renderer) self.failIf(result is view) self.assertEqual(view.__module__, result.__module__) self.assertEqual(view.__doc__, result.__doc__) @@ -3863,8 +3879,7 @@ class Test__map_view(unittest.TestCase): renderer = self._registerRenderer() def view(context, request): return {'a':'1'} - info = {'name':renderer.spec, 'package':None} - result = self._callFUT(view, renderer=info) + result = self._callFUT(view, renderer=renderer) self.failIf(result is view) self.assertEqual(view.__module__, result.__module__) self.assertEqual(view.__doc__, result.__doc__) diff --git a/pyramid/tests/test_view.py b/pyramid/tests/test_view.py index 79e363756..7fc066319 100644 --- a/pyramid/tests/test_view.py +++ b/pyramid/tests/test_view.py @@ -317,10 +317,9 @@ class TestViewConfigDecorator(unittest.TestCase): settings = call_venusian(venusian) self.assertEqual(len(settings), 1) renderer = settings[0]['renderer'] - self.assertEqual(renderer, - {'name':'fixtures/minimal.pt', - 'package':pyramid.tests, - }) + self.assertEqual(renderer.name, 'fixtures/minimal.pt') + self.assertEqual(renderer.package, pyramid.tests) + self.assertEqual(renderer.registry.__class__, DummyRegistry) def test_call_with_renderer_dict(self): decorator = self._makeOne(renderer={'a':1}) @@ -494,9 +493,13 @@ class DummyVenusian(object): self.attachments.append((wrapped, callback, category)) return self.info +class DummyRegistry(object): + pass + class DummyConfig(object): def __init__(self): self.settings = [] + self.registry = DummyRegistry() def add_view(self, **kw): self.settings.append(kw) diff --git a/pyramid/view.py b/pyramid/view.py index 3dc110863..776185d8b 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -17,8 +17,10 @@ from zope.interface import providedBy from pyramid.interfaces import IRoutesMapper from pyramid.interfaces import IView from pyramid.interfaces import IViewClassifier +from pyramid.interfaces import IRendererFactory from pyramid.httpexceptions import HTTPFound +from pyramid.renderers import RendererHelper from pyramid.static import static_view from pyramid.threadlocal import get_current_registry @@ -404,6 +406,12 @@ class view_config(object): settings = self.__dict__.copy() def callback(context, name, ob): + renderer = settings.get('renderer') + if isinstance(renderer, basestring): + renderer = RendererHelper(name=renderer, + package=info.module, + registry=context.config.registry) + settings['renderer'] = renderer context.config.add_view(view=ob, **settings) info = self.venusian.attach(wrapped, callback, category='pyramid') @@ -415,10 +423,6 @@ class view_config(object): if settings['attr'] is None: settings['attr'] = wrapped.__name__ - renderer_name = settings.get('renderer') - if renderer_name is not None and not isinstance(renderer_name, dict): - settings['renderer'] = {'name':renderer_name, - 'package':info.module} settings['_info'] = info.codeinfo return wrapped diff --git a/pyramid/zcml.py b/pyramid/zcml.py index f668e3b4b..a2088e505 100644 --- a/pyramid/zcml.py +++ b/pyramid/zcml.py @@ -161,12 +161,7 @@ def view( cacheable=True, # not used, here for b/w compat < 0.8 ): - if renderer is not None: - package = getattr(_context, 'package', None) - renderer = {'name':renderer, 'package':package} - context = context or for_ - config = Configurator.with_context(_context) config.add_view( permission=permission, context=context, view=view, name=name, -- cgit v1.2.3 From fcff8cda8f7c60f181e902ca5a349eb8b5655205 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 30 Dec 2010 02:09:23 -0500 Subject: stray header line --- CHANGES.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index 018de8cf7..743f20e3b 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,3 @@ -======= Next release ============ -- cgit v1.2.3 From ae6513b9e93d876902936c257aef6a506f850aa3 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 30 Dec 2010 03:21:56 -0500 Subject: factor defaultviewmapper to dispatch in a slightly more readable way --- pyramid/config.py | 132 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 73 insertions(+), 59 deletions(-) diff --git a/pyramid/config.py b/pyramid/config.py index a53c21b81..eefdaae1f 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -2872,68 +2872,39 @@ class DefaultViewMapper(object): self.attr = kw.get('attr') self.decorator = kw.get('decorator') - def requestonly(self, view): - attr = self.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 = argspec[0] - defaults = argspec[3] - - 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 - - elif args[0] == 'request': - if len(args) - len(defaults) == 1: - return True - - return False - def __call__(self, view): - attr = self.attr decorator = self.decorator - isclass = inspect.isclass(view) - ronly = self.requestonly(view) - if isclass and ronly: - view = preserve_view_attrs(view, self.map_requestonly_class(view)) - elif isclass: + if inspect.isclass(view): view = preserve_view_attrs(view, self.map_class(view)) - elif ronly: - view = preserve_view_attrs(view, self.map_requestonly_func(view)) - elif attr: - view = preserve_view_attrs(view, self.map_attr(view)) - elif self.renderer is not None: - view = preserve_view_attrs(view, self.map_rendered(view)) + else: + view = preserve_view_attrs(view, self.map_nonclass(view)) if decorator is not None: view = preserve_view_attrs(view, decorator(view)) return view - def map_requestonly_class(self, view): + def map_class(self, view): + ronly = self.requestonly(view) + if ronly: + mapped_view = self._map_class_requestonly(view) + else: + mapped_view = self._map_class_native(view) + return mapped_view + + def map_nonclass(self, view): + # We do more work here than appears necessary to avoid wrapping the + # view unless it actually requires wrapping (to avoid function call + # overhead). + mapped_view = view + ronly = self.requestonly(view) + if ronly: + mapped_view = self._map_nonclass_requestonly(view) + elif self.attr: + mapped_view = self._map_nonclass_attr(view) + elif self.renderer is not None: + mapped_view = self._map_nonclass_rendered(view) + return mapped_view + + def _map_class_requestonly(self, view): # its a class that has an __init__ which only accepts request attr = self.attr def _class_requestonly_view(context, request): @@ -2948,7 +2919,7 @@ class DefaultViewMapper(object): return response return _class_requestonly_view - def map_class(self, view): + def _map_class_native(self, view): # its a class that has an __init__ which accepts both context and # request attr = self.attr @@ -2964,7 +2935,7 @@ class DefaultViewMapper(object): return response return _class_view - def map_requestonly_func(self, view): + def _map_nonclass_requestonly(self, view): # its a function that has a __call__ which accepts only a single # request argument attr = self.attr @@ -2979,7 +2950,7 @@ class DefaultViewMapper(object): return response return _requestonly_view - def map_attr(self, view): + def _map_nonclass_attr(self, view): # its a function that has a __call__ which accepts both context and # request, but still has an attr attr = self.attr @@ -2991,7 +2962,7 @@ class DefaultViewMapper(object): return response return _attr_view - def map_rendered(self, view): + def _map_nonclass_rendered(self, view): # it's a function that has a __call__ that accepts both context and # request, but requires rendering def _rendered_view(context, request): @@ -3002,6 +2973,49 @@ class DefaultViewMapper(object): return response return _rendered_view + def requestonly(self, view): + attr = self.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 = argspec[0] + defaults = argspec[3] + + 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 + + elif args[0] == 'request': + if len(args) - len(defaults) == 1: + return True + + return False + + def isexception(o): if IInterface.providedBy(o): if IException.isEqualOrExtendedBy(o): -- cgit v1.2.3 From ae40397a29b6fc3068a61c5e7acc7e3f7d801086 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 30 Dec 2010 17:14:50 -0500 Subject: typos --- pyramid/url.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyramid/url.py b/pyramid/url.py index e1eaaaa1e..ac569eecb 100644 --- a/pyramid/url.py +++ b/pyramid/url.py @@ -53,7 +53,7 @@ def route_url(route_name, request, *elements, **kw): ``*remainder`` replacement value, it is tacked on to the URL untouched. - If a keyword argument ``_query`` is present, it will used to + If a keyword argument ``_query`` is present, it will be used to compose a query string that will be tacked on to the end of the URL. The value of ``_query`` must be a sequence of two-tuples *or* a data structure with an ``.items()`` method that returns a @@ -221,7 +221,7 @@ def resource_url(resource, request, *elements, **kw): ``elements`` are used, the generated URL will *not* end in trailing a slash. - If a keyword argument ``query`` is present, it will used to + If a keyword argument ``query`` is present, it will be used to compose a query string that will be tacked on to the end of the URL. The value of ``query`` must be a sequence of two-tuples *or* a data structure with an ``.items()`` method that returns a -- cgit v1.2.3 From 9779255462d9809565ef36ccc31923b91237a83e Mon Sep 17 00:00:00 2001 From: Marius Gedminas Date: Fri, 31 Dec 2010 01:37:40 +0200 Subject: ReST formatting fix in Request class docstring. --- pyramid/request.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/request.py b/pyramid/request.py index 74418f1bb..475df744a 100644 --- a/pyramid/request.py +++ b/pyramid/request.py @@ -24,7 +24,7 @@ class Request(WebobRequest): argument. The documentation below (save for the ``add_response_callback`` and - ''add_finished_callback`` methods, which are defined in this subclass + ``add_finished_callback`` methods, which are defined in this subclass itself, and the attributes ``context``, ``registry``, ``root``, ``subpath``, ``traversed``, ``view_name``, ``virtual_root`` , and ``virtual_root_path``, each of which is added to the request by the -- cgit v1.2.3 From 106145c52354bf7de610173638012e6b2beeb54e Mon Sep 17 00:00:00 2001 From: Marius Gedminas Date: Fri, 31 Dec 2010 01:47:25 +0200 Subject: Typo: runnning -> running. --- CONTRIBUTORS.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index b48e769a1..2df5abfae 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -90,7 +90,7 @@ Licensing Exceptions Code committed within the ``docs/`` subdirectory of the Pyramid source control repository and "docstrings" which appear in the documentation -generated by runnning "make" within this directory is licensed under the +generated by running "make" within this directory is licensed under the Creative Commons Attribution-Noncommercial-Share Alike 3.0 United States License (http://creativecommons.org/licenses/by-nc-sa/3.0/us/). @@ -116,4 +116,3 @@ Contributors - Chris Rossi, 2010/11/10 - Casey Duncan, 2010/12/27 - -- cgit v1.2.3 From f82d090ef137ba56b84ad67e88bd9ae281884081 Mon Sep 17 00:00:00 2001 From: Marius Gedminas Date: Fri, 31 Dec 2010 01:47:43 +0200 Subject: Sign the contributor agreement. --- CONTRIBUTORS.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 2df5abfae..ab75197e7 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -116,3 +116,5 @@ Contributors - Chris Rossi, 2010/11/10 - Casey Duncan, 2010/12/27 + +- Marius Gedminas, 2010/12/31 -- cgit v1.2.3 From 9c0bef71a6df2f93e8b96e47c9d4562c115141d1 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 30 Dec 2010 21:17:17 -0500 Subject: move translator out of method scope to make intent clear that its not a closure --- pyramid/config.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pyramid/config.py b/pyramid/config.py index f6b4a2112..01341d92b 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -2158,11 +2158,6 @@ class Configurator(object): # same function once for each added translation directory, # which does too much work, but has the same effect. - def translator(msg): - request = get_current_request() - localizer = get_localizer(request) - return localizer.translate(msg) - ctranslate = ChameleonTranslate(translator) self.registry.registerUtility(ctranslate, IChameleonTranslate) @@ -2984,3 +2979,8 @@ class PyramidConfigurationMachine(ConfigurationMachine): self._seen_files.add(spec) return True +def translator(msg): + request = get_current_request() + localizer = get_localizer(request) + return localizer.translate(msg) + -- cgit v1.2.3 From b13311851c15b5177211ef0fb4f3be9152a85750 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 30 Dec 2010 21:17:47 -0500 Subject: clean up chameleon text test setup and teardown --- pyramid/tests/test_chameleon_text.py | 25 +++---------------------- 1 file changed, 3 insertions(+), 22 deletions(-) diff --git a/pyramid/tests/test_chameleon_text.py b/pyramid/tests/test_chameleon_text.py index 79bc7984c..789c78bbe 100644 --- a/pyramid/tests/test_chameleon_text.py +++ b/pyramid/tests/test_chameleon_text.py @@ -1,23 +1,16 @@ import unittest -from pyramid.testing import cleanUp from pyramid.testing import skip_on from pyramid import testing class Base: def setUp(self): - cleanUp() - import os - try: - # avoid spew from chameleon logger? - os.unlink(self._getTemplatePath('minimal.txt.py')) - except: - pass + self.config = testing.setUp() from zope.deprecation import __show__ __show__.off() def tearDown(self): - cleanUp() + testing.tearDown() from zope.deprecation import __show__ __show__.on() @@ -27,22 +20,10 @@ class Base: return os.path.join(here, 'fixtures', name) def _registerUtility(self, utility, iface, name=''): - from pyramid.threadlocal import get_current_registry - reg = get_current_registry() + reg = self.config.registry reg.registerUtility(utility, iface, name=name) - return reg - class TextTemplateRendererTests(Base, unittest.TestCase): - def setUp(self): - from pyramid.registry import Registry - registry = Registry() - self.config = testing.setUp(registry=registry) - self.config.begin() - - def tearDown(self): - self.config.end() - def _getTargetClass(self): from pyramid.chameleon_text import TextTemplateRenderer return TextTemplateRenderer -- cgit v1.2.3 From 87754b3ca824b2dc1e7a160e5bb03561603b2a92 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 30 Dec 2010 21:19:30 -0500 Subject: clean up chameleon_zpt test setup and teardown --- pyramid/tests/test_chameleon_zpt.py | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/pyramid/tests/test_chameleon_zpt.py b/pyramid/tests/test_chameleon_zpt.py index 4601c2f12..4fceb809c 100644 --- a/pyramid/tests/test_chameleon_zpt.py +++ b/pyramid/tests/test_chameleon_zpt.py @@ -1,17 +1,16 @@ import unittest -from pyramid.testing import cleanUp from pyramid.testing import skip_on from pyramid import testing class Base(object): def setUp(self): - cleanUp() + self.config = testing.setUp() from zope.deprecation import __show__ __show__.off() def tearDown(self): - cleanUp() + testing.tearDown() from zope.deprecation import __show__ __show__.on() @@ -21,21 +20,11 @@ class Base(object): return os.path.join(here, 'fixtures', name) def _registerUtility(self, utility, iface, name=''): - from pyramid.threadlocal import get_current_registry - reg = get_current_registry() + reg = self.config.registry reg.registerUtility(utility, iface, name=name) return reg class ZPTTemplateRendererTests(Base, unittest.TestCase): - def setUp(self): - from pyramid.registry import Registry - registry = Registry() - self.config = testing.setUp(registry=registry) - self.config.begin() - - def tearDown(self): - self.config.end() - def _getTargetClass(self): from pyramid.chameleon_zpt import ZPTTemplateRenderer return ZPTTemplateRenderer -- cgit v1.2.3 From b4bf06d74308c7d52764ed12b46bc58b2a90e6b8 Mon Sep 17 00:00:00 2001 From: Charlie Choiniere Date: Thu, 30 Dec 2010 21:24:17 -0800 Subject: Renamed 'home' route to 'view_wiki' for consistency with the following __init__.py examples. --- docs/tutorials/wiki2/src/views/tutorial/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorials/wiki2/src/views/tutorial/__init__.py b/docs/tutorials/wiki2/src/views/tutorial/__init__.py index 334fde814..1a8d24499 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/__init__.py +++ b/docs/tutorials/wiki2/src/views/tutorial/__init__.py @@ -10,7 +10,7 @@ def main(global_config, **settings): initialize_sql(engine) config = Configurator(settings=settings) config.add_static_view('static', 'tutorial:static') - config.add_route('home', '/', view='tutorial.views.view_wiki') + config.add_route('view_wiki', '/', view='tutorial.views.view_wiki') config.add_route('view_page', '/{pagename}', view='tutorial.views.view_page', view_renderer='tutorial:templates/view.pt') -- cgit v1.2.3 From e874b2b4fb9ca973eed1f019162c917750088f58 Mon Sep 17 00:00:00 2001 From: Jamaludin Ahmad Date: Fri, 31 Dec 2010 13:42:18 +0700 Subject: handlers_chapter -> views_chapter --- docs/glossary.rst | 2 +- docs/zcml/handler.rst | 2 +- pyramid/config.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/glossary.rst b/docs/glossary.rst index 49d273197..1abbcf84b 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -834,7 +834,7 @@ Glossary :meth:`pyramid.config.Configurator.add_route` and :meth:`pyramid.config.Configurator.add_view` to make it more convenient to register a collection of views as a single class when - using :term:`url dispatch`. See also :ref:`handlers_chapter`. + using :term:`url dispatch`. See also :ref:`views_chapter`. Deployment settings Deployment settings are settings passed to the :term:`Configurator` as a diff --git a/docs/zcml/handler.rst b/docs/zcml/handler.rst index 01d442ab6..64aac7e78 100644 --- a/docs/zcml/handler.rst +++ b/docs/zcml/handler.rst @@ -155,4 +155,4 @@ You can also add a :term:`route configuration` via: See Also ~~~~~~~~ -See also :ref:`handlers_chapter`. +See also :ref:`views_chapter`. diff --git a/pyramid/config.py b/pyramid/config.py index f6b4a2112..1cf0a4a53 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -923,7 +923,7 @@ class Configurator(object): Any extra keyword arguments are passed along to ``add_route``. - See :ref:`handlers_chapter` for more explanatory documentation. + See :ref:`views_chapter` for more explanatory documentation. This method returns the result of add_route.""" handler = self.maybe_dotted(handler) -- cgit v1.2.3 From 6d8f4dcb74df5b5c8ce9d0320138599af1085c7b Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 31 Dec 2010 02:46:06 -0500 Subject: use inbuilt PageTextTemplateFile rather than a hand-rolled subclass of TemplateFile --- pyramid/chameleon_text.py | 33 ++++++++------------------------- 1 file changed, 8 insertions(+), 25 deletions(-) diff --git a/pyramid/chameleon_text.py b/pyramid/chameleon_text.py index 32896b8e9..b687ecda9 100644 --- a/pyramid/chameleon_text.py +++ b/pyramid/chameleon_text.py @@ -4,39 +4,22 @@ from zope.deprecation import deprecated from zope.interface import implements try: - from chameleon.core.template import TemplateFile - TemplateFile # prevent pyflakes complaining about a redefinition below + from chameleon.zpt.template import PageTextTemplateFile + # prevent pyflakes complaining about a redefinition below + PageTextTemplateFile except ImportError: # pragma: no cover exc_class, exc, tb = sys.exc_info() # Chameleon doesn't work on non-CPython platforms - class TemplateFile(object): + class PageTextTemplateFile(object): def __init__(self, *arg, **kw): raise ImportError, exc, tb -try: - from chameleon.zpt.language import Parser - Parser # prevent pyflakes complaining about a redefinition below -except ImportError: # pragma: no cover - # Chameleon doesn't work on non-CPython platforms - class Parser(object): - pass - from pyramid.interfaces import ITemplateRenderer from pyramid.decorator import reify from pyramid import renderers from pyramid.path import caller_package -class TextTemplateFile(TemplateFile): - default_parser = Parser() - - def __init__(self, filename, parser=None, format='text', doctype=None, - **kwargs): - if parser is None: - parser = self.default_parser - super(TextTemplateFile, self).__init__(filename, parser, format, - doctype, **kwargs) - def renderer_factory(info): return renderers.template_renderer_factory(info, TextTemplateRenderer) @@ -51,10 +34,10 @@ class TextTemplateRenderer(object): if sys.platform.startswith('java'): # pragma: no cover raise RuntimeError( 'Chameleon templates are not compatible with Jython') - return TextTemplateFile(self.path, - auto_reload=self.lookup.auto_reload, - debug=self.lookup.debug, - translate=self.lookup.translate) + return PageTextTemplateFile(self.path, + auto_reload=self.lookup.auto_reload, + debug=self.lookup.debug, + translate=self.lookup.translate) def implementation(self): return self.template -- cgit v1.2.3 From b26badf557847bf5a55f896c63a3b6a97b468936 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 31 Dec 2010 02:46:15 -0500 Subject: ignore another subdir --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index ae0b17b8e..562abec68 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ .coverage tutorial.db env26/ +env26-debug/ env24/ env27/ jyenv/ -- cgit v1.2.3 From 13bfb5ae063fde4bb45f73340ca97d37279e4b34 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 31 Dec 2010 02:56:47 -0500 Subject: note previous change --- CHANGES.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 743f20e3b..04f5a7d05 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -48,6 +48,9 @@ Internals of ``pyramid.renderers.RendererHelper`` rather than a dictionary (a side effect of ``view_mapper`` refactoring). +- The class used as the "page template" in ``pyramid.chameleon_text`` was + removed, in preference to using a Chameleon-inbuilt version. + 1.0a8 (2010-12-27) ================== -- cgit v1.2.3 From adfcf6d579496495fb71f8c1af293a953b3a13cb Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Fri, 31 Dec 2010 10:02:55 -0500 Subject: Typo. --- docs/designdefense.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/designdefense.rst b/docs/designdefense.rst index 53b95b9d0..1d6941283 100644 --- a/docs/designdefense.rst +++ b/docs/designdefense.rst @@ -1604,10 +1604,10 @@ If you can understand this hello world program, you can use Pyramid: app = config.make_wsgi_app() serve(app) -Pyramid has ~ 650 of documentation (printed), covering topics from the very -basic to the most advanced. *Nothing* is left undocumented, quite literally. -It also has an *awesome*, very helpful community. Visit the #repoze and/or -#pylons IRC channels on freenode.net and see. +Pyramid has ~ 650 pages of documentation (printed), covering topics from the +very basic to the most advanced. *Nothing* is left undocumented, quite +literally. It also has an *awesome*, very helpful community. Visit the +#repoze and/or #pylons IRC channels on freenode.net and see. Hate Zope +++++++++ -- cgit v1.2.3 From f4c5f1a60612749ef36aae01d9a3a559b6acdfff Mon Sep 17 00:00:00 2001 From: Casey Duncan Date: Fri, 31 Dec 2010 13:48:07 -0700 Subject: add Much ado about traversal chapter from Rob Miller, with light adaptations. Also remove some now redundant overview content in the Traversal chapter, which is now only details. --- docs/index.rst | 1 + docs/narr/muchadoabouttraversal.rst | 293 ++++++++++++++++++++++++++++++++++++ docs/narr/traversal.rst | 50 +++--- 3 files changed, 313 insertions(+), 31 deletions(-) create mode 100644 docs/narr/muchadoabouttraversal.rst diff --git a/docs/index.rst b/docs/index.rst index 23ffb3b1b..3281de13c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -40,6 +40,7 @@ Narrative documentation in chapter form explaining how to use narr/startup narr/resourcelocation narr/urldispatch + narr/muchadoabouttraversal narr/traversal narr/views narr/renderers diff --git a/docs/narr/muchadoabouttraversal.rst b/docs/narr/muchadoabouttraversal.rst new file mode 100644 index 000000000..52b6dd3a7 --- /dev/null +++ b/docs/narr/muchadoabouttraversal.rst @@ -0,0 +1,293 @@ +.. _much_ado_about_traversal_chapter: + +======================== +Much Ado About Traversal +======================== + +Introduction +------------ + +A lot of folks who have been using Pylons (and, therefore, Routes-based +URL matching) are being exposed for the first time, via :app:`Pyramid`, +to new ideas such as ":term:`traversal`" and ":term:`view lookup`" as a +way to route incoming HTTP requests to callable code. This has caused a +bit of consternation in some circles. Many think that traversal is hard +to understand. Others question its usefulness; URL matching has worked +for them so far, why should they even consider dealing with another +approach, one which doesn't fit their brain and which doesn't provide +any immediately obvious value? + +This chapter is an attempt to counter these opinions. Traversal and +view lookup *are* useful. There are some straightforward, real-world +use cases that are much more easily served by a traversal-based approach +than by a pattern-matching mechanism. Even if you haven't yet hit one +of these use cases yourself, understanding these new ideas is worth the +effort for any web developer so you know when you might want to use +them. Especially because (WARNING: Bold Assertion Ahead) these ideas +are *not* particularly hard to understand. In fact, :term:`traversal` +is a straightforward metaphor easily comprehended by anyone who's ever +used a run-of-the-mill file system with folders and files. + +.. note:: + + Those of you who are already familiar with traversal and view lookup + conceptually, may want to skip directly to the + :ref:`traversal_chapter` chapter, which discusses the technical + details. + +URL Matching +------------ + +Let's take a step back. The problem we're trying to solve is +simple. We have an HTTP request for a particular path that +has been routed to our web application. The requested path will +possibly invoke a specific callable function defined somewhere in our +app, or it may point to nothing in which case a 404 response should be +generated. What we're trying to do is figure out is which callable +function, if any, should be invoked for a given requested path. + +URL matching (or :term:`URL dispatch` in :app:`Pyramid` parlance) +approaches this problem by parsing the URL path and comparing the +results to a set of registered "patterns", defined by a set of regular +expressions, or some other URL path templating syntax. Each pattern is +mapped to a callable function somewhere; if the request path matches a +specific pattern, the associated function is called. If the request +path matches more than one pattern, some conflict resolution scheme is +used, usually a simple order precedence so that the first match will +take priority over any subsequent matches. If a request path doesn't +match any of the defined patterns, we've got a 404. + +Just in case it's not crystal clear, we'll give an example. Using +:app:`Pyramid`'s syntax, we might have a match pattern such as +``/{userid}/photos/{photoid}``, mapped to a ``photo_view()`` function +defined somewhere in our code. Then a request for a path such as +``/joeschmoe/photos/photo1`` would be a match, and the ``photo_view()`` +function would be invoked to handle the request. Similarly, +``/{userid}/blog/{year}/{month}/{postid}`` might map to a +``blog_post_view()`` function, so +``/joeschmoe/blog/2010/12/urlmatching`` would trigger the function, +which presumably would know how to find and render the ``urlmatching`` +blog post. + +Historical Refresher +-------------------- + +Okay, we've got :term:`URL dispatch` out of the way, soon we'll dig in +to the supposedly "harder to understand" idea of traversal. Before we +do, though, let's take a trip down memory lane. If you've been doing +web work for a while, you may remember a time when we didn't have these +fancy web frameworks. Instead, we had general purpose HTTP servers that +primarily served files off of a file system. The "root" of a given site +mapped to a particular folder somewhere on the file system. Each +segment of the request path represented a subdirectory. The final path +segment would be either a directory or a file, and once the server found +the right file it would package it up in an HTTP response and send it +back to the client. So serving up a request for +``/joeschmoe/photos/photo1`` literally meant that there was a +``joeschmoe`` folder somewhere, which contained a ``photos`` folder, +which in turn contained a ``photo1`` file. If at any point along the +way we find that there is not a folder or file matching the requested +path, we return a 404 response. + +As the web grew more dynamic, however, a little bit of extra +complexity was added. Technologies such as CGI and HTTP server +modules were developed. Files were still looked up on the file +system, but if the file ended with (for example) ``.cgi`` or ``.php``, +or if it lived in a special folder, instead of simply sending the file +to the client the server would read the file, execute it using an +interpreter of some sort, and then send the output from this process +to the client as the final result. The server configuration specified +which files would trigger some dynamic code, with the default case +being to just serve the static file. + +Traversal (aka Resource Location) +--------------------------------- + +You with me so far? Good. Because if you understand how serving +files from a file system works, then you pretty much understand +traversal. And if you understand that a server might do something +different based on what type of file a given request specifies, then +you pretty much understand view lookup. + +Wait... what!?! + +.. index:: + single: traversal overview + +The only difference between file system lookup and traversal is that a +file system lookup is stepping through nested directories and files in +a file system tree, while traversal is stepping through nested +dictionary-type objects in an object tree. Let's take a detailed look +at one of our example paths, so we can see what I mean: + +With ``/joeschmoe/photos/photo1``, we've got 4 segments: ``/``, +``joeschmoe/``, ``photos/`` and ``photo1``. With file system +lookup we have a root folder (``/``) containing a nested folder +(``joeschmoe``), which contains ANOTHER nested folder (``photos``), +which finally contains a JPG file ("photo1"). With traversal, we +have a dictionary-like root object. Asking for the ``joeschmoe`` key +gives us another dictionary-like object. Asking this in turn for the +``photos`` key gives us yet another mapping object, which finally +(hopefully) contains the resource that we're looking for within its +values, referenced by the ``photo1`` key. + +In pure Python terms, then, the traversal or "resource location" +portion of satisfying the ``/joeschmoe/photos/photo1`` request +will look like this:: + + get_root()['joeschmoe']['photos']['photo1'] + +Where ``get_root()`` is some function that returns our root traversal +resource. If all of the specified keys exist, then the returned object +will be the resource that is being requested, analogous to the JPG file +that was retrieved in the file system example. If a :exc:`KeyError` is +generated anywhere along the way, we get a 404. (Well, this isn't +precisely true, as you'll see when we learn about view lookup below, but +the basic idea holds.) + +What is a "resource"? +--------------------- + +Okay, okay... files on a file system I understand, you might say. But +what are these nested dictionary things? Where do these objects, these +"resources", live? What *are* they? + +Well, since :app:`Pyramid` is not a highly opinionated framework, there +is no restriction on how a resource is implemented; the developer can do +whatever he wants. One common pattern is to persist all of the +resources, including the root, in a database. The root object stores +the ids of all of its subresources, and provides a ``__getitem__`` +implementation that fetches them. So ``get_root()`` fetches the unique +root object, while ``get_root()['joeschmoe']`` returns a different +object, also stored in the database, which in turn has its own +subresources and ``__getitem__`` implementation, etc. These resources +could be persisted in a relational database, one of the many "NoSQL" +solutions that are becoming popular these days, or anywhere else, it +doesn't matter. As long as the returned objects provide the +dictionary-like API (i.e. as long as they have an appropriately +implemented ``__getitem__`` method) then traversal will work. + +In fact, you don't need a "database" at all. You could trivially +implement a set of objects with ``__getitem__`` methods that search +for files in specific directories, and thus precisely recreate the +older mechanism of having the URL path mapped directly to a folder +structure on the file system. Traversal is in fact a superset of file +system lookup. + +View Lookup +----------- + +At this point we're nearly there. We've covered traversal, which is +the process by which a specific resource is retrieved according to a +specific URL path. But what is this "view lookup" business? + +View lookup comes from a simple realization, namely, that there is more +than one possible action that you might want to take for a single +resource. With our photo example, for instance, you might want to view +the photo in a page, but you might also want to provide a way for the +user to edit the photo and any associated metadata. We'll call the +former the ``view`` view, and the latter will be the ``edit`` view +(Original, I know.) :app:`Pyramid` has a centralized view registry +where named views can be associated with specific resource types. So in +our example, we'll assume that we've registered ``view`` and ``edit`` +views for photo objects, and that we've specified the ``view`` view as +the default, so that ``/joeschmoe/photos/photo1/view`` and +``/joeschmoe/photos/photo1`` are equivalent. The edit view would +sensibly be provided by a request for ``/joeschmoe/photos/photo1/edit``. + +Hopefully it's clear that the first portion of the edit view's URL path +is going to resolve to the same resource as the non-edit version, +specifically the resource returned by +``get_root()['joeschmoe']['photos']['photo1']``. But traveral ends +there; the ``photo1`` resource doesn't have an ``edit`` key. In fact, +it might not even be a dictionary-like object, in which case +``photo1['edit']`` would be meaningless. When :app:`Pyramid`'s resource +location has resolved to a *leaf* resource but the entire request path +has not yet been expended, the next path segment is treated as a view +name. The registry is then checked to see if a view of the given name +has been specified for a resource of the given type. If so, the view +callable is invoked, with the resource passed in as the ``context`` +object; if not, we 404. + +This is a slight simplification, but to summarize you can think of a +request for ``/joeschmoe/photos/photo1/edit`` as ultimately converted +into the following piece of Python:: + + context = get_root()['joeschmoe']['photos']['photo1'] + view_callable = registry.get_view(context, 'edit') + view_callable(context, request) + +That's not too hard to conceptualize, is it? + +Use Cases +--------- + +Let's come back around to look at why we even care. Yes, maybe +traversal and view lookup isn't mind-bending rocket science. But URL +matching is easier to explain, and it's good enough, right? + +In some cases, yes, but certainly not in all cases. So far we've had +very structured URLs, where our paths have had a specific, small +number of pieces, like this:: + + /{userid}/{typename}/{objectid}[/{view_name}] + +In all of the examples thus far, we've hard coded the typename value, +assuming that we'd know at development time what names were going to +be used ("photos", "blog", etc.). But what if we don't know what +these names will be? Or, worse yet, what if we don't know *anything* +about the structure of the URLs inside a user's folder? We could be +writing a CMS where we want the end user to be able to arbitrarily add +content and other folders inside his folder. He might decide to nest +folders dozens of layers deep. How would you construct matching +patterns that could account for every possible combination of paths +that might develop? + +It may be possible, but it's tricky at best. And your matching +patterns are going to become quite complex very quickly as you try +to handle all of the edge cases. + +With traversal, however, it's straightforward. You want 20 layers of +nesting? No problem, :app:`Pyramid` will happily call ``__getitem__`` +as long as it needs to, until it runs out of path segments or until it +gets a :exc:`KeyError`. Each resource only needs to know how to fetch +its immediate children, the traversal algorithm takes care of the rest. + +The key advantage of traversal here is that the structure of the +resource tree can live in the database, and not in the code. It's +simple to let users modify the tree at runtime to set up their own +personalized directory structures. + +Another use case in which traversal shines is when there is a need to +support a context-dependent security policy. One example might be a +document management infrastructure for a large corporation, where +members of different departments have varying access levels to the +various other departments' files. Reasonably, even specific files +might need to be made available to specific individuals. Traversal +does well here because the idea of a resource context is baked right +into the code resolution and calling process. Resource objects can +store ACLs, which can be inherited and/or overridden by the +subresources. + +If each resource can thus generate a context-based ACL, then whenever +view code is attempting to perform a sensitive action, it can check +against that ACL to see whether the current user should be allowed to +perform the action. In this way you achieve so called "instance based" +or "row level" security which is considerably harder to model using a +traditional tabular approach. :app:`Pyramid` actively supports such a +scheme, and in fact if you register your views with guard permissions +and use an authorization policy, :app:`Pyramid` can check against a +resource's ACL when deciding whether or not the view itself is available +to the current user. + +In summary, there are entire classes of problems that are more easily +served by traversal and view lookup than by :term:`URL dispatch`. If +your problems aren't of this nature, great, stick with :term:`URL +dispatch`. But if you're using :app:`Pyramid` and you ever find that +you *do* need to support one of these use cases, you'll be glad you have +traversal in your toolkit. + +.. note:: + It is even possible to mix and match :term:`traversal` with + :term:`URL dispatch` in the same :app:`Pyramid` application. See the + :ref:`hybrid_chapter` chapter for details. diff --git a/docs/narr/traversal.rst b/docs/narr/traversal.rst index 2d7878265..e8949880c 100644 --- a/docs/narr/traversal.rst +++ b/docs/narr/traversal.rst @@ -3,34 +3,22 @@ Traversal ========= -:term:`Traversal` provides an alternative to using :term:`URL dispatch` to -map a URL to a :term:`view callable`. It is the act of locating a -:term:`context` resource by walking over a :term:`resource tree`, starting -from a :term:`root` resource, using a :term:`request` object as a source of -path information. Once a context resource is found, a view callable is -looked up and invoked. - -Using :term:`Traversal` to map a URL to code is optional. It is often less -easy to understand than URL dispatch, so if you're a rank beginner, it -probably makes sense to use URL dispatch to map URLs to code instead of -traversal. In that case, you can skip this chapter. - -.. index:: - single: traversal overview - -A High-Level Overview of Traversal ----------------------------------- - A :term:`traversal` uses the URL (Universal Resource Locator) to find a -:term:`resource`. This is done by mapping each segment of the path portion -of the URL into a set of nested dictionary-like objects called the -:term:`resource tree`. You might think of this as looking up files and -directories in a file system. Traversal walks down the path until it finds a -published "directory" or "file". The resource we find as the result of a -traversal becomes the :term:`context`. A separate :term:`view lookup` -subsystem is used to then find some view code willing "publish" the context +:term:`resource` located in a :term:`resource tree`, which is a set of +nested dictionary-like objects. Traversal is done by using each segment +of the path portion of the URL to navigate through the :term:`resource +tree`. You might think of this as looking up files and directories in a +file system. Traversal walks down the path until it finds a published +"directory" or "file". The resource we find as the result of a +traversal becomes the :term:`context`. Then, the :term:`view lookup` +subsystem is used to find some view code willing "publish" this resource. +Using :term:`Traversal` to map a URL to code is optional. It is often +less easy to understand than :term:`URL dispatch`, so if you're a rank +beginner, it probably makes sense to use URL dispatch to map URLs to +code instead of traversal. In that case, you can skip this chapter.` + .. index:: single: traversal details @@ -76,7 +64,7 @@ element cannot be resolved to a resource. In either case, a :term:`context` resource is chosen. Traversal "stops" when it either reaches a leaf level resource in your -resource tree or when the path segments implied by the URL "run out". The +resource tree or when the path segments from the URL "run out". The resource that traversal "stops on" becomes the :term:`context`. If at any point during traversal any resource in the tree doesn't have a ``__getitem__`` method, or if the ``__getitem__`` method of a resource raises @@ -88,11 +76,11 @@ The results of a :term:`traversal` also include a :term:`view name`. The segments "left over" in the path segment list popped by the traversal process *after* traversal finds a context resource. -The combination of the context resource and the :term:`view name` found via -traversal is used later in the same request by a separate :app:`Pyramid` -subsystem -- the :term:`view lookup` subsystem -- to find a :term:`view -callable` later within the same request. How :app:`Pyramid` performs view -lookup is explained within the :ref:`views_chapter` chapter. +The combination of the context resource and the :term:`view name` found +via traversal is used later in the same request by the :term:`view +lookup` subsystem to find a :term:`view callable`. How :app:`Pyramid` +performs view lookup is explained within the :ref:`views_chapter` +chapter. .. index:: single: object tree -- cgit v1.2.3 From f4e5b83c671307c23203ed94916ce04ea29b4329 Mon Sep 17 00:00:00 2001 From: Casey Duncan Date: Fri, 31 Dec 2010 13:52:51 -0700 Subject: remove stray quote --- docs/narr/traversal.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/narr/traversal.rst b/docs/narr/traversal.rst index e8949880c..9226cf411 100644 --- a/docs/narr/traversal.rst +++ b/docs/narr/traversal.rst @@ -17,7 +17,7 @@ resource. Using :term:`Traversal` to map a URL to code is optional. It is often less easy to understand than :term:`URL dispatch`, so if you're a rank beginner, it probably makes sense to use URL dispatch to map URLs to -code instead of traversal. In that case, you can skip this chapter.` +code instead of traversal. In that case, you can skip this chapter. .. index:: single: traversal details -- cgit v1.2.3 From 8dcc5e13e1b32d19327ec4dbe5b92b6fbc59018e Mon Sep 17 00:00:00 2001 From: Casey Duncan Date: Fri, 31 Dec 2010 13:57:23 -0700 Subject: add much ado chapt to book toc --- docs/latexindex.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/latexindex.rst b/docs/latexindex.rst index 058835937..e2433746c 100644 --- a/docs/latexindex.rst +++ b/docs/latexindex.rst @@ -33,6 +33,7 @@ Narrative Documentation narr/project narr/resourcelocation narr/urldispatch + narr/muchadoabouttraversal narr/traversal narr/views narr/renderers -- cgit v1.2.3 From 26bb68f8f351ce8481ee066b5108274320794c58 Mon Sep 17 00:00:00 2001 From: Casey Duncan Date: Fri, 31 Dec 2010 13:57:52 -0700 Subject: remove heading at top --- docs/narr/muchadoabouttraversal.rst | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/narr/muchadoabouttraversal.rst b/docs/narr/muchadoabouttraversal.rst index 52b6dd3a7..13d279a42 100644 --- a/docs/narr/muchadoabouttraversal.rst +++ b/docs/narr/muchadoabouttraversal.rst @@ -4,9 +4,6 @@ Much Ado About Traversal ======================== -Introduction ------------- - A lot of folks who have been using Pylons (and, therefore, Routes-based URL matching) are being exposed for the first time, via :app:`Pyramid`, to new ideas such as ":term:`traversal`" and ":term:`view lookup`" as a -- cgit v1.2.3 From 597a6d7a6cca0865e114359010d3e15f5fb036b1 Mon Sep 17 00:00:00 2001 From: Casey Duncan Date: Fri, 31 Dec 2010 14:01:39 -0700 Subject: be more gender-neutral --- docs/narr/muchadoabouttraversal.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/narr/muchadoabouttraversal.rst b/docs/narr/muchadoabouttraversal.rst index 13d279a42..fcefb9811 100644 --- a/docs/narr/muchadoabouttraversal.rst +++ b/docs/narr/muchadoabouttraversal.rst @@ -151,7 +151,7 @@ what are these nested dictionary things? Where do these objects, these Well, since :app:`Pyramid` is not a highly opinionated framework, there is no restriction on how a resource is implemented; the developer can do -whatever he wants. One common pattern is to persist all of the +whatever they want. One common pattern is to persist all of the resources, including the root, in a database. The root object stores the ids of all of its subresources, and provides a ``__getitem__`` implementation that fetches them. So ``get_root()`` fetches the unique -- cgit v1.2.3 From 9f33f3b04079bf70482227afbbbbe86e325a5b8a Mon Sep 17 00:00:00 2001 From: Casey Duncan Date: Fri, 31 Dec 2010 14:03:37 -0700 Subject: URL Matching to URL Dispatch --- docs/narr/muchadoabouttraversal.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/narr/muchadoabouttraversal.rst b/docs/narr/muchadoabouttraversal.rst index fcefb9811..9bd829754 100644 --- a/docs/narr/muchadoabouttraversal.rst +++ b/docs/narr/muchadoabouttraversal.rst @@ -32,7 +32,7 @@ used a run-of-the-mill file system with folders and files. :ref:`traversal_chapter` chapter, which discusses the technical details. -URL Matching +URL Dispatch ------------ Let's take a step back. The problem we're trying to solve is -- cgit v1.2.3 From f9ccdf1c8ae253abee8e0a6dee7273ec2d15a286 Mon Sep 17 00:00:00 2001 From: Casey Duncan Date: Fri, 31 Dec 2010 14:09:43 -0700 Subject: add attribution for Rob Miller --- docs/copyright.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/copyright.rst b/docs/copyright.rst index fa564a785..6c59799fd 100644 --- a/docs/copyright.rst +++ b/docs/copyright.rst @@ -59,6 +59,9 @@ Used with permission: The :ref:`webob_chapter` chapter is adapted, with permission, from documentation originally written by Ian Bicking. + The :ref:`much_ado_about_traversal_chapter` chapter is adapted, + with permission, from an article written by Rob Miller. + .. Print Production .. ---------------- -- cgit v1.2.3 From 0dc6fea3f048c5d3c96fb2ae993d6a3956340546 Mon Sep 17 00:00:00 2001 From: Casey Duncan Date: Fri, 31 Dec 2010 14:10:50 -0700 Subject: add myself as a contributor, ain't I something? --- docs/copyright.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/copyright.rst b/docs/copyright.rst index 6c59799fd..691080ba7 100644 --- a/docs/copyright.rst +++ b/docs/copyright.rst @@ -50,7 +50,7 @@ Attributions Contributors: Ben Bangert, Blaise Laflamme, Carlos de la Guardia, Paul Everitt, - Marius Gedminas + Marius Gedminas, Casey Duncan .. Cover Designer: .. Nat Hardwick of `Electrosoup `_. -- cgit v1.2.3 From 9476cf70cb2eb27be79c1c973ffea70dd7eab808 Mon Sep 17 00:00:00 2001 From: Casey Duncan Date: Fri, 31 Dec 2010 14:16:36 -0700 Subject: remove rendundant sentence that was redundant --- docs/narr/traversal.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/narr/traversal.rst b/docs/narr/traversal.rst index 9226cf411..fe0dc6bed 100644 --- a/docs/narr/traversal.rst +++ b/docs/narr/traversal.rst @@ -95,8 +95,7 @@ The Resource Tree When your application uses :term:`traversal` to resolve URLs to code, the application must supply a :term:`resource tree` to :app:`Pyramid`. The resource tree is a set of nested dictionary-like objects. The root of the -tree is represented by a :term:`root` resource. The tree is effectively a -nested set of dictionary-like objects. +tree is represented by a :term:`root` resource. In order to supply a root resource for an application, at system startup time, the :app:`Pyramid` :term:`Router` is configured with a callback known -- cgit v1.2.3 From e653d5db0e2bf20b25024eb9bdb55aa35b3f2012 Mon Sep 17 00:00:00 2001 From: Casey Duncan Date: Fri, 31 Dec 2010 14:17:52 -0700 Subject: clarify --- docs/narr/traversal.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/narr/traversal.rst b/docs/narr/traversal.rst index fe0dc6bed..65e9ae3c7 100644 --- a/docs/narr/traversal.rst +++ b/docs/narr/traversal.rst @@ -125,7 +125,7 @@ constructor tells your :app:`Pyramid` application to call this root factory to generate a root resource whenever a request enters the application. This root factory is also known as the global root factory. A root factory can alternately be passed to the ``Configurator`` as a :term:`dotted Python name` -which refers to a root factory defined in a different module. +which can refer to a root factory defined in a different module. A root factory is passed a :term:`request` object and it is expected to return an object which represents the root of the resource tree. All -- cgit v1.2.3 From 45cc6fcd6a8e4c093e734f7fdbc69878d4df2bb4 Mon Sep 17 00:00:00 2001 From: Casey Duncan Date: Fri, 31 Dec 2010 14:19:15 -0700 Subject: clarify that the default root resource is empty --- docs/narr/traversal.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/narr/traversal.rst b/docs/narr/traversal.rst index 65e9ae3c7..fa8d7d7ee 100644 --- a/docs/narr/traversal.rst +++ b/docs/narr/traversal.rst @@ -137,7 +137,7 @@ or another persistence mechanism. If no :term:`root factory` is passed to the :app:`Pyramid` :term:`Configurator` constructor, or the ``root_factory`` is specified as the value ``None``, a *default* root factory is used. The default root factory -always returns a resource that has no child resources. +always returns a resource that has no child resources; it is effectively empty. .. sidebar:: Emulating the Default Root Factory -- cgit v1.2.3 From 95c95bf924a8c08e0a5b686d7a5d12fa4e49d87e Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 31 Dec 2010 16:47:13 -0500 Subject: - A view callable wrapper registered in the registry now contains an ``__original_view__`` attribute which references the original view callable (or class). --- CHANGES.txt | 4 ++++ pyramid/config.py | 4 ++++ pyramid/tests/test_config.py | 33 +++++++++++++++++++-------------- 3 files changed, 27 insertions(+), 14 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 04f5a7d05..826be0be6 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -51,6 +51,10 @@ Internals - The class used as the "page template" in ``pyramid.chameleon_text`` was removed, in preference to using a Chameleon-inbuilt version. +- A view callable wrapper registered in the registry now contains an + ``__original_view__`` attribute which references the original view callable + (or class). + 1.0a8 (2010-12-27) ================== diff --git a/pyramid/config.py b/pyramid/config.py index ee34adae1..01d453cd1 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -2695,6 +2695,10 @@ def wraps_view(wrapped): def preserve_view_attrs(view, wrapped_view): if wrapped_view is view: return view + original_view = getattr(view, '__original_view__', None) + if original_view is None: + original_view = view + wrapped_view.__original_view__ = original_view wrapped_view.__module__ = view.__module__ wrapped_view.__doc__ = view.__doc__ try: diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py index b2fa0e329..1760d119a 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -3886,19 +3886,25 @@ class Test__map_view(unittest.TestCase): request = self._makeRequest() self.assertEqual(result(None, request).body, 'Hello!') -class Test_wraps_view(unittest.TestCase): - def _callFUT(self, fn, view): - from pyramid.config import wraps_view - return wraps_view(fn)(None, view) +class Test_preserve_view_attrs(unittest.TestCase): + def _callFUT(self, view, wrapped_view): + from pyramid.config import preserve_view_attrs + return preserve_view_attrs(view, wrapped_view) def test_it_same(self): def view(context, request): """ """ - def afunc(self, view): - return view - result = self._callFUT(afunc, view) + result = self._callFUT(view, view) self.failUnless(result is view) + def test_it_different_with_existing_original_view(self): + def view1(context, request): pass + view1.__original_view__ = 'abc' + def view2(context, request): pass + result = self._callFUT(view1, view2) + self.assertEqual(result.__original_view__, 'abc') + self.failIf(result is view1) + def test_it_different(self): class DummyView1: """ 1 """ @@ -3906,9 +3912,9 @@ class Test_wraps_view(unittest.TestCase): __module__ = '1' def __call__(self, context, request): """ """ - def __call_permissive__(self, context, reuqest): + def __call_permissive__(self, context, request): """ """ - def __predicated__(self, context, reuqest): + def __predicated__(self, context, request): """ """ def __permitted__(self, context, request): """ """ @@ -3918,18 +3924,17 @@ class Test_wraps_view(unittest.TestCase): __module__ = '2' def __call__(self, context, request): """ """ - def __call_permissive__(self, context, reuqest): + def __call_permissive__(self, context, request): """ """ - def __predicated__(self, context, reuqest): + def __predicated__(self, context, request): """ """ def __permitted__(self, context, request): """ """ view1 = DummyView1() view2 = DummyView2() - def afunc(self, view): - return view1 - result = self._callFUT(afunc, view2) + result = self._callFUT(view2, view1) self.assertEqual(result, view1) + self.failUnless(view1.__original_view__ is view2) self.failUnless(view1.__doc__ is view2.__doc__) self.failUnless(view1.__module__ is view2.__module__) self.failUnless(view1.__name__ is view2.__name__) -- cgit v1.2.3 From 2526d8bdec3c2d84f3ab8ec1983150927fce7eae Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 31 Dec 2010 17:15:43 -0500 Subject: - The ``pyramid.interfaces.IAuthenticationPolicy`` interface now specifies an ``unauthenticated_userid`` method. This method supports an important optimization required by people who are using persistent storages which do not support object caching and whom want to create a "user object" as a request attribute. - A new API has been added to the ``pyramid.security`` module named ``unauthenticated_userid``. This API function calls the ``unauthenticated_userid`` method of the effective security policy. - An ``unauthenticated_userid`` method has been added to the dummy authentication policy returned by ``pyramid.config.Configurator.testing_securitypolicy``. It returns the same thing as that the dummy authentication policy's ``authenticated_userid`` method. - Since the ``pyramid.interfaces.IAuthenticationPolicy`` interface now specifies that a policy implementation must implement an ``unauthenticated_userid`` method, all third-party custom authentication policies now must implement this method. It, however, will only be called when the global function named ``pyramid.security.unauthenticated_userid`` is invoked, so if you're not invoking that, you will not notice any issues. - The (non-API) method of all internal authentication policy implementations previously named ``_get_userid`` is now named ``unauthenticated_userid``, promoted to an API method. If you were overriding this method, you'll now need to override it as ``unauthenticated_userid`` instead. --- CHANGES.txt | 31 +++++++++++++++++++++++++++++++ docs/api/security.rst | 2 ++ pyramid/authentication.py | 14 ++++++++++---- pyramid/interfaces.py | 15 +++++++++++++-- pyramid/security.py | 18 ++++++++++++++++++ pyramid/testing.py | 8 ++++++-- pyramid/tests/test_authentication.py | 33 ++++++++++++++++++++++++++++++++- pyramid/tests/test_security.py | 33 +++++++++++++++++++++++++++++++++ pyramid/tests/test_testing.py | 6 +++++- 9 files changed, 150 insertions(+), 10 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 826be0be6..19732a623 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -26,6 +26,32 @@ Features classmethod or staticmethod), use that as the decorator for each view registration for that handler. +- The ``pyramid.interfaces.IAuthenticationPolicy`` interface now specifies an + ``unauthenticated_userid`` method. This method supports an important + optimization required by people who are using persistent storages which do + not support object caching and whom want to create a "user object" as a + request attribute. + +- A new API has been added to the ``pyramid.security`` module named + ``unauthenticated_userid``. This API function calls the + ``unauthenticated_userid`` method of the effective security policy. + +- An ``unauthenticated_userid`` method has been added to the dummy + authentication policy returned by + ``pyramid.config.Configurator.testing_securitypolicy``. It returns the + same thing as that the dummy authentication policy's + ``authenticated_userid`` method. + +Backwards Incompatibilities +--------------------------- + +- Since the ``pyramid.interfaces.IAuthenticationPolicy`` interface now + specifies that a policy implementation must implement an + ``unauthenticated_userid`` method, all third-party custom authentication + policies now must implement this method. It, however, will only be called + when the global function named ``pyramid.security.unauthenticated_userid`` + is invoked, so if you're not invoking that, you will not notice any issues. + Documentation ------------- @@ -55,6 +81,11 @@ Internals ``__original_view__`` attribute which references the original view callable (or class). +- The (non-API) method of all internal authentication policy implementations + previously named ``_get_userid`` is now named ``unauthenticated_userid``, + promoted to an API method. If you were overriding this method, you'll now + need to override it as ``unauthenticated_userid`` instead. + 1.0a8 (2010-12-27) ================== diff --git a/docs/api/security.rst b/docs/api/security.rst index 4acf5fe4d..de249355d 100644 --- a/docs/api/security.rst +++ b/docs/api/security.rst @@ -10,6 +10,8 @@ Authentication API Functions .. autofunction:: authenticated_userid +.. autofunction:: unauthenticated_userid + .. autofunction:: effective_principals .. autofunction:: forget diff --git a/pyramid/authentication.py b/pyramid/authentication.py index 86d725bcf..e8ae48ed9 100644 --- a/pyramid/authentication.py +++ b/pyramid/authentication.py @@ -17,7 +17,7 @@ from pyramid.security import Everyone class CallbackAuthenticationPolicy(object): """ Abstract class """ def authenticated_userid(self, request): - userid = self._get_userid(request) + userid = self.unauthenticated_userid(request) if userid is None: return None if self.callback is None: @@ -27,7 +27,7 @@ class CallbackAuthenticationPolicy(object): def effective_principals(self, request): effective_principals = [Everyone] - userid = self._get_userid(request) + userid = self.unauthenticated_userid(request) if userid is None: return effective_principals if self.callback is None: @@ -89,6 +89,12 @@ class RepozeWho1AuthenticationPolicy(CallbackAuthenticationPolicy): if self.callback(identity, request) is not None: # is not None! return identity['repoze.who.userid'] + def unauthenticated_userid(self, request): + identity = self._get_identity(request) + if identity is None: + return None + return identity['repoze.who.userid'] + def effective_principals(self, request): effective_principals = [Everyone] identity = self._get_identity(request) @@ -147,7 +153,7 @@ class RemoteUserAuthenticationPolicy(CallbackAuthenticationPolicy): self.environ_key = environ_key self.callback = callback - def _get_userid(self, request): + def unauthenticated_userid(self, request): return request.environ.get(self.environ_key) def remember(self, request, principal, **kw): @@ -264,7 +270,7 @@ class AuthTktAuthenticationPolicy(CallbackAuthenticationPolicy): ) self.callback = callback - def _get_userid(self, request): + def unauthenticated_userid(self, request): result = self.cookie.identify(request) if result: return result['userid'] diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index 10a324b28..0a6b39de4 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -155,8 +155,19 @@ class IRouteRequest(Interface): class IAuthenticationPolicy(Interface): """ An object representing a Pyramid authentication policy. """ def authenticated_userid(request): - """ Return the authenticated userid or ``None`` if no - authenticated userid can be found. """ + """ Return the authenticated userid or ``None`` if no authenticated + userid can be found. This method of the policy should ensure that a + record exists in whatever persistent store is used related to the + user (the user should not have been deleted); if a record associated + with the current id does not exist in a persistent store, it should + return ``None``.""" + + def unauthenticated_userid(request): + """ Return the *unauthenticated* userid. This method performs the + same duty as ``authenticated_userid`` but is permitted to return the + userid based only on data present in the request; it neednt (and + shouldn't) check any persistent store to ensure that the user record + related to the request userid exists.""" def effective_principals(request): """ Return a sequence representing the effective principals diff --git a/pyramid/security.py b/pyramid/security.py index 723e87a87..51c0802d5 100644 --- a/pyramid/security.py +++ b/pyramid/security.py @@ -64,6 +64,24 @@ def authenticated_userid(request): return None return policy.authenticated_userid(request) +def unauthenticated_userid(request): + """ Return an object which represents the *claimed* (not verified) user + id of the credentials present in the request. ``None`` if there is no + :term:`authentication policy` in effect or there is no user data + associated with the current request. This differs from + :func:`~pyramid.security.authenticated_userid`, because the effective + authentication policy will not ensure that a record associated with the + userid exists in persistent storage.""" + try: + reg = request.registry + except AttributeError: + reg = get_current_registry() # b/c + + policy = reg.queryUtility(IAuthenticationPolicy) + if policy is None: + return None + return policy.unauthenticated_userid(request) + def effective_principals(request): """ Return the list of 'effective' :term:`principal` identifiers for the ``request``. This will include the userid of the diff --git a/pyramid/testing.py b/pyramid/testing.py index 61bb1843a..15fc385cd 100644 --- a/pyramid/testing.py +++ b/pyramid/testing.py @@ -44,9 +44,10 @@ def registerDummySecurityPolicy(userid=None, groupids=(), permissive=True): :func:`pyramid.security.authenticated_userid` or :func:`pyramid.security.effective_principals` APIs are used. - This function is most useful when testing code that uses the APIs - named :func:`pyramid.security.has_permission`, + This function is most useful when testing code that uses the APIs named + :func:`pyramid.security.has_permission`, :func:`pyramid.security.authenticated_userid`, + :func:`pyramid.security.unauthenticated_userid`, :func:`pyramid.security.effective_principals`, and :func:`pyramid.security.principals_allowed_by_permission`. @@ -332,6 +333,9 @@ class DummySecurityPolicy(object): def authenticated_userid(self, request): return self.userid + def unauthenticated_userid(self, request): + return self.userid + def effective_principals(self, request): effective_principals = [Everyone] if self.userid: diff --git a/pyramid/tests/test_authentication.py b/pyramid/tests/test_authentication.py index d9d0c2c97..49d655466 100644 --- a/pyramid/tests/test_authentication.py +++ b/pyramid/tests/test_authentication.py @@ -18,11 +18,22 @@ class TestRepozeWho1AuthenticationPolicy(unittest.TestCase): from pyramid.interfaces import IAuthenticationPolicy verifyObject(IAuthenticationPolicy, self._makeOne()) + def test_unauthenticated_userid_returns_None(self): + request = DummyRequest({}) + policy = self._makeOne() + self.assertEqual(policy.unauthenticated_userid(request), None) + + def test_unauthenticated_userid(self): + request = DummyRequest( + {'repoze.who.identity':{'repoze.who.userid':'fred'}}) + policy = self._makeOne() + self.assertEqual(policy.unauthenticated_userid(request), 'fred') + def test_authenticated_userid_None(self): request = DummyRequest({}) policy = self._makeOne() self.assertEqual(policy.authenticated_userid(request), None) - + def test_authenticated_userid(self): request = DummyRequest( {'repoze.who.identity':{'repoze.who.userid':'fred'}}) @@ -132,6 +143,16 @@ class TestRemoteUserAuthenticationPolicy(unittest.TestCase): from pyramid.interfaces import IAuthenticationPolicy verifyObject(IAuthenticationPolicy, self._makeOne()) + def test_unauthenticated_userid_returns_None(self): + request = DummyRequest({}) + policy = self._makeOne() + self.assertEqual(policy.unauthenticated_userid(request), None) + + def test_unauthenticated_userid(self): + request = DummyRequest({'REMOTE_USER':'fred'}) + policy = self._makeOne() + self.assertEqual(policy.unauthenticated_userid(request), 'fred') + def test_authenticated_userid_None(self): request = DummyRequest({}) policy = self._makeOne() @@ -196,6 +217,16 @@ class TestAutkTktAuthenticationPolicy(unittest.TestCase): from pyramid.interfaces import IAuthenticationPolicy verifyObject(IAuthenticationPolicy, self._makeOne(None, None)) + def test_unauthenticated_userid_returns_None(self): + request = DummyRequest({}) + policy = self._makeOne(None, None) + self.assertEqual(policy.unauthenticated_userid(request), None) + + def test_unauthenticated_userid(self): + request = DummyRequest({'REMOTE_USER':'fred'}) + policy = self._makeOne(None, {'userid':'fred'}) + self.assertEqual(policy.unauthenticated_userid(request), 'fred') + def test_authenticated_userid_no_cookie_identity(self): request = DummyRequest({}) policy = self._makeOne(None, None) diff --git a/pyramid/tests/test_security.py b/pyramid/tests/test_security.py index dd9d48f45..94cefa642 100644 --- a/pyramid/tests/test_security.py +++ b/pyramid/tests/test_security.py @@ -224,6 +224,36 @@ class TestAuthenticatedUserId(unittest.TestCase): result = self._callFUT(request) self.assertEqual(result, 'yo') +class TestUnauthenticatedUserId(unittest.TestCase): + def setUp(self): + cleanUp() + + def tearDown(self): + cleanUp() + + def _callFUT(self, request): + from pyramid.security import unauthenticated_userid + return unauthenticated_userid(request) + + def test_no_authentication_policy(self): + request = _makeRequest() + result = self._callFUT(request) + self.assertEqual(result, None) + + def test_with_authentication_policy(self): + request = _makeRequest() + _registerAuthenticationPolicy(request.registry, 'yo') + result = self._callFUT(request) + self.assertEqual(result, 'yo') + + def test_with_authentication_policy_no_reg_on_request(self): + from pyramid.threadlocal import get_current_registry + request = DummyRequest({}) + registry = get_current_registry() + _registerAuthenticationPolicy(registry, 'yo') + result = self._callFUT(request) + self.assertEqual(result, 'yo') + class TestEffectivePrincipals(unittest.TestCase): def setUp(self): cleanUp() @@ -355,6 +385,9 @@ class DummyAuthenticationPolicy: def effective_principals(self, request): return self.result + def unauthenticated_userid(self, request): + return self.result + def authenticated_userid(self, request): return self.result diff --git a/pyramid/tests/test_testing.py b/pyramid/tests/test_testing.py index ec6fdac5f..d2ed957f2 100644 --- a/pyramid/tests/test_testing.py +++ b/pyramid/tests/test_testing.py @@ -297,7 +297,11 @@ class TestDummySecurityPolicy(unittest.TestCase): def test_authenticated_userid(self): policy = self._makeOne('user') self.assertEqual(policy.authenticated_userid(None), 'user') - + + def test_unauthenticated_userid(self): + policy = self._makeOne('user') + self.assertEqual(policy.unauthenticated_userid(None), 'user') + def test_effective_principals_userid(self): policy = self._makeOne('user', ('group1',)) from pyramid.security import Everyone -- cgit v1.2.3 From ee50aec09576620537ff68895cfb81fd4663a45f Mon Sep 17 00:00:00 2001 From: Casey Duncan Date: Fri, 31 Dec 2010 18:24:18 -0700 Subject: Remove resource location chapter and move intro parts to url dispatch. The new much ado about traversal chapter takes care of selling traversal now --- docs/index.rst | 1 - docs/latexindex.rst | 1 - docs/narr/firstapp.rst | 2 +- docs/narr/resourcelocation.rst | 103 ----------------------------------------- docs/narr/router.rst | 2 +- docs/narr/security.rst | 3 +- docs/narr/urldispatch.rst | 25 +++++++--- docs/narr/views.rst | 10 ++-- 8 files changed, 26 insertions(+), 121 deletions(-) delete mode 100644 docs/narr/resourcelocation.rst diff --git a/docs/index.rst b/docs/index.rst index ecd401ddb..2f3589499 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -38,7 +38,6 @@ Narrative documentation in chapter form explaining how to use narr/firstapp narr/project narr/startup - narr/resourcelocation narr/urldispatch narr/muchadoabouttraversal narr/traversal diff --git a/docs/latexindex.rst b/docs/latexindex.rst index e2433746c..9635e6e8a 100644 --- a/docs/latexindex.rst +++ b/docs/latexindex.rst @@ -31,7 +31,6 @@ Narrative Documentation narr/configuration narr/firstapp narr/project - narr/resourcelocation narr/urldispatch narr/muchadoabouttraversal narr/traversal diff --git a/docs/narr/firstapp.rst b/docs/narr/firstapp.rst index cb1e54b19..1a81134f5 100644 --- a/docs/narr/firstapp.rst +++ b/docs/narr/firstapp.rst @@ -10,7 +10,7 @@ more detail how it works. .. note:: If you're a "theory-first" kind of person, you might choose to read - :ref:`resourcelocation_chapter` and :ref:`views_chapter` before diving into + :ref:`urldispatch_chapter` and :ref:`views_chapter` before diving into the code that follows, but it's not necessary if -- like many programmers -- you're willing to "go with the flow". diff --git a/docs/narr/resourcelocation.rst b/docs/narr/resourcelocation.rst deleted file mode 100644 index 8ddc890ed..000000000 --- a/docs/narr/resourcelocation.rst +++ /dev/null @@ -1,103 +0,0 @@ -.. index:: - single: resource location - -.. _resourcelocation_chapter: - -Resource Location and View Lookup ---------------------------------- - -:app:`Pyramid` uses two separate but cooperating subsystems to find and -invoke :term:`view callable` code written by the application developer: -:term:`resource location` and :term:`view lookup`. - -- First, a :app:`Pyramid` :term:`resource location` subsystem is given a - :term:`request`; it is responsible for finding a :term:`resource` object - based on information present in the request. When a resource is found via - resource location, it becomes known as the :term:`context`. - -- Next, using the context resource found by :term:`resource location` and the - :term:`request`, :term:`view lookup` is then responsible for finding and - invoking a :term:`view callable`. A view callable is a specific bit of - code written and registered by the application developer which receives the - :term:`request` and which returns a :term:`response`. - -These two subsystems are used by :app:`Pyramid` serially: first, a -:term:`resource location` subsystem does its job. Then the result of -resource location is passed to the :term:`view lookup` subsystem. The view -lookup system finds a :term:`view callable` written by an application -developer, and invokes it. A view callable returns a :term:`response`. The -response is returned to the requesting user. - -There are two separate :term:`resource location` subsystems in -:app:`Pyramid`: :term:`traversal` and :term:`URL dispatch`. They can be used -separately or they can be combined. Three chapters which follow describe -:term:`resource location`: :ref:`traversal_chapter`, -:ref:`urldispatch_chapter` and :ref:`hybrid_chapter`. - -There is only one :term:`view lookup` subsystem present in :app:`Pyramid`. -Where appropriate, we will describe how view lookup interacts with context -finding. One chapter which follows describes :term:`view lookup`: -:ref:`views_chapter`. - -Should I Use Traversal or URL Dispatch for Resource Location? -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -When you use :app:`Pyramid`, you have a choice about how you'd like to -resolve URLs to code: you can use either :term:`traversal` or :term:`URL -dispatch`. The choice to use traversal vs. URL dispatch is largely -"religious". Since :app:`Pyramid` provides support for both approaches, you -can use either exclusively or combine them as you see fit. - -:term:`URL dispatch` is very straightforward. When you limit your -application to using URL dispatch, you know every URL that your application -might generate or respond to, all the URL matching elements are listed in a -single place, and you needn't think about :term:`resource location` or -:term:`view lookup` at all. - -URL dispatch can easily handle URLs such as -``http://example.com/members/Chris``, where it's assumed that each item -"below" ``members`` in the URL represents a single member in some system. -You just match everything "below" ``members`` to a particular :term:`view -callable`, e.g. ``/members/{memberid}``. - -However, URL dispatch is not very convenient if you'd like your URLs to -represent an arbitrary-depth hierarchy. For example, if you need to infer -the difference between sets of URLs such as these, where the ``document`` in -the first URL represents a PDF document, and ``/stuff/page`` in the second -represents an OpenOffice document in a "stuff" folder. - -.. code-block:: text - - http://example.com/members/Chris/document - http://example.com/members/Chris/stuff/page - -It takes more pattern matching assertions to be able to make hierarchies work -in URL-dispatch based systems, and some assertions just aren't possible. -URL-dispatch based systems just don't deal very well with URLs that represent -arbitrary-depth hierarchies. - -:term:`URL dispatch` tends to collapse the two steps of :term:`resource -location` and :term:`view lookup` into a single step. Thus, a URL can map -*directly* to a view callable. This makes URL dispatch easier to understand -than traversal, because traversal makes you understand how :term:`resource -location` works. But explicitly locating a resource provides extra -flexibility. For example, it makes it possible to protect your application -with declarative context-sensitive instance-level :term:`authorization`. - -Unlike URL dispatch, :term:`traversal` works well for URLs that represent -arbitrary-depth hierarchies. Since the path segments that compose a URL are -addressed separately, it becomes very easy to form URLs that represent -arbitrary depth hierarchies in a system that uses traversal. When you're -willing to treat your application resources as a tree that can be traversed, -it also becomes easy to provide "instance-level security": you just attach an -:term:`ACL` security declaration to each resource in the tree. This is not -nearly as easy to do when using URL dispatch. - -Traversal probably just doesn't make any sense when you possess completely -"square" data stored in a relational database because it requires the -construction and maintenance of a resource tree and requires that the -developer think about mapping URLs to code in terms of traversing that tree. - -We'll examine both :term:`URL dispatch` and :term:`traversal` in the next two -chapters. - diff --git a/docs/narr/router.rst b/docs/narr/router.rst index d3d5bd370..f9e98373c 100644 --- a/docs/narr/router.rst +++ b/docs/narr/router.rst @@ -133,6 +133,6 @@ processing? This is a very high-level overview that leaves out various details. For more detail about subsystems invoked by the :app:`Pyramid` router such as traversal, URL dispatch, views, and event processing, see -:ref:`resourcelocation_chapter`, :ref:`views_chapter`, and +:ref:`urldispatch_chapter`, :ref:`views_chapter`, and :ref:`events_chapter`. diff --git a/docs/narr/security.rst b/docs/narr/security.rst index c5262faa2..62a4727bc 100644 --- a/docs/narr/security.rst +++ b/docs/narr/security.rst @@ -18,8 +18,7 @@ works at a high level: :term:`resource location`. A context is located differently depending on whether the application uses :term:`traversal` or :term:`URL dispatch`, but a context is ultimately found in either case. See - :ref:`resourcelocation_chapter` for more information about resource - location. + the :ref:`urldispatch_chapter` chapter for more information. - A :term:`view callable` is located by :term:`view lookup` using the context as well as other attributes of the request. diff --git a/docs/narr/urldispatch.rst b/docs/narr/urldispatch.rst index 0d28a0e96..d9228bf52 100644 --- a/docs/narr/urldispatch.rst +++ b/docs/narr/urldispatch.rst @@ -9,13 +9,24 @@ URL Dispatch :term:`URL dispatch` provides a simple way to map URLs :term:`view` code using a simple pattern matching language. An ordered set of patterns is checked one-by-one. If one of the patterns matches the path information -associated with a request, a particular :term:`view callable` is invoked. If -no route matches, :app:`Pyramid` falls back to trying to use -:term:`traversal` to map the current request to a :term:`view callable`. - -The presence of calls to the :meth:`pyramid.config.Configurator.add_route` -method within your application is a sign that you're using :term:`URL -dispatch`. +associated with a request, a particular :term:`view callable` is invoked. + +:term:`URL dispatch` is one of two ways to perform :term:`resource +location` in :app:`Pyramid`; the other way is using :term:`traversal`. +If no route is matched using :term:`URL dispatch`, :app:`Pyramid` falls +back to :term:`traversal` to handle the :term:`request`. + +It is the responsibility of the :term:`resource location` subsystem +(i.e., :term:`URL dispatch` or :term:`traversal`) to find the resource +object that is the :term:`context` of the :term:`request`. Once the +:term:`context` is determined, :term:`view lookup` is then responsible +for finding and invoking a :term:`view callable`. A view callable is a +specific bit of code, defined in your application, that receives the +:term:`request` and returns a :term:`response` object. + +Where appropriate, we will describe how view lookup interacts with +:term:`resource location`. The :ref:`views_chapter` describes the +details of :term:`view lookup`. High-Level Operational Overview ------------------------------- diff --git a/docs/narr/views.rst b/docs/narr/views.rst index ad28e48d4..81f3e644f 100644 --- a/docs/narr/views.rst +++ b/docs/narr/views.rst @@ -17,11 +17,11 @@ request made to your application. that implements a view *callable*, and the process of view *lookup*. -The chapter :ref:`resourcelocation_chapter` describes how, using information -from the :term:`request`, a :term:`context` resource is computed. But the -context resource itself isn't very useful without an associated :term:`view -callable`. A view callable returns a response to a user, often using the -context resource to do so. +The :ref:`urldispatch_chapter`, and :ref:`traversal_chapter` describes how, +using information from the :term:`request`, a :term:`context` resource is +computed. But the context resource itself isn't very useful without an +associated :term:`view callable`. A view callable returns a response to a +user, often using the context resource to do so. The job of actually locating and invoking the "best" :term:`view callable` is the job of the :term:`view lookup` subsystem. The view lookup subsystem -- cgit v1.2.3 From 096e80e94071d5066fd417517c8e5ec724fcc30d Mon Sep 17 00:00:00 2001 From: Casey Duncan Date: Fri, 31 Dec 2010 19:12:33 -0700 Subject: add missing word --- docs/narr/urldispatch.rst | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/narr/urldispatch.rst b/docs/narr/urldispatch.rst index d9228bf52..7f2aee26c 100644 --- a/docs/narr/urldispatch.rst +++ b/docs/narr/urldispatch.rst @@ -6,10 +6,11 @@ URL Dispatch ============ -:term:`URL dispatch` provides a simple way to map URLs :term:`view` code -using a simple pattern matching language. An ordered set of patterns is -checked one-by-one. If one of the patterns matches the path information -associated with a request, a particular :term:`view callable` is invoked. +:term:`URL dispatch` provides a simple way to map URLs to :term:`view` +code using a simple pattern matching language. An ordered set of +patterns is checked one-by-one. If one of the patterns matches the path +information associated with a request, a particular :term:`view +callable` is invoked. :term:`URL dispatch` is one of two ways to perform :term:`resource location` in :app:`Pyramid`; the other way is using :term:`traversal`. -- cgit v1.2.3 From c76dc8e3d8cd8b419aeebff06e7b31abaf350561 Mon Sep 17 00:00:00 2001 From: Casey Duncan Date: Fri, 31 Dec 2010 19:13:51 -0700 Subject: add word chapter --- docs/narr/urldispatch.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/narr/urldispatch.rst b/docs/narr/urldispatch.rst index 7f2aee26c..5b8cb493e 100644 --- a/docs/narr/urldispatch.rst +++ b/docs/narr/urldispatch.rst @@ -25,9 +25,9 @@ for finding and invoking a :term:`view callable`. A view callable is a specific bit of code, defined in your application, that receives the :term:`request` and returns a :term:`response` object. -Where appropriate, we will describe how view lookup interacts with -:term:`resource location`. The :ref:`views_chapter` describes the -details of :term:`view lookup`. +Where appropriate, we will describe how view lookup interacts with +:term:`resource location`. The :ref:`views_chapter` chapter describes +the details of :term:`view lookup`. High-Level Operational Overview ------------------------------- -- cgit v1.2.3 From 51c5727393178461868a8072d921b7215c626257 Mon Sep 17 00:00:00 2001 From: Casey Duncan Date: Fri, 31 Dec 2010 19:17:59 -0700 Subject: simplify route/traversal relationship. No need to repeat ourselves --- docs/narr/urldispatch.rst | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/narr/urldispatch.rst b/docs/narr/urldispatch.rst index 5b8cb493e..c2d778dd9 100644 --- a/docs/narr/urldispatch.rst +++ b/docs/narr/urldispatch.rst @@ -36,10 +36,9 @@ If route configuration is present in an application, the :app:`Pyramid` :term:`Router` checks every incoming request against an ordered set of URL matching patterns present in a *route map*. -If any route pattern matches the information in the :term:`request` provided -to :app:`Pyramid`, :app:`Pyramid` will shortcut :term:`traversal`, and will -invoke :term:`view lookup` using a :term:`context` resource generated by the -route match. +If any route pattern matches the information in the :term:`request`, +:app:`Pyramid` will invoke :term:`view lookup` using a :term:`context` +resource generated by the route match. However, if no route pattern matches the information in the :term:`request` provided to :app:`Pyramid`, it will fail over to using :term:`traversal` to -- cgit v1.2.3 From 9423c199cd87f15526b09a49b1c2bd4d029258ea Mon Sep 17 00:00:00 2001 From: Casey Duncan Date: Fri, 31 Dec 2010 19:20:32 -0700 Subject: don't repeat the version so many times --- docs/narr/urldispatch.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/narr/urldispatch.rst b/docs/narr/urldispatch.rst index c2d778dd9..c0b132a04 100644 --- a/docs/narr/urldispatch.rst +++ b/docs/narr/urldispatch.rst @@ -83,7 +83,7 @@ example: .. versionchanged:: 1.0a4 Prior to 1.0a4, routes allow for a marker starting with a ``:``, for - example ``/prefix/:one/:two``. Starting in 1.0a4, this style is deprecated + example ``/prefix/:one/:two``. This style is now deprecated in favor or ``{}`` usage which allows for additional functionality. .. index:: -- cgit v1.2.3 From 55f967479038b66a7b1eb017ef31e2466a10c34f Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 1 Jan 2011 00:49:45 -0500 Subject: resolve missing reference --- docs/designdefense.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/designdefense.rst b/docs/designdefense.rst index 1d6941283..a54a47025 100644 --- a/docs/designdefense.rst +++ b/docs/designdefense.rst @@ -895,9 +895,9 @@ Pyramid Applications are Extensible; I Don't Believe In Application Extensibilit Any :app:`Pyramid` application written obeying certain constraints is *extensible*. This feature is discussed in the :app:`Pyramid` documentation -chapters named :ref:`extending_chapter` and :ref:`advconf_narr`. It is made -possible by the use of the :term:`Zope Component Architecture` and within -:app:`Pyramid`. +chapters named :ref:`extending_chapter` and :ref:`advconfig_narr`. It is +made possible by the use of the :term:`Zope Component Architecture` and +within :app:`Pyramid`. "Extensible", in this context, means: -- cgit v1.2.3 From 83fa670377d797f48e5a30eaf7bf99d117520f95 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 1 Jan 2011 00:53:16 -0500 Subject: fix url --- docs/designdefense.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/designdefense.rst b/docs/designdefense.rst index a54a47025..df14fb440 100644 --- a/docs/designdefense.rst +++ b/docs/designdefense.rst @@ -1018,7 +1018,7 @@ Challenge :app:`Pyramid` performs automatic authorization checks only at :term:`view` execution time. Zope 3 wraps context objects with a `security proxy -`, which causes Zope 3 to +`_, which causes Zope 3 to do also security checks during attribute access. I like this, because it means: -- cgit v1.2.3 From f69e1986538ff12fbea36f378b9a2e74cfd11aaf Mon Sep 17 00:00:00 2001 From: Casey Duncan Date: Sat, 1 Jan 2011 12:17:07 -0700 Subject: clarify opening paragraph --- docs/narr/traversal.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/narr/traversal.rst b/docs/narr/traversal.rst index fa8d7d7ee..d1d695dce 100644 --- a/docs/narr/traversal.rst +++ b/docs/narr/traversal.rst @@ -9,10 +9,11 @@ nested dictionary-like objects. Traversal is done by using each segment of the path portion of the URL to navigate through the :term:`resource tree`. You might think of this as looking up files and directories in a file system. Traversal walks down the path until it finds a published -"directory" or "file". The resource we find as the result of a -traversal becomes the :term:`context`. Then, the :term:`view lookup` +resource, analogous to a file system "directory" or "file". The +resource found as the result of a traversal becomes the +:term:`context` of the :term:`request`. Then, the :term:`view lookup` subsystem is used to find some view code willing "publish" this -resource. +resource by generating a :term:`response`. Using :term:`Traversal` to map a URL to code is optional. It is often less easy to understand than :term:`URL dispatch`, so if you're a rank -- cgit v1.2.3 From 2ceb1f3064de5be4765a47800135400e992127ee Mon Sep 17 00:00:00 2001 From: Casey Duncan Date: Sat, 1 Jan 2011 12:20:23 -0700 Subject: simplify/clarify --- docs/narr/traversal.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/narr/traversal.rst b/docs/narr/traversal.rst index d1d695dce..bee9ce57b 100644 --- a/docs/narr/traversal.rst +++ b/docs/narr/traversal.rst @@ -26,11 +26,11 @@ code instead of traversal. In that case, you can skip this chapter. Traversal Details ----------------- -:term:`Traversal` is dependent on information in a :term:`request` object. -Every :term:`request` object contains URL path information in the -``PATH_INFO`` portion of the :term:`WSGI` environment. The ``PATH_INFO`` -portion of the WSGI environment is the portion of a request's URL following -the hostname and port number, but before any query string elements or +:term:`Traversal` is dependent on information in a :term:`request` +object. Every :term:`request` object contains URL path information in +the ``PATH_INFO`` portion of the :term:`WSGI` environment. The +``PATH_INFO`` string is the portion of a request's URL following the +hostname and port number, but before any query string elements or fragment element. For example the ``PATH_INFO`` portion of the URL ``http://example.com:8080/a/b/c?foo=1`` is ``/a/b/c``. -- cgit v1.2.3 From 234c0d612a82e9ae00a5c44953b0900520df5184 Mon Sep 17 00:00:00 2001 From: Casey Duncan Date: Sat, 1 Jan 2011 12:25:24 -0700 Subject: reword for clarity --- docs/narr/traversal.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/narr/traversal.rst b/docs/narr/traversal.rst index bee9ce57b..291faf725 100644 --- a/docs/narr/traversal.rst +++ b/docs/narr/traversal.rst @@ -34,13 +34,13 @@ hostname and port number, but before any query string elements or fragment element. For example the ``PATH_INFO`` portion of the URL ``http://example.com:8080/a/b/c?foo=1`` is ``/a/b/c``. -Traversal treats the ``PATH_INFO`` segment of a URL as a sequence of path -segments. For example, the ``PATH_INFO`` string ``/a/b/c`` is converted to -the sequence ``['a', 'b', 'c']``. +Traversal treats the ``PATH_INFO`` segment of a URL as a sequence of +path segments. For example, the ``PATH_INFO`` string ``/a/b/c`` is +converted to the sequence ``['a', 'b', 'c']``. -After the path info is converted, a lookup is performed against the resource -tree for each path segment. Each lookup uses the ``__getitem__`` method of a -resource in the tree. +This path sequence is then used to descend through the :term:`resource +tree`, looking up a resource for each path segment. Each lookup uses the +``__getitem__`` method of a resource in the tree. For example, if the path info sequence is ``['a', 'b', 'c']``: -- cgit v1.2.3 From 1dff47ead5b1beedab1b8cbbab4ad7a93fc4c5d7 Mon Sep 17 00:00:00 2001 From: Casey Duncan Date: Sat, 1 Jan 2011 12:58:25 -0700 Subject: clarify traversal details --- docs/narr/traversal.rst | 44 +++++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/docs/narr/traversal.rst b/docs/narr/traversal.rst index 291faf725..2b40c4d94 100644 --- a/docs/narr/traversal.rst +++ b/docs/narr/traversal.rst @@ -44,33 +44,35 @@ tree`, looking up a resource for each path segment. Each lookup uses the For example, if the path info sequence is ``['a', 'b', 'c']``: -- :term:`Traversal` pops the first element (``a``) from the path segment - sequence and attempts to call the root resource's ``__getitem__`` method - using that value (``a``) as an argument; we'll presume it succeeds. +- :term:`Traversal` starts by acquiring the :term:`root` resource of the + application by calling the :term:`root factory`. The :term:`root factory` + can be configured to return whatever object is appropriate as the + traversal root of your application. -- When the root resource's ``__getitem__`` succeeds it will return another - resource, which we'll call "A". The :term:`context` temporarily becomes - the "A" resource. +- Next, the first element (``a``) is popped from the path segment + sequence and is used as a key to lookup the corresponding resource + in the root. This invokes the root resource's ``__getitem__`` method + using that value (``a``) as an argument. + +- If the root resource "contains" a resource with key ``a``, its + ``__getitem__`` method will return it. The :term:`context` temporarily + becomes the "A" resource. - The next segment (``b``) is popped from the path sequence, and the "A" resource's ``__getitem__`` is called with that value (``b``) as an argument; we'll presume it succeeds. -- When the "A" resource's ``__getitem__`` succeeds it will return another - resource, which we'll call "B". The :term:`context` temporarily becomes - the "B" resource. - -This process continues until the path segment sequence is exhausted or a path -element cannot be resolved to a resource. In either case, a :term:`context` -resource is chosen. - -Traversal "stops" when it either reaches a leaf level resource in your -resource tree or when the path segments from the URL "run out". The -resource that traversal "stops on" becomes the :term:`context`. If at any -point during traversal any resource in the tree doesn't have a -``__getitem__`` method, or if the ``__getitem__`` method of a resource raises -a :exc:`KeyError`, traversal ends immediately, and that resource becomes the -:term:`context`. +- The "A" resource's ``__getitem__`` returns another resource, which + we'll call "B". The :term:`context` temporarily becomes the "B" + resource. + +Traversal continues until the path segment sequence is exhausted or a +path element cannot be resolved to a resource. In either case, the +:term:`context` resource is the last object that the traversal +successfully resolved. If any resource found during traversal lacks a +``__getitem__`` method, or if its ``__getitem__`` method raises a +:exc:`KeyError`, traversal ends immediately, and that resource becomes +the :term:`context`. The results of a :term:`traversal` also include a :term:`view name`. The :term:`view name` is the *first* URL path segment in the set of ``PATH_INFO`` -- cgit v1.2.3 From f4b5b973c23ac629c7dd6a093a69ebdbc206b42a Mon Sep 17 00:00:00 2001 From: Casey Duncan Date: Sat, 1 Jan 2011 13:08:37 -0700 Subject: XXX try to clearly explain how the view name is derived during traversal, confirm this is sufficiently truthy XXX --- docs/narr/traversal.rst | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/narr/traversal.rst b/docs/narr/traversal.rst index 2b40c4d94..d5f61fa1b 100644 --- a/docs/narr/traversal.rst +++ b/docs/narr/traversal.rst @@ -74,10 +74,11 @@ successfully resolved. If any resource found during traversal lacks a :exc:`KeyError`, traversal ends immediately, and that resource becomes the :term:`context`. -The results of a :term:`traversal` also include a :term:`view name`. The -:term:`view name` is the *first* URL path segment in the set of ``PATH_INFO`` -segments "left over" in the path segment list popped by the traversal process -*after* traversal finds a context resource. +The results of a :term:`traversal` also include a :term:`view name`. If +traversal ends before the path segment sequence is exhausted, the +:term:`view name` is the *next* remaining path segment element. If the +:term:`traversal` expends all of the path segments, then the :term:`view +name` is the empty string (`''`). The combination of the context resource and the :term:`view name` found via traversal is used later in the same request by the :term:`view -- cgit v1.2.3 From ad838b5d01980b156f763bea4cd655bf50a61783 Mon Sep 17 00:00:00 2001 From: Casey Duncan Date: Sat, 1 Jan 2011 13:23:15 -0700 Subject: rework opening explanation of the resource tree for clarity --- docs/narr/traversal.rst | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/docs/narr/traversal.rst b/docs/narr/traversal.rst index d5f61fa1b..c6da5e5ef 100644 --- a/docs/narr/traversal.rst +++ b/docs/narr/traversal.rst @@ -96,18 +96,20 @@ chapter. The Resource Tree ----------------- -When your application uses :term:`traversal` to resolve URLs to code, the -application must supply a :term:`resource tree` to :app:`Pyramid`. The -resource tree is a set of nested dictionary-like objects. The root of the -tree is represented by a :term:`root` resource. - -In order to supply a root resource for an application, at system startup -time, the :app:`Pyramid` :term:`Router` is configured with a callback known -as a :term:`root factory`. The root factory is supplied by the application -developer as the ``root_factory`` argument to the application's -:term:`Configurator`. - -Here's an example of a simple root factory: +The resource tree is a set of nested dictionary-like resource objects +that begins with a :term:`root` resource. In order to use +:term:`traversal` to resolve URLs to code, your application must supply +a :term:`resource tree` to :app:`Pyramid`. + +In order to supply a root resource for an application the :app:`Pyramid` +:term:`Router` is configured with a callback known as a :term:`root +factory`. The root factory is supplied by the application, at startup +time, as the ``root_factory`` argument to the :term:`Configurator`. + +The root factory is a Python callable that accepts a :term:`request` +object, and returns the root object of the :term:`resource tree`. A +function, or class is typically used as an application's root factory. +Here's an example of a simple root factory class: .. code-block:: python :linenos: -- cgit v1.2.3 From 44166612ba6fcf42f20b1812c5fa79b8504aa3c4 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 1 Jan 2011 17:05:46 -0500 Subject: - The class ``pyramid.authentication.AuthTktCookieHelper`` is now an API. This class can be used by third-party authentication policy developers to help in the mechanics of authentication cookie-setting. --- CHANGES.txt | 4 ++++ docs/api/authentication.rst | 10 ++++++++++ pyramid/authentication.py | 13 ++++++++++++- 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index 19732a623..853cb1360 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -42,6 +42,10 @@ Features same thing as that the dummy authentication policy's ``authenticated_userid`` method. +- The class ``pyramid.authentication.AuthTktCookieHelper`` is now an API. + This class can be used by third-party authentication policy developers to + help in the mechanics of authentication cookie-setting. + Backwards Incompatibilities --------------------------- diff --git a/docs/api/authentication.rst b/docs/api/authentication.rst index 54db77417..a6d4c1e18 100644 --- a/docs/api/authentication.rst +++ b/docs/api/authentication.rst @@ -3,6 +3,9 @@ :mod:`pyramid.authentication` -------------------------------- +Authentication Policies +~~~~~~~~~~~~~~~~~~~~~~~ + .. automodule:: pyramid.authentication .. autoclass:: AuthTktAuthenticationPolicy @@ -11,3 +14,10 @@ .. autoclass:: RemoteUserAuthenticationPolicy +Helper Classes +~~~~~~~~~~~~~~ + + .. autoclass:: AuthTktCookieHelper + + + diff --git a/pyramid/authentication.py b/pyramid/authentication.py index e8ae48ed9..bf08b519a 100644 --- a/pyramid/authentication.py +++ b/pyramid/authentication.py @@ -291,6 +291,12 @@ def b64decode(v): EXPIRE = object() class AuthTktCookieHelper(object): + """ + A helper class for use in third-party authentication policy + implementations. See + :class:`pyramid.authentication.AuthTktAuthenticationPolicy' for the + meanings of the constructor arguments. + """ auth_tkt = auth_tkt # for tests now = None # for tests @@ -362,6 +368,8 @@ class AuthTktCookieHelper(object): return cookies def identify(self, request): + """ Return a dictionary with authentication information, or ``None`` + if no valid auth_tkt is attached to ``request``""" environ = request.environ cookies = get_cookies(environ) cookie = cookies.get(self.cookie_name) @@ -417,11 +425,14 @@ class AuthTktCookieHelper(object): return identity def forget(self, request): - # return a set of expires Set-Cookie headers + """ Return a set of expires Set-Cookie headers, which will destroy + any existing auth_tkt cookie when attached to a response""" environ = request.environ return self._get_cookies(environ, '', max_age=EXPIRE) def remember(self, request, userid, max_age=None): + """ Return a set of Set-Cookie headers; when set into a response, + these headers will represent a valid authentication ticket.""" max_age = max_age or self.max_age environ = request.environ -- cgit v1.2.3 From 60eccf86ed9073644e525a84f2bed5390651e3a4 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 1 Jan 2011 17:09:56 -0500 Subject: quote literal --- CHANGES.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 853cb1360..6fe68fb48 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -22,8 +22,8 @@ Features have an arbitrary argument list and an arbitrary result. This feature will be used by Pyramid extension developers, not by "civilians". -- If a handler class provides an __action_decorator__ attribute (usually a - classmethod or staticmethod), use that as the decorator for each view +- If a handler class provides an ``__action_decorator__`` attribute (usually + a classmethod or staticmethod), use that as the decorator for each view registration for that handler. - The ``pyramid.interfaces.IAuthenticationPolicy`` interface now specifies an -- cgit v1.2.3 From 419b2260ad97cbc9816b2fe075ef3d9acdb79581 Mon Sep 17 00:00:00 2001 From: Casey Duncan Date: Sat, 1 Jan 2011 21:15:43 -0700 Subject: rework paragraphs discussifng root factot config --- docs/narr/traversal.rst | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/docs/narr/traversal.rst b/docs/narr/traversal.rst index c6da5e5ef..0b0bb1d3e 100644 --- a/docs/narr/traversal.rst +++ b/docs/narr/traversal.rst @@ -126,24 +126,23 @@ passing it to an instance of a :term:`Configurator` named ``config``: config = Configurator(root_factory=Root) -Using the ``root_factory`` argument to a :class:`pyramid.config.Configurator` -constructor tells your :app:`Pyramid` application to call this root factory -to generate a root resource whenever a request enters the application. This -root factory is also known as the global root factory. A root factory can -alternately be passed to the ``Configurator`` as a :term:`dotted Python name` -which can refer to a root factory defined in a different module. - -A root factory is passed a :term:`request` object and it is expected to -return an object which represents the root of the resource tree. All -:term:`traversal` will begin at this root resource. Usually a root factory -for a traversal-based application will be more complicated than the above -``Root`` class; in particular it may be associated with a database connection -or another persistence mechanism. +The ``root_factory`` argument to the +:class:`pyramid.config.Configurator` constructor registers this root +factory to be called to generate a root resource whenever a request +enters the application. The root factory registered this way is also +known as the global root factory. A root factory can alternately be +passed to the ``Configurator`` as a :term:`dotted Python name` which can +refer to a root factory defined in a different module. If no :term:`root factory` is passed to the :app:`Pyramid` -:term:`Configurator` constructor, or the ``root_factory`` is specified as the -value ``None``, a *default* root factory is used. The default root factory -always returns a resource that has no child resources; it is effectively empty. +:term:`Configurator` constructor, or if the ``root_factory`` value +specified is ``None``, a *default* root factory is used. The default +root factory always returns a resource that has no child resources; it +is effectively empty. + +Usually a root factory for a traversal-based application will be more +complicated than the above ``Root`` class; in particular it may be +associated with a database connection or another persistence mechanism. .. sidebar:: Emulating the Default Root Factory -- cgit v1.2.3 From 80aa770ad2230f01611eb6f49080321faf77d9fe Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 2 Jan 2011 04:08:18 -0500 Subject: - add a ``add_view_mapper`` API to Configurator. This API allows you to add a named implementation of a ``pyramid.interfaces.IViewMapperFactory`` interface. Its name can be passed as a ``view_mapper`` argument to ``config.add_view``. A view mapper allows objects that are meant to be used as view callables to have an arbitrary argument list and an arbitrary result. This feature will be used by Pyramid extension developers, not by "civilians". - New constructor argument to Configurator: ``default_view_mapper``. Useful to create systems that have view callables with alternate default calling conventions. - ``view_mapper`` argument to ``add_view`` should now be a view mapper *name* rather than an implementation. - Add ``view_mapper`` argument to ``view_config`` decorator constructor. - Remove (non-API) function of config.py named _map_view. - Fix docstring for ``decorator`` argument to add_view. - Factor invocation of view mapper into a viewderiver method. - Promote view rendering and decorating into viewderiver, out of view mapper. - Make requestonly into a function rather than a method of the default view mapper. --- CHANGES.txt | 20 +- TODO.txt | 4 + docs/glossary.rst | 7 +- pyramid/config.py | 306 ++++---- pyramid/tests/test_config.py | 1633 ++++++++++++++++++++++-------------------- pyramid/tests/test_view.py | 3 +- pyramid/view.py | 4 +- 7 files changed, 1066 insertions(+), 911 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 6fe68fb48..4279f950d 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -15,12 +15,21 @@ Features - ``config.add_view`` now accepts a ``decorator`` keyword argument, a callable which will decorate the view callable before it is added to the registry. +- add a ``add_view_mapper`` API to Configurator. This API allows you to add + a named implementation of a ``pyramid.interfaces.IViewMapperFactory`` + interface. Its name can be passed as a ``view_mapper`` argument to + ``config.add_view``. A view mapper allows objects that are meant to be + used as view callables to have an arbitrary argument list and an arbitrary + result. This feature will be used by Pyramid extension developers, not by + "civilians". + +- New constructor argument to Configurator: ``default_view_mapper``. Useful + to create systems that have view callables with alternate default calling + conventions. + - ``config.add_view`` now accepts a ``view_mapper`` keyword argument, which - should be a class which implements the new - ``pyramid.interfaces.IViewMapperFactory`` interface. Use of an alternate - view mapper allows objects that are meant to be used as view callables to - have an arbitrary argument list and an arbitrary result. This feature will - be used by Pyramid extension developers, not by "civilians". + should either be ``None`` or the name of a view mapper previously + registered via ``add_view_mapper``. - If a handler class provides an ``__action_decorator__`` attribute (usually a classmethod or staticmethod), use that as the decorator for each view @@ -90,6 +99,7 @@ Internals promoted to an API method. If you were overriding this method, you'll now need to override it as ``unauthenticated_userid`` instead. +- Remove (non-API) function of config.py named _map_view. 1.0a8 (2010-12-27) ================== diff --git a/TODO.txt b/TODO.txt index 5acc923a1..99944874c 100644 --- a/TODO.txt +++ b/TODO.txt @@ -16,6 +16,10 @@ Must-Have (before 1.0) - Allow ``decorator=`` and ``view_mapper=`` to be passed via ZCML and the ``view_config`` decorator. +- Document ``Configurator.add_view_mapper``. + +- Consider the ability to defer the choice of view mapper. + Should-Have ----------- diff --git a/docs/glossary.rst b/docs/glossary.rst index 49d273197..4d0c53d60 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -850,6 +850,11 @@ Glossary WSGI middleware which can display debuggable traceback information in the browser when an exception is raised by a Pyramid application. See http://pypi.python.org/pypi/WebError . - + view mapper + + A view mapper is a class which implements the + :class:`pyramid.interfaces.IViewMapperFactory` interface, which performs + view argument and return value mapping. This is a plug point for + extension builders, not normally used by "civilians". diff --git a/pyramid/config.py b/pyramid/config.py index 01d453cd1..c890a0c59 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -246,7 +246,13 @@ class Configurator(object): ``autocommit`` is ``True``. If a conflict is detected a ``ConfigurationConflictError`` will be raised. Calling :meth:`pyramid.config.Configurator.make_wsgi_app` always implies a final - commit.""" + commit. + + If ``default_view_mapper`` is passed, it will be used as the default + view mapper factory for view configurations that don't otherwise specify + one (see :class:`pyramid.interfaces.IViewMapperFactory`). + If a default_view_mapper is not passed, a superdefault view mapper will + be used. """ manager = manager # for testing injection venusian = venusian # for testing injection @@ -267,6 +273,7 @@ class Configurator(object): renderer_globals_factory=None, default_permission=None, session_factory=None, + default_view_mapper=None, autocommit=False, ): if package is None: @@ -292,6 +299,7 @@ class Configurator(object): renderer_globals_factory=renderer_globals_factory, default_permission=default_permission, session_factory=session_factory, + default_view_mapper=default_view_mapper, ) def _set_settings(self, mapping): @@ -596,9 +604,8 @@ class Configurator(object): authentication_policy=None, authorization_policy=None, renderers=DEFAULT_RENDERERS, debug_logger=None, locale_negotiator=None, request_factory=None, - renderer_globals_factory=None, - default_permission=None, - session_factory=None): + renderer_globals_factory=None, default_permission=None, + session_factory=None, default_view_mapper=None): """ When you pass a non-``None`` ``registry`` argument to the :term:`Configurator` constructor, no initial 'setup' is performed against the registry. This is because the registry you pass in may @@ -644,7 +651,13 @@ class Configurator(object): self.set_default_permission(default_permission) if session_factory is not None: self.set_session_factory(session_factory) + # commit before adding default_view_mapper, as the + # default_exceptionresponse_view above requires the superdefault view + # mapper self.commit() + if default_view_mapper is not None: + self.add_view_mapper(None, default_view_mapper) + self.commit() # getSiteManager is a unit testing dep injection def hook_zca(self, getSiteManager=None): @@ -1140,11 +1153,11 @@ class Configurator(object): decorator A function which will be used to decorate the registered - :term:`view callable`. The decorator function will be - called with the view callable as a single argument, and it - must return a replacement view callable which accepts the - same arguments and returns the same type of values as the - original function. + :term:`view callable`. The decorator function will be called with + the view callable as a single argument. The view callable it is + passed will accept ``(context, request)`. The decorator must + return a replacement view callable which also accepts ``(context, + request)``. Predicate Arguments @@ -1285,14 +1298,12 @@ class Configurator(object): 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. + The name of a previously registered :term:`view mapper` or + ``None``. 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) @@ -1369,19 +1380,20 @@ class Configurator(object): # intent: will be None if no default permission is registered permission = self.registry.queryUtility(IDefaultPermission) - # NO_PERMISSION_REQUIRED handled by _secure_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) + # __no_permission_required__ handled by _secure_view + deriver = 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) + derived_view = deriver(view) registered = self.registry.adapters.registered @@ -2162,6 +2174,36 @@ class Configurator(object): self.registry.registerUtility(permission, IDefaultPermission) self.action(IDefaultPermission, None) + @action_method + def add_view_mapper(self, name, mapper): + """ + Adding a :term:`view mapper` makes it possible to make use of + :term:`view callable` objects which implement a different call + signature than the ones described in the :app:`Pyramid` + documentation. This is an advanced feature, not usually consumed by + 'civilians'. + + ``name`` must be a string or ``None``. If ``name`` is a string, the + view mapper will be registered under the specified name for + consumption by extensions. If ``name`` is ``None``, the provided + mapper will become the *default* view mapper to be used by all + subsequent :term:`view configuration` registrations, as if you had + passed a ``default_view_mapper`` argument to the + :class:`pyramid.config.Configurator` constructor. + + The ``mapper`` should argument be an object implementing + :class:`pyramid.interfaces.IViewMapperFactory` or a :term:`dotted + Python name` to such an object. + + See also :ref:`using_an_alternate_view_mapper`. + """ + if mapper is not None: + mapper = self.maybe_dotted(mapper) + if name is None: + name = '' + self.registry.registerUtility(mapper, IViewMapperFactory, name=name) + self.action((IViewMapperFactory, name), None) + @action_method def set_session_factory(self, session_factory): """ @@ -2738,19 +2780,29 @@ class ViewDeriver(object): 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))))) + self.owrapped_view( + self.decorated_view( + self.rendered_view( + self.mapped_view(view)))))))) @wraps_view - def owrap_view(self, view): + def mapped_view(self, view): + mapper_name = self.kw.get('view_mapper') + mapper = self.registry.queryUtility(IViewMapperFactory, + name=mapper_name) + + if mapper is None: + mapper = DefaultViewMapper + + mapped_view = mapper(**self.kw)(view) + return mapped_view + + @wraps_view + def owrapped_view(self, view): wrapper_viewname = self.kw.get('wrapper_viewname') viewname = self.kw.get('viewname') if not wrapper_viewname: @@ -2864,29 +2916,53 @@ class ViewDeriver(object): attr_view.__phash__ = phash return attr_view + @wraps_view + def rendered_view(self, view): + wrapped_view = view + renderer = self.kw.get('renderer') + if renderer is None: + return view + + def _rendered_view(context, request): + response = wrapped_view(context, request) + if not is_response(response): + attrs = getattr(request, '__dict__', {}) + if '__view__' in attrs: + view_inst = attrs.pop('__view__') + else: + view_inst = getattr(wrapped_view, '__original_view__', + wrapped_view) + return renderer.render_view(request, response, view_inst, + context) + return response + + return _rendered_view + + @wraps_view + def decorated_view(self, view): + decorator = self.kw.get('decorator') + if decorator is None: + return view + return decorator(view) + class DefaultViewMapper(object): implements(IViewMapperFactory) def __init__(self, **kw): - self.renderer = kw.get('renderer') self.attr = kw.get('attr') - self.decorator = kw.get('decorator') def __call__(self, view): - decorator = self.decorator if inspect.isclass(view): - view = preserve_view_attrs(view, self.map_class(view)) + view = self.map_class(view) else: - view = preserve_view_attrs(view, self.map_nonclass(view)) - if decorator is not None: - view = preserve_view_attrs(view, decorator(view)) + view = self.map_nonclass(view) return view def map_class(self, view): - ronly = self.requestonly(view) + ronly = requestonly(view, self.attr) if ronly: - mapped_view = self._map_class_requestonly(view) + mapped_view = self.map_class_requestonly(view) else: - mapped_view = self._map_class_native(view) + mapped_view = self.map_class_native(view) return mapped_view def map_nonclass(self, view): @@ -2894,47 +2970,41 @@ class DefaultViewMapper(object): # view unless it actually requires wrapping (to avoid function call # overhead). mapped_view = view - ronly = self.requestonly(view) + ronly = requestonly(view, self.attr) if ronly: - mapped_view = self._map_nonclass_requestonly(view) + mapped_view = self.map_nonclass_requestonly(view) elif self.attr: - mapped_view = self._map_nonclass_attr(view) - elif self.renderer is not None: - mapped_view = self._map_nonclass_rendered(view) + mapped_view = self.map_nonclass_attr(view) return mapped_view - def _map_class_requestonly(self, view): + def map_class_requestonly(self, view): # its a class that has an __init__ which only accepts request attr = self.attr def _class_requestonly_view(context, request): inst = view(request) + request.__view__ = inst if attr is None: response = inst() else: response = getattr(inst, attr)() - if self.renderer is not None and not is_response(response): - response = self.renderer.render_view(request, response, view, - context) return response return _class_requestonly_view - def _map_class_native(self, view): + def map_class_native(self, view): # its a class that has an __init__ which accepts both context and # request attr = self.attr def _class_view(context, request): inst = view(context, request) + request.__view__ = inst if attr is None: response = inst() else: response = getattr(inst, attr)() - if self.renderer is not None and not is_response(response): - response = self.renderer.render_view(request, response, view, - context) return response return _class_view - def _map_nonclass_requestonly(self, view): + def map_nonclass_requestonly(self, view): # its a function that has a __call__ which accepts only a single # request argument attr = self.attr @@ -2943,86 +3013,58 @@ class DefaultViewMapper(object): response = view(request) else: response = getattr(view, attr)(request) - if self.renderer is not None and not is_response(response): - response = self.renderer.render_view(request, response, view, - context) return response return _requestonly_view - def _map_nonclass_attr(self, view): + def map_nonclass_attr(self, view): # its a function that has a __call__ which accepts both context and # request, but still has an attr - attr = self.attr def _attr_view(context, request): - response = getattr(view, attr)(context, request) - if self.renderer is not None and not is_response(response): - response = self.renderer.render_view(request, response, view, - context) + response = getattr(view, self.attr)(context, request) return response return _attr_view - def _map_nonclass_rendered(self, view): - # it's a function that has a __call__ that accepts both context and - # request, but requires rendering - def _rendered_view(context, request): - response = view(context, request) - if self.renderer is not None and not is_response(response): - response = self.renderer.render_view(request, response, view, - context) - return response - return _rendered_view - - def requestonly(self, view): - attr = self.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 - +def requestonly(view, attr=None): + if attr is None: + attr = '__call__' + if inspect.isfunction(view): + fn = view + elif inspect.isclass(view): try: - argspec = inspect.getargspec(fn) - except TypeError: + fn = view.__init__ + except AttributeError: return False - - args = argspec[0] - defaults = argspec[3] - - if hasattr(fn, 'im_func'): - # it's an instance method - if not args: - return False - args = args[1:] - if not args: + else: + try: + fn = getattr(view, attr) + except AttributeError: return False - if len(args) == 1: - return True + try: + argspec = inspect.getargspec(fn) + except TypeError: + return False - elif args[0] == 'request': - if len(args) - len(defaults) == 1: - return True + args = argspec[0] + defaults = argspec[3] + 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 -def isexception(o): - if IInterface.providedBy(o): - if IException.isEqualOrExtendedBy(o): + elif args[0] == 'request': + if len(args) - len(defaults) == 1: return True - return ( - isinstance(o, Exception) or - (inspect.isclass(o) and (issubclass(o, Exception))) - ) + + return False + class ActionPredicate(object): action_name = 'action' @@ -3069,20 +3111,18 @@ def translator(msg): localizer = get_localizer(request) return localizer.translate(msg) -# 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) - def is_response(ob): if ( hasattr(ob, 'app_iter') and hasattr(ob, 'headerlist') and hasattr(ob, 'status') ): return True return False +def isexception(o): + if IInterface.providedBy(o): + if IException.isEqualOrExtendedBy(o): + return True + return ( + isinstance(o, Exception) or + (inspect.isclass(o) and (issubclass(o, Exception))) + ) + diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py index 1760d119a..ceb62612f 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -63,27 +63,11 @@ class ConfiguratorTests(unittest.TestCase): config.registry.registerHandler(subscriber, (event_iface,)) return L - def _registerLogger(self, config): - from pyramid.interfaces import IDebugLogger - logger = DummyLogger() - config.registry.registerUtility(logger, IDebugLogger) - return logger - def _makeRequest(self, config): request = DummyRequest() request.registry = config.registry return request - def _registerSecurityPolicy(self, config, permissive): - from pyramid.interfaces import IAuthenticationPolicy - from pyramid.interfaces import IAuthorizationPolicy - policy = DummySecurityPolicy(permissive) - config.registry.registerUtility(policy, IAuthenticationPolicy) - config.registry.registerUtility(policy, IAuthorizationPolicy) - - def _registerSettings(self, config, **settings): - config.registry.settings = settings - def test_ctor_no_registry(self): import sys from pyramid.interfaces import ISettings @@ -196,6 +180,13 @@ class ConfiguratorTests(unittest.TestCase): config = self._makeOne(session_factory='factory') self.assertEqual(config.registry.getUtility(ISessionFactory), 'factory') + def test_ctor_default_view_mapper(self): + from pyramid.interfaces import IViewMapperFactory + mapper = object() + config = self._makeOne(default_view_mapper=mapper) + self.assertEqual(config.registry.getUtility(IViewMapperFactory), + mapper) + def test_with_package_module(self): from pyramid.tests import test_configuration import pyramid.tests @@ -790,8 +781,10 @@ class ConfiguratorTests(unittest.TestCase): config = self._makeOne(autocommit=True) config.add_view(view=view) wrapper = self._getViewCallable(config) - result = wrapper(None, None) + request = self._makeRequest(config) + result = wrapper(None, request) self.assertEqual(result, 'OK') + self.assertEqual(request.__view__.__class__, view) def test_add_view_as_oldstyle_class_requestonly(self): class view: @@ -803,8 +796,11 @@ class ConfiguratorTests(unittest.TestCase): config = self._makeOne(autocommit=True) config.add_view(view=view) wrapper = self._getViewCallable(config) - result = wrapper(None, None) + + request = self._makeRequest(config) + result = wrapper(None, request) self.assertEqual(result, 'OK') + self.assertEqual(request.__view__.__class__, view) def test_add_view_context_as_class(self): from zope.interface import implementedBy @@ -1427,8 +1423,6 @@ class ConfiguratorTests(unittest.TestCase): self.assertEqual(result.settings, settings) def test_add_view_with_default_renderer(self): - import pyramid.tests - from pyramid.interfaces import ISettings class view(object): def __init__(self, context, request): self.request = request @@ -2340,7 +2334,8 @@ class ConfiguratorTests(unittest.TestCase): request_type = self._getRouteRequestIface(config, 'name') wrapper = self._getViewCallable(config, None, request_type) self._assertRoute(config, 'name', 'path') - self.assertEqual(wrapper(None, None), 'OK') + request = self._makeRequest(config) + self.assertEqual(wrapper(None, request), 'OK') def test_add_route_with_view_renderer_alias(self): config = self._makeOne(autocommit=True) @@ -2437,6 +2432,52 @@ class ConfiguratorTests(unittest.TestCase): else: # pragma: no cover raise AssertionError + def test_derive_view_function(self): + def view(request): + return 'OK' + config = self._makeOne() + result = config.derive_view(view) + self.failIf(result is view) + self.assertEqual(result(None, None), 'OK') + + def test_derive_view_dottedname(self): + config = self._makeOne() + result = config.derive_view( + 'pyramid.tests.test_config.dummy_view') + self.failIf(result is dummy_view) + self.assertEqual(result(None, None), 'OK') + + def test_derive_view_with_default_renderer_no_explicit_renderer(self): + config = self._makeOne() + class moo(object): + def __init__(self, view): + pass + def __call__(self, *arg, **kw): + return 'moo' + config.add_renderer(None, moo) + def view(request): + return 'OK' + result = config.derive_view(view) + self.failIf(result is view) + self.assertEqual(result(None, None).body, 'moo') + + def test_derive_view_with_default_renderer_with_explicit_renderer(self): + class moo(object): pass + class foo(object): + def __init__(self, view): + pass + def __call__(self, *arg, **kw): + return 'foo' + def view(request): + return 'OK' + config = self._makeOne() + config.add_renderer(None, moo) + config.add_renderer('foo', foo) + result = config.derive_view(view, renderer='foo') + self.failIf(result is view) + request = self._makeRequest(config) + self.assertEqual(result(None, request).body, 'foo') + def test__override_not_yet_registered(self): from pyramid.interfaces import IPackageOverrides package = DummyPackage('package') @@ -2633,6 +2674,22 @@ class ConfiguratorTests(unittest.TestCase): self.assertEqual(config.registry.getUtility(IDefaultPermission), 'view') + def test_add_view_mapper(self): + from pyramid.interfaces import IViewMapperFactory + config = self._makeOne(autocommit=True) + mapper = object() + config.add_view_mapper('mapper', mapper) + result = config.registry.getUtility(IViewMapperFactory, name='mapper') + self.assertEqual(result, mapper) + + def test_add_view_mapper_dottedname(self): + from pyramid.interfaces import IViewMapperFactory + config = self._makeOne(autocommit=True) + config.add_view_mapper('mapper', 'pyramid.tests.test_config') + result = config.registry.getUtility(IViewMapperFactory, name='mapper') + from pyramid.tests import test_config + self.assertEqual(result, test_config) + def test_set_session_factory(self): from pyramid.interfaces import ISessionFactory config = self._makeOne(autocommit=True) @@ -2680,542 +2737,144 @@ class ConfiguratorTests(unittest.TestCase): self.assertEqual(config.registry.getUtility(ITranslationDirectories), [locale]) - def test_derive_view_function(self): - def view(request): - return 'OK' + def test_override_asset_samename(self): + from pyramid.exceptions import ConfigurationError config = self._makeOne() - result = config.derive_view(view) - self.failIf(result is view) - self.assertEqual(result(None, None), 'OK') + self.assertRaises(ConfigurationError, config.override_asset,'a', 'a') - def test_derive_view_dottedname(self): + def test_override_asset_directory_with_file(self): + from pyramid.exceptions import ConfigurationError config = self._makeOne() - result = config.derive_view( - 'pyramid.tests.test_config.dummy_view') - self.failIf(result is dummy_view) - self.assertEqual(result(None, None), 'OK') + self.assertRaises(ConfigurationError, config.override_asset, + 'a:foo/', 'a:foo.pt') - def test_derive_view_with_renderer(self): - def view(request): - return 'OK' + def test_override_asset_file_with_directory(self): + from pyramid.exceptions import ConfigurationError + config = self._makeOne() + self.assertRaises(ConfigurationError, config.override_asset, + 'a:foo.pt', 'a:foo/') + + def test_override_asset_success(self): config = self._makeOne(autocommit=True) - class moo(object): - def __init__(self, *arg, **kw): - pass - def __call__(self, *arg, **kw): - return 'moo' - config.add_renderer('moo', moo) - result = config.derive_view(view, renderer='moo') - self.failIf(result is view) - self.assertEqual(result(None, None).body, 'moo') + override = DummyUnderOverride() + config.override_asset( + 'pyramid.tests.fixtureapp:templates/foo.pt', + 'pyramid.tests.fixtureapp.subpackage:templates/bar.pt', + _override=override) + from pyramid.tests import fixtureapp + from pyramid.tests.fixtureapp import subpackage + self.assertEqual(override.package, fixtureapp) + self.assertEqual(override.path, 'templates/foo.pt') + self.assertEqual(override.override_package, subpackage) + self.assertEqual(override.override_prefix, 'templates/bar.pt') - def test_derive_view_with_default_renderer_no_explicit_renderer(self): - def view(request): - return 'OK' + def test_add_renderer(self): + from pyramid.interfaces import IRendererFactory config = self._makeOne(autocommit=True) - class moo(object): - def __init__(self, *arg, **kw): - pass - def __call__(self, *arg, **kw): - return 'moo' - config.add_renderer(None, moo) - result = config.derive_view(view) - self.failIf(result is view) - self.assertEqual(result(None, None).body, 'moo') + renderer = object() + config.add_renderer('name', renderer) + self.assertEqual(config.registry.getUtility(IRendererFactory, 'name'), + renderer) - def test_derive_view_with_default_renderer_with_explicit_renderer(self): - def view(request): - return 'OK' + def test_add_renderer_dottedname_factory(self): + from pyramid.interfaces import IRendererFactory config = self._makeOne(autocommit=True) - class moo(object): pass - class foo(object): - def __init__(self, *arg, **kw): - pass - def __call__(self, *arg, **kw): - return 'foo' - config.add_renderer(None, moo) - config.add_renderer('foo', foo) - result = config.derive_view(view, renderer='foo') - self.failIf(result is view) - self.assertEqual(result(None, None).body, 'foo') + import pyramid.tests + config.add_renderer('name', 'pyramid.tests') + self.assertEqual(config.registry.getUtility(IRendererFactory, 'name'), + pyramid.tests) - def test_derive_view_class_without_attr(self): - class View(object): - def __init__(self, request): - pass - def __call__(self): - return 'OK' - config = self._makeOne() - result = config.derive_view(View) - self.assertEqual(result(None, None), 'OK') + def test_scan_integration(self): + import os + from zope.interface import alsoProvides + from pyramid.interfaces import IRequest + from pyramid.view import render_view_to_response + import pyramid.tests.grokkedapp as package + config = self._makeOne(autocommit=True) + config.scan(package) - def test_derive_view_class_with_attr(self): - class View(object): - def __init__(self, request): - pass - def another(self): - return 'OK' - config = self._makeOne() - result = config.derive_view(View, attr='another') - self.assertEqual(result(None, None), 'OK') + ctx = DummyContext() + req = DummyRequest() + alsoProvides(req, IRequest) + req.registry = config.registry - def test__derive_view_as_function_context_and_request(self): - def view(context, request): - return 'OK' - config = self._makeOne() - result = config._derive_view(view) - self.failUnless(result is view) - self.failIf(hasattr(result, '__call_permissive__')) - self.assertEqual(view(None, None), 'OK') + req.method = 'GET' + result = render_view_to_response(ctx, req, '') + self.assertEqual(result, 'grokked') - def test__derive_view_as_function_requestonly(self): - def view(request): - return 'OK' - config = self._makeOne() - result = config._derive_view(view) - self.failIf(result is view) - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.failIf(hasattr(result, '__call_permissive__')) - self.assertEqual(result(None, None), 'OK') + req.method = 'POST' + result = render_view_to_response(ctx, req, '') + self.assertEqual(result, 'grokked_post') - def test__derive_view_as_newstyle_class_context_and_request(self): - class view(object): - def __init__(self, context, request): - pass - def __call__(self): - return 'OK' - config = self._makeOne() - result = config._derive_view(view) - self.failIf(result is view) - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.failIf(hasattr(result, '__call_permissive__')) - self.assertEqual(result(None, None), 'OK') + result= render_view_to_response(ctx, req, 'grokked_class') + self.assertEqual(result, 'grokked_class') - def test__derive_view_as_newstyle_class_requestonly(self): - class view(object): - def __init__(self, context, request): - pass - def __call__(self): - return 'OK' - config = self._makeOne() - result = config._derive_view(view) - self.failIf(result is view) - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.failIf(hasattr(result, '__call_permissive__')) - self.assertEqual(result(None, None), 'OK') + result= render_view_to_response(ctx, req, 'grokked_instance') + self.assertEqual(result, 'grokked_instance') - def test__derive_view_as_oldstyle_class_context_and_request(self): - class view: - def __init__(self, context, request): - pass - def __call__(self): - return 'OK' - config = self._makeOne() - result = config._derive_view(view) - self.failIf(result is view) - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.failIf(hasattr(result, '__call_permissive__')) - self.assertEqual(result(None, None), 'OK') + result= render_view_to_response(ctx, req, 'oldstyle_grokked_class') + self.assertEqual(result, 'oldstyle_grokked_class') - def test__derive_view_as_oldstyle_class_requestonly(self): - class view: - def __init__(self, context, request): - pass - def __call__(self): - return 'OK' - config = self._makeOne() - result = config._derive_view(view) - self.failIf(result is view) - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.failIf(hasattr(result, '__call_permissive__')) - self.assertEqual(result(None, None), 'OK') + req.method = 'GET' + result = render_view_to_response(ctx, req, 'another') + self.assertEqual(result, 'another_grokked') - def test__derive_view_as_instance_context_and_request(self): - class View: - def __call__(self, context, request): - return 'OK' - view = View() - config = self._makeOne() - result = config._derive_view(view) - self.failUnless(result is view) - self.failIf(hasattr(result, '__call_permissive__')) - self.assertEqual(result(None, None), 'OK') + req.method = 'POST' + result = render_view_to_response(ctx, req, 'another') + self.assertEqual(result, 'another_grokked_post') - def test__derive_view_as_instance_requestonly(self): - class View: - def __call__(self, request): - return 'OK' - view = View() - config = self._makeOne() - result = config._derive_view(view) - self.failIf(result is view) - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.failUnless('instance' in result.__name__) - self.failIf(hasattr(result, '__call_permissive__')) - self.assertEqual(result(None, None), 'OK') + result= render_view_to_response(ctx, req, 'another_grokked_class') + self.assertEqual(result, 'another_grokked_class') - def test__derive_view_with_debug_authorization_no_authpol(self): - view = lambda *arg: 'OK' - config = self._makeOne() - self._registerSettings(config, - debug_authorization=True, reload_templates=True) - logger = self._registerLogger(config) - result = config._derive_view(view, permission='view') - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.failIf(hasattr(result, '__call_permissive__')) - request = self._makeRequest(config) - request.view_name = 'view_name' - request.url = 'url' - self.assertEqual(result(None, request), 'OK') - self.assertEqual(len(logger.messages), 1) - self.assertEqual(logger.messages[0], - "debug_authorization of url url (view name " - "'view_name' against context None): Allowed " - "(no authorization policy in use)") + result= render_view_to_response(ctx, req, 'another_grokked_instance') + self.assertEqual(result, 'another_grokked_instance') - def test__derive_view_with_debug_authorization_no_permission(self): - view = lambda *arg: 'OK' - config = self._makeOne() - self._registerSettings(config, - debug_authorization=True, reload_templates=True) - self._registerSecurityPolicy(config, True) - logger = self._registerLogger(config) - result = config._derive_view(view) - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.failIf(hasattr(result, '__call_permissive__')) - request = self._makeRequest(config) - request.view_name = 'view_name' - request.url = 'url' - self.assertEqual(result(None, request), 'OK') - self.assertEqual(len(logger.messages), 1) - self.assertEqual(logger.messages[0], - "debug_authorization of url url (view name " - "'view_name' against context None): Allowed (" - "no permission registered)") + result= render_view_to_response(ctx, req, + 'another_oldstyle_grokked_class') + self.assertEqual(result, 'another_oldstyle_grokked_class') - def test__derive_view_debug_auth_permission_authpol_permitted(self): - view = lambda *arg: 'OK' - config = self._makeOne() - self._registerSettings(config, debug_authorization=True, - reload_templates=True) - logger = self._registerLogger(config) - self._registerSecurityPolicy(config, True) - result = config._derive_view(view, permission='view') - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.assertEqual(result.__call_permissive__, view) - request = self._makeRequest(config) - request.view_name = 'view_name' - request.url = 'url' - self.assertEqual(result(None, request), 'OK') - self.assertEqual(len(logger.messages), 1) - self.assertEqual(logger.messages[0], - "debug_authorization of url url (view name " - "'view_name' against context None): True") + result = render_view_to_response(ctx, req, 'stacked1') + self.assertEqual(result, 'stacked') - def test__derive_view_debug_auth_permission_authpol_denied(self): - from pyramid.exceptions import Forbidden - view = lambda *arg: 'OK' - config = self._makeOne() - self._registerSettings(config, - debug_authorization=True, reload_templates=True) - logger = self._registerLogger(config) - self._registerSecurityPolicy(config, False) - result = config._derive_view(view, permission='view') - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.assertEqual(result.__call_permissive__, view) - request = self._makeRequest(config) - request.view_name = 'view_name' - request.url = 'url' - self.assertRaises(Forbidden, result, None, request) - self.assertEqual(len(logger.messages), 1) - self.assertEqual(logger.messages[0], - "debug_authorization of url url (view name " - "'view_name' against context None): False") + result = render_view_to_response(ctx, req, 'stacked2') + self.assertEqual(result, 'stacked') - def test__derive_view_debug_auth_permission_authpol_denied2(self): - view = lambda *arg: 'OK' - config = self._makeOne() - self._registerSettings(config, - debug_authorization=True, reload_templates=True) - self._registerLogger(config) - self._registerSecurityPolicy(config, False) - result = config._derive_view(view, permission='view') - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - request = self._makeRequest(config) - request.view_name = 'view_name' - request.url = 'url' - permitted = result.__permitted__(None, None) - self.assertEqual(permitted, False) + result = render_view_to_response(ctx, req, 'another_stacked1') + self.assertEqual(result, 'another_stacked') - def test__derive_view_debug_auth_permission_authpol_overridden(self): - view = lambda *arg: 'OK' - config = self._makeOne() - self._registerSettings(config, - debug_authorization=True, reload_templates=True) - logger = self._registerLogger(config) - self._registerSecurityPolicy(config, False) - result = config._derive_view(view, - permission='__no_permission_required__') - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.failIf(hasattr(result, '__call_permissive__')) - request = self._makeRequest(config) - request.view_name = 'view_name' - request.url = 'url' - self.assertEqual(result(None, request), 'OK') - self.assertEqual(len(logger.messages), 1) - self.assertEqual(logger.messages[0], - "debug_authorization of url url (view name " - "'view_name' against context None): False") + result = render_view_to_response(ctx, req, 'another_stacked2') + self.assertEqual(result, 'another_stacked') - def test__derive_view_with_predicates_all(self): - view = lambda *arg: 'OK' - predicates = [] - def predicate1(context, request): - predicates.append(True) - return True - def predicate2(context, request): - predicates.append(True) - return True - config = self._makeOne() - result = config._derive_view(view, predicates=[predicate1, predicate2]) - request = self._makeRequest(config) - request.method = 'POST' - next = result(None, None) - self.assertEqual(next, 'OK') - self.assertEqual(predicates, [True, True]) + result = render_view_to_response(ctx, req, 'stacked_class1') + self.assertEqual(result, 'stacked_class') - def test__derive_view_with_predicates_checker(self): - view = lambda *arg: 'OK' - predicates = [] - def predicate1(context, request): - predicates.append(True) - return True - def predicate2(context, request): - predicates.append(True) - return True - config = self._makeOne() - result = config._derive_view(view, predicates=[predicate1, predicate2]) - request = self._makeRequest(config) - request.method = 'POST' - next = result.__predicated__(None, None) - self.assertEqual(next, True) - self.assertEqual(predicates, [True, True]) + result = render_view_to_response(ctx, req, 'stacked_class2') + self.assertEqual(result, 'stacked_class') - def test__derive_view_with_predicates_notall(self): - from pyramid.exceptions import NotFound - view = lambda *arg: 'OK' - predicates = [] - def predicate1(context, request): - predicates.append(True) - return True - def predicate2(context, request): - predicates.append(True) - return False - config = self._makeOne() - result = config._derive_view(view, predicates=[predicate1, predicate2]) - request = self._makeRequest(config) - request.method = 'POST' - self.assertRaises(NotFound, result, None, None) - self.assertEqual(predicates, [True, True]) + result = render_view_to_response(ctx, req, 'another_stacked_class1') + self.assertEqual(result, 'another_stacked_class') - def test__derive_view_with_wrapper_viewname(self): - from webob import Response - from pyramid.interfaces import IView - from pyramid.interfaces import IViewClassifier - inner_response = Response('OK') - def inner_view(context, request): - return inner_response - def outer_view(context, request): - self.assertEqual(request.wrapped_response, inner_response) - self.assertEqual(request.wrapped_body, inner_response.body) - self.assertEqual(request.wrapped_view, inner_view) - return Response('outer ' + request.wrapped_body) - config = self._makeOne() - config.registry.registerAdapter( - outer_view, (IViewClassifier, None, None), IView, 'owrap') - result = config._derive_view(inner_view, viewname='inner', - wrapper_viewname='owrap') - self.failIf(result is inner_view) - self.assertEqual(inner_view.__module__, result.__module__) - self.assertEqual(inner_view.__doc__, result.__doc__) - request = self._makeRequest(config) - request.registry = config.registry - response = result(None, request) - self.assertEqual(response.body, 'outer OK') + result = render_view_to_response(ctx, req, 'another_stacked_class2') + self.assertEqual(result, 'another_stacked_class') - def test__derive_view_with_wrapper_viewname_notfound(self): - from webob import Response - inner_response = Response('OK') - def inner_view(context, request): - return inner_response - config = self._makeOne() - request = self._makeRequest(config) - request.registry = config.registry - wrapped = config._derive_view( - inner_view, viewname='inner', wrapper_viewname='owrap') - self.assertRaises(ValueError, wrapped, None, request) + if not os.name.startswith('java'): + # on Jython, a class without an __init__ apparently accepts + # any number of arguments without raising a TypeError. - def test_override_asset_samename(self): - from pyramid.exceptions import ConfigurationError - config = self._makeOne() - self.assertRaises(ConfigurationError, config.override_asset,'a', 'a') + self.assertRaises(TypeError, + render_view_to_response, ctx, req, 'basemethod') - def test_override_asset_directory_with_file(self): - from pyramid.exceptions import ConfigurationError - config = self._makeOne() - self.assertRaises(ConfigurationError, config.override_asset, - 'a:foo/', 'a:foo.pt') + result = render_view_to_response(ctx, req, 'method1') + self.assertEqual(result, 'method1') - def test_override_asset_file_with_directory(self): - from pyramid.exceptions import ConfigurationError - config = self._makeOne() - self.assertRaises(ConfigurationError, config.override_asset, - 'a:foo.pt', 'a:foo/') + result = render_view_to_response(ctx, req, 'method2') + self.assertEqual(result, 'method2') - def test_override_asset_success(self): - config = self._makeOne(autocommit=True) - override = DummyUnderOverride() - config.override_asset( - 'pyramid.tests.fixtureapp:templates/foo.pt', - 'pyramid.tests.fixtureapp.subpackage:templates/bar.pt', - _override=override) - from pyramid.tests import fixtureapp - from pyramid.tests.fixtureapp import subpackage - self.assertEqual(override.package, fixtureapp) - self.assertEqual(override.path, 'templates/foo.pt') - self.assertEqual(override.override_package, subpackage) - self.assertEqual(override.override_prefix, 'templates/bar.pt') + result = render_view_to_response(ctx, req, 'stacked_method1') + self.assertEqual(result, 'stacked_method') - def test_add_renderer(self): - from pyramid.interfaces import IRendererFactory - config = self._makeOne(autocommit=True) - renderer = object() - config.add_renderer('name', renderer) - self.assertEqual(config.registry.getUtility(IRendererFactory, 'name'), - renderer) - - def test_add_renderer_dottedname_factory(self): - from pyramid.interfaces import IRendererFactory - config = self._makeOne(autocommit=True) - import pyramid.tests - config.add_renderer('name', 'pyramid.tests') - self.assertEqual(config.registry.getUtility(IRendererFactory, 'name'), - pyramid.tests) - - def test_scan_integration(self): - import os - from zope.interface import alsoProvides - from pyramid.interfaces import IRequest - from pyramid.view import render_view_to_response - import pyramid.tests.grokkedapp as package - config = self._makeOne(autocommit=True) - config.scan(package) - - ctx = DummyContext() - req = DummyRequest() - alsoProvides(req, IRequest) - req.registry = config.registry - - req.method = 'GET' - result = render_view_to_response(ctx, req, '') - self.assertEqual(result, 'grokked') - - req.method = 'POST' - result = render_view_to_response(ctx, req, '') - self.assertEqual(result, 'grokked_post') - - result= render_view_to_response(ctx, req, 'grokked_class') - self.assertEqual(result, 'grokked_class') - - result= render_view_to_response(ctx, req, 'grokked_instance') - self.assertEqual(result, 'grokked_instance') - - result= render_view_to_response(ctx, req, 'oldstyle_grokked_class') - self.assertEqual(result, 'oldstyle_grokked_class') - - req.method = 'GET' - result = render_view_to_response(ctx, req, 'another') - self.assertEqual(result, 'another_grokked') - - req.method = 'POST' - result = render_view_to_response(ctx, req, 'another') - self.assertEqual(result, 'another_grokked_post') - - result= render_view_to_response(ctx, req, 'another_grokked_class') - self.assertEqual(result, 'another_grokked_class') - - result= render_view_to_response(ctx, req, 'another_grokked_instance') - self.assertEqual(result, 'another_grokked_instance') - - result= render_view_to_response(ctx, req, - 'another_oldstyle_grokked_class') - self.assertEqual(result, 'another_oldstyle_grokked_class') - - result = render_view_to_response(ctx, req, 'stacked1') - self.assertEqual(result, 'stacked') - - result = render_view_to_response(ctx, req, 'stacked2') - self.assertEqual(result, 'stacked') - - result = render_view_to_response(ctx, req, 'another_stacked1') - self.assertEqual(result, 'another_stacked') - - result = render_view_to_response(ctx, req, 'another_stacked2') - self.assertEqual(result, 'another_stacked') - - result = render_view_to_response(ctx, req, 'stacked_class1') - self.assertEqual(result, 'stacked_class') - - result = render_view_to_response(ctx, req, 'stacked_class2') - self.assertEqual(result, 'stacked_class') - - result = render_view_to_response(ctx, req, 'another_stacked_class1') - self.assertEqual(result, 'another_stacked_class') - - result = render_view_to_response(ctx, req, 'another_stacked_class2') - self.assertEqual(result, 'another_stacked_class') - - if not os.name.startswith('java'): - # on Jython, a class without an __init__ apparently accepts - # any number of arguments without raising a TypeError. - - self.assertRaises(TypeError, - render_view_to_response, ctx, req, 'basemethod') - - result = render_view_to_response(ctx, req, 'method1') - self.assertEqual(result, 'method1') - - result = render_view_to_response(ctx, req, 'method2') - self.assertEqual(result, 'method2') - - result = render_view_to_response(ctx, req, 'stacked_method1') - self.assertEqual(result, 'stacked_method') - - result = render_view_to_response(ctx, req, 'stacked_method2') - self.assertEqual(result, 'stacked_method') + result = render_view_to_response(ctx, req, 'stacked_method2') + self.assertEqual(result, 'stacked_method') result = render_view_to_response(ctx, req, 'subpackage_init') self.assertEqual(result, 'subpackage_init') @@ -3530,361 +3189,782 @@ class ConfiguratorTests(unittest.TestCase): else: # pragma: no cover raise AssertionError - def test_scan_conflict(self): - from zope.configuration.config import ConfigurationConflictError - from pyramid.tests import selfscanapp - from pyramid.config import Configurator - c = Configurator() - c.scan(selfscanapp) - c.scan(selfscanapp) - try: - c.commit() - except ConfigurationConflictError, why: - def scanconflicts(e): - conflicts = e._conflicts.values() - for conflict in conflicts: - for confinst in conflict: - yield confinst[3] - c1, c2, c3, c4 = scanconflicts(why) - self.assertEqual(c1, "@view_config(renderer='string')") - self.assertEqual(c2, "@view_config(renderer='string')") - self.assertEqual(c3, "@view_config(name='two', renderer='string')") - self.assertEqual(c4, "@view_config(name='two', renderer='string')") + def test_scan_conflict(self): + from zope.configuration.config import ConfigurationConflictError + from pyramid.tests import selfscanapp + from pyramid.config import Configurator + c = Configurator() + c.scan(selfscanapp) + c.scan(selfscanapp) + try: + c.commit() + except ConfigurationConflictError, why: + def scanconflicts(e): + conflicts = e._conflicts.values() + for conflict in conflicts: + for confinst in conflict: + yield confinst[3] + c1, c2, c3, c4 = scanconflicts(why) + self.assertEqual(c1, "@view_config(renderer='string')") + self.assertEqual(c2, "@view_config(renderer='string')") + self.assertEqual(c3, "@view_config(name='two', renderer='string')") + self.assertEqual(c4, "@view_config(name='two', renderer='string')") + + def _conflictFunctions(self, e): + conflicts = e._conflicts.values() + for conflict in conflicts: + for confinst in conflict: + yield confinst[2] + +class TestViewDeriver(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + self.config = None + + def _makeOne(self, **kw): + kw['registry'] = self.config.registry + from pyramid.config import ViewDeriver + return ViewDeriver(**kw) + + def _makeRequest(self): + request = DummyRequest() + request.registry = self.config.registry + return request + + def _registerLogger(self): + from pyramid.interfaces import IDebugLogger + logger = DummyLogger() + self.config.registry.registerUtility(logger, IDebugLogger) + return logger + + def _registerSecurityPolicy(self, permissive): + from pyramid.interfaces import IAuthenticationPolicy + from pyramid.interfaces import IAuthorizationPolicy + policy = DummySecurityPolicy(permissive) + self.config.registry.registerUtility(policy, IAuthenticationPolicy) + self.config.registry.registerUtility(policy, IAuthorizationPolicy) + + def test_requestonly_function(self): + def view(request): + return 'OK' + deriver = self._makeOne() + result = deriver(view) + self.failIf(result is view) + self.assertEqual(result(None, None), 'OK') + + def test_requestonly_function_with_renderer(self): + class moo(object): + def render_view(inself, req, resp, view_inst, ctx): + self.assertEqual(req, request) + self.assertEqual(resp, 'OK') + self.assertEqual(view_inst, view) + self.assertEqual(ctx, context) + return 'moo' + def view(request): + return 'OK' + deriver = self._makeOne(renderer=moo()) + result = deriver(view) + self.failIf(result is view) + request = self._makeRequest() + context = testing.DummyResource() + self.assertEqual(result(context, request), 'moo') + + def test_requestonly_function_with_renderer_request_has_view(self): + class moo(object): + def render_view(inself, req, resp, view_inst, ctx): + self.assertEqual(req, request) + self.assertEqual(resp, 'OK') + self.assertEqual(view_inst, 'view') + self.assertEqual(ctx, context) + return 'moo' + def view(request): + return 'OK' + deriver = self._makeOne(renderer=moo()) + result = deriver(view) + self.failIf(result is view) + request = self._makeRequest() + request.__view__ = 'view' + context = testing.DummyResource() + self.assertEqual(result(context, request), 'moo') + self.failIf(hasattr(request, '__view__')) + + def test_class_without_attr(self): + class View(object): + def __init__(self, request): + pass + def __call__(self): + return 'OK' + deriver = self._makeOne() + result = deriver(View) + request = self._makeRequest() + self.assertEqual(result(None, request), 'OK') + self.assertEqual(request.__view__.__class__, View) + + def test_class_with_attr(self): + class View(object): + def __init__(self, request): + pass + def another(self): + return 'OK' + deriver = self._makeOne(attr='another') + result = deriver(View) + request = self._makeRequest() + self.assertEqual(result(None, request), 'OK') + self.assertEqual(request.__view__.__class__, View) + + def test_as_function_context_and_request(self): + def view(context, request): + return 'OK' + deriver = self._makeOne() + result = deriver(view) + self.failUnless(result is view) + self.failIf(hasattr(result, '__call_permissive__')) + self.assertEqual(view(None, None), 'OK') + + def test_as_function_requestonly(self): + def view(request): + return 'OK' + deriver = self._makeOne() + result = deriver(view) + self.failIf(result is view) + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.failIf(hasattr(result, '__call_permissive__')) + self.assertEqual(result(None, None), 'OK') + + def test_as_newstyle_class_context_and_request(self): + class view(object): + def __init__(self, context, request): + pass + def __call__(self): + return 'OK' + deriver = self._makeOne() + result = deriver(view) + self.failIf(result is view) + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.failIf(hasattr(result, '__call_permissive__')) + request = self._makeRequest() + self.assertEqual(result(None, request), 'OK') + self.assertEqual(request.__view__.__class__, view) + + def test_as_newstyle_class_requestonly(self): + class view(object): + def __init__(self, context, request): + pass + def __call__(self): + return 'OK' + deriver = self._makeOne() + result = deriver(view) + self.failIf(result is view) + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.failIf(hasattr(result, '__call_permissive__')) + request = self._makeRequest() + self.assertEqual(result(None, request), 'OK') + self.assertEqual(request.__view__.__class__, view) + + def test_as_oldstyle_class_context_and_request(self): + class view: + def __init__(self, context, request): + pass + def __call__(self): + return 'OK' + deriver = self._makeOne() + result = deriver(view) + self.failIf(result is view) + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.failIf(hasattr(result, '__call_permissive__')) + request = self._makeRequest() + self.assertEqual(result(None, request), 'OK') + self.assertEqual(request.__view__.__class__, view) + + def test_as_oldstyle_class_requestonly(self): + class view: + def __init__(self, context, request): + pass + def __call__(self): + return 'OK' + deriver = self._makeOne() + result = deriver(view) + self.failIf(result is view) + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.failIf(hasattr(result, '__call_permissive__')) + request = self._makeRequest() + self.assertEqual(result(None, request), 'OK') + self.assertEqual(request.__view__.__class__, view) + + def test_as_instance_context_and_request(self): + class View: + def __call__(self, context, request): + return 'OK' + view = View() + deriver = self._makeOne() + result = deriver(view) + self.failUnless(result is view) + self.failIf(hasattr(result, '__call_permissive__')) + self.assertEqual(result(None, None), 'OK') + + def test_as_instance_requestonly(self): + class View: + def __call__(self, request): + return 'OK' + view = View() + deriver = self._makeOne() + result = deriver(view) + self.failIf(result is view) + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.failUnless('instance' in result.__name__) + self.failIf(hasattr(result, '__call_permissive__')) + self.assertEqual(result(None, None), 'OK') + + def test_with_debug_authorization_no_authpol(self): + view = lambda *arg: 'OK' + self.config.registry.settings = dict( + debug_authorization=True, reload_templates=True) + logger = self._registerLogger() + deriver = self._makeOne(permission='view') + result = deriver(view) + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.failIf(hasattr(result, '__call_permissive__')) + request = self._makeRequest() + request.view_name = 'view_name' + request.url = 'url' + self.assertEqual(result(None, request), 'OK') + self.assertEqual(len(logger.messages), 1) + self.assertEqual(logger.messages[0], + "debug_authorization of url url (view name " + "'view_name' against context None): Allowed " + "(no authorization policy in use)") + + def test_with_debug_authorization_no_permission(self): + view = lambda *arg: 'OK' + self.config.registry.settings = dict( + debug_authorization=True, reload_templates=True) + self._registerSecurityPolicy(True) + logger = self._registerLogger() + deriver = self._makeOne() + result = deriver(view) + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.failIf(hasattr(result, '__call_permissive__')) + request = self._makeRequest() + request.view_name = 'view_name' + request.url = 'url' + self.assertEqual(result(None, request), 'OK') + self.assertEqual(len(logger.messages), 1) + self.assertEqual(logger.messages[0], + "debug_authorization of url url (view name " + "'view_name' against context None): Allowed (" + "no permission registered)") + + def test_debug_auth_permission_authpol_permitted(self): + view = lambda *arg: 'OK' + self.config.registry.settings = dict( + debug_authorization=True, reload_templates=True) + logger = self._registerLogger() + self._registerSecurityPolicy(True) + deriver = self._makeOne(permission='view') + result = deriver(view) + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertEqual(result.__call_permissive__, view) + request = self._makeRequest() + request.view_name = 'view_name' + request.url = 'url' + self.assertEqual(result(None, request), 'OK') + self.assertEqual(len(logger.messages), 1) + self.assertEqual(logger.messages[0], + "debug_authorization of url url (view name " + "'view_name' against context None): True") + + def test_debug_auth_permission_authpol_denied(self): + from pyramid.exceptions import Forbidden + view = lambda *arg: 'OK' + self.config.registry.settings = dict( + debug_authorization=True, reload_templates=True) + logger = self._registerLogger() + self._registerSecurityPolicy(False) + deriver = self._makeOne(permission='view') + result = deriver(view) + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertEqual(result.__call_permissive__, view) + request = self._makeRequest() + request.view_name = 'view_name' + request.url = 'url' + self.assertRaises(Forbidden, result, None, request) + self.assertEqual(len(logger.messages), 1) + self.assertEqual(logger.messages[0], + "debug_authorization of url url (view name " + "'view_name' against context None): False") + + def test_debug_auth_permission_authpol_denied2(self): + view = lambda *arg: 'OK' + self.config.registry.settings = dict( + debug_authorization=True, reload_templates=True) + self._registerLogger() + self._registerSecurityPolicy(False) + deriver = self._makeOne(permission='view') + result = deriver(view) + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + request = self._makeRequest() + request.view_name = 'view_name' + request.url = 'url' + permitted = result.__permitted__(None, None) + self.assertEqual(permitted, False) + + def test_debug_auth_permission_authpol_overridden(self): + view = lambda *arg: 'OK' + self.config.registry.settings = dict( + debug_authorization=True, reload_templates=True) + logger = self._registerLogger() + self._registerSecurityPolicy(False) + deriver = self._makeOne(permission='__no_permission_required__') + result = deriver(view) + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.failIf(hasattr(result, '__call_permissive__')) + request = self._makeRequest() + request.view_name = 'view_name' + request.url = 'url' + self.assertEqual(result(None, request), 'OK') + self.assertEqual(len(logger.messages), 1) + self.assertEqual(logger.messages[0], + "debug_authorization of url url (view name " + "'view_name' against context None): False") + + def test_with_predicates_all(self): + view = lambda *arg: 'OK' + predicates = [] + def predicate1(context, request): + predicates.append(True) + return True + def predicate2(context, request): + predicates.append(True) + return True + deriver = self._makeOne(predicates=[predicate1, predicate2]) + result = deriver(view) + request = self._makeRequest() + request.method = 'POST' + next = result(None, None) + self.assertEqual(next, 'OK') + self.assertEqual(predicates, [True, True]) + + def test_with_predicates_checker(self): + view = lambda *arg: 'OK' + predicates = [] + def predicate1(context, request): + predicates.append(True) + return True + def predicate2(context, request): + predicates.append(True) + return True + deriver = self._makeOne(predicates=[predicate1, predicate2]) + result = deriver(view) + request = self._makeRequest() + request.method = 'POST' + next = result.__predicated__(None, None) + self.assertEqual(next, True) + self.assertEqual(predicates, [True, True]) + + def test_with_predicates_notall(self): + from pyramid.exceptions import NotFound + view = lambda *arg: 'OK' + predicates = [] + def predicate1(context, request): + predicates.append(True) + return True + def predicate2(context, request): + predicates.append(True) + return False + deriver = self._makeOne(predicates=[predicate1, predicate2]) + result = deriver(view) + request = self._makeRequest() + request.method = 'POST' + self.assertRaises(NotFound, result, None, None) + self.assertEqual(predicates, [True, True]) + + def test_with_wrapper_viewname(self): + from webob import Response + from pyramid.interfaces import IView + from pyramid.interfaces import IViewClassifier + inner_response = Response('OK') + def inner_view(context, request): + return inner_response + def outer_view(context, request): + self.assertEqual(request.wrapped_response, inner_response) + self.assertEqual(request.wrapped_body, inner_response.body) + self.assertEqual(request.wrapped_view, inner_view) + return Response('outer ' + request.wrapped_body) + self.config.registry.registerAdapter( + outer_view, (IViewClassifier, None, None), IView, 'owrap') + deriver = self._makeOne(viewname='inner', + wrapper_viewname='owrap') + result = deriver(inner_view) + self.failIf(result is inner_view) + self.assertEqual(inner_view.__module__, result.__module__) + self.assertEqual(inner_view.__doc__, result.__doc__) + request = self._makeRequest() + response = result(None, request) + self.assertEqual(response.body, 'outer OK') - def _conflictFunctions(self, e): - conflicts = e._conflicts.values() - for conflict in conflicts: - for confinst in conflict: - yield confinst[2] + def test_with_wrapper_viewname_notfound(self): + from webob import Response + inner_response = Response('OK') + def inner_view(context, request): + return inner_response + deriver = self._makeOne(viewname='inner', wrapper_viewname='owrap') + wrapped = deriver(inner_view) + request = self._makeRequest() + self.assertRaises(ValueError, wrapped, None, request) + + def test_as_newstyle_class_context_and_request_attr_and_renderer(self): + class renderer(object): + def render_view(inself, req, resp, view_inst, ctx): + self.assertEqual(req, request) + self.assertEqual(resp, {'a':'1'}) + self.assertEqual(view_inst.__class__, View) + self.assertEqual(ctx, context) + return resp + class View(object): + def __init__(self, context, request): + pass + def index(self): + return {'a':'1'} + deriver = self._makeOne(renderer=renderer(), attr='index') + result = deriver(View) + self.failIf(result is View) + self.assertEqual(result.__module__, View.__module__) + self.assertEqual(result.__doc__, View.__doc__) + self.assertEqual(result.__name__, View.__name__) + request = self._makeRequest() + context = testing.DummyResource() + self.assertEqual(result(context, request), {'a':'1'}) + + def test_as_newstyle_class_requestonly_attr_and_renderer(self): + class renderer(object): + def render_view(inself, req, resp, view_inst, ctx): + self.assertEqual(req, request) + self.assertEqual(resp, {'a':'1'}) + self.assertEqual(view_inst.__class__, View) + self.assertEqual(ctx, context) + return resp + class View(object): + def __init__(self, request): + pass + def index(self): + return {'a':'1'} + deriver = self._makeOne(renderer=renderer(), attr='index') + result = deriver(View) + self.failIf(result is View) + self.assertEqual(result.__module__, View.__module__) + self.assertEqual(result.__doc__, View.__doc__) + self.assertEqual(result.__name__, View.__name__) + request = self._makeRequest() + context = testing.DummyResource() + self.assertEqual(result(context, request), {'a':'1'}) + + def test_as_oldstyle_cls_context_request_attr_and_renderer(self): + class renderer(object): + def render_view(inself, req, resp, view_inst, ctx): + self.assertEqual(req, request) + self.assertEqual(resp, {'a':'1'}) + self.assertEqual(view_inst.__class__, View) + self.assertEqual(ctx, context) + return resp + class View: + def __init__(self, context, request): + pass + def index(self): + return {'a':'1'} + deriver = self._makeOne(renderer=renderer(), attr='index') + result = deriver(View) + self.failIf(result is View) + self.assertEqual(result.__module__, View.__module__) + self.assertEqual(result.__doc__, View.__doc__) + self.assertEqual(result.__name__, View.__name__) + request = self._makeRequest() + context = testing.DummyResource() + self.assertEqual(result(context, request), {'a':'1'}) + + def test_as_oldstyle_cls_requestonly_attr_and_renderer(self): + class renderer(object): + def render_view(inself, req, resp, view_inst, ctx): + self.assertEqual(req, request) + self.assertEqual(resp, {'a':'1'}) + self.assertEqual(view_inst.__class__, View) + self.assertEqual(ctx, context) + return resp + class View: + def __init__(self, request): + pass + def index(self): + return {'a':'1'} + deriver = self._makeOne(renderer=renderer(), attr='index') + result = deriver(View) + self.failIf(result is View) + self.assertEqual(result.__module__, View.__module__) + self.assertEqual(result.__doc__, View.__doc__) + self.assertEqual(result.__name__, View.__name__) + request = self._makeRequest() + context = testing.DummyResource() + self.assertEqual(result(context, request), {'a':'1'}) + + def test_as_instance_context_and_request_attr_and_renderer(self): + class renderer(object): + def render_view(inself, req, resp, view_inst, ctx): + self.assertEqual(req, request) + self.assertEqual(resp, {'a':'1'}) + self.assertEqual(view_inst, view) + self.assertEqual(ctx, context) + return resp + class View: + def index(self, context, request): + return {'a':'1'} + deriver = self._makeOne(renderer=renderer(), attr='index') + view = View() + result = deriver(view) + self.failIf(result is view) + self.assertEqual(result.__module__, view.__module__) + self.assertEqual(result.__doc__, view.__doc__) + request = self._makeRequest() + context = testing.DummyResource() + self.assertEqual(result(context, request), {'a':'1'}) + + def test_as_instance_requestonly_attr_and_renderer(self): + class renderer(object): + def render_view(inself, req, resp, view_inst, ctx): + self.assertEqual(req, request) + self.assertEqual(resp, {'a':'1'}) + self.assertEqual(view_inst, view) + self.assertEqual(ctx, context) + return resp + class View: + def index(self, request): + return {'a':'1'} + deriver = self._makeOne(renderer=renderer(), attr='index') + view = View() + result = deriver(view) + self.failIf(result is view) + self.assertEqual(result.__module__, view.__module__) + self.assertEqual(result.__doc__, view.__doc__) + request = self._makeRequest() + context = testing.DummyResource() + self.assertEqual(result(context, request), {'a':'1'}) -class Test__map_view(unittest.TestCase): +class TestDefaultViewMapper(unittest.TestCase): def setUp(self): - from pyramid.registry import Registry - self.registry = Registry() - testing.setUp(registry=self.registry) + self.config = testing.setUp() + self.registry = self.config.registry def tearDown(self): del self.registry testing.tearDown() - def _registerRenderer(self, typ='.txt'): - from pyramid.interfaces import IRendererFactory - from pyramid.interfaces import ITemplateRenderer - from pyramid.renderers import RendererHelper - from zope.interface import implements - class DummyRenderer: - implements(ITemplateRenderer) - def __init__(self, path): - self.__class__.path = path - def __call__(self, *arg): - return 'Hello!' - self.registry.registerUtility(DummyRenderer, IRendererFactory, name=typ) - renderer = RendererHelper(name='abc' + typ, registry=self.registry) - return renderer + def _makeOne(self, **kw): + from pyramid.config import DefaultViewMapper + kw['registry'] = self.registry + return DefaultViewMapper(**kw) def _makeRequest(self): request = DummyRequest() request.registry = self.registry return request - def _callFUT(self, view, **kw): - from pyramid.config import _map_view - return _map_view(view, self.registry, **kw) - - def test__map_view_as_function_context_and_request(self): + def test_view_as_function_context_and_request(self): def view(context, request): return 'OK' - result = self._callFUT(view) + mapper = self._makeOne() + result = mapper(view) self.failUnless(result is view) - self.assertEqual(result(None, None), 'OK') + request = self._makeRequest() + self.assertEqual(result(None, request), 'OK') - def test__map_view_as_function_with_attr(self): + def test__view_as_function_with_attr(self): def view(context, request): """ """ - result = self._callFUT(view, attr='__name__') - self.failIf(result is view) - self.assertRaises(TypeError, result, None, None) - - def test__map_view_as_function_with_attr_and_renderer(self): - renderer = self._registerRenderer() - view = lambda *arg: 'OK' - result = self._callFUT(view, attr='__name__', renderer=renderer) + mapper = self._makeOne(attr='__name__') + result = mapper(view) self.failIf(result is view) - self.assertRaises(TypeError, result, None, None) + request = self._makeRequest() + self.assertRaises(TypeError, result, None, request) - def test__map_view_as_function_requestonly(self): + def test_view_as_function_requestonly(self): def view(request): return 'OK' - result = self._callFUT(view) + mapper = self._makeOne() + result = mapper(view) self.failIf(result is view) - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.assertEqual(result(None, None), 'OK') + request = self._makeRequest() + self.assertEqual(result(None, request), 'OK') - def test__map_view_as_function_requestonly_with_attr(self): + def test_view_as_function_requestonly_with_attr(self): def view(request): """ """ - result = self._callFUT(view, attr='__name__') + mapper = self._makeOne(attr='__name__') + result = mapper(view) self.failIf(result is view) - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.assertRaises(TypeError, result, None, None) + request = self._makeRequest() + self.assertRaises(TypeError, result, None, request) - def test__map_view_as_newstyle_class_context_and_request(self): + def test_view_as_newstyle_class_context_and_request(self): class view(object): def __init__(self, context, request): pass def __call__(self): return 'OK' - result = self._callFUT(view) + mapper = self._makeOne() + result = mapper(view) self.failIf(result is view) - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.assertEqual(result(None, None), 'OK') + request = self._makeRequest() + self.assertEqual(result(None, request), 'OK') - def test__map_view_as_newstyle_class_context_and_request_with_attr(self): + def test_view_as_newstyle_class_context_and_request_with_attr(self): class view(object): def __init__(self, context, request): pass def index(self): return 'OK' - result = self._callFUT(view, attr='index') - self.failIf(result is view) - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.assertEqual(result(None, None), 'OK') - - def test__map_view_as_newstyle_class_context_and_request_attr_and_renderer( - self): - renderer = self._registerRenderer() - class view(object): - def __init__(self, context, request): - pass - def index(self): - return {'a':'1'} - result = self._callFUT(view, attr='index', renderer=renderer) + mapper = self._makeOne(attr='index') + result = mapper(view) self.failIf(result is view) - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) request = self._makeRequest() - self.assertEqual(result(None, request).body, 'Hello!') + self.assertEqual(result(None, request), 'OK') - def test__map_view_as_newstyle_class_requestonly(self): + def test_view_as_newstyle_class_requestonly(self): class view(object): def __init__(self, request): pass def __call__(self): return 'OK' - result = self._callFUT(view) + mapper = self._makeOne() + result = mapper(view) self.failIf(result is view) - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.assertEqual(result(None, None), 'OK') + request = self._makeRequest() + self.assertEqual(result(None, request), 'OK') - def test__map_view_as_newstyle_class_requestonly_with_attr(self): + def test_view_as_newstyle_class_requestonly_with_attr(self): class view(object): def __init__(self, request): pass def index(self): return 'OK' - result = self._callFUT(view, attr='index') - self.failIf(result is view) - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.assertEqual(result(None, None), 'OK') - - def test__map_view_as_newstyle_class_requestonly_attr_and_renderer(self): - renderer = self._registerRenderer() - class view(object): - def __init__(self, request): - pass - def index(self): - return {'a':'1'} - result = self._callFUT(view, attr='index', renderer=renderer) + mapper = self._makeOne(attr='index') + result = mapper(view) self.failIf(result is view) - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) request = self._makeRequest() - self.assertEqual(result(None, request).body, 'Hello!') + self.assertEqual(result(None, request), 'OK') - def test__map_view_as_oldstyle_class_context_and_request(self): + def test_view_as_oldstyle_class_context_and_request(self): class view: def __init__(self, context, request): pass def __call__(self): return 'OK' - result = self._callFUT(view) + mapper = self._makeOne() + result = mapper(view) self.failIf(result is view) - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.assertEqual(result(None, None), 'OK') + request = self._makeRequest() + self.assertEqual(result(None, request), 'OK') - def test__map_view_as_oldstyle_class_context_and_request_with_attr(self): + def test_view_as_oldstyle_class_context_and_request_with_attr(self): class view: def __init__(self, context, request): pass def index(self): return 'OK' - result = self._callFUT(view, attr='index') - self.failIf(result is view) - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.assertEqual(result(None, None), 'OK') - - def test__map_view_as_oldstyle_cls_context_request_attr_and_renderer(self): - renderer = self._registerRenderer() - class view: - def __init__(self, context, request): - pass - def index(self): - return {'a':'1'} - result = self._callFUT(view, attr='index', renderer=renderer) + mapper = self._makeOne(attr='index') + result = mapper(view) self.failIf(result is view) - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) request = self._makeRequest() - self.assertEqual(result(None, request).body, 'Hello!') + self.assertEqual(result(None, request), 'OK') - def test__map_view_as_oldstyle_class_requestonly(self): + def test_view_as_oldstyle_class_requestonly(self): class view: def __init__(self, request): pass def __call__(self): return 'OK' - result = self._callFUT(view) + mapper = self._makeOne() + result = mapper(view) self.failIf(result is view) - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.assertEqual(result(None, None), 'OK') + request = self._makeRequest() + self.assertEqual(result(None, request), 'OK') - def test__map_view_as_oldstyle_class_requestonly_with_attr(self): + def test_view_as_oldstyle_class_requestonly_with_attr(self): class view: def __init__(self, request): pass def index(self): return 'OK' - result = self._callFUT(view, attr='index') - self.failIf(result is view) - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.assertEqual(result(None, None), 'OK') - - def test__map_view_as_oldstyle_class_requestonly_attr_and_renderer(self): - renderer = self._registerRenderer() - class view: - def __init__(self, request): - pass - def index(self): - return {'a':'1'} - result = self._callFUT(view, attr='index', renderer=renderer) + mapper = self._makeOne(attr='index') + result = mapper(view) self.failIf(result is view) - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) request = self._makeRequest() - self.assertEqual(result(None, request).body, 'Hello!') + self.assertEqual(result(None, request), 'OK') - def test__map_view_as_instance_context_and_request(self): + def test_view_as_instance_context_and_request(self): class View: def __call__(self, context, request): return 'OK' view = View() - result = self._callFUT(view) + mapper = self._makeOne() + result = mapper(view) self.failUnless(result is view) - self.assertEqual(result(None, None), 'OK') + request = self._makeRequest() + self.assertEqual(result(None, request), 'OK') - def test__map_view_as_instance_context_and_request_and_attr(self): + def test_view_as_instance_context_and_request_and_attr(self): class View: def index(self, context, request): return 'OK' view = View() - result = self._callFUT(view, attr='index') - self.failIf(result is view) - self.assertEqual(result(None, None), 'OK') - - def test__map_view_as_instance_context_and_request_attr_and_renderer(self): - renderer = self._registerRenderer() - class View: - def index(self, context, request): - return {'a':'1'} - view = View() - result = self._callFUT(view, attr='index', renderer=renderer) + mapper = self._makeOne(attr='index') + result = mapper(view) self.failIf(result is view) request = self._makeRequest() - self.assertEqual(result(None, request).body, 'Hello!') + self.assertEqual(result(None, request), 'OK') - def test__map_view_as_instance_requestonly(self): + def test_view_as_instance_requestonly(self): class View: def __call__(self, request): return 'OK' view = View() - result = self._callFUT(view) + mapper = self._makeOne() + result = mapper(view) self.failIf(result is view) - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.failUnless('instance' in result.__name__) - self.assertEqual(result(None, None), 'OK') + request = self._makeRequest() + self.assertEqual(result(None, request), 'OK') - def test__map_view_as_instance_requestonly_with_attr(self): + def test_view_as_instance_requestonly_with_attr(self): class View: def index(self, request): return 'OK' view = View() - result = self._callFUT(view, attr='index') - self.failIf(result is view) - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.failUnless('instance' in result.__name__) - self.assertEqual(result(None, None), 'OK') - - def test__map_view_as_instance_requestonly_with_attr_and_renderer(self): - renderer = self._registerRenderer() - class View: - def index(self, request): - return {'a':'1'} - view = View() - result = self._callFUT(view, attr='index', renderer=renderer) - self.failIf(result is view) - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.failUnless('instance' in result.__name__) - request = self._makeRequest() - self.assertEqual(result(None, request).body, 'Hello!') - - def test__map_view_rendereronly(self): - renderer = self._registerRenderer() - def view(context, request): - return {'a':'1'} - result = self._callFUT(view, renderer=renderer) - self.failIf(result is view) - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - request = self._makeRequest() - self.assertEqual(result(None, request).body, 'Hello!') - - def test__map_view_with_registry(self): - renderer = self._registerRenderer() - def view(context, request): - return {'a':'1'} - result = self._callFUT(view, renderer=renderer) + mapper = self._makeOne(attr='index') + result = mapper(view) self.failIf(result is view) - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) request = self._makeRequest() - self.assertEqual(result(None, request).body, 'Hello!') + self.assertEqual(result(None, request), 'OK') class Test_preserve_view_attrs(unittest.TestCase): def _callFUT(self, view, wrapped_view): @@ -4400,24 +4480,23 @@ class TestMultiView(unittest.TestCase): response = mv(context, request) self.assertEqual(response, expected_response) - -class TestRequestOnly(unittest.TestCase): - def _callFUT(self, arg): +class Test_requestonly(unittest.TestCase): + def _callFUT(self, view, attr=None): from pyramid.config import requestonly - return requestonly(arg) + return requestonly(view, attr) - def test_newstyle_class_no_init(self): + def test_requestonly_newstyle_class_no_init(self): class foo(object): """ """ self.assertFalse(self._callFUT(foo)) - def test_newstyle_class_init_toomanyargs(self): + def test_requestonly_newstyle_class_init_toomanyargs(self): class foo(object): def __init__(self, context, request): """ """ self.assertFalse(self._callFUT(foo)) - def test_newstyle_class_init_onearg_named_request(self): + def test_requestonly_newstyle_class_init_onearg_named_request(self): class foo(object): def __init__(self, request): """ """ @@ -4493,6 +4572,22 @@ class TestRequestOnly(unittest.TestCase): """ """ self.assertFalse(self._callFUT(foo)) + def test_function_with_attr_false(self): + def bar(context, request): + """ """ + def foo(context, request): + """ """ + foo.bar = bar + self.assertFalse(self._callFUT(foo, 'bar')) + + def test_function_with_attr_true(self): + def bar(context, request): + """ """ + def foo(request): + """ """ + foo.bar = bar + self.assertTrue(self._callFUT(foo, 'bar')) + def test_function_onearg_named_request(self): def foo(request): """ """ diff --git a/pyramid/tests/test_view.py b/pyramid/tests/test_view.py index 7fc066319..69d74ee6e 100644 --- a/pyramid/tests/test_view.py +++ b/pyramid/tests/test_view.py @@ -229,11 +229,12 @@ class TestViewConfigDecorator(unittest.TestCase): def test_create_nondefaults(self): decorator = self._makeOne(name=None, request_type=None, for_=None, - permission='foo') + permission='foo', view_mapper='mapper') self.assertEqual(decorator.name, None) self.assertEqual(decorator.request_type, None) self.assertEqual(decorator.context, None) self.assertEqual(decorator.permission, 'foo') + self.assertEqual(decorator.view_mapper, 'mapper') def test_call_function(self): decorator = self._makeOne() diff --git a/pyramid/view.py b/pyramid/view.py index 776185d8b..8f201f6d1 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -17,7 +17,6 @@ from zope.interface import providedBy from pyramid.interfaces import IRoutesMapper from pyramid.interfaces import IView from pyramid.interfaces import IViewClassifier -from pyramid.interfaces import IRendererFactory from pyramid.httpexceptions import HTTPFound from pyramid.renderers import RendererHelper @@ -384,7 +383,7 @@ class view_config(object): route_name=None, request_method=None, request_param=None, containment=None, attr=None, renderer=None, wrapper=None, xhr=False, accept=None, header=None, path_info=None, - custom_predicates=(), context=None): + custom_predicates=(), context=None, view_mapper=None): self.name = name self.request_type = request_type self.context = context or for_ @@ -401,6 +400,7 @@ class view_config(object): self.header = header self.path_info = path_info self.custom_predicates = custom_predicates + self.view_mapper = view_mapper def __call__(self, wrapped): settings = self.__dict__.copy() -- cgit v1.2.3 From c35a792f38cf29ec5358b8236a41f935dd8969cc Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 2 Jan 2011 04:49:17 -0500 Subject: garden --- TODO.txt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/TODO.txt b/TODO.txt index 99944874c..5b8429d4d 100644 --- a/TODO.txt +++ b/TODO.txt @@ -18,7 +18,11 @@ Must-Have (before 1.0) - Document ``Configurator.add_view_mapper``. -- Consider the ability to defer the choice of view mapper. +- Consider the ability to defer the choice of view mapper and/or renderer + (note that we could also just suggest that people use a default renderer + instead). + +- ZCML directive for view mapper (or just use "utility", but it's not eager). Should-Have ----------- -- cgit v1.2.3 From 3d187b491e2f8a2e4ac700f0b229d9d2bb92daa2 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 2 Jan 2011 04:50:43 -0500 Subject: typo, add example --- docs/narr/project.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/narr/project.rst b/docs/narr/project.rst index 55a2711f3..d572256a0 100644 --- a/docs/narr/project.rst +++ b/docs/narr/project.rst @@ -958,12 +958,14 @@ To this: .. code-block:: python :linenos: - config.add_view('myproject.views.blogs.my_view', + config.add_view('myproject.views.blog.my_view', renderer='myproject:templates/mytemplate.pt') You can then continue to add files to the ``views`` directory, and refer to views or handler classes/functions within those files via the dotted name -passed as the first argument to ``add_view``. For example: +passed as the first argument to ``add_view``. For example, if you added a +file named ``anothermodule.py`` to the ``views`` subdirectory, and added a +view callable named ``my_view`` to it: .. code-block:: python :linenos: -- cgit v1.2.3 From c82eb9793b7bb597893b09251a079902f3952605 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 2 Jan 2011 12:35:25 -0500 Subject: fix oldstyle route syntax --- pyramid/url.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/url.py b/pyramid/url.py index ac569eecb..c11e39143 100644 --- a/pyramid/url.py +++ b/pyramid/url.py @@ -32,7 +32,7 @@ def route_url(route_name, request, *elements, **kw): enough arguments, for example). For example, if you've defined a route named "foobar" with the path - ``:foo/{bar}/*traverse``:: + ``{foo}/{bar}/*traverse``:: route_url('foobar', request, foo='1') => route_url('foobar', request, foo='1', bar='2') => -- cgit v1.2.3 From 7bd0640cfb656b07d6a6ae4c91720eb9621d4748 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 2 Jan 2011 14:15:30 -0500 Subject: indirection is hard --- docs/narr/templates.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/narr/templates.rst b/docs/narr/templates.rst index 437b823e9..7ef8e1923 100644 --- a/docs/narr/templates.rst +++ b/docs/narr/templates.rst @@ -628,7 +628,7 @@ application's configuration section, e.g.: .. code-block:: ini :linenos: - [app:main] + [app:MyProject] use = egg:MyProject#app debug_templates = true -- cgit v1.2.3 From fd9c5b97220e59436c5af59ffa467877fef0df2f Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 2 Jan 2011 14:28:53 -0500 Subject: provide a reference target for modifying package structure --- docs/narr/project.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/narr/project.rst b/docs/narr/project.rst index d572256a0..5e84a4fa7 100644 --- a/docs/narr/project.rst +++ b/docs/narr/project.rst @@ -910,6 +910,8 @@ example. See :ref:`testing_chapter` for more information about writing :app:`Pyramid` unit tests. +.. _modifying_package_structure: + Modifying Package Structure ---------------------------- -- cgit v1.2.3 From d54c521adb087540e9954ac4f266fe699b5f648d Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 2 Jan 2011 15:33:20 -0500 Subject: suggestion from raydeo --- TODO.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/TODO.txt b/TODO.txt index 5b8429d4d..2414658d2 100644 --- a/TODO.txt +++ b/TODO.txt @@ -27,6 +27,10 @@ Must-Have (before 1.0) Should-Have ----------- +- Convert paster template and tutorial HTML templates to use + ``request.static_url('{{package}}:static/foo.css') rather than + ``${request.application_url}/static/foo.css``. + - Add notes about renderer response attrs to request docs. - Add an example of using a cascade to serve static assets from the root. -- cgit v1.2.3 From 405fcc2b4a5c6d3ab3d0680a216e48528e5aaf1a Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 2 Jan 2011 16:55:11 -0500 Subject: add todo for current_route_url --- TODO.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/TODO.txt b/TODO.txt index 2414658d2..4c9429625 100644 --- a/TODO.txt +++ b/TODO.txt @@ -27,6 +27,8 @@ Must-Have (before 1.0) Should-Have ----------- +- ``current_route_url`` function. https://gist.github.com/762842 + - Convert paster template and tutorial HTML templates to use ``request.static_url('{{package}}:static/foo.css') rather than ``${request.application_url}/static/foo.css``. -- cgit v1.2.3 From b23e6e4a939ddaf764137828539f63df6077f4c7 Mon Sep 17 00:00:00 2001 From: Marcin Lulek Date: Sun, 2 Jan 2011 23:29:22 +0100 Subject: initial implementation of current_route_url --- CONTRIBUTORS.txt | 2 ++ pyramid/url.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 7b0364b6d..ec9042f08 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -120,3 +120,5 @@ Contributors - Rob Miller, 2010/12/28 - Marius Gedminas, 2010/12/31 + +- Marcin Lulek, 2011/01/02 \ No newline at end of file diff --git a/pyramid/url.py b/pyramid/url.py index c11e39143..cdeb0d6c8 100644 --- a/pyramid/url.py +++ b/pyramid/url.py @@ -366,6 +366,38 @@ def static_url(path, request, **kw): return info.generate(path, request, **kw) +def current_route_url(request, *elements, **query): + """Generates a fully qualified URL for a named :app:`Pyramid` + :term:`route configuration` based on current route. + + This function is to supplement :func:`pyramid.url.route_url`, it's purpose + is to allow for easy overriding parameters of current route. + + Example:: + If our url route is /foo/{page} - and current url is /foo/1 : + current_route_url(request, page='2') + Will return the string ``/foo/2``. + + Alternatively we may have routes /foo/{action} and /foo/{action}/{page}, + on url /foo/view we may want to have a template macro/def that allows us + to output next/prev buttons that contain page numbers. The ability to + override route name helps with this task. + + Example:: + where ``foo_pages`` is route name for ``/foo/{action}/{page}`` + current_url(request, _route_name='foo_pages', page=paginator.page+1) + Will return the string like: ``/foo/view/5`` + """ + if '_route_name' in query: + route_name = query['_route_name'] + else: + route_name = getattr(request, 'matched_route', None) + route_name = getattr(route_name, 'name') + matchdict = {} + matchdict.update(getattr(request, 'matchdict', {}) or {}) + matchdict.update(query) + return route_url(route_name, request, *elements, **matchdict) + @lru_cache(1000) def _join_elements(elements): return '/'.join([quote_path_segment(s) for s in elements]) -- cgit v1.2.3 From d95cd9b81d4e536c1e7f6f84457deb2fb34f1ea3 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 2 Jan 2011 19:26:42 -0500 Subject: - Allow static renderer provided during view registration to be overridden at request time via a request attribute named ``override_renderer``, which should be the name of a registered renderer. Useful to provide "omnipresent" RPC for existing views. - Convert ``add_view_mapper`` to ``set_view_mapper``. There will only be a single default view mapper. We no longer register named utilities for view mappers to provide alternates. - Previoulsy the ``view_mapper`` argument was required to be the *name* of an existing registered view mapper. We no longer register named utilities for view mappers, so now if an extension wants to override the view mapper used for a particular view registration, it can use the dotted name to a view mapper class or provide that class as ``view_mapper``. --- CHANGES.txt | 37 ++++++++++++---------- pyramid/config.py | 74 +++++++++++++++++++++++--------------------- pyramid/tests/test_config.py | 27 +++++++++++++--- 3 files changed, 83 insertions(+), 55 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 4279f950d..ec631cc78 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -15,22 +15,6 @@ Features - ``config.add_view`` now accepts a ``decorator`` keyword argument, a callable which will decorate the view callable before it is added to the registry. -- add a ``add_view_mapper`` API to Configurator. This API allows you to add - a named implementation of a ``pyramid.interfaces.IViewMapperFactory`` - interface. Its name can be passed as a ``view_mapper`` argument to - ``config.add_view``. A view mapper allows objects that are meant to be - used as view callables to have an arbitrary argument list and an arbitrary - result. This feature will be used by Pyramid extension developers, not by - "civilians". - -- New constructor argument to Configurator: ``default_view_mapper``. Useful - to create systems that have view callables with alternate default calling - conventions. - -- ``config.add_view`` now accepts a ``view_mapper`` keyword argument, which - should either be ``None`` or the name of a view mapper previously - registered via ``add_view_mapper``. - - If a handler class provides an ``__action_decorator__`` attribute (usually a classmethod or staticmethod), use that as the decorator for each view registration for that handler. @@ -55,6 +39,27 @@ Features This class can be used by third-party authentication policy developers to help in the mechanics of authentication cookie-setting. +- New constructor argument to Configurator: ``default_view_mapper``. Useful + to create systems that have alternate view calling conventions. A view + mapper allows objects that are meant to be used as view callables to have + an arbitrary argument list and an arbitrary result. The object passed as + ``default_view_mapper`` should implement the + ``pyramid.interfaces.IViewMapperFactory`` interface. + +- add a ``set_view_mapper`` API to Configurator. Has + the same result as passing ``default_view_mapper`` to the Configurator + constructor. + +- ``config.add_view`` now accepts a ``view_mapper`` keyword argument, which + should either be ``None``, a string representing a Python dotted name, or + an object which is an ``IViewMapperFactory``. This feature is not useful + for "civilians", only for extension writers. + +- Allow static renderer provided during view registration to be overridden at + request time via a request attribute named ``override_renderer``, which + should be the name of a previously registered renderer. Useful to provide + "omnipresent" RPC using existing rendered views. + Backwards Incompatibilities --------------------------- diff --git a/pyramid/config.py b/pyramid/config.py index c890a0c59..5ecbfa064 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -656,7 +656,7 @@ class Configurator(object): # mapper self.commit() if default_view_mapper is not None: - self.add_view_mapper(None, default_view_mapper) + self.set_view_mapper(default_view_mapper) self.commit() # getSiteManager is a unit testing dep injection @@ -1298,18 +1298,19 @@ class Configurator(object): view_mapper - The name of a previously registered :term:`view mapper` or - ``None``. 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. + A Python object or :term:`dotted Python name` which refers to a + :term:`view mapper`, or ``None``. 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) for_ = self.maybe_dotted(for_) containment = self.maybe_dotted(containment) + view_mapper = self.maybe_dotted(view_mapper) if not view: if renderer: @@ -2175,34 +2176,27 @@ class Configurator(object): self.action(IDefaultPermission, None) @action_method - def add_view_mapper(self, name, mapper): + def set_view_mapper(self, mapper): """ - Adding a :term:`view mapper` makes it possible to make use of - :term:`view callable` objects which implement a different call - signature than the ones described in the :app:`Pyramid` - documentation. This is an advanced feature, not usually consumed by - 'civilians'. - - ``name`` must be a string or ``None``. If ``name`` is a string, the - view mapper will be registered under the specified name for - consumption by extensions. If ``name`` is ``None``, the provided - mapper will become the *default* view mapper to be used by all - subsequent :term:`view configuration` registrations, as if you had - passed a ``default_view_mapper`` argument to the - :class:`pyramid.config.Configurator` constructor. - + Setting a :term:`view mapper` makes it possible to make use of + :term:`view callable` objects which implement different call + signatures than the ones supported by :app:`Pyramid` as described in + its narrative documentation. + The ``mapper`` should argument be an object implementing :class:`pyramid.interfaces.IViewMapperFactory` or a :term:`dotted Python name` to such an object. + The provided ``mapper`` will become the default view mapper to be + used by all subsequent :term:`view configuration` registrations, as + if you had passed a ``default_view_mapper`` argument to the + :class:`pyramid.config.Configurator` constructor. + See also :ref:`using_an_alternate_view_mapper`. """ - if mapper is not None: - mapper = self.maybe_dotted(mapper) - if name is None: - name = '' - self.registry.registerUtility(mapper, IViewMapperFactory, name=name) - self.action((IViewMapperFactory, name), None) + mapper = self.maybe_dotted(mapper) + self.registry.registerUtility(mapper, IViewMapperFactory) + self.action(IViewMapperFactory, None) @action_method def set_session_factory(self, session_factory): @@ -2791,12 +2785,11 @@ class ViewDeriver(object): @wraps_view def mapped_view(self, view): - mapper_name = self.kw.get('view_mapper') - mapper = self.registry.queryUtility(IViewMapperFactory, - name=mapper_name) - + mapper = self.kw.get('view_mapper') if mapper is None: - mapper = DefaultViewMapper + mapper = self.registry.queryUtility(IViewMapperFactory) + if mapper is None: + mapper = DefaultViewMapper mapped_view = mapper(**self.kw)(view) return mapped_view @@ -2919,14 +2912,25 @@ class ViewDeriver(object): @wraps_view def rendered_view(self, view): wrapped_view = view - renderer = self.kw.get('renderer') - if renderer is None: + static_renderer = self.kw.get('renderer') + if static_renderer is None: + # register a default renderer if you want super-dynamic + # rendering. registering a default renderer will also allow + # override_renderer to work if a renderer is left unspecified for + # a view registration. return view def _rendered_view(context, request): + renderer = static_renderer response = wrapped_view(context, request) if not is_response(response): attrs = getattr(request, '__dict__', {}) + if 'override_renderer' in attrs: + # renderer overridden by newrequest event or other + renderer_name = attrs.pop('override_renderer') + renderer = RendererHelper(name=renderer_name, + package=self.kw.get('package'), + registry = self.kw['registry']) if '__view__' in attrs: view_inst = attrs.pop('__view__') else: diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py index ceb62612f..52760d6d7 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -2678,15 +2678,15 @@ class ConfiguratorTests(unittest.TestCase): from pyramid.interfaces import IViewMapperFactory config = self._makeOne(autocommit=True) mapper = object() - config.add_view_mapper('mapper', mapper) - result = config.registry.getUtility(IViewMapperFactory, name='mapper') + config.set_view_mapper(mapper) + result = config.registry.getUtility(IViewMapperFactory) self.assertEqual(result, mapper) def test_add_view_mapper_dottedname(self): from pyramid.interfaces import IViewMapperFactory config = self._makeOne(autocommit=True) - config.add_view_mapper('mapper', 'pyramid.tests.test_config') - result = config.registry.getUtility(IViewMapperFactory, name='mapper') + config.set_view_mapper('pyramid.tests.test_config') + result = config.registry.getUtility(IViewMapperFactory) from pyramid.tests import test_config self.assertEqual(result, test_config) @@ -3271,6 +3271,25 @@ class TestViewDeriver(unittest.TestCase): context = testing.DummyResource() self.assertEqual(result(context, request), 'moo') + def test_requestonly_function_with_renderer_request_override(self): + def moo(info): + def inner(value, system): + self.assertEqual(value, 'OK') + self.assertEqual(system['request'], request) + self.assertEqual(system['context'], context) + return 'moo' + return inner + def view(request): + return 'OK' + self.config.add_renderer('moo', moo) + deriver = self._makeOne(renderer='string') + result = deriver(view) + self.failIf(result is view) + request = self._makeRequest() + request.override_renderer = 'moo' + context = testing.DummyResource() + self.assertEqual(result(context, request).body, 'moo') + def test_requestonly_function_with_renderer_request_has_view(self): class moo(object): def render_view(inself, req, resp, view_inst, ctx): -- cgit v1.2.3 From 4066057b93030c4fc83cf38e1f2e61aa686c4b64 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 2 Jan 2011 19:50:41 -0500 Subject: allow _derive_view to pass all kwargs to ViewDeriver, pass 'package' from add_view to view deriver --- pyramid/config.py | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/pyramid/config.py b/pyramid/config.py index 5ecbfa064..78cb13b65 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -351,8 +351,10 @@ class Configurator(object): 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): + phash=DEFAULT_PHASH, decorator=None, + view_mapper=None): view = self.maybe_dotted(view) + view_mapper = self.maybe_dotted(view_mapper) if isinstance(renderer, basestring): renderer = RendererHelper(name=renderer, package=self.package, registry = self.registry) @@ -362,18 +364,21 @@ class Configurator(object): renderer = RendererHelper(name=None, package=self.package, registry=self.registry) - 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) + + 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, + view_mapper=view_mapper, + decorator=decorator) + return deriver(view) def _override(self, package, path, override_package, override_prefix, @@ -1392,8 +1397,9 @@ class Configurator(object): accept=accept, order=order, phash=phash, - decorator=decorator, - view_mapper=view_mapper) + package=self.package, + view_mapper=view_mapper, + decorator=decorator) derived_view = deriver(view) registered = self.registry.adapters.registered -- cgit v1.2.3 From 1d9ade4f8bf93dd88f38daba868e7a619919a98e Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 2 Jan 2011 20:17:53 -0500 Subject: - Allow ``decorator=`` and ``view_mapper`` parameters to add_view and ``@view_config``. --- pyramid/tests/test_view.py | 4 +++- pyramid/view.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pyramid/tests/test_view.py b/pyramid/tests/test_view.py index 69d74ee6e..33c2b606d 100644 --- a/pyramid/tests/test_view.py +++ b/pyramid/tests/test_view.py @@ -229,12 +229,14 @@ class TestViewConfigDecorator(unittest.TestCase): def test_create_nondefaults(self): decorator = self._makeOne(name=None, request_type=None, for_=None, - permission='foo', view_mapper='mapper') + permission='foo', view_mapper='mapper', + decorator='decorator') self.assertEqual(decorator.name, None) self.assertEqual(decorator.request_type, None) self.assertEqual(decorator.context, None) self.assertEqual(decorator.permission, 'foo') self.assertEqual(decorator.view_mapper, 'mapper') + self.assertEqual(decorator.decorator, 'decorator') def test_call_function(self): decorator = self._makeOne() diff --git a/pyramid/view.py b/pyramid/view.py index 8f201f6d1..afd1c6d49 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -383,7 +383,8 @@ class view_config(object): route_name=None, request_method=None, request_param=None, containment=None, attr=None, renderer=None, wrapper=None, xhr=False, accept=None, header=None, path_info=None, - custom_predicates=(), context=None, view_mapper=None): + custom_predicates=(), context=None, decorator=None, + view_mapper=None): self.name = name self.request_type = request_type self.context = context or for_ @@ -400,6 +401,7 @@ class view_config(object): self.header = header self.path_info = path_info self.custom_predicates = custom_predicates + self.decorator = decorator self.view_mapper = view_mapper def __call__(self, wrapped): -- cgit v1.2.3 From 8f2db27ffe8376c313d09e5c3608d326bb4f0e25 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 2 Jan 2011 20:19:57 -0500 Subject: - Allow view callables to specify their preferred view mapper by respecting a ``__view_mapper__`` attribute. This setting can still be overridden by configuration-specified ``view_mapper`` argument to ``add_view``. --- pyramid/config.py | 6 ++++-- pyramid/tests/test_config.py | 47 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/pyramid/config.py b/pyramid/config.py index 78cb13b65..338be2a98 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -2793,9 +2793,11 @@ class ViewDeriver(object): def mapped_view(self, view): mapper = self.kw.get('view_mapper') if mapper is None: - mapper = self.registry.queryUtility(IViewMapperFactory) + mapper = getattr(view, '__view_mapper__', None) if mapper is None: - mapper = DefaultViewMapper + mapper = self.registry.queryUtility(IViewMapperFactory) + if mapper is None: + mapper = DefaultViewMapper mapped_view = mapper(**self.kw)(view) return mapped_view diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py index 52760d6d7..1632a4e5c 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -3790,6 +3790,53 @@ class TestViewDeriver(unittest.TestCase): context = testing.DummyResource() self.assertEqual(result(context, request), {'a':'1'}) + def test_with_view_mapper_config_specified(self): + class mapper(object): + def __init__(self, **kw): + self.kw = kw + def __call__(self, view): + def wrapped(context, request): + return 'OK' + return wrapped + def view(context, request): + return 'NOTOK' + deriver = self._makeOne(view_mapper=mapper) + result = deriver(view) + self.failIf(result is view) + self.assertEqual(result(None, None), 'OK') + + def test_with_view_mapper_view_specified(self): + def mapper(**kw): + def inner(view): + def superinner(context, request): + self.assertEqual(request, None) + return 'OK' + return superinner + return inner + def view(context, request): + return 'NOTOK' + view.__view_mapper__ = mapper + deriver = self._makeOne() + result = deriver(view) + self.failIf(result is view) + self.assertEqual(result(None, None), 'OK') + + def test_with_view_mapper_default_mapper_specified(self): + def mapper(**kw): + def inner(view): + def superinner(context, request): + self.assertEqual(request, None) + return 'OK' + return superinner + return inner + self.config.set_view_mapper(mapper) + def view(context, request): + return 'NOTOK' + deriver = self._makeOne() + result = deriver(view) + self.failIf(result is view) + self.assertEqual(result(None, None), 'OK') + class TestDefaultViewMapper(unittest.TestCase): def setUp(self): self.config = testing.setUp() -- cgit v1.2.3 From 57cc5b77e7e1e1a9ef20902ff50e747a66ee3d13 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 2 Jan 2011 20:20:17 -0500 Subject: garden --- TODO.txt | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/TODO.txt b/TODO.txt index 4c9429625..d87db1480 100644 --- a/TODO.txt +++ b/TODO.txt @@ -11,16 +11,15 @@ Must-Have (before 1.0) - Re-make testing.setUp() and testing.tearDown() the canonical APIs for test configuration. -- Document ``decorator=`` and ``view_mapper`` parameters to add_view. +- Document ``decorator=`` and ``view_mapper`` parameters to add_view and + ``@view_config``. -- Allow ``decorator=`` and ``view_mapper=`` to be passed via ZCML and the - ``view_config`` decorator. +- Allow ``decorator=`` and ``view_mapper=`` to be passed via ZCML. -- Document ``Configurator.add_view_mapper``. +- Document ``Configurator.set_view_mapper``. -- Consider the ability to defer the choice of view mapper and/or renderer - (note that we could also just suggest that people use a default renderer - instead). +- Document ``__view_mapper__`` attribute for view callable view mapper + preference. - ZCML directive for view mapper (or just use "utility", but it's not eager). -- cgit v1.2.3 From fa1dec9ec10e4aca0b1bb69c544e6dc64e297fdf Mon Sep 17 00:00:00 2001 From: Casey Duncan Date: Sun, 2 Jan 2011 20:15:38 -0700 Subject: add word chapters --- docs/narr/views.rst | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/narr/views.rst b/docs/narr/views.rst index 81f3e644f..e1a7c4679 100644 --- a/docs/narr/views.rst +++ b/docs/narr/views.rst @@ -17,11 +17,12 @@ request made to your application. that implements a view *callable*, and the process of view *lookup*. -The :ref:`urldispatch_chapter`, and :ref:`traversal_chapter` describes how, -using information from the :term:`request`, a :term:`context` resource is -computed. But the context resource itself isn't very useful without an -associated :term:`view callable`. A view callable returns a response to a -user, often using the context resource to do so. +The :ref:`urldispatch_chapter`, and :ref:`traversal_chapter` chapters +describes how, using information from the :term:`request`, a +:term:`context` resource is computed. But the context resource itself +isn't very useful without an associated :term:`view callable`. A view +callable returns a response to a user, often using the context resource +to do so. The job of actually locating and invoking the "best" :term:`view callable` is the job of the :term:`view lookup` subsystem. The view lookup subsystem -- cgit v1.2.3 From 4f729f40369c0c1d4b1d48db3c9dee520f94a255 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 2 Jan 2011 23:34:29 -0500 Subject: garden --- TODO.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/TODO.txt b/TODO.txt index d87db1480..1badf9b2a 100644 --- a/TODO.txt +++ b/TODO.txt @@ -23,6 +23,9 @@ Must-Have (before 1.0) - ZCML directive for view mapper (or just use "utility", but it's not eager). +- Decide whether ``self.decorated_view(view)`` is in the right place in the + view deriver chain. + Should-Have ----------- -- cgit v1.2.3 From f9bd7aa61148fada7c0b36df96b5c42670d8c85e Mon Sep 17 00:00:00 2001 From: Casey Duncan Date: Sun, 2 Jan 2011 21:37:24 -0700 Subject: rework view callables intro --- docs/narr/views.rst | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/docs/narr/views.rst b/docs/narr/views.rst index e1a7c4679..39e40a23a 100644 --- a/docs/narr/views.rst +++ b/docs/narr/views.rst @@ -38,24 +38,24 @@ a detailed explanation of view lookup. View Callables -------------- -No matter how a view callable is eventually found, all view callables -used by :app:`Pyramid` must be constructed in the same way, and -must return the same kind of return value. - -Most view callables accept a single argument named ``request``. This -argument represents a :app:`Pyramid` :term:`Request` object. A request -object encapsulates a WSGI environment as represented to :app:`Pyramid` by -the upstream :term:`WSGI` server. - -In general, a view callable must return a :mod:`Pyramid` :term:`Response` -object. - -.. note:: The above statement, though it sounds definitive, isn't always - true. See :ref:`renderers_chapter` for information related to using a - :term:`renderer` to convert a non-Response view callable return value into - a Response object. - -View callables can be functions, instances, or classes. +View callables are, at the risk of sounding obvious, callable Python +objects. Specifically, view callables can be functions, classes, or +instances that implement an ``__call__`` method (making the +instance callable). + +View callables must, at a minimum, accept a single argument named +``request``. This argument represents a :app:`Pyramid` :term:`Request` +object. A request object encapsulates a WSGI environment provided to +:app:`Pyramid` by the upstream :term:`WSGI` server. As you might expect, +the request object contains everything your application needs to know +about the specific HTTP request being made. + +In general, a view callable must return a :mod:`Pyramid` +:term:`Response` object. If a view callable does not return a response +itself, it will typically be configured with a :term:`renderer` that +converts its response value into a :term:`Response` object. Using +renderers is the common way that templates are bound to view callables. +See the :ref:`renderers_chapter` chapter for details. .. index:: single: view calling convention -- cgit v1.2.3 From bb9a3783046db24b2fda6787c232d4168d256729 Mon Sep 17 00:00:00 2001 From: Casey Duncan Date: Sun, 2 Jan 2011 21:46:24 -0700 Subject: rework paragraph about view callable return values and the possibility of renderers --- docs/narr/views.rst | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/narr/views.rst b/docs/narr/views.rst index 39e40a23a..08ae5d5d8 100644 --- a/docs/narr/views.rst +++ b/docs/narr/views.rst @@ -50,12 +50,14 @@ object. A request object encapsulates a WSGI environment provided to the request object contains everything your application needs to know about the specific HTTP request being made. -In general, a view callable must return a :mod:`Pyramid` -:term:`Response` object. If a view callable does not return a response -itself, it will typically be configured with a :term:`renderer` that -converts its response value into a :term:`Response` object. Using -renderers is the common way that templates are bound to view callables. -See the :ref:`renderers_chapter` chapter for details. +A view callable's ultimate responsibility is to create a :mod:`Pyramid` +:term:`Response` object. This can be done by creating the response in +the view callable code and returning it directly. However, if a view +callable does not return a response itself, it can be configured to use +a :term:`renderer` that converts its return value into a +:term:`Response` object. Using renderers is the common way that +templates are used with view callables to generate markup. See the +:ref:`renderers_chapter` chapter for details. .. index:: single: view calling convention -- cgit v1.2.3 From af9935209396d741ca6ec90c82b0a984e1d3b27f Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 2 Jan 2011 23:53:52 -0500 Subject: typo: --- TODO.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TODO.txt b/TODO.txt index 1badf9b2a..e7ac216e7 100644 --- a/TODO.txt +++ b/TODO.txt @@ -32,7 +32,7 @@ Should-Have - ``current_route_url`` function. https://gist.github.com/762842 - Convert paster template and tutorial HTML templates to use - ``request.static_url('{{package}}:static/foo.css') rather than + ``request.static_url('{{package}}:static/foo.css')`` rather than ``${request.application_url}/static/foo.css``. - Add notes about renderer response attrs to request docs. -- cgit v1.2.3 From f8b6d2982292fe323008ad5af394786015206922 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 3 Jan 2011 00:12:36 -0500 Subject: garden --- TODO.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/TODO.txt b/TODO.txt index e7ac216e7..5c04bec6f 100644 --- a/TODO.txt +++ b/TODO.txt @@ -26,6 +26,8 @@ Must-Have (before 1.0) - Decide whether ``self.decorated_view(view)`` is in the right place in the view deriver chain. +- API docs for ``pyramid.views.action``. + Should-Have ----------- -- cgit v1.2.3 From 61c6c10c29d40297c8c47fed3a56f7a6266b2c86 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 3 Jan 2011 01:21:29 -0500 Subject: fix rendering --- docs/glossary.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/glossary.rst b/docs/glossary.rst index 5deb9f5c6..ce2d77d8d 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -852,7 +852,6 @@ Glossary http://pypi.python.org/pypi/WebError . view mapper - A view mapper is a class which implements the :class:`pyramid.interfaces.IViewMapperFactory` interface, which performs view argument and return value mapping. This is a plug point for -- cgit v1.2.3 From e1490d7a8831456d1c7cf1a2b53b442de3a291fa Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 3 Jan 2011 01:21:40 -0500 Subject: rendering --- pyramid/authentication.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/authentication.py b/pyramid/authentication.py index bf08b519a..9de306b80 100644 --- a/pyramid/authentication.py +++ b/pyramid/authentication.py @@ -294,7 +294,7 @@ class AuthTktCookieHelper(object): """ A helper class for use in third-party authentication policy implementations. See - :class:`pyramid.authentication.AuthTktAuthenticationPolicy' for the + :class:`pyramid.authentication.AuthTktAuthenticationPolicy` for the meanings of the constructor arguments. """ auth_tkt = auth_tkt # for tests -- cgit v1.2.3 From 160c2b13a5b97f3918003735262eace91399bffe Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 3 Jan 2011 01:22:54 -0500 Subject: use glossary entry --- pyramid/config.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyramid/config.py b/pyramid/config.py index 8a908725f..2a6319d00 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -249,10 +249,10 @@ class Configurator(object): commit. If ``default_view_mapper`` is passed, it will be used as the default - view mapper factory for view configurations that don't otherwise specify - one (see :class:`pyramid.interfaces.IViewMapperFactory`). - If a default_view_mapper is not passed, a superdefault view mapper will - be used. """ + :term:`view mapper` factory for view configurations that don't otherwise + specify one (see :class:`pyramid.interfaces.IViewMapperFactory`). If a + default_view_mapper is not passed, a superdefault view mapper will be + used. """ manager = manager # for testing injection venusian = venusian # for testing injection -- cgit v1.2.3 From a65bec430170f09b851a237ebe80b4ce703c08ef Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 3 Jan 2011 01:28:52 -0500 Subject: gardening --- CHANGES.txt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 6471dd7a8..8d0e50722 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -80,6 +80,13 @@ Documentation removed from the tutorials section. It was moved to the ``pyramid_tutorials`` Github repository. +- The "Resource Location and View Lookup" chapter has been replaced with a + variant of Rob Miller's "Much Ado About Traversal" (originally published at + http://blog.nonsequitarian.org/2010/much-ado-about-traversal/). + +- Many minor wording tweaks and refactorings (merged Casey Duncan's docs + fork, in which he is working on general editing). + Internals --------- -- cgit v1.2.3 From 8492927535d6b16b2c087d3f0be1c98ba09592a9 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 3 Jan 2011 01:44:52 -0500 Subject: contributors --- docs/copyright.rst | 10 ++++++++-- docs/narr/muchadoabouttraversal.rst | 5 +++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/docs/copyright.rst b/docs/copyright.rst index 691080ba7..f5f12ad03 100644 --- a/docs/copyright.rst +++ b/docs/copyright.rst @@ -48,9 +48,15 @@ with respect to the use of the information contained herein. Attributions ------------ +Editor: + Casey Duncan + Contributors: - Ben Bangert, Blaise Laflamme, Carlos de la Guardia, Paul Everitt, - Marius Gedminas, Casey Duncan + + Ben Bangert, Blaise Laflamme, Rob Miller, Mike Orr, Carlos de la Guardia, + Paul Everitt, Tres Seaver, Marius Gedminas, Chris Rossi, Joachim Krebs, + Xavier Spriet, Reed O'Brien, William Chambers, Charlie Choiniere, Jamaludin + Ahmad. .. Cover Designer: .. Nat Hardwick of `Electrosoup `_. diff --git a/docs/narr/muchadoabouttraversal.rst b/docs/narr/muchadoabouttraversal.rst index 9bd829754..cb1c6f73a 100644 --- a/docs/narr/muchadoabouttraversal.rst +++ b/docs/narr/muchadoabouttraversal.rst @@ -4,6 +4,11 @@ Much Ado About Traversal ======================== +.. note:: This chapter was adapted, with permission, from a blog post by `Rob + Miller `_, originally published at + `http://blog.nonsequitarian.org/2010/much-ado-about-traversal/ + `_. + A lot of folks who have been using Pylons (and, therefore, Routes-based URL matching) are being exposed for the first time, via :app:`Pyramid`, to new ideas such as ":term:`traversal`" and ":term:`view lookup`" as a -- cgit v1.2.3 From 8a1b50bc4027e25d5450cc6968f1f005af9d389f Mon Sep 17 00:00:00 2001 From: Casey Duncan Date: Sun, 2 Jan 2011 23:45:33 -0700 Subject: Split view chapter, move view config after templates, some reordering in view config --- docs/glossary.rst | 13 +- docs/index.rst | 1 + docs/latexindex.rst | 1 + docs/narr/firstapp.rst | 2 +- docs/narr/traversal.rst | 19 +- docs/narr/urldispatch.rst | 7 +- docs/narr/viewconfig.rst | 965 +++++++++++++++++++++++++++++++++++++++ docs/narr/views.rst | 1116 +-------------------------------------------- 8 files changed, 1007 insertions(+), 1117 deletions(-) create mode 100644 docs/narr/viewconfig.rst diff --git a/docs/glossary.rst b/docs/glossary.rst index 49d273197..7090b5f93 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -114,12 +114,13 @@ Glossary view configuration View configuration is the act of associating a :term:`view callable` with configuration information. This configuration - information helps map a given :term:`request` to a particular view - callable and it can influence the response of a view callable. - :app:`Pyramid` views can be configured via :term:`imperative - configuration`, :term:`ZCML` or by a special ``@view_config`` - decorator coupled with a :term:`scan`. See :ref:`views_chapter` - for more information about view configuration. + information helps map a given :term:`request` to a particular view + callable and it can influence the response of a view callable. + :app:`Pyramid` views can be configured via :term:`imperative + configuration`, :term:`ZCML` or by a special ``@view_config`` + decorator coupled with a :term:`scan`. See + :ref:`view_config_chapter` for more information about view + configuration. view name The "URL name" of a view, e.g ``index.html``. If a view is diff --git a/docs/index.rst b/docs/index.rst index 2f3589499..fe3cf2ce8 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -44,6 +44,7 @@ Narrative documentation in chapter form explaining how to use narr/views narr/renderers narr/templates + narr/viewconfig narr/resources narr/assets narr/webob diff --git a/docs/latexindex.rst b/docs/latexindex.rst index 9635e6e8a..608a49678 100644 --- a/docs/latexindex.rst +++ b/docs/latexindex.rst @@ -37,6 +37,7 @@ Narrative Documentation narr/views narr/renderers narr/templates + narr/viewconfig narr/resources narr/assets narr/webob diff --git a/docs/narr/firstapp.rst b/docs/narr/firstapp.rst index 1a81134f5..3f1098da4 100644 --- a/docs/narr/firstapp.rst +++ b/docs/narr/firstapp.rst @@ -323,7 +323,7 @@ For more information about the API of a :term:`Configurator` object, see :class:`pyramid.config.Configurator` . For more information about :term:`view configuration`, see -:ref:`views_chapter`. +:ref:`view_config_chapter`. An example of using *declarative* configuration (:term:`ZCML`) instead of imperative configuration to create a similar "hello world" is available diff --git a/docs/narr/traversal.rst b/docs/narr/traversal.rst index 0b0bb1d3e..7c6280ba1 100644 --- a/docs/narr/traversal.rst +++ b/docs/narr/traversal.rst @@ -83,7 +83,7 @@ name` is the empty string (`''`). The combination of the context resource and the :term:`view name` found via traversal is used later in the same request by the :term:`view lookup` subsystem to find a :term:`view callable`. How :app:`Pyramid` -performs view lookup is explained within the :ref:`views_chapter` +performs view lookup is explained within the :ref:`view_config_chapter` chapter. .. index:: @@ -217,13 +217,14 @@ We'll provide a description of the algorithm, a diagram of how the algorithm works, and some example traversal scenarios that might help you understand how the algorithm operates against a specific resource tree. -We'll also talk a bit about :term:`view lookup`. The :ref:`views_chapter` -chapter discusses :term:`view lookup` in detail, and it is the canonical -source for information about views. Technically, :term:`view lookup` is a -:app:`Pyramid` subsystem that is separated from traversal entirely. However, -we'll describe the fundamental behavior of view lookup in the examples in the -next few sections to give you an idea of how traversal and view lookup -cooperate, because they are almost always used together. +We'll also talk a bit about :term:`view lookup`. The +:ref:`view_config_chapter` chapter discusses :term:`view lookup` in +detail, and it is the canonical source for information about views. +Technically, :term:`view lookup` is a :app:`Pyramid` subsystem that is +separated from traversal entirely. However, we'll describe the +fundamental behavior of view lookup in the examples in the next few +sections to give you an idea of how traversal and view lookup cooperate, +because they are almost always used together. .. index:: single: view name @@ -461,7 +462,7 @@ References A tutorial showing how :term:`traversal` can be used within a :app:`Pyramid` application exists in :ref:`bfg_wiki_tutorial`. -See the :ref:`views_chapter` chapter for detailed information about +See the :ref:`view_config_chapter` chapter for detailed information about :term:`view lookup`. The :mod:`pyramid.traversal` module contains API functions that deal with diff --git a/docs/narr/urldispatch.rst b/docs/narr/urldispatch.rst index c0b132a04..e64513a96 100644 --- a/docs/narr/urldispatch.rst +++ b/docs/narr/urldispatch.rst @@ -26,7 +26,7 @@ specific bit of code, defined in your application, that receives the :term:`request` and returns a :term:`response` object. Where appropriate, we will describe how view lookup interacts with -:term:`resource location`. The :ref:`views_chapter` chapter describes +:term:`resource location`. The :ref:`view_config_chapter` chapter describes the details of :term:`view lookup`. High-Level Operational Overview @@ -95,7 +95,7 @@ Route Configuration That Names a View Callable When a route configuration declaration names a ``view`` attribute, the value of the attribute will reference a :term:`view callable`. This view callable will be invoked when the route matches. A view callable, as described in -:ref:`views_chapter`, is developer-supplied code that "does stuff" as the +:ref:`view_chapter`, is developer-supplied code that "does stuff" as the result of a request. For more information about how to create view callables, see :ref:`views_chapter`. @@ -865,7 +865,8 @@ The ``mypackage.views`` module referred to above might look like so: The view has access to the matchdict directly via the request, and can access variables within it that match keys present as a result of the route pattern. -See :ref:`views_chapter` for more information about views. +See :ref:`views_chapter`, and :ref:`view_config_chapter` for more +information about views. Example 2 ~~~~~~~~~ diff --git a/docs/narr/viewconfig.rst b/docs/narr/viewconfig.rst new file mode 100644 index 000000000..39c0a0d1f --- /dev/null +++ b/docs/narr/viewconfig.rst @@ -0,0 +1,965 @@ +.. _view_config_chapter: + +.. _view_configuration: + +View Configuration +================== + +.. index:: + single: view lookup + +.. _view_lookup: + +View Lookup and Invocation +-------------------------- + +:term:`View lookup` is the :app:`Pyramid` subsystem responsible for finding +an invoking a :term:`view callable`. The view lookup subsystem is passed a +:term:`context` and a :term:`request` object. + +:term:`View configuration` information stored within in the +:term:`application registry` is compared against the context and request by +the view lookup subsystem in order to find the "best" view callable for the +set of circumstances implied by the context and request. + +:term:`View predicate` attributes are an important part of view +configuration that enables the :term:`View lookup` subsystem to find and +invoke the appropriate view. Predicate attributes can be thought of +like "narrowers". In general, the greater number of predicate +attributes possessed by a view's configuration, the more specific the +circumstances need to be before the registered view callable will be +invoked. + +Mapping a Resource or URL Pattern to a View Callable +---------------------------------------------------- + +A developer makes a :term:`view callable` available for use within a +:app:`Pyramid` application via :term:`view configuration`. A view +configuration associates a view callable with a set of statements that +determine the set of circumstances which must be true for the view callable +to be invoked. + +A view configuration statement is made about information present in the +:term:`context` resource and the :term:`request`. + +View configuration is performed in one of these ways: + +- by running a :term:`scan` against application source code which has a + :class:`pyramid.view.view_config` decorator attached to a Python object as + per :class:`pyramid.view.view_config` and + :ref:`mapping_views_using_a_decorator_section`. + +- by using the :meth:`pyramid.config.Configurator.add_view` method as per + :meth:`pyramid.config.Configurator.add_view` and + :ref:`mapping_views_using_imperative_config_section`. + +- By specifying a view within a :term:`route configuration`. View + configuration via a route configuration is performed by using the + :meth:`pyramid.config.Configurator.add_route` method, passing a ``view`` + argument specifying a view callable. + +- by using the :meth:`pyramid.config.Configurator.add_handler` against a + :term:`view handler` class (useful only for :term:`URL dispatch` + applications). + +.. note:: You can also add view configuration by adding a ````, + ```` or ```` declaration to :term:`ZCML` used by your + application as per :ref:`mapping_views_using_zcml_section`, + :ref:`view_directive`, :ref:`route_directive` or :ref:`handler_directive`. + +.. _view_configuration_parameters: + +View Configuration Parameters +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +All forms of view configuration accept the same general types of arguments. + +Many arguments supplied during view configuration are :term:`view predicate` +arguments. View predicate arguments used during view configuration are used +to narrow the set of circumstances in which :mod:`view lookup` will find a +particular view callable. + +In general, the fewer number of predicates which are supplied to a +particular view configuration, the more likely it is that the associated +view callable will be invoked. The greater the number supplied, the +less likely. A view with five predicates will always be found and +evaluated before a view with two, for example. All predicates must +match for the associated view to be called. + +This does not mean however, that :app:`Pyramid` "stops looking" when it +finds a view registration with predicates that don't match. If one set +of view predicates does not match, the "next most specific" view (if +any) is consulted for predicates, and so on, until a view is found, or +no view can be matched up with the request. The first view with a set +of predicates all of which match the request environment will be +invoked. + +If no view can be found with predicates which allow it to be matched up with +the request, :app:`Pyramid` will return an error to the user's browser, +representing a "not found" (404) page. See :ref:`changing_the_notfound_view` +for more information about changing the default notfound view. + +Some view configuration arguments are non-predicate arguments. These tend to +modify the response of the view callable or prevent the view callable from +being invoked due to an authorization policy. The presence of non-predicate +arguments in a view configuration does not narrow the circumstances in which +the view callable will be invoked. + +Non-Predicate Arguments ++++++++++++++++++++++++ + +``permission`` + The name of a :term:`permission` that the user must possess in order to + invoke the :term:`view callable`. See :ref:`view_security_section` for + more information about view security and permissions. + + If ``permission`` is not supplied, no permission is registered for this + view (it's accessible by any caller). + +``attr`` + The view machinery defaults to using the ``__call__`` method of the + :term:`view callable` (or the function itself, if the view callable is a + function) to obtain a response. The ``attr`` value allows you to vary the + method attribute used to obtain the response. For example, if your view + was a class, and the class has a method named ``index`` and you wanted to + use this method instead of the class' ``__call__`` method to return the + response, you'd say ``attr="index"`` in the view configuration for the + view. This is most useful when the view definition is a class. + + If ``attr`` is not supplied, ``None`` is used (implying the function itself + if the view is a function, or the ``__call__`` callable attribute if the + view is a class). + +``renderer`` + Denotes the :term:`renderer` implementation which will be used to construct + a :term:`response` from the associated view callable's return value. (see + also :ref:`renderers_chapter`). + + This is either a single string term (e.g. ``json``) or a string implying a + path or :term:`asset specification` (e.g. ``templates/views.pt``) naming a + :term:`renderer` implementation. If the ``renderer`` value does not + contain a dot (``.``), the specified string will be used to look up a + renderer implementation, and that renderer implementation will be used to + construct a response from the view return value. If the ``renderer`` value + contains a dot (``.``), the specified term will be treated as a path, and + the filename extension of the last element in the path will be used to look + up the renderer implementation, which will be passed the full path. + + When the renderer is a path, although a path is usually just a simple + relative pathname (e.g. ``templates/foo.pt``, implying that a template + named "foo.pt" is in the "templates" directory relative to the directory of + the current :term:`package`), a path can be absolute, starting with a slash + on UNIX or a drive letter prefix on Windows. The path can alternately be a + :term:`asset specification` in the form + ``some.dotted.package_name:relative/path``, making it possible to address + template assets which live in a separate package. + + The ``renderer`` attribute is optional. If it is not defined, the "null" + renderer is assumed (no rendering is performed and the value is passed back + to the upstream :app:`Pyramid` machinery unmolested). Note that if the + view callable itself returns a :term:`response` (see :ref:`the_response`), + the specified renderer implementation is never called. + +``wrapper`` + The :term:`view name` of a different :term:`view configuration` which will + receive the response body of this view as the ``request.wrapped_body`` + attribute of its own :term:`request`, and the :term:`response` returned by + this view as the ``request.wrapped_response`` attribute of its own request. + Using a wrapper makes it possible to "chain" views together to form a + composite response. The response of the outermost wrapper view will be + returned to the user. The wrapper view will be found as any view is found: + see :ref:`view_lookup`. The "best" wrapper view will be found based on the + lookup ordering: "under the hood" this wrapper view is looked up via + ``pyramid.view.render_view_to_response(context, request, + 'wrapper_viewname')``. The context and request of a wrapper view is the + same context and request of the inner view. + + If ``wrapper`` is not supplied, no wrapper view is used. + +Predicate Arguments ++++++++++++++++++++ + +These arguments modify view lookup behavior. In general, the more predicate +arguments that are supplied, the more specific, and narrower the usage of the +configured view. + +``name`` + The :term:`view name` required to match this view callable. Read + :ref:`traversal_chapter` to understand the concept of a view name. + + If ``name`` is not supplied, the empty string is used (implying the default + view). + +``context`` + An object representing a Python class that the :term:`context` resource + must be an instance of *or* the :term:`interface` that the :term:`context` + resource must provide in order for this view to be found and called. This + predicate is true when the :term:`context` resource is an instance of the + represented class or if the :term:`context` resource provides the + represented interface; it is otherwise false. + + If ``context`` is not supplied, the value ``None``, which matches any + resource, is used. + +``route_name`` + If ``route_name`` is supplied, the view callable will be invoked only when + the named route has matched. + + This value must match the ``name`` of a :term:`route configuration` + declaration (see :ref:`urldispatch_chapter`) that must match before this + view will be called. Note that the ``route`` configuration referred to by + ``route_name`` will usually have a ``*traverse`` token in the value of its + ``pattern``, representing a part of the path that will be used by + :term:`traversal` against the result of the route's :term:`root factory`. + + If ``route_name`` is not supplied, the view callable will be have a chance + of being invoked if no other route was matched. This is when the + request/context pair found via :term:`resource location` does not indicate + it matched any configured route. + +``request_type`` + This value should be an :term:`interface` that the :term:`request` must + provide in order for this view to be found and called. + + If ``request_type`` is not supplied, the value ``None`` is used, implying + any request type. + + *This is an advanced feature, not often used by "civilians"*. + +``request_method`` + This value can either be one of the strings ``GET``, ``POST``, ``PUT``, + ``DELETE``, or ``HEAD`` representing an HTTP ``REQUEST_METHOD``. A view + declaration with this argument ensures that the view will only be called + when the request's ``method`` attribute (aka the ``REQUEST_METHOD`` of the + WSGI environment) string matches the supplied value. + + If ``request_method`` is not supplied, the view will be invoked regardless + of the ``REQUEST_METHOD`` of the :term:`WSGI` environment. + +``request_param`` + This value can be any string. A view declaration with this argument + ensures that the view will only be called when the :term:`request` has a + key in the ``request.params`` dictionary (an HTTP ``GET`` or ``POST`` + variable) that has a name which matches the supplied value. + + If the value supplied has a ``=`` sign in it, + e.g. ``request_params="foo=123"``, then the key (``foo``) must both exist + in the ``request.params`` dictionary, *and* the value must match the right + hand side of the expression (``123``) for the view to "match" the current + request. + + If ``request_param`` is not supplied, the view will be invoked without + consideration of keys and values in the ``request.params`` dictionary. + +``containment`` + This value should be a reference to a Python class or :term:`interface` + that a parent object in the context resource's :term:`lineage` must provide + in order for this view to be found and called. The resources in your + resource tree must be "location-aware" to use this feature. + + If ``containment`` is not supplied, the interfaces and classes in the + lineage are not considered when deciding whether or not to invoke the view + callable. + + See :ref:`location_aware` for more information about location-awareness. + +``xhr`` + This value should be either ``True`` or ``False``. If this value is + specified and is ``True``, the :term:`WSGI` environment must possess an + ``HTTP_X_REQUESTED_WITH`` (aka ``X-Requested-With``) header that has the + value ``XMLHttpRequest`` for the associated view callable to be found and + called. This is useful for detecting AJAX requests issued from jQuery, + Prototype and other Javascript libraries. + + If ``xhr`` is not specified, the ``HTTP_X_REQUESTED_WITH`` HTTP header is + not taken into consideration when deciding whether or not to invoke the + associated view callable. + +``accept`` + The value of this argument represents a match query for one or more + mimetypes in the ``Accept`` HTTP request header. If this value is + specified, it must be in one of the following forms: a mimetype match token + in the form ``text/plain``, a wildcard mimetype match token in the form + ``text/*`` or a match-all wildcard mimetype match token in the form + ``*/*``. If any of the forms matches the ``Accept`` header of the request, + this predicate will be true. + + If ``accept`` is not specified, the ``HTTP_ACCEPT`` HTTP header is not + taken into consideration when deciding whether or not to invoke the + associated view callable. + +``header`` + This value represents an HTTP header name or a header name/value pair. + + If ``header`` is specified, it must be a header name or a + ``headername:headervalue`` pair. + + If ``header`` is specified without a value (a bare header name only, + e.g. ``If-Modified-Since``), the view will only be invoked if the HTTP + header exists with any value in the request. + + If ``header`` is specified, and possesses a name/value pair + (e.g. ``User-Agent:Mozilla/.*``), the view will only be invoked if the HTTP + header exists *and* the HTTP header matches the value requested. When the + ``headervalue`` contains a ``:`` (colon), it will be considered a + name/value pair (e.g. ``User-Agent:Mozilla/.*`` or ``Host:localhost``). + The value portion should be a regular expression. + + Whether or not the value represents a header name or a header name/value + pair, the case of the header name is not significant. + + If ``header`` is not specified, the composition, presence or absence of + HTTP headers is not taken into consideration when deciding whether or not + to invoke the associated view callable. + +``path_info`` + This value represents a regular expression pattern that will be tested + against the ``PATH_INFO`` WSGI environment variable to decide whether or + not to call the associated view callable. If the regex matches, this + predicate will be ``True``. + + If ``path_info`` is not specified, the WSGI ``PATH_INFO`` is not taken into + consideration when deciding whether or not to invoke the associated view + callable. + +``custom_predicates`` + If ``custom_predicates`` is specified, it must be a sequence of references + to custom predicate callables. Use custom predicates when no set of + predefined predicates do 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 ``False`` after doing arbitrary evaluation of the + context resource and/or the request. If all callables return ``True``, the + associated view callable will be considered viable for a given request. + + If ``custom_predicates`` is not specified, no custom predicates are + used. + +.. index:: + single: view_config decorator + +.. _mapping_views_using_a_decorator_section: + +View Configuration Using the ``@view_config`` Decorator +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +For better locality of reference, you may use the +:class:`pyramid.view.view_config` decorator to associate your view functions +with URLs instead of using :term:`ZCML` or imperative configuration for the +same purpose. + +.. warning:: + + Using this feature tends to slows down application startup slightly, as + more work is performed at application startup to scan for view + declarations. + +Usage of the ``view_config`` decorator is a form of :term:`declarative +configuration`, like ZCML, but in decorator form. +:class:`pyramid.view.view_config` can be used to associate :term:`view +configuration` information -- as done via the equivalent imperative code or +ZCML -- with a function that acts as a :app:`Pyramid` view callable. All +arguments to the :meth:`pyramid.config.Configurator.add_view` method (save +for the ``view`` argument) are available in decorator form and mean precisely +the same thing. + +An example of the :class:`pyramid.view.view_config` decorator might reside in +a :app:`Pyramid` application module ``views.py``: + +.. ignore-next-block +.. code-block:: python + :linenos: + + from resources import MyResource + from pyramid.view import view_config + from pyramid.response import Response + + @view_config(name='my_view', request_method='POST', context=MyResource, + permission='read') + def my_view(request): + return Response('OK') + +Using this decorator as above replaces the need to add this imperative +configuration stanza: + +.. ignore-next-block +.. code-block:: python + :linenos: + + config.add_view('.views.my_view', name='my_view', request_method='POST', + context=MyResource, permission='read') + +All arguments to ``view_config`` may be omitted. For example: + +.. code-block:: python + :linenos: + + from pyramid.response import Response + from pyramid.view import view_config + + @view_config() + def my_view(request): + """ My view """ + return Response() + +Such a registration as the one directly above implies that the view name will +be ``my_view``, registered with a ``context`` argument that matches any +resource type, using no permission, registered against requests with any +request method, request type, request param, route name, or containment. + +The mere existence of a ``@view_config`` decorator doesn't suffice to perform +view configuration. All that the decorator does is "annotate" the function +with your configuration declarations, it doesn't process them. To make +:app:`Pyramid` process your :class:`pyramid.view.view_config` declarations, +you *must* do use the ``scan`` method of a +:class:`pyramid.config.Configurator`: + +.. code-block:: python + :linenos: + + # config is assumed to be an instance of the + # pyramid.config.Configurator class + config.scan() + +.. note:: See :ref:`zcml_scanning` for information about how to invoke a scan + via ZCML (if you're not using imperative configuration). + +Please see :ref:`decorations_and_code_scanning` for detailed information +about what happens when code is scanned for configuration declarations +resulting from use of decorators like :class:`pyramid.view.view_config`. + +See :ref:`configuration_module` for additional API arguments to the +:meth:`pyramid.config.Configurator.scan` method. For example, the method +allows you to supply a ``package`` argument to better control exactly *which* +code will be scanned. + +``@view_config`` Placement +++++++++++++++++++++++++++ + +A :class:`pyramid.view.view_config` decorator can be placed in various points +in your application. + +If your view callable is a function, it may be used as a function decorator: + +.. code-block:: python + :linenos: + + from pyramid.view import view_config + from pyramid.response import Response + + @view_config(name='edit') + def edit(request): + return Response('edited!') + +If your view callable is a class, the decorator can also be used as a class +decorator in Python 2.6 and better (Python 2.5 and below do not support class +decorators). All the arguments to the decorator are the same when applied +against a class as when they are applied against a function. For example: + +.. code-block:: python + :linenos: + + from pyramid.response import Response + from pyramid.view import view_config + + @view_config() + class MyView(object): + def __init__(self, request): + self.request = request + + def __call__(self): + return Response('hello') + +You can use the :class:`pyramid.view.view_config` decorator as a simple +callable to manually decorate classes in Python 2.5 and below without the +decorator syntactic sugar, if you wish: + +.. code-block:: python + :linenos: + + from pyramid.response import Response + from pyramid.view import view_config + + class MyView(object): + def __init__(self, request): + self.request = request + + def __call__(self): + return Response('hello') + + my_view = view_config()(MyView) + +More than one :class:`pyramid.view.view_config` decorator can be stacked on +top of any number of others. Each decorator creates a separate view +registration. For example: + +.. code-block:: python + :linenos: + + from pyramid.view import view_config + from pyramid.response import Response + + @view_config(name='edit') + @view_config(name='change') + def edit(request): + return Response('edited!') + +This registers the same view under two different names. + +The decorator can also be used against class methods: + +.. code-block:: python + :linenos: + + from pyramid.response import Response + from pyramid.view import view_config + + class MyView(object): + def __init__(self, request): + self.request = request + + @view_config(name='hello') + def amethod(self): + return Response('hello') + +When the decorator is used against a class method, a view is registered for +the *class*, so the class constructor must accept an argument list in one of +two forms: either it must accept a single argument ``request`` or it must +accept two arguments, ``context, request``. + +The method which is decorated must return a :term:`response`. + +Using the decorator against a particular method of a class is equivalent to +using the ``attr`` parameter in a decorator attached to the class itself. +For example, the above registration implied by the decorator being used +against the ``amethod`` method could be spelled equivalently as the below: + +.. code-block:: python + :linenos: + + from pyramid.response import Response + from pyramid.view import view_config + + @view_config(attr='amethod', name='hello') + class MyView(object): + def __init__(self, request): + self.request = request + + def amethod(self): + return Response('hello') + +.. index:: + single: add_view + +.. _mapping_views_using_imperative_config_section: + +View Registration Using :meth:`~pyramid.config.Configurator.add_view` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The :meth:`pyramid.config.Configurator.add_view` method within +:ref:`configuration_module` is used to configure a view imperatively. The +arguments to this method are very similar to the arguments that you provide +to the ``@view_config`` decorator. For example: + +.. code-block:: python + :linenos: + + from pyramid.response import Response + + def hello_world(request): + return Response('hello!') + + # config is assumed to be an instance of the + # pyramid.config.Configurator class + config.add_view(hello_world, name='hello.html') + +The first argument, ``view``, is required. It must either be a Python object +which is the view itself or a :term:`dotted Python name` to such an object. +All other arguments are optional. See +:meth:`pyramid.config.Configurator.add_view` for more information. + +.. _using_add_handler: + +Handler Registration Using :meth:`~pyramid.config.Configurator.add_handler` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:app:`Pyramid` provides the special concept of a :term:`view handler`. View +handlers are view classes that implement a number of methods, each of which +is a :term:`view callable` as a convenience for :term:`URL dispatch` users. + +.. note:: + + View handlers are *not* useful when using :term:`traversal`, only when using + :term:`url dispatch`. + +Using a view handler instead of a plain function or class :term:`view +callable` makes it unnecessary to call +:meth:`pyramid.config.Configurator.add_route` (and/or +:meth:`pyramid.config.Configurator.add_view`) "by hand" multiple times, +making it more pleasant to register a collection of views as a single class +when using :term:`url dispatch`. The view handler machinery also introduces +the concept of an ``action``, which is used as a :term:`view predicate` to +control which method of the handler is called. The method name is the +default *action name* of a handler view callable. + +The concept of a view handler is analogous to a "controller" in Pylons 1.0. + +The view handler class is initialized by :app:`Pyramid` in the same manner as +a "plain" view class. Its ``__init__`` is called with a request object (see +:ref:`class_as_view`). It implements methods, each of which is a :term:`view +callable`. When a request enters the system which corresponds with an +*action* related to one of its view callable methods, this method is called, +and it is expected to return a response. + +Here's an example view handler class: + +.. code-block:: python + :linenos: + + from pyramid.response import Response + + from pyramid.view import action + + class Hello(object): + def __init__(self, request): + self.request = request + + def index(self): + return Response('Hello world!') + + @action(renderer="mytemplate.mak") + def bye(self): + return {} + +The :class:`pyramid.view.action` decorator is used to fine-tune the view +parameters for each potential view callable which is a method of the handler. + +Handlers are added to application configuration via the +:meth:`pyramid.config.Configurator.add_handler` API. The +:meth:`~pyramid.config.Configurator.add_handler` method will scan a +:term:`view handler` class and automatically set up view configurations for +its methods that represent "auto-exposed" view callable, or those that were +decorated explicitly with the :class:`~pyramid.view.action` decorator. This +decorator is used to setup additional view configuration information for +individual methods of the class, and can be used repeatedly for a single view +method to register multiple view configurations for it. + +.. code-block:: python + :linenos: + + from myapp.handlers import Hello + config.add_handler('hello', '/hello/{action}', handler=Hello) + +This example will result in a route being added for the pattern +``/hello/{action}``, and each method of the ``Hello`` class will then be +examined to see if it should be registered as a potential view callable when +the ``/hello/{action}`` pattern matches. The value of ``{action}`` in the +route pattern will be used to determine which view should be called, and each +view in the class will be setup with a view predicate that requires a +specific ``action`` name. By default, the action name for a method of a +handler is the method name. + +If the URL was ``/hello/index``, the above example pattern would match, and, +by default, the ``index`` method of the ``Hello`` class would be called. + +Alternatively, the action can be declared specifically for a URL to be +registered for a *specific* ``action`` name: + +.. code-block:: python + :linenos: + + from myapp.handlers import Hello + config.add_handler('hello_index', '/hello/index', + handler=Hello, action='index') + +This will result one of the methods that are configured for the ``action`` of +'index' in the ``Hello`` handler class to be called. In this case the name of +the method is the same as the action name: ``index``. However, this need not +be the case, as we will see below. + +When calling :meth:`~pyramid.config.Configurator.add_handler`, an ``action`` +is required in either the route pattern or as a keyword argument, but +**cannot appear in both places**. A ``handler`` argument must also be +supplied, which can be either a :term:`asset specification` or a Python +reference to the handler class. Additional keyword arguments are passed +directly through to :meth:`pyramid.config.Configurator.add_route`. + +For example: + +.. code-block:: python + :linenos: + + config.add_handler('hello', '/hello/{action}', + handler='mypackage.handlers.MyHandler') + +Multiple :meth:`~pyramid.config.Configurator.add_handler` calls can specify +the same handler, to register specific route names for different +handler/action combinations. For example: + +.. code-block:: python + :linenos: + + config.add_handler('hello_index', '/hello/index', + handler=Hello, action='index') + config.add_handler('bye_index', '/hello/bye', + handler=Hello, action='bye') + +.. note:: + + Handler configuration may also be added to the system via :term:`ZCML` (see + :ref:`zcml_handler_configuration`). + +View Setup in the Handler Class ++++++++++++++++++++++++++++++++ + +A handler class can have a single class level attribute called +``__autoexpose__`` which should be a regular expression or the value +``None``. It's used to determine which method names will result in additional +view configurations being registered. + +When :meth:`~pyramid.config.Configurator.add_handler` runs, every method in +the handler class will be searched and a view registered if the method name +matches the ``__autoexpose__`` regular expression, or if the method was +decorated with :class:`~pyramid.view.action`. + +Every method in the handler class that has a name meeting the +``__autoexpose__`` regular expression will have a view registered for an +``action`` name corresponding to the method name. This functionality can be +disabled by setting the ``__autoexpose__`` attribute to ``None``: + +.. code-block:: python + :linenos: + + from pyramid.view import action + + class Hello(object): + __autoexpose__ = None + + def __init__(self, request): + self.request = request + + @action() + def index(self): + return Response('Hello world!') + + @action(renderer="mytemplate.mak") + def bye(self): + return {} + +With auto-expose effectively disabled, no views will be registered for a +method unless it is specifically decorated with +:class:`~pyramid.view.action`. + +Action Decorators in a Handler +++++++++++++++++++++++++++++++ + +The :class:`~pyramid.view.action` decorator registers view configuration +information on the handler method, which is used by +:meth:`~pyramid.config.Configurator.add_handler` to setup the view +configuration. + +All keyword arguments are recorded, and passed to +:meth:`~pyramid.config.Configurator.add_view`. Any valid keyword arguments +for :meth:`~pyramid.config.Configurator.add_view` can thus be used with the +:class:`~pyramid.view.action` decorator to further restrict when the view +will be called. + +One important difference is that a handler method can respond to an +``action`` name that is different from the method name by passing in a +``name`` argument. + +Example: + +.. code-block:: python + :linenos: + + from pyramid.view import action + + class Hello(object): + def __init__(self, request): + self.request = request + + @action(name='index', renderer='created.mak', request_method='POST') + def create(self): + return {} + + @action(renderer="view_all.mak", request_method='GET') + def index(self): + return {} + +This will register two views that require the ``action`` to be ``index``, +with the additional view predicate requiring a specific request method. + +It can be useful to decorate a single method multiple times with +:class:`~pyramid.view.action`. Each action decorator will register a new view +for the method. By specifying different names and renderers for each action, +the same view logic can be exposed and rendered differently on multiple URLs. + +Example: + +.. code-block:: python + :linenos: + + from pyramid.view import action + + class Hello(object): + def __init__(self, request): + self.request = request + + @action(name='home', renderer='home.mak') + @action(name='about', renderer='about.mak') + def show_template(self): + # prep some template vars + return {} + + # in the config + config.add_handler('hello', '/hello/{action}', handler=Hello) + +With this configuration, the url ``/hello/home`` will find a view +configuration that results in calling the ``show_template`` method, then +rendering the template with ``home.mak``, and the url ``/hello/about`` will +call the same method and render the ``about.mak`` template. + +.. index:: + single: resource interfaces + +.. _using_resource_interfaces: + +Using Resource Interfaces In View Configuration +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Instead of registering your views with a ``context`` that names a Python +resource *class*, you can optionally register a view callable with a +``context`` which is an :term:`interface`. An interface can be attached +arbitrarily to any resource object. View lookup treats context interfaces +specially, and therefore the identity of a resource can be divorced from that +of the class which implements it. As a result, associating a view with an +interface can provide more flexibility for sharing a single view between two +or more different implementations of a resource type. For example, if two +resource objects of different Python class types share the same interface, +you can use the same view configuration to specify both of them as a +``context``. + +In order to make use of interfaces in your application during view dispatch, +you must create an interface and mark up your resource classes or instances +with interface declarations that refer to this interface. + +To attach an interface to a resource *class*, you define the interface and +use the :func:`zope.interface.implements` function to associate the interface +with the class. + +.. code-block:: python + :linenos: + + from zope.interface import Interface + from zope.interface import implements + + class IHello(Interface): + """ A marker interface """ + + class Hello(object): + implements(IHello) + +To attach an interface to a resource *instance*, you define the interface and +use the :func:`zope.interface.alsoProvides` function to associate the +interface with the instance. This function mutates the instance in such a +way that the interface is attached to it. + +.. code-block:: python + :linenos: + + from zope.interface import Interface + from zope.interface import alsoProvides + + class IHello(Interface): + """ A marker interface """ + + class Hello(object): + pass + + def make_hello(): + hello = Hello() + alsoProvides(hello, IHello) + return hello + +Regardless of how you associate an interface, with a resource instance, or a +resource class, the resulting code to associate that interface with a view +callable is the same. Assuming the above code that defines an ``IHello`` +interface lives in the root of your application, and its module is named +"resources.py", the interface declaration below will associate the +``mypackage.views.hello_world`` view with resources that implement, or +provide, this interface. + +.. code-block:: python + :linenos: + + # config is an instance of pyramid.config.Configurator + + config.add_view('mypackage.views.hello_world', name='hello.html', + context='mypackage.resources.IHello') + +Any time a resource that is determined to be the :term:`context` provides +this interface, and a view named ``hello.html`` is looked up against it as +per the URL, the ``mypackage.views.hello_world`` view callable will be +invoked. + +Note, in cases where a view is registered against a resource class, and a +view is also registered against an interface that the resource class +implements, an ambiguity arises. Views registered for the resource class take +precedence over any views registered for any interface the resource class +implements. Thus, if one view configuration names a ``context`` of both the +class type of a resource, and another view configuration names a ``context`` +of interface implemented by the resource's class, and both view +configurations are otherwise identical, the view registered for the context's +class will "win". + +For more information about defining resources with interfaces for use within +view configuration, see :ref:`resources_which_implement_interfaces`. + +.. index:: + single: view security + pair: security; view + +.. _view_security_section: + +Configuring View Security +~~~~~~~~~~~~~~~~~~~~~~~~~ + +If an :term:`authorization policy` is active, any :term:`permission` attached +to a :term:`view configuration` found during view lookup will be verified. +This will ensure that the currently authenticated user possesses that +permission against the :term:`context` resource before the view function is +actually called. Here's an example of specifying a permission in a view +configuration using :meth:`pyramid.config.Configurator.add_view`: + +.. code-block:: python + :linenos: + + # config is an instance of pyramid.config.Configurator + + config.add_view('myproject.views.add_entry', name='add.html', + context='myproject.resources.IBlog', permission='add') + +When an :term:`authorization policy` is enabled, this view will be protected +with the ``add`` permission. The view will *not be called* if the user does +not possess the ``add`` permission relative to the current :term:`context`. +Instead the :term:`forbidden view` result will be returned to the client as +per :ref:`protecting_views`. + +.. index:: + single: debugging not found errors + single: not found error (debugging) + +.. _debug_notfound_section: + +:exc:`NotFound` Errors +~~~~~~~~~~~~~~~~~~~~~~ + +It's useful to be able to debug :exc:`NotFound` error responses when they +occur unexpectedly due to an application registry misconfiguration. To debug +these errors, use the ``PYRAMID_DEBUG_NOTFOUND`` environment variable or the +``debug_notfound`` configuration file setting. Details of why a view was not +found will be printed to ``stderr``, and the browser representation of the +error will include the same information. See :ref:`environment_chapter` for +more information about how, and where to set these values. + diff --git a/docs/narr/views.rst b/docs/narr/views.rst index 08ae5d5d8..48bb7f829 100644 --- a/docs/narr/views.rst +++ b/docs/narr/views.rst @@ -31,9 +31,9 @@ in the :term:`request` against :term:`view configuration` statements made by the developer to choose the most appropriate view callable for a specific set of circumstances. -This chapter provides documentation detailing the process of creating -view callables, documentation about performing view configuration, and -a detailed explanation of view lookup. +This chapter describes how view callables work. In the +:ref:`view_config_chapter` chapter, there are details about performing +view configuration, and a detailed explanation of view lookup. View Callables -------------- @@ -51,13 +51,14 @@ the request object contains everything your application needs to know about the specific HTTP request being made. A view callable's ultimate responsibility is to create a :mod:`Pyramid` -:term:`Response` object. This can be done by creating the response in -the view callable code and returning it directly. However, if a view -callable does not return a response itself, it can be configured to use -a :term:`renderer` that converts its return value into a -:term:`Response` object. Using renderers is the common way that -templates are used with view callables to generate markup. See the -:ref:`renderers_chapter` chapter for details. +:term:`Response` object. This can be done by creating the response +object in the view callable code and returning it directly, as we will +be doing in this chapter. However, if a view callable does not return a +response itself, it can be configured to use a :term:`renderer` that +converts its return value into a :term:`Response` object. Using +renderers is the common way that templates are used with view callables +to generate markup. See the :ref:`renderers_chapter` chapter for +details. .. index:: single: view calling convention @@ -66,7 +67,7 @@ templates are used with view callables to generate markup. See the .. _function_as_view: Defining a View Callable as a Function -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +-------------------------------------- One of the easiest way to define a view callable is to create a function that accepts a single argument named ``request``, and which returns a @@ -88,7 +89,7 @@ implemented as a function: .. _class_as_view: Defining a View Callable as a Class -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +----------------------------------- A view callable may also be represented by a Python class instead of a function. When a view callable is a class, the calling semantics are @@ -138,7 +139,7 @@ method expected to return a response, you can either: .. _request_and_context_view_definitions: Alternate View Callable Argument/Calling Conventions -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +---------------------------------------------------- Usually, view callables are defined to accept only a single argument: ``request``. However, view callables may alternately be defined as classes, @@ -213,7 +214,7 @@ access to the context via ``request.context``. .. _the_response: View Callable Responses -~~~~~~~~~~~~~~~~~~~~~~~ +----------------------- A view callable may always return an object that implements the :app:`Pyramid` :term:`Response` interface. The easiest way to return something that @@ -259,7 +260,7 @@ These attributes form the notional "Pyramid Response interface". .. _http_redirect: Using a View Callable to Do an HTTP Redirect -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +-------------------------------------------- You can issue an HTTP redirect from within a view by returning a particular kind of response. @@ -298,7 +299,7 @@ Unauthorized``. .. _special_exceptions_in_callables: Using Special Exceptions In View Callables -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------------------------ Usually when a Python exception is raised within a view callable, :app:`Pyramid` allows the exception to propagate all the way out to the @@ -328,7 +329,7 @@ available to the view which :app:`Pyramid` invokes as .. _exception_views: Exception Views -~~~~~~~~~~~~~~~~ +--------------- The machinery which allows the special :exc:`pyramid.exceptions.NotFound` and :exc:`pyramid.exceptions.Forbidden` exceptions to be caught by specialized @@ -412,1084 +413,3 @@ Exception views can be configured with any view registration mechanism: single: forms, views, and unicode single: views, forms, and unicode -Handling Form Submissions in View Callables (Unicode and Character Set Issues) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Most web applications need to accept form submissions from web browsers and -various other clients. In :app:`Pyramid`, form submission handling logic is -always part of a :term:`view`. For a general overview of how to handle form -submission data using the :term:`WebOb` API, see :ref:`webob_chapter` and -`"Query and POST variables" within the WebOb documentation -`_. -:app:`Pyramid` defers to WebOb for its request and response implementations, -and handling form submission data is a property of the request -implementation. Understanding WebOb's request API is the key to -understanding how to process form submission data. - -There are some defaults that you need to be aware of when trying to handle -form submission data in a :app:`Pyramid` view. Having high-order (i.e., -non-ASCII) characters in data contained within form submissions is -exceedingly common, and the UTF-8 encoding is the most common encoding used -on the web for character data. Since Unicode values are much saner than -working with and storing bytestrings, :app:`Pyramid` configures the -:term:`WebOb` request machinery to attempt to decode form submission values -into Unicode from UTF-8 implicitly. This implicit decoding happens when view -code obtains form field values via the ``request.params``, ``request.GET``, -or ``request.POST`` APIs (see :ref:`request_module` for details about these -APIs). - -.. note:: - - Many people find the difference between Unicode and UTF-8 confusing. - Unicode is a standard for representing text that supports most of the - world's writing systems. However, there are many ways that Unicode data - can be encoded into bytes for transit and storage. UTF-8 is a specific - encoding for Unicode, that is backwards-compatible with ASCII. This makes - UTF-8 very convenient for encoding data where a large subset of that data - is ASCII characters, which is largely true on the web. UTF-8 is also the - standard character encoding for URLs. - -As an example, let's assume that the following form page is served up to a -browser client, and its ``action`` points at some :app:`Pyramid` view code: - -.. code-block:: xml - :linenos: - - - - - -
-
- -
-
- -
- -
- - -The ``myview`` view code in the :app:`Pyramid` application *must* expect that -the values returned by ``request.params`` will be of type ``unicode``, as -opposed to type ``str``. The following will work to accept a form post from -the above form: - -.. code-block:: python - :linenos: - - def myview(request): - firstname = request.params['firstname'] - lastname = request.params['lastname'] - -But the following ``myview`` view code *may not* work, as it tries to decode -already-decoded (``unicode``) values obtained from ``request.params``: - -.. code-block:: python - :linenos: - - def myview(request): - # the .decode('utf-8') will break below if there are any high-order - # characters in the firstname or lastname - firstname = request.params['firstname'].decode('utf-8') - lastname = request.params['lastname'].decode('utf-8') - -For implicit decoding to work reliably, you should ensure that every form you -render that posts to a :app:`Pyramid` view explicitly defines a charset -encoding of UTF-8. This can be done via a response that has a -``;charset=UTF-8`` in its ``Content-Type`` header; or, as in the form above, -with a ``meta http-equiv`` tag that implies that the charset is UTF-8 within -the HTML ``head`` of the page containing the form. This must be done -explicitly because all known browser clients assume that they should encode -form data in the same character set implied by ``Content-Type`` value of the -response containing the form when subsequently submitting that form. There is -no other generally accepted way to tell browser clients which charset to use -to encode form data. If you do not specify an encoding explicitly, the -browser client will choose to encode form data in its default character set -before submitting it, which may not be UTF-8 as the server expects. If a -request containing form data encoded in a non-UTF8 charset is handled by your -view code, eventually the request code accessed within your view will throw -an error when it can't decode some high-order character encoded in another -character set within form data, e.g., when ``request.params['somename']`` is -accessed. - -If you are using the :class:`pyramid.response.Response` class to generate a -response, or if you use the ``render_template_*`` templating APIs, the UTF-8 -charset is set automatically as the default via the ``Content-Type`` header. -If you return a ``Content-Type`` header without an explicit charset, a -request will add a ``;charset=utf-8`` trailer to the ``Content-Type`` header -value for you, for response content types that are textual -(e.g. ``text/html``, ``application/xml``, etc) as it is rendered. If you are -using your own response object, you will need to ensure you do this yourself. - -.. note:: Only the *values* of request params obtained via - ``request.params``, ``request.GET`` or ``request.POST`` are decoded - to Unicode objects implicitly in the :app:`Pyramid` default - configuration. The keys are still (byte) strings. - -.. index:: - single: view configuration - -.. _view_configuration: - -View Configuration: Mapping a Resource or URL Pattern to a View Callable ------------------------------------------------------------------------- - -A developer makes a :term:`view callable` available for use within a -:app:`Pyramid` application via :term:`view configuration`. A view -configuration associates a view callable with a set of statements that -determine the set of circumstances which must be true for the view callable -to be invoked. - -A view configuration statement is made about information present in the -:term:`context` resource and the :term:`request`. - -View configuration is performed in one of these ways: - -- by running a :term:`scan` against application source code which has a - :class:`pyramid.view.view_config` decorator attached to a Python object as - per :class:`pyramid.view.view_config` and - :ref:`mapping_views_using_a_decorator_section`. - -- by using the :meth:`pyramid.config.Configurator.add_view` method as per - :meth:`pyramid.config.Configurator.add_view` and - :ref:`mapping_views_using_imperative_config_section`. - -- By specifying a view within a :term:`route configuration`. View - configuration via a route configuration is performed by using the - :meth:`pyramid.config.Configurator.add_route` method, passing a ``view`` - argument specifying a view callable. - -- by using the :meth:`pyramid.config.Configurator.add_handler` against a - :term:`view handler` class (useful only for :term:`URL dispatch` - applications). - -.. note:: You can also add view configuration by adding a ````, - ```` or ```` declaration to :term:`ZCML` used by your - application as per :ref:`mapping_views_using_zcml_section`, - :ref:`view_directive`, :ref:`route_directive` or :ref:`handler_directive`. - -.. _view_configuration_parameters: - -View Configuration Parameters -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -All forms of view configuration accept the same general types of arguments. - -Many arguments supplied during view configuration are :term:`view predicate` -arguments. View predicate arguments used during view configuration are used -to narrow the set of circumstances in which :mod:`view lookup` will find a -particular view callable. In general, the fewer number of predicates which -are supplied to a particular view configuration, the more likely it is that -the associated view callable will be invoked. The greater the number -supplied, the less likely. - -Some view configuration arguments are non-predicate arguments. These tend to -modify the response of the view callable or prevent the view callable from -being invoked due to an authorization policy. The presence of non-predicate -arguments in a view configuration does not narrow the circumstances in which -the view callable will be invoked. - -Non-Predicate Arguments -+++++++++++++++++++++++ - -``permission`` - The name of a :term:`permission` that the user must possess in order to - invoke the :term:`view callable`. See :ref:`view_security_section` for - more information about view security and permissions. - - If ``permission`` is not supplied, no permission is registered for this - view (it's accessible by any caller). - -``attr`` - The view machinery defaults to using the ``__call__`` method of the - :term:`view callable` (or the function itself, if the view callable is a - function) to obtain a response. The ``attr`` value allows you to vary the - method attribute used to obtain the response. For example, if your view - was a class, and the class has a method named ``index`` and you wanted to - use this method instead of the class' ``__call__`` method to return the - response, you'd say ``attr="index"`` in the view configuration for the - view. This is most useful when the view definition is a class. - - If ``attr`` is not supplied, ``None`` is used (implying the function itself - if the view is a function, or the ``__call__`` callable attribute if the - view is a class). - -``renderer`` - Denotes the :term:`renderer` implementation which will be used to construct - a :term:`response` from the associated view callable's return value. (see - also :ref:`renderers_chapter`). - - This is either a single string term (e.g. ``json``) or a string implying a - path or :term:`asset specification` (e.g. ``templates/views.pt``) naming a - :term:`renderer` implementation. If the ``renderer`` value does not - contain a dot (``.``), the specified string will be used to look up a - renderer implementation, and that renderer implementation will be used to - construct a response from the view return value. If the ``renderer`` value - contains a dot (``.``), the specified term will be treated as a path, and - the filename extension of the last element in the path will be used to look - up the renderer implementation, which will be passed the full path. - - When the renderer is a path, although a path is usually just a simple - relative pathname (e.g. ``templates/foo.pt``, implying that a template - named "foo.pt" is in the "templates" directory relative to the directory of - the current :term:`package`), a path can be absolute, starting with a slash - on UNIX or a drive letter prefix on Windows. The path can alternately be a - :term:`asset specification` in the form - ``some.dotted.package_name:relative/path``, making it possible to address - template assets which live in a separate package. - - The ``renderer`` attribute is optional. If it is not defined, the "null" - renderer is assumed (no rendering is performed and the value is passed back - to the upstream :app:`Pyramid` machinery unmolested). Note that if the - view callable itself returns a :term:`response` (see :ref:`the_response`), - the specified renderer implementation is never called. - -``wrapper`` - The :term:`view name` of a different :term:`view configuration` which will - receive the response body of this view as the ``request.wrapped_body`` - attribute of its own :term:`request`, and the :term:`response` returned by - this view as the ``request.wrapped_response`` attribute of its own request. - Using a wrapper makes it possible to "chain" views together to form a - composite response. The response of the outermost wrapper view will be - returned to the user. The wrapper view will be found as any view is found: - see :ref:`view_lookup`. The "best" wrapper view will be found based on the - lookup ordering: "under the hood" this wrapper view is looked up via - ``pyramid.view.render_view_to_response(context, request, - 'wrapper_viewname')``. The context and request of a wrapper view is the - same context and request of the inner view. - - If ``wrapper`` is not supplied, no wrapper view is used. - -Predicate Arguments -+++++++++++++++++++ - -These arguments modify view lookup behavior. In general, the more predicate -arguments that are supplied, the more specific, and narrower the usage of the -configured view. - -``name`` - The :term:`view name` required to match this view callable. Read - :ref:`traversal_chapter` to understand the concept of a view name. - - If ``name`` is not supplied, the empty string is used (implying the default - view). - -``context`` - An object representing a Python class that the :term:`context` resource - must be an instance of *or* the :term:`interface` that the :term:`context` - resource must provide in order for this view to be found and called. This - predicate is true when the :term:`context` resource is an instance of the - represented class or if the :term:`context` resource provides the - represented interface; it is otherwise false. - - If ``context`` is not supplied, the value ``None``, which matches any - resource, is used. - -``route_name`` - If ``route_name`` is supplied, the view callable will be invoked only when - the named route has matched. - - This value must match the ``name`` of a :term:`route configuration` - declaration (see :ref:`urldispatch_chapter`) that must match before this - view will be called. Note that the ``route`` configuration referred to by - ``route_name`` will usually have a ``*traverse`` token in the value of its - ``pattern``, representing a part of the path that will be used by - :term:`traversal` against the result of the route's :term:`root factory`. - - If ``route_name`` is not supplied, the view callable will be have a chance - of being invoked if no other route was matched. This is when the - request/context pair found via :term:`resource location` does not indicate - it matched any configured route. - -``request_type`` - This value should be an :term:`interface` that the :term:`request` must - provide in order for this view to be found and called. - - If ``request_type`` is not supplied, the value ``None`` is used, implying - any request type. - - *This is an advanced feature, not often used by "civilians"*. - -``request_method`` - This value can either be one of the strings ``GET``, ``POST``, ``PUT``, - ``DELETE``, or ``HEAD`` representing an HTTP ``REQUEST_METHOD``. A view - declaration with this argument ensures that the view will only be called - when the request's ``method`` attribute (aka the ``REQUEST_METHOD`` of the - WSGI environment) string matches the supplied value. - - If ``request_method`` is not supplied, the view will be invoked regardless - of the ``REQUEST_METHOD`` of the :term:`WSGI` environment. - -``request_param`` - This value can be any string. A view declaration with this argument - ensures that the view will only be called when the :term:`request` has a - key in the ``request.params`` dictionary (an HTTP ``GET`` or ``POST`` - variable) that has a name which matches the supplied value. - - If the value supplied has a ``=`` sign in it, - e.g. ``request_params="foo=123"``, then the key (``foo``) must both exist - in the ``request.params`` dictionary, *and* the value must match the right - hand side of the expression (``123``) for the view to "match" the current - request. - - If ``request_param`` is not supplied, the view will be invoked without - consideration of keys and values in the ``request.params`` dictionary. - -``containment`` - This value should be a reference to a Python class or :term:`interface` - that a parent object in the context resource's :term:`lineage` must provide - in order for this view to be found and called. The resources in your - resource tree must be "location-aware" to use this feature. - - If ``containment`` is not supplied, the interfaces and classes in the - lineage are not considered when deciding whether or not to invoke the view - callable. - - See :ref:`location_aware` for more information about location-awareness. - -``xhr`` - This value should be either ``True`` or ``False``. If this value is - specified and is ``True``, the :term:`WSGI` environment must possess an - ``HTTP_X_REQUESTED_WITH`` (aka ``X-Requested-With``) header that has the - value ``XMLHttpRequest`` for the associated view callable to be found and - called. This is useful for detecting AJAX requests issued from jQuery, - Prototype and other Javascript libraries. - - If ``xhr`` is not specified, the ``HTTP_X_REQUESTED_WITH`` HTTP header is - not taken into consideration when deciding whether or not to invoke the - associated view callable. - -``accept`` - The value of this argument represents a match query for one or more - mimetypes in the ``Accept`` HTTP request header. If this value is - specified, it must be in one of the following forms: a mimetype match token - in the form ``text/plain``, a wildcard mimetype match token in the form - ``text/*`` or a match-all wildcard mimetype match token in the form - ``*/*``. If any of the forms matches the ``Accept`` header of the request, - this predicate will be true. - - If ``accept`` is not specified, the ``HTTP_ACCEPT`` HTTP header is not - taken into consideration when deciding whether or not to invoke the - associated view callable. - -``header`` - This value represents an HTTP header name or a header name/value pair. - - If ``header`` is specified, it must be a header name or a - ``headername:headervalue`` pair. - - If ``header`` is specified without a value (a bare header name only, - e.g. ``If-Modified-Since``), the view will only be invoked if the HTTP - header exists with any value in the request. - - If ``header`` is specified, and possesses a name/value pair - (e.g. ``User-Agent:Mozilla/.*``), the view will only be invoked if the HTTP - header exists *and* the HTTP header matches the value requested. When the - ``headervalue`` contains a ``:`` (colon), it will be considered a - name/value pair (e.g. ``User-Agent:Mozilla/.*`` or ``Host:localhost``). - The value portion should be a regular expression. - - Whether or not the value represents a header name or a header name/value - pair, the case of the header name is not significant. - - If ``header`` is not specified, the composition, presence or absence of - HTTP headers is not taken into consideration when deciding whether or not - to invoke the associated view callable. - -``path_info`` - This value represents a regular expression pattern that will be tested - against the ``PATH_INFO`` WSGI environment variable to decide whether or - not to call the associated view callable. If the regex matches, this - predicate will be ``True``. - - If ``path_info`` is not specified, the WSGI ``PATH_INFO`` is not taken into - consideration when deciding whether or not to invoke the associated view - callable. - -``custom_predicates`` - If ``custom_predicates`` is specified, it must be a sequence of references - to custom predicate callables. Use custom predicates when no set of - predefined predicates do 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 ``False`` after doing arbitrary evaluation of the - context resource and/or the request. If all callables return ``True``, the - associated view callable will be considered viable for a given request. - - If ``custom_predicates`` is not specified, no custom predicates are - used. - -.. index:: - single: view_config decorator - -.. _mapping_views_using_a_decorator_section: - -View Configuration Using the ``@view_config`` Decorator -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -For better locality of reference, you may use the -:class:`pyramid.view.view_config` decorator to associate your view functions -with URLs instead of using :term:`ZCML` or imperative configuration for the -same purpose. - -.. warning:: - - Using this feature tends to slows down application startup slightly, as - more work is performed at application startup to scan for view - declarations. - -Usage of the ``view_config`` decorator is a form of :term:`declarative -configuration`, like ZCML, but in decorator form. -:class:`pyramid.view.view_config` can be used to associate :term:`view -configuration` information -- as done via the equivalent imperative code or -ZCML -- with a function that acts as a :app:`Pyramid` view callable. All -arguments to the :meth:`pyramid.config.Configurator.add_view` method (save -for the ``view`` argument) are available in decorator form and mean precisely -the same thing. - -An example of the :class:`pyramid.view.view_config` decorator might reside in -a :app:`Pyramid` application module ``views.py``: - -.. ignore-next-block -.. code-block:: python - :linenos: - - from resources import MyResource - from pyramid.view import view_config - from pyramid.response import Response - - @view_config(name='my_view', request_method='POST', context=MyResource, - permission='read') - def my_view(request): - return Response('OK') - -Using this decorator as above replaces the need to add this imperative -configuration stanza: - -.. ignore-next-block -.. code-block:: python - :linenos: - - config.add_view('.views.my_view', name='my_view', request_method='POST', - context=MyResource, permission='read') - -All arguments to ``view_config`` may be omitted. For example: - -.. code-block:: python - :linenos: - - from pyramid.response import Response - from pyramid.view import view_config - - @view_config() - def my_view(request): - """ My view """ - return Response() - -Such a registration as the one directly above implies that the view name will -be ``my_view``, registered with a ``context`` argument that matches any -resource type, using no permission, registered against requests with any -request method, request type, request param, route name, or containment. - -The mere existence of a ``@view_config`` decorator doesn't suffice to perform -view configuration. All that the decorator does is "annotate" the function -with your configuration declarations, it doesn't process them. To make -:app:`Pyramid` process your :class:`pyramid.view.view_config` declarations, -you *must* do use the ``scan`` method of a -:class:`pyramid.config.Configurator`: - -.. code-block:: python - :linenos: - - # config is assumed to be an instance of the - # pyramid.config.Configurator class - config.scan() - -.. note:: See :ref:`zcml_scanning` for information about how to invoke a scan - via ZCML (if you're not using imperative configuration). - -Please see :ref:`decorations_and_code_scanning` for detailed information -about what happens when code is scanned for configuration declarations -resulting from use of decorators like :class:`pyramid.view.view_config`. - -See :ref:`configuration_module` for additional API arguments to the -:meth:`pyramid.config.Configurator.scan` method. For example, the method -allows you to supply a ``package`` argument to better control exactly *which* -code will be scanned. - -``@view_config`` Placement -++++++++++++++++++++++++++ - -A :class:`pyramid.view.view_config` decorator can be placed in various points -in your application. - -If your view callable is a function, it may be used as a function decorator: - -.. code-block:: python - :linenos: - - from pyramid.view import view_config - from pyramid.response import Response - - @view_config(name='edit') - def edit(request): - return Response('edited!') - -If your view callable is a class, the decorator can also be used as a class -decorator in Python 2.6 and better (Python 2.5 and below do not support class -decorators). All the arguments to the decorator are the same when applied -against a class as when they are applied against a function. For example: - -.. code-block:: python - :linenos: - - from pyramid.response import Response - from pyramid.view import view_config - - @view_config() - class MyView(object): - def __init__(self, request): - self.request = request - - def __call__(self): - return Response('hello') - -You can use the :class:`pyramid.view.view_config` decorator as a simple -callable to manually decorate classes in Python 2.5 and below without the -decorator syntactic sugar, if you wish: - -.. code-block:: python - :linenos: - - from pyramid.response import Response - from pyramid.view import view_config - - class MyView(object): - def __init__(self, request): - self.request = request - - def __call__(self): - return Response('hello') - - my_view = view_config()(MyView) - -More than one :class:`pyramid.view.view_config` decorator can be stacked on -top of any number of others. Each decorator creates a separate view -registration. For example: - -.. code-block:: python - :linenos: - - from pyramid.view import view_config - from pyramid.response import Response - - @view_config(name='edit') - @view_config(name='change') - def edit(request): - return Response('edited!') - -This registers the same view under two different names. - -The decorator can also be used against class methods: - -.. code-block:: python - :linenos: - - from pyramid.response import Response - from pyramid.view import view_config - - class MyView(object): - def __init__(self, request): - self.request = request - - @view_config(name='hello') - def amethod(self): - return Response('hello') - -When the decorator is used against a class method, a view is registered for -the *class*, so the class constructor must accept an argument list in one of -two forms: either it must accept a single argument ``request`` or it must -accept two arguments, ``context, request``. - -The method which is decorated must return a :term:`response`. - -Using the decorator against a particular method of a class is equivalent to -using the ``attr`` parameter in a decorator attached to the class itself. -For example, the above registration implied by the decorator being used -against the ``amethod`` method could be spelled equivalently as the below: - -.. code-block:: python - :linenos: - - from pyramid.response import Response - from pyramid.view import view_config - - @view_config(attr='amethod', name='hello') - class MyView(object): - def __init__(self, request): - self.request = request - - def amethod(self): - return Response('hello') - -.. index:: - single: add_view - -.. _mapping_views_using_imperative_config_section: - -View Registration Using :meth:`~pyramid.config.Configurator.add_view` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The :meth:`pyramid.config.Configurator.add_view` method within -:ref:`configuration_module` is used to configure a view imperatively. The -arguments to this method are very similar to the arguments that you provide -to the ``@view_config`` decorator. For example: - -.. code-block:: python - :linenos: - - from pyramid.response import Response - - def hello_world(request): - return Response('hello!') - - # config is assumed to be an instance of the - # pyramid.config.Configurator class - config.add_view(hello_world, name='hello.html') - -The first argument, ``view``, is required. It must either be a Python object -which is the view itself or a :term:`dotted Python name` to such an object. -All other arguments are optional. See -:meth:`pyramid.config.Configurator.add_view` for more information. - -.. _using_add_handler: - -Handler Registration Using :meth:`~pyramid.config.Configurator.add_handler` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -:app:`Pyramid` provides the special concept of a :term:`view handler`. View -handlers are view classes that implement a number of methods, each of which -is a :term:`view callable` as a convenience for :term:`URL dispatch` users. - -.. note:: - - View handlers are *not* useful when using :term:`traversal`, only when using - :term:`url dispatch`. - -Using a view handler instead of a plain function or class :term:`view -callable` makes it unnecessary to call -:meth:`pyramid.config.Configurator.add_route` (and/or -:meth:`pyramid.config.Configurator.add_view`) "by hand" multiple times, -making it more pleasant to register a collection of views as a single class -when using :term:`url dispatch`. The view handler machinery also introduces -the concept of an ``action``, which is used as a :term:`view predicate` to -control which method of the handler is called. The method name is the -default *action name* of a handler view callable. - -The concept of a view handler is analogous to a "controller" in Pylons 1.0. - -The view handler class is initialized by :app:`Pyramid` in the same manner as -a "plain" view class. Its ``__init__`` is called with a request object (see -:ref:`class_as_view`). It implements methods, each of which is a :term:`view -callable`. When a request enters the system which corresponds with an -*action* related to one of its view callable methods, this method is called, -and it is expected to return a response. - -Here's an example view handler class: - -.. code-block:: python - :linenos: - - from pyramid.response import Response - - from pyramid.view import action - - class Hello(object): - def __init__(self, request): - self.request = request - - def index(self): - return Response('Hello world!') - - @action(renderer="mytemplate.mak") - def bye(self): - return {} - -The :class:`pyramid.view.action` decorator is used to fine-tune the view -parameters for each potential view callable which is a method of the handler. - -Handlers are added to application configuration via the -:meth:`pyramid.config.Configurator.add_handler` API. The -:meth:`~pyramid.config.Configurator.add_handler` method will scan a -:term:`view handler` class and automatically set up view configurations for -its methods that represent "auto-exposed" view callable, or those that were -decorated explicitly with the :class:`~pyramid.view.action` decorator. This -decorator is used to setup additional view configuration information for -individual methods of the class, and can be used repeatedly for a single view -method to register multiple view configurations for it. - -.. code-block:: python - :linenos: - - from myapp.handlers import Hello - config.add_handler('hello', '/hello/{action}', handler=Hello) - -This example will result in a route being added for the pattern -``/hello/{action}``, and each method of the ``Hello`` class will then be -examined to see if it should be registered as a potential view callable when -the ``/hello/{action}`` pattern matches. The value of ``{action}`` in the -route pattern will be used to determine which view should be called, and each -view in the class will be setup with a view predicate that requires a -specific ``action`` name. By default, the action name for a method of a -handler is the method name. - -If the URL was ``/hello/index``, the above example pattern would match, and, -by default, the ``index`` method of the ``Hello`` class would be called. - -Alternatively, the action can be declared specifically for a URL to be -registered for a *specific* ``action`` name: - -.. code-block:: python - :linenos: - - from myapp.handlers import Hello - config.add_handler('hello_index', '/hello/index', - handler=Hello, action='index') - -This will result one of the methods that are configured for the ``action`` of -'index' in the ``Hello`` handler class to be called. In this case the name of -the method is the same as the action name: ``index``. However, this need not -be the case, as we will see below. - -When calling :meth:`~pyramid.config.Configurator.add_handler`, an ``action`` -is required in either the route pattern or as a keyword argument, but -**cannot appear in both places**. A ``handler`` argument must also be -supplied, which can be either a :term:`asset specification` or a Python -reference to the handler class. Additional keyword arguments are passed -directly through to :meth:`pyramid.config.Configurator.add_route`. - -For example: - -.. code-block:: python - :linenos: - - config.add_handler('hello', '/hello/{action}', - handler='mypackage.handlers.MyHandler') - -Multiple :meth:`~pyramid.config.Configurator.add_handler` calls can specify -the same handler, to register specific route names for different -handler/action combinations. For example: - -.. code-block:: python - :linenos: - - config.add_handler('hello_index', '/hello/index', - handler=Hello, action='index') - config.add_handler('bye_index', '/hello/bye', - handler=Hello, action='bye') - -.. note:: - - Handler configuration may also be added to the system via :term:`ZCML` (see - :ref:`zcml_handler_configuration`). - -View Setup in the Handler Class -+++++++++++++++++++++++++++++++ - -A handler class can have a single class level attribute called -``__autoexpose__`` which should be a regular expression or the value -``None``. It's used to determine which method names will result in additional -view configurations being registered. - -When :meth:`~pyramid.config.Configurator.add_handler` runs, every method in -the handler class will be searched and a view registered if the method name -matches the ``__autoexpose__`` regular expression, or if the method was -decorated with :class:`~pyramid.view.action`. - -Every method in the handler class that has a name meeting the -``__autoexpose__`` regular expression will have a view registered for an -``action`` name corresponding to the method name. This functionality can be -disabled by setting the ``__autoexpose__`` attribute to ``None``: - -.. code-block:: python - :linenos: - - from pyramid.view import action - - class Hello(object): - __autoexpose__ = None - - def __init__(self, request): - self.request = request - - @action() - def index(self): - return Response('Hello world!') - - @action(renderer="mytemplate.mak") - def bye(self): - return {} - -With auto-expose effectively disabled, no views will be registered for a -method unless it is specifically decorated with -:class:`~pyramid.view.action`. - -Action Decorators in a Handler -++++++++++++++++++++++++++++++ - -The :class:`~pyramid.view.action` decorator registers view configuration -information on the handler method, which is used by -:meth:`~pyramid.config.Configurator.add_handler` to setup the view -configuration. - -All keyword arguments are recorded, and passed to -:meth:`~pyramid.config.Configurator.add_view`. Any valid keyword arguments -for :meth:`~pyramid.config.Configurator.add_view` can thus be used with the -:class:`~pyramid.view.action` decorator to further restrict when the view -will be called. - -One important difference is that a handler method can respond to an -``action`` name that is different from the method name by passing in a -``name`` argument. - -Example: - -.. code-block:: python - :linenos: - - from pyramid.view import action - - class Hello(object): - def __init__(self, request): - self.request = request - - @action(name='index', renderer='created.mak', request_method='POST') - def create(self): - return {} - - @action(renderer="view_all.mak", request_method='GET') - def index(self): - return {} - -This will register two views that require the ``action`` to be ``index``, -with the additional view predicate requiring a specific request method. - -It can be useful to decorate a single method multiple times with -:class:`~pyramid.view.action`. Each action decorator will register a new view -for the method. By specifying different names and renderers for each action, -the same view logic can be exposed and rendered differently on multiple URLs. - -Example: - -.. code-block:: python - :linenos: - - from pyramid.view import action - - class Hello(object): - def __init__(self, request): - self.request = request - - @action(name='home', renderer='home.mak') - @action(name='about', renderer='about.mak') - def show_template(self): - # prep some template vars - return {} - - # in the config - config.add_handler('hello', '/hello/{action}', handler=Hello) - -With this configuration, the url ``/hello/home`` will find a view -configuration that results in calling the ``show_template`` method, then -rendering the template with ``home.mak``, and the url ``/hello/about`` will -call the same method and render the ``about.mak`` template. - -.. index:: - single: resource interfaces - -.. _using_resource_interfaces: - -Using Resource Interfaces In View Configuration -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Instead of registering your views with a ``context`` that names a Python -resource *class*, you can optionally register a view callable with a -``context`` which is an :term:`interface`. An interface can be attached -arbitrarily to any resource object. View lookup treats context interfaces -specially, and therefore the identity of a resource can be divorced from that -of the class which implements it. As a result, associating a view with an -interface can provide more flexibility for sharing a single view between two -or more different implementations of a resource type. For example, if two -resource objects of different Python class types share the same interface, -you can use the same view configuration to specify both of them as a -``context``. - -In order to make use of interfaces in your application during view dispatch, -you must create an interface and mark up your resource classes or instances -with interface declarations that refer to this interface. - -To attach an interface to a resource *class*, you define the interface and -use the :func:`zope.interface.implements` function to associate the interface -with the class. - -.. code-block:: python - :linenos: - - from zope.interface import Interface - from zope.interface import implements - - class IHello(Interface): - """ A marker interface """ - - class Hello(object): - implements(IHello) - -To attach an interface to a resource *instance*, you define the interface and -use the :func:`zope.interface.alsoProvides` function to associate the -interface with the instance. This function mutates the instance in such a -way that the interface is attached to it. - -.. code-block:: python - :linenos: - - from zope.interface import Interface - from zope.interface import alsoProvides - - class IHello(Interface): - """ A marker interface """ - - class Hello(object): - pass - - def make_hello(): - hello = Hello() - alsoProvides(hello, IHello) - return hello - -Regardless of how you associate an interface, with a resource instance, or a -resource class, the resulting code to associate that interface with a view -callable is the same. Assuming the above code that defines an ``IHello`` -interface lives in the root of your application, and its module is named -"resources.py", the interface declaration below will associate the -``mypackage.views.hello_world`` view with resources that implement, or -provide, this interface. - -.. code-block:: python - :linenos: - - # config is an instance of pyramid.config.Configurator - - config.add_view('mypackage.views.hello_world', name='hello.html', - context='mypackage.resources.IHello') - -Any time a resource that is determined to be the :term:`context` provides -this interface, and a view named ``hello.html`` is looked up against it as -per the URL, the ``mypackage.views.hello_world`` view callable will be -invoked. - -Note, in cases where a view is registered against a resource class, and a -view is also registered against an interface that the resource class -implements, an ambiguity arises. Views registered for the resource class take -precedence over any views registered for any interface the resource class -implements. Thus, if one view configuration names a ``context`` of both the -class type of a resource, and another view configuration names a ``context`` -of interface implemented by the resource's class, and both view -configurations are otherwise identical, the view registered for the context's -class will "win". - -For more information about defining resources with interfaces for use within -view configuration, see :ref:`resources_which_implement_interfaces`. - -.. index:: - single: view security - pair: security; view - -.. _view_security_section: - -Configuring View Security -~~~~~~~~~~~~~~~~~~~~~~~~~ - -If an :term:`authorization policy` is active, any :term:`permission` attached -to a :term:`view configuration` found during view lookup will be verified. -This will ensure that the currently authenticated user possesses that -permission against the :term:`context` resource before the view function is -actually called. Here's an example of specifying a permission in a view -configuration using :meth:`pyramid.config.Configurator.add_view`: - -.. code-block:: python - :linenos: - - # config is an instance of pyramid.config.Configurator - - config.add_view('myproject.views.add_entry', name='add.html', - context='myproject.resources.IBlog', permission='add') - -When an :term:`authorization policy` is enabled, this view will be protected -with the ``add`` permission. The view will *not be called* if the user does -not possess the ``add`` permission relative to the current :term:`context`. -Instead the :term:`forbidden view` result will be returned to the client as -per :ref:`protecting_views`. - -.. index:: - single: view lookup - -.. _view_lookup: - -View Lookup and Invocation --------------------------- - -:term:`View lookup` is the :app:`Pyramid` subsystem responsible for finding -an invoking a :term:`view callable`. The view lookup subsystem is passed a -:term:`context` and a :term:`request` object. - -:term:`View configuration` information stored within in the -:term:`application registry` is compared against the context and request by -the view lookup subsystem in order to find the "best" view callable for the -set of circumstances implied by the context and request. - -Predicate attributes of view configuration can be thought of like -"narrowers". In general, the greater number of predicate attributes -possessed by a view's configuration, the more specific the circumstances need -to be before the registered view callable will be invoked. - -For any given request, a view with five predicates will always be found and -evaluated before a view with two, for example. All predicates must match for -the associated view to be called. - -This does not mean however, that :app:`Pyramid` "stops looking" when it finds -a view registration with predicates that don't match. If one set of view -predicates does not match, the "next most specific" view (if any) view is -consulted for predicates, and so on, until a view is found, or no view can be -matched up with the request. The first view with a set of predicates all of -which match the request environment will be invoked. - -If no view can be found with predicates which allow it to be matched up with -the request, :app:`Pyramid` will return an error to the user's browser, -representing a "not found" (404) page. See :ref:`changing_the_notfound_view` -for more information about changing the default notfound view. - -.. index:: - single: debugging not found errors - single: not found error (debugging) - -.. _debug_notfound_section: - -:exc:`NotFound` Errors -~~~~~~~~~~~~~~~~~~~~~~ - -It's useful to be able to debug :exc:`NotFound` error responses when they -occur unexpectedly due to an application registry misconfiguration. To debug -these errors, use the ``PYRAMID_DEBUG_NOTFOUND`` environment variable or the -``debug_notfound`` configuration file setting. Details of why a view was not -found will be printed to ``stderr``, and the browser representation of the -error will include the same information. See :ref:`environment_chapter` for -more information about how, and where to set these values. - -Further Information -------------------- - -The chapter entitled :ref:`renderers_chapter` explains how to create -functions (or instances/classes) which do not return a :term:`Response` -object, yet which still can be used as view callables. - -- cgit v1.2.3 From c7af1563eaf89694233ddd1ebf2c48b163146d01 Mon Sep 17 00:00:00 2001 From: Casey Duncan Date: Sun, 2 Jan 2011 23:50:39 -0700 Subject: remove unneeded liars remorse paragraph atop renderers --- docs/narr/renderers.rst | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/docs/narr/renderers.rst b/docs/narr/renderers.rst index 76e9562fa..d888e3376 100644 --- a/docs/narr/renderers.rst +++ b/docs/narr/renderers.rst @@ -3,18 +3,10 @@ Renderers ========= -In the :ref:`views_chapter` chapter, we said that a view callable must -return a :term:`Response` object. We lied. A :term:`renderer` is a service -that attempts to convert a non-Response return value of a function, class, or -instance that acts as a :term:`view callable` to a :term:`Response` object. - -Overview --------- - -A view needn't *always* return a Response object. If a view happens to -return something which does not implement the Pyramid Response interface, -:app:`Pyramid` will attempt to use a :term:`renderer` to construct a -response. For example: +A view needn't *always* return a :term:`Response` object. If a view +happens to return something which does not implement the Pyramid +Response interface, :app:`Pyramid` will attempt to use a +:term:`renderer` to construct a response. For example: .. code-block:: python :linenos: -- cgit v1.2.3 From db14e3eea2a56f67a2db34369283e7a15813d971 Mon Sep 17 00:00:00 2001 From: Casey Duncan Date: Sun, 2 Jan 2011 23:52:55 -0700 Subject: :term: not :mod: --- docs/narr/viewconfig.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/narr/viewconfig.rst b/docs/narr/viewconfig.rst index 39c0a0d1f..299342b74 100644 --- a/docs/narr/viewconfig.rst +++ b/docs/narr/viewconfig.rst @@ -76,7 +76,7 @@ All forms of view configuration accept the same general types of arguments. Many arguments supplied during view configuration are :term:`view predicate` arguments. View predicate arguments used during view configuration are used -to narrow the set of circumstances in which :mod:`view lookup` will find a +to narrow the set of circumstances in which :term:`view lookup` will find a particular view callable. In general, the fewer number of predicates which are supplied to a -- cgit v1.2.3 From 245316b0159f75269a1a7a4c5327c712e1475cc2 Mon Sep 17 00:00:00 2001 From: Casey Duncan Date: Sun, 2 Jan 2011 23:57:43 -0700 Subject: add intro paragraph to view config chapt --- docs/narr/viewconfig.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/narr/viewconfig.rst b/docs/narr/viewconfig.rst index 299342b74..372e520b4 100644 --- a/docs/narr/viewconfig.rst +++ b/docs/narr/viewconfig.rst @@ -8,6 +8,11 @@ View Configuration .. index:: single: view lookup +:term:`View configuration` controls how :term:`view lookup` operates in +your application. In earlier chapters, you have been exposed to a few +simple view configuration declarations without much explanation. In this +chapter we will explore the subject in detail. + .. _view_lookup: View Lookup and Invocation -- cgit v1.2.3 From 81bd32edadc54928fb84e1a35d7ccd4b34c4dc22 Mon Sep 17 00:00:00 2001 From: Casey Duncan Date: Mon, 3 Jan 2011 00:10:04 -0700 Subject: add placeholder form handling chapt, may move to cookbook --- docs/index.rst | 1 + docs/latexindex.rst | 1 + docs/narr/forms.rst | 121 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 123 insertions(+) create mode 100644 docs/narr/forms.rst diff --git a/docs/index.rst b/docs/index.rst index fe3cf2ce8..df0bbcd2f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -65,6 +65,7 @@ Narrative documentation in chapter form explaining how to use narr/router narr/threadlocals narr/zca + narr/forms Tutorials ========= diff --git a/docs/latexindex.rst b/docs/latexindex.rst index 608a49678..59ced2ac8 100644 --- a/docs/latexindex.rst +++ b/docs/latexindex.rst @@ -59,6 +59,7 @@ Narrative Documentation narr/startup narr/threadlocals narr/zca + narr/forms .. _tutorials: diff --git a/docs/narr/forms.rst b/docs/narr/forms.rst new file mode 100644 index 000000000..9ba862022 --- /dev/null +++ b/docs/narr/forms.rst @@ -0,0 +1,121 @@ +.. _forms_chapter: + +Form Handling +============= + +Handling Form Submissions in View Callables (Unicode and Character Set Issues) +------------------------------------------------------------------------------ + +Most web applications need to accept form submissions from web browsers and +various other clients. In :app:`Pyramid`, form submission handling logic is +always part of a :term:`view`. For a general overview of how to handle form +submission data using the :term:`WebOb` API, see :ref:`webob_chapter` and +`"Query and POST variables" within the WebOb documentation +`_. +:app:`Pyramid` defers to WebOb for its request and response implementations, +and handling form submission data is a property of the request +implementation. Understanding WebOb's request API is the key to +understanding how to process form submission data. + +There are some defaults that you need to be aware of when trying to handle +form submission data in a :app:`Pyramid` view. Having high-order (i.e., +non-ASCII) characters in data contained within form submissions is +exceedingly common, and the UTF-8 encoding is the most common encoding used +on the web for character data. Since Unicode values are much saner than +working with and storing bytestrings, :app:`Pyramid` configures the +:term:`WebOb` request machinery to attempt to decode form submission values +into Unicode from UTF-8 implicitly. This implicit decoding happens when view +code obtains form field values via the ``request.params``, ``request.GET``, +or ``request.POST`` APIs (see :ref:`request_module` for details about these +APIs). + +.. note:: + + Many people find the difference between Unicode and UTF-8 confusing. + Unicode is a standard for representing text that supports most of the + world's writing systems. However, there are many ways that Unicode data + can be encoded into bytes for transit and storage. UTF-8 is a specific + encoding for Unicode, that is backwards-compatible with ASCII. This makes + UTF-8 very convenient for encoding data where a large subset of that data + is ASCII characters, which is largely true on the web. UTF-8 is also the + standard character encoding for URLs. + +As an example, let's assume that the following form page is served up to a +browser client, and its ``action`` points at some :app:`Pyramid` view code: + +.. code-block:: xml + :linenos: + + + + + +
+
+ +
+
+ +
+ +
+ + +The ``myview`` view code in the :app:`Pyramid` application *must* expect that +the values returned by ``request.params`` will be of type ``unicode``, as +opposed to type ``str``. The following will work to accept a form post from +the above form: + +.. code-block:: python + :linenos: + + def myview(request): + firstname = request.params['firstname'] + lastname = request.params['lastname'] + +But the following ``myview`` view code *may not* work, as it tries to decode +already-decoded (``unicode``) values obtained from ``request.params``: + +.. code-block:: python + :linenos: + + def myview(request): + # the .decode('utf-8') will break below if there are any high-order + # characters in the firstname or lastname + firstname = request.params['firstname'].decode('utf-8') + lastname = request.params['lastname'].decode('utf-8') + +For implicit decoding to work reliably, you should ensure that every form you +render that posts to a :app:`Pyramid` view explicitly defines a charset +encoding of UTF-8. This can be done via a response that has a +``;charset=UTF-8`` in its ``Content-Type`` header; or, as in the form above, +with a ``meta http-equiv`` tag that implies that the charset is UTF-8 within +the HTML ``head`` of the page containing the form. This must be done +explicitly because all known browser clients assume that they should encode +form data in the same character set implied by ``Content-Type`` value of the +response containing the form when subsequently submitting that form. There is +no other generally accepted way to tell browser clients which charset to use +to encode form data. If you do not specify an encoding explicitly, the +browser client will choose to encode form data in its default character set +before submitting it, which may not be UTF-8 as the server expects. If a +request containing form data encoded in a non-UTF8 charset is handled by your +view code, eventually the request code accessed within your view will throw +an error when it can't decode some high-order character encoded in another +character set within form data, e.g., when ``request.params['somename']`` is +accessed. + +If you are using the :class:`pyramid.response.Response` class to generate a +response, or if you use the ``render_template_*`` templating APIs, the UTF-8 +charset is set automatically as the default via the ``Content-Type`` header. +If you return a ``Content-Type`` header without an explicit charset, a +request will add a ``;charset=utf-8`` trailer to the ``Content-Type`` header +value for you, for response content types that are textual +(e.g. ``text/html``, ``application/xml``, etc) as it is rendered. If you are +using your own response object, you will need to ensure you do this yourself. + +.. note:: Only the *values* of request params obtained via + ``request.params``, ``request.GET`` or ``request.POST`` are decoded + to Unicode objects implicitly in the :app:`Pyramid` default + configuration. The keys are still (byte) strings. + + -- cgit v1.2.3 From f7c36724a22cb951f2c6cc7cdd49602be6506873 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 3 Jan 2011 02:23:42 -0500 Subject: edit much ado chapter --- docs/narr/muchadoabouttraversal.rst | 477 +++++++++++++++++++----------------- docs/narr/resources.rst | 2 + 2 files changed, 249 insertions(+), 230 deletions(-) diff --git a/docs/narr/muchadoabouttraversal.rst b/docs/narr/muchadoabouttraversal.rst index cb1c6f73a..bc1b48462 100644 --- a/docs/narr/muchadoabouttraversal.rst +++ b/docs/narr/muchadoabouttraversal.rst @@ -9,285 +9,302 @@ Much Ado About Traversal `http://blog.nonsequitarian.org/2010/much-ado-about-traversal/ `_. -A lot of folks who have been using Pylons (and, therefore, Routes-based -URL matching) are being exposed for the first time, via :app:`Pyramid`, -to new ideas such as ":term:`traversal`" and ":term:`view lookup`" as a -way to route incoming HTTP requests to callable code. This has caused a -bit of consternation in some circles. Many think that traversal is hard -to understand. Others question its usefulness; URL matching has worked -for them so far, why should they even consider dealing with another -approach, one which doesn't fit their brain and which doesn't provide -any immediately obvious value? - -This chapter is an attempt to counter these opinions. Traversal and -view lookup *are* useful. There are some straightforward, real-world -use cases that are much more easily served by a traversal-based approach -than by a pattern-matching mechanism. Even if you haven't yet hit one -of these use cases yourself, understanding these new ideas is worth the -effort for any web developer so you know when you might want to use -them. Especially because (WARNING: Bold Assertion Ahead) these ideas -are *not* particularly hard to understand. In fact, :term:`traversal` -is a straightforward metaphor easily comprehended by anyone who's ever -used a run-of-the-mill file system with folders and files. +Traversal is an alternative to :term:`URL dispatch` which allows +:app:`Pyramid` applications to map URLs to code. .. note:: - Those of you who are already familiar with traversal and view lookup - conceptually, may want to skip directly to the - :ref:`traversal_chapter` chapter, which discusses the technical - details. + Ex-Zope users whom are already familiar with traversal and view lookup + conceptually may want to skip directly to the :ref:`traversal_chapter` + chapter, which discusses technical details. This chapter is mostly aimed + at people who have previous :term:`Pylons` experience or experience in + another framework which does not provide traversal, and need an + introduction to the "why" of traversal. + +Some folks who have been using Pylons and its Routes-based URL matching for a +long time are being exposed for the first time, via :app:`Pyramid`, to new +ideas such as ":term:`traversal`" and ":term:`view lookup`" as a way to route +incoming HTTP requests to callable code. Some of the same folks believe that +traversal is hard to understand. Others question its usefulness; URL +matching has worked for them so far, why should they even consider dealing +with another approach, one which doesn't fit their brain and which doesn't +provide any immediately obvious value? + +You can be assured that if you don't want to understand traversal, you don't +have to. You can happily build :app:`Pyramid` applications with only +:term:`URL dispatch`. However, there are some straightforward, real-world +use cases that are much more easily served by a traversal-based approach than +by a pattern-matching mechanism. Even if you haven't yet hit one of these +use cases yourself, understanding these new ideas is worth the effort for any +web developer so you know when you might want to use them. :term:`Traversal` +is actually a straightforward metaphor easily comprehended by anyone who's +ever used a run-of-the-mill file system with folders and files. URL Dispatch ------------ -Let's take a step back. The problem we're trying to solve is -simple. We have an HTTP request for a particular path that -has been routed to our web application. The requested path will -possibly invoke a specific callable function defined somewhere in our -app, or it may point to nothing in which case a 404 response should be -generated. What we're trying to do is figure out is which callable -function, if any, should be invoked for a given requested path. - -URL matching (or :term:`URL dispatch` in :app:`Pyramid` parlance) -approaches this problem by parsing the URL path and comparing the -results to a set of registered "patterns", defined by a set of regular -expressions, or some other URL path templating syntax. Each pattern is -mapped to a callable function somewhere; if the request path matches a -specific pattern, the associated function is called. If the request -path matches more than one pattern, some conflict resolution scheme is -used, usually a simple order precedence so that the first match will -take priority over any subsequent matches. If a request path doesn't -match any of the defined patterns, we've got a 404. - -Just in case it's not crystal clear, we'll give an example. Using -:app:`Pyramid`'s syntax, we might have a match pattern such as -``/{userid}/photos/{photoid}``, mapped to a ``photo_view()`` function -defined somewhere in our code. Then a request for a path such as +Let's step back and consider the problem we're trying to solve, which is +simple. An HTTP request for a particular path has been routed to our web +application. The requested path will possibly invoke a specific :term:`view +callable` function defined somewhere in our app. We're trying to determine +*which* callable function, if any, should be invoked for a given requested +URL. + +Many systems, including Pyramid, offer a simple solution. They offer the +concept of "URL matching". URL matching approaches this problem by parsing +the URL path and comparing the results to a set of registered "patterns", +defined by a set of regular expressions, or some other URL path templating +syntax. Each pattern is mapped to a callable function somewhere; if the +request path matches a specific pattern, the associated function is called. +If the request path matches more than one pattern, some conflict resolution +scheme is used, usually a simple order precedence so that the first match +will take priority over any subsequent matches. If a request path doesn't +match any of the defined patterns, :app:`Pyramid` a "404 Not Found" response +is returned. + +In Pyramid, we offer an implementation of URL mapping which we call +:term:`URL dispatch`. Using :app:`Pyramid` syntax, we might have a match +pattern such as ``/{userid}/photos/{photoid}``, mapped to a ``photo_view()`` +function defined somewhere in our code. Then a request for a path such as ``/joeschmoe/photos/photo1`` would be a match, and the ``photo_view()`` function would be invoked to handle the request. Similarly, ``/{userid}/blog/{year}/{month}/{postid}`` might map to a -``blog_post_view()`` function, so -``/joeschmoe/blog/2010/12/urlmatching`` would trigger the function, -which presumably would know how to find and render the ``urlmatching`` -blog post. +``blog_post_view()`` function, so ``/joeschmoe/blog/2010/12/urlmatching`` +would trigger the function, which presumably would know how to find and +render the ``urlmatching`` blog post. Historical Refresher -------------------- -Okay, we've got :term:`URL dispatch` out of the way, soon we'll dig in -to the supposedly "harder to understand" idea of traversal. Before we -do, though, let's take a trip down memory lane. If you've been doing -web work for a while, you may remember a time when we didn't have these -fancy web frameworks. Instead, we had general purpose HTTP servers that -primarily served files off of a file system. The "root" of a given site -mapped to a particular folder somewhere on the file system. Each -segment of the request path represented a subdirectory. The final path -segment would be either a directory or a file, and once the server found -the right file it would package it up in an HTTP response and send it -back to the client. So serving up a request for -``/joeschmoe/photos/photo1`` literally meant that there was a -``joeschmoe`` folder somewhere, which contained a ``photos`` folder, -which in turn contained a ``photo1`` file. If at any point along the -way we find that there is not a folder or file matching the requested -path, we return a 404 response. - -As the web grew more dynamic, however, a little bit of extra -complexity was added. Technologies such as CGI and HTTP server -modules were developed. Files were still looked up on the file -system, but if the file ended with (for example) ``.cgi`` or ``.php``, -or if it lived in a special folder, instead of simply sending the file -to the client the server would read the file, execute it using an -interpreter of some sort, and then send the output from this process -to the client as the final result. The server configuration specified -which files would trigger some dynamic code, with the default case -being to just serve the static file. +Now that we've refreshed our understanding of :term:`URL dispatch`, we'll dig +in to the idea of traversal. Before we do, though, let's take a trip down +memory lane. If you've been doing web work for a while, you may remember a +time when we didn't have fancy web frameworks like :term:`Pylons` and +:app:`Pyramid`. Instead, we had general purpose HTTP servers that primarily +served files off of a file system. The "root" of a given site mapped to a +particular folder somewhere on the file system. Each segment of the request +URL path represented a subdirectory. The final path segment would be either +a directory or a file, and once the server found the right file it would +package it up in an HTTP response and send it back to the client. So serving +up a request for ``/joeschmoe/photos/photo1`` literally meant that there was +a ``joeschmoe`` folder somewhere, which contained a ``photos`` folder, which +in turn contained a ``photo1`` file. If at any point along the way we find +that there is not a folder or file matching the requested path, we return a +404 response. + +As the web grew more dynamic, however, a little bit of extra complexity was +added. Technologies such as CGI and HTTP server modules were developed. +Files were still looked up on the file system, but if the file ended with +(for example) ``.cgi`` or ``.php``, or if it lived in a special folder, +instead of simply sending the file to the client the server would read the +file, execute it using an interpreter of some sort, and then send the output +from this process to the client as the final result. The server +configuration specified which files would trigger some dynamic code, with the +default case being to just serve the static file. Traversal (aka Resource Location) --------------------------------- -You with me so far? Good. Because if you understand how serving -files from a file system works, then you pretty much understand -traversal. And if you understand that a server might do something -different based on what type of file a given request specifies, then -you pretty much understand view lookup. - -Wait... what!?! - .. index:: single: traversal overview -The only difference between file system lookup and traversal is that a -file system lookup is stepping through nested directories and files in -a file system tree, while traversal is stepping through nested -dictionary-type objects in an object tree. Let's take a detailed look -at one of our example paths, so we can see what I mean: - -With ``/joeschmoe/photos/photo1``, we've got 4 segments: ``/``, -``joeschmoe/``, ``photos/`` and ``photo1``. With file system -lookup we have a root folder (``/``) containing a nested folder -(``joeschmoe``), which contains ANOTHER nested folder (``photos``), -which finally contains a JPG file ("photo1"). With traversal, we -have a dictionary-like root object. Asking for the ``joeschmoe`` key -gives us another dictionary-like object. Asking this in turn for the -``photos`` key gives us yet another mapping object, which finally -(hopefully) contains the resource that we're looking for within its -values, referenced by the ``photo1`` key. +Believe it or not, if you understand how serving files from a file system +works,you understand traversal. And if you understand that a server might do +something different based on what type of file a given request specifies, +then you understand view lookup. + +The major difference between file system lookup and traversal is that a file +system lookup steps through nested directories and files in a file system +tree, while traversal steps through nested dictionary-type objects in an +:term:`resource tree`. Let's take a detailed look at one of our example +paths, so we can see what I mean: + +The path ``/joeschmoe/photos/photo1``, has four segments: ``/``, +``joeschmoe``, ``photos`` and ``photo1``. With file system lookup we might +have a root folder (``/``) containing a nested folder (``joeschmoe``), which +contains another nested folder (``photos``), which finally contains a JPG +file ("photo1"). With traversal, we instead have a dictionary-like root +object. Asking for the ``joeschmoe`` key gives us another dictionary-like +object. Asking this in turn for the ``photos`` key gives us yet another +mapping object, which finally (hopefully) contains the resource that we're +looking for within its values, referenced by the ``photo1`` key. In pure Python terms, then, the traversal or "resource location" portion of satisfying the ``/joeschmoe/photos/photo1`` request -will look like this:: +will look something like this pseudocode:: get_root()['joeschmoe']['photos']['photo1'] -Where ``get_root()`` is some function that returns our root traversal -resource. If all of the specified keys exist, then the returned object -will be the resource that is being requested, analogous to the JPG file -that was retrieved in the file system example. If a :exc:`KeyError` is -generated anywhere along the way, we get a 404. (Well, this isn't -precisely true, as you'll see when we learn about view lookup below, but -the basic idea holds.) +``get_root()`` is some function that returns a root traversal +:term:`resource`. If all of the specified keys exist, then the returned +object will be the resource that is being requested, analogous to the JPG +file that was retrieved in the file system example. If a :exc:`KeyError` is +generated anywhere along the way, :app:`Pyramid` will return 404. (This +isn't precisely true, as you'll see when we learn about view lookup below, +but the basic idea holds.) -What is a "resource"? +What Is a "Resource"? --------------------- -Okay, okay... files on a file system I understand, you might say. But -what are these nested dictionary things? Where do these objects, these -"resources", live? What *are* they? - -Well, since :app:`Pyramid` is not a highly opinionated framework, there -is no restriction on how a resource is implemented; the developer can do -whatever they want. One common pattern is to persist all of the -resources, including the root, in a database. The root object stores -the ids of all of its subresources, and provides a ``__getitem__`` -implementation that fetches them. So ``get_root()`` fetches the unique -root object, while ``get_root()['joeschmoe']`` returns a different -object, also stored in the database, which in turn has its own -subresources and ``__getitem__`` implementation, etc. These resources -could be persisted in a relational database, one of the many "NoSQL" -solutions that are becoming popular these days, or anywhere else, it -doesn't matter. As long as the returned objects provide the -dictionary-like API (i.e. as long as they have an appropriately +"Files on a file system I understand", you might say. "But what are these +nested dictionary things? Where do these objects, these 'resources', live? +What *are* they?" + +Since :app:`Pyramid` is not a highly opinionated framework, it makes no +restriction on how a :term:`resource` is implemented; a developer can +implement them as he wishes. One common pattern used is to persist all of +the resources, including the root, in a database as a graph. The root object +is a dictionarylike object. Dictionarylike objects in Python supply a +``__getitem__`` method which is called when key lookup is done. Under the +hood, when ``adict`` is a dictionarylike object, Python translates +``adict['a']`` to ``adict.__getitem__('a')``. Try doing this in a Python +interpreter prompt if you don't believe us: + +.. code-block:: text + :linenos: + + Python 2.4.6 (#2, Apr 29 2010, 00:31:48) + [GCC 4.4.3] on linux2 + Type "help", "copyright", "credits" or "license" for more information. + >>> adict = {} + >>> adict['a'] = 1 + >>> adict['a'] + 1 + >>> adict.__getitem__('a') + 1 + + +The dictionarylike root object stores the ids of all of its subresources as +keys, and provides a ``__getitem__`` implementation that fetches them. So +``get_root()`` fetches the unique root object, while +``get_root()['joeschmoe']`` returns a different object, also stored in the +database, which in turn has its own subresources and ``__getitem__`` +implementation, etc. These resources might be persisted in a relational +database, one of the many "NoSQL" solutions that are becoming popular these +days, or anywhere else, it doesn't matter. As long as the returned objects +provide the dictionary-like API (i.e. as long as they have an appropriately implemented ``__getitem__`` method) then traversal will work. -In fact, you don't need a "database" at all. You could trivially -implement a set of objects with ``__getitem__`` methods that search -for files in specific directories, and thus precisely recreate the -older mechanism of having the URL path mapped directly to a folder -structure on the file system. Traversal is in fact a superset of file +In fact, you don't need a "database" at all. You could trivially implement a +set of objects with ``__getitem__`` methods that search for files in specific +directories, and thus precisely recreate the older mechanism of having the +URL path mapped directly to a folder structure on the file system. Or you +could use plain dictionaries too. Traversal is in fact a superset of file system lookup. +.. note:: See the chapter entitled :ref:`resources_chapter` for a more + technical overview of resources. + View Lookup ----------- -At this point we're nearly there. We've covered traversal, which is -the process by which a specific resource is retrieved according to a -specific URL path. But what is this "view lookup" business? - -View lookup comes from a simple realization, namely, that there is more -than one possible action that you might want to take for a single -resource. With our photo example, for instance, you might want to view -the photo in a page, but you might also want to provide a way for the -user to edit the photo and any associated metadata. We'll call the -former the ``view`` view, and the latter will be the ``edit`` view -(Original, I know.) :app:`Pyramid` has a centralized view registry -where named views can be associated with specific resource types. So in -our example, we'll assume that we've registered ``view`` and ``edit`` -views for photo objects, and that we've specified the ``view`` view as -the default, so that ``/joeschmoe/photos/photo1/view`` and -``/joeschmoe/photos/photo1`` are equivalent. The edit view would -sensibly be provided by a request for ``/joeschmoe/photos/photo1/edit``. - -Hopefully it's clear that the first portion of the edit view's URL path -is going to resolve to the same resource as the non-edit version, -specifically the resource returned by -``get_root()['joeschmoe']['photos']['photo1']``. But traveral ends -there; the ``photo1`` resource doesn't have an ``edit`` key. In fact, -it might not even be a dictionary-like object, in which case -``photo1['edit']`` would be meaningless. When :app:`Pyramid`'s resource -location has resolved to a *leaf* resource but the entire request path -has not yet been expended, the next path segment is treated as a view -name. The registry is then checked to see if a view of the given name -has been specified for a resource of the given type. If so, the view -callable is invoked, with the resource passed in as the ``context`` -object; if not, we 404. - -This is a slight simplification, but to summarize you can think of a -request for ``/joeschmoe/photos/photo1/edit`` as ultimately converted -into the following piece of Python:: +At this point we're nearly there. We've covered traversal, which is the +process by which a specific resource is retrieved according to a specific URL +path. But what is "view lookup"? + +The need for view lookup is simple: there is more than one possible action +that you might want to take after finding a :term:`resource`. With our photo +example, for instance, you might want to view the photo in a page, but you +might also want to provide a way for the user to edit the photo and any +associated metadata. We'll call the former the ``view`` view, and the latter +will be the ``edit`` view (Original, I know.) :app:`Pyramid` has a +centralized view registry where named views can be associated with specific +resource types. So in our example, we'll assume that we've registered +``view`` and ``edit`` views for photo objects, and that we've specified the +``view`` view as the default, so that ``/joeschmoe/photos/photo1/view`` and +``/joeschmoe/photos/photo1`` are equivalent. The edit view would sensibly be +provided by a request for ``/joeschmoe/photos/photo1/edit``. + +Hopefully it's clear that the first portion of the edit view's URL path is +going to resolve to the same resource as the non-edit version, specifically +the resource returned by ``get_root()['joeschmoe']['photos']['photo1']``. +But traveral ends there; the ``photo1`` resource doesn't have an ``edit`` +key. In fact, it might not even be a dictionary-like object, in which case +``photo1['edit']`` would be meaningless. When the :app:`Pyramid` resource +location has been resolved to a *leaf* resource, but the entire request path +has not yet been expended, the *very next* path segment is treated as a +:term:`view name`. The registry is then checked to see if a view of the +given name has been specified for a resource of the given type. If so, the +view callable is invoked, with the resource passed in as the related +``context`` object (also available as ``request.context``). If a view +callable could not be found, :app:`Pyramid` will return a "404 Not Found" +response. + +You might conceptualize a request for ``/joeschmoe/photos/photo1/edit`` as +ultimately converted into the following piece of Pythonic pseudocode:: context = get_root()['joeschmoe']['photos']['photo1'] - view_callable = registry.get_view(context, 'edit') - view_callable(context, request) + view_callable = get_view(context, 'edit') + request.context = context + view_callable(request) -That's not too hard to conceptualize, is it? +The ``get_root`` and ``get_view`` functions don't really exist. Internally, +:app:`Pyramid` does something more complicated. But the example above is a +reasonable approximation of the view lookup algorithm in pseudocode. Use Cases --------- -Let's come back around to look at why we even care. Yes, maybe -traversal and view lookup isn't mind-bending rocket science. But URL -matching is easier to explain, and it's good enough, right? +Why should we care about traversal? URL matching is easier to explain, and +it's good enough, right? -In some cases, yes, but certainly not in all cases. So far we've had -very structured URLs, where our paths have had a specific, small -number of pieces, like this:: +In some cases, yes, but certainly not in all cases. So far we've had very +structured URLs, where our paths have had a specific, small number of pieces, +like this:: /{userid}/{typename}/{objectid}[/{view_name}] In all of the examples thus far, we've hard coded the typename value, -assuming that we'd know at development time what names were going to -be used ("photos", "blog", etc.). But what if we don't know what -these names will be? Or, worse yet, what if we don't know *anything* -about the structure of the URLs inside a user's folder? We could be -writing a CMS where we want the end user to be able to arbitrarily add -content and other folders inside his folder. He might decide to nest -folders dozens of layers deep. How would you construct matching -patterns that could account for every possible combination of paths -that might develop? - -It may be possible, but it's tricky at best. And your matching -patterns are going to become quite complex very quickly as you try -to handle all of the edge cases. - -With traversal, however, it's straightforward. You want 20 layers of -nesting? No problem, :app:`Pyramid` will happily call ``__getitem__`` -as long as it needs to, until it runs out of path segments or until it -gets a :exc:`KeyError`. Each resource only needs to know how to fetch -its immediate children, the traversal algorithm takes care of the rest. - -The key advantage of traversal here is that the structure of the -resource tree can live in the database, and not in the code. It's -simple to let users modify the tree at runtime to set up their own -personalized directory structures. - -Another use case in which traversal shines is when there is a need to -support a context-dependent security policy. One example might be a -document management infrastructure for a large corporation, where -members of different departments have varying access levels to the -various other departments' files. Reasonably, even specific files -might need to be made available to specific individuals. Traversal -does well here because the idea of a resource context is baked right -into the code resolution and calling process. Resource objects can -store ACLs, which can be inherited and/or overridden by the -subresources. - -If each resource can thus generate a context-based ACL, then whenever -view code is attempting to perform a sensitive action, it can check -against that ACL to see whether the current user should be allowed to -perform the action. In this way you achieve so called "instance based" -or "row level" security which is considerably harder to model using a -traditional tabular approach. :app:`Pyramid` actively supports such a -scheme, and in fact if you register your views with guard permissions -and use an authorization policy, :app:`Pyramid` can check against a -resource's ACL when deciding whether or not the view itself is available -to the current user. - -In summary, there are entire classes of problems that are more easily -served by traversal and view lookup than by :term:`URL dispatch`. If -your problems aren't of this nature, great, stick with :term:`URL -dispatch`. But if you're using :app:`Pyramid` and you ever find that -you *do* need to support one of these use cases, you'll be glad you have -traversal in your toolkit. +assuming that we'd know at development time what names were going to be used +("photos", "blog", etc.). But what if we don't know what these names will +be? Or, worse yet, what if we don't know *anything* about the structure of +the URLs inside a user's folder? We could be writing a CMS where we want the +end user to be able to arbitrarily add content and other folders inside his +folder. He might decide to nest folders dozens of layers deep. How will you +construct matching patterns that could account for every possible combination +of paths that might develop? + +It's possible, but it will make for some somewhat ugly URLs. And the +matching patterns are going to become complex quickly as you try to handle +all of the edge cases. + +With traversal, however, it's straightforward. If you want 20 layers of +nesting, it's no problem. :app:`Pyramid` will happily call ``__getitem__`` +as many times as it needs to, until it runs out of path segments or until a +resource raises a :exc:`KeyError`. Each resource only needs to know how to +fetch its immediate children, the traversal algorithm takes care of the rest. + +One of the key advantages of traversal is that the structure of the resource +tree can live in the database, and not in the code. It's simple to let users +modify the tree at runtime to set up their own personalized directory +structures. + +Another use case in which traversal shines is when there is a need to support +a context-dependent security policy. One example might be a document +management infrastructure for a large corporation, where members of different +departments have varying access levels to the various other departments' +files. Reasonably, even specific files might need to be made available to +specific individuals. Traversal does well here if your resources actually +represent the data objects related to your documents, because the idea of a +resource authorization is baked right into the code resolution and calling +process. Resource objects can store ACLs, which can be inherited and/or +overridden by the subresources. + +If each resource can thus generate a context-based ACL, then whenever view +code is attempting to perform a sensitive action, it can check against that +ACL to see whether the current user should be allowed to perform the action. +In this way you achieve so called "instance based" or "row level" security +which is considerably harder to model using a traditional tabular approach. +:app:`Pyramid` actively supports such a scheme, and in fact if you register +your views with guard permissions and use an authorization policy, +:app:`Pyramid` can check against a resource's ACL when deciding whether or +not the view itself is available to the current user. + +In summary, there are entire classes of problems that are more easily served +by traversal and view lookup than by :term:`URL dispatch`. If your problems +don't require it, great: stick with :term:`URL dispatch`. But if you're +using :app:`Pyramid` and you ever find that you *do* need to support one of +these use cases, you'll be glad you have traversal in your toolkit. .. note:: It is even possible to mix and match :term:`traversal` with diff --git a/docs/narr/resources.rst b/docs/narr/resources.rst index b892cf3cd..cf13f8c8d 100644 --- a/docs/narr/resources.rst +++ b/docs/narr/resources.rst @@ -1,3 +1,5 @@ +.. _resources_chapter: + Resources ========= -- cgit v1.2.3 From 070a798934bd39223fad37471e8e2168d78909a2 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 3 Jan 2011 02:35:27 -0500 Subject: garden --- TODO.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/TODO.txt b/TODO.txt index 5c04bec6f..0b2bacce2 100644 --- a/TODO.txt +++ b/TODO.txt @@ -28,6 +28,9 @@ Must-Have (before 1.0) - API docs for ``pyramid.views.action``. +- Use a commit veto when configuring repoze.tm2 in paster templates for + non-1X, 2X, or 3X responses. + Should-Have ----------- -- cgit v1.2.3 From 5653d13e554433adf34fd81b2c6593a54e7c4ea1 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 3 Jan 2011 03:10:15 -0500 Subject: - Add a new API ``pyramid.url.current_route_url``, which computes a URL based on the "current" route (if any) and its matchdict values. --- CHANGES.txt | 3 ++ TODO.txt | 2 -- docs/api/url.rst | 2 ++ docs/glossary.rst | 5 +++ pyramid/tests/test_config.py | 9 ++---- pyramid/tests/test_url.py | 42 +++++++++++++++++++++++++ pyramid/url.py | 74 ++++++++++++++++++++++++++++---------------- 7 files changed, 103 insertions(+), 34 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 8d0e50722..520a73847 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -15,6 +15,9 @@ Bug Fixes Features -------- +- Add a new API ``pyramid.url.current_route_url``, which computes a URL based + on the "current" route (if any) and its matchdict values. + - ``config.add_view`` now accepts a ``decorator`` keyword argument, a callable which will decorate the view callable before it is added to the registry. diff --git a/TODO.txt b/TODO.txt index 0b2bacce2..15ce6ef05 100644 --- a/TODO.txt +++ b/TODO.txt @@ -34,8 +34,6 @@ Must-Have (before 1.0) Should-Have ----------- -- ``current_route_url`` function. https://gist.github.com/762842 - - Convert paster template and tutorial HTML templates to use ``request.static_url('{{package}}:static/foo.css')`` rather than ``${request.application_url}/static/foo.css``. diff --git a/docs/api/url.rst b/docs/api/url.rst index 1aa3082b7..01be76283 100644 --- a/docs/api/url.rst +++ b/docs/api/url.rst @@ -9,6 +9,8 @@ .. autofunction:: route_url + .. autofunction:: current_route_url + .. autofunction:: route_path .. autofunction:: static_url diff --git a/docs/glossary.rst b/docs/glossary.rst index ce2d77d8d..890f7e837 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -857,3 +857,8 @@ Glossary view argument and return value mapping. This is a plug point for extension builders, not normally used by "civilians". + matchdict + The dictionary attached to the :term:`request` object as + ``request.matchdict`` when a :term:`URL dispatch` route has been matched. + Its keys are names as identified within the route pattern; its values are + the values matched by each pattern name. diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py index 1632a4e5c..70e4df07e 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -3798,8 +3798,7 @@ class TestViewDeriver(unittest.TestCase): def wrapped(context, request): return 'OK' return wrapped - def view(context, request): - return 'NOTOK' + def view(context, request): return 'NOTOK' deriver = self._makeOne(view_mapper=mapper) result = deriver(view) self.failIf(result is view) @@ -3813,8 +3812,7 @@ class TestViewDeriver(unittest.TestCase): return 'OK' return superinner return inner - def view(context, request): - return 'NOTOK' + def view(context, request): return 'NOTOK' view.__view_mapper__ = mapper deriver = self._makeOne() result = deriver(view) @@ -3830,8 +3828,7 @@ class TestViewDeriver(unittest.TestCase): return superinner return inner self.config.set_view_mapper(mapper) - def view(context, request): - return 'NOTOK' + def view(context, request): return 'NOTOK' deriver = self._makeOne() result = deriver(view) self.failIf(result is view) diff --git a/pyramid/tests/test_url.py b/pyramid/tests/test_url.py index f11d36aca..a40727e9b 100644 --- a/pyramid/tests/test_url.py +++ b/pyramid/tests/test_url.py @@ -209,6 +209,47 @@ class TestRouteUrl(unittest.TestCase): self.assertEqual(result, 'http://example2.com/1/2/3/a') self.assertEqual(route.kw, {}) # shouldnt have anchor/query +class TestCurrentRouteUrl(unittest.TestCase): + def setUp(self): + cleanUp() + + def tearDown(self): + cleanUp() + + def _callFUT(self, *arg, **kw): + from pyramid.url import current_route_url + return current_route_url(*arg, **kw) + + def test_current_request_has_no_route(self): + request = _makeRequest() + self.assertRaises(ValueError, self._callFUT, request) + + def test_with_elements_query_and_anchor(self): + from pyramid.interfaces import IRoutesMapper + request = _makeRequest() + route = DummyRoute('/1/2/3') + mapper = DummyRoutesMapper(route=route) + request.matched_route = route + request.matchdict = {} + request.registry.registerUtility(mapper, IRoutesMapper) + result = self._callFUT(request, 'extra1', 'extra2', _query={'a':1}, + _anchor=u"foo") + self.assertEqual(result, + 'http://example.com:5432/1/2/3/extra1/extra2?a=1#foo') + + def test_with__route_name(self): + from pyramid.interfaces import IRoutesMapper + request = _makeRequest() + route = DummyRoute('/1/2/3') + mapper = DummyRoutesMapper(route=route) + request.matched_route = route + request.matchdict = {} + request.registry.registerUtility(mapper, IRoutesMapper) + result = self._callFUT(request, 'extra1', 'extra2', _query={'a':1}, + _anchor=u"foo", _route_name='bar') + self.assertEqual(result, + 'http://example.com:5432/1/2/3/extra1/extra2?a=1#foo') + class TestRoutePath(unittest.TestCase): def setUp(self): cleanUp() @@ -302,6 +343,7 @@ class DummyRoutesMapper: class DummyRoute: pregenerator = None + name = 'route' def __init__(self, result='/1/2/3'): self.result = result diff --git a/pyramid/url.py b/pyramid/url.py index cdeb0d6c8..3126ad26c 100644 --- a/pyramid/url.py +++ b/pyramid/url.py @@ -366,37 +366,59 @@ def static_url(path, request, **kw): return info.generate(path, request, **kw) -def current_route_url(request, *elements, **query): +def current_route_url(request, *elements, **kw): """Generates a fully qualified URL for a named :app:`Pyramid` - :term:`route configuration` based on current route. + :term:`route configuration` based on the 'current route'. - This function is to supplement :func:`pyramid.url.route_url`, it's purpose - is to allow for easy overriding parameters of current route. - - Example:: - If our url route is /foo/{page} - and current url is /foo/1 : - current_route_url(request, page='2') - Will return the string ``/foo/2``. + This function supplements :func:`pyramid.url.route_url`. It presents an + easy way to generate a URL for the 'current route' (defined as the route + which matched when the request was generated). + + The arguments to this function have the same meaning as those with the + same names passed to :func:`pyramid.url.route_url`. It also understands + an extra argument which ``route_url`` does not named ``_route_name``. + + The route name used to generate a URL is taken from either the + ``_route_name`` keyword argument or the name of the route which is + currently associated with the request if ``_route_name`` was not passed. + Keys and values from the current request :term:`matchdict` are combined + with the ``kw`` arguments to form a set of defaults named ``newkw``. + Then ``route_url(route_name, request, *elements, **newkw)`` is called, + returning a URL. + + Examples follow. + + If the 'current route' has the route pattern ``/foo/{page}`` and the + current url path is ``/foo/1`` , the matchdict will be ``{'page':'1'}``. + The result of ``current_route_url(request)`` in this situation will be + ``/foo/1``. + + If the 'current route' has the route pattern ``/foo/{page}`` and the + current current url path is ``/foo/1``, the matchdict will be + ``{'page':'1'}``. The result of ``current_route_url(request, page='2')`` + in this situation will be ``/foo/2``. - Alternatively we may have routes /foo/{action} and /foo/{action}/{page}, - on url /foo/view we may want to have a template macro/def that allows us - to output next/prev buttons that contain page numbers. The ability to - override route name helps with this task. - - Example:: - where ``foo_pages`` is route name for ``/foo/{action}/{page}`` - current_url(request, _route_name='foo_pages', page=paginator.page+1) - Will return the string like: ``/foo/view/5`` + Usage of the ``_route_name`` keyword argument: if our routing table + defines routes ``/foo/{action}`` named 'foo' and ``/foo/{action}/{page}`` + named ``fooaction``, and the current url pattern is ``/foo/view`` (which + has matched the ``/foo/{action}`` route), we may want to use the + matchdict args to generate a URL to the ``fooaction`` route. In this + scenario, ``current_url(request, _route_name='fooaction', page='5')`` + Will return string like: ``/foo/view/5``. """ - if '_route_name' in query: - route_name = query['_route_name'] + + if '_route_name' in kw: + route_name = kw.pop('_route_name') else: - route_name = getattr(request, 'matched_route', None) - route_name = getattr(route_name, 'name') - matchdict = {} - matchdict.update(getattr(request, 'matchdict', {}) or {}) - matchdict.update(query) - return route_url(route_name, request, *elements, **matchdict) + route = getattr(request, 'matched_route', None) + route_name = getattr(route, 'name', None) + if route_name is None: + raise ValueError('Current request matches no route') + + newkw = {} + newkw.update(request.matchdict) + newkw.update(kw) + return route_url(route_name, request, *elements, **newkw) @lru_cache(1000) def _join_elements(elements): -- cgit v1.2.3 From ed7a965859b54703efa05ea5ebd4ab255c498244 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 3 Jan 2011 03:26:49 -0500 Subject: remove stray slash which prevented _themes from being downloaded if they didnt exist --- docs/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Makefile b/docs/Makefile index 3d706d17e..1d032cf45 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -25,7 +25,7 @@ help: clean: -rm -rf _build/* -html: _themes/ +html: _themes mkdir -p _build/html _build/doctrees $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) _build/html @echo -- cgit v1.2.3 From ddc3bf7c97100198c51003ca56435431a61cb931 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 3 Jan 2011 15:47:25 -0500 Subject: fix rendering --- pyramid/config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyramid/config.py b/pyramid/config.py index 2a6319d00..ff4cb948a 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -435,8 +435,8 @@ class Configurator(object): immediately if ``autocommit`` is ``True``). .. note:: This method is typically only used by :app:`Pyramid` - framework extension authors, not by :app:`Pyramid` application - developers. + framework extension authors, not by :app:`Pyramid` application + developers. The ``discriminator`` uniquely identifies the action. It must be given, but it can be ``None``, to indicate that the action never -- cgit v1.2.3 From 74c3c37843d769b39ad93b326e5e154ef0fd5f46 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 3 Jan 2011 15:47:39 -0500 Subject: copyrights --- COPYRIGHT.txt | 2 +- docs/conf.py | 2 +- docs/copyright.rst | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/COPYRIGHT.txt b/COPYRIGHT.txt index aa870dc84..51c76455e 100644 --- a/COPYRIGHT.txt +++ b/COPYRIGHT.txt @@ -1,4 +1,4 @@ -Copyright (c) 2008-2010 Agendaless Consulting and Contributors. +Copyright (c) 2008-2011 Agendaless Consulting and Contributors. (http://www.agendaless.com), All Rights Reserved Portions (c) Django Project (http://djangoproject.com/). diff --git a/docs/conf.py b/docs/conf.py index 8c238cecd..d69936cf8 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -426,7 +426,7 @@ def setup(app): epub_title = 'The Pyramid Web Application Development Framework, Version 1.0' epub_author = 'Chris McDonough' epub_publisher = 'Agendaless Consulting' -epub_copyright = '2008-2010' +epub_copyright = '2008-2011' # The language of the text. It defaults to the language option # or en if the language is not set. diff --git a/docs/copyright.rst b/docs/copyright.rst index f5f12ad03..9ef093d0c 100644 --- a/docs/copyright.rst +++ b/docs/copyright.rst @@ -7,7 +7,7 @@ by Chris McDonough .. |copy| unicode:: U+000A9 .. COPYRIGHT SIGN -Copyright |copy| 2008-2010, Agendaless Consulting. +Copyright |copy| 2008-2011, Agendaless Consulting. .. ISBN-10: 0615345379 -- cgit v1.2.3 From 21013ca63c8b1ce1c7d5ff277dd08922110576f2 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 3 Jan 2011 22:27:30 -0500 Subject: done --- TODO.txt | 3 --- 1 file changed, 3 deletions(-) diff --git a/TODO.txt b/TODO.txt index 15ce6ef05..517b085f9 100644 --- a/TODO.txt +++ b/TODO.txt @@ -23,9 +23,6 @@ Must-Have (before 1.0) - ZCML directive for view mapper (or just use "utility", but it's not eager). -- Decide whether ``self.decorated_view(view)`` is in the right place in the - view deriver chain. - - API docs for ``pyramid.views.action``. - Use a commit veto when configuring repoze.tm2 in paster templates for -- cgit v1.2.3 From 4088fe16acad7c2d7bb79a2001b7a5cebb729420 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 3 Jan 2011 22:28:04 -0500 Subject: unused imports --- pyramid/paster.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyramid/paster.py b/pyramid/paster.py index 5efbae51c..5ac043c19 100644 --- a/pyramid/paster.py +++ b/pyramid/paster.py @@ -179,12 +179,10 @@ class PRoutesCommand(PCommand): print msg def command(self): - from pyramid.request import Request from pyramid.interfaces import IRouteRequest from pyramid.interfaces import IViewClassifier from pyramid.interfaces import IView from zope.interface import Interface - from zope.interface import providedBy config_file, section_name = self.args app = self.get_app(config_file, section_name, loadapp=self.loadapp[0]) registry = app.registry -- cgit v1.2.3 From fec457db29d81b399dbcded6026e6b830e55f04b Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 3 Jan 2011 23:59:29 -0500 Subject: fix docstring --- pyramid/testing.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pyramid/testing.py b/pyramid/testing.py index 15fc385cd..25aba33ce 100644 --- a/pyramid/testing.py +++ b/pyramid/testing.py @@ -433,7 +433,12 @@ class DummyResource: that will be attached to the resulting resource via :func:`zope.interface.alsoProvides`. Any extra keywords passed in the ``kw`` argumnent will be set as direct attributes of - the resource object.""" + the resource object. + + .. note:: For backwards compatibility purposes, this class can also + be imported as :class:`pyramid.testing.DummyModel`. + + """ self.__name__ = __name__ self.__parent__ = __parent__ if __provides__ is not None: @@ -521,8 +526,6 @@ class DummyRequest(object): Extra keyword arguments are assigned as attributes of the request itself. - .. note:: For backwards compatibility purposes, this class can also be - imported as :class:`pyramid.testing.DummyModel`. """ implements(IRequest) method = 'GET' -- cgit v1.2.3 From 6ed4127f19bf2fdf88151d1893fba047423537a8 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 4 Jan 2011 00:00:55 -0500 Subject: garden --- TODO.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/TODO.txt b/TODO.txt index 517b085f9..a84b26ff2 100644 --- a/TODO.txt +++ b/TODO.txt @@ -31,6 +31,8 @@ Must-Have (before 1.0) Should-Have ----------- +- Add a session attribute to DummyRequest. + - Convert paster template and tutorial HTML templates to use ``request.static_url('{{package}}:static/foo.css')`` rather than ``${request.application_url}/static/foo.css``. -- cgit v1.2.3 From 1822100256ce5077dbeb1886c833ef4a1f9538a3 Mon Sep 17 00:00:00 2001 From: Carsten Senger Date: Wed, 5 Jan 2011 15:41:07 +0100 Subject: add model_path_tuple for backward compatibility --- pyramid/traversal.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyramid/traversal.py b/pyramid/traversal.py index f3377b0d0..f32b43493 100644 --- a/pyramid/traversal.py +++ b/pyramid/traversal.py @@ -352,6 +352,9 @@ def resource_path_tuple(resource, *elements): """ return tuple(_resource_path_list(resource, *elements)) +model_path_tuple = resource_path_tuple # b/w compat + + def _resource_path_list(resource, *elements): """ Implementation detail shared by resource_path and resource_path_tuple""" path = [loc.__name__ or '' for loc in lineage(resource)] -- cgit v1.2.3 From 51b2e8cf88a6451e974103c0c2cb7bba29e58b7f Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 5 Jan 2011 14:38:41 -0500 Subject: - Instances of ``pyramid.testing.DummyRequest`` now have a ``session`` object, which is mostly a dictionary, but also implements the other session API methods for flash and CSRF. --- CHANGES.txt | 4 +++ pyramid/testing.py | 33 +++++++++++++++++ pyramid/tests/test_testing.py | 82 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 119 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 520a73847..e66675e64 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -66,6 +66,10 @@ Features should be the name of a previously registered renderer. Useful to provide "omnipresent" RPC using existing rendered views. +- Instances of ``pyramid.testing.DummyRequest`` now have a ``session`` + object, which is mostly a dictionary, but also implements the other session + API methods for flash and CSRF. + Backwards Incompatibilities --------------------------- diff --git a/pyramid/testing.py b/pyramid/testing.py index 25aba33ce..96388b709 100644 --- a/pyramid/testing.py +++ b/pyramid/testing.py @@ -11,6 +11,7 @@ from pyramid.interfaces import IRequest from pyramid.interfaces import ISecuredView from pyramid.interfaces import IView from pyramid.interfaces import IViewClassifier +from pyramid.interfaces import ISession from pyramid.config import Configurator from pyramid.exceptions import Forbidden @@ -511,6 +512,37 @@ class DummyResource: DummyModel = DummyResource # b/w compat (forever) +class DummySession(dict): + implements(ISession) + created = None + new = True + def changed(self): + pass + + def invalidate(self): + self.clear() + + def flash(self, msg, queue='', allow_duplicate=True): + storage = self.setdefault('_f_' + queue, []) + if allow_duplicate or (msg not in storage): + storage.append(msg) + + def pop_flash(self, queue=''): + storage = self.pop('_f_' + queue, []) + return storage + + def peek_flash(self, queue=''): + storage = self.get('_f_' + queue, []) + return storage + + def new_csrf_token(self): + token = 'csrft' + self['_csrft_'] = token + return token + + def get_csrf_token(self): + return self.get('_csrft_', None) + class DummyRequest(object): """ A dummy request object (imitates a :term:`request` object). @@ -572,6 +604,7 @@ class DummyRequest(object): self.virtual_root = None self.marshalled = params # repoze.monty self.registry = get_current_registry() + self.session = DummySession() self.__dict__.update(kw) def add_response_callback(self, callback): diff --git a/pyramid/tests/test_testing.py b/pyramid/tests/test_testing.py index d2ed957f2..eaaad6aef 100644 --- a/pyramid/tests/test_testing.py +++ b/pyramid/tests/test_testing.py @@ -413,6 +413,7 @@ class TestDummyRequest(unittest.TestCase): def test_defaults(self): from pyramid.threadlocal import get_current_registry + from pyramid.testing import DummySession request = self._makeOne() self.assertEqual(request.method, 'GET') self.assertEqual(request.application_url, 'http://example.com') @@ -438,6 +439,7 @@ class TestDummyRequest(unittest.TestCase): self.assertEqual(request.virtual_root, None) self.assertEqual(request.virtual_root_path, ()) self.assertEqual(request.registry, get_current_registry()) + self.assertEqual(request.session.__class__, DummySession) def test_params_explicit(self): request = self._makeOne(params = {'foo':'bar'}) @@ -723,6 +725,86 @@ class Test_skip_on(unittest.TestCase): decorated = self._callFUT('ok')(foo) self.assertEqual(decorated(), True) +class TestDummySession(unittest.TestCase): + def _makeOne(self): + from pyramid.testing import DummySession + return DummySession() + + def test_instance_conforms(self): + from zope.interface.verify import verifyObject + from pyramid.interfaces import ISession + session = self._makeOne() + verifyObject(ISession, session) + + def test_changed(self): + session = self._makeOne() + self.assertEqual(session.changed(), None) + + def test_invalidate(self): + session = self._makeOne() + session['a'] = 1 + self.assertEqual(session.invalidate(), None) + self.failIf('a' in session) + + def test_flash_default(self): + session = self._makeOne() + session.flash('msg1') + session.flash('msg2') + self.assertEqual(session['_f_'], ['msg1', 'msg2']) + + def test_flash_mixed(self): + session = self._makeOne() + session.flash('warn1', 'warn') + session.flash('warn2', 'warn') + session.flash('err1', 'error') + session.flash('err2', 'error') + self.assertEqual(session['_f_warn'], ['warn1', 'warn2']) + + def test_pop_flash_default_queue(self): + session = self._makeOne() + queue = ['one', 'two'] + session['_f_'] = queue + result = session.pop_flash() + self.assertEqual(result, queue) + self.assertEqual(session.get('_f_'), None) + + def test_pop_flash_nodefault_queue(self): + session = self._makeOne() + queue = ['one', 'two'] + session['_f_error'] = queue + result = session.pop_flash('error') + self.assertEqual(result, queue) + self.assertEqual(session.get('_f_error'), None) + + def test_peek_flash_default_queue(self): + session = self._makeOne() + queue = ['one', 'two'] + session['_f_'] = queue + result = session.peek_flash() + self.assertEqual(result, queue) + self.assertEqual(session.get('_f_'), queue) + + def test_peek_flash_nodefault_queue(self): + session = self._makeOne() + queue = ['one', 'two'] + session['_f_error'] = queue + result = session.peek_flash('error') + self.assertEqual(result, queue) + self.assertEqual(session.get('_f_error'), queue) + + def test_new_csrf_token(self): + session = self._makeOne() + token = session.new_csrf_token() + self.assertEqual(token, session['_csrft_']) + + def test_get_csrf_token(self): + session = self._makeOne() + session['_csrft_'] = 'token' + token = session.get_csrf_token() + self.assertEqual(token, 'token') + self.failUnless('_csrft_' in session) + + from zope.interface import Interface from zope.interface import implements -- cgit v1.2.3 From 94e93a8d784a89169529ca279b4421cc8c63b92c Mon Sep 17 00:00:00 2001 From: Blaise Laflamme Date: Wed, 5 Jan 2011 20:22:19 -0500 Subject: Updated all paster templates to use the new theme, use request.static_url('{{package}}:static/foo.css') instead of ${request.application_url}/static/foo.css for static files --- .../alchemy/+package+/static/footerbg.png | Bin 0 -> 333 bytes .../alchemy/+package+/static/headerbg.png | Bin 0 -> 203 bytes .../alchemy/+package+/static/ie6.css | 8 ++ .../alchemy/+package+/static/logo.png | Bin 6641 -> 0 bytes .../alchemy/+package+/static/middlebg.png | Bin 0 -> 2797 bytes .../alchemy/+package+/static/pylons.css | 47 +++++------- .../alchemy/+package+/static/pyramid.png | Bin 0 -> 33055 bytes .../alchemy/+package+/static/transparent.gif | Bin 0 -> 49 bytes .../alchemy/+package+/templates/model.pt_tmpl | 82 +++++++++++++++++++++ .../alchemy/+package+/templates/root.pt_tmpl | 80 ++++++++++++++++++++ .../pylons_basic/+package+/static/footerbg.png | Bin 0 -> 333 bytes .../pylons_basic/+package+/static/headerbg.png | Bin 0 -> 203 bytes .../pylons_basic/+package+/static/ie6.css | 8 ++ .../pylons_basic/+package+/static/logo.png | Bin 6641 -> 0 bytes .../pylons_basic/+package+/static/middlebg.png | Bin 0 -> 2797 bytes .../pylons_basic/+package+/static/pylons.css | 47 +++++------- .../pylons_basic/+package+/static/pyramid.png | Bin 0 -> 33055 bytes .../pylons_basic/+package+/static/transparent.gif | Bin 0 -> 49 bytes .../+package+/templates/mytemplate.mako | 79 -------------------- .../+package+/templates/mytemplate.mako_tmpl | 76 +++++++++++++++++++ .../pylons_minimal/+package+/static/footerbg.png | Bin 0 -> 333 bytes .../pylons_minimal/+package+/static/headerbg.png | Bin 0 -> 203 bytes .../pylons_minimal/+package+/static/ie6.css | 8 ++ .../pylons_minimal/+package+/static/logo.png | Bin 6641 -> 0 bytes .../pylons_minimal/+package+/static/middlebg.png | Bin 0 -> 2797 bytes .../pylons_minimal/+package+/static/pylons.css | 47 +++++------- .../pylons_minimal/+package+/static/pyramid.png | Bin 0 -> 33055 bytes .../+package+/static/transparent.gif | Bin 0 -> 49 bytes .../+package+/templates/mytemplate.mako_tmpl | 76 +++++++++++++++++++ .../pylons_sqla/+package+/static/footerbg.png | Bin 0 -> 333 bytes .../pylons_sqla/+package+/static/headerbg.png | Bin 0 -> 203 bytes .../pylons_sqla/+package+/static/ie6.css | 8 ++ .../pylons_sqla/+package+/static/logo.png | Bin 6641 -> 0 bytes .../pylons_sqla/+package+/static/middlebg.png | Bin 0 -> 2797 bytes .../pylons_sqla/+package+/static/pylons.css | 47 +++++------- .../pylons_sqla/+package+/static/pyramid.png | Bin 0 -> 33055 bytes .../pylons_sqla/+package+/static/transparent.gif | Bin 0 -> 49 bytes .../+package+/templates/mytemplate.mako_tmpl | 78 ++++++++++++++++++++ .../routesalchemy/+package+/static/footerbg.png | Bin 0 -> 333 bytes .../routesalchemy/+package+/static/headerbg.png | Bin 0 -> 203 bytes .../routesalchemy/+package+/static/ie6.css | 8 ++ .../routesalchemy/+package+/static/logo.png | Bin 6641 -> 0 bytes .../routesalchemy/+package+/static/middlebg.png | Bin 0 -> 2797 bytes .../routesalchemy/+package+/static/pylons.css | 47 +++++------- .../routesalchemy/+package+/static/pyramid.png | Bin 0 -> 33055 bytes .../routesalchemy/+package+/static/transparent.gif | Bin 0 -> 49 bytes .../+package+/templates/mytemplate.pt | 79 -------------------- .../+package+/templates/mytemplate.pt_tmpl | 76 +++++++++++++++++++ .../starter/+package+/templates/mytemplate.pt_tmpl | 76 +++++++++++++++++++ .../starter_zcml/+package+/static/footerbg.png | Bin 0 -> 333 bytes .../starter_zcml/+package+/static/headerbg.png | Bin 0 -> 203 bytes .../starter_zcml/+package+/static/ie6.css | 8 ++ .../starter_zcml/+package+/static/logo.png | Bin 6641 -> 0 bytes .../starter_zcml/+package+/static/middlebg.png | Bin 0 -> 2797 bytes .../starter_zcml/+package+/static/pylons.css | 47 +++++------- .../starter_zcml/+package+/static/pyramid.png | Bin 0 -> 33055 bytes .../starter_zcml/+package+/static/transparent.gif | Bin 0 -> 49 bytes .../starter_zcml/+package+/templates/mytemplate.pt | 79 -------------------- .../+package+/templates/mytemplate.pt_tmpl | 76 +++++++++++++++++++ .../zodb/+package+/static/footerbg.png | Bin 0 -> 333 bytes .../zodb/+package+/static/headerbg.png | Bin 0 -> 203 bytes .../paster_templates/zodb/+package+/static/ie6.css | 8 ++ .../zodb/+package+/static/logo.png | Bin 6641 -> 0 bytes .../zodb/+package+/static/middlebg.png | Bin 0 -> 2797 bytes .../zodb/+package+/static/pylons.css | 47 +++++------- .../zodb/+package+/static/pyramid.png | Bin 0 -> 33055 bytes .../zodb/+package+/static/transparent.gif | Bin 0 -> 49 bytes .../zodb/+package+/templates/mytemplate.pt | 79 -------------------- .../zodb/+package+/templates/mytemplate.pt_tmpl | 76 +++++++++++++++++++ 69 files changed, 885 insertions(+), 512 deletions(-) create mode 100644 pyramid/paster_templates/alchemy/+package+/static/footerbg.png create mode 100644 pyramid/paster_templates/alchemy/+package+/static/headerbg.png create mode 100644 pyramid/paster_templates/alchemy/+package+/static/ie6.css delete mode 100644 pyramid/paster_templates/alchemy/+package+/static/logo.png create mode 100644 pyramid/paster_templates/alchemy/+package+/static/middlebg.png create mode 100644 pyramid/paster_templates/alchemy/+package+/static/pyramid.png create mode 100644 pyramid/paster_templates/alchemy/+package+/static/transparent.gif create mode 100644 pyramid/paster_templates/alchemy/+package+/templates/model.pt_tmpl create mode 100644 pyramid/paster_templates/alchemy/+package+/templates/root.pt_tmpl create mode 100644 pyramid/paster_templates/pylons_basic/+package+/static/footerbg.png create mode 100644 pyramid/paster_templates/pylons_basic/+package+/static/headerbg.png create mode 100644 pyramid/paster_templates/pylons_basic/+package+/static/ie6.css delete mode 100644 pyramid/paster_templates/pylons_basic/+package+/static/logo.png create mode 100644 pyramid/paster_templates/pylons_basic/+package+/static/middlebg.png create mode 100644 pyramid/paster_templates/pylons_basic/+package+/static/pyramid.png create mode 100644 pyramid/paster_templates/pylons_basic/+package+/static/transparent.gif delete mode 100644 pyramid/paster_templates/pylons_basic/+package+/templates/mytemplate.mako create mode 100644 pyramid/paster_templates/pylons_basic/+package+/templates/mytemplate.mako_tmpl create mode 100644 pyramid/paster_templates/pylons_minimal/+package+/static/footerbg.png create mode 100644 pyramid/paster_templates/pylons_minimal/+package+/static/headerbg.png create mode 100644 pyramid/paster_templates/pylons_minimal/+package+/static/ie6.css delete mode 100644 pyramid/paster_templates/pylons_minimal/+package+/static/logo.png create mode 100644 pyramid/paster_templates/pylons_minimal/+package+/static/middlebg.png create mode 100644 pyramid/paster_templates/pylons_minimal/+package+/static/pyramid.png create mode 100644 pyramid/paster_templates/pylons_minimal/+package+/static/transparent.gif create mode 100644 pyramid/paster_templates/pylons_minimal/+package+/templates/mytemplate.mako_tmpl create mode 100644 pyramid/paster_templates/pylons_sqla/+package+/static/footerbg.png create mode 100644 pyramid/paster_templates/pylons_sqla/+package+/static/headerbg.png create mode 100644 pyramid/paster_templates/pylons_sqla/+package+/static/ie6.css delete mode 100644 pyramid/paster_templates/pylons_sqla/+package+/static/logo.png create mode 100644 pyramid/paster_templates/pylons_sqla/+package+/static/middlebg.png create mode 100644 pyramid/paster_templates/pylons_sqla/+package+/static/pyramid.png create mode 100644 pyramid/paster_templates/pylons_sqla/+package+/static/transparent.gif create mode 100644 pyramid/paster_templates/pylons_sqla/+package+/templates/mytemplate.mako_tmpl create mode 100644 pyramid/paster_templates/routesalchemy/+package+/static/footerbg.png create mode 100644 pyramid/paster_templates/routesalchemy/+package+/static/headerbg.png create mode 100644 pyramid/paster_templates/routesalchemy/+package+/static/ie6.css delete mode 100644 pyramid/paster_templates/routesalchemy/+package+/static/logo.png create mode 100644 pyramid/paster_templates/routesalchemy/+package+/static/middlebg.png create mode 100644 pyramid/paster_templates/routesalchemy/+package+/static/pyramid.png create mode 100644 pyramid/paster_templates/routesalchemy/+package+/static/transparent.gif delete mode 100644 pyramid/paster_templates/routesalchemy/+package+/templates/mytemplate.pt create mode 100644 pyramid/paster_templates/routesalchemy/+package+/templates/mytemplate.pt_tmpl create mode 100644 pyramid/paster_templates/starter/+package+/templates/mytemplate.pt_tmpl create mode 100644 pyramid/paster_templates/starter_zcml/+package+/static/footerbg.png create mode 100644 pyramid/paster_templates/starter_zcml/+package+/static/headerbg.png create mode 100644 pyramid/paster_templates/starter_zcml/+package+/static/ie6.css delete mode 100644 pyramid/paster_templates/starter_zcml/+package+/static/logo.png create mode 100644 pyramid/paster_templates/starter_zcml/+package+/static/middlebg.png create mode 100644 pyramid/paster_templates/starter_zcml/+package+/static/pyramid.png create mode 100644 pyramid/paster_templates/starter_zcml/+package+/static/transparent.gif delete mode 100644 pyramid/paster_templates/starter_zcml/+package+/templates/mytemplate.pt create mode 100644 pyramid/paster_templates/starter_zcml/+package+/templates/mytemplate.pt_tmpl create mode 100644 pyramid/paster_templates/zodb/+package+/static/footerbg.png create mode 100644 pyramid/paster_templates/zodb/+package+/static/headerbg.png create mode 100644 pyramid/paster_templates/zodb/+package+/static/ie6.css delete mode 100644 pyramid/paster_templates/zodb/+package+/static/logo.png create mode 100644 pyramid/paster_templates/zodb/+package+/static/middlebg.png create mode 100644 pyramid/paster_templates/zodb/+package+/static/pyramid.png create mode 100644 pyramid/paster_templates/zodb/+package+/static/transparent.gif delete mode 100644 pyramid/paster_templates/zodb/+package+/templates/mytemplate.pt create mode 100644 pyramid/paster_templates/zodb/+package+/templates/mytemplate.pt_tmpl diff --git a/pyramid/paster_templates/alchemy/+package+/static/footerbg.png b/pyramid/paster_templates/alchemy/+package+/static/footerbg.png new file mode 100644 index 000000000..1fbc873da Binary files /dev/null and b/pyramid/paster_templates/alchemy/+package+/static/footerbg.png differ diff --git a/pyramid/paster_templates/alchemy/+package+/static/headerbg.png b/pyramid/paster_templates/alchemy/+package+/static/headerbg.png new file mode 100644 index 000000000..0596f2020 Binary files /dev/null and b/pyramid/paster_templates/alchemy/+package+/static/headerbg.png differ diff --git a/pyramid/paster_templates/alchemy/+package+/static/ie6.css b/pyramid/paster_templates/alchemy/+package+/static/ie6.css new file mode 100644 index 000000000..b7c8493d8 --- /dev/null +++ b/pyramid/paster_templates/alchemy/+package+/static/ie6.css @@ -0,0 +1,8 @@ +* html img, +* html .png{position:relative;behavior:expression((this.runtimeStyle.behavior="none")&&(this.pngSet?this.pngSet=true:(this.nodeName == "IMG" && this.src.toLowerCase().indexOf('.png')>-1?(this.runtimeStyle.backgroundImage = "none", +this.runtimeStyle.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + this.src + "',sizingMethod='image')", +this.src = "static/transparent.gif"):(this.origBg = this.origBg? this.origBg :this.currentStyle.backgroundImage.toString().replace('url("','').replace('")',''), +this.runtimeStyle.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + this.origBg + "',sizingMethod='crop')", +this.runtimeStyle.backgroundImage = "none")),this.pngSet=true) +);} +#wrap{display:table;height:100%} diff --git a/pyramid/paster_templates/alchemy/+package+/static/logo.png b/pyramid/paster_templates/alchemy/+package+/static/logo.png deleted file mode 100644 index 88f5d9865..000000000 Binary files a/pyramid/paster_templates/alchemy/+package+/static/logo.png and /dev/null differ diff --git a/pyramid/paster_templates/alchemy/+package+/static/middlebg.png b/pyramid/paster_templates/alchemy/+package+/static/middlebg.png new file mode 100644 index 000000000..2369cfb7d Binary files /dev/null and b/pyramid/paster_templates/alchemy/+package+/static/middlebg.png differ diff --git a/pyramid/paster_templates/alchemy/+package+/static/pylons.css b/pyramid/paster_templates/alchemy/+package+/static/pylons.css index c153be07f..33b21ac1a 100644 --- a/pyramid/paster_templates/alchemy/+package+/static/pylons.css +++ b/pyramid/paster_templates/alchemy/+package+/static/pylons.css @@ -4,34 +4,23 @@ body{line-height:1;} ol,ul{list-style:none;} blockquote,q{quotes:none;} blockquote:before,blockquote:after,q:before,q:after{content:'';content:none;} -/* remember to define focus styles! */ :focus{outline:0;} -/* remember to highlight inserts somehow! */ ins{text-decoration:none;} del{text-decoration:line-through;} -/* tables still need 'cellspacing="0"' in the markup */ table{border-collapse:collapse;border-spacing:0;} -/* restyling */ sub{vertical-align:sub;font-size:smaller;line-height:normal;} sup{vertical-align:super;font-size:smaller;line-height:normal;} -/* lists */ ul,menu,dir{display:block;list-style-type:disc;margin:1em 0;padding-left:40px;} ol{display:block;list-style-type:decimal-leading-zero;margin:1em 0;padding-left:40px;} li{display:list-item;} -/* nested lists have no top/bottom margins */ ul ul,ul ol,ul dir,ul menu,ul dl,ol ul,ol ol,ol dir,ol menu,ol dl,dir ul,dir ol,dir dir,dir menu,dir dl,menu ul,menu ol,menu dir,menu menu,menu dl,dl ul,dl ol,dl dir,dl menu,dl dl{margin-top:0;margin-bottom:0;} -/* 2 deep unordered lists use a circle */ ol ul,ul ul,menu ul,dir ul,ol menu,ul menu,menu menu,dir menu,ol dir,ul dir,menu dir,dir dir{list-style-type:circle;} -/* 3 deep (or more) unordered lists use a square */ ol ol ul,ol ul ul,ol menu ul,ol dir ul,ol ol menu,ol ul menu,ol menu menu,ol dir menu,ol ol dir,ol ul dir,ol menu dir,ol dir dir,ul ol ul,ul ul ul,ul menu ul,ul dir ul,ul ol menu,ul ul menu,ul menu menu,ul dir menu,ul ol dir,ul ul dir,ul menu dir,ul dir dir,menu ol ul,menu ul ul,menu menu ul,menu dir ul,menu ol menu,menu ul menu,menu menu menu,menu dir menu,menu ol dir,menu ul dir,menu menu dir,menu dir dir,dir ol ul,dir ul ul,dir menu ul,dir dir ul,dir ol menu,dir ul menu,dir menu menu,dir dir menu,dir ol dir,dir ul dir,dir menu dir,dir dir dir{list-style-type:square;} .hidden{display:none;} p{line-height:1.5em;} -h1{font-size:1.75em;/* 28px */ -line-height:1.7em;font-family:helvetica,verdana;} -h2{font-size:1.5em;/* 24px */ -line-height:1.7em;font-family:helvetica,verdana;} -h3{font-size:1.25em;/* 20px */ -line-height:1.7em;font-family:helvetica,verdana;} +h1{font-size:1.75em;line-height:1.7em;font-family:helvetica,verdana;} +h2{font-size:1.5em;line-height:1.7em;font-family:helvetica,verdana;} +h3{font-size:1.25em;line-height:1.7em;font-family:helvetica,verdana;} h4{font-size:1em;line-height:1.7em;font-family:helvetica,verdana;} html,body{width:100%;height:100%;} body{margin:0;padding:0;background-color:#ffffff;position:relative;font:16px/24px "Nobile","Lucida Grande",Lucida,Verdana,sans-serif;} @@ -42,23 +31,25 @@ body h2, body h3, body h4, body h5, -body h6{font-family:"Nobile","Lucida Grande",Lucida,Verdana,sans-serif;font-weight:normal;color:#144fb2;font-style:normal;} -#wrap {min-height: 100%;} -#header,#footer{width:100%;color:#ffffff;height:40px;position:absolute;text-align:center;line-height:40px;overflow:hidden;font-size:12px;} -#header{background-color:#e88f00;top:0;font-size:14px;} -#footer{background-color:#000000;bottom:0;position: relative;margin-top:-40px;clear:both;} -.header,.footer{width:700px;margin-right:auto;margin-left:auto;} +body h6{font-family:"Neuton","Lucida Grande",Lucida,Verdana,sans-serif;font-weight:normal;color:#373839;font-style:normal;} +#wrap{min-height:100%;} +#header,#footer{width:100%;color:#ffffff;height:40px;position:absolute;text-align:center;line-height:40px;overflow:hidden;font-size:12px;vertical-align:middle;} +#header{background:#000000;top:0;font-size:14px;} +#footer{bottom:0;background:#000000 url(footerbg.png) repeat-x 0 top;position:relative;margin-top:-40px;clear:both;} +.header,.footer{width:750px;margin-right:auto;margin-left:auto;} .wrapper{width:100%} #top,#bottom{width:100%;} -#top{color:#888;background-color:#eee;height:300px;border-bottom:2px solid #ddd;} -#bottom{color:#222;background-color:#ffffff;overflow:hidden;padding-bottom:80px;} -.top,.bottom{width:700px;margin-right:auto;margin-left:auto;} -.top{padding-top:100px;} +#top{color:#000000;height:230px; +background:#ffffff url(headerbg.png) repeat-x 0 top;position:relative;} +#bottom{color:#222;background-color:#ffffff;} +.top,.middle,.bottom{width:750px;margin-right:auto;margin-left:auto;} +.top{padding-top:40px;} +#middle{width:100%;height:100px;background:url(middlebg.png) repeat-x;border-top:2px solid #ffffff;border-bottom:2px solid #b2b2b2;} .app-welcome{margin-top:25px;} .app-name{color:#000000;font-weight:bold;} .bottom{padding-top:50px;} -#left{width:325px;float:left;padding-right:25px;} -#right{width:325px;float:right;padding-left:25px;} +#left{width:350px;float:left;padding-right:25px;} +#right{width:350px;float:right;padding-left:25px;} .align-left{text-align:left;} .align-right{text-align:right;} .align-center{text-align:center;} @@ -67,7 +58,7 @@ ul.links li{list-style-type:none;font-size:14px;} form{border-style:none;} fieldset{border-style:none;} input{color:#222;border:1px solid #ccc;font-family:sans-serif;font-size:12px;line-height:16px;} -input[type=text]{} +input[type=text]{width:205px;} input[type=submit]{background-color:#ddd;font-weight:bold;} /*Opera Fix*/ -body:before {content:"";height:100%;float:left;width:0;margin-top:-32767px;} +body:before{content:"";height:100%;float:left;width:0;margin-top:-32767px;} diff --git a/pyramid/paster_templates/alchemy/+package+/static/pyramid.png b/pyramid/paster_templates/alchemy/+package+/static/pyramid.png new file mode 100644 index 000000000..347e05549 Binary files /dev/null and b/pyramid/paster_templates/alchemy/+package+/static/pyramid.png differ diff --git a/pyramid/paster_templates/alchemy/+package+/static/transparent.gif b/pyramid/paster_templates/alchemy/+package+/static/transparent.gif new file mode 100644 index 000000000..0341802e5 Binary files /dev/null and b/pyramid/paster_templates/alchemy/+package+/static/transparent.gif differ diff --git a/pyramid/paster_templates/alchemy/+package+/templates/model.pt_tmpl b/pyramid/paster_templates/alchemy/+package+/templates/model.pt_tmpl new file mode 100644 index 000000000..9cfe1d185 --- /dev/null +++ b/pyramid/paster_templates/alchemy/+package+/templates/model.pt_tmpl @@ -0,0 +1,82 @@ + + + + The Pyramid Web Application Development Framework + + + + + + + + + + +
+
+
+
pyramid
+
+
+
+
+

+ Welcome to ${project}, an application generated by
+ the Pyramid web application development framework. +

+
+
+
+
+
+

Search documentation

+
+ + +
+
+

+ Id: ${item.id}
+ Name: ${item.name}
+ Value: ${item.value} +

+
+ +
+
+
+ + + \ No newline at end of file diff --git a/pyramid/paster_templates/alchemy/+package+/templates/root.pt_tmpl b/pyramid/paster_templates/alchemy/+package+/templates/root.pt_tmpl new file mode 100644 index 000000000..e9108a6cf --- /dev/null +++ b/pyramid/paster_templates/alchemy/+package+/templates/root.pt_tmpl @@ -0,0 +1,80 @@ + + + + The Pyramid Web Application Development Framework + + + + + + + + + + +
+
+
+
pyramid
+
+
+
+
+

+ Welcome to ${project}, an application generated by
+ the Pyramid web application development framework. +

+
+
+
+
+
+

Search documentation

+
+ + +
+
+

+ ${item.name} +

+
+ +
+
+
+ + + \ No newline at end of file diff --git a/pyramid/paster_templates/pylons_basic/+package+/static/footerbg.png b/pyramid/paster_templates/pylons_basic/+package+/static/footerbg.png new file mode 100644 index 000000000..1fbc873da Binary files /dev/null and b/pyramid/paster_templates/pylons_basic/+package+/static/footerbg.png differ diff --git a/pyramid/paster_templates/pylons_basic/+package+/static/headerbg.png b/pyramid/paster_templates/pylons_basic/+package+/static/headerbg.png new file mode 100644 index 000000000..0596f2020 Binary files /dev/null and b/pyramid/paster_templates/pylons_basic/+package+/static/headerbg.png differ diff --git a/pyramid/paster_templates/pylons_basic/+package+/static/ie6.css b/pyramid/paster_templates/pylons_basic/+package+/static/ie6.css new file mode 100644 index 000000000..b7c8493d8 --- /dev/null +++ b/pyramid/paster_templates/pylons_basic/+package+/static/ie6.css @@ -0,0 +1,8 @@ +* html img, +* html .png{position:relative;behavior:expression((this.runtimeStyle.behavior="none")&&(this.pngSet?this.pngSet=true:(this.nodeName == "IMG" && this.src.toLowerCase().indexOf('.png')>-1?(this.runtimeStyle.backgroundImage = "none", +this.runtimeStyle.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + this.src + "',sizingMethod='image')", +this.src = "static/transparent.gif"):(this.origBg = this.origBg? this.origBg :this.currentStyle.backgroundImage.toString().replace('url("','').replace('")',''), +this.runtimeStyle.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + this.origBg + "',sizingMethod='crop')", +this.runtimeStyle.backgroundImage = "none")),this.pngSet=true) +);} +#wrap{display:table;height:100%} diff --git a/pyramid/paster_templates/pylons_basic/+package+/static/logo.png b/pyramid/paster_templates/pylons_basic/+package+/static/logo.png deleted file mode 100644 index 88f5d9865..000000000 Binary files a/pyramid/paster_templates/pylons_basic/+package+/static/logo.png and /dev/null differ diff --git a/pyramid/paster_templates/pylons_basic/+package+/static/middlebg.png b/pyramid/paster_templates/pylons_basic/+package+/static/middlebg.png new file mode 100644 index 000000000..2369cfb7d Binary files /dev/null and b/pyramid/paster_templates/pylons_basic/+package+/static/middlebg.png differ diff --git a/pyramid/paster_templates/pylons_basic/+package+/static/pylons.css b/pyramid/paster_templates/pylons_basic/+package+/static/pylons.css index c153be07f..33b21ac1a 100644 --- a/pyramid/paster_templates/pylons_basic/+package+/static/pylons.css +++ b/pyramid/paster_templates/pylons_basic/+package+/static/pylons.css @@ -4,34 +4,23 @@ body{line-height:1;} ol,ul{list-style:none;} blockquote,q{quotes:none;} blockquote:before,blockquote:after,q:before,q:after{content:'';content:none;} -/* remember to define focus styles! */ :focus{outline:0;} -/* remember to highlight inserts somehow! */ ins{text-decoration:none;} del{text-decoration:line-through;} -/* tables still need 'cellspacing="0"' in the markup */ table{border-collapse:collapse;border-spacing:0;} -/* restyling */ sub{vertical-align:sub;font-size:smaller;line-height:normal;} sup{vertical-align:super;font-size:smaller;line-height:normal;} -/* lists */ ul,menu,dir{display:block;list-style-type:disc;margin:1em 0;padding-left:40px;} ol{display:block;list-style-type:decimal-leading-zero;margin:1em 0;padding-left:40px;} li{display:list-item;} -/* nested lists have no top/bottom margins */ ul ul,ul ol,ul dir,ul menu,ul dl,ol ul,ol ol,ol dir,ol menu,ol dl,dir ul,dir ol,dir dir,dir menu,dir dl,menu ul,menu ol,menu dir,menu menu,menu dl,dl ul,dl ol,dl dir,dl menu,dl dl{margin-top:0;margin-bottom:0;} -/* 2 deep unordered lists use a circle */ ol ul,ul ul,menu ul,dir ul,ol menu,ul menu,menu menu,dir menu,ol dir,ul dir,menu dir,dir dir{list-style-type:circle;} -/* 3 deep (or more) unordered lists use a square */ ol ol ul,ol ul ul,ol menu ul,ol dir ul,ol ol menu,ol ul menu,ol menu menu,ol dir menu,ol ol dir,ol ul dir,ol menu dir,ol dir dir,ul ol ul,ul ul ul,ul menu ul,ul dir ul,ul ol menu,ul ul menu,ul menu menu,ul dir menu,ul ol dir,ul ul dir,ul menu dir,ul dir dir,menu ol ul,menu ul ul,menu menu ul,menu dir ul,menu ol menu,menu ul menu,menu menu menu,menu dir menu,menu ol dir,menu ul dir,menu menu dir,menu dir dir,dir ol ul,dir ul ul,dir menu ul,dir dir ul,dir ol menu,dir ul menu,dir menu menu,dir dir menu,dir ol dir,dir ul dir,dir menu dir,dir dir dir{list-style-type:square;} .hidden{display:none;} p{line-height:1.5em;} -h1{font-size:1.75em;/* 28px */ -line-height:1.7em;font-family:helvetica,verdana;} -h2{font-size:1.5em;/* 24px */ -line-height:1.7em;font-family:helvetica,verdana;} -h3{font-size:1.25em;/* 20px */ -line-height:1.7em;font-family:helvetica,verdana;} +h1{font-size:1.75em;line-height:1.7em;font-family:helvetica,verdana;} +h2{font-size:1.5em;line-height:1.7em;font-family:helvetica,verdana;} +h3{font-size:1.25em;line-height:1.7em;font-family:helvetica,verdana;} h4{font-size:1em;line-height:1.7em;font-family:helvetica,verdana;} html,body{width:100%;height:100%;} body{margin:0;padding:0;background-color:#ffffff;position:relative;font:16px/24px "Nobile","Lucida Grande",Lucida,Verdana,sans-serif;} @@ -42,23 +31,25 @@ body h2, body h3, body h4, body h5, -body h6{font-family:"Nobile","Lucida Grande",Lucida,Verdana,sans-serif;font-weight:normal;color:#144fb2;font-style:normal;} -#wrap {min-height: 100%;} -#header,#footer{width:100%;color:#ffffff;height:40px;position:absolute;text-align:center;line-height:40px;overflow:hidden;font-size:12px;} -#header{background-color:#e88f00;top:0;font-size:14px;} -#footer{background-color:#000000;bottom:0;position: relative;margin-top:-40px;clear:both;} -.header,.footer{width:700px;margin-right:auto;margin-left:auto;} +body h6{font-family:"Neuton","Lucida Grande",Lucida,Verdana,sans-serif;font-weight:normal;color:#373839;font-style:normal;} +#wrap{min-height:100%;} +#header,#footer{width:100%;color:#ffffff;height:40px;position:absolute;text-align:center;line-height:40px;overflow:hidden;font-size:12px;vertical-align:middle;} +#header{background:#000000;top:0;font-size:14px;} +#footer{bottom:0;background:#000000 url(footerbg.png) repeat-x 0 top;position:relative;margin-top:-40px;clear:both;} +.header,.footer{width:750px;margin-right:auto;margin-left:auto;} .wrapper{width:100%} #top,#bottom{width:100%;} -#top{color:#888;background-color:#eee;height:300px;border-bottom:2px solid #ddd;} -#bottom{color:#222;background-color:#ffffff;overflow:hidden;padding-bottom:80px;} -.top,.bottom{width:700px;margin-right:auto;margin-left:auto;} -.top{padding-top:100px;} +#top{color:#000000;height:230px; +background:#ffffff url(headerbg.png) repeat-x 0 top;position:relative;} +#bottom{color:#222;background-color:#ffffff;} +.top,.middle,.bottom{width:750px;margin-right:auto;margin-left:auto;} +.top{padding-top:40px;} +#middle{width:100%;height:100px;background:url(middlebg.png) repeat-x;border-top:2px solid #ffffff;border-bottom:2px solid #b2b2b2;} .app-welcome{margin-top:25px;} .app-name{color:#000000;font-weight:bold;} .bottom{padding-top:50px;} -#left{width:325px;float:left;padding-right:25px;} -#right{width:325px;float:right;padding-left:25px;} +#left{width:350px;float:left;padding-right:25px;} +#right{width:350px;float:right;padding-left:25px;} .align-left{text-align:left;} .align-right{text-align:right;} .align-center{text-align:center;} @@ -67,7 +58,7 @@ ul.links li{list-style-type:none;font-size:14px;} form{border-style:none;} fieldset{border-style:none;} input{color:#222;border:1px solid #ccc;font-family:sans-serif;font-size:12px;line-height:16px;} -input[type=text]{} +input[type=text]{width:205px;} input[type=submit]{background-color:#ddd;font-weight:bold;} /*Opera Fix*/ -body:before {content:"";height:100%;float:left;width:0;margin-top:-32767px;} +body:before{content:"";height:100%;float:left;width:0;margin-top:-32767px;} diff --git a/pyramid/paster_templates/pylons_basic/+package+/static/pyramid.png b/pyramid/paster_templates/pylons_basic/+package+/static/pyramid.png new file mode 100644 index 000000000..347e05549 Binary files /dev/null and b/pyramid/paster_templates/pylons_basic/+package+/static/pyramid.png differ diff --git a/pyramid/paster_templates/pylons_basic/+package+/static/transparent.gif b/pyramid/paster_templates/pylons_basic/+package+/static/transparent.gif new file mode 100644 index 000000000..0341802e5 Binary files /dev/null and b/pyramid/paster_templates/pylons_basic/+package+/static/transparent.gif differ diff --git a/pyramid/paster_templates/pylons_basic/+package+/templates/mytemplate.mako b/pyramid/paster_templates/pylons_basic/+package+/templates/mytemplate.mako deleted file mode 100644 index 63ec0172a..000000000 --- a/pyramid/paster_templates/pylons_basic/+package+/templates/mytemplate.mako +++ /dev/null @@ -1,79 +0,0 @@ - - - - The Pyramid Web Application Development Framework - - - - - - - - - -
- -
-
- -

- Welcome to ${project}, an application generated by
- the Pyramid web application development framework. -

-
-
-
-
-
-

Search Pyramid documentation

-
- - -
-
- -
-
-
- - - \ No newline at end of file diff --git a/pyramid/paster_templates/pylons_basic/+package+/templates/mytemplate.mako_tmpl b/pyramid/paster_templates/pylons_basic/+package+/templates/mytemplate.mako_tmpl new file mode 100644 index 000000000..440b9476c --- /dev/null +++ b/pyramid/paster_templates/pylons_basic/+package+/templates/mytemplate.mako_tmpl @@ -0,0 +1,76 @@ + + + + The Pyramid Web Application Development Framework + + + + + + + + + + +
+
+
+
pyramid
+
+
+
+
+

+ Welcome to ${project}, an application generated by
+ the Pyramid web application development framework. +

+
+
+
+
+
+

Search documentation

+
+ + +
+
+ +
+
+
+ + + \ No newline at end of file diff --git a/pyramid/paster_templates/pylons_minimal/+package+/static/footerbg.png b/pyramid/paster_templates/pylons_minimal/+package+/static/footerbg.png new file mode 100644 index 000000000..1fbc873da Binary files /dev/null and b/pyramid/paster_templates/pylons_minimal/+package+/static/footerbg.png differ diff --git a/pyramid/paster_templates/pylons_minimal/+package+/static/headerbg.png b/pyramid/paster_templates/pylons_minimal/+package+/static/headerbg.png new file mode 100644 index 000000000..0596f2020 Binary files /dev/null and b/pyramid/paster_templates/pylons_minimal/+package+/static/headerbg.png differ diff --git a/pyramid/paster_templates/pylons_minimal/+package+/static/ie6.css b/pyramid/paster_templates/pylons_minimal/+package+/static/ie6.css new file mode 100644 index 000000000..b7c8493d8 --- /dev/null +++ b/pyramid/paster_templates/pylons_minimal/+package+/static/ie6.css @@ -0,0 +1,8 @@ +* html img, +* html .png{position:relative;behavior:expression((this.runtimeStyle.behavior="none")&&(this.pngSet?this.pngSet=true:(this.nodeName == "IMG" && this.src.toLowerCase().indexOf('.png')>-1?(this.runtimeStyle.backgroundImage = "none", +this.runtimeStyle.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + this.src + "',sizingMethod='image')", +this.src = "static/transparent.gif"):(this.origBg = this.origBg? this.origBg :this.currentStyle.backgroundImage.toString().replace('url("','').replace('")',''), +this.runtimeStyle.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + this.origBg + "',sizingMethod='crop')", +this.runtimeStyle.backgroundImage = "none")),this.pngSet=true) +);} +#wrap{display:table;height:100%} diff --git a/pyramid/paster_templates/pylons_minimal/+package+/static/logo.png b/pyramid/paster_templates/pylons_minimal/+package+/static/logo.png deleted file mode 100644 index 88f5d9865..000000000 Binary files a/pyramid/paster_templates/pylons_minimal/+package+/static/logo.png and /dev/null differ diff --git a/pyramid/paster_templates/pylons_minimal/+package+/static/middlebg.png b/pyramid/paster_templates/pylons_minimal/+package+/static/middlebg.png new file mode 100644 index 000000000..2369cfb7d Binary files /dev/null and b/pyramid/paster_templates/pylons_minimal/+package+/static/middlebg.png differ diff --git a/pyramid/paster_templates/pylons_minimal/+package+/static/pylons.css b/pyramid/paster_templates/pylons_minimal/+package+/static/pylons.css index c153be07f..33b21ac1a 100644 --- a/pyramid/paster_templates/pylons_minimal/+package+/static/pylons.css +++ b/pyramid/paster_templates/pylons_minimal/+package+/static/pylons.css @@ -4,34 +4,23 @@ body{line-height:1;} ol,ul{list-style:none;} blockquote,q{quotes:none;} blockquote:before,blockquote:after,q:before,q:after{content:'';content:none;} -/* remember to define focus styles! */ :focus{outline:0;} -/* remember to highlight inserts somehow! */ ins{text-decoration:none;} del{text-decoration:line-through;} -/* tables still need 'cellspacing="0"' in the markup */ table{border-collapse:collapse;border-spacing:0;} -/* restyling */ sub{vertical-align:sub;font-size:smaller;line-height:normal;} sup{vertical-align:super;font-size:smaller;line-height:normal;} -/* lists */ ul,menu,dir{display:block;list-style-type:disc;margin:1em 0;padding-left:40px;} ol{display:block;list-style-type:decimal-leading-zero;margin:1em 0;padding-left:40px;} li{display:list-item;} -/* nested lists have no top/bottom margins */ ul ul,ul ol,ul dir,ul menu,ul dl,ol ul,ol ol,ol dir,ol menu,ol dl,dir ul,dir ol,dir dir,dir menu,dir dl,menu ul,menu ol,menu dir,menu menu,menu dl,dl ul,dl ol,dl dir,dl menu,dl dl{margin-top:0;margin-bottom:0;} -/* 2 deep unordered lists use a circle */ ol ul,ul ul,menu ul,dir ul,ol menu,ul menu,menu menu,dir menu,ol dir,ul dir,menu dir,dir dir{list-style-type:circle;} -/* 3 deep (or more) unordered lists use a square */ ol ol ul,ol ul ul,ol menu ul,ol dir ul,ol ol menu,ol ul menu,ol menu menu,ol dir menu,ol ol dir,ol ul dir,ol menu dir,ol dir dir,ul ol ul,ul ul ul,ul menu ul,ul dir ul,ul ol menu,ul ul menu,ul menu menu,ul dir menu,ul ol dir,ul ul dir,ul menu dir,ul dir dir,menu ol ul,menu ul ul,menu menu ul,menu dir ul,menu ol menu,menu ul menu,menu menu menu,menu dir menu,menu ol dir,menu ul dir,menu menu dir,menu dir dir,dir ol ul,dir ul ul,dir menu ul,dir dir ul,dir ol menu,dir ul menu,dir menu menu,dir dir menu,dir ol dir,dir ul dir,dir menu dir,dir dir dir{list-style-type:square;} .hidden{display:none;} p{line-height:1.5em;} -h1{font-size:1.75em;/* 28px */ -line-height:1.7em;font-family:helvetica,verdana;} -h2{font-size:1.5em;/* 24px */ -line-height:1.7em;font-family:helvetica,verdana;} -h3{font-size:1.25em;/* 20px */ -line-height:1.7em;font-family:helvetica,verdana;} +h1{font-size:1.75em;line-height:1.7em;font-family:helvetica,verdana;} +h2{font-size:1.5em;line-height:1.7em;font-family:helvetica,verdana;} +h3{font-size:1.25em;line-height:1.7em;font-family:helvetica,verdana;} h4{font-size:1em;line-height:1.7em;font-family:helvetica,verdana;} html,body{width:100%;height:100%;} body{margin:0;padding:0;background-color:#ffffff;position:relative;font:16px/24px "Nobile","Lucida Grande",Lucida,Verdana,sans-serif;} @@ -42,23 +31,25 @@ body h2, body h3, body h4, body h5, -body h6{font-family:"Nobile","Lucida Grande",Lucida,Verdana,sans-serif;font-weight:normal;color:#144fb2;font-style:normal;} -#wrap {min-height: 100%;} -#header,#footer{width:100%;color:#ffffff;height:40px;position:absolute;text-align:center;line-height:40px;overflow:hidden;font-size:12px;} -#header{background-color:#e88f00;top:0;font-size:14px;} -#footer{background-color:#000000;bottom:0;position: relative;margin-top:-40px;clear:both;} -.header,.footer{width:700px;margin-right:auto;margin-left:auto;} +body h6{font-family:"Neuton","Lucida Grande",Lucida,Verdana,sans-serif;font-weight:normal;color:#373839;font-style:normal;} +#wrap{min-height:100%;} +#header,#footer{width:100%;color:#ffffff;height:40px;position:absolute;text-align:center;line-height:40px;overflow:hidden;font-size:12px;vertical-align:middle;} +#header{background:#000000;top:0;font-size:14px;} +#footer{bottom:0;background:#000000 url(footerbg.png) repeat-x 0 top;position:relative;margin-top:-40px;clear:both;} +.header,.footer{width:750px;margin-right:auto;margin-left:auto;} .wrapper{width:100%} #top,#bottom{width:100%;} -#top{color:#888;background-color:#eee;height:300px;border-bottom:2px solid #ddd;} -#bottom{color:#222;background-color:#ffffff;overflow:hidden;padding-bottom:80px;} -.top,.bottom{width:700px;margin-right:auto;margin-left:auto;} -.top{padding-top:100px;} +#top{color:#000000;height:230px; +background:#ffffff url(headerbg.png) repeat-x 0 top;position:relative;} +#bottom{color:#222;background-color:#ffffff;} +.top,.middle,.bottom{width:750px;margin-right:auto;margin-left:auto;} +.top{padding-top:40px;} +#middle{width:100%;height:100px;background:url(middlebg.png) repeat-x;border-top:2px solid #ffffff;border-bottom:2px solid #b2b2b2;} .app-welcome{margin-top:25px;} .app-name{color:#000000;font-weight:bold;} .bottom{padding-top:50px;} -#left{width:325px;float:left;padding-right:25px;} -#right{width:325px;float:right;padding-left:25px;} +#left{width:350px;float:left;padding-right:25px;} +#right{width:350px;float:right;padding-left:25px;} .align-left{text-align:left;} .align-right{text-align:right;} .align-center{text-align:center;} @@ -67,7 +58,7 @@ ul.links li{list-style-type:none;font-size:14px;} form{border-style:none;} fieldset{border-style:none;} input{color:#222;border:1px solid #ccc;font-family:sans-serif;font-size:12px;line-height:16px;} -input[type=text]{} +input[type=text]{width:205px;} input[type=submit]{background-color:#ddd;font-weight:bold;} /*Opera Fix*/ -body:before {content:"";height:100%;float:left;width:0;margin-top:-32767px;} +body:before{content:"";height:100%;float:left;width:0;margin-top:-32767px;} diff --git a/pyramid/paster_templates/pylons_minimal/+package+/static/pyramid.png b/pyramid/paster_templates/pylons_minimal/+package+/static/pyramid.png new file mode 100644 index 000000000..347e05549 Binary files /dev/null and b/pyramid/paster_templates/pylons_minimal/+package+/static/pyramid.png differ diff --git a/pyramid/paster_templates/pylons_minimal/+package+/static/transparent.gif b/pyramid/paster_templates/pylons_minimal/+package+/static/transparent.gif new file mode 100644 index 000000000..0341802e5 Binary files /dev/null and b/pyramid/paster_templates/pylons_minimal/+package+/static/transparent.gif differ diff --git a/pyramid/paster_templates/pylons_minimal/+package+/templates/mytemplate.mako_tmpl b/pyramid/paster_templates/pylons_minimal/+package+/templates/mytemplate.mako_tmpl new file mode 100644 index 000000000..440b9476c --- /dev/null +++ b/pyramid/paster_templates/pylons_minimal/+package+/templates/mytemplate.mako_tmpl @@ -0,0 +1,76 @@ + + + + The Pyramid Web Application Development Framework + + + + + + + + + + +
+
+
+
pyramid
+
+
+
+
+

+ Welcome to ${project}, an application generated by
+ the Pyramid web application development framework. +

+
+
+
+
+
+

Search documentation

+
+ + +
+
+ +
+
+
+ + + \ No newline at end of file diff --git a/pyramid/paster_templates/pylons_sqla/+package+/static/footerbg.png b/pyramid/paster_templates/pylons_sqla/+package+/static/footerbg.png new file mode 100644 index 000000000..1fbc873da Binary files /dev/null and b/pyramid/paster_templates/pylons_sqla/+package+/static/footerbg.png differ diff --git a/pyramid/paster_templates/pylons_sqla/+package+/static/headerbg.png b/pyramid/paster_templates/pylons_sqla/+package+/static/headerbg.png new file mode 100644 index 000000000..0596f2020 Binary files /dev/null and b/pyramid/paster_templates/pylons_sqla/+package+/static/headerbg.png differ diff --git a/pyramid/paster_templates/pylons_sqla/+package+/static/ie6.css b/pyramid/paster_templates/pylons_sqla/+package+/static/ie6.css new file mode 100644 index 000000000..b7c8493d8 --- /dev/null +++ b/pyramid/paster_templates/pylons_sqla/+package+/static/ie6.css @@ -0,0 +1,8 @@ +* html img, +* html .png{position:relative;behavior:expression((this.runtimeStyle.behavior="none")&&(this.pngSet?this.pngSet=true:(this.nodeName == "IMG" && this.src.toLowerCase().indexOf('.png')>-1?(this.runtimeStyle.backgroundImage = "none", +this.runtimeStyle.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + this.src + "',sizingMethod='image')", +this.src = "static/transparent.gif"):(this.origBg = this.origBg? this.origBg :this.currentStyle.backgroundImage.toString().replace('url("','').replace('")',''), +this.runtimeStyle.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + this.origBg + "',sizingMethod='crop')", +this.runtimeStyle.backgroundImage = "none")),this.pngSet=true) +);} +#wrap{display:table;height:100%} diff --git a/pyramid/paster_templates/pylons_sqla/+package+/static/logo.png b/pyramid/paster_templates/pylons_sqla/+package+/static/logo.png deleted file mode 100644 index 88f5d9865..000000000 Binary files a/pyramid/paster_templates/pylons_sqla/+package+/static/logo.png and /dev/null differ diff --git a/pyramid/paster_templates/pylons_sqla/+package+/static/middlebg.png b/pyramid/paster_templates/pylons_sqla/+package+/static/middlebg.png new file mode 100644 index 000000000..2369cfb7d Binary files /dev/null and b/pyramid/paster_templates/pylons_sqla/+package+/static/middlebg.png differ diff --git a/pyramid/paster_templates/pylons_sqla/+package+/static/pylons.css b/pyramid/paster_templates/pylons_sqla/+package+/static/pylons.css index c153be07f..33b21ac1a 100644 --- a/pyramid/paster_templates/pylons_sqla/+package+/static/pylons.css +++ b/pyramid/paster_templates/pylons_sqla/+package+/static/pylons.css @@ -4,34 +4,23 @@ body{line-height:1;} ol,ul{list-style:none;} blockquote,q{quotes:none;} blockquote:before,blockquote:after,q:before,q:after{content:'';content:none;} -/* remember to define focus styles! */ :focus{outline:0;} -/* remember to highlight inserts somehow! */ ins{text-decoration:none;} del{text-decoration:line-through;} -/* tables still need 'cellspacing="0"' in the markup */ table{border-collapse:collapse;border-spacing:0;} -/* restyling */ sub{vertical-align:sub;font-size:smaller;line-height:normal;} sup{vertical-align:super;font-size:smaller;line-height:normal;} -/* lists */ ul,menu,dir{display:block;list-style-type:disc;margin:1em 0;padding-left:40px;} ol{display:block;list-style-type:decimal-leading-zero;margin:1em 0;padding-left:40px;} li{display:list-item;} -/* nested lists have no top/bottom margins */ ul ul,ul ol,ul dir,ul menu,ul dl,ol ul,ol ol,ol dir,ol menu,ol dl,dir ul,dir ol,dir dir,dir menu,dir dl,menu ul,menu ol,menu dir,menu menu,menu dl,dl ul,dl ol,dl dir,dl menu,dl dl{margin-top:0;margin-bottom:0;} -/* 2 deep unordered lists use a circle */ ol ul,ul ul,menu ul,dir ul,ol menu,ul menu,menu menu,dir menu,ol dir,ul dir,menu dir,dir dir{list-style-type:circle;} -/* 3 deep (or more) unordered lists use a square */ ol ol ul,ol ul ul,ol menu ul,ol dir ul,ol ol menu,ol ul menu,ol menu menu,ol dir menu,ol ol dir,ol ul dir,ol menu dir,ol dir dir,ul ol ul,ul ul ul,ul menu ul,ul dir ul,ul ol menu,ul ul menu,ul menu menu,ul dir menu,ul ol dir,ul ul dir,ul menu dir,ul dir dir,menu ol ul,menu ul ul,menu menu ul,menu dir ul,menu ol menu,menu ul menu,menu menu menu,menu dir menu,menu ol dir,menu ul dir,menu menu dir,menu dir dir,dir ol ul,dir ul ul,dir menu ul,dir dir ul,dir ol menu,dir ul menu,dir menu menu,dir dir menu,dir ol dir,dir ul dir,dir menu dir,dir dir dir{list-style-type:square;} .hidden{display:none;} p{line-height:1.5em;} -h1{font-size:1.75em;/* 28px */ -line-height:1.7em;font-family:helvetica,verdana;} -h2{font-size:1.5em;/* 24px */ -line-height:1.7em;font-family:helvetica,verdana;} -h3{font-size:1.25em;/* 20px */ -line-height:1.7em;font-family:helvetica,verdana;} +h1{font-size:1.75em;line-height:1.7em;font-family:helvetica,verdana;} +h2{font-size:1.5em;line-height:1.7em;font-family:helvetica,verdana;} +h3{font-size:1.25em;line-height:1.7em;font-family:helvetica,verdana;} h4{font-size:1em;line-height:1.7em;font-family:helvetica,verdana;} html,body{width:100%;height:100%;} body{margin:0;padding:0;background-color:#ffffff;position:relative;font:16px/24px "Nobile","Lucida Grande",Lucida,Verdana,sans-serif;} @@ -42,23 +31,25 @@ body h2, body h3, body h4, body h5, -body h6{font-family:"Nobile","Lucida Grande",Lucida,Verdana,sans-serif;font-weight:normal;color:#144fb2;font-style:normal;} -#wrap {min-height: 100%;} -#header,#footer{width:100%;color:#ffffff;height:40px;position:absolute;text-align:center;line-height:40px;overflow:hidden;font-size:12px;} -#header{background-color:#e88f00;top:0;font-size:14px;} -#footer{background-color:#000000;bottom:0;position: relative;margin-top:-40px;clear:both;} -.header,.footer{width:700px;margin-right:auto;margin-left:auto;} +body h6{font-family:"Neuton","Lucida Grande",Lucida,Verdana,sans-serif;font-weight:normal;color:#373839;font-style:normal;} +#wrap{min-height:100%;} +#header,#footer{width:100%;color:#ffffff;height:40px;position:absolute;text-align:center;line-height:40px;overflow:hidden;font-size:12px;vertical-align:middle;} +#header{background:#000000;top:0;font-size:14px;} +#footer{bottom:0;background:#000000 url(footerbg.png) repeat-x 0 top;position:relative;margin-top:-40px;clear:both;} +.header,.footer{width:750px;margin-right:auto;margin-left:auto;} .wrapper{width:100%} #top,#bottom{width:100%;} -#top{color:#888;background-color:#eee;height:300px;border-bottom:2px solid #ddd;} -#bottom{color:#222;background-color:#ffffff;overflow:hidden;padding-bottom:80px;} -.top,.bottom{width:700px;margin-right:auto;margin-left:auto;} -.top{padding-top:100px;} +#top{color:#000000;height:230px; +background:#ffffff url(headerbg.png) repeat-x 0 top;position:relative;} +#bottom{color:#222;background-color:#ffffff;} +.top,.middle,.bottom{width:750px;margin-right:auto;margin-left:auto;} +.top{padding-top:40px;} +#middle{width:100%;height:100px;background:url(middlebg.png) repeat-x;border-top:2px solid #ffffff;border-bottom:2px solid #b2b2b2;} .app-welcome{margin-top:25px;} .app-name{color:#000000;font-weight:bold;} .bottom{padding-top:50px;} -#left{width:325px;float:left;padding-right:25px;} -#right{width:325px;float:right;padding-left:25px;} +#left{width:350px;float:left;padding-right:25px;} +#right{width:350px;float:right;padding-left:25px;} .align-left{text-align:left;} .align-right{text-align:right;} .align-center{text-align:center;} @@ -67,7 +58,7 @@ ul.links li{list-style-type:none;font-size:14px;} form{border-style:none;} fieldset{border-style:none;} input{color:#222;border:1px solid #ccc;font-family:sans-serif;font-size:12px;line-height:16px;} -input[type=text]{} +input[type=text]{width:205px;} input[type=submit]{background-color:#ddd;font-weight:bold;} /*Opera Fix*/ -body:before {content:"";height:100%;float:left;width:0;margin-top:-32767px;} +body:before{content:"";height:100%;float:left;width:0;margin-top:-32767px;} diff --git a/pyramid/paster_templates/pylons_sqla/+package+/static/pyramid.png b/pyramid/paster_templates/pylons_sqla/+package+/static/pyramid.png new file mode 100644 index 000000000..347e05549 Binary files /dev/null and b/pyramid/paster_templates/pylons_sqla/+package+/static/pyramid.png differ diff --git a/pyramid/paster_templates/pylons_sqla/+package+/static/transparent.gif b/pyramid/paster_templates/pylons_sqla/+package+/static/transparent.gif new file mode 100644 index 000000000..0341802e5 Binary files /dev/null and b/pyramid/paster_templates/pylons_sqla/+package+/static/transparent.gif differ diff --git a/pyramid/paster_templates/pylons_sqla/+package+/templates/mytemplate.mako_tmpl b/pyramid/paster_templates/pylons_sqla/+package+/templates/mytemplate.mako_tmpl new file mode 100644 index 000000000..591cfecb4 --- /dev/null +++ b/pyramid/paster_templates/pylons_sqla/+package+/templates/mytemplate.mako_tmpl @@ -0,0 +1,78 @@ + + + + The Pyramid Web Application Development Framework + + + + + + + + + + +
+
+
+
pyramid
+
+
+
+
+

+ Welcome to ${project}, an application generated by
+ the Pyramid web application development framework. +

+
+
+
+
+
+

Search documentation

+
+ + +
+
+

The root object's name is "${root.name}"

+
+ +
+
+
+ + + \ No newline at end of file diff --git a/pyramid/paster_templates/routesalchemy/+package+/static/footerbg.png b/pyramid/paster_templates/routesalchemy/+package+/static/footerbg.png new file mode 100644 index 000000000..1fbc873da Binary files /dev/null and b/pyramid/paster_templates/routesalchemy/+package+/static/footerbg.png differ diff --git a/pyramid/paster_templates/routesalchemy/+package+/static/headerbg.png b/pyramid/paster_templates/routesalchemy/+package+/static/headerbg.png new file mode 100644 index 000000000..0596f2020 Binary files /dev/null and b/pyramid/paster_templates/routesalchemy/+package+/static/headerbg.png differ diff --git a/pyramid/paster_templates/routesalchemy/+package+/static/ie6.css b/pyramid/paster_templates/routesalchemy/+package+/static/ie6.css new file mode 100644 index 000000000..b7c8493d8 --- /dev/null +++ b/pyramid/paster_templates/routesalchemy/+package+/static/ie6.css @@ -0,0 +1,8 @@ +* html img, +* html .png{position:relative;behavior:expression((this.runtimeStyle.behavior="none")&&(this.pngSet?this.pngSet=true:(this.nodeName == "IMG" && this.src.toLowerCase().indexOf('.png')>-1?(this.runtimeStyle.backgroundImage = "none", +this.runtimeStyle.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + this.src + "',sizingMethod='image')", +this.src = "static/transparent.gif"):(this.origBg = this.origBg? this.origBg :this.currentStyle.backgroundImage.toString().replace('url("','').replace('")',''), +this.runtimeStyle.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + this.origBg + "',sizingMethod='crop')", +this.runtimeStyle.backgroundImage = "none")),this.pngSet=true) +);} +#wrap{display:table;height:100%} diff --git a/pyramid/paster_templates/routesalchemy/+package+/static/logo.png b/pyramid/paster_templates/routesalchemy/+package+/static/logo.png deleted file mode 100644 index 88f5d9865..000000000 Binary files a/pyramid/paster_templates/routesalchemy/+package+/static/logo.png and /dev/null differ diff --git a/pyramid/paster_templates/routesalchemy/+package+/static/middlebg.png b/pyramid/paster_templates/routesalchemy/+package+/static/middlebg.png new file mode 100644 index 000000000..2369cfb7d Binary files /dev/null and b/pyramid/paster_templates/routesalchemy/+package+/static/middlebg.png differ diff --git a/pyramid/paster_templates/routesalchemy/+package+/static/pylons.css b/pyramid/paster_templates/routesalchemy/+package+/static/pylons.css index c153be07f..33b21ac1a 100644 --- a/pyramid/paster_templates/routesalchemy/+package+/static/pylons.css +++ b/pyramid/paster_templates/routesalchemy/+package+/static/pylons.css @@ -4,34 +4,23 @@ body{line-height:1;} ol,ul{list-style:none;} blockquote,q{quotes:none;} blockquote:before,blockquote:after,q:before,q:after{content:'';content:none;} -/* remember to define focus styles! */ :focus{outline:0;} -/* remember to highlight inserts somehow! */ ins{text-decoration:none;} del{text-decoration:line-through;} -/* tables still need 'cellspacing="0"' in the markup */ table{border-collapse:collapse;border-spacing:0;} -/* restyling */ sub{vertical-align:sub;font-size:smaller;line-height:normal;} sup{vertical-align:super;font-size:smaller;line-height:normal;} -/* lists */ ul,menu,dir{display:block;list-style-type:disc;margin:1em 0;padding-left:40px;} ol{display:block;list-style-type:decimal-leading-zero;margin:1em 0;padding-left:40px;} li{display:list-item;} -/* nested lists have no top/bottom margins */ ul ul,ul ol,ul dir,ul menu,ul dl,ol ul,ol ol,ol dir,ol menu,ol dl,dir ul,dir ol,dir dir,dir menu,dir dl,menu ul,menu ol,menu dir,menu menu,menu dl,dl ul,dl ol,dl dir,dl menu,dl dl{margin-top:0;margin-bottom:0;} -/* 2 deep unordered lists use a circle */ ol ul,ul ul,menu ul,dir ul,ol menu,ul menu,menu menu,dir menu,ol dir,ul dir,menu dir,dir dir{list-style-type:circle;} -/* 3 deep (or more) unordered lists use a square */ ol ol ul,ol ul ul,ol menu ul,ol dir ul,ol ol menu,ol ul menu,ol menu menu,ol dir menu,ol ol dir,ol ul dir,ol menu dir,ol dir dir,ul ol ul,ul ul ul,ul menu ul,ul dir ul,ul ol menu,ul ul menu,ul menu menu,ul dir menu,ul ol dir,ul ul dir,ul menu dir,ul dir dir,menu ol ul,menu ul ul,menu menu ul,menu dir ul,menu ol menu,menu ul menu,menu menu menu,menu dir menu,menu ol dir,menu ul dir,menu menu dir,menu dir dir,dir ol ul,dir ul ul,dir menu ul,dir dir ul,dir ol menu,dir ul menu,dir menu menu,dir dir menu,dir ol dir,dir ul dir,dir menu dir,dir dir dir{list-style-type:square;} .hidden{display:none;} p{line-height:1.5em;} -h1{font-size:1.75em;/* 28px */ -line-height:1.7em;font-family:helvetica,verdana;} -h2{font-size:1.5em;/* 24px */ -line-height:1.7em;font-family:helvetica,verdana;} -h3{font-size:1.25em;/* 20px */ -line-height:1.7em;font-family:helvetica,verdana;} +h1{font-size:1.75em;line-height:1.7em;font-family:helvetica,verdana;} +h2{font-size:1.5em;line-height:1.7em;font-family:helvetica,verdana;} +h3{font-size:1.25em;line-height:1.7em;font-family:helvetica,verdana;} h4{font-size:1em;line-height:1.7em;font-family:helvetica,verdana;} html,body{width:100%;height:100%;} body{margin:0;padding:0;background-color:#ffffff;position:relative;font:16px/24px "Nobile","Lucida Grande",Lucida,Verdana,sans-serif;} @@ -42,23 +31,25 @@ body h2, body h3, body h4, body h5, -body h6{font-family:"Nobile","Lucida Grande",Lucida,Verdana,sans-serif;font-weight:normal;color:#144fb2;font-style:normal;} -#wrap {min-height: 100%;} -#header,#footer{width:100%;color:#ffffff;height:40px;position:absolute;text-align:center;line-height:40px;overflow:hidden;font-size:12px;} -#header{background-color:#e88f00;top:0;font-size:14px;} -#footer{background-color:#000000;bottom:0;position: relative;margin-top:-40px;clear:both;} -.header,.footer{width:700px;margin-right:auto;margin-left:auto;} +body h6{font-family:"Neuton","Lucida Grande",Lucida,Verdana,sans-serif;font-weight:normal;color:#373839;font-style:normal;} +#wrap{min-height:100%;} +#header,#footer{width:100%;color:#ffffff;height:40px;position:absolute;text-align:center;line-height:40px;overflow:hidden;font-size:12px;vertical-align:middle;} +#header{background:#000000;top:0;font-size:14px;} +#footer{bottom:0;background:#000000 url(footerbg.png) repeat-x 0 top;position:relative;margin-top:-40px;clear:both;} +.header,.footer{width:750px;margin-right:auto;margin-left:auto;} .wrapper{width:100%} #top,#bottom{width:100%;} -#top{color:#888;background-color:#eee;height:300px;border-bottom:2px solid #ddd;} -#bottom{color:#222;background-color:#ffffff;overflow:hidden;padding-bottom:80px;} -.top,.bottom{width:700px;margin-right:auto;margin-left:auto;} -.top{padding-top:100px;} +#top{color:#000000;height:230px; +background:#ffffff url(headerbg.png) repeat-x 0 top;position:relative;} +#bottom{color:#222;background-color:#ffffff;} +.top,.middle,.bottom{width:750px;margin-right:auto;margin-left:auto;} +.top{padding-top:40px;} +#middle{width:100%;height:100px;background:url(middlebg.png) repeat-x;border-top:2px solid #ffffff;border-bottom:2px solid #b2b2b2;} .app-welcome{margin-top:25px;} .app-name{color:#000000;font-weight:bold;} .bottom{padding-top:50px;} -#left{width:325px;float:left;padding-right:25px;} -#right{width:325px;float:right;padding-left:25px;} +#left{width:350px;float:left;padding-right:25px;} +#right{width:350px;float:right;padding-left:25px;} .align-left{text-align:left;} .align-right{text-align:right;} .align-center{text-align:center;} @@ -67,7 +58,7 @@ ul.links li{list-style-type:none;font-size:14px;} form{border-style:none;} fieldset{border-style:none;} input{color:#222;border:1px solid #ccc;font-family:sans-serif;font-size:12px;line-height:16px;} -input[type=text]{} +input[type=text]{width:205px;} input[type=submit]{background-color:#ddd;font-weight:bold;} /*Opera Fix*/ -body:before {content:"";height:100%;float:left;width:0;margin-top:-32767px;} +body:before{content:"";height:100%;float:left;width:0;margin-top:-32767px;} diff --git a/pyramid/paster_templates/routesalchemy/+package+/static/pyramid.png b/pyramid/paster_templates/routesalchemy/+package+/static/pyramid.png new file mode 100644 index 000000000..347e05549 Binary files /dev/null and b/pyramid/paster_templates/routesalchemy/+package+/static/pyramid.png differ diff --git a/pyramid/paster_templates/routesalchemy/+package+/static/transparent.gif b/pyramid/paster_templates/routesalchemy/+package+/static/transparent.gif new file mode 100644 index 000000000..0341802e5 Binary files /dev/null and b/pyramid/paster_templates/routesalchemy/+package+/static/transparent.gif differ diff --git a/pyramid/paster_templates/routesalchemy/+package+/templates/mytemplate.pt b/pyramid/paster_templates/routesalchemy/+package+/templates/mytemplate.pt deleted file mode 100644 index 6ad23d44f..000000000 --- a/pyramid/paster_templates/routesalchemy/+package+/templates/mytemplate.pt +++ /dev/null @@ -1,79 +0,0 @@ - - - - The Pyramid Web Application Development Framework - - - - - - - - - -
- -
-
- -

- Welcome to ${project}, an application generated by
- the Pyramid web application development framework. -

-
-
-
-
-
-

Search Pyramid documentation

-
- - -
-
- -
-
-
- - - \ No newline at end of file diff --git a/pyramid/paster_templates/routesalchemy/+package+/templates/mytemplate.pt_tmpl b/pyramid/paster_templates/routesalchemy/+package+/templates/mytemplate.pt_tmpl new file mode 100644 index 000000000..440b9476c --- /dev/null +++ b/pyramid/paster_templates/routesalchemy/+package+/templates/mytemplate.pt_tmpl @@ -0,0 +1,76 @@ + + + + The Pyramid Web Application Development Framework + + + + + + + + + + +
+
+
+
pyramid
+
+
+
+
+

+ Welcome to ${project}, an application generated by
+ the Pyramid web application development framework. +

+
+
+
+
+
+

Search documentation

+
+ + +
+
+ +
+
+
+ + + \ No newline at end of file diff --git a/pyramid/paster_templates/starter/+package+/templates/mytemplate.pt_tmpl b/pyramid/paster_templates/starter/+package+/templates/mytemplate.pt_tmpl new file mode 100644 index 000000000..440b9476c --- /dev/null +++ b/pyramid/paster_templates/starter/+package+/templates/mytemplate.pt_tmpl @@ -0,0 +1,76 @@ + + + + The Pyramid Web Application Development Framework + + + + + + + + + + +
+
+
+
pyramid
+
+
+
+
+

+ Welcome to ${project}, an application generated by
+ the Pyramid web application development framework. +

+
+
+
+
+
+

Search documentation

+
+ + +
+
+ +
+
+
+ + + \ No newline at end of file diff --git a/pyramid/paster_templates/starter_zcml/+package+/static/footerbg.png b/pyramid/paster_templates/starter_zcml/+package+/static/footerbg.png new file mode 100644 index 000000000..1fbc873da Binary files /dev/null and b/pyramid/paster_templates/starter_zcml/+package+/static/footerbg.png differ diff --git a/pyramid/paster_templates/starter_zcml/+package+/static/headerbg.png b/pyramid/paster_templates/starter_zcml/+package+/static/headerbg.png new file mode 100644 index 000000000..0596f2020 Binary files /dev/null and b/pyramid/paster_templates/starter_zcml/+package+/static/headerbg.png differ diff --git a/pyramid/paster_templates/starter_zcml/+package+/static/ie6.css b/pyramid/paster_templates/starter_zcml/+package+/static/ie6.css new file mode 100644 index 000000000..b7c8493d8 --- /dev/null +++ b/pyramid/paster_templates/starter_zcml/+package+/static/ie6.css @@ -0,0 +1,8 @@ +* html img, +* html .png{position:relative;behavior:expression((this.runtimeStyle.behavior="none")&&(this.pngSet?this.pngSet=true:(this.nodeName == "IMG" && this.src.toLowerCase().indexOf('.png')>-1?(this.runtimeStyle.backgroundImage = "none", +this.runtimeStyle.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + this.src + "',sizingMethod='image')", +this.src = "static/transparent.gif"):(this.origBg = this.origBg? this.origBg :this.currentStyle.backgroundImage.toString().replace('url("','').replace('")',''), +this.runtimeStyle.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + this.origBg + "',sizingMethod='crop')", +this.runtimeStyle.backgroundImage = "none")),this.pngSet=true) +);} +#wrap{display:table;height:100%} diff --git a/pyramid/paster_templates/starter_zcml/+package+/static/logo.png b/pyramid/paster_templates/starter_zcml/+package+/static/logo.png deleted file mode 100644 index 88f5d9865..000000000 Binary files a/pyramid/paster_templates/starter_zcml/+package+/static/logo.png and /dev/null differ diff --git a/pyramid/paster_templates/starter_zcml/+package+/static/middlebg.png b/pyramid/paster_templates/starter_zcml/+package+/static/middlebg.png new file mode 100644 index 000000000..2369cfb7d Binary files /dev/null and b/pyramid/paster_templates/starter_zcml/+package+/static/middlebg.png differ diff --git a/pyramid/paster_templates/starter_zcml/+package+/static/pylons.css b/pyramid/paster_templates/starter_zcml/+package+/static/pylons.css index c153be07f..33b21ac1a 100644 --- a/pyramid/paster_templates/starter_zcml/+package+/static/pylons.css +++ b/pyramid/paster_templates/starter_zcml/+package+/static/pylons.css @@ -4,34 +4,23 @@ body{line-height:1;} ol,ul{list-style:none;} blockquote,q{quotes:none;} blockquote:before,blockquote:after,q:before,q:after{content:'';content:none;} -/* remember to define focus styles! */ :focus{outline:0;} -/* remember to highlight inserts somehow! */ ins{text-decoration:none;} del{text-decoration:line-through;} -/* tables still need 'cellspacing="0"' in the markup */ table{border-collapse:collapse;border-spacing:0;} -/* restyling */ sub{vertical-align:sub;font-size:smaller;line-height:normal;} sup{vertical-align:super;font-size:smaller;line-height:normal;} -/* lists */ ul,menu,dir{display:block;list-style-type:disc;margin:1em 0;padding-left:40px;} ol{display:block;list-style-type:decimal-leading-zero;margin:1em 0;padding-left:40px;} li{display:list-item;} -/* nested lists have no top/bottom margins */ ul ul,ul ol,ul dir,ul menu,ul dl,ol ul,ol ol,ol dir,ol menu,ol dl,dir ul,dir ol,dir dir,dir menu,dir dl,menu ul,menu ol,menu dir,menu menu,menu dl,dl ul,dl ol,dl dir,dl menu,dl dl{margin-top:0;margin-bottom:0;} -/* 2 deep unordered lists use a circle */ ol ul,ul ul,menu ul,dir ul,ol menu,ul menu,menu menu,dir menu,ol dir,ul dir,menu dir,dir dir{list-style-type:circle;} -/* 3 deep (or more) unordered lists use a square */ ol ol ul,ol ul ul,ol menu ul,ol dir ul,ol ol menu,ol ul menu,ol menu menu,ol dir menu,ol ol dir,ol ul dir,ol menu dir,ol dir dir,ul ol ul,ul ul ul,ul menu ul,ul dir ul,ul ol menu,ul ul menu,ul menu menu,ul dir menu,ul ol dir,ul ul dir,ul menu dir,ul dir dir,menu ol ul,menu ul ul,menu menu ul,menu dir ul,menu ol menu,menu ul menu,menu menu menu,menu dir menu,menu ol dir,menu ul dir,menu menu dir,menu dir dir,dir ol ul,dir ul ul,dir menu ul,dir dir ul,dir ol menu,dir ul menu,dir menu menu,dir dir menu,dir ol dir,dir ul dir,dir menu dir,dir dir dir{list-style-type:square;} .hidden{display:none;} p{line-height:1.5em;} -h1{font-size:1.75em;/* 28px */ -line-height:1.7em;font-family:helvetica,verdana;} -h2{font-size:1.5em;/* 24px */ -line-height:1.7em;font-family:helvetica,verdana;} -h3{font-size:1.25em;/* 20px */ -line-height:1.7em;font-family:helvetica,verdana;} +h1{font-size:1.75em;line-height:1.7em;font-family:helvetica,verdana;} +h2{font-size:1.5em;line-height:1.7em;font-family:helvetica,verdana;} +h3{font-size:1.25em;line-height:1.7em;font-family:helvetica,verdana;} h4{font-size:1em;line-height:1.7em;font-family:helvetica,verdana;} html,body{width:100%;height:100%;} body{margin:0;padding:0;background-color:#ffffff;position:relative;font:16px/24px "Nobile","Lucida Grande",Lucida,Verdana,sans-serif;} @@ -42,23 +31,25 @@ body h2, body h3, body h4, body h5, -body h6{font-family:"Nobile","Lucida Grande",Lucida,Verdana,sans-serif;font-weight:normal;color:#144fb2;font-style:normal;} -#wrap {min-height: 100%;} -#header,#footer{width:100%;color:#ffffff;height:40px;position:absolute;text-align:center;line-height:40px;overflow:hidden;font-size:12px;} -#header{background-color:#e88f00;top:0;font-size:14px;} -#footer{background-color:#000000;bottom:0;position: relative;margin-top:-40px;clear:both;} -.header,.footer{width:700px;margin-right:auto;margin-left:auto;} +body h6{font-family:"Neuton","Lucida Grande",Lucida,Verdana,sans-serif;font-weight:normal;color:#373839;font-style:normal;} +#wrap{min-height:100%;} +#header,#footer{width:100%;color:#ffffff;height:40px;position:absolute;text-align:center;line-height:40px;overflow:hidden;font-size:12px;vertical-align:middle;} +#header{background:#000000;top:0;font-size:14px;} +#footer{bottom:0;background:#000000 url(footerbg.png) repeat-x 0 top;position:relative;margin-top:-40px;clear:both;} +.header,.footer{width:750px;margin-right:auto;margin-left:auto;} .wrapper{width:100%} #top,#bottom{width:100%;} -#top{color:#888;background-color:#eee;height:300px;border-bottom:2px solid #ddd;} -#bottom{color:#222;background-color:#ffffff;overflow:hidden;padding-bottom:80px;} -.top,.bottom{width:700px;margin-right:auto;margin-left:auto;} -.top{padding-top:100px;} +#top{color:#000000;height:230px; +background:#ffffff url(headerbg.png) repeat-x 0 top;position:relative;} +#bottom{color:#222;background-color:#ffffff;} +.top,.middle,.bottom{width:750px;margin-right:auto;margin-left:auto;} +.top{padding-top:40px;} +#middle{width:100%;height:100px;background:url(middlebg.png) repeat-x;border-top:2px solid #ffffff;border-bottom:2px solid #b2b2b2;} .app-welcome{margin-top:25px;} .app-name{color:#000000;font-weight:bold;} .bottom{padding-top:50px;} -#left{width:325px;float:left;padding-right:25px;} -#right{width:325px;float:right;padding-left:25px;} +#left{width:350px;float:left;padding-right:25px;} +#right{width:350px;float:right;padding-left:25px;} .align-left{text-align:left;} .align-right{text-align:right;} .align-center{text-align:center;} @@ -67,7 +58,7 @@ ul.links li{list-style-type:none;font-size:14px;} form{border-style:none;} fieldset{border-style:none;} input{color:#222;border:1px solid #ccc;font-family:sans-serif;font-size:12px;line-height:16px;} -input[type=text]{} +input[type=text]{width:205px;} input[type=submit]{background-color:#ddd;font-weight:bold;} /*Opera Fix*/ -body:before {content:"";height:100%;float:left;width:0;margin-top:-32767px;} +body:before{content:"";height:100%;float:left;width:0;margin-top:-32767px;} diff --git a/pyramid/paster_templates/starter_zcml/+package+/static/pyramid.png b/pyramid/paster_templates/starter_zcml/+package+/static/pyramid.png new file mode 100644 index 000000000..347e05549 Binary files /dev/null and b/pyramid/paster_templates/starter_zcml/+package+/static/pyramid.png differ diff --git a/pyramid/paster_templates/starter_zcml/+package+/static/transparent.gif b/pyramid/paster_templates/starter_zcml/+package+/static/transparent.gif new file mode 100644 index 000000000..0341802e5 Binary files /dev/null and b/pyramid/paster_templates/starter_zcml/+package+/static/transparent.gif differ diff --git a/pyramid/paster_templates/starter_zcml/+package+/templates/mytemplate.pt b/pyramid/paster_templates/starter_zcml/+package+/templates/mytemplate.pt deleted file mode 100644 index 6ad23d44f..000000000 --- a/pyramid/paster_templates/starter_zcml/+package+/templates/mytemplate.pt +++ /dev/null @@ -1,79 +0,0 @@ - - - - The Pyramid Web Application Development Framework - - - - - - - - - -
- -
-
- -

- Welcome to ${project}, an application generated by
- the Pyramid web application development framework. -

-
-
-
-
-
-

Search Pyramid documentation

-
- - -
-
- -
-
-
- - - \ No newline at end of file diff --git a/pyramid/paster_templates/starter_zcml/+package+/templates/mytemplate.pt_tmpl b/pyramid/paster_templates/starter_zcml/+package+/templates/mytemplate.pt_tmpl new file mode 100644 index 000000000..440b9476c --- /dev/null +++ b/pyramid/paster_templates/starter_zcml/+package+/templates/mytemplate.pt_tmpl @@ -0,0 +1,76 @@ + + + + The Pyramid Web Application Development Framework + + + + + + + + + + +
+
+
+
pyramid
+
+
+
+
+

+ Welcome to ${project}, an application generated by
+ the Pyramid web application development framework. +

+
+
+
+
+
+

Search documentation

+
+ + +
+
+ +
+
+
+ + + \ No newline at end of file diff --git a/pyramid/paster_templates/zodb/+package+/static/footerbg.png b/pyramid/paster_templates/zodb/+package+/static/footerbg.png new file mode 100644 index 000000000..1fbc873da Binary files /dev/null and b/pyramid/paster_templates/zodb/+package+/static/footerbg.png differ diff --git a/pyramid/paster_templates/zodb/+package+/static/headerbg.png b/pyramid/paster_templates/zodb/+package+/static/headerbg.png new file mode 100644 index 000000000..0596f2020 Binary files /dev/null and b/pyramid/paster_templates/zodb/+package+/static/headerbg.png differ diff --git a/pyramid/paster_templates/zodb/+package+/static/ie6.css b/pyramid/paster_templates/zodb/+package+/static/ie6.css new file mode 100644 index 000000000..b7c8493d8 --- /dev/null +++ b/pyramid/paster_templates/zodb/+package+/static/ie6.css @@ -0,0 +1,8 @@ +* html img, +* html .png{position:relative;behavior:expression((this.runtimeStyle.behavior="none")&&(this.pngSet?this.pngSet=true:(this.nodeName == "IMG" && this.src.toLowerCase().indexOf('.png')>-1?(this.runtimeStyle.backgroundImage = "none", +this.runtimeStyle.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + this.src + "',sizingMethod='image')", +this.src = "static/transparent.gif"):(this.origBg = this.origBg? this.origBg :this.currentStyle.backgroundImage.toString().replace('url("','').replace('")',''), +this.runtimeStyle.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + this.origBg + "',sizingMethod='crop')", +this.runtimeStyle.backgroundImage = "none")),this.pngSet=true) +);} +#wrap{display:table;height:100%} diff --git a/pyramid/paster_templates/zodb/+package+/static/logo.png b/pyramid/paster_templates/zodb/+package+/static/logo.png deleted file mode 100644 index 88f5d9865..000000000 Binary files a/pyramid/paster_templates/zodb/+package+/static/logo.png and /dev/null differ diff --git a/pyramid/paster_templates/zodb/+package+/static/middlebg.png b/pyramid/paster_templates/zodb/+package+/static/middlebg.png new file mode 100644 index 000000000..2369cfb7d Binary files /dev/null and b/pyramid/paster_templates/zodb/+package+/static/middlebg.png differ diff --git a/pyramid/paster_templates/zodb/+package+/static/pylons.css b/pyramid/paster_templates/zodb/+package+/static/pylons.css index c153be07f..33b21ac1a 100644 --- a/pyramid/paster_templates/zodb/+package+/static/pylons.css +++ b/pyramid/paster_templates/zodb/+package+/static/pylons.css @@ -4,34 +4,23 @@ body{line-height:1;} ol,ul{list-style:none;} blockquote,q{quotes:none;} blockquote:before,blockquote:after,q:before,q:after{content:'';content:none;} -/* remember to define focus styles! */ :focus{outline:0;} -/* remember to highlight inserts somehow! */ ins{text-decoration:none;} del{text-decoration:line-through;} -/* tables still need 'cellspacing="0"' in the markup */ table{border-collapse:collapse;border-spacing:0;} -/* restyling */ sub{vertical-align:sub;font-size:smaller;line-height:normal;} sup{vertical-align:super;font-size:smaller;line-height:normal;} -/* lists */ ul,menu,dir{display:block;list-style-type:disc;margin:1em 0;padding-left:40px;} ol{display:block;list-style-type:decimal-leading-zero;margin:1em 0;padding-left:40px;} li{display:list-item;} -/* nested lists have no top/bottom margins */ ul ul,ul ol,ul dir,ul menu,ul dl,ol ul,ol ol,ol dir,ol menu,ol dl,dir ul,dir ol,dir dir,dir menu,dir dl,menu ul,menu ol,menu dir,menu menu,menu dl,dl ul,dl ol,dl dir,dl menu,dl dl{margin-top:0;margin-bottom:0;} -/* 2 deep unordered lists use a circle */ ol ul,ul ul,menu ul,dir ul,ol menu,ul menu,menu menu,dir menu,ol dir,ul dir,menu dir,dir dir{list-style-type:circle;} -/* 3 deep (or more) unordered lists use a square */ ol ol ul,ol ul ul,ol menu ul,ol dir ul,ol ol menu,ol ul menu,ol menu menu,ol dir menu,ol ol dir,ol ul dir,ol menu dir,ol dir dir,ul ol ul,ul ul ul,ul menu ul,ul dir ul,ul ol menu,ul ul menu,ul menu menu,ul dir menu,ul ol dir,ul ul dir,ul menu dir,ul dir dir,menu ol ul,menu ul ul,menu menu ul,menu dir ul,menu ol menu,menu ul menu,menu menu menu,menu dir menu,menu ol dir,menu ul dir,menu menu dir,menu dir dir,dir ol ul,dir ul ul,dir menu ul,dir dir ul,dir ol menu,dir ul menu,dir menu menu,dir dir menu,dir ol dir,dir ul dir,dir menu dir,dir dir dir{list-style-type:square;} .hidden{display:none;} p{line-height:1.5em;} -h1{font-size:1.75em;/* 28px */ -line-height:1.7em;font-family:helvetica,verdana;} -h2{font-size:1.5em;/* 24px */ -line-height:1.7em;font-family:helvetica,verdana;} -h3{font-size:1.25em;/* 20px */ -line-height:1.7em;font-family:helvetica,verdana;} +h1{font-size:1.75em;line-height:1.7em;font-family:helvetica,verdana;} +h2{font-size:1.5em;line-height:1.7em;font-family:helvetica,verdana;} +h3{font-size:1.25em;line-height:1.7em;font-family:helvetica,verdana;} h4{font-size:1em;line-height:1.7em;font-family:helvetica,verdana;} html,body{width:100%;height:100%;} body{margin:0;padding:0;background-color:#ffffff;position:relative;font:16px/24px "Nobile","Lucida Grande",Lucida,Verdana,sans-serif;} @@ -42,23 +31,25 @@ body h2, body h3, body h4, body h5, -body h6{font-family:"Nobile","Lucida Grande",Lucida,Verdana,sans-serif;font-weight:normal;color:#144fb2;font-style:normal;} -#wrap {min-height: 100%;} -#header,#footer{width:100%;color:#ffffff;height:40px;position:absolute;text-align:center;line-height:40px;overflow:hidden;font-size:12px;} -#header{background-color:#e88f00;top:0;font-size:14px;} -#footer{background-color:#000000;bottom:0;position: relative;margin-top:-40px;clear:both;} -.header,.footer{width:700px;margin-right:auto;margin-left:auto;} +body h6{font-family:"Neuton","Lucida Grande",Lucida,Verdana,sans-serif;font-weight:normal;color:#373839;font-style:normal;} +#wrap{min-height:100%;} +#header,#footer{width:100%;color:#ffffff;height:40px;position:absolute;text-align:center;line-height:40px;overflow:hidden;font-size:12px;vertical-align:middle;} +#header{background:#000000;top:0;font-size:14px;} +#footer{bottom:0;background:#000000 url(footerbg.png) repeat-x 0 top;position:relative;margin-top:-40px;clear:both;} +.header,.footer{width:750px;margin-right:auto;margin-left:auto;} .wrapper{width:100%} #top,#bottom{width:100%;} -#top{color:#888;background-color:#eee;height:300px;border-bottom:2px solid #ddd;} -#bottom{color:#222;background-color:#ffffff;overflow:hidden;padding-bottom:80px;} -.top,.bottom{width:700px;margin-right:auto;margin-left:auto;} -.top{padding-top:100px;} +#top{color:#000000;height:230px; +background:#ffffff url(headerbg.png) repeat-x 0 top;position:relative;} +#bottom{color:#222;background-color:#ffffff;} +.top,.middle,.bottom{width:750px;margin-right:auto;margin-left:auto;} +.top{padding-top:40px;} +#middle{width:100%;height:100px;background:url(middlebg.png) repeat-x;border-top:2px solid #ffffff;border-bottom:2px solid #b2b2b2;} .app-welcome{margin-top:25px;} .app-name{color:#000000;font-weight:bold;} .bottom{padding-top:50px;} -#left{width:325px;float:left;padding-right:25px;} -#right{width:325px;float:right;padding-left:25px;} +#left{width:350px;float:left;padding-right:25px;} +#right{width:350px;float:right;padding-left:25px;} .align-left{text-align:left;} .align-right{text-align:right;} .align-center{text-align:center;} @@ -67,7 +58,7 @@ ul.links li{list-style-type:none;font-size:14px;} form{border-style:none;} fieldset{border-style:none;} input{color:#222;border:1px solid #ccc;font-family:sans-serif;font-size:12px;line-height:16px;} -input[type=text]{} +input[type=text]{width:205px;} input[type=submit]{background-color:#ddd;font-weight:bold;} /*Opera Fix*/ -body:before {content:"";height:100%;float:left;width:0;margin-top:-32767px;} +body:before{content:"";height:100%;float:left;width:0;margin-top:-32767px;} diff --git a/pyramid/paster_templates/zodb/+package+/static/pyramid.png b/pyramid/paster_templates/zodb/+package+/static/pyramid.png new file mode 100644 index 000000000..347e05549 Binary files /dev/null and b/pyramid/paster_templates/zodb/+package+/static/pyramid.png differ diff --git a/pyramid/paster_templates/zodb/+package+/static/transparent.gif b/pyramid/paster_templates/zodb/+package+/static/transparent.gif new file mode 100644 index 000000000..0341802e5 Binary files /dev/null and b/pyramid/paster_templates/zodb/+package+/static/transparent.gif differ diff --git a/pyramid/paster_templates/zodb/+package+/templates/mytemplate.pt b/pyramid/paster_templates/zodb/+package+/templates/mytemplate.pt deleted file mode 100644 index 6ad23d44f..000000000 --- a/pyramid/paster_templates/zodb/+package+/templates/mytemplate.pt +++ /dev/null @@ -1,79 +0,0 @@ - - - - The Pyramid Web Application Development Framework - - - - - - - - - -
- -
-
- -

- Welcome to ${project}, an application generated by
- the Pyramid web application development framework. -

-
-
-
-
-
-

Search Pyramid documentation

-
- - -
-
- -
-
-
- - - \ No newline at end of file diff --git a/pyramid/paster_templates/zodb/+package+/templates/mytemplate.pt_tmpl b/pyramid/paster_templates/zodb/+package+/templates/mytemplate.pt_tmpl new file mode 100644 index 000000000..440b9476c --- /dev/null +++ b/pyramid/paster_templates/zodb/+package+/templates/mytemplate.pt_tmpl @@ -0,0 +1,76 @@ + + + + The Pyramid Web Application Development Framework + + + + + + + + + + +
+
+
+
pyramid
+
+
+
+
+

+ Welcome to ${project}, an application generated by
+ the Pyramid web application development framework. +

+
+
+
+
+
+

Search documentation

+
+ + +
+
+ +
+
+
+ + + \ No newline at end of file -- cgit v1.2.3 From 76d25aad2222cfd401d83dff07a0d7dfd14bd5a9 Mon Sep 17 00:00:00 2001 From: Blaise Laflamme Date: Wed, 5 Jan 2011 20:22:51 -0500 Subject: Removed unused files --- .../alchemy/+package+/templates/model.pt | 85 ---------------------- .../alchemy/+package+/templates/root.pt | 83 --------------------- .../+package+/templates/mytemplate.mako | 79 -------------------- .../+package+/templates/mytemplate.mako | 81 --------------------- .../starter/+package+/templates/mytemplate.pt | 76 ------------------- 5 files changed, 404 deletions(-) delete mode 100644 pyramid/paster_templates/alchemy/+package+/templates/model.pt delete mode 100644 pyramid/paster_templates/alchemy/+package+/templates/root.pt delete mode 100644 pyramid/paster_templates/pylons_minimal/+package+/templates/mytemplate.mako delete mode 100644 pyramid/paster_templates/pylons_sqla/+package+/templates/mytemplate.mako delete mode 100644 pyramid/paster_templates/starter/+package+/templates/mytemplate.pt diff --git a/pyramid/paster_templates/alchemy/+package+/templates/model.pt b/pyramid/paster_templates/alchemy/+package+/templates/model.pt deleted file mode 100644 index 2d54d969e..000000000 --- a/pyramid/paster_templates/alchemy/+package+/templates/model.pt +++ /dev/null @@ -1,85 +0,0 @@ - - - - The Pyramid Web Application Development Framework - - - - - - - - - -
- -
-
- -

- Welcome to ${project}, an application generated by
- the Pyramid web application development framework. -

-
-
-
-
-
-

Search Pyramid documentation

-
- - -
-
-

- Id: ${item.id}
- Name: ${item.name}
- Value: ${item.value} -

-
- -
-
-
- - - \ No newline at end of file diff --git a/pyramid/paster_templates/alchemy/+package+/templates/root.pt b/pyramid/paster_templates/alchemy/+package+/templates/root.pt deleted file mode 100644 index 7dda6a033..000000000 --- a/pyramid/paster_templates/alchemy/+package+/templates/root.pt +++ /dev/null @@ -1,83 +0,0 @@ - - - - The Pyramid Web Application Development Framework - - - - - - - - - -
- -
-
- -

- Welcome to ${project}, an application generated by
- the Pyramid web application development framework. -

-
-
-
-
-
-

Search Pyramid documentation

-
- - -
-
-

- ${item.name} -

-
- -
-
-
- - - \ No newline at end of file diff --git a/pyramid/paster_templates/pylons_minimal/+package+/templates/mytemplate.mako b/pyramid/paster_templates/pylons_minimal/+package+/templates/mytemplate.mako deleted file mode 100644 index 6ad23d44f..000000000 --- a/pyramid/paster_templates/pylons_minimal/+package+/templates/mytemplate.mako +++ /dev/null @@ -1,79 +0,0 @@ - - - - The Pyramid Web Application Development Framework - - - - - - - - - -
- -
-
- -

- Welcome to ${project}, an application generated by
- the Pyramid web application development framework. -

-
-
-
-
-
-

Search Pyramid documentation

-
- - -
-
- -
-
-
- - - \ No newline at end of file diff --git a/pyramid/paster_templates/pylons_sqla/+package+/templates/mytemplate.mako b/pyramid/paster_templates/pylons_sqla/+package+/templates/mytemplate.mako deleted file mode 100644 index 8b65d6a2b..000000000 --- a/pyramid/paster_templates/pylons_sqla/+package+/templates/mytemplate.mako +++ /dev/null @@ -1,81 +0,0 @@ - - - - The Pyramid Web Application Development Framework - - - - - - - - - -
- -
-
- -

- Welcome to ${project}, an application generated by
- the Pyramid web application development framework. -

-
-
-
-
-
-

Search Pyramid documentation

-
- - -
-
-

The root object's name is "${root.name}"

-
- -
-
-
- - - \ No newline at end of file diff --git a/pyramid/paster_templates/starter/+package+/templates/mytemplate.pt b/pyramid/paster_templates/starter/+package+/templates/mytemplate.pt deleted file mode 100644 index 02fc00eeb..000000000 --- a/pyramid/paster_templates/starter/+package+/templates/mytemplate.pt +++ /dev/null @@ -1,76 +0,0 @@ - - - - The Pyramid Web Application Development Framework - - - - - - - - - - -
-
-
-
pyramid
-
-
-
-
-

- Welcome to ${project}, an application generated by
- the Pyramid web application development framework. -

-
-
-
-
-
-

Search documentation

-
- - -
-
- -
-
-
- - - \ No newline at end of file -- cgit v1.2.3 From 17f57b7253757e91b7055b4aab50ab4b78fc99a7 Mon Sep 17 00:00:00 2001 From: Blaise Laflamme Date: Wed, 5 Jan 2011 22:31:32 -0500 Subject: Normalized stylesheet tags --- pyramid/paster_templates/alchemy/+package+/templates/model.pt_tmpl | 2 +- pyramid/paster_templates/alchemy/+package+/templates/root.pt_tmpl | 2 +- .../pylons_basic/+package+/templates/mytemplate.mako_tmpl | 2 +- .../pylons_minimal/+package+/templates/mytemplate.mako_tmpl | 2 +- .../pylons_sqla/+package+/templates/mytemplate.mako_tmpl | 2 +- .../routesalchemy/+package+/templates/mytemplate.pt_tmpl | 2 +- pyramid/paster_templates/starter/+package+/templates/mytemplate.pt_tmpl | 2 +- .../starter_zcml/+package+/templates/mytemplate.pt_tmpl | 2 +- pyramid/paster_templates/zodb/+package+/templates/mytemplate.pt_tmpl | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pyramid/paster_templates/alchemy/+package+/templates/model.pt_tmpl b/pyramid/paster_templates/alchemy/+package+/templates/model.pt_tmpl index 9cfe1d185..1fd3c0b20 100644 --- a/pyramid/paster_templates/alchemy/+package+/templates/model.pt_tmpl +++ b/pyramid/paster_templates/alchemy/+package+/templates/model.pt_tmpl @@ -7,7 +7,7 @@ - + - - -
-
Viewing - Page Name Goes Here
- You can return to the FrontPage. - Logout -
- -
-
-