summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitmodules3
-rw-r--r--.travis.yml19
-rw-r--r--CHANGES.txt31
-rw-r--r--CONTRIBUTORS.txt8
-rw-r--r--HACKING.txt45
-rw-r--r--TODO.txt14
-rw-r--r--docs/.gitignore1
-rw-r--r--docs/Makefile6
m---------docs/_themes0
-rw-r--r--docs/api/renderers.rst2
-rw-r--r--docs/conf.py22
-rw-r--r--docs/designdefense.rst112
-rw-r--r--docs/glossary.rst10
-rw-r--r--docs/narr/advconfig.rst2
-rw-r--r--docs/narr/commandline.rst11
-rw-r--r--docs/narr/hooks.rst36
-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/narr/views.rst11
-rw-r--r--docs/tutorials/modwsgi/index.rst14
-rw-r--r--docs/tutorials/wiki/authorization.rst481
-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/wiki/src/authorization/tutorial/models.py6
-rw-r--r--docs/tutorials/wiki/src/authorization/tutorial/views.py22
-rw-r--r--docs/tutorials/wiki/src/tests/tutorial/models.py6
-rw-r--r--docs/tutorials/wiki/src/tests/tutorial/views.py22
-rw-r--r--docs/tutorials/wiki2/authorization.rst426
-rw-r--r--docs/tutorials/wiki2/definingviews.rst92
-rw-r--r--docs/tutorials/wiki2/design.rst19
-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/authorization/tutorial/views.py6
-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/tests/tutorial/views.py6
-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/factories.py11
-rw-r--r--pyramid/config/util.py7
-rw-r--r--pyramid/config/views.py4
-rw-r--r--pyramid/events.py32
-rw-r--r--pyramid/interfaces.py6
-rw-r--r--pyramid/mako_templating.py34
-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/static.py12
-rw-r--r--pyramid/tests/fixtures/components.mak3
-rw-r--r--pyramid/tests/fixtures/hellocompo.mak3
-rw-r--r--pyramid/tests/test_config/test_factories.py24
-rw-r--r--pyramid/tests/test_mako_templating.py34
-rw-r--r--pyramid/tests/test_renderers.py52
-rw-r--r--pyramid/tests/test_response.py2
-rw-r--r--pyramid/tests/test_scaffolds/test_copydir.py6
-rw-r--r--pyramid/tests/test_static.py66
-rw-r--r--pyramid/tests/test_url.py17
-rw-r--r--pyramid/tests/test_urldispatch.py8
-rw-r--r--pyramid/tests/test_util.py27
-rw-r--r--pyramid/url.py10
-rw-r--r--pyramid/urldispatch.py4
-rw-r--r--pyramid/util.py87
-rw-r--r--pyramid/view.py10
-rw-r--r--setup.cfg2
-rw-r--r--setup.py23
-rw-r--r--tox.ini10
83 files changed, 1485 insertions, 945 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 f0fef62ea..c6afaf0c7 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -9,6 +9,14 @@ 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.
+
Features
--------
@@ -17,11 +25,32 @@ Features
values natively serializable by ``json.dumps`` (such as ints, lists,
dictionaries, strings, and so forth).
+- The JSON renderer now allows for the definition of custom type adapters to
+ convert unknown objects to JSON serializations.
+
- As of this release, the ``request_method`` predicate, when used, will also
imply that ``HEAD`` is implied when you use ``GET``. For example, using
``@view_config(request_method='GET')`` is equivalent to using
- ``@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
to return an empty body when a HEAD is used.
+
+- ``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', {}).
diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt
index 365e3e455..c3995aaba 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,9 @@ 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
diff --git a/HACKING.txt b/HACKING.txt
index 593e89ac1..38c263ed7 100644
--- a/HACKING.txt
+++ b/HACKING.txt
@@ -113,23 +113,34 @@ Test Coverage
``nose`` and ``coverage`` into your virtualenv, and running ``setup.py
nosetests --with-coverage``.
-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.
-
-- To build and review docs:
-
- 1. Install ``tests_require`` dependencies from Pyramid's setup.py into your
- virtualenv.
-
- 2. From the ``docs`` directory of the Pyramid checkout run ``make html
- SPHINXBUILD=/path/to/your/virtualenv/bin/sphinx-build``.
-
- 3. Open the _build/html/index.html file to see the resulting rendering.
+Documentation Coverage and Building HTML Documentation
+------------------------------------------------------
+
+If you fix a bug, and the bug requires an API or behavior modification, all
+documentation in this package which references that API or behavior must
+change to reflect the bug fix, ideally in the same commit that fixes the bug
+or adds the feature.
+
+To build and review docs (where ``$yourvenv`` refers to the virtualenv you're
+using to develop Pyramid):
+
+1. Run ``$yourvenv/bin/python setup.py dev docs``. This will cause Sphinx
+ and all development requirements to be installed in your virtualenv.
+
+2. Update all git submodules from the top-level of your Pyramid checkout, like
+ so:
+ git submodule update --init --recursive
+ This will checkout theme subrepositories and prevent error conditions when
+ HTML docs are generated.
+
+3. cd to the ``docs`` directory within your Pyramid checkout and execute
+ ``make clean html SPHINXBUILD=$yourvenv/bin/sphinx-build``. The
+ ``SPHINXBUILD=...`` hair is there in order to tell it to use the
+ virtualenv Python, which will have both Sphinx and Pyramid (for API
+ documentation generation) installed.
+
+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 7c0695727..4b4f48499 100644
--- a/TODO.txt
+++ b/TODO.txt
@@ -4,6 +4,15 @@ 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
deprecation warnings).
@@ -62,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)"?
@@ -121,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/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 ffbf15808..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'))
@@ -162,7 +160,7 @@ html_theme_path = ['_themes']
html_theme = 'pyramid'
html_theme_options = dict(
github_url='https://github.com/Pylons/pyramid',
-# in_progress='true'
+ in_progress='true',
)
# The style sheet to use for HTML and HTML Help pages. A file of that name
# must exist either in Sphinx' static/ path, or in one of the custom paths
@@ -476,7 +474,7 @@ def resig(app, what, name, obj, options, signature, return_annotation):
# -- Options for Epub output ---------------------------------------------------
# Bibliographic Dublin Core info.
-epub_title = 'The Pyramid Web Application Development Framework, Version 1.3'
+epub_title = 'The Pyramid Web Application Development Framework, Version 1.4'
epub_author = 'Chris McDonough'
epub_publisher = 'Agendaless Consulting'
epub_copyright = '2008-2011'
@@ -493,7 +491,7 @@ epub_scheme = 'ISBN'
epub_identifier = '0615445675'
# A unique identification for the text.
-epub_uid = 'The Pyramid Web Application Development Framework, Version 1.3'
+epub_uid = 'The Pyramid Web Application Development Framework, Version 1.4'
# HTML files that should be inserted before the pages created by sphinx.
# The format is a list of tuples containing the path and title.
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..45a79326f 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
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..af53c1f78 100644
--- a/docs/narr/commandline.rst
+++ b/docs/narr/commandline.rst
@@ -460,7 +460,7 @@ to the console.
You can add request header values by using the ``--header`` option::
- $ bin/prequest --header=Host=example.com development.ini /
+ $ bin/prequest --header=Host:example.com development.ini /
Headers are added to the WSGI environment by converting them to their
CGI/WSGI equivalents (e.g. ``Host=example.com`` will insert the ``HTTP_HOST``
@@ -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/hooks.rst b/docs/narr/hooks.rst
index b6e3dd163..332805152 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:
diff --git a/docs/narr/introduction.rst b/docs/narr/introduction.rst
index 8f7b17dc3..b5fa6a9f7 100644
--- a/docs/narr/introduction.rst
+++ b/docs/narr/introduction.rst
@@ -534,14 +534,14 @@ Configuration extensibility
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Unlike other systems, Pyramid provides a structured "include" mechanism (see
-:meth:`~pyramid.config.Configurator.include`) that allows you to compose
+:meth:`~pyramid.config.Configurator.include`) that allows you to combine
applications from multiple Python packages. All the configuration statements
that can be performed in your "main" Pyramid application can also be
performed by included packages including the addition of views, routes,
subscribers, and even authentication and authorization policies. You can even
extend or override an existing application by including another application's
configuration in your own, overriding or adding new views and routes to
-it. This has the potential to allow you to compose a big application out of
+it. This has the potential to allow you to create a big application out of
many other smaller ones. For example, if you want to reuse an existing
application that already has a bunch of routes, you can just use the
``include`` statement with a ``route_prefix``; the new application will live
@@ -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/narr/views.rst b/docs/narr/views.rst
index c3bbbd50e..f6ee9a8d5 100644
--- a/docs/narr/views.rst
+++ b/docs/narr/views.rst
@@ -572,6 +572,17 @@ No matter which view calling convention is used, the view code always has
access to the context via ``request.context``.
.. index::
+ single: Passing in configuration variables
+
+.. _passing_in_config_variables:
+
+Passing Configuration Variables to a View
+-----------------------------------------
+
+For information on passing a variable from the configuration .ini files to a
+view, see :ref:`deployment_settings`.
+
+.. index::
single: Pylons-style controller dispatch
Pylons-1.0-Style "Controller" Dispatch
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/authorization.rst b/docs/tutorials/wiki/authorization.rst
index e599e7086..9e0bf0f09 100644
--- a/docs/tutorials/wiki/authorization.rst
+++ b/docs/tutorials/wiki/authorization.rst
@@ -2,121 +2,186 @@
Adding Authorization
====================
-Our application currently allows anyone with access to the server to view,
-edit, and add pages to our wiki. For purposes of demonstration we'll change
-our application to allow people who are members of a *group* named
-``group:editors`` to add and edit wiki pages but we'll continue allowing
-anyone with access to the server to view pages. :app:`Pyramid` provides
-facilities for :term:`authorization` and :term:`authentication`. We'll make
-use of both features to provide security to our application.
-
-We will add an :term:`authentication policy` and an
-:term:`authorization policy` to our :term:`application
-registry`, add a ``security.py`` module and give our :term:`root`
-resource an :term:`ACL`.
-
-Then we will add ``login`` and ``logout`` views, and modify the
-existing views to make them return a ``logged_in`` flag to the
-renderer and add :term:`permission` declarations to their ``view_config``
-decorators.
-
-Finally, we will add a ``login.pt`` template and change the existing
-``view.pt`` and ``edit.pt`` to show a "Logout" link when not logged in.
-
-The source code for this tutorial stage can be browsed via
+:app:`Pyramid` provides facilities for :term:`authentication` and
+:term:`authorization`. We'll make use of both features to provide security
+to our application. Our application currently allows anyone with access to
+the server to view, edit, and add pages to our wiki. We'll change that
+to allow only people who are members of a *group* named ``group:editors``
+to add and edit wiki pages but we'll continue allowing
+anyone with access to the server to view pages.
+
+We will also add a login page and a logout link on all the
+pages. The login page will be shown when a user is denied
+access to any of the views that require a permission, instead of
+a default "403 Forbidden" page.
+
+We will implement the access control with the following steps:
+
+* Add users and groups (``security.py``, a new module).
+* Add an :term:`ACL` (``models.py``).
+* Add an :term:`authentication policy` and an :term:`authorization policy`
+ (``__init__.py``).
+* Add :term:`permission` declarations to the ``edit_page`` and ``add_page``
+ views (``views.py``).
+
+Then we will add the login and logout feature:
+
+* Add ``login`` and ``logout`` views (``views.py``).
+* Add a login template (``login.pt``).
+* Make the existing views return a ``logged_in`` flag to the renderer (``views.py``).
+* Add a "Logout" link to be shown when logged in and viewing or editing a page
+ (``view.pt``, ``edit.pt``).
+
+The source code for this tutorial stage can be browsed at
`http://github.com/Pylons/pyramid/tree/1.3-branch/docs/tutorials/wiki/src/authorization/
<http://github.com/Pylons/pyramid/tree/1.3-branch/docs/tutorials/wiki/src/authorization/>`_.
-Add Authentication and Authorization Policies
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Access Control
+--------------
-We'll change our package's ``__init__.py`` file to enable an
-``AuthTktAuthenticationPolicy`` and an ``ACLAuthorizationPolicy`` to enable
-declarative security checking. We need to import the new policies:
+Add users and groups
+~~~~~~~~~~~~~~~~~~~~
-.. literalinclude:: src/authorization/tutorial/__init__.py
- :lines: 4-5,8
+Create a new ``tutorial/tutorial/security.py`` module with the
+following content:
+
+.. literalinclude:: src/authorization/tutorial/security.py
:linenos:
:language: python
-Then, we'll add those policies to the configuration:
+The ``groupfinder`` function accepts a userid and a request and
+returns one of these values:
-.. literalinclude:: src/authorization/tutorial/__init__.py
- :lines: 17-22
+- If the userid exists in the system, it will return a
+ sequence of group identifiers (or an empty sequence if the user
+ isn't a member of any groups).
+- If the userid *does not* exist in the system, it will
+ return ``None``.
+
+For example, ``groupfinder('editor', request )`` returns ['group:editor'],
+``groupfinder('viewer', request)`` returns [], and ``groupfinder('admin', request)``
+returns ``None``. We will use ``groupfinder()`` as an :term:`authentication policy`
+"callback" that will provide the :term:`principal` or principals
+for a user.
+
+In a production system, user and group
+data will most often come from a database, but here we use "dummy"
+data to represent user and groups sources.
+
+Add an ACL
+~~~~~~~~~~
+
+Open ``tutorial/tutorial/models.py`` and add the following import
+statement at the head:
+
+.. literalinclude:: src/authorization/tutorial/models.py
+ :lines: 4-7
:linenos:
:language: python
-Note that the creation of an ``AuthTktAuthenticationPolicy`` requires two
-arguments: ``secret`` and ``callback``. ``secret`` is a string representing
-an encryption key used by the "authentication ticket" machinery represented
-by this policy: it is required. The ``callback`` is a reference to a
-``groupfinder`` function in the ``tutorial`` package's ``security.py`` file.
-We haven't added that module yet, but we're about to.
+Add the following lines to the ``Wiki`` class:
+
+.. literalinclude:: src/authorization/tutorial/models.py
+ :lines: 9-13
+ :linenos:
+ :emphasize-lines: 4-5
+ :language: python
-When you're done, your ``__init__.py`` will
-look like so:
+We import :data:`~pyramid.security.Allow`, an action that
+means that permission is allowed:, and
+:data:`~pyramid.security.Everyone`, a special :term:`principal`
+that is associated to all requests. Both are used in the
+:term:`ACE` entries that make up the ACL.
+
+The ACL is a list that needs to be named `__acl__` and be an
+attribute of a class. We define an :term:`ACL` with two
+:term:`ACE` entries: the first entry allows any user the `view`
+permission. The second entry allows the ``group:editors``
+principal the `edit` permission.
+
+The ``Wiki`` class that contains the ACL is the :term:`resource`
+constructor for the :term:`root` resource, which is
+a ``Wiki`` instance. The ACL is
+provided to each view in the :term:`context` of the request, as
+the ``context`` attribute.
+
+It's only happenstance that we're assigning this ACL at class scope. An ACL
+can be attached to an object *instance* too; this is how "row level security"
+can be achieved in :app:`Pyramid` applications. We actually only need *one*
+ACL for the entire system, however, because our security requirements are
+simple, so this feature is not demonstrated. See
+:ref:`assigning_acls` for more information about what an
+:term:`ACL` represents.
+
+Add Authentication and Authorization Policies
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Open ``tutorial/__init__.py`` and
+add these import statements:
.. literalinclude:: src/authorization/tutorial/__init__.py
+ :lines: 4-5,8
:linenos:
:language: python
-Add ``security.py``
-~~~~~~~~~~~~~~~~~~~
-
-Add a ``security.py`` module within your package (in the same
-directory as ``__init__.py``, ``views.py``, etc.) with the following
-content:
+Now add those policies to the configuration:
-.. literalinclude:: src/authorization/tutorial/security.py
+.. literalinclude:: src/authorization/tutorial/__init__.py
+ :lines: 17-22
:linenos:
+ :emphasize-lines: 1-3,5-6
:language: python
-The ``groupfinder`` function defined here is an :term:`authentication policy`
-"callback"; it is a callable that accepts a userid and a request. If the
-userid exists in the system, the callback will return a sequence of group
-identifiers (or an empty sequence if the user isn't a member of any groups).
-If the userid *does not* exist in the system, the callback will return
-``None``. In a production system, user and group data will most often come
-from a database, but here we use "dummy" data to represent user and groups
-sources. Note that the ``editor`` user is a member of the ``group:editors``
-group in our dummy group data (the ``GROUPS`` data structure).
-
-Give Our Root Resource an ACL
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+(Only the highlighted lines need to be added.)
+
+We are enabling an ``AuthTktAuthenticationPolicy``, it is based in an auth
+ticket that may be included in the request, and an ``ACLAuthorizationPolicy``
+that uses an ACL to determine the allow or deny outcome for a view.
-We need to give our root resource object an :term:`ACL`. This ACL will be
-sufficient to provide enough information to the :app:`Pyramid` security
-machinery to challenge a user who doesn't have appropriate credentials when
-he attempts to invoke the ``add_page`` or ``edit_page`` views.
+Note that the
+:class:`pyramid.authentication.AuthTktAuthenticationPolicy` constructor
+accepts two arguments: ``secret`` and ``callback``. ``secret`` is a string
+representing an encryption key used by the "authentication ticket" machinery
+represented by this policy: it is required. The ``callback`` is the
+``groupfinder()`` function that we created before.
-We need to perform some imports at module scope in our ``models.py`` file:
+Add permission declarations
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Add a ``permission='edit'`` parameter to the ``@view_config``
+decorator for ``add_page()`` and ``edit_page()``, for example:
.. code-block:: python
:linenos:
+ :emphasize-lines: 2
+
+ @view_config(route_name='add_page', renderer='templates/edit.pt',
+ permission='edit')
+
+(Only the highlighted line needs to be added.)
- from pyramid.security import Allow
- from pyramid.security import Everyone
+The result is that only users who possess the ``edit``
+permission at the time of the request may invoke those two views.
-Our root resource object is a ``Wiki`` instance. We'll add the following
-line at class scope to our ``Wiki`` class:
+Add a ``permission='view'`` parameter to the ``@view_config``
+decorator for ``view_wiki()`` and ``view_page()``, like this:
.. code-block:: python
:linenos:
+ :emphasize-lines: 2
- __acl__ = [ (Allow, Everyone, 'view'),
- (Allow, 'group:editors', 'edit') ]
+ @view_config(route_name='view_page', renderer='templates/view.pt',
+ permission='view')
-It's only happenstance that we're assigning this ACL at class scope. An ACL
-can be attached to an object *instance* too; this is how "row level security"
-can be achieved in :app:`Pyramid` applications. We actually only need *one*
-ACL for the entire system, however, because our security requirements are
-simple, so this feature is not demonstrated.
+(Only the highlighted line needs to be added.)
-Our resulting ``models.py`` file will now look like so:
+This allows anyone to invoke these two views.
-.. literalinclude:: src/authorization/tutorial/models.py
- :linenos:
- :language: python
+We are done with the changes needed to control access. The
+changes that follow will add the login and logout feature.
+
+Login, Logout
+-------------
Add Login and Logout Views
~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -124,124 +189,103 @@ Add Login and Logout Views
We'll add a ``login`` view which renders a login form and processes
the post from the login form, checking credentials.
-We'll also add a ``logout`` view to our application and provide a link
-to it. This view will clear the credentials of the logged in user and
-redirect back to the front page.
+We'll also add a ``logout`` view callable to our application and
+provide a link to it. This view will clear the credentials of the
+logged in user and redirect back to the front page.
-We'll add these views to the existing ``views.py`` file we have in our
-project. Here's what the ``login`` view callable will look like:
+Add the following import statements to the
+head of ``tutorial/tutorial/views.py``:
.. literalinclude:: src/authorization/tutorial/views.py
- :lines: 86-113
+ :lines: 6-13,15-17
:linenos:
+ :emphasize-lines: 3,6-9,11
:language: python
-Here's what the ``logout`` view callable will look like:
+(Only the highlighted lines need to be added.)
+
+:meth:`~pyramid.view.forbidden_view_config` will be used
+to customize the default 403 Forbidden page.
+:meth:`~pyramid.security.remember` and
+:meth:`~pyramid.security.forget` help to create and
+expire an auth ticket cookie.
+
+Now add the ``login`` and ``logout`` views:
.. literalinclude:: src/authorization/tutorial/views.py
- :lines: 115-119
+ :lines: 87-120
:linenos:
:language: python
-Note that the ``login`` view callable has *two* view configuration
-decorators. The order of these decorators is unimportant. Each just adds a
-different :term:`view configuration` for the ``login`` view callable.
-
-The first view configuration decorator configures the ``login`` view callable
-so it will be invoked when someone visits ``/login`` (when the context is a
-Wiki and the view name is ``login``). The second decorator, named
-``forbidden_view_config`` specifies a :term:`forbidden view`. This
-configures our login view to be presented to the user when :app:`Pyramid`
-detects that a view invocation can not be authorized. Because we've
-configured a forbidden view, the ``login`` view callable will be invoked
-whenever one of our users tries to execute a view callable that they are not
-allowed to invoke as determined by the :term:`authorization policy` in use.
-In our application, for example, this means that if a user has not logged in,
-and he tries to add or edit a Wiki page, he will be shown the login form.
-Before being allowed to continue on to the add or edit form, he will have to
-provide credentials that give him permission to add or edit via this login
-form.
-
-Note that we're relying on some additional imports within the bodies of these
-views (e.g. ``remember`` and ``forget``). We'll see a rendering of the
-entire views.py file a little later here to show you where those come from.
-
-Change Existing Views
-~~~~~~~~~~~~~~~~~~~~~
-
-In order to indicate whether the current user is logged in, we need to change
-each of our ``view_page``, ``edit_page`` and ``add_page`` views in
-``views.py`` to pass a "logged in" parameter into its template. We'll add
-something like this to each view body:
+``login()`` is decorated with two decorators:
-.. code-block:: python
+- a ``@view_config`` decorator which associates it with the
+ ``login`` route and makes it visible when we visit ``/login``,
+- a ``@forbidden_view_config`` decorator which turns it into
+ an :term:`forbidden view`. ``login()`` will be invoked
+ when a users tries to execute a view callable that
+ they are not allowed to. For example, if a user has not logged in
+ and tries to add or edit a Wiki page, he will be shown the
+ login form before being allowed to continue on.
+
+The order of these two :term:`view configuration` decorators
+is unimportant.
+
+``logout()`` is decorated with a ``@view_config`` decorator
+which associates it with the ``logout`` route. It will be
+invoked when we visit ``/logout``.
+
+Add the ``login.pt`` Template
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Create ``tutorial/tutorial/templates/login.pt`` with the following
+content:
+
+.. literalinclude:: src/authorization/tutorial/templates/login.pt
+ :language: xml
+
+The above template is referred to within the login view we just
+added to ``views.py``.
+
+Return a logged_in flag to the renderer
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Add the following line to the import at the head of
+``tutorial/tutorial/views.py``:
+
+.. literalinclude:: src/authorization/tutorial/views.py
+ :lines: 11-15
:linenos:
+ :emphasize-lines: 4
+ :language: python
- from pyramid.security import authenticated_userid
- logged_in = authenticated_userid(request)
+(Only the highlighted line needs to be added.)
-We'll then change the return value of each view that has an associated
-``renderer`` to pass the resulting ``logged_in`` value to the
-template. For example:
+Add a ``logged_in`` parameter to the return value of
+``view_page()``, ``edit_page()`` and ``add_page()``,
+like this:
.. code-block:: python
:linenos:
+ :emphasize-lines: 4
- return dict(page = context,
+ return dict(page = page,
content = content,
- logged_in = logged_in,
- edit_url = edit_url)
-
-Add ``permission`` Declarations to our ``view_config`` Decorators
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-To protect each of our views with a particular permission, we need to pass a
-``permission`` argument to each of our :class:`pyramid.view.view_config`
-decorators. To do so, within ``views.py``:
-
-- We add ``permission='view'`` to the decorator attached to the
- ``view_wiki`` and ``view_page`` view functions. This makes the
- assertion that only users who possess the ``view`` permission
- against the context resource at the time of the request may
- invoke these views. We've granted
- :data:`pyramid.security.Everyone` the view permission at the
- root model via its ACL, so everyone will be able to invoke the
- ``view_wiki`` and ``view_page`` views.
-
-- We add ``permission='edit'`` to the decorator attached to the
- ``add_page`` and ``edit_page`` view functions. This makes the
- assertion that only users who possess the effective ``edit``
- permission against the context resource at the time of the
- request may invoke these views. We've granted the
- ``group:editors`` principal the ``edit`` permission at the
- root model via its ACL, so only a user whom is a member of
- the group named ``group:editors`` will able to invoke the
- ``add_page`` or ``edit_page`` views. We've likewise given
- the ``editor`` user membership to this group via the
- ``security.py`` file by mapping him to the ``group:editors``
- group in the ``GROUPS`` data structure (``GROUPS
- = {'editor':['group:editors']}``); the ``groupfinder``
- function consults the ``GROUPS`` data structure. This means
- that the ``editor`` user can add and edit pages.
+ edit_url = edit_url,
+ logged_in = authenticated_userid(request))
-Add the ``login.pt`` Template
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Add a ``login.pt`` template to your templates directory. It's
-referred to within the login view we just added to ``views.py``.
+(Only the highlighted line needs to be added.)
-.. literalinclude:: src/authorization/tutorial/templates/login.pt
- :language: xml
+:meth:`~pyramid.security.authenticated_userid()` will return None
+if the user is not authenticated, or some user id it the user
+is authenticated.
-Change ``view.pt`` and ``edit.pt``
+Add a "Logout" link when logged in
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-We'll also need to change our ``edit.pt`` and ``view.pt`` templates to
-display a "Logout" link if someone is logged in. This link will
-invoke the logout view.
-
-To do so we'll add this to both templates within the ``<div id="right"
-class="app-welcome align-right">`` div:
+Open ``tutorial/tutorial/templates/edit.pt`` and
+``tutorial/tutorial/templates/view.pt`` and add this within the
+``<div id="right" class="app-welcome align-right">`` div:
.. code-block:: xml
@@ -249,57 +293,96 @@ class="app-welcome align-right">`` div:
<a href="${request.application_url}/logout">Logout</a>
</span>
-See Our Changes To ``views.py`` and our Templates
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+The attribute ``tal:condition="logged_in"`` will make the element be
+included when ``logged_in`` is any user id. The link will invoke
+the logout view. The above element will not be included if ``logged_in``
+is ``None``, such as when a user is not authenticated.
+
+Seeing Our Changes
+------------------
+
+Our ``tutorial/tutorial/__init__.py`` will look something like this
+when we're done:
+
+.. literalinclude:: src/authorization/tutorial/__init__.py
+ :linenos:
+ :emphasize-lines: 4-5,8,17-19,21-22
+ :language: python
+
+(Only the highlighted lines need to be added.)
+
+Our ``tutorial/tutorial/models.py`` will look something like this
+when we're done:
+
+.. literalinclude:: src/authorization/tutorial/models.py
+ :linenos:
+ :emphasize-lines: 4-7,12-13
+ :language: python
-Our ``views.py`` module will look something like this when we're done:
+(Only the highlighted lines need to be added.)
+
+Our ``tutorial/tutorial/views.py`` will look something like this
+when we're done:
.. literalinclude:: src/authorization/tutorial/views.py
:linenos:
+ :emphasize-lines: 8,11-15,17,24,29,48,52,68,72,80,82-120
:language: python
-Our ``edit.pt`` template will look something like this when we're done:
+(Only the highlighted lines need to be added.)
+
+Our ``tutorial/tutorial/templates/edit.pt`` template will look
+something like this when we're done:
.. literalinclude:: src/authorization/tutorial/templates/edit.pt
:linenos:
+ :emphasize-lines: 41-43
:language: xml
-Our ``view.pt`` template will look something like this when we're done:
+(Only the highlighted lines need to be added.)
+
+Our ``tutorial/tutorial/templates/view.pt`` template will look
+something like this when we're done:
.. literalinclude:: src/authorization/tutorial/templates/view.pt
:linenos:
+ :emphasize-lines: 41-43
:language: xml
-View the Application in a Browser
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+(Only the highlighted lines need to be added.)
-We can finally examine our application in a browser. The views we'll try are
-as follows:
+Viewing the Application in a Browser
+------------------------------------
-- Visiting ``http://localhost:6543/`` in a browser invokes the ``view_wiki``
- view. This always redirects to the ``view_page`` view of the ``FrontPage``
- page resource. It is executable by any user.
+We can finally examine our application in a browser (See
+: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/FrontPage/`` in a browser invokes the
- ``view_page`` view of the ``FrontPage`` Page resource. This is because
+- ``http://localhost:6543/`` invokes the
+ ``view_wiki`` view. This always redirects to the ``view_page`` view
+ of the ``FrontPage`` Page resource. It is executable by any user.
+
+- ``http://localhost:6543/FrontPage`` invokes
+ the ``view_page`` view of the ``FrontPage`` Page resource. This is because
it's the :term:`default view` (a view without a ``name``) for ``Page``
resources. It is executable by any user.
-- Visiting ``http://localhost:6543/FrontPage/edit_page`` in a browser invokes
- the edit view for the ``FrontPage`` Page resource. It is executable by
- only the ``editor`` user. If a different user (or the anonymous user)
- invokes it, a login form will be displayed. Supplying the credentials with
- the username ``editor``, password ``editor`` will show the edit page form
- being displayed.
+- ``http://localhost:6543/FrontPage/edit_page``
+ invokes the edit view for the FrontPage object. It is executable by
+ only the ``editor`` user. If a different user (or the anonymous
+ user) invokes it, a login form will be displayed. Supplying the
+ credentials with the username ``editor``, password ``editor`` will
+ display the edit page form.
-- Visiting ``http://localhost:6543/add_page/SomePageName`` in a
- browser invokes the add view for a page. It is executable by only
+- ``http://localhost:6543/add_page/SomePageName``
+ invokes the add view for a page. It is executable by only
the ``editor`` user. If a different user (or the anonymous user)
invokes it, a login form will be displayed. Supplying the
credentials with the username ``editor``, password ``editor`` will
- show the edit page form being displayed.
+ display the edit page form.
-- After logging in (as a result of hitting an edit or add page and
- submitting the login form with the ``editor`` credentials), we'll see
- a Logout link in the upper right hand corner. When we click it,
- we're logged out, and redirected back to the front page.
+- After logging in (as a result of hitting an edit or add page
+ and submitting the login form with the ``editor``
+ credentials), we'll see a Logout link in the upper right hand
+ corner. When we click it, we're logged out, and redirected
+ back to the front page.
diff --git a/docs/tutorials/wiki/definingviews.rst b/docs/tutorials/wiki/definingviews.rst
index 12bfa8b84..28cecb787 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/wiki/src/authorization/tutorial/models.py b/docs/tutorials/wiki/src/authorization/tutorial/models.py
index 0a31c38be..582ff0d7e 100644
--- a/docs/tutorials/wiki/src/authorization/tutorial/models.py
+++ b/docs/tutorials/wiki/src/authorization/tutorial/models.py
@@ -1,8 +1,10 @@
from persistent import Persistent
from persistent.mapping import PersistentMapping
-from pyramid.security import Allow
-from pyramid.security import Everyone
+from pyramid.security import (
+ Allow,
+ Everyone,
+ )
class Wiki(PersistentMapping):
__name__ = None
diff --git a/docs/tutorials/wiki/src/authorization/tutorial/views.py b/docs/tutorials/wiki/src/authorization/tutorial/views.py
index fcbe6fe25..21f12b31d 100644
--- a/docs/tutorials/wiki/src/authorization/tutorial/views.py
+++ b/docs/tutorials/wiki/src/authorization/tutorial/views.py
@@ -9,9 +9,9 @@ from pyramid.view import (
)
from pyramid.security import (
- authenticated_userid,
remember,
forget,
+ authenticated_userid,
)
from .security import USERS
@@ -20,12 +20,13 @@ from .models import Page
# regular expression used to find WikiWords
wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)")
-@view_config(context='.models.Wiki', permission='view')
+@view_config(context='.models.Wiki',
+ permission='view')
def view_wiki(context, request):
return HTTPFound(location=request.resource_url(context, 'FrontPage'))
-@view_config(context='.models.Page',
- renderer='templates/view.pt', permission='view')
+@view_config(context='.models.Page', renderer='templates/view.pt',
+ permission='view')
def view_page(context, request):
wiki = context.__parent__
@@ -43,10 +44,8 @@ def view_page(context, request):
content = wikiwords.sub(check, content)
edit_url = request.resource_url(context, 'edit_page')
- logged_in = authenticated_userid(request)
-
return dict(page = context, content = content, edit_url = edit_url,
- logged_in = logged_in)
+ logged_in = authenticated_userid(request))
@view_config(name='add_page', context='.models.Wiki',
renderer='templates/edit.pt',
@@ -65,9 +64,8 @@ def add_page(context, request):
page.__name__ = name
page.__parent__ = context
- logged_in = authenticated_userid(request)
-
- return dict(page = page, save_url = save_url, logged_in = logged_in)
+ return dict(page = page, save_url = save_url,
+ logged_in = authenticated_userid(request))
@view_config(name='edit_page', context='.models.Page',
renderer='templates/edit.pt',
@@ -77,11 +75,9 @@ def edit_page(context, request):
context.data = request.params['body']
return HTTPFound(location = request.resource_url(context))
- logged_in = authenticated_userid(request)
-
return dict(page = context,
save_url = request.resource_url(context, 'edit_page'),
- logged_in = logged_in)
+ logged_in = authenticated_userid(request))
@view_config(context='.models.Wiki', name='login',
renderer='templates/login.pt')
diff --git a/docs/tutorials/wiki/src/tests/tutorial/models.py b/docs/tutorials/wiki/src/tests/tutorial/models.py
index 0a31c38be..582ff0d7e 100644
--- a/docs/tutorials/wiki/src/tests/tutorial/models.py
+++ b/docs/tutorials/wiki/src/tests/tutorial/models.py
@@ -1,8 +1,10 @@
from persistent import Persistent
from persistent.mapping import PersistentMapping
-from pyramid.security import Allow
-from pyramid.security import Everyone
+from pyramid.security import (
+ Allow,
+ Everyone,
+ )
class Wiki(PersistentMapping):
__name__ = None
diff --git a/docs/tutorials/wiki/src/tests/tutorial/views.py b/docs/tutorials/wiki/src/tests/tutorial/views.py
index fcbe6fe25..21f12b31d 100644
--- a/docs/tutorials/wiki/src/tests/tutorial/views.py
+++ b/docs/tutorials/wiki/src/tests/tutorial/views.py
@@ -9,9 +9,9 @@ from pyramid.view import (
)
from pyramid.security import (
- authenticated_userid,
remember,
forget,
+ authenticated_userid,
)
from .security import USERS
@@ -20,12 +20,13 @@ from .models import Page
# regular expression used to find WikiWords
wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)")
-@view_config(context='.models.Wiki', permission='view')
+@view_config(context='.models.Wiki',
+ permission='view')
def view_wiki(context, request):
return HTTPFound(location=request.resource_url(context, 'FrontPage'))
-@view_config(context='.models.Page',
- renderer='templates/view.pt', permission='view')
+@view_config(context='.models.Page', renderer='templates/view.pt',
+ permission='view')
def view_page(context, request):
wiki = context.__parent__
@@ -43,10 +44,8 @@ def view_page(context, request):
content = wikiwords.sub(check, content)
edit_url = request.resource_url(context, 'edit_page')
- logged_in = authenticated_userid(request)
-
return dict(page = context, content = content, edit_url = edit_url,
- logged_in = logged_in)
+ logged_in = authenticated_userid(request))
@view_config(name='add_page', context='.models.Wiki',
renderer='templates/edit.pt',
@@ -65,9 +64,8 @@ def add_page(context, request):
page.__name__ = name
page.__parent__ = context
- logged_in = authenticated_userid(request)
-
- return dict(page = page, save_url = save_url, logged_in = logged_in)
+ return dict(page = page, save_url = save_url,
+ logged_in = authenticated_userid(request))
@view_config(name='edit_page', context='.models.Page',
renderer='templates/edit.pt',
@@ -77,11 +75,9 @@ def edit_page(context, request):
context.data = request.params['body']
return HTTPFound(location = request.resource_url(context))
- logged_in = authenticated_userid(request)
-
return dict(page = context,
save_url = request.resource_url(context, 'edit_page'),
- logged_in = logged_in)
+ logged_in = authenticated_userid(request))
@view_config(context='.models.Wiki', name='login',
renderer='templates/login.pt')
diff --git a/docs/tutorials/wiki2/authorization.rst b/docs/tutorials/wiki2/authorization.rst
index 8c1c50ff6..2ef55d15b 100644
--- a/docs/tutorials/wiki2/authorization.rst
+++ b/docs/tutorials/wiki2/authorization.rst
@@ -8,21 +8,31 @@ Adding Authorization
:term:`authorization`. We'll make use of both features to provide security
to our application. Our application currently allows anyone with access to
the server to view, edit, and add pages to our wiki. We'll change that
-to allow only people who possess a specific username (`editor`)
-to add and edit wiki pages but we'll continue allowing anyone with access to
-the server to view pages.
+to allow only people who are members of a *group* named ``group:editors``
+to add and edit wiki pages but we'll continue allowing
+anyone with access to the server to view pages.
-We will do the following steps:
+We will also add a login page and a logout link on all the
+pages. The login page will be shown when a user is denied
+access to any of the views that require a permission, instead of
+a default "403 Forbidden" page.
-* Add a :term:`root factory` with an :term:`ACL` (``models.py``).
+We will implement the access control with the following steps:
+
+* Add users and groups (``security.py``, a new module).
+* Add an :term:`ACL` (``models.py`` and
+ ``__init__.py``).
* Add an :term:`authentication policy` and an :term:`authorization policy`
(``__init__.py``).
-* Add an authentication policy callback (new ``security.py`` module).
-* Add ``login`` and ``logout`` views (``views.py``).
* Add :term:`permission` declarations to the ``edit_page`` and ``add_page``
views (``views.py``).
+
+Then we will add the login and logout feature:
+
+* Add routes for /login and /logout (``__init__.py``).
+* Add ``login`` and ``logout`` views (``views.py``).
+* Add a login template (``login.pt``).
* Make the existing views return a ``logged_in`` flag to the renderer (``views.py``).
-* Add a login template (new ``login.pt``).
* Add a "Logout" link to be shown when logged in and viewing or editing a page
(``view.pt``, ``edit.pt``).
@@ -30,39 +40,88 @@ The source code for this tutorial stage can be browsed at
`http://github.com/Pylons/pyramid/tree/1.3-branch/docs/tutorials/wiki2/src/authorization/
<http://github.com/Pylons/pyramid/tree/1.3-branch/docs/tutorials/wiki2/src/authorization/>`_.
-Adding A Root Factory
-~~~~~~~~~~~~~~~~~~~~~
+Access Control
+--------------
+
+Add users and groups
+~~~~~~~~~~~~~~~~~~~~
+
+Create a new ``tutorial/tutorial/security.py`` module with the
+following content:
+
+.. literalinclude:: src/authorization/tutorial/security.py
+ :linenos:
+ :language: python
+
+The ``groupfinder`` function accepts a userid and a request and
+returns one of these values:
+
+- If the userid exists in the system, it will return a
+ sequence of group identifiers (or an empty sequence if the user
+ isn't a member of any groups).
+- If the userid *does not* exist in the system, it will
+ return ``None``.
-Open ``models.py`` and add the following statements:
+For example, ``groupfinder('editor', request )`` returns ['group:editor'],
+``groupfinder('viewer', request)`` returns [], and ``groupfinder('admin', request)``
+returns ``None``. We will use ``groupfinder()`` as an :term:`authentication policy`
+"callback" that will provide the :term:`principal` or principals
+for a user.
+
+In a production system, user and group
+data will most often come from a database, but here we use "dummy"
+data to represent user and groups sources.
+
+Add an ACL
+~~~~~~~~~~
+
+Open ``tutorial/tutorial/models.py`` and add the following import
+statement at the head:
.. literalinclude:: src/authorization/tutorial/models.py
- :lines: 1-4,35-39
+ :lines: 1-4
:linenos:
:language: python
-We're going to start to use a custom :term:`root factory` within our
-``__init__.py`` file. The objects generated by the root factory will
-be used as the :term:`context` of each request to our application.
-Those context objects will be decorated with security
-declarations. When we use a custom root factory to generate
-our contexts, we can begin to make use of the declarative security
-features of :app:`Pyramid`.
-
-We'll modify our ``__init__.py``, passing in a :term:`root factory` to our
-:term:`Configurator` constructor. We'll point it at the new class we created
-inside our ``models.py`` file.
-
-The ``RootFactory`` class we've just added will be used by :app:`Pyramid` to
-construct a ``context`` object. The context is attached to the request
-object passed to our view callables as the ``context`` attribute.
-
-The context object generated by our root factory will possess an ``__acl__``
-attribute that allows :data:`pyramid.security.Everyone` (a special principal)
-to view all pages, while allowing only a :term:`principal` named
-``group:editors`` to edit and add pages. The ``__acl__`` attribute attached
-to a context is interpreted specially by :app:`Pyramid` as an access control
-list during view callable execution. See :ref:`assigning_acls` for more
-information about what an :term:`ACL` represents.
+Add the following class definition:
+
+.. literalinclude:: src/authorization/tutorial/models.py
+ :lines: 35-39
+ :linenos:
+ :language: python
+
+We import :data:`~pyramid.security.Allow`, an action that
+means that permission is allowed:, and
+:data:`~pyramid.security.Everyone`, a special :term:`principal`
+that is associated to all requests. Both are used in the
+:term:`ACE` entries that make up the ACL.
+
+The ACL is a list that needs to be named `__acl__` and be an
+attribute of a class. We define an :term:`ACL` with two
+:term:`ACE` entries: the first entry allows any user the `view`
+permission. The second entry allows the ``group:editors``
+principal the `edit` permission.
+
+The ``RootFactory`` class that contains the ACL is a :term:`root factory`.
+We need to associate it to our :app:`Pyramid` application, so the ACL is
+provided to each view in the :term:`context` of the request, as
+the ``context`` attribute.
+
+Open ``tutorial/tutorial/__init__.py`` and add a ``root_factory``
+parameter to our :term:`Configurator` constructor, that points to
+the class we created above:
+
+.. literalinclude:: src/authorization/tutorial/__init__.py
+ :lines: 19-20
+ :linenos:
+ :emphasize-lines: 2
+ :language: python
+
+(Only the highlighted line needs to be added.)
+
+We are now providing the ACL to the application. See
+:ref:`assigning_acls` for more information about what an
+:term:`ACL` represents.
.. note::
@@ -71,23 +130,10 @@ information about what an :term:`ACL` represents.
the ``factory`` argument to
:meth:`pyramid.config.Configurator.add_route` for more info.
-We'll pass the ``RootFactory`` we created in the step above in as the
-``root_factory`` argument to a :term:`Configurator`.
-
-Add an Authorization Policy and an Authentication Policy
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-We're going to be making several changes to our ``__init__.py`` file which
-will help us configure an authorization policy.
+Add Authentication and Authorization Policies
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-For any :app:`Pyramid` application to perform authorization, we need to add a
-``security.py`` module (we'll do that shortly) and we'll need to change our
-``__init__.py`` file to add an :term:`authentication policy` and an
-:term:`authorization policy` which uses the ``security.py`` file for a
-*callback*.
-
-We'll enable an ``AuthTktAuthenticationPolicy`` and an ``ACLAuthorizationPolicy``
-to implement declarative security checking. Open ``tutorial/__init__.py`` and
+Open ``tutorial/__init__.py`` and
add these import statements:
.. literalinclude:: src/authorization/tutorial/__init__.py
@@ -100,183 +146,173 @@ Now add those policies to the configuration:
.. literalinclude:: src/authorization/tutorial/__init__.py
:lines: 16-22
:linenos:
+ :emphasize-lines: 1-3,6-7
:language: python
+(Only the highlighted lines need to be added.)
+
+We are enabling an ``AuthTktAuthenticationPolicy``, it is based in an auth
+ticket that may be included in the request, and an ``ACLAuthorizationPolicy``
+that uses an ACL to determine the allow or deny outcome for a view.
+
Note that the
:class:`pyramid.authentication.AuthTktAuthenticationPolicy` constructor
accepts two arguments: ``secret`` and ``callback``. ``secret`` is a string
representing an encryption key used by the "authentication ticket" machinery
-represented by this policy: it is required. The ``callback`` is a
-``groupfinder`` function in the current directory's ``security.py`` file. We
-haven't added that module yet, but we're about to.
+represented by this policy: it is required. The ``callback`` is the
+``groupfinder()`` function that we created before.
-Viewing Your Changes
---------------------
+Add permission declarations
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
-When we're done configuring a root factory, adding a authentication and
-authorization policies, and adding routes for ``/login`` and ``/logout``,
-your application's ``__init__.py`` will look like this:
+Add a ``permission='edit'`` parameter to the ``@view_config``
+decorator for ``add_page()`` and ``edit_page()``, for example:
-.. literalinclude:: src/authorization/tutorial/__init__.py
+.. code-block:: python
:linenos:
- :emphasize-lines: 2-3,7,16-18,20-22,25-26
- :language: python
+ :emphasize-lines: 2
-Adding an authentication policy callback
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ @view_config(route_name='add_page', renderer='templates/edit.pt',
+ permission='edit')
-Add a ``tutorial/security.py`` module within your package (in the same
-directory as :file:`__init__.py`, :file:`views.py`, etc.) with the
-following content:
+(Only the highlighted line needs to be added.)
-.. literalinclude:: src/authorization/tutorial/security.py
+The result is that only users who possess the ``edit``
+permission at the time of the request may invoke those two views.
+
+Add a ``permission='view'`` parameter to the ``@view_config``
+decorator for ``view_wiki()`` and ``view_page()``, like this:
+
+.. code-block:: python
+ :linenos:
+ :emphasize-lines: 2
+
+ @view_config(route_name='view_page', renderer='templates/view.pt',
+ permission='view')
+
+(Only the highlighted line needs to be added.)
+
+This allows anyone to invoke these two views.
+
+We are done with the changes needed to control access. The
+changes that follow will add the login and logout feature.
+
+Login, Logout
+-------------
+
+Add routes for /login and /logout
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Go back to ``tutorial/tutorial/__init__.py`` and add these two
+routes:
+
+.. literalinclude:: src/authorization/tutorial/__init__.py
+ :lines: 25-26
:linenos:
:language: python
-The ``groupfinder`` function defined here is an :term:`authentication policy`
-"callback"; it is a callable that accepts a userid and a request. If
-the userid exists in the system, the callback will return a sequence
-of group identifiers (or an empty sequence if the user isn't a member
-of any groups). If the userid *does not* exist in the system, the
-callback will return ``None``. In a production system, user and group
-data will most often come from a database, but here we use "dummy"
-data to represent user and groups sources. Note that the ``editor``
-user is a member of the ``group:editors`` group in our dummy group
-data (the ``GROUPS`` data structure).
-
-We've given the ``editor`` user membership to the ``group:editors`` by
-mapping him to this group in the ``GROUPS`` data structure (``GROUPS =
-{'editor':['group:editors']}``). Since the ``groupfinder`` function
-consults the ``GROUPS`` data structure, this will mean that, as a
-result of the ACL attached to the :term:`context` object returned by
-the root factory, and the permission associated with the ``add_page``
-and ``edit_page`` views, the ``editor`` user should be able to add and
-edit pages.
-
-Adding Login and Logout Views
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Add Login and Logout Views
+~~~~~~~~~~~~~~~~~~~~~~~~~~
-To our ``views.py`` we'll add a ``login`` view callable which renders a login
-form and processes the post from the login form, checking credentials.
+We'll add a ``login`` view which renders a login form and processes
+the post from the login form, checking credentials.
We'll also add a ``logout`` view callable to our application and
provide a link to it. This view will clear the credentials of the
logged in user and redirect back to the front page.
-The ``login`` view callable will look something like this:
+Add the following import statements to the
+head of ``tutorial/tutorial/views.py``:
.. literalinclude:: src/authorization/tutorial/views.py
- :lines: 89-115
+ :lines: 9-16,18,24-25
:linenos:
+ :emphasize-lines: 3,6-9,11
:language: python
-The ``logout`` view callable will look something like this:
+(Only the highlighted lines need to be added.)
-.. literalinclude:: src/authorization/tutorial/views.py
- :lines: 117-121
- :linenos:
- :language: python
+:meth:`~pyramid.view.forbidden_view_config` will be used
+to customize the default 403 Forbidden page.
+:meth:`~pyramid.security.remember` and
+:meth:`~pyramid.security.forget` help to create and
+expire an auth ticket cookie.
-The ``login`` view callable is decorated with two decorators, a
-``@view_config`` decorator, which associates it with the ``login``
-route, and a ``@forbidden_view_config`` decorator which turns it in to
-an :term:`exception view`. The one which associates it with the
-``login`` route makes it visible when we visit ``/login``. The other
-one makes it a :term:`forbidden view`. The forbidden view is
-displayed whenever Pyramid or your application raises an
-:class:`pyramid.httpexceptions.HTTPForbidden` exception. In this
-case, we'll be relying on the forbidden view to show the login form
-whenver someone attempts to execute an action which they're not yet
-authorized to perform.
-
-The ``logout`` view callable is decorated with a ``@view_config`` decorator
-which associates it with the ``logout`` route. This makes it visible when we
-visit ``/logout``.
-
-We'll need to import some stuff to service the needs of these two functions:
-the ``pyramid.view.forbidden_view_config`` class, a number of values from the
-``pyramid.security`` module, and a value from our newly added
-``tutorial.security`` package. Add the following import statements to the
-head of the ``views.py`` file:
+Now add the ``login`` and ``logout`` views:
.. literalinclude:: src/authorization/tutorial/views.py
- :lines: 9-18,24-25
+ :lines: 91-123
:linenos:
:language: python
-Changing Existing Views
-~~~~~~~~~~~~~~~~~~~~~~~
+``login()`` is decorated with two decorators:
-Add permision declarations
---------------------------
+- a ``@view_config`` decorator which associates it with the
+ ``login`` route and makes it visible when we visit ``/login``,
+- a ``@forbidden_view_config`` decorator which turns it into
+ an :term:`forbidden view`. ``login()`` will be invoked
+ when a users tries to execute a view callable that
+ they are not allowed to. For example, if a user has not logged in
+ and tries to add or edit a Wiki page, he will be shown the
+ login form before being allowed to continue on.
-Then we need to change each of our ``view_page``, ``edit_page`` and
-``add_page`` view callables in ``views.py``. Within each of these views,
-we'll need to pass a "logged in" parameter to its template. We'll add
-something like this to each view body:
+The order of these two :term:`view configuration` decorators
+is unimportant.
-.. code-block:: python
- :linenos:
+``logout()`` is decorated with a ``@view_config`` decorator
+which associates it with the ``logout`` route. It will be
+invoked when we visit ``/logout``.
- from pyramid.security import authenticated_userid
- logged_in = authenticated_userid(request)
+Add the ``login.pt`` Template
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-Return a logged_in flag to the renderer
----------------------------------------
+Create ``tutorial/tutorial/templates/login.pt`` with the following
+content:
-We'll then change the return value of these views to pass the resulting
-``logged_in`` value to the template, e.g.:
+.. literalinclude:: src/authorization/tutorial/templates/login.pt
+ :language: xml
-.. code-block:: python
- :linenos:
+The above template is referred to within the login view we just
+added to ``views.py``.
- return dict(page = page,
- content = content,
- logged_in = logged_in,
- edit_url = edit_url)
+Return a logged_in flag to the renderer
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-We'll also need to add a ``permission`` value to the ``@view_config``
-decorator for each of the ``add_page`` and ``edit_page`` view callables. For
-each, we'll add ``permission='edit'``, for example:
+Add the following line to the import at the head of
+``tutorial/tutorial/views.py``:
-.. code-block:: python
+.. literalinclude:: src/authorization/tutorial/views.py
+ :lines: 14-18
:linenos:
+ :emphasize-lines: 4
+ :language: python
- @view_config(route_name='edit_page', renderer='templates/edit.pt',
- permission='edit')
+(Only the highlighted line needs to be added.)
-See the ``permission='edit'`` we added there? This indicates that the view
-callables which these views reference cannot be invoked without the
-authenticated user possessing the ``edit`` permission with respect to the
-current :term:`context`.
+Add a ``logged_in`` parameter to the return value of
+``view_page()``, ``edit_page()`` and ``add_page()``,
+like this:
-Adding these ``permission`` arguments causes Pyramid to make the
-assertion that only users who possess the effective ``edit``
-permission at the time of the request may invoke those two views.
-We've granted the ``group:editors`` :term:`principal` the ``edit``
-permission in the :term:`root factory` via its ACL, so only a user who
-is a member of the group named ``group:editors`` will be able to
-invoke the views associated with the ``add_page`` or ``edit_page``
-routes.
+.. code-block:: python
+ :linenos:
+ :emphasize-lines: 4
-Adding the ``login.pt`` Template
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ return dict(page = page,
+ content = content,
+ edit_url = edit_url,
+ logged_in = authenticated_userid(request))
-Add a ``login.pt`` template to your templates directory. It's
-referred to within the login view we just added to ``views.py``.
+(Only the highlighted line needs to be added.)
-.. literalinclude:: src/authorization/tutorial/templates/login.pt
- :language: xml
+:meth:`~pyramid.security.authenticated_userid()` will return None
+if the user is not authenticated, or some user id it the user
+is authenticated.
Add a "Logout" link when logged in
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-We'll also need to change our ``edit.pt`` and ``view.pt`` templates to
-display a "Logout" link if someone is logged in. This link will
-invoke the logout view.
-
-To do so we'll add this to both templates within the ``<div id="right"
-class="app-welcome align-right">`` div:
+Open ``tutorial/tutorial/templates/edit.pt`` and
+``tutorial/tutorial/templates/view.pt`` and add this within the
+``<div id="right" class="app-welcome align-right">`` div:
.. code-block:: xml
@@ -284,36 +320,66 @@ class="app-welcome align-right">`` div:
<a href="${request.application_url}/logout">Logout</a>
</span>
-Seeing Our Changes To ``views.py`` and our Templates
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+The attribute ``tal:condition="logged_in"`` will make the element be
+included when ``logged_in`` is any user id. The link will invoke
+the logout view. The above element will not be included if ``logged_in``
+is ``None``, such as when a user is not authenticated.
+
+Seeing Our Changes
+------------------
+
+Our ``tutorial/tutorial/__init__.py`` will look something like this
+when we're done:
+
+.. literalinclude:: src/authorization/tutorial/__init__.py
+ :linenos:
+ :emphasize-lines: 2-3,7,16-18,20-22,25-26
+ :language: python
+
+(Only the highlighted lines need to be added.)
-Our ``views.py`` module will look something like this when we're done:
+Our ``tutorial/tutorial/models.py`` will look something like this
+when we're done:
+
+.. literalinclude:: src/authorization/tutorial/models.py
+ :linenos:
+ :emphasize-lines: 1-4,35-39
+ :language: python
+
+(Only the highlighted lines need to be added.)
+
+Our ``tutorial/tutorial/views.py`` will look something like this
+when we're done:
.. literalinclude:: src/authorization/tutorial/views.py
:linenos:
- :emphasize-lines: 11,14-18,56,59,71,74,89-115,117-121
+ :emphasize-lines: 11,14-18,31,37,58,61,73,76,88,91-117,119-123
:language: python
(Only the highlighted lines need to be added.)
-Our ``edit.pt`` template will look something like this when we're done:
+Our ``tutorial/tutorial/templates/edit.pt`` template will look
+something like this when we're done:
.. literalinclude:: src/authorization/tutorial/templates/edit.pt
+ :linenos:
:emphasize-lines: 41-43
:language: xml
(Only the highlighted lines need to be added.)
-Our ``view.pt`` template will look something like this when we're done:
+Our ``tutorial/tutorial/templates/view.pt`` template will look
+something like this when we're done:
.. literalinclude:: src/authorization/tutorial/templates/view.pt
+ :linenos:
:emphasize-lines: 41-43
:language: xml
(Only the highlighted lines need to be added.)
Viewing the Application in a Browser
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+------------------------------------
We can finally examine our application in a browser (See
:ref:`wiki2-start-the-application`). Launch a browser and visit
diff --git a/docs/tutorials/wiki2/definingviews.rst b/docs/tutorials/wiki2/definingviews.rst
index ac58e1e46..efb72230e 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 1ff000549..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
-------
@@ -87,7 +96,7 @@ listed in the following table:
| | | | | |
| | | | | |
+----------------------+-----------------------+-------------+------------+------------+
-| /edit_page/PageName | Display edit form | edit_page | edit.pt | edit |
+| /PageName/edit_page | Display edit form | edit_page | edit.pt | edit |
| | with existing | | | |
| | content. | | | |
| | | | | |
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/authorization/tutorial/views.py b/docs/tutorials/wiki2/src/authorization/tutorial/views.py
index 1453cd2e6..c7670b049 100644
--- a/docs/tutorials/wiki2/src/authorization/tutorial/views.py
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/views.py
@@ -27,12 +27,14 @@ from .security import USERS
# regular expression used to find WikiWords
wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)")
-@view_config(route_name='view_wiki')
+@view_config(route_name='view_wiki',
+ permission='view')
def view_wiki(request):
return HTTPFound(location = request.route_url('view_page',
pagename='FrontPage'))
-@view_config(route_name='view_page', renderer='templates/view.pt')
+@view_config(route_name='view_page', renderer='templates/view.pt',
+ permission='view')
def view_page(request):
pagename = request.matchdict['pagename']
page = DBSession.query(Page).filter_by(name=pagename).first()
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/tests/tutorial/views.py b/docs/tutorials/wiki2/src/tests/tutorial/views.py
index 465d98ae1..f2a33af1e 100644
--- a/docs/tutorials/wiki2/src/tests/tutorial/views.py
+++ b/docs/tutorials/wiki2/src/tests/tutorial/views.py
@@ -27,12 +27,14 @@ from .security import USERS
# regular expression used to find WikiWords
wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)")
-@view_config(route_name='view_wiki')
+@view_config(route_name='view_wiki',
+ permission='view')
def view_wiki(request):
return HTTPFound(location = request.route_url('view_page',
pagename='FrontPage'))
-@view_config(route_name='view_page', renderer='templates/view.pt')
+@view_config(route_name='view_page', renderer='templates/view.pt',
+ permission='view')
def view_page(request):
pagename = request.matchdict['pagename']
session = DBSession()
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/factories.py b/pyramid/config/factories.py
index eb4442e98..ccbf3bbe9 100644
--- a/pyramid/config/factories.py
+++ b/pyramid/config/factories.py
@@ -10,6 +10,7 @@ from pyramid.interfaces import (
)
from pyramid.traversal import DefaultRootFactory
+from pyramid.util import InstancePropertyMixin
class FactoriesConfiguratorMixin(object):
@action_method
@@ -118,8 +119,8 @@ class FactoriesConfiguratorMixin(object):
"""
callable = self.maybe_dotted(callable)
- if name is None:
- name = callable.__name__
+ name, callable = InstancePropertyMixin._make_property(
+ callable, name=name, reify=reify)
def register():
plist = self.registry.queryUtility(IRequestProperties)
@@ -130,7 +131,7 @@ class FactoriesConfiguratorMixin(object):
self.registry.registerHandler(_set_request_properties,
(INewRequest,))
- plist.append((name, callable, reify))
+ plist.append((name, callable))
intr = self.introspectable('request properties', name,
self.object_description(callable),
@@ -143,6 +144,4 @@ class FactoriesConfiguratorMixin(object):
def _set_request_properties(event):
request = event.request
plist = request.registry.queryUtility(IRequestProperties)
- for prop in plist:
- name, callable, reify = prop
- request.set_property(callable, name=name, reify=reify)
+ request._set_properties(plist)
diff --git a/pyramid/config/util.py b/pyramid/config/util.py
index b8d0f2319..4e4c93be3 100644
--- a/pyramid/config/util.py
+++ b/pyramid/config/util.py
@@ -113,6 +113,12 @@ def make_predicates(xhr=None, request_method=None, path_info=None,
# any predicates get an order of MAX_ORDER, meaning that they will
# be tried very last.
+ # NB: each predicate callable constructed by this function (or examined
+ # by this function, in the case of custom predicates) must leave this
+ # function with a ``__text__`` attribute. The subsystem which reports
+ # errors when no predicates match depends upon the existence of this
+ # attribute on each predicate callable.
+
predicates = []
weights = []
h = md5()
@@ -273,6 +279,7 @@ def make_predicates(xhr=None, request_method=None, path_info=None,
tvalue = tgenerate(m) # tvalue will be urlquoted string
m['traverse'] = traversal_path(tvalue) # will be seq of unicode
return True
+ traverse_predicate.__text__ = 'traverse matchdict pseudo-predicate'
# This isn't actually a predicate, it's just a infodict
# modifier that injects ``traverse`` into the matchdict. As a
# result, the ``traverse_predicate`` function above always
diff --git a/pyramid/config/views.py b/pyramid/config/views.py
index ac41f7363..4354b4691 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
@@ -387,8 +387,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')
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 5d9d29afa..1445ee394 100644
--- a/pyramid/interfaces.py
+++ b/pyramid/interfaces.py
@@ -1105,6 +1105,12 @@ class IAssetDescriptor(Interface):
Returns True if asset exists, otherwise returns False.
"""
+class IJSONAdapter(Interface):
+ """
+ Marker interface for objects that can convert an arbitrary object
+ into a JSON-serializable primitive.
+ """
+
# configuration phases: a lower phase number means the actions associated
# with this phase will be executed earlier than those with later phase
# numbers. The default phase number is 0, FTR.
diff --git a/pyramid/mako_templating.py b/pyramid/mako_templating.py
index 208e54bf5..bb4ccb2f0 100644
--- a/pyramid/mako_templating.py
+++ b/pyramid/mako_templating.py
@@ -1,4 +1,5 @@
import os
+import re
import sys
import threading
@@ -76,7 +77,13 @@ class MakoRendererFactoryHelper(object):
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
@@ -141,7 +148,7 @@ class MakoRendererFactoryHelper(object):
finally:
registry_lock.release()
- return MakoLookupTemplateRenderer(path, lookup)
+ return MakoLookupTemplateRenderer(path, defname, lookup)
renderer_factory = MakoRendererFactoryHelper('mako.')
@@ -156,8 +163,16 @@ 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):
@@ -167,16 +182,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/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/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/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_factories.py b/pyramid/tests/test_config/test_factories.py
index 0930f9603..1dfeda34c 100644
--- a/pyramid/tests/test_config/test_factories.py
+++ b/pyramid/tests/test_config/test_factories.py
@@ -73,7 +73,7 @@ class TestFactoriesMixin(unittest.TestCase):
callable = lambda x: None
config.set_request_property(callable, name='foo')
plist = config.registry.getUtility(IRequestProperties)
- self.assertEqual(plist, [('foo', callable, False)])
+ self.assertEqual(set(p[0] for p in plist), set(['foo']))
def test_set_request_property_with_unnamed_callable(self):
from pyramid.interfaces import IRequestProperties
@@ -81,7 +81,7 @@ class TestFactoriesMixin(unittest.TestCase):
def foo(self): pass
config.set_request_property(foo, reify=True)
plist = config.registry.getUtility(IRequestProperties)
- self.assertEqual(plist, [('foo', foo, True)])
+ self.assertEqual(set(p[0] for p in plist), set(['foo']))
def test_set_request_property_with_property(self):
from pyramid.interfaces import IRequestProperties
@@ -89,7 +89,7 @@ class TestFactoriesMixin(unittest.TestCase):
callable = property(lambda x: None)
config.set_request_property(callable, name='foo')
plist = config.registry.getUtility(IRequestProperties)
- self.assertEqual(plist, [('foo', callable, False)])
+ self.assertEqual(set(p[0] for p in plist), set(['foo']))
def test_set_multiple_request_properties(self):
from pyramid.interfaces import IRequestProperties
@@ -100,8 +100,7 @@ class TestFactoriesMixin(unittest.TestCase):
config.set_request_property(bar, name='bar')
config.commit()
plist = config.registry.getUtility(IRequestProperties)
- self.assertEqual(plist, [('foo', foo, True),
- ('bar', bar, False)])
+ self.assertEqual(set(p[0] for p in plist), set(['foo', 'bar']))
def test_set_multiple_request_properties_conflict(self):
from pyramid.exceptions import ConfigurationConflictError
@@ -125,20 +124,19 @@ class TestFactoriesMixin(unittest.TestCase):
request = DummyRequest(config.registry)
event = Event()
config.registry.notify(event)
- callables = event.request.callables
- self.assertEqual(callables, [('foo', foo, False),
- ('bar', foo, True)])
+ plist = event.request.plist
+ self.assertEqual(set(p[0] for p in plist), set(['foo', 'bar']))
class DummyRequest(object):
- callables = None
+ plist = None
def __init__(self, registry):
self.registry = registry
- def set_property(self, callable, name, reify):
- if self.callables is None:
- self.callables = []
- self.callables.append((name, callable, reify))
+ def _set_properties(self, properties):
+ if self.plist is None:
+ self.plist = []
+ self.plist.extend(properties)
diff --git a/pyramid/tests/test_mako_templating.py b/pyramid/tests/test_mako_templating.py
index fbb04273b..41fa9bdc4 100644
--- a/pyramid/tests/test_mako_templating.py
+++ b/pyramid/tests/test_mako_templating.py
@@ -315,7 +315,7 @@ class MakoLookupTemplateRendererTests(Base, unittest.TestCase):
def test_instance_implements_ITemplate(self):
from zope.interface.verify import verifyObject
from pyramid.interfaces import ITemplateRenderer
- verifyObject(ITemplateRenderer, self._makeOne(None, None))
+ verifyObject(ITemplateRenderer, self._makeOne(None, None, None))
def test_class_implements_ITemplate(self):
from zope.interface.verify import verifyClass
@@ -324,7 +324,7 @@ class MakoLookupTemplateRendererTests(Base, unittest.TestCase):
def test_call(self):
lookup = DummyLookup()
- instance = self._makeOne('path', lookup)
+ instance = self._makeOne('path', None, lookup)
result = instance({}, {'system':1})
self.assertTrue(isinstance(result, text_type))
self.assertEqual(result, text_('result'))
@@ -332,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,7 +379,7 @@ class MakoLookupTemplateRendererTests(Base, unittest.TestCase):
def test_implementation(self):
lookup = DummyLookup()
- instance = self._makeOne('path', lookup)
+ instance = self._makeOne('path', None, lookup)
result = instance.implementation().render_unicode()
self.assertTrue(isinstance(result, text_type))
self.assertEqual(result, text_('result'))
@@ -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})
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_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..68cefbe6e 100644
--- a/pyramid/tests/test_scaffolds/test_copydir.py
+++ b/pyramid/tests/test_scaffolds/test_copydir.py
@@ -170,9 +170,11 @@ class Test_makedirs(unittest.TestCase):
def test_makedirs_parent_dir(self):
import shutil
- target = "/tmp/nonexistent_dir/nonexistent_subdir"
+ import tempfile
+ tmpdir = tempfile.mkdtemp()
+ target = os.path.join(tmpdir, 'nonexistent_subdir')
self._callFUT(target, 2, None)
- shutil.rmtree("/tmp/nonexistent_dir")
+ shutil.rmtree(tmpdir)
class Test_support_functions(unittest.TestCase):
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_url.py b/pyramid/tests/test_url.py
index 0dff1e648..50deb63f3 100644
--- a/pyramid/tests/test_url.py
+++ b/pyramid/tests/test_url.py
@@ -113,6 +113,14 @@ class TestURLMethodsMixin(unittest.TestCase):
self.assertEqual(result,
'http://example.com:5432/context/a?a=hi+there&b=La+Pe%C3%B1a')
+ def test_resource_url_with_query_empty(self):
+ request = self._makeOne()
+ self._registerResourceURL(request.registry)
+ context = DummyContext()
+ result = request.resource_url(context, 'a', query=[])
+ self.assertEqual(result,
+ 'http://example.com:5432/context/a')
+
def test_resource_url_anchor_is_after_root_when_no_elements(self):
request = self._makeOne()
self._registerResourceURL(request.registry)
@@ -334,6 +342,15 @@ class TestURLMethodsMixin(unittest.TestCase):
self.assertEqual(result,
'http://example.com:5432/1/2/3?q=1')
+ def test_route_url_with_empty_query(self):
+ from pyramid.interfaces import IRoutesMapper
+ request = self._makeOne()
+ mapper = DummyRoutesMapper(route=DummyRoute('/1/2/3'))
+ request.registry.registerUtility(mapper, IRoutesMapper)
+ result = request.route_url('flub', _query={})
+ self.assertEqual(result,
+ 'http://example.com:5432/1/2/3')
+
def test_route_url_with_app_url(self):
from pyramid.interfaces import IRoutesMapper
request = self._makeOne()
diff --git a/pyramid/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_util.py b/pyramid/tests/test_util.py
index 824ee329f..e83ad5922 100644
--- a/pyramid/tests/test_util.py
+++ b/pyramid/tests/test_util.py
@@ -3,12 +3,12 @@ from pyramid.compat import PY3
class Test_InstancePropertyMixin(unittest.TestCase):
def _makeOne(self):
- cls = self._targetClass()
+ cls = self._getTargetClass()
class Foo(cls):
pass
return Foo()
- def _targetClass(self):
+ def _getTargetClass(self):
from pyramid.util import InstancePropertyMixin
return InstancePropertyMixin
@@ -109,6 +109,29 @@ class Test_InstancePropertyMixin(unittest.TestCase):
foo.set_property(lambda _: 2, name='x', reify=True)
self.assertEqual(1, foo.x)
+ def test__make_property(self):
+ from pyramid.decorator import reify
+ cls = self._getTargetClass()
+ name, fn = cls._make_property(lambda x: 1, name='x', reify=True)
+ self.assertEqual(name, 'x')
+ self.assertTrue(isinstance(fn, reify))
+
+ def test__set_properties_with_iterable(self):
+ foo = self._makeOne()
+ x = foo._make_property(lambda _: 1, name='x', reify=True)
+ y = foo._make_property(lambda _: 2, name='y')
+ foo._set_properties([x, y])
+ self.assertEqual(1, foo.x)
+ self.assertEqual(2, foo.y)
+
+ def test__set_properties_with_dict(self):
+ foo = self._makeOne()
+ x_name, x_fn = foo._make_property(lambda _: 1, name='x', reify=True)
+ y_name, y_fn = foo._make_property(lambda _: 2, name='y')
+ foo._set_properties({x_name: x_fn, y_name: y_fn})
+ self.assertEqual(1, foo.x)
+ self.assertEqual(2, foo.y)
+
class Test_WeakOrderedSet(unittest.TestCase):
def _makeOne(self):
from pyramid.config import WeakOrderedSet
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 cca1872b7..7d5c97814 100644
--- a/pyramid/util.py
+++ b/pyramid/util.py
@@ -20,6 +20,60 @@ class InstancePropertyMixin(object):
on the class itself.
"""
+ @classmethod
+ def _make_property(cls, callable, name=None, reify=False):
+ """ Convert a callable into one suitable for adding to the
+ instance. This will return a 2-tuple containing the computed
+ (name, property) pair.
+ """
+
+ is_property = isinstance(callable, property)
+ if is_property:
+ fn = callable
+ if name is None:
+ raise ValueError('must specify "name" for a property')
+ if reify:
+ raise ValueError('cannot reify a property')
+ elif name is not None:
+ fn = lambda this: callable(this)
+ fn.__name__ = name
+ fn.__doc__ = callable.__doc__
+ else:
+ name = callable.__name__
+ fn = callable
+ if reify:
+ import pyramid.decorator # avoid circular import
+ fn = pyramid.decorator.reify(fn)
+ elif not is_property:
+ fn = property(fn)
+
+ return name, fn
+
+ def _set_properties(self, properties):
+ """ Create several properties on the instance at once.
+
+ This is a more efficient version of
+ :meth:`pyramid.util.InstancePropertyMixin.set_property` which
+ can accept multiple ``(name, property)`` pairs generated via
+ :meth:`pyramid.util.InstancePropertyMixin._make_property`.
+
+ ``attrs`` is a sequence of 2-tuples *or* a data structure with
+ an ``.items()`` method which returns a sequence of 2-tuples
+ (presumably a dictionary). It will be used used to add several
+ properties to the instance in a manner that is more efficient
+ than simply calling ``set_property`` repeatedly.
+ """
+
+ if hasattr(properties, 'items'):
+ attrs = properties.items()
+ else:
+ attrs = properties
+ attrs = dict(properties)
+
+ parent = self.__class__
+ cls = type(parent.__name__, (parent, object), attrs)
+ self.__class__ = cls
+
def set_property(self, callable, name=None, reify=False):
""" Add a callable or a property descriptor to the instance.
@@ -31,12 +85,11 @@ class InstancePropertyMixin(object):
A property may also be reified via the
:class:`pyramid.decorator.reify` decorator by setting
``reify=True``, allowing the result of the evaluation to be
- cached. Thus the value of the property is only computed once for
- the lifetime of the object.
+ cached. Using this method, the value of the property is only
+ computed once for the lifetime of the object.
``callable`` can either be a callable that accepts the instance
- as
- its single positional parameter, or it can be a property
+ as its single positional parameter, or it can be a property
descriptor.
If the ``callable`` is a property descriptor, the ``name``
@@ -73,30 +126,8 @@ class InstancePropertyMixin(object):
>>> foo.y # notice y keeps the original value
1
"""
-
- is_property = isinstance(callable, property)
- if is_property:
- fn = callable
- if name is None:
- raise ValueError('must specify "name" for a property')
- if reify:
- raise ValueError('cannot reify a property')
- elif name is not None:
- fn = lambda this: callable(this)
- fn.__name__ = name
- fn.__doc__ = callable.__doc__
- else:
- name = callable.__name__
- fn = callable
- if reify:
- import pyramid.decorator
- fn = pyramid.decorator.reify(fn)
- elif not is_property:
- fn = property(fn)
- attrs = { name: fn }
- parent = self.__class__
- cls = type(parent.__name__, (parent, object), attrs)
- self.__class__ = cls
+ prop = self._make_property(callable, name=name, reify=reify)
+ self._set_properties([prop])
class WeakOrderedSet(object):
""" Maintain a set of items.
diff --git a/pyramid/view.py b/pyramid/view.py
index d722c0cbb..1df0849c0 100644
--- a/pyramid/view.py
+++ b/pyramid/view.py
@@ -199,7 +199,7 @@ class view_config(object):
custom_predicates=default, context=default,
decorator=default, mapper=default, http_cache=default,
match_param=default):
- L = locals()
+ L = dict(locals()) # See issue #635 for dict() rationale
if (context is not default) or (for_ is not default):
L['context'] = context or for_
for k, v in L.items():
@@ -335,7 +335,7 @@ class notfound_view_config(object):
from pyramid.view import notfound_view_config
from pyramid.response import Response
- notfound_view_config()
+ @notfound_view_config()
def notfound(request):
return Response('Not found, dude!', status='404 Not Found')
@@ -367,7 +367,7 @@ class notfound_view_config(object):
path_info=default, custom_predicates=default,
decorator=default, mapper=default, match_param=default,
append_slash=False):
- L = locals()
+ L = dict(locals()) # See issue #635 for dict() rationale
for k, v in L.items():
if k not in ('self', 'L') and v is not default:
self.__dict__[k] = v
@@ -409,7 +409,7 @@ class forbidden_view_config(object):
from pyramid.view import forbidden_view_config
from pyramid.response import Response
- forbidden_view_config()
+ @forbidden_view_config()
def notfound(request):
return Response('You are not allowed', status='401 Unauthorized')
@@ -432,7 +432,7 @@ class forbidden_view_config(object):
xhr=default, accept=default, header=default,
path_info=default, custom_predicates=default,
decorator=default, mapper=default, match_param=default):
- L = locals()
+ L = dict(locals()) # See issue #635 for dict() rationale
for k, v in L.items():
if k not in ('self', 'L') and v is not default:
self.__dict__[k] = v
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