From 45f5751cdf8f88ff77f4bd9c30a2f97dcac1b6e0 Mon Sep 17 00:00:00 2001 From: Toni Mueller Date: Tue, 20 Mar 2012 14:46:49 +0100 Subject: generate more common filenames for sqlite --- pyramid/scaffolds/alchemy/development.ini_tmpl | 2 +- pyramid/scaffolds/alchemy/production.ini_tmpl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyramid/scaffolds/alchemy/development.ini_tmpl b/pyramid/scaffolds/alchemy/development.ini_tmpl index bcba06c1c..eebfbcc3e 100644 --- a/pyramid/scaffolds/alchemy/development.ini_tmpl +++ b/pyramid/scaffolds/alchemy/development.ini_tmpl @@ -10,7 +10,7 @@ pyramid.includes = pyramid_debugtoolbar pyramid_tm -sqlalchemy.url = sqlite:///%(here)s/{{project}}.db +sqlalchemy.url = sqlite:///%(here)s/{{project}}.sqlite [server:main] use = egg:waitress#main diff --git a/pyramid/scaffolds/alchemy/production.ini_tmpl b/pyramid/scaffolds/alchemy/production.ini_tmpl index dc9145d12..9488f1811 100644 --- a/pyramid/scaffolds/alchemy/production.ini_tmpl +++ b/pyramid/scaffolds/alchemy/production.ini_tmpl @@ -9,7 +9,7 @@ pyramid.default_locale_name = en pyramid.includes = pyramid_tm -sqlalchemy.url = sqlite:///%(here)s/{{project}}.db +sqlalchemy.url = sqlite:///%(here)s/{{project}}.sqlite [server:main] use = egg:waitress#main -- cgit v1.2.3 From 4e3de46840501d20472e5e54510ed598906c0b21 Mon Sep 17 00:00:00 2001 From: Toni Mueller Date: Tue, 20 Mar 2012 14:46:49 +0100 Subject: generate more common filenames for sqlite and patch the wiki2 tutorial, too --- docs/tutorials/wiki2/installation.rst | 2 +- docs/tutorials/wiki2/src/authorization/development.ini | 2 +- docs/tutorials/wiki2/src/authorization/production.ini | 2 +- docs/tutorials/wiki2/src/basiclayout/development.ini | 2 +- docs/tutorials/wiki2/src/basiclayout/production.ini | 2 +- docs/tutorials/wiki2/src/models/development.ini | 2 +- docs/tutorials/wiki2/src/models/production.ini | 2 +- docs/tutorials/wiki2/src/tests/development.ini | 2 +- docs/tutorials/wiki2/src/tests/production.ini | 2 +- docs/tutorials/wiki2/src/views/development.ini | 2 +- docs/tutorials/wiki2/src/views/production.ini | 2 +- pyramid/scaffolds/alchemy/development.ini_tmpl | 2 +- pyramid/scaffolds/alchemy/production.ini_tmpl | 2 +- 13 files changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/tutorials/wiki2/installation.rst b/docs/tutorials/wiki2/installation.rst index 4ee2728c2..40486057e 100644 --- a/docs/tutorials/wiki2/installation.rst +++ b/docs/tutorials/wiki2/installation.rst @@ -254,7 +254,7 @@ The output to your console should be something like this:: 2011-11-26 14:42:25,140 INFO [sqlalchemy.engine.base.Engine][MainThread] COMMIT -Success! You should now have a ``tutorial.db`` file in your current working +Success! You should now have a ``tutorial.sqlite`` file in your current working directory. This will be a SQLite database with a single table defined in it (``models``). diff --git a/docs/tutorials/wiki2/src/authorization/development.ini b/docs/tutorials/wiki2/src/authorization/development.ini index 38738f3c6..eb2f878c5 100644 --- a/docs/tutorials/wiki2/src/authorization/development.ini +++ b/docs/tutorials/wiki2/src/authorization/development.ini @@ -10,7 +10,7 @@ pyramid.includes = pyramid_debugtoolbar pyramid_tm -sqlalchemy.url = sqlite:///%(here)s/tutorial.db +sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite [server:main] use = egg:waitress#main diff --git a/docs/tutorials/wiki2/src/authorization/production.ini b/docs/tutorials/wiki2/src/authorization/production.ini index c4034abad..4684d2f7a 100644 --- a/docs/tutorials/wiki2/src/authorization/production.ini +++ b/docs/tutorials/wiki2/src/authorization/production.ini @@ -9,7 +9,7 @@ pyramid.default_locale_name = en pyramid.includes = pyramid_tm -sqlalchemy.url = sqlite:///%(here)s/tutorial.db +sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite [server:main] use = egg:waitress#main diff --git a/docs/tutorials/wiki2/src/basiclayout/development.ini b/docs/tutorials/wiki2/src/basiclayout/development.ini index 38738f3c6..eb2f878c5 100644 --- a/docs/tutorials/wiki2/src/basiclayout/development.ini +++ b/docs/tutorials/wiki2/src/basiclayout/development.ini @@ -10,7 +10,7 @@ pyramid.includes = pyramid_debugtoolbar pyramid_tm -sqlalchemy.url = sqlite:///%(here)s/tutorial.db +sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite [server:main] use = egg:waitress#main diff --git a/docs/tutorials/wiki2/src/basiclayout/production.ini b/docs/tutorials/wiki2/src/basiclayout/production.ini index c4034abad..4684d2f7a 100644 --- a/docs/tutorials/wiki2/src/basiclayout/production.ini +++ b/docs/tutorials/wiki2/src/basiclayout/production.ini @@ -9,7 +9,7 @@ pyramid.default_locale_name = en pyramid.includes = pyramid_tm -sqlalchemy.url = sqlite:///%(here)s/tutorial.db +sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite [server:main] use = egg:waitress#main diff --git a/docs/tutorials/wiki2/src/models/development.ini b/docs/tutorials/wiki2/src/models/development.ini index 38738f3c6..eb2f878c5 100644 --- a/docs/tutorials/wiki2/src/models/development.ini +++ b/docs/tutorials/wiki2/src/models/development.ini @@ -10,7 +10,7 @@ pyramid.includes = pyramid_debugtoolbar pyramid_tm -sqlalchemy.url = sqlite:///%(here)s/tutorial.db +sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite [server:main] use = egg:waitress#main diff --git a/docs/tutorials/wiki2/src/models/production.ini b/docs/tutorials/wiki2/src/models/production.ini index c4034abad..4684d2f7a 100644 --- a/docs/tutorials/wiki2/src/models/production.ini +++ b/docs/tutorials/wiki2/src/models/production.ini @@ -9,7 +9,7 @@ pyramid.default_locale_name = en pyramid.includes = pyramid_tm -sqlalchemy.url = sqlite:///%(here)s/tutorial.db +sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite [server:main] use = egg:waitress#main diff --git a/docs/tutorials/wiki2/src/tests/development.ini b/docs/tutorials/wiki2/src/tests/development.ini index 38738f3c6..eb2f878c5 100644 --- a/docs/tutorials/wiki2/src/tests/development.ini +++ b/docs/tutorials/wiki2/src/tests/development.ini @@ -10,7 +10,7 @@ pyramid.includes = pyramid_debugtoolbar pyramid_tm -sqlalchemy.url = sqlite:///%(here)s/tutorial.db +sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite [server:main] use = egg:waitress#main diff --git a/docs/tutorials/wiki2/src/tests/production.ini b/docs/tutorials/wiki2/src/tests/production.ini index c4034abad..4684d2f7a 100644 --- a/docs/tutorials/wiki2/src/tests/production.ini +++ b/docs/tutorials/wiki2/src/tests/production.ini @@ -9,7 +9,7 @@ pyramid.default_locale_name = en pyramid.includes = pyramid_tm -sqlalchemy.url = sqlite:///%(here)s/tutorial.db +sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite [server:main] use = egg:waitress#main diff --git a/docs/tutorials/wiki2/src/views/development.ini b/docs/tutorials/wiki2/src/views/development.ini index 38738f3c6..eb2f878c5 100644 --- a/docs/tutorials/wiki2/src/views/development.ini +++ b/docs/tutorials/wiki2/src/views/development.ini @@ -10,7 +10,7 @@ pyramid.includes = pyramid_debugtoolbar pyramid_tm -sqlalchemy.url = sqlite:///%(here)s/tutorial.db +sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite [server:main] use = egg:waitress#main diff --git a/docs/tutorials/wiki2/src/views/production.ini b/docs/tutorials/wiki2/src/views/production.ini index c4034abad..4684d2f7a 100644 --- a/docs/tutorials/wiki2/src/views/production.ini +++ b/docs/tutorials/wiki2/src/views/production.ini @@ -9,7 +9,7 @@ pyramid.default_locale_name = en pyramid.includes = pyramid_tm -sqlalchemy.url = sqlite:///%(here)s/tutorial.db +sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite [server:main] use = egg:waitress#main diff --git a/pyramid/scaffolds/alchemy/development.ini_tmpl b/pyramid/scaffolds/alchemy/development.ini_tmpl index bcba06c1c..eebfbcc3e 100644 --- a/pyramid/scaffolds/alchemy/development.ini_tmpl +++ b/pyramid/scaffolds/alchemy/development.ini_tmpl @@ -10,7 +10,7 @@ pyramid.includes = pyramid_debugtoolbar pyramid_tm -sqlalchemy.url = sqlite:///%(here)s/{{project}}.db +sqlalchemy.url = sqlite:///%(here)s/{{project}}.sqlite [server:main] use = egg:waitress#main diff --git a/pyramid/scaffolds/alchemy/production.ini_tmpl b/pyramid/scaffolds/alchemy/production.ini_tmpl index dc9145d12..9488f1811 100644 --- a/pyramid/scaffolds/alchemy/production.ini_tmpl +++ b/pyramid/scaffolds/alchemy/production.ini_tmpl @@ -9,7 +9,7 @@ pyramid.default_locale_name = en pyramid.includes = pyramid_tm -sqlalchemy.url = sqlite:///%(here)s/{{project}}.db +sqlalchemy.url = sqlite:///%(here)s/{{project}}.sqlite [server:main] use = egg:waitress#main -- cgit v1.2.3 From c52e649c6773a99d6077f28ebe0f505d32e72ba6 Mon Sep 17 00:00:00 2001 From: Arndt Droullier Date: Fri, 30 Mar 2012 15:23:55 +0300 Subject: Use predicate.__text__ in predicate mismatch exceptions. See Issue #502 --- pyramid/config/views.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index ad4df28d8..ac41f7363 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -276,11 +276,13 @@ class ViewDeriver(object): if not predicates: return view def predicate_wrapper(context, request): - if all((predicate(context, request) for predicate in predicates)): - return view(context, request) - view_name = getattr(view, '__name__', view) - raise PredicateMismatch( - 'predicate mismatch for view %s' % view_name) + for predicate in predicates: + if not predicate(context, request): + view_name = getattr(view, '__name__', view) + raise PredicateMismatch( + 'predicate mismatch for view %s (%s)' % ( + view_name, predicate.__text__)) + return view(context, request) def checker(context, request): return all((predicate(context, request) for predicate in predicates)) -- cgit v1.2.3 From 7c3cf41997cb9c903d7ca8a712062f3846381ead Mon Sep 17 00:00:00 2001 From: Veeti Paananen Date: Fri, 13 Apr 2012 00:40:06 +0300 Subject: Fix a typo in the documentation --- docs/narr/project.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/narr/project.rst b/docs/narr/project.rst index 57073900f..d18d93605 100644 --- a/docs/narr/project.rst +++ b/docs/narr/project.rst @@ -447,7 +447,7 @@ first column instead, for example like this: pyramid.includes = #pyramid_debugtoolbar -When you attempt to restart the application with a section like the abvoe +When you attempt to restart the application with a section like the above you'll receive an error that ends something like this, and the application will not start: -- cgit v1.2.3 From 8e7df02fb76a0ce965254141918d6c405bcaa264 Mon Sep 17 00:00:00 2001 From: Patricio Paez Date: Thu, 12 Apr 2012 21:51:42 -0500 Subject: Improve the Adding Templates section in both tutorials - As suggested by Paulo in the PyCon sprint --- docs/tutorials/wiki/definingviews.rst | 62 ++++++++++++++++++-------------- docs/tutorials/wiki2/definingviews.rst | 66 +++++++++++++++++++--------------- 2 files changed, 73 insertions(+), 55 deletions(-) diff --git a/docs/tutorials/wiki/definingviews.rst b/docs/tutorials/wiki/definingviews.rst index 12bfa8b84..9eeee1758 100644 --- a/docs/tutorials/wiki/definingviews.rst +++ b/docs/tutorials/wiki/definingviews.rst @@ -243,46 +243,54 @@ recognized as such. The ``view.pt`` Template ------------------------ -The ``view.pt`` template is used for viewing a single Page. It is used by -the ``view_page`` view function. It should have a div that is "structure -replaced" with the ``content`` value provided by the view. It should also -have a link on the rendered page that points at the "edit" URL (the URL which -invokes the ``edit_page`` view for the page being viewed). - -Once we're done with the ``view.pt`` template, it will look a lot like -the below: +Create ``tutorial/tutorial/templates/view.pt`` and add the following +content: .. literalinclude:: src/views/tutorial/templates/view.pt + :linenos: :language: xml -.. note:: +This template is used by ``view_page()`` for displaying a single +wiki page. It includes: - The names available for our use in a template are always those that - are present in the dictionary returned by the view callable. But our - templates make use of a ``request`` object that none of our tutorial views - return in their dictionary. This value appears as if "by magic". - However, ``request`` is one of several names that are available "by - default" in a template when a template renderer is used. See - :ref:`chameleon_template_renderers` for more information about other names - that are available by default in a template when a template is used as a - renderer. +- A ``div`` element that is replaced with the ``content`` + value provided by the view (rows 45-47). ``content`` + contains HTML, so the ``structure`` keyword is used + to prevent escaping it (i.e. changing ">" to >, etc.) +- A link that points + at the "edit" URL which invokes the ``edit_page`` view for + the page being viewed (rows 49-51). The ``edit.pt`` Template ------------------------ -The ``edit.pt`` template is used for adding and editing a Page. It is used -by the ``add_page`` and ``edit_page`` view functions. It should display a -page containing a form that POSTs back to the "save_url" argument supplied by -the view. The form should have a "body" textarea field (the page data), and -a submit button that has the name "form.submitted". The textarea in the form -should be filled with any existing page data when it is rendered. - -Once we're done with the ``edit.pt`` template, it will look a lot like the -below: +Create ``tutorial/tutorial/templates/edit.pt`` and add the following +content: .. literalinclude:: src/views/tutorial/templates/edit.pt + :linenos: :language: xml +This template is used by ``add_page()`` and ``edit_page()`` for adding +and editing a wiki page. It displays +a page containing a form that includes: + +- A 10 row by 60 column ``textarea`` field named ``body`` that is filled + with any existing page data when it is rendered (rows 46-47). +- A submit button that has the name ``form.submitted`` (row 48). + +The form POSTs back to the "save_url" argument supplied +by the view (row 45). The view will use the ``body`` and +``form.submitted`` values. + +.. note:: Our templates use a ``request`` object that + none of our tutorial views return in their dictionary. + ``request`` is one of several + names that are available "by default" in a template when a template + renderer is used. See :ref:`chameleon_template_renderers` for + information about other names that are available by default + when a Chameleon template is used as a renderer. + Static Assets ------------- diff --git a/docs/tutorials/wiki2/definingviews.rst b/docs/tutorials/wiki2/definingviews.rst index ac58e1e46..a2c8ba8c5 100644 --- a/docs/tutorials/wiki2/definingviews.rst +++ b/docs/tutorials/wiki2/definingviews.rst @@ -228,50 +228,60 @@ Adding Templates The views we've added all reference a :term:`template`. Each template is a :term:`Chameleon` :term:`ZPT` template. These templates will live in the -``templates`` directory of our tutorial package. +``templates`` directory of our tutorial package. Chameleon templates +must have a ``.pt`` extension to be recognized as such. The ``view.pt`` Template ------------------------ -The ``view.pt`` template is used for viewing a single wiki page. It -is used by the ``view_page`` view function. It should have a ``div`` -that is "structure replaced" with the ``content`` value provided by -the view. It should also have a link on the rendered page that points -at the "edit" URL (the URL which invokes the ``edit_page`` view for -the page being viewed). - -Once we're done with the ``view.pt`` template, it will look a lot like the -below: +Create ``tutorial/tutorial/templates/view.pt`` and add the following +content: .. literalinclude:: src/views/tutorial/templates/view.pt + :linenos: :language: xml -.. note:: The names available for our use in a template are always - those that are present in the dictionary returned by the view - callable. But our templates make use of a ``request`` object that - none of our tutorial views return in their dictionary. This value - appears as if "by magic". However, ``request`` is one of several - names that are available "by default" in a template when a template - renderer is used. See :ref:`chameleon_template_renderers` for more - information about other names that are available by default in a - template when a Chameleon template is used as a renderer. +This template is used by ``view_page()`` for displaying a single +wiki page. It includes: + +- A ``div`` element that is replaced with the ``content`` + value provided by the view (rows 45-47). ``content`` + contains HTML, so the ``structure`` keyword is used + to prevent escaping it (i.e. changing ">" to >, etc.) +- A link that points + at the "edit" URL which invokes the ``edit_page`` view for + the page being viewed (rows 49-51). The ``edit.pt`` Template ------------------------ -The ``edit.pt`` template is used for adding and editing a wiki page. It is -used by the ``add_page`` and ``edit_page`` view functions. It should display -a page containing a form that POSTs back to the "save_url" argument supplied -by the view. The form should have a "body" textarea field (the page data), -and a submit button that has the name "form.submitted". The textarea in the -form should be filled with any existing page data when it is rendered. - -Once we're done with the ``edit.pt`` template, it will look a lot like -the following: +Create ``tutorial/tutorial/templates/edit.pt`` and add the following +content: .. literalinclude:: src/views/tutorial/templates/edit.pt + :linenos: :language: xml +This template is used by ``add_page()`` and ``edit_page()`` for adding +and editing a wiki page. It displays +a page containing a form that includes: + +- A 10 row by 60 column ``textarea`` field named ``body`` that is filled + with any existing page data when it is rendered (rows 46-47). +- A submit button that has the name ``form.submitted`` (row 48). + +The form POSTs back to the "save_url" argument supplied +by the view (row 45). The view will use the ``body`` and +``form.submitted`` values. + +.. note:: Our templates use a ``request`` object that + none of our tutorial views return in their dictionary. + ``request`` is one of several + names that are available "by default" in a template when a template + renderer is used. See :ref:`chameleon_template_renderers` for + information about other names that are available by default + when a Chameleon template is used as a renderer. + Static Assets ------------- -- cgit v1.2.3 From bce621a99ee495d8d82f744eaa209ae0f1ac504e Mon Sep 17 00:00:00 2001 From: Patricio Paez Date: Fri, 6 Apr 2012 18:22:13 -0500 Subject: Typos --- docs/narr/hooks.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index b6e3dd163..a2143b3c5 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -145,7 +145,7 @@ the view which generates it can be overridden as necessary. The :term:`forbidden view` callable is a view callable like any other. The :term:`view configuration` which causes it to be a "forbidden" view consists -of using the meth:`pyramid.config.Configurator.add_forbidden_view` API or the +of using the :meth:`pyramid.config.Configurator.add_forbidden_view` API or the :class:`pyramid.view.forbidden_view_config` decorator. For example, you can add a forbidden view by using the @@ -171,7 +171,7 @@ as a forbidden view: from pyramid.view import forbidden_view_config - forbidden_view_config() + @forbidden_view_config() def forbidden(request): return Response('forbidden') @@ -625,7 +625,7 @@ converts the arbitrary return value into something that implements :class:`~pyramid.interfaces.IResponse`. For example, if you'd like to allow view callables to return bare string -objects (without requiring a a :term:`renderer` to convert a string to a +objects (without requiring a :term:`renderer` to convert a string to a response object), you can register an adapter which converts the string to a Response: -- cgit v1.2.3 From 6751531f0eb874cb70f5f7decc129f592359311c Mon Sep 17 00:00:00 2001 From: Patricio Paez Date: Fri, 13 Apr 2012 06:34:18 -0500 Subject: Fixed wsgi.org URL --- docs/glossary.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/glossary.rst b/docs/glossary.rst index 60920a73a..88598354a 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -290,7 +290,7 @@ Glossary :term:`principal` (or principals) associated with a request. WSGI - `Web Server Gateway Interface `_. This is a + `Web Server Gateway Interface `_. This is a Python standard for connecting web applications to web servers, similar to the concept of Java Servlets. :app:`Pyramid` requires that your application be served as a WSGI application. @@ -299,7 +299,7 @@ Glossary *Middleware* is a :term:`WSGI` concept. It is a WSGI component that acts both as a server and an application. Interesting uses for middleware exist, such as caching, content-transport - encoding, and other functions. See `WSGI.org `_ + encoding, and other functions. See `WSGI.org `_ or `PyPI `_ to find middleware for your application. -- cgit v1.2.3 From f97aa6332c08d5edb35665a7afda2aefd98f662b Mon Sep 17 00:00:00 2001 From: Patricio Paez Date: Fri, 13 Apr 2012 13:10:00 -0500 Subject: Simplify the introduction - Moved the selection of the templates to the Design chapter - Improved the Views section in the Design chapter - Normalize in both tutorials --- docs/tutorials/wiki/definingviews.rst | 11 +++-------- docs/tutorials/wiki/design.rst | 19 ++++++++++++++----- docs/tutorials/wiki2/definingviews.rst | 9 +++++---- docs/tutorials/wiki2/design.rst | 15 ++++++++++++--- 4 files changed, 34 insertions(+), 20 deletions(-) diff --git a/docs/tutorials/wiki/definingviews.rst b/docs/tutorials/wiki/definingviews.rst index 9eeee1758..32c81f057 100644 --- a/docs/tutorials/wiki/definingviews.rst +++ b/docs/tutorials/wiki/definingviews.rst @@ -229,14 +229,9 @@ this: Adding Templates ================ -Most view callables we've added expected to be rendered via a -:term:`template`. The default templating systems in :app:`Pyramid` are -:term:`Chameleon` and :term:`Mako`. Chameleon is a variant of :term:`ZPT`, -which is an XML-based templating language. Mako is a non-XML-based -templating language. Because we had to pick one, we chose Chameleon for this -tutorial. - -The templates we create will live in the ``templates`` directory of our +The ``view_page``, ``add_page`` and ``edit_page`` views that we've added +reference a :term:`template`. Each template is a :term:`Chameleon` :term:`ZPT` +template. These templates will live in the ``templates`` directory of our tutorial package. Chameleon templates must have a ``.pt`` extension to be recognized as such. diff --git a/docs/tutorials/wiki/design.rst b/docs/tutorials/wiki/design.rst index 2b613377a..c94612fb1 100644 --- a/docs/tutorials/wiki/design.rst +++ b/docs/tutorials/wiki/design.rst @@ -36,9 +36,16 @@ be used as the wiki home page. Views ----- -There will be four views to handle the normal operations of -viewing, editing and adding wiki pages. Two additional views -will handle the login and logout tasks related to security. +There will be three views to handle the normal operations of adding, +editing and viewing wiki pages, plus one view for the wiki front page. +Two templates will be used, one for viewing, and one for both for adding +and editing wiki pages. + +The default templating systems in :app:`Pyramid` are +:term:`Chameleon` and :term:`Mako`. Chameleon is a variant of +:term:`ZPT`, which is an XML-based templating language. Mako is a +non-XML-based templating language. Because we had to pick one, +we chose Chameleon for this tutorial. Security -------- @@ -52,11 +59,11 @@ use to do this are below. - GROUPS, a dictionary mapping user names to a list of groups they belong to. -- *groupfinder*, an *authorization callback* that looks up +- ``groupfinder``, an *authorization callback* that looks up USERS and GROUPS. It will be provided in a new *security.py* file. -- An :term:`ACL` is attached to the root resource. Each +- An :term:`ACL` is attached to the root :term:`resource`. Each row below details an :term:`ACE`: +----------+----------------+----------------+ @@ -70,6 +77,8 @@ use to do this are below. - Permission declarations are added to the views to assert the security policies as each request is handled. +Two additional views and one template will handle the login and +logout tasks. Summary ------- diff --git a/docs/tutorials/wiki2/definingviews.rst b/docs/tutorials/wiki2/definingviews.rst index a2c8ba8c5..84017ce5a 100644 --- a/docs/tutorials/wiki2/definingviews.rst +++ b/docs/tutorials/wiki2/definingviews.rst @@ -226,10 +226,11 @@ of the wiki page. Adding Templates ================ -The views we've added all reference a :term:`template`. Each template is a -:term:`Chameleon` :term:`ZPT` template. These templates will live in the -``templates`` directory of our tutorial package. Chameleon templates -must have a ``.pt`` extension to be recognized as such. +The ``view_page``, ``add_page`` and ``edit_page`` views that we've added +reference a :term:`template`. Each template is a :term:`Chameleon` :term:`ZPT` +template. These templates will live in the ``templates`` directory of our +tutorial package. Chameleon templates must have a ``.pt`` extension to be +recognized as such. The ``view.pt`` Template ------------------------ diff --git a/docs/tutorials/wiki2/design.rst b/docs/tutorials/wiki2/design.rst index 4481153a3..2e6fc0e77 100644 --- a/docs/tutorials/wiki2/design.rst +++ b/docs/tutorials/wiki2/design.rst @@ -36,9 +36,16 @@ page. Views ----- -There will be four views to handle the normal operations of adding and -editing wiki pages, and viewing pages and the wiki front page. Two -additional views will handle the login and logout tasks related to security. +There will be three views to handle the normal operations of adding, +editing and viewing wiki pages, plus one view for the wiki front page. +Two templates will be used, one for viewing, and one for both for adding +and editing wiki pages. + +The default templating systems in :app:`Pyramid` are +:term:`Chameleon` and :term:`Mako`. Chameleon is a variant of +:term:`ZPT`, which is an XML-based templating language. Mako is a +non-XML-based templating language. Because we had to pick one, +we chose Chameleon for this tutorial. Security -------- @@ -67,6 +74,8 @@ use to do this are below. - Permission declarations are added to the views to assert the security policies as each request is handled. +Two additional views and one template will handle the login and +logout tasks. Summary ------- -- cgit v1.2.3 From 4aafed62140a7e3937b7e2fd6c349a771b07e25c Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 16 Apr 2012 10:28:34 -0500 Subject: replaced pylons with pyramid trove classifier --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f8c8c41e9..e325b8a29 100644 --- a/setup.py +++ b/setup.py @@ -77,7 +77,7 @@ setup(name='pyramid', "Programming Language :: Python :: 3.2", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", - "Framework :: Pylons", + "Framework :: Pyramid", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: WSGI", "License :: Repoze Public License", -- cgit v1.2.3 From 2861f1df38b79f4a1e31b5ac1bd86acd8c25928e Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 16 Apr 2012 11:48:14 -0400 Subject: garden --- TODO.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/TODO.txt b/TODO.txt index 544ad0b4e..e0fb0fa27 100644 --- a/TODO.txt +++ b/TODO.txt @@ -4,6 +4,9 @@ Pyramid TODOs Nice-to-Have ------------ +- Provide the presumed renderer name to the called view as an attribute of + the request. + - Have action methods return their discriminators. - Add docs about upgrading between Pyramid versions (e.g. how to see -- cgit v1.2.3 From 61a378024eb8231290c8bfd0a8977ae9b9065204 Mon Sep 17 00:00:00 2001 From: Christopher Lambacher Date: Mon, 16 Apr 2012 13:15:33 -0400 Subject: Fix a typo in contributors file. --- CONTRIBUTORS.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 365e3e455..4b780d3a7 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -127,7 +127,7 @@ Contributors - Wichert Akkerman, 2011/01/19 -- Christopher Lambacehr, 2011/02/12 +- Christopher Lambacher, 2011/02/12 - Malthe Borch, 2011/02/28 -- cgit v1.2.3 From 607524abda505e53a9851026e8e9d16de7b58053 Mon Sep 17 00:00:00 2001 From: Patricio Paez Date: Mon, 16 Apr 2012 19:21:22 -0500 Subject: Normalize and update the last section --- docs/tutorials/wiki/definingviews.rst | 19 ++++++++++--------- docs/tutorials/wiki2/definingviews.rst | 21 ++++++++++----------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/docs/tutorials/wiki/definingviews.rst b/docs/tutorials/wiki/definingviews.rst index 32c81f057..28cecb787 100644 --- a/docs/tutorials/wiki/definingviews.rst +++ b/docs/tutorials/wiki/definingviews.rst @@ -305,24 +305,25 @@ Viewing the Application in a Browser ==================================== We can finally examine our application in a browser (See -:ref:`wiki-start-the-application`). The views we'll try are as follows: +:ref:`wiki-start-the-application`). Launch a browser and visit +each of the following URLs, check that the result is as expected: -- Visiting ``http://localhost:6543/`` in a browser invokes the ``view_wiki`` +- ``http://localhost:6543/`` invokes the ``view_wiki`` view. This always redirects to the ``view_page`` view of the ``FrontPage`` Page resource. -- Visiting ``http://localhost:6543/FrontPage/`` in a browser invokes +- ``http://localhost:6543/FrontPage/`` invokes the ``view_page`` view of the front page resource. This is - because it's the *default view* (a view without a ``name``) for Page + because it's the :term:`default view` (a view without a ``name``) for Page resources. -- Visiting ``http://localhost:6543/FrontPage/edit_page`` in a browser +- ``http://localhost:6543/FrontPage/edit_page`` invokes the edit view for the ``FrontPage`` Page resource. -- Visiting ``http://localhost:6543/add_page/SomePageName`` in a - browser invokes the add view for a Page. +- ``http://localhost:6543/add_page/SomePageName`` + invokes the add view for a Page. - To generate an error, visit ``http://localhost:6543/add_page`` which - will generate an ``IndexError`` for the expression - ``request.subpath[0]``. You'll see an interactive traceback + will generate an ``IndexErrorr: tuple index out of range`` error. + You'll see an interactive traceback facility provided by :term:`pyramid_debugtoolbar`. diff --git a/docs/tutorials/wiki2/definingviews.rst b/docs/tutorials/wiki2/definingviews.rst index 84017ce5a..efb72230e 100644 --- a/docs/tutorials/wiki2/definingviews.rst +++ b/docs/tutorials/wiki2/definingviews.rst @@ -350,25 +350,24 @@ Viewing the Application in a Browser ==================================== We can finally examine our application in a browser (See -:ref:`wiki2-start-the-application`). The views we'll try are -as follows: +:ref:`wiki2-start-the-application`). Launch a browser and visit +each of the following URLs, check that the result is as expected: -- Visiting ``http://localhost:6543`` in a browser invokes the +- ``http://localhost:6543`` in a browser invokes the ``view_wiki`` view. This always redirects to the ``view_page`` view of the FrontPage page object. -- Visiting ``http://localhost:6543/FrontPage`` in a browser invokes +- ``http://localhost:6543/FrontPage`` in a browser invokes the ``view_page`` view of the front page page object. -- Visiting ``http://localhost:6543/FrontPage/edit_page`` in a browser +- ``http://localhost:6543/FrontPage/edit_page`` in a browser invokes the edit view for the front page object. -- Visiting ``http://localhost:6543/add_page/SomePageName`` in a +- ``http://localhost:6543/add_page/SomePageName`` in a browser invokes the add view for a page. -Try generating an error within the body of a view by adding code to -the top of it that generates an exception (e.g. ``raise -Exception('Forced Exception')``). Then visit the error-raising view -in a browser. You should see an interactive exception handler in the -browser which allows you to examine values in a post-mortem mode. +- To generate an error, visit ``http://localhost:6543/add_page`` which + will generate a ``NoResultFound: No row was found for one()`` error. + You'll see an interactive traceback facility provided + by :term:`pyramid_debugtoolbar`. -- cgit v1.2.3 From 85d6f81c6b517e6f973fcaba8bcb7df503a16c50 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 16 Apr 2012 22:43:23 -0500 Subject: first cut at removing ObjectJSONEncoder --- pyramid/renderers.py | 72 ++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 58 insertions(+), 14 deletions(-) diff --git a/pyramid/renderers.py b/pyramid/renderers.py index 0adadf726..54ba5cf6b 100644 --- a/pyramid/renderers.py +++ b/pyramid/renderers.py @@ -4,6 +4,7 @@ import pkg_resources import threading from zope.interface import implementer +from zope.interface.registry import Components from pyramid.interfaces import ( IChameleonLookup, @@ -190,7 +191,7 @@ class JSON(object): """ Renderer that returns a JSON-encoded string. Configure a custom JSON renderer using the - :meth:`pyramid.config.Configurator.add_renderer` API at application + :meth:`~pyramid.config.Configurator.add_renderer` API at application startup time: .. code-block:: python @@ -198,12 +199,11 @@ class JSON(object): from pyramid.config import Configurator config = Configurator() - config.add_renderer('myjson', JSON(indent=4, cls=MyJSONEncoder)) + config.add_renderer('myjson', JSON(indent=4)) - Once this renderer is registered via - :meth:`~pyramid.config.Configurator.add_renderer` as above, you can use + Once this renderer is registered as above, you can use ``myjson`` as the ``renderer=`` parameter to ``@view_config`` or - :meth:`pyramid.config.Configurator.add_view``: + :meth:`~pyramid.config.Configurator.add_view``: .. code-block:: python @@ -222,10 +222,19 @@ class JSON(object): """ def __init__(self, **kw): - """ Any keyword arguments will be forwarded to - :func:`json.dumps`. - """ + """ Any keyword arguments will be passed to the encoder + within :meth:`~pyramid.renderers.JSON.dumps`.""" + # we wrap the default with our own + self._default = kw.pop('default', None) + self.kw = kw + self.encoders = Components() + + self._register_default_encoders() + + def _register_default_encoders(self): + import datetime + self.add_encoder(lambda o: o.isoformat(), datetime.datetime) def __call__(self, info): """ Returns a plain JSON-encoded string with content-type @@ -238,16 +247,51 @@ class JSON(object): ct = response.content_type if ct == response.default_content_type: response.content_type = 'application/json' - return self.value_to_json(value) + return self.dumps(value) return _render - def value_to_json(self, value): - """ Convert a Python object to a JSON string. + def add_encoder(self, encoder, type_or_iface): + """ When an object of type (or interface) ``type_or_iface`` + fails to automatically encode using the default encoder, the + renderer will use the encoder ``encoder`` to convert it into a + string. + + .. code-block:: python + + class Foo(object): + x = 5 + + def foo_encoder(obj): + return str(obj.x) + + renderer = JSON(indent=4) + renderer.add_encoder(foo_encoder, foo) + """ + self.encoders.registerUtility(encoder, type_or_iface) + + def default_encode(self, obj): + """ Encode a custom Python object to a JSON string. + + This should be used by subclasses that have overridden + :meth:`~pyramid.renderers.JSON.dumps` in order to encode objects + that are not otherwise serializable. This should raise a + ``TypeError`` when an object cannot be serialized.""" + if hasattr(obj, '__json__'): + return obj.__json__() + + encoder = self.encoders.queryUtility(obj) + if encoder is not None: + return encoder(obj) + + if self._default is not None: + return self._default(obj) + raise TypeError(repr(obj) + ' is not JSON serializable') + + def dumps(self, obj): + """ Encode a Python object to a JSON string. By default, this uses the :func:`json.dumps` from the stdlib.""" - if not self.kw.get('cls'): - self.kw['cls'] = ObjectJSONEncoder - return json.dumps(value, **self.kw) + return json.dumps(obj, default=self.default_encode, **self.kw) json_renderer_factory = JSON() # bw compat -- cgit v1.2.3 From 8c44765063c5e969e8afb049fe31c4014500d339 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 16 Apr 2012 22:56:35 -0500 Subject: removed the component registry from json renderer --- pyramid/renderers.py | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/pyramid/renderers.py b/pyramid/renderers.py index 54ba5cf6b..3b1e0f44f 100644 --- a/pyramid/renderers.py +++ b/pyramid/renderers.py @@ -4,7 +4,6 @@ import pkg_resources import threading from zope.interface import implementer -from zope.interface.registry import Components from pyramid.interfaces import ( IChameleonLookup, @@ -228,13 +227,6 @@ class JSON(object): self._default = kw.pop('default', None) self.kw = kw - self.encoders = Components() - - self._register_default_encoders() - - def _register_default_encoders(self): - import datetime - self.add_encoder(lambda o: o.isoformat(), datetime.datetime) def __call__(self, info): """ Returns a plain JSON-encoded string with content-type @@ -250,25 +242,6 @@ class JSON(object): return self.dumps(value) return _render - def add_encoder(self, encoder, type_or_iface): - """ When an object of type (or interface) ``type_or_iface`` - fails to automatically encode using the default encoder, the - renderer will use the encoder ``encoder`` to convert it into a - string. - - .. code-block:: python - - class Foo(object): - x = 5 - - def foo_encoder(obj): - return str(obj.x) - - renderer = JSON(indent=4) - renderer.add_encoder(foo_encoder, foo) - """ - self.encoders.registerUtility(encoder, type_or_iface) - def default_encode(self, obj): """ Encode a custom Python object to a JSON string. @@ -279,10 +252,6 @@ class JSON(object): if hasattr(obj, '__json__'): return obj.__json__() - encoder = self.encoders.queryUtility(obj) - if encoder is not None: - return encoder(obj) - if self._default is not None: return self._default(obj) raise TypeError(repr(obj) + ' is not JSON serializable') -- cgit v1.2.3 From dfaf54a7018971c08ed4a437dbec0ffb57d1ff8a Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 16 Apr 2012 22:57:23 -0500 Subject: fixed coverage, removed json encoder --- pyramid/renderers.py | 31 +------------------------------ pyramid/tests/test_renderers.py | 8 +++----- 2 files changed, 4 insertions(+), 35 deletions(-) diff --git a/pyramid/renderers.py b/pyramid/renderers.py index 3b1e0f44f..441976ce4 100644 --- a/pyramid/renderers.py +++ b/pyramid/renderers.py @@ -157,35 +157,6 @@ def string_renderer_factory(info): return value return _render -class ObjectJSONEncoder(json.JSONEncoder): - """ The default JSON object encoder (a subclass of json.Encoder) used by - :class:`pyramid.renderers.JSON` and :class:`pyramid.renderers.JSONP`. It - is used when an object returned from a view and presented to a JSON-based - renderer is not a builtin Python type otherwise serializable to JSON. - - This ``json.Encoder`` subclass overrides the ``json.Encoder.default`` - method. The overridden method looks for a ``__json__`` attribute on the - object it is passed. If it's found, the encoder will assume it's - callable, and will call it with no arguments to obtain a value. The - overridden ``default`` method will then return that value (which must be - a JSON-serializable basic Python type). - - If the object passed to the overridden ``default`` method has no - ``__json__`` attribute, the ``json.JSONEncoder.default`` method is called - with the object that it was passed (which will end up raising a - :exc:`TypeError`, as it would with any other unserializable type). - - This class will be used only when you set a JSON or JSONP - renderer and you do not define your own custom encoder class. - - .. note:: This feature is new in Pyramid 1.4. - """ - - def default(self, obj): - if hasattr(obj, '__json__'): - return obj.__json__() - return json.JSONEncoder.default(self, obj) - class JSON(object): """ Renderer that returns a JSON-encoded string. @@ -332,7 +303,7 @@ class JSONP(JSON): plain-JSON encoded string with content-type ``application/json``""" def _render(value, system): request = system['request'] - val = self.value_to_json(value) + val = self.dumps(value) callback = request.GET.get(self.param_name) if callback is None: ct = 'application/json' diff --git a/pyramid/tests/test_renderers.py b/pyramid/tests/test_renderers.py index f03c7acda..55ed3f7fd 100644 --- a/pyramid/tests/test_renderers.py +++ b/pyramid/tests/test_renderers.py @@ -371,12 +371,10 @@ class TestJSON(unittest.TestCase): def test_with_custom_encoder(self): from datetime import datetime - from json import JSONEncoder - class MyEncoder(JSONEncoder): - def default(self, obj): - return obj.isoformat() + def default(obj): + return obj.isoformat() now = datetime.utcnow() - renderer = self._makeOne(cls=MyEncoder)(None) + renderer = self._makeOne(default=default)(None) result = renderer({'a':now}, {}) self.assertEqual(result, '{"a": "%s"}' % now.isoformat()) -- cgit v1.2.3 From 1f0d9d2193bb9557d4475885776b5679c8dbfa23 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 16 Apr 2012 23:19:14 -0500 Subject: docs for json defaults --- docs/api/renderers.rst | 2 -- docs/narr/renderers.rst | 29 +++++++++++++++-------------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/docs/api/renderers.rst b/docs/api/renderers.rst index ab182365e..ea000ad02 100644 --- a/docs/api/renderers.rst +++ b/docs/api/renderers.rst @@ -15,8 +15,6 @@ .. autoclass:: JSONP -.. autoclass:: ObjectJSONEncoder - .. attribute:: null_renderer An object that can be used in advanced integration cases as input to the diff --git a/docs/narr/renderers.rst b/docs/narr/renderers.rst index 34bee3c7f..50349c409 100644 --- a/docs/narr/renderers.rst +++ b/docs/narr/renderers.rst @@ -223,9 +223,9 @@ You can configure a view to use the JSON renderer by naming ``json`` as the :linenos: config.add_view('myproject.views.hello_world', - name='hello', - context='myproject.resources.Hello', - renderer='json') + name='hello', + context='myproject.resources.Hello', + renderer='json') Views which use the JSON renderer can vary non-body response attributes by using the api of the ``request.response`` attribute. See @@ -260,20 +260,21 @@ strings, and so forth). # the JSON value returned by ``objects`` will be: # [{"x": 1}, {"x": 2}] -.. note:: +If you don't own the objects being serialized, it's difficult to add a custom +``__json__`` method to the object. In this case, a callback can be supplied +to the renderer which is invoked when other options have failed. - Honoring the ``__json__`` method of custom objects is a feature new in - Pyramid 1.4. +.. code-block:: python + :linenos: + + def default(obj): + if isinstance(obj, datetime.datetime): + return obj.isoformat() + raise TypeError -.. warning:: +.. note:: - The machinery which performs the ``__json__`` method-calling magic is in - the :class:`pyramid.renderers.ObjectJSONEncoder` class. This class will - be used for encoding any non-basic Python object when you use the default - ```json`` or ``jsonp`` renderers. But if you later define your own custom - JSON renderer and pass it a "cls" argument signifying a different encoder, - the encoder you pass will override Pyramid's use of - :class:`pyramid.renderers.ObjectJSONEncoder`. + Serializing custom objects is a feature new in Pyramid 1.4. .. index:: pair: renderer; JSONP -- cgit v1.2.3 From 18410a6d9d64786f272268db6368981955ff9f10 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 17 Apr 2012 04:59:33 -0400 Subject: default_encode->_default_encode, dumps->_dumps, massage docs --- docs/narr/renderers.rst | 27 +++++++++++++++++++++++---- pyramid/renderers.py | 44 ++++++++++++++++++++++++-------------------- 2 files changed, 47 insertions(+), 24 deletions(-) diff --git a/docs/narr/renderers.rst b/docs/narr/renderers.rst index 50349c409..02063a112 100644 --- a/docs/narr/renderers.rst +++ b/docs/narr/renderers.rst @@ -177,6 +177,8 @@ using the API of the ``request.response`` attribute. See .. index:: pair: renderer; JSON +.. _json_renderer: + JSON Renderer ~~~~~~~~~~~~~ @@ -260,17 +262,34 @@ strings, and so forth). # the JSON value returned by ``objects`` will be: # [{"x": 1}, {"x": 2}] -If you don't own the objects being serialized, it's difficult to add a custom -``__json__`` method to the object. In this case, a callback can be supplied -to the renderer which is invoked when other options have failed. +If you aren't the author of the objects being serialized, it won't be +possible (or at least not reasonable) to add a custom ``__json__`` method to +to their classes in order to influence serialization. If the object passed +to the renderer is not a serializable type, and has no ``__json__`` method, +usually a :exc:`TypeError` will be raised during serialization. You can +change this behavior by creating a JSON renderer with a "default" function +which tries to "sniff" at the object, and returns a valid serialization (a +string) or raises a TypeError if it can't determine what to do with the +object. A short example follows: .. code-block:: python :linenos: + from pyramid.renderers import JSON + def default(obj): if isinstance(obj, datetime.datetime): return obj.isoformat() - raise TypeError + raise TypeError('%r is not serializable % (obj,)) + + json_renderer = JSON(default=default) + + # then during configuration .... + config = Configurator() + config.add_renderer('json', json_renderer) + +See :class:`pyramid.renderers.JSON` and +:ref:`adding_and_overriding_renderers` for more information. .. note:: diff --git a/pyramid/renderers.py b/pyramid/renderers.py index 441976ce4..b393a40a6 100644 --- a/pyramid/renderers.py +++ b/pyramid/renderers.py @@ -189,14 +189,20 @@ class JSON(object): no public API for supplying options to the underlying :func:`json.dumps` without defining a custom renderer. + You can pass a ``default`` argument to this class' constructor (which + should be a function) to customize what happens when it attempts to + serialize types unrecognized by the base ``json`` module. See + :ref:`json_serializing_custom_objects` for more information. """ def __init__(self, **kw): - """ Any keyword arguments will be passed to the encoder - within :meth:`~pyramid.renderers.JSON.dumps`.""" - # we wrap the default with our own + """ Any keyword arguments will be passed to the ``json.dumps`` + function. A notable exception is the keyword argument ``default``, + which is wrapped in a function that sniffs for ``__json__`` + attributes before it is passed along to ``json.dumps``""" + # we wrap the default callback with our own to get __json__ attr + # sniffing self._default = kw.pop('default', None) - self.kw = kw def __call__(self, info): @@ -210,35 +216,29 @@ class JSON(object): ct = response.content_type if ct == response.default_content_type: response.content_type = 'application/json' - return self.dumps(value) + return self._dumps(value) return _render - def default_encode(self, obj): - """ Encode a custom Python object to a JSON string. - - This should be used by subclasses that have overridden - :meth:`~pyramid.renderers.JSON.dumps` in order to encode objects - that are not otherwise serializable. This should raise a - ``TypeError`` when an object cannot be serialized.""" + def _default_encode(self, obj): if hasattr(obj, '__json__'): return obj.__json__() if self._default is not None: return self._default(obj) - raise TypeError(repr(obj) + ' is not JSON serializable') + raise TypeError('%r is not JSON serializable' % (obj,)) - def dumps(self, obj): + def _dumps(self, obj): """ Encode a Python object to a JSON string. By default, this uses the :func:`json.dumps` from the stdlib.""" - return json.dumps(obj, default=self.default_encode, **self.kw) + return json.dumps(obj, default=self._default_encode, **self.kw) json_renderer_factory = JSON() # bw compat class JSONP(JSON): """ `JSONP `_ renderer factory helper which implements a hybrid json/jsonp renderer. JSONP is useful for - making cross-domain AJAX requests. + making cross-domain AJAX requests. Configure a JSONP renderer using the :meth:`pyramid.config.Configurator.add_renderer` API at application @@ -251,9 +251,9 @@ class JSONP(JSON): config = Configurator() config.add_renderer('jsonp', JSONP(param_name='callback')) - The class also accepts arbitrary keyword arguments; all keyword arguments - except ``param_name`` are passed to the ``json.dumps`` function as - keyword arguments: + The class' constructor also accepts arbitrary keyword arguments. All + keyword arguments except ``param_name`` are passed to the ``json.dumps`` + function as its keyword arguments. .. code-block:: python @@ -265,6 +265,10 @@ class JSONP(JSON): .. note:: The ability of this class to accept a ``**kw`` in its constructor is new as of Pyramid 1.4. + The arguments passed to this class' constructor mean the same thing as + the arguments passed to :class:`pyramid.renderers.JSON` (including + ``default``). + Once this renderer is registered via :meth:`~pyramid.config.Configurator.add_renderer` as above, you can use ``jsonp`` as the ``renderer=`` parameter to ``@view_config`` or @@ -303,7 +307,7 @@ class JSONP(JSON): plain-JSON encoded string with content-type ``application/json``""" def _render(value, system): request = system['request'] - val = self.dumps(value) + val = self._dumps(value) callback = request.GET.get(self.param_name) if callback is None: ct = 'application/json' -- cgit v1.2.3 From 15e3b1929a2b7ec23fdf83db0d79391101cdfac0 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Tue, 17 Apr 2012 18:56:24 +0300 Subject: Add @ to examples of forbidden_view_config & notfound_view_config --- pyramid/view.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyramid/view.py b/pyramid/view.py index d722c0cbb..bb531c326 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -335,7 +335,7 @@ class notfound_view_config(object): from pyramid.view import notfound_view_config from pyramid.response import Response - notfound_view_config() + @notfound_view_config() def notfound(request): return Response('Not found, dude!', status='404 Not Found') @@ -409,7 +409,7 @@ class forbidden_view_config(object): from pyramid.view import forbidden_view_config from pyramid.response import Response - forbidden_view_config() + @forbidden_view_config() def notfound(request): return Response('You are not allowed', status='401 Unauthorized') -- cgit v1.2.3 From 677216d2c4ddc5f0df857b8f9e8fa6ccfd5fd55a Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Wed, 18 Apr 2012 00:50:10 -0500 Subject: reverted back to using a component registry during json encoding --- docs/narr/renderers.rst | 32 +++++++--------- pyramid/interfaces.py | 6 +++ pyramid/renderers.py | 82 ++++++++++++++++++++++++++++------------- pyramid/tests/test_renderers.py | 37 ++++++++++++++++--- 4 files changed, 108 insertions(+), 49 deletions(-) diff --git a/docs/narr/renderers.rst b/docs/narr/renderers.rst index 02063a112..c36caeb87 100644 --- a/docs/narr/renderers.rst +++ b/docs/narr/renderers.rst @@ -182,10 +182,10 @@ using the API of the ``request.response`` attribute. See JSON Renderer ~~~~~~~~~~~~~ -The ``json`` renderer renders view callable results to :term:`JSON`. It -passes the return value through the ``json.dumps`` standard library function, -and wraps the result in a response object. It also sets the response -content-type to ``application/json``. +The ``json`` renderer renders view callable results to :term:`JSON`. By +default, it passes the return value through the ``json.dumps`` standard +library function, and wraps the result in a response object. It also sets +the response content-type to ``application/json``. Here's an example of a view that returns a dictionary. Since the ``json`` renderer is specified in the configuration for this view, the view will @@ -209,11 +209,11 @@ representing the JSON serialization of the return value: '{"content": "Hello!"}' The return value needn't be a dictionary, but the return value must contain -values serializable by ``json.dumps``. +values serializable by the configured serializer (by default ``json.dumps``). .. note:: - Extra arguments can be passed to ``json.dumps`` by overriding the default + Extra arguments can be passed to the serializer by overriding the default ``json`` renderer. See :class:`pyramid.renderers.JSON` and :ref:`adding_and_overriding_renderers` for more information. @@ -240,8 +240,8 @@ Serializing Custom Objects Custom objects can be made easily JSON-serializable in Pyramid by defining a ``__json__`` method on the object's class. This method should return values -natively serializable by ``json.dumps`` (such as ints, lists, dictionaries, -strings, and so forth). +natively JSON-serializable (such as ints, lists, dictionaries, strings, and +so forth). .. code-block:: python :linenos: @@ -267,22 +267,18 @@ possible (or at least not reasonable) to add a custom ``__json__`` method to to their classes in order to influence serialization. If the object passed to the renderer is not a serializable type, and has no ``__json__`` method, usually a :exc:`TypeError` will be raised during serialization. You can -change this behavior by creating a JSON renderer with a "default" function -which tries to "sniff" at the object, and returns a valid serialization (a -string) or raises a TypeError if it can't determine what to do with the -object. A short example follows: +change this behavior by creating a custom JSON renderer and adding adapters +to handle custom types. The renderer will attempt to adapt non-serializable +objects using the registered adapters. It will raise a :exc:`TypeError` if it +can't determine what to do with the object. A short example follows: .. code-block:: python :linenos: from pyramid.renderers import JSON - def default(obj): - if isinstance(obj, datetime.datetime): - return obj.isoformat() - raise TypeError('%r is not serializable % (obj,)) - - json_renderer = JSON(default=default) + json_renderer = JSON() + json_renderer.add_adapter(datetime.datetime, lambda x: x.isoformat()) # then during configuration .... config = Configurator() diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index 5d9d29afa..1445ee394 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -1105,6 +1105,12 @@ class IAssetDescriptor(Interface): Returns True if asset exists, otherwise returns False. """ +class IJSONAdapter(Interface): + """ + Marker interface for objects that can convert an arbitrary object + into a JSON-serializable primitive. + """ + # configuration phases: a lower phase number means the actions associated # with this phase will be executed earlier than those with later phase # numbers. The default phase number is 0, FTR. diff --git a/pyramid/renderers.py b/pyramid/renderers.py index b393a40a6..efd7cdf42 100644 --- a/pyramid/renderers.py +++ b/pyramid/renderers.py @@ -4,10 +4,12 @@ import pkg_resources import threading from zope.interface import implementer +from zope.interface.registry import Components from pyramid.interfaces import ( IChameleonLookup, IChameleonTranslate, + IJSONAdapter, IRendererGlobalsFactory, IRendererFactory, IResponseFactory, @@ -157,6 +159,8 @@ def string_renderer_factory(info): return value return _render +_marker = object() + class JSON(object): """ Renderer that returns a JSON-encoded string. @@ -183,27 +187,52 @@ class JSON(object): def myview(request): return {'greeting':'Hello world'} + Custom objects can be serialized using the renderer by either + implementing the ``__json__`` magic method, or by registering + adapters with the renderer. See + :ref:`json_serializing_custom_objects` for more information. + + The default serializer uses ``json.JSONEncoder``. A different + serializer can be specified via the ``serializer`` argument. + Custom serializers should accept the object, a callback + ``default``, and any extra ``kw`` keyword argments passed during + renderer construction. + .. note:: This feature is new in Pyramid 1.4. Prior to 1.4 there was no public API for supplying options to the underlying - :func:`json.dumps` without defining a custom renderer. - - You can pass a ``default`` argument to this class' constructor (which - should be a function) to customize what happens when it attempts to - serialize types unrecognized by the base ``json`` module. See - :ref:`json_serializing_custom_objects` for more information. + serializer without defining a custom renderer. """ - def __init__(self, **kw): - """ Any keyword arguments will be passed to the ``json.dumps`` - function. A notable exception is the keyword argument ``default``, - which is wrapped in a function that sniffs for ``__json__`` - attributes before it is passed along to ``json.dumps``""" - # we wrap the default callback with our own to get __json__ attr - # sniffing - self._default = kw.pop('default', None) + def __init__(self, serializer=json.dumps, adapters=(), **kw): + """ Any keyword arguments will be passed to the ``serializer`` + function.""" + self.serializer = serializer self.kw = kw + self.components = Components() + for type, adapter in adapters: + self.add_adapter(type, adapter) + + def add_adapter(self, type_or_iface, adapter): + """ When an object of type (or interface) ``type_or_iface`` + fails to automatically encode using the serializer, the renderer + will use the adapter ``adapter`` to convert it into a + JSON-serializable object. + + .. code-block:: python + + class Foo(object): + x = 5 + + def foo_adapter(obj): + return obj.x + + renderer = JSON(indent=4) + renderer.adapt(Foo, foo_adapter) + """ + self.components.registerAdapter(adapter, (type_or_iface,), + IJSONAdapter) def __call__(self, info): """ Returns a plain JSON-encoded string with content-type @@ -216,23 +245,26 @@ class JSON(object): ct = response.content_type if ct == response.default_content_type: response.content_type = 'application/json' - return self._dumps(value) + return self.serializer(value, default=self.default, **self.kw) return _render - def _default_encode(self, obj): + def default(self, obj): + """ Returns a JSON-serializable representation of ``obj``. If + no representation can be found, a ``TypeError`` is raised. + + If the object implements the ``__json__`` magic method, it will + be preferred. Otherwise, attempt to adapt ``obj`` into a + serializable type using one of the registered adapters. + """ if hasattr(obj, '__json__'): return obj.__json__() - if self._default is not None: - return self._default(obj) + result = self.components.queryAdapter(obj, IJSONAdapter, + default=_marker) + if result is not _marker: + return result raise TypeError('%r is not JSON serializable' % (obj,)) - def _dumps(self, obj): - """ Encode a Python object to a JSON string. - - By default, this uses the :func:`json.dumps` from the stdlib.""" - return json.dumps(obj, default=self._default_encode, **self.kw) - json_renderer_factory = JSON() # bw compat class JSONP(JSON): @@ -307,7 +339,7 @@ class JSONP(JSON): plain-JSON encoded string with content-type ``application/json``""" def _render(value, system): request = system['request'] - val = self._dumps(value) + val = self.serializer(value, default=self.default, **self.kw) callback = request.GET.get(self.param_name) if callback is None: ct = 'application/json' diff --git a/pyramid/tests/test_renderers.py b/pyramid/tests/test_renderers.py index 55ed3f7fd..55c5c2f5a 100644 --- a/pyramid/tests/test_renderers.py +++ b/pyramid/tests/test_renderers.py @@ -369,16 +369,41 @@ class TestJSON(unittest.TestCase): renderer({'a':1}, {'request':request}) self.assertEqual(request.response.content_type, 'text/mishmash') - def test_with_custom_encoder(self): + def test_with_custom_adapter(self): from datetime import datetime - def default(obj): + def adapter(obj): return obj.isoformat() now = datetime.utcnow() - renderer = self._makeOne(default=default)(None) - result = renderer({'a':now}, {}) + renderer = self._makeOne() + renderer.add_adapter(datetime, adapter) + result = renderer(None)({'a':now}, {}) self.assertEqual(result, '{"a": "%s"}' % now.isoformat()) - def test_with_object_encoder(self): + def test_with_custom_adapter2(self): + from datetime import datetime + def adapter(obj): + return obj.isoformat() + now = datetime.utcnow() + renderer = self._makeOne(adapters=((datetime, adapter),)) + result = renderer(None)({'a':now}, {}) + self.assertEqual(result, '{"a": "%s"}' % now.isoformat()) + + def test_with_custom_serializer(self): + class Serializer(object): + def __call__(self, obj, **kw): + self.obj = obj + self.kw = kw + return 'foo' + serializer = Serializer() + renderer = self._makeOne(serializer=serializer, baz=5) + obj = {'a':'b'} + result = renderer(None)(obj, {}) + self.assertEqual(result, 'foo') + self.assertEqual(serializer.obj, obj) + self.assertEqual(serializer.kw['baz'], 5) + self.assertTrue('default' in serializer.kw) + + def test_with_object_adapter(self): class MyObject(object): def __init__(self, x): self.x = x @@ -390,7 +415,7 @@ class TestJSON(unittest.TestCase): result = renderer(objects, {}) self.assertEqual(result, '[{"x": 1}, {"x": 2}]') - def test_with_object_encoder_no___json__(self): + def test_with_object_adapter_no___json__(self): class MyObject(object): def __init__(self, x): self.x = x -- cgit v1.2.3 From c3df7a76fa9f92201fbf57693b580b8904fac038 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Wed, 18 Apr 2012 09:14:18 -0500 Subject: garden --- pyramid/renderers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/renderers.py b/pyramid/renderers.py index efd7cdf42..ad09ad927 100644 --- a/pyramid/renderers.py +++ b/pyramid/renderers.py @@ -229,7 +229,7 @@ class JSON(object): return obj.x renderer = JSON(indent=4) - renderer.adapt(Foo, foo_adapter) + renderer.add_adapter(Foo, foo_adapter) """ self.components.registerAdapter(adapter, (type_or_iface,), IJSONAdapter) -- cgit v1.2.3 From aca78fe3f08ad31be2cc154301c95a318d74ead8 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 18 Apr 2012 14:47:51 -0400 Subject: break docs stuff out of testing to hopefully appease shiningpanda --- setup.cfg | 2 +- setup.py | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/setup.cfg b/setup.cfg index 8aac0afd1..d7622683f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -10,4 +10,4 @@ cover-erase=1 [aliases] dev = develop easy_install pyramid[testing] - +docs = develop easy_install pyramid[docs] diff --git a/setup.py b/setup.py index e325b8a29..29a0f22ca 100644 --- a/setup.py +++ b/setup.py @@ -49,19 +49,22 @@ install_requires=[ ] tests_require = [ + 'nose', + 'coverage', 'WebTest >= 1.3.1', # py3 compat 'virtualenv', ] if not PY3: tests_require.extend([ - 'Sphinx', - 'docutils', - 'repoze.sphinx.autointerface', 'zope.component>=3.11.0', ]) -testing_extras = tests_require + ['nose', 'coverage'] +docs_extras = [ + 'Sphinx', + 'docutils', + 'repoze.sphinx.autointerface', + ] setup(name='pyramid', version='1.4dev', @@ -92,7 +95,8 @@ setup(name='pyramid', zip_safe=False, install_requires = install_requires, extras_require = { - 'testing':testing_extras, + 'testing':tests_require, + 'docs':docs_extras, }, tests_require = tests_require, test_suite="pyramid.tests", -- cgit v1.2.3 From ff5a6a6b039c04f8d55c3ab513257d2aff8d5e9e Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 18 Apr 2012 15:42:13 -0400 Subject: try to fix windows test failure --- pyramid/tests/test_scaffolds/test_copydir.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pyramid/tests/test_scaffolds/test_copydir.py b/pyramid/tests/test_scaffolds/test_copydir.py index 42edd9d23..68cefbe6e 100644 --- a/pyramid/tests/test_scaffolds/test_copydir.py +++ b/pyramid/tests/test_scaffolds/test_copydir.py @@ -170,9 +170,11 @@ class Test_makedirs(unittest.TestCase): def test_makedirs_parent_dir(self): import shutil - target = "/tmp/nonexistent_dir/nonexistent_subdir" + import tempfile + tmpdir = tempfile.mkdtemp() + target = os.path.join(tmpdir, 'nonexistent_subdir') self._callFUT(target, 2, None) - shutil.rmtree("/tmp/nonexistent_dir") + shutil.rmtree(tmpdir) class Test_support_functions(unittest.TestCase): -- cgit v1.2.3 From 136ca148ffec69e54ed0d3c5645e8f7d96413726 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 18 Apr 2012 15:53:07 -0400 Subject: test checkin for automating shiningpanda build from github post-receive url --- CHANGES.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index 337754162..7a0a6f28c 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -9,7 +9,6 @@ Bug Fixes return the empty list. This was incorrect, it should have unconditionally returned ``[Everyone]``, and now does. - Features -------- -- cgit v1.2.3 From 2b2451ad920af1defabd8b06509d2100f627dc3f Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 18 Apr 2012 15:56:11 -0400 Subject: test checkin for automating shiningpanda build from github post-receive url (this time for sure) --- CHANGES.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.txt b/CHANGES.txt index 7a0a6f28c..337754162 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -9,6 +9,7 @@ Bug Fixes return the empty list. This was incorrect, it should have unconditionally returned ``[Everyone]``, and now does. + Features -------- -- cgit v1.2.3 From 60ea901969e7ea87a68e7ca3da4032724ca90bb7 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 19 Apr 2012 16:43:31 -0400 Subject: take out 'or dotted python name' --- docs/narr/startup.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/narr/startup.rst b/docs/narr/startup.rst index 8e28835af..2a764b0ec 100644 --- a/docs/narr/startup.rst +++ b/docs/narr/startup.rst @@ -42,8 +42,8 @@ Here's a high-level time-ordered overview of what happens when you press ``[pipeline:main]``, or ``[composite:main]`` in the ``.ini`` file. This section represents the configuration of a :term:`WSGI` application that will be served. If you're using a simple application (e.g. - ``[app:main]``), the application :term:`entry point` or :term:`dotted - Python name` will be named on the ``use=`` line within the section's + ``[app:main]``), the application's ``paste.app_factory`` :term:`entry + point` will be named on the ``use=`` line within the section's configuration. If, instead of a simple application, you're using a WSGI :term:`pipeline` (e.g. a ``[pipeline:main]`` section), the application named on the "last" element will refer to your :app:`Pyramid` application. -- cgit v1.2.3 From 0569f999db99e50ffd962eb06d51ec1fb4731181 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 19 Apr 2012 16:45:51 -0400 Subject: squash another reference to dotted name --- docs/narr/startup.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/narr/startup.rst b/docs/narr/startup.rst index 2a764b0ec..f5c741f52 100644 --- a/docs/narr/startup.rst +++ b/docs/narr/startup.rst @@ -59,11 +59,11 @@ Here's a high-level time-ordered overview of what happens when you press system for this application. See :ref:`logging_config` for more information. -#. The application's *constructor* named by the entry point reference or - dotted Python name on the ``use=`` line of the section representing your - :app:`Pyramid` application is passed the key/value parameters mentioned - within the section in which it's defined. The constructor is meant to - return a :term:`router` instance, which is a :term:`WSGI` application. +#. The application's *constructor* named by the entry point reference on the + ``use=`` line of the section representing your :app:`Pyramid` application + is passed the key/value parameters mentioned within the section in which + it's defined. The constructor is meant to return a :term:`router` + instance, which is a :term:`WSGI` application. For :app:`Pyramid` applications, the constructor will be a function named ``main`` in the ``__init__.py`` file within the :term:`package` in which -- cgit v1.2.3 From 7793ec4dfe69462f2db00340a2299a59cd0e73da Mon Sep 17 00:00:00 2001 From: Ralph Bean Date: Thu, 19 Apr 2012 18:22:56 -0400 Subject: Change alchemy scaffold README to match the console_scripts setup. --- pyramid/scaffolds/alchemy/README.txt_tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/scaffolds/alchemy/README.txt_tmpl b/pyramid/scaffolds/alchemy/README.txt_tmpl index efea71c5c..9e4aa1125 100644 --- a/pyramid/scaffolds/alchemy/README.txt_tmpl +++ b/pyramid/scaffolds/alchemy/README.txt_tmpl @@ -8,7 +8,7 @@ Getting Started - $venv/bin/python setup.py develop -- $venv/bin/populate_{{project}} development.ini +- $venv/bin/initialize_{{project}}_db development.ini - $venv/bin/pserve development.ini -- cgit v1.2.3 From 90da3b3b1363beb443d190fdcdee8d9323beb64b Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 21 Apr 2012 12:11:59 -0400 Subject: make virtualenv, nose, and coverage testing extras rather than test requirements --- setup.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/setup.py b/setup.py index 29a0f22ca..cbc1075bd 100644 --- a/setup.py +++ b/setup.py @@ -49,16 +49,11 @@ install_requires=[ ] tests_require = [ - 'nose', - 'coverage', 'WebTest >= 1.3.1', # py3 compat - 'virtualenv', ] if not PY3: - tests_require.extend([ - 'zope.component>=3.11.0', - ]) + tests_require.append('zope.component>=3.11.0') docs_extras = [ 'Sphinx', @@ -66,6 +61,12 @@ docs_extras = [ 'repoze.sphinx.autointerface', ] +testing_extras = tests_require + [ + 'nose', + 'coverage', + 'virtualenv', # for scaffolding tests + ] + setup(name='pyramid', version='1.4dev', description=('The Pyramid web application development framework, a ' @@ -95,7 +96,7 @@ setup(name='pyramid', zip_safe=False, install_requires = install_requires, extras_require = { - 'testing':tests_require, + 'testing':testing_extras, 'docs':docs_extras, }, tests_require = tests_require, -- cgit v1.2.3 From f549346f7e524b255c28a25a8de7b5dab8fdce33 Mon Sep 17 00:00:00 2001 From: Ralph Bean Date: Sat, 21 Apr 2012 21:25:12 -0400 Subject: Use the new Pyramid trove classifiers for the scaffolds. --- pyramid/scaffolds/alchemy/setup.py_tmpl | 2 +- pyramid/scaffolds/starter/setup.py_tmpl | 2 +- pyramid/scaffolds/zodb/setup.py_tmpl | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyramid/scaffolds/alchemy/setup.py_tmpl b/pyramid/scaffolds/alchemy/setup.py_tmpl index b80fc52a8..2d8ed028f 100644 --- a/pyramid/scaffolds/alchemy/setup.py_tmpl +++ b/pyramid/scaffolds/alchemy/setup.py_tmpl @@ -22,7 +22,7 @@ setup(name='{{project}}', long_description=README + '\n\n' + CHANGES, classifiers=[ "Programming Language :: Python", - "Framework :: Pylons", + "Framework :: Pyramid", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", ], diff --git a/pyramid/scaffolds/starter/setup.py_tmpl b/pyramid/scaffolds/starter/setup.py_tmpl index 39ac6de9d..58c0a79fc 100644 --- a/pyramid/scaffolds/starter/setup.py_tmpl +++ b/pyramid/scaffolds/starter/setup.py_tmpl @@ -18,7 +18,7 @@ setup(name='{{project}}', long_description=README + '\n\n' + CHANGES, classifiers=[ "Programming Language :: Python", - "Framework :: Pylons", + "Framework :: Pyramid", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", ], diff --git a/pyramid/scaffolds/zodb/setup.py_tmpl b/pyramid/scaffolds/zodb/setup.py_tmpl index 965c0178f..acdf095d5 100644 --- a/pyramid/scaffolds/zodb/setup.py_tmpl +++ b/pyramid/scaffolds/zodb/setup.py_tmpl @@ -21,7 +21,7 @@ setup(name='{{project}}', long_description=README + '\n\n' + CHANGES, classifiers=[ "Programming Language :: Python", - "Framework :: Pylons", + "Framework :: Pyramid", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", ], -- cgit v1.2.3 From ea7a26856e27f4256ec0157635b4c64197f37053 Mon Sep 17 00:00:00 2001 From: VlAleVas Date: Fri, 27 Apr 2012 16:10:13 +0300 Subject: Update docs/narr/urldispatch.rst --- docs/narr/urldispatch.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/narr/urldispatch.rst b/docs/narr/urldispatch.rst index f036ce94e..acbccbdfd 100644 --- a/docs/narr/urldispatch.rst +++ b/docs/narr/urldispatch.rst @@ -547,7 +547,7 @@ add to your application: config.add_route('idea', 'ideas/{idea}') config.add_route('user', 'users/{user}') - config.add_route('tag', 'tags/{tags}') + config.add_route('tag', 'tags/{tag}') config.add_view('mypackage.views.idea_view', route_name='idea') config.add_view('mypackage.views.user_view', route_name='user') -- cgit v1.2.3 From 0e0c83fabc77e2888e26c56df9acda7a5c839530 Mon Sep 17 00:00:00 2001 From: Christopher Lambacher Date: Fri, 27 Apr 2012 11:06:59 -0400 Subject: Don't add a ? to url if query string is empty. --- pyramid/tests/test_url.py | 17 +++++++++++++++++ pyramid/url.py | 8 ++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/pyramid/tests/test_url.py b/pyramid/tests/test_url.py index 0dff1e648..50deb63f3 100644 --- a/pyramid/tests/test_url.py +++ b/pyramid/tests/test_url.py @@ -113,6 +113,14 @@ class TestURLMethodsMixin(unittest.TestCase): self.assertEqual(result, 'http://example.com:5432/context/a?a=hi+there&b=La+Pe%C3%B1a') + def test_resource_url_with_query_empty(self): + request = self._makeOne() + self._registerResourceURL(request.registry) + context = DummyContext() + result = request.resource_url(context, 'a', query=[]) + self.assertEqual(result, + 'http://example.com:5432/context/a') + def test_resource_url_anchor_is_after_root_when_no_elements(self): request = self._makeOne() self._registerResourceURL(request.registry) @@ -334,6 +342,15 @@ class TestURLMethodsMixin(unittest.TestCase): self.assertEqual(result, 'http://example.com:5432/1/2/3?q=1') + def test_route_url_with_empty_query(self): + from pyramid.interfaces import IRoutesMapper + request = self._makeOne() + mapper = DummyRoutesMapper(route=DummyRoute('/1/2/3')) + request.registry.registerUtility(mapper, IRoutesMapper) + result = request.route_url('flub', _query={}) + self.assertEqual(result, + 'http://example.com:5432/1/2/3') + def test_route_url_with_app_url(self): from pyramid.interfaces import IRoutesMapper request = self._makeOne() diff --git a/pyramid/url.py b/pyramid/url.py index 022867967..dd83bb631 100644 --- a/pyramid/url.py +++ b/pyramid/url.py @@ -218,7 +218,9 @@ class URLMethodsMixin(object): port = None if '_query' in kw: - qs = '?' + urlencode(kw.pop('_query'), doseq=True) + query = kw.pop('_query') + if query: + qs = '?' + urlencode(query, doseq=True) if '_anchor' in kw: anchor = kw.pop('_anchor') @@ -494,7 +496,9 @@ class URLMethodsMixin(object): anchor = '' if 'query' in kw: - qs = '?' + urlencode(kw['query'], doseq=True) + query = kw['query'] + if query: + qs = '?' + urlencode(query, doseq=True) if 'anchor' in kw: anchor = kw['anchor'] -- cgit v1.2.3 From fcb209534f069c79ecf90c5499d2955b049aca78 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 27 Apr 2012 11:15:24 -0400 Subject: garden --- CHANGES.txt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index 337754162..b8d42ba6f 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -9,7 +9,6 @@ Bug Fixes return the empty list. This was incorrect, it should have unconditionally returned ``[Everyone]``, and now does. - Features -------- @@ -29,3 +28,9 @@ Features - ``config.set_request_property`` now causes less code to be executed at request construction time. + +- Don't add a ``?`` to URLs generated by request.resource_url if the + ``query`` argument is provided but empty. + +- Don't add a ``?`` to URLs generated by request.route_url if the + ``_query`` argument is provided but empty. -- cgit v1.2.3 From 561a443369bea131fcdbf0476c7e6a4935638d62 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 27 Apr 2012 11:20:08 -0400 Subject: fix example --- CHANGES.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index b8d42ba6f..67b03d59b 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -20,7 +20,7 @@ Features - As of this release, the ``request_method`` predicate, when used, will also imply that ``HEAD`` is implied when you use ``GET``. For example, using ``@view_config(request_method='GET')`` is equivalent to using - ``@view_config(request_method='HEAD')``. Using + ``@view_config(request_method=('GET', 'HEAD'))``. Using ``@view_config(request_method=('GET', 'POST')`` is equivalent to using ``@view_config(request_method=('GET', 'HEAD', 'POST')``. This is because HEAD is a variant of GET that omits the body, and WebOb has special support -- cgit v1.2.3 From 99e617c44d4f55041f2da31c81cff520d403b80a Mon Sep 17 00:00:00 2001 From: Marin Rukavina Date: Fri, 27 Apr 2012 23:52:32 +0200 Subject: Updated static_view to raise HTTP exceptions instead of returning --- pyramid/static.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pyramid/static.py b/pyramid/static.py index dfb602ee0..63ca58597 100644 --- a/pyramid/static.py +++ b/pyramid/static.py @@ -101,17 +101,17 @@ class static_view(object): path = _secure_path(path_tuple) if path is None: - return HTTPNotFound('Out of bounds: %s' % request.url) + raise HTTPNotFound('Out of bounds: %s' % request.url) if self.package_name: # package resource resource_path ='%s/%s' % (self.docroot.rstrip('/'), path) if resource_isdir(self.package_name, resource_path): if not request.path_url.endswith('/'): - return self.add_slash_redirect(request) + self.add_slash_redirect(request) resource_path = '%s/%s' % (resource_path.rstrip('/'),self.index) if not resource_exists(self.package_name, resource_path): - return HTTPNotFound(request.url) + raise HTTPNotFound(request.url) filepath = resource_filename(self.package_name, resource_path) else: # filesystem file @@ -120,10 +120,10 @@ class static_view(object): filepath = normcase(normpath(join(self.norm_docroot, path))) if isdir(filepath): if not request.path_url.endswith('/'): - return self.add_slash_redirect(request) + self.add_slash_redirect(request) filepath = join(filepath, self.index) if not exists(filepath): - return HTTPNotFound(request.url) + raise HTTPNotFound(request.url) return FileResponse(filepath, request, self.cache_max_age) @@ -132,7 +132,7 @@ class static_view(object): qs = request.query_string if qs: url = url + '?' + qs - return HTTPMovedPermanently(url) + raise HTTPMovedPermanently(url) _seps = set(['/', os.sep]) def _contains_slash(item): -- cgit v1.2.3 From 58572aa89dd721f33e90db17a895c920078b8a4c Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sat, 28 Apr 2012 19:11:25 -0700 Subject: Link the first usage of "scaffold" to its definition in the glossary. --- docs/tutorials/wiki/installation.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorials/wiki/installation.rst b/docs/tutorials/wiki/installation.rst index 63b30da5a..d0f476610 100644 --- a/docs/tutorials/wiki/installation.rst +++ b/docs/tutorials/wiki/installation.rst @@ -131,7 +131,7 @@ Make a Project ============== Your next step is to create a project. :app:`Pyramid` supplies a variety of -scaffolds to generate sample projects. For this tutorial, we will use the +:term:`scaffolds` to generate sample projects. For this tutorial, we will use the :term:`ZODB` -oriented scaffold named ``zodb``. The below instructions assume your current working directory is the -- cgit v1.2.3 From 9895440e51c74a46d94a098492f191a3d4b977cb Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sat, 28 Apr 2012 19:31:10 -0700 Subject: expand glossary term for scaffold --- docs/glossary.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/glossary.rst b/docs/glossary.rst index 88598354a..45a79326f 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -922,9 +922,9 @@ Glossary http://docs.pylonsproject.org/projects/pyramid_debugtoolbar/dev/ . scaffold - A project template that helps users get started writing a Pyramid - application quickly. Scaffolds are usually used via the ``pcreate`` - command. + A project template that generates some of the major parts of a Pyramid + application and helps users to quickly get started writing larger + applications. Scaffolds are usually used via the ``pcreate`` command. pyramid_exclog A package which logs Pyramid application exception (error) information -- cgit v1.2.3 From 2bb472b31999b66ff89e9073aa99f2713f68ace3 Mon Sep 17 00:00:00 2001 From: Patricio Paez Date: Sun, 29 Apr 2012 19:29:46 -0500 Subject: Normalize 'Make a project' on both wiki tutorials --- docs/tutorials/wiki/installation.rst | 7 ++++--- docs/tutorials/wiki2/installation.rst | 8 ++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/tutorials/wiki/installation.rst b/docs/tutorials/wiki/installation.rst index d0f476610..868c99dee 100644 --- a/docs/tutorials/wiki/installation.rst +++ b/docs/tutorials/wiki/installation.rst @@ -130,9 +130,10 @@ Preparation, Windows Make a Project ============== -Your next step is to create a project. :app:`Pyramid` supplies a variety of -:term:`scaffolds` to generate sample projects. For this tutorial, we will use the -:term:`ZODB` -oriented scaffold named ``zodb``. +Your next step is to create a project. For this tutorial, we will use the +:term:`scaffold` named ``zodb``, which generates an application +that uses :term:`ZODB` and :term:`traversal`. :app:`Pyramid` +supplies a variety of scaffolds to generate sample projects. The below instructions assume your current working directory is the "virtualenv" named "pyramidtut". diff --git a/docs/tutorials/wiki2/installation.rst b/docs/tutorials/wiki2/installation.rst index 40486057e..6589a1557 100644 --- a/docs/tutorials/wiki2/installation.rst +++ b/docs/tutorials/wiki2/installation.rst @@ -67,10 +67,10 @@ Preparation, Windows Making a Project ================ -Your next step is to create a project. :app:`Pyramid` supplies a -variety of scaffolds to generate sample projects. We will use the -``alchemy`` scaffold, which generates an application -that uses :term:`SQLAlchemy` and :term:`URL dispatch`. +Your next step is to create a project. For this tutorial, we will use the +:term:`scaffold` named ``alchemy``, which generates an application +that uses :term:`SQLAlchemy` and :term:`URL dispatch`. :app:`Pyramid` +supplies a variety of scaffolds to generate sample projects. The below instructions assume your current working directory is the "virtualenv" named "pyramidtut". -- cgit v1.2.3 From 39e0d1d2b8e9bc1169c6b2f159fa16d468aaf6c5 Mon Sep 17 00:00:00 2001 From: Dan Jacka Date: Mon, 30 Apr 2012 12:46:44 +1200 Subject: Separator between name and value in --header option to prequest is ':', not '=' --- docs/narr/commandline.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/narr/commandline.rst b/docs/narr/commandline.rst index 886e075e3..1485caefc 100644 --- a/docs/narr/commandline.rst +++ b/docs/narr/commandline.rst @@ -460,7 +460,7 @@ to the console. You can add request header values by using the ``--header`` option:: - $ bin/prequest --header=Host=example.com development.ini / + $ bin/prequest --header=Host:example.com development.ini / Headers are added to the WSGI environment by converting them to their CGI/WSGI equivalents (e.g. ``Host=example.com`` will insert the ``HTTP_HOST`` -- cgit v1.2.3 From b9cead35e09b11c13693d6f6838b70948b568f6c Mon Sep 17 00:00:00 2001 From: Dan Jacka Date: Tue, 1 May 2012 13:33:35 +1200 Subject: Change the example script's description string to match the console_script's name: show_settings --- docs/narr/commandline.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/narr/commandline.rst b/docs/narr/commandline.rst index 1485caefc..4be436836 100644 --- a/docs/narr/commandline.rst +++ b/docs/narr/commandline.rst @@ -718,7 +718,7 @@ we'll pretend you have a distribution with a package in it named def settings_show(): description = """\ Print the deployment settings for a Pyramid application. Example: - 'psettings deployment.ini' + 'show_settings deployment.ini' """ usage = "usage: %prog config_uri" parser = optparse.OptionParser( -- cgit v1.2.3 From 04af145ab4b19d6fe0c4a5087c1722868d6fcedc Mon Sep 17 00:00:00 2001 From: Marin Rukavina Date: Wed, 2 May 2012 02:24:39 +0200 Subject: Fixed up the tests and returned HTTPMovedPermanently --- pyramid/static.py | 6 +-- pyramid/tests/test_static.py | 109 +++++++++++++++++++++++++++++++++---------- 2 files changed, 87 insertions(+), 28 deletions(-) diff --git a/pyramid/static.py b/pyramid/static.py index 63ca58597..50b274dae 100644 --- a/pyramid/static.py +++ b/pyramid/static.py @@ -108,7 +108,7 @@ class static_view(object): resource_path ='%s/%s' % (self.docroot.rstrip('/'), path) if resource_isdir(self.package_name, resource_path): if not request.path_url.endswith('/'): - self.add_slash_redirect(request) + return self.add_slash_redirect(request) resource_path = '%s/%s' % (resource_path.rstrip('/'),self.index) if not resource_exists(self.package_name, resource_path): raise HTTPNotFound(request.url) @@ -120,7 +120,7 @@ class static_view(object): filepath = normcase(normpath(join(self.norm_docroot, path))) if isdir(filepath): if not request.path_url.endswith('/'): - self.add_slash_redirect(request) + return self.add_slash_redirect(request) filepath = join(filepath, self.index) if not exists(filepath): raise HTTPNotFound(request.url) @@ -132,7 +132,7 @@ class static_view(object): qs = request.query_string if qs: url = url + '?' + qs - raise HTTPMovedPermanently(url) + return HTTPMovedPermanently(url) _seps = set(['/', os.sep]) def _contains_slash(item): diff --git a/pyramid/tests/test_static.py b/pyramid/tests/test_static.py index 7f94df990..300647099 100644 --- a/pyramid/tests/test_static.py +++ b/pyramid/tests/test_static.py @@ -70,16 +70,26 @@ class Test_static_view_use_subpath_False(unittest.TestCase): inst = self._makeOne('pyramid.tests:fixtures/static') request = self._makeRequest({'PATH_INFO':'/subdir/../../minimal.pt'}) context = DummyContext() - response = inst(context, request) - self.assertEqual(response.status, '404 Not Found') + from pyramid.httpexceptions import HTTPNotFound + try: + response = inst(context, request) + except HTTPNotFound as e: + self.assertEqual(e.code, 404) + else: + self.assertEqual(response.status, '404 Not Found') def test_oob_dotdotslash_encoded(self): inst = self._makeOne('pyramid.tests:fixtures/static') request = self._makeRequest( {'PATH_INFO':'/subdir/%2E%2E%2F%2E%2E/minimal.pt'}) context = DummyContext() - response = inst(context, request) - self.assertEqual(response.status, '404 Not Found') + from pyramid.httpexceptions import HTTPNotFound + try: + response = inst(context, request) + except HTTPNotFound as e: + self.assertEqual(e.code, 404) + else: + self.assertEqual(response.status, '404 Not Found') def test_oob_os_sep(self): import os @@ -88,15 +98,25 @@ class Test_static_view_use_subpath_False(unittest.TestCase): request = self._makeRequest({'PATH_INFO':'/subdir/%s%sminimal.pt' % (dds, dds)}) context = DummyContext() - response = inst(context, request) - self.assertEqual(response.status, '404 Not Found') + from pyramid.httpexceptions import HTTPNotFound + try: + response = inst(context, request) + except HTTPNotFound as e: + self.assertEqual(e.code, 404) + else: + self.assertEqual(response.status, '404 Not Found') def test_resource_doesnt_exist(self): inst = self._makeOne('pyramid.tests:fixtures/static') request = self._makeRequest({'PATH_INFO':'/notthere'}) context = DummyContext() - response = inst(context, request) - self.assertEqual(response.status, '404 Not Found') + from pyramid.httpexceptions import HTTPNotFound + try: + response = inst(context, request) + except HTTPNotFound as e: + self.assertEqual(e.code, 404) + else: + self.assertEqual(response.status, '404 Not Found') def test_resource_isdir(self): inst = self._makeOne('pyramid.tests:fixtures/static') @@ -174,8 +194,13 @@ class Test_static_view_use_subpath_False(unittest.TestCase): inst = self._makeOne('pyramid.tests:fixtures/static') request = self._makeRequest({'PATH_INFO':'/notthere.html'}) context = DummyContext() - response = inst(context, request) - self.assertEqual(response.status, '404 Not Found') + from pyramid.httpexceptions import HTTPNotFound + try: + response = inst(context, request) + except HTTPNotFound as e: + self.assertEqual(e.code, 404) + else: + self.assertEqual(response.status, '404 Not Found') def test_resource_with_content_encoding(self): inst = self._makeOne('pyramid.tests:fixtures/static') @@ -251,33 +276,52 @@ class Test_static_view_use_subpath_True(unittest.TestCase): request = self._makeRequest() request.subpath = ('.', 'index.html') context = DummyContext() - response = inst(context, request) - self.assertEqual(response.status, '404 Not Found') + from pyramid.httpexceptions import HTTPNotFound + try: + response = inst(context, request) + except HTTPNotFound as e: + self.assertEqual(e.code, 404) + else: + self.assertEqual(response.status, '404 Not Found') def test_oob_emptyelement(self): inst = self._makeOne('pyramid.tests:fixtures/static') request = self._makeRequest() request.subpath = ('', 'index.html') context = DummyContext() - response = inst(context, request) - self.assertEqual(response.status, '404 Not Found') + from pyramid.httpexceptions import HTTPNotFound + try: + response = inst(context, request) + except HTTPNotFound as e: + self.assertEqual(e.code, 404) + else: + self.assertEqual(response.status, '404 Not Found') def test_oob_dotdotslash(self): inst = self._makeOne('pyramid.tests:fixtures/static') request = self._makeRequest() request.subpath = ('subdir', '..', '..', 'minimal.pt') context = DummyContext() - response = inst(context, request) - self.assertEqual(response.status, '404 Not Found') + from pyramid.httpexceptions import HTTPNotFound + try: + response = inst(context, request) + except HTTPNotFound as e: + self.assertEqual(e.code, 404) + else: + self.assertEqual(response.status, '404 Not Found') def test_oob_dotdotslash_encoded(self): inst = self._makeOne('pyramid.tests:fixtures/static') request = self._makeRequest() request.subpath = ('subdir', '%2E%2E', '%2E%2E', 'minimal.pt') context = DummyContext() - response = inst(context, request) - self.assertEqual(response.status, '404 Not Found') - + from pyramid.httpexceptions import HTTPNotFound + try: + response = inst(context, request) + except HTTPNotFound as e: + self.assertEqual(e.code, 404) + else: + self.assertEqual(response.status, '404 Not Found') def test_oob_os_sep(self): import os inst = self._makeOne('pyramid.tests:fixtures/static') @@ -285,16 +329,26 @@ class Test_static_view_use_subpath_True(unittest.TestCase): request = self._makeRequest() request.subpath = ('subdir', dds, dds, 'minimal.pt') context = DummyContext() - response = inst(context, request) - self.assertEqual(response.status, '404 Not Found') + from pyramid.httpexceptions import HTTPNotFound + try: + response = inst(context, request) + except HTTPNotFound as e: + self.assertEqual(e.code, 404) + else: + self.assertEqual(response.status, '404 Not Found') def test_resource_doesnt_exist(self): inst = self._makeOne('pyramid.tests:fixtures/static') request = self._makeRequest() request.subpath = ('notthere,') context = DummyContext() - response = inst(context, request) - self.assertEqual(response.status, '404 Not Found') + from pyramid.httpexceptions import HTTPNotFound + try: + response = inst(context, request) + except HTTPNotFound as e: + self.assertEqual(e.code, 404) + else: + self.assertEqual(response.status, '404 Not Found') def test_resource_isdir(self): inst = self._makeOne('pyramid.tests:fixtures/static') @@ -361,8 +415,13 @@ class Test_static_view_use_subpath_True(unittest.TestCase): request = self._makeRequest() request.subpath = ('notthere.html',) context = DummyContext() - response = inst(context, request) - self.assertEqual(response.status, '404 Not Found') + from pyramid.httpexceptions import HTTPNotFound + try: + response = inst(context, request) + except HTTPNotFound as e: + self.assertEqual(e.code, 404) + else: + self.assertEqual(response.status, '404 Not Found') class DummyContext: pass -- cgit v1.2.3 From 3d65afea0d2bd529a87f72d87a986c989ebffd29 Mon Sep 17 00:00:00 2001 From: Marin Rukavina Date: Wed, 2 May 2012 02:55:03 +0200 Subject: Updated tests for static files and made static.py raise all HTTP exceptions --- pyramid/static.py | 6 +++--- pyramid/tests/test_static.py | 32 ++++++++++++++++++++++---------- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/pyramid/static.py b/pyramid/static.py index 50b274dae..63ca58597 100644 --- a/pyramid/static.py +++ b/pyramid/static.py @@ -108,7 +108,7 @@ class static_view(object): resource_path ='%s/%s' % (self.docroot.rstrip('/'), path) if resource_isdir(self.package_name, resource_path): if not request.path_url.endswith('/'): - return self.add_slash_redirect(request) + self.add_slash_redirect(request) resource_path = '%s/%s' % (resource_path.rstrip('/'),self.index) if not resource_exists(self.package_name, resource_path): raise HTTPNotFound(request.url) @@ -120,7 +120,7 @@ class static_view(object): filepath = normcase(normpath(join(self.norm_docroot, path))) if isdir(filepath): if not request.path_url.endswith('/'): - return self.add_slash_redirect(request) + self.add_slash_redirect(request) filepath = join(filepath, self.index) if not exists(filepath): raise HTTPNotFound(request.url) @@ -132,7 +132,7 @@ class static_view(object): qs = request.query_string if qs: url = url + '?' + qs - return HTTPMovedPermanently(url) + raise HTTPMovedPermanently(url) _seps = set(['/', os.sep]) def _contains_slash(item): diff --git a/pyramid/tests/test_static.py b/pyramid/tests/test_static.py index 300647099..0141405e9 100644 --- a/pyramid/tests/test_static.py +++ b/pyramid/tests/test_static.py @@ -38,11 +38,17 @@ class Test_static_view_use_subpath_False(unittest.TestCase): inst = self._makeOne('pyramid.tests:fixtures/static') request = self._makeRequest({'PATH_INFO':''}) context = DummyContext() - response = inst(context, request) - response.prepare(request.environ) - self.assertEqual(response.status, '301 Moved Permanently') - self.assertTrue(b'http://example.com:6543/' in response.body) - + from pyramid.httpexceptions import HTTPMovedPermanently + try: + response = inst(context, request) + except HTTPMovedPermanently as e: + self.assertEqual(e.code, 301) + self.assertTrue(b'http://example.com:6543/' in e.location) + else: + response.prepare(request.environ) + self.assertEqual(response.status, '301 Moved Permanently') + self.assertTrue(b'http://example.com:6543/' in response.body) + def test_path_info_slash_means_index_html(self): inst = self._makeOne('pyramid.tests:fixtures/static') request = self._makeRequest() @@ -258,11 +264,17 @@ class Test_static_view_use_subpath_True(unittest.TestCase): request = self._makeRequest({'PATH_INFO':''}) request.subpath = () context = DummyContext() - response = inst(context, request) - response.prepare(request.environ) - self.assertEqual(response.status, '301 Moved Permanently') - self.assertTrue(b'http://example.com:6543/' in response.body) - + from pyramid.httpexceptions import HTTPMovedPermanently + try: + response = inst(context, request) + except HTTPMovedPermanently as e: + self.assertEqual(e.code, 301) + self.assertTrue(b'http://example.com:6543/' in e.location) + else: + response.prepare(request.environ) + self.assertEqual(response.status, '301 Moved Permanently') + self.assertTrue(b'http://example.com:6543/' in response.body) + def test_path_info_slash_means_index_html(self): inst = self._makeOne('pyramid.tests:fixtures/static') request = self._makeRequest() -- cgit v1.2.3 From e7c4dd632daa71f72bd5e7b6824621cad0dac7cc Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 2 May 2012 23:14:25 -0400 Subject: simplify tests --- pyramid/tests/test_static.py | 105 +++++++------------------------------------ 1 file changed, 15 insertions(+), 90 deletions(-) diff --git a/pyramid/tests/test_static.py b/pyramid/tests/test_static.py index 0141405e9..94497d4f6 100644 --- a/pyramid/tests/test_static.py +++ b/pyramid/tests/test_static.py @@ -39,15 +39,7 @@ class Test_static_view_use_subpath_False(unittest.TestCase): request = self._makeRequest({'PATH_INFO':''}) context = DummyContext() from pyramid.httpexceptions import HTTPMovedPermanently - try: - response = inst(context, request) - except HTTPMovedPermanently as e: - self.assertEqual(e.code, 301) - self.assertTrue(b'http://example.com:6543/' in e.location) - else: - response.prepare(request.environ) - self.assertEqual(response.status, '301 Moved Permanently') - self.assertTrue(b'http://example.com:6543/' in response.body) + self.assertRaises(HTTPMovedPermanently, inst, context, request) def test_path_info_slash_means_index_html(self): inst = self._makeOne('pyramid.tests:fixtures/static') @@ -77,12 +69,7 @@ class Test_static_view_use_subpath_False(unittest.TestCase): request = self._makeRequest({'PATH_INFO':'/subdir/../../minimal.pt'}) context = DummyContext() from pyramid.httpexceptions import HTTPNotFound - try: - response = inst(context, request) - except HTTPNotFound as e: - self.assertEqual(e.code, 404) - else: - self.assertEqual(response.status, '404 Not Found') + self.assertRaises(HTTPNotFound, inst, context, request) def test_oob_dotdotslash_encoded(self): inst = self._makeOne('pyramid.tests:fixtures/static') @@ -90,12 +77,7 @@ class Test_static_view_use_subpath_False(unittest.TestCase): {'PATH_INFO':'/subdir/%2E%2E%2F%2E%2E/minimal.pt'}) context = DummyContext() from pyramid.httpexceptions import HTTPNotFound - try: - response = inst(context, request) - except HTTPNotFound as e: - self.assertEqual(e.code, 404) - else: - self.assertEqual(response.status, '404 Not Found') + self.assertRaises(HTTPNotFound, inst, context, request) def test_oob_os_sep(self): import os @@ -105,24 +87,14 @@ class Test_static_view_use_subpath_False(unittest.TestCase): (dds, dds)}) context = DummyContext() from pyramid.httpexceptions import HTTPNotFound - try: - response = inst(context, request) - except HTTPNotFound as e: - self.assertEqual(e.code, 404) - else: - self.assertEqual(response.status, '404 Not Found') + self.assertRaises(HTTPNotFound, inst, context, request) def test_resource_doesnt_exist(self): inst = self._makeOne('pyramid.tests:fixtures/static') request = self._makeRequest({'PATH_INFO':'/notthere'}) context = DummyContext() from pyramid.httpexceptions import HTTPNotFound - try: - response = inst(context, request) - except HTTPNotFound as e: - self.assertEqual(e.code, 404) - else: - self.assertEqual(response.status, '404 Not Found') + self.assertRaises(HTTPNotFound, inst, context, request) def test_resource_isdir(self): inst = self._makeOne('pyramid.tests:fixtures/static') @@ -201,12 +173,7 @@ class Test_static_view_use_subpath_False(unittest.TestCase): request = self._makeRequest({'PATH_INFO':'/notthere.html'}) context = DummyContext() from pyramid.httpexceptions import HTTPNotFound - try: - response = inst(context, request) - except HTTPNotFound as e: - self.assertEqual(e.code, 404) - else: - self.assertEqual(response.status, '404 Not Found') + self.assertRaises(HTTPNotFound, inst, context, request) def test_resource_with_content_encoding(self): inst = self._makeOne('pyramid.tests:fixtures/static') @@ -265,15 +232,7 @@ class Test_static_view_use_subpath_True(unittest.TestCase): request.subpath = () context = DummyContext() from pyramid.httpexceptions import HTTPMovedPermanently - try: - response = inst(context, request) - except HTTPMovedPermanently as e: - self.assertEqual(e.code, 301) - self.assertTrue(b'http://example.com:6543/' in e.location) - else: - response.prepare(request.environ) - self.assertEqual(response.status, '301 Moved Permanently') - self.assertTrue(b'http://example.com:6543/' in response.body) + self.assertRaises(HTTPMovedPermanently, inst, context, request) def test_path_info_slash_means_index_html(self): inst = self._makeOne('pyramid.tests:fixtures/static') @@ -289,12 +248,7 @@ class Test_static_view_use_subpath_True(unittest.TestCase): request.subpath = ('.', 'index.html') context = DummyContext() from pyramid.httpexceptions import HTTPNotFound - try: - response = inst(context, request) - except HTTPNotFound as e: - self.assertEqual(e.code, 404) - else: - self.assertEqual(response.status, '404 Not Found') + self.assertRaises(HTTPNotFound, inst, context, request) def test_oob_emptyelement(self): inst = self._makeOne('pyramid.tests:fixtures/static') @@ -302,12 +256,7 @@ class Test_static_view_use_subpath_True(unittest.TestCase): request.subpath = ('', 'index.html') context = DummyContext() from pyramid.httpexceptions import HTTPNotFound - try: - response = inst(context, request) - except HTTPNotFound as e: - self.assertEqual(e.code, 404) - else: - self.assertEqual(response.status, '404 Not Found') + self.assertRaises(HTTPNotFound, inst, context, request) def test_oob_dotdotslash(self): inst = self._makeOne('pyramid.tests:fixtures/static') @@ -315,12 +264,7 @@ class Test_static_view_use_subpath_True(unittest.TestCase): request.subpath = ('subdir', '..', '..', 'minimal.pt') context = DummyContext() from pyramid.httpexceptions import HTTPNotFound - try: - response = inst(context, request) - except HTTPNotFound as e: - self.assertEqual(e.code, 404) - else: - self.assertEqual(response.status, '404 Not Found') + self.assertRaises(HTTPNotFound, inst, context, request) def test_oob_dotdotslash_encoded(self): inst = self._makeOne('pyramid.tests:fixtures/static') @@ -328,12 +272,8 @@ class Test_static_view_use_subpath_True(unittest.TestCase): request.subpath = ('subdir', '%2E%2E', '%2E%2E', 'minimal.pt') context = DummyContext() from pyramid.httpexceptions import HTTPNotFound - try: - response = inst(context, request) - except HTTPNotFound as e: - self.assertEqual(e.code, 404) - else: - self.assertEqual(response.status, '404 Not Found') + self.assertRaises(HTTPNotFound, inst, context, request) + def test_oob_os_sep(self): import os inst = self._makeOne('pyramid.tests:fixtures/static') @@ -342,12 +282,7 @@ class Test_static_view_use_subpath_True(unittest.TestCase): request.subpath = ('subdir', dds, dds, 'minimal.pt') context = DummyContext() from pyramid.httpexceptions import HTTPNotFound - try: - response = inst(context, request) - except HTTPNotFound as e: - self.assertEqual(e.code, 404) - else: - self.assertEqual(response.status, '404 Not Found') + self.assertRaises(HTTPNotFound, inst, context, request) def test_resource_doesnt_exist(self): inst = self._makeOne('pyramid.tests:fixtures/static') @@ -355,12 +290,7 @@ class Test_static_view_use_subpath_True(unittest.TestCase): request.subpath = ('notthere,') context = DummyContext() from pyramid.httpexceptions import HTTPNotFound - try: - response = inst(context, request) - except HTTPNotFound as e: - self.assertEqual(e.code, 404) - else: - self.assertEqual(response.status, '404 Not Found') + self.assertRaises(HTTPNotFound, inst, context, request) def test_resource_isdir(self): inst = self._makeOne('pyramid.tests:fixtures/static') @@ -428,12 +358,7 @@ class Test_static_view_use_subpath_True(unittest.TestCase): request.subpath = ('notthere.html',) context = DummyContext() from pyramid.httpexceptions import HTTPNotFound - try: - response = inst(context, request) - except HTTPNotFound as e: - self.assertEqual(e.code, 404) - else: - self.assertEqual(response.status, '404 Not Found') + self.assertRaises(HTTPNotFound, inst, context, request) class DummyContext: pass -- cgit v1.2.3 From 988035afbb745237ea8bec0a7c5e4552d2fc98ba Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 2 May 2012 23:15:46 -0400 Subject: add a change note --- CHANGES.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 67b03d59b..34d60090d 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -34,3 +34,7 @@ Features - Don't add a ``?`` to URLs generated by request.route_url if the ``_query`` argument is provided but empty. + +- The static view machinery now raises (rather than returns) ``HTTPNotFound`` + and ``HTTPMovedPermanently`` exceptions, so these can be caught by the + NotFound view (and other exception views). -- cgit v1.2.3 From 004882434aa166a58c3b2148322e08ce61ec4cb7 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 2 May 2012 23:39:54 -0400 Subject: garden --- TODO.txt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/TODO.txt b/TODO.txt index e0fb0fa27..4b4f48499 100644 --- a/TODO.txt +++ b/TODO.txt @@ -4,6 +4,10 @@ Pyramid TODOs Nice-to-Have ------------ +- config.set_registry_attr (with conflict detection). + +- _fix_registry should dictify the registry being fixed. + - Provide the presumed renderer name to the called view as an attribute of the request. @@ -67,8 +71,6 @@ Nice-to-Have app1" and "domain app1.localhost = app1"), ProxyPreserveHost and the nginx equivalent, preserving HTTPS URLs. -- _fix_registry should dictify the registry being fixed. - - Make "localizer" a property of request (instead of requiring "get_localizer(request)"? @@ -126,6 +128,9 @@ Future - 1.5: Remove ``pyramid.requests.DeprecatedRequestMethodsMixin``. +- 1.5: Maybe? deprecate set_request_property in favor of pointing people at + set_request_method. + - 1.6: Remove IContextURL and TraversalContextURL. Probably Bad Ideas -- cgit v1.2.3 From e012aa12760f6c29bfc9967c50a51d3f47db47da Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 3 May 2012 02:42:55 -0400 Subject: allow __json__ and custom adapters to accept request arg --- CHANGES.txt | 3 ++ docs/narr/renderers.rst | 17 ++++++++---- pyramid/renderers.py | 61 ++++++++++++++++++++++++----------------- pyramid/tests/test_renderers.py | 19 +++++++++---- 4 files changed, 64 insertions(+), 36 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 34d60090d..7c2af4451 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -17,6 +17,9 @@ Features values natively serializable by ``json.dumps`` (such as ints, lists, dictionaries, strings, and so forth). +- The JSON renderer now allows for the definition of custom type adapters to + convert unknown objects to JSON serializations. + - As of this release, the ``request_method`` predicate, when used, will also imply that ``HEAD`` is implied when you use ``GET``. For example, using ``@view_config(request_method='GET')`` is equivalent to using diff --git a/docs/narr/renderers.rst b/docs/narr/renderers.rst index c36caeb87..57b5bc65b 100644 --- a/docs/narr/renderers.rst +++ b/docs/narr/renderers.rst @@ -241,7 +241,8 @@ Serializing Custom Objects Custom objects can be made easily JSON-serializable in Pyramid by defining a ``__json__`` method on the object's class. This method should return values natively JSON-serializable (such as ints, lists, dictionaries, strings, and -so forth). +so forth). It should accept a single additional argument, ``request``, which +will be the active request object at render time. .. code-block:: python :linenos: @@ -252,7 +253,7 @@ so forth). def __init__(self, x): self.x = x - def __json__(self): + def __json__(self, request): return {'x':self.x} @view_config(renderer='json') @@ -269,8 +270,7 @@ to the renderer is not a serializable type, and has no ``__json__`` method, usually a :exc:`TypeError` will be raised during serialization. You can change this behavior by creating a custom JSON renderer and adding adapters to handle custom types. The renderer will attempt to adapt non-serializable -objects using the registered adapters. It will raise a :exc:`TypeError` if it -can't determine what to do with the object. A short example follows: +objects using the registered adapters. A short example follows: .. code-block:: python :linenos: @@ -278,12 +278,19 @@ can't determine what to do with the object. A short example follows: from pyramid.renderers import JSON json_renderer = JSON() - json_renderer.add_adapter(datetime.datetime, lambda x: x.isoformat()) + def datetime_adapter(obj, request): + return obj.isoformat() + json_renderer.add_adapter(datetime.datetime, datetime_adapter) # then during configuration .... config = Configurator() config.add_renderer('json', json_renderer) +The adapter should accept two arguments: the object needing to be serialized +and ``request``, which will be the current request object at render time. +The adapter should raise a :exc:`TypeError` if it can't determine what to do +with the object. + See :class:`pyramid.renderers.JSON` and :ref:`adding_and_overriding_renderers` for more information. diff --git a/pyramid/renderers.py b/pyramid/renderers.py index ad09ad927..bdef6f561 100644 --- a/pyramid/renderers.py +++ b/pyramid/renderers.py @@ -3,7 +3,10 @@ import os import pkg_resources import threading -from zope.interface import implementer +from zope.interface import ( + implementer, + providedBy, + ) from zope.interface.registry import Components from pyramid.interfaces import ( @@ -215,17 +218,18 @@ class JSON(object): self.add_adapter(type, adapter) def add_adapter(self, type_or_iface, adapter): - """ When an object of type (or interface) ``type_or_iface`` - fails to automatically encode using the serializer, the renderer - will use the adapter ``adapter`` to convert it into a - JSON-serializable object. + """ When an object of type (or interface) ``type_or_iface`` fails to + automatically encode using the serializer, the renderer will use the + adapter ``adapter`` to convert it into a JSON-serializable object. + The adapter must accept two arguments: the object and the currently + active request. .. code-block:: python class Foo(object): x = 5 - def foo_adapter(obj): + def foo_adapter(obj, request): return obj.x renderer = JSON(indent=4) @@ -245,25 +249,21 @@ class JSON(object): ct = response.content_type if ct == response.default_content_type: response.content_type = 'application/json' - return self.serializer(value, default=self.default, **self.kw) - return _render - - def default(self, obj): - """ Returns a JSON-serializable representation of ``obj``. If - no representation can be found, a ``TypeError`` is raised. - If the object implements the ``__json__`` magic method, it will - be preferred. Otherwise, attempt to adapt ``obj`` into a - serializable type using one of the registered adapters. - """ - if hasattr(obj, '__json__'): - return obj.__json__() - - result = self.components.queryAdapter(obj, IJSONAdapter, - default=_marker) - if result is not _marker: - return result - raise TypeError('%r is not JSON serializable' % (obj,)) + def default(obj): + if hasattr(obj, '__json__'): + return obj.__json__(request) + obj_iface = providedBy(obj) + adapters = self.components.adapters + result = adapters.lookup((obj_iface,), IJSONAdapter, + default=_marker) + if result is _marker: + raise TypeError('%r is not JSON serializable' % (obj,)) + return result(obj, request) + + return self.serializer(value, default=default, **self.kw) + + return _render json_renderer_factory = JSON() # bw compat @@ -339,7 +339,18 @@ class JSONP(JSON): plain-JSON encoded string with content-type ``application/json``""" def _render(value, system): request = system['request'] - val = self.serializer(value, default=self.default, **self.kw) + + def default(obj): + if hasattr(obj, '__json__'): + return obj.__json__(request) + + result = self.components.queryAdapter(obj, IJSONAdapter, + default=_marker) + if result is not _marker: + return result + raise TypeError('%r is not JSON serializable' % (obj,)) + + val = self.serializer(value, default=default, **self.kw) callback = request.GET.get(self.param_name) if callback is None: ct = 'application/json' diff --git a/pyramid/tests/test_renderers.py b/pyramid/tests/test_renderers.py index 55c5c2f5a..495d7dc23 100644 --- a/pyramid/tests/test_renderers.py +++ b/pyramid/tests/test_renderers.py @@ -370,22 +370,26 @@ class TestJSON(unittest.TestCase): self.assertEqual(request.response.content_type, 'text/mishmash') def test_with_custom_adapter(self): + request = testing.DummyRequest() from datetime import datetime - def adapter(obj): + def adapter(obj, req): + self.assertEqual(req, request) return obj.isoformat() now = datetime.utcnow() renderer = self._makeOne() renderer.add_adapter(datetime, adapter) - result = renderer(None)({'a':now}, {}) + result = renderer(None)({'a':now}, {'request':request}) self.assertEqual(result, '{"a": "%s"}' % now.isoformat()) def test_with_custom_adapter2(self): + request = testing.DummyRequest() from datetime import datetime - def adapter(obj): + def adapter(obj, req): + self.assertEqual(req, request) return obj.isoformat() now = datetime.utcnow() renderer = self._makeOne(adapters=((datetime, adapter),)) - result = renderer(None)({'a':now}, {}) + result = renderer(None)({'a':now}, {'request':request}) self.assertEqual(result, '{"a": "%s"}' % now.isoformat()) def test_with_custom_serializer(self): @@ -404,15 +408,18 @@ class TestJSON(unittest.TestCase): self.assertTrue('default' in serializer.kw) def test_with_object_adapter(self): + request = testing.DummyRequest() + outerself = self class MyObject(object): def __init__(self, x): self.x = x - def __json__(self): + def __json__(self, req): + outerself.assertEqual(req, request) return {'x': self.x} objects = [MyObject(1), MyObject(2)] renderer = self._makeOne()(None) - result = renderer(objects, {}) + result = renderer(objects, {'request':request}) self.assertEqual(result, '[{"x": 1}, {"x": 2}]') def test_with_object_adapter_no___json__(self): -- cgit v1.2.3 From e261b6f4343b83d2904bfe9481a546b7d97e7af3 Mon Sep 17 00:00:00 2001 From: Marin Rukavina Date: Thu, 3 May 2012 10:26:45 +0200 Subject: Updated contributors file --- CONTRIBUTORS.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 4b780d3a7..98f73d5f9 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -170,3 +170,5 @@ Contributors - Steve Piercy, 2012/03/27 - Wayne Witzel III, 2012/03/27 + +- Marin Rukavina, 2012/05/03 -- cgit v1.2.3 From 0ce30393a763628379d40bcfa667b3e05a433ec4 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 3 May 2012 11:09:06 -0400 Subject: use the same default logic for jsonp as for json --- pyramid/renderers.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pyramid/renderers.py b/pyramid/renderers.py index bdef6f561..701878264 100644 --- a/pyramid/renderers.py +++ b/pyramid/renderers.py @@ -343,12 +343,13 @@ class JSONP(JSON): def default(obj): if hasattr(obj, '__json__'): return obj.__json__(request) - - result = self.components.queryAdapter(obj, IJSONAdapter, - default=_marker) - if result is not _marker: - return result - raise TypeError('%r is not JSON serializable' % (obj,)) + obj_iface = providedBy(obj) + adapters = self.components.adapters + result = adapters.lookup((obj_iface,), IJSONAdapter, + default=_marker) + if result is _marker: + raise TypeError('%r is not JSON serializable' % (obj,)) + return result(obj, request) val = self.serializer(value, default=default, **self.kw) callback = request.GET.get(self.param_name) -- cgit v1.2.3 From 5851d82e08209a83bb6aa311cfbf4e3ab65c29fe Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 3 May 2012 11:24:44 -0400 Subject: reuse default logic between json and jsonp --- pyramid/renderers.py | 39 +++++++++++++++------------------------ 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/pyramid/renderers.py b/pyramid/renderers.py index 701878264..c9ae5b433 100644 --- a/pyramid/renderers.py +++ b/pyramid/renderers.py @@ -249,22 +249,24 @@ class JSON(object): ct = response.content_type if ct == response.default_content_type: response.content_type = 'application/json' - - def default(obj): - if hasattr(obj, '__json__'): - return obj.__json__(request) - obj_iface = providedBy(obj) - adapters = self.components.adapters - result = adapters.lookup((obj_iface,), IJSONAdapter, - default=_marker) - if result is _marker: - raise TypeError('%r is not JSON serializable' % (obj,)) - return result(obj, request) - + default = self._make_default(request) return self.serializer(value, default=default, **self.kw) return _render + def _make_default(self, request): + def default(obj): + if hasattr(obj, '__json__'): + return obj.__json__(request) + obj_iface = providedBy(obj) + adapters = self.components.adapters + result = adapters.lookup((obj_iface,), IJSONAdapter, + default=_marker) + if result is _marker: + raise TypeError('%r is not JSON serializable' % (obj,)) + return result(obj, request) + return default + json_renderer_factory = JSON() # bw compat class JSONP(JSON): @@ -339,18 +341,7 @@ class JSONP(JSON): plain-JSON encoded string with content-type ``application/json``""" def _render(value, system): request = system['request'] - - def default(obj): - if hasattr(obj, '__json__'): - return obj.__json__(request) - obj_iface = providedBy(obj) - adapters = self.components.adapters - result = adapters.lookup((obj_iface,), IJSONAdapter, - default=_marker) - if result is _marker: - raise TypeError('%r is not JSON serializable' % (obj,)) - return result(obj, request) - + default = self._make_default(request) val = self.serializer(value, default=default, **self.kw) callback = request.GET.get(self.param_name) if callback is None: -- cgit v1.2.3 From cfabb1bbd36d6614eff6576cd87598de5af376c1 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 3 May 2012 11:40:45 -0400 Subject: garden --- pyramid/renderers.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/pyramid/renderers.py b/pyramid/renderers.py index c9ae5b433..c5d33dc16 100644 --- a/pyramid/renderers.py +++ b/pyramid/renderers.py @@ -218,11 +218,11 @@ class JSON(object): self.add_adapter(type, adapter) def add_adapter(self, type_or_iface, adapter): - """ When an object of type (or interface) ``type_or_iface`` fails to - automatically encode using the serializer, the renderer will use the - adapter ``adapter`` to convert it into a JSON-serializable object. - The adapter must accept two arguments: the object and the currently - active request. + """ When an object of the type (or interface) ``type_or_iface`` fails + to automatically encode using the serializer, the renderer will use + the adapter ``adapter`` to convert it into a JSON-serializable + object. The adapter must accept two arguments: the object and the + currently active request. .. code-block:: python @@ -234,7 +234,11 @@ class JSON(object): renderer = JSON(indent=4) renderer.add_adapter(Foo, foo_adapter) - """ + + When you've done this, the JSON renderer will be able to serialize + instances of the ``Foo`` class when they're encountered in your view + results.""" + self.components.registerAdapter(adapter, (type_or_iface,), IJSONAdapter) @@ -301,7 +305,7 @@ class JSONP(JSON): The arguments passed to this class' constructor mean the same thing as the arguments passed to :class:`pyramid.renderers.JSON` (including - ``default``). + ``serializer`` and ``adapters``). Once this renderer is registered via :meth:`~pyramid.config.Configurator.add_renderer` as above, you can use -- cgit v1.2.3 From 5d0989efeb3eecd4cc55fd9c1dcaf1134ced56b2 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 4 May 2012 19:56:00 -0400 Subject: add introspection to whats unique --- docs/narr/introduction.rst | 28 ++++++++++++++++++++++++++++ docs/narr/introspector.rst | 2 +- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/docs/narr/introduction.rst b/docs/narr/introduction.rst index 8f7b17dc3..2d04a4f5a 100644 --- a/docs/narr/introduction.rst +++ b/docs/narr/introduction.rst @@ -801,6 +801,34 @@ within a function called when another user uses the See also :ref:`add_directive`. +Programmatic Introspection +-------------------------- + +If you're building a large system that other users may plug code into, it's +useful to be able to get an enumeration of what code they plugged in *at +application runtime*. For example, you might want to show them a set of tabs +at the top of the screen based on an enumeration of views they registered. + +This is possible using Pyramid's :term:`introspector`. + +Here's an example of using Pyramid's introspector from within a view +callable: + +.. code-block:: python + :linenos: + + from pyramid.view import view_config + from pyramid.response import Response + + @view_config(route_name='bar') + def show_current_route_pattern(request): + introspector = request.registry.introspector + route_name = request.matched_route.name + route_intr = introspector.get('routes', route_name) + return Response(str(route_intr['pattern'])) + +See also :ref:`using_introspection`. + Testing ~~~~~~~ diff --git a/docs/narr/introspector.rst b/docs/narr/introspector.rst index 74595cac8..6bfaf11c0 100644 --- a/docs/narr/introspector.rst +++ b/docs/narr/introspector.rst @@ -32,7 +32,7 @@ callable: from pyramid.response import Response @view_config(route_name='bar') - def route_accepts(request): + def show_current_route_pattern(request): introspector = request.registry.introspector route_name = request.matched_route.name route_intr = introspector.get('routes', route_name) -- cgit v1.2.3 From d6a9543c1149c02c19aca3d053a5afd9ca0f1dbf Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 4 May 2012 19:59:50 -0400 Subject: garden --- docs/narr/introduction.rst | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/narr/introduction.rst b/docs/narr/introduction.rst index 2d04a4f5a..a1f1c7d5e 100644 --- a/docs/narr/introduction.rst +++ b/docs/narr/introduction.rst @@ -593,11 +593,12 @@ it is to shoehorn a route into an ordered list of other routes, or to create another entire instance of an application to service a department and glue code to allow disparate apps to share data. It's a great fit for sites that naturally lend themselves to changing departmental hierarchies, such as -content management systems and document management systems. Traversal also lends itself well to -systems that require very granular security ("Bob can edit *this* document" -as opposed to "Bob can edit documents"). +content management systems and document management systems. Traversal also +lends itself well to systems that require very granular security ("Bob can +edit *this* document" as opposed to "Bob can edit documents"). -Example: :ref:`hello_traversal_chapter` and :ref:`much_ado_about_traversal_chapter`. +Examples: :ref:`hello_traversal_chapter` and +:ref:`much_ado_about_traversal_chapter`. Tweens ~~~~~~ -- cgit v1.2.3 From 1252ab764fda606003aa23a0e3bfa89ba948e3f1 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 4 May 2012 20:07:10 -0400 Subject: add python 3 as a uniqueness --- docs/narr/introduction.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/narr/introduction.rst b/docs/narr/introduction.rst index a1f1c7d5e..9b3a63089 100644 --- a/docs/narr/introduction.rst +++ b/docs/narr/introduction.rst @@ -830,6 +830,14 @@ callable: See also :ref:`using_introspection`. +Python 3 Compatibility +---------------------- + +Pyramid and most of its add-ons are Python 3 compatible. If you develop a +Pyramid application today, you won't need to worry that five years from now +you'll be backwatered because there are language features you'd like to use +but your framework doesn't support newer Python versions. + Testing ~~~~~~~ -- cgit v1.2.3 From eba3b0763cf4fa3aa4b099adb2e10c53c6d99e74 Mon Sep 17 00:00:00 2001 From: Blaise Laflamme Date: Sun, 6 May 2012 19:56:21 -0400 Subject: added sphinx theme as submodule, added rtd hack to update theme --- .gitmodules | 3 +++ docs/.gitignore | 1 - docs/Makefile | 6 ++++-- docs/_themes | 1 + docs/conf.py | 15 ++++++--------- 5 files changed, 14 insertions(+), 12 deletions(-) create mode 100644 .gitmodules create mode 160000 docs/_themes diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..45397942b --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "docs/_themes"] + path = docs/_themes + url = git://github.com/Pylons/pylons_sphinx_theme.git diff --git a/docs/.gitignore b/docs/.gitignore index da7abd0c0..30d731d4a 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -1,4 +1,3 @@ -_themes _build diff --git a/docs/Makefile b/docs/Makefile index bb381fc53..e4a325022 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -25,7 +25,7 @@ help: clean: -rm -rf _build/* -html: +html: _themes mkdir -p _build/html _build/doctrees $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) _build/html @echo @@ -47,7 +47,7 @@ pickle: web: pickle -htmlhelp: +htmlhelp: _themes mkdir -p _build/htmlhelp _build/doctrees $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) _build/htmlhelp @echo @@ -84,3 +84,5 @@ epub: @echo @echo "Build finished. The epub file is in _build/epub." +_themes: + git submodule update --init diff --git a/docs/_themes b/docs/_themes new file mode 160000 index 000000000..f59f7bfce --- /dev/null +++ b/docs/_themes @@ -0,0 +1 @@ +Subproject commit f59f7bfce5259f50fbb67b9040c03ecb080130b4 diff --git a/docs/conf.py b/docs/conf.py index db972261d..fc3d184ed 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -132,18 +132,15 @@ if 'sphinx-build' in ' '.join(sys.argv): # protect against dumb importers from subprocess import call, Popen, PIPE p = Popen('which git', shell=True, stdout=PIPE) - git = p.stdout.read().strip() + cwd = os.getcwd() _themes = os.path.join(cwd, '_themes') - - if not os.path.isdir(_themes): - call([git, 'clone', 'git://github.com/Pylons/pylons_sphinx_theme.git', - '_themes']) + p = Popen('which git', shell=True, stdout=PIPE) + git = p.stdout.read().strip() + if not os.listdir(_themes): + call([git, 'submodule', '--init']) else: - os.chdir(_themes) - call([git, 'checkout', 'master']) - call([git, 'pull']) - os.chdir(cwd) + call([git, 'submodule', 'update']) sys.path.append(os.path.abspath('_themes')) -- cgit v1.2.3 From 1d03bbca7fe9f005d4a08c7dfe7eb139ab4b0df1 Mon Sep 17 00:00:00 2001 From: Patricio Paez Date: Mon, 7 May 2012 21:45:40 -0500 Subject: Two grammatical fixes --- docs/narr/advconfig.rst | 2 +- docs/narr/introduction.rst | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/narr/advconfig.rst b/docs/narr/advconfig.rst index 9cb4db325..2949dc808 100644 --- a/docs/narr/advconfig.rst +++ b/docs/narr/advconfig.rst @@ -282,7 +282,7 @@ Pyramid application, and they want to customize the configuration of this application without hacking its code "from outside", they can "include" a configuration function from the package and override only some of its configuration statements within the code that does the include. No conflicts -will be generated by configuration statements within the code which does the +will be generated by configuration statements within the code that does the including, even if configuration statements in the included code would conflict if it was moved "up" to the calling code. diff --git a/docs/narr/introduction.rst b/docs/narr/introduction.rst index 9b3a63089..b5fa6a9f7 100644 --- a/docs/narr/introduction.rst +++ b/docs/narr/introduction.rst @@ -534,14 +534,14 @@ Configuration extensibility ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Unlike other systems, Pyramid provides a structured "include" mechanism (see -:meth:`~pyramid.config.Configurator.include`) that allows you to compose +:meth:`~pyramid.config.Configurator.include`) that allows you to combine applications from multiple Python packages. All the configuration statements that can be performed in your "main" Pyramid application can also be performed by included packages including the addition of views, routes, subscribers, and even authentication and authorization policies. You can even extend or override an existing application by including another application's configuration in your own, overriding or adding new views and routes to -it. This has the potential to allow you to compose a big application out of +it. This has the potential to allow you to create a big application out of many other smaller ones. For example, if you want to reuse an existing application that already has a bunch of routes, you can just use the ``include`` statement with a ``route_prefix``; the new application will live -- cgit v1.2.3 From 25f3e44681459deff685b7b5c769d98f21532704 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 9 May 2012 09:31:23 -0400 Subject: activate sphinx.ext.viewcode extension --- docs/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/conf.py b/docs/conf.py index db972261d..21843933c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -48,6 +48,7 @@ extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.doctest', 'repoze.sphinx.autointerface', + 'sphinx.ext.viewcode', # 'sphinx.ext.intersphinx' ] -- cgit v1.2.3 From 49ebfee8804717e9e6d3346eff71a20f34ed6256 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 11 May 2012 16:45:10 -0400 Subject: add link to explanation --- docs/tutorials/modwsgi/index.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/tutorials/modwsgi/index.rst b/docs/tutorials/modwsgi/index.rst index c2baa5bd8..e070f8eda 100644 --- a/docs/tutorials/modwsgi/index.rst +++ b/docs/tutorials/modwsgi/index.rst @@ -99,7 +99,8 @@ commands and files. .. code-block:: apache # Use only 1 Python sub-interpreter. Multiple sub-interpreters - # play badly with C extensions. + # play badly with C extensions. See + # http://stackoverflow.com/a/10558360/209039 WSGIApplicationGroup %{GLOBAL} WSGIPassAuthorization On WSGIDaemonProcess pyramid user=chrism group=staff threads=4 \ -- cgit v1.2.3 From 9182856c7b56c6f4376fc5674cda83860a11952f Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 12 May 2012 23:29:28 -0400 Subject: remove uses-interfaces-too-liberally (this was only true of bfg) and update dependencies section --- docs/designdefense.rst | 110 +++++++------------------------------------------ 1 file changed, 16 insertions(+), 94 deletions(-) diff --git a/docs/designdefense.rst b/docs/designdefense.rst index bbcf9c2ec..8fd54161c 100644 --- a/docs/designdefense.rst +++ b/docs/designdefense.rst @@ -375,73 +375,6 @@ at least some ZCA concepts. In some places it's used unabashedly, and will be forever. We know it's quirky, but it's also useful and fundamentally understandable if you take the time to do some reading about it. -Pyramid Uses Interfaces Too Liberally -------------------------------------- - -In this `TOPP Engineering blog entry -`_, -Ian Bicking asserts that the way :mod:`repoze.bfg` used a Zope interface to -represent an HTTP request method added too much indirection for not enough -gain. We agreed in general, and for this reason, :mod:`repoze.bfg` version -1.1 (and subsequent versions including :app:`Pyramid` 1.0+) added :term:`view -predicate` and :term:`route predicate` modifiers to view configuration. -Predicates are request-specific (or :term:`context` -specific) matching -narrowers which don't use interfaces. Instead, each predicate uses a -domain-specific string as a match value. - -For example, to write a view configuration which matches only requests with -the ``POST`` HTTP request method, you might write a ``@view_config`` -decorator which mentioned the ``request_method`` predicate: - -.. code-block:: python - :linenos: - - from pyramid.view import view_config - @view_config(name='post_view', request_method='POST', renderer='json') - def post_view(request): - return 'POSTed' - -You might further narrow the matching scenario by adding an ``accept`` -predicate that narrows matching to something that accepts a JSON response: - -.. code-block:: python - :linenos: - - from pyramid.view import view_config - @view_config(name='post_view', request_method='POST', - accept='application/json', renderer='json') - def post_view(request): - return 'POSTed' - -Such a view would only match when the request indicated that HTTP request -method was ``POST`` and that the remote user agent passed -``application/json`` (or, for that matter, ``application/*``) in its -``Accept`` request header. - -Under the hood, these features make no use of interfaces. - -Many prebaked predicates exist. However, use of only prebaked predicates, -however, doesn't entirely meet Ian's criterion. He would like to be able to -match a request using a lambda or another function which interrogates the -request imperatively. In :mod:`repoze.bfg` version 1.2, we acommodate this -by allowing people to define custom view predicates: - -.. code-block:: python - :linenos: - - from pyramid.view import view_config - from pyramid.response import Response - - def subpath(context, request): - return request.subpath and request.subpath[0] == 'abc' - - @view_config(custom_predicates=(subpath,)) - def aview(request): - return Response('OK') - -The above view will only match when the first element of the request's -:term:`subpath` is ``abc``. - .. _zcml_encouragement: Pyramid "Encourages Use of ZCML" @@ -711,33 +644,22 @@ over 2K lines of Python code, excluding tests. Pyramid Has Too Many Dependencies --------------------------------- -This is true. At the time of this writing, the total number of Python -package distributions that :app:`Pyramid` depends upon transitively is 15 if -you use Python 2.7, or 17 if you use Python 2.5 or 2.6. This is a lot more -than zero package distribution dependencies: a metric which various Python -microframeworks and Django boast. - -The :mod:`zope.component`, package on which :app:`Pyramid` depends has -transitive dependencies on several other packages (:mod:`zope.event`, and -:mod:`zope.interface`). :app:`Pyramid` also has its own direct dependencies, -such as :term:`PasteDeploy`, :term:`Chameleon`, :term:`Mako`, :term:`WebOb`, -:mod:`zope.deprecation` and some of these in turn have their own transitive -dependencies. - -We try not to reinvent too many wheels (at least the ones that don't need -reinventing), and this comes at the cost of some number of dependencies. -However, "number of package distributions" is just not a terribly great -metric to measure complexity. For example, the :mod:`zope.event` -distribution on which :app:`Pyramid` depends has a grand total of four lines -of runtime code. - -In the meantime, :app:`Pyramid` has a number of package distribution -dependencies comparable to similarly-targeted frameworks such as Pylons 1.X. -It may be in the future that we shed more dependencies as the result of a -port to Python 3 (the less code we need to port, the better). In the future, -we may also move templating system dependencies out of the core and place -them in add-on packages, to be included by developers instead of by the -framework. This would reduce the number of core dependencies by about five. +This is true. At the time of this writing (Pyramid 1.3), the total number of +Python package distributions that :app:`Pyramid` depends upon transitively is +if you use Python 3.2 or Python 2.7 is 10. If you use Python 2.6, Pyramid +will pull in 12 package distributions. This is a lot more than zero package +distribution dependencies: a metric which various Python microframeworks and +Django boast. + +However, Pyramid 1.2 relied on 15 packages under Python 2.7 and 17 packages +under Python 2.6, so we've made progress here. A port to Python 3 completed +in Pyramid 1.3 helped us shed a good number of dependencies by forcing us to +make better packaging decisions. + +In the future, we may also move templating system dependencies out of the +core and place them in add-on packages, to be included by developers instead +of by the framework. This would reduce the number of core dependencies by +about five, leaving us with only five remaining core dependencies. Pyramid "Cheats" To Obtain Speed -------------------------------- -- cgit v1.2.3 From 60055439b06eee11468a70787698fab33673a7ae Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 12 May 2012 23:37:48 -0400 Subject: configurator, not configuration --- docs/designdefense.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/designdefense.rst b/docs/designdefense.rst index 8fd54161c..d896022e6 100644 --- a/docs/designdefense.rst +++ b/docs/designdefense.rst @@ -1569,7 +1569,7 @@ Pyramid Doesn't Offer Pluggable Apps ------------------------------------ It is "Pyramidic" to compose multiple external sources into the same -configuration using :meth:`~pyramid.config.Configuration.include`. Any +configuration using :meth:`~pyramid.config.Configurator.include`. Any number of includes can be done to compose an application; includes can even be done from within other includes. Any directive can be used within an include that can be used outside of one (such as -- cgit v1.2.3 From f35062faf63dc62addc8f05e9d8f0637bbee58e0 Mon Sep 17 00:00:00 2001 From: Bryce Boe Date: Tue, 15 May 2012 19:38:46 -0700 Subject: Fixed spelling errors and double backslash in `C:\`. --- docs/narr/project.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/narr/project.rst b/docs/narr/project.rst index d18d93605..da184ada7 100644 --- a/docs/narr/project.rst +++ b/docs/narr/project.rst @@ -144,13 +144,13 @@ directories which he creates within his ``~/projects`` directory. On Windows, it's a good idea to put project directories within a directory that contains no space characters, so it's wise to *avoid* a path that contains i.e. ``My Documents``. As a result, the author, when he uses Windows, just -puts his projects in ``C:\\projects``. +puts his projects in ``C:\projects``. .. warning:: You’ll need to avoid using ``pcreate`` to create a project with the same as a Python standard library component. In particular, this means you - should avoid using names the names ``site`` or ``test``, both of which + should avoid using the names ``site`` or ``test``, both of which conflict with Python standard library packages. You should also avoid using the name ``pyramid``, which will conflict with Pyramid itself. @@ -684,7 +684,7 @@ testing your application, packaging, and distributing your application. .. note:: - ``setup.py`` is the defacto standard which Python developers use to + ``setup.py`` is the de facto standard which Python developers use to distribute their reusable code. You can read more about ``setup.py`` files and their usage in the `Setuptools documentation `_ and `The @@ -966,7 +966,7 @@ named ``views`` instead of within a single ``views.py`` file, you might: You can then continue to add view callable functions to the ``blog.py`` module, but you can also add other ``.py`` files which contain view callable functions to the ``views`` directory. As long as you use the -``@view_config`` directive to register views in conjuction with +``@view_config`` directive to register views in conjunction with ``config.scan()`` they will be picked up automatically when the application is restarted. @@ -994,7 +994,7 @@ run a :app:`Pyramid` application is purely conventional based on the output of its scaffolding. But we strongly recommend using while developing your application, because many other convenience introspection commands (such as ``pviews``, ``prequest``, ``proutes`` and others) are also implemented in -terms of configuration availaibility of this ``.ini`` file format. It also +terms of configuration availability of this ``.ini`` file format. It also configures Pyramid logging and provides the ``--reload`` switch for convenient restarting of the server when code changes. -- cgit v1.2.3 From c9888fcafe4b78924fc0f8b55c4730667bba8558 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Wed, 16 May 2012 17:46:08 -0400 Subject: Remove last class advice usage (breaks under Py3k with new zope.interface. --- pyramid/config/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index ad4df28d8..9e9b5321b 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -5,9 +5,9 @@ from functools import wraps from zope.interface import ( Interface, - classProvides, implementedBy, implementer, + provider, ) from zope.interface.interfaces import IInterface @@ -385,8 +385,8 @@ class ViewDeriver(object): return decorator(view) @implementer(IViewMapper) +@provider(IViewMapperFactory) class DefaultViewMapper(object): - classProvides(IViewMapperFactory) def __init__(self, **kw): self.attr = kw.get('attr') -- cgit v1.2.3 From 68eea7d468b1a53f6ee69f19db44e5c60a563c87 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Wed, 16 May 2012 17:46:30 -0400 Subject: Fix resource leak warning under Py3k. --- pyramid/tests/test_response.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyramid/tests/test_response.py b/pyramid/tests/test_response.py index 03d96c1c4..e6d90f979 100644 --- a/pyramid/tests/test_response.py +++ b/pyramid/tests/test_response.py @@ -31,11 +31,13 @@ class TestFileResponse(unittest.TestCase): path = self._getPath() r = self._makeOne(path, content_type='image/jpeg') self.assertEqual(r.content_type, 'image/jpeg') + r.app_iter.close() def test_without_content_type(self): path = self._getPath() r = self._makeOne(path) self.assertEqual(r.content_type, 'text/plain') + r.app_iter.close() class TestFileIter(unittest.TestCase): def _makeOne(self, file, block_size): -- cgit v1.2.3 From 1aa978c074afce7f821634e5aa8366caa07ee437 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20B=C3=BCchler?= Date: Wed, 23 May 2012 09:28:53 +0300 Subject: Fixed a few glitches in the "Using a Route Prefix to Compose Applications" section of the docs/narr/urldispatch.rst docs. --- 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 acbccbdfd..ecf3d026a 100644 --- a/docs/narr/urldispatch.rst +++ b/docs/narr/urldispatch.rst @@ -954,7 +954,7 @@ will be prepended with the first: from pyramid.config import Configurator def timing_include(config): - config.add_route('show_times', /times') + config.add_route('show_times', '/times') def users_include(config): config.add_route('show_users', '/show') @@ -966,7 +966,7 @@ will be prepended with the first: In the above configuration, the ``show_users`` route will still have an effective route pattern of ``/users/show``. The ``show_times`` route -however, will have an effective pattern of ``/users/timing/show_times``. +however, will have an effective pattern of ``/users/timing/times``. Route prefixes have no impact on the requirement that the set of route *names* in any given Pyramid configuration must be entirely unique. If you @@ -981,7 +981,7 @@ that may be added in the future. For example: from pyramid.config import Configurator def timing_include(config): - config.add_route('timing.show_times', /times') + config.add_route('timing.show_times', '/times') def users_include(config): config.add_route('users.show_users', '/show') -- cgit v1.2.3 From be8e3acc64767e5a0ba798037118da9f262bdf93 Mon Sep 17 00:00:00 2001 From: Zeb Palmer Date: Thu, 31 May 2012 21:03:38 -0600 Subject: Added missing word "name" --- docs/narr/project.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/narr/project.rst b/docs/narr/project.rst index da184ada7..1e2c225d2 100644 --- a/docs/narr/project.rst +++ b/docs/narr/project.rst @@ -149,7 +149,7 @@ puts his projects in ``C:\projects``. .. warning:: You’ll need to avoid using ``pcreate`` to create a project with the same - as a Python standard library component. In particular, this means you + name as a Python standard library component. In particular, this means you should avoid using the names ``site`` or ``test``, both of which conflict with Python standard library packages. You should also avoid using the name ``pyramid``, which will conflict with Pyramid itself. -- cgit v1.2.3 From 566a2a641a03b2d4bec6b19e8d20148dbc2769b4 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 4 Jun 2012 19:19:24 -0400 Subject: point back to renderer_system_values in render and render_to_response --- pyramid/renderers.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/pyramid/renderers.py b/pyramid/renderers.py index c5d33dc16..e526f9997 100644 --- a/pyramid/renderers.py +++ b/pyramid/renderers.py @@ -65,10 +65,11 @@ def render(renderer_name, value, request=None, package=None): dictionary. For other renderers, this will need to be whatever sort of value the renderer expects. - The 'system' values supplied to the renderer will include a basic - set of top-level system names, such as ``request``, ``context``, - and ``renderer_name``. If :term:`renderer globals` have been - specified, these will also be used to agument the value. + The 'system' values supplied to the renderer will include a basic set of + top-level system names, such as ``request``, ``context``, + ``renderer_name``, and ``view``. See :ref:`renderer_system_values` for + the full list. If :term:`renderer globals` have been specified, these + will also be used to agument the value. Supply a ``request`` parameter in order to provide the renderer with the most correct 'system' values (``request`` and ``context`` @@ -108,10 +109,11 @@ def render_to_response(renderer_name, value, request=None, package=None): dictionary. For other renderers, this will need to be whatever sort of value the renderer expects. - The 'system' values supplied to the renderer will include a basic - set of top-level system names, such as ``request``, ``context``, - and ``renderer_name``. If :term:`renderer globals` have been - specified, these will also be used to agument the value. + The 'system' values supplied to the renderer will include a basic set of + top-level system names, such as ``request``, ``context``, + ``renderer_name``, and ``view``. See :ref:`renderer_system_values` for + the full list. If :term:`renderer globals` have been specified, these + will also be used to agument the value. Supply a ``request`` parameter in order to provide the renderer with the most correct 'system' values (``request`` and ``context`` -- cgit v1.2.3 From a319249fdb6e0539e65e0b297829ed8c7f799b98 Mon Sep 17 00:00:00 2001 From: Jeff Cook Date: Thu, 7 Jun 2012 14:00:07 -0600 Subject: Update documentation to clarify purpose of BeforeRender.rendering_val. --- docs/narr/hooks.rst | 15 +++++++++++++++ pyramid/events.py | 20 ++++++++++++++++---- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index a2143b3c5..30eec04f0 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -289,6 +289,21 @@ keys added to the renderer globals dictionary by all :class:`pyramid.events.BeforeRender` subscribers and renderer globals factories must be unique. +The dictionary returned from the view is accessible through the +:attr:`rendering_val` attribute of a :class:`~pyramid.events.BeforeRender` +event, like so: + +.. code-block:: python + :linenos: + + from pyramid.events import subscriber + from pyramid.events import BeforeRender + + @subscriber(BeforeRender) + def read_return(event): + # 'mykey' is returned from the view + print(event.rendering_val['mykey']) + See the API documentation for the :class:`~pyramid.events.BeforeRender` event interface at :class:`pyramid.interfaces.IBeforeRender`. diff --git a/pyramid/events.py b/pyramid/events.py index e181ef33f..1941c594c 100644 --- a/pyramid/events.py +++ b/pyramid/events.py @@ -200,10 +200,22 @@ class BeforeRender(dict): setting an overriding value (which can be done using ``.get`` or ``__contains__`` of the event object). - The event has an additional attribute named ``rendering_val``. This is - the (non-system) value returned by a view or passed to ``render*`` as - ``value``. This feature is new in Pyramid 1.2. - + The dictionary returned from the view is accessible through the + :attr:`rendering_val` attribute of a :class:`~pyramid.events.BeforeRender` + event, like so:: + + from pyramid.events import subscriber + from pyramid.events import BeforeRender + + @subscriber(BeforeRender) + def read_return(event): + # "mykey" is returned from the view + print(event.rendering_val['mykey']) + + In other words, ``rendering_val`` is the (non-system) value returned by a + view or passed to ``render*`` as ``value``. This feature is new in Pyramid + 1.2. + For a description of the values present in the renderer globals dictionary, see :ref:`renderer_system_values`. -- cgit v1.2.3 From dcab614e3252054eba5f2333af8bc3c4120e9980 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 7 Jun 2012 18:21:13 -0400 Subject: rendering --- pyramid/url.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/url.py b/pyramid/url.py index dd83bb631..52e172d3f 100644 --- a/pyramid/url.py +++ b/pyramid/url.py @@ -711,7 +711,7 @@ class URLMethodsMixin(object): _app_url=request.script_name)``. :meth:`pyramid.request.Request.current_route_path` is, in fact, implemented in terms of - `:meth:`pyramid.request.Request.current_route_url` in just this + :meth:`pyramid.request.Request.current_route_url` in just this way. As a result, any ``_app_url`` passed within the ``**kw`` values to ``current_route_path`` will be ignored. """ -- cgit v1.2.3 From 8c4210f94132e2ba844732bd1dada82696fc20db Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 11 Jun 2012 11:03:56 -0400 Subject: how to build HTML docs --- HACKING.txt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/HACKING.txt b/HACKING.txt index 593e89ac1..ec0bcb000 100644 --- a/HACKING.txt +++ b/HACKING.txt @@ -48,6 +48,23 @@ checkout. $ cd starter $ ../bin/python setup.py develop +Building the HTML Docs +----------------------- + +- Check out Pyramid from Github. + +- Create a virtualenv or reuse an existing one that you're using to develop + Pyramid. + +- Run ``$yourvenv/bin/python setup.py dev docs``. + +- cd to ``docs`` within the Pyramid checkout and execute ``make clean html + SPHINXBUILD=$yourvenv/bin/sphinx-build``. The ``SPHINXBUILD=...`` hair is + there in order to tell it to use the virtualenv Python, which will have + both Sphinx and Pyramid (for API documentation generation) installed. + +- The rendered HTML docs will end up in ``docs/_build/html``. + Adding Features --------------- -- cgit v1.2.3 From c7fcdf1665cfdc1173559baa0a56d9a06fcba448 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 11 Jun 2012 11:16:07 -0400 Subject: consolidate --- HACKING.txt | 46 +++++++++++++++++----------------------------- 1 file changed, 17 insertions(+), 29 deletions(-) diff --git a/HACKING.txt b/HACKING.txt index ec0bcb000..dd735bf22 100644 --- a/HACKING.txt +++ b/HACKING.txt @@ -48,23 +48,6 @@ checkout. $ cd starter $ ../bin/python setup.py develop -Building the HTML Docs ------------------------ - -- Check out Pyramid from Github. - -- Create a virtualenv or reuse an existing one that you're using to develop - Pyramid. - -- Run ``$yourvenv/bin/python setup.py dev docs``. - -- cd to ``docs`` within the Pyramid checkout and execute ``make clean html - SPHINXBUILD=$yourvenv/bin/sphinx-build``. The ``SPHINXBUILD=...`` hair is - there in order to tell it to use the virtualenv Python, which will have - both Sphinx and Pyramid (for API documentation generation) installed. - -- The rendered HTML docs will end up in ``docs/_build/html``. - Adding Features --------------- @@ -130,23 +113,28 @@ Test Coverage ``nose`` and ``coverage`` into your virtualenv, and running ``setup.py nosetests --with-coverage``. -Documentation Coverage ----------------------- +Documentation Coverage and Building HTML Documentation +------------------------------------------------------ -- If you fix a bug, and the bug requires an API or behavior - modification, all documentation in this package which references - that API or behavior must change to reflect the bug fix, ideally in - the same commit that fixes the bug or adds the feature. +If you fix a bug, and the bug requires an API or behavior modification, all +documentation in this package which references that API or behavior must +change to reflect the bug fix, ideally in the same commit that fixes the bug +or adds the feature. -- To build and review docs: +To build and review docs (where ``$yourvenv`` refers to the virtualenv you're +using to develop Pyramid): - 1. Install ``tests_require`` dependencies from Pyramid's setup.py into your - virtualenv. +1. Run ``$yourvenv/bin/python setup.py dev docs``. This will cause Sphinx + and all development requirements to be installed in your virtualenv. - 2. From the ``docs`` directory of the Pyramid checkout run ``make html - SPHINXBUILD=/path/to/your/virtualenv/bin/sphinx-build``. +2. cd to the ``docs`` directory within your Pyramid checkout and execute + ``make clean html SPHINXBUILD=$yourvenv/bin/sphinx-build``. The + ``SPHINXBUILD=...`` hair is there in order to tell it to use the + virtualenv Python, which will have both Sphinx and Pyramid (for API + documentation generation) installed. - 3. Open the _build/html/index.html file to see the resulting rendering. +3. Open the ``docs/_build/html/index.html`` file to see the resulting HTML + rendering. Change Log ---------- -- cgit v1.2.3 From 0487d5e05dd61d6d7482212d40fb5884e06f582a Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 12 Jun 2012 11:18:37 -0500 Subject: docs reference setup_logging instead of fileConfig --- docs/narr/commandline.rst | 7 +++++-- docs/narr/logging.rst | 7 ++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/narr/commandline.rst b/docs/narr/commandline.rst index 4be436836..af53c1f78 100644 --- a/docs/narr/commandline.rst +++ b/docs/narr/commandline.rst @@ -654,8 +654,11 @@ use the following command: .. code-block:: python - import logging.config - logging.config.fileConfig('/path/to/my/development.ini') + import pyramid.paster + pyramid.paster.setup_logging('/path/to/my/development.ini') + +See :ref:`logging_chapter` for more information on logging within +:app:`Pyramid`. .. index:: single: console script diff --git a/docs/narr/logging.rst b/docs/narr/logging.rst index 044655c1f..f4c38abb6 100644 --- a/docs/narr/logging.rst +++ b/docs/narr/logging.rst @@ -14,7 +14,7 @@ how to send log messages to loggers that you've configured. which help configure logging. All of the scaffolds which ship along with :app:`Pyramid` do this. If you're not using a scaffold, or if you've used a third-party scaffold which does not create these files, the - configuration information in this chapter will not be applicable. + configuration information in this chapter may not be applicable. .. _logging_config: @@ -36,10 +36,11 @@ application-related and logging-related sections in the configuration file can coexist peacefully, and the logging-related sections in the file are used from when you run ``pserve``. -The ``pserve`` command calls the `logging.fileConfig function +The ``pserve`` command calls the :func:`pyramid.paster.setup_logging` +function, a thin wrapper around the `logging.fileConfig `_ using the specified ini file if it contains a ``[loggers]`` section (all of the -scaffold-generated ``.ini`` files do). ``logging.fileConfig`` reads the +scaffold-generated ``.ini`` files do). ``setup_logging`` reads the logging configuration from the ini file upon which ``pserve`` was invoked. -- cgit v1.2.3 From b555e98c477056a7b32a46294c42c11a3b96b432 Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Fri, 1 Jun 2012 07:56:13 -0700 Subject: Add "py33" to tox.ini --- tox.ini | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 97aa6c4d0..85bd41bda 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] envlist = - py26,py27,py32,pypy,cover + py26,py27,py32,py33,pypy,cover [testenv] commands = @@ -21,6 +21,14 @@ deps = virtualenv venusian +[testenv:py33] +commands = + python setup.py test -q +deps = + WebTest + virtualenv + venusian + [testenv:cover] basepython = python2.6 -- cgit v1.2.3 From 366f9d5960c0cb85ef0ab9403e37e19fc85961d0 Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Fri, 1 Jun 2012 07:59:11 -0700 Subject: Add .travis.yml for Travis CI (http://travis-ci.org/) --- .travis.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..cb4cc69e1 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,14 @@ +language: python + +python: + - 2.6 + - 2.7 + - pypy + - 3.2 + +matrix: + allow_failures: + - python: pypy + +script: python setup.py test + -- cgit v1.2.3 From b5478966861d3de23ca299c26891f9709cf09ad7 Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Wed, 13 Jun 2012 08:06:15 -0700 Subject: Add "Marc Abramowitz" to CONTRIBUTORS.txt --- CONTRIBUTORS.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 98f73d5f9..027fc0857 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -172,3 +172,5 @@ Contributors - Wayne Witzel III, 2012/03/27 - Marin Rukavina, 2012/05/03 + +- Marc Abramowitz, 2012/06/13 -- cgit v1.2.3 From e07b4f8edd32766dd0c8327d04a9c7b99d8dc2e9 Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Thu, 14 Jun 2012 17:27:28 -0700 Subject: Remove blank line from .travis.yml --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index cb4cc69e1..2e737af04 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,4 +11,3 @@ matrix: - python: pypy script: python setup.py test - -- cgit v1.2.3 From 2516df30d73afdf6606ad3d89b7c24ab496f356d Mon Sep 17 00:00:00 2001 From: Jeff Cook Date: Sun, 17 Jun 2012 01:20:37 -0600 Subject: docs: Add view callable example to section on rendering_val. --- docs/narr/hooks.rst | 31 +++++++++++++++++++++++-------- pyramid/events.py | 18 +++++++++++++++--- 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index 30eec04f0..332805152 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -291,18 +291,33 @@ factories must be unique. The dictionary returned from the view is accessible through the :attr:`rendering_val` attribute of a :class:`~pyramid.events.BeforeRender` -event, like so: +event. + +Suppose you return ``{'mykey': 'somevalue', 'mykey2': 'somevalue2'}`` from +your view callable, like so: .. code-block:: python - :linenos: + :linenos: - from pyramid.events import subscriber - from pyramid.events import BeforeRender + from pyramid.view import view_config - @subscriber(BeforeRender) - def read_return(event): - # 'mykey' is returned from the view - print(event.rendering_val['mykey']) + @view_config(renderer='some_renderer') + def myview(request): + return {'mykey': 'somevalue', 'mykey2': 'somevalue2'} + +:attr:`rendering_val` can be used to access these values from the +:class:`~pyramid.events.BeforeRender` object: + +.. code-block:: python + :linenos: + + from pyramid.events import subscriber + from pyramid.events import BeforeRender + + @subscriber(BeforeRender) + def read_return(event): + # {'mykey': 'somevalue'} is returned from the view + print(event.rendering_val['mykey']) See the API documentation for the :class:`~pyramid.events.BeforeRender` event interface at :class:`pyramid.interfaces.IBeforeRender`. diff --git a/pyramid/events.py b/pyramid/events.py index 1941c594c..db274823c 100644 --- a/pyramid/events.py +++ b/pyramid/events.py @@ -202,17 +202,29 @@ class BeforeRender(dict): The dictionary returned from the view is accessible through the :attr:`rendering_val` attribute of a :class:`~pyramid.events.BeforeRender` - event, like so:: + event. + + Suppose you return ``{'mykey': 'somevalue', 'mykey2': 'somevalue2'}`` from + your view callable, like so:: + + from pyramid.view import view_config + + @view_config(renderer='some_renderer') + def myview(request): + return {'mykey': 'somevalue', 'mykey2': 'somevalue2'} + + :attr:`rendering_val` can be used to access these values from the + :class:`~pyramid.events.BeforeRender` object:: from pyramid.events import subscriber from pyramid.events import BeforeRender @subscriber(BeforeRender) def read_return(event): - # "mykey" is returned from the view + # {'mykey': 'somevalue'} is returned from the view print(event.rendering_val['mykey']) - In other words, ``rendering_val`` is the (non-system) value returned by a + In other words, :attr:`rendering_val` is the (non-system) value returned by a view or passed to ``render*`` as ``value``. This feature is new in Pyramid 1.2. -- cgit v1.2.3 From 4761ec79e1f3e0daeb4ba8351c27eb2a715f07a4 Mon Sep 17 00:00:00 2001 From: Jeff Cook Date: Sun, 17 Jun 2012 01:24:20 -0600 Subject: CONTRIBUTORS: Add self / accept contribution terms --- CONTRIBUTORS.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 98f73d5f9..2baeea3dc 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -172,3 +172,5 @@ Contributors - Wayne Witzel III, 2012/03/27 - Marin Rukavina, 2012/05/03 + +- Jeff Cook, 2012/06/16 -- cgit v1.2.3 From 5b1f04fb91b2da701c9ea913883874eda5c3dafb Mon Sep 17 00:00:00 2001 From: Blaise Laflamme Date: Tue, 19 Jun 2012 14:42:56 -0400 Subject: added namespace test --- pyramid/tests/fixtures/components.mak | 3 +++ pyramid/tests/fixtures/hellocompo.mak | 3 +++ pyramid/tests/test_mako_templating.py | 5 +++++ 3 files changed, 11 insertions(+) create mode 100644 pyramid/tests/fixtures/components.mak create mode 100644 pyramid/tests/fixtures/hellocompo.mak diff --git a/pyramid/tests/fixtures/components.mak b/pyramid/tests/fixtures/components.mak new file mode 100644 index 000000000..cc886805c --- /dev/null +++ b/pyramid/tests/fixtures/components.mak @@ -0,0 +1,3 @@ +<%def name="comp()"> +World! + \ No newline at end of file diff --git a/pyramid/tests/fixtures/hellocompo.mak b/pyramid/tests/fixtures/hellocompo.mak new file mode 100644 index 000000000..142676a11 --- /dev/null +++ b/pyramid/tests/fixtures/hellocompo.mak @@ -0,0 +1,3 @@ +<%namespace name="comp" file="pyramid.tests:fixtures/components.mak"/> +Namespace +Hello ${comp.comp()} \ No newline at end of file diff --git a/pyramid/tests/test_mako_templating.py b/pyramid/tests/test_mako_templating.py index fbb04273b..8b738c21d 100644 --- a/pyramid/tests/test_mako_templating.py +++ b/pyramid/tests/test_mako_templating.py @@ -402,6 +402,11 @@ class TestIntegration(unittest.TestCase): result = render('hello_inherit_pkg.mak', {}).replace('\r','') self.assertEqual(result, text_('Layout\nHello World!\n')) + def test_render_namespace(self): + from pyramid.renderers import render + result = render('hellocompo.mak', {}).replace('\r','') + self.assertEqual(result, text_('\nNamespace\nHello \nWorld!\n')) + def test_render_to_response(self): from pyramid.renderers import render_to_response result = render_to_response('helloworld.mak', {'a':1}) -- cgit v1.2.3 From f71ed59edb74e9a13362521918e2660e4e4263ba Mon Sep 17 00:00:00 2001 From: Blaise Laflamme Date: Tue, 19 Jun 2012 16:32:41 -0400 Subject: added a new makodef renderer to call a def inside a mako template, fixed tests and removed old tuple way of calling def. Based on zzzeek example of client/server templating. --- pyramid/config/rendering.py | 1 + pyramid/mako_templating.py | 19 +++++++++++-------- pyramid/tests/test_mako_templating.py | 21 ++++++++++----------- 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/pyramid/config/rendering.py b/pyramid/config/rendering.py index 926511b7b..bfa41ee03 100644 --- a/pyramid/config/rendering.py +++ b/pyramid/config/rendering.py @@ -21,6 +21,7 @@ DEFAULT_RENDERERS = ( ('.pt', chameleon_zpt.renderer_factory), ('.mak', mako_renderer_factory), ('.mako', mako_renderer_factory), + ('.makodef', mako_renderer_factory), ('json', renderers.json_renderer_factory), ('string', renderers.string_renderer_factory), ) diff --git a/pyramid/mako_templating.py b/pyramid/mako_templating.py index 208e54bf5..48288c930 100644 --- a/pyramid/mako_templating.py +++ b/pyramid/mako_templating.py @@ -1,4 +1,5 @@ import os +import re import sys import threading @@ -76,7 +77,11 @@ class MakoRendererFactoryHelper(object): self.settings_prefix = settings_prefix def __call__(self, info): - path = info.name + p = re.compile( + r'(?P[\w_.:/]+)' + r'(?:\#(?P[\w_]+))?' + ) + path, defname = p.match(info.name).group("path", "defname") registry = info.registry settings = info.settings settings_prefix = self.settings_prefix @@ -141,7 +146,7 @@ class MakoRendererFactoryHelper(object): finally: registry_lock.release() - return MakoLookupTemplateRenderer(path, lookup) + return MakoLookupTemplateRenderer(path, defname, lookup) renderer_factory = MakoRendererFactoryHelper('mako.') @@ -156,8 +161,9 @@ class MakoRenderingException(Exception): @implementer(ITemplateRenderer) class MakoLookupTemplateRenderer(object): - def __init__(self, path, lookup): + def __init__(self, path, defname, lookup): self.path = path + self.defname = defname self.lookup = lookup def implementation(self): @@ -167,16 +173,13 @@ class MakoLookupTemplateRenderer(object): context = system.pop('context', None) if context is not None: system['_context'] = context - def_name = None - if isinstance(value, tuple): - def_name, value = value try: system.update(value) except (TypeError, ValueError): raise ValueError('renderer was passed non-dictionary as value') template = self.implementation() - if def_name is not None: - template = template.get_def(def_name) + if self.defname is not None: + template = template.get_def(self.defname) try: result = template.render_unicode(**system) except: diff --git a/pyramid/tests/test_mako_templating.py b/pyramid/tests/test_mako_templating.py index 8b738c21d..cd7b140d6 100644 --- a/pyramid/tests/test_mako_templating.py +++ b/pyramid/tests/test_mako_templating.py @@ -315,7 +315,7 @@ class MakoLookupTemplateRendererTests(Base, unittest.TestCase): def test_instance_implements_ITemplate(self): from zope.interface.verify import verifyObject from pyramid.interfaces import ITemplateRenderer - verifyObject(ITemplateRenderer, self._makeOne(None, None)) + verifyObject(ITemplateRenderer, self._makeOne(None, None, None)) def test_class_implements_ITemplate(self): from zope.interface.verify import verifyClass @@ -324,7 +324,7 @@ class MakoLookupTemplateRendererTests(Base, unittest.TestCase): def test_call(self): lookup = DummyLookup() - instance = self._makeOne('path', lookup) + instance = self._makeOne('path', None, lookup) result = instance({}, {'system':1}) self.assertTrue(isinstance(result, text_type)) self.assertEqual(result, text_('result')) @@ -332,29 +332,28 @@ class MakoLookupTemplateRendererTests(Base, unittest.TestCase): def test_call_with_system_context(self): # lame lookup = DummyLookup() - instance = self._makeOne('path', lookup) + instance = self._makeOne('path', None, lookup) result = instance({}, {'context':1}) self.assertTrue(isinstance(result, text_type)) self.assertEqual(result, text_('result')) self.assertEqual(lookup.values, {'_context':1}) - def test_call_with_tuple_value(self): + def test_call_with_defname(self): lookup = DummyLookup() - instance = self._makeOne('path', lookup) - result = instance(('fub', {}), {'context':1}) - self.assertEqual(lookup.deffed, 'fub') + instance = self._makeOne('path', 'defname', lookup) + result = instance({}, {'system':1}) + self.assertTrue(isinstance(result, text_type)) self.assertEqual(result, text_('result')) - self.assertEqual(lookup.values, {'_context':1}) def test_call_with_nondict_value(self): lookup = DummyLookup() - instance = self._makeOne('path', lookup) + instance = self._makeOne('path', None, lookup) self.assertRaises(ValueError, instance, None, {}) def test_call_render_raises(self): from pyramid.mako_templating import MakoRenderingException lookup = DummyLookup(exc=NotImplementedError) - instance = self._makeOne('path', lookup) + instance = self._makeOne('path', None, lookup) try: instance({}, {}) except MakoRenderingException as e: @@ -364,7 +363,7 @@ class MakoLookupTemplateRendererTests(Base, unittest.TestCase): def test_implementation(self): lookup = DummyLookup() - instance = self._makeOne('path', lookup) + instance = self._makeOne('path', None, lookup) result = instance.implementation().render_unicode() self.assertTrue(isinstance(result, text_type)) self.assertEqual(result, text_('result')) -- cgit v1.2.3 From c358304043e7e68c7fc97dff42f88633b8f15c69 Mon Sep 17 00:00:00 2001 From: Blaise Laflamme Date: Tue, 19 Jun 2012 18:49:53 -0400 Subject: added removed tuple for bw compat --- pyramid/mako_templating.py | 3 +++ pyramid/tests/test_mako_templating.py | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/pyramid/mako_templating.py b/pyramid/mako_templating.py index 48288c930..f866e2630 100644 --- a/pyramid/mako_templating.py +++ b/pyramid/mako_templating.py @@ -173,6 +173,9 @@ class MakoLookupTemplateRenderer(object): context = system.pop('context', None) if context is not None: system['_context'] = context + if self.defname is None: + if isinstance(value, tuple): + self.defname, value = value try: system.update(value) except (TypeError, ValueError): diff --git a/pyramid/tests/test_mako_templating.py b/pyramid/tests/test_mako_templating.py index cd7b140d6..6cfa3ea4b 100644 --- a/pyramid/tests/test_mako_templating.py +++ b/pyramid/tests/test_mako_templating.py @@ -338,6 +338,14 @@ class MakoLookupTemplateRendererTests(Base, unittest.TestCase): self.assertEqual(result, text_('result')) self.assertEqual(lookup.values, {'_context':1}) + def test_call_with_tuple_value(self): + lookup = DummyLookup() + instance = self._makeOne('path', None, lookup) + result = instance(('fub', {}), {'context':1}) + self.assertEqual(lookup.deffed, 'fub') + self.assertEqual(result, text_('result')) + self.assertEqual(lookup.values, {'_context':1}) + def test_call_with_defname(self): lookup = DummyLookup() instance = self._makeOne('path', 'defname', lookup) -- cgit v1.2.3 From 59f8017b4a4aa9767bab49b39db362e973bdacf1 Mon Sep 17 00:00:00 2001 From: Blaise Laflamme Date: Tue, 19 Jun 2012 19:28:55 -0400 Subject: removed .makodef, use tuple value if defname in renderer (bw compat), changed format for package:some/template#defname.mako --- pyramid/config/rendering.py | 1 - pyramid/mako_templating.py | 7 ++++++- pyramid/tests/test_mako_templating.py | 8 ++++++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/pyramid/config/rendering.py b/pyramid/config/rendering.py index bfa41ee03..926511b7b 100644 --- a/pyramid/config/rendering.py +++ b/pyramid/config/rendering.py @@ -21,7 +21,6 @@ DEFAULT_RENDERERS = ( ('.pt', chameleon_zpt.renderer_factory), ('.mak', mako_renderer_factory), ('.mako', mako_renderer_factory), - ('.makodef', mako_renderer_factory), ('json', renderers.json_renderer_factory), ('string', renderers.string_renderer_factory), ) diff --git a/pyramid/mako_templating.py b/pyramid/mako_templating.py index f866e2630..8bd9381f0 100644 --- a/pyramid/mako_templating.py +++ b/pyramid/mako_templating.py @@ -80,8 +80,10 @@ class MakoRendererFactoryHelper(object): p = re.compile( r'(?P[\w_.:/]+)' r'(?:\#(?P[\w_]+))?' + r'(\.(?P.*))' ) - path, defname = p.match(info.name).group("path", "defname") + asset, defname, ext = p.match(info.name).group('path', 'defname', 'ext') + path = '%s.%s' % (asset, ext) registry = info.registry settings = info.settings settings_prefix = self.settings_prefix @@ -176,6 +178,9 @@ class MakoLookupTemplateRenderer(object): if self.defname is None: if isinstance(value, tuple): self.defname, value = value + else: + if isinstance(value, tuple): + _, value = value try: system.update(value) except (TypeError, ValueError): diff --git a/pyramid/tests/test_mako_templating.py b/pyramid/tests/test_mako_templating.py index 6cfa3ea4b..41fa9bdc4 100644 --- a/pyramid/tests/test_mako_templating.py +++ b/pyramid/tests/test_mako_templating.py @@ -353,6 +353,14 @@ class MakoLookupTemplateRendererTests(Base, unittest.TestCase): self.assertTrue(isinstance(result, text_type)) self.assertEqual(result, text_('result')) + def test_call_with_defname_with_tuple_value(self): + lookup = DummyLookup() + instance = self._makeOne('path', 'defname', lookup) + result = instance(('defname', {}), {'context':1}) + self.assertEqual(lookup.deffed, 'defname') + self.assertEqual(result, text_('result')) + self.assertEqual(lookup.values, {'_context':1}) + def test_call_with_nondict_value(self): lookup = DummyLookup() instance = self._makeOne('path', None, lookup) -- cgit v1.2.3 From b015d702d4c5367cd24fa05bd8d83462b6d59ac1 Mon Sep 17 00:00:00 2001 From: Blaise Laflamme Date: Tue, 19 Jun 2012 19:30:01 -0400 Subject: renamed path for asset in regex --- pyramid/mako_templating.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyramid/mako_templating.py b/pyramid/mako_templating.py index 8bd9381f0..b02daa23c 100644 --- a/pyramid/mako_templating.py +++ b/pyramid/mako_templating.py @@ -78,11 +78,11 @@ class MakoRendererFactoryHelper(object): def __call__(self, info): p = re.compile( - r'(?P[\w_.:/]+)' + r'(?P[\w_.:/]+)' r'(?:\#(?P[\w_]+))?' r'(\.(?P.*))' ) - asset, defname, ext = p.match(info.name).group('path', 'defname', 'ext') + asset, defname, ext = p.match(info.name).group('asset', 'defname', 'ext') path = '%s.%s' % (asset, ext) registry = info.registry settings = info.settings -- cgit v1.2.3 From ea009a6d4a1ffa8585faa85581848f6e74a57dfc Mon Sep 17 00:00:00 2001 From: Blaise Laflamme Date: Tue, 19 Jun 2012 20:12:55 -0400 Subject: added docs and changes for using defs in mako renderer --- CHANGES.txt | 5 +++++ docs/narr/templates.rst | 16 ++++++++++++++++ pyramid/mako_templating.py | 7 +++++++ 3 files changed, 28 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 7c2af4451..3cb2f2848 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -41,3 +41,8 @@ Features - The static view machinery now raises (rather than returns) ``HTTPNotFound`` and ``HTTPMovedPermanently`` exceptions, so these can be caught by the NotFound view (and other exception views). + +- The mako renderer now accepts a def name and returns the template def + result for the view being called. The uri format using an asset spec is + package:path/to/template#defname.mako. The old way of returning a tuple + from the view is supported for backward compatibility, ('defname', {}). diff --git a/docs/narr/templates.rst b/docs/narr/templates.rst index 9db0b1c4d..4ac01c96e 100644 --- a/docs/narr/templates.rst +++ b/docs/narr/templates.rst @@ -714,6 +714,22 @@ This template doesn't use any advanced features of Mako, only the :term:`renderer globals`. See the `the Mako documentation `_ to use more advanced features. +Using def inside Mako Templates +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To use a def inside a Mako template, given a :term:`Mako` template file named +``foo.mak`` and a def named ``bar``, you can configure the template as a +:term:`renderer` like so: + +.. code-block:: python + :linenos: + + from pyramid.view import view_config + + @view_config(renderer='foo#defname.mak') + def my_view(request): + return {'project':'my project'} + .. index:: single: automatic reloading of templates single: template automatic reload diff --git a/pyramid/mako_templating.py b/pyramid/mako_templating.py index b02daa23c..bb4ccb2f0 100644 --- a/pyramid/mako_templating.py +++ b/pyramid/mako_templating.py @@ -163,6 +163,13 @@ class MakoRenderingException(Exception): @implementer(ITemplateRenderer) class MakoLookupTemplateRenderer(object): + """ Render a :term:`Mako` template using the template + implied by the ``path`` argument.The ``path`` argument may be a + package-relative path, an absolute path, or a :term:`asset + specification`. If a defname is defined, in the form of + package:path/to/template#defname.mako, a function named ``defname`` + inside the template will then be rendered. + """ def __init__(self, path, defname, lookup): self.path = path self.defname = defname -- cgit v1.2.3 From 6cea47e9c34841cdf109899e8d965c67af3a5ce9 Mon Sep 17 00:00:00 2001 From: Blaise Laflamme Date: Tue, 19 Jun 2012 20:20:17 -0400 Subject: fixed typos --- CHANGES.txt | 2 +- docs/narr/templates.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 3cb2f2848..1fd92fe19 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -42,7 +42,7 @@ Features and ``HTTPMovedPermanently`` exceptions, so these can be caught by the NotFound view (and other exception views). -- The mako renderer now accepts a def name and returns the template def +- The mako renderer now accepts a defname and returns the template def result for the view being called. The uri format using an asset spec is package:path/to/template#defname.mako. The old way of returning a tuple from the view is supported for backward compatibility, ('defname', {}). diff --git a/docs/narr/templates.rst b/docs/narr/templates.rst index 4ac01c96e..5656026ae 100644 --- a/docs/narr/templates.rst +++ b/docs/narr/templates.rst @@ -718,7 +718,7 @@ Using def inside Mako Templates ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To use a def inside a Mako template, given a :term:`Mako` template file named -``foo.mak`` and a def named ``bar``, you can configure the template as a +``foo.mak`` and a defname ``bar``, you can configure the template as a :term:`renderer` like so: .. code-block:: python -- cgit v1.2.3 From c2d65ff71dac6a9b15119db8c2fb09884f4060e3 Mon Sep 17 00:00:00 2001 From: Blaise Laflamme Date: Tue, 19 Jun 2012 20:22:34 -0400 Subject: fixed typos --- CHANGES.txt | 2 +- docs/narr/templates.rst | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 1fd92fe19..3cb2f2848 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -42,7 +42,7 @@ Features and ``HTTPMovedPermanently`` exceptions, so these can be caught by the NotFound view (and other exception views). -- The mako renderer now accepts a defname and returns the template def +- The mako renderer now accepts a def name and returns the template def result for the view being called. The uri format using an asset spec is package:path/to/template#defname.mako. The old way of returning a tuple from the view is supported for backward compatibility, ('defname', {}). diff --git a/docs/narr/templates.rst b/docs/narr/templates.rst index 5656026ae..860010a1a 100644 --- a/docs/narr/templates.rst +++ b/docs/narr/templates.rst @@ -718,7 +718,7 @@ Using def inside Mako Templates ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To use a def inside a Mako template, given a :term:`Mako` template file named -``foo.mak`` and a defname ``bar``, you can configure the template as a +``foo.mak`` and a def named ``bar``, you can configure the template as a :term:`renderer` like so: .. code-block:: python @@ -726,7 +726,7 @@ To use a def inside a Mako template, given a :term:`Mako` template file named from pyramid.view import view_config - @view_config(renderer='foo#defname.mak') + @view_config(renderer='foo#bar.mak') def my_view(request): return {'project':'my project'} -- cgit v1.2.3 From fe9316332511de945924effd8a049db79f34e761 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 19 Jun 2012 21:42:44 -0400 Subject: point at pyramid_beaker docs rather than its github page --- docs/narr/sessions.rst | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/docs/narr/sessions.rst b/docs/narr/sessions.rst index 6ff9e3dea..1aa1b6341 100644 --- a/docs/narr/sessions.rst +++ b/docs/narr/sessions.rst @@ -151,13 +151,12 @@ Using Alternate Session Factories --------------------------------- At the time of this writing, exactly one alternate session factory -implementation exists, named ``pyramid_beaker``. This is a session -factory that uses the `Beaker `_ library -as a backend. Beaker has support for file-based sessions, database -based sessions, and encrypted cookie-based sessions. See -`http://github.com/Pylons/pyramid_beaker -`_ for more information about -``pyramid_beaker``. +implementation exists, named ``pyramid_beaker``. This is a session factory +that uses the `Beaker `_ library as a backend. +Beaker has support for file-based sessions, database based sessions, and +encrypted cookie-based sessions. See `the pyramid_beaker documentation +`_ for more +information about ``pyramid_beaker``. .. index:: single: session factory (custom) -- cgit v1.2.3 From 5f8493cbfd9dac153f7442f1cffc117f85280716 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Sun, 24 Jun 2012 10:25:40 -0400 Subject: Try newly-enabled travis-ci. See http://travis-ci.org/#\!/Pylons/pyramid --- .travis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2e737af04..665d15384 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,8 +6,8 @@ python: - pypy - 3.2 -matrix: - allow_failures: - - python: pypy +#matrix: +# allow_failures: +# - python: pypy script: python setup.py test -- cgit v1.2.3 From a0e59d530726ab3f67b1c389cf8baecc13309ed6 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Sun, 24 Jun 2012 10:35:49 -0400 Subject: Un-comment the PyPy 'allow_failures' bit. We need an explanation for why the PyPy tests fail under travis-ci. --- .travis.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 665d15384..c637dc215 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,8 +6,10 @@ python: - pypy - 3.2 -#matrix: -# allow_failures: -# - python: pypy +# Why does travis-ci's PyPy blow up? Pyramid's tests +# run fine under tox. +matrix: + allow_failures: + - python: pypy script: python setup.py test -- cgit v1.2.3 From 71b473705f73b12a35422d3aa257906e4a99d853 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Sun, 24 Jun 2012 10:52:16 -0400 Subject: Send notifications to pyramid-checkins. Note that we rely on the default settings (failures always send, success sends only on change.). --- .travis.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.travis.yml b/.travis.yml index c637dc215..490fd2df7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,3 +13,7 @@ matrix: - python: pypy script: python setup.py test + +notifications: + email: + - pyramid-checkins@lists.repoze.org -- cgit v1.2.3 From d4147eb8dc4962fa86863c77fc190717113994a7 Mon Sep 17 00:00:00 2001 From: Blaise Laflamme Date: Fri, 29 Jun 2012 16:46:03 -0400 Subject: fixed mako bug #606 for inheritance and includes --- pyramid/mako_templating.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/pyramid/mako_templating.py b/pyramid/mako_templating.py index bb4ccb2f0..9aeaa9153 100644 --- a/pyramid/mako_templating.py +++ b/pyramid/mako_templating.py @@ -42,12 +42,14 @@ class PkgResourceTemplateLookup(TemplateLookup): def get_template(self, uri): """Fetch a template from the cache, or check the filesystem for it - + In addition to the basic filesystem lookup, this subclass will use pkg_resource to load a file using the asset specification syntax. - + """ + if '$' in uri: + uri = uri.replace('$', ':') isabs = os.path.isabs(uri) if (not isabs) and (':' in uri): # Windows can't cope with colons in filenames, so we replace the @@ -70,7 +72,7 @@ class PkgResourceTemplateLookup(TemplateLookup): return TemplateLookup.get_template(self, uri) -registry_lock = threading.Lock() +registry_lock = threading.Lock() class MakoRendererFactoryHelper(object): def __init__(self, settings_prefix=None): @@ -143,7 +145,7 @@ class MakoRendererFactoryHelper(object): registry_lock.acquire() try: - registry.registerUtility(lookup, IMakoLookup, + registry.registerUtility(lookup, IMakoLookup, name=settings_prefix) finally: registry_lock.release() @@ -166,15 +168,15 @@ class MakoLookupTemplateRenderer(object): """ Render a :term:`Mako` template using the template implied by the ``path`` argument.The ``path`` argument may be a package-relative path, an absolute path, or a :term:`asset - specification`. If a defname is defined, in the form of - package:path/to/template#defname.mako, a function named ``defname`` + specification`. If a defname is defined, in the form of + package:path/to/template#defname.mako, a function named ``defname`` inside the template will then be rendered. """ def __init__(self, path, defname, lookup): self.path = path self.defname = defname self.lookup = lookup - + def implementation(self): return self.lookup.get_template(self.path) -- cgit v1.2.3 From 6ef753257617acf8aaf3daaca22de3783970cffe Mon Sep 17 00:00:00 2001 From: Jeff Cook Date: Wed, 4 Jul 2012 18:52:54 -0600 Subject: Include instructions for recursive submodule checkout of Sphinx themes. --- HACKING.txt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/HACKING.txt b/HACKING.txt index dd735bf22..38c263ed7 100644 --- a/HACKING.txt +++ b/HACKING.txt @@ -127,13 +127,19 @@ using to develop Pyramid): 1. Run ``$yourvenv/bin/python setup.py dev docs``. This will cause Sphinx and all development requirements to be installed in your virtualenv. -2. cd to the ``docs`` directory within your Pyramid checkout and execute +2. Update all git submodules from the top-level of your Pyramid checkout, like + so: + git submodule update --init --recursive + This will checkout theme subrepositories and prevent error conditions when + HTML docs are generated. + +3. cd to the ``docs`` directory within your Pyramid checkout and execute ``make clean html SPHINXBUILD=$yourvenv/bin/sphinx-build``. The ``SPHINXBUILD=...`` hair is there in order to tell it to use the virtualenv Python, which will have both Sphinx and Pyramid (for API documentation generation) installed. -3. Open the ``docs/_build/html/index.html`` file to see the resulting HTML +4. Open the ``docs/_build/html/index.html`` file to see the resulting HTML rendering. Change Log -- cgit v1.2.3 From 88bbd46c9a5fab52548f26fe4655a89c00332ad5 Mon Sep 17 00:00:00 2001 From: Maxim Avanov Date: Tue, 10 Jul 2012 21:26:34 +0400 Subject: Replacement markers of url dispatcher can contain regex with colons. --- pyramid/tests/test_urldispatch.py | 8 ++++++++ pyramid/urldispatch.py | 4 +++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/pyramid/tests/test_urldispatch.py b/pyramid/tests/test_urldispatch.py index e15242f75..b2164717e 100644 --- a/pyramid/tests/test_urldispatch.py +++ b/pyramid/tests/test_urldispatch.py @@ -311,6 +311,14 @@ class TestCompileRoute(unittest.TestCase): self.assertEqual(matcher('foo/baz/biz/buz/bar'), None) self.assertEqual(generator({'baz':1, 'buz':2, 'bar': 'html'}), '/foo/1/biz/2.html') + + def test_custom_regex_with_colons(self): + matcher, generator = self._callFUT('foo/{baz}/biz/{buz:(?:[^/\.]+)}.{bar}') + self.assertEqual(matcher('/foo/baz/biz/buz.bar'), + {'baz':'baz', 'buz':'buz', 'bar':'bar'}) + self.assertEqual(matcher('foo/baz/biz/buz/bar'), None) + self.assertEqual(generator({'baz':1, 'buz':2, 'bar': 'html'}), + '/foo/1/biz/2.html') def test_mixed_newstyle_oldstyle_pattern_defaults_to_newstyle(self): # pattern: '\\/foo\\/(?Pabc)\\/biz\\/(?P[^/]+)\\/bar$' diff --git a/pyramid/urldispatch.py b/pyramid/urldispatch.py index cccff14ba..4182ea665 100644 --- a/pyramid/urldispatch.py +++ b/pyramid/urldispatch.py @@ -148,7 +148,9 @@ def _compile_route(route): name = pat.pop() # unicode name = name[1:-1] if ':' in name: - name, reg = name.split(':') + # reg may contain colons as well, + # so we must strictly split name into two parts + name, reg = name.split(':', 1) else: reg = '[^/]+' gen.append('%%(%s)s' % native_(name)) # native -- cgit v1.2.3 From 3074e78a5b3818bf6c6cb43b832e4ab1df845e16 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 10 Jul 2012 15:56:38 -0400 Subject: garden --- CHANGES.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 3cb2f2848..c06b8106d 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -9,6 +9,9 @@ Bug Fixes return the empty list. This was incorrect, it should have unconditionally returned ``[Everyone]``, and now does. +- Explicit url dispatch regexes can now contain colons. + https://github.com/Pylons/pyramid/issues/629 + Features -------- -- cgit v1.2.3 From e652518e00cde05b55b065196a460efd9bc86e32 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 13 Jul 2012 12:08:18 -0400 Subject: - On at least one 64-bit Ubuntu system under Python 3.2, using the ``view_config`` decorator caused a ``RuntimeError: dictionary changed size during iteration`` exception. It no longer does. See https://github.com/Pylons/pyramid/issues/635 for more information. - Fixes issue #635. --- CHANGES.txt | 5 +++++ pyramid/view.py | 6 +++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index c06b8106d..c6afaf0c7 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -12,6 +12,11 @@ Bug Fixes - Explicit url dispatch regexes can now contain colons. https://github.com/Pylons/pyramid/issues/629 +- On at least one 64-bit Ubuntu system under Python 3.2, using the + ``view_config`` decorator caused a ``RuntimeError: dictionary changed size + during iteration`` exception. It no longer does. See + https://github.com/Pylons/pyramid/issues/635 for more information. + Features -------- diff --git a/pyramid/view.py b/pyramid/view.py index bb531c326..1df0849c0 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -199,7 +199,7 @@ class view_config(object): custom_predicates=default, context=default, decorator=default, mapper=default, http_cache=default, match_param=default): - L = locals() + L = dict(locals()) # See issue #635 for dict() rationale if (context is not default) or (for_ is not default): L['context'] = context or for_ for k, v in L.items(): @@ -367,7 +367,7 @@ class notfound_view_config(object): path_info=default, custom_predicates=default, decorator=default, mapper=default, match_param=default, append_slash=False): - L = locals() + L = dict(locals()) # See issue #635 for dict() rationale for k, v in L.items(): if k not in ('self', 'L') and v is not default: self.__dict__[k] = v @@ -432,7 +432,7 @@ class forbidden_view_config(object): xhr=default, accept=default, header=default, path_info=default, custom_predicates=default, decorator=default, mapper=default, match_param=default): - L = locals() + L = dict(locals()) # See issue #635 for dict() rationale for k, v in L.items(): if k not in ('self', 'L') and v is not default: self.__dict__[k] = v -- cgit v1.2.3 From 70ba5b97f53ac178dac8f81109596fb47bbadde5 Mon Sep 17 00:00:00 2001 From: Siddhartha Kasivajhula Date: Sat, 21 Jul 2012 14:29:00 -0700 Subject: corrected table name tables->pages --- docs/tutorials/wiki2/design.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorials/wiki2/design.rst b/docs/tutorials/wiki2/design.rst index 2e6fc0e77..deaf32ef6 100644 --- a/docs/tutorials/wiki2/design.rst +++ b/docs/tutorials/wiki2/design.rst @@ -20,7 +20,7 @@ Models We'll be using a SQLite database to hold our wiki data, and we'll be using :term:`SQLAlchemy` to access the data in this database. -Within the database, we define a single table named `tables`, whose elements +Within the database, we define a single table named `pages`, whose elements will store the wiki pages. There are two columns: `name` and `data`. URLs like ``/PageName`` will try to find an element in -- cgit v1.2.3 From 14f9fe44ec75c055d89374a7852e1ca2af0ff31c Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Wed, 25 Jul 2012 14:02:10 -0500 Subject: add logging call to wsgi setup --- docs/tutorials/modwsgi/index.rst | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/tutorials/modwsgi/index.rst b/docs/tutorials/modwsgi/index.rst index e070f8eda..d11167344 100644 --- a/docs/tutorials/modwsgi/index.rst +++ b/docs/tutorials/modwsgi/index.rst @@ -73,9 +73,10 @@ commands and files. .. code-block:: python - from pyramid.paster import get_app - application = get_app( - '/Users/chrism/modwsgi/env/myapp/production.ini', 'main') + from pyramid.paster import get_app, setup_logging + ini_path = '/Users/chrism/modwsgi/env/myapp/production.ini' + setup_logging(ini_path) + application = get_app(ini_path, 'main') The first argument to ``get_app`` is the project configuration file name. It's best to use the ``production.ini`` file provided by your @@ -85,6 +86,10 @@ commands and files. ``application`` is important: mod_wsgi requires finding such an assignment when it opens the file. + The call to ``setup_logging`` initializes the standard library's + `logging` module to allow logging within your application. + See :ref:`logging_config`. + #. Make the ``pyramid.wsgi`` script executable. .. code-block:: text -- cgit v1.2.3 From 440e2e7d94648973a3f9b5aa6136792d02ae1e9e Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 29 Jul 2012 13:18:44 -0400 Subject: give traverse_predicate a __text__ and add a developer note about __text__ --- pyramid/config/util.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pyramid/config/util.py b/pyramid/config/util.py index b8d0f2319..4e4c93be3 100644 --- a/pyramid/config/util.py +++ b/pyramid/config/util.py @@ -113,6 +113,12 @@ def make_predicates(xhr=None, request_method=None, path_info=None, # any predicates get an order of MAX_ORDER, meaning that they will # be tried very last. + # NB: each predicate callable constructed by this function (or examined + # by this function, in the case of custom predicates) must leave this + # function with a ``__text__`` attribute. The subsystem which reports + # errors when no predicates match depends upon the existence of this + # attribute on each predicate callable. + predicates = [] weights = [] h = md5() @@ -273,6 +279,7 @@ def make_predicates(xhr=None, request_method=None, path_info=None, tvalue = tgenerate(m) # tvalue will be urlquoted string m['traverse'] = traversal_path(tvalue) # will be seq of unicode return True + traverse_predicate.__text__ = 'traverse matchdict pseudo-predicate' # This isn't actually a predicate, it's just a infodict # modifier that injects ``traverse`` into the matchdict. As a # result, the ``traverse_predicate`` function above always -- cgit v1.2.3 From 61a57eaaa82c3e001ee3b0102e7a1b6cdb42532d Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 29 Jul 2012 14:54:34 -0400 Subject: - When there is a predicate mismatch exception (seen when no view matches for a given request due to predicates not working), the exception now contains a textual description of the predicate which didn't match. - Fixes issue #502 and issue #519. --- CHANGES.txt | 4 ++++ pyramid/tests/test_config/test_views.py | 31 +++++++++++++++++++++++++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index c6afaf0c7..ecb2bf659 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -54,3 +54,7 @@ Features result for the view being called. The uri format using an asset spec is package:path/to/template#defname.mako. The old way of returning a tuple from the view is supported for backward compatibility, ('defname', {}). + +- When there is a predicate mismatch exception (seen when no view matches for + a given request due to predicates not working), the exception now contains + a textual description of the predicate which didn't match. diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index 9b46f83c9..ebf1dfb39 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -2905,6 +2905,7 @@ class TestViewDeriver(unittest.TestCase): view = lambda *arg: response def predicate1(context, request): return False + predicate1.__text__ = 'text' deriver = self._makeOne(predicates=[predicate1]) result = deriver(view) request = self._makeRequest() @@ -2912,7 +2913,8 @@ class TestViewDeriver(unittest.TestCase): try: result(None, None) except PredicateMismatch as e: - self.assertEqual(e.detail, 'predicate mismatch for view ') + self.assertEqual(e.detail, + 'predicate mismatch for view (text)') else: # pragma: no cover raise AssertionError @@ -2921,6 +2923,7 @@ class TestViewDeriver(unittest.TestCase): def myview(request): pass def predicate1(context, request): return False + predicate1.__text__ = 'text' deriver = self._makeOne(predicates=[predicate1]) result = deriver(myview) request = self._makeRequest() @@ -2928,7 +2931,29 @@ class TestViewDeriver(unittest.TestCase): try: result(None, None) except PredicateMismatch as e: - self.assertEqual(e.detail, 'predicate mismatch for view myview') + self.assertEqual(e.detail, + 'predicate mismatch for view myview (text)') + else: # pragma: no cover + raise AssertionError + + def test_predicate_mismatch_exception_has_text_in_detail(self): + from pyramid.exceptions import PredicateMismatch + def myview(request): pass + def predicate1(context, request): + return True + predicate1.__text__ = 'pred1' + def predicate2(context, request): + return False + predicate2.__text__ = 'pred2' + deriver = self._makeOne(predicates=[predicate1, predicate2]) + result = deriver(myview) + request = self._makeRequest() + request.method = 'POST' + try: + result(None, None) + except PredicateMismatch as e: + self.assertEqual(e.detail, + 'predicate mismatch for view myview (pred2)') else: # pragma: no cover raise AssertionError @@ -2974,9 +2999,11 @@ class TestViewDeriver(unittest.TestCase): def predicate1(context, request): predicates.append(True) return True + predicate1.__text__ = 'text' def predicate2(context, request): predicates.append(True) return False + predicate2.__text__ = 'text' deriver = self._makeOne(predicates=[predicate1, predicate2]) result = deriver(view) request = self._makeRequest() -- cgit v1.2.3 From b4f193258837f94d6e4d069f37538dc6f55af709 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 2 Aug 2012 00:37:26 -0400 Subject: closes #643 --- docs/tutorials/wiki2/authorization.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorials/wiki2/authorization.rst b/docs/tutorials/wiki2/authorization.rst index 2ef55d15b..d7bd24a53 100644 --- a/docs/tutorials/wiki2/authorization.rst +++ b/docs/tutorials/wiki2/authorization.rst @@ -353,7 +353,7 @@ when we're done: .. literalinclude:: src/authorization/tutorial/views.py :linenos: - :emphasize-lines: 11,14-18,31,37,58,61,73,76,88,91-117,119-123 + :emphasize-lines: 11,14-18,25,31,37,58,61,73,76,88,91-117,119-123 :language: python (Only the highlighted lines need to be added.) -- cgit v1.2.3 From 763646c2d0ed887223d71d03a52c62679bb456fc Mon Sep 17 00:00:00 2001 From: Blaise Laflamme Date: Fri, 3 Aug 2012 00:17:02 -0400 Subject: added test for adjusted uri in mako templates --- pyramid/mako_templating.py | 3 +++ pyramid/tests/test_mako_templating.py | 32 +++++++++++++++++++++----------- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/pyramid/mako_templating.py b/pyramid/mako_templating.py index 9aeaa9153..d1ee68878 100644 --- a/pyramid/mako_templating.py +++ b/pyramid/mako_templating.py @@ -49,6 +49,9 @@ class PkgResourceTemplateLookup(TemplateLookup): """ if '$' in uri: + # Checks if the uri is already adjusted and brings it back to + # an asset spec. Normally occurs with inherited templates or + # included components. uri = uri.replace('$', ':') isabs = os.path.isabs(uri) if (not isabs) and (':' in uri): diff --git a/pyramid/tests/test_mako_templating.py b/pyramid/tests/test_mako_templating.py index 41fa9bdc4..46826d9dd 100644 --- a/pyramid/tests/test_mako_templating.py +++ b/pyramid/tests/test_mako_templating.py @@ -135,7 +135,7 @@ class Test_renderer_factory(Base, unittest.TestCase): self._callFUT(info) lookup = self._getLookup() self.assertEqual(lookup.template_args['input_encoding'], 'utf-16') - + def test_with_error_handler(self): settings = {'mako.directories':self.templates_dir, 'mako.error_handler':'pyramid.tests'} @@ -383,7 +383,7 @@ class MakoLookupTemplateRendererTests(Base, unittest.TestCase): result = instance.implementation().render_unicode() self.assertTrue(isinstance(result, text_type)) self.assertEqual(result, text_('result')) - + class TestIntegration(unittest.TestCase): def setUp(self): import pyramid.mako_templating @@ -406,7 +406,7 @@ class TestIntegration(unittest.TestCase): self.config.add_settings({'reload_templates': True}) result = render('helloworld.mak', {'a':1}).replace('\r','') self.assertEqual(result, text_('\nHello föö\n', 'utf-8')) - + def test_render_inheritance(self): from pyramid.renderers import render result = render('helloinherit.mak', {}).replace('\r','') @@ -434,7 +434,7 @@ class TestIntegration(unittest.TestCase): {'a':1}) self.assertEqual(result.ubody.replace('\r', ''), text_('\nHello föö\n', 'utf-8')) - + def test_render_with_abs_path(self): from pyramid.renderers import render result = render('/helloworld.mak', {'a':1}).replace('\r','') @@ -446,7 +446,7 @@ class TestIntegration(unittest.TestCase): self.assertEqual( result.implementation().render_unicode().replace('\r',''), text_('\nHello föö\n', 'utf-8')) - + def test_template_not_found(self): from pyramid.renderers import render from mako.exceptions import TemplateLookupException @@ -484,7 +484,7 @@ class TestPkgResourceTemplateLookup(unittest.TestCase): inst = self._makeOne(directories=[fixturedir]) result = inst.get_template('helloworld.mak') self.assertFalse(result is None) - + def test_get_template_asset_spec_with_filesystem_checks(self): inst = self._makeOne(filesystem_checks=True) result = inst.get_template('pyramid.tests:fixtures/helloworld.mak') @@ -498,7 +498,17 @@ class TestPkgResourceTemplateLookup(unittest.TestCase): self.assertFalse(result is None) finally: shutil.rmtree(tmpdir, ignore_errors=True) - + + def test_get_template_asset_spec_with_uri_adjusted(self): + inst = self._makeOne(filesystem_checks=True) + result = inst.get_template('pyramid.tests$fixtures/helloworld.mak') + self.assertFalse(result is None) + + def test_get_template_asset_spec_with_uri_not_adjusted(self): + inst = self._makeOne(filesystem_checks=True) + result = inst.get_template('pyramid.tests:fixtures/helloworld.mak') + self.assertFalse(result is None) + def test_get_template_asset_spec_missing(self): from mako.exceptions import TopLevelLookupException fixturedir = self.get_fixturedir() @@ -510,7 +520,7 @@ class TestMakoRenderingException(unittest.TestCase): def _makeOne(self, text): from pyramid.mako_templating import MakoRenderingException return MakoRenderingException(text) - + def test_repr_and_str(self): exc = self._makeOne('text') self.assertEqual(str(exc), 'text') @@ -519,7 +529,7 @@ class TestMakoRenderingException(unittest.TestCase): class DummyLookup(object): def __init__(self, exc=None): self.exc = exc - + def get_template(self, path): self.path = path return self @@ -533,8 +543,8 @@ class DummyLookup(object): raise self.exc self.values = values return text_('result') - + class DummyRendererInfo(object): def __init__(self, kw): self.__dict__.update(kw) - + -- cgit v1.2.3 From fc3f23c094795e6c889531c9706ec9b1153aac67 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 3 Aug 2012 00:19:51 -0400 Subject: generalize TopologicalSorter out of Tweens class for use elsewhere --- pyramid/config/tweens.py | 114 ++------------ pyramid/config/util.py | 132 ++++++++++++++++ pyramid/tests/test_config/test_tweens.py | 46 +----- pyramid/tests/test_config/test_util.py | 249 +++++++++++++++++++++++++++++++ 4 files changed, 399 insertions(+), 142 deletions(-) diff --git a/pyramid/config/tweens.py b/pyramid/config/tweens.py index 1a83f0de9..1bc6dc95c 100644 --- a/pyramid/config/tweens.py +++ b/pyramid/config/tweens.py @@ -16,7 +16,10 @@ from pyramid.tweens import ( EXCVIEW, ) -from pyramid.config.util import action_method +from pyramid.config.util import ( + action_method, + TopologicalSorter, + ) class TweensConfiguratorMixin(object): def add_tween(self, tween_factory, under=None, over=None): @@ -177,119 +180,24 @@ class TweensConfiguratorMixin(object): introspectables.append(intr) self.action(discriminator, register, introspectables=introspectables) -class CyclicDependencyError(Exception): - def __init__(self, cycles): - self.cycles = cycles - - def __str__(self): - L = [] - cycles = self.cycles - for cycle in cycles: - dependent = cycle - dependees = cycles[cycle] - L.append('%r sorts over %r' % (dependent, dependees)) - msg = 'Implicit tween ordering cycle:' + '; '.join(L) - return msg - @implementer(ITweens) class Tweens(object): def __init__(self): + self.sorter = TopologicalSorter( + default_before=None, + default_after=INGRESS, + first=INGRESS, + last=MAIN) self.explicit = [] - self.names = [] - self.req_over = set() - self.req_under = set() - self.factories = {} - self.order = [] def add_explicit(self, name, factory): self.explicit.append((name, factory)) def add_implicit(self, name, factory, under=None, over=None): - self.names.append(name) - self.factories[name] = factory - if under is None and over is None: - under = INGRESS - if under is not None: - if not is_nonstr_iter(under): - under = (under,) - self.order += [(u, name) for u in under] - self.req_under.add(name) - if over is not None: - if not is_nonstr_iter(over): - over = (over,) - self.order += [(name, o) for o in over] - self.req_over.add(name) + self.sorter.add(name, factory, after=under, before=over) def implicit(self): - order = [(INGRESS, MAIN)] - roots = [] - graph = {} - names = [INGRESS, MAIN] - names.extend(self.names) - - for a, b in self.order: - order.append((a, b)) - - def add_node(node): - if not node in graph: - roots.append(node) - graph[node] = [0] # 0 = number of arcs coming into this node - - def add_arc(fromnode, tonode): - graph[fromnode].append(tonode) - graph[tonode][0] += 1 - if tonode in roots: - roots.remove(tonode) - - for name in names: - add_node(name) - - has_over, has_under = set(), set() - for a, b in order: - if a in names and b in names: # deal with missing dependencies - add_arc(a, b) - has_over.add(a) - has_under.add(b) - - if not self.req_over.issubset(has_over): - raise ConfigurationError( - 'Detected tweens with no satisfied over dependencies: %s' - % (', '.join(sorted(self.req_over - has_over))) - ) - if not self.req_under.issubset(has_under): - raise ConfigurationError( - 'Detected tweens with no satisfied under dependencies: %s' - % (', '.join(sorted(self.req_under - has_under))) - ) - - sorted_names = [] - - while roots: - root = roots.pop(0) - sorted_names.append(root) - children = graph[root][1:] - for child in children: - arcs = graph[child][0] - arcs -= 1 - graph[child][0] = arcs - if arcs == 0: - roots.insert(0, child) - del graph[root] - - if graph: - # loop in input - cycledeps = {} - for k, v in graph.items(): - cycledeps[k] = v[1:] - raise CyclicDependencyError(cycledeps) - - result = [] - - for name in sorted_names: - if name in self.names: - result.append((name, self.factories[name])) - - return result + return self.sorter.sorted() def __call__(self, handler, registry): if self.explicit: diff --git a/pyramid/config/util.py b/pyramid/config/util.py index 4e4c93be3..027060db3 100644 --- a/pyramid/config/util.py +++ b/pyramid/config/util.py @@ -300,3 +300,135 @@ def as_sorted_tuple(val): val = tuple(sorted(val)) return val +# under = after +# over = before + +class Singleton(object): + def __init__(self, repr): + self.repr = repr + + def __repr__(self): + return self.repr + +FIRST = Singleton('FIRST') +LAST = Singleton('LAST') + +class TopologicalSorter(object): + def __init__( + self, + default_before=LAST, + default_after=None, + first=FIRST, + last=LAST, + ): + self.names = [] + self.req_before = set() + self.req_after = set() + self.name2val = {} + self.order = [] + self.default_before = default_before + self.default_after = default_after + self.first = first + self.last = last + + def add(self, name, val, after=None, before=None): + self.names.append(name) + self.name2val[name] = val + if after is None and before is None: + before = self.default_before + after = self.default_after + if after is not None: + if not is_nonstr_iter(after): + after = (after,) + self.order += [(u, name) for u in after] + self.req_after.add(name) + if before is not None: + if not is_nonstr_iter(before): + before = (before,) + self.order += [(name, o) for o in before] + self.req_before.add(name) + + def sorted(self): + order = [(self.first, self.last)] + roots = [] + graph = {} + names = [self.first, self.last] + names.extend(self.names) + + for a, b in self.order: + order.append((a, b)) + + def add_node(node): + if not node in graph: + roots.append(node) + graph[node] = [0] # 0 = number of arcs coming into this node + + def add_arc(fromnode, tonode): + graph[fromnode].append(tonode) + graph[tonode][0] += 1 + if tonode in roots: + roots.remove(tonode) + + for name in names: + add_node(name) + + has_before, has_after = set(), set() + for a, b in order: + if a in names and b in names: # deal with missing dependencies + add_arc(a, b) + has_before.add(a) + has_after.add(b) + + if not self.req_before.issubset(has_before): + raise ConfigurationError( + 'Unsatisfied before dependencies: %s' + % (', '.join(sorted(self.req_before - has_before))) + ) + if not self.req_after.issubset(has_after): + raise ConfigurationError( + 'Unsatisfied after dependencies: %s' + % (', '.join(sorted(self.req_after - has_after))) + ) + + sorted_names = [] + + while roots: + root = roots.pop(0) + sorted_names.append(root) + children = graph[root][1:] + for child in children: + arcs = graph[child][0] + arcs -= 1 + graph[child][0] = arcs + if arcs == 0: + roots.insert(0, child) + del graph[root] + + if graph: + # loop in input + cycledeps = {} + for k, v in graph.items(): + cycledeps[k] = v[1:] + raise CyclicDependencyError(cycledeps) + + result = [] + + for name in sorted_names: + if name in self.names: + result.append((name, self.name2val[name])) + + return result + +class CyclicDependencyError(Exception): + def __init__(self, cycles): + self.cycles = cycles + + def __str__(self): + L = [] + cycles = self.cycles + for cycle in cycles: + dependent = cycle + dependees = cycles[cycle] + L.append('%r sorts before %r' % (dependent, dependees)) + msg = 'Implicit ordering cycle:' + '; '.join(L) + return msg diff --git a/pyramid/tests/test_config/test_tweens.py b/pyramid/tests/test_config/test_tweens.py index 0d96bf601..8853b6899 100644 --- a/pyramid/tests/test_config/test_tweens.py +++ b/pyramid/tests/test_config/test_tweens.py @@ -179,28 +179,12 @@ class TestTweens(unittest.TestCase): ('name2', 'factory2')]) def test_add_implicit(self): - from pyramid.tweens import INGRESS tweens = self._makeOne() tweens.add_implicit('name', 'factory') - self.assertEqual(tweens.names, ['name']) - self.assertEqual(tweens.factories, - {'name':'factory'}) - self.assertEqual(tweens.order, [(INGRESS, 'name')]) tweens.add_implicit('name2', 'factory2') - self.assertEqual(tweens.names, ['name', 'name2']) - self.assertEqual(tweens.factories, - {'name':'factory', 'name2':'factory2'}) - self.assertEqual(tweens.order, - [(INGRESS, 'name'), (INGRESS, 'name2')]) - tweens.add_implicit('name3', 'factory3', over='name2') - self.assertEqual(tweens.names, - ['name', 'name2', 'name3']) - self.assertEqual(tweens.factories, - {'name':'factory', 'name2':'factory2', - 'name3':'factory3'}) - self.assertEqual(tweens.order, - [(INGRESS, 'name'), (INGRESS, 'name2'), - ('name3', 'name2')]) + self.assertEqual(tweens.sorter.sorted(), + [('name2', 'factory2'), + ('name', 'factory')]) def test___call___explicit(self): tweens = self._makeOne() @@ -212,18 +196,13 @@ class TestTweens(unittest.TestCase): self.assertEqual(tweens(None, None), '123') def test___call___implicit(self): - from pyramid.tweens import INGRESS tweens = self._makeOne() def factory1(handler, registry): return handler def factory2(handler, registry): return '123' - tweens.names = ['name', 'name2'] - tweens.alias_to_name = {'name':'name', 'name2':'name2'} - tweens.name_to_alias = {'name':'name', 'name2':'name2'} - tweens.req_under = set(['name', 'name2']) - tweens.order = [(INGRESS, 'name'), (INGRESS, 'name2')] - tweens.factories = {'name':factory1, 'name2':factory2} + tweens.add_implicit('name2', factory2) + tweens.add_implicit('name1', factory1) self.assertEqual(tweens(None, None), '123') def test_implicit_ordering_1(self): @@ -413,7 +392,7 @@ class TestTweens(unittest.TestCase): self.assertRaises(ConfigurationError, tweens.implicit) def test_implicit_ordering_conflict_direct(self): - from pyramid.config.tweens import CyclicDependencyError + from pyramid.config.util import CyclicDependencyError tweens = self._makeOne() add = tweens.add_implicit add('browserid', 'browserid_factory') @@ -421,7 +400,7 @@ class TestTweens(unittest.TestCase): self.assertRaises(CyclicDependencyError, tweens.implicit) def test_implicit_ordering_conflict_indirect(self): - from pyramid.config.tweens import CyclicDependencyError + from pyramid.config.util import CyclicDependencyError tweens = self._makeOne() add = tweens.add_implicit add('browserid', 'browserid_factory') @@ -429,14 +408,3 @@ class TestTweens(unittest.TestCase): add('dbt', 'dbt_factory', under='browserid', over='auth') self.assertRaises(CyclicDependencyError, tweens.implicit) -class TestCyclicDependencyError(unittest.TestCase): - def _makeOne(self, cycles): - from pyramid.config.tweens import CyclicDependencyError - return CyclicDependencyError(cycles) - - def test___str__(self): - exc = self._makeOne({'a':['c', 'd'], 'c':['a']}) - result = str(exc) - self.assertTrue("'a' sorts over ['c', 'd']" in result) - self.assertTrue("'c' sorts over ['a']" in result) - diff --git a/pyramid/tests/test_config/test_util.py b/pyramid/tests/test_config/test_util.py index 1ad1fb3c1..06e63ca40 100644 --- a/pyramid/tests/test_config/test_util.py +++ b/pyramid/tests/test_config/test_util.py @@ -368,6 +368,255 @@ class TestActionInfo(unittest.TestCase): self.assertEqual(str(inst), "Line 0 of file filename:\n linerepr ") +class TestTopologicalSorter(unittest.TestCase): + def _makeOne(self, *arg, **kw): + from pyramid.config.util import TopologicalSorter + return TopologicalSorter(*arg, **kw) + + def test_add(self): + from pyramid.config.util import LAST + sorter = self._makeOne() + sorter.add('name', 'factory') + self.assertEqual(sorter.names, ['name']) + self.assertEqual(sorter.name2val, + {'name':'factory'}) + self.assertEqual(sorter.order, [('name', LAST)]) + sorter.add('name2', 'factory2') + self.assertEqual(sorter.names, ['name', 'name2']) + self.assertEqual(sorter.name2val, + {'name':'factory', 'name2':'factory2'}) + self.assertEqual(sorter.order, + [('name', LAST), ('name2', LAST)]) + sorter.add('name3', 'factory3', before='name2') + self.assertEqual(sorter.names, + ['name', 'name2', 'name3']) + self.assertEqual(sorter.name2val, + {'name':'factory', 'name2':'factory2', + 'name3':'factory3'}) + self.assertEqual(sorter.order, + [('name', LAST), ('name2', LAST), + ('name3', 'name2')]) + + def test_sorted_ordering_1(self): + sorter = self._makeOne() + sorter.add('name1', 'factory1') + sorter.add('name2', 'factory2') + self.assertEqual(sorter.sorted(), + [ + ('name1', 'factory1'), + ('name2', 'factory2'), + ]) + + def test_sorted_ordering_2(self): + from pyramid.config.util import FIRST + sorter = self._makeOne() + sorter.add('name1', 'factory1') + sorter.add('name2', 'factory2', after=FIRST) + self.assertEqual(sorter.sorted(), + [ + ('name2', 'factory2'), + ('name1', 'factory1'), + ]) + + def test_sorted_ordering_3(self): + from pyramid.config.util import FIRST + sorter = self._makeOne() + add = sorter.add + add('auth', 'auth_factory', after='browserid') + add('dbt', 'dbt_factory') + add('retry', 'retry_factory', before='txnmgr', after='exceptionview') + add('browserid', 'browserid_factory') + add('txnmgr', 'txnmgr_factory', after='exceptionview') + add('exceptionview', 'excview_factory', after=FIRST) + self.assertEqual(sorter.sorted(), + [ + ('exceptionview', 'excview_factory'), + ('retry', 'retry_factory'), + ('txnmgr', 'txnmgr_factory'), + ('dbt', 'dbt_factory'), + ('browserid', 'browserid_factory'), + ('auth', 'auth_factory'), + ]) + + def test_sorted_ordering_4(self): + from pyramid.config.util import FIRST + sorter = self._makeOne() + add = sorter.add + add('exceptionview', 'excview_factory', after=FIRST) + add('auth', 'auth_factory', after='browserid') + add('retry', 'retry_factory', before='txnmgr', after='exceptionview') + add('browserid', 'browserid_factory') + add('txnmgr', 'txnmgr_factory', after='exceptionview') + add('dbt', 'dbt_factory') + self.assertEqual(sorter.sorted(), + [ + ('exceptionview', 'excview_factory'), + ('retry', 'retry_factory'), + ('txnmgr', 'txnmgr_factory'), + ('browserid', 'browserid_factory'), + ('auth', 'auth_factory'), + ('dbt', 'dbt_factory'), + ]) + + def test_sorted_ordering_5(self): + from pyramid.config.util import LAST, FIRST + sorter = self._makeOne() + add = sorter.add + add('exceptionview', 'excview_factory') + add('auth', 'auth_factory', after=FIRST) + add('retry', 'retry_factory', before='txnmgr', after='exceptionview') + add('browserid', 'browserid_factory', after=FIRST) + add('txnmgr', 'txnmgr_factory', after='exceptionview', before=LAST) + add('dbt', 'dbt_factory') + self.assertEqual(sorter.sorted(), + [ + ('browserid', 'browserid_factory'), + ('auth', 'auth_factory'), + ('exceptionview', 'excview_factory'), + ('retry', 'retry_factory'), + ('txnmgr', 'txnmgr_factory'), + ('dbt', 'dbt_factory'), + ]) + + def test_sorted_ordering_missing_before_partial(self): + from pyramid.exceptions import ConfigurationError + sorter = self._makeOne() + add = sorter.add + add('dbt', 'dbt_factory') + add('auth', 'auth_factory', after='browserid') + add('retry', 'retry_factory', before='txnmgr', after='exceptionview') + add('browserid', 'browserid_factory') + self.assertRaises(ConfigurationError, sorter.sorted) + + def test_sorted_ordering_missing_after_partial(self): + from pyramid.exceptions import ConfigurationError + sorter = self._makeOne() + add = sorter.add + add('dbt', 'dbt_factory') + add('auth', 'auth_factory', after='txnmgr') + add('retry', 'retry_factory', before='dbt', after='exceptionview') + add('browserid', 'browserid_factory') + self.assertRaises(ConfigurationError, sorter.sorted) + + def test_sorted_ordering_missing_before_and_after_partials(self): + from pyramid.exceptions import ConfigurationError + sorter = self._makeOne() + add = sorter.add + add('dbt', 'dbt_factory') + add('auth', 'auth_factory', after='browserid') + add('retry', 'retry_factory', before='foo', after='txnmgr') + add('browserid', 'browserid_factory') + self.assertRaises(ConfigurationError, sorter.sorted) + + def test_sorted_ordering_missing_before_partial_with_fallback(self): + from pyramid.config.util import LAST + sorter = self._makeOne() + add = sorter.add + add('exceptionview', 'excview_factory', before=LAST) + add('auth', 'auth_factory', after='browserid') + add('retry', 'retry_factory', before=('txnmgr', LAST), + after='exceptionview') + add('browserid', 'browserid_factory') + add('dbt', 'dbt_factory') + self.assertEqual(sorter.sorted(), + [ + ('exceptionview', 'excview_factory'), + ('retry', 'retry_factory'), + ('browserid', 'browserid_factory'), + ('auth', 'auth_factory'), + ('dbt', 'dbt_factory'), + ]) + + def test_sorted_ordering_missing_after_partial_with_fallback(self): + from pyramid.config.util import FIRST + sorter = self._makeOne() + add = sorter.add + add('exceptionview', 'excview_factory', after=FIRST) + add('auth', 'auth_factory', after=('txnmgr','browserid')) + add('retry', 'retry_factory', after='exceptionview') + add('browserid', 'browserid_factory') + add('dbt', 'dbt_factory') + self.assertEqual(sorter.sorted(), + [ + ('exceptionview', 'excview_factory'), + ('retry', 'retry_factory'), + ('browserid', 'browserid_factory'), + ('auth', 'auth_factory'), + ('dbt', 'dbt_factory'), + ]) + + def test_sorted_ordering_with_partial_fallbacks(self): + from pyramid.config.util import LAST + sorter = self._makeOne() + add = sorter.add + add('exceptionview', 'excview_factory', before=('wontbethere', LAST)) + add('retry', 'retry_factory', after='exceptionview') + add('browserid', 'browserid_factory', before=('wont2', 'exceptionview')) + self.assertEqual(sorter.sorted(), + [ + ('browserid', 'browserid_factory'), + ('exceptionview', 'excview_factory'), + ('retry', 'retry_factory'), + ]) + + def test_sorted_ordering_with_multiple_matching_fallbacks(self): + from pyramid.config.util import LAST + sorter = self._makeOne() + add = sorter.add + add('exceptionview', 'excview_factory', before=LAST) + add('retry', 'retry_factory', after='exceptionview') + add('browserid', 'browserid_factory', before=('retry', 'exceptionview')) + self.assertEqual(sorter.sorted(), + [ + ('browserid', 'browserid_factory'), + ('exceptionview', 'excview_factory'), + ('retry', 'retry_factory'), + ]) + + def test_sorted_ordering_with_missing_fallbacks(self): + from pyramid.exceptions import ConfigurationError + from pyramid.config.util import LAST + sorter = self._makeOne() + add = sorter.add + add('exceptionview', 'excview_factory', before=LAST) + add('retry', 'retry_factory', after='exceptionview') + add('browserid', 'browserid_factory', before=('txnmgr', 'auth')) + self.assertRaises(ConfigurationError, sorter.sorted) + + def test_sorted_ordering_conflict_direct(self): + from pyramid.config.util import CyclicDependencyError + sorter = self._makeOne() + add = sorter.add + add('browserid', 'browserid_factory') + add('auth', 'auth_factory', before='browserid', after='browserid') + self.assertRaises(CyclicDependencyError, sorter.sorted) + + def test_sorted_ordering_conflict_indirect(self): + from pyramid.config.util import CyclicDependencyError + sorter = self._makeOne() + add = sorter.add + add('browserid', 'browserid_factory') + add('auth', 'auth_factory', before='browserid') + add('dbt', 'dbt_factory', after='browserid', before='auth') + self.assertRaises(CyclicDependencyError, sorter.sorted) + +class TestSingleton(unittest.TestCase): + def test_repr(self): + from pyramid.config.util import Singleton + r = repr(Singleton('ABC')) + self.assertEqual(r, 'ABC') + +class TestCyclicDependencyError(unittest.TestCase): + def _makeOne(self, cycles): + from pyramid.config.util import CyclicDependencyError + return CyclicDependencyError(cycles) + + def test___str__(self): + exc = self._makeOne({'a':['c', 'd'], 'c':['a']}) + result = str(exc) + self.assertTrue("'a' sorts before ['c', 'd']" in result) + self.assertTrue("'c' sorts before ['a']" in result) + class DummyCustomPredicate(object): def __init__(self): self.__text__ = 'custom predicate' -- cgit v1.2.3 From 8b26752c4e54b1c9a6f8b14b14c0236be9c239b7 Mon Sep 17 00:00:00 2001 From: Blaise Laflamme Date: Fri, 3 Aug 2012 00:48:00 -0400 Subject: added entry to changes.txt for mako fix --- CHANGES.txt | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index ecb2bf659..94553955c 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -17,6 +17,12 @@ Bug Fixes during iteration`` exception. It no longer does. See https://github.com/Pylons/pyramid/issues/635 for more information. +- In Mako Templates lookup, check if the uri is already adjusted and bring + it back to an asset spec. Normally occurs with inherited templates or + included components. + https://github.com/Pylons/pyramid/issues/606 + https://github.com/Pylons/pyramid/issues/607 + Features -------- @@ -50,9 +56,9 @@ Features and ``HTTPMovedPermanently`` exceptions, so these can be caught by the NotFound view (and other exception views). -- The mako renderer now accepts a def name and returns the template def - result for the view being called. The uri format using an asset spec is - package:path/to/template#defname.mako. The old way of returning a tuple +- The mako renderer now accepts a def name and returns the template def + result for the view being called. The uri format using an asset spec is + package:path/to/template#defname.mako. The old way of returning a tuple from the view is supported for backward compatibility, ('defname', {}). - When there is a predicate mismatch exception (seen when no view matches for -- cgit v1.2.3 From c746ba4d0893ed7f4322492fdba548f3cd2a1ac5 Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Fri, 3 Aug 2012 00:44:41 -0700 Subject: Adding helpful link to installing chapter from first app --- docs/narr/firstapp.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/narr/firstapp.rst b/docs/narr/firstapp.rst index 1ca188d7e..a86826d86 100644 --- a/docs/narr/firstapp.rst +++ b/docs/narr/firstapp.rst @@ -8,7 +8,8 @@ Creating Your First :app:`Pyramid` Application In this chapter, we will walk through the creation of a tiny :app:`Pyramid` application. After we're finished creating the application, we'll explain in -more detail how it works. +more detail how it works. It assumes you already have :app:`Pyramid` installed. +If you do not, head over to the :ref:`installing_chapter` section. .. _helloworld_imperative: -- cgit v1.2.3 From 41b6ebf715b9c4935aa876c879e562e767d63fc0 Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Fri, 3 Aug 2012 07:52:54 -0700 Subject: Removing easy_install and demoing lines from index example. firstapp has installation chapter links and having peopel using pyramid outside a virtualenv is a bad idea --- docs/index.rst | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 31c2fde6b..699c89449 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -13,15 +13,9 @@ Here is one of the simplest :app:`Pyramid` applications you can make: .. literalinclude:: narr/helloworld.py -When saved to ``helloworld.py``, the above application can be run via: - -.. code-block:: text - - $ easy_install pyramid - $ python helloworld.py - -When you visit ``http://localhost:8080/hello/world`` in a browser, you will -see the text ``Hello, world!``. +After you install :app:`Pyramid` and run this application. When you visit +``http://localhost:8080/hello/world`` in a browser, you will see the +text ``Hello, world!`` See :ref:`firstapp_chapter` for a full explanation of how this application works. Read the :ref:`html_narrative_documentation` to understand how -- cgit v1.2.3 From db9468a22d8a9adbb29e70eb17c8ee9612797d61 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 3 Aug 2012 12:21:02 -0400 Subject: no period required here --- docs/index.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 699c89449..c84314274 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -13,9 +13,9 @@ Here is one of the simplest :app:`Pyramid` applications you can make: .. literalinclude:: narr/helloworld.py -After you install :app:`Pyramid` and run this application. When you visit -``http://localhost:8080/hello/world`` in a browser, you will see the -text ``Hello, world!`` +After you install :app:`Pyramid` and run this application, when you visit +``http://localhost:8080/hello/world`` in a browser, you will see the text +``Hello, world!`` See :ref:`firstapp_chapter` for a full explanation of how this application works. Read the :ref:`html_narrative_documentation` to understand how -- cgit v1.2.3 From a00621e45ef29cde34469798144156c80a17a1e9 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 3 Aug 2012 12:55:12 -0400 Subject: first cut at extensible view predicates via config.add_view_predicate; still requires testing of predicates themselves --- pyramid/config/__init__.py | 2 + pyramid/config/predicates.py | 218 ++++++++++++++++++++++++++++++++ pyramid/config/util.py | 56 +++++++- pyramid/config/views.py | 116 ++++++++++++++--- pyramid/interfaces.py | 3 + pyramid/testing.py | 1 + pyramid/tests/test_config/test_init.py | 21 ++- pyramid/tests/test_config/test_views.py | 12 +- pyramid/tests/test_url.py | 10 +- pyramid/tests/test_view.py | 13 +- 10 files changed, 408 insertions(+), 44 deletions(-) create mode 100644 pyramid/config/predicates.py diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index 52d7aca83..5eb860ed5 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -353,6 +353,8 @@ class Configurator( for name, renderer in DEFAULT_RENDERERS: self.add_renderer(name, renderer) + self.add_default_view_predicates() + if exceptionresponse_view is not None: exceptionresponse_view = self.maybe_dotted(exceptionresponse_view) self.add_view(exceptionresponse_view, context=IExceptionResponse) diff --git a/pyramid/config/predicates.py b/pyramid/config/predicates.py new file mode 100644 index 000000000..24ec89c6b --- /dev/null +++ b/pyramid/config/predicates.py @@ -0,0 +1,218 @@ +import re + +from pyramid.compat import is_nonstr_iter + +from pyramid.exceptions import ConfigurationError + +from pyramid.traversal import ( + find_interface, + traversal_path, + ) + +from pyramid.urldispatch import _compile_route + +from .util import as_sorted_tuple + +class XHRPredicate(object): + def __init__(self, val): + self.val = bool(val) + + def __text__(self): + return 'xhr = True' + + def __phash__(self): + return 'xhr:%r' % (self.val,) + + def __call__(self, context, request): + return request.is_xhr + + +class RequestMethodPredicate(object): + def __init__(self, val): + self.val = as_sorted_tuple(val) + + def __text__(self): + return 'request method = %r' % (self.val,) + + def __phash__(self): + L = [] + for v in self.val: + L.append('request_method:%r' % v) + return L + + def __call__(self, context, request): + return request.method in self.val + +class PathInfoPredicate(object): + def __init__(self, val): + self.orig = val + try: + val = re.compile(val) + except re.error as why: + raise ConfigurationError(why.args[0]) + self.val = val + + def __text__(self): + return 'path_info = %s' % (self.orig,) + + def __phash__(self): + return 'path_info:%r' % (self.orig,) + + def __call__(self, context, request): + return self.val.match(request.upath_info) is not None + +class RequestParamPredicate(object): + def __init__(self, val): + name = val + v = None + if '=' in name: + name, v = name.split('=', 1) + if v is None: + self.text = 'request_param %s' % (name,) + else: + self.text = 'request_param %s = %s' % (name, v) + self.name = name + self.val = v + + def __text__(self): + return self.text + + def __phash__(self): + return 'request_param:%r=%r' % (self.name, self.val) + + def __call__(self, context, request): + if self.val is None: + return self.name in request.params + return request.params.get(self.name) == self.val + + +class HeaderPredicate(object): + def __init__(self, val): + name = val + v = None + if ':' in name: + name, v = name.split(':', 1) + try: + v = re.compile(v) + except re.error as why: + raise ConfigurationError(why.args[0]) + if v is None: + self.text = 'header %s' % (name,) + else: + self.text = 'header %s = %s' % (name, v) + self.name = name + self.val = v + + def __text__(self): + return self.text + + def __phash__(self): + return 'header:%r=%r' % (self.name, self.val) + + def __call__(self, context, request): + if self.val is None: + return self.name in request.headers + val = request.headers.get(self.name) + if val is None: + return False + return self.val.match(val) is not None + +class AcceptPredicate(object): + def __init__(self, val): + self.val = val + + def __text__(self): + return 'accept = %s' % (self.val,) + + def __phash__(self): + return 'accept:%r' % (self.val,) + + def __call__(self, context, request): + return self.val in request.accept + +class ContainmentPredicate(object): + def __init__(self, val): + self.val = val + + def __text__(self): + return 'containment = %s' % (self.val,) + + def __phash__(self): + return 'containment:%r' % hash(self.val) + + def __call__(self, context, request): + ctx = getattr(request, 'context', context) + return find_interface(ctx, self.val) is not None + +class RequestTypePredicate(object): + def __init__(self, val): + self.val = val + + def __text__(self): + return 'request_type = %s' % (self.val,) + + def __phash__(self): + return 'request_type:%r' % hash(self.val) + + def __call__(self, context, request): + return self.val.providedBy(request) + +class MatchParamPredicate(object): + def __init__(self, val): + if not is_nonstr_iter(val): + val = (val,) + val = sorted(val) + self.val = val + self.reqs = [ + (x.strip(), y.strip()) for x, y in [ p.split('=', 1) for p in val ] + ] + + def __text__(self): + return 'match_param %s' % (self.val,) + + def __phash__(self): + L = [] + for k, v in self.reqs: + L.append('match_param:%r=%r' % (k, v)) + return L + + def __call__(self, context, request): + for k, v in self.reqs: + if request.matchdict.get(k) != v: + return False + return True + +class CustomPredicate(object): + def __init__(self, func): + self.func = func + + def __text__(self): + return getattr(self.func, '__text__', repr(self.func)) + + def __phash__(self): + return 'custom:%r' % hash(self.func) + + def __call__(self, context, request): + return self.func(context, request) + + +class TraversePredicate(object): + def __init__(self, val): + _, self.tgenerate = _compile_route(val) + self.val = val + + def __text__(self): + return 'traverse matchdict pseudo-predicate' + + def __phash__(self): + return '' + + def __call__(self, context, request): + if 'traverse' in context: + return True + m = context['match'] + tvalue = self.tgenerate(m) + m['traverse'] = traversal_path(tvalue) + return True + + diff --git a/pyramid/config/util.py b/pyramid/config/util.py index 027060db3..1574d7da6 100644 --- a/pyramid/config/util.py +++ b/pyramid/config/util.py @@ -324,14 +324,26 @@ class TopologicalSorter(object): self.names = [] self.req_before = set() self.req_after = set() + self.name2before = {} + self.name2after = {} self.name2val = {} self.order = [] self.default_before = default_before self.default_after = default_after self.first = first self.last = last - + + def remove(self, name): + if name in self.names: + self.names.remove(name) + del self.name2val[name] + for u in self.name2after.get(name, []): + self.order.remove((u, name)) + for u in self.name2before.get(name, []): + self.order.remove((name, u)) + def add(self, name, val, after=None, before=None): + self.remove(name) self.names.append(name) self.name2val[name] = val if after is None and before is None: @@ -340,11 +352,13 @@ class TopologicalSorter(object): if after is not None: if not is_nonstr_iter(after): after = (after,) + self.name2after[name] = after self.order += [(u, name) for u in after] self.req_after.add(name) if before is not None: if not is_nonstr_iter(before): before = (before,) + self.name2before[name] = before self.order += [(name, o) for o in before] self.req_before.add(name) @@ -432,3 +446,43 @@ class CyclicDependencyError(Exception): L.append('%r sorts before %r' % (dependent, dependees)) msg = 'Implicit ordering cycle:' + '; '.join(L) return msg + +class PredicateList(object): + def __init__(self): + self.sorter = TopologicalSorter() + + def add(self, name, factory, weighs_more_than=None, weighs_less_than=None): + self.sorter.add(name, factory, after=weighs_more_than, + before=weighs_less_than) + + def make(self, **kw): + ordered = self.sorter.sorted() + phash = md5() + weights = [] + predicates = [] + for order, (name, predicate_factory) in enumerate(ordered): + vals = kw.pop(name, None) + if vals is None: + continue + if not isinstance(vals, SequenceOfPredicateValues): + vals = (vals,) + for val in vals: + predicate = predicate_factory(val) + hashes = predicate.__phash__() + if not is_nonstr_iter(hashes): + hashes = [hashes] + for h in hashes: + phash.update(bytes_(h)) + predicate = predicate_factory(val) + weights.append(1 << order) + predicates.append(predicate) + if kw: + raise ConfigurationError('Unknown predicate values: %r' % (kw,)) + score = 0 + for bit in weights: + score = score | bit + order = (MAX_ORDER - score) / (len(predicates) + 1) + return order, predicates, phash.hexdigest() + +class SequenceOfPredicateValues(tuple): + pass diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 4354b4691..b59d18400 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -20,6 +20,7 @@ from pyramid.interfaces import ( IException, IExceptionViewClassifier, IMultiView, + IPredicateList, IRendererFactory, IRequest, IResponse, @@ -65,12 +66,15 @@ from pyramid.view import ( from pyramid.util import object_description +from pyramid.config import predicates + from pyramid.config.util import ( DEFAULT_PHASH, MAX_ORDER, action_method, as_sorted_tuple, - make_predicates, + PredicateList, + SequenceOfPredicateValues, ) urljoin = urlparse.urljoin @@ -272,11 +276,11 @@ class ViewDeriver(object): @wraps_view def predicated_view(self, view): - predicates = self.kw.get('predicates', ()) - if not predicates: + preds = self.kw.get('predicates', ()) + if not preds: return view def predicate_wrapper(context, request): - for predicate in predicates: + for predicate in preds: if not predicate(context, request): view_name = getattr(view, '__name__', view) raise PredicateMismatch( @@ -285,9 +289,9 @@ class ViewDeriver(object): return view(context, request) def checker(context, request): return all((predicate(context, request) for predicate in - predicates)) + preds)) predicate_wrapper.__predicated__ = checker - predicate_wrapper.__predicates__ = predicates + predicate_wrapper.__predicates__ = preds return predicate_wrapper @wraps_view @@ -634,10 +638,10 @@ class ViewsConfiguratorMixin(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, + renderer=None, wrapper=None, xhr=None, accept=None, header=None, path_info=None, custom_predicates=(), context=None, decorator=None, mapper=None, http_cache=None, - match_param=None): + match_param=None, **other_predicates): """ Add a :term:`view configuration` to the current configuration state. Arguments to ``add_view`` are broken down below into *predicate* arguments and *non-predicate* @@ -1003,12 +1007,6 @@ class ViewsConfiguratorMixin(object): # GET implies HEAD too request_method = as_sorted_tuple(request_method + ('HEAD',)) - order, predicates, phash = make_predicates(xhr=xhr, - request_method=request_method, path_info=path_info, - request_param=request_param, header=header, accept=accept, - containment=containment, request_type=request_type, - match_param=match_param, custom=custom_predicates) - if context is None: context = for_ @@ -1024,12 +1022,24 @@ class ViewsConfiguratorMixin(object): registry = self.registry) introspectables = [] - discriminator = [ - 'view', context, name, request_type, IView, containment, - request_param, request_method, route_name, attr, - xhr, accept, header, path_info, match_param] - discriminator.extend(sorted([hash(x) for x in custom_predicates])) - discriminator = tuple(discriminator) + pvals = other_predicates + pvals.update( + dict( + xhr=xhr, + request_method=request_method, + path_info=path_info, + request_param=request_param, + header=header, + accept=accept, + containment=containment, + request_type=request_type, + match_param=match_param, + custom=SequenceOfPredicateValues(custom_predicates), + ) + ) + + discriminator = ('view', context, name, route_name, attr, + str(sorted(pvals.items()))) if inspect.isclass(view) and attr: view_desc = 'method %r of %s' % ( attr, self.object_description(view)) @@ -1057,9 +1067,13 @@ class ViewsConfiguratorMixin(object): decorator=decorator, ) ) + view_intr.update(**other_predicates) introspectables.append(view_intr) + predlist = self.view_predlist def register(permission=permission, renderer=renderer): + order, preds, phash = predlist.make(**pvals) + view_intr.update({'phash':phash}) request_iface = IRequest if route_name is not None: request_iface = self.registry.queryUtility(IRouteRequest, @@ -1087,7 +1101,7 @@ class ViewsConfiguratorMixin(object): # __no_permission_required__ handled by _secure_view deriver = ViewDeriver(registry=self.registry, permission=permission, - predicates=predicates, + predicates=preds, attr=attr, renderer=renderer, wrapper_viewname=wrapper, @@ -1230,6 +1244,66 @@ class ViewsConfiguratorMixin(object): introspectables.append(perm_intr) self.action(discriminator, register, introspectables=introspectables) + @property + def view_predlist(self): + predlist = self.registry.queryUtility(IPredicateList, name='view') + if predlist is None: + predlist = PredicateList() + self.registry.registerUtility(predlist, IPredicateList, name='view') + return predlist + + @action_method + def add_view_predicate(self, name, factory, weighs_more_than=None, + weighs_less_than=None): + """ Adds a view predicate factory. The view predicate can later be + named as a keyword argument to + :meth:`pyramid.config.Configurator.add_view`. + + ``name`` should be the name of the predicate. It must be a valid + Python identifier (it will be used as a keyword argument to + ``add_view``). + + ``factory`` should be a :term:`predicate factory`. + """ + discriminator = ('view predicate', name) + intr = self.introspectable( + 'view predicates', + discriminator, + 'view predicate named %s' % name, + 'view predicate') + intr['name'] = name + intr['factory'] = factory + intr['weighs_more_than'] = weighs_more_than + intr['weighs_less_than'] = weighs_less_than + def register(): + predlist = self.view_predlist + predlist.add(name, factory, weighs_more_than=weighs_more_than, + weighs_less_than=weighs_less_than) + self.action(discriminator, register, introspectables=(intr,), + order=PHASE1_CONFIG) # must be registered before views added + + def add_default_view_predicates(self): + self.add_view_predicate( + 'xhr', predicates.XHRPredicate) + self.add_view_predicate( + 'request_method', predicates.RequestMethodPredicate) + self.add_view_predicate( + 'path_info', predicates.PathInfoPredicate) + self.add_view_predicate( + 'request_param', predicates.RequestParamPredicate) + self.add_view_predicate( + 'header', predicates.HeaderPredicate) + self.add_view_predicate( + 'accept', predicates.AcceptPredicate) + self.add_view_predicate( + 'containment', predicates.ContainmentPredicate) + self.add_view_predicate( + 'request_type', predicates.RequestTypePredicate) + self.add_view_predicate( + 'match_param', predicates.MatchParamPredicate) + self.add_view_predicate( + 'custom', predicates.CustomPredicate) + def derive_view(self, view, attr=None, renderer=None): """ Create a :term:`view callable` using the function, instance, diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index 1445ee394..114a01854 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -1111,6 +1111,9 @@ class IJSONAdapter(Interface): into a JSON-serializable primitive. """ +class IPredicateList(Interface): + """ Interface representing a predicate list """ + # configuration phases: a lower phase number means the actions associated # with this phase will be executed earlier than those with later phase # numbers. The default phase number is 0, FTR. diff --git a/pyramid/testing.py b/pyramid/testing.py index 40e90cda6..2628dc817 100644 --- a/pyramid/testing.py +++ b/pyramid/testing.py @@ -824,6 +824,7 @@ def setUp(registry=None, request=None, hook_zca=True, autocommit=True, # ``render_template`` and friends went behind the back of # any existing renderer factory lookup system. config.add_renderer(name, renderer) + config.add_default_view_predicates() config.commit() global have_zca try: diff --git a/pyramid/tests/test_config/test_init.py b/pyramid/tests/test_config/test_init.py index 37c3de275..b23168aaa 100644 --- a/pyramid/tests/test_config/test_init.py +++ b/pyramid/tests/test_config/test_init.py @@ -349,7 +349,7 @@ class ConfiguratorTests(unittest.TestCase): config.setup_registry() self.assertEqual(reg.has_listeners, True) - def test_setup_registry_registers_default_exceptionresponse_view(self): + def test_setup_registry_registers_default_exceptionresponse_views(self): from webob.exc import WSGIHTTPException from pyramid.interfaces import IExceptionResponse from pyramid.view import default_exceptionresponse_view @@ -357,6 +357,7 @@ class ConfiguratorTests(unittest.TestCase): config = self._makeOne(reg) views = [] config.add_view = lambda *arg, **kw: views.append((arg, kw)) + config.add_default_view_predicates = lambda *arg: None config._add_tween = lambda *arg, **kw: False config.setup_registry() self.assertEqual(views[0], ((default_exceptionresponse_view,), @@ -364,6 +365,16 @@ class ConfiguratorTests(unittest.TestCase): self.assertEqual(views[1], ((default_exceptionresponse_view,), {'context':WSGIHTTPException})) + def test_setup_registry_registers_default_view_predicates(self): + reg = DummyRegistry() + config = self._makeOne(reg) + vp_called = [] + config.add_view = lambda *arg, **kw: None + config.add_default_view_predicates = lambda *arg: vp_called.append(True) + config._add_tween = lambda *arg, **kw: False + config.setup_registry() + self.assertTrue(vp_called) + def test_setup_registry_registers_default_webob_iresponse_adapter(self): from webob import Response from pyramid.interfaces import IResponse @@ -1940,10 +1951,11 @@ class DummyEvent: pass class DummyRegistry(object): - def __init__(self, adaptation=None): + def __init__(self, adaptation=None, util=None): self.utilities = [] self.adapters = [] self.adaptation = adaptation + self.util = util def subscribers(self, events, name): self.events = events return events @@ -1953,6 +1965,8 @@ class DummyRegistry(object): self.adapters.append((arg, kw)) def queryAdapter(self, *arg, **kw): return self.adaptation + def queryUtility(self, *arg, **kw): + return self.util from pyramid.interfaces import IResponse @implementer(IResponse) @@ -1983,3 +1997,6 @@ class DummyIntrospectable(object): def register(self, introspector, action_info): self.registered.append((introspector, action_info)) +class DummyPredicateList(object): + def add(self, name, factory, weighs_more_than=None, weighs_less_than=None): + pass diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index ebf1dfb39..ea8883478 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -970,8 +970,8 @@ class TestViewsConfigurationMixin(unittest.TestCase): wrapper = self._getViewCallable(config) self.assertTrue(IMultiView.providedBy(wrapper)) request = self._makeRequest(config) - self.assertEqual(wrapper.__discriminator__(foo, request)[5], IFoo) - self.assertEqual(wrapper.__discriminator__(bar, request)[5], IBar) + self.assertTrue('IFoo' in wrapper.__discriminator__(foo, request)[5]) + self.assertTrue('IBar' in wrapper.__discriminator__(bar, request)[5]) def test_add_view_with_template_renderer(self): from pyramid.tests import test_config @@ -1217,8 +1217,8 @@ class TestViewsConfigurationMixin(unittest.TestCase): def test_add_view_with_header_badregex(self): view = lambda *arg: 'OK' config = self._makeOne() - self.assertRaises(ConfigurationError, - config.add_view, view=view, header='Host:a\\') + config.add_view(view, header='Host:a\\') + self.assertRaises(ConfigurationError, config.commit) def test_add_view_with_header_noval_match(self): from pyramid.renderers import null_renderer @@ -1323,8 +1323,8 @@ class TestViewsConfigurationMixin(unittest.TestCase): def test_add_view_with_path_info_badregex(self): view = lambda *arg: 'OK' config = self._makeOne() - self.assertRaises(ConfigurationError, - config.add_view, view=view, path_info='\\') + config.add_view(view, path_info='\\') + self.assertRaises(ConfigurationError, config.commit) def test_add_view_with_path_info_match(self): from pyramid.renderers import null_renderer diff --git a/pyramid/tests/test_url.py b/pyramid/tests/test_url.py index 50deb63f3..a7a565356 100644 --- a/pyramid/tests/test_url.py +++ b/pyramid/tests/test_url.py @@ -2,10 +2,8 @@ import os import unittest import warnings -from pyramid.testing import ( - setUp, - tearDown, - ) +from pyramid import testing + from pyramid.compat import ( text_, native_, @@ -14,10 +12,10 @@ from pyramid.compat import ( class TestURLMethodsMixin(unittest.TestCase): def setUp(self): - self.config = setUp() + self.config = testing.setUp() def tearDown(self): - tearDown() + testing.tearDown() def _makeOne(self, environ=None): from pyramid.url import URLMethodsMixin diff --git a/pyramid/tests/test_view.py b/pyramid/tests/test_view.py index a105adb70..ee4994172 100644 --- a/pyramid/tests/test_view.py +++ b/pyramid/tests/test_view.py @@ -3,17 +3,14 @@ import sys from zope.interface import implementer -from pyramid.testing import ( - setUp, - tearDown, - ) +from pyramid import testing class BaseTest(object): def setUp(self): - self.config = setUp() + self.config = testing.setUp() def tearDown(self): - tearDown() + testing.tearDown() def _registerView(self, reg, app, name): from pyramid.interfaces import IRequest @@ -334,10 +331,10 @@ class TestIsResponse(unittest.TestCase): class TestViewConfigDecorator(unittest.TestCase): def setUp(self): - setUp() + testing.setUp() def tearDown(self): - tearDown() + testing.tearDown() def _getTargetClass(self): from pyramid.view import view_config -- cgit v1.2.3 From 4f0b02e36591d0cfbfa0e328e1e428b5a286c09c Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 3 Aug 2012 16:55:03 -0400 Subject: add some tests --- pyramid/config/predicates.py | 16 +-- pyramid/tests/test_config/test_predicates.py | 143 +++++++++++++++++++++++++++ 2 files changed, 151 insertions(+), 8 deletions(-) create mode 100644 pyramid/tests/test_config/test_predicates.py diff --git a/pyramid/config/predicates.py b/pyramid/config/predicates.py index 24ec89c6b..6ea4b9ac8 100644 --- a/pyramid/config/predicates.py +++ b/pyramid/config/predicates.py @@ -18,26 +18,25 @@ class XHRPredicate(object): self.val = bool(val) def __text__(self): - return 'xhr = True' + return 'xhr = %s' % self.val def __phash__(self): - return 'xhr:%r' % (self.val,) + return 'xhr:%s' % self.val def __call__(self, context, request): - return request.is_xhr - + return bool(request.is_xhr) is self.val class RequestMethodPredicate(object): def __init__(self, val): self.val = as_sorted_tuple(val) def __text__(self): - return 'request method = %r' % (self.val,) + return 'request method = %s' % (','.join(self.val)) def __phash__(self): L = [] for v in self.val: - L.append('request_method:%r' % v) + L.append('request_method:%s' % v) return L def __call__(self, context, request): @@ -56,7 +55,7 @@ class PathInfoPredicate(object): return 'path_info = %s' % (self.orig,) def __phash__(self): - return 'path_info:%r' % (self.orig,) + return 'path_info:%s' % (self.orig,) def __call__(self, context, request): return self.val.match(request.upath_info) is not None @@ -67,6 +66,7 @@ class RequestParamPredicate(object): v = None if '=' in name: name, v = name.split('=', 1) + name, v = name.strip(), v.strip() if v is None: self.text = 'request_param %s' % (name,) else: @@ -78,7 +78,7 @@ class RequestParamPredicate(object): return self.text def __phash__(self): - return 'request_param:%r=%r' % (self.name, self.val) + return 'request_param:%s=%r' % (self.name, self.val) def __call__(self, context, request): if self.val is None: diff --git a/pyramid/tests/test_config/test_predicates.py b/pyramid/tests/test_config/test_predicates.py new file mode 100644 index 000000000..22d992404 --- /dev/null +++ b/pyramid/tests/test_config/test_predicates.py @@ -0,0 +1,143 @@ +import unittest + +from pyramid.compat import text_ + +class TestXHRPredicate(unittest.TestCase): + def _makeOne(self, val): + from pyramid.config.predicates import XHRPredicate + return XHRPredicate(val) + + def test___call___true(self): + inst = self._makeOne(True) + request = Dummy() + request.is_xhr = True + result = inst(None, request) + self.assertTrue(result) + + def test___call___false(self): + inst = self._makeOne(True) + request = Dummy() + request.is_xhr = False + result = inst(None, request) + self.assertFalse(result) + + def test___text__(self): + inst = self._makeOne(True) + self.assertEqual(inst.__text__(), 'xhr = True') + + def test___phash__(self): + inst = self._makeOne(True) + self.assertEqual(inst.__phash__(), 'xhr:True') + +class TestRequestMethodPredicate(unittest.TestCase): + def _makeOne(self, val): + from pyramid.config.predicates import RequestMethodPredicate + return RequestMethodPredicate(val) + + def test___call___true_single(self): + inst = self._makeOne('GET') + request = Dummy() + request.method = 'GET' + result = inst(None, request) + self.assertTrue(result) + + def test___call___true_multi(self): + inst = self._makeOne(('GET','HEAD')) + request = Dummy() + request.method = 'GET' + result = inst(None, request) + self.assertTrue(result) + + def test___call___false(self): + inst = self._makeOne(('GET','HEAD')) + request = Dummy() + request.method = 'POST' + result = inst(None, request) + self.assertFalse(result) + + def test___text__(self): + inst = self._makeOne(('HEAD','GET')) + self.assertEqual(inst.__text__(), 'request method = GET,HEAD') + + def test___phash__(self): + inst = self._makeOne(('HEAD','GET')) + self.assertEqual(inst.__phash__(), ['request_method:GET', + 'request_method:HEAD']) + +class TestPathInfoPredicate(unittest.TestCase): + def _makeOne(self, val): + from pyramid.config.predicates import PathInfoPredicate + return PathInfoPredicate(val) + + def test_ctor_compilefail(self): + from pyramid.exceptions import ConfigurationError + self.assertRaises(ConfigurationError, self._makeOne, '\\') + + def test___call___true(self): + inst = self._makeOne(r'/\d{2}') + request = Dummy() + request.upath_info = text_('/12') + result = inst(None, request) + self.assertTrue(result) + + def test___call___false(self): + inst = self._makeOne(r'/\d{2}') + request = Dummy() + request.upath_info = text_('/n12') + result = inst(None, request) + self.assertFalse(result) + + def test___text__(self): + inst = self._makeOne('/') + self.assertEqual(inst.__text__(), 'path_info = /') + + def test___phash__(self): + inst = self._makeOne('/') + self.assertEqual(inst.__phash__(), 'path_info:/') + +class TestRequestParamPredicate(unittest.TestCase): + def _makeOne(self, val): + from pyramid.config.predicates import RequestParamPredicate + return RequestParamPredicate(val) + + def test___call___true_exists(self): + inst = self._makeOne('abc') + request = Dummy() + request.params = {'abc':1} + result = inst(None, request) + self.assertTrue(result) + + def test___call___true_withval(self): + inst = self._makeOne('abc=1') + request = Dummy() + request.params = {'abc':'1'} + result = inst(None, request) + self.assertTrue(result) + + def test___call___false(self): + inst = self._makeOne('abc') + request = Dummy() + request.params = {} + result = inst(None, request) + self.assertFalse(result) + + def test___text__exists(self): + inst = self._makeOne('abc') + self.assertEqual(inst.__text__(), 'request_param abc') + + def test___text__withval(self): + inst = self._makeOne('abc= 1') + self.assertEqual(inst.__text__(), 'request_param abc = 1') + + def test___phash__exists(self): + inst = self._makeOne('abc') + self.assertEqual(inst.__phash__(), 'request_param:abc=None') + + def test___phash__withval(self): + inst = self._makeOne('abc= 1') + self.assertEqual(inst.__phash__(), "request_param:abc='1'") + +class Dummy(object): + def __init__(self, **kw): + self.__dict__.update(**kw) + -- cgit v1.2.3 From 4d2602cecb90ff75e2ba33e4435b9dcee830085f Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 3 Aug 2012 23:57:37 -0400 Subject: test standalone default predicates; make protocol "phash()" and "text()" rather than "__phash__()" and "__text__()" --- pyramid/config/predicates.py | 84 ++++++-------- pyramid/config/util.py | 2 +- pyramid/config/views.py | 2 +- pyramid/scripts/pviews.py | 8 +- pyramid/tests/test_config/test_predicates.py | 163 +++++++++++++++++++++++---- pyramid/tests/test_config/test_views.py | 16 +-- pyramid/tests/test_scripts/test_pviews.py | 4 +- 7 files changed, 192 insertions(+), 87 deletions(-) diff --git a/pyramid/config/predicates.py b/pyramid/config/predicates.py index 6ea4b9ac8..37f75462d 100644 --- a/pyramid/config/predicates.py +++ b/pyramid/config/predicates.py @@ -17,11 +17,10 @@ class XHRPredicate(object): def __init__(self, val): self.val = bool(val) - def __text__(self): + def text(self): return 'xhr = %s' % self.val - def __phash__(self): - return 'xhr:%s' % self.val + phash = text def __call__(self, context, request): return bool(request.is_xhr) is self.val @@ -30,14 +29,10 @@ class RequestMethodPredicate(object): def __init__(self, val): self.val = as_sorted_tuple(val) - def __text__(self): - return 'request method = %s' % (','.join(self.val)) + def text(self): + return 'request_method = %s' % (','.join(self.val)) - def __phash__(self): - L = [] - for v in self.val: - L.append('request_method:%s' % v) - return L + phash = text def __call__(self, context, request): return request.method in self.val @@ -51,11 +46,10 @@ class PathInfoPredicate(object): raise ConfigurationError(why.args[0]) self.val = val - def __text__(self): + def text(self): return 'path_info = %s' % (self.orig,) - def __phash__(self): - return 'path_info:%s' % (self.orig,) + phash = text def __call__(self, context, request): return self.val.match(request.upath_info) is not None @@ -68,17 +62,16 @@ class RequestParamPredicate(object): name, v = name.split('=', 1) name, v = name.strip(), v.strip() if v is None: - self.text = 'request_param %s' % (name,) + self._text = 'request_param %s' % (name,) else: - self.text = 'request_param %s = %s' % (name, v) + self._text = 'request_param %s = %s' % (name, v) self.name = name self.val = v - def __text__(self): - return self.text + def text(self): + return self._text - def __phash__(self): - return 'request_param:%s=%r' % (self.name, self.val) + phash = text def __call__(self, context, request): if self.val is None: @@ -97,17 +90,16 @@ class HeaderPredicate(object): except re.error as why: raise ConfigurationError(why.args[0]) if v is None: - self.text = 'header %s' % (name,) + self._text = 'header %s' % (name,) else: - self.text = 'header %s = %s' % (name, v) + self._text = 'header %s = %s' % (name, v) self.name = name self.val = v - def __text__(self): - return self.text + def text(self): + return self._text - def __phash__(self): - return 'header:%r=%r' % (self.name, self.val) + phash = text def __call__(self, context, request): if self.val is None: @@ -121,11 +113,10 @@ class AcceptPredicate(object): def __init__(self, val): self.val = val - def __text__(self): + def text(self): return 'accept = %s' % (self.val,) - def __phash__(self): - return 'accept:%r' % (self.val,) + phash = text def __call__(self, context, request): return self.val in request.accept @@ -134,11 +125,10 @@ class ContainmentPredicate(object): def __init__(self, val): self.val = val - def __text__(self): + def text(self): return 'containment = %s' % (self.val,) - def __phash__(self): - return 'containment:%r' % hash(self.val) + phash = text def __call__(self, context, request): ctx = getattr(request, 'context', context) @@ -148,11 +138,10 @@ class RequestTypePredicate(object): def __init__(self, val): self.val = val - def __text__(self): + def text(self): return 'request_type = %s' % (self.val,) - def __phash__(self): - return 'request_type:%r' % hash(self.val) + phash = text def __call__(self, context, request): return self.val.providedBy(request) @@ -163,18 +152,15 @@ class MatchParamPredicate(object): val = (val,) val = sorted(val) self.val = val - self.reqs = [ - (x.strip(), y.strip()) for x, y in [ p.split('=', 1) for p in val ] - ] + reqs = [ p.split('=', 1) for p in val ] + self.reqs = [ (x.strip(), y.strip()) for x, y in reqs ] - def __text__(self): - return 'match_param %s' % (self.val,) + def text(self): + return 'match_param %s' % ','.join( + ['%s=%s' % (x,y) for x, y in self.reqs] + ) - def __phash__(self): - L = [] - for k, v in self.reqs: - L.append('match_param:%r=%r' % (k, v)) - return L + phash = text def __call__(self, context, request): for k, v in self.reqs: @@ -186,10 +172,10 @@ class CustomPredicate(object): def __init__(self, func): self.func = func - def __text__(self): + def text(self): return getattr(self.func, '__text__', repr(self.func)) - def __phash__(self): + def phash(self): return 'custom:%r' % hash(self.func) def __call__(self, context, request): @@ -201,10 +187,10 @@ class TraversePredicate(object): _, self.tgenerate = _compile_route(val) self.val = val - def __text__(self): + def text(self): return 'traverse matchdict pseudo-predicate' - def __phash__(self): + def phash(self): return '' def __call__(self, context, request): @@ -214,5 +200,3 @@ class TraversePredicate(object): tvalue = self.tgenerate(m) m['traverse'] = traversal_path(tvalue) return True - - diff --git a/pyramid/config/util.py b/pyramid/config/util.py index 1574d7da6..da3766deb 100644 --- a/pyramid/config/util.py +++ b/pyramid/config/util.py @@ -468,7 +468,7 @@ class PredicateList(object): vals = (vals,) for val in vals: predicate = predicate_factory(val) - hashes = predicate.__phash__() + hashes = predicate.phash() if not is_nonstr_iter(hashes): hashes = [hashes] for h in hashes: diff --git a/pyramid/config/views.py b/pyramid/config/views.py index b59d18400..f2fe83673 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -285,7 +285,7 @@ class ViewDeriver(object): view_name = getattr(view, '__name__', view) raise PredicateMismatch( 'predicate mismatch for view %s (%s)' % ( - view_name, predicate.__text__)) + view_name, predicate.text())) return view(context, request) def checker(context, request): return all((predicate(context, request) for predicate in diff --git a/pyramid/scripts/pviews.py b/pyramid/scripts/pviews.py index 72a9800c3..a9db59dc1 100644 --- a/pyramid/scripts/pviews.py +++ b/pyramid/scripts/pviews.py @@ -17,11 +17,11 @@ class PViewsCommand(object): each route+predicate set, print each view that might match and its predicates. - This command accepts two positional arguments: "config_uri" specifies the + This command accepts two positional arguments: 'config_uri' specifies the PasteDeploy config file to use for the interactive shell. The format is - "inifile#name". If the name is left off, "main" will be assumed. "url" + 'inifile#name'. If the name is left off, 'main' will be assumed. 'url' specifies the path info portion of a URL that will be used to find - matching views. Example: "proutes myapp.ini#main /url" + matching views. Example: 'proutes myapp.ini#main /url' """ stdout = sys.stdout @@ -223,7 +223,7 @@ class PViewsCommand(object): self.out("%srequired permission = %s" % (indent, permission)) predicates = getattr(view_wrapper, '__predicates__', None) if predicates is not None: - predicate_text = ', '.join([p.__text__ for p in predicates]) + predicate_text = ', '.join([p.text() for p in predicates]) self.out("%sview predicates (%s)" % (indent, predicate_text)) def run(self): diff --git a/pyramid/tests/test_config/test_predicates.py b/pyramid/tests/test_config/test_predicates.py index 22d992404..8a9da7a41 100644 --- a/pyramid/tests/test_config/test_predicates.py +++ b/pyramid/tests/test_config/test_predicates.py @@ -21,13 +21,13 @@ class TestXHRPredicate(unittest.TestCase): result = inst(None, request) self.assertFalse(result) - def test___text__(self): + def test_text(self): inst = self._makeOne(True) - self.assertEqual(inst.__text__(), 'xhr = True') + self.assertEqual(inst.text(), 'xhr = True') - def test___phash__(self): + def test_phash(self): inst = self._makeOne(True) - self.assertEqual(inst.__phash__(), 'xhr:True') + self.assertEqual(inst.phash(), 'xhr = True') class TestRequestMethodPredicate(unittest.TestCase): def _makeOne(self, val): @@ -55,14 +55,13 @@ class TestRequestMethodPredicate(unittest.TestCase): result = inst(None, request) self.assertFalse(result) - def test___text__(self): + def test_text(self): inst = self._makeOne(('HEAD','GET')) - self.assertEqual(inst.__text__(), 'request method = GET,HEAD') + self.assertEqual(inst.text(), 'request_method = GET,HEAD') - def test___phash__(self): + def test_phash(self): inst = self._makeOne(('HEAD','GET')) - self.assertEqual(inst.__phash__(), ['request_method:GET', - 'request_method:HEAD']) + self.assertEqual(inst.phash(), 'request_method = GET,HEAD') class TestPathInfoPredicate(unittest.TestCase): def _makeOne(self, val): @@ -87,13 +86,13 @@ class TestPathInfoPredicate(unittest.TestCase): result = inst(None, request) self.assertFalse(result) - def test___text__(self): + def test_text(self): inst = self._makeOne('/') - self.assertEqual(inst.__text__(), 'path_info = /') + self.assertEqual(inst.text(), 'path_info = /') - def test___phash__(self): + def test_phash(self): inst = self._makeOne('/') - self.assertEqual(inst.__phash__(), 'path_info:/') + self.assertEqual(inst.phash(), 'path_info = /') class TestRequestParamPredicate(unittest.TestCase): def _makeOne(self, val): @@ -121,22 +120,144 @@ class TestRequestParamPredicate(unittest.TestCase): result = inst(None, request) self.assertFalse(result) - def test___text__exists(self): + def test_text_exists(self): inst = self._makeOne('abc') - self.assertEqual(inst.__text__(), 'request_param abc') + self.assertEqual(inst.text(), 'request_param abc') - def test___text__withval(self): + def test_text_withval(self): inst = self._makeOne('abc= 1') - self.assertEqual(inst.__text__(), 'request_param abc = 1') + self.assertEqual(inst.text(), 'request_param abc = 1') - def test___phash__exists(self): + def test_phash_exists(self): inst = self._makeOne('abc') - self.assertEqual(inst.__phash__(), 'request_param:abc=None') + self.assertEqual(inst.phash(), 'request_param abc') - def test___phash__withval(self): + def test_phash_withval(self): inst = self._makeOne('abc= 1') - self.assertEqual(inst.__phash__(), "request_param:abc='1'") + self.assertEqual(inst.phash(), "request_param abc = 1") +class TestMatchParamPredicate(unittest.TestCase): + def _makeOne(self, val): + from pyramid.config.predicates import MatchParamPredicate + return MatchParamPredicate(val) + + def test___call___true_single(self): + inst = self._makeOne('abc=1') + request = Dummy() + request.matchdict = {'abc':'1'} + result = inst(None, request) + self.assertTrue(result) + + + def test___call___true_multi(self): + inst = self._makeOne(('abc=1', 'def=2')) + request = Dummy() + request.matchdict = {'abc':'1', 'def':'2'} + result = inst(None, request) + self.assertTrue(result) + + def test___call___false(self): + inst = self._makeOne('abc=1') + request = Dummy() + request.matchdict = {} + result = inst(None, request) + self.assertFalse(result) + + def test_text(self): + inst = self._makeOne(('def= 1', 'abc =2')) + self.assertEqual(inst.text(), 'match_param abc=2,def=1') + + def test_phash(self): + inst = self._makeOne(('def= 1', 'abc =2')) + self.assertEqual(inst.phash(), 'match_param abc=2,def=1') + +class TestCustomPredicate(unittest.TestCase): + def _makeOne(self, val): + from pyramid.config.predicates import CustomPredicate + return CustomPredicate(val) + + def test___call___true(self): + def func(context, request): + self.assertEqual(context, None) + self.assertEqual(request, None) + return True + inst = self._makeOne(func) + result = inst(None, None) + self.assertTrue(result) + + def test___call___false(self): + def func(context, request): + self.assertEqual(context, None) + self.assertEqual(request, None) + return False + inst = self._makeOne(func) + result = inst(None, None) + self.assertFalse(result) + + def test_text_func_has___text__(self): + pred = predicate() + pred.__text__ = 'text' + inst = self._makeOne(pred) + self.assertEqual(inst.text(), 'text') + + def test_text_func_repr(self): + pred = predicate() + inst = self._makeOne(pred) + self.assertTrue(inst.text(), 'predicate') + + def test_phash(self): + pred = predicate() + inst = self._makeOne(pred) + self.assertTrue(inst.phash(), 'custom:1') + +class TestTraversePredicate(unittest.TestCase): + def _makeOne(self, val): + from pyramid.config.predicates import TraversePredicate + return TraversePredicate(val) + + def test___call__traverse_has_remainder_already(self): + inst = self._makeOne('/1/:a/:b') + info = {'traverse':'abc'} + request = Dummy() + result = inst(info, request) + self.assertEqual(result, True) + self.assertEqual(info, {'traverse':'abc'}) + + def test___call__traverse_matches(self): + inst = self._makeOne('/1/:a/:b') + info = {'match':{'a':'a', 'b':'b'}} + request = Dummy() + result = inst(info, request) + self.assertEqual(result, True) + self.assertEqual(info, {'match': + {'a':'a', 'b':'b', 'traverse':('1', 'a', 'b')}}) + + def test___call__traverse_matches_with_highorder_chars(self): + inst = self._makeOne(text_(b'/La Pe\xc3\xb1a/{x}', 'utf-8')) + info = {'match':{'x':text_(b'Qu\xc3\xa9bec', 'utf-8')}} + request = Dummy() + result = inst(info, request) + self.assertEqual(result, True) + self.assertEqual( + info['match']['traverse'], + (text_(b'La Pe\xc3\xb1a', 'utf-8'), + text_(b'Qu\xc3\xa9bec', 'utf-8')) + ) + + def test_text(self): + inst = self._makeOne('/abc') + self.assertEqual(inst.text(), 'traverse matchdict pseudo-predicate') + + def test_phash(self): + inst = self._makeOne('/abc') + self.assertEqual(inst.phash(), '') + +class predicate(object): + def __repr__(self): + return 'predicate' + def __hash__(self): + return 1 + class Dummy(object): def __init__(self, **kw): self.__dict__.update(**kw) diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index ea8883478..9ff83e956 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -400,7 +400,7 @@ class TestViewsConfigurationMixin(unittest.TestCase): from pyramid.interfaces import IViewClassifier from pyramid.interfaces import IMultiView phash = md5() - phash.update(b'xhr:True') + phash.update(b'xhr = True') view = lambda *arg: 'NOT OK' view.__phash__ = phash.hexdigest() config = self._makeOne(autocommit=True) @@ -424,7 +424,7 @@ class TestViewsConfigurationMixin(unittest.TestCase): from pyramid.interfaces import IExceptionViewClassifier from pyramid.interfaces import IMultiView phash = md5() - phash.update(b'xhr:True') + phash.update(b'xhr = True') view = lambda *arg: 'NOT OK' view.__phash__ = phash.hexdigest() config = self._makeOne(autocommit=True) @@ -2905,7 +2905,7 @@ class TestViewDeriver(unittest.TestCase): view = lambda *arg: response def predicate1(context, request): return False - predicate1.__text__ = 'text' + predicate1.text = lambda *arg: 'text' deriver = self._makeOne(predicates=[predicate1]) result = deriver(view) request = self._makeRequest() @@ -2923,7 +2923,7 @@ class TestViewDeriver(unittest.TestCase): def myview(request): pass def predicate1(context, request): return False - predicate1.__text__ = 'text' + predicate1.text = lambda *arg: 'text' deriver = self._makeOne(predicates=[predicate1]) result = deriver(myview) request = self._makeRequest() @@ -2941,10 +2941,10 @@ class TestViewDeriver(unittest.TestCase): def myview(request): pass def predicate1(context, request): return True - predicate1.__text__ = 'pred1' + predicate1.text = lambda *arg: 'pred1' def predicate2(context, request): return False - predicate2.__text__ = 'pred2' + predicate2.text = lambda *arg: 'pred2' deriver = self._makeOne(predicates=[predicate1, predicate2]) result = deriver(myview) request = self._makeRequest() @@ -2999,11 +2999,11 @@ class TestViewDeriver(unittest.TestCase): def predicate1(context, request): predicates.append(True) return True - predicate1.__text__ = 'text' + predicate1.text = lambda *arg: 'text' def predicate2(context, request): predicates.append(True) return False - predicate2.__text__ = 'text' + predicate2.text = lambda *arg: 'text' deriver = self._makeOne(predicates=[predicate1, predicate2]) result = deriver(view) request = self._makeRequest() diff --git a/pyramid/tests/test_scripts/test_pviews.py b/pyramid/tests/test_scripts/test_pviews.py index 680d48cee..6a919c31b 100644 --- a/pyramid/tests/test_scripts/test_pviews.py +++ b/pyramid/tests/test_scripts/test_pviews.py @@ -309,7 +309,7 @@ class TestPViewsCommand(unittest.TestCase): L = [] command.out = L.append def predicate(): pass - predicate.__text__ = "predicate = x" + predicate.text = lambda *arg: "predicate = x" view = dummy.DummyView(context='context', view_name='a') view.__predicates__ = [predicate] command._find_view = lambda arg1, arg2: view @@ -448,7 +448,7 @@ class TestPViewsCommand(unittest.TestCase): L = [] command.out = L.append def predicate(): pass - predicate.__text__ = "predicate = x" + predicate.text = lambda *arg: "predicate = x" view = dummy.DummyView(context='context') view.__name__ = 'view' view.__view_attr__ = 'call' -- cgit v1.2.3 From 36428038ecb47083ca256d6e0ec45c9bad0bc773 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 4 Aug 2012 00:36:22 -0400 Subject: unhose triplequote rendering in emacs --- pyramid/scripts/proutes.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyramid/scripts/proutes.py b/pyramid/scripts/proutes.py index 29ec9e72a..f64107d2b 100644 --- a/pyramid/scripts/proutes.py +++ b/pyramid/scripts/proutes.py @@ -15,10 +15,10 @@ class PRoutesCommand(object): route, the pattern of the route, and the view callable which will be invoked when the route is matched. - This command accepts one positional argument named "config_uri". It + This command accepts one positional argument named 'config_uri'. It specifies the PasteDeploy config file to use for the interactive - shell. The format is "inifile#name". If the name is left off, "main" - will be assumed. Example: "proutes myapp.ini". + shell. The format is 'inifile#name'. If the name is left off, 'main' + will be assumed. Example: 'proutes myapp.ini'. """ bootstrap = (bootstrap,) -- cgit v1.2.3 From 0f372cd870caf36dfea59319df2ca26a92a74dc9 Mon Sep 17 00:00:00 2001 From: Daniel Haaker Date: Sat, 4 Aug 2012 21:54:22 +0200 Subject: fix two minor typos in docs --- docs/tutorials/wiki2/basiclayout.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorials/wiki2/basiclayout.rst b/docs/tutorials/wiki2/basiclayout.rst index 5f4ea671c..b3184c4fc 100644 --- a/docs/tutorials/wiki2/basiclayout.rst +++ b/docs/tutorials/wiki2/basiclayout.rst @@ -100,7 +100,7 @@ used when the URL is ``/``: :language: py Since this route has a ``pattern`` equalling ``/`` it is the route that will -be matched when the URL ``/`` is visted, e.g. ``http://localhost:6543/``. +be matched when the URL ``/`` is visited, e.g. ``http://localhost:6543/``. ``main`` next calls the ``scan`` method of the configurator, which will recursively scan our ``tutorial`` package, looking for ``@view_config`` (and @@ -190,7 +190,7 @@ Next we set up a SQLAlchemy "DBSession" object: ``scoped_session`` allows us to access our database connection globally. ``sessionmaker`` creates a database session object. We pass to ``sessionmaker`` the ``extension=ZopeTransactionExtension()`` extension -option in order to allow the system to automatically manage datbase +option in order to allow the system to automatically manage database transactions. With ``ZopeTransactionExtension`` activated, our application will automatically issue a transaction commit after every request unless an exception is raised, in which case the transaction will be aborted. -- cgit v1.2.3 From 1d627da85d8b7603b28f276be482f9cb4ec79dae Mon Sep 17 00:00:00 2001 From: Daniel Haaker Date: Sat, 4 Aug 2012 21:54:44 +0200 Subject: add quotes for clarity --- docs/tutorials/wiki/definingviews.rst | 2 +- docs/tutorials/wiki2/definingviews.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorials/wiki/definingviews.rst b/docs/tutorials/wiki/definingviews.rst index 28cecb787..529603546 100644 --- a/docs/tutorials/wiki/definingviews.rst +++ b/docs/tutorials/wiki/definingviews.rst @@ -251,7 +251,7 @@ wiki page. It includes: - A ``div`` element that is replaced with the ``content`` value provided by the view (rows 45-47). ``content`` contains HTML, so the ``structure`` keyword is used - to prevent escaping it (i.e. changing ">" to >, etc.) + to prevent escaping it (i.e. changing ">" to ">", etc.) - A link that points at the "edit" URL which invokes the ``edit_page`` view for the page being viewed (rows 49-51). diff --git a/docs/tutorials/wiki2/definingviews.rst b/docs/tutorials/wiki2/definingviews.rst index efb72230e..24ac4338d 100644 --- a/docs/tutorials/wiki2/definingviews.rst +++ b/docs/tutorials/wiki2/definingviews.rst @@ -248,7 +248,7 @@ wiki page. It includes: - A ``div`` element that is replaced with the ``content`` value provided by the view (rows 45-47). ``content`` contains HTML, so the ``structure`` keyword is used - to prevent escaping it (i.e. changing ">" to >, etc.) + to prevent escaping it (i.e. changing ">" to ">", etc.) - A link that points at the "edit" URL which invokes the ``edit_page`` view for the page being viewed (rows 49-51). -- cgit v1.2.3 From 9c8ec5c7b7f12abb741f9d4467bc85f15b893420 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 5 Aug 2012 17:28:01 -0400 Subject: use a predicate list for routes, introduce the concept of deferred discriminators, change conflict resolution to deal with deferred discriminators, make predicates take a config argument, get rid of legacy make_predicates function --- pyramid/config/__init__.py | 159 ++++++++----- pyramid/config/predicates.py | 50 ++-- pyramid/config/routes.py | 101 +++++++-- pyramid/config/util.py | 328 ++++++--------------------- pyramid/config/views.py | 208 ++++++++++------- pyramid/testing.py | 2 + pyramid/tests/test_config/test_init.py | 41 ++-- pyramid/tests/test_config/test_predicates.py | 18 +- pyramid/tests/test_config/test_routes.py | 2 +- pyramid/tests/test_config/test_util.py | 95 +++++--- pyramid/tests/test_config/test_views.py | 9 +- pyramid/tests/test_testing.py | 3 + 12 files changed, 526 insertions(+), 490 deletions(-) diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index 5eb860ed5..2fca7a162 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -1,4 +1,5 @@ import inspect +import itertools import logging import operator import os @@ -71,6 +72,7 @@ from pyramid.config.tweens import TweensConfiguratorMixin from pyramid.config.util import ( action_method, ActionInfo, + Deferred, ) from pyramid.config.views import ViewsConfiguratorMixin from pyramid.config.zca import ZCAConfiguratorMixin @@ -354,6 +356,7 @@ class Configurator( self.add_renderer(name, renderer) self.add_default_view_predicates() + self.add_default_route_predicates() if exceptionresponse_view is not None: exceptionresponse_view = self.maybe_dotted(exceptionresponse_view) @@ -549,6 +552,10 @@ class Configurator( introspectables = () if autocommit: + if isinstance(discriminator, Deferred): + # callables can depend on the side effects of resolving a + # deferred discriminator + discriminator.resolve() if callable is not None: callable(*args, **kw) for introspectable in introspectables: @@ -1060,6 +1067,11 @@ class ActionState(object): if clear: del self.actions[:] +def undefer(v): + if isinstance(v, Deferred): + v = v.resolve() + return v + # this function is licensed under the ZPL (stolen from Zope) def resolveConflicts(actions): """Resolve conflicting actions @@ -1072,73 +1084,94 @@ def resolveConflicts(actions): other conflicting actions. """ - # organize actions by discriminators - unique = {} - output = [] - for i, action in enumerate(actions): - if not isinstance(action, dict): + def orderandpos((n, v)): + if not isinstance(v, dict): # old-style tuple action - action = expand_action(*action) + v = expand_action(*v) + return (v['order'] or 0, n) + + sactions = sorted(enumerate(actions), key=orderandpos) + def orderonly((n,v)): + if not isinstance(v, dict): + # old-style tuple action + v = expand_action(*v) + return v['order'] or 0 + + for order, actiongroup in itertools.groupby(sactions, orderonly): # "order" is an integer grouping. Actions in a lower order will be - # executed before actions in a higher order. Within an order, - # actions are executed sequentially based on original action ordering - # ("i"). - order = action['order'] or 0 - discriminator = action['discriminator'] - - # "ainfo" is a tuple of (order, i, action) where "order" is a - # user-supplied grouping, "i" is an integer expressing the relative - # position of this action in the action list being resolved, and - # "action" is an action dictionary. The purpose of an ainfo is to - # associate an "order" and an "i" with a particular action; "order" - # and "i" exist for sorting purposes after conflict resolution. - ainfo = (order, i, action) - - if discriminator is None: - # The discriminator is None, so this action can never conflict. - # We can add it directly to the result. + # executed before actions in a higher order. All of the actions in + # one grouping will be executed (its callable, if any will be called) + # before any of the actions in the next. + + unique = {} + output = [] + + for i, action in actiongroup: + # Within an order, actions are executed sequentially based on + # original action ordering ("i"). + + if not isinstance(action, dict): + # old-style tuple action + action = expand_action(*action) + + # "ainfo" is a tuple of (order, i, action) where "order" is a + # user-supplied grouping, "i" is an integer expressing the relative + # position of this action in the action list being resolved, and + # "action" is an action dictionary. The purpose of an ainfo is to + # associate an "order" and an "i" with a particular action; "order" + # and "i" exist for sorting purposes after conflict resolution. + ainfo = (order, i, action) + + discriminator = undefer(action['discriminator']) + action['discriminator'] = discriminator + + if discriminator is None: + # The discriminator is None, so this action can never conflict. + # We can add it directly to the result. + output.append(ainfo) + continue + + L = unique.setdefault(discriminator, []) + L.append(ainfo) + + # Check for conflicts + conflicts = {} + + for discriminator, ainfos in unique.items(): + # We use (includepath, order, i) as a sort key because we need to + # sort the actions by the paths so that the shortest path with a + # given prefix comes first. The "first" action is the one with the + # shortest include path. We break sorting ties using "order", then + # "i". + def bypath(ainfo): + path, order, i = ainfo[2]['includepath'], ainfo[0], ainfo[1] + return path, order, i + + ainfos.sort(key=bypath) + ainfo, rest = ainfos[0], ainfos[1:] output.append(ainfo) - continue - - L = unique.setdefault(discriminator, []) - L.append(ainfo) - - # Check for conflicts - conflicts = {} - - for discriminator, ainfos in unique.items(): - - # We use (includepath, order, i) as a sort key because we need to - # sort the actions by the paths so that the shortest path with a - # given prefix comes first. The "first" action is the one with the - # shortest include path. We break sorting ties using "order", then - # "i". - def bypath(ainfo): - path, order, i = ainfo[2]['includepath'], ainfo[0], ainfo[1] - return path, order, i - - ainfos.sort(key=bypath) - ainfo, rest = ainfos[0], ainfos[1:] - output.append(ainfo) - _, _, action = ainfo - basepath, baseinfo, discriminator = (action['includepath'], - action['info'], - action['discriminator']) - - for _, _, action in rest: - includepath = action['includepath'] - # Test whether path is a prefix of opath - if (includepath[:len(basepath)] != basepath # not a prefix - or includepath == basepath): - L = conflicts.setdefault(discriminator, [baseinfo]) - L.append(action['info']) - - if conflicts: - raise ConfigurationConflictError(conflicts) - - # sort conflict-resolved actions by (order, i) and return them - return [ x[2] for x in sorted(output, key=operator.itemgetter(0, 1))] + _, _, action = ainfo + basepath, baseinfo, discriminator = ( + action['includepath'], + action['info'], + action['discriminator'], + ) + + for _, _, action in rest: + includepath = action['includepath'] + # Test whether path is a prefix of opath + if (includepath[:len(basepath)] != basepath # not a prefix + or includepath == basepath): + L = conflicts.setdefault(discriminator, [baseinfo]) + L.append(action['info']) + + if conflicts: + raise ConfigurationConflictError(conflicts) + + # sort conflict-resolved actions by (order, i) and yield them one by one + for a in [x[2] for x in sorted(output, key=operator.itemgetter(0, 1))]: + yield a def expand_action(discriminator, callable=None, args=(), kw=None, includepath=(), info=None, order=0, introspectables=()): diff --git a/pyramid/config/predicates.py b/pyramid/config/predicates.py index 37f75462d..311d47860 100644 --- a/pyramid/config/predicates.py +++ b/pyramid/config/predicates.py @@ -11,10 +11,12 @@ from pyramid.traversal import ( from pyramid.urldispatch import _compile_route +from pyramid.util import object_description + from .util import as_sorted_tuple class XHRPredicate(object): - def __init__(self, val): + def __init__(self, val, config): self.val = bool(val) def text(self): @@ -26,7 +28,7 @@ class XHRPredicate(object): return bool(request.is_xhr) is self.val class RequestMethodPredicate(object): - def __init__(self, val): + def __init__(self, val, config): self.val = as_sorted_tuple(val) def text(self): @@ -38,7 +40,7 @@ class RequestMethodPredicate(object): return request.method in self.val class PathInfoPredicate(object): - def __init__(self, val): + def __init__(self, val, config): self.orig = val try: val = re.compile(val) @@ -55,7 +57,7 @@ class PathInfoPredicate(object): return self.val.match(request.upath_info) is not None class RequestParamPredicate(object): - def __init__(self, val): + def __init__(self, val, config): name = val v = None if '=' in name: @@ -80,7 +82,7 @@ class RequestParamPredicate(object): class HeaderPredicate(object): - def __init__(self, val): + def __init__(self, val, config): name = val v = None if ':' in name: @@ -110,7 +112,7 @@ class HeaderPredicate(object): return self.val.match(val) is not None class AcceptPredicate(object): - def __init__(self, val): + def __init__(self, val, config): self.val = val def text(self): @@ -122,8 +124,8 @@ class AcceptPredicate(object): return self.val in request.accept class ContainmentPredicate(object): - def __init__(self, val): - self.val = val + def __init__(self, val, config): + self.val = config.maybe_dotted(val) def text(self): return 'containment = %s' % (self.val,) @@ -135,7 +137,7 @@ class ContainmentPredicate(object): return find_interface(ctx, self.val) is not None class RequestTypePredicate(object): - def __init__(self, val): + def __init__(self, val, config): self.val = val def text(self): @@ -147,7 +149,7 @@ class RequestTypePredicate(object): return self.val.providedBy(request) class MatchParamPredicate(object): - def __init__(self, val): + def __init__(self, val, config): if not is_nonstr_iter(val): val = (val,) val = sorted(val) @@ -169,13 +171,23 @@ class MatchParamPredicate(object): return True class CustomPredicate(object): - def __init__(self, func): + def __init__(self, func, config): self.func = func def text(self): - return getattr(self.func, '__text__', repr(self.func)) + return getattr( + self.func, + '__text__', + 'custom predicate: %s' % object_description(self.func) + ) def phash(self): + # using hash() here rather than id() is intentional: we + # want to allow custom predicates that are part of + # frameworks to be able to define custom __hash__ + # functions for custom predicates, so that the hash output + # of predicate instances which are "logically the same" + # may compare equal. return 'custom:%r' % hash(self.func) def __call__(self, context, request): @@ -183,7 +195,11 @@ class CustomPredicate(object): class TraversePredicate(object): - def __init__(self, val): + # Can only be used as a *route* "predicate"; it adds 'traverse' to the + # matchdict if it's specified in the routing args. This causes the + # ResourceTreeTraverser to use the resolved traverse pattern as the + # traversal path. + def __init__(self, val, config): _, self.tgenerate = _compile_route(val) self.val = val @@ -191,12 +207,18 @@ class TraversePredicate(object): return 'traverse matchdict pseudo-predicate' def phash(self): + # This isn't actually a predicate, it's just a infodict modifier that + # injects ``traverse`` into the matchdict. As a result, we don't + # need to update the hash. return '' def __call__(self, context, request): if 'traverse' in context: return True m = context['match'] - tvalue = self.tgenerate(m) + tvalue = self.tgenerate(m) # tvalue will be urlquoted string m['traverse'] = traversal_path(tvalue) + # This isn't actually a predicate, it's just a infodict modifier that + # injects ``traverse`` into the matchdict. As a result, we just + # return True. return True diff --git a/pyramid/config/routes.py b/pyramid/config/routes.py index ea39b6805..ff285569d 100644 --- a/pyramid/config/routes.py +++ b/pyramid/config/routes.py @@ -1,9 +1,11 @@ import warnings from pyramid.interfaces import ( + IPredicateList, IRequest, IRouteRequest, IRoutesMapper, + PHASE1_CONFIG, PHASE2_CONFIG, ) @@ -13,10 +15,13 @@ from pyramid.urldispatch import RoutesMapper from pyramid.config.util import ( action_method, - make_predicates, as_sorted_tuple, + PredicateList, + predvalseq, ) +from pyramid.config import predicates + class RoutesConfiguratorMixin(object): @action_method def add_route(self, @@ -28,7 +33,7 @@ class RoutesConfiguratorMixin(object): factory=None, for_=None, header=None, - xhr=False, + xhr=None, accept=None, path_info=None, request_method=None, @@ -44,7 +49,7 @@ class RoutesConfiguratorMixin(object): path=None, pregenerator=None, static=False, - ): + **other_predicates): """ Add a :term:`route configuration` to the current configuration state, as well as possibly a :term:`view configuration` to be used to specify a :term:`view callable` @@ -254,6 +259,14 @@ class RoutesConfiguratorMixin(object): :ref:`custom_route_predicates` for more information about ``info``. + other_predicates + + Pass a key/value pair here to use a third-party predicate registered + via :meth:`pyramid.config.Configurator.add_view_predicate`. More + than one key/value pair can be used at the same time. See + :ref:`registering_thirdparty_predicates` for more information + about third-party predicates. + View-Related Arguments .. warning:: @@ -351,17 +364,6 @@ class RoutesConfiguratorMixin(object): if request_method is not None: request_method = as_sorted_tuple(request_method) - ignored, predicates, ignored = make_predicates( - xhr=xhr, - request_method=request_method, - path_info=path_info, - request_param=request_param, - header=header, - accept=accept, - traverse=traverse, - custom=custom_predicates - ) - factory = self.maybe_dotted(factory) if pattern is None: pattern = path @@ -417,8 +419,24 @@ class RoutesConfiguratorMixin(object): request_iface, IRouteRequest, name=name) def register_connect(): + pvals = other_predicates + pvals.update( + dict( + xhr=xhr, + request_method=request_method, + path_info=path_info, + request_param=request_param, + header=header, + accept=accept, + traverse=traverse, + custom=predvalseq(custom_predicates), + ) + ) + + predlist = self.route_predlist + _, preds, _ = predlist.make(self, **pvals) route = mapper.connect( - name, pattern, factory, predicates=predicates, + name, pattern, factory, predicates=preds, pregenerator=pregenerator, static=static ) intr['object'] = route @@ -447,6 +465,59 @@ class RoutesConfiguratorMixin(object): attr=view_attr, ) + @property + def route_predlist(self): + predlist = self.registry.queryUtility(IPredicateList, name='route') + if predlist is None: + predlist = PredicateList() + self.registry.registerUtility(predlist, IPredicateList, + name='route') + return predlist + + @action_method + def add_route_predicate(self, name, factory, weighs_more_than=None, + weighs_less_than=None): + """ Adds a route predicate factory. The view predicate can later be + named as a keyword argument to + :meth:`pyramid.config.Configurator.add_route`. + + ``name`` should be the name of the predicate. It must be a valid + Python identifier (it will be used as a keyword argument to + ``add_view``). + + ``factory`` should be a :term:`predicate factory`. + """ + discriminator = ('route predicate', name) + intr = self.introspectable( + 'route predicates', + discriminator, + 'route predicate named %s' % name, + 'route predicate') + intr['name'] = name + intr['factory'] = factory + intr['weighs_more_than'] = weighs_more_than + intr['weighs_less_than'] = weighs_less_than + def register(): + predlist = self.route_predlist + predlist.add(name, factory, weighs_more_than=weighs_more_than, + weighs_less_than=weighs_less_than) + # must be registered before routes connected + self.action(discriminator, register, introspectables=(intr,), + order=PHASE1_CONFIG) + + def add_default_route_predicates(self): + for (name, factory) in ( + ('xhr', predicates.XHRPredicate), + ('request_method', predicates.RequestMethodPredicate), + ('path_info', predicates.PathInfoPredicate), + ('request_param', predicates.RequestParamPredicate), + ('header', predicates.HeaderPredicate), + ('accept', predicates.AcceptPredicate), + ('custom', predicates.CustomPredicate), + ('traverse', predicates.TraversePredicate), + ): + self.add_route_predicate(name, factory) + def get_routes_mapper(self): """ Return the :term:`routes mapper` object associated with this configurator's :term:`registry`.""" diff --git a/pyramid/config/util.py b/pyramid/config/util.py index da3766deb..ce4a4a728 100644 --- a/pyramid/config/util.py +++ b/pyramid/config/util.py @@ -1,4 +1,3 @@ -import re import traceback from zope.interface import implementer @@ -12,11 +11,6 @@ from pyramid.compat import ( from pyramid.exceptions import ConfigurationError -from pyramid.traversal import ( - find_interface, - traversal_path, - ) - from hashlib import md5 MAX_ORDER = 1 << 30 @@ -64,236 +58,6 @@ def action_method(wrapped): wrapper.__docobj__ = wrapped # for sphinx return wrapper -def make_predicates(xhr=None, request_method=None, path_info=None, - request_param=None, match_param=None, header=None, - accept=None, containment=None, request_type=None, - traverse=None, custom=()): - - # PREDICATES - # ---------- - # - # Given an argument list, a predicate list is computed. - # Predicates are added to a predicate list in (presumed) - # computation expense order. All predicates associated with a - # view or route must evaluate true for the view or route to - # "match" during a request. Elsewhere in the code, we evaluate - # predicates using a generator expression. The fastest predicate - # should be evaluated first, then the next fastest, and so on, as - # if one returns false, the remainder of the predicates won't need - # to be evaluated. - # - # While we compute predicates, we also compute a predicate hash - # (aka phash) that can be used by a caller to identify identical - # predicate lists. - # - # ORDERING - # -------- - # - # A "order" is computed for the predicate list. An order is - # a scoring. - # - # Each predicate is associated with a weight value, which is a - # multiple of 2. The weight of a predicate symbolizes the - # relative potential "importance" of the predicate to all other - # predicates. A larger weight indicates greater importance. - # - # All weights for a given predicate list are bitwise ORed together - # to create a "score"; this score is then subtracted from - # MAX_ORDER and divided by an integer representing the number of - # predicates+1 to determine the order. - # - # The order represents the ordering in which a "multiview" ( a - # collection of views that share the same context/request/name - # triad but differ in other ways via predicates) will attempt to - # call its set of views. Views with lower orders will be tried - # first. The intent is to a) ensure that views with more - # predicates are always evaluated before views with fewer - # predicates and b) to ensure a stable call ordering of views that - # share the same number of predicates. Views which do not have - # any predicates get an order of MAX_ORDER, meaning that they will - # be tried very last. - - # NB: each predicate callable constructed by this function (or examined - # by this function, in the case of custom predicates) must leave this - # function with a ``__text__`` attribute. The subsystem which reports - # errors when no predicates match depends upon the existence of this - # attribute on each predicate callable. - - predicates = [] - weights = [] - h = md5() - - if xhr: - def xhr_predicate(context, request): - return request.is_xhr - xhr_predicate.__text__ = "xhr = True" - weights.append(1 << 1) - predicates.append(xhr_predicate) - h.update(bytes_('xhr:%r' % bool(xhr))) - - if request_method is not None: - if not is_nonstr_iter(request_method): - request_method = (request_method,) - request_method = sorted(request_method) - def request_method_predicate(context, request): - return request.method in request_method - text = "request method = %r" % request_method - request_method_predicate.__text__ = text - weights.append(1 << 2) - predicates.append(request_method_predicate) - for m in request_method: - h.update(bytes_('request_method:%r' % m)) - - if path_info is not None: - try: - path_info_val = re.compile(path_info) - except re.error as why: - raise ConfigurationError(why.args[0]) - def path_info_predicate(context, request): - return path_info_val.match(request.upath_info) is not None - text = "path_info = %s" - path_info_predicate.__text__ = text % path_info - weights.append(1 << 3) - predicates.append(path_info_predicate) - h.update(bytes_('path_info:%r' % path_info)) - - if request_param is not None: - request_param_val = None - if '=' in request_param: - request_param, request_param_val = request_param.split('=', 1) - if request_param_val is None: - text = "request_param %s" % request_param - else: - text = "request_param %s = %s" % (request_param, request_param_val) - def request_param_predicate(context, request): - if request_param_val is None: - return request_param in request.params - return request.params.get(request_param) == request_param_val - request_param_predicate.__text__ = text - weights.append(1 << 4) - predicates.append(request_param_predicate) - h.update( - bytes_('request_param:%r=%r' % (request_param, request_param_val))) - - if header is not None: - header_name = header - header_val = None - if ':' in header: - header_name, header_val = header.split(':', 1) - try: - header_val = re.compile(header_val) - except re.error as why: - raise ConfigurationError(why.args[0]) - if header_val is None: - text = "header %s" % header_name - else: - text = "header %s = %s" % (header_name, header_val) - def header_predicate(context, request): - if header_val is None: - return header_name in request.headers - val = request.headers.get(header_name) - if val is None: - return False - return header_val.match(val) is not None - header_predicate.__text__ = text - weights.append(1 << 5) - predicates.append(header_predicate) - h.update(bytes_('header:%r=%r' % (header_name, header_val))) - - if accept is not None: - def accept_predicate(context, request): - return accept in request.accept - accept_predicate.__text__ = "accept = %s" % accept - weights.append(1 << 6) - predicates.append(accept_predicate) - h.update(bytes_('accept:%r' % accept)) - - if containment is not None: - def containment_predicate(context, request): - ctx = getattr(request, 'context', context) - return find_interface(ctx, containment) is not None - containment_predicate.__text__ = "containment = %s" % containment - weights.append(1 << 7) - predicates.append(containment_predicate) - h.update(bytes_('containment:%r' % hash(containment))) - - if request_type is not None: - def request_type_predicate(context, request): - return request_type.providedBy(request) - text = "request_type = %s" - request_type_predicate.__text__ = text % request_type - weights.append(1 << 8) - predicates.append(request_type_predicate) - h.update(bytes_('request_type:%r' % hash(request_type))) - - if match_param is not None: - if not is_nonstr_iter(match_param): - match_param = (match_param,) - match_param = sorted(match_param) - text = "match_param %s" % repr(match_param) - reqs = [p.split('=', 1) for p in match_param] - def match_param_predicate(context, request): - for k, v in reqs: - if request.matchdict.get(k) != v: - return False - return True - match_param_predicate.__text__ = text - weights.append(1 << 9) - predicates.append(match_param_predicate) - for p in match_param: - h.update(bytes_('match_param:%r' % p)) - - if custom: - for num, predicate in enumerate(custom): - if getattr(predicate, '__text__', None) is None: - text = '' - try: - predicate.__text__ = text - except AttributeError: - # if this happens the predicate is probably a classmethod - if hasattr(predicate, '__func__'): - predicate.__func__.__text__ = text - else: # pragma: no cover ; 2.5 doesn't have __func__ - predicate.im_func.__text__ = text - predicates.append(predicate) - # using hash() here rather than id() is intentional: we - # want to allow custom predicates that are part of - # frameworks to be able to define custom __hash__ - # functions for custom predicates, so that the hash output - # of predicate instances which are "logically the same" - # may compare equal. - h.update(bytes_('custom%s:%r' % (num, hash(predicate)))) - weights.append(1 << 10) - - if traverse is not None: - # ``traverse`` can only be used as a *route* "predicate"; it - # adds 'traverse' to the matchdict if it's specified in the - # routing args. This causes the ResourceTreeTraverser to use - # the resolved traverse pattern as the traversal path. - from pyramid.urldispatch import _compile_route - _, tgenerate = _compile_route(traverse) - def traverse_predicate(context, request): - if 'traverse' in context: - return True - m = context['match'] - tvalue = tgenerate(m) # tvalue will be urlquoted string - m['traverse'] = traversal_path(tvalue) # will be seq of unicode - return True - traverse_predicate.__text__ = 'traverse matchdict pseudo-predicate' - # This isn't actually a predicate, it's just a infodict - # modifier that injects ``traverse`` into the matchdict. As a - # result, the ``traverse_predicate`` function above always - # returns True, and we don't need to update the hash or attach - # a weight to it - predicates.append(traverse_predicate) - - score = 0 - for bit in weights: - score = score | bit - order = (MAX_ORDER - score) / (len(predicates) + 1) - phash = h.hexdigest() - return order, predicates, phash - def as_sorted_tuple(val): if not is_nonstr_iter(val): val = (val,) @@ -334,16 +98,22 @@ class TopologicalSorter(object): self.last = last def remove(self, name): - if name in self.names: - self.names.remove(name) - del self.name2val[name] - for u in self.name2after.get(name, []): + self.names.remove(name) + del self.name2val[name] + after = self.name2after.pop(name, []) + if after: + self.req_after.remove(name) + for u in after: self.order.remove((u, name)) - for u in self.name2before.get(name, []): + before = self.name2before.pop(name, []) + if before: + self.req_before.remove(name) + for u in before: self.order.remove((name, u)) def add(self, name, val, after=None, before=None): - self.remove(name) + if name in self.names: + self.remove(name) self.names.append(name) self.name2val[name] = val if after is None and before is None: @@ -448,41 +218,89 @@ class CyclicDependencyError(Exception): return msg class PredicateList(object): + def __init__(self): self.sorter = TopologicalSorter() + self.last_added = None def add(self, name, factory, weighs_more_than=None, weighs_less_than=None): + # Predicates should be added to a predicate list in (presumed) + # computation expense order. + ## if weighs_more_than is None and weighs_less_than is None: + ## weighs_more_than = self.last_added or FIRST + ## weighs_less_than = LAST + self.last_added = name self.sorter.add(name, factory, after=weighs_more_than, before=weighs_less_than) - def make(self, **kw): + def make(self, config, **kw): + # Given a configurator and a list of keywords, a predicate list is + # computed. Elsewhere in the code, we evaluate predicates using a + # generator expression. All predicates associated with a view or + # route must evaluate true for the view or route to "match" during a + # request. The fastest predicate should be evaluated first, then the + # next fastest, and so on, as if one returns false, the remainder of + # the predicates won't need to be evaluated. + # + # While we compute predicates, we also compute a predicate hash (aka + # phash) that can be used by a caller to identify identical predicate + # lists. ordered = self.sorter.sorted() phash = md5() weights = [] - predicates = [] - for order, (name, predicate_factory) in enumerate(ordered): + preds = [] + for n, (name, predicate_factory) in enumerate(ordered): vals = kw.pop(name, None) - if vals is None: + if vals is None: # XXX should this be a sentinel other than None? continue - if not isinstance(vals, SequenceOfPredicateValues): + if not isinstance(vals, predvalseq): vals = (vals,) for val in vals: - predicate = predicate_factory(val) - hashes = predicate.phash() + pred = predicate_factory(val, config) + hashes = pred.phash() if not is_nonstr_iter(hashes): hashes = [hashes] for h in hashes: phash.update(bytes_(h)) - predicate = predicate_factory(val) - weights.append(1 << order) - predicates.append(predicate) + weights.append(1 << n+1) + preds.append(pred) if kw: raise ConfigurationError('Unknown predicate values: %r' % (kw,)) + # A "order" is computed for the predicate list. An order is + # a scoring. + # + # Each predicate is associated with a weight value. The weight of a + # predicate symbolizes the relative potential "importance" of the + # predicate to all other predicates. A larger weight indicates + # greater importance. + # + # All weights for a given predicate list are bitwise ORed together + # to create a "score"; this score is then subtracted from + # MAX_ORDER and divided by an integer representing the number of + # predicates+1 to determine the order. + # + # For views, the order represents the ordering in which a "multiview" + # ( a collection of views that share the same context/request/name + # triad but differ in other ways via predicates) will attempt to call + # its set of views. Views with lower orders will be tried first. + # The intent is to a) ensure that views with more predicates are + # always evaluated before views with fewer predicates and b) to + # ensure a stable call ordering of views that share the same number + # of predicates. Views which do not have any predicates get an order + # of MAX_ORDER, meaning that they will be tried very last. score = 0 for bit in weights: score = score | bit - order = (MAX_ORDER - score) / (len(predicates) + 1) - return order, predicates, phash.hexdigest() + order = (MAX_ORDER - score) / (len(preds) + 1) + return order, preds, phash.hexdigest() -class SequenceOfPredicateValues(tuple): +class predvalseq(tuple): pass + +class Deferred(object): + def __init__(self, func): + self.func = func + + def resolve(self): + return self.func() + diff --git a/pyramid/config/views.py b/pyramid/config/views.py index f2fe83673..3f0c5c7c8 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -69,12 +69,13 @@ from pyramid.util import object_description from pyramid.config import predicates from pyramid.config.util import ( + Deferred, DEFAULT_PHASH, MAX_ORDER, action_method, as_sorted_tuple, PredicateList, - SequenceOfPredicateValues, + predvalseq, ) urljoin = urlparse.urljoin @@ -635,13 +636,31 @@ def viewdefaults(wrapped): class ViewsConfiguratorMixin(object): @viewdefaults @action_method - 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=None, accept=None, - header=None, path_info=None, custom_predicates=(), - context=None, decorator=None, mapper=None, http_cache=None, - match_param=None, **other_predicates): + 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=None, + accept=None, + header=None, + path_info=None, + custom_predicates=(), + context=None, + decorator=None, + mapper=None, + http_cache=None, + match_param=None, + **other_predicates): """ Add a :term:`view configuration` to the current configuration state. Arguments to ``add_view`` are broken down below into *predicate* arguments and *non-predicate* @@ -664,24 +683,27 @@ class ViewsConfiguratorMixin(object): permission - The name of a :term:`permission` that the user must possess - in order to invoke the :term:`view callable`. See - :ref:`view_security_section` for more information about view - security and permissions. If ``permission`` is omitted, a - *default* permission may be used for this view registration - if one was named as the + A :term:`permission` that the user must possess in order to invoke + the :term:`view callable`. See :ref:`view_security_section` for + more information about view security and permissions. This is + often a string like ``view`` or ``edit``. + + If ``permission`` is omitted, a *default* permission may be used + for this view registration if one was named as the :class:`pyramid.config.Configurator` constructor's ``default_permission`` argument, or if - :meth:`pyramid.config.Configurator.set_default_permission` - was used prior to this view registration. Pass the string - :data:`pyramid.security.NO_PERMISSION_REQUIRED` as the - permission argument to explicitly indicate that the view should - always be executable by entirely anonymous users, regardless of - the default permission, bypassing any :term:`authorization - policy` that may be in effect. + :meth:`pyramid.config.Configurator.set_default_permission` was used + prior to this view registration. Pass the value + :data:`pyramid.security.NO_PERMISSION_REQUIRED` as the permission + argument to explicitly indicate that the view should always be + executable by entirely anonymous users, regardless of the default + permission, bypassing any :term:`authorization policy` that may be + in effect. attr + This knob is most useful when the view definition is a class. + The view machinery defaults to using the ``__call__`` method of the :term:`view callable` (or the function itself, if the view callable is a function) to obtain a response. The @@ -690,8 +712,7 @@ class ViewsConfiguratorMixin(object): class, and the class has a method named ``index`` and you wanted to use this method instead of the class' ``__call__`` method to return the response, you'd say ``attr="index"`` in the - view configuration for the view. This is - most useful when the view definition is a class. + view configuration for the view. renderer @@ -975,9 +996,15 @@ class ViewsConfiguratorMixin(object): Each custom predicate callable should accept two arguments: ``context`` and ``request`` and should return either ``True`` or ``False`` after doing arbitrary evaluation of - the context and/or the request. If all callables return - ``True``, the associated view callable will be considered - viable for a given request. + the context and/or the request. + + other_predicates + + Pass a key/value pair here to use a third-party predicate registered + via :meth:`pyramid.config.Configurator.add_view_predicate`. More + than one key/value pair can be used at the same time. See + :ref:`registering_thirdparty_predicates` for more information + about third-party predicates. """ view = self.maybe_dotted(view) @@ -1034,17 +1061,26 @@ class ViewsConfiguratorMixin(object): containment=containment, request_type=request_type, match_param=match_param, - custom=SequenceOfPredicateValues(custom_predicates), + custom=predvalseq(custom_predicates), ) ) - - discriminator = ('view', context, name, route_name, attr, - str(sorted(pvals.items()))) + + def discrim_func(): + # We need to defer the discriminator until we know what the phash + # is. It can't be computed any sooner because thirdparty + # predicates may not yet exist when add_view is called. + order, preds, phash = predlist.make(self, **pvals) + view_intr.update({'phash':phash, 'order':order, 'predicates':preds}) + return ('view', context, name, route_name, phash) + + discriminator = Deferred(discrim_func) + if inspect.isclass(view) and attr: view_desc = 'method %r of %s' % ( attr, self.object_description(view)) else: view_desc = self.object_description(view) + view_intr = self.introspectable('views', discriminator, view_desc, @@ -1072,8 +1108,10 @@ class ViewsConfiguratorMixin(object): predlist = self.view_predlist def register(permission=permission, renderer=renderer): - order, preds, phash = predlist.make(**pvals) - view_intr.update({'phash':phash}) + # the discrim_func above is guaranteed to have been called already + order = view_intr['order'] + preds = view_intr['predicates'] + phash = view_intr['phash'] request_iface = IRequest if route_name is not None: request_iface = self.registry.queryUtility(IRouteRequest, @@ -1098,21 +1136,28 @@ class ViewsConfiguratorMixin(object): # (reg'd in phase 1) permission = self.registry.queryUtility(IDefaultPermission) + # added by discrim_func above during conflict resolving + preds = view_intr['predicates'] + order = view_intr['order'] + phash = view_intr['phash'] + # __no_permission_required__ handled by _secure_view - deriver = ViewDeriver(registry=self.registry, - permission=permission, - predicates=preds, - attr=attr, - renderer=renderer, - wrapper_viewname=wrapper, - viewname=name, - accept=accept, - order=order, - phash=phash, - package=self.package, - mapper=mapper, - decorator=decorator, - http_cache=http_cache) + deriver = ViewDeriver( + registry=self.registry, + permission=permission, + predicates=preds, + attr=attr, + renderer=renderer, + wrapper_viewname=wrapper, + viewname=name, + accept=accept, + order=order, + phash=phash, + package=self.package, + mapper=mapper, + decorator=decorator, + http_cache=http_cache, + ) derived_view = deriver(view) derived_view.__discriminator__ = lambda *arg: discriminator # __discriminator__ is used by superdynamic systems @@ -1217,19 +1262,25 @@ class ViewsConfiguratorMixin(object): IMultiView, name=name) if mapper: - mapper_intr = self.introspectable('view mappers', - discriminator, - 'view mapper for %s' % view_desc, - 'view mapper') + mapper_intr = self.introspectable( + 'view mappers', + discriminator, + 'view mapper for %s' % view_desc, + 'view mapper' + ) mapper_intr['mapper'] = mapper mapper_intr.relate('views', discriminator) introspectables.append(mapper_intr) if route_name: view_intr.relate('routes', route_name) # see add_route if renderer is not None and renderer.name and '.' in renderer.name: - # it's a template - tmpl_intr = self.introspectable('templates', discriminator, - renderer.name, 'template') + # the renderer is a template + tmpl_intr = self.introspectable( + 'templates', + discriminator, + renderer.name, + 'template' + ) tmpl_intr.relate('views', discriminator) tmpl_intr['name'] = renderer.name tmpl_intr['type'] = renderer.type @@ -1237,8 +1288,13 @@ class ViewsConfiguratorMixin(object): tmpl_intr.relate('renderer factories', renderer.type) introspectables.append(tmpl_intr) if permission is not None: - perm_intr = self.introspectable('permissions', permission, - permission, 'permission') + # if a permission exists, register a permission introspectable + perm_intr = self.introspectable( + 'permissions', + permission, + permission, + 'permission' + ) perm_intr['value'] = permission perm_intr.relate('views', discriminator) introspectables.append(perm_intr) @@ -1255,13 +1311,14 @@ class ViewsConfiguratorMixin(object): @action_method def add_view_predicate(self, name, factory, weighs_more_than=None, weighs_less_than=None): - """ Adds a view predicate factory. The view predicate can later be - named as a keyword argument to - :meth:`pyramid.config.Configurator.add_view`. + """ Adds a view predicate factory. The associated view predicate can + later be named as a keyword argument to + :meth:`pyramid.config.Configurator.add_view` in the + ``other_predicates`` anonyous keyword argument dictionary. ``name`` should be the name of the predicate. It must be a valid Python identifier (it will be used as a keyword argument to - ``add_view``). + ``add_view`` by others). ``factory`` should be a :term:`predicate factory`. """ @@ -1283,26 +1340,19 @@ class ViewsConfiguratorMixin(object): order=PHASE1_CONFIG) # must be registered before views added def add_default_view_predicates(self): - self.add_view_predicate( - 'xhr', predicates.XHRPredicate) - self.add_view_predicate( - 'request_method', predicates.RequestMethodPredicate) - self.add_view_predicate( - 'path_info', predicates.PathInfoPredicate) - self.add_view_predicate( - 'request_param', predicates.RequestParamPredicate) - self.add_view_predicate( - 'header', predicates.HeaderPredicate) - self.add_view_predicate( - 'accept', predicates.AcceptPredicate) - self.add_view_predicate( - 'containment', predicates.ContainmentPredicate) - self.add_view_predicate( - 'request_type', predicates.RequestTypePredicate) - self.add_view_predicate( - 'match_param', predicates.MatchParamPredicate) - self.add_view_predicate( - 'custom', predicates.CustomPredicate) + for (name, factory) in ( + ('xhr', predicates.XHRPredicate), + ('request_method', predicates.RequestMethodPredicate), + ('path_info', predicates.PathInfoPredicate), + ('request_param', predicates.RequestParamPredicate), + ('header', predicates.HeaderPredicate), + ('accept', predicates.AcceptPredicate), + ('containment', predicates.ContainmentPredicate), + ('request_type', predicates.RequestTypePredicate), + ('match_param', predicates.MatchParamPredicate), + ('custom', predicates.CustomPredicate), + ): + self.add_view_predicate(name, factory) def derive_view(self, view, attr=None, renderer=None): """ diff --git a/pyramid/testing.py b/pyramid/testing.py index 2628dc817..89eec84b0 100644 --- a/pyramid/testing.py +++ b/pyramid/testing.py @@ -372,6 +372,7 @@ def registerRoute(pattern, name, factory=None): """ reg = get_current_registry() config = Configurator(registry=reg) + config.setup_registry() result = config.add_route(name, pattern, factory=factory) config.commit() return result @@ -825,6 +826,7 @@ def setUp(registry=None, request=None, hook_zca=True, autocommit=True, # any existing renderer factory lookup system. config.add_renderer(name, renderer) config.add_default_view_predicates() + config.add_default_route_predicates() config.commit() global have_zca try: diff --git a/pyramid/tests/test_config/test_init.py b/pyramid/tests/test_config/test_init.py index b23168aaa..3d952caf7 100644 --- a/pyramid/tests/test_config/test_init.py +++ b/pyramid/tests/test_config/test_init.py @@ -1396,13 +1396,9 @@ class TestConfiguratorDeprecatedFeatures(unittest.TestCase): try: config.commit() except ConfigurationConflictError as why: - c1, c2, c3, c4, c5, c6 = _conflictFunctions(why) + c1, c2 = _conflictFunctions(why) self.assertEqual(c1, 'test_conflict_route_with_view') self.assertEqual(c2, 'test_conflict_route_with_view') - self.assertEqual(c3, 'test_conflict_route_with_view') - self.assertEqual(c4, 'test_conflict_route_with_view') - self.assertEqual(c5, 'test_conflict_route_with_view') - self.assertEqual(c6, 'test_conflict_route_with_view') else: # pragma: no cover raise AssertionError @@ -1693,6 +1689,7 @@ class Test_resolveConflicts(unittest.TestCase): (3, f, (3,), {}, ('y',)), (None, f, (5,), {}, ('y',)), ]) + result = list(result) self.assertEqual( result, [{'info': None, @@ -1754,6 +1751,7 @@ class Test_resolveConflicts(unittest.TestCase): expand_action(3, f, (3,), {}, ('y',)), expand_action(None, f, (5,), {}, ('y',)), ]) + result = list(result) self.assertEqual( result, [{'info': None, @@ -1805,32 +1803,31 @@ class Test_resolveConflicts(unittest.TestCase): def test_it_conflict(self): from pyramid.tests.test_config import dummyfactory as f - self.assertRaises( - ConfigurationConflictError, - self._callFUT, [ - (None, f), - (1, f, (2,), {}, ('x',), 'eek'), - (1, f, (3,), {}, ('y',), 'ack'), - (4, f, (4,), {}, ('y',)), - (3, f, (3,), {}, ('y',)), - (None, f, (5,), {}, ('y',)), - ] - ) + result = self._callFUT([ + (None, f), + (1, f, (2,), {}, ('x',), 'eek'), # will conflict + (1, f, (3,), {}, ('y',), 'ack'), # will conflict + (4, f, (4,), {}, ('y',)), + (3, f, (3,), {}, ('y',)), + (None, f, (5,), {}, ('y',)), + ]) + self.assertRaises(ConfigurationConflictError, list, result) def test_it_with_actions_grouped_by_order(self): from pyramid.tests.test_config import dummyfactory as f from pyramid.config import expand_action result = self._callFUT([ - expand_action(None, f), - expand_action(1, f, (1,), {}, (), 'third', 10), + expand_action(None, f), # X + expand_action(1, f, (1,), {}, (), 'third', 10), # X expand_action(1, f, (2,), {}, ('x',), 'fourth', 10), expand_action(1, f, (3,), {}, ('y',), 'fifth', 10), - expand_action(2, f, (1,), {}, (), 'sixth', 10), - expand_action(3, f, (1,), {}, (), 'seventh', 10), - expand_action(5, f, (4,), {}, ('y',), 'eighth', 99999), - expand_action(4, f, (3,), {}, (), 'first', 5), + expand_action(2, f, (1,), {}, (), 'sixth', 10), # X + expand_action(3, f, (1,), {}, (), 'seventh', 10), # X + expand_action(5, f, (4,), {}, ('y',), 'eighth', 99999), # X + expand_action(4, f, (3,), {}, (), 'first', 5), # X expand_action(4, f, (5,), {}, ('y',), 'second', 5), ]) + result = list(result) self.assertEqual(len(result), 6) # resolved actions should be grouped by (order, i) self.assertEqual( diff --git a/pyramid/tests/test_config/test_predicates.py b/pyramid/tests/test_config/test_predicates.py index 8a9da7a41..79dabd5d4 100644 --- a/pyramid/tests/test_config/test_predicates.py +++ b/pyramid/tests/test_config/test_predicates.py @@ -5,7 +5,7 @@ from pyramid.compat import text_ class TestXHRPredicate(unittest.TestCase): def _makeOne(self, val): from pyramid.config.predicates import XHRPredicate - return XHRPredicate(val) + return XHRPredicate(val, None) def test___call___true(self): inst = self._makeOne(True) @@ -32,7 +32,7 @@ class TestXHRPredicate(unittest.TestCase): class TestRequestMethodPredicate(unittest.TestCase): def _makeOne(self, val): from pyramid.config.predicates import RequestMethodPredicate - return RequestMethodPredicate(val) + return RequestMethodPredicate(val, None) def test___call___true_single(self): inst = self._makeOne('GET') @@ -66,7 +66,7 @@ class TestRequestMethodPredicate(unittest.TestCase): class TestPathInfoPredicate(unittest.TestCase): def _makeOne(self, val): from pyramid.config.predicates import PathInfoPredicate - return PathInfoPredicate(val) + return PathInfoPredicate(val, None) def test_ctor_compilefail(self): from pyramid.exceptions import ConfigurationError @@ -97,7 +97,7 @@ class TestPathInfoPredicate(unittest.TestCase): class TestRequestParamPredicate(unittest.TestCase): def _makeOne(self, val): from pyramid.config.predicates import RequestParamPredicate - return RequestParamPredicate(val) + return RequestParamPredicate(val, None) def test___call___true_exists(self): inst = self._makeOne('abc') @@ -139,7 +139,7 @@ class TestRequestParamPredicate(unittest.TestCase): class TestMatchParamPredicate(unittest.TestCase): def _makeOne(self, val): from pyramid.config.predicates import MatchParamPredicate - return MatchParamPredicate(val) + return MatchParamPredicate(val, None) def test___call___true_single(self): inst = self._makeOne('abc=1') @@ -174,7 +174,7 @@ class TestMatchParamPredicate(unittest.TestCase): class TestCustomPredicate(unittest.TestCase): def _makeOne(self, val): from pyramid.config.predicates import CustomPredicate - return CustomPredicate(val) + return CustomPredicate(val, None) def test___call___true(self): def func(context, request): @@ -203,17 +203,17 @@ class TestCustomPredicate(unittest.TestCase): def test_text_func_repr(self): pred = predicate() inst = self._makeOne(pred) - self.assertTrue(inst.text(), 'predicate') + self.assertEqual(inst.text(), u'custom predicate: object predicate') def test_phash(self): pred = predicate() inst = self._makeOne(pred) - self.assertTrue(inst.phash(), 'custom:1') + self.assertEqual(inst.phash(), 'custom:1') class TestTraversePredicate(unittest.TestCase): def _makeOne(self, val): from pyramid.config.predicates import TraversePredicate - return TraversePredicate(val) + return TraversePredicate(val, None) def test___call__traverse_has_remainder_already(self): inst = self._makeOne('/1/:a/:b') diff --git a/pyramid/tests/test_config/test_routes.py b/pyramid/tests/test_config/test_routes.py index bb47d2d7e..6fb5189f6 100644 --- a/pyramid/tests/test_config/test_routes.py +++ b/pyramid/tests/test_config/test_routes.py @@ -158,7 +158,7 @@ class RoutesConfiguratorMixinTests(unittest.TestCase): def pred2(context, request): pass config.add_route('name', 'path', custom_predicates=(pred1, pred2)) route = self._assertRoute(config, 'name', 'path', 2) - self.assertEqual(route.predicates, [pred1, pred2]) + self.assertEqual(len(route.predicates), 2) def test_add_route_with_header(self): config = self._makeOne(autocommit=True) diff --git a/pyramid/tests/test_config/test_util.py b/pyramid/tests/test_config/test_util.py index 06e63ca40..310d04535 100644 --- a/pyramid/tests/test_config/test_util.py +++ b/pyramid/tests/test_config/test_util.py @@ -1,10 +1,32 @@ import unittest from pyramid.compat import text_ -class Test__make_predicates(unittest.TestCase): +class TestPredicateList(unittest.TestCase): + + def _makeOne(self): + from pyramid.config.util import PredicateList + from pyramid.config import predicates + inst = PredicateList() + for name, factory in ( + ('xhr', predicates.XHRPredicate), + ('request_method', predicates.RequestMethodPredicate), + ('path_info', predicates.PathInfoPredicate), + ('request_param', predicates.RequestParamPredicate), + ('header', predicates.HeaderPredicate), + ('accept', predicates.AcceptPredicate), + ('containment', predicates.ContainmentPredicate), + ('request_type', predicates.RequestTypePredicate), + ('match_param', predicates.MatchParamPredicate), + ('custom', predicates.CustomPredicate), + ('traverse', predicates.TraversePredicate), + ): + inst.add(name, factory) + return inst + def _callFUT(self, **kw): - from pyramid.config.util import make_predicates - return make_predicates(**kw) + inst = self._makeOne() + config = DummyConfigurator() + return inst.make(config, **kw) def test_ordering_xhr_and_request_method_trump_only_containment(self): order1, _, _ = self._callFUT(xhr=True, request_method='GET') @@ -12,6 +34,7 @@ class Test__make_predicates(unittest.TestCase): self.assertTrue(order1 < order2) def test_ordering_number_of_predicates(self): + from pyramid.config.util import predvalseq order1, _, _ = self._callFUT( xhr='xhr', request_method='request_method', @@ -22,7 +45,7 @@ class Test__make_predicates(unittest.TestCase): accept='accept', containment='containment', request_type='request_type', - custom=(DummyCustomPredicate(),), + custom=predvalseq([DummyCustomPredicate()]), ) order2, _, _ = self._callFUT( xhr='xhr', @@ -34,7 +57,7 @@ class Test__make_predicates(unittest.TestCase): accept='accept', containment='containment', request_type='request_type', - custom=(DummyCustomPredicate(),), + custom=predvalseq([DummyCustomPredicate()]), ) order3, _, _ = self._callFUT( xhr='xhr', @@ -114,6 +137,7 @@ class Test__make_predicates(unittest.TestCase): self.assertTrue(order12 > order10) def test_ordering_importance_of_predicates(self): + from pyramid.config.util import predvalseq order1, _, _ = self._callFUT( xhr='xhr', ) @@ -142,7 +166,7 @@ class Test__make_predicates(unittest.TestCase): match_param='foo=bar', ) order10, _, _ = self._callFUT( - custom=(DummyCustomPredicate(),), + custom=predvalseq([DummyCustomPredicate()]), ) self.assertTrue(order1 > order2) self.assertTrue(order2 > order3) @@ -155,12 +179,13 @@ class Test__make_predicates(unittest.TestCase): self.assertTrue(order9 > order10) def test_ordering_importance_and_number(self): + from pyramid.config.util import predvalseq order1, _, _ = self._callFUT( xhr='xhr', request_method='request_method', ) order2, _, _ = self._callFUT( - custom=(DummyCustomPredicate(),), + custom=predvalseq([DummyCustomPredicate()]), ) self.assertTrue(order1 < order2) @@ -170,7 +195,7 @@ class Test__make_predicates(unittest.TestCase): ) order2, _, _ = self._callFUT( request_method='request_method', - custom=(DummyCustomPredicate(),), + custom=predvalseq([DummyCustomPredicate()]), ) self.assertTrue(order1 > order2) @@ -181,7 +206,7 @@ class Test__make_predicates(unittest.TestCase): ) order2, _, _ = self._callFUT( request_method='request_method', - custom=(DummyCustomPredicate(),), + custom=predvalseq([DummyCustomPredicate()]), ) self.assertTrue(order1 < order2) @@ -193,18 +218,19 @@ class Test__make_predicates(unittest.TestCase): order2, _, _ = self._callFUT( xhr='xhr', request_method='request_method', - custom=(DummyCustomPredicate(),), + custom=predvalseq([DummyCustomPredicate()]), ) self.assertTrue(order1 > order2) def test_different_custom_predicates_with_same_hash(self): + from pyramid.config.util import predvalseq class PredicateWithHash(object): def __hash__(self): return 1 a = PredicateWithHash() b = PredicateWithHash() - _, _, a_phash = self._callFUT(custom=(a,)) - _, _, b_phash = self._callFUT(custom=(b,)) + _, _, a_phash = self._callFUT(custom=predvalseq([a])) + _, _, b_phash = self._callFUT(custom=predvalseq([b])) self.assertEqual(a_phash, b_phash) def test_traverse_has_remainder_already(self): @@ -244,12 +270,14 @@ class Test__make_predicates(unittest.TestCase): ) def test_custom_predicates_can_affect_traversal(self): + from pyramid.config.util import predvalseq def custom(info, request): m = info['match'] m['dummy'] = 'foo' return True - _, predicates, _ = self._callFUT(custom=(custom,), - traverse='/1/:dummy/:a') + _, predicates, _ = self._callFUT( + custom=predvalseq([custom]), + traverse='/1/:dummy/:a') self.assertEqual(len(predicates), 2) info = {'match':{'a':'a'}} request = DummyRequest() @@ -259,6 +287,7 @@ class Test__make_predicates(unittest.TestCase): 'traverse':('1', 'foo', 'a')}}) def test_predicate_text_is_correct(self): + from pyramid.config.util import predvalseq _, predicates, _ = self._callFUT( xhr='xhr', request_method='request_method', @@ -268,23 +297,27 @@ class Test__make_predicates(unittest.TestCase): accept='accept', containment='containment', request_type='request_type', - custom=(DummyCustomPredicate(), + custom=predvalseq( + [ + DummyCustomPredicate(), DummyCustomPredicate.classmethod_predicate, - DummyCustomPredicate.classmethod_predicate_no_text), + DummyCustomPredicate.classmethod_predicate_no_text, + ] + ), match_param='foo=bar') - self.assertEqual(predicates[0].__text__, 'xhr = True') - self.assertEqual(predicates[1].__text__, - "request method = ['request_method']") - self.assertEqual(predicates[2].__text__, 'path_info = path_info') - self.assertEqual(predicates[3].__text__, 'request_param param') - self.assertEqual(predicates[4].__text__, 'header header') - self.assertEqual(predicates[5].__text__, 'accept = accept') - self.assertEqual(predicates[6].__text__, 'containment = containment') - self.assertEqual(predicates[7].__text__, 'request_type = request_type') - self.assertEqual(predicates[8].__text__, "match_param ['foo=bar']") - self.assertEqual(predicates[9].__text__, 'custom predicate') - self.assertEqual(predicates[10].__text__, 'classmethod predicate') - self.assertEqual(predicates[11].__text__, '') + self.assertEqual(predicates[0].text(), 'xhr = True') + self.assertEqual(predicates[1].text(), + "request_method = request_method") + self.assertEqual(predicates[2].text(), 'path_info = path_info') + self.assertEqual(predicates[3].text(), 'request_param param') + self.assertEqual(predicates[4].text(), 'header header') + self.assertEqual(predicates[5].text(), 'accept = accept') + self.assertEqual(predicates[6].text(), 'containment = containment') + self.assertEqual(predicates[7].text(), 'request_type = request_type') + self.assertEqual(predicates[8].text(), "match_param foo=bar") + self.assertEqual(predicates[9].text(), 'custom predicate') + self.assertEqual(predicates[10].text(), 'classmethod predicate') + self.assertTrue(predicates[11].text().startswith('custom predicate')) def test_match_param_from_string(self): _, predicates, _ = self._callFUT(match_param='foo=bar') @@ -641,3 +674,7 @@ class DummyRequest: self.params = {} self.cookies = {} +class DummyConfigurator(object): + def maybe_dotted(self, thing): + return thing + diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index 9ff83e956..f2daf0c34 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -118,7 +118,8 @@ class TestViewsConfigurationMixin(unittest.TestCase): self.assertEqual(wrapper.__module__, view.__module__) self.assertEqual(wrapper.__name__, view.__name__) self.assertEqual(wrapper.__doc__, view.__doc__) - self.assertEqual(wrapper.__discriminator__(None, None)[0], 'view') + self.assertEqual(wrapper.__discriminator__(None, None).resolve()[0], + 'view') def test_add_view_view_callable_dottedname(self): from pyramid.renderers import null_renderer @@ -970,8 +971,10 @@ class TestViewsConfigurationMixin(unittest.TestCase): wrapper = self._getViewCallable(config) self.assertTrue(IMultiView.providedBy(wrapper)) request = self._makeRequest(config) - self.assertTrue('IFoo' in wrapper.__discriminator__(foo, request)[5]) - self.assertTrue('IBar' in wrapper.__discriminator__(bar, request)[5]) + self.assertNotEqual( + wrapper.__discriminator__(foo, request), + wrapper.__discriminator__(bar, request), + ) def test_add_view_with_template_renderer(self): from pyramid.tests import test_config diff --git a/pyramid/tests/test_testing.py b/pyramid/tests/test_testing.py index 5b0073b81..a9e50442f 100644 --- a/pyramid/tests/test_testing.py +++ b/pyramid/tests/test_testing.py @@ -253,6 +253,7 @@ class Test_registerSubscriber(TestBase): class Test_registerRoute(TestBase): def test_registerRoute(self): + from pyramid.config import Configurator from pyramid.request import Request from pyramid.interfaces import IRoutesMapper from pyramid.testing import registerRoute @@ -261,6 +262,8 @@ class Test_registerRoute(TestBase): self.assertEqual(len(mapper.routelist), 1) request = Request.blank('/') request.registry = self.registry + config = Configurator(registry=self.registry) + config.setup_registry() self.assertEqual(request.route_url('home', pagename='abc'), 'http://localhost/abc') -- cgit v1.2.3 From adf32395eaef658bc9053037a9bed4b182397faf Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 5 Aug 2012 23:58:32 -0400 Subject: add coverage, get rid of test that doesnt belong here --- pyramid/tests/test_config/test_init.py | 3 --- pyramid/tests/test_config/test_util.py | 32 +++++++++++++++++++++++--------- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/pyramid/tests/test_config/test_init.py b/pyramid/tests/test_config/test_init.py index 3d952caf7..abe22400b 100644 --- a/pyramid/tests/test_config/test_init.py +++ b/pyramid/tests/test_config/test_init.py @@ -1994,6 +1994,3 @@ class DummyIntrospectable(object): def register(self, introspector, action_info): self.registered.append((introspector, action_info)) -class DummyPredicateList(object): - def add(self, name, factory, weighs_more_than=None, weighs_less_than=None): - pass diff --git a/pyramid/tests/test_config/test_util.py b/pyramid/tests/test_config/test_util.py index 310d04535..13cb27526 100644 --- a/pyramid/tests/test_config/test_util.py +++ b/pyramid/tests/test_config/test_util.py @@ -361,15 +361,10 @@ class TestPredicateList(unittest.TestCase): hash2, _, __= self._callFUT(request_method='GET') self.assertEqual(hash1, hash2) - def test_match_param_hashable(self): - # https://github.com/Pylons/pyramid/issues/425 - import pyramid.testing - def view(request): pass - config = pyramid.testing.setUp(autocommit=False) - config.add_route('foo', '/foo/{a}/{b}') - config.add_view(view, route_name='foo', match_param='a=bar') - config.add_view(view, route_name='foo', match_param=('a=bar', 'b=baz')) - config.commit() + def test_unknown_predicate(self): + from pyramid.exceptions import ConfigurationError + self.assertRaises(ConfigurationError, self._callFUT, unknown=1) + class TestActionInfo(unittest.TestCase): def _getTargetClass(self): @@ -406,6 +401,25 @@ class TestTopologicalSorter(unittest.TestCase): from pyramid.config.util import TopologicalSorter return TopologicalSorter(*arg, **kw) + def test_remove(self): + inst = self._makeOne() + inst.names.append('name') + inst.name2val['name'] = 1 + inst.req_after.add('name') + inst.req_before.add('name') + inst.name2after['name'] = ('bob',) + inst.name2before['name'] = ('fred',) + inst.order.append(('bob', 'name')) + inst.order.append(('name', 'fred')) + inst.remove('name') + self.assertFalse(inst.names) + self.assertFalse(inst.req_before) + self.assertFalse(inst.req_after) + self.assertFalse(inst.name2before) + self.assertFalse(inst.name2after) + self.assertFalse(inst.name2val) + self.assertFalse(inst.order) + def test_add(self): from pyramid.config.util import LAST sorter = self._makeOne() -- cgit v1.2.3 From 88435e58b45b7c00387508bb2960f784d627bf00 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 6 Aug 2012 00:02:31 -0400 Subject: coverage --- pyramid/tests/test_scaffolds/test_copydir.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pyramid/tests/test_scaffolds/test_copydir.py b/pyramid/tests/test_scaffolds/test_copydir.py index 68cefbe6e..d757b837c 100644 --- a/pyramid/tests/test_scaffolds/test_copydir.py +++ b/pyramid/tests/test_scaffolds/test_copydir.py @@ -176,6 +176,14 @@ class Test_makedirs(unittest.TestCase): self._callFUT(target, 2, None) shutil.rmtree(tmpdir) + def test_makedirs_no_parent_dir(self): + import shutil + import tempfile + tmpdir = tempfile.mkdtemp() + target = os.path.join(tmpdir, 'nonexistent_subdir', 'non2') + self._callFUT(target, 2, None) + shutil.rmtree(tmpdir) + class Test_support_functions(unittest.TestCase): def _call_html_quote(self, *arg, **kw): -- cgit v1.2.3 From 859e947aae832194e1a22905898be9ebe05b20ed Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 6 Aug 2012 00:11:12 -0400 Subject: fix under py3 --- pyramid/config/__init__.py | 6 ++++-- pyramid/tests/test_config/test_predicates.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index 2fca7a162..7e6649c14 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -1084,7 +1084,8 @@ def resolveConflicts(actions): other conflicting actions. """ - def orderandpos((n, v)): + def orderandpos(v): + n, v = v if not isinstance(v, dict): # old-style tuple action v = expand_action(*v) @@ -1092,7 +1093,8 @@ def resolveConflicts(actions): sactions = sorted(enumerate(actions), key=orderandpos) - def orderonly((n,v)): + def orderonly(v): + n, v = v if not isinstance(v, dict): # old-style tuple action v = expand_action(*v) diff --git a/pyramid/tests/test_config/test_predicates.py b/pyramid/tests/test_config/test_predicates.py index 79dabd5d4..94e613715 100644 --- a/pyramid/tests/test_config/test_predicates.py +++ b/pyramid/tests/test_config/test_predicates.py @@ -203,7 +203,7 @@ class TestCustomPredicate(unittest.TestCase): def test_text_func_repr(self): pred = predicate() inst = self._makeOne(pred) - self.assertEqual(inst.text(), u'custom predicate: object predicate') + self.assertEqual(inst.text(), 'custom predicate: object predicate') def test_phash(self): pred = predicate() -- cgit v1.2.3 From 0196b2a06ef66d2e8b33a03cc84373ab84ba44be Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 6 Aug 2012 00:48:29 -0400 Subject: add docs for third-party view predicates --- CHANGES.txt | 15 +++++++++ docs/api/config.rst | 2 ++ docs/glossary.rst | 6 ++++ docs/narr/hooks.rst | 94 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 117 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index ecb2bf659..5e0bf0968 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -20,6 +20,21 @@ Bug Fixes Features -------- +- Third-party custom view and route predicates can now be added for use by + view authors via ``pyramid.config.Configurator.add_view_predicate`` and + ``pyramid.config.Configurator.add_route_predicate``. So, for example, + doing this:: + + config.add_view_predicate('abc', my.package.ABCPredicate) + + Might allow a view author to do this in an application that configured that + predicate:: + + @view_config(abc=1) + + See "Adding A Third Party View or Route Predicate" in the Hooks chapter for + more information. + - Custom objects can be made easily JSON-serializable in Pyramid by defining a ``__json__`` method on the object's class. This method should return values natively serializable by ``json.dumps`` (such as ints, lists, diff --git a/docs/api/config.rst b/docs/api/config.rst index cd58e74d3..bc9e067b1 100644 --- a/docs/api/config.rst +++ b/docs/api/config.rst @@ -66,6 +66,8 @@ .. automethod:: add_response_adapter .. automethod:: add_traverser .. automethod:: add_tween + .. automethod:: add_route_predicate + .. automethod:: add_view_predicate .. automethod:: set_request_factory .. automethod:: set_root_factory .. automethod:: set_session_factory diff --git a/docs/glossary.rst b/docs/glossary.rst index 45a79326f..ba3203f89 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -994,3 +994,9 @@ Glossary Aka ``gunicorn``, a fast :term:`WSGI` server that runs on UNIX under Python 2.5+ (although at the time of this writing does not support Python 3). See http://gunicorn.org/ for detailed information. + + predicate factory + A callable which is used by a third party during the registration of a + route or view predicates to extend the view and route configuration + system. See :ref:`registering_thirdparty_predicates` for more + information. diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index 332805152..bdd968362 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -1232,3 +1232,97 @@ Displaying Tween Ordering The ``ptweens`` command-line utility can be used to report the current implict and explicit tween chains used by an application. See :ref:`displaying_tweens`. + +.. _registering_thirdparty_predicates: + +Adding A Third Party View or Route Predicate +-------------------------------------------- + +View and route predicates used during view configuration allow you to narrow +the set of circumstances under which a view or route will match. For +example, the ``request_method`` view predicate can be used to ensure a view +callable is only invoked when the request's method is ``POST``: + +.. code-block:: python + + @view_config(request_method='POST') + def someview(request): + ... + +Likewise, a similar predicate can be used as a *route* predicate: + +.. code-block:: python + + config.add_route('name', '/foo', request_method='POST') + +Many other built-in predicates exists (``request_param``, and others). You +can add third-party predicates to the list of available predicates by using +one of :meth:`pyramid.config.Configurator.add_view_predicate` or +:meth:`pyramid.config.Configurator.add_route_predicate`. The former adds a +view predicate, the latter a route predicate. + +When using one of those APIs, you pass a *name* and a *factory* to add a +predicate during Pyramid's configuration stage. For example: + +.. code-block:: python + + config.add_view_predicate('content_type', ContentTypePredicate) + +The above example adds a new predicate named ``content_type`` to the list of +available predicates for views. This will allow the following view +configuration statement to work: + +.. code-block:: python + :linenos: + + @view_config(content_type='File') + def aview(request): ... + +The first argument to :meth:`pyramid.config.Configurator.add_view_predicate`, +the name, is a string representing the name that is expected to be passed to +``view_config`` (or its imperative analogue ``add_view``). + +The second argument is a predicate factory. A predicate factory is most +often a class with a constructor (``__init__``), a ``text`` method, a +``phash`` method and a ``__call__`` method. For example: + +.. code-block:: python + :linenos: + + class ContentTypePredicate(object): + def __init__(self, val, config): + self.val + + def text(self): + return 'content_type = %s' % (self.val,) + + phash = text + + def __call__(self, context, request): + return getattr(context, 'content_type', None) == self.val + +The constructor of a predicate factory takes two arguments: ``val`` and +``config``. The ``val`` argument will be the argument passed to +``view_config`` (or ``add_view``). In the example above, it will be the +string ``File``. The second arg, ``config`` will be the Configurator +instance at the time of configuration. + +The ``text`` method must return a string. It should be useful to describe +the behavior of the predicate in error messages. + +The ``phash`` method must return a string or a sequence of strings. It's +most often the same as ``text``, as long as ``text`` uniquely describes the +predicate's name and the value passed to the constructor. If ``text`` is +more general, or doesn't describe things that way, ``phash`` should return a +string with the name and the value serialized. The result of ``phash`` is +not seen in output anywhere, it just informs the uniqueness constraints for +view configuration. + +The ``__call__`` method of a predicate factory must accept a resource +(``context``) and a request, and must return ``True`` or ``False``. It is +the "meat" of the predicate. + +You can use the same predicate factory as both a view predicate and as a +route predicate, but you'll need to call ``add_view_predicate`` and +``add_route_predicate`` separately with the same factory. + -- cgit v1.2.3 From cff71c316eb8b77b936bfc14a29d7ff9727edf70 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 6 Aug 2012 00:05:12 -0500 Subject: garden --- docs/narr/hooks.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index bdd968362..9482bfcf8 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -1291,7 +1291,7 @@ often a class with a constructor (``__init__``), a ``text`` method, a class ContentTypePredicate(object): def __init__(self, val, config): - self.val + self.val = val def text(self): return 'content_type = %s' % (self.val,) -- cgit v1.2.3 From 5664c428e22d73f8b4315b678588150d9bb2f0f5 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 6 Aug 2012 11:10:15 -0400 Subject: add as-of-version notes --- docs/narr/hooks.rst | 4 ++++ pyramid/config/routes.py | 17 ++++++++++++----- pyramid/config/views.py | 17 ++++++++++++----- 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index bdd968362..73ee655ea 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -1238,6 +1238,10 @@ implict and explicit tween chains used by an application. See Adding A Third Party View or Route Predicate -------------------------------------------- +.. note:: + + Third-party predicates are a feature new as of Pyramid 1.4. + View and route predicates used during view configuration allow you to narrow the set of circumstances under which a view or route will match. For example, the ``request_method`` view predicate can be used to ensure a view diff --git a/pyramid/config/routes.py b/pyramid/config/routes.py index ff285569d..d8ce4804c 100644 --- a/pyramid/config/routes.py +++ b/pyramid/config/routes.py @@ -261,11 +261,12 @@ class RoutesConfiguratorMixin(object): other_predicates - Pass a key/value pair here to use a third-party predicate registered - via :meth:`pyramid.config.Configurator.add_view_predicate`. More - than one key/value pair can be used at the same time. See - :ref:`registering_thirdparty_predicates` for more information - about third-party predicates. + Pass a key/value pair here to use a third-party predicate + registered via + :meth:`pyramid.config.Configurator.add_view_predicate`. More than + one key/value pair can be used at the same time. See + :ref:`registering_thirdparty_predicates` for more information about + third-party predicates. This argument is new as of Pyramid 1.4. View-Related Arguments @@ -486,6 +487,12 @@ class RoutesConfiguratorMixin(object): ``add_view``). ``factory`` should be a :term:`predicate factory`. + + See :ref:`registering_thirdparty_predicates` for more information. + + .. note:: + + This method is new as of Pyramid 1.4. """ discriminator = ('route predicate', name) intr = self.introspectable( diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 3f0c5c7c8..4491272d3 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1000,11 +1000,12 @@ class ViewsConfiguratorMixin(object): other_predicates - Pass a key/value pair here to use a third-party predicate registered - via :meth:`pyramid.config.Configurator.add_view_predicate`. More - than one key/value pair can be used at the same time. See - :ref:`registering_thirdparty_predicates` for more information - about third-party predicates. + Pass a key/value pair here to use a third-party predicate + registered via + :meth:`pyramid.config.Configurator.add_view_predicate`. More than + one key/value pair can be used at the same time. See + :ref:`registering_thirdparty_predicates` for more information about + third-party predicates. This argument is new as of Pyramid 1.4. """ view = self.maybe_dotted(view) @@ -1321,6 +1322,12 @@ class ViewsConfiguratorMixin(object): ``add_view`` by others). ``factory`` should be a :term:`predicate factory`. + + See :ref:`registering_thirdparty_predicates` for more information. + + .. note:: + + This method is new as of Pyramid 1.4. """ discriminator = ('view predicate', name) intr = self.introspectable( -- cgit v1.2.3 From 02ce7d6425bcd81590ae50fa298f50ea47422a47 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Mon, 6 Aug 2012 12:41:06 -0400 Subject: Normalize interface conformance tests. --- pyramid/tests/test_events.py | 98 ++++++++++++++++++++++--------------------- pyramid/tests/test_path.py | 20 ++++----- pyramid/tests/test_request.py | 55 ++++++++++++------------ 3 files changed, 88 insertions(+), 85 deletions(-) diff --git a/pyramid/tests/test_events.py b/pyramid/tests/test_events.py index f35083c02..3e9c959d9 100644 --- a/pyramid/tests/test_events.py +++ b/pyramid/tests/test_events.py @@ -9,13 +9,13 @@ class NewRequestEventTests(unittest.TestCase): def _makeOne(self, request): return self._getTargetClass()(request) - def test_class_implements(self): + def test_class_conforms_to_INewRequest(self): from pyramid.interfaces import INewRequest from zope.interface.verify import verifyClass klass = self._getTargetClass() verifyClass(INewRequest, klass) - def test_instance_implements(self): + def test_instance_conforms_to_INewRequest(self): from pyramid.interfaces import INewRequest from zope.interface.verify import verifyObject request = DummyRequest() @@ -35,13 +35,13 @@ class NewResponseEventTests(unittest.TestCase): def _makeOne(self, request, response): return self._getTargetClass()(request, response) - def test_class_implements(self): + def test_class_conforms_to_INewResponse(self): from pyramid.interfaces import INewResponse from zope.interface.verify import verifyClass klass = self._getTargetClass() verifyClass(INewResponse, klass) - def test_instance_implements(self): + def test_instance_conforms_to_INewResponse(self): from pyramid.interfaces import INewResponse from zope.interface.verify import verifyObject request = DummyRequest() @@ -57,68 +57,72 @@ class NewResponseEventTests(unittest.TestCase): self.assertEqual(inst.response, response) class ApplicationCreatedEventTests(unittest.TestCase): - def test_alias_object_implements(self): - from pyramid.events import WSGIApplicationCreatedEvent - event = WSGIApplicationCreatedEvent(object()) - from pyramid.interfaces import IWSGIApplicationCreatedEvent - from pyramid.interfaces import IApplicationCreated - from zope.interface.verify import verifyObject - verifyObject(IWSGIApplicationCreatedEvent, event) - verifyObject(IApplicationCreated, event) + def _getTargetClass(self): + from pyramid.events import ApplicationCreated + return ApplicationCreated - def test_alias_class_implements(self): - from pyramid.events import WSGIApplicationCreatedEvent - from pyramid.interfaces import IWSGIApplicationCreatedEvent + def _makeOne(self, context=object()): + return self._getTargetClass()(context) + + def test_class_conforms_to_IApplicationCreated(self): from pyramid.interfaces import IApplicationCreated from zope.interface.verify import verifyClass - verifyClass(IWSGIApplicationCreatedEvent, WSGIApplicationCreatedEvent) - verifyClass(IApplicationCreated, WSGIApplicationCreatedEvent) + verifyClass(IApplicationCreated, self._getTargetClass()) - def test_object_implements(self): - from pyramid.events import ApplicationCreated - event = ApplicationCreated(object()) + def test_object_conforms_to_IApplicationCreated(self): from pyramid.interfaces import IApplicationCreated from zope.interface.verify import verifyObject - verifyObject(IApplicationCreated, event) + verifyObject(IApplicationCreated, self._makeOne()) - def test_class_implements(self): - from pyramid.events import ApplicationCreated - from pyramid.interfaces import IApplicationCreated +class WSGIApplicationCreatedEventTests(ApplicationCreatedEventTests): + def _getTargetClass(self): + from pyramid.events import WSGIApplicationCreatedEvent + return WSGIApplicationCreatedEvent + + def test_class_conforms_to_IWSGIApplicationCreatedEvent(self): + from pyramid.interfaces import IWSGIApplicationCreatedEvent from zope.interface.verify import verifyClass - verifyClass(IApplicationCreated, ApplicationCreated) + verifyClass(IWSGIApplicationCreatedEvent, self._getTargetClass()) + + def test_object_conforms_to_IWSGIApplicationCreatedEvent(self): + from pyramid.interfaces import IWSGIApplicationCreatedEvent + from zope.interface.verify import verifyObject + verifyObject(IWSGIApplicationCreatedEvent, self._makeOne()) class ContextFoundEventTests(unittest.TestCase): - def test_alias_class_implements(self): + def _getTargetClass(self): + from pyramid.events import ContextFound + return ContextFound + + def _makeOne(self, request=None): + if request is None: + request = DummyRequest() + return self._getTargetClass()(request) + + def test_class_conforms_to_IContextFound(self): from zope.interface.verify import verifyClass - from pyramid.events import AfterTraversal - from pyramid.interfaces import IAfterTraversal from pyramid.interfaces import IContextFound - verifyClass(IAfterTraversal, AfterTraversal) - verifyClass(IContextFound, AfterTraversal) + verifyClass(IContextFound, self._getTargetClass()) - def test_alias_instance_implements(self): + def test_instance_conforms_to_IContextFound(self): from zope.interface.verify import verifyObject - from pyramid.events import AfterTraversal - from pyramid.interfaces import IAfterTraversal from pyramid.interfaces import IContextFound - request = DummyRequest() - inst = AfterTraversal(request) - verifyObject(IAfterTraversal, inst) - verifyObject(IContextFound, inst) + verifyObject(IContextFound, self._makeOne()) - def test_class_implements(self): +class AfterTraversalEventTests(ContextFoundEventTests): + def _getTargetClass(self): + from pyramid.events import AfterTraversal + return AfterTraversal + + def test_class_conforms_to_IAfterTraversal(self): from zope.interface.verify import verifyClass - from pyramid.events import ContextFound - from pyramid.interfaces import IContextFound - verifyClass(IContextFound, ContextFound) + from pyramid.interfaces import IAfterTraversal + verifyClass(IAfterTraversal, self._getTargetClass()) - def test_instance_implements(self): + def test_instance_conforms_to_IAfterTraversal(self): from zope.interface.verify import verifyObject - from pyramid.events import ContextFound - from pyramid.interfaces import IContextFound - request = DummyRequest() - inst = ContextFound(request) - verifyObject(IContextFound, inst) + from pyramid.interfaces import IAfterTraversal + verifyObject(IAfterTraversal, self._makeOne()) class TestSubscriber(unittest.TestCase): def setUp(self): diff --git a/pyramid/tests/test_path.py b/pyramid/tests/test_path.py index 42b38d785..ccc56fb0d 100644 --- a/pyramid/tests/test_path.py +++ b/pyramid/tests/test_path.py @@ -259,17 +259,15 @@ class TestPkgResourcesAssetDescriptor(unittest.TestCase): def _makeOne(self, pkg='pyramid.tests', path='test_asset.py'): return self._getTargetClass()(pkg, path) - def test_class_implements(self): + def test_class_conforms_to_IAssetDescriptor(self): from pyramid.interfaces import IAssetDescriptor from zope.interface.verify import verifyClass - klass = self._getTargetClass() - verifyClass(IAssetDescriptor, klass) + verifyClass(IAssetDescriptor, self._getTargetClass()) - def test_instance_implements(self): + def test_instance_conforms_to_IAssetDescriptor(self): from pyramid.interfaces import IAssetDescriptor from zope.interface.verify import verifyObject - inst = self._makeOne() - verifyObject(IAssetDescriptor, inst) + verifyObject(IAssetDescriptor, self._makeOne()) def test_absspec(self): inst = self._makeOne() @@ -316,17 +314,15 @@ class TestFSAssetDescriptor(unittest.TestCase): def _makeOne(self, path=os.path.join(here, 'test_asset.py')): return self._getTargetClass()(path) - def test_class_implements(self): + def test_class_conforms_to_IAssetDescriptor(self): from pyramid.interfaces import IAssetDescriptor from zope.interface.verify import verifyClass - klass = self._getTargetClass() - verifyClass(IAssetDescriptor, klass) + verifyClass(IAssetDescriptor, self._getTargetClass()) - def test_instance_implements(self): + def test_instance_conforms_to_IAssetDescriptor(self): from pyramid.interfaces import IAssetDescriptor from zope.interface.verify import verifyObject - inst = self._makeOne() - verifyObject(IAssetDescriptor, inst) + verifyObject(IAssetDescriptor, self._makeOne()) def test_absspec(self): inst = self._makeOne() diff --git a/pyramid/tests/test_request.py b/pyramid/tests/test_request.py index 6d5131013..a95d614f9 100644 --- a/pyramid/tests/test_request.py +++ b/pyramid/tests/test_request.py @@ -18,13 +18,15 @@ class TestRequest(unittest.TestCase): def tearDown(self): testing.tearDown() - def _makeOne(self, environ): - return self._getTargetClass()(environ) - def _getTargetClass(self): from pyramid.request import Request return Request + def _makeOne(self, environ=None): + if environ is None: + environ = {} + return self._getTargetClass()(environ) + def _registerResourceURL(self): from pyramid.interfaces import IResourceURL from zope.interface import Interface @@ -36,6 +38,17 @@ class TestRequest(unittest.TestCase): DummyResourceURL, (Interface, Interface), IResourceURL) + def test_class_conforms_to_IRequest(self): + from zope.interface.verify import verifyClass + from pyramid.interfaces import IRequest + verifyClass(IRequest, self._getTargetClass()) + klass = self._getTargetClass() + + def test_instance_conforms_to_IRequest(self): + from zope.interface.verify import verifyObject + from pyramid.interfaces import IRequest + verifyObject(IRequest, self._makeOne()) + def test_charset_defaults_to_utf8(self): r = self._makeOne({'PATH_INFO':'/'}) self.assertEqual(r.charset, 'UTF-8') @@ -61,25 +74,15 @@ class TestRequest(unittest.TestCase): request.charset = None self.assertEqual(request.GET['la'], text_(b'La Pe\xf1a')) - def test_class_implements(self): - from pyramid.interfaces import IRequest - klass = self._getTargetClass() - self.assertTrue(IRequest.implementedBy(klass)) - - def test_instance_provides(self): - from pyramid.interfaces import IRequest - inst = self._makeOne({}) - self.assertTrue(IRequest.providedBy(inst)) - def test_tmpl_context(self): from pyramid.request import TemplateContext - inst = self._makeOne({}) + inst = self._makeOne() result = inst.tmpl_context self.assertEqual(result.__class__, TemplateContext) def test_session_configured(self): from pyramid.interfaces import ISessionFactory - inst = self._makeOne({}) + inst = self._makeOne() def factory(request): return 'orangejuice' self.config.registry.registerUtility(factory, ISessionFactory) @@ -88,12 +91,12 @@ class TestRequest(unittest.TestCase): self.assertEqual(inst.__dict__['session'], 'orangejuice') def test_session_not_configured(self): - inst = self._makeOne({}) + inst = self._makeOne() inst.registry = self.config.registry self.assertRaises(AttributeError, getattr, inst, 'session') def test_setattr_and_getattr_dotnotation(self): - inst = self._makeOne({}) + inst = self._makeOne() inst.foo = 1 self.assertEqual(inst.foo, 1) @@ -105,7 +108,7 @@ class TestRequest(unittest.TestCase): self.assertEqual(environ, {}) # make sure we're not using adhoc attrs def test_add_response_callback(self): - inst = self._makeOne({}) + inst = self._makeOne() self.assertEqual(inst.response_callbacks, ()) def callback(request, response): """ """ @@ -115,7 +118,7 @@ class TestRequest(unittest.TestCase): self.assertEqual(inst.response_callbacks, [callback, callback]) def test__process_response_callbacks(self): - inst = self._makeOne({}) + inst = self._makeOne() def callback1(request, response): request.called1 = True response.called1 = True @@ -132,7 +135,7 @@ class TestRequest(unittest.TestCase): self.assertEqual(inst.response_callbacks, []) def test_add_finished_callback(self): - inst = self._makeOne({}) + inst = self._makeOne() self.assertEqual(inst.finished_callbacks, ()) def callback(request): """ """ @@ -142,7 +145,7 @@ class TestRequest(unittest.TestCase): self.assertEqual(inst.finished_callbacks, [callback, callback]) def test__process_finished_callbacks(self): - inst = self._makeOne({}) + inst = self._makeOne() def callback1(request): request.called1 = True def callback2(request): @@ -219,13 +222,13 @@ class TestRequest(unittest.TestCase): ('pyramid.tests:static/foo.css', request, {}) ) def test_is_response_false(self): - request = self._makeOne({}) + request = self._makeOne() request.registry = self.config.registry self.assertEqual(request.is_response('abc'), False) def test_is_response_false_adapter_is_not_self(self): from pyramid.interfaces import IResponse - request = self._makeOne({}) + request = self._makeOne() request.registry = self.config.registry def adapter(ob): return object() @@ -237,7 +240,7 @@ class TestRequest(unittest.TestCase): def test_is_response_adapter_true(self): from pyramid.interfaces import IResponse - request = self._makeOne({}) + request = self._makeOne() request.registry = self.config.registry class Foo(object): pass @@ -277,7 +280,7 @@ class TestRequest(unittest.TestCase): self.assertRaises(ValueError, getattr, request, 'json_body') def test_set_property(self): - request = self._makeOne({}) + request = self._makeOne() opts = [2, 1] def connect(obj): return opts.pop() @@ -286,7 +289,7 @@ class TestRequest(unittest.TestCase): self.assertEqual(2, request.db) def test_set_property_reify(self): - request = self._makeOne({}) + request = self._makeOne() opts = [2, 1] def connect(obj): return opts.pop() -- cgit v1.2.3 From d986123332806aadc43b1daab396ff5200ec10ce Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 6 Aug 2012 23:56:24 -0400 Subject: move stuff from config.util to registry so it can be a set of API (which are now documented), resolve deferred discriminators in introspectable.register so that a directive can depend on a deferred discriminator, put head-adding code in predicate instead of in add_view itself --- docs/api/registry.rst | 14 ++++++++++++ pyramid/config/__init__.py | 8 ++----- pyramid/config/predicates.py | 6 ++++- pyramid/config/routes.py | 2 +- pyramid/config/util.py | 12 ++-------- pyramid/config/views.py | 18 +++++++-------- pyramid/registry.py | 33 ++++++++++++++++++++++++++++ pyramid/tests/test_config/test_predicates.py | 4 ++++ pyramid/util.py | 1 + 9 files changed, 70 insertions(+), 28 deletions(-) diff --git a/docs/api/registry.rst b/docs/api/registry.rst index e62e2ba6f..1d5d52248 100644 --- a/docs/api/registry.rst +++ b/docs/api/registry.rst @@ -38,3 +38,17 @@ This class is new as of :app:`Pyramid` 1.3. +.. autoclass:: Deferred + + This class is new as of :app:`Pyramid` 1.4. + +.. autofunction:: undefer + + This function is new as of :app:`Pyramid` 1.4. + +.. autoclass:: predvalseq + + This class is new as of :app:`Pyramid` 1.4. + + + diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index 7e6649c14..a45dca255 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -45,6 +45,8 @@ from pyramid.registry import ( Introspectable, Introspector, Registry, + Deferred, + undefer, ) from pyramid.router import Router @@ -72,7 +74,6 @@ from pyramid.config.tweens import TweensConfiguratorMixin from pyramid.config.util import ( action_method, ActionInfo, - Deferred, ) from pyramid.config.views import ViewsConfiguratorMixin from pyramid.config.zca import ZCAConfiguratorMixin @@ -1067,11 +1068,6 @@ class ActionState(object): if clear: del self.actions[:] -def undefer(v): - if isinstance(v, Deferred): - v = v.resolve() - return v - # this function is licensed under the ZPL (stolen from Zope) def resolveConflicts(actions): """Resolve conflicting actions diff --git a/pyramid/config/predicates.py b/pyramid/config/predicates.py index 311d47860..9e0ee28c1 100644 --- a/pyramid/config/predicates.py +++ b/pyramid/config/predicates.py @@ -29,7 +29,11 @@ class XHRPredicate(object): class RequestMethodPredicate(object): def __init__(self, val, config): - self.val = as_sorted_tuple(val) + request_method = as_sorted_tuple(val) + if 'GET' in request_method and 'HEAD' not in request_method: + # GET implies HEAD too + request_method = as_sorted_tuple(request_method + ('HEAD',)) + self.val = request_method def text(self): return 'request_method = %s' % (','.join(self.val)) diff --git a/pyramid/config/routes.py b/pyramid/config/routes.py index d8ce4804c..8f7f3612b 100644 --- a/pyramid/config/routes.py +++ b/pyramid/config/routes.py @@ -10,6 +10,7 @@ from pyramid.interfaces import ( ) from pyramid.exceptions import ConfigurationError +from pyramid.registry import predvalseq from pyramid.request import route_request_iface from pyramid.urldispatch import RoutesMapper @@ -17,7 +18,6 @@ from pyramid.config.util import ( action_method, as_sorted_tuple, PredicateList, - predvalseq, ) from pyramid.config import predicates diff --git a/pyramid/config/util.py b/pyramid/config/util.py index ce4a4a728..cabcab649 100644 --- a/pyramid/config/util.py +++ b/pyramid/config/util.py @@ -11,6 +11,8 @@ from pyramid.compat import ( from pyramid.exceptions import ConfigurationError +from pyramid.registry import predvalseq + from hashlib import md5 MAX_ORDER = 1 << 30 @@ -294,13 +296,3 @@ class PredicateList(object): order = (MAX_ORDER - score) / (len(preds) + 1) return order, preds, phash.hexdigest() -class predvalseq(tuple): - pass - -class Deferred(object): - def __init__(self, func): - self.func = func - - def resolve(self): - return self.func() - diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 4491272d3..6fb598847 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -55,6 +55,11 @@ from pyramid.httpexceptions import ( HTTPNotFound, ) +from pyramid.registry import ( + predvalseq, + Deferred, + ) + from pyramid.security import NO_PERMISSION_REQUIRED from pyramid.static import static_view from pyramid.threadlocal import get_current_registry @@ -64,18 +69,17 @@ from pyramid.view import ( AppendSlashNotFoundViewFactory, ) -from pyramid.util import object_description +from pyramid.util import ( + object_description, + ) from pyramid.config import predicates from pyramid.config.util import ( - Deferred, DEFAULT_PHASH, MAX_ORDER, action_method, - as_sorted_tuple, PredicateList, - predvalseq, ) urljoin = urlparse.urljoin @@ -1029,12 +1033,6 @@ class ViewsConfiguratorMixin(object): raise ConfigurationError( 'request_type must be an interface, not %s' % request_type) - if request_method is not None: - request_method = as_sorted_tuple(request_method) - if 'GET' in request_method and 'HEAD' not in request_method: - # GET implies HEAD too - request_method = as_sorted_tuple(request_method + ('HEAD',)) - if context is None: context = for_ diff --git a/pyramid/registry.py b/pyramid/registry.py index f0f9c83ea..606251a8d 100644 --- a/pyramid/registry.py +++ b/pyramid/registry.py @@ -191,14 +191,20 @@ class Introspectable(dict): def unrelate(self, category_name, discriminator): self._relations.append((False, category_name, discriminator)) + def _assert_resolved(self): + assert undefer(self.discriminator) is self.discriminator + @property def discriminator_hash(self): + self._assert_resolved() return hash(self.discriminator) def __hash__(self): + self._assert_resolved() return hash((self.category_name,) + (self.discriminator,)) def __repr__(self): + self._assert_resolved() return '<%s category %r, discriminator %r>' % (self.__class__.__name__, self.category_name, self.discriminator) @@ -209,9 +215,11 @@ class Introspectable(dict): __bool__ = __nonzero__ # py3 def register(self, introspector, action_info): + self.discriminator = undefer(self.discriminator) self.action_info = action_info introspector.add(self) for relate, category_name, discriminator in self._relations: + discriminator = undefer(discriminator) if relate: method = introspector.relate else: @@ -221,4 +229,29 @@ class Introspectable(dict): (category_name, discriminator) ) +class Deferred(object): + """ Can be used by a third-party configuration extender to wrap a + :term:`discriminator` during configuration if an immediately hashable + discriminator cannot be computed because it relies on unresolved values. + The function should accept no arguments and should return a hashable + discriminator.""" + def __init__(self, func): + self.func = func + + def resolve(self): + return self.func() + +def undefer(v): + """ Function which accepts an object and returns it unless it is a + :class:`pyramid.registry.Deferred` instance. If it is an instance of + that class, its ``resolve`` method is called, and the result of the + method is returned.""" + if isinstance(v, Deferred): + v = v.resolve() + return v + +class predvalseq(tuple): + """ A subtype of tuple used to represent a sequence of predicate values """ + pass + global_registry = Registry('global') diff --git a/pyramid/tests/test_config/test_predicates.py b/pyramid/tests/test_config/test_predicates.py index 94e613715..e33a31458 100644 --- a/pyramid/tests/test_config/test_predicates.py +++ b/pyramid/tests/test_config/test_predicates.py @@ -33,6 +33,10 @@ class TestRequestMethodPredicate(unittest.TestCase): def _makeOne(self, val): from pyramid.config.predicates import RequestMethodPredicate return RequestMethodPredicate(val, None) + + def test_ctor_get_but_no_head(self): + inst = self._makeOne('GET') + self.assertEqual(inst.val, ('GET', 'HEAD')) def test___call___true_single(self): inst = self._makeOne('GET') diff --git a/pyramid/util.py b/pyramid/util.py index 7d5c97814..dabd84695 100644 --- a/pyramid/util.py +++ b/pyramid/util.py @@ -280,3 +280,4 @@ def shortrepr(object, closer): if len(r) > 100: r = r[:100] + ' ... %s' % closer return r + -- cgit v1.2.3 From 735abf49f936cedc845907516a3922cdffa83665 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 7 Aug 2012 00:08:59 -0400 Subject: note conflict behavior change --- CHANGES.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index ecffea5d9..e092b1545 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -41,6 +41,12 @@ Features See "Adding A Third Party View or Route Predicate" in the Hooks chapter for more information. + Note that changes made to support the above feature now means that only + actions registered using the same "order" can conflict with one another. + It used to be the case that actions registered at different orders could + potentially conflict, but to my knowledge nothing ever depended on this + behavior (it was a bit silly). + - Custom objects can be made easily JSON-serializable in Pyramid by defining a ``__json__`` method on the object's class. This method should return values natively serializable by ``json.dumps`` (such as ints, lists, -- cgit v1.2.3 From 73226aaacb52ad96f4d2e3653747b54f36fa9bc1 Mon Sep 17 00:00:00 2001 From: Blaise Laflamme Date: Thu, 9 Aug 2012 22:32:34 -0600 Subject: should return the uri and not the adjusted uri if not in collection --- pyramid/mako_templating.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/mako_templating.py b/pyramid/mako_templating.py index d1ee68878..16170aa0f 100644 --- a/pyramid/mako_templating.py +++ b/pyramid/mako_templating.py @@ -69,7 +69,7 @@ class PkgResourceTemplateLookup(TemplateLookup): pname, path = resolve_asset_spec(uri) srcfile = abspath_from_asset_spec(path, pname) if os.path.isfile(srcfile): - return self._load(srcfile, adjusted) + return self._load(srcfile, uri) raise exceptions.TopLevelLookupException( "Can not locate template for uri %r" % uri) return TemplateLookup.get_template(self, uri) -- cgit v1.2.3 From 6d1af7678ad0ce57c451ffe52764a15d49666b6a Mon Sep 17 00:00:00 2001 From: Blaise Laflamme Date: Thu, 9 Aug 2012 22:32:34 -0600 Subject: Revert "should return the uri and not the adjusted uri if not in collection" This reverts commit 918d54da858ae754eaaf82c351ddaee55bf19d99. --- pyramid/mako_templating.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/mako_templating.py b/pyramid/mako_templating.py index d1ee68878..16170aa0f 100644 --- a/pyramid/mako_templating.py +++ b/pyramid/mako_templating.py @@ -69,7 +69,7 @@ class PkgResourceTemplateLookup(TemplateLookup): pname, path = resolve_asset_spec(uri) srcfile = abspath_from_asset_spec(path, pname) if os.path.isfile(srcfile): - return self._load(srcfile, adjusted) + return self._load(srcfile, uri) raise exceptions.TopLevelLookupException( "Can not locate template for uri %r" % uri) return TemplateLookup.get_template(self, uri) -- cgit v1.2.3 From a961aa646ed175089066ff1d37d57bb6546956b5 Mon Sep 17 00:00:00 2001 From: Blaise Laflamme Date: Thu, 9 Aug 2012 23:22:30 -0600 Subject: manually revert back to adjusted instead of uri in mako template lookup --- pyramid/mako_templating.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/mako_templating.py b/pyramid/mako_templating.py index 16170aa0f..d1ee68878 100644 --- a/pyramid/mako_templating.py +++ b/pyramid/mako_templating.py @@ -69,7 +69,7 @@ class PkgResourceTemplateLookup(TemplateLookup): pname, path = resolve_asset_spec(uri) srcfile = abspath_from_asset_spec(path, pname) if os.path.isfile(srcfile): - return self._load(srcfile, uri) + return self._load(srcfile, adjusted) raise exceptions.TopLevelLookupException( "Can not locate template for uri %r" % uri) return TemplateLookup.get_template(self, uri) -- cgit v1.2.3 From 8463c3663cf0bc1b417c8417bedc2c5d6b78adbc Mon Sep 17 00:00:00 2001 From: Blaise Laflamme Date: Fri, 10 Aug 2012 14:20:27 -0600 Subject: new fix for mako adjust_uri --- pyramid/mako_templating.py | 16 +++++++++++----- pyramid/tests/test_mako_templating.py | 30 ++++++++++++++++++++---------- 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/pyramid/mako_templating.py b/pyramid/mako_templating.py index d1ee68878..489c1f11a 100644 --- a/pyramid/mako_templating.py +++ b/pyramid/mako_templating.py @@ -1,4 +1,5 @@ import os +import posixpath import re import sys import threading @@ -37,6 +38,16 @@ class PkgResourceTemplateLookup(TemplateLookup): isabs = os.path.isabs(uri) if (not isabs) and (':' in uri): return uri + if not(isabs) and ('$' in uri): + return uri.replace('$', ':') + if relativeto is not None: + relativeto = relativeto.replace('$', ':') + if not(':' in uri) and (':' in relativeto): + pkg, relto = relativeto.split(':') + _uri = posixpath.join(posixpath.dirname(relto), uri) + return '{0}:{1}'.format(pkg, _uri) + if not(':' in uri) and not(':' in relativeto): + return posixpath.join(posixpath.dirname(relativeto), uri) return TemplateLookup.adjust_uri(self, uri, relativeto) def get_template(self, uri): @@ -48,11 +59,6 @@ class PkgResourceTemplateLookup(TemplateLookup): specification syntax. """ - if '$' in uri: - # Checks if the uri is already adjusted and brings it back to - # an asset spec. Normally occurs with inherited templates or - # included components. - uri = uri.replace('$', ':') isabs = os.path.isabs(uri) if (not isabs) and (':' in uri): # Windows can't cope with colons in filenames, so we replace the diff --git a/pyramid/tests/test_mako_templating.py b/pyramid/tests/test_mako_templating.py index 46826d9dd..aced6c586 100644 --- a/pyramid/tests/test_mako_templating.py +++ b/pyramid/tests/test_mako_templating.py @@ -479,6 +479,26 @@ class TestPkgResourceTemplateLookup(unittest.TestCase): result = inst.adjust_uri('a:b', None) self.assertEqual(result, 'a:b') + def test_adjust_uri_asset_spec_with_modified_asset_spec(self): + inst = self._makeOne() + result = inst.adjust_uri('a$b', None) + self.assertEqual(result, 'a:b') + + def test_adjust_uri_not_asset_spec_with_relativeto_asset_spec(self): + inst = self._makeOne() + result = inst.adjust_uri('c', 'a:b') + self.assertEqual(result, 'a:c') + + def test_adjust_uri_not_asset_spec_with_relativeto_modified_asset_spec(self): + inst = self._makeOne() + result = inst.adjust_uri('c', 'a$b') + self.assertEqual(result, 'a:c') + + def test_adjust_uri_not_asset_spec_with_relativeto_not_asset_spec(self): + inst = self._makeOne() + result = inst.adjust_uri('b', '../a') + self.assertEqual(result, '../b') + def test_get_template_not_asset_spec(self): fixturedir = self.get_fixturedir() inst = self._makeOne(directories=[fixturedir]) @@ -499,16 +519,6 @@ class TestPkgResourceTemplateLookup(unittest.TestCase): finally: shutil.rmtree(tmpdir, ignore_errors=True) - def test_get_template_asset_spec_with_uri_adjusted(self): - inst = self._makeOne(filesystem_checks=True) - result = inst.get_template('pyramid.tests$fixtures/helloworld.mak') - self.assertFalse(result is None) - - def test_get_template_asset_spec_with_uri_not_adjusted(self): - inst = self._makeOne(filesystem_checks=True) - result = inst.get_template('pyramid.tests:fixtures/helloworld.mak') - self.assertFalse(result is None) - def test_get_template_asset_spec_missing(self): from mako.exceptions import TopLevelLookupException fixturedir = self.get_fixturedir() -- cgit v1.2.3 From 32178572428cafec64f73dc06cad5c02feaceba8 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 11 Aug 2012 02:18:21 -0600 Subject: get heading levels right --- docs/narr/introduction.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/narr/introduction.rst b/docs/narr/introduction.rst index b5fa6a9f7..7c0f9223f 100644 --- a/docs/narr/introduction.rst +++ b/docs/narr/introduction.rst @@ -803,7 +803,7 @@ within a function called when another user uses the See also :ref:`add_directive`. Programmatic Introspection --------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~~~ If you're building a large system that other users may plug code into, it's useful to be able to get an enumeration of what code they plugged in *at @@ -831,7 +831,7 @@ callable: See also :ref:`using_introspection`. Python 3 Compatibility ----------------------- +~~~~~~~~~~~~~~~~~~~~~~ Pyramid and most of its add-ons are Python 3 compatible. If you develop a Pyramid application today, you won't need to worry that five years from now -- cgit v1.2.3 From 8e789469552158f1af321e7ad5102e737f0d9f1d Mon Sep 17 00:00:00 2001 From: Roman Kozlovskyi Date: Sat, 11 Aug 2012 16:23:34 +0300 Subject: signed contributors agreement --- CONTRIBUTORS.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index c3995aaba..a2da7fbfd 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -176,3 +176,5 @@ Contributors - Marc Abramowitz, 2012/06/13 - Jeff Cook, 2012/06/16 + +- Roman Kozlovskyi, 2012/08/11 -- cgit v1.2.3 From bad3d5184bbc9a8bc42e9d15a611b7f9f84d8131 Mon Sep 17 00:00:00 2001 From: Kees Hink Date: Tue, 14 Aug 2012 18:45:04 +0300 Subject: Typo pwteens -> ptweens --- docs/narr/commandline.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/narr/commandline.rst b/docs/narr/commandline.rst index af53c1f78..3bdf8c5cd 100644 --- a/docs/narr/commandline.rst +++ b/docs/narr/commandline.rst @@ -349,7 +349,7 @@ setting) orderings using the ``ptweens`` command. Tween factories will show up represented by their standard Python dotted name in the ``ptweens`` output. -For example, here's the ``pwteens`` command run against a system +For example, here's the ``ptweens`` command run against a system configured without any explicit tweens: .. code-block:: text @@ -367,7 +367,7 @@ configured without any explicit tweens: 1 pyramid.tweens.excview_tween_factory excview - - MAIN -Here's the ``pwteens`` command run against a system configured *with* +Here's the ``ptweens`` command run against a system configured *with* explicit tweens defined in its ``development.ini`` file: .. code-block:: text -- cgit v1.2.3 From 8ec8e2d23fab2a9eebbb68d0e0b72a6c94c251aa Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 15 Aug 2012 04:05:34 -0400 Subject: - Anonymous predicate keyword list for all add-view-ish methods and decorators as well as add_route is now named "**predicates" rather than "**other_predicates". - Fix bug in add_view and add_route where pvals was not a copy of other_predicates (caused discriminators to not match up when computed elsewhere). - Various formatting nits fixed. - add_notfound_view and add_forbidden_view now accept **predicates. - view_config, notfound_view_config, forbidden_view_config, and view_defaults now accept **predicates. Their implementations were changed to avoid repetition. --- pyramid/config/routes.py | 25 +++---- pyramid/config/views.py | 112 ++++++++++++++++++++++++-------- pyramid/tests/test_config/test_views.py | 74 +++++++++++++++++++++ pyramid/tests/test_view.py | 6 +- pyramid/view.py | 68 +++++-------------- 5 files changed, 192 insertions(+), 93 deletions(-) diff --git a/pyramid/config/routes.py b/pyramid/config/routes.py index 8f7f3612b..18fe39e45 100644 --- a/pyramid/config/routes.py +++ b/pyramid/config/routes.py @@ -20,7 +20,7 @@ from pyramid.config.util import ( PredicateList, ) -from pyramid.config import predicates +import pyramid.config.predicates class RoutesConfiguratorMixin(object): @action_method @@ -49,7 +49,7 @@ class RoutesConfiguratorMixin(object): path=None, pregenerator=None, static=False, - **other_predicates): + **predicates): """ Add a :term:`route configuration` to the current configuration state, as well as possibly a :term:`view configuration` to be used to specify a :term:`view callable` @@ -259,7 +259,7 @@ class RoutesConfiguratorMixin(object): :ref:`custom_route_predicates` for more information about ``info``. - other_predicates + predicates Pass a key/value pair here to use a third-party predicate registered via @@ -420,7 +420,7 @@ class RoutesConfiguratorMixin(object): request_iface, IRouteRequest, name=name) def register_connect(): - pvals = other_predicates + pvals = predicates.copy() pvals.update( dict( xhr=xhr, @@ -513,15 +513,16 @@ class RoutesConfiguratorMixin(object): order=PHASE1_CONFIG) def add_default_route_predicates(self): + p = pyramid.config.predicates for (name, factory) in ( - ('xhr', predicates.XHRPredicate), - ('request_method', predicates.RequestMethodPredicate), - ('path_info', predicates.PathInfoPredicate), - ('request_param', predicates.RequestParamPredicate), - ('header', predicates.HeaderPredicate), - ('accept', predicates.AcceptPredicate), - ('custom', predicates.CustomPredicate), - ('traverse', predicates.TraversePredicate), + ('xhr', p.XHRPredicate), + ('request_method', p.RequestMethodPredicate), + ('path_info', p.PathInfoPredicate), + ('request_param', p.RequestParamPredicate), + ('header', p.HeaderPredicate), + ('accept', p.AcceptPredicate), + ('custom', p.CustomPredicate), + ('traverse', p.TraversePredicate), ): self.add_route_predicate(name, factory) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 6fb598847..1c4e20dd6 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -73,7 +73,7 @@ from pyramid.util import ( object_description, ) -from pyramid.config import predicates +import pyramid.config.predicates from pyramid.config.util import ( DEFAULT_PHASH, @@ -664,7 +664,7 @@ class ViewsConfiguratorMixin(object): mapper=None, http_cache=None, match_param=None, - **other_predicates): + **predicates): """ Add a :term:`view configuration` to the current configuration state. Arguments to ``add_view`` are broken down below into *predicate* arguments and *non-predicate* @@ -1002,7 +1002,7 @@ class ViewsConfiguratorMixin(object): ``True`` or ``False`` after doing arbitrary evaluation of the context and/or the request. - other_predicates + predicates Pass a key/value pair here to use a third-party predicate registered via @@ -1048,7 +1048,7 @@ class ViewsConfiguratorMixin(object): registry = self.registry) introspectables = [] - pvals = other_predicates + pvals = predicates.copy() pvals.update( dict( xhr=xhr, @@ -1102,7 +1102,7 @@ class ViewsConfiguratorMixin(object): decorator=decorator, ) ) - view_intr.update(**other_predicates) + view_intr.update(**predicates) introspectables.append(view_intr) predlist = self.view_predlist @@ -1313,7 +1313,7 @@ class ViewsConfiguratorMixin(object): """ Adds a view predicate factory. The associated view predicate can later be named as a keyword argument to :meth:`pyramid.config.Configurator.add_view` in the - ``other_predicates`` anonyous keyword argument dictionary. + ``predicates`` anonyous keyword argument dictionary. ``name`` should be the name of the predicate. It must be a valid Python identifier (it will be used as a keyword argument to @@ -1345,17 +1345,18 @@ class ViewsConfiguratorMixin(object): order=PHASE1_CONFIG) # must be registered before views added def add_default_view_predicates(self): + p = pyramid.config.predicates for (name, factory) in ( - ('xhr', predicates.XHRPredicate), - ('request_method', predicates.RequestMethodPredicate), - ('path_info', predicates.PathInfoPredicate), - ('request_param', predicates.RequestParamPredicate), - ('header', predicates.HeaderPredicate), - ('accept', predicates.AcceptPredicate), - ('containment', predicates.ContainmentPredicate), - ('request_type', predicates.RequestTypePredicate), - ('match_param', predicates.MatchParamPredicate), - ('custom', predicates.CustomPredicate), + ('xhr', p.XHRPredicate), + ('request_method', p.RequestMethodPredicate), + ('path_info', p.PathInfoPredicate), + ('request_param', p.RequestParamPredicate), + ('header', p.HeaderPredicate), + ('accept', p.AcceptPredicate), + ('containment', p.ContainmentPredicate), + ('request_type', p.RequestTypePredicate), + ('match_param', p.MatchParamPredicate), + ('custom', p.CustomPredicate), ): self.add_view_predicate(name, factory) @@ -1476,11 +1477,26 @@ class ViewsConfiguratorMixin(object): @action_method def add_forbidden_view( - self, view=None, attr=None, renderer=None, wrapper=None, - route_name=None, request_type=None, request_method=None, - request_param=None, containment=None, xhr=None, accept=None, - header=None, path_info=None, custom_predicates=(), decorator=None, - mapper=None, match_param=None): + self, + view=None, + attr=None, + renderer=None, + wrapper=None, + route_name=None, + request_type=None, + request_method=None, + request_param=None, + containment=None, + xhr=None, + accept=None, + header=None, + path_info=None, + custom_predicates=(), + decorator=None, + mapper=None, + match_param=None, + **predicates + ): """ Add a forbidden view to the current configuration state. The view will be called when Pyramid or application code raises a :exc:`pyramid.httpexceptions.HTTPForbidden` exception and the set of @@ -1497,12 +1513,23 @@ class ViewsConfiguratorMixin(object): All arguments have the same meaning as :meth:`pyramid.config.Configurator.add_view` and each predicate argument restricts the set of circumstances under which this notfound - view will be invoked. + view will be invoked. Unlike + :meth:`pyramid.config.Configurator.add_view`, this method will raise + an exception if passed ``name``, ``permission``, ``context``, + ``for_``, or ``http_cache`` keyword arguments. These argument values + make no sense in the context of a forbidden view. .. note:: This method is new as of Pyramid 1.3. """ + for arg in ('name', 'permission', 'context', 'for_', 'http_cache'): + if arg in predicates: + raise ConfigurationError( + '%s may not be used as an argument to add_forbidden_view' + % arg + ) + settings = dict( view=view, context=HTTPForbidden, @@ -1524,17 +1551,34 @@ class ViewsConfiguratorMixin(object): attr=attr, renderer=renderer, ) + settings.update(predicates) return self.add_view(**settings) set_forbidden_view = add_forbidden_view # deprecated sorta-bw-compat alias @action_method def add_notfound_view( - self, view=None, attr=None, renderer=None, wrapper=None, - route_name=None, request_type=None, request_method=None, - request_param=None, containment=None, xhr=None, accept=None, - header=None, path_info=None, custom_predicates=(), decorator=None, - mapper=None, match_param=None, append_slash=False): + self, + view=None, + attr=None, + renderer=None, + wrapper=None, + route_name=None, + request_type=None, + request_method=None, + request_param=None, + containment=None, + xhr=None, + accept=None, + header=None, + path_info=None, + custom_predicates=(), + decorator=None, + mapper=None, + match_param=None, + append_slash=False, + **predicates + ): """ Add a default notfound view to the current configuration state. The view will be called when Pyramid or application code raises an :exc:`pyramid.httpexceptions.HTTPForbidden` exception (e.g. when a @@ -1550,7 +1594,11 @@ class ViewsConfiguratorMixin(object): All arguments except ``append_slash`` have the same meaning as :meth:`pyramid.config.Configurator.add_view` and each predicate argument restricts the set of circumstances under which this notfound - view will be invoked. + view will be invoked. Unlike + :meth:`pyramid.config.Configurator.add_view`, this method will raise + an exception if passed ``name``, ``permission``, ``context``, + ``for_``, or ``http_cache`` keyword arguments. These argument values + make no sense in the context of a notfound view. If ``append_slash`` is ``True``, when this notfound view is invoked, and the current path info does not end in a slash, the notfound logic @@ -1564,6 +1612,13 @@ class ViewsConfiguratorMixin(object): This method is new as of Pyramid 1.3. """ + for arg in ('name', 'permission', 'context', 'for_', 'http_cache'): + if arg in predicates: + raise ConfigurationError( + '%s may not be used as an argument to add_notfound_view' + % arg + ) + settings = dict( view=view, context=HTTPNotFound, @@ -1583,6 +1638,7 @@ class ViewsConfiguratorMixin(object): route_name=route_name, permission=NO_PERMISSION_REQUIRED, ) + settings.update(predicates) if append_slash: view = self._derive_view(view, attr=attr, renderer=renderer) view = AppendSlashNotFoundViewFactory(view) diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index f2daf0c34..38f60d79b 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -1701,6 +1701,38 @@ class TestViewsConfigurationMixin(unittest.TestCase): result = view(None, request) self.assertEqual(result, 'OK') + def test_add_forbidden_view_allows_other_predicates(self): + from pyramid.renderers import null_renderer + config = self._makeOne(autocommit=True) + # doesnt blow up + config.add_view_predicate('dummy', DummyPredicate) + config.add_forbidden_view(renderer=null_renderer, dummy='abc') + + def test_add_forbidden_view_disallows_name(self): + config = self._makeOne(autocommit=True) + self.assertRaises(ConfigurationError, + config.add_forbidden_view, name='foo') + + def test_add_forbidden_view_disallows_permission(self): + config = self._makeOne(autocommit=True) + self.assertRaises(ConfigurationError, + config.add_forbidden_view, permission='foo') + + def test_add_forbidden_view_disallows_context(self): + config = self._makeOne(autocommit=True) + self.assertRaises(ConfigurationError, + config.add_forbidden_view, context='foo') + + def test_add_forbidden_view_disallows_for_(self): + config = self._makeOne(autocommit=True) + self.assertRaises(ConfigurationError, + config.add_forbidden_view, for_='foo') + + def test_add_forbidden_view_disallows_http_cache(self): + config = self._makeOne(autocommit=True) + self.assertRaises(ConfigurationError, + config.add_forbidden_view, http_cache='foo') + def test_add_notfound_view(self): from pyramid.renderers import null_renderer from zope.interface import implementedBy @@ -1716,6 +1748,38 @@ class TestViewsConfigurationMixin(unittest.TestCase): result = view(None, request) self.assertEqual(result, (None, request)) + def test_add_notfound_view_allows_other_predicates(self): + from pyramid.renderers import null_renderer + config = self._makeOne(autocommit=True) + # doesnt blow up + config.add_view_predicate('dummy', DummyPredicate) + config.add_notfound_view(renderer=null_renderer, dummy='abc') + + def test_add_notfound_view_disallows_name(self): + config = self._makeOne(autocommit=True) + self.assertRaises(ConfigurationError, + config.add_notfound_view, name='foo') + + def test_add_notfound_view_disallows_permission(self): + config = self._makeOne(autocommit=True) + self.assertRaises(ConfigurationError, + config.add_notfound_view, permission='foo') + + def test_add_notfound_view_disallows_context(self): + config = self._makeOne(autocommit=True) + self.assertRaises(ConfigurationError, + config.add_notfound_view, context='foo') + + def test_add_notfound_view_disallows_for_(self): + config = self._makeOne(autocommit=True) + self.assertRaises(ConfigurationError, + config.add_notfound_view, for_='foo') + + def test_add_notfound_view_disallows_http_cache(self): + config = self._makeOne(autocommit=True) + self.assertRaises(ConfigurationError, + config.add_notfound_view, http_cache='foo') + def test_add_notfound_view_append_slash(self): from pyramid.response import Response from pyramid.renderers import null_renderer @@ -3973,3 +4037,13 @@ class DummyViewDefaultsClass(object): pass def __call__(self): return 'OK' + +class DummyPredicate(object): + def __init__(self, val, config): + self.val = val + + def text(self): + return 'dummy' + + phash = text + diff --git a/pyramid/tests/test_view.py b/pyramid/tests/test_view.py index ee4994172..f63e17bd8 100644 --- a/pyramid/tests/test_view.py +++ b/pyramid/tests/test_view.py @@ -87,7 +87,7 @@ class Test_notfound_view_config(BaseTest, unittest.TestCase): config = call_venusian(venusian) settings = config.settings self.assertEqual(len(settings), 1) - self.assertEqual(len(settings[0]), 5) + self.assertEqual(len(settings[0]), 4) self.assertEqual(settings[0]['venusian'], venusian) self.assertEqual(settings[0]['view'], None) # comes from call_venusian self.assertEqual(settings[0]['attr'], 'view') @@ -368,6 +368,10 @@ class TestViewConfigDecorator(unittest.TestCase): self.assertEqual(decorator.mapper, 'mapper') self.assertEqual(decorator.decorator, 'decorator') self.assertEqual(decorator.match_param, 'match_param') + + def test_create_with_other_predicates(self): + decorator = self._makeOne(foo=1) + self.assertEqual(decorator.foo, 1) def test_call_function(self): decorator = self._makeOne() diff --git a/pyramid/view.py b/pyramid/view.py index 1df0849c0..12a2efde6 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -138,15 +138,6 @@ def render_view(context, request, name='', secure=True): return None return ''.join(iterable) -class _default(object): - def __nonzero__(self): - return False - __bool__ = __nonzero__ - def __repr__(self): # pragma: no cover - return '(default)' - -default = _default() - class view_config(object): """ A function, class or method :term:`decorator` which allows a developer to create view registrations nearer to a :term:`view @@ -174,12 +165,12 @@ class view_config(object): backwards compatibility purposes, as the name :class:`pyramid.view.bfg_view`. - The following arguments are supported to + The following keyword arguments are supported to :class:`pyramid.view.view_config`: ``context``, ``permission``, ``name``, ``request_type``, ``route_name``, ``request_method``, ``request_param``, ``containment``, ``xhr``, ``accept``, ``header``, ``path_info``, ``custom_predicates``, ``decorator``, ``mapper``, ``http_cache``, - and ``match_param``. + ``match_param``, and ``predicates``. The meanings of these arguments are the same as the arguments passed to :meth:`pyramid.config.Configurator.add_view`. If any argument is left @@ -190,21 +181,11 @@ class view_config(object): """ venusian = venusian # for testing injection - def __init__(self, name=default, request_type=default, for_=default, - permission=default, route_name=default, - request_method=default, request_param=default, - containment=default, attr=default, renderer=default, - wrapper=default, xhr=default, accept=default, - header=default, path_info=default, - custom_predicates=default, context=default, - decorator=default, mapper=default, http_cache=default, - match_param=default): - L = dict(locals()) # See issue #635 for dict() rationale - if (context is not default) or (for_ is not default): - L['context'] = context or for_ - for k, v in L.items(): - if k not in ('self', 'L') and v is not default: - setattr(self, k, v) + def __init__(self, **settings): + if 'for_' in settings: + if settings.get('context') is None: + settings['context'] = settings['for_'] + self.__dict__.update(settings) def __call__(self, wrapped): settings = self.__dict__.copy() @@ -326,7 +307,7 @@ class notfound_view_config(object): The notfound_view_config constructor accepts most of the same arguments as the constructor of :class:`pyramid.view.view_config`. It can be used in the same places, and behaves in largely the same way, except it always - registers a not found exception view instead of a "normal" view. + registers a not found exception view instead of a 'normal' view. Example: @@ -360,17 +341,8 @@ class notfound_view_config(object): venusian = venusian - def __init__(self, request_type=default, request_method=default, - route_name=default, request_param=default, attr=default, - renderer=default, containment=default, wrapper=default, - xhr=default, accept=default, header=default, - path_info=default, custom_predicates=default, - decorator=default, mapper=default, match_param=default, - append_slash=False): - L = dict(locals()) # See issue #635 for dict() rationale - for k, v in L.items(): - if k not in ('self', 'L') and v is not default: - self.__dict__[k] = v + def __init__(self, **settings): + self.__dict__.update(settings) def __call__(self, wrapped): settings = self.__dict__.copy() @@ -400,7 +372,7 @@ class forbidden_view_config(object): The forbidden_view_config constructor accepts most of the same arguments as the constructor of :class:`pyramid.view.view_config`. It can be used in the same places, and behaves in largely the same way, except it always - registers a forbidden exception view instead of a "normal" view. + registers a forbidden exception view instead of a 'normal' view. Example: @@ -413,9 +385,9 @@ class forbidden_view_config(object): def notfound(request): return Response('You are not allowed', status='401 Unauthorized') - All have the same meaning as :meth:`pyramid.view.view_config` and each - predicate argument restricts the set of circumstances under which this - notfound view will be invoked. + All arguments passed to this function have the same meaning as + :meth:`pyramid.view.view_config` and each predicate argument restricts + the set of circumstances under which this notfound view will be invoked. See :ref:`changing_the_forbidden_view` for detailed usage information. @@ -426,16 +398,8 @@ class forbidden_view_config(object): venusian = venusian - def __init__(self, request_type=default, request_method=default, - route_name=default, request_param=default, attr=default, - renderer=default, containment=default, wrapper=default, - xhr=default, accept=default, header=default, - path_info=default, custom_predicates=default, - decorator=default, mapper=default, match_param=default): - L = dict(locals()) # See issue #635 for dict() rationale - for k, v in L.items(): - if k not in ('self', 'L') and v is not default: - self.__dict__[k] = v + def __init__(self, **settings): + self.__dict__.update(settings) def __call__(self, wrapped): settings = self.__dict__.copy() -- cgit v1.2.3 From cdcea948c3f6f3abbd1772500029066e80825082 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 15 Aug 2012 12:57:29 -0400 Subject: note whitespace rules, explain setup.py dev --- HACKING.txt | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/HACKING.txt b/HACKING.txt index 38c263ed7..87d1422dc 100644 --- a/HACKING.txt +++ b/HACKING.txt @@ -30,11 +30,13 @@ checkout. $ env/bin/easy_install setuptools-git - Install Pyramid from the checkout into the virtualenv using ``setup.py - develop`` (running ``setup.py develop`` *must* be done while the current - working directory is the ``pyramid`` checkout directory):: + dev``. ``setup.py dev`` is an alias for "setup.py develop" which also + installs testing requirements such as nose and coverage. Running + ``setup.py dev`` *must* be done while the current working directory is the + ``pyramid`` checkout directory:: $ cd pyramid - $ ../env/bin/python setup.py develop + $ ../env/bin/python setup.py dev - At that point, you should be able to create new Pyramid projects by using ``pcreate``:: @@ -85,6 +87,9 @@ Coding Style 2 newlines between classes. But 80-column lines, in particular, are mandatory. +- Please do not remove trailing whitespace. Configure your editor to reduce + diff noise. + Running Tests -------------- @@ -110,8 +115,8 @@ Test Coverage - The codebase *must* have 100% test statement coverage after each commit. You can test coverage via ``tox -e coverage``, or alternately by installing - ``nose`` and ``coverage`` into your virtualenv, and running ``setup.py - nosetests --with-coverage``. + ``nose`` and ``coverage`` into your virtualenv (easiest via ``setup.py + dev``) , and running ``setup.py nosetests --with-coverage``. Documentation Coverage and Building HTML Documentation ------------------------------------------------------ -- cgit v1.2.3 From 6b180cbb77d6c5bee0e75220d93fc1800d1217df Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 15 Aug 2012 22:49:59 -0400 Subject: - An ``add_permission`` directive method was added to the Configurator. This directive registers a free-standing permission introspectable into the Pyramid introspection system. Frameworks built atop Pyramid can thus use the the ``permissions`` introspectable category data to build a comprehensive list of permissions supported by a running system. Before this method was added, permissions were already registered in this introspectable category as a side effect of naming them in an ``add_view`` call, this method just makes it possible to arrange for a permission to be put into the ``permissions`` introspectable category without naming it along with an associated view. Here's an example of usage of ``add_permission``:: config = Configurator() config.add_permission('view') --- CHANGES.txt | 15 +++++++++++++++ docs/api/config.rst | 1 + pyramid/config/security.py | 20 ++++++++++++++++++++ pyramid/tests/test_config/test_security.py | 9 +++++++++ 4 files changed, 45 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index e092b1545..f02925585 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -85,3 +85,18 @@ Features - When there is a predicate mismatch exception (seen when no view matches for a given request due to predicates not working), the exception now contains a textual description of the predicate which didn't match. + +- An ``add_permission`` directive method was added to the Configurator. This + directive registers a free-standing permission introspectable into the + Pyramid introspection system. Frameworks built atop Pyramid can thus use + the the ``permissions`` introspectable category data to build a + comprehensive list of permissions supported by a running system. Before + this method was added, permissions were already registered in this + introspectable category as a side effect of naming them in an ``add_view`` + call, this method just makes it possible to arrange for a permission to be + put into the ``permissions`` introspectable category without naming it + along with an associated view. Here's an example of usage of + ``add_permission``:: + + config = Configurator() + config.add_permission('view') diff --git a/docs/api/config.rst b/docs/api/config.rst index bc9e067b1..1b887988a 100644 --- a/docs/api/config.rst +++ b/docs/api/config.rst @@ -36,6 +36,7 @@ .. automethod:: set_authentication_policy .. automethod:: set_authorization_policy .. automethod:: set_default_permission + .. automethod:: add_permission :methodcategory:`Setting Request Properties` diff --git a/pyramid/config/security.py b/pyramid/config/security.py index e8ef1518d..567999cc4 100644 --- a/pyramid/config/security.py +++ b/pyramid/config/security.py @@ -137,3 +137,23 @@ class SecurityConfiguratorMixin(object): introspectables=(intr, perm_intr,)) + def add_permission(self, permission_name): + """ + A configurator directive which registers a free-standing + permission without associating it with a view callable. This can be + used so that the permission shows up in the introspectable data under + the ``permissions`` category (permissions mentioned via ``add_view`` + already end up in there). For example:: + + config = Configurator() + config.add_permission('view') + """ + intr = self.introspectable( + 'permissions', + permission_name, + permission_name, + 'permission' + ) + intr['value'] = permission_name + self.action(None, introspectables=(intr,)) + diff --git a/pyramid/tests/test_config/test_security.py b/pyramid/tests/test_config/test_security.py index d05d1d471..817f6ce02 100644 --- a/pyramid/tests/test_config/test_security.py +++ b/pyramid/tests/test_config/test_security.py @@ -89,3 +89,12 @@ class ConfiguratorSecurityMethodsTests(unittest.TestCase): self.assertEqual(config.registry.getUtility(IDefaultPermission), 'view') + def test_add_permission(self): + config = self._makeOne(autocommit=True) + config.add_permission('perm') + cat = config.registry.introspector.get_category('permissions') + self.assertEqual(len(cat), 1) + D = cat[0] + intr = D['introspectable'] + self.assertEqual(intr['value'], 'perm') + -- cgit v1.2.3