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 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 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 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 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