summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichael Merickel <michael@merickel.org>2012-08-16 00:59:18 -0500
committerMichael Merickel <michael@merickel.org>2012-08-16 00:59:18 -0500
commit717537cdd6611511f783542034f00cf0099d515e (patch)
tree1f0a28530836a647713c22f2dd91767270f42458
parenta54b5e46f63ff3154c5d9f191ad3b78a2f506a8a (diff)
parent6b180cbb77d6c5bee0e75220d93fc1800d1217df (diff)
downloadpyramid-717537cdd6611511f783542034f00cf0099d515e.tar.gz
pyramid-717537cdd6611511f783542034f00cf0099d515e.tar.bz2
pyramid-717537cdd6611511f783542034f00cf0099d515e.zip
Merge branch 'master' into feature.instance-properties
-rw-r--r--.gitmodules3
-rw-r--r--.travis.yml19
-rw-r--r--CHANGES.txt73
-rw-r--r--CONTRIBUTORS.txt10
-rw-r--r--HACKING.txt50
-rw-r--r--TODO.txt12
-rw-r--r--docs/.gitignore1
-rw-r--r--docs/Makefile6
m---------docs/_themes0
-rw-r--r--docs/api/config.rst3
-rw-r--r--docs/api/registry.rst14
-rw-r--r--docs/api/renderers.rst2
-rw-r--r--docs/conf.py16
-rw-r--r--docs/designdefense.rst112
-rw-r--r--docs/glossary.rst16
-rw-r--r--docs/index.rst12
-rw-r--r--docs/narr/advconfig.rst2
-rw-r--r--docs/narr/commandline.rst15
-rw-r--r--docs/narr/firstapp.rst3
-rw-r--r--docs/narr/hooks.rst134
-rw-r--r--docs/narr/introduction.rst49
-rw-r--r--docs/narr/introspector.rst2
-rw-r--r--docs/narr/logging.rst7
-rw-r--r--docs/narr/project.rst14
-rw-r--r--docs/narr/renderers.rst69
-rw-r--r--docs/narr/sessions.rst13
-rw-r--r--docs/narr/startup.rst14
-rw-r--r--docs/narr/templates.rst16
-rw-r--r--docs/narr/urldispatch.rst8
-rw-r--r--docs/tutorials/modwsgi/index.rst14
-rw-r--r--docs/tutorials/wiki/definingviews.rst92
-rw-r--r--docs/tutorials/wiki/design.rst19
-rw-r--r--docs/tutorials/wiki/installation.rst7
-rw-r--r--docs/tutorials/wiki2/authorization.rst2
-rw-r--r--docs/tutorials/wiki2/basiclayout.rst4
-rw-r--r--docs/tutorials/wiki2/definingviews.rst92
-rw-r--r--docs/tutorials/wiki2/design.rst17
-rw-r--r--docs/tutorials/wiki2/installation.rst10
-rw-r--r--docs/tutorials/wiki2/src/authorization/development.ini2
-rw-r--r--docs/tutorials/wiki2/src/authorization/production.ini2
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/development.ini2
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/production.ini2
-rw-r--r--docs/tutorials/wiki2/src/models/development.ini2
-rw-r--r--docs/tutorials/wiki2/src/models/production.ini2
-rw-r--r--docs/tutorials/wiki2/src/tests/development.ini2
-rw-r--r--docs/tutorials/wiki2/src/tests/production.ini2
-rw-r--r--docs/tutorials/wiki2/src/views/development.ini2
-rw-r--r--docs/tutorials/wiki2/src/views/production.ini2
-rw-r--r--pyramid/config/__init__.py159
-rw-r--r--pyramid/config/predicates.py228
-rw-r--r--pyramid/config/routes.py109
-rw-r--r--pyramid/config/security.py20
-rw-r--r--pyramid/config/tweens.py114
-rw-r--r--pyramid/config/util.py459
-rw-r--r--pyramid/config/views.py371
-rw-r--r--pyramid/events.py32
-rw-r--r--pyramid/interfaces.py9
-rw-r--r--pyramid/mako_templating.py55
-rw-r--r--pyramid/registry.py33
-rw-r--r--pyramid/renderers.py151
-rw-r--r--pyramid/scaffolds/alchemy/README.txt_tmpl2
-rw-r--r--pyramid/scaffolds/alchemy/development.ini_tmpl2
-rw-r--r--pyramid/scaffolds/alchemy/production.ini_tmpl2
-rw-r--r--pyramid/scaffolds/alchemy/setup.py_tmpl2
-rw-r--r--pyramid/scaffolds/starter/setup.py_tmpl2
-rw-r--r--pyramid/scaffolds/zodb/setup.py_tmpl2
-rw-r--r--pyramid/scripts/proutes.py6
-rw-r--r--pyramid/scripts/pviews.py8
-rw-r--r--pyramid/static.py12
-rw-r--r--pyramid/testing.py3
-rw-r--r--pyramid/tests/fixtures/components.mak3
-rw-r--r--pyramid/tests/fixtures/hellocompo.mak3
-rw-r--r--pyramid/tests/test_config/test_init.py59
-rw-r--r--pyramid/tests/test_config/test_predicates.py268
-rw-r--r--pyramid/tests/test_config/test_routes.py2
-rw-r--r--pyramid/tests/test_config/test_security.py9
-rw-r--r--pyramid/tests/test_config/test_tweens.py46
-rw-r--r--pyramid/tests/test_config/test_util.py376
-rw-r--r--pyramid/tests/test_config/test_views.py126
-rw-r--r--pyramid/tests/test_events.py98
-rw-r--r--pyramid/tests/test_mako_templating.py76
-rw-r--r--pyramid/tests/test_path.py20
-rw-r--r--pyramid/tests/test_renderers.py52
-rw-r--r--pyramid/tests/test_request.py55
-rw-r--r--pyramid/tests/test_response.py2
-rw-r--r--pyramid/tests/test_scaffolds/test_copydir.py14
-rw-r--r--pyramid/tests/test_scripts/test_pviews.py4
-rw-r--r--pyramid/tests/test_static.py66
-rw-r--r--pyramid/tests/test_testing.py3
-rw-r--r--pyramid/tests/test_url.py27
-rw-r--r--pyramid/tests/test_urldispatch.py8
-rw-r--r--pyramid/tests/test_view.py19
-rw-r--r--pyramid/url.py10
-rw-r--r--pyramid/urldispatch.py4
-rw-r--r--pyramid/util.py1
-rw-r--r--pyramid/view.py72
-rw-r--r--setup.cfg2
-rw-r--r--setup.py23
-rw-r--r--tox.ini10
99 files changed, 2931 insertions, 1280 deletions
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/.travis.yml b/.travis.yml
new file mode 100644
index 000000000..490fd2df7
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,19 @@
+language: python
+
+python:
+ - 2.6
+ - 2.7
+ - pypy
+ - 3.2
+
+# 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
+
+notifications:
+ email:
+ - pyramid-checkins@lists.repoze.org
diff --git a/CHANGES.txt b/CHANGES.txt
index 337754162..f02925585 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -9,19 +9,56 @@ 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
+
+- 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.
+
+- 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
--------
+- 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.
+
+ 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,
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
- ``@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
@@ -29,3 +66,37 @@ 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.
+
+- 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', {}).
+
+- 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/CONTRIBUTORS.txt b/CONTRIBUTORS.txt
index 365e3e455..a2da7fbfd 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
@@ -170,3 +170,11 @@ Contributors
- Steve Piercy, 2012/03/27
- Wayne Witzel III, 2012/03/27
+
+- Marin Rukavina, 2012/05/03
+
+- Marc Abramowitz, 2012/06/13
+
+- Jeff Cook, 2012/06/16
+
+- Roman Kozlovskyi, 2012/08/11
diff --git a/HACKING.txt b/HACKING.txt
index 593e89ac1..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,26 +115,37 @@ 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
+------------------------------------------------------
-Documentation Coverage
-----------------------
+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 (where ``$yourvenv`` refers to the virtualenv you're
+using to develop Pyramid):
-- To build and review docs:
+1. Run ``$yourvenv/bin/python setup.py dev docs``. This will cause Sphinx
+ and all development requirements to be installed in your virtualenv.
- 1. Install ``tests_require`` dependencies from Pyramid's setup.py into your
- virtualenv.
+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.
- 2. From the ``docs`` directory of the Pyramid checkout run ``make html
- SPHINXBUILD=/path/to/your/virtualenv/bin/sphinx-build``.
+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 _build/html/index.html file to see the resulting rendering.
+4. Open the ``docs/_build/html/index.html`` file to see the resulting HTML
+ rendering.
Change Log
----------
diff --git a/TODO.txt b/TODO.txt
index 544ad0b4e..4b4f48499 100644
--- a/TODO.txt
+++ b/TODO.txt
@@ -4,6 +4,13 @@ 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.
+
- Have action methods return their discriminators.
- Add docs about upgrading between Pyramid versions (e.g. how to see
@@ -64,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)"?
@@ -123,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
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
+Subproject f59f7bfce5259f50fbb67b9040c03ecb080130b
diff --git a/docs/api/config.rst b/docs/api/config.rst
index cd58e74d3..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`
@@ -66,6 +67,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/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/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/conf.py b/docs/conf.py
index db972261d..80ee0d2e5 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'
]
@@ -132,18 +133,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'))
diff --git a/docs/designdefense.rst b/docs/designdefense.rst
index bbcf9c2ec..d896022e6 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
-<http://www.coactivate.org/projects/topp-engineering/blog/2008/10/20/what-bothers-me-about-the-component-architecture/>`_,
-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
--------------------------------
@@ -1647,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
diff --git a/docs/glossary.rst b/docs/glossary.rst
index 60920a73a..ba3203f89 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 <http://wsgi.org/>`_. This is a
+ `Web Server Gateway Interface <http://www.wsgi.org/>`_. 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 <http://wsgi.org>`_
+ encoding, and other functions. See `WSGI.org <http://www.wsgi.org>`_
or `PyPI <http://python.org/pypi>`_ to find middleware for your
application.
@@ -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
@@ -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/index.rst b/docs/index.rst
index 31c2fde6b..c84314274 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
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/commandline.rst b/docs/narr/commandline.rst
index 886e075e3..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
@@ -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``
@@ -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
@@ -718,7 +721,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(
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:
diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst
index b6e3dd163..2c15cd690 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')
@@ -289,6 +289,36 @@ 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.
+
+Suppose you return ``{'mykey': 'somevalue', 'mykey2': 'somevalue2'}`` from
+your view callable, like so:
+
+.. code-block:: python
+ :linenos:
+
+ 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:
+
+.. 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`.
@@ -625,7 +655,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:
@@ -1202,3 +1232,101 @@ 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
+--------------------------------------------
+
+.. 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
+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 = 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.
+
diff --git a/docs/narr/introduction.rst b/docs/narr/introduction.rst
index 8f7b17dc3..7c0f9223f 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
@@ -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
~~~~~~
@@ -801,6 +802,42 @@ 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`.
+
+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
~~~~~~~
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)
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
<http://docs.python.org/lib/logging-config-api.html>`_ 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.
diff --git a/docs/narr/project.rst b/docs/narr/project.rst
index 57073900f..1e2c225d2 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
+ 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.
@@ -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:
@@ -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
<http://peak.telecommunity.com/DevCenter/setuptools>`_ 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.
diff --git a/docs/narr/renderers.rst b/docs/narr/renderers.rst
index 34bee3c7f..57b5bc65b 100644
--- a/docs/narr/renderers.rst
+++ b/docs/narr/renderers.rst
@@ -177,13 +177,15 @@ using the API of the ``request.response`` attribute. See
.. index::
pair: renderer; JSON
+.. _json_renderer:
+
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
@@ -207,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.
@@ -223,9 +225,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
@@ -238,8 +240,9 @@ 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). It should accept a single additional argument, ``request``, which
+will be the active request object at render time.
.. code-block:: python
:linenos:
@@ -250,7 +253,7 @@ strings, and so forth).
def __init__(self, x):
self.x = x
- def __json__(self):
+ def __json__(self, request):
return {'x':self.x}
@view_config(renderer='json')
@@ -260,20 +263,40 @@ strings, and so forth).
# the JSON value returned by ``objects`` will be:
# [{"x": 1}, {"x": 2}]
-.. note::
+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 custom JSON renderer and adding adapters
+to handle custom types. The renderer will attempt to adapt non-serializable
+objects using the registered adapters. A short example follows:
+
+.. code-block:: python
+ :linenos:
- Honoring the ``__json__`` method of custom objects is a feature new in
- Pyramid 1.4.
+ from pyramid.renderers import JSON
-.. warning::
+ json_renderer = JSON()
+ 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.
+
+.. 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
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 <http://beaker.groovie.org/>`_ 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
-<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 <http://beaker.groovie.org/>`_ library as a backend.
+Beaker has support for file-based sessions, database based sessions, and
+encrypted cookie-based sessions. See `the pyramid_beaker documentation
+<http://docs.pylonsproject.org/projects/pyramid_beaker/en/latest/>`_ for more
+information about ``pyramid_beaker``.
.. index::
single: session factory (custom)
diff --git a/docs/narr/startup.rst b/docs/narr/startup.rst
index 8e28835af..f5c741f52 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.
@@ -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
diff --git a/docs/narr/templates.rst b/docs/narr/templates.rst
index 9db0b1c4d..860010a1a 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
<http://www.makotemplates.org/>`_ 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#bar.mak')
+ def my_view(request):
+ return {'project':'my project'}
+
.. index::
single: automatic reloading of templates
single: template automatic reload
diff --git a/docs/narr/urldispatch.rst b/docs/narr/urldispatch.rst
index f036ce94e..ecf3d026a 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')
@@ -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')
diff --git a/docs/tutorials/modwsgi/index.rst b/docs/tutorials/modwsgi/index.rst
index c2baa5bd8..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
@@ -99,7 +104,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 \
diff --git a/docs/tutorials/wiki/definingviews.rst b/docs/tutorials/wiki/definingviews.rst
index 12bfa8b84..529603546 100644
--- a/docs/tutorials/wiki/definingviews.rst
+++ b/docs/tutorials/wiki/definingviews.rst
@@ -229,60 +229,63 @@ 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.
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 "&gt;", 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
-------------
@@ -302,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/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/wiki/installation.rst b/docs/tutorials/wiki/installation.rst
index 63b30da5a..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
-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/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.)
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.
diff --git a/docs/tutorials/wiki2/definingviews.rst b/docs/tutorials/wiki2/definingviews.rst
index ac58e1e46..24ac4338d 100644
--- a/docs/tutorials/wiki2/definingviews.rst
+++ b/docs/tutorials/wiki2/definingviews.rst
@@ -226,52 +226,63 @@ 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.
+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
------------------------
-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 "&gt;", 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
-------------
@@ -339,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`.
diff --git a/docs/tutorials/wiki2/design.rst b/docs/tutorials/wiki2/design.rst
index 4481153a3..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
@@ -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
-------
diff --git a/docs/tutorials/wiki2/installation.rst b/docs/tutorials/wiki2/installation.rst
index 4ee2728c2..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".
@@ -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/config/__init__.py b/pyramid/config/__init__.py
index 52d7aca83..a45dca255 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
@@ -44,6 +45,8 @@ from pyramid.registry import (
Introspectable,
Introspector,
Registry,
+ Deferred,
+ undefer,
)
from pyramid.router import Router
@@ -353,6 +356,9 @@ class Configurator(
for name, renderer in DEFAULT_RENDERERS:
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)
self.add_view(exceptionresponse_view, context=IExceptionResponse)
@@ -547,6 +553,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:
@@ -1070,73 +1080,96 @@ 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(v):
+ n, v = 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(v):
+ n, v = 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
new file mode 100644
index 000000000..9e0ee28c1
--- /dev/null
+++ b/pyramid/config/predicates.py
@@ -0,0 +1,228 @@
+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 pyramid.util import object_description
+
+from .util import as_sorted_tuple
+
+class XHRPredicate(object):
+ def __init__(self, val, config):
+ self.val = bool(val)
+
+ def text(self):
+ return 'xhr = %s' % self.val
+
+ phash = text
+
+ def __call__(self, context, request):
+ return bool(request.is_xhr) is self.val
+
+class RequestMethodPredicate(object):
+ def __init__(self, val, config):
+ 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))
+
+ phash = text
+
+ def __call__(self, context, request):
+ return request.method in self.val
+
+class PathInfoPredicate(object):
+ def __init__(self, val, config):
+ 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,)
+
+ phash = text
+
+ def __call__(self, context, request):
+ return self.val.match(request.upath_info) is not None
+
+class RequestParamPredicate(object):
+ def __init__(self, val, config):
+ name = val
+ 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:
+ self._text = 'request_param %s = %s' % (name, v)
+ self.name = name
+ self.val = v
+
+ def text(self):
+ return self._text
+
+ phash = text
+
+ 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, config):
+ 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
+
+ phash = text
+
+ 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, config):
+ self.val = val
+
+ def text(self):
+ return 'accept = %s' % (self.val,)
+
+ phash = text
+
+ def __call__(self, context, request):
+ return self.val in request.accept
+
+class ContainmentPredicate(object):
+ def __init__(self, val, config):
+ self.val = config.maybe_dotted(val)
+
+ def text(self):
+ return 'containment = %s' % (self.val,)
+
+ phash = text
+
+ 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, config):
+ self.val = val
+
+ def text(self):
+ return 'request_type = %s' % (self.val,)
+
+ phash = text
+
+ def __call__(self, context, request):
+ return self.val.providedBy(request)
+
+class MatchParamPredicate(object):
+ def __init__(self, val, config):
+ if not is_nonstr_iter(val):
+ val = (val,)
+ val = sorted(val)
+ self.val = 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' % ','.join(
+ ['%s=%s' % (x,y) for x, y in self.reqs]
+ )
+
+ phash = text
+
+ 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, config):
+ self.func = func
+
+ def text(self):
+ 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):
+ return self.func(context, request)
+
+
+class TraversePredicate(object):
+ # 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
+
+ def text(self):
+ 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 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..18fe39e45 100644
--- a/pyramid/config/routes.py
+++ b/pyramid/config/routes.py
@@ -1,22 +1,27 @@
import warnings
from pyramid.interfaces import (
+ IPredicateList,
IRequest,
IRouteRequest,
IRoutesMapper,
+ PHASE1_CONFIG,
PHASE2_CONFIG,
)
from pyramid.exceptions import ConfigurationError
+from pyramid.registry import predvalseq
from pyramid.request import route_request_iface
from pyramid.urldispatch import RoutesMapper
from pyramid.config.util import (
action_method,
- make_predicates,
as_sorted_tuple,
+ PredicateList,
)
+import pyramid.config.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,
- ):
+ **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,15 @@ class RoutesConfiguratorMixin(object):
:ref:`custom_route_predicates` for more information about
``info``.
+ 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
.. warning::
@@ -351,17 +365,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 +420,24 @@ class RoutesConfiguratorMixin(object):
request_iface, IRouteRequest, name=name)
def register_connect():
+ pvals = predicates.copy()
+ 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 +466,66 @@ 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`.
+
+ 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(
+ '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):
+ p = pyramid.config.predicates
+ for (name, factory) in (
+ ('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)
+
def get_routes_mapper(self):
""" Return the :term:`routes mapper` object associated with
this configurator's :term:`registry`."""
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/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 b8d0f2319..cabcab649 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,10 +11,7 @@ from pyramid.compat import (
from pyramid.exceptions import ConfigurationError
-from pyramid.traversal import (
- find_interface,
- traversal_path,
- )
+from pyramid.registry import predvalseq
from hashlib import md5
@@ -64,232 +60,239 @@ 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.
-
- 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 = '<unknown custom predicate>'
- 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
- # 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,)
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.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):
+ 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))
+ 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):
+ if name in self.names:
+ self.remove(name)
+ 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.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)
+
+ 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
+
+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, 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 = []
+ preds = []
+ for n, (name, predicate_factory) in enumerate(ordered):
+ vals = kw.pop(name, None)
+ if vals is None: # XXX should this be a sentinel other than None?
+ continue
+ if not isinstance(vals, predvalseq):
+ vals = (vals,)
+ for val in vals:
+ pred = predicate_factory(val, config)
+ hashes = pred.phash()
+ if not is_nonstr_iter(hashes):
+ hashes = [hashes]
+ for h in hashes:
+ phash.update(bytes_(h))
+ 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(preds) + 1)
+ return order, preds, phash.hexdigest()
+
diff --git a/pyramid/config/views.py b/pyramid/config/views.py
index ad4df28d8..1c4e20dd6 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
@@ -20,6 +20,7 @@ from pyramid.interfaces import (
IException,
IExceptionViewClassifier,
IMultiView,
+ IPredicateList,
IRendererFactory,
IRequest,
IResponse,
@@ -54,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
@@ -63,14 +69,17 @@ from pyramid.view import (
AppendSlashNotFoundViewFactory,
)
-from pyramid.util import object_description
+from pyramid.util import (
+ object_description,
+ )
+
+import pyramid.config.predicates
from pyramid.config.util import (
DEFAULT_PHASH,
MAX_ORDER,
action_method,
- as_sorted_tuple,
- make_predicates,
+ PredicateList,
)
urljoin = urlparse.urljoin
@@ -272,20 +281,22 @@ 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):
- 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 preds:
+ 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))
+ preds))
predicate_wrapper.__predicated__ = checker
- predicate_wrapper.__predicates__ = predicates
+ predicate_wrapper.__predicates__ = preds
return predicate_wrapper
@wraps_view
@@ -385,8 +396,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')
@@ -629,13 +640,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=False, accept=None,
- header=None, path_info=None, custom_predicates=(),
- context=None, decorator=None, mapper=None, http_cache=None,
- match_param=None):
+ 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,
+ **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*
@@ -658,24 +687,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
@@ -684,8 +716,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
@@ -969,9 +1000,16 @@ 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.
+
+ 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)
@@ -995,18 +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',))
-
- 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_
@@ -1022,17 +1048,38 @@ 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 = predicates.copy()
+ 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=predvalseq(custom_predicates),
+ )
+ )
+
+ 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,
@@ -1055,9 +1102,15 @@ class ViewsConfiguratorMixin(object):
decorator=decorator,
)
)
+ view_intr.update(**predicates)
introspectables.append(view_intr)
+ predlist = self.view_predlist
def register(permission=permission, renderer=renderer):
+ # 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,
@@ -1082,21 +1135,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=predicates,
- 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
@@ -1201,19 +1261,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
@@ -1221,13 +1287,79 @@ 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)
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 associated view predicate can
+ later be named as a keyword argument to
+ :meth:`pyramid.config.Configurator.add_view` in the
+ ``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`` 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(
+ '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):
+ p = pyramid.config.predicates
+ for (name, factory) in (
+ ('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)
+
def derive_view(self, view, attr=None, renderer=None):
"""
Create a :term:`view callable` using the function, instance,
@@ -1345,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
@@ -1366,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,
@@ -1393,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
@@ -1419,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
@@ -1433,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,
@@ -1452,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/events.py b/pyramid/events.py
index e181ef33f..db274823c 100644
--- a/pyramid/events.py
+++ b/pyramid/events.py
@@ -200,10 +200,34 @@ 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.
+
+ 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': 'somevalue'} is returned from the view
+ print(event.rendering_val['mykey'])
+
+ 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.
+
For a description of the values present in the renderer globals dictionary,
see :ref:`renderer_system_values`.
diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py
index d7aae4f9f..042b4487b 100644
--- a/pyramid/interfaces.py
+++ b/pyramid/interfaces.py
@@ -1109,6 +1109,15 @@ 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.
+ """
+
+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/mako_templating.py b/pyramid/mako_templating.py
index 208e54bf5..489c1f11a 100644
--- a/pyramid/mako_templating.py
+++ b/pyramid/mako_templating.py
@@ -1,4 +1,6 @@
import os
+import posixpath
+import re
import sys
import threading
@@ -36,16 +38,26 @@ 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):
"""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.
-
+
"""
isabs = os.path.isabs(uri)
if (not isabs) and (':' in uri):
@@ -69,14 +81,20 @@ 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):
self.settings_prefix = settings_prefix
def __call__(self, info):
- path = info.name
+ p = re.compile(
+ r'(?P<asset>[\w_.:/]+)'
+ r'(?:\#(?P<defname>[\w_]+))?'
+ r'(\.(?P<ext>.*))'
+ )
+ asset, defname, ext = p.match(info.name).group('asset', 'defname', 'ext')
+ path = '%s.%s' % (asset, ext)
registry = info.registry
settings = info.settings
settings_prefix = self.settings_prefix
@@ -136,12 +154,12 @@ class MakoRendererFactoryHelper(object):
registry_lock.acquire()
try:
- registry.registerUtility(lookup, IMakoLookup,
+ registry.registerUtility(lookup, IMakoLookup,
name=settings_prefix)
finally:
registry_lock.release()
- return MakoLookupTemplateRenderer(path, lookup)
+ return MakoLookupTemplateRenderer(path, defname, lookup)
renderer_factory = MakoRendererFactoryHelper('mako.')
@@ -156,10 +174,18 @@ class MakoRenderingException(Exception):
@implementer(ITemplateRenderer)
class MakoLookupTemplateRenderer(object):
- def __init__(self, path, lookup):
+ """ 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
self.lookup = lookup
-
+
def implementation(self):
return self.lookup.get_template(self.path)
@@ -167,16 +193,19 @@ 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
+ 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):
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/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/renderers.py b/pyramid/renderers.py
index 0adadf726..e526f9997 100644
--- a/pyramid/renderers.py
+++ b/pyramid/renderers.py
@@ -3,11 +3,16 @@ 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 (
IChameleonLookup,
IChameleonTranslate,
+ IJSONAdapter,
IRendererGlobalsFactory,
IRendererFactory,
IResponseFactory,
@@ -60,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``
@@ -103,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``
@@ -157,40 +164,13 @@ 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)
+_marker = object()
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 +178,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
@@ -213,19 +192,57 @@ 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.
-
+ serializer without defining a custom renderer.
"""
- def __init__(self, **kw):
- """ Any keyword arguments will be forwarded to
- :func:`json.dumps`.
- """
+ 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 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
+
+ class Foo(object):
+ x = 5
+
+ def foo_adapter(obj, request):
+ return obj.x
+
+ 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)
def __call__(self, info):
""" Returns a plain JSON-encoded string with content-type
@@ -238,23 +255,30 @@ class JSON(object):
ct = response.content_type
if ct == response.default_content_type:
response.content_type = 'application/json'
- return self.value_to_json(value)
+ default = self._make_default(request)
+ return self.serializer(value, default=default, **self.kw)
+
return _render
- def value_to_json(self, value):
- """ Convert 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)
+ 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):
""" `JSONP <http://en.wikipedia.org/wiki/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
@@ -267,9 +291,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
@@ -281,6 +305,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
+ ``serializer`` and ``adapters``).
+
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
@@ -319,7 +347,8 @@ 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)
+ default = self._make_default(request)
+ 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/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
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
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",
],
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,)
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/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):
diff --git a/pyramid/testing.py b/pyramid/testing.py
index 40e90cda6..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
@@ -824,6 +825,8 @@ 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.add_default_route_predicates()
config.commit()
global have_zca
try:
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!
+</%def> \ 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_config/test_init.py b/pyramid/tests/test_config/test_init.py
index 37c3de275..abe22400b 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
@@ -1385,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
@@ -1682,6 +1689,7 @@ class Test_resolveConflicts(unittest.TestCase):
(3, f, (3,), {}, ('y',)),
(None, f, (5,), {}, ('y',)),
])
+ result = list(result)
self.assertEqual(
result,
[{'info': None,
@@ -1743,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,
@@ -1794,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(
@@ -1940,10 +1948,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 +1962,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)
diff --git a/pyramid/tests/test_config/test_predicates.py b/pyramid/tests/test_config/test_predicates.py
new file mode 100644
index 000000000..e33a31458
--- /dev/null
+++ b/pyramid/tests/test_config/test_predicates.py
@@ -0,0 +1,268 @@
+import unittest
+
+from pyramid.compat import text_
+
+class TestXHRPredicate(unittest.TestCase):
+ def _makeOne(self, val):
+ from pyramid.config.predicates import XHRPredicate
+ return XHRPredicate(val, None)
+
+ 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, 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')
+ 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,HEAD')
+
+class TestPathInfoPredicate(unittest.TestCase):
+ def _makeOne(self, val):
+ from pyramid.config.predicates import PathInfoPredicate
+ return PathInfoPredicate(val, None)
+
+ 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, None)
+
+ 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')
+
+ def test_phash_withval(self):
+ inst = self._makeOne('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, None)
+
+ 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, None)
+
+ 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.assertEqual(inst.text(), 'custom predicate: object predicate')
+
+ def test_phash(self):
+ pred = predicate()
+ inst = self._makeOne(pred)
+ self.assertEqual(inst.phash(), 'custom:1')
+
+class TestTraversePredicate(unittest.TestCase):
+ def _makeOne(self, val):
+ from pyramid.config.predicates import TraversePredicate
+ return TraversePredicate(val, None)
+
+ 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_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_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')
+
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..13cb27526 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__, '<unknown custom predicate>')
+ 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')
@@ -328,15 +361,10 @@ class Test__make_predicates(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):
@@ -368,6 +396,274 @@ 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_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()
+ 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'
@@ -392,3 +688,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 9b46f83c9..38f60d79b 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
@@ -400,7 +401,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 +425,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)
@@ -970,8 +971,10 @@ 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.assertNotEqual(
+ wrapper.__discriminator__(foo, request),
+ wrapper.__discriminator__(bar, request),
+ )
def test_add_view_with_template_renderer(self):
from pyramid.tests import test_config
@@ -1217,8 +1220,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 +1326,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
@@ -1698,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
@@ -1713,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
@@ -2905,6 +2972,7 @@ class TestViewDeriver(unittest.TestCase):
view = lambda *arg: response
def predicate1(context, request):
return False
+ predicate1.text = lambda *arg: 'text'
deriver = self._makeOne(predicates=[predicate1])
result = deriver(view)
request = self._makeRequest()
@@ -2912,7 +2980,8 @@ class TestViewDeriver(unittest.TestCase):
try:
result(None, None)
except PredicateMismatch as e:
- self.assertEqual(e.detail, 'predicate mismatch for view <lambda>')
+ self.assertEqual(e.detail,
+ 'predicate mismatch for view <lambda> (text)')
else: # pragma: no cover
raise AssertionError
@@ -2921,6 +2990,7 @@ class TestViewDeriver(unittest.TestCase):
def myview(request): pass
def predicate1(context, request):
return False
+ predicate1.text = lambda *arg: 'text'
deriver = self._makeOne(predicates=[predicate1])
result = deriver(myview)
request = self._makeRequest()
@@ -2928,7 +2998,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 = lambda *arg: 'pred1'
+ def predicate2(context, request):
+ return False
+ predicate2.text = lambda *arg: '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 +3066,11 @@ class TestViewDeriver(unittest.TestCase):
def predicate1(context, request):
predicates.append(True)
return True
+ predicate1.text = lambda *arg: 'text'
def predicate2(context, request):
predicates.append(True)
return False
+ predicate2.text = lambda *arg: 'text'
deriver = self._makeOne(predicates=[predicate1, predicate2])
result = deriver(view)
request = self._makeRequest()
@@ -3943,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_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_mako_templating.py b/pyramid/tests/test_mako_templating.py
index fbb04273b..aced6c586 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'}
@@ -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,7 +332,7 @@ 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'))
@@ -340,21 +340,36 @@ class MakoLookupTemplateRendererTests(Base, unittest.TestCase):
def test_call_with_tuple_value(self):
lookup = DummyLookup()
- instance = self._makeOne('path', lookup)
+ 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)
+ result = instance({}, {'system':1})
+ 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', 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,11 +379,11 @@ 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'))
-
+
class TestIntegration(unittest.TestCase):
def setUp(self):
import pyramid.mako_templating
@@ -391,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','')
@@ -402,6 +417,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})
@@ -414,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','')
@@ -426,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
@@ -459,12 +479,32 @@ 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])
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')
@@ -478,7 +518,7 @@ class TestPkgResourceTemplateLookup(unittest.TestCase):
self.assertFalse(result is None)
finally:
shutil.rmtree(tmpdir, ignore_errors=True)
-
+
def test_get_template_asset_spec_missing(self):
from mako.exceptions import TopLevelLookupException
fixturedir = self.get_fixturedir()
@@ -490,7 +530,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')
@@ -499,7 +539,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
@@ -513,8 +553,8 @@ class DummyLookup(object):
raise self.exc
self.values = values
return text_('result')
-
+
class DummyRendererInfo(object):
def __init__(self, kw):
self.__dict__.update(kw)
-
+
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_renderers.py b/pyramid/tests/test_renderers.py
index f03c7acda..495d7dc23 100644
--- a/pyramid/tests/test_renderers.py
+++ b/pyramid/tests/test_renderers.py
@@ -369,30 +369,60 @@ 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):
+ request = testing.DummyRequest()
from datetime import datetime
- from json import JSONEncoder
- class MyEncoder(JSONEncoder):
- def default(self, obj):
- return obj.isoformat()
+ def adapter(obj, req):
+ self.assertEqual(req, request)
+ return obj.isoformat()
now = datetime.utcnow()
- renderer = self._makeOne(cls=MyEncoder)(None)
- result = renderer({'a':now}, {})
+ renderer = self._makeOne()
+ renderer.add_adapter(datetime, adapter)
+ result = renderer(None)({'a':now}, {'request':request})
self.assertEqual(result, '{"a": "%s"}' % now.isoformat())
- def test_with_object_encoder(self):
+ def test_with_custom_adapter2(self):
+ request = testing.DummyRequest()
+ from datetime import datetime
+ 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}, {'request':request})
+ 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):
+ 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_encoder_no___json__(self):
+ def test_with_object_adapter_no___json__(self):
class MyObject(object):
def __init__(self, x):
self.x = x
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()
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):
diff --git a/pyramid/tests/test_scaffolds/test_copydir.py b/pyramid/tests/test_scaffolds/test_copydir.py
index 42edd9d23..d757b837c 100644
--- a/pyramid/tests/test_scaffolds/test_copydir.py
+++ b/pyramid/tests/test_scaffolds/test_copydir.py
@@ -170,9 +170,19 @@ 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(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("/tmp/nonexistent_dir")
+ shutil.rmtree(tmpdir)
class Test_support_functions(unittest.TestCase):
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'
diff --git a/pyramid/tests/test_static.py b/pyramid/tests/test_static.py
index 7f94df990..94497d4f6 100644
--- a/pyramid/tests/test_static.py
+++ b/pyramid/tests/test_static.py
@@ -38,11 +38,9 @@ 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
+ self.assertRaises(HTTPMovedPermanently, inst, context, request)
+
def test_path_info_slash_means_index_html(self):
inst = self._makeOne('pyramid.tests:fixtures/static')
request = self._makeRequest()
@@ -70,16 +68,16 @@ 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
+ self.assertRaises(HTTPNotFound, inst, context, request)
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
+ self.assertRaises(HTTPNotFound, inst, context, request)
def test_oob_os_sep(self):
import os
@@ -88,15 +86,15 @@ 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
+ 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()
- response = inst(context, request)
- self.assertEqual(response.status, '404 Not Found')
+ from pyramid.httpexceptions import HTTPNotFound
+ self.assertRaises(HTTPNotFound, inst, context, request)
def test_resource_isdir(self):
inst = self._makeOne('pyramid.tests:fixtures/static')
@@ -174,8 +172,8 @@ 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
+ self.assertRaises(HTTPNotFound, inst, context, request)
def test_resource_with_content_encoding(self):
inst = self._makeOne('pyramid.tests:fixtures/static')
@@ -233,11 +231,9 @@ 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
+ self.assertRaises(HTTPMovedPermanently, inst, context, request)
+
def test_path_info_slash_means_index_html(self):
inst = self._makeOne('pyramid.tests:fixtures/static')
request = self._makeRequest()
@@ -251,33 +247,33 @@ 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
+ self.assertRaises(HTTPNotFound, inst, context, request)
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
+ self.assertRaises(HTTPNotFound, inst, context, request)
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
+ self.assertRaises(HTTPNotFound, inst, context, request)
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
+ self.assertRaises(HTTPNotFound, inst, context, request)
+
def test_oob_os_sep(self):
import os
inst = self._makeOne('pyramid.tests:fixtures/static')
@@ -285,16 +281,16 @@ 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
+ self.assertRaises(HTTPNotFound, inst, context, request)
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
+ self.assertRaises(HTTPNotFound, inst, context, request)
def test_resource_isdir(self):
inst = self._makeOne('pyramid.tests:fixtures/static')
@@ -361,8 +357,8 @@ 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
+ self.assertRaises(HTTPNotFound, inst, context, request)
class DummyContext:
pass
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')
diff --git a/pyramid/tests/test_url.py b/pyramid/tests/test_url.py
index 0dff1e648..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
@@ -113,6 +111,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 +340,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/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\\/(?P<baz>abc)\\/biz\\/(?P<buz>[^/]+)\\/bar$'
diff --git a/pyramid/tests/test_view.py b/pyramid/tests/test_view.py
index a105adb70..f63e17bd8 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
@@ -90,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')
@@ -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
@@ -371,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/url.py b/pyramid/url.py
index 022867967..52e172d3f 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']
@@ -707,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.
"""
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
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
+
diff --git a/pyramid/view.py b/pyramid/view.py
index d722c0cbb..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 = locals()
- 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:
@@ -335,7 +316,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')
@@ -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 = locals()
- 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:
@@ -409,13 +381,13 @@ 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')
- 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 = locals()
- 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()
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 f8c8c41e9..cbc1075bd 100644
--- a/setup.py
+++ b/setup.py
@@ -50,18 +50,22 @@ install_requires=[
tests_require = [
'WebTest >= 1.3.1', # py3 compat
- 'virtualenv',
]
if not PY3:
- tests_require.extend([
- 'Sphinx',
- 'docutils',
- 'repoze.sphinx.autointerface',
- 'zope.component>=3.11.0',
- ])
+ tests_require.append('zope.component>=3.11.0')
-testing_extras = tests_require + ['nose', 'coverage']
+docs_extras = [
+ 'Sphinx',
+ 'docutils',
+ 'repoze.sphinx.autointerface',
+ ]
+
+testing_extras = tests_require + [
+ 'nose',
+ 'coverage',
+ 'virtualenv', # for scaffolding tests
+ ]
setup(name='pyramid',
version='1.4dev',
@@ -77,7 +81,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",
@@ -93,6 +97,7 @@ setup(name='pyramid',
install_requires = install_requires,
extras_require = {
'testing':testing_extras,
+ 'docs':docs_extras,
},
tests_require = tests_require,
test_suite="pyramid.tests",
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