summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorcewing <cris@crisewing.com>2016-10-22 12:35:21 -0400
committercewing <cris@crisewing.com>2016-10-22 12:35:21 -0400
commit03f6d630364a9adf438e0bd7d7b9f3bc3175f093 (patch)
treede2aed9476c9d569375287a4ea89e9faf0f7b73a
parent1610c8fd3605f2ed481c37da27a1ce059419888a (diff)
parente73ae375581539ed42aa97d7cd6e96e6fbd64c79 (diff)
downloadpyramid-03f6d630364a9adf438e0bd7d7b9f3bc3175f093.tar.gz
pyramid-03f6d630364a9adf438e0bd7d7b9f3bc3175f093.tar.bz2
pyramid-03f6d630364a9adf438e0bd7d7b9f3bc3175f093.zip
Merge branch 'master' into issue.2614
-rw-r--r--.travis.yml6
-rw-r--r--CHANGES.txt94
-rw-r--r--CONTRIBUTORS.txt10
-rw-r--r--HACKING.txt239
-rw-r--r--README.rst24
-rw-r--r--RELEASING.txt14
-rw-r--r--docs/api/authentication.rst5
-rw-r--r--docs/conf.py2
-rw-r--r--docs/conventions.rst15
-rw-r--r--docs/narr/commandline.rst31
-rw-r--r--docs/narr/firstapp.rst6
-rw-r--r--docs/narr/hooks.rst60
-rw-r--r--docs/narr/i18n.rst2
-rw-r--r--docs/narr/install.rst73
-rw-r--r--docs/narr/introduction.rst2
-rw-r--r--docs/narr/project.rst14
-rw-r--r--docs/narr/sessions.rst9
-rw-r--r--docs/narr/upgrading.rst6
-rw-r--r--docs/narr/urldispatch.rst8
-rw-r--r--docs/narr/viewconfig.rst19
-rw-r--r--docs/narr/views.rst40
-rw-r--r--docs/quick_tour.rst46
-rw-r--r--docs/quick_tour/json/hello_world.jinja22
-rw-r--r--docs/quick_tour/sqla_demo/sqla_demo/models/__init__.py2
-rw-r--r--docs/quick_tour/sqla_demo/sqla_demo/models/meta.py2
-rw-r--r--docs/quick_tour/static_assets/hello_world.jinja22
-rw-r--r--docs/quick_tour/static_assets/hello_world_static.jinja22
-rw-r--r--docs/quick_tutorial/authentication.rst35
-rw-r--r--docs/quick_tutorial/authentication/setup.py3
-rw-r--r--docs/quick_tutorial/authentication/tutorial/security.py16
-rw-r--r--docs/quick_tutorial/authentication/tutorial/views.py7
-rw-r--r--docs/quick_tutorial/authorization/setup.py3
-rw-r--r--docs/quick_tutorial/authorization/tutorial/security.py16
-rw-r--r--docs/quick_tutorial/authorization/tutorial/views.py7
-rw-r--r--docs/quick_tutorial/debugtoolbar.rst2
-rw-r--r--docs/quick_tutorial/forms.rst10
-rw-r--r--docs/quick_tutorial/hello_world.rst2
-rw-r--r--docs/quick_tutorial/ini.rst2
-rw-r--r--docs/quick_tutorial/requirements.rst7
-rw-r--r--docs/quick_tutorial/routing.rst2
-rw-r--r--docs/quick_tutorial/scaffolds.rst6
-rw-r--r--docs/quick_tutorial/static_assets.rst2
-rw-r--r--docs/quick_tutorial/unit_testing.rst2
-rw-r--r--docs/tutorials/wiki2/basiclayout.rst2
-rw-r--r--docs/tutorials/wiki2/installation.rst7
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/models/__init__.py4
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/models/meta.py2
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/models/user.py5
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py4
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/models/meta.py2
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/models/user.py5
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/models/__init__.py2
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/models/meta.py2
-rw-r--r--docs/tutorials/wiki2/src/installation/tutorial/models/__init__.py2
-rw-r--r--docs/tutorials/wiki2/src/installation/tutorial/models/meta.py2
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/models/__init__.py4
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/models/meta.py2
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/models/user.py5
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/models/__init__.py4
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/models/meta.py2
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/models/user.py5
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/models/__init__.py4
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/models/meta.py2
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/models/user.py5
-rw-r--r--pyramid/authentication.py100
-rw-r--r--pyramid/config/__init__.py206
-rw-r--r--pyramid/config/security.py19
-rw-r--r--pyramid/config/tweens.py15
-rw-r--r--pyramid/config/views.py279
-rw-r--r--pyramid/decorator.py21
-rw-r--r--pyramid/exceptions.py1
-rw-r--r--pyramid/httpexceptions.py5
-rw-r--r--pyramid/interfaces.py2
-rw-r--r--pyramid/paster.py41
-rw-r--r--pyramid/renderers.py2
-rw-r--r--pyramid/router.py9
-rw-r--r--pyramid/scaffolds/__init__.py7
-rw-r--r--pyramid/scaffolds/alchemy/+package+/models/__init__.py_tmpl2
-rw-r--r--pyramid/scaffolds/alchemy/+package+/models/meta.py2
-rw-r--r--pyramid/scripting.py35
-rw-r--r--pyramid/scripts/common.py26
-rw-r--r--pyramid/scripts/pcreate.py24
-rw-r--r--pyramid/scripts/prequest.py3
-rw-r--r--pyramid/scripts/proutes.py13
-rw-r--r--pyramid/scripts/pserve.py3
-rw-r--r--pyramid/scripts/pshell.py3
-rw-r--r--pyramid/testing.py1
-rw-r--r--pyramid/tests/test_authentication.py78
-rw-r--r--pyramid/tests/test_config/test_init.py123
-rw-r--r--pyramid/tests/test_config/test_security.py6
-rw-r--r--pyramid/tests/test_config/test_views.py324
-rw-r--r--pyramid/tests/test_exceptions.py2
-rw-r--r--pyramid/tests/test_scripting.py21
-rw-r--r--pyramid/tests/test_scripts/dummy.py2
-rw-r--r--pyramid/tests/test_scripts/test_common.py30
-rw-r--r--pyramid/tests/test_scripts/test_pcreate.py21
-rw-r--r--pyramid/tests/test_scripts/test_prequest.py43
-rw-r--r--pyramid/tests/test_scripts/test_proutes.py27
-rw-r--r--pyramid/tests/test_view.py55
-rw-r--r--pyramid/tests/test_viewderivers.py50
-rw-r--r--pyramid/view.py75
-rw-r--r--pyramid/viewderivers.py56
-rw-r--r--setup.py10
-rw-r--r--tox.ini11
104 files changed, 1937 insertions, 745 deletions
diff --git a/.travis.yml b/.travis.yml
index fbdd88224..b46f677a6 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -6,8 +6,6 @@ matrix:
include:
- python: 2.7
env: TOXENV=py27
- - python: 3.3
- env: TOXENV=py33
- python: 3.4
env: TOXENV=py34
- python: 3.5
@@ -20,6 +18,10 @@ matrix:
env: TOXENV=docs
- python: 3.5
env: TOXENV=pep8
+ - python: nightly
+ env: TOXENV=py36
+ allow_failures:
+ - env: TOXENV=py36
install:
- travis_retry pip install tox
diff --git a/CHANGES.txt b/CHANGES.txt
index 4fdd7bf2f..434557f89 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -14,14 +14,108 @@ Backward Incompatibilities
To run your server as a daemon you should use a process manager instead of
pserve.
+ See https://github.com/Pylons/pyramid/pull/2615
+
+- ``pcreate`` is now interactive by default. You will be prompted if it
+ a file already exists with different content. Previously if there were
+ similar files it would silently skip them unless you specified
+ ``--interactive`` or ``--overwrite``.
+ See https://github.com/Pylons/pyramid/pull/2775
+
Features
--------
+- pcreate learned about --package-name to allow you to create a new project in
+ an existing folder with a different package name than the project name. See
+ https://github.com/Pylons/pyramid/pull/2783
+
+- The `_get_credentials` private method of `BasicAuthAuthenticationPolicy`
+ has been extracted into standalone function ``extract_http_basic_credentials`
+ in `pyramid.authentication` module, this function extracts HTTP Basic
+ credentials from a ``request`` object, and returns them as a named tuple.
+ See https://github.com/Pylons/pyramid/pull/2662
+
+- Pyramid 1.4 silently dropped a feature of the configurator that has been
+ restored. It's again possible for action discriminators to conflict across
+ different action orders.
+ See https://github.com/Pylons/pyramid/pull/2757
+
+- ``pyramid.paster.bootstrap`` and its sibling ``pyramid.scripting.prepare``
+ can now be used as context managers to automatically invoke the ``closer``
+ and pop threadlocals off of the stack to prevent memory leaks.
+ See https://github.com/Pylons/pyramid/pull/2760
+
+- Added ``pyramid.config.Configurator.add_exception_view`` and the
+ ``pyramid.view.exception_view_config`` decorator. It is now possible using
+ these methods or via the new ``exception_only=True`` option to ``add_view``
+ to add a view which will only be matched when handling an exception.
+ Previously any exception views were also registered for a traversal
+ context that inherited from the exception class which prevented any
+ exception-only optimizations.
+ See https://github.com/Pylons/pyramid/pull/2660
+
+- Added the ``exception_only`` boolean to
+ ``pyramid.interfaces.IViewDeriverInfo`` which can be used by view derivers
+ to determine if they are wrapping a view which only handles exceptions.
+ This means that it is no longer necessary to perform request-time checks
+ for ``request.exception`` to determine if the view is handling an exception
+ - the pipeline can be optimized at config-time.
+ See https://github.com/Pylons/pyramid/pull/2660
+
+- ``pserve`` should now work with ``gevent`` and other workers that need
+ to monkeypatch the process, assuming the server and / or the app do so
+ as soon as possible before importing the rest of pyramid.
+ See https://github.com/Pylons/pyramid/pull/2797
+
Bug Fixes
---------
+- Fixed bug in `proutes` such that it now shows the correct view when a class
+ and `attr` is involved.
+ See: https://github.com/Pylons/pyramid/pull/2687
+
+- Fix a ``FutureWarning`` in Python 3.5 when using ``re.split`` on the
+ ``format`` setting to the ``proutes`` script.
+ See https://github.com/Pylons/pyramid/pull/2714
+
+- Fix a ``RuntimeWarning`` emitted by WebOb when using arbitrary objects
+ as the ``userid`` in the ``AuthTktAuthenticationPolicy``. This is now caught
+ by the policy and the object is serialized as a base64 string to avoid
+ the cryptic warning. Since the userid will be read back as a string on
+ subsequent requests a more useful warning is emitted encouraging you to
+ use a primitive type instead.
+ See https://github.com/Pylons/pyramid/pull/2715
+
+- Pyramid 1.6 introduced the ability for an action to invoke another action.
+ There was a bug in the way that ``config.add_view`` would interact with
+ custom view derivers introduced in Pyramid 1.7 because the view's
+ discriminator cannot be computed until view derivers and view predicates
+ have been created in earlier orders. Invoking an action from another action
+ would trigger an unrolling of the pipeline and would compute discriminators
+ before they were ready. The new behavior respects the ``order`` of the action
+ and ensures the discriminators are not computed until dependent actions
+ from previous orders have executed.
+ See https://github.com/Pylons/pyramid/pull/2757
+
Deprecations
------------
Documentation Changes
---------------------
+- Add pyramid_nacl_session to session factories.
+ See https://github.com/Pylons/pyramid/issues/2791
+
+- Update HACKING.txt from stale branch that was never merged to master.
+ See https://github.com/Pylons/pyramid/pull/2782
+
+- Updated Windows installation instructions and related bits.
+ See https://github.com/Pylons/pyramid/issues/2661
+
+- Fix an inconsistency in the documentation between view predicates and
+ route predicates and highlight the differences in their APIs.
+ See https://github.com/Pylons/pyramid/pull/2764
+
+- Clarify a possible misuse of the ``headers`` kwarg to subclasses of
+ :class:`pyramid.httpexceptions.HTTPException` in which more appropriate
+ kwargs from the parent class :class:`pyramid.response.Response` should be
+ used instead. See https://github.com/Pylons/pyramid/pull/2750
diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt
index 9f1ca0eae..bb21337e2 100644
--- a/CONTRIBUTORS.txt
+++ b/CONTRIBUTORS.txt
@@ -275,4 +275,12 @@ Contributors
- Marco Martinez, 2016/06/02
-- Cris Ewing, 2016/06/03 \ No newline at end of file
+- Cris Ewing, 2016/06/03
+
+- Jean-Christophe Bohin, 2016/06/13
+
+- Dariusz Gorecki, 2016/07/15
+
+- Jon Davidson, 2016/07/18
+
+- Keith Yang, 2016/07/22
diff --git a/HACKING.txt b/HACKING.txt
index 5bbdce0c6..953c386f9 100644
--- a/HACKING.txt
+++ b/HACKING.txt
@@ -3,14 +3,16 @@ Hacking on Pyramid
Here are some guidelines for hacking on Pyramid.
+
Using a Development Checkout
----------------------------
You'll have to create a development environment to hack on Pyramid, using a
Pyramid checkout. You can either do this by hand, or if you have ``tox``
-installed (it's on PyPI), you can use tox to set up a working development
+installed (it's on PyPI), you can use ``tox`` to set up a working development
environment. Each installation method is described below.
+
By Hand
+++++++
@@ -26,180 +28,196 @@ By Hand
substituting your account username and specifying the destination as
"hack-on-pyramid".
- $ cd ~
- $ git clone git@github.com:USERNAME/pyramid.git hack-on-pyramid
- $ cd hack-on-pyramid
- # Configure remotes such that you can pull changes from the Pyramid
- # repository into your local repository.
- $ git remote add upstream https://github.com/Pylons/pyramid.git
- # fetch and merge changes from upstream into master
- $ git fetch upstream
- $ git merge upstream/master
+ $ cd ~
+ $ git clone git@github.com:USERNAME/pyramid.git hack-on-pyramid
+ $ cd hack-on-pyramid
+ # Configure remotes such that you can pull changes from the Pyramid
+ # repository into your local repository.
+ $ git remote add upstream https://github.com/Pylons/pyramid.git
+ # fetch and merge changes from upstream into master
+ $ git fetch upstream
+ $ git merge upstream/master
Now your local repo is set up such that you will push changes to your GitHub
repo, from which you can submit a pull request.
-- Create a virtualenv in which to install Pyramid:
-
- $ cd ~/hack-on-pyramid
- $ virtualenv -ppython2.7 env
+- Create a virtual environment in which to install Pyramid:
- Note that very old versions of virtualenv (virtualenv versions below, say,
- 1.10 or thereabouts) require you to pass a ``--no-site-packages`` flag to
- get a completely isolated environment.
-
- You can choose which Python version you want to use by passing a ``-p``
- flag to ``virtualenv``. For example, ``virtualenv -ppython2.7``
- chooses the Python 2.7 interpreter to be installed.
+ $ cd ~/hack-on-pyramid
+ $ python3 -m venv env
From here on in within these instructions, the ``~/hack-on-pyramid/env``
virtual environment you created above will be referred to as ``$VENV``.
To use the instructions in the steps that follow literally, use the
``export VENV=~/hack-on-pyramid/env`` command.
-- Install ``setuptools-git`` into the virtualenv (for good measure, as we're
- using git to do version control):
+- Install ``setuptools-git`` into the virtual environment (for good measure, as
+ we're using git to do version control):
- $ $VENV/bin/easy_install setuptools-git
+ $ $VENV/bin/pip install setuptools-git
-- Install Pyramid from the checkout into the virtualenv using ``setup.py
- dev``. ``setup.py dev`` is an alias for "setup.py develop" which also
- installs testing requirements such as nose and coverage. Running
- ``setup.py dev`` *must* be done while the current working directory is the
- ``pyramid`` checkout directory:
+- Install Pyramid from the checkout into the virtual environment, where the
+ current working directory is the ``pyramid`` checkout directory. We will
+ install Pyramid in editable (development) mode as well as its testing
+ requirements.
- $ cd ~/hack-on-pyramid
- $ $VENV/bin/python setup.py dev
+ $ cd ~/hack-on-pyramid
+ $ $VENV/bin/pip install -e ".[testing,docs]"
- Optionally create a new Pyramid project using ``pcreate``:
- $ cd $VENV
- $ bin/pcreate -s starter starter
+ $ cd $VENV
+ $ bin/pcreate -s starter starter
+
+- ...and install the new project into the virtual environment:
-- ...and install the new project (also using ``setup.py develop``) into the
- virtualenv:
+ $ cd $VENV/starter
+ $ $VENV/bin/pip install -e .
- $ cd $VENV/starter
- $ $VENV/bin/python setup.py develop
-Using Tox
-+++++++++
+Using ``Tox``
++++++++++++++
Alternatively, if you already have ``tox`` installed, there is an easier
way to get going.
- Create a new directory somewhere and ``cd`` to it:
- $ mkdir ~/hack-on-pyramid
- $ cd ~/hack-on-pyramid
+ $ mkdir ~/hack-on-pyramid
+ $ cd ~/hack-on-pyramid
- Check out a read-only copy of the Pyramid source:
- $ git clone git://github.com/Pylons/pyramid.git .
+ $ git clone git://github.com/Pylons/pyramid.git .
+
+ Alternatively, create a writeable fork on GitHub and clone it.
- (alternately, create a writeable fork on GitHub and check that out).
+Since Pyramid is a framework and not an application, it can be convenient to
+work against a sample application, preferably in its own virtual environment. A
+quick way to achieve this is to use `tox
+<http://tox.readthedocs.org/en/latest/>`_ with a custom configuration file
+that is part of the checkout:
-Since Pyramid is a framework and not an application, it can be
-convenient to work against a sample application, preferably in its own
-virtualenv. A quick way to achieve this is to (ab-)use ``tox``
-(http://tox.readthedocs.org/en/latest/) with a custom configuration
-file that's part of the checkout:
+ $ tox -c hacking-tox.ini
- tox -c hacking-tox.ini
+This will create a python-2.7 based virtual environment named ``env27``
+(Pyramid's ``.gitconfig` ignores all top-level folders that start with ``env``
+specifically in our use case), and inside that a simple pyramid application
+named ``hacking`` that you can then fire up like so:
-This will create a python-2.7 based virtualenv named ``env27`` (Pyramid's
-``.gitconfig` ignores all top-level folders that start with ``env`` specifically
-for this use case) and inside that a simple pyramid application named
-``hacking`` that you can then fire up like so:
+ $ cd env27/hacking
+ $ ../bin/pip install -e ".[testing,docs]"
+ $ ../bin/pserve development.ini
- cd env27/hacking
- ../bin/python setup.py develop
- ../bin/pserve development.ini
Adding Features
---------------
In order to add a feature to Pyramid:
-- The feature must be documented in both the API and narrative
- documentation (in ``docs/``).
+- The feature must be documented in both the API and narrative documentation
+ (in ``docs/``).
-- The feature must work fully on the following CPython versions: 2.6, 2.7, 3.2,
- 3.3, 3.4, and 3.5 on both UNIX and Windows.
+- The feature must work fully on the following CPython versions: 2.7, 3.4,
+ and 3.5 on both UNIX and Windows.
-- The feature must work on the latest version of PyPy and PyPy3.
+- The feature must work on the latest version of PyPy.
-- The feature must not cause installation or runtime failure on App Engine.
- If it doesn't cause installation or runtime failure, but doesn't actually
- *work* on these platforms, that caveat should be spelled out in the
- documentation.
+- The feature must not depend on any particular persistence layer (filesystem,
+ SQL, etc).
-- The feature must not depend on any particular persistence layer
- (filesystem, SQL, etc).
+- The feature must not add unnecessary dependencies (where "unnecessary" is of
+ course subjective, but new dependencies should be discussed).
-- The feature must not add unnecessary dependencies (where
- "unnecessary" is of course subjective, but new dependencies should
- be discussed).
+The above requirements are relaxed for scaffolding dependencies. If a scaffold
+has an install-time dependency on something that doesn't work on a particular
+platform, that caveat should be spelled out clearly in *its* documentation
+(within its ``docs/`` directory).
-The above requirements are relaxed for scaffolding dependencies. If a
-scaffold has an install-time dependency on something that doesn't work on a
-particular platform, that caveat should be spelled out clearly in *its*
-documentation (within its ``docs/`` directory).
Coding Style
------------
-- PEP8 compliance. Whitespace rules are relaxed: not necessary to put
- 2 newlines between classes. But 79-column lines, in particular, are
- mandatory. See
- http://docs.pylonsproject.org/en/latest/community/codestyle.html for more
+- PEP8 compliance. Whitespace rules are relaxed: not necessary to put two
+ newlines between classes. But 79-column lines, in particular, are mandatory.
+ See http://docs.pylonsproject.org/en/latest/community/codestyle.html for more
information.
- Please do not remove trailing whitespace. Configure your editor to reduce
diff noise. See https://github.com/Pylons/pyramid/issues/788 for more.
+
Running Tests
---------------
+-------------
+
+- To run all tests for Pyramid on a single Python version from your development
+ virtual environment (See *Using a Development Checkout* above), run
+ ``nosetests``:
+
+ $ $VENV/bin/nosetests
+
+- To run individual tests (i.e., during development), you can use ``nosetests``
+ syntax as follows:
-- To run all tests for Pyramid on a single Python version, run ``nosetests``
- from your development virtualenv (See *Using a Development Checkout* above).
+ # run a single test
+ $ $VENV/bin/nosetests pyramid.tests.test_module:ClassName.test_mytestname
-- To run individual tests (i.e. during development) you can use a regular
- expression with the ``-t`` parameter courtesy of the `nose-selecttests
- <https://pypi.python.org/pypi/nose-selecttests/>`_ plugin that's been
- installed (along with nose itself) via ``python setup.py dev``. The
- easiest usage is to simply provide the verbatim name of the test you're
- working on.
+ # run all tests in a class
+ $ $VENV/bin/nosetests pyramid.tests.test_module:ClassName
-- To run the full set of Pyramid tests on all platforms, install ``tox``
- (http://codespeak.net/~hpk/tox/) into a system Python. The ``tox`` console
- script will be installed into the scripts location for that Python. While
+ Optionally you can install a nose plugin, `nose-selecttests
+ <https://pypi.python.org/pypi/nose-selecttests/>`_, and use a regular
+ expression with the ``-t`` parameter to run tests.
+
+ # run a single test
+ $ $VENV/bin/nosetests -t test_mytestname
+
+- The ``tox.ini`` uses ``nose`` and ``coverage``. As such ``tox`` may be used
+ to run groups of tests or only a specific version of Python. For example, the
+ following command will run tests on Python 2.7 only without coverage:
+
+ $ tox -e py27
+
+ This command will run tests on the latest versions of Python 2 and 3 with
+ coverage totaled for both versions.
+
+ $ tox -e py2-cover,py3-cover,coverage
+
+- To run the full set of Pyramid tests on all platforms, install `tox
+ <http://codespeak.net/~hpk/tox/>`_ into a system Python. The ``tox`` console
+ script will be installed into the scripts location for that Python. While
``cd``'ed to the Pyramid checkout root directory (it contains ``tox.ini``),
- invoke the ``tox`` console script. This will read the ``tox.ini`` file and
- execute the tests on multiple Python versions and platforms; while it runs,
- it creates a virtualenv for each version/platform combination. For
- example::
+ invoke the ``tox`` console script. This will read the ``tox.ini`` file and
+ execute the tests on multiple Python versions and platforms. While it runs,
+ it creates a virtual environment for each version/platform combination. For
+ example:
- $ sudo /usr/bin/easy_install tox
- $ cd ~/hack-on-pyramid/
- $ /usr/bin/tox
+ $ sudo /usr/bin/pip install tox
+ $ cd ~/hack-on-pyramid/
+ $ /usr/bin/tox
-- The tests can also be run using ``pytest`` (http://pytest.org/). This is
- intended as a convenience for people who are more used or fond of ``pytest``.
- Run the tests like so::
+- The tests can also be run using `pytest <http://pytest.org/>`_. This is
+ intended as a convenience for people who are more used to or fond of
+ ``pytest``. Run the tests like so:
- $ $VENV/bin/easy_install pytest
- $ $VENV/bin/py.test --strict pyramid/
+ $ $VENV/bin/pip install pytest
+ $ $VENV/bin/py.test --strict pyramid/
+
+ To run individual tests (i.e., during development), see "py.test usage -
+ Specifying tests / selecting tests":
+ http://pytest.org/latest/usage.html#specifying-tests-selecting-tests
- Functional tests related to the "scaffolds" (starter, zodb, alchemy) which
- create a virtualenv, install the scaffold package and its dependencies, start
- a server, and hit a URL on the server can be run like so::
+ create a virtual environment, install the scaffold package and its
+ dependencies, start a server, and hit a URL on the server, can be run like
+ so:
+
+ $ ./scaffoldtests.sh
- $ ./scaffoldtests.sh
+ Alternatively:
- Alternately::
+ $ tox -e{py27,py34,py35,pypy}-scaffolds,
- $ tox -e{py26,py27,py32,py33,py34,py35,pypy,pypy3}-scaffolds,
Test Coverage
-------------
@@ -208,6 +226,7 @@ Test Coverage
can test coverage via ``./coverage.sh`` (which itself just executes ``tox
-epy2-cover,py3-cover,coverage``).
+
Documentation Coverage and Building HTML Documentation
------------------------------------------------------
@@ -217,13 +236,14 @@ changed to reflect the bug fix, ideally in the same commit that fixes the bug
or adds the feature. To build and review docs, use the following steps.
1. In the main Pyramid checkout directory, run ``./builddocs.sh`` (which just
- turns around and runs ``tox -e docs``)::
+ turns around and runs ``tox -e docs``):
- $ ./builddocs.sh
+ $ ./builddocs.sh
2. Open the ``docs/_build/html/index.html`` file to see the resulting HTML
rendering.
+
Change Log
----------
@@ -231,4 +251,3 @@ Change Log
file in the prevailing style. Changelog entries should be long and
descriptive, not cryptic. Other developers should be able to know
what your changelog entry means.
-
diff --git a/README.rst b/README.rst
index a9e38f636..6ef75e899 100644
--- a/README.rst
+++ b/README.rst
@@ -21,6 +21,23 @@ Pyramid
source Python web framework. It makes real-world web application development
and deployment more fun, more predictable, and more productive.
+.. code-block:: python
+
+ from wsgiref.simple_server import make_server
+ from pyramid.config import Configurator
+ from pyramid.response import Response
+
+ def hello_world(request):
+ return Response('Hello %(name)s!' % request.matchdict)
+
+ if __name__ == '__main__':
+ config = Configurator()
+ config.add_route('hello', '/hello/{name}')
+ config.add_view(hello_world, route_name='hello')
+ app = config.make_wsgi_app()
+ server = make_server('0.0.0.0', 8080, app)
+ server.serve_forever()
+
Pyramid is a project of the `Pylons Project <http://www.pylonsproject.org/>`_.
Support and Documentation
@@ -33,9 +50,10 @@ for documentation, reporting bugs, and getting support.
Developing and Contributing
---------------------------
-See ``HACKING.txt`` and ``contributing.md`` for guidelines for running tests,
-adding features, coding style, and updating documentation when developing in or
-contributing to Pyramid.
+See `HACKING.txt <https://github.com/Pylons/pyramid/blob/master/HACKING.txt>`_ and
+`contributing.md <https://github.com/Pylons/pyramid/blob/master/contributing.md>`_
+for guidelines on running tests, adding features, coding style, and updating
+documentation when developing in or contributing to Pyramid.
License
-------
diff --git a/RELEASING.txt b/RELEASING.txt
index d8572fa94..4690fbd37 100644
--- a/RELEASING.txt
+++ b/RELEASING.txt
@@ -33,8 +33,8 @@ Prepare new release branch
- Run tests on Windows if feasible.
-- Make sure all scaffold tests pass (Py 2.7, 3.3, 3.4, 3.5, and pypy on UNIX;
- this doesn't work on Windows):
+- Make sure all scaffold tests pass (CPython 2.7, 3.4, and 3.5, and PyPy on
+ UNIX; this doesn't work on Windows):
$ ./scaffoldtests.sh
@@ -148,12 +148,12 @@ Here are the changes:
<<changes>>
-A "What's New In Pyramid 1.X" document exists at
-http://docs.pylonsproject.org/projects/pyramid/1.X-branch/whatsnew-1.X.html .
+What's New In Pyramid 1.X:
+http://docs.pylonsproject.org/projects/pyramid/en/1.X-branch/whatsnew-1.X.html
-You will be able to see the 1.X release documentation (across all
-alphas and betas, as well as when it eventually gets to final release)
-at http://docs.pylonsproject.org/projects/pyramid/1.X-branch/ .
+1.X release documentation (across all alphas and betas, as well as when it gets
+to final release):
+http://docs.pylonsproject.org/projects/pyramid/en/1.X-branch/
You can install it via PyPI:
diff --git a/docs/api/authentication.rst b/docs/api/authentication.rst
index 19d08618b..57f32327a 100644
--- a/docs/api/authentication.rst
+++ b/docs/api/authentication.rst
@@ -34,5 +34,10 @@ Helper Classes
.. autoclass:: AuthTktCookieHelper
:members:
+ .. autoclass:: HTTPBasicCredentials
+ :members:
+Helper Functions
+~~~~~~~~~~~~~~~~
+ .. autofunction:: extract_http_basic_credentials
diff --git a/docs/conf.py b/docs/conf.py
index 518f7e784..c3a7170fc 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -78,7 +78,7 @@ intersphinx_mapping = {
'webtest': ('http://webtest.pythonpaste.org/en/latest', None),
'who': ('http://repozewho.readthedocs.org/en/latest', None),
'zcml': ('http://docs.pylonsproject.org/projects/pyramid-zcml/en/latest', None),
- 'zcomponent': ('http://docs.zope.org/zope.component', None),
+ 'zcomponent': ('http://zopecomponent.readthedocs.io/en/stable/', None),
}
diff --git a/docs/conventions.rst b/docs/conventions.rst
index 43853882c..de041da04 100644
--- a/docs/conventions.rst
+++ b/docs/conventions.rst
@@ -57,23 +57,16 @@ character, e.g.:
$ $VENV/bin/py.test -q
-(See :term:`venv` for the meaning of ``$VENV``)
+See :term:`venv` for the meaning of ``$VENV``.
-Example blocks representing Windows ``cmd.exe`` commands are prefixed with a
-drive letter and/or a directory name, e.g.:
+Example blocks representing Windows commands are prefixed with a drive letter
+with an optional directory name, e.g.:
.. code-block:: doscon
c:\examples> %VENV%\Scripts\py.test -q
-(See :term:`venv` for the meaning of ``%VENV%``)
-
-Sometimes, when it's unknown which directory is current, Windows ``cmd.exe``
-example block commands are prefixed only with a ``>`` character, e.g.:
-
- .. code-block:: doscon
-
- > %VENV%\Scripts\py.test -q
+See :term:`venv` for the meaning of ``%VENV%``.
When a command that should be typed on one line is too long to fit on a page,
the backslash ``\`` is used to indicate that the following printed line should
diff --git a/docs/narr/commandline.rst b/docs/narr/commandline.rst
index 6cd90d42f..242bc7ec7 100644
--- a/docs/narr/commandline.rst
+++ b/docs/narr/commandline.rst
@@ -649,6 +649,10 @@ using the :func:`pyramid.paster.bootstrap` command in the body of your script.
.. versionadded:: 1.1
:func:`pyramid.paster.bootstrap`
+.. versionchanged:: 1.8
+ Added the ability for ``bootstrap`` to cleanup automatically via the
+ ``with`` statement.
+
In the simplest case, :func:`pyramid.paster.bootstrap` can be used with a
single argument, which accepts the :term:`PasteDeploy` ``.ini`` file
representing your Pyramid application's configuration as a single argument:
@@ -656,8 +660,9 @@ representing your Pyramid application's configuration as a single argument:
.. code-block:: python
from pyramid.paster import bootstrap
- env = bootstrap('/path/to/my/development.ini')
- print(env['request'].route_url('home'))
+
+ with bootstrap('/path/to/my/development.ini') as env:
+ print(env['request'].route_url('home'))
:func:`pyramid.paster.bootstrap` returns a dictionary containing
framework-related information. This dictionary will always contain a
@@ -723,8 +728,9 @@ load instead of ``main``:
.. code-block:: python
from pyramid.paster import bootstrap
- env = bootstrap('/path/to/my/development.ini#another')
- print(env['request'].route_url('home'))
+
+ with bootstrap('/path/to/my/development.ini#another') as env:
+ print(env['request'].route_url('home'))
The above example specifies the ``another`` ``app``, ``pipeline``, or
``composite`` section of your PasteDeploy configuration file. The ``app``
@@ -761,9 +767,9 @@ desired request and passing it into :func:`~pyramid.paster.bootstrap`:
from pyramid.request import Request
request = Request.blank('/', base_url='https://example.com/prefix')
- env = bootstrap('/path/to/my/development.ini#another', request=request)
- print(env['request'].application_url)
- # will print 'https://example.com/prefix'
+ with bootstrap('/path/to/my/development.ini#another', request=request) as env:
+ print(env['request'].application_url)
+ # will print 'https://example.com/prefix'
Now you can readily use Pyramid's APIs for generating URLs:
@@ -776,7 +782,9 @@ Now you can readily use Pyramid's APIs for generating URLs:
Cleanup
~~~~~~~
-When your scripting logic finishes, it's good manners to call the ``closer``
+If you're using the ``with``-statement variant then there's nothing to
+worry about. However if you're using the returned environment directly then
+when your scripting logic finishes, it's good manners to call the ``closer``
callback:
.. code-block:: python
@@ -891,15 +899,12 @@ contains the following code:
omit = options.omit
if omit is None:
omit = []
- env = bootstrap(config_uri)
- settings, closer = env['registry'].settings, env['closer']
- try:
+ with bootstrap(config_uri) as env:
+ settings = env['registry'].settings
for k, v in settings.items():
if any([k.startswith(x) for x in omit]):
continue
print('%-40s %-20s' % (k, v))
- finally:
- closer()
This script uses the Python ``optparse`` module to allow us to make sense out
of extra arguments passed to the script. It uses the
diff --git a/docs/narr/firstapp.rst b/docs/narr/firstapp.rst
index a8491eabd..ad05976c0 100644
--- a/docs/narr/firstapp.rst
+++ b/docs/narr/firstapp.rst
@@ -27,15 +27,15 @@ installed, an HTTP server is started on TCP port 8080.
On UNIX:
-.. code-block:: text
+.. code-block:: bash
$ $VENV/bin/python helloworld.py
On Windows:
-.. code-block:: text
+.. code-block:: doscon
- C:\> %VENV%\Scripts\python.exe helloworld.py
+ c:\> %VENV%\Scripts\python helloworld.py
This command will not return and nothing will be printed to the console. When
port 8080 is visited by a browser on the URL ``/hello/world``, the server will
diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst
index 49ef29d3f..b22b31bf9 100644
--- a/docs/narr/hooks.rst
+++ b/docs/narr/hooks.rst
@@ -26,7 +26,7 @@ Not Found View by using the
:linenos:
def notfound(request):
- return Response('Not Found, dude', status='404 Not Found')
+ return Response('Not Found', status='404 Not Found')
def main(globals, **settings):
config = Configurator()
@@ -45,7 +45,7 @@ and a :term:`scan`, you can replace the Not Found View by using the
@notfound_view_config()
def notfound(request):
- return Response('Not Found, dude', status='404 Not Found')
+ return Response('Not Found', status='404 Not Found')
def main(globals, **settings):
config = Configurator()
@@ -67,11 +67,11 @@ Views can carry predicates limiting their applicability. For example:
@notfound_view_config(request_method='GET')
def notfound_get(request):
- return Response('Not Found during GET, dude', status='404 Not Found')
+ return Response('Not Found during GET', status='404 Not Found')
@notfound_view_config(request_method='POST')
def notfound_post(request):
- return Response('Not Found during POST, dude', status='404 Not Found')
+ return Response('Not Found during POST', status='404 Not Found')
def main(globals, **settings):
config = Configurator()
@@ -1481,7 +1481,7 @@ method. For example:
phash = text
def __call__(self, context, request):
- return getattr(context, 'content_type', None) == self.val
+ return request.content_type == self.val
The constructor of a predicate factory takes two arguments: ``val`` and
``config``. The ``val`` argument will be the argument passed to
@@ -1500,13 +1500,28 @@ with the name and the value serialized. The result of ``phash`` is not seen in
output anywhere, it just informs the uniqueness constraints for view
configuration.
-The ``__call__`` method of a predicate factory must accept a resource
-(``context``) and a request, and must return ``True`` or ``False``. It is the
-"meat" of the predicate.
+The ``__call__`` method differs depending on whether the predicate is used as
+a :term:`view predicate` or a :term:`route predicate`:
-You can use the same predicate factory as both a view predicate and as a route
-predicate, but you'll need to call ``add_view_predicate`` and
-``add_route_predicate`` separately with the same factory.
+- When used as a route predicate, the ``__call__`` signature is
+ ``(info, request)``. The ``info`` object is a dictionary containing two
+ keys: ``match`` and ``route``. ``info['match']`` is the matchdict containing
+ the patterns matched in the route pattern. ``info['route']`` is the
+ :class:`pyramid.interfaces.IRoute` object for the current route.
+
+- When used as a view predicate, the ``__call__`` signature is
+ ``(context, request)``. The ``context`` is the result of :term:`traversal`
+ performed using either the route's :term:`root factory` or the app's
+ :term:`default root factory`.
+
+In both cases the ``__call__`` method is expected to return ``True`` or
+``False``.
+
+It is possible to use the same predicate factory as both a view predicate and
+as a route predicate, but they'll need to handle the ``info`` or ``context``
+argument specially (many predicates do not need this argument) and you'll need
+to call ``add_view_predicate`` and ``add_route_predicate`` separately with
+the same factory.
.. _subscriber_predicates:
@@ -1639,7 +1654,8 @@ the user-defined :term:`view callable`:
Enforce the ``permission`` defined on the view. This element is a no-op if no
permission is defined. Note there will always be a permission defined if a
default permission was assigned via
- :meth:`pyramid.config.Configurator.set_default_permission`.
+ :meth:`pyramid.config.Configurator.set_default_permission` unless the
+ view is an :term:`exception view`.
This element will also output useful debugging information when
``pyramid.debug_authorization`` is enabled.
@@ -1649,7 +1665,8 @@ the user-defined :term:`view callable`:
Used to check the CSRF token provided in the request. This element is a
no-op if ``require_csrf`` view option is not ``True``. Note there will
always be a ``require_csrf`` option if a default value was assigned via
- :meth:`pyramid.config.Configurator.set_default_csrf_options`.
+ :meth:`pyramid.config.Configurator.set_default_csrf_options` unless
+ the view is an :term:`exception view`.
``owrapped_view``
@@ -1695,6 +1712,8 @@ around monitoring and security. In order to register a custom :term:`view
deriver`, you should create a callable that conforms to the
:class:`pyramid.interfaces.IViewDeriver` interface, and then register it with
your application using :meth:`pyramid.config.Configurator.add_view_deriver`.
+The callable should accept the ``view`` to be wrapped and the ``info`` object
+which is an instance of :class:`pyramid.interfaces.IViewDeriverInfo`.
For example, below is a callable that can provide timing information for the
view pipeline:
@@ -1745,6 +1764,21 @@ View derivers are unique in that they have access to most of the options
passed to :meth:`pyramid.config.Configurator.add_view` in order to decide what
to do, and they have a chance to affect every view in the application.
+.. _exception_view_derivers:
+
+Exception Views and View Derivers
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+A :term:`view deriver` has the opportunity to wrap any view, including
+an :term:`exception view`. In general this is fine, but certain view derivers
+may wish to avoid doing certain things when handling exceptions. For example,
+the ``csrf_view`` and ``secured_view`` built-in view derivers will not perform
+security checks on exception views unless explicitly told to do so.
+
+You can check for ``info.exception_only`` on the
+:class:`pyramid.interfaces.IViewDeriverInfo` object when wrapping the view
+to determine whether you are wrapping an exception view or a normal view.
+
Ordering View Derivers
~~~~~~~~~~~~~~~~~~~~~~
diff --git a/docs/narr/i18n.rst b/docs/narr/i18n.rst
index 131832aae..3549b53a5 100644
--- a/docs/narr/i18n.rst
+++ b/docs/narr/i18n.rst
@@ -294,7 +294,7 @@ Lingua like so:
.. code-block:: doscon
- C> %VENV%\Scripts\pip install lingua
+ c:\> %VENV%\Scripts\pip install lingua
.. index::
diff --git a/docs/narr/install.rst b/docs/narr/install.rst
index 7d96f4074..570cb2285 100644
--- a/docs/narr/install.rst
+++ b/docs/narr/install.rst
@@ -21,9 +21,8 @@ the following sections.
.. sidebar:: Python Versions
- As of this writing, :app:`Pyramid` has been tested under Python 2.7,
- Python 3.3, Python 3.4, Python 3.5, PyPy, and PyPy3. :app:`Pyramid` does
- not run under any version of Python before 2.7.
+ As of this writing, :app:`Pyramid` is tested against Python 2.7,
+ Python 3.4, Python 3.5, PyPy.
:app:`Pyramid` is known to run on all popular UNIX-like systems such as Linux,
Mac OS X, and FreeBSD, as well as on Windows platforms. It is also known to
@@ -92,8 +91,24 @@ If your Windows system doesn't have a Python interpreter, you'll need to
install it by downloading a Python 3.x-series interpreter executable from
`python.org's download section <https://www.python.org/downloads/>`_ (the files
labeled "Windows Installer"). Once you've downloaded it, double click on the
-executable and accept the defaults during the installation process. You may
-also need to download and install the Python for Windows extensions.
+executable, and select appropriate options during the installation process. To
+standardize this documentation, we used the GUI installer and selected the
+following options:
+
+- Screen 1: Install Python 3.x.x (32- or 64-bit)
+ - Check "Install launcher for all users (recommended)"
+ - Check "Add Python 3.x to PATH"
+ - Click "Customize installation"
+- Screen 2: Optional Features
+ - Check all options
+ - Click "Next"
+- Screen 3: Advanced Options
+ - Check all options
+ - Customize install location: "C:\\Python3x", where "x" is the minor
+ version of Python
+ - Click "Next"
+
+You might also need to download and install the Python for Windows extensions.
.. seealso:: See the official Python documentation :ref:`Using Python on
Windows <python:using-on-windows>` for full details.
@@ -104,14 +119,19 @@ also need to download and install the Python for Windows extensions.
directions. Make sure you get the proper 32- or 64-bit build and Python
version.
+.. seealso:: `Python launcher for Windows
+ <https://docs.python.org/3/using/windows.html#launcher>`_ provides a command
+ ``py`` that allows users to run any installed version of Python.
+
.. warning::
- After you install Python on Windows, you may need to add the ``C:\Python3x``
- directory to your environment's ``Path``, where ``x`` is the minor version
- of installed Python, in order to make it possible to invoke Python from a
- command prompt by typing ``python``. To do so, right click ``My Computer``,
- select ``Properties`` --> ``Advanced Tab`` --> ``Environment Variables`` and
- add that directory to the end of the ``Path`` environment variable.
+ After you install Python on Windows, you might need to add the
+ ``c:\Python3x`` directory to your environment's ``Path``, where ``x`` is the
+ minor version of installed Python, in order to make it possible to invoke
+ Python from a command prompt by typing ``python``. To do so, right click
+ ``My Computer``, select ``Properties`` --> ``Advanced Tab`` -->
+ ``Environment Variables``, and add that directory to the end of the ``Path``
+ environment variable.
.. seealso:: See `Configuring Python (on Windows)
<https://docs.python.org/3/using/windows.html#configuring-python>`_ for
@@ -171,6 +191,29 @@ After installing Python as described previously in :ref:`for-mac-os-x-users` or
$ $VENV/bin/pip install "pyramid==\ |release|\ "
+.. index::
+ single: $VENV/bin/pip vs. source bin/activate
+
+.. _venv-bin-pip-vs-source-bin-activate:
+
+.. note:: Why use ``$VENV/bin/pip`` instead of ``source bin/activate``, then
+ ``pip``?
+
+ ``$VENV/bin/pip`` clearly specifies that ``pip`` is run from within the
+ virtual environment and not at the system level.
+
+ ``activate`` drops turds into the user's shell environment, leaving them
+ vulnerable to executing commands in the wrong context. ``deactivate`` might
+ not correctly restore previous shell environment variables.
+
+ Although using ``source bin/activate``, then ``pip``, requires fewer key
+ strokes to issue commands once invoked, there are other things to consider.
+ Michael F. Lamb (datagrok) presents a summary in `Virtualenv's bin/activate
+ is Doing It Wrong <https://gist.github.com/datagrok/2199506>`_.
+
+ Ultimately we prefer to keep things clear and simple, so we use
+ ``$VENV/bin/pip``.
+
.. index::
single: installing on Windows
@@ -190,7 +233,8 @@ After installing Python as described previously in
c:\> set VENV=c:\env
# replace "x" with your minor version of Python 3
- c:\> c:\Python3x\Scripts\python3 -m venv %VENV%
+ c:\> c:\Python3x\python -m venv %VENV%
+ c:\> cd %VENV%
You can either follow the use of the environment variable ``%VENV%``, or
replace it with the root directory of the virtual environment. If you choose
@@ -204,7 +248,10 @@ After installing Python as described previously in
.. parsed-literal::
- c:\\env> %VENV%\\Scripts\\pip install "pyramid==\ |release|\ "
+ c:\\> %VENV%\\Scripts\\pip install "pyramid==\ |release|\ "
+
+.. note:: See the note above for :ref:`Why use $VENV/bin/pip instead of source
+ bin/activate, then pip <venv-bin-pip-vs-source-bin-activate>`.
What Gets Installed
diff --git a/docs/narr/introduction.rst b/docs/narr/introduction.rst
index 112754b6a..40d9c14a8 100644
--- a/docs/narr/introduction.rst
+++ b/docs/narr/introduction.rst
@@ -821,7 +821,7 @@ Every release of Pyramid has 100% statement coverage via unit and integration
tests, as measured by the ``coverage`` tool available on PyPI. It also has
greater than 95% decision/condition coverage as measured by the
``instrumental`` tool available on PyPI. It is automatically tested by Travis,
-and Jenkins on Python 2.7, Python 3.3, Python 3.4, Python 3.5, PyPy, and PyPy3
+and Jenkins on Python 2.7, Python 3.4, Python 3.5, and PyPy
after each commit to its GitHub repository. Official Pyramid add-ons are held
to a similar testing standard. We still find bugs in Pyramid and its official
add-ons, but we've noticed we find a lot more of them while working on other
diff --git a/docs/narr/project.rst b/docs/narr/project.rst
index 1ce12a938..71bd176f6 100644
--- a/docs/narr/project.rst
+++ b/docs/narr/project.rst
@@ -87,9 +87,9 @@ On UNIX:
Or on Windows:
-.. code-block:: text
+.. code-block:: doscon
- > %VENV%\Scripts\pcreate -s starter MyProject
+ c:\> %VENV%\Scripts\pcreate -s starter MyProject
As a result of invoking the ``pcreate`` command, a directory named
``MyProject`` is created. That directory is a :term:`project` directory. The
@@ -161,8 +161,8 @@ Or on Windows:
.. code-block:: doscon
- > cd MyProject
- > %VENV%\Scripts\pip install -e .
+ c:\> cd MyProject
+ c:\> %VENV%\Scripts\pip install -e .
Elided output from a run of this command on UNIX is shown below:
@@ -199,7 +199,7 @@ On Windows:
.. code-block:: doscon
- > %VENV%\Scripts\pip install -e ".[testing]"
+ c:\> %VENV%\Scripts\pip install -e ".[testing]"
Once the testing requirements are installed, then you can run the tests using
the ``py.test`` command that was just installed in the ``bin`` directory of
@@ -215,7 +215,7 @@ On Windows:
.. code-block:: doscon
- > %VENV%\Scripts\py.test -q
+ c:\> %VENV%\Scripts\py.test -q
Here's sample output from a test run on UNIX:
@@ -282,7 +282,7 @@ On Windows:
.. code-block:: text
- > %VENV%\Scripts\pserve development.ini
+ c:\> %VENV%\Scripts\pserve development.ini
Here's sample output from a run of ``pserve`` on UNIX:
diff --git a/docs/narr/sessions.rst b/docs/narr/sessions.rst
index a1319e45f..5b24201a9 100644
--- a/docs/narr/sessions.rst
+++ b/docs/narr/sessions.rst
@@ -157,6 +157,12 @@ The following session factories exist at the time of this writing.
======================= ======= =============================
Session Factory Backend Description
======================= ======= =============================
+pyramid_nacl_session_ PyNaCl_ Defines an encrypting,
+ pickle-based cookie
+ serializer, using PyNaCl to
+ generate the symmetric
+ encryption for the cookie
+ state.
pyramid_redis_sessions_ Redis_ Server-side session library
for Pyramid, using Redis for
storage.
@@ -165,6 +171,9 @@ pyramid_beaker_ Beaker_ Session factory for Pyramid
sessioning system.
======================= ======= =============================
+.. _pyramid_nacl_session: https://pypi.python.org/pypi/pyramid_nacl_session
+.. _PyNaCl: https://pynacl.readthedocs.io/en/latest/secret/
+
.. _pyramid_redis_sessions: https://pypi.python.org/pypi/pyramid_redis_sessions
.. _Redis: http://redis.io/
diff --git a/docs/narr/upgrading.rst b/docs/narr/upgrading.rst
index 21b696775..4e434c3c6 100644
--- a/docs/narr/upgrading.rst
+++ b/docs/narr/upgrading.rst
@@ -205,10 +205,10 @@ On UNIX, you can do that via:
On Windows, you need to issue two commands:
-.. code-block:: bash
+.. code-block:: doscon
- C:\> set PYTHONWARNINGS=default
- C:\> Scripts/pserve.exe development.ini
+ c:\> set PYTHONWARNINGS=default
+ c:\> Scripts/pserve.exe development.ini
At this point, it's ensured that deprecation warnings will be printed to the
console whenever a codepath is hit that generates one. You can then click
diff --git a/docs/narr/urldispatch.rst b/docs/narr/urldispatch.rst
index 2472ace31..9ac01e24a 100644
--- a/docs/narr/urldispatch.rst
+++ b/docs/narr/urldispatch.rst
@@ -557,7 +557,7 @@ Here is an example of a corresponding ``mypackage.views`` module:
@view_config(route_name='idea')
def idea_view(request):
- return Response(request.matchdict['id'])
+ return Response(request.matchdict['idea'])
@view_config(route_name='user')
def user_view(request):
@@ -850,7 +850,7 @@ application:
from pyramid.httpexceptions import HTTPNotFound
def notfound(request):
- return HTTPNotFound('Not found, bro.')
+ return HTTPNotFound()
def no_slash(request):
return Response('No slash')
@@ -871,7 +871,7 @@ If a request enters the application with the ``PATH_INFO`` value of
However, if a request enters the application with the ``PATH_INFO`` value of
``/no_slash/``, *no* route will match, and the slash-appending not found view
will not find a matching route with an appended slash. As a result, the
-``notfound`` view will be called and it will return a "Not found, bro." body.
+``notfound`` view will be called and it will return a "Not found" body.
If a request enters the application with the ``PATH_INFO`` value of
``/has_slash/``, the second route will match. If a request enters the
@@ -892,7 +892,7 @@ exactly the same job:
@notfound_view_config(append_slash=True)
def notfound(request):
- return HTTPNotFound('Not found, bro.')
+ return HTTPNotFound()
@view_config(route_name='noslash')
def no_slash(request):
diff --git a/docs/narr/viewconfig.rst b/docs/narr/viewconfig.rst
index cd5b8feb0..7cb8e0306 100644
--- a/docs/narr/viewconfig.rst
+++ b/docs/narr/viewconfig.rst
@@ -34,7 +34,7 @@ determine the set of circumstances which must be true for the view callable to
be invoked.
A view configuration statement is made about information present in the
-:term:`context` resource and the :term:`request`.
+:term:`context` resource (or exception) and the :term:`request`.
View configuration is performed in one of two ways:
@@ -306,9 +306,26 @@ configured view.
represented class or if the :term:`context` resource provides the represented
interface; it is otherwise false.
+ It is possible to pass an exception class as the context if your context may
+ subclass an exception. In this case *two* views will be registered. One
+ will match normal incoming requests, and the other will match as an
+ :term:`exception view` which only occurs when an exception is raised during
+ the normal request processing pipeline.
+
If ``context`` is not supplied, the value ``None``, which matches any
resource, is used.
+``exception_only``
+
+ When this value is ``True``, the ``context`` argument must be a subclass of
+ ``Exception``. This flag indicates that only an :term:`exception view` should
+ be created, and that this view should not match if the traversal
+ :term:`context` matches the ``context`` argument. If the ``context`` is a
+ subclass of ``Exception`` and this value is ``False`` (the default), then a
+ view will be registered to match the traversal :term:`context` as well.
+
+ .. versionadded:: 1.8
+
``route_name``
If ``route_name`` is supplied, the view callable will be invoked only when
the named route has matched.
diff --git a/docs/narr/views.rst b/docs/narr/views.rst
index 770d27919..ab139ea19 100644
--- a/docs/narr/views.rst
+++ b/docs/narr/views.rst
@@ -262,10 +262,16 @@ specialized views as described in :ref:`special_exceptions_in_callables` can
also be used by application developers to convert arbitrary exceptions to
responses.
-To register a view that should be called whenever a particular exception is
-raised from within :app:`Pyramid` view code, use the exception class (or one of
-its superclasses) as the :term:`context` of a view configuration which points
-at a view callable for which you'd like to generate a response.
+To register an :term:`exception view` that should be called whenever a
+particular exception is raised from within :app:`Pyramid` view code, use
+:meth:`pyramid.config.Configurator.add_exception_view` to register a view
+configuration which matches the exception (or a subclass of the exception) and
+points at a view callable for which you'd like to generate a response. The
+exception will be passed as the ``context`` argument to any
+:term:`view predicate` registered with the view, as well as to the view itself.
+For convenience a new decorator exists,
+:class:`pyramid.views.exception_view_config`, which may be used to easily
+register exception views.
For example, given the following exception class in a module named
``helloworld.exceptions``:
@@ -277,17 +283,16 @@ For example, given the following exception class in a module named
def __init__(self, msg):
self.msg = msg
-
You can wire a view callable to be called whenever any of your *other* code
raises a ``helloworld.exceptions.ValidationFailure`` exception:
.. code-block:: python
:linenos:
- from pyramid.view import view_config
+ from pyramid.view import exception_view_config
from helloworld.exceptions import ValidationFailure
- @view_config(context=ValidationFailure)
+ @exception_view_config(ValidationFailure)
def failed_validation(exc, request):
response = Response('Failed validation: %s' % exc.msg)
response.status_int = 500
@@ -308,7 +313,7 @@ view registration:
from pyramid.view import view_config
from helloworld.exceptions import ValidationFailure
- @view_config(context=ValidationFailure, route_name='home')
+ @exception_view_config(ValidationFailure, route_name='home')
def failed_validation(exc, request):
response = Response('Failed validation: %s' % exc.msg)
response.status_int = 500
@@ -327,14 +332,21 @@ which have a name will be ignored.
.. note::
- Normal (i.e., non-exception) views registered against a context resource type
- which inherits from :exc:`Exception` will work normally. When an exception
- view configuration is processed, *two* views are registered. One as a
- "normal" view, the other as an "exception" view. This means that you can use
- an exception as ``context`` for a normal view.
+ In most cases, you should register an :term:`exception view` by using
+ :meth:`pyramid.config.Configurator.add_exception_view`. However, it is
+ possible to register "normal" (i.e., non-exception) views against a context
+ resource type which inherits from :exc:`Exception` (i.e.,
+ ``config.add_view(context=Exception)``). When the view configuration is
+ processed, *two* views are registered. One as a "normal" view, the other
+ as an :term:`exception view`. This means that you can use an exception as
+ ``context`` for a normal view.
+
+ The view derivers that wrap these two views may behave differently.
+ See :ref:`exception_view_derivers` for more information about this.
Exception views can be configured with any view registration mechanism:
-``@view_config`` decorator or imperative ``add_view`` styles.
+``@exception_view_config`` decorator or imperative ``add_exception_view``
+styles.
.. note::
diff --git a/docs/quick_tour.rst b/docs/quick_tour.rst
index b170e5d98..39b4cafb3 100644
--- a/docs/quick_tour.rst
+++ b/docs/quick_tour.rst
@@ -1,3 +1,4 @@
+
.. _quick_tour:
=====================
@@ -43,22 +44,24 @@ For Windows:
.. parsed-literal::
# set an environment variable to where you want your virtual environment
- c:\> set VENV=c:\env
+ c:\\> set VENV=c:\\env
# create the virtual environment
- c:\\> c:\\Python35\\python3 -m venv %VENV%
+ c:\\> %VENV%\\Scripts\\python -m venv %VENV%
# install pyramid
c:\\> %VENV%\\Scripts\\pip install pyramid
# or for a specific released version
c:\\> %VENV%\\Scripts\\pip install "pyramid==\ |release|\ "
-Of course Pyramid runs fine on Python 2.6+, as do the examples in this *Quick
+Of course Pyramid runs fine on Python 2.7+, as do the examples in this *Quick
Tour*. We're showing Python 3 for simplicity. (Pyramid had production support
for Python 3 in October 2011.) Also for simplicity, the remaining examples will
show only UNIX commands.
.. seealso:: See also:
:ref:`Quick Tutorial section on Requirements <qtut_requirements>`,
- :ref:`installing_unix`, :ref:`Before You Install <installing_chapter>`, and
+ :ref:`installing_unix`, :ref:`Before You Install <installing_chapter>`,
+ :ref:`Why use $VENV/bin/pip instead of source bin/activate, then pip
+ <venv-bin-pip-vs-source-bin-activate>`, and
:ref:`Installing Pyramid on a Windows System <installing_windows>`.
@@ -70,6 +73,7 @@ step. Here's a tiny application in Pyramid:
.. literalinclude:: quick_tour/hello_world/app.py
:linenos:
+ :language: python
This simple example is easy to run. Save this as ``app.py`` and run it:
@@ -120,6 +124,7 @@ library for request and response handling. In our example above, Pyramid hands
Let's see some features of requests and responses in action:
.. literalinclude:: quick_tour/requests/app.py
+ :language: python
:pyobject: hello_world
In this Pyramid view, we get the URL being visited from ``request.url``. Also
@@ -159,6 +164,7 @@ Let's move the views out to their own ``views.py`` module and change the
First our revised ``app.py``:
.. literalinclude:: quick_tour/views/app.py
+ :language: python
:linenos:
We added some more routes, but we also removed the view code. Our views and
@@ -169,6 +175,7 @@ We now have a ``views.py`` module that is focused on handling requests and
responses:
.. literalinclude:: quick_tour/views/views.py
+ :language: python
:linenos:
We have four views, each leading to the other. If you start at
@@ -214,6 +221,7 @@ What if we want part of the URL to be available as data in my view? We can use
this route declaration, for example:
.. literalinclude:: quick_tour/routing/app.py
+ :language: python
:linenos:
:lines: 6
:lineno-start: 6
@@ -222,6 +230,7 @@ With this, URLs such as ``/howdy/amy/smith`` will assign ``amy`` to ``first``
and ``smith`` to ``last``. We can then use this data in our view:
.. literalinclude:: quick_tour/routing/views.py
+ :language: python
:linenos:
:lines: 5-8
:lineno-start: 5
@@ -260,6 +269,7 @@ With the package installed, we can include the template bindings into our
configuration in ``app.py``:
.. literalinclude:: quick_tour/templating/app.py
+ :language: python
:linenos:
:lines: 6-8
:lineno-start: 6
@@ -268,6 +278,7 @@ configuration in ``app.py``:
Now lets change our ``views.py`` file:
.. literalinclude:: quick_tour/templating/views.py
+ :language: python
:linenos:
:emphasize-lines: 4,6
@@ -304,6 +315,7 @@ With the package installed, we can include the template bindings into our
configuration:
.. literalinclude:: quick_tour/jinja2/app.py
+ :language: python
:linenos:
:lines: 6-8
:lineno-start: 6
@@ -312,6 +324,7 @@ configuration:
The only change in our view is to point the renderer at the ``.jinja2`` file:
.. literalinclude:: quick_tour/jinja2/views.py
+ :language: python
:linenos:
:lines: 4-6
:lineno-start: 4
@@ -339,9 +352,10 @@ Static assets
Of course the Web is more than just markup. You need static assets: CSS, JS,
and images. Let's point our web app at a directory from which Pyramid will
serve some static assets. First let's make another call to the
-:term:`configurator`:
+:term:`configurator` in ``app.py``:
.. literalinclude:: quick_tour/static_assets/app.py
+ :language: python
:linenos:
:lines: 6-8
:lineno-start: 6
@@ -359,7 +373,7 @@ Next make a directory named ``static``, and place ``app.css`` inside:
All we need to do now is point to it in the ``<head>`` of our Jinja2 template,
``hello_world.jinja2``:
-.. literalinclude:: quick_tour/static_assets/hello_world.jinja2
+.. literalinclude:: quick_tour/static_assets/hello_world_static.jinja2
:language: jinja
:linenos:
:lines: 4-6
@@ -371,16 +385,16 @@ the site is later moved under ``/somesite/static/``? Or perhaps a web developer
changes the arrangement on disk? Pyramid provides a helper to allow flexibility
on URL generation:
-.. literalinclude:: quick_tour/static_assets/hello_world_static.jinja2
+.. literalinclude:: quick_tour/static_assets/hello_world.jinja2
:language: jinja
:linenos:
:lines: 4-6
:lineno-start: 4
:emphasize-lines: 2
-By using ``request.static_url`` to generate the full URL to the static
-assets, you both ensure you stay in sync with the configuration and
-gain refactoring flexibility later.
+By using ``request.static_url`` to generate the full URL to the static assets,
+you ensure that you stay in sync with the configuration and gain refactoring
+flexibility later.
.. seealso:: See also:
:ref:`Quick Tutorial Static Assets <qtut_static_assets>`,
@@ -396,6 +410,7 @@ to update the UI in the browser by requesting server data as JSON. Pyramid
supports this with a JSON renderer:
.. literalinclude:: quick_tour/json/views.py
+ :language: python
:linenos:
:lines: 9-
:lineno-start: 9
@@ -408,6 +423,7 @@ We also need to add a route to ``app.py`` so that our app will know how to
respond to a request for ``hello.json``.
.. literalinclude:: quick_tour/json/app.py
+ :language: python
:linenos:
:lines: 6-8
:lineno-start: 6
@@ -437,6 +453,7 @@ The following shows a "Hello World" example with three operations: view a form,
save a change, or press the delete button in our ``views.py``:
.. literalinclude:: quick_tour/view_classes/views.py
+ :language: python
:linenos:
:lines: 7-
:lineno-start: 7
@@ -489,10 +506,10 @@ Pyramid's ``pcreate`` command can list the available scaffolds:
$ pcreate --list
Available scaffolds:
- alchemy: Pyramid SQLAlchemy project using url dispatch
+ alchemy: Pyramid project using SQLAlchemy, SQLite, URL dispatch, and Jinja2
pyramid_jinja2_starter: Pyramid Jinja2 starter project
- starter: Pyramid starter project
- zodb: Pyramid ZODB project using traversal
+ starter: Pyramid starter project using URL dispatch and Chameleon
+ zodb: Pyramid project using ZODB, traversal, and Chameleon
The ``pyramid_jinja2`` add-on gave us a scaffold that we can use. From the
parent directory of where we want our Python package to be generated, let's use
@@ -728,6 +745,7 @@ Our unit test passed, although its coverage is incomplete. What did our test
look like?
.. literalinclude:: quick_tour/package/hello_world/tests.py
+ :language: python
:linenos:
Pyramid supplies helpers for test writing, which we use in the test setup and
@@ -881,6 +899,7 @@ SQLAlchemy uses "models" for this mapping. The scaffold generated a sample
model:
.. literalinclude:: quick_tour/sqla_demo/sqla_demo/models/mymodel.py
+ :language: python
:start-after: Start Sphinx Include
:end-before: End Sphinx Include
@@ -888,6 +907,7 @@ View code, which mediates the logic between web requests and the rest of the
system, can then easily get at the data thanks to SQLAlchemy:
.. literalinclude:: quick_tour/sqla_demo/sqla_demo/views/default.py
+ :language: python
:start-after: Start Sphinx Include
:end-before: End Sphinx Include
diff --git a/docs/quick_tour/json/hello_world.jinja2 b/docs/quick_tour/json/hello_world.jinja2
index 4fb9be074..a55865231 100644
--- a/docs/quick_tour/json/hello_world.jinja2
+++ b/docs/quick_tour/json/hello_world.jinja2
@@ -2,7 +2,7 @@
<html lang="en">
<head>
<title>Hello World</title>
- <link rel="stylesheet" href="{{ request.static_url('static/app.css') }}"/>
+ <link rel="stylesheet" href="{{ request.static_url('__main__:static/app.css') }}"/>
</head>
<body>
<h1>Hello {{ name }}!</h1>
diff --git a/docs/quick_tour/sqla_demo/sqla_demo/models/__init__.py b/docs/quick_tour/sqla_demo/sqla_demo/models/__init__.py
index 6ffc10a78..76e0fd26b 100644
--- a/docs/quick_tour/sqla_demo/sqla_demo/models/__init__.py
+++ b/docs/quick_tour/sqla_demo/sqla_demo/models/__init__.py
@@ -1,7 +1,7 @@
from sqlalchemy.orm import configure_mappers
# import all models classes here for sqlalchemy mappers
# to pick up
-from .mymodel import MyModel # flake8: noqa
+from .mymodel import MyModel # noqa
# run configure mappers to ensure we avoid any race conditions
configure_mappers()
diff --git a/docs/quick_tour/sqla_demo/sqla_demo/models/meta.py b/docs/quick_tour/sqla_demo/sqla_demo/models/meta.py
index 80ececd8c..03c50ae93 100644
--- a/docs/quick_tour/sqla_demo/sqla_demo/models/meta.py
+++ b/docs/quick_tour/sqla_demo/sqla_demo/models/meta.py
@@ -6,7 +6,7 @@ import zope.sqlalchemy
# Recommended naming convention used by Alembic, as various different database
# providers will autogenerate vastly different names making migrations more
-# difficult. See: http://alembic.readthedocs.org/en/latest/naming.html
+# difficult. See: http://alembic.zzzcomputing.com/en/latest/naming.html
NAMING_CONVENTION = {
"ix": 'ix_%(column_0_label)s',
"uq": "uq_%(table_name)s_%(column_0_name)s",
diff --git a/docs/quick_tour/static_assets/hello_world.jinja2 b/docs/quick_tour/static_assets/hello_world.jinja2
index 0fb2ce296..a55865231 100644
--- a/docs/quick_tour/static_assets/hello_world.jinja2
+++ b/docs/quick_tour/static_assets/hello_world.jinja2
@@ -2,7 +2,7 @@
<html lang="en">
<head>
<title>Hello World</title>
- <link rel="stylesheet" href="/static/app.css"/>
+ <link rel="stylesheet" href="{{ request.static_url('__main__:static/app.css') }}"/>
</head>
<body>
<h1>Hello {{ name }}!</h1>
diff --git a/docs/quick_tour/static_assets/hello_world_static.jinja2 b/docs/quick_tour/static_assets/hello_world_static.jinja2
index 4fb9be074..0fb2ce296 100644
--- a/docs/quick_tour/static_assets/hello_world_static.jinja2
+++ b/docs/quick_tour/static_assets/hello_world_static.jinja2
@@ -2,7 +2,7 @@
<html lang="en">
<head>
<title>Hello World</title>
- <link rel="stylesheet" href="{{ request.static_url('static/app.css') }}"/>
+ <link rel="stylesheet" href="/static/app.css"/>
</head>
<body>
<h1>Hello {{ name }}!</h1>
diff --git a/docs/quick_tutorial/authentication.rst b/docs/quick_tutorial/authentication.rst
index acff97f3b..892beb3ec 100644
--- a/docs/quick_tutorial/authentication.rst
+++ b/docs/quick_tutorial/authentication.rst
@@ -1,7 +1,7 @@
.. _qtut_authentication:
==============================
-20: Logins With Authentication
+20: Logins with authentication
==============================
Login views that authenticate a username and password against a list of users.
@@ -34,6 +34,18 @@ Steps
.. code-block:: bash
$ cd ..; cp -r view_classes authentication; cd authentication
+
+#. Add ``bcrypt`` as a dependency in ``authentication/setup.py``:
+
+ .. literalinclude:: authentication/setup.py
+ :language: python
+ :emphasize-lines: 5-6
+ :linenos:
+
+#. We can now install our project in development mode:
+
+ .. code-block:: bash
+
$ $VENV/bin/pip install -e .
#. Put the security hash in the ``authentication/development.ini``
@@ -96,8 +108,8 @@ Unlike many web frameworks, Pyramid includes a built-in but optional security
model for authentication and authorization. This security system is intended to
be flexible and support many needs. In this security model, authentication (who
are you) and authorization (what are you allowed to do) are not just pluggable,
-but de-coupled. To learn one step at a time, we provide a system that
-identifies users and lets them log out.
+but decoupled. To learn one step at a time, we provide a system that identifies
+users and lets them log out.
In this example we chose to use the bundled :ref:`AuthTktAuthenticationPolicy
<authentication_module>` policy. We enabled it in our configuration and
@@ -108,6 +120,20 @@ returned a login form. When reached via ``POST``, it processed the submitted
username and password against the "groupfinder" callable that we registered in
the configuration.
+The function ``hash_password`` uses a one-way hashing algorithm with a salt on
+the user's password via ``bcrypt``, instead of storing the password in plain
+text. This is considered to be a "best practice" for security.
+
+.. note::
+ There are alternative libraries to ``bcrypt`` if it is an issue on your
+ system. Just make sure that the library uses an algorithm approved for
+ storing passwords securely.
+
+The function ``check_password`` will compare the two hashed values of the
+submitted password and the user's password stored in the database. If the
+hashed values are equivalent, then the user is authenticated, else
+authentication fails.
+
In our template, we fetched the ``logged_in`` value from the view class. We use
this to calculate the logged-in user, if any. In the template we can then
choose to show a login link to anonymous visitors or a logout link to logged-in
@@ -125,4 +151,5 @@ Extra credit
request? Use ``import pdb; pdb.set_trace()`` to answer this.
.. seealso:: See also :ref:`security_chapter`,
- :ref:`AuthTktAuthenticationPolicy <authentication_module>`.
+ :ref:`AuthTktAuthenticationPolicy <authentication_module>`, `bcrypt
+ <https://pypi.python.org/pypi/bcrypt>`_
diff --git a/docs/quick_tutorial/authentication/setup.py b/docs/quick_tutorial/authentication/setup.py
index 2221b72e9..7a6ff4226 100644
--- a/docs/quick_tutorial/authentication/setup.py
+++ b/docs/quick_tutorial/authentication/setup.py
@@ -2,7 +2,8 @@ from setuptools import setup
requires = [
'pyramid',
- 'pyramid_chameleon'
+ 'pyramid_chameleon',
+ 'bcrypt'
]
setup(name='tutorial',
diff --git a/docs/quick_tutorial/authentication/tutorial/security.py b/docs/quick_tutorial/authentication/tutorial/security.py
index ab90bab2c..e585e2642 100644
--- a/docs/quick_tutorial/authentication/tutorial/security.py
+++ b/docs/quick_tutorial/authentication/tutorial/security.py
@@ -1,5 +1,17 @@
-USERS = {'editor': 'editor',
- 'viewer': 'viewer'}
+import bcrypt
+
+
+def hash_password(pw):
+ pwhash = bcrypt.hashpw(pw.encode('utf8'), bcrypt.gensalt())
+ return pwhash.decode('utf8')
+
+def check_password(pw, hashed_pw):
+ expected_hash = hashed_pw.encode('utf8')
+ return bcrypt.checkpw(pw.encode('utf8'), expected_hash)
+
+
+USERS = {'editor': hash_password('editor'),
+ 'viewer': hash_password('viewer')}
GROUPS = {'editor': ['group:editors']}
diff --git a/docs/quick_tutorial/authentication/tutorial/views.py b/docs/quick_tutorial/authentication/tutorial/views.py
index ab46eb2dd..b07538d5e 100644
--- a/docs/quick_tutorial/authentication/tutorial/views.py
+++ b/docs/quick_tutorial/authentication/tutorial/views.py
@@ -9,7 +9,10 @@ from pyramid.view import (
view_defaults
)
-from .security import USERS
+from .security import (
+ USERS,
+ check_password
+)
@view_defaults(renderer='home.pt')
@@ -40,7 +43,7 @@ class TutorialViews:
if 'form.submitted' in request.params:
login = request.params['login']
password = request.params['password']
- if USERS.get(login) == password:
+ if check_password(password, USERS.get(login)):
headers = remember(request, login)
return HTTPFound(location=came_from,
headers=headers)
diff --git a/docs/quick_tutorial/authorization/setup.py b/docs/quick_tutorial/authorization/setup.py
index 2221b72e9..7a6ff4226 100644
--- a/docs/quick_tutorial/authorization/setup.py
+++ b/docs/quick_tutorial/authorization/setup.py
@@ -2,7 +2,8 @@ from setuptools import setup
requires = [
'pyramid',
- 'pyramid_chameleon'
+ 'pyramid_chameleon',
+ 'bcrypt'
]
setup(name='tutorial',
diff --git a/docs/quick_tutorial/authorization/tutorial/security.py b/docs/quick_tutorial/authorization/tutorial/security.py
index ab90bab2c..e585e2642 100644
--- a/docs/quick_tutorial/authorization/tutorial/security.py
+++ b/docs/quick_tutorial/authorization/tutorial/security.py
@@ -1,5 +1,17 @@
-USERS = {'editor': 'editor',
- 'viewer': 'viewer'}
+import bcrypt
+
+
+def hash_password(pw):
+ pwhash = bcrypt.hashpw(pw.encode('utf8'), bcrypt.gensalt())
+ return pwhash.decode('utf8')
+
+def check_password(pw, hashed_pw):
+ expected_hash = hashed_pw.encode('utf8')
+ return bcrypt.checkpw(pw.encode('utf8'), expected_hash)
+
+
+USERS = {'editor': hash_password('editor'),
+ 'viewer': hash_password('viewer')}
GROUPS = {'editor': ['group:editors']}
diff --git a/docs/quick_tutorial/authorization/tutorial/views.py b/docs/quick_tutorial/authorization/tutorial/views.py
index 43d14455a..b2dc905c0 100644
--- a/docs/quick_tutorial/authorization/tutorial/views.py
+++ b/docs/quick_tutorial/authorization/tutorial/views.py
@@ -10,7 +10,10 @@ from pyramid.view import (
forbidden_view_config
)
-from .security import USERS
+from .security import (
+ USERS,
+ check_password
+)
@view_defaults(renderer='home.pt')
@@ -42,7 +45,7 @@ class TutorialViews:
if 'form.submitted' in request.params:
login = request.params['login']
password = request.params['password']
- if USERS.get(login) == password:
+ if check_password(password, USERS.get(login)):
headers = remember(request, login)
return HTTPFound(location=came_from,
headers=headers)
diff --git a/docs/quick_tutorial/debugtoolbar.rst b/docs/quick_tutorial/debugtoolbar.rst
index aaf904390..b02363d40 100644
--- a/docs/quick_tutorial/debugtoolbar.rst
+++ b/docs/quick_tutorial/debugtoolbar.rst
@@ -90,7 +90,7 @@ temporarily.
.. seealso:: See also :ref:`pyramid_debugtoolbar <toolbar:overview>`.
-Extra Credit
+Extra credit
============
#. Why don't we add ``pyramid_debugtoolbar`` to the list of
diff --git a/docs/quick_tutorial/forms.rst b/docs/quick_tutorial/forms.rst
index 66e77491d..84ceb13d6 100644
--- a/docs/quick_tutorial/forms.rst
+++ b/docs/quick_tutorial/forms.rst
@@ -41,6 +41,7 @@ Steps
pulls in Colander as a dependency:
.. literalinclude:: forms/setup.py
+ :emphasize-lines: 5-6
:linenos:
#. We can now install our project in development mode:
@@ -74,13 +75,18 @@ Steps
:language: html
:linenos:
-#. Finally, a template at ``forms/tutorial/wikipage_view.pt`` for viewing a
- wiki page:
+#. Add a template at ``forms/tutorial/wikipage_view.pt`` for viewing a wiki
+ page:
.. literalinclude:: forms/tutorial/wikipage_view.pt
:language: html
:linenos:
+#. Our tests in ``forms/tutorial/tests.py`` don't run, so let's modify them:
+
+ .. literalinclude:: forms/tutorial/tests.py
+ :linenos:
+
#. Run the tests:
.. code-block:: bash
diff --git a/docs/quick_tutorial/hello_world.rst b/docs/quick_tutorial/hello_world.rst
index 4e35da7bb..56dccde58 100644
--- a/docs/quick_tutorial/hello_world.rst
+++ b/docs/quick_tutorial/hello_world.rst
@@ -88,7 +88,7 @@ Pyramid development. Building an application from loosely-coupled parts via
revisit regularly in this *Quick Tutorial*.
-Extra Credit
+Extra credit
============
#. Why do we do this:
diff --git a/docs/quick_tutorial/ini.rst b/docs/quick_tutorial/ini.rst
index fba5ce29e..9a65d66d1 100644
--- a/docs/quick_tutorial/ini.rst
+++ b/docs/quick_tutorial/ini.rst
@@ -120,7 +120,7 @@ filesystem for changes to relevant code (Python files, the INI file, etc.) and,
when something changes, restart the application. Very handy during development.
-Extra Credit
+Extra credit
============
#. If you don't like configuration and/or ``.ini`` files, could you do this
diff --git a/docs/quick_tutorial/requirements.rst b/docs/quick_tutorial/requirements.rst
index 62dd570fc..afa8ed104 100644
--- a/docs/quick_tutorial/requirements.rst
+++ b/docs/quick_tutorial/requirements.rst
@@ -19,7 +19,7 @@ virtual environment.)
This *Quick Tutorial* is based on:
-* **Python 3.5**. Pyramid fully supports Python 3.3+ and Python 2.7+. This
+* **Python 3.5**. Pyramid fully supports Python 3.4+ and Python 2.7+. This
tutorial uses **Python 3.5** but runs fine under Python 2.7.
* **venv**. We believe in virtual environments. For this tutorial, we use
@@ -156,7 +156,7 @@ environment variable.
.. code-block:: doscon
# Windows
- c:\> c:\Python35\python3 -m venv %VENV%
+ c:\> c:\Python35\python -m venv %VENV%
.. seealso:: See also Python 3's :mod:`venv module <python:venv>` and Python
2's `virtualenv <https://virtualenv.pypa.io/en/latest/>`_ package.
@@ -179,6 +179,9 @@ time of its release.
# Windows
c:\> %VENV%\Scripts\pip install --upgrade pip setuptools
+.. seealso:: See also :ref:`Why use $VENV/bin/pip instead of source
+ bin/activate, then pip <venv-bin-pip-vs-source-bin-activate>`.
+
.. _install-pyramid:
diff --git a/docs/quick_tutorial/routing.rst b/docs/quick_tutorial/routing.rst
index 27c8c2c22..d88adfa1e 100644
--- a/docs/quick_tutorial/routing.rst
+++ b/docs/quick_tutorial/routing.rst
@@ -79,7 +79,7 @@ Steps
.. code-block:: bash
- $ $VENV/bin/$VENV/bin/py.test tutorial/tests.py -q
+ $ $VENV/bin/py.test tutorial/tests.py -q
..
2 passed in 0.39 seconds
diff --git a/docs/quick_tutorial/scaffolds.rst b/docs/quick_tutorial/scaffolds.rst
index 7845f2b71..ad002f4fd 100644
--- a/docs/quick_tutorial/scaffolds.rst
+++ b/docs/quick_tutorial/scaffolds.rst
@@ -38,9 +38,9 @@ Steps
$ $VENV/bin/pcreate --list
Available scaffolds:
- alchemy: Pyramid SQLAlchemy project using url dispatch
- starter: Pyramid starter project
- zodb: Pyramid ZODB project using traversal
+ alchemy: Pyramid project using SQLAlchemy, SQLite, URL dispatch, and Jinja2
+ starter: Pyramid starter project using URL dispatch and Chameleon
+ zodb: Pyramid project using ZODB, traversal, and Chameleon
#. Tell ``pcreate`` to use the ``starter`` scaffold to make our project:
diff --git a/docs/quick_tutorial/static_assets.rst b/docs/quick_tutorial/static_assets.rst
index 65b34f8f9..b8482492d 100644
--- a/docs/quick_tutorial/static_assets.rst
+++ b/docs/quick_tutorial/static_assets.rst
@@ -47,7 +47,7 @@ Steps
.. code-block:: bash
- $ $VENV/bin/$VENV/bin/py.test tutorial/tests.py -q
+ $ $VENV/bin/py.test tutorial/tests.py -q
....
4 passed in 0.50 seconds
diff --git a/docs/quick_tutorial/unit_testing.rst b/docs/quick_tutorial/unit_testing.rst
index 56fd2b297..7c85d5289 100644
--- a/docs/quick_tutorial/unit_testing.rst
+++ b/docs/quick_tutorial/unit_testing.rst
@@ -92,7 +92,7 @@ necessary when your test needs to make use of the ``config`` object (it's a
Configurator) to add stuff to the configuration state before calling the view.
-Extra Credit
+Extra credit
============
#. Change the test to assert that the response status code should be ``404``
diff --git a/docs/tutorials/wiki2/basiclayout.rst b/docs/tutorials/wiki2/basiclayout.rst
index ce67bb9e3..98a14c644 100644
--- a/docs/tutorials/wiki2/basiclayout.rst
+++ b/docs/tutorials/wiki2/basiclayout.rst
@@ -114,7 +114,7 @@ Finally ``main`` is finished configuring things, so it uses the
Route declarations
------------------
-Open the ``tutorials/routes.py`` file. It should already contain the following:
+Open the ``tutorial/routes.py`` file. It should already contain the following:
.. literalinclude:: src/basiclayout/tutorial/routes.py
:linenos:
diff --git a/docs/tutorials/wiki2/installation.rst b/docs/tutorials/wiki2/installation.rst
index a214b1306..0440c2d1d 100644
--- a/docs/tutorials/wiki2/installation.rst
+++ b/docs/tutorials/wiki2/installation.rst
@@ -402,13 +402,6 @@ initialize our database.
already have a database, you should delete it before running
``initialize_tutorial_db`` again.
-.. note::
-
- The ``initialize_tutorial_db`` command is not performing a migration but
- rather simply creating missing tables and adding some dummy data. If you
- already have a database, you should delete it before running
- ``initialize_tutorial_db`` again.
-
Type the following command, making sure you are still in the ``tutorial``
directory (the directory with a ``development.ini`` in it):
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/authentication/tutorial/models/__init__.py
index a8871f6f5..8147052ad 100644
--- a/docs/tutorials/wiki2/src/authentication/tutorial/models/__init__.py
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/models/__init__.py
@@ -5,8 +5,8 @@ import zope.sqlalchemy
# import or define all models here to ensure they are attached to the
# Base.metadata prior to any initialization routines
-from .page import Page # flake8: noqa
-from .user import User # flake8: noqa
+from .page import Page # noqa
+from .user import User # noqa
# run configure_mappers after defining all of the models to ensure
# all relationships can be setup
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/models/meta.py b/docs/tutorials/wiki2/src/authentication/tutorial/models/meta.py
index fc3e8f1dd..0682247b5 100644
--- a/docs/tutorials/wiki2/src/authentication/tutorial/models/meta.py
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/models/meta.py
@@ -3,7 +3,7 @@ from sqlalchemy.schema import MetaData
# Recommended naming convention used by Alembic, as various different database
# providers will autogenerate vastly different names making migrations more
-# difficult. See: http://alembic.readthedocs.org/en/latest/naming.html
+# difficult. See: http://alembic.zzzcomputing.com/en/latest/naming.html
NAMING_CONVENTION = {
"ix": 'ix_%(column_0_label)s',
"uq": "uq_%(table_name)s_%(column_0_name)s",
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/models/user.py b/docs/tutorials/wiki2/src/authentication/tutorial/models/user.py
index 6fb32a1b2..9228b48f7 100644
--- a/docs/tutorials/wiki2/src/authentication/tutorial/models/user.py
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/models/user.py
@@ -19,11 +19,10 @@ class User(Base):
def set_password(self, pw):
pwhash = bcrypt.hashpw(pw.encode('utf8'), bcrypt.gensalt())
- self.password_hash = pwhash
+ self.password_hash = pwhash.decode('utf8')
def check_password(self, pw):
if self.password_hash is not None:
expected_hash = self.password_hash.encode('utf8')
- actual_hash = bcrypt.hashpw(pw.encode('utf8'), expected_hash)
- return expected_hash == actual_hash
+ return bcrypt.checkpw(pw.encode('utf8'), expected_hash)
return False
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py
index a8871f6f5..8147052ad 100644
--- a/docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py
@@ -5,8 +5,8 @@ import zope.sqlalchemy
# import or define all models here to ensure they are attached to the
# Base.metadata prior to any initialization routines
-from .page import Page # flake8: noqa
-from .user import User # flake8: noqa
+from .page import Page # noqa
+from .user import User # noqa
# run configure_mappers after defining all of the models to ensure
# all relationships can be setup
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models/meta.py b/docs/tutorials/wiki2/src/authorization/tutorial/models/meta.py
index fc3e8f1dd..0682247b5 100644
--- a/docs/tutorials/wiki2/src/authorization/tutorial/models/meta.py
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/models/meta.py
@@ -3,7 +3,7 @@ from sqlalchemy.schema import MetaData
# Recommended naming convention used by Alembic, as various different database
# providers will autogenerate vastly different names making migrations more
-# difficult. See: http://alembic.readthedocs.org/en/latest/naming.html
+# difficult. See: http://alembic.zzzcomputing.com/en/latest/naming.html
NAMING_CONVENTION = {
"ix": 'ix_%(column_0_label)s',
"uq": "uq_%(table_name)s_%(column_0_name)s",
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models/user.py b/docs/tutorials/wiki2/src/authorization/tutorial/models/user.py
index 6fb32a1b2..9228b48f7 100644
--- a/docs/tutorials/wiki2/src/authorization/tutorial/models/user.py
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/models/user.py
@@ -19,11 +19,10 @@ class User(Base):
def set_password(self, pw):
pwhash = bcrypt.hashpw(pw.encode('utf8'), bcrypt.gensalt())
- self.password_hash = pwhash
+ self.password_hash = pwhash.decode('utf8')
def check_password(self, pw):
if self.password_hash is not None:
expected_hash = self.password_hash.encode('utf8')
- actual_hash = bcrypt.hashpw(pw.encode('utf8'), expected_hash)
- return expected_hash == actual_hash
+ return bcrypt.checkpw(pw.encode('utf8'), expected_hash)
return False
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/__init__.py
index 48a957ecb..3fc82cfba 100644
--- a/docs/tutorials/wiki2/src/basiclayout/tutorial/models/__init__.py
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/__init__.py
@@ -5,7 +5,7 @@ import zope.sqlalchemy
# import or define all models here to ensure they are attached to the
# Base.metadata prior to any initialization routines
-from .mymodel import MyModel # flake8: noqa
+from .mymodel import MyModel # noqa
# run configure_mappers after defining all of the models to ensure
# all relationships can be setup
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/models/meta.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/meta.py
index fc3e8f1dd..0682247b5 100644
--- a/docs/tutorials/wiki2/src/basiclayout/tutorial/models/meta.py
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/meta.py
@@ -3,7 +3,7 @@ from sqlalchemy.schema import MetaData
# Recommended naming convention used by Alembic, as various different database
# providers will autogenerate vastly different names making migrations more
-# difficult. See: http://alembic.readthedocs.org/en/latest/naming.html
+# difficult. See: http://alembic.zzzcomputing.com/en/latest/naming.html
NAMING_CONVENTION = {
"ix": 'ix_%(column_0_label)s',
"uq": "uq_%(table_name)s_%(column_0_name)s",
diff --git a/docs/tutorials/wiki2/src/installation/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/installation/tutorial/models/__init__.py
index 48a957ecb..3fc82cfba 100644
--- a/docs/tutorials/wiki2/src/installation/tutorial/models/__init__.py
+++ b/docs/tutorials/wiki2/src/installation/tutorial/models/__init__.py
@@ -5,7 +5,7 @@ import zope.sqlalchemy
# import or define all models here to ensure they are attached to the
# Base.metadata prior to any initialization routines
-from .mymodel import MyModel # flake8: noqa
+from .mymodel import MyModel # noqa
# run configure_mappers after defining all of the models to ensure
# all relationships can be setup
diff --git a/docs/tutorials/wiki2/src/installation/tutorial/models/meta.py b/docs/tutorials/wiki2/src/installation/tutorial/models/meta.py
index fc3e8f1dd..0682247b5 100644
--- a/docs/tutorials/wiki2/src/installation/tutorial/models/meta.py
+++ b/docs/tutorials/wiki2/src/installation/tutorial/models/meta.py
@@ -3,7 +3,7 @@ from sqlalchemy.schema import MetaData
# Recommended naming convention used by Alembic, as various different database
# providers will autogenerate vastly different names making migrations more
-# difficult. See: http://alembic.readthedocs.org/en/latest/naming.html
+# difficult. See: http://alembic.zzzcomputing.com/en/latest/naming.html
NAMING_CONVENTION = {
"ix": 'ix_%(column_0_label)s',
"uq": "uq_%(table_name)s_%(column_0_name)s",
diff --git a/docs/tutorials/wiki2/src/models/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/models/tutorial/models/__init__.py
index a8871f6f5..8147052ad 100644
--- a/docs/tutorials/wiki2/src/models/tutorial/models/__init__.py
+++ b/docs/tutorials/wiki2/src/models/tutorial/models/__init__.py
@@ -5,8 +5,8 @@ import zope.sqlalchemy
# import or define all models here to ensure they are attached to the
# Base.metadata prior to any initialization routines
-from .page import Page # flake8: noqa
-from .user import User # flake8: noqa
+from .page import Page # noqa
+from .user import User # noqa
# run configure_mappers after defining all of the models to ensure
# all relationships can be setup
diff --git a/docs/tutorials/wiki2/src/models/tutorial/models/meta.py b/docs/tutorials/wiki2/src/models/tutorial/models/meta.py
index fc3e8f1dd..0682247b5 100644
--- a/docs/tutorials/wiki2/src/models/tutorial/models/meta.py
+++ b/docs/tutorials/wiki2/src/models/tutorial/models/meta.py
@@ -3,7 +3,7 @@ from sqlalchemy.schema import MetaData
# Recommended naming convention used by Alembic, as various different database
# providers will autogenerate vastly different names making migrations more
-# difficult. See: http://alembic.readthedocs.org/en/latest/naming.html
+# difficult. See: http://alembic.zzzcomputing.com/en/latest/naming.html
NAMING_CONVENTION = {
"ix": 'ix_%(column_0_label)s',
"uq": "uq_%(table_name)s_%(column_0_name)s",
diff --git a/docs/tutorials/wiki2/src/models/tutorial/models/user.py b/docs/tutorials/wiki2/src/models/tutorial/models/user.py
index 6fb32a1b2..9228b48f7 100644
--- a/docs/tutorials/wiki2/src/models/tutorial/models/user.py
+++ b/docs/tutorials/wiki2/src/models/tutorial/models/user.py
@@ -19,11 +19,10 @@ class User(Base):
def set_password(self, pw):
pwhash = bcrypt.hashpw(pw.encode('utf8'), bcrypt.gensalt())
- self.password_hash = pwhash
+ self.password_hash = pwhash.decode('utf8')
def check_password(self, pw):
if self.password_hash is not None:
expected_hash = self.password_hash.encode('utf8')
- actual_hash = bcrypt.hashpw(pw.encode('utf8'), expected_hash)
- return expected_hash == actual_hash
+ return bcrypt.checkpw(pw.encode('utf8'), expected_hash)
return False
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/tests/tutorial/models/__init__.py
index a8871f6f5..8147052ad 100644
--- a/docs/tutorials/wiki2/src/tests/tutorial/models/__init__.py
+++ b/docs/tutorials/wiki2/src/tests/tutorial/models/__init__.py
@@ -5,8 +5,8 @@ import zope.sqlalchemy
# import or define all models here to ensure they are attached to the
# Base.metadata prior to any initialization routines
-from .page import Page # flake8: noqa
-from .user import User # flake8: noqa
+from .page import Page # noqa
+from .user import User # noqa
# run configure_mappers after defining all of the models to ensure
# all relationships can be setup
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/models/meta.py b/docs/tutorials/wiki2/src/tests/tutorial/models/meta.py
index fc3e8f1dd..0682247b5 100644
--- a/docs/tutorials/wiki2/src/tests/tutorial/models/meta.py
+++ b/docs/tutorials/wiki2/src/tests/tutorial/models/meta.py
@@ -3,7 +3,7 @@ from sqlalchemy.schema import MetaData
# Recommended naming convention used by Alembic, as various different database
# providers will autogenerate vastly different names making migrations more
-# difficult. See: http://alembic.readthedocs.org/en/latest/naming.html
+# difficult. See: http://alembic.zzzcomputing.com/en/latest/naming.html
NAMING_CONVENTION = {
"ix": 'ix_%(column_0_label)s',
"uq": "uq_%(table_name)s_%(column_0_name)s",
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/models/user.py b/docs/tutorials/wiki2/src/tests/tutorial/models/user.py
index 6fb32a1b2..9228b48f7 100644
--- a/docs/tutorials/wiki2/src/tests/tutorial/models/user.py
+++ b/docs/tutorials/wiki2/src/tests/tutorial/models/user.py
@@ -19,11 +19,10 @@ class User(Base):
def set_password(self, pw):
pwhash = bcrypt.hashpw(pw.encode('utf8'), bcrypt.gensalt())
- self.password_hash = pwhash
+ self.password_hash = pwhash.decode('utf8')
def check_password(self, pw):
if self.password_hash is not None:
expected_hash = self.password_hash.encode('utf8')
- actual_hash = bcrypt.hashpw(pw.encode('utf8'), expected_hash)
- return expected_hash == actual_hash
+ return bcrypt.checkpw(pw.encode('utf8'), expected_hash)
return False
diff --git a/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py
index a8871f6f5..8147052ad 100644
--- a/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py
+++ b/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py
@@ -5,8 +5,8 @@ import zope.sqlalchemy
# import or define all models here to ensure they are attached to the
# Base.metadata prior to any initialization routines
-from .page import Page # flake8: noqa
-from .user import User # flake8: noqa
+from .page import Page # noqa
+from .user import User # noqa
# run configure_mappers after defining all of the models to ensure
# all relationships can be setup
diff --git a/docs/tutorials/wiki2/src/views/tutorial/models/meta.py b/docs/tutorials/wiki2/src/views/tutorial/models/meta.py
index fc3e8f1dd..0682247b5 100644
--- a/docs/tutorials/wiki2/src/views/tutorial/models/meta.py
+++ b/docs/tutorials/wiki2/src/views/tutorial/models/meta.py
@@ -3,7 +3,7 @@ from sqlalchemy.schema import MetaData
# Recommended naming convention used by Alembic, as various different database
# providers will autogenerate vastly different names making migrations more
-# difficult. See: http://alembic.readthedocs.org/en/latest/naming.html
+# difficult. See: http://alembic.zzzcomputing.com/en/latest/naming.html
NAMING_CONVENTION = {
"ix": 'ix_%(column_0_label)s',
"uq": "uq_%(table_name)s_%(column_0_name)s",
diff --git a/docs/tutorials/wiki2/src/views/tutorial/models/user.py b/docs/tutorials/wiki2/src/views/tutorial/models/user.py
index 6fb32a1b2..9228b48f7 100644
--- a/docs/tutorials/wiki2/src/views/tutorial/models/user.py
+++ b/docs/tutorials/wiki2/src/views/tutorial/models/user.py
@@ -19,11 +19,10 @@ class User(Base):
def set_password(self, pw):
pwhash = bcrypt.hashpw(pw.encode('utf8'), bcrypt.gensalt())
- self.password_hash = pwhash
+ self.password_hash = pwhash.decode('utf8')
def check_password(self, pw):
if self.password_hash is not None:
expected_hash = self.password_hash.encode('utf8')
- actual_hash = bcrypt.hashpw(pw.encode('utf8'), expected_hash)
- return expected_hash == actual_hash
+ return bcrypt.checkpw(pw.encode('utf8'), expected_hash)
return False
diff --git a/pyramid/authentication.py b/pyramid/authentication.py
index e6b888db2..2ee5576d9 100644
--- a/pyramid/authentication.py
+++ b/pyramid/authentication.py
@@ -1,10 +1,12 @@
import binascii
from codecs import utf_8_decode
from codecs import utf_8_encode
+from collections import namedtuple
import hashlib
import base64
import re
import time as time_mod
+import warnings
from zope.interface import implementer
@@ -947,8 +949,19 @@ class AuthTktCookieHelper(object):
if encoding_data:
encoding, encoder = encoding_data
- userid = encoder(userid)
- user_data = 'userid_type:%s' % encoding
+ else:
+ warnings.warn(
+ "userid is of type {}, and is not supported by the "
+ "AuthTktAuthenticationPolicy. Explicitly converting to string "
+ "and storing as base64. Subsequent requests will receive a "
+ "string as the userid, it will not be decoded back to the type "
+ "provided.".format(type(userid)), RuntimeWarning
+ )
+ encoding, encoder = self.userid_type_encoders.get(text_type)
+ userid = str(userid)
+
+ userid = encoder(userid)
+ user_data = 'userid_type:%s' % encoding
new_tokens = []
for token in tokens:
@@ -1083,7 +1096,7 @@ class BasicAuthAuthenticationPolicy(CallbackAuthenticationPolicy):
def unauthenticated_userid(self, request):
""" The userid parsed from the ``Authorization`` request header."""
- credentials = self._get_credentials(request)
+ credentials = extract_http_basic_credentials(request)
if credentials:
return credentials[0]
@@ -1100,42 +1113,15 @@ class BasicAuthAuthenticationPolicy(CallbackAuthenticationPolicy):
return [('WWW-Authenticate', 'Basic realm="%s"' % self.realm)]
def callback(self, username, request):
- # Username arg is ignored. Unfortunately _get_credentials winds up
- # getting called twice when authenticated_userid is called. Avoiding
- # that, however, winds up duplicating logic from the superclass.
- credentials = self._get_credentials(request)
+ # Username arg is ignored. Unfortunately
+ # extract_http_basic_credentials winds up getting called twice when
+ # authenticated_userid is called. Avoiding that, however,
+ # winds up duplicating logic from the superclass.
+ credentials = extract_http_basic_credentials(request)
if credentials:
username, password = credentials
return self.check(username, password, request)
- def _get_credentials(self, request):
- authorization = request.headers.get('Authorization')
- if not authorization:
- return None
- try:
- authmeth, auth = authorization.split(' ', 1)
- except ValueError: # not enough values to unpack
- return None
- if authmeth.lower() != 'basic':
- return None
-
- try:
- authbytes = b64decode(auth.strip())
- except (TypeError, binascii.Error): # can't decode
- return None
-
- # try utf-8 first, then latin-1; see discussion in
- # https://github.com/Pylons/pyramid/issues/898
- try:
- auth = authbytes.decode('utf-8')
- except UnicodeDecodeError:
- auth = authbytes.decode('latin-1')
-
- try:
- username, password = auth.split(':', 1)
- except ValueError: # not enough values to unpack
- return None
- return username, password
class _SimpleSerializer(object):
def loads(self, bstruct):
@@ -1143,3 +1129,47 @@ class _SimpleSerializer(object):
def dumps(self, appstruct):
return bytes_(appstruct)
+
+
+HTTPBasicCredentials = namedtuple(
+ 'HTTPBasicCredentials', ['username', 'password'])
+
+
+def extract_http_basic_credentials(request):
+ """ A helper function for extraction of HTTP Basic credentials
+ from a given :term:`request`.
+
+ Returns a :class:`.HTTPBasicCredentials` 2-tuple with ``username`` and
+ ``password`` attributes or ``None`` if no credentials could be found.
+
+ """
+ authorization = request.headers.get('Authorization')
+ if not authorization:
+ return None
+
+ try:
+ authmeth, auth = authorization.split(' ', 1)
+ except ValueError: # not enough values to unpack
+ return None
+
+ if authmeth.lower() != 'basic':
+ return None
+
+ try:
+ authbytes = b64decode(auth.strip())
+ except (TypeError, binascii.Error): # can't decode
+ return None
+
+ # try utf-8 first, then latin-1; see discussion in
+ # https://github.com/Pylons/pyramid/issues/898
+ try:
+ auth = authbytes.decode('utf-8')
+ except UnicodeDecodeError:
+ auth = authbytes.decode('latin-1')
+
+ try:
+ username, password = auth.split(':', 1)
+ except ValueError: # not enough values to unpack
+ return None
+
+ return HTTPBasicCredentials(username, password)
diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py
index 553f32c9b..d4064dc78 100644
--- a/pyramid/config/__init__.py
+++ b/pyramid/config/__init__.py
@@ -27,7 +27,6 @@ from pyramid.compat import (
text_,
reraise,
string_types,
- zip_longest,
)
from pyramid.events import ApplicationCreated
@@ -380,6 +379,7 @@ class Configurator(
self.add_default_view_predicates()
self.add_default_view_derivers()
self.add_default_route_predicates()
+ self.add_default_tweens()
if exceptionresponse_view is not None:
exceptionresponse_view = self.maybe_dotted(exceptionresponse_view)
@@ -1110,29 +1110,8 @@ class ActionState(object):
try:
all_actions = []
executed_actions = []
- pending_actions = iter([])
-
- # resolve the new action list against what we have already
- # executed -- if a new action appears intertwined in the list
- # of already-executed actions then someone wrote a broken
- # re-entrant action because it scheduled the action *after* it
- # should have been executed (as defined by the action order)
- def resume(actions):
- for a, b in zip_longest(actions, executed_actions):
- if b is None and a is not None:
- # common case is that we are executing every action
- yield a
- elif b is not None and a != b:
- raise ConfigurationError(
- 'During execution a re-entrant action was added '
- 'that modified the planned execution order in a '
- 'way that is incompatible with what has already '
- 'been executed.')
- else:
- # resolved action is in the same location as before,
- # so we are in good shape, but the action is already
- # executed so we skip it
- assert b is not None and a == b
+ action_iter = iter([])
+ conflict_state = ConflictResolverState()
while True:
# We clear the actions list prior to execution so if there
@@ -1141,26 +1120,14 @@ class ActionState(object):
# ensures that the previously executed actions have no new
# conflicts.
if self.actions:
- # Only resolve the new actions against executed_actions
- # and pending_actions instead of everything to avoid
- # redundant checks.
- # Assume ``actions = resolveConflicts([A, B, C])`` which
- # after conflict checks, resulted in ``actions == [A]``
- # then we know action A won out or a conflict would have
- # been raised. Thus, when action D is added later, we only
- # need to check the new action against A.
- # ``actions = resolveConflicts([A, D]) should drop the
- # number of redundant checks down from O(n^2) closer to
- # O(n lg n).
all_actions.extend(self.actions)
- pending_actions = resume(resolveConflicts(
- executed_actions +
- list(pending_actions) +
- self.actions
- ))
+ action_iter = resolveConflicts(
+ self.actions,
+ state=conflict_state,
+ )
self.actions = []
- action = next(pending_actions, None)
+ action = next(action_iter, None)
if action is None:
# we are done!
break
@@ -1176,9 +1143,7 @@ class ActionState(object):
try:
if callable is not None:
callable(*args, **kw)
- except (KeyboardInterrupt, SystemExit): # pragma: no cover
- raise
- except:
+ except Exception:
t, v, tb = sys.exc_info()
try:
reraise(ConfigurationExecutionError,
@@ -1193,65 +1158,102 @@ class ActionState(object):
executed_actions.append(action)
+ self.actions = all_actions
+ return executed_actions
+
finally:
if clear:
- del self.actions[:]
- else:
- self.actions = all_actions
+ self.actions = []
+
+
+class ConflictResolverState(object):
+ def __init__(self):
+ # keep a set of resolved discriminators to test against to ensure
+ # that a new action does not conflict with something already executed
+ self.resolved_ainfos = {}
+
+ # actions left over from a previous iteration
+ self.remaining_actions = []
+
+ # after executing an action we memoize its order to avoid any new
+ # actions sending us backward
+ self.min_order = None
+
+ # unique tracks the index of the action so we need it to increase
+ # monotonically across invocations to resolveConflicts
+ self.start = 0
+
# this function is licensed under the ZPL (stolen from Zope)
-def resolveConflicts(actions):
+def resolveConflicts(actions, state=None):
"""Resolve conflicting actions
Given an actions list, identify and try to resolve conflicting actions.
Actions conflict if they have the same non-None discriminator.
+
Conflicting actions can be resolved if the include path of one of
the actions is a prefix of the includepaths of the other
conflicting actions and is unequal to the include paths in the
other conflicting actions.
+
+ Actions are resolved on a per-order basis because some discriminators
+ cannot be computed until earlier actions have executed. An action in an
+ earlier order may execute successfully only to find out later that it was
+ overridden by another action with a smaller include path. This will result
+ in a conflict as there is no way to revert the original action.
+
+ ``state`` may be an instance of ``ConflictResolverState`` that
+ can be used to resume execution and resolve the new actions against the
+ list of executed actions from a previous call.
+
"""
+ if state is None:
+ state = ConflictResolverState()
+
+ # pick up where we left off last time, but track the new actions as well
+ state.remaining_actions.extend(normalize_actions(actions))
+ actions = state.remaining_actions
def orderandpos(v):
n, v = v
- if not isinstance(v, dict):
- # old-style tuple action
- v = expand_action(*v)
return (v['order'] or 0, n)
- sactions = sorted(enumerate(actions), key=orderandpos)
-
def orderonly(v):
n, v = v
- if not isinstance(v, dict):
- # old-style tuple action
- v = expand_action(*v)
return v['order'] or 0
+ sactions = sorted(enumerate(actions, start=state.start), key=orderandpos)
for order, actiongroup in itertools.groupby(sactions, orderonly):
# "order" is an integer grouping. Actions in a lower order will be
# executed before actions in a higher order. All of the actions in
# one grouping will be executed (its callable, if any will be called)
# before any of the actions in the next.
-
- unique = {}
output = []
+ unique = {}
+
+ # error out if we went backward in order
+ if state.min_order is not None and order < state.min_order:
+ r = ['Actions were added to order={0} after execution had moved '
+ 'on to order={1}. Conflicting actions: '
+ .format(order, state.min_order)]
+ for i, action in actiongroup:
+ for line in str(action['info']).rstrip().split('\n'):
+ r.append(" " + line)
+ raise ConfigurationError('\n'.join(r))
for i, action in actiongroup:
# Within an order, actions are executed sequentially based on
# original action ordering ("i").
- if not isinstance(action, dict):
- # old-style tuple action
- action = expand_action(*action)
-
- # "ainfo" is a tuple of (order, i, action) where "order" is a
- # user-supplied grouping, "i" is an integer expressing the relative
- # position of this action in the action list being resolved, and
- # "action" is an action dictionary. The purpose of an ainfo is to
- # associate an "order" and an "i" with a particular action; "order"
- # and "i" exist for sorting purposes after conflict resolution.
- ainfo = (order, i, action)
+ # "ainfo" is a tuple of (i, action) where "i" is an integer
+ # expressing the relative position of this action in the action
+ # list being resolved, and "action" is an action dictionary. The
+ # purpose of an ainfo is to associate an "i" with a particular
+ # action; "i" exists for sorting after conflict resolution.
+ ainfo = (i, action)
+ # wait to defer discriminators until we are on their order because
+ # the discriminator may depend on state from a previous order
discriminator = undefer(action['discriminator'])
action['discriminator'] = discriminator
@@ -1266,28 +1268,39 @@ def resolveConflicts(actions):
# Check for conflicts
conflicts = {}
-
for discriminator, ainfos in unique.items():
- # We use (includepath, order, i) as a sort key because we need to
+ # We use (includepath, i) as a sort key because we need to
# sort the actions by the paths so that the shortest path with a
# given prefix comes first. The "first" action is the one with the
- # shortest include path. We break sorting ties using "order", then
- # "i".
+ # shortest include path. We break sorting ties using "i".
def bypath(ainfo):
- path, order, i = ainfo[2]['includepath'], ainfo[0], ainfo[1]
+ path, i = ainfo[1]['includepath'], ainfo[0]
return path, order, i
ainfos.sort(key=bypath)
ainfo, rest = ainfos[0], ainfos[1:]
- output.append(ainfo)
- _, _, action = ainfo
- basepath, baseinfo, discriminator = (
- action['includepath'],
- action['info'],
- action['discriminator'],
- )
+ _, action = ainfo
+
+ # ensure this new action does not conflict with a previously
+ # resolved action from an earlier order / invocation
+ prev_ainfo = state.resolved_ainfos.get(discriminator)
+ if prev_ainfo is not None:
+ _, paction = prev_ainfo
+ basepath, baseinfo = paction['includepath'], paction['info']
+ includepath = action['includepath']
+ # if the new action conflicts with the resolved action then
+ # note the conflict, otherwise drop the action as it's
+ # effectively overriden by the previous action
+ if (includepath[:len(basepath)] != basepath or
+ includepath == basepath):
+ L = conflicts.setdefault(discriminator, [baseinfo])
+ L.append(action['info'])
+
+ else:
+ output.append(ainfo)
- for _, _, action in rest:
+ basepath, baseinfo = action['includepath'], action['info']
+ for _, action in rest:
includepath = action['includepath']
# Test whether path is a prefix of opath
if (includepath[:len(basepath)] != basepath or # not a prefix
@@ -1298,14 +1311,30 @@ def resolveConflicts(actions):
if conflicts:
raise ConfigurationConflictError(conflicts)
- # sort conflict-resolved actions by (order, i) and yield them one
- # by one
- for a in [x[2] for x in sorted(output, key=operator.itemgetter(0, 1))]:
- yield a
+ # sort resolved actions by "i" and yield them one by one
+ for i, action in sorted(output, key=operator.itemgetter(0)):
+ # do not memoize the order until we resolve an action inside it
+ state.min_order = action['order']
+ state.start = i + 1
+ state.remaining_actions.remove(action)
+ state.resolved_ainfos[action['discriminator']] = (i, action)
+ yield action
-def expand_action(discriminator, callable=None, args=(), kw=None,
- includepath=(), info=None, order=0, introspectables=()):
+def normalize_actions(actions):
+ """Convert old-style tuple actions to new-style dicts."""
+ result = []
+ for v in actions:
+ if not isinstance(v, dict):
+ v = expand_action_tuple(*v)
+ result.append(v)
+ return result
+
+
+def expand_action_tuple(
+ discriminator, callable=None, args=(), kw=None, includepath=(),
+ info=None, order=0, introspectables=(),
+):
if kw is None:
kw = {}
return dict(
@@ -1319,4 +1348,5 @@ def expand_action(discriminator, callable=None, args=(), kw=None,
introspectables=introspectables,
)
+
global_registries = WeakOrderedSet()
diff --git a/pyramid/config/security.py b/pyramid/config/security.py
index e387eade9..02732c042 100644
--- a/pyramid/config/security.py
+++ b/pyramid/config/security.py
@@ -169,6 +169,7 @@ class SecurityConfiguratorMixin(object):
token='csrf_token',
header='X-CSRF-Token',
safe_methods=('GET', 'HEAD', 'OPTIONS', 'TRACE'),
+ callback=None,
):
"""
Set the default CSRF options used by subsequent view registrations.
@@ -192,8 +193,20 @@ class SecurityConfiguratorMixin(object):
never be automatically checked for CSRF tokens.
Default: ``('GET', 'HEAD', 'OPTIONS', TRACE')``.
+ If ``callback`` is set, it must be a callable accepting ``(request)``
+ and returning ``True`` if the request should be checked for a valid
+ CSRF token. This callback allows an application to support
+ alternate authentication methods that do not rely on cookies which
+ are not subject to CSRF attacks. For example, if a request is
+ authenticated using the ``Authorization`` header instead of a cookie,
+ this may return ``False`` for that request so that clients do not
+ need to send the ``X-CSRF-Token` header. The callback is only tested
+ for non-safe methods as defined by ``safe_methods``.
+
"""
- options = DefaultCSRFOptions(require_csrf, token, header, safe_methods)
+ options = DefaultCSRFOptions(
+ require_csrf, token, header, safe_methods, callback,
+ )
def register():
self.registry.registerUtility(options, IDefaultCSRFOptions)
intr = self.introspectable('default csrf view options',
@@ -204,13 +217,15 @@ class SecurityConfiguratorMixin(object):
intr['token'] = token
intr['header'] = header
intr['safe_methods'] = as_sorted_tuple(safe_methods)
+ intr['callback'] = callback
self.action(IDefaultCSRFOptions, register, order=PHASE1_CONFIG,
introspectables=(intr,))
@implementer(IDefaultCSRFOptions)
class DefaultCSRFOptions(object):
- def __init__(self, require_csrf, token, header, safe_methods):
+ def __init__(self, require_csrf, token, header, safe_methods, callback):
self.require_csrf = require_csrf
self.token = token
self.header = header
self.safe_methods = frozenset(safe_methods)
+ self.callback = callback
diff --git a/pyramid/config/tweens.py b/pyramid/config/tweens.py
index 8e1800f33..0aeb01fe3 100644
--- a/pyramid/config/tweens.py
+++ b/pyramid/config/tweens.py
@@ -10,7 +10,6 @@ from pyramid.compat import (
from pyramid.exceptions import ConfigurationError
from pyramid.tweens import (
- excview_tween_factory,
MAIN,
INGRESS,
EXCVIEW,
@@ -107,6 +106,9 @@ class TweensConfiguratorMixin(object):
return self._add_tween(tween_factory, under=under, over=over,
explicit=False)
+ def add_default_tweens(self):
+ self.add_tween(EXCVIEW)
+
@action_method
def _add_tween(self, tween_factory, under=None, over=None, explicit=False):
@@ -142,17 +144,6 @@ class TweensConfiguratorMixin(object):
if tweens is None:
tweens = Tweens()
registry.registerUtility(tweens, ITweens)
- ex_intr = self.introspectable('tweens',
- ('tween', EXCVIEW, False),
- EXCVIEW,
- 'implicit tween')
- ex_intr['name'] = EXCVIEW
- ex_intr['factory'] = excview_tween_factory
- ex_intr['type'] = 'implicit'
- ex_intr['under'] = None
- ex_intr['over'] = MAIN
- introspectables.append(ex_intr)
- tweens.add_implicit(EXCVIEW, excview_tween_factory, over=MAIN)
def register():
if explicit:
diff --git a/pyramid/config/views.py b/pyramid/config/views.py
index 5cb3f5099..acdc00704 100644
--- a/pyramid/config/views.py
+++ b/pyramid/config/views.py
@@ -9,12 +9,11 @@ from zope.interface import (
implementedBy,
implementer,
)
-
from zope.interface.interfaces import IInterface
from pyramid.interfaces import (
- IException,
IExceptionViewClassifier,
+ IException,
IMultiView,
IPackageOverrides,
IRendererFactory,
@@ -213,6 +212,7 @@ class ViewsConfiguratorMixin(object):
match_param=None,
check_csrf=None,
require_csrf=None,
+ exception_only=False,
**view_options):
""" Add a :term:`view configuration` to the current
configuration state. Arguments to ``add_view`` are broken
@@ -502,7 +502,20 @@ class ViewsConfiguratorMixin(object):
if the :term:`context` provides the represented interface;
it is otherwise false. This argument may also be provided
to ``add_view`` as ``for_`` (an older, still-supported
- spelling).
+ spelling). If the view should *only* match when handling
+ exceptions, then set the ``exception_only`` to ``True``.
+
+ exception_only
+
+ .. versionadded:: 1.8
+
+ When this value is ``True``, the ``context`` argument must be
+ a subclass of ``Exception``. This flag indicates that only an
+ :term:`exception view` should be created, and that this view should
+ not match if the traversal :term:`context` matches the ``context``
+ argument. If the ``context`` is a subclass of ``Exception`` and
+ this value is ``False`` (the default), then a view will be
+ registered to match the traversal :term:`context` as well.
route_name
@@ -684,7 +697,7 @@ class ViewsConfiguratorMixin(object):
obsoletes this argument, but it is kept around for backwards
compatibility.
- view_options:
+ view_options
Pass a key/value pair here to use a third-party predicate or set a
value for a view deriver. See
@@ -762,6 +775,12 @@ class ViewsConfiguratorMixin(object):
if context is None:
context = for_
+ isexc = isexception(context)
+ if exception_only and not isexc:
+ raise ConfigurationError(
+ 'view "context" must be an exception type when '
+ '"exception_only" is True')
+
r_context = context
if r_context is None:
r_context = Interface
@@ -797,6 +816,7 @@ class ViewsConfiguratorMixin(object):
# is. It can't be computed any sooner because thirdparty
# predicates/view derivers may not yet exist when add_view is
# called.
+ predlist = self.get_predlist('view')
valid_predicates = predlist.names()
pvals = {}
dvals = {}
@@ -835,6 +855,7 @@ class ViewsConfiguratorMixin(object):
view_intr.update(dict(
name=name,
context=context,
+ exception_only=exception_only,
containment=containment,
request_param=request_param,
request_methods=request_method,
@@ -854,7 +875,6 @@ class ViewsConfiguratorMixin(object):
))
view_intr.update(view_options)
introspectables.append(view_intr)
- predlist = self.get_predlist('view')
def register(permission=permission, renderer=renderer):
request_iface = IRequest
@@ -877,12 +897,54 @@ class ViewsConfiguratorMixin(object):
registry=self.registry
)
+ renderer_type = getattr(renderer, 'type', None)
+ intrspc = self.introspector
+ if (
+ renderer_type is not None and
+ tmpl_intr is not None and
+ intrspc is not None and
+ intrspc.get('renderer factories', renderer_type) is not None
+ ):
+ # allow failure of registered template factories to be deferred
+ # until view execution, like other bad renderer factories; if
+ # we tried to relate this to an existing renderer factory
+ # without checking if the factory actually existed, we'd end
+ # up with a KeyError at startup time, which is inconsistent
+ # with how other bad renderer registrations behave (they throw
+ # a ValueError at view execution time)
+ tmpl_intr.relate('renderer factories', renderer.type)
+
+ # make a new view separately for normal and exception paths
+ if not exception_only:
+ derived_view = derive_view(False, renderer)
+ register_view(IViewClassifier, request_iface, derived_view)
+ if isexc:
+ derived_exc_view = derive_view(True, renderer)
+ register_view(IExceptionViewClassifier, request_iface,
+ derived_exc_view)
+
+ if exception_only:
+ derived_view = derived_exc_view
+
+ # if there are two derived views, combine them into one for
+ # introspection purposes
+ if not exception_only and isexc:
+ derived_view = runtime_exc_view(derived_view, derived_exc_view)
+
+ derived_view.__discriminator__ = lambda *arg: discriminator
+ # __discriminator__ is used by superdynamic systems
+ # that require it for introspection after manual view lookup;
+ # see also MultiView.__discriminator__
+ view_intr['derived_callable'] = derived_view
+
+ self.registry._clear_view_lookup_cache()
+
+ def derive_view(isexc_only, renderer):
# added by discrim_func above during conflict resolving
preds = view_intr['predicates']
order = view_intr['order']
phash = view_intr['phash']
- # __no_permission_required__ handled by _secure_view
derived_view = self._derive_view(
view,
route_name=route_name,
@@ -890,6 +952,7 @@ class ViewsConfiguratorMixin(object):
predicates=preds,
attr=attr,
context=context,
+ exception_only=isexc_only,
renderer=renderer,
wrapper_viewname=wrapper,
viewname=name,
@@ -902,14 +965,9 @@ class ViewsConfiguratorMixin(object):
require_csrf=require_csrf,
extra_options=ovals,
)
- derived_view.__discriminator__ = lambda *arg: discriminator
- # __discriminator__ is used by superdynamic systems
- # that require it for introspection after manual view lookup;
- # see also MultiView.__discriminator__
- view_intr['derived_callable'] = derived_view
-
- registered = self.registry.adapters.registered
+ return derived_view
+ def register_view(classifier, request_iface, derived_view):
# A multiviews is a set of views which are registered for
# exactly the same context type/request type/name triad. Each
# consituent view in a multiview differs only by the
@@ -929,15 +987,16 @@ class ViewsConfiguratorMixin(object):
# matches on all the arguments it receives.
old_view = None
+ order, phash = view_intr['order'], view_intr['phash']
+ registered = self.registry.adapters.registered
for view_type in (IView, ISecuredView, IMultiView):
- old_view = registered((IViewClassifier, request_iface,
- r_context), view_type, name)
+ old_view = registered(
+ (classifier, request_iface, r_context),
+ view_type, name)
if old_view is not None:
break
- isexc = isexception(context)
-
def regclosure():
if hasattr(derived_view, '__call_permissive__'):
view_iface = ISecuredView
@@ -945,13 +1004,10 @@ class ViewsConfiguratorMixin(object):
view_iface = IView
self.registry.registerAdapter(
derived_view,
- (IViewClassifier, request_iface, context), view_iface, name
+ (classifier, request_iface, context),
+ view_iface,
+ name
)
- if isexc:
- self.registry.registerAdapter(
- derived_view,
- (IExceptionViewClassifier, request_iface, context),
- view_iface, name)
is_multiview = IMultiView.providedBy(old_view)
old_phash = getattr(old_view, '__phash__', DEFAULT_PHASH)
@@ -988,39 +1044,12 @@ class ViewsConfiguratorMixin(object):
for view_type in (IView, ISecuredView):
# unregister any existing views
self.registry.adapters.unregister(
- (IViewClassifier, request_iface, r_context),
+ (classifier, request_iface, r_context),
view_type, name=name)
- if isexc:
- self.registry.adapters.unregister(
- (IExceptionViewClassifier, request_iface,
- r_context), view_type, name=name)
self.registry.registerAdapter(
multiview,
- (IViewClassifier, request_iface, context),
+ (classifier, request_iface, context),
IMultiView, name=name)
- if isexc:
- self.registry.registerAdapter(
- multiview,
- (IExceptionViewClassifier, request_iface, context),
- IMultiView, name=name)
-
- self.registry._clear_view_lookup_cache()
- renderer_type = getattr(renderer, 'type', None) # gard against None
- intrspc = self.introspector
- if (
- renderer_type is not None and
- tmpl_intr is not None and
- intrspc is not None and
- intrspc.get('renderer factories', renderer_type) is not None
- ):
- # allow failure of registered template factories to be deferred
- # until view execution, like other bad renderer factories; if
- # we tried to relate this to an existing renderer factory
- # without checking if it the factory actually existed, we'd end
- # up with a KeyError at startup time, which is inconsistent
- # with how other bad renderer registrations behave (they throw
- # a ValueError at view execution time)
- tmpl_intr.relate('renderer factories', renderer.type)
if mapper:
mapper_intr = self.introspectable(
@@ -1334,7 +1363,8 @@ class ViewsConfiguratorMixin(object):
viewname=None, accept=None, order=MAX_ORDER,
phash=DEFAULT_PHASH, decorator=None, route_name=None,
mapper=None, http_cache=None, context=None,
- require_csrf=None, extra_options=None):
+ require_csrf=None, exception_only=False,
+ extra_options=None):
view = self.maybe_dotted(view)
mapper = self.maybe_dotted(mapper)
if isinstance(renderer, string_types):
@@ -1372,6 +1402,7 @@ class ViewsConfiguratorMixin(object):
registry=self.registry,
package=self.package,
predicates=predicates,
+ exception_only=exception_only,
options=options,
)
@@ -1426,21 +1457,25 @@ class ViewsConfiguratorMixin(object):
argument restricts the set of circumstances under which this forbidden
view will be invoked. Unlike
:meth:`pyramid.config.Configurator.add_view`, this method will raise
- an exception if passed ``name``, ``permission``, ``context``,
- ``for_``, or ``http_cache`` keyword arguments. These argument values
- make no sense in the context of a forbidden view.
+ an exception if passed ``name``, ``permission``, ``require_csrf``,
+ ``context``, ``for_``, or ``exception_only`` keyword arguments. These
+ argument values make no sense in the context of a forbidden
+ :term:`exception view`.
.. versionadded:: 1.3
+
+ .. versionchanged:: 1.8
+
+ The view is created using ``exception_only=True``.
"""
for arg in (
- 'name', 'permission', 'context', 'for_', 'http_cache',
- 'require_csrf',
+ 'name', 'permission', 'context', 'for_', 'require_csrf',
+ 'exception_only',
):
if arg in view_options:
raise ConfigurationError(
'%s may not be used as an argument to add_forbidden_view'
- % arg
- )
+ % (arg,))
if view is None:
view = default_exceptionresponse_view
@@ -1448,6 +1483,7 @@ class ViewsConfiguratorMixin(object):
settings = dict(
view=view,
context=HTTPForbidden,
+ exception_only=True,
wrapper=wrapper,
request_type=request_type,
request_method=request_method,
@@ -1496,9 +1532,9 @@ class ViewsConfiguratorMixin(object):
append_slash=False,
**view_options
):
- """ Add a default Not Found View to the current configuration state.
- The view will be called when Pyramid or application code raises an
- :exc:`pyramid.httpexceptions.HTTPNotFound` exception (e.g. when a
+ """ Add a default :term:`Not Found View` to the current configuration
+ state. The view will be called when Pyramid or application code raises
+ an :exc:`pyramid.httpexceptions.HTTPNotFound` exception (e.g., when a
view cannot be found for the request). The simplest example is:
.. code-block:: python
@@ -1516,9 +1552,9 @@ class ViewsConfiguratorMixin(object):
argument restricts the set of circumstances under which this notfound
view will be invoked. Unlike
:meth:`pyramid.config.Configurator.add_view`, this method will raise
- an exception if passed ``name``, ``permission``, ``context``,
- ``for_``, or ``http_cache`` keyword arguments. These argument values
- make no sense in the context of a Not Found View.
+ an exception if passed ``name``, ``permission``, ``require_csrf``,
+ ``context``, ``for_``, or ``exception_only`` keyword arguments. These
+ argument values make no sense in the context of a Not Found View.
If ``append_slash`` is ``True``, when this Not Found View is invoked,
and the current path info does not end in a slash, the notfound logic
@@ -1545,18 +1581,26 @@ class ViewsConfiguratorMixin(object):
being used, :class:`~pyramid.httpexceptions.HTTPMovedPermanently will
be used` for the redirect response if a slash-appended route is found.
- .. versionchanged:: 1.6
.. versionadded:: 1.3
+
+ .. versionchanged:: 1.6
+
+ The ``append_slash`` argument was modified to allow any object that
+ implements the ``IResponse`` interface to specify the response class
+ used when a redirect is performed.
+
+ .. versionchanged:: 1.8
+
+ The view is created using ``exception_only=True``.
"""
for arg in (
- 'name', 'permission', 'context', 'for_', 'http_cache',
- 'require_csrf',
+ 'name', 'permission', 'context', 'for_', 'require_csrf',
+ 'exception_only',
):
if arg in view_options:
raise ConfigurationError(
'%s may not be used as an argument to add_notfound_view'
- % arg
- )
+ % (arg,))
if view is None:
view = default_exceptionresponse_view
@@ -1564,6 +1608,7 @@ class ViewsConfiguratorMixin(object):
settings = dict(
view=view,
context=HTTPNotFound,
+ exception_only=True,
wrapper=wrapper,
request_type=request_type,
request_method=request_method,
@@ -1598,6 +1643,47 @@ class ViewsConfiguratorMixin(object):
set_notfound_view = add_notfound_view # deprecated sorta-bw-compat alias
+ @viewdefaults
+ @action_method
+ def add_exception_view(
+ self,
+ view=None,
+ context=None,
+ # force all other arguments to be specified as key=value
+ **view_options
+ ):
+ """ Add an :term:`exception view` for the specified ``exception`` to
+ the current configuration state. The view will be called when Pyramid
+ or application code raises the given exception.
+
+ This method accepts almost all of the same arguments as
+ :meth:`pyramid.config.Configurator.add_view` except for ``name``,
+ ``permission``, ``for_``, ``require_csrf``, and ``exception_only``.
+
+ By default, this method will set ``context=Exception``, thus
+ registering for most default Python exceptions. Any subclass of
+ ``Exception`` may be specified.
+
+ .. versionadded:: 1.8
+ """
+ for arg in (
+ 'name', 'for_', 'exception_only', 'require_csrf', 'permission',
+ ):
+ if arg in view_options:
+ raise ConfigurationError(
+ '%s may not be used as an argument to add_exception_view'
+ % (arg,))
+ if context is None:
+ context = Exception
+ view_options.update(dict(
+ view=view,
+ context=context,
+ exception_only=True,
+ permission=NO_PERMISSION_REQUIRED,
+ require_csrf=False,
+ ))
+ return self.add_view(**view_options)
+
@action_method
def set_view_mapper(self, mapper):
"""
@@ -1777,14 +1863,63 @@ def isexception(o):
(inspect.isclass(o) and (issubclass(o, Exception)))
)
+def runtime_exc_view(view, excview):
+ # create a view callable which can pretend to be both a normal view
+ # and an exception view, dispatching to the appropriate one based
+ # on the state of request.exception
+ def wrapper_view(context, request):
+ if getattr(request, 'exception', None):
+ return excview(context, request)
+ return view(context, request)
+
+ # these constants are the same between the two views
+ wrapper_view.__wraps__ = wrapper_view
+ wrapper_view.__original_view__ = getattr(view, '__original_view__', view)
+ wrapper_view.__module__ = view.__module__
+ wrapper_view.__doc__ = view.__doc__
+ wrapper_view.__name__ = view.__name__
+
+ wrapper_view.__accept__ = getattr(view, '__accept__', None)
+ wrapper_view.__order__ = getattr(view, '__order__', MAX_ORDER)
+ wrapper_view.__phash__ = getattr(view, '__phash__', DEFAULT_PHASH)
+ wrapper_view.__view_attr__ = getattr(view, '__view_attr__', None)
+ wrapper_view.__permission__ = getattr(view, '__permission__', None)
+
+ def wrap_fn(attr):
+ def wrapper(context, request):
+ if getattr(request, 'exception', None):
+ selected_view = excview
+ else:
+ selected_view = view
+ fn = getattr(selected_view, attr, None)
+ if fn is not None:
+ return fn(context, request)
+ return wrapper
+
+ # these methods are dynamic per-request and should dispatch to their
+ # respective views based on whether it's an exception or not
+ wrapper_view.__call_permissive__ = wrap_fn('__call_permissive__')
+ wrapper_view.__permitted__ = wrap_fn('__permitted__')
+ wrapper_view.__predicated__ = wrap_fn('__predicated__')
+ wrapper_view.__predicates__ = wrap_fn('__predicates__')
+ return wrapper_view
+
@implementer(IViewDeriverInfo)
class ViewDeriverInfo(object):
- def __init__(self, view, registry, package, predicates, options):
+ def __init__(self,
+ view,
+ registry,
+ package,
+ predicates,
+ exception_only,
+ options,
+ ):
self.original_view = view
self.registry = registry
self.package = package
self.predicates = predicates or []
self.options = options or {}
+ self.exception_only = exception_only
@reify
def settings(self):
diff --git a/pyramid/decorator.py b/pyramid/decorator.py
index ea518bfcb..065a3feed 100644
--- a/pyramid/decorator.py
+++ b/pyramid/decorator.py
@@ -6,21 +6,18 @@ class reify(object):
Python ``@property`` decorator, but it puts the result of the method it
decorates into the instance dict after the first call, effectively
replacing the function it decorates with an instance variable. It is, in
- Python parlance, a non-data descriptor. An example:
+ Python parlance, a non-data descriptor. The following is an example and
+ its usage:
- .. testsetup::
-
- from pyramid.decorator import reify
-
- class Foo(object):
- @reify
- def jammy(self):
- print('jammy called')
- return 1
+ .. doctest::
- And usage of Foo:
+ >>> from pyramid.decorator import reify
- .. doctest::
+ >>> class Foo(object):
+ ... @reify
+ ... def jammy(self):
+ ... print('jammy called')
+ ... return 1
>>> f = Foo()
>>> v = f.jammy
diff --git a/pyramid/exceptions.py b/pyramid/exceptions.py
index a8a10f927..c95922eb0 100644
--- a/pyramid/exceptions.py
+++ b/pyramid/exceptions.py
@@ -109,6 +109,7 @@ class ConfigurationExecutionError(ConfigurationError):
def __str__(self):
return "%s: %s\n in:\n %s" % (self.etype, self.evalue, self.info)
+
class CyclicDependencyError(Exception):
""" The exception raised when the Pyramid topological sorter detects a
cyclic dependency."""
diff --git a/pyramid/httpexceptions.py b/pyramid/httpexceptions.py
index e76f43c8a..054917dfa 100644
--- a/pyramid/httpexceptions.py
+++ b/pyramid/httpexceptions.py
@@ -98,7 +98,10 @@ be forwarded to its :class:`~pyramid.response.Response` superclass:
a plain-text override of the default ``detail``
``headers``
- a list of (k,v) header pairs
+ a list of (k,v) header pairs, or a dict, to be added to the
+ response; use the content_type='application/json' kwarg and other
+ similar kwargs to to change properties of the response supported by the
+ :class:`pyramid.response.Response` superclass
``comment``
a plain-text additional information which is
diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py
index b252d0f4a..c1ddea63f 100644
--- a/pyramid/interfaces.py
+++ b/pyramid/interfaces.py
@@ -925,6 +925,7 @@ class IDefaultCSRFOptions(Interface):
token = Attribute('The key to be matched in the body of the request.')
header = Attribute('The header to be matched with the CSRF token.')
safe_methods = Attribute('A set of safe methods that skip CSRF checks.')
+ callback = Attribute('A callback to disable CSRF checks per-request.')
class ISessionFactory(Interface):
""" An interface representing a factory which accepts a request object and
@@ -1234,6 +1235,7 @@ class IViewDeriverInfo(Interface):
'default values that were not overriden')
predicates = Attribute('The list of predicates active on the view')
original_view = Attribute('The original view object being wrapped')
+ exception_only = Attribute('The view will only be invoked for exceptions')
class IViewDerivers(Interface):
""" Interface for view derivers list """
diff --git a/pyramid/paster.py b/pyramid/paster.py
index 3916be8f0..5429a7860 100644
--- a/pyramid/paster.py
+++ b/pyramid/paster.py
@@ -5,9 +5,8 @@ from paste.deploy import (
appconfig,
)
-from pyramid.compat import configparser
-from logging.config import fileConfig
from pyramid.scripting import prepare
+from pyramid.scripts.common import setup_logging # noqa, api
def get_app(config_uri, name=None, options=None, loadapp=loadapp):
""" Return the WSGI application named ``name`` in the PasteDeploy
@@ -52,30 +51,6 @@ def get_appsettings(config_uri, name=None, options=None, appconfig=appconfig):
relative_to=here_dir,
global_conf=options)
-def setup_logging(config_uri, global_conf=None,
- fileConfig=fileConfig,
- configparser=configparser):
- """
- Set up logging via :func:`logging.config.fileConfig` with the filename
- specified via ``config_uri`` (a string in the form
- ``filename#sectionname``).
-
- ConfigParser defaults are specified for the special ``__file__``
- and ``here`` variables, similar to PasteDeploy config loading.
- Extra defaults can optionally be specified as a dict in ``global_conf``.
- """
- path, _ = _getpathsec(config_uri, None)
- parser = configparser.ConfigParser()
- parser.read([path])
- if parser.has_section('loggers'):
- config_file = os.path.abspath(path)
- full_global_conf = dict(
- __file__=config_file,
- here=os.path.dirname(config_file))
- if global_conf:
- full_global_conf.update(global_conf)
- return fileConfig(config_file, full_global_conf)
-
def _getpathsec(config_uri, name):
if '#' in config_uri:
path, section = config_uri.split('#', 1)
@@ -129,8 +104,22 @@ def bootstrap(config_uri, request=None, options=None):
{'http_port': 8080} and then use %(http_port)s in the
config file.
+ This function may be used as a context manager to call the ``closer``
+ automatically:
+
+ .. code-block:: python
+
+ with bootstrap('development.ini') as env:
+ request = env['request']
+ # ...
+
See :ref:`writing_a_script` for more information about how to use this
function.
+
+ .. versionchanged:: 1.8
+
+ Added the ability to use the return value as a context manager.
+
"""
app = get_app(config_uri, options=options)
env = prepare(request)
diff --git a/pyramid/renderers.py b/pyramid/renderers.py
index 9b3f19510..47705d5d9 100644
--- a/pyramid/renderers.py
+++ b/pyramid/renderers.py
@@ -194,7 +194,7 @@ class JSON(object):
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
diff --git a/pyramid/router.py b/pyramid/router.py
index 19773cf62..fd11925e9 100644
--- a/pyramid/router.py
+++ b/pyramid/router.py
@@ -34,8 +34,6 @@ from pyramid.traversal import (
ResourceTreeTraverser,
)
-from pyramid.tweens import excview_tween_factory
-
@implementer(IRouter)
class Router(object):
@@ -51,11 +49,10 @@ class Router(object):
self.routes_mapper = q(IRoutesMapper)
self.request_factory = q(IRequestFactory, default=Request)
self.request_extensions = q(IRequestExtensions)
- tweens = q(ITweens)
- if tweens is None:
- tweens = excview_tween_factory
self.orig_handle_request = self.handle_request
- self.handle_request = tweens(self.handle_request, registry)
+ tweens = q(ITweens)
+ if tweens is not None:
+ self.handle_request = tweens(self.handle_request, registry)
self.root_policy = self.root_factory # b/w compat
self.registry = registry
settings = registry.settings
diff --git a/pyramid/scaffolds/__init__.py b/pyramid/scaffolds/__init__.py
index 62c3eeecc..841dc403e 100644
--- a/pyramid/scaffolds/__init__.py
+++ b/pyramid/scaffolds/__init__.py
@@ -4,7 +4,7 @@ from textwrap import dedent
from pyramid.compat import native_
-from pyramid.scaffolds.template import Template # API
+from pyramid.scaffolds.template import Template # API
class PyramidTemplate(Template):
"""
@@ -60,5 +60,6 @@ class ZODBProjectTemplate(PyramidTemplate):
class AlchemyProjectTemplate(PyramidTemplate):
_template_dir = 'alchemy'
- summary = 'Pyramid project using SQLAlchemy, SQLite, URL dispatch, and'
- ' Chameleon'
+ summary = (
+ 'Pyramid project using SQLAlchemy, SQLite, URL dispatch, and '
+ 'Jinja2')
diff --git a/pyramid/scaffolds/alchemy/+package+/models/__init__.py_tmpl b/pyramid/scaffolds/alchemy/+package+/models/__init__.py_tmpl
index 26b50aaf6..f626d1ef0 100644
--- a/pyramid/scaffolds/alchemy/+package+/models/__init__.py_tmpl
+++ b/pyramid/scaffolds/alchemy/+package+/models/__init__.py_tmpl
@@ -5,7 +5,7 @@ import zope.sqlalchemy
# import or define all models here to ensure they are attached to the
# Base.metadata prior to any initialization routines
-from .mymodel import MyModel # flake8: noqa
+from .mymodel import MyModel # noqa
# run configure_mappers after defining all of the models to ensure
# all relationships can be setup
diff --git a/pyramid/scaffolds/alchemy/+package+/models/meta.py b/pyramid/scaffolds/alchemy/+package+/models/meta.py
index fc3e8f1dd..0682247b5 100644
--- a/pyramid/scaffolds/alchemy/+package+/models/meta.py
+++ b/pyramid/scaffolds/alchemy/+package+/models/meta.py
@@ -3,7 +3,7 @@ from sqlalchemy.schema import MetaData
# Recommended naming convention used by Alembic, as various different database
# providers will autogenerate vastly different names making migrations more
-# difficult. See: http://alembic.readthedocs.org/en/latest/naming.html
+# difficult. See: http://alembic.zzzcomputing.com/en/latest/naming.html
NAMING_CONVENTION = {
"ix": 'ix_%(column_0_label)s',
"uq": "uq_%(table_name)s_%(column_0_name)s",
diff --git a/pyramid/scripting.py b/pyramid/scripting.py
index d9587338f..7607d3ea3 100644
--- a/pyramid/scripting.py
+++ b/pyramid/scripting.py
@@ -56,12 +56,25 @@ def prepare(request=None, registry=None):
``root`` returned is the application's root resource object. The
``closer`` returned is a callable (accepting no arguments) that
should be called when your scripting application is finished
- using the root. ``registry`` is the registry object passed or
- the last registry loaded into
- :attr:`pyramid.config.global_registries` if no registry is passed.
+ using the root. ``registry`` is the resolved registry object.
``request`` is the request object passed or the constructed request
if no request is passed. ``root_factory`` is the root factory used
to construct the root.
+
+ This function may be used as a context manager to call the ``closer``
+ automatically:
+
+ .. code-block:: python
+
+ registry = config.registry
+ with prepare(registry) as env:
+ request = env['request']
+ # ...
+
+ .. versionchanged:: 1.8
+
+ Added the ability to use the return value as a context manager.
+
"""
if registry is None:
registry = getattr(request, 'registry', global_registries.last)
@@ -85,8 +98,20 @@ def prepare(request=None, registry=None):
root = root_factory(request)
if getattr(request, 'context', None) is None:
request.context = root
- return {'root':root, 'closer':closer, 'registry':registry,
- 'request':request, 'root_factory':root_factory}
+ return AppEnvironment(
+ root=root,
+ closer=closer,
+ registry=registry,
+ request=request,
+ root_factory=root_factory,
+ )
+
+class AppEnvironment(dict):
+ def __enter__(self):
+ return self
+
+ def __exit__(self, type, value, traceback):
+ self['closer']()
def _make_request(path, registry=None):
""" Return a :meth:`pyramid.request.Request` object anchored at a
diff --git a/pyramid/scripts/common.py b/pyramid/scripts/common.py
index cbc172e9b..fc141f6e2 100644
--- a/pyramid/scripts/common.py
+++ b/pyramid/scripts/common.py
@@ -17,20 +17,26 @@ def parse_vars(args):
result[name] = value
return result
-def logging_file_config(config_file, fileConfig=fileConfig,
- configparser=configparser):
+def setup_logging(config_uri, global_conf=None,
+ fileConfig=fileConfig,
+ configparser=configparser):
"""
- Setup logging via the logging module's fileConfig function with the
- specified ``config_file``, if applicable.
+ Set up logging via :func:`logging.config.fileConfig` with the filename
+ specified via ``config_uri`` (a string in the form
+ ``filename#sectionname``).
ConfigParser defaults are specified for the special ``__file__``
and ``here`` variables, similar to PasteDeploy config loading.
+ Extra defaults can optionally be specified as a dict in ``global_conf``.
"""
+ path = config_uri.split('#', 1)[0]
parser = configparser.ConfigParser()
- parser.read([config_file])
+ parser.read([path])
if parser.has_section('loggers'):
- config_file = os.path.abspath(config_file)
- return fileConfig(
- config_file,
- dict(__file__=config_file, here=os.path.dirname(config_file))
- )
+ config_file = os.path.abspath(path)
+ full_global_conf = dict(
+ __file__=config_file,
+ here=os.path.dirname(config_file))
+ if global_conf:
+ full_global_conf.update(global_conf)
+ return fileConfig(config_file, full_global_conf)
diff --git a/pyramid/scripts/pcreate.py b/pyramid/scripts/pcreate.py
index 1e8074fc5..a954d3be6 100644
--- a/pyramid/scripts/pcreate.py
+++ b/pyramid/scripts/pcreate.py
@@ -45,6 +45,14 @@ class PCreateCommand(object):
action='store_true',
help=("A backwards compatibility alias for -l/--list. "
"List all available scaffold names."))
+ parser.add_option('--package-name',
+ dest='package_name',
+ action='store',
+ type='string',
+ help='Package name to use. The name provided is assumed '
+ 'to be a valid Python package name, and will not '
+ 'be validated. By default the package name is '
+ 'derived from the value of output_directory.')
parser.add_option('--simulate',
dest='simulate',
action='store_true',
@@ -56,7 +64,9 @@ class PCreateCommand(object):
parser.add_option('--interactive',
dest='interactive',
action='store_true',
- help='When a file would be overwritten, interrogate')
+ help='When a file would be overwritten, interrogate '
+ '(this is the default, but you may specify it to '
+ 'override --overwrite)')
parser.add_option('--ignore-conflicting-name',
dest='force_bad_name',
action='store_true',
@@ -70,6 +80,8 @@ class PCreateCommand(object):
def __init__(self, argv, quiet=False):
self.quiet = quiet
self.options, self.args = self.parser.parse_args(argv[1:])
+ if not self.options.interactive and not self.options.overwrite:
+ self.options.interactive = True
self.scaffolds = self.all_scaffolds()
def run(self):
@@ -95,9 +107,13 @@ class PCreateCommand(object):
def project_vars(self):
output_dir = self.output_path
project_name = os.path.basename(os.path.split(output_dir)[1])
- pkg_name = _bad_chars_re.sub(
- '', project_name.lower().replace('-', '_'))
- safe_name = pkg_resources.safe_name(project_name)
+ if self.options.package_name is None:
+ pkg_name = _bad_chars_re.sub(
+ '', project_name.lower().replace('-', '_'))
+ safe_name = pkg_resources.safe_name(project_name)
+ else:
+ pkg_name = self.options.package_name
+ safe_name = pkg_name
egg_name = pkg_resources.to_filename(safe_name)
# get pyramid package version
diff --git a/pyramid/scripts/prequest.py b/pyramid/scripts/prequest.py
index e07f9d10e..14a132bdb 100644
--- a/pyramid/scripts/prequest.py
+++ b/pyramid/scripts/prequest.py
@@ -5,8 +5,9 @@ import textwrap
from pyramid.compat import url_unquote
from pyramid.request import Request
-from pyramid.paster import get_app, setup_logging
+from pyramid.paster import get_app
from pyramid.scripts.common import parse_vars
+from pyramid.scripts.common import setup_logging
def main(argv=sys.argv, quiet=False):
command = PRequestCommand(argv, quiet)
diff --git a/pyramid/scripts/proutes.py b/pyramid/scripts/proutes.py
index a389c303c..f75810c06 100644
--- a/pyramid/scripts/proutes.py
+++ b/pyramid/scripts/proutes.py
@@ -184,8 +184,15 @@ def get_route_data(route, registry):
request_method = view.get('request_methods')
if request_method is not None:
- view_callable = view['callable']
- view_module = _get_view_module(view_callable)
+ if view.get('attr') is not None:
+ view_callable = getattr(view['callable'], view['attr'])
+ view_module = '%s.%s' % (
+ _get_view_module(view['callable']),
+ view['attr']
+ )
+ else:
+ view_callable = view['callable']
+ view_module = _get_view_module(view_callable)
if view_module not in view_request_methods:
view_request_methods[view_module] = []
@@ -289,7 +296,7 @@ class PRoutesCommand(object):
items = config.items('proutes')
for k, v in items:
if 'format' == k:
- cols = re.split(r'[,|\s|\n]*', v)
+ cols = re.split(r'[,|\s\n]+', v)
self.column_format = [x.strip() for x in cols]
except configparser.NoSectionError:
diff --git a/pyramid/scripts/pserve.py b/pyramid/scripts/pserve.py
index ec7f31704..0d22c9f3f 100644
--- a/pyramid/scripts/pserve.py
+++ b/pyramid/scripts/pserve.py
@@ -30,9 +30,8 @@ from paste.deploy.loadwsgi import loadcontext, SERVER
from pyramid.compat import PY2
from pyramid.compat import WIN
-from pyramid.paster import setup_logging
-
from pyramid.scripts.common import parse_vars
+from pyramid.scripts.common import setup_logging
MAXFD = 1024
diff --git a/pyramid/scripts/pshell.py b/pyramid/scripts/pshell.py
index 0a7cfbbe5..56b1a15fa 100644
--- a/pyramid/scripts/pshell.py
+++ b/pyramid/scripts/pshell.py
@@ -10,11 +10,10 @@ from pyramid.compat import exec_
from pyramid.util import DottedNameResolver
from pyramid.paster import bootstrap
-from pyramid.paster import setup_logging
-
from pyramid.settings import aslist
from pyramid.scripts.common import parse_vars
+from pyramid.scripts.common import setup_logging
def main(argv=sys.argv, quiet=False):
command = PShellCommand(argv, quiet)
diff --git a/pyramid/testing.py b/pyramid/testing.py
index ec06fe379..877b351db 100644
--- a/pyramid/testing.py
+++ b/pyramid/testing.py
@@ -478,6 +478,7 @@ def setUp(registry=None, request=None, hook_zca=True, autocommit=True,
config.add_default_view_predicates()
config.add_default_view_derivers()
config.add_default_route_predicates()
+ config.add_default_tweens()
config.commit()
global have_zca
try:
diff --git a/pyramid/tests/test_authentication.py b/pyramid/tests/test_authentication.py
index 0a22e5965..b9a4c6be4 100644
--- a/pyramid/tests/test_authentication.py
+++ b/pyramid/tests/test_authentication.py
@@ -1089,7 +1089,10 @@ class TestAuthTktCookieHelper(unittest.TestCase):
helper = self._makeOne('secret')
request = self._makeRequest()
userid = object()
- result = helper.remember(request, userid)
+ with warnings.catch_warnings(record=True) as w:
+ warnings.simplefilter('always', RuntimeWarning)
+ result = helper.remember(request, userid)
+ self.assertTrue(str(w[-1].message).startswith('userid is of type'))
values = self._parseHeaders(result)
self.assertEqual(len(result), 3)
value = values[0]
@@ -1476,6 +1479,79 @@ class TestBasicAuthAuthenticationPolicy(unittest.TestCase):
self.assertEqual(policy.forget(None), [
('WWW-Authenticate', 'Basic realm="SomeRealm"')])
+
+class TestExtractHTTPBasicCredentials(unittest.TestCase):
+ def _get_func(self):
+ from pyramid.authentication import extract_http_basic_credentials
+ return extract_http_basic_credentials
+
+ def test_no_auth_header(self):
+ request = testing.DummyRequest()
+ fn = self._get_func()
+
+ self.assertIsNone(fn(request))
+
+ def test_invalid_payload(self):
+ import base64
+ request = testing.DummyRequest()
+ request.headers['Authorization'] = 'Basic %s' % base64.b64encode(
+ bytes_('chrisrpassword')).decode('ascii')
+ fn = self._get_func()
+ self.assertIsNone(fn(request))
+
+ def test_not_a_basic_auth_scheme(self):
+ import base64
+ request = testing.DummyRequest()
+ request.headers['Authorization'] = 'OtherScheme %s' % base64.b64encode(
+ bytes_('chrisr:password')).decode('ascii')
+ fn = self._get_func()
+ self.assertIsNone(fn(request))
+
+ def test_no_base64_encoding(self):
+ request = testing.DummyRequest()
+ request.headers['Authorization'] = 'Basic ...'
+ fn = self._get_func()
+ self.assertIsNone(fn(request))
+
+ def test_latin1_payload(self):
+ import base64
+ request = testing.DummyRequest()
+ inputs = (b'm\xc3\xb6rk\xc3\xb6:'
+ b'm\xc3\xb6rk\xc3\xb6password').decode('utf-8')
+ request.headers['Authorization'] = 'Basic %s' % (
+ base64.b64encode(inputs.encode('latin-1')).decode('latin-1'))
+ fn = self._get_func()
+ self.assertEqual(fn(request), (
+ b'm\xc3\xb6rk\xc3\xb6'.decode('utf-8'),
+ b'm\xc3\xb6rk\xc3\xb6password'.decode('utf-8')
+ ))
+
+ def test_utf8_payload(self):
+ import base64
+ request = testing.DummyRequest()
+ inputs = (b'm\xc3\xb6rk\xc3\xb6:'
+ b'm\xc3\xb6rk\xc3\xb6password').decode('utf-8')
+ request.headers['Authorization'] = 'Basic %s' % (
+ base64.b64encode(inputs.encode('utf-8')).decode('latin-1'))
+ fn = self._get_func()
+ self.assertEqual(fn(request), (
+ b'm\xc3\xb6rk\xc3\xb6'.decode('utf-8'),
+ b'm\xc3\xb6rk\xc3\xb6password'.decode('utf-8')
+ ))
+
+ def test_namedtuple_return(self):
+ import base64
+ request = testing.DummyRequest()
+ request.headers['Authorization'] = 'Basic %s' % base64.b64encode(
+ bytes_('chrisr:pass')).decode('ascii')
+ fn = self._get_func()
+ result = fn(request)
+
+ self.assertEqual(result.username, 'chrisr')
+ self.assertEqual(result.password, 'pass')
+
+
+
class TestSimpleSerializer(unittest.TestCase):
def _makeOne(self):
from pyramid.authentication import _SimpleSerializer
diff --git a/pyramid/tests/test_config/test_init.py b/pyramid/tests/test_config/test_init.py
index de199d079..7078d7e26 100644
--- a/pyramid/tests/test_config/test_init.py
+++ b/pyramid/tests/test_config/test_init.py
@@ -1545,6 +1545,31 @@ class TestActionState(unittest.TestCase):
c.execute_actions()
self.assertEqual(output, [('f', (1,), {}), ('g', (8,), {})])
+ def test_reentrant_action_with_deferred_discriminator(self):
+ # see https://github.com/Pylons/pyramid/issues/2697
+ from pyramid.registry import Deferred
+ output = []
+ c = self._makeOne()
+ def f(*a, **k):
+ output.append(('f', a, k))
+ c.actions.append((4, g, (4,), {}, (), None, 2))
+ def g(*a, **k):
+ output.append(('g', a, k))
+ def h(*a, **k):
+ output.append(('h', a, k))
+ def discrim():
+ self.assertEqual(output, [('f', (1,), {}), ('g', (2,), {})])
+ return 3
+ d = Deferred(discrim)
+ c.actions = [
+ (d, h, (3,), {}, (), None, 1), # order 1
+ (1, f, (1,)), # order 0
+ (2, g, (2,)), # order 0
+ ]
+ c.execute_actions()
+ self.assertEqual(output, [
+ ('f', (1,), {}), ('g', (2,), {}), ('h', (3,), {}), ('g', (4,), {})])
+
def test_reentrant_action_error(self):
from pyramid.exceptions import ConfigurationError
c = self._makeOne()
@@ -1570,6 +1595,28 @@ class TestActionState(unittest.TestCase):
(3, g, (8,)),
])
+ def test_executing_conflicting_action_across_orders(self):
+ from pyramid.exceptions import ConfigurationConflictError
+ c = self._makeOne()
+ def f(*a, **k): pass
+ def g(*a, **k): pass
+ c.actions = [
+ (1, f, (1,), {}, (), None, -1),
+ (1, g, (2,)),
+ ]
+ self.assertRaises(ConfigurationConflictError, c.execute_actions)
+
+ def test_executing_conflicting_action_across_reentrant_orders(self):
+ from pyramid.exceptions import ConfigurationConflictError
+ c = self._makeOne()
+ def f(*a, **k):
+ c.actions.append((1, g, (8,)))
+ def g(*a, **k): pass
+ c.actions = [
+ (1, f, (1,), {}, (), None, -1),
+ ]
+ self.assertRaises(ConfigurationConflictError, c.execute_actions)
+
class Test_reentrant_action_functional(unittest.TestCase):
def _makeConfigurator(self, *arg, **kw):
from pyramid.config import Configurator
@@ -1597,6 +1644,21 @@ class Test_reentrant_action_functional(unittest.TestCase):
self.assertEqual(route.name, 'foo')
self.assertEqual(route.path, '/foo')
+ def test_deferred_discriminator(self):
+ # see https://github.com/Pylons/pyramid/issues/2697
+ from pyramid.config import PHASE0_CONFIG
+ config = self._makeConfigurator()
+ def deriver(view, info): return view
+ deriver.options = ('foo',)
+ config.add_view_deriver(deriver, 'foo_view')
+ # add_view uses a deferred discriminator and will fail if executed
+ # prior to add_view_deriver executing its action
+ config.add_view(lambda r: r.response, name='', foo=1)
+ def dummy_action():
+ # trigger a re-entrant action
+ config.action(None, lambda: None)
+ config.action(None, dummy_action, order=PHASE0_CONFIG)
+ config.commit()
class Test_resolveConflicts(unittest.TestCase):
def _callFUT(self, actions):
@@ -1666,15 +1728,14 @@ class Test_resolveConflicts(unittest.TestCase):
def test_it_success_dicts(self):
from pyramid.tests.test_config import dummyfactory as f
- from pyramid.config import expand_action
result = self._callFUT([
- expand_action(None, f),
- expand_action(1, f, (1,), {}, (), 'first'),
- expand_action(1, f, (2,), {}, ('x',), 'second'),
- expand_action(1, f, (3,), {}, ('y',), 'third'),
- expand_action(4, f, (4,), {}, ('y',), 'should be last', 99999),
- expand_action(3, f, (3,), {}, ('y',)),
- expand_action(None, f, (5,), {}, ('y',)),
+ (None, f),
+ (1, f, (1,), {}, (), 'first'),
+ (1, f, (2,), {}, ('x',), 'second'),
+ (1, f, (3,), {}, ('y',), 'third'),
+ (4, f, (4,), {}, ('y',), 'should be last', 99999),
+ (3, f, (3,), {}, ('y',)),
+ (None, f, (5,), {}, ('y',)),
])
result = list(result)
self.assertEqual(
@@ -1740,17 +1801,16 @@ class Test_resolveConflicts(unittest.TestCase):
def test_it_with_actions_grouped_by_order(self):
from pyramid.tests.test_config import dummyfactory as f
- from pyramid.config import expand_action
result = self._callFUT([
- expand_action(None, f), # X
- expand_action(1, f, (1,), {}, (), 'third', 10), # X
- expand_action(1, f, (2,), {}, ('x',), 'fourth', 10),
- expand_action(1, f, (3,), {}, ('y',), 'fifth', 10),
- expand_action(2, f, (1,), {}, (), 'sixth', 10), # X
- expand_action(3, f, (1,), {}, (), 'seventh', 10), # X
- expand_action(5, f, (4,), {}, ('y',), 'eighth', 99999), # X
- expand_action(4, f, (3,), {}, (), 'first', 5), # X
- expand_action(4, f, (5,), {}, ('y',), 'second', 5),
+ (None, f), # X
+ (1, f, (1,), {}, (), 'third', 10), # X
+ (1, f, (2,), {}, ('x',), 'fourth', 10),
+ (1, f, (3,), {}, ('y',), 'fifth', 10),
+ (2, f, (1,), {}, (), 'sixth', 10), # X
+ (3, f, (1,), {}, (), 'seventh', 10), # X
+ (5, f, (4,), {}, ('y',), 'eighth', 99999), # X
+ (4, f, (3,), {}, (), 'first', 5), # X
+ (4, f, (5,), {}, ('y',), 'second', 5),
])
result = list(result)
self.assertEqual(len(result), 6)
@@ -1812,7 +1872,32 @@ class Test_resolveConflicts(unittest.TestCase):
'order': 99999}
]
)
-
+
+ def test_override_success_across_orders(self):
+ from pyramid.tests.test_config import dummyfactory as f
+ result = self._callFUT([
+ (1, f, (2,), {}, ('x',), 'eek', 0),
+ (1, f, (3,), {}, ('x', 'y'), 'ack', 10),
+ ])
+ result = list(result)
+ self.assertEqual(result, [
+ {'info': 'eek',
+ 'args': (2,),
+ 'callable': f,
+ 'introspectables': (),
+ 'kw': {},
+ 'discriminator': 1,
+ 'includepath': ('x',),
+ 'order': 0},
+ ])
+
+ def test_conflicts_across_orders(self):
+ from pyramid.tests.test_config import dummyfactory as f
+ result = self._callFUT([
+ (1, f, (2,), {}, ('x', 'y'), 'eek', 0),
+ (1, f, (3,), {}, ('x'), 'ack', 10),
+ ])
+ self.assertRaises(ConfigurationConflictError, list, result)
class TestGlobalRegistriesIntegration(unittest.TestCase):
def setUp(self):
diff --git a/pyramid/tests/test_config/test_security.py b/pyramid/tests/test_config/test_security.py
index e461bfd4a..5db8e21fc 100644
--- a/pyramid/tests/test_config/test_security.py
+++ b/pyramid/tests/test_config/test_security.py
@@ -108,14 +108,18 @@ class ConfiguratorSecurityMethodsTests(unittest.TestCase):
self.assertEqual(result.header, 'X-CSRF-Token')
self.assertEqual(list(sorted(result.safe_methods)),
['GET', 'HEAD', 'OPTIONS', 'TRACE'])
+ self.assertTrue(result.callback is None)
def test_changing_set_default_csrf_options(self):
from pyramid.interfaces import IDefaultCSRFOptions
config = self._makeOne(autocommit=True)
+ def callback(request): return True
config.set_default_csrf_options(
- require_csrf=False, token='DUMMY', header=None, safe_methods=('PUT',))
+ require_csrf=False, token='DUMMY', header=None,
+ safe_methods=('PUT',), callback=callback)
result = config.registry.getUtility(IDefaultCSRFOptions)
self.assertEqual(result.require_csrf, False)
self.assertEqual(result.token, 'DUMMY')
self.assertEqual(result.header, None)
self.assertEqual(list(sorted(result.safe_methods)), ['PUT'])
+ self.assertTrue(result.callback is callback)
diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py
index 878574e88..f020485de 100644
--- a/pyramid/tests/test_config/test_views.py
+++ b/pyramid/tests/test_config/test_views.py
@@ -20,15 +20,16 @@ class TestViewsConfigurationMixin(unittest.TestCase):
config = Configurator(*arg, **kw)
return config
- def _getViewCallable(self, config, ctx_iface=None, request_iface=None,
- name='', exception_view=False):
+ def _getViewCallable(self, config, ctx_iface=None, exc_iface=None,
+ request_iface=None, name=''):
from zope.interface import Interface
from pyramid.interfaces import IRequest
from pyramid.interfaces import IView
from pyramid.interfaces import IViewClassifier
from pyramid.interfaces import IExceptionViewClassifier
- if exception_view:
+ if exc_iface:
classifier = IExceptionViewClassifier
+ ctx_iface = exc_iface
else:
classifier = IViewClassifier
if ctx_iface is None:
@@ -489,7 +490,7 @@ class TestViewsConfigurationMixin(unittest.TestCase):
config.add_view(view=newview, xhr=True, context=RuntimeError,
renderer=null_renderer)
wrapper = self._getViewCallable(
- config, ctx_iface=implementedBy(RuntimeError), exception_view=True)
+ config, exc_iface=implementedBy(RuntimeError))
self.assertFalse(IMultiView.providedBy(wrapper))
request = DummyRequest()
request.is_xhr = True
@@ -533,7 +534,7 @@ class TestViewsConfigurationMixin(unittest.TestCase):
config.add_view(view=newview, context=RuntimeError,
renderer=null_renderer)
wrapper = self._getViewCallable(
- config, ctx_iface=implementedBy(RuntimeError), exception_view=True)
+ config, exc_iface=implementedBy(RuntimeError))
self.assertFalse(IMultiView.providedBy(wrapper))
request = DummyRequest()
request.is_xhr = True
@@ -581,7 +582,7 @@ class TestViewsConfigurationMixin(unittest.TestCase):
config.add_view(view=newview, context=RuntimeError,
renderer=null_renderer)
wrapper = self._getViewCallable(
- config, ctx_iface=implementedBy(RuntimeError), exception_view=True)
+ config, exc_iface=implementedBy(RuntimeError))
self.assertFalse(IMultiView.providedBy(wrapper))
request = DummyRequest()
request.is_xhr = True
@@ -626,7 +627,7 @@ class TestViewsConfigurationMixin(unittest.TestCase):
config.add_view(view=view, context=RuntimeError,
renderer=null_renderer)
wrapper = self._getViewCallable(
- config, ctx_iface=implementedBy(RuntimeError), exception_view=True)
+ config, exc_iface=implementedBy(RuntimeError))
self.assertTrue(IMultiView.providedBy(wrapper))
self.assertEqual(wrapper(None, None), 'OK')
@@ -669,7 +670,7 @@ class TestViewsConfigurationMixin(unittest.TestCase):
ISecuredView, name='')
config.add_view(view=view, context=RuntimeError, renderer=null_renderer)
wrapper = self._getViewCallable(
- config, ctx_iface=implementedBy(RuntimeError), exception_view=True)
+ config, exc_iface=implementedBy(RuntimeError))
self.assertTrue(IMultiView.providedBy(wrapper))
self.assertEqual(wrapper(None, None), 'OK')
@@ -755,7 +756,7 @@ class TestViewsConfigurationMixin(unittest.TestCase):
config.add_view(view=view2, accept='text/html', context=RuntimeError,
renderer=null_renderer)
wrapper = self._getViewCallable(
- config, ctx_iface=implementedBy(RuntimeError), exception_view=True)
+ config, exc_iface=implementedBy(RuntimeError))
self.assertTrue(IMultiView.providedBy(wrapper))
self.assertEqual(len(wrapper.views), 1)
self.assertEqual(len(wrapper.media_views), 1)
@@ -816,7 +817,7 @@ class TestViewsConfigurationMixin(unittest.TestCase):
config.add_view(view=view2, context=RuntimeError,
renderer=null_renderer)
wrapper = self._getViewCallable(
- config, ctx_iface=implementedBy(RuntimeError), exception_view=True)
+ config, exc_iface=implementedBy(RuntimeError))
self.assertTrue(IMultiView.providedBy(wrapper))
self.assertEqual(len(wrapper.views), 1)
self.assertEqual(len(wrapper.media_views), 1)
@@ -843,31 +844,71 @@ class TestViewsConfigurationMixin(unittest.TestCase):
self.assertEqual([x[:2] for x in wrapper.views], [(view2, None)])
self.assertEqual(wrapper(None, None), 'OK1')
- def test_add_view_exc_multiview_replaces_multiview(self):
+ def test_add_view_exc_multiview_replaces_multiviews(self):
from pyramid.renderers import null_renderer
from zope.interface import implementedBy
from pyramid.interfaces import IRequest
from pyramid.interfaces import IMultiView
from pyramid.interfaces import IViewClassifier
from pyramid.interfaces import IExceptionViewClassifier
- view = DummyMultiView()
+ hot_view = DummyMultiView()
+ exc_view = DummyMultiView()
config = self._makeOne(autocommit=True)
config.registry.registerAdapter(
- view,
+ hot_view,
(IViewClassifier, IRequest, implementedBy(RuntimeError)),
IMultiView, name='')
config.registry.registerAdapter(
- view,
+ exc_view,
(IExceptionViewClassifier, IRequest, implementedBy(RuntimeError)),
IMultiView, name='')
view2 = lambda *arg: 'OK2'
config.add_view(view=view2, context=RuntimeError,
renderer=null_renderer)
- wrapper = self._getViewCallable(
- config, ctx_iface=implementedBy(RuntimeError), exception_view=True)
- self.assertTrue(IMultiView.providedBy(wrapper))
- self.assertEqual([x[:2] for x in wrapper.views], [(view2, None)])
- self.assertEqual(wrapper(None, None), 'OK1')
+ hot_wrapper = self._getViewCallable(
+ config, ctx_iface=implementedBy(RuntimeError))
+ self.assertTrue(IMultiView.providedBy(hot_wrapper))
+ self.assertEqual([x[:2] for x in hot_wrapper.views], [(view2, None)])
+ self.assertEqual(hot_wrapper(None, None), 'OK1')
+
+ exc_wrapper = self._getViewCallable(
+ config, exc_iface=implementedBy(RuntimeError))
+ self.assertTrue(IMultiView.providedBy(exc_wrapper))
+ self.assertEqual([x[:2] for x in exc_wrapper.views], [(view2, None)])
+ self.assertEqual(exc_wrapper(None, None), 'OK1')
+
+ def test_add_view_exc_multiview_replaces_only_exc_multiview(self):
+ from pyramid.renderers import null_renderer
+ from zope.interface import implementedBy
+ from pyramid.interfaces import IRequest
+ from pyramid.interfaces import IMultiView
+ from pyramid.interfaces import IViewClassifier
+ from pyramid.interfaces import IExceptionViewClassifier
+ hot_view = DummyMultiView()
+ exc_view = DummyMultiView()
+ config = self._makeOne(autocommit=True)
+ config.registry.registerAdapter(
+ hot_view,
+ (IViewClassifier, IRequest, implementedBy(RuntimeError)),
+ IMultiView, name='')
+ config.registry.registerAdapter(
+ exc_view,
+ (IExceptionViewClassifier, IRequest, implementedBy(RuntimeError)),
+ IMultiView, name='')
+ view2 = lambda *arg: 'OK2'
+ config.add_view(view=view2, context=RuntimeError, exception_only=True,
+ renderer=null_renderer)
+ hot_wrapper = self._getViewCallable(
+ config, ctx_iface=implementedBy(RuntimeError))
+ self.assertTrue(IMultiView.providedBy(hot_wrapper))
+ self.assertEqual(len(hot_wrapper.views), 0)
+ self.assertEqual(hot_wrapper(None, None), 'OK1')
+
+ exc_wrapper = self._getViewCallable(
+ config, exc_iface=implementedBy(RuntimeError))
+ self.assertTrue(IMultiView.providedBy(exc_wrapper))
+ self.assertEqual([x[:2] for x in exc_wrapper.views], [(view2, None)])
+ self.assertEqual(exc_wrapper(None, None), 'OK1')
def test_add_view_multiview_context_superclass_then_subclass(self):
from pyramid.renderers import null_renderer
@@ -886,10 +927,12 @@ class TestViewsConfigurationMixin(unittest.TestCase):
config.registry.registerAdapter(
view, (IViewClassifier, IRequest, ISuper), IView, name='')
config.add_view(view=view2, for_=ISub, renderer=null_renderer)
- wrapper = self._getViewCallable(config, ISuper, IRequest)
+ wrapper = self._getViewCallable(config, ctx_iface=ISuper,
+ request_iface=IRequest)
self.assertFalse(IMultiView.providedBy(wrapper))
self.assertEqual(wrapper(None, None), 'OK')
- wrapper = self._getViewCallable(config, ISub, IRequest)
+ wrapper = self._getViewCallable(config, ctx_iface=ISub,
+ request_iface=IRequest)
self.assertFalse(IMultiView.providedBy(wrapper))
self.assertEqual(wrapper(None, None), 'OK2')
@@ -914,16 +957,16 @@ class TestViewsConfigurationMixin(unittest.TestCase):
view, (IExceptionViewClassifier, IRequest, Super), IView, name='')
config.add_view(view=view2, for_=Sub, renderer=null_renderer)
wrapper = self._getViewCallable(
- config, implementedBy(Super), IRequest)
+ config, ctx_iface=implementedBy(Super), request_iface=IRequest)
wrapper_exc_view = self._getViewCallable(
- config, implementedBy(Super), IRequest, exception_view=True)
+ config, exc_iface=implementedBy(Super), request_iface=IRequest)
self.assertEqual(wrapper_exc_view, wrapper)
self.assertFalse(IMultiView.providedBy(wrapper_exc_view))
self.assertEqual(wrapper_exc_view(None, None), 'OK')
wrapper = self._getViewCallable(
- config, implementedBy(Sub), IRequest)
+ config, ctx_iface=implementedBy(Sub), request_iface=IRequest)
wrapper_exc_view = self._getViewCallable(
- config, implementedBy(Sub), IRequest, exception_view=True)
+ config, exc_iface=implementedBy(Sub), request_iface=IRequest)
self.assertEqual(wrapper_exc_view, wrapper)
self.assertFalse(IMultiView.providedBy(wrapper_exc_view))
self.assertEqual(wrapper_exc_view(None, None), 'OK2')
@@ -1233,8 +1276,8 @@ class TestViewsConfigurationMixin(unittest.TestCase):
renderer=null_renderer)
request_iface = self._getRouteRequestIface(config, 'foo')
wrapper_exc_view = self._getViewCallable(
- config, ctx_iface=implementedBy(RuntimeError),
- request_iface=request_iface, exception_view=True)
+ config, exc_iface=implementedBy(RuntimeError),
+ request_iface=request_iface)
self.assertNotEqual(wrapper_exc_view, None)
wrapper = self._getViewCallable(
config, ctx_iface=implementedBy(RuntimeError),
@@ -1815,6 +1858,124 @@ class TestViewsConfigurationMixin(unittest.TestCase):
self.assertRaises(ConfigurationError, configure_view)
+ def test_add_view_exception_only_no_regular_view(self):
+ from zope.interface import implementedBy
+ from pyramid.renderers import null_renderer
+ view1 = lambda *arg: 'OK'
+ config = self._makeOne(autocommit=True)
+ config.add_view(view=view1, context=Exception, exception_only=True,
+ renderer=null_renderer)
+ view = self._getViewCallable(config, ctx_iface=implementedBy(Exception))
+ self.assertTrue(view is None)
+
+ def test_add_view_exception_only(self):
+ from zope.interface import implementedBy
+ from pyramid.renderers import null_renderer
+ view1 = lambda *arg: 'OK'
+ config = self._makeOne(autocommit=True)
+ config.add_view(view=view1, context=Exception, exception_only=True,
+ renderer=null_renderer)
+ view = self._getViewCallable(
+ config, exc_iface=implementedBy(Exception))
+ self.assertEqual(view1, view)
+
+ def test_add_view_exception_only_misconfiguration(self):
+ view = lambda *arg: 'OK'
+ config = self._makeOne(autocommit=True)
+ class NotAnException(object):
+ pass
+ self.assertRaises(
+ ConfigurationError,
+ config.add_view, view, context=NotAnException, exception_only=True)
+
+ def test_add_exception_view(self):
+ from zope.interface import implementedBy
+ from pyramid.renderers import null_renderer
+ view1 = lambda *arg: 'OK'
+ config = self._makeOne(autocommit=True)
+ config.add_exception_view(view=view1, renderer=null_renderer)
+ wrapper = self._getViewCallable(
+ config, exc_iface=implementedBy(Exception))
+ context = Exception()
+ request = self._makeRequest(config)
+ self.assertEqual(wrapper(context, request), 'OK')
+
+ def test_add_exception_view_with_subclass(self):
+ from zope.interface import implementedBy
+ from pyramid.renderers import null_renderer
+ view1 = lambda *arg: 'OK'
+ config = self._makeOne(autocommit=True)
+ config.add_exception_view(view=view1, context=ValueError,
+ renderer=null_renderer)
+ wrapper = self._getViewCallable(
+ config, exc_iface=implementedBy(ValueError))
+ context = ValueError()
+ request = self._makeRequest(config)
+ self.assertEqual(wrapper(context, request), 'OK')
+
+ def test_add_exception_view_disallows_name(self):
+ config = self._makeOne(autocommit=True)
+ self.assertRaises(ConfigurationError,
+ config.add_exception_view,
+ context=Exception(),
+ name='foo')
+
+ def test_add_exception_view_disallows_permission(self):
+ config = self._makeOne(autocommit=True)
+ self.assertRaises(ConfigurationError,
+ config.add_exception_view,
+ context=Exception(),
+ permission='foo')
+
+ def test_add_exception_view_disallows_require_csrf(self):
+ config = self._makeOne(autocommit=True)
+ self.assertRaises(ConfigurationError,
+ config.add_exception_view,
+ context=Exception(),
+ require_csrf=True)
+
+ def test_add_exception_view_disallows_for_(self):
+ config = self._makeOne(autocommit=True)
+ self.assertRaises(ConfigurationError,
+ config.add_exception_view,
+ context=Exception(),
+ for_='foo')
+
+ def test_add_exception_view_disallows_exception_only(self):
+ config = self._makeOne(autocommit=True)
+ self.assertRaises(ConfigurationError,
+ config.add_exception_view,
+ context=Exception(),
+ exception_only=True)
+
+ def test_add_exception_view_with_view_defaults(self):
+ from pyramid.renderers import null_renderer
+ from pyramid.exceptions import PredicateMismatch
+ from zope.interface import directlyProvides
+ from zope.interface import implementedBy
+ class view(object):
+ __view_defaults__ = {
+ 'containment': 'pyramid.tests.test_config.IDummy'
+ }
+ def __init__(self, request):
+ pass
+ def __call__(self):
+ return 'OK'
+ config = self._makeOne(autocommit=True)
+ config.add_exception_view(
+ view=view,
+ context=Exception,
+ renderer=null_renderer)
+ wrapper = self._getViewCallable(
+ config, exc_iface=implementedBy(Exception))
+ context = DummyContext()
+ directlyProvides(context, IDummy)
+ request = self._makeRequest(config)
+ self.assertEqual(wrapper(context, request), 'OK')
+ context = DummyContext()
+ request = self._makeRequest(config)
+ self.assertRaises(PredicateMismatch, wrapper, context, request)
+
def test_derive_view_function(self):
from pyramid.renderers import null_renderer
def view(request):
@@ -1927,7 +2088,7 @@ class TestViewsConfigurationMixin(unittest.TestCase):
config.add_forbidden_view(view, renderer=null_renderer)
request = self._makeRequest(config)
view = self._getViewCallable(config,
- ctx_iface=implementedBy(HTTPForbidden),
+ exc_iface=implementedBy(HTTPForbidden),
request_iface=IRequest)
result = view(None, request)
self.assertEqual(result, 'OK')
@@ -1941,7 +2102,7 @@ class TestViewsConfigurationMixin(unittest.TestCase):
config.add_forbidden_view()
request = self._makeRequest(config)
view = self._getViewCallable(config,
- ctx_iface=implementedBy(HTTPForbidden),
+ exc_iface=implementedBy(HTTPForbidden),
request_iface=IRequest)
context = HTTPForbidden()
result = view(context, request)
@@ -1964,6 +2125,11 @@ class TestViewsConfigurationMixin(unittest.TestCase):
self.assertRaises(ConfigurationError,
config.add_forbidden_view, permission='foo')
+ def test_add_forbidden_view_disallows_require_csrf(self):
+ config = self._makeOne(autocommit=True)
+ self.assertRaises(ConfigurationError,
+ config.add_forbidden_view, require_csrf=True)
+
def test_add_forbidden_view_disallows_context(self):
config = self._makeOne(autocommit=True)
self.assertRaises(ConfigurationError,
@@ -1974,11 +2140,6 @@ class TestViewsConfigurationMixin(unittest.TestCase):
self.assertRaises(ConfigurationError,
config.add_forbidden_view, for_='foo')
- def test_add_forbidden_view_disallows_http_cache(self):
- config = self._makeOne(autocommit=True)
- self.assertRaises(ConfigurationError,
- config.add_forbidden_view, http_cache='foo')
-
def test_add_forbidden_view_with_view_defaults(self):
from pyramid.interfaces import IRequest
from pyramid.renderers import null_renderer
@@ -1999,7 +2160,7 @@ class TestViewsConfigurationMixin(unittest.TestCase):
view=view,
renderer=null_renderer)
wrapper = self._getViewCallable(
- config, ctx_iface=implementedBy(HTTPForbidden),
+ config, exc_iface=implementedBy(HTTPForbidden),
request_iface=IRequest)
context = DummyContext()
directlyProvides(context, IDummy)
@@ -2019,7 +2180,7 @@ class TestViewsConfigurationMixin(unittest.TestCase):
config.add_notfound_view(view, renderer=null_renderer)
request = self._makeRequest(config)
view = self._getViewCallable(config,
- ctx_iface=implementedBy(HTTPNotFound),
+ exc_iface=implementedBy(HTTPNotFound),
request_iface=IRequest)
result = view(None, request)
self.assertEqual(result, (None, request))
@@ -2033,7 +2194,7 @@ class TestViewsConfigurationMixin(unittest.TestCase):
config.add_notfound_view()
request = self._makeRequest(config)
view = self._getViewCallable(config,
- ctx_iface=implementedBy(HTTPNotFound),
+ exc_iface=implementedBy(HTTPNotFound),
request_iface=IRequest)
context = HTTPNotFound()
result = view(context, request)
@@ -2056,6 +2217,11 @@ class TestViewsConfigurationMixin(unittest.TestCase):
self.assertRaises(ConfigurationError,
config.add_notfound_view, permission='foo')
+ def test_add_notfound_view_disallows_require_csrf(self):
+ config = self._makeOne(autocommit=True)
+ self.assertRaises(ConfigurationError,
+ config.add_notfound_view, require_csrf=True)
+
def test_add_notfound_view_disallows_context(self):
config = self._makeOne(autocommit=True)
self.assertRaises(ConfigurationError,
@@ -2066,11 +2232,6 @@ class TestViewsConfigurationMixin(unittest.TestCase):
self.assertRaises(ConfigurationError,
config.add_notfound_view, for_='foo')
- def test_add_notfound_view_disallows_http_cache(self):
- config = self._makeOne(autocommit=True)
- self.assertRaises(ConfigurationError,
- config.add_notfound_view, http_cache='foo')
-
def test_add_notfound_view_append_slash(self):
from pyramid.response import Response
from pyramid.renderers import null_renderer
@@ -2086,7 +2247,7 @@ class TestViewsConfigurationMixin(unittest.TestCase):
request.query_string = 'a=1&b=2'
request.path = '/scriptname/foo'
view = self._getViewCallable(config,
- ctx_iface=implementedBy(HTTPNotFound),
+ exc_iface=implementedBy(HTTPNotFound),
request_iface=IRequest)
result = view(None, request)
self.assertTrue(isinstance(result, HTTPFound))
@@ -2109,7 +2270,7 @@ class TestViewsConfigurationMixin(unittest.TestCase):
request.query_string = 'a=1&b=2'
request.path = '/scriptname/foo'
view = self._getViewCallable(config,
- ctx_iface=implementedBy(HTTPNotFound),
+ exc_iface=implementedBy(HTTPNotFound),
request_iface=IRequest)
result = view(None, request)
self.assertTrue(isinstance(result, HTTPMovedPermanently))
@@ -2135,7 +2296,7 @@ class TestViewsConfigurationMixin(unittest.TestCase):
view=view,
renderer=null_renderer)
wrapper = self._getViewCallable(
- config, ctx_iface=implementedBy(HTTPNotFound),
+ config, exc_iface=implementedBy(HTTPNotFound),
request_iface=IRequest)
context = DummyContext()
directlyProvides(context, IDummy)
@@ -2165,7 +2326,7 @@ class TestViewsConfigurationMixin(unittest.TestCase):
renderer='json')
request = self._makeRequest(config)
view = self._getViewCallable(config,
- ctx_iface=implementedBy(HTTPNotFound),
+ exc_iface=implementedBy(HTTPNotFound),
request_iface=IRequest)
result = view(None, request)
self._assertBody(result, '{}')
@@ -2182,7 +2343,7 @@ class TestViewsConfigurationMixin(unittest.TestCase):
renderer='json')
request = self._makeRequest(config)
view = self._getViewCallable(config,
- ctx_iface=implementedBy(HTTPForbidden),
+ exc_iface=implementedBy(HTTPForbidden),
request_iface=IRequest)
result = view(None, request)
self._assertBody(result, '{}')
@@ -2203,6 +2364,75 @@ class TestViewsConfigurationMixin(unittest.TestCase):
from pyramid.tests import test_config
self.assertEqual(result, test_config)
+ def test_add_normal_and_exception_view_intr_derived_callable(self):
+ from pyramid.renderers import null_renderer
+ from pyramid.exceptions import BadCSRFToken
+ config = self._makeOne(autocommit=True)
+ introspector = DummyIntrospector()
+ config.introspector = introspector
+ view = lambda r: 'OK'
+ config.set_default_csrf_options(require_csrf=True)
+ config.add_view(view, context=Exception, renderer=null_renderer)
+ view_intr = introspector.introspectables[1]
+ self.assertTrue(view_intr.type_name, 'view')
+ self.assertEqual(view_intr['callable'], view)
+ derived_view = view_intr['derived_callable']
+
+ request = self._makeRequest(config)
+ request.method = 'POST'
+ request.scheme = 'http'
+ request.POST = {}
+ request.headers = {}
+ request.session = DummySession({'csrf_token': 'foo'})
+ self.assertRaises(BadCSRFToken, lambda: derived_view(None, request))
+ request.exception = Exception()
+ self.assertEqual(derived_view(None, request), 'OK')
+
+class Test_runtime_exc_view(unittest.TestCase):
+ def _makeOne(self, view1, view2):
+ from pyramid.config.views import runtime_exc_view
+ return runtime_exc_view(view1, view2)
+
+ def test_call(self):
+ def view1(context, request): return 'OK'
+ def view2(context, request): raise AssertionError
+ result_view = self._makeOne(view1, view2)
+ request = DummyRequest()
+ result = result_view(None, request)
+ self.assertEqual(result, 'OK')
+
+ def test_call_dispatches_on_exception(self):
+ def view1(context, request): raise AssertionError
+ def view2(context, request): return 'OK'
+ result_view = self._makeOne(view1, view2)
+ request = DummyRequest()
+ request.exception = Exception()
+ result = result_view(None, request)
+ self.assertEqual(result, 'OK')
+
+ def test_permitted(self):
+ def errfn(context, request): raise AssertionError
+ def view1(context, request): raise AssertionError
+ view1.__permitted__ = lambda c, r: 'OK'
+ def view2(context, request): raise AssertionError
+ view2.__permitted__ = errfn
+ result_view = self._makeOne(view1, view2)
+ request = DummyRequest()
+ result = result_view.__permitted__(None, request)
+ self.assertEqual(result, 'OK')
+
+ def test_permitted_dispatches_on_exception(self):
+ def errfn(context, request): raise AssertionError
+ def view1(context, request): raise AssertionError
+ view1.__permitted__ = errfn
+ def view2(context, request): raise AssertionError
+ view2.__permitted__ = lambda c, r: 'OK'
+ result_view = self._makeOne(view1, view2)
+ request = DummyRequest()
+ request.exception = Exception()
+ result = result_view.__permitted__(None, request)
+ self.assertEqual(result, 'OK')
+
class Test_requestonly(unittest.TestCase):
def _callFUT(self, view, attr=None):
from pyramid.config.views import requestonly
diff --git a/pyramid/tests/test_exceptions.py b/pyramid/tests/test_exceptions.py
index 993209046..9cb0f58d1 100644
--- a/pyramid/tests/test_exceptions.py
+++ b/pyramid/tests/test_exceptions.py
@@ -90,5 +90,3 @@ class TestCyclicDependencyError(unittest.TestCase):
result = str(exc)
self.assertTrue("'a' sorts before ['c', 'd']" in result)
self.assertTrue("'c' sorts before ['a']" in result)
-
-
diff --git a/pyramid/tests/test_scripting.py b/pyramid/tests/test_scripting.py
index 1e952062b..00f738e02 100644
--- a/pyramid/tests/test_scripting.py
+++ b/pyramid/tests/test_scripting.py
@@ -134,6 +134,27 @@ class Test_prepare(unittest.TestCase):
root, closer = info['root'], info['closer']
closer()
+ def test_it_is_a_context_manager(self):
+ request = DummyRequest({})
+ registry = request.registry = self._makeRegistry()
+ closer_called = [False]
+ with self._callFUT(request=request) as info:
+ root, request = info['root'], info['request']
+ pushed = self.manager.get()
+ self.assertEqual(pushed['request'], request)
+ self.assertEqual(pushed['registry'], registry)
+ self.assertEqual(pushed['request'].registry, registry)
+ self.assertEqual(root.a, (request,))
+ orig_closer = info['closer']
+ def closer():
+ orig_closer()
+ closer_called[0] = True
+ info['closer'] = closer
+ self.assertTrue(closer_called[0])
+ self.assertEqual(self.default, self.manager.get())
+ self.assertEqual(request.context, root)
+ self.assertEqual(request.registry, registry)
+
class Test__make_request(unittest.TestCase):
def _callFUT(self, path='/', registry=None):
from pyramid.scripting import _make_request
diff --git a/pyramid/tests/test_scripts/dummy.py b/pyramid/tests/test_scripts/dummy.py
index a872e197c..f3aa20e7c 100644
--- a/pyramid/tests/test_scripts/dummy.py
+++ b/pyramid/tests/test_scripts/dummy.py
@@ -70,6 +70,8 @@ class DummyView(object):
def __init__(self, **attrs):
self.__request_attrs__ = attrs
+ def view(context, request): pass
+
from zope.interface import implementer
from pyramid.interfaces import IMultiView
@implementer(IMultiView)
diff --git a/pyramid/tests/test_scripts/test_common.py b/pyramid/tests/test_scripts/test_common.py
index 13ab0ae6a..60741db92 100644
--- a/pyramid/tests/test_scripts/test_common.py
+++ b/pyramid/tests/test_scripts/test_common.py
@@ -1,22 +1,5 @@
-import os
import unittest
-class Test_logging_file_config(unittest.TestCase):
- def _callFUT(self, config_file):
- from pyramid.scripts.common import logging_file_config
- dummy_cp = DummyConfigParserModule
- return logging_file_config(config_file, self.fileConfig, dummy_cp)
-
- def test_it(self):
- config_file, dict = self._callFUT('/abc')
- # use of os.path.abspath here is a sop to Windows
- self.assertEqual(config_file, os.path.abspath('/abc'))
- self.assertEqual(dict['__file__'], os.path.abspath('/abc'))
- self.assertEqual(dict['here'], os.path.abspath('/'))
-
- def fileConfig(self, config_file, dict):
- return config_file, dict
-
class TestParseVars(unittest.TestCase):
def test_parse_vars_good(self):
from pyramid.scripts.common import parse_vars
@@ -28,16 +11,3 @@ class TestParseVars(unittest.TestCase):
from pyramid.scripts.common import parse_vars
vars = ['a']
self.assertRaises(ValueError, parse_vars, vars)
-
-
-class DummyConfigParser(object):
- def read(self, x):
- pass
-
- def has_section(self, name):
- return True
-
-class DummyConfigParserModule(object):
- ConfigParser = DummyConfigParser
-
-
diff --git a/pyramid/tests/test_scripts/test_pcreate.py b/pyramid/tests/test_scripts/test_pcreate.py
index eaa7c1464..b7013bc73 100644
--- a/pyramid/tests/test_scripts/test_pcreate.py
+++ b/pyramid/tests/test_scripts/test_pcreate.py
@@ -80,6 +80,27 @@ class TestPCreateCommand(unittest.TestCase):
{'project': 'Distro', 'egg': 'Distro', 'package': 'distro',
'pyramid_version': '0.1', 'pyramid_docs_branch':'0.1-branch'})
+ def test_scaffold_with_package_name(self):
+ import os
+ cmd = self._makeOne('-s', 'dummy', '--package-name', 'dummy_package',
+ 'Distro')
+ scaffold = DummyScaffold('dummy')
+ cmd.scaffolds = [scaffold]
+ cmd.pyramid_dist = DummyDist("0.1")
+ result = cmd.run()
+
+ self.assertEqual(result, 0)
+ self.assertEqual(
+ scaffold.output_dir,
+ os.path.normpath(os.path.join(os.getcwd(), 'Distro'))
+ )
+ self.assertEqual(
+ scaffold.vars,
+ {'project': 'Distro', 'egg': 'dummy_package',
+ 'package': 'dummy_package', 'pyramid_version': '0.1',
+ 'pyramid_docs_branch':'0.1-branch'})
+
+
def test_scaffold_with_hyphen_in_project_name(self):
import os
cmd = self._makeOne('-s', 'dummy', 'Distro-')
diff --git a/pyramid/tests/test_scripts/test_prequest.py b/pyramid/tests/test_scripts/test_prequest.py
index 95cec0518..45db0dbaf 100644
--- a/pyramid/tests/test_scripts/test_prequest.py
+++ b/pyramid/tests/test_scripts/test_prequest.py
@@ -34,7 +34,8 @@ class TestPRequestCommand(unittest.TestCase):
self.assertEqual(self._out, ['You must provide at least two arguments'])
def test_command_two_args(self):
- command = self._makeOne(['', 'development.ini', '/'])
+ command = self._makeOne(['', 'development.ini', '/'],
+ [('Content-Type', 'text/html; charset=UTF-8')])
command.run()
self.assertEqual(self._path_info, '/')
self.assertEqual(self._spec, 'development.ini')
@@ -42,7 +43,8 @@ class TestPRequestCommand(unittest.TestCase):
self.assertEqual(self._out, ['abc'])
def test_command_path_doesnt_start_with_slash(self):
- command = self._makeOne(['', 'development.ini', 'abc'])
+ command = self._makeOne(['', 'development.ini', 'abc'],
+ [('Content-Type', 'text/html; charset=UTF-8')])
command.run()
self.assertEqual(self._path_info, '/abc')
self.assertEqual(self._spec, 'development.ini')
@@ -60,7 +62,8 @@ class TestPRequestCommand(unittest.TestCase):
def test_command_has_good_header_var(self):
command = self._makeOne(
- ['', '--header=name:value','development.ini', '/'])
+ ['', '--header=name:value','development.ini', '/'],
+ [('Content-Type', 'text/html; charset=UTF-8')])
command.run()
self.assertEqual(self._environ['HTTP_NAME'], 'value')
self.assertEqual(self._path_info, '/')
@@ -71,7 +74,8 @@ class TestPRequestCommand(unittest.TestCase):
def test_command_w_basic_auth(self):
command = self._makeOne(
['', '--login=user:password',
- '--header=name:value','development.ini', '/'])
+ '--header=name:value','development.ini', '/'],
+ [('Content-Type', 'text/html; charset=UTF-8')])
command.run()
self.assertEqual(self._environ['HTTP_NAME'], 'value')
self.assertEqual(self._environ['HTTP_AUTHORIZATION'],
@@ -83,7 +87,8 @@ class TestPRequestCommand(unittest.TestCase):
def test_command_has_content_type_header_var(self):
command = self._makeOne(
- ['', '--header=content-type:app/foo','development.ini', '/'])
+ ['', '--header=content-type:app/foo','development.ini', '/'],
+ [('Content-Type', 'text/html; charset=UTF-8')])
command.run()
self.assertEqual(self._environ['CONTENT_TYPE'], 'app/foo')
self.assertEqual(self._path_info, '/')
@@ -97,7 +102,9 @@ class TestPRequestCommand(unittest.TestCase):
'--header=name:value',
'--header=name2:value2',
'development.ini',
- '/'])
+ '/'],
+ [('Content-Type', 'text/html; charset=UTF-8')]
+ )
command.run()
self.assertEqual(self._environ['HTTP_NAME'], 'value')
self.assertEqual(self._environ['HTTP_NAME2'], 'value2')
@@ -107,7 +114,8 @@ class TestPRequestCommand(unittest.TestCase):
self.assertEqual(self._out, ['abc'])
def test_command_method_get(self):
- command = self._makeOne(['', '--method=GET', 'development.ini', '/'])
+ command = self._makeOne(['', '--method=GET', 'development.ini', '/'],
+ [('Content-Type', 'text/html; charset=UTF-8')])
command.run()
self.assertEqual(self._environ['REQUEST_METHOD'], 'GET')
self.assertEqual(self._path_info, '/')
@@ -117,7 +125,8 @@ class TestPRequestCommand(unittest.TestCase):
def test_command_method_post(self):
from pyramid.compat import NativeIO
- command = self._makeOne(['', '--method=POST', 'development.ini', '/'])
+ command = self._makeOne(['', '--method=POST', 'development.ini', '/'],
+ [('Content-Type', 'text/html; charset=UTF-8')])
stdin = NativeIO()
command.stdin = stdin
command.run()
@@ -131,7 +140,8 @@ class TestPRequestCommand(unittest.TestCase):
def test_command_method_put(self):
from pyramid.compat import NativeIO
- command = self._makeOne(['', '--method=PUT', 'development.ini', '/'])
+ command = self._makeOne(['', '--method=PUT', 'development.ini', '/'],
+ [('Content-Type', 'text/html; charset=UTF-8')])
stdin = NativeIO()
command.stdin = stdin
command.run()
@@ -145,7 +155,8 @@ class TestPRequestCommand(unittest.TestCase):
def test_command_method_patch(self):
from pyramid.compat import NativeIO
- command = self._makeOne(['', '--method=PATCH', 'development.ini', '/'])
+ command = self._makeOne(['', '--method=PATCH', 'development.ini', '/'],
+ [('Content-Type', 'text/html; charset=UTF-8')])
stdin = NativeIO()
command.stdin = stdin
command.run()
@@ -160,7 +171,8 @@ class TestPRequestCommand(unittest.TestCase):
def test_command_method_propfind(self):
from pyramid.compat import NativeIO
command = self._makeOne(['', '--method=PROPFIND', 'development.ini',
- '/'])
+ '/'],
+ [('Content-Type', 'text/html; charset=UTF-8')])
stdin = NativeIO()
command.stdin = stdin
command.run()
@@ -173,7 +185,8 @@ class TestPRequestCommand(unittest.TestCase):
def test_command_method_options(self):
from pyramid.compat import NativeIO
command = self._makeOne(['', '--method=OPTIONS', 'development.ini',
- '/'])
+ '/'],
+ [('Content-Type', 'text/html; charset=UTF-8')])
stdin = NativeIO()
command.stdin = stdin
command.run()
@@ -184,7 +197,8 @@ class TestPRequestCommand(unittest.TestCase):
self.assertEqual(self._out, ['abc'])
def test_command_with_query_string(self):
- command = self._makeOne(['', 'development.ini', '/abc?a=1&b=2&c'])
+ command = self._makeOne(['', 'development.ini', '/abc?a=1&b=2&c'],
+ [('Content-Type', 'text/html; charset=UTF-8')])
command.run()
self.assertEqual(self._environ['QUERY_STRING'], 'a=1&b=2&c')
self.assertEqual(self._path_info, '/abc')
@@ -194,7 +208,8 @@ class TestPRequestCommand(unittest.TestCase):
def test_command_display_headers(self):
command = self._makeOne(
- ['', '--display-headers', 'development.ini', '/'])
+ ['', '--display-headers', 'development.ini', '/'],
+ [('Content-Type', 'text/html; charset=UTF-8')])
command.run()
self.assertEqual(self._path_info, '/')
self.assertEqual(self._spec, 'development.ini')
diff --git a/pyramid/tests/test_scripts/test_proutes.py b/pyramid/tests/test_scripts/test_proutes.py
index 876572b01..aeaa57060 100644
--- a/pyramid/tests/test_scripts/test_proutes.py
+++ b/pyramid/tests/test_scripts/test_proutes.py
@@ -200,6 +200,33 @@ class TestPRoutesCommand(unittest.TestCase):
'pyramid.tests.test_scripts.test_proutes.view']
)
+ def test_class_view(self):
+ from pyramid.renderers import null_renderer as nr
+
+ config = self._makeConfig(autocommit=True)
+ config.add_route('foo', '/a/b')
+ config.add_view(
+ route_name='foo',
+ view=dummy.DummyView,
+ attr='view',
+ renderer=nr,
+ request_method='POST'
+ )
+
+ command = self._makeOne()
+ L = []
+ command.out = L.append
+ command.bootstrap = (dummy.DummyBootstrap(registry=config.registry),)
+ result = command.run()
+ self.assertEqual(result, 0)
+ self.assertEqual(len(L), 3)
+ compare_to = L[-1].split()
+ expected = [
+ 'foo', '/a/b',
+ 'pyramid.tests.test_scripts.dummy.DummyView.view', 'POST'
+ ]
+ self.assertEqual(compare_to, expected)
+
def test_single_route_one_view_registered_with_factory(self):
from zope.interface import Interface
from pyramid.interfaces import IRouteRequest
diff --git a/pyramid/tests/test_view.py b/pyramid/tests/test_view.py
index 2de44d579..cab42cf48 100644
--- a/pyramid/tests/test_view.py
+++ b/pyramid/tests/test_view.py
@@ -132,7 +132,58 @@ class Test_forbidden_view_config(BaseTest, unittest.TestCase):
self.assertEqual(settings[0]['view'], None) # comes from call_venusian
self.assertEqual(settings[0]['attr'], 'view')
self.assertEqual(settings[0]['_info'], 'codeinfo')
-
+
+class Test_exception_view_config(BaseTest, unittest.TestCase):
+ def _makeOne(self, *args, **kw):
+ from pyramid.view import exception_view_config
+ return exception_view_config(*args, **kw)
+
+ def test_ctor(self):
+ inst = self._makeOne(context=Exception, path_info='path_info')
+ self.assertEqual(inst.__dict__,
+ {'context':Exception, 'path_info':'path_info'})
+
+ def test_ctor_positional_exception(self):
+ inst = self._makeOne(Exception, path_info='path_info')
+ self.assertEqual(inst.__dict__,
+ {'context':Exception, 'path_info':'path_info'})
+
+ def test_ctor_positional_extras(self):
+ from pyramid.exceptions import ConfigurationError
+ self.assertRaises(ConfigurationError, lambda: self._makeOne(Exception, True))
+
+ def test_it_function(self):
+ def view(request): pass
+ decorator = self._makeOne(context=Exception, renderer='renderer')
+ venusian = DummyVenusian()
+ decorator.venusian = venusian
+ wrapped = decorator(view)
+ self.assertTrue(wrapped is view)
+ config = call_venusian(venusian)
+ settings = config.settings
+ self.assertEqual(
+ settings,
+ [{'venusian': venusian, 'context': Exception,
+ 'renderer': 'renderer', '_info': 'codeinfo', 'view': None}]
+ )
+
+ def test_it_class(self):
+ decorator = self._makeOne()
+ venusian = DummyVenusian()
+ decorator.venusian = venusian
+ decorator.venusian.info.scope = 'class'
+ class view(object): pass
+ wrapped = decorator(view)
+ self.assertTrue(wrapped is view)
+ config = call_venusian(venusian)
+ settings = config.settings
+ self.assertEqual(len(settings), 1)
+ self.assertEqual(len(settings[0]), 4)
+ self.assertEqual(settings[0]['venusian'], venusian)
+ self.assertEqual(settings[0]['view'], None) # comes from call_venusian
+ self.assertEqual(settings[0]['attr'], 'view')
+ self.assertEqual(settings[0]['_info'], 'codeinfo')
+
class RenderViewToResponseTests(BaseTest, unittest.TestCase):
def _callFUT(self, *arg, **kw):
from pyramid.view import render_view_to_response
@@ -898,7 +949,7 @@ class DummyConfig(object):
def add_view(self, **kw):
self.settings.append(kw)
- add_notfound_view = add_forbidden_view = add_view
+ add_notfound_view = add_forbidden_view = add_exception_view = add_view
def with_package(self, pkg):
self.pkg = pkg
diff --git a/pyramid/tests/test_viewderivers.py b/pyramid/tests/test_viewderivers.py
index 79fcd6e71..51d0bd367 100644
--- a/pyramid/tests/test_viewderivers.py
+++ b/pyramid/tests/test_viewderivers.py
@@ -551,6 +551,28 @@ class TestDeriveView(unittest.TestCase):
"'view_name' against context None): "
"Allowed (NO_PERMISSION_REQUIRED)")
+ def test_debug_auth_permission_authpol_permitted_excview(self):
+ response = DummyResponse()
+ view = lambda *arg: response
+ self.config.registry.settings = dict(
+ debug_authorization=True, reload_templates=True)
+ logger = self._registerLogger()
+ self._registerSecurityPolicy(True)
+ result = self.config._derive_view(
+ view, context=Exception, permission='view')
+ self.assertEqual(view.__module__, result.__module__)
+ self.assertEqual(view.__doc__, result.__doc__)
+ self.assertEqual(view.__name__, result.__name__)
+ self.assertEqual(result.__call_permissive__.__wraps__, view)
+ request = self._makeRequest()
+ request.view_name = 'view_name'
+ request.url = 'url'
+ self.assertEqual(result(Exception(), request), response)
+ self.assertEqual(len(logger.messages), 1)
+ self.assertEqual(logger.messages[0],
+ "debug_authorization of url url (view name "
+ "'view_name' against context Exception()): True")
+
def test_secured_view_authn_policy_no_authz_policy(self):
response = DummyResponse()
view = lambda *arg: response
@@ -1269,6 +1291,34 @@ class TestDeriveView(unittest.TestCase):
view = self.config._derive_view(inner_view)
self.assertRaises(BadCSRFToken, lambda: view(None, request))
+ def test_csrf_view_enabled_via_callback(self):
+ def callback(request):
+ return True
+ from pyramid.exceptions import BadCSRFToken
+ def inner_view(request): pass
+ request = self._makeRequest()
+ request.scheme = "http"
+ request.method = 'POST'
+ request.session = DummySession({'csrf_token': 'foo'})
+ self.config.set_default_csrf_options(require_csrf=True, callback=callback)
+ view = self.config._derive_view(inner_view)
+ self.assertRaises(BadCSRFToken, lambda: view(None, request))
+
+ def test_csrf_view_disabled_via_callback(self):
+ def callback(request):
+ return False
+ response = DummyResponse()
+ def inner_view(request):
+ return response
+ request = self._makeRequest()
+ request.scheme = "http"
+ request.method = 'POST'
+ request.session = DummySession({'csrf_token': 'foo'})
+ self.config.set_default_csrf_options(require_csrf=True, callback=callback)
+ view = self.config._derive_view(inner_view)
+ result = view(None, request)
+ self.assertTrue(result is response)
+
def test_csrf_view_uses_custom_csrf_token(self):
response = DummyResponse()
def inner_view(request):
diff --git a/pyramid/view.py b/pyramid/view.py
index 88c6397af..498bdde45 100644
--- a/pyramid/view.py
+++ b/pyramid/view.py
@@ -17,7 +17,10 @@ from pyramid.interfaces import (
from pyramid.compat import decode_path_info
-from pyramid.exceptions import PredicateMismatch
+from pyramid.exceptions import (
+ ConfigurationError,
+ PredicateMismatch,
+)
from pyramid.httpexceptions import (
HTTPFound,
@@ -166,7 +169,7 @@ class view_config(object):
:class:`pyramid.view.bfg_view`.
:class:`pyramid.view.view_config` supports the following keyword
- arguments: ``context``, ``permission``, ``name``,
+ arguments: ``context``, ``exception``, ``permission``, ``name``,
``request_type``, ``route_name``, ``request_method``, ``request_param``,
``containment``, ``xhr``, ``accept``, ``header``, ``path_info``,
``custom_predicates``, ``decorator``, ``mapper``, ``http_cache``,
@@ -325,7 +328,8 @@ class notfound_view_config(object):
.. versionadded:: 1.3
An analogue of :class:`pyramid.view.view_config` which registers a
- :term:`Not Found View`.
+ :term:`Not Found View` using
+ :meth:`pyramid.config.Configurator.add_notfound_view`.
The ``notfound_view_config`` constructor accepts most of the same arguments
as the constructor of :class:`pyramid.view.view_config`. It can be used
@@ -341,7 +345,7 @@ class notfound_view_config(object):
@notfound_view_config()
def notfound(request):
- return Response('Not found, dude!', status='404 Not Found')
+ return Response('Not found!', status='404 Not Found')
All arguments except ``append_slash`` have the same meaning as
:meth:`pyramid.view.view_config` and each predicate
@@ -413,7 +417,8 @@ class forbidden_view_config(object):
.. versionadded:: 1.3
An analogue of :class:`pyramid.view.view_config` which registers a
- :term:`forbidden view`.
+ :term:`forbidden view` using
+ :meth:`pyramid.config.Configurator.add_forbidden_view`.
The forbidden_view_config constructor accepts most of the same arguments
as the constructor of :class:`pyramid.view.view_config`. It can be used
@@ -463,6 +468,66 @@ class forbidden_view_config(object):
settings['_info'] = info.codeinfo # fbo "action_method"
return wrapped
+class exception_view_config(object):
+ """
+ .. versionadded:: 1.8
+
+ An analogue of :class:`pyramid.view.view_config` which registers an
+ :term:`exception view` using
+ :meth:`pyramid.config.Configurator.add_exception_view`.
+
+ The ``exception_view_config`` constructor requires an exception context,
+ and additionally accepts most of the same arguments as the constructor of
+ :class:`pyramid.view.view_config`. It can be used in the same places,
+ and behaves in largely the same way, except it always registers an
+ exception view instead of a "normal" view that dispatches on the request
+ :term:`context`.
+
+ Example:
+
+ .. code-block:: python
+
+ from pyramid.view import exception_view_config
+ from pyramid.response import Response
+
+ @exception_view_config(ValueError, renderer='json')
+ def error_view(request):
+ return {'error': str(request.exception)}
+
+ All arguments passed to this function have the same meaning as
+ :meth:`pyramid.view.view_config`, and each predicate argument restricts
+ the set of circumstances under which this exception view will be invoked.
+
+ """
+ venusian = venusian
+
+ def __init__(self, *args, **settings):
+ if 'context' not in settings and len(args) > 0:
+ exception, args = args[0], args[1:]
+ settings['context'] = exception
+ if len(args) > 0:
+ raise ConfigurationError('unknown positional arguments')
+ self.__dict__.update(settings)
+
+ def __call__(self, wrapped):
+ settings = self.__dict__.copy()
+
+ def callback(context, name, ob):
+ config = context.config.with_package(info.module)
+ config.add_exception_view(view=ob, **settings)
+
+ info = self.venusian.attach(wrapped, callback, category='pyramid')
+
+ if info.scope == 'class':
+ # if the decorator was attached to a method in a class, or
+ # otherwise executed at class scope, we need to set an
+ # 'attr' in the settings if one isn't already in there
+ if settings.get('attr') is None:
+ settings['attr'] = wrapped.__name__
+
+ settings['_info'] = info.codeinfo # fbo "action_method"
+ return wrapped
+
def _find_views(
registry,
request_iface,
diff --git a/pyramid/viewderivers.py b/pyramid/viewderivers.py
index 5d138a02a..4eb0ce704 100644
--- a/pyramid/viewderivers.py
+++ b/pyramid/viewderivers.py
@@ -286,18 +286,16 @@ def _secured_view(view, info):
authn_policy = info.registry.queryUtility(IAuthenticationPolicy)
authz_policy = info.registry.queryUtility(IAuthorizationPolicy)
+ # no-op on exception-only views without an explicit permission
+ if explicit_val is None and info.exception_only:
+ return view
+
if authn_policy and authz_policy and (permission is not None):
- def _permitted(context, request):
+ def permitted(context, request):
principals = authn_policy.effective_principals(request)
return authz_policy.permits(context, principals, permission)
- def _secured_view(context, request):
- if (
- getattr(request, 'exception', None) is not None and
- explicit_val is None
- ):
- return view(context, request)
-
- result = _permitted(context, request)
+ def secured_view(context, request):
+ result = permitted(context, request)
if result:
return view(context, request)
view_name = getattr(view, '__name__', view)
@@ -305,10 +303,10 @@ def _secured_view(view, info):
request, 'authdebug_message',
'Unauthorized: %s failed permission check' % view_name)
raise HTTPForbidden(msg, result=result)
- _secured_view.__call_permissive__ = view
- _secured_view.__permitted__ = _permitted
- _secured_view.__permission__ = permission
- wrapped_view = _secured_view
+ wrapped_view = secured_view
+ wrapped_view.__call_permissive__ = view
+ wrapped_view.__permitted__ = permitted
+ wrapped_view.__permission__ = permission
return wrapped_view
@@ -321,14 +319,13 @@ def _authdebug_view(view, info):
authn_policy = info.registry.queryUtility(IAuthenticationPolicy)
authz_policy = info.registry.queryUtility(IAuthorizationPolicy)
logger = info.registry.queryUtility(IDebugLogger)
- if settings and settings.get('debug_authorization', False):
- def _authdebug_view(context, request):
- if (
- getattr(request, 'exception', None) is not None and
- explicit_val is None
- ):
- return view(context, request)
+ # no-op on exception-only views without an explicit permission
+ if explicit_val is None and info.exception_only:
+ return view
+
+ if settings and settings.get('debug_authorization', False):
+ def authdebug_view(context, request):
view_name = getattr(request, 'view_name', None)
if authn_policy and authz_policy:
@@ -352,8 +349,7 @@ def _authdebug_view(view, info):
if request is not None:
request.authdebug_message = msg
return view(context, request)
-
- wrapped_view = _authdebug_view
+ wrapped_view = authdebug_view
return wrapped_view
@@ -485,14 +481,22 @@ def csrf_view(view, info):
token = 'csrf_token'
header = 'X-CSRF-Token'
safe_methods = frozenset(["GET", "HEAD", "OPTIONS", "TRACE"])
+ callback = None
else:
default_val = defaults.require_csrf
token = defaults.token
header = defaults.header
safe_methods = defaults.safe_methods
+ callback = defaults.callback
+
enabled = (
explicit_val is True or
- (explicit_val is not False and default_val)
+ # fallback to the default val if not explicitly enabled
+ # but only if the view is not an exception view
+ (
+ explicit_val is not False and default_val and
+ not info.exception_only
+ )
)
# disable if both header and token are disabled
enabled = enabled and (token or header)
@@ -501,11 +505,7 @@ def csrf_view(view, info):
def csrf_view(context, request):
if (
request.method not in safe_methods and
- (
- # skip exception views unless value is explicitly defined
- getattr(request, 'exception', None) is None or
- explicit_val is not None
- )
+ (callback is None or callback(request))
):
check_csrf_origin(request, raises=True)
check_csrf_token(request, token, header, raises=True)
diff --git a/setup.py b/setup.py
index 021da2b5f..f738ee623 100644
--- a/setup.py
+++ b/setup.py
@@ -18,16 +18,15 @@ import sys
from setuptools import setup, find_packages
py_version = sys.version_info[:2]
-is_pypy = '__pypy__' in sys.builtin_module_names
PY3 = py_version[0] == 3
if PY3:
- if py_version < (3, 3) and not is_pypy: # PyPy3 masquerades as Python 3.2...
- raise RuntimeError('On Python 3, Pyramid requires Python 3.3 or better')
+ if py_version < (3, 4):
+ raise RuntimeError('On Python 3, Pyramid requires Python 3.4 or better')
else:
- if py_version < (2, 6):
- raise RuntimeError('On Python 2, Pyramid requires Python 2.6 or better')
+ if py_version < (2, 7):
+ raise RuntimeError('On Python 2, Pyramid requires Python 2.7 or better')
here = os.path.abspath(os.path.dirname(__file__))
try:
@@ -81,7 +80,6 @@ setup(name='pyramid',
"Programming Language :: Python",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3",
- "Programming Language :: Python :: 3.3",
"Programming Language :: Python :: 3.4",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: Implementation :: CPython",
diff --git a/tox.ini b/tox.ini
index 0156d9e51..8ceb142cb 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,17 +1,18 @@
[tox]
envlist =
- py27,py33,py34,py35,pypy,
+ py27,py34,py35,py36,pypy,
docs,pep8,
{py2,py3}-cover,coverage,
+skip-missing-interpreters = True
[testenv]
# Most of these are defaults but if you specify any you can't fall back
# to defaults for others.
basepython =
py27: python2.7
- py33: python3.3
py34: python3.4
py35: python3.5
+ py36: python3.6
pypy: pypy
py2: python2.7
py3: python3.5
@@ -26,12 +27,6 @@ commands =
python pyramid/scaffolds/tests.py
deps = virtualenv
-[testenv:py33-scaffolds]
-basepython = python3.3
-commands =
- python pyramid/scaffolds/tests.py
-deps = virtualenv
-
[testenv:py34-scaffolds]
basepython = python3.4
commands =