summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris McDonough <chrism@plope.com>2015-02-17 18:57:31 -0500
committerChris McDonough <chrism@plope.com>2015-02-17 18:57:31 -0500
commit4dacb8c24efe98cb14b3ef89f6c9a8b2fd196790 (patch)
tree039ca762ddcf6e4946a2570136927d2773374969
parentc5802723a4ea035076573838138878caf01735c1 (diff)
parent75f85c57e0f4d1069cef9feb6ab6182b5f651492 (diff)
downloadpyramid-4dacb8c24efe98cb14b3ef89f6c9a8b2fd196790.tar.gz
pyramid-4dacb8c24efe98cb14b3ef89f6c9a8b2fd196790.tar.bz2
pyramid-4dacb8c24efe98cb14b3ef89f6c9a8b2fd196790.zip
Merge branch 'master' of github.com:Pylons/pyramid
-rw-r--r--.travis.yml1
-rw-r--r--CHANGES.txt59
-rw-r--r--CONTRIBUTORS.txt6
-rw-r--r--HACKING.txt2
-rw-r--r--HISTORY.txt2
-rw-r--r--docs/_static/pyramid_router.graffle1621
-rw-r--r--docs/_static/pyramid_router.pngbin0 -> 120643 bytes
-rw-r--r--docs/_static/pyramid_router.svg3
m---------docs/_themes0
-rw-r--r--docs/api/index.rst12
-rw-r--r--docs/api/interfaces.rst3
-rw-r--r--docs/api/request.rst1
-rw-r--r--docs/api/static.rst6
-rw-r--r--docs/glossary.rst5
-rw-r--r--docs/index.rst3
-rw-r--r--docs/narr/assets.rst15
-rw-r--r--docs/narr/commandline.rst56
-rw-r--r--docs/narr/hooks.rst51
-rw-r--r--docs/narr/hybrid.rst2
-rw-r--r--docs/narr/introspector.rst16
-rw-r--r--docs/narr/logging.rst18
-rw-r--r--docs/narr/router.rst5
-rw-r--r--docs/narr/sessions.rst4
-rw-r--r--pyramid/compat.py104
-rw-r--r--pyramid/config/__init__.py16
-rw-r--r--pyramid/config/adapters.py2
-rw-r--r--pyramid/config/factories.py43
-rw-r--r--pyramid/config/routes.py31
-rw-r--r--pyramid/config/util.py3
-rw-r--r--pyramid/config/views.py36
-rw-r--r--pyramid/decorator.py8
-rw-r--r--pyramid/interfaces.py27
-rw-r--r--pyramid/path.py10
-rw-r--r--pyramid/renderers.py35
-rw-r--r--pyramid/request.py35
-rw-r--r--pyramid/response.py17
-rw-r--r--pyramid/router.py3
-rw-r--r--pyramid/scripting.py8
-rw-r--r--pyramid/scripts/pcreate.py12
-rw-r--r--pyramid/scripts/prequest.py8
-rw-r--r--pyramid/scripts/proutes.py408
-rw-r--r--pyramid/scripts/pserve.py36
-rw-r--r--pyramid/security.py14
-rw-r--r--pyramid/session.py4
-rw-r--r--pyramid/static.py63
-rw-r--r--pyramid/testing.py8
-rw-r--r--pyramid/tests/test_compat.py26
-rw-r--r--pyramid/tests/test_config/test_factories.py34
-rw-r--r--pyramid/tests/test_config/test_init.py12
-rw-r--r--pyramid/tests/test_config/test_util.py7
-rw-r--r--pyramid/tests/test_config/test_views.py26
-rw-r--r--pyramid/tests/test_decorator.py18
-rw-r--r--pyramid/tests/test_renderers.py40
-rw-r--r--pyramid/tests/test_request.py45
-rw-r--r--pyramid/tests/test_response.py17
-rw-r--r--pyramid/tests/test_router.py16
-rw-r--r--pyramid/tests/test_scripting.py16
-rw-r--r--pyramid/tests/test_scripts/dummy.py2
-rw-r--r--pyramid/tests/test_scripts/test_pcreate.py11
-rw-r--r--pyramid/tests/test_scripts/test_prequest.py13
-rw-r--r--pyramid/tests/test_scripts/test_proutes.py535
-rw-r--r--pyramid/tests/test_static.py16
-rw-r--r--pyramid/tests/test_testing.py4
-rw-r--r--pyramid/tests/test_util.py254
-rw-r--r--pyramid/urldispatch.py11
-rw-r--r--pyramid/util.py94
-rw-r--r--rtd.txt2
-rw-r--r--setup.py4
-rw-r--r--tox.ini9
69 files changed, 3608 insertions, 426 deletions
diff --git a/.travis.yml b/.travis.yml
index 5a205b268..cb98fddbe 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,5 +1,6 @@
# Wire up travis
language: python
+sudo: false
env:
- TOXENV=py26
diff --git a/CHANGES.txt b/CHANGES.txt
index 46c331268..596e5f506 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -4,12 +4,26 @@ Next release
Features
--------
+- Add ``pyramid.request.apply_request_extensions`` function which can be
+ used in testing to apply any request extensions configured via
+ ``config.add_request_method``. Previously it was only possible to test
+ the extensions by going through Pyramid's router.
+ See https://github.com/Pylons/pyramid/pull/1581
+
+- pcreate when run without a scaffold argument will now print information on
+ the missing flag, as well as a list of available scaffolds.
+ See https://github.com/Pylons/pyramid/pull/1566 and
+ https://github.com/Pylons/pyramid/issues/1297
+
- Added support / testing for 'pypy3' under Tox and Travis.
See https://github.com/Pylons/pyramid/pull/1469
- Cache busting for static resources has been added and is available via a new
argument to ``pyramid.config.Configurator.add_static_view``: ``cachebust``.
- See https://github.com/Pylons/pyramid/pull/1380
+ Core APIs are shipped for both cache busting via query strings and
+ path segments and may be extended to fit into custom asset pipelines.
+ See https://github.com/Pylons/pyramid/pull/1380 and
+ https://github.com/Pylons/pyramid/pull/1583
- Add ``pyramid.config.Configurator.root_package`` attribute and init
parameter to assist with includeable packages that wish to resolve
@@ -59,9 +73,34 @@ Features
via ``request.static_url('myapp:static/foo.png')``.
See https://github.com/Pylons/pyramid/issues/1252
+- Added ``pyramid.config.Configurator.set_response_factory`` and the
+ ``response_factory`` keyword argument to the ``Configurator`` for defining
+ a factory that will return a custom ``Response`` class.
+ See https://github.com/Pylons/pyramid/pull/1499
+
+- Allow an iterator to be returned from a renderer. Previously it was only
+ possible to return bytes or unicode.
+ See https://github.com/Pylons/pyramid/pull/1417
+
+- ``pserve`` can now take a ``-b`` or ``--browser`` option to open the server
+ URL in a web browser. See https://github.com/Pylons/pyramid/pull/1533
+
+- Overall improvments for the ``proutes`` command. Added ``--format`` and
+ ``--glob`` arguments to the command, introduced the ``method``
+ column for displaying available request methods, and improved the ``view``
+ output by showing the module instead of just ``__repr__``.
+ See https://github.com/Pylons/pyramid/pull/1488
+
+- Support keyword-only arguments and function annotations in views in
+ Python 3. See https://github.com/Pylons/pyramid/pull/1556
+
Bug Fixes
---------
+- Work around an issue where ``pserve --reload`` would leave terminal echo
+ disabled if it reloaded during a pdb session.
+ See https://github.com/Pylons/pyramid/pull/1577
+
- ``pyramid.wsgi.wsgiapp`` and ``pyramid.wsgi.wsgiapp2`` now raise
``ValueError`` when accidentally passed ``None``.
See https://github.com/Pylons/pyramid/pull/1320
@@ -99,6 +138,17 @@ Bug Fixes
- Fix route generation for static view asset specifications having no path.
See https://github.com/Pylons/pyramid/pull/1377
+- Allow the ``pyramid.renderers.JSONP`` renderer to work even if there is no
+ valid request object. In this case it will not wrap the object in a
+ callback and thus behave just like the ``pyramid.renderers.JSON` renderer.
+ See https://github.com/Pylons/pyramid/pull/1561
+
+- Prevent "parameters to load are deprecated" ``DeprecationWarning``
+ from setuptools>=11.3. See https://github.com/Pylons/pyramid/pull/1541
+
+- Avoiding timing attacks against CSRF tokens.
+ See https://github.com/Pylons/pyramid/pull/1574
+
Deprecations
------------
@@ -109,6 +159,13 @@ Deprecations
Docs
----
+- Moved the documentation for ``accept`` on ``Configurator.add_view`` to no
+ longer be part of the predicate list. See
+ https://github.com/Pylons/pyramid/issues/1391 for a bug report stating
+ ``not_`` was failing on ``accept``. Discussion with @mcdonc led to the
+ conclusion that it should not be documented as a predicate.
+ See https://github.com/Pylons/pyramid/pull/1487 for this PR
+
- Removed logging configuration from Quick Tutorial ini files except for
scaffolding- and logging-related chapters to avoid needing to explain it too
early.
diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt
index e4132cda5..4f9bd6e41 100644
--- a/CONTRIBUTORS.txt
+++ b/CONTRIBUTORS.txt
@@ -238,3 +238,9 @@ Contributors
- Hugo Branquinho, 2014/11/25
- Adrian Teng, 2014/12/17
+
+- Ilja Everila, 2015/02/05
+
+- Geoffrey T. Dairiki, 2015/02/06
+
+- David Glick, 2015/02/12
diff --git a/HACKING.txt b/HACKING.txt
index 16c17699c..e104869ec 100644
--- a/HACKING.txt
+++ b/HACKING.txt
@@ -195,7 +195,7 @@ Test Coverage
-------------
- The codebase *must* have 100% test statement coverage after each commit.
- You can test coverage via ``tox -e coverage``, or alternately by installing
+ You can test coverage via ``tox -e cover``, or alternately by installing
``nose`` and ``coverage`` into your virtualenv (easiest via ``setup.py
dev``) , and running ``setup.py nosetests --with-coverage``.
diff --git a/HISTORY.txt b/HISTORY.txt
index 6aad221a8..242568e98 100644
--- a/HISTORY.txt
+++ b/HISTORY.txt
@@ -1327,7 +1327,7 @@ Bug Fixes
- Make test suite pass on 32-bit systems; closes #286. closes #306.
See also https://github.com/Pylons/pyramid/issues/286
-- The ``pryamid.view.view_config`` decorator did not accept a ``match_params``
+- The ``pyramid.view.view_config`` decorator did not accept a ``match_params``
predicate argument. See https://github.com/Pylons/pyramid/pull/308
- The AuthTktCookieHelper could potentially generate Unicode headers
diff --git a/docs/_static/pyramid_router.graffle b/docs/_static/pyramid_router.graffle
new file mode 100644
index 000000000..217878426
--- /dev/null
+++ b/docs/_static/pyramid_router.graffle
@@ -0,0 +1,1621 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>ActiveLayerIndex</key>
+ <integer>0</integer>
+ <key>ApplicationVersion</key>
+ <array>
+ <string>com.omnigroup.OmniGrafflePro</string>
+ <string>139.18.0.187838</string>
+ </array>
+ <key>AutoAdjust</key>
+ <true/>
+ <key>BackgroundGraphic</key>
+ <dict>
+ <key>Bounds</key>
+ <string>{{0, 0}, {576, 733}}</string>
+ <key>Class</key>
+ <string>SolidGraphic</string>
+ <key>ID</key>
+ <integer>2</integer>
+ <key>Style</key>
+ <dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ <key>stroke</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ </dict>
+ </dict>
+ <key>BaseZoom</key>
+ <integer>0</integer>
+ <key>CanvasOrigin</key>
+ <string>{0, 0}</string>
+ <key>ColumnAlign</key>
+ <integer>1</integer>
+ <key>ColumnSpacing</key>
+ <real>36</real>
+ <key>CreationDate</key>
+ <string>2014-12-01 08:25:13 +0000</string>
+ <key>Creator</key>
+ <string>Steve Piercy</string>
+ <key>DisplayScale</key>
+ <string>1 0/72 in = 1 0/72 in</string>
+ <key>GraphDocumentVersion</key>
+ <integer>8</integer>
+ <key>GraphicsList</key>
+ <array>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>ControlPoints</key>
+ <array>
+ <string>{0, 6.9840087890625}</string>
+ <string>{0, -9}</string>
+ </array>
+ <key>FontInfo</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>w</key>
+ <string>0</string>
+ </dict>
+ <key>Font</key>
+ <string>Helvetica</string>
+ <key>Size</key>
+ <real>12</real>
+ </dict>
+ <key>Head</key>
+ <dict>
+ <key>ID</key>
+ <integer>169413</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169414</integer>
+ <key>Points</key>
+ <array>
+ <string>{202.04165903727232, 501.05557886759294}</string>
+ <string>{202.04165903727232, 528.77776209513161}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Bezier</key>
+ <true/>
+ <key>Color</key>
+ <dict>
+ <key>b</key>
+ <string>0.0980392</string>
+ <key>g</key>
+ <string>0.0980392</string>
+ <key>r</key>
+ <string>0.0980392</string>
+ </dict>
+ <key>HeadArrow</key>
+ <string>SharpArrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169412</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{104.41666666666686, 528.77776209513161}, {195.24998474121094, 29}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169413</integer>
+ <key>Magnets</key>
+ <array>
+ <string>{0, 1}</string>
+ <string>{0, -1}</string>
+ <string>{1, 0}</string>
+ <string>{-1, 0}</string>
+ </array>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>b</key>
+ <string>0.999449</string>
+ <key>g</key>
+ <string>0.743511</string>
+ <key>r</key>
+ <string>0.872276</string>
+ </dict>
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400
+\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc
+
+\f0\fs20 \cf0 Return the
+\b response}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{104.41666666666657, 471.55557886759294}, {195.24998474121094, 29}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169412</integer>
+ <key>Magnets</key>
+ <array>
+ <string>{0, 1}</string>
+ <string>{0, -1}</string>
+ <string>{1, 0}</string>
+ <string>{-1, 0}</string>
+ </array>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>b</key>
+ <string>0.422927</string>
+ <key>g</key>
+ <string>1</string>
+ <key>r</key>
+ <string>1</string>
+ </dict>
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400
+\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc
+
+\f0\fs20 \cf0 Invoke the
+\b view callable
+\b0 ,\
+which returns a
+\b response}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{291.21562524160186, 379.55555343627816}, {26, 24}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>FitText</key>
+ <string>YES</string>
+ <key>Flow</key>
+ <string>Resize</string>
+ <key>FontInfo</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>w</key>
+ <string>0</string>
+ </dict>
+ <key>Font</key>
+ <string>Helvetica</string>
+ <key>Size</key>
+ <real>12</real>
+ </dict>
+ <key>ID</key>
+ <integer>169411</integer>
+ <key>Line</key>
+ <dict>
+ <key>ID</key>
+ <integer>169410</integer>
+ <key>Offset</key>
+ <real>7.3333320617675781</real>
+ <key>Position</key>
+ <real>0.4865129292011261</real>
+ <key>RotationType</key>
+ <integer>0</integer>
+ </dict>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ <key>stroke</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400
+\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc
+
+\f0\fs24 \cf0 No}</string>
+ </dict>
+ <key>Wrap</key>
+ <string>NO</string>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>ControlPoints</key>
+ <array>
+ <string>{34.791667904111534, 0}</string>
+ <string>{-33.999994913736998, 0}</string>
+ </array>
+ <key>FontInfo</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>w</key>
+ <string>0</string>
+ </dict>
+ <key>Font</key>
+ <string>Helvetica</string>
+ <key>Size</key>
+ <real>12</real>
+ </dict>
+ <key>Head</key>
+ <dict>
+ <key>ID</key>
+ <integer>169409</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169410</integer>
+ <key>Points</key>
+ <array>
+ <string>{280.85416589389337, 398.88888549804574}</string>
+ <string>{327.47912214508739, 398.88888549804574}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Bezier</key>
+ <true/>
+ <key>Color</key>
+ <dict>
+ <key>b</key>
+ <string>0.0980392</string>
+ <key>g</key>
+ <string>0.0980392</string>
+ <key>r</key>
+ <string>0.0980392</string>
+ </dict>
+ <key>HeadArrow</key>
+ <string>SharpArrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169404</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{327.47912214508739, 384.38888549804574}, {156.62496948242188, 29}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169409</integer>
+ <key>Magnets</key>
+ <array>
+ <string>{0, 1}</string>
+ <string>{0, -1}</string>
+ <string>{1, 0}</string>
+ <string>{-1, 0}</string>
+ </array>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>b</key>
+ <string>0.756045</string>
+ <key>g</key>
+ <string>0.75004</string>
+ <key>r</key>
+ <string>0.994455</string>
+ </dict>
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400
+\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc
+
+\f0\fs20 \cf0 Return the
+\b Forbidden View}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{175.11595161998204, 438.9999954213917}, {30, 24}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>FitText</key>
+ <string>YES</string>
+ <key>Flow</key>
+ <string>Resize</string>
+ <key>FontInfo</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>w</key>
+ <string>0</string>
+ </dict>
+ <key>Font</key>
+ <string>Helvetica</string>
+ <key>Size</key>
+ <real>12</real>
+ </dict>
+ <key>ID</key>
+ <integer>169408</integer>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ <key>stroke</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400
+\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc
+
+\f0\fs24 \cf0 Yes}</string>
+ </dict>
+ <key>Wrap</key>
+ <string>NO</string>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>ControlPoints</key>
+ <array>
+ <string>{0, 6.9840087890625}</string>
+ <string>{0, -9}</string>
+ </array>
+ <key>FontInfo</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>w</key>
+ <string>0</string>
+ </dict>
+ <key>Font</key>
+ <string>Helvetica</string>
+ <key>Size</key>
+ <real>12</real>
+ </dict>
+ <key>Head</key>
+ <dict>
+ <key>ID</key>
+ <integer>169412</integer>
+ <key>Info</key>
+ <integer>2</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169407</integer>
+ <key>Points</key>
+ <array>
+ <string>{202.04165267944353, 437.33333079020139}</string>
+ <string>{202.04165903727204, 471.55557886759294}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Bezier</key>
+ <true/>
+ <key>Color</key>
+ <dict>
+ <key>b</key>
+ <string>0.0980392</string>
+ <key>g</key>
+ <string>0.0980392</string>
+ <key>r</key>
+ <string>0.0980392</string>
+ </dict>
+ <key>HeadArrow</key>
+ <string>SharpArrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169404</integer>
+ <key>Info</key>
+ <integer>1</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{171.708317756653, 329.24978243601743}, {30, 24}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>FitText</key>
+ <string>YES</string>
+ <key>Flow</key>
+ <string>Resize</string>
+ <key>FontInfo</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>w</key>
+ <string>0</string>
+ </dict>
+ <key>Font</key>
+ <string>Helvetica</string>
+ <key>Size</key>
+ <real>12</real>
+ </dict>
+ <key>ID</key>
+ <integer>169406</integer>
+ <key>Line</key>
+ <dict>
+ <key>ID</key>
+ <integer>169405</integer>
+ <key>Offset</key>
+ <real>-15.333334922790527</real>
+ <key>Position</key>
+ <real>0.45895844697952271</real>
+ <key>RotationType</key>
+ <integer>0</integer>
+ </dict>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ <key>stroke</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400
+\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc
+
+\f0\fs24 \cf0 Yes}</string>
+ </dict>
+ <key>Wrap</key>
+ <string>NO</string>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>ControlPoints</key>
+ <array>
+ <string>{0, 6.9840087890625}</string>
+ <string>{0, -9}</string>
+ </array>
+ <key>FontInfo</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>w</key>
+ <string>0</string>
+ </dict>
+ <key>Font</key>
+ <string>Helvetica</string>
+ <key>Size</key>
+ <real>12</real>
+ </dict>
+ <key>Head</key>
+ <dict>
+ <key>ID</key>
+ <integer>169404</integer>
+ <key>Info</key>
+ <integer>2</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169405</integer>
+ <key>Points</key>
+ <array>
+ <string>{202.04165267944353, 326.72223360222029}</string>
+ <string>{202.04165267944353, 360.44446818033811}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Bezier</key>
+ <true/>
+ <key>Color</key>
+ <dict>
+ <key>b</key>
+ <string>0.0980392</string>
+ <key>g</key>
+ <string>0.0980392</string>
+ <key>r</key>
+ <string>0.0980392</string>
+ </dict>
+ <key>HeadArrow</key>
+ <string>SharpArrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>3</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{123.72916793823259, 360.44446818033811}, {156.62496948242188, 76.888862609863281}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169404</integer>
+ <key>Magnets</key>
+ <array>
+ <string>{0, 1}</string>
+ <string>{0, -1}</string>
+ <string>{1, 0}</string>
+ <string>{-1, 0}</string>
+ </array>
+ <key>Shape</key>
+ <string>Diamond</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>b</key>
+ <string>0.422927</string>
+ <key>g</key>
+ <string>1</string>
+ <key>r</key>
+ <string>1</string>
+ </dict>
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400
+\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc
+
+\f0\fs20 \cf0 Current user has
+\b authorization
+\b0 to invoke the view callable?}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{283.07625736262997, 281.88889694213805}, {26, 24}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>FitText</key>
+ <string>YES</string>
+ <key>Flow</key>
+ <string>Resize</string>
+ <key>FontInfo</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>w</key>
+ <string>0</string>
+ </dict>
+ <key>Font</key>
+ <string>Helvetica</string>
+ <key>Size</key>
+ <real>12</real>
+ </dict>
+ <key>ID</key>
+ <integer>169403</integer>
+ <key>Line</key>
+ <dict>
+ <key>ID</key>
+ <integer>169402</integer>
+ <key>Offset</key>
+ <real>7.3333320617675781</real>
+ <key>Position</key>
+ <real>0.4865129292011261</real>
+ <key>RotationType</key>
+ <integer>0</integer>
+ </dict>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ <key>stroke</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400
+\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc
+
+\f0\fs24 \cf0 No}</string>
+ </dict>
+ <key>Wrap</key>
+ <string>NO</string>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>ControlPoints</key>
+ <array>
+ <string>{34.791667904111534, 0}</string>
+ <string>{-33.999994913736998, 0}</string>
+ </array>
+ <key>FontInfo</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>w</key>
+ <string>0</string>
+ </dict>
+ <key>Font</key>
+ <string>Helvetica</string>
+ <key>Size</key>
+ <real>12</real>
+ </dict>
+ <key>Head</key>
+ <dict>
+ <key>ID</key>
+ <integer>169401</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169402</integer>
+ <key>Points</key>
+ <array>
+ <string>{265.20833208871704, 301.22222900390562}</string>
+ <string>{327.47911580403627, 301.22222900390562}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Bezier</key>
+ <true/>
+ <key>Color</key>
+ <dict>
+ <key>b</key>
+ <string>0.0980392</string>
+ <key>g</key>
+ <string>0.0980392</string>
+ <key>r</key>
+ <string>0.0980392</string>
+ </dict>
+ <key>HeadArrow</key>
+ <string>SharpArrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>3</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{327.47911580403627, 286.72222900390562}, {156.62496948242188, 29}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169401</integer>
+ <key>Magnets</key>
+ <array>
+ <string>{0, 1}</string>
+ <string>{0, -1}</string>
+ <string>{1, 0}</string>
+ <string>{-1, 0}</string>
+ </array>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>b</key>
+ <string>0.756045</string>
+ <key>g</key>
+ <string>0.75004</string>
+ <key>r</key>
+ <string>0.994455</string>
+ </dict>
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400
+\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc
+
+\f0\fs20 \cf0 Return the
+\b Not Found View}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>ControlPoints</key>
+ <array>
+ <string>{0, 6.9840087890625}</string>
+ <string>{0, -9}</string>
+ </array>
+ <key>FontInfo</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>w</key>
+ <string>0</string>
+ </dict>
+ <key>Font</key>
+ <string>Helvetica</string>
+ <key>Size</key>
+ <real>12</real>
+ </dict>
+ <key>Head</key>
+ <dict>
+ <key>ID</key>
+ <integer>3</integer>
+ <key>Info</key>
+ <integer>2</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169400</integer>
+ <key>Points</key>
+ <array>
+ <string>{202.04165903727255, 251}</string>
+ <string>{202.04165776570633, 276.22223154703772}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Bezier</key>
+ <true/>
+ <key>Color</key>
+ <dict>
+ <key>b</key>
+ <string>0.0980392</string>
+ <key>g</key>
+ <string>0.0980392</string>
+ <key>r</key>
+ <string>0.0980392</string>
+ </dict>
+ <key>HeadArrow</key>
+ <string>SharpArrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169393</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{139.37498982747391, 276.22223154703778}, {125.33333587646484, 50}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>3</integer>
+ <key>Magnets</key>
+ <array>
+ <string>{0, 1}</string>
+ <string>{0, -1}</string>
+ <string>{1, 0}</string>
+ <string>{-1, 0}</string>
+ </array>
+ <key>Shape</key>
+ <string>Diamond</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>b</key>
+ <string>0.422927</string>
+ <key>g</key>
+ <string>1</string>
+ <key>r</key>
+ <string>1</string>
+ </dict>
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400
+\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc
+
+\f0\fs20 \cf0 View callable found?}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>ControlPoints</key>
+ <array>
+ <string>{0, 6.9840087890625}</string>
+ <string>{0, -9}</string>
+ </array>
+ <key>FontInfo</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>w</key>
+ <string>0</string>
+ </dict>
+ <key>Font</key>
+ <string>Helvetica</string>
+ <key>Size</key>
+ <real>12</real>
+ </dict>
+ <key>Head</key>
+ <dict>
+ <key>ID</key>
+ <integer>169393</integer>
+ <key>Info</key>
+ <integer>2</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169396</integer>
+ <key>Points</key>
+ <array>
+ <string>{202.04165903727255, 196.77777862548834}</string>
+ <string>{202.04165903727255, 222}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Bezier</key>
+ <true/>
+ <key>Color</key>
+ <dict>
+ <key>b</key>
+ <string>0.0980392</string>
+ <key>g</key>
+ <string>0.0980392</string>
+ <key>r</key>
+ <string>0.0980392</string>
+ </dict>
+ <key>HeadArrow</key>
+ <string>SharpArrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169392</integer>
+ <key>Info</key>
+ <integer>1</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>ControlPoints</key>
+ <array>
+ <string>{0, 6.9840087890625}</string>
+ <string>{0, -9}</string>
+ </array>
+ <key>FontInfo</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>w</key>
+ <string>0</string>
+ </dict>
+ <key>Font</key>
+ <string>Helvetica</string>
+ <key>Size</key>
+ <real>12</real>
+ </dict>
+ <key>Head</key>
+ <dict>
+ <key>ID</key>
+ <integer>169392</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169395</integer>
+ <key>Points</key>
+ <array>
+ <string>{202.04165903727255, 142.55555725097662}</string>
+ <string>{202.04165903727255, 167.77777862548834}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Bezier</key>
+ <true/>
+ <key>Color</key>
+ <dict>
+ <key>b</key>
+ <string>0.0980392</string>
+ <key>g</key>
+ <string>0.0980392</string>
+ <key>r</key>
+ <string>0.0980392</string>
+ </dict>
+ <key>HeadArrow</key>
+ <string>SharpArrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>169391</integer>
+ <key>Info</key>
+ <integer>1</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>ControlPoints</key>
+ <array>
+ <string>{0, 6.9840087890625}</string>
+ <string>{0, -9}</string>
+ </array>
+ <key>FontInfo</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>w</key>
+ <string>0</string>
+ </dict>
+ <key>Font</key>
+ <string>Helvetica</string>
+ <key>Size</key>
+ <real>12</real>
+ </dict>
+ <key>Head</key>
+ <dict>
+ <key>ID</key>
+ <integer>169391</integer>
+ </dict>
+ <key>ID</key>
+ <integer>169385</integer>
+ <key>Points</key>
+ <array>
+ <string>{202.04165903727255, 82.666667938232479}</string>
+ <string>{202.04165903727255, 107.88888931274418}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Bezier</key>
+ <true/>
+ <key>Color</key>
+ <dict>
+ <key>b</key>
+ <string>0.0980392</string>
+ <key>g</key>
+ <string>0.0980392</string>
+ <key>r</key>
+ <string>0.0980392</string>
+ </dict>
+ <key>HeadArrow</key>
+ <string>SharpArrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>19</integer>
+ <key>Info</key>
+ <integer>1</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{104.41666666666708, 222}, {195.24998474121094, 29}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169393</integer>
+ <key>Magnets</key>
+ <array>
+ <string>{0, 1}</string>
+ <string>{0, -1}</string>
+ <string>{1, 0}</string>
+ <string>{-1, 0}</string>
+ </array>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>b</key>
+ <string>0.815377</string>
+ <key>g</key>
+ <string>1</string>
+ <key>r</key>
+ <string>0.820561</string>
+ </dict>
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400
+\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc
+
+\f0\fs20 \cf0 Look up a
+\b view callable
+\b0 in the
+\b registry
+\b0 using the
+\b context
+\b0 and
+\b view name}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{104.41666666666708, 167.77777862548834}, {195.24998474121094, 29}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169392</integer>
+ <key>Magnets</key>
+ <array>
+ <string>{0, 1}</string>
+ <string>{0, -1}</string>
+ <string>{1, 0}</string>
+ <string>{-1, 0}</string>
+ </array>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>b</key>
+ <string>0.815377</string>
+ <key>g</key>
+ <string>1</string>
+ <key>r</key>
+ <string>0.820561</string>
+ </dict>
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400
+\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc
+
+\f0\b\fs20 \cf0 Traversal
+\b0 locates\
+the
+\b context
+\b0 and
+\b view name}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{104.41666666666708, 107.88888931274418}, {195.24998474121094, 34.666667938232422}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>169391</integer>
+ <key>Magnets</key>
+ <array>
+ <string>{0, 1}</string>
+ <string>{0, -1}</string>
+ <string>{1, 0}</string>
+ <string>{-1, 0}</string>
+ </array>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>b</key>
+ <string>0.815377</string>
+ <key>g</key>
+ <string>1</string>
+ <key>r</key>
+ <string>0.820561</string>
+ </dict>
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400
+\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc
+
+\f0\fs20 \cf0 Traverse the model graph\
+from the
+\b root
+\b0 using the
+\b path}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{104.41666666666708, 48.000000000000043}, {195.24998474121094, 34.666667938232422}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>19</integer>
+ <key>Magnets</key>
+ <array>
+ <string>{0, 1}</string>
+ <string>{0, -1}</string>
+ <string>{1, 0}</string>
+ <string>{-1, 0}</string>
+ </array>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>b</key>
+ <string>0.815377</string>
+ <key>g</key>
+ <string>1</string>
+ <key>r</key>
+ <string>0.820561</string>
+ </dict>
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400
+\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc
+
+\f0\fs20 \cf0 Obtain a root object from the
+\b root factory}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{229.04165903727255, 20.000000000000934}, {90, 14}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>FitText</key>
+ <string>YES</string>
+ <key>Flow</key>
+ <string>Resize</string>
+ <key>FontInfo</key>
+ <dict>
+ <key>Font</key>
+ <string>Helvetica</string>
+ <key>Size</key>
+ <real>12</real>
+ </dict>
+ <key>ID</key>
+ <integer>169390</integer>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ <key>stroke</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Pad</key>
+ <integer>0</integer>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf400
+\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc
+
+\f0\b\fs24 \cf0 &lt;%Canvas%&gt;}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ <key>Wrap</key>
+ <string>NO</string>
+ </dict>
+ </array>
+ <key>GridInfo</key>
+ <dict/>
+ <key>GuidesLocked</key>
+ <string>NO</string>
+ <key>GuidesVisible</key>
+ <string>YES</string>
+ <key>HPages</key>
+ <integer>1</integer>
+ <key>ImageCounter</key>
+ <integer>1</integer>
+ <key>KeepToScale</key>
+ <false/>
+ <key>Layers</key>
+ <array>
+ <dict>
+ <key>Lock</key>
+ <string>NO</string>
+ <key>Name</key>
+ <string>Layer 1</string>
+ <key>Print</key>
+ <string>YES</string>
+ <key>View</key>
+ <string>YES</string>
+ </dict>
+ </array>
+ <key>LayoutInfo</key>
+ <dict>
+ <key>Animate</key>
+ <string>NO</string>
+ <key>circoMinDist</key>
+ <real>18</real>
+ <key>circoSeparation</key>
+ <real>0.0</real>
+ <key>layoutEngine</key>
+ <string>dot</string>
+ <key>neatoSeparation</key>
+ <real>0.0</real>
+ <key>twopiSeparation</key>
+ <real>0.0</real>
+ </dict>
+ <key>LinksVisible</key>
+ <string>NO</string>
+ <key>MagnetsVisible</key>
+ <string>NO</string>
+ <key>MasterSheets</key>
+ <array/>
+ <key>ModificationDate</key>
+ <string>2014-12-01 09:19:51 +0000</string>
+ <key>Modifier</key>
+ <string>Steve Piercy</string>
+ <key>NotesVisible</key>
+ <string>NO</string>
+ <key>Orientation</key>
+ <integer>2</integer>
+ <key>OriginVisible</key>
+ <string>NO</string>
+ <key>PageBreaks</key>
+ <string>YES</string>
+ <key>PrintInfo</key>
+ <dict>
+ <key>NSBottomMargin</key>
+ <array>
+ <string>float</string>
+ <string>41</string>
+ </array>
+ <key>NSHorizonalPagination</key>
+ <array>
+ <string>coded</string>
+ <string>BAtzdHJlYW10eXBlZIHoA4QBQISEhAhOU051bWJlcgCEhAdOU1ZhbHVlAISECE5TT2JqZWN0AIWEASqEhAFxlwCG</string>
+ </array>
+ <key>NSLeftMargin</key>
+ <array>
+ <string>float</string>
+ <string>18</string>
+ </array>
+ <key>NSPaperSize</key>
+ <array>
+ <string>size</string>
+ <string>{612, 792}</string>
+ </array>
+ <key>NSPrintReverseOrientation</key>
+ <array>
+ <string>int</string>
+ <string>0</string>
+ </array>
+ <key>NSRightMargin</key>
+ <array>
+ <string>float</string>
+ <string>18</string>
+ </array>
+ <key>NSTopMargin</key>
+ <array>
+ <string>float</string>
+ <string>18</string>
+ </array>
+ </dict>
+ <key>PrintOnePage</key>
+ <false/>
+ <key>ReadOnly</key>
+ <string>NO</string>
+ <key>RowAlign</key>
+ <integer>1</integer>
+ <key>RowSpacing</key>
+ <real>36</real>
+ <key>SheetTitle</key>
+ <string>Pyramid Router</string>
+ <key>SmartAlignmentGuidesActive</key>
+ <string>YES</string>
+ <key>SmartDistanceGuidesActive</key>
+ <string>YES</string>
+ <key>UniqueID</key>
+ <integer>1</integer>
+ <key>UseEntirePage</key>
+ <false/>
+ <key>VPages</key>
+ <integer>1</integer>
+ <key>WindowInfo</key>
+ <dict>
+ <key>CurrentSheet</key>
+ <integer>0</integer>
+ <key>ExpandedCanvases</key>
+ <array>
+ <dict>
+ <key>name</key>
+ <string>Pyramid Router</string>
+ </dict>
+ </array>
+ <key>Frame</key>
+ <string>{{96, 20}, {1076, 1286}}</string>
+ <key>ListView</key>
+ <false/>
+ <key>OutlineWidth</key>
+ <integer>142</integer>
+ <key>RightSidebar</key>
+ <true/>
+ <key>ShowRuler</key>
+ <true/>
+ <key>Sidebar</key>
+ <true/>
+ <key>SidebarWidth</key>
+ <integer>120</integer>
+ <key>VisibleRegion</key>
+ <string>{{8, -10}, {532, 754.66666666666663}}</string>
+ <key>Zoom</key>
+ <real>1.5</real>
+ <key>ZoomValues</key>
+ <array>
+ <array>
+ <string>Pyramid Router</string>
+ <real>1.5</real>
+ <real>1</real>
+ </array>
+ </array>
+ </dict>
+</dict>
+</plist>
diff --git a/docs/_static/pyramid_router.png b/docs/_static/pyramid_router.png
new file mode 100644
index 000000000..3c9f81158
--- /dev/null
+++ b/docs/_static/pyramid_router.png
Binary files differ
diff --git a/docs/_static/pyramid_router.svg b/docs/_static/pyramid_router.svg
new file mode 100644
index 000000000..1537777c9
--- /dev/null
+++ b/docs/_static/pyramid_router.svg
@@ -0,0 +1,3 @@
+<?xml version="1.0"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="93 11 403 558" width="403pt" height="558pt"><metadata xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:date>2014-12-01 09:19Z</dc:date><!-- Produced by OmniGraffle Professional 5.4.4 --></metadata><defs><font-face font-family="Helvetica" font-size="12" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.316406" slope="0" x-height="532.22656" cap-height="719.72656" ascent="770.01953" descent="-229.98047" font-weight="bold"><font-face-src><font-face-name name="Helvetica-Bold"/></font-face-src></font-face><font-face font-family="Helvetica" font-size="10" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.316406" slope="0" x-height="522.94922" cap-height="717.28516" ascent="770.01953" descent="-229.98047" font-weight="500"><font-face-src><font-face-name name="Helvetica"/></font-face-src></font-face><font-face font-family="Helvetica" font-size="10" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.316406" slope="0" x-height="532.22656" cap-height="719.72656" ascent="770.01953" descent="-229.98047" font-weight="bold"><font-face-src><font-face-name name="Helvetica-Bold"/></font-face-src></font-face><marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="SharpArrow_Marker" viewBox="-4 -4 10 8" markerWidth="10" markerHeight="8" color="#191919"><g><path d="M 5 0 L -3 -3 L 0 0 L 0 0 L -3 3 Z" fill="currentColor" stroke="currentColor" stroke-width="1"/></g></marker><font-face font-family="Helvetica" font-size="12" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.316406" slope="0" x-height="522.94922" cap-height="717.28516" ascent="770.01953" descent="-229.98047" font-weight="500"><font-face-src><font-face-name name="Helvetica"/></font-face-src></font-face></defs><g stroke="none" stroke-opacity="1" stroke-dasharray="none" fill="none" fill-opacity="1"><title>Pyramid Router</title><rect fill="white" width="576" height="733"/><g><title>Layer 1</title><text transform="translate(229.04166 20)" fill="black"><tspan font-family="Helvetica" font-size="12" font-weight="bold" x=".32226562" y="11" textLength="89.35547">Pyramid Router</tspan></text><rect x="104.416667" y="48" width="195.24998" height="34.666668" fill="#d2ffd0"/><rect x="104.416667" y="48" width="195.24998" height="34.666668" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(109.416667 59.333334)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x=".088371277" y="10" textLength="129.51172">Obtain a root object from the </tspan><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="129.60009" y="10" textLength="55.561523">root factory</tspan></text><rect x="104.416667" y="107.88889" width="195.24998" height="34.666668" fill="#d2ffd0"/><rect x="104.416667" y="107.88889" width="195.24998" height="34.666668" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(109.416667 113.22222)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="35.557121" y="10" textLength="6.1083984">T</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="41.29931" y="10" textLength="108.393555">raverse the model graph</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="29.551262" y="22" textLength="39.458008">from the </tspan><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="69.00927" y="22" textLength="19.438477">root</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="88.447746" y="22" textLength="46.142578"> using the </tspan><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="134.590324" y="22" textLength="21.108398">path</tspan></text><rect x="104.416667" y="167.77778" width="195.24998" height="29" fill="#d2ffd0"/><rect x="104.416667" y="167.77778" width="195.24998" height="29" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(109.416667 170.27778)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="53.428215" y="10" textLength="6.1083984">T</tspan><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="58.98974" y="10" textLength="38.36914">raversal</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="97.35888" y="10" textLength="34.46289"> locates</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="30.093254" y="22" textLength="16.6796875">the </tspan><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="46.772942" y="22" textLength="35.561523">context</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="82.334465" y="22" textLength="22.241211"> and </tspan><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="104.575676" y="22" textLength="50.581055">view name</tspan></text><rect x="104.416667" y="222" width="195.24998" height="29" fill="#d2ffd0"/><rect x="104.416667" y="222" width="195.24998" height="29" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(109.416667 224.5)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="5.3471603" y="10" textLength="46.704102">Look up a </tspan><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="52.051262" y="10" textLength="61.14746">view callable</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="113.19872" y="10" textLength="30.019531"> in the </tspan><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="143.21825" y="10" textLength="36.68457">registry</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="179.90282" y="10" textLength="2.7783203"> </tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="16.750969" y="22" textLength="43.364258">using the </tspan><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="60.115227" y="22" textLength="35.561523">context</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="95.67675" y="22" textLength="22.241211"> and </tspan><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="117.91796" y="22" textLength="50.581055">view name</tspan></text><path d="M 202.04166 82.66667 C 202.04166 87.86648 202.04166 94.31586 202.04166 100.98615" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><path d="M 202.04166 142.55556 C 202.04166 147.75537 202.04166 154.20475 202.04166 160.87504" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><path d="M 202.04166 196.77778 C 202.04166 201.97759 202.04166 208.42697 202.04166 215.09726" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><path d="M 202.04166 276.22223 L 264.70833 301.22223 L 202.04166 326.22223 L 139.37499 301.22223 Z" fill="#ffff6c"/><path d="M 202.04166 276.22223 L 264.70833 301.22223 L 202.04166 326.22223 L 139.37499 301.22223 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(161.29499 288.72223)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="12.905763" y="10" textLength="6.669922">V</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="19.399903" y="10" textLength="54.472656">iew callable </tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="26.707032" y="22" textLength="30.585938">found?</tspan></text><path d="M 202.04166 251 C 202.04166 256.19981 202.04166 262.6492 202.04166 269.31949" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><rect x="327.47912" y="286.72223" width="156.62497" height="29" fill="#fec0c1"/><rect x="327.47912" y="286.72223" width="156.62497" height="29" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(332.47912 295.22223)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="10.89061" y="10" textLength="49.472656">Return the </tspan><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="60.363266" y="10" textLength="59.42871">Not Found V</tspan><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="119.616196" y="10" textLength="16.118164">iew</tspan></text><path d="M 265.20833 301.22223 C 297.4314 301.22223 294.2168 301.22223 320.5783 301.22223" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(288.07626 286.8889)" fill="black"><tspan font-family="Helvetica" font-size="12" font-weight="500" fill="black" x=".33007812" y="11" textLength="15.339844">No</tspan></text><path d="M 202.04165 360.44447 L 280.35414 398.8889 L 202.04165 437.33333 L 123.72917 398.8889 Z" fill="#ffff6c"/><path d="M 202.04165 360.44447 L 280.35414 398.8889 L 202.04165 437.33333 L 123.72917 398.8889 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(149.87354 380.12)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="16.495594" y="10" textLength="77.25586">Current user has </tspan><tspan font-family="Helvetica" font-size="10" font-weight="bold" x=".9462776" y="22" textLength="62.773438">authorization</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="63.719715" y="22" textLength="45.581055"> to invoke </tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="14.26659" y="34" textLength="78.935547">the view callable?</tspan></text><path d="M 202.04165 326.72223 C 202.04165 332.1894 202.04165 344.2467 202.04165 353.54354" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(176.70832 334.24978)" fill="black"><tspan font-family="Helvetica" font-size="12" font-weight="500" fill="black" x=".21191406" y="11" textLength="8.0039062">Y</tspan><tspan font-family="Helvetica" font-size="12" font-weight="500" fill="black" x="7.114258" y="11" textLength="12.673828">es</tspan></text><path d="M 202.04165 437.33333 C 202.04165 442.81278 202.04166 455.21977 202.04166 464.65762" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(180.11595 444)" fill="black"><tspan font-family="Helvetica" font-size="12" font-weight="500" x=".21191406" y="11" textLength="8.0039062">Y</tspan><tspan font-family="Helvetica" font-size="12" font-weight="500" x="7.114258" y="11" textLength="12.673828">es</tspan></text><rect x="327.47912" y="384.38889" width="156.62497" height="29" fill="#fec0c1"/><rect x="327.47912" y="384.38889" width="156.62497" height="29" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(332.47912 392.88889)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="11.439926" y="10" textLength="49.472656">Return the </tspan><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="60.912582" y="10" textLength="58.330078">Forbidden V</tspan><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="119.06688" y="10" textLength="16.118164">iew</tspan></text><path d="M 280.85417 398.88889 C 312.9685 398.88889 296.55343 398.88889 320.57617 398.88889" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(296.21563 384.55555)" fill="black"><tspan font-family="Helvetica" font-size="12" font-weight="500" x=".33007812" y="11" textLength="15.339844">No</tspan></text><rect x="104.416667" y="471.55558" width="195.24998" height="29" fill="#ffff6c"/><rect x="104.416667" y="471.55558" width="195.24998" height="29" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(109.416667 474.05558)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="36.201653" y="10" textLength="48.920898">Invoke the </tspan><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="85.12255" y="10" textLength="61.14746">view callable</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="146.27001" y="10" textLength="2.7783203">,</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="35.100578" y="22" textLength="70.585938">which returns a </tspan><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="105.686516" y="22" textLength="44.46289">response</tspan></text><rect x="104.416667" y="528.77776" width="195.24998" height="29" fill="#dfbeff"/><rect x="104.416667" y="528.77776" width="195.24998" height="29" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(109.416667 537.27776)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="45.65722" y="10" textLength="49.472656">Return the </tspan><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="95.129875" y="10" textLength="44.46289">response</tspan></text><path d="M 202.04166 501.05558 C 202.04166 506.35088 202.04166 514.3792 202.04166 521.8749" marker-end="url(#SharpArrow_Marker)" stroke="#191919" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/></g></g></svg>
diff --git a/docs/_themes b/docs/_themes
-Subproject 3bec9280a6cedb15e97e5899021aa8d723c2538
+Subproject b14bf8c2a0d95ae8e3d38d07ad3721370ae6f3f
diff --git a/docs/api/index.rst b/docs/api/index.rst
new file mode 100644
index 000000000..cb38aa0b2
--- /dev/null
+++ b/docs/api/index.rst
@@ -0,0 +1,12 @@
+.. _html_api_documentation:
+
+API Documentation
+=================
+
+Comprehensive reference material for every public API exposed by :app:`Pyramid`:
+
+.. toctree::
+ :maxdepth: 1
+ :glob:
+
+ *
diff --git a/docs/api/interfaces.rst b/docs/api/interfaces.rst
index a62976d8a..de2a664a4 100644
--- a/docs/api/interfaces.rst
+++ b/docs/api/interfaces.rst
@@ -56,6 +56,9 @@ Other Interfaces
.. autointerface:: IRenderer
:members:
+ .. autointerface:: IResponseFactory
+ :members:
+
.. autointerface:: IViewMapperFactory
:members:
diff --git a/docs/api/request.rst b/docs/api/request.rst
index dd68fa09c..b325ad076 100644
--- a/docs/api/request.rst
+++ b/docs/api/request.rst
@@ -369,3 +369,4 @@
that used as ``request.GET``, ``request.POST``, and ``request.params``),
see :class:`pyramid.interfaces.IMultiDict`.
+.. autofunction:: apply_request_extensions(request)
diff --git a/docs/api/static.rst b/docs/api/static.rst
index 543e526ad..b6b279139 100644
--- a/docs/api/static.rst
+++ b/docs/api/static.rst
@@ -9,6 +9,12 @@
:members:
:inherited-members:
+ .. autoclass:: PathSegmentCacheBuster
+ :members:
+
+ .. autoclass:: QueryStringCacheBuster
+ :members:
+
.. autoclass:: PathSegmentMd5CacheBuster
:members:
diff --git a/docs/glossary.rst b/docs/glossary.rst
index 01300a0be..9c0ea8598 100644
--- a/docs/glossary.rst
+++ b/docs/glossary.rst
@@ -16,6 +16,11 @@ Glossary
An object which, provided a :term:`WSGI` environment as a single
positional argument, returns a Pyramid-compatible request.
+ response factory
+ An object which, provided a :term:`request` as a single positional
+ argument, returns a Pyramid-compatible response. See
+ :class:`pyramid.interfaces.IResponseFactory`.
+
response
An object returned by a :term:`view callable` that represents response
data returned to the requesting user agent. It must implement the
diff --git a/docs/index.rst b/docs/index.rst
index ac16ff237..fc7560e8f 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -135,8 +135,6 @@ platforms.
tutorials/wiki/index.rst
tutorials/modwsgi/index.rst
-.. _html_api_documentation:
-
API Documentation
=================
@@ -146,6 +144,7 @@ Comprehensive reference material for every public API exposed by :app:`Pyramid`:
:maxdepth: 1
:glob:
+ api/index
api/*
Change History
diff --git a/docs/narr/assets.rst b/docs/narr/assets.rst
index fc908c2b4..d6bc8cbb8 100644
--- a/docs/narr/assets.rst
+++ b/docs/narr/assets.rst
@@ -446,19 +446,20 @@ In order to implement your own cache buster, you can write your own class from
scratch which implements the :class:`~pyramid.interfaces.ICacheBuster`
interface. Alternatively you may choose to subclass one of the existing
implementations. One of the most likely scenarios is you'd want to change the
-way the asset token is generated. To do this just subclass an existing
-implementation and replace the :meth:`~pyramid.interfaces.ICacheBuster.token`
-method. Here is an example which just uses Git to get the hash of the
-currently checked out code:
+way the asset token is generated. To do this just subclass either
+:class:`~pyramid.static.PathSegmentCacheBuster` or
+:class:`~pyramid.static.QueryStringCacheBuster` and define a
+``tokenize(pathspec)`` method. Here is an example which just uses Git to get
+the hash of the currently checked out code:
.. code-block:: python
:linenos:
import os
import subprocess
- from pyramid.static import PathSegmentMd5CacheBuster
+ from pyramid.static import PathSegmentCacheBuster
- class GitCacheBuster(PathSegmentMd5CacheBuster):
+ class GitCacheBuster(PathSegmentCacheBuster):
"""
Assuming your code is installed as a Git checkout, as opposed to as an
egg from an egg repository like PYPI, you can use this cachebuster to
@@ -470,7 +471,7 @@ currently checked out code:
['git', 'rev-parse', 'HEAD'],
cwd=here).strip()
- def token(self, pathspec):
+ def tokenize(self, pathspec):
return self.sha1
Choosing a Cache Buster
diff --git a/docs/narr/commandline.rst b/docs/narr/commandline.rst
index 4f16617c4..1fe2d9278 100644
--- a/docs/narr/commandline.rst
+++ b/docs/narr/commandline.rst
@@ -312,24 +312,60 @@ For example:
:linenos:
$ $VENV/bin/proutes development.ini
- Name Pattern View
- ---- ------- ----
- home / <function my_view>
- home2 / <function my_view>
- another /another None
- static/ static/*subpath <static_view object>
- catchall /*subpath <function static_view>
-
-``proutes`` generates a table with three columns: *Name*, *Pattern*,
+ Name Pattern View
+ ---- ------- ----
+ debugtoolbar /_debug_toolbar/*subpath <wsgiapp> *
+ __static/ /static/*subpath dummy_starter:static/ *
+ __static2/ /static2/*subpath /var/www/static/ *
+ __pdt_images/ /pdt_images/*subpath pyramid_debugtoolbar:static/img/ *
+ a / <unknown> *
+ no_view_attached / <unknown> *
+ route_and_view_attached / app1.standard_views.route_and_view_attached *
+ method_conflicts /conflicts app1.standard_conflicts <route mismatch>
+ multiview /multiview app1.standard_views.multiview GET,PATCH
+ not_post /not_post app1.standard_views.multview !POST,*
+
+``proutes`` generates a table with four columns: *Name*, *Pattern*, *Method*,
and *View*. The items listed in the
Name column are route names, the items listed in the Pattern column are route
patterns, and the items listed in the View column are representations of the
view callable that will be invoked when a request matches the associated
-route pattern. The view column may show ``None`` if no associated view
+route pattern. The view column may show ``<unknown>`` if no associated view
callable could be found. If no routes are configured within your
application, nothing will be printed to the console when ``proutes``
is executed.
+It is convenient when using the ``proutes`` often to configure which columns
+and the order you would like to view them. To facilitate this, ``proutes`` will
+look for a special ``[proutes]`` section in your INI file and use those as
+defaults.
+
+For example you may remove request method and place the view first:
+
+.. code-block:: text
+ :linenos:
+
+ [proutes]
+ format = view
+ name
+ pattern
+
+You can also separate the formats with commas or spaces:
+
+.. code-block:: text
+ :linenos:
+
+ [proutes]
+ format = view name pattern
+
+ [proutes]
+ format = view, name, pattern
+
+If you want to temporarily configure the columns and order there is the
+``--format`` which is a comma separated list of columns you want to include. The
+current available formats are ``name``, ``pattern``, ``view``, and ``method``.
+
+
.. index::
pair: tweens; printing
single: ptweens
diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst
index 4da36e730..4fd7670b9 100644
--- a/docs/narr/hooks.rst
+++ b/docs/narr/hooks.rst
@@ -349,6 +349,55 @@ We attach and cache an object named ``extra`` to the ``request`` object.
the property
.. index::
+ single: response factory
+
+.. _changing_the_response_factory:
+
+Changing the Response Factory
+-------------------------------
+
+.. versionadded:: 1.6
+
+Whenever :app:`Pyramid` returns a response from a view it creates a
+:term:`response` object. By default, an instance of the
+:class:`pyramid.response.Response` class is created to represent the response
+object.
+
+The factory that :app:`Pyramid` uses to create a response object instance can be
+changed by passing a :class:`pyramid.interfaces.IResponseFactory` argument to
+the constructor of the :term:`configurator`. This argument can be either a
+callable or a :term:`dotted Python name` representing a callable.
+
+The factory takes a single positional argument, which is a :term:`Request`
+object. The argument may be ``None``.
+
+.. code-block:: python
+ :linenos:
+
+ from pyramid.response import Response
+
+ class MyResponse(Response):
+ pass
+
+ config = Configurator(response_factory=lambda r: MyResponse())
+
+If you're doing imperative configuration, and you'd rather do it after you've
+already constructed a :term:`configurator` it can also be registered via the
+:meth:`pyramid.config.Configurator.set_response_factory` method:
+
+.. code-block:: python
+ :linenos:
+
+ from pyramid.config import Configurator
+ from pyramid.response import Response
+
+ class MyResponse(Response):
+ pass
+
+ config = Configurator()
+ config.set_response_factory(lambda r: MyResponse())
+
+.. index::
single: before render event
single: adding renderer globals
@@ -730,7 +779,7 @@ If you want to implement your own Response object instead of using the
:class:`pyramid.response.Response` object in any capacity at all, you'll have
to make sure the object implements every attribute and method outlined in
:class:`pyramid.interfaces.IResponse` and you'll have to ensure that it uses
-``zope.interface.implementer(IResponse)`` as a class decoratoror.
+``zope.interface.implementer(IResponse)`` as a class decorator.
.. code-block:: python
:linenos:
diff --git a/docs/narr/hybrid.rst b/docs/narr/hybrid.rst
index 4a3258d35..1c324d22b 100644
--- a/docs/narr/hybrid.rst
+++ b/docs/narr/hybrid.rst
@@ -453,7 +453,7 @@ commonly in route declarations that look like this:
.. code-block:: python
:linenos:
- from pryamid.static import static_view
+ from pyramid.static import static_view
www = static_view('mypackage:static', use_subpath=True)
diff --git a/docs/narr/introspector.rst b/docs/narr/introspector.rst
index a7bde4cf7..8caba522c 100644
--- a/docs/narr/introspector.rst
+++ b/docs/narr/introspector.rst
@@ -121,7 +121,7 @@ introspectables in categories not described here.
``subscriber``
The subscriber callable object (the resolution of the ``subscriber``
- argument passed to ``add_susbcriber``).
+ argument passed to ``add_subscriber``).
``interfaces``
@@ -137,12 +137,12 @@ introspectables in categories not described here.
``predicates``
The predicate objects created as the result of passing predicate arguments
- to ``add_susbcriber``
+ to ``add_subscriber``
``derived_predicates``
Wrappers around the predicate objects created as the result of passing
- predicate arguments to ``add_susbcriber`` (to be used when predicates take
+ predicate arguments to ``add_subscriber`` (to be used when predicates take
only one value but must be passed more than one).
``response adapters``
@@ -450,9 +450,9 @@ introspectables in categories not described here.
The :class:`pyramid.interfaces.IRendererInfo` object which represents
this template's renderer.
-``view mapper``
+``view mappers``
- Each introspectable in the ``permissions`` category represents a call to
+ Each introspectable in the ``view mappers`` category represents a call to
:meth:`pyramid.config.Configurator.add_view` that has an explicit
``mapper`` argument to *or* a call to
:meth:`pyramid.config.Configurator.set_view_mapper`; each will have
@@ -481,8 +481,8 @@ introspectables in categories not described here.
``translation directories``
- Each introspectable in the ``asset overrides`` category represents an
- individual element in a ``specs`` argument passed to
+ Each introspectable in the ``translation directories`` category represents
+ an individual element in a ``specs`` argument passed to
:meth:`pyramid.config.Configurator.add_translation_dirs`; each will have
the following data.
@@ -511,7 +511,7 @@ introspectables in categories not described here.
``type``
- ``implict`` or ``explicit`` as a string.
+ ``implicit`` or ``explicit`` as a string.
``under``
diff --git a/docs/narr/logging.rst b/docs/narr/logging.rst
index c16673ae6..921883091 100644
--- a/docs/narr/logging.rst
+++ b/docs/narr/logging.rst
@@ -254,16 +254,15 @@ level unless they're explicitly set differently. Meaning the ``myapp.views``,
``myapp.models`` (and all your app's modules') loggers by default have an
effective level of ``DEBUG`` too.
-For more advanced filtering, the logging module provides a `Filter
-<http://docs.python.org/lib/node423.html>`_ object; however it cannot be used
-directly from the configuration file.
+For more advanced filtering, the logging module provides a
+:class:`logging.Filter` object; however it cannot be used directly from the
+configuration file.
-Advanced Configuration
+Advanced Configuration
----------------------
-To capture log output to a separate file, use a `FileHandler
-<http://docs.python.org/lib/node412.html>`_ (or a `RotatingFileHandler
-<http://docs.python.org/lib/node413.html>`_):
+To capture log output to a separate file, use :class:`logging.FileHandler` (or
+:class:`logging.handlers.RotatingFileHandler`):
.. code-block:: ini
@@ -317,8 +316,9 @@ output, etc., but not web traffic. For web traffic logging Paste provides the
:term:`middleware`. TransLogger produces logs in the `Apache Combined Log
Format <http://httpd.apache.org/docs/2.2/logs.html#combined>`_. But
TransLogger does not write to files, the Python logging system must be
-configured to do this. The Python FileHandler_ logging handler can be used
-alongside TransLogger to create an ``access.log`` file similar to Apache's.
+configured to do this. The Python :class:`logging.FileHandler` logging
+handler can be used alongside TransLogger to create an ``access.log`` file
+similar to Apache's.
Like any standard :term:`middleware` with a Paste entry point, TransLogger can
be configured to wrap your application using ``.ini`` file syntax. First,
diff --git a/docs/narr/router.rst b/docs/narr/router.rst
index e82b66801..6f90c70cc 100644
--- a/docs/narr/router.rst
+++ b/docs/narr/router.rst
@@ -9,7 +9,7 @@
Request Processing
==================
-.. image:: ../_static/pyramid_request_processing.svg
+.. image:: ../_static/pyramid_request_processing.*
:alt: Request Processing
Once a :app:`Pyramid` application is up and running, it is ready to accept
@@ -119,7 +119,8 @@ request enters a :app:`Pyramid` application through to the point that
#. The :term:`thread local` stack is popped.
-.. image:: router.png
+.. image:: ../_static/pyramid_router.*
+ :alt: Pyramid Router
This is a very high-level overview that leaves out various details. For more
detail about subsystems invoked by the :app:`Pyramid` router such as
diff --git a/docs/narr/sessions.rst b/docs/narr/sessions.rst
index 8da743a01..5c103405a 100644
--- a/docs/narr/sessions.rst
+++ b/docs/narr/sessions.rst
@@ -44,7 +44,7 @@ It is digitally signed, however, and thus its data cannot easily be
tampered with.
You can configure this session factory in your :app:`Pyramid` application
-by using the :meth:`pyramid.config.Configurator.set_session_factory`` method.
+by using the :meth:`pyramid.config.Configurator.set_session_factory` method.
.. code-block:: python
:linenos:
@@ -380,7 +380,7 @@ Checking CSRF Tokens Manually
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In request handling code, you can check the presence and validity of a CSRF
-token with :func:`pyramid.session.check_csrf_token(request)``. If the token is
+token with :func:`pyramid.session.check_csrf_token`. If the token is
valid, it will return ``True``, otherwise it will raise ``HTTPBadRequest``.
Optionally, you can specify ``raises=False`` to have the check return ``False``
instead of raising an exception.
diff --git a/pyramid/compat.py b/pyramid/compat.py
index bfa345b88..3aa191968 100644
--- a/pyramid/compat.py
+++ b/pyramid/compat.py
@@ -3,27 +3,27 @@ import platform
import sys
import types
-if platform.system() == 'Windows': # pragma: no cover
+if platform.system() == 'Windows': # pragma: no cover
WIN = True
-else: # pragma: no cover
+else: # pragma: no cover
WIN = False
-try: # pragma: no cover
+try: # pragma: no cover
import __pypy__
PYPY = True
-except: # pragma: no cover
+except: # pragma: no cover
__pypy__ = None
PYPY = False
try:
import cPickle as pickle
-except ImportError: # pragma: no cover
+except ImportError: # pragma: no cover
import pickle
# True if we are running on Python 3.
PY3 = sys.version_info[0] == 3
-if PY3: # pragma: no cover
+if PY3: # pragma: no cover
string_types = str,
integer_types = int,
class_types = type,
@@ -38,21 +38,23 @@ else:
binary_type = str
long = long
+
def text_(s, encoding='latin-1', errors='strict'):
""" If ``s`` is an instance of ``binary_type``, return
``s.decode(encoding, errors)``, otherwise return ``s``"""
if isinstance(s, binary_type):
return s.decode(encoding, errors)
- return s # pragma: no cover
+ return s # pragma: no cover
+
def bytes_(s, encoding='latin-1', errors='strict'):
""" If ``s`` is an instance of ``text_type``, return
``s.encode(encoding, errors)``, otherwise return ``s``"""
- if isinstance(s, text_type): # pragma: no cover
+ if isinstance(s, text_type): # pragma: no cover
return s.encode(encoding, errors)
return s
-if PY3: # pragma: no cover
+if PY3: # pragma: no cover
def ascii_native_(s):
if isinstance(s, text_type):
s = s.encode('ascii')
@@ -72,7 +74,7 @@ Python 2: If ``s`` is an instance of ``text_type``, return
"""
-if PY3: # pragma: no cover
+if PY3: # pragma: no cover
def native_(s, encoding='latin-1', errors='strict'):
""" If ``s`` is an instance of ``text_type``, return
``s``, otherwise return ``str(s, encoding, errors)``"""
@@ -95,7 +97,7 @@ Python 2: If ``s`` is an instance of ``text_type``, return
``s.encode(encoding, errors)``, otherwise return ``str(s)``
"""
-if PY3: # pragma: no cover
+if PY3: # pragma: no cover
from urllib import parse
urlparse = parse
from urllib.parse import quote as url_quote
@@ -112,18 +114,19 @@ else:
from urllib import unquote as url_unquote
from urllib import urlencode as url_encode
from urllib2 import urlopen as url_open
+
def url_unquote_text(v, encoding='utf-8', errors='replace'): # pragma: no cover
v = url_unquote(v)
return v.decode(encoding, errors)
+
def url_unquote_native(v, encoding='utf-8', errors='replace'): # pragma: no cover
return native_(url_unquote_text(v, encoding, errors))
-
-if PY3: # pragma: no cover
+
+if PY3: # pragma: no cover
import builtins
exec_ = getattr(builtins, "exec")
-
def reraise(tp, value, tb=None):
if value is None:
value = tp
@@ -131,10 +134,9 @@ if PY3: # pragma: no cover
raise value.with_traceback(tb)
raise value
-
del builtins
-else: # pragma: no cover
+else: # pragma: no cover
def exec_(code, globs=None, locs=None):
"""Execute code in a namespace."""
if globs is None:
@@ -147,35 +149,38 @@ else: # pragma: no cover
locs = globs
exec("""exec code in globs, locs""")
-
exec_("""def reraise(tp, value, tb=None):
raise tp, value, tb
""")
-if PY3: # pragma: no cover
+if PY3: # pragma: no cover
def iteritems_(d):
return d.items()
+
def itervalues_(d):
return d.values()
+
def iterkeys_(d):
return d.keys()
-else: # pragma: no cover
+else: # pragma: no cover
def iteritems_(d):
return d.iteritems()
+
def itervalues_(d):
return d.itervalues()
+
def iterkeys_(d):
return d.iterkeys()
-if PY3: # pragma: no cover
+if PY3: # pragma: no cover
def map_(*arg):
return list(map(*arg))
else:
map_ = map
-
-if PY3: # pragma: no cover
+
+if PY3: # pragma: no cover
def is_nonstr_iter(v):
if isinstance(v, str):
return False
@@ -183,46 +188,52 @@ if PY3: # pragma: no cover
else:
def is_nonstr_iter(v):
return hasattr(v, '__iter__')
-
-if PY3: # pragma: no cover
+
+if PY3: # pragma: no cover
im_func = '__func__'
im_self = '__self__'
else:
im_func = 'im_func'
im_self = 'im_self'
-try: # pragma: no cover
+try: # pragma: no cover
import configparser
-except ImportError: # pragma: no cover
+except ImportError: # pragma: no cover
import ConfigParser as configparser
try:
from Cookie import SimpleCookie
-except ImportError: # pragma: no cover
+except ImportError: # pragma: no cover
from http.cookies import SimpleCookie
-if PY3: # pragma: no cover
+if PY3: # pragma: no cover
from html import escape
else:
from cgi import escape
-try: # pragma: no cover
+try: # pragma: no cover
input_ = raw_input
-except NameError: # pragma: no cover
+except NameError: # pragma: no cover
input_ = input
-try:
+# support annotations and keyword-only arguments in PY3
+if PY3: # pragma: no cover
+ from inspect import getfullargspec as getargspec
+else:
+ from inspect import getargspec
+
+try:
from StringIO import StringIO as NativeIO
-except ImportError: # pragma: no cover
+except ImportError: # pragma: no cover
from io import StringIO as NativeIO
# "json" is not an API; it's here to support older pyramid_debugtoolbar
# versions which attempt to import it
import json
-
-if PY3: # pragma: no cover
+
+if PY3: # pragma: no cover
# see PEP 3333 for why we encode WSGI PATH_INFO to latin-1 before
# decoding it to utf-8
def decode_path_info(path):
@@ -231,16 +242,37 @@ else:
def decode_path_info(path):
return path.decode('utf-8')
-if PY3: # pragma: no cover
- # see PEP 3333 for why we decode the path to latin-1
+if PY3: # pragma: no cover
+ # see PEP 3333 for why we decode the path to latin-1
from urllib.parse import unquote_to_bytes
+
def unquote_bytes_to_wsgi(bytestring):
return unquote_to_bytes(bytestring).decode('latin-1')
else:
from urlparse import unquote as unquote_to_bytes
+
def unquote_bytes_to_wsgi(bytestring):
return unquote_to_bytes(bytestring)
+
def is_bound_method(ob):
return inspect.ismethod(ob) and getattr(ob, im_self, None) is not None
+
+def is_unbound_method(fn):
+ """
+ This consistently verifies that the callable is bound to a
+ class.
+ """
+ is_bound = is_bound_method(fn)
+
+ if not is_bound and inspect.isroutine(fn):
+ spec = inspect.getargspec(fn)
+ has_self = len(spec.args) > 0 and spec.args[0] == 'self'
+
+ if PY3 and inspect.isfunction(fn) and has_self: # pragma: no cover
+ return True
+ elif inspect.ismethod(fn):
+ return True
+
+ return False
diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py
index cfa35ec6c..2ab654b9a 100644
--- a/pyramid/config/__init__.py
+++ b/pyramid/config/__init__.py
@@ -179,6 +179,11 @@ class Configurator(
See :ref:`changing_the_request_factory`. By default it is ``None``,
which means use the default request factory.
+ If ``response_factory`` is passed, it should be a :term:`response
+ factory` implementation or a :term:`dotted Python name` to the same.
+ See :ref:`changing_the_response_factory`. By default it is ``None``,
+ which means use the default response factory.
+
If ``default_permission`` is passed, it should be a
:term:`permission` string to be used as the default permission for
all view configuration registrations performed against this
@@ -190,7 +195,7 @@ class Configurator(
configurations which do not explicitly declare a permission will
always be executable by entirely anonymous users (any
authorization policy in effect is ignored).
-
+
.. seealso::
See also :ref:`setting_a_default_permission`.
@@ -254,6 +259,7 @@ class Configurator(
.. versionadded:: 1.6
The ``root_package`` argument.
+ The ``response_factory`` argument.
"""
manager = manager # for testing injection
venusian = venusian # for testing injection
@@ -276,6 +282,7 @@ class Configurator(
debug_logger=None,
locale_negotiator=None,
request_factory=None,
+ response_factory=None,
default_permission=None,
session_factory=None,
default_view_mapper=None,
@@ -310,6 +317,7 @@ class Configurator(
debug_logger=debug_logger,
locale_negotiator=locale_negotiator,
request_factory=request_factory,
+ response_factory=response_factory,
default_permission=default_permission,
session_factory=session_factory,
default_view_mapper=default_view_mapper,
@@ -325,6 +333,7 @@ class Configurator(
debug_logger=None,
locale_negotiator=None,
request_factory=None,
+ response_factory=None,
default_permission=None,
session_factory=None,
default_view_mapper=None,
@@ -412,6 +421,9 @@ class Configurator(
if request_factory:
self.set_request_factory(request_factory)
+ if response_factory:
+ self.set_response_factory(response_factory)
+
if default_permission:
self.set_default_permission(default_permission)
@@ -469,7 +481,7 @@ class Configurator(
_registry.registerSelfAdapter = registerSelfAdapter
# API
-
+
def _get_introspector(self):
introspector = getattr(self.registry, 'introspector', _marker)
if introspector is _marker:
diff --git a/pyramid/config/adapters.py b/pyramid/config/adapters.py
index f6a652e3d..3d11980da 100644
--- a/pyramid/config/adapters.py
+++ b/pyramid/config/adapters.py
@@ -143,7 +143,7 @@ class AdaptersConfiguratorMixin(object):
Adds a subscriber predicate factory. The associated subscriber
predicate can later be named as a keyword argument to
:meth:`pyramid.config.Configurator.add_subscriber` in the
- ``**predicates`` anonyous keyword argument dictionary.
+ ``**predicates`` anonymous keyword argument dictionary.
``name`` should be the name of the predicate. It must be a valid
Python identifier (it will be used as a ``**predicates`` keyword
diff --git a/pyramid/config/factories.py b/pyramid/config/factories.py
index 5ce1081c6..f0b6252ae 100644
--- a/pyramid/config/factories.py
+++ b/pyramid/config/factories.py
@@ -4,6 +4,7 @@ from zope.interface import implementer
from pyramid.interfaces import (
IDefaultRootFactory,
IRequestFactory,
+ IResponseFactory,
IRequestExtensions,
IRootFactory,
ISessionFactory,
@@ -13,9 +14,11 @@ from pyramid.traversal import DefaultRootFactory
from pyramid.util import (
action_method,
- InstancePropertyMixin,
+ get_callable_name,
+ InstancePropertyHelper,
)
+
class FactoriesConfiguratorMixin(object):
@action_method
def set_root_factory(self, factory):
@@ -32,9 +35,10 @@ class FactoriesConfiguratorMixin(object):
factory = self.maybe_dotted(factory)
if factory is None:
factory = DefaultRootFactory
+
def register():
self.registry.registerUtility(factory, IRootFactory)
- self.registry.registerUtility(factory, IDefaultRootFactory) # b/c
+ self.registry.registerUtility(factory, IDefaultRootFactory) # b/c
intr = self.introspectable('root factories',
None,
@@ -43,7 +47,7 @@ class FactoriesConfiguratorMixin(object):
intr['factory'] = factory
self.action(IRootFactory, register, introspectables=(intr,))
- _set_root_factory = set_root_factory # bw compat
+ _set_root_factory = set_root_factory # bw compat
@action_method
def set_session_factory(self, factory):
@@ -59,6 +63,7 @@ class FactoriesConfiguratorMixin(object):
achieve the same purpose.
"""
factory = self.maybe_dotted(factory)
+
def register():
self.registry.registerUtility(factory, ISessionFactory)
intr = self.introspectable('session factory', None,
@@ -88,6 +93,7 @@ class FactoriesConfiguratorMixin(object):
can be used to achieve the same purpose.
"""
factory = self.maybe_dotted(factory)
+
def register():
self.registry.registerUtility(factory, IRequestFactory)
intr = self.introspectable('request factory', None,
@@ -97,6 +103,31 @@ class FactoriesConfiguratorMixin(object):
self.action(IRequestFactory, register, introspectables=(intr,))
@action_method
+ def set_response_factory(self, factory):
+ """ The object passed as ``factory`` should be an object (or a
+ :term:`dotted Python name` which refers to an object) which
+ will be used by the :app:`Pyramid` as the default response
+ objects. The factory should conform to the
+ :class:`pyramid.interfaces.IResponseFactory` interface.
+
+ .. note::
+
+ Using the ``response_factory`` argument to the
+ :class:`pyramid.config.Configurator` constructor
+ can be used to achieve the same purpose.
+ """
+ factory = self.maybe_dotted(factory)
+
+ def register():
+ self.registry.registerUtility(factory, IResponseFactory)
+
+ intr = self.introspectable('response factory', None,
+ self.object_description(factory),
+ 'response factory')
+ intr['factory'] = factory
+ self.action(IResponseFactory, register, introspectables=(intr,))
+
+ @action_method
def add_request_method(self,
callable=None,
name=None,
@@ -143,10 +174,12 @@ class FactoriesConfiguratorMixin(object):
property = property or reify
if property:
- name, callable = InstancePropertyMixin._make_property(
+ name, callable = InstancePropertyHelper.make_property(
callable, name=name, reify=reify)
elif name is None:
name = callable.__name__
+ else:
+ name = get_callable_name(name)
def register():
exts = self.registry.queryUtility(IRequestExtensions)
@@ -198,9 +231,9 @@ class FactoriesConfiguratorMixin(object):
'set_request_propery() is deprecated as of Pyramid 1.5; use '
'add_request_method() with the property=True argument instead')
+
@implementer(IRequestExtensions)
class _RequestExtensions(object):
def __init__(self):
self.descriptors = {}
self.methods = {}
-
diff --git a/pyramid/config/routes.py b/pyramid/config/routes.py
index f1463b50b..24f38a4fd 100644
--- a/pyramid/config/routes.py
+++ b/pyramid/config/routes.py
@@ -138,6 +138,18 @@ class RoutesConfiguratorMixin(object):
.. versionadded:: 1.1
+ accept
+
+ This value represents a match query for one or more mimetypes in the
+ ``Accept`` HTTP request header. If this value is specified, it must
+ be in one of the following forms: a mimetype match token in the form
+ ``text/plain``, a wildcard mimetype match token in the form
+ ``text/*`` or a match-all wildcard mimetype match token in the form
+ ``*/*``. If any of the forms matches the ``Accept`` header of the
+ request, or if the ``Accept`` header isn't set at all in the request,
+ this will match the current route. If this does not match the
+ ``Accept`` header of the request, route matching continues.
+
Predicate Arguments
pattern
@@ -220,19 +232,6 @@ class RoutesConfiguratorMixin(object):
case of the header name is not significant. If this
predicate returns ``False``, route matching continues.
- accept
-
- This value represents a match query for one or more
- mimetypes in the ``Accept`` HTTP request header. If this
- value is specified, it must be in one of the following
- forms: a mimetype match token in the form ``text/plain``, a
- wildcard mimetype match token in the form ``text/*`` or a
- match-all wildcard mimetype match token in the form ``*/*``.
- If any of the forms matches the ``Accept`` header of the
- request, or if the ``Accept`` header isn't set at all in the
- request, this predicate will be true. If this predicate
- returns ``False``, route matching continues.
-
effective_principals
If specified, this value should be a :term:`principal` identifier or
@@ -303,6 +302,8 @@ class RoutesConfiguratorMixin(object):
# check for an external route; an external route is one which is
# is a full url (e.g. 'http://example.com/{id}')
parsed = urlparse.urlparse(pattern)
+ external_url = pattern
+
if parsed.hostname:
pattern = parsed.path
@@ -357,6 +358,10 @@ class RoutesConfiguratorMixin(object):
intr['pregenerator'] = pregenerator
intr['static'] = static
intr['use_global_views'] = use_global_views
+
+ if static is True:
+ intr['external_url'] = external_url
+
introspectables.append(intr)
if factory:
diff --git a/pyramid/config/util.py b/pyramid/config/util.py
index 892592196..23cdc6be8 100644
--- a/pyramid/config/util.py
+++ b/pyramid/config/util.py
@@ -3,6 +3,7 @@ import inspect
from pyramid.compat import (
bytes_,
+ getargspec,
is_nonstr_iter,
)
@@ -201,7 +202,7 @@ def takes_one_arg(callee, attr=None, argname=None):
return False
try:
- argspec = inspect.getargspec(fn)
+ argspec = getargspec(fn)
except TypeError:
return False
diff --git a/pyramid/config/views.py b/pyramid/config/views.py
index c01b72e12..24c592f7a 100644
--- a/pyramid/config/views.py
+++ b/pyramid/config/views.py
@@ -42,7 +42,8 @@ from pyramid.compat import (
url_quote,
WIN,
is_bound_method,
- is_nonstr_iter
+ is_unbound_method,
+ is_nonstr_iter,
)
from pyramid.exceptions import (
@@ -418,6 +419,12 @@ class DefaultViewMapper(object):
self.attr = kw.get('attr')
def __call__(self, view):
+ if is_unbound_method(view) and self.attr is None:
+ raise ConfigurationError((
+ 'Unbound method calls are not supported, please set the class '
+ 'as your `view` and the method as your `attr`'
+ ))
+
if inspect.isclass(view):
view = self.map_class(view)
else:
@@ -841,6 +848,18 @@ class ViewsConfiguratorMixin(object):
very useful for 'civilians' who are just developing stock Pyramid
applications. Pay no attention to the man behind the curtain.
+ accept
+
+ This value represents a match query for one or more mimetypes in the
+ ``Accept`` HTTP request header. If this value is specified, it must
+ be in one of the following forms: a mimetype match token in the form
+ ``text/plain``, a wildcard mimetype match token in the form
+ ``text/*`` or a match-all wildcard mimetype match token in the form
+ ``*/*``. If any of the forms matches the ``Accept`` header of the
+ request, or if the ``Accept`` header isn't set at all in the request,
+ this will match the current view. If this does not match the
+ ``Accept`` header of the request, view matching continues.
+
Predicate Arguments
name
@@ -941,17 +960,6 @@ class ViewsConfiguratorMixin(object):
This is useful for detecting AJAX requests issued from
jQuery, Prototype and other Javascript libraries.
- accept
-
- The value of this argument represents a match query for one
- or more mimetypes in the ``Accept`` HTTP request header. If
- this value is specified, it must be in one of the following
- forms: a mimetype match token in the form ``text/plain``, a
- wildcard mimetype match token in the form ``text/*`` or a
- match-all wildcard mimetype match token in the form ``*/*``.
- If any of the forms matches the ``Accept`` header of the
- request, this predicate will be true.
-
header
This value represents an HTTP header name or a header
@@ -1972,9 +1980,9 @@ class StaticURLInfo(object):
cb = self._default_cachebust()
if cb:
def cachebust(subpath, kw):
- token = cb.token(spec + subpath)
subpath_tuple = tuple(subpath.split('/'))
- subpath_tuple, kw = cb.pregenerate(token, subpath_tuple, kw)
+ subpath_tuple, kw = cb.pregenerate(
+ spec + subpath, subpath_tuple, kw)
return '/'.join(subpath_tuple), kw
else:
cachebust = None
diff --git a/pyramid/decorator.py b/pyramid/decorator.py
index 0d17bc398..df30c5e10 100644
--- a/pyramid/decorator.py
+++ b/pyramid/decorator.py
@@ -1,3 +1,6 @@
+import functools
+
+
class reify(object):
""" Use as a class method decorator. It operates almost exactly like the
Python ``@property`` decorator, but it puts the result of the method it
@@ -26,10 +29,7 @@ class reify(object):
"""
def __init__(self, wrapped):
self.wrapped = wrapped
- try:
- self.__doc__ = wrapped.__doc__
- except: # pragma: no cover
- pass
+ functools.update_wrapper(self, wrapped)
def __get__(self, inst, objtype=None):
if inst is None:
diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py
index b21c6b9cc..1508f282e 100644
--- a/pyramid/interfaces.py
+++ b/pyramid/interfaces.py
@@ -582,18 +582,16 @@ class IStaticURLInfo(Interface):
""" Generate a URL for the given path """
class IResponseFactory(Interface):
- """ A utility which generates a response factory """
- def __call__():
- """ Return a response factory (e.g. a callable that returns an object
- implementing IResponse, e.g. :class:`pyramid.response.Response`). It
- should accept all the arguments that the Pyramid Response class
- accepts."""
+ """ A utility which generates a response """
+ def __call__(request):
+ """ Return a response object implementing IResponse,
+ e.g. :class:`pyramid.response.Response`). It should handle the
+ case when ``request`` is ``None``."""
class IRequestFactory(Interface):
""" A utility which generates a request """
def __call__(environ):
- """ Return an object implementing IRequest, e.g. an instance
- of ``pyramid.request.Request``"""
+ """ Return an instance of ``pyramid.request.Request``"""
def blank(path):
""" Return an empty request object (see
@@ -1194,18 +1192,11 @@ class ICacheBuster(Interface):
.. versionadded:: 1.6
"""
- def token(pathspec):
- """
- Computes and returns a token string used for cache busting.
- ``pathspec`` is the path specification for the resource to be cache
- busted. """
-
- def pregenerate(token, subpath, kw):
+ def pregenerate(pathspec, subpath, kw):
"""
Modifies a subpath and/or keyword arguments from which a static asset
- URL will be computed during URL generation. The ``token`` argument is
- a token string computed by
- :meth:`~pyramid.interfaces.ICacheBuster.token` for a particular asset.
+ URL will be computed during URL generation. The ``pathspec`` argument
+ is the path specification for the resource to be cache busted.
The ``subpath`` argument is a tuple of path elements that represent the
portion of the asset URL which is used to find the asset. The ``kw``
argument is a dict of keywords that are to be passed eventually to
diff --git a/pyramid/path.py b/pyramid/path.py
index 470e766f8..f2d8fff55 100644
--- a/pyramid/path.py
+++ b/pyramid/path.py
@@ -337,8 +337,14 @@ class DottedNameResolver(Resolver):
value = package.__name__
else:
value = package.__name__ + value
- return pkg_resources.EntryPoint.parse(
- 'x=%s' % value).load(False)
+ # Calling EntryPoint.load with an argument is deprecated.
+ # See https://pythonhosted.org/setuptools/history.html#id8
+ ep = pkg_resources.EntryPoint.parse('x=%s' % value)
+ if hasattr(ep, 'resolve'):
+ # setuptools>=10.2
+ return ep.resolve() # pragma: NO COVER
+ else:
+ return ep.load(False) # pragma: NO COVER
def _zope_dottedname_style(self, value, package):
""" package.module.attr style """
diff --git a/pyramid/renderers.py b/pyramid/renderers.py
index e647ebacf..3c35551ea 100644
--- a/pyramid/renderers.py
+++ b/pyramid/renderers.py
@@ -10,7 +10,6 @@ from zope.interface.registry import Components
from pyramid.interfaces import (
IJSONAdapter,
IRendererFactory,
- IResponseFactory,
IRendererInfo,
)
@@ -25,7 +24,7 @@ from pyramid.events import BeforeRender
from pyramid.path import caller_package
-from pyramid.response import Response
+from pyramid.response import _get_response_factory
from pyramid.threadlocal import get_current_registry
# API
@@ -356,19 +355,19 @@ class JSONP(JSON):
``self.param_name`` is present in request.GET; otherwise returns
plain-JSON encoded string with content-type ``application/json``"""
def _render(value, system):
- request = system['request']
+ request = system.get('request')
default = self._make_default(request)
val = self.serializer(value, default=default, **self.kw)
- callback = request.GET.get(self.param_name)
- if callback is None:
- ct = 'application/json'
- body = val
- else:
- ct = 'application/javascript'
- body = '%s(%s);' % (callback, val)
- response = request.response
- if response.content_type == response.default_content_type:
- response.content_type = ct
+ ct = 'application/json'
+ body = val
+ if request is not None:
+ callback = request.GET.get(self.param_name)
+ if callback is not None:
+ ct = 'application/javascript'
+ body = '%s(%s);' % (callback, val)
+ response = request.response
+ if response.content_type == response.default_content_type:
+ response.content_type = ct
return body
return _render
@@ -448,14 +447,16 @@ class RendererHelper(object):
if response is None:
# request is None or request is not a pyramid.response.Response
registry = self.registry
- response_factory = registry.queryUtility(IResponseFactory,
- default=Response)
-
- response = response_factory()
+ response_factory = _get_response_factory(registry)
+ response = response_factory(request)
if result is not None:
if isinstance(result, text_type):
response.text = result
+ elif isinstance(result, bytes):
+ response.body = result
+ elif hasattr(result, '__iter__'):
+ response.app_iter = result
else:
response.body = result
diff --git a/pyramid/request.py b/pyramid/request.py
index bc2889310..3cbe5d9e3 100644
--- a/pyramid/request.py
+++ b/pyramid/request.py
@@ -8,26 +8,30 @@ from webob import BaseRequest
from pyramid.interfaces import (
IRequest,
+ IRequestExtensions,
IResponse,
ISessionFactory,
- IResponseFactory,
)
from pyramid.compat import (
text_,
bytes_,
native_,
+ iteritems_,
)
from pyramid.decorator import reify
from pyramid.i18n import LocalizerRequestMixin
-from pyramid.response import Response
+from pyramid.response import Response, _get_response_factory
from pyramid.security import (
AuthenticationAPIMixin,
AuthorizationAPIMixin,
)
from pyramid.url import URLMethodsMixin
-from pyramid.util import InstancePropertyMixin
+from pyramid.util import (
+ InstancePropertyHelper,
+ InstancePropertyMixin,
+)
class TemplateContext(object):
pass
@@ -214,10 +218,8 @@ class Request(
right" attributes (e.g. by calling ``request.response.set_cookie()``)
within a view that uses a renderer. Mutations to this response object
will be preserved in the response sent to the client."""
- registry = self.registry
- response_factory = registry.queryUtility(IResponseFactory,
- default=Response)
- return response_factory()
+ response_factory = _get_response_factory(self.registry)
+ return response_factory(self)
def is_response(self, ob):
""" Return ``True`` if the object passed as ``ob`` is a valid
@@ -310,3 +312,22 @@ def call_app_with_subpath_as_path_info(request, app):
new_request.environ['PATH_INFO'] = new_path_info
return new_request.get_response(app)
+
+def apply_request_extensions(request, extensions=None):
+ """Apply request extensions (methods and properties) to an instance of
+ :class:`pyramid.interfaces.IRequest`. This method is dependent on the
+ ``request`` containing a properly initialized registry.
+
+ After invoking this method, the ``request`` should have the methods
+ and properties that were defined using
+ :meth:`pyramid.config.Configurator.add_request_method`.
+ """
+ if extensions is None:
+ extensions = request.registry.queryUtility(IRequestExtensions)
+ if extensions is not None:
+ for name, fn in iteritems_(extensions.methods):
+ method = fn.__get__(request, request.__class__)
+ setattr(request, name, method)
+
+ InstancePropertyHelper.apply_properties(
+ request, extensions.descriptors)
diff --git a/pyramid/response.py b/pyramid/response.py
index d11fd0123..892e5dfff 100644
--- a/pyramid/response.py
+++ b/pyramid/response.py
@@ -8,7 +8,8 @@ import venusian
from webob import Response as _Response
from zope.interface import implementer
-from pyramid.interfaces import IResponse
+from pyramid.interfaces import IResponse, IResponseFactory
+
def init_mimetypes(mimetypes):
# this is a function so it can be unittested
@@ -143,7 +144,7 @@ class response_adapter(object):
@response_adapter(dict, list)
def myadapter(ob):
return Response(json.dumps(ob))
-
+
This method will have no effect until a :term:`scan` is performed
agains the package or module which contains it, ala:
@@ -167,3 +168,15 @@ class response_adapter(object):
def __call__(self, wrapped):
self.venusian.attach(wrapped, self.register, category='pyramid')
return wrapped
+
+
+def _get_response_factory(registry):
+ """ Obtain a :class: `pyramid.response.Response` using the
+ `pyramid.interfaces.IResponseFactory`.
+ """
+ response_factory = registry.queryUtility(
+ IResponseFactory,
+ default=lambda r: Response()
+ )
+
+ return response_factory
diff --git a/pyramid/router.py b/pyramid/router.py
index ba4f85b18..0b1ecade7 100644
--- a/pyramid/router.py
+++ b/pyramid/router.py
@@ -27,6 +27,7 @@ from pyramid.events import (
from pyramid.exceptions import PredicateMismatch
from pyramid.httpexceptions import HTTPNotFound
from pyramid.request import Request
+from pyramid.request import apply_request_extensions
from pyramid.threadlocal import manager
from pyramid.traversal import (
@@ -213,7 +214,7 @@ class Router(object):
try:
extensions = self.request_extensions
if extensions is not None:
- request._set_extensions(extensions)
+ apply_request_extensions(request, extensions=extensions)
response = handle_request(request)
if request.response_callbacks:
diff --git a/pyramid/scripting.py b/pyramid/scripting.py
index fdb4aa430..d9587338f 100644
--- a/pyramid/scripting.py
+++ b/pyramid/scripting.py
@@ -1,12 +1,12 @@
from pyramid.config import global_registries
from pyramid.exceptions import ConfigurationError
-from pyramid.request import Request
from pyramid.interfaces import (
- IRequestExtensions,
IRequestFactory,
IRootFactory,
)
+from pyramid.request import Request
+from pyramid.request import apply_request_extensions
from pyramid.threadlocal import manager as threadlocal_manager
from pyramid.traversal import DefaultRootFactory
@@ -77,9 +77,7 @@ def prepare(request=None, registry=None):
request.registry = registry
threadlocals = {'registry':registry, 'request':request}
threadlocal_manager.push(threadlocals)
- extensions = registry.queryUtility(IRequestExtensions)
- if extensions is not None:
- request._set_extensions(extensions)
+ apply_request_extensions(request)
def closer():
threadlocal_manager.pop()
root_factory = registry.queryUtility(IRootFactory,
diff --git a/pyramid/scripts/pcreate.py b/pyramid/scripts/pcreate.py
index edf2c39f7..d2c5f8c27 100644
--- a/pyramid/scripts/pcreate.py
+++ b/pyramid/scripts/pcreate.py
@@ -18,7 +18,7 @@ def main(argv=sys.argv, quiet=False):
class PCreateCommand(object):
verbosity = 1 # required
description = "Render Pyramid scaffolding to an output directory"
- usage = "usage: %prog [options] output_directory"
+ usage = "usage: %prog [options] -s <scaffold> output_directory"
parser = optparse.OptionParser(usage, description=description)
parser.add_option('-s', '--scaffold',
dest='scaffold_name',
@@ -63,8 +63,16 @@ class PCreateCommand(object):
def run(self):
if self.options.list:
return self.show_scaffolds()
+ if not self.options.scaffold_name and not self.args:
+ if not self.quiet: # pragma: no cover
+ self.parser.print_help()
+ self.out('')
+ self.show_scaffolds()
+ return 2
if not self.options.scaffold_name:
- self.out('You must provide at least one scaffold name')
+ self.out('You must provide at least one scaffold name: -s <scaffold name>')
+ self.out('')
+ self.show_scaffolds()
return 2
if not self.args:
self.out('You must provide a project name')
diff --git a/pyramid/scripts/prequest.py b/pyramid/scripts/prequest.py
index 2ab3b8bb9..34eeadf32 100644
--- a/pyramid/scripts/prequest.py
+++ b/pyramid/scripts/prequest.py
@@ -5,7 +5,7 @@ import textwrap
from pyramid.compat import url_unquote
from pyramid.request import Request
-from pyramid.paster import get_app
+from pyramid.paster import get_app, setup_logging
from pyramid.scripts.common import parse_vars
def main(argv=sys.argv, quiet=False):
@@ -97,12 +97,18 @@ class PRequestCommand(object):
if not self.quiet:
print(msg)
+ def configure_logging(self, app_spec):
+ setup_logging(app_spec)
+
def run(self):
if not len(self.args) >= 2:
self.out('You must provide at least two arguments')
return 2
app_spec = self.args[0]
path = self.args[1]
+
+ self.configure_logging(app_spec)
+
if not path.startswith('/'):
path = '/' + path
diff --git a/pyramid/scripts/proutes.py b/pyramid/scripts/proutes.py
index d0c1aa13e..544947724 100644
--- a/pyramid/scripts/proutes.py
+++ b/pyramid/scripts/proutes.py
@@ -1,12 +1,26 @@
+import fnmatch
import optparse
import sys
import textwrap
+import re
from pyramid.paster import bootstrap
+from pyramid.compat import (string_types, configparser)
+from pyramid.interfaces import (
+ IRouteRequest,
+ IViewClassifier,
+ IView,
+)
+from pyramid.config import not_
+
from pyramid.scripts.common import parse_vars
+from pyramid.static import static_view
+from zope.interface import Interface
PAD = 3
+ANY_KEY = '*'
+UNKNOWN_KEY = '<unknown>'
def main(argv=sys.argv, quiet=False):
@@ -14,6 +28,206 @@ def main(argv=sys.argv, quiet=False):
return command.run()
+def _get_pattern(route):
+ pattern = route.pattern
+
+ if not pattern.startswith('/'):
+ pattern = '/%s' % pattern
+ return pattern
+
+
+def _get_print_format(fmt, max_name, max_pattern, max_view, max_method):
+ print_fmt = ''
+ max_map = {
+ 'name': max_name,
+ 'pattern': max_pattern,
+ 'view': max_view,
+ 'method': max_method,
+ }
+ sizes = []
+
+ for index, col in enumerate(fmt):
+ size = max_map[col] + PAD
+ print_fmt += '{{%s: <{%s}}} ' % (col, index)
+ sizes.append(size)
+
+ return print_fmt.format(*sizes)
+
+
+def _get_request_methods(route_request_methods, view_request_methods):
+ excludes = set()
+
+ if route_request_methods:
+ route_request_methods = set(route_request_methods)
+
+ if view_request_methods:
+ view_request_methods = set(view_request_methods)
+
+ for method in view_request_methods.copy():
+ if method.startswith('!'):
+ view_request_methods.remove(method)
+ excludes.add(method[1:])
+
+ has_route_methods = route_request_methods is not None
+ has_view_methods = len(view_request_methods) > 0
+ has_methods = has_route_methods or has_view_methods
+
+ if has_route_methods is False and has_view_methods is False:
+ request_methods = [ANY_KEY]
+ elif has_route_methods is False and has_view_methods is True:
+ request_methods = view_request_methods
+ elif has_route_methods is True and has_view_methods is False:
+ request_methods = route_request_methods
+ else:
+ request_methods = route_request_methods.intersection(
+ view_request_methods
+ )
+
+ request_methods = set(request_methods).difference(excludes)
+
+ if has_methods and not request_methods:
+ request_methods = '<route mismatch>'
+ elif request_methods:
+ if excludes and request_methods == set([ANY_KEY]):
+ for exclude in excludes:
+ request_methods.add('!%s' % exclude)
+
+ request_methods = ','.join(sorted(request_methods))
+
+ return request_methods
+
+
+def _get_view_module(view_callable):
+ if view_callable is None:
+ return UNKNOWN_KEY
+
+ if hasattr(view_callable, '__name__'):
+ if hasattr(view_callable, '__original_view__'):
+ original_view = view_callable.__original_view__
+ else:
+ original_view = None
+
+ if isinstance(original_view, static_view):
+ if original_view.package_name is not None:
+ return '%s:%s' % (
+ original_view.package_name,
+ original_view.docroot
+ )
+ else:
+ return original_view.docroot
+ else:
+ view_name = view_callable.__name__
+ else:
+ # Currently only MultiView hits this,
+ # we could just not run _get_view_module
+ # for them and remove this logic
+ view_name = str(view_callable)
+
+ view_module = '%s.%s' % (
+ view_callable.__module__,
+ view_name,
+ )
+
+ # If pyramid wraps something in wsgiapp or wsgiapp2 decorators
+ # that is currently returned as pyramid.router.decorator, lets
+ # hack a nice name in:
+ if view_module == 'pyramid.router.decorator':
+ view_module = '<wsgiapp>'
+
+ return view_module
+
+
+def get_route_data(route, registry):
+ pattern = _get_pattern(route)
+
+ request_iface = registry.queryUtility(
+ IRouteRequest,
+ name=route.name
+ )
+
+ route_request_methods = None
+ view_request_methods_order = []
+ view_request_methods = {}
+ view_callable = None
+
+ route_intr = registry.introspector.get(
+ 'routes', route.name
+ )
+
+ if request_iface is None:
+ return [
+ (route.name, _get_pattern(route), UNKNOWN_KEY, ANY_KEY)
+ ]
+
+ view_callable = registry.adapters.lookup(
+ (IViewClassifier, request_iface, Interface),
+ IView,
+ name='',
+ default=None
+ )
+ view_module = _get_view_module(view_callable)
+
+ # Introspectables can be turned off, so there could be a chance
+ # that we have no `route_intr` but we do have a route + callable
+ if route_intr is None:
+ view_request_methods[view_module] = []
+ view_request_methods_order.append(view_module)
+ else:
+ if route_intr.get('static', False) is True:
+ return [
+ (route.name, route_intr['external_url'], UNKNOWN_KEY, ANY_KEY)
+ ]
+
+
+ route_request_methods = route_intr['request_methods']
+ view_intr = registry.introspector.related(route_intr)
+
+ if view_intr:
+ for view in view_intr:
+ 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_module not in view_request_methods:
+ view_request_methods[view_module] = []
+ view_request_methods_order.append(view_module)
+
+ if isinstance(request_method, string_types):
+ request_method = (request_method,)
+ elif isinstance(request_method, not_):
+ request_method = ('!%s' % request_method.value,)
+
+ view_request_methods[view_module].extend(request_method)
+ else:
+ if view_module not in view_request_methods:
+ view_request_methods[view_module] = []
+ view_request_methods_order.append(view_module)
+
+ else:
+ view_request_methods[view_module] = []
+ view_request_methods_order.append(view_module)
+
+ final_routes = []
+
+ for view_module in view_request_methods_order:
+ methods = view_request_methods[view_module]
+ request_methods = _get_request_methods(
+ route_request_methods,
+ methods
+ )
+
+ final_routes.append((
+ route.name,
+ pattern,
+ view_module,
+ request_methods,
+ ))
+
+ return final_routes
+
+
class PRoutesCommand(object):
description = """\
Print all URL dispatch routes used by a Pyramid application in the
@@ -30,111 +244,153 @@ class PRoutesCommand(object):
bootstrap = (bootstrap,)
stdout = sys.stdout
usage = '%prog config_uri'
-
+ ConfigParser = configparser.ConfigParser # testing
parser = optparse.OptionParser(
usage,
description=textwrap.dedent(description)
- )
+ )
+ parser.add_option('-g', '--glob',
+ action='store', type='string', dest='glob',
+ default='', help='Display routes matching glob pattern')
+
+ parser.add_option('-f', '--format',
+ action='store', type='string', dest='format',
+ default='', help=('Choose which columns to display, this '
+ 'will override the format key in the '
+ '[proutes] ini section'))
def __init__(self, argv, quiet=False):
self.options, self.args = self.parser.parse_args(argv[1:])
self.quiet = quiet
+ self.available_formats = [
+ 'name', 'pattern', 'view', 'method'
+ ]
+ self.column_format = self.available_formats
+
+ def validate_formats(self, formats):
+ invalid_formats = []
+ for fmt in formats:
+ if fmt not in self.available_formats:
+ invalid_formats.append(fmt)
+
+ msg = (
+ 'You provided invalid formats %s, '
+ 'Available formats are %s'
+ )
- def _get_mapper(self, registry):
- from pyramid.config import Configurator
- config = Configurator(registry = registry)
- return config.get_routes_mapper()
+ if invalid_formats:
+ msg = msg % (invalid_formats, self.available_formats)
+ self.out(msg)
+ return False
+
+ return True
+
+ def proutes_file_config(self, filename):
+ config = self.ConfigParser()
+ config.read(filename)
+ try:
+ items = config.items('proutes')
+ for k, v in items:
+ if 'format' == k:
+ cols = re.split(r'[,|\s|\n]*', v)
+ self.column_format = [x.strip() for x in cols]
- def out(self, msg): # pragma: no cover
+ except configparser.NoSectionError:
+ return
+
+ def out(self, msg): # pragma: no cover
if not self.quiet:
print(msg)
+ def _get_mapper(self, registry):
+ from pyramid.config import Configurator
+ config = Configurator(registry=registry)
+ return config.get_routes_mapper()
+
def run(self, quiet=False):
if not self.args:
self.out('requires a config file argument')
return 2
- from pyramid.interfaces import IRouteRequest
- from pyramid.interfaces import IViewClassifier
- from pyramid.interfaces import IView
- from pyramid.interfaces import IMultiView
-
- from zope.interface import Interface
config_uri = self.args[0]
-
env = self.bootstrap[0](config_uri, options=parse_vars(self.args[1:]))
registry = env['registry']
mapper = self._get_mapper(registry)
- if mapper is not None:
- mapped_routes = [('Name', 'Pattern', 'View')]
-
- max_name = len('Name')
- max_pattern = len('Pattern')
- max_view = len('View')
-
- routes = mapper.get_routes()
-
- if not routes:
- return 0
-
- mapped_routes.append((
- '-' * max_name,
- '-' * max_pattern,
- '-' * max_view,
- ))
-
- for route in routes:
- pattern = route.pattern
- if not pattern.startswith('/'):
- pattern = '/' + pattern
- request_iface = registry.queryUtility(IRouteRequest,
- name=route.name)
- view_callable = None
-
- if (request_iface is None) or (route.factory is not None):
- view_callable = '<unknown>'
- else:
- view_callable = registry.adapters.lookup(
- (IViewClassifier, request_iface, Interface),
- IView, name='', default=None)
-
- if view_callable is not None:
- if IMultiView.providedBy(view_callable):
- view_callables = [
- x[1] for x in view_callable.views
- ]
- else:
- view_callables = [view_callable]
-
- for view_func in view_callables:
- view_callable = '%s.%s' % (
- view_func.__module__,
- view_func.__name__,
- )
- else:
- view_callable = str(None)
-
- if len(route.name) > max_name:
- max_name = len(route.name)
+
+ self.proutes_file_config(config_uri)
+
+ if self.options.format:
+ columns = self.options.format.split(',')
+ self.column_format = [x.strip() for x in columns]
+
+ is_valid = self.validate_formats(self.column_format)
+
+ if is_valid is False:
+ return 2
+
+ if mapper is None:
+ return 0
+
+ max_name = len('Name')
+ max_pattern = len('Pattern')
+ max_view = len('View')
+ max_method = len('Method')
+
+ routes = mapper.get_routes(include_static=True)
+
+ if len(routes) == 0:
+ return 0
+
+ mapped_routes = [{
+ 'name': 'Name',
+ 'pattern': 'Pattern',
+ 'view': 'View',
+ 'method': 'Method'
+ },{
+ 'name': '----',
+ 'pattern': '-------',
+ 'view': '----',
+ 'method': '------'
+ }]
+
+ for route in routes:
+ route_data = get_route_data(route, registry)
+
+ for name, pattern, view, method in route_data:
+ if self.options.glob:
+ match = (fnmatch.fnmatch(name, self.options.glob) or
+ fnmatch.fnmatch(pattern, self.options.glob))
+ if not match:
+ continue
+
+ if len(name) > max_name:
+ max_name = len(name)
if len(pattern) > max_pattern:
max_pattern = len(pattern)
- if len(view_callable) > max_view:
- max_view = len(view_callable)
+ if len(view) > max_view:
+ max_view = len(view)
- mapped_routes.append((route.name, pattern, view_callable))
+ if len(method) > max_method:
+ max_method = len(method)
- fmt = '%-{0}s %-{1}s %-{2}s'.format(
- max_name + PAD,
- max_pattern + PAD,
- max_view + PAD,
- )
+ mapped_routes.append({
+ 'name': name,
+ 'pattern': pattern,
+ 'view': view,
+ 'method': method
+ })
- for route_data in mapped_routes:
- self.out(fmt % route_data)
+ fmt = _get_print_format(
+ self.column_format, max_name, max_pattern, max_view, max_method
+ )
+
+ for route in mapped_routes:
+ self.out(fmt.format(**route))
return 0
-if __name__ == '__main__': # pragma: no cover
+
+if __name__ == '__main__': # pragma: no cover
sys.exit(main() or 0)
diff --git a/pyramid/scripts/pserve.py b/pyramid/scripts/pserve.py
index ea125a0dd..3b79aabd7 100644
--- a/pyramid/scripts/pserve.py
+++ b/pyramid/scripts/pserve.py
@@ -21,9 +21,11 @@ import textwrap
import threading
import time
import traceback
+import webbrowser
from paste.deploy import loadserver
from paste.deploy import loadapp
+from paste.deploy.loadwsgi import loadcontext, SERVER
from pyramid.compat import PY3
from pyramid.compat import WIN
@@ -34,6 +36,11 @@ from pyramid.scripts.common import parse_vars
MAXFD = 1024
+try:
+ import termios
+except ImportError: # pragma: no cover
+ termios = None
+
if WIN and not hasattr(os, 'kill'): # pragma: no cover
# py 2.6 on windows
def kill(pid, sig=None):
@@ -122,6 +129,11 @@ class PServeCommand(object):
action='store_true',
help="Auto-restart server if it dies")
parser.add_option(
+ '-b', '--browser',
+ dest='browser',
+ action='store_true',
+ help="Open a web browser to server url")
+ parser.add_option(
'--status',
action='store_true',
dest='show_status',
@@ -334,6 +346,17 @@ class PServeCommand(object):
msg = ''
self.out('Exiting%s (-v to see traceback)' % msg)
+ if self.options.browser:
+ def open_browser():
+ context = loadcontext(SERVER, app_spec, name=app_name, relative_to=base,
+ global_conf=vars)
+ url = 'http://{host}:{port}/'.format(**context.config())
+ time.sleep(1)
+ webbrowser.open(url)
+ t = threading.Thread(target=open_browser)
+ t.setDaemon(True)
+ t.start()
+
serve()
def loadapp(self, app_spec, name, relative_to, **kw): # pragma: no cover
@@ -691,15 +714,22 @@ def _turn_sigterm_into_systemexit(): # pragma: no cover
raise SystemExit
signal.signal(signal.SIGTERM, handle_term)
+def ensure_echo_on(): # pragma: no cover
+ if termios:
+ fd = sys.stdin.fileno()
+ attr_list = termios.tcgetattr(fd)
+ if not attr_list[3] & termios.ECHO:
+ attr_list[3] |= termios.ECHO
+ termios.tcsetattr(fd, termios.TCSANOW, attr_list)
+
def install_reloader(poll_interval=1, extra_files=None): # pragma: no cover
"""
Install the reloading monitor.
On some platforms server threads may not terminate when the main
- thread does, causing ports to remain open/locked. The
- ``raise_keyboard_interrupt`` option creates a unignorable signal
- which causes the whole application to shut-down (rudely).
+ thread does, causing ports to remain open/locked.
"""
+ ensure_echo_on()
mon = Monitor(poll_interval=poll_interval)
if extra_files is None:
extra_files = []
diff --git a/pyramid/security.py b/pyramid/security.py
index cbb4b895f..f993ef353 100644
--- a/pyramid/security.py
+++ b/pyramid/security.py
@@ -126,7 +126,7 @@ def remember(request, userid=_marker, **kw):
current :term:`authentication policy`. Common usage might look
like so within the body of a view function (``response`` is
assumed to be a :term:`WebOb` -style :term:`response` object
- computed previously by the view code)::
+ computed previously by the view code):
.. code-block:: python
@@ -170,12 +170,14 @@ def forget(request):
possessed by the currently authenticated user. A common usage
might look like so within the body of a view function
(``response`` is assumed to be an :term:`WebOb` -style
- :term:`response` object computed previously by the view code)::
+ :term:`response` object computed previously by the view code):
- from pyramid.security import forget
- headers = forget(request)
- response.headerlist.extend(headers)
- return response
+ .. code-block:: python
+
+ from pyramid.security import forget
+ headers = forget(request)
+ response.headerlist.extend(headers)
+ return response
If no :term:`authentication policy` is in use, this function will
always return an empty sequence.
diff --git a/pyramid/session.py b/pyramid/session.py
index a95c3f258..c4cfc1949 100644
--- a/pyramid/session.py
+++ b/pyramid/session.py
@@ -125,8 +125,8 @@ def check_csrf_token(request,
.. versionadded:: 1.4a2
"""
- supplied_token = request.params.get(token, request.headers.get(header))
- if supplied_token != request.session.get_csrf_token():
+ supplied_token = request.params.get(token, request.headers.get(header, ""))
+ if strings_differ(request.session.get_csrf_token(), supplied_token):
if raises:
raise BadCSRFToken('check_csrf_token(): Invalid token')
return False
diff --git a/pyramid/static.py b/pyramid/static.py
index c4a9e3cc4..4ff02f798 100644
--- a/pyramid/static.py
+++ b/pyramid/static.py
@@ -174,7 +174,7 @@ class Md5AssetTokenGenerator(object):
def __init__(self):
self.token_cache = {}
- def token(self, pathspec):
+ def tokenize(self, pathspec):
# An astute observer will notice that this use of token_cache doesn't
# look particularly thread safe. Basic read/write operations on Python
# dicts, however, are atomic, so simply accessing and writing values
@@ -192,38 +192,54 @@ class Md5AssetTokenGenerator(object):
self.token_cache[pathspec] = token = _generate_md5(pathspec)
return token
-class PathSegmentMd5CacheBuster(Md5AssetTokenGenerator):
+class PathSegmentCacheBuster(object):
"""
An implementation of :class:`~pyramid.interfaces.ICacheBuster` which
- inserts an md5 checksum token for cache busting in the path portion of an
- asset URL. Generated md5 checksums are cached in order to speed up
- subsequent calls.
+ inserts a token for cache busting in the path portion of an asset URL.
+
+ To use this class, subclass it and provide a ``tokenize`` method which
+ accepts a ``pathspec`` and returns a token.
.. versionadded:: 1.6
"""
- def pregenerate(self, token, subpath, kw):
+ def pregenerate(self, pathspec, subpath, kw):
+ token = self.tokenize(pathspec)
return (token,) + subpath, kw
def match(self, subpath):
return subpath[1:]
-class QueryStringMd5CacheBuster(Md5AssetTokenGenerator):
+class PathSegmentMd5CacheBuster(PathSegmentCacheBuster,
+ Md5AssetTokenGenerator):
+ """
+ An implementation of :class:`~pyramid.interfaces.ICacheBuster` which
+ inserts an md5 checksum token for cache busting in the path portion of an
+ asset URL. Generated md5 checksums are cached in order to speed up
+ subsequent calls.
+
+ .. versionadded:: 1.6
+ """
+ def __init__(self):
+ super(PathSegmentMd5CacheBuster, self).__init__()
+
+class QueryStringCacheBuster(object):
"""
An implementation of :class:`~pyramid.interfaces.ICacheBuster` which adds
- an md5 checksum token for cache busting in the query string of an asset
- URL. Generated md5 checksums are cached in order to speed up subsequent
- calls.
+ a token for cache busting in the query string of an asset URL.
The optional ``param`` argument determines the name of the parameter added
to the query string and defaults to ``'x'``.
+ To use this class, subclass it and provide a ``tokenize`` method which
+ accepts a ``pathspec`` and returns a token.
+
.. versionadded:: 1.6
"""
def __init__(self, param='x'):
- super(QueryStringMd5CacheBuster, self).__init__()
self.param = param
- def pregenerate(self, token, subpath, kw):
+ def pregenerate(self, pathspec, subpath, kw):
+ token = self.tokenize(pathspec)
query = kw.setdefault('_query', {})
if isinstance(query, dict):
query[self.param] = token
@@ -231,7 +247,23 @@ class QueryStringMd5CacheBuster(Md5AssetTokenGenerator):
kw['_query'] = tuple(query) + ((self.param, token),)
return subpath, kw
-class QueryStringConstantCacheBuster(QueryStringMd5CacheBuster):
+class QueryStringMd5CacheBuster(QueryStringCacheBuster,
+ Md5AssetTokenGenerator):
+ """
+ An implementation of :class:`~pyramid.interfaces.ICacheBuster` which adds
+ an md5 checksum token for cache busting in the query string of an asset
+ URL. Generated md5 checksums are cached in order to speed up subsequent
+ calls.
+
+ The optional ``param`` argument determines the name of the parameter added
+ to the query string and defaults to ``'x'``.
+
+ .. versionadded:: 1.6
+ """
+ def __init__(self, param='x'):
+ super(QueryStringMd5CacheBuster, self).__init__(param=param)
+
+class QueryStringConstantCacheBuster(QueryStringCacheBuster):
"""
An implementation of :class:`~pyramid.interfaces.ICacheBuster` which adds
an arbitrary token for cache busting in the query string of an asset URL.
@@ -245,9 +277,8 @@ class QueryStringConstantCacheBuster(QueryStringMd5CacheBuster):
.. versionadded:: 1.6
"""
def __init__(self, token, param='x'):
+ super(QueryStringConstantCacheBuster, self).__init__(param=param)
self._token = token
- self.param = param
- def token(self, pathspec):
+ def tokenize(self, pathspec):
return self._token
-
diff --git a/pyramid/testing.py b/pyramid/testing.py
index f77889e72..667e6af4e 100644
--- a/pyramid/testing.py
+++ b/pyramid/testing.py
@@ -9,7 +9,6 @@ from zope.interface import (
from pyramid.interfaces import (
IRequest,
- IResponseFactory,
ISession,
)
@@ -22,7 +21,7 @@ from pyramid.compat import (
from pyramid.config import Configurator
from pyramid.decorator import reify
from pyramid.path import caller_package
-from pyramid.response import Response
+from pyramid.response import Response, _get_response_factory
from pyramid.registry import Registry
from pyramid.security import (
@@ -42,6 +41,7 @@ from pyramid.request import CallbackMethodsMixin
from pyramid.url import URLMethodsMixin
from pyramid.util import InstancePropertyMixin
+
_marker = object()
class DummyRootFactory(object):
@@ -383,8 +383,8 @@ class DummyRequest(
@reify
def response(self):
- f = self.registry.queryUtility(IResponseFactory, default=Response)
- return f()
+ f = _get_response_factory(self.registry)
+ return f(self)
have_zca = True
diff --git a/pyramid/tests/test_compat.py b/pyramid/tests/test_compat.py
new file mode 100644
index 000000000..23ccce82e
--- /dev/null
+++ b/pyramid/tests/test_compat.py
@@ -0,0 +1,26 @@
+import unittest
+from pyramid.compat import is_unbound_method
+
+class TestUnboundMethods(unittest.TestCase):
+ def test_old_style_bound(self):
+ self.assertFalse(is_unbound_method(OldStyle().run))
+
+ def test_new_style_bound(self):
+ self.assertFalse(is_unbound_method(NewStyle().run))
+
+ def test_old_style_unbound(self):
+ self.assertTrue(is_unbound_method(OldStyle.run))
+
+ def test_new_style_unbound(self):
+ self.assertTrue(is_unbound_method(NewStyle.run))
+
+ def test_normal_func_unbound(self):
+ def func(): return 'OK'
+
+ self.assertFalse(is_unbound_method(func))
+
+class OldStyle:
+ def run(self): return 'OK'
+
+class NewStyle(object):
+ def run(self): return 'OK'
diff --git a/pyramid/tests/test_config/test_factories.py b/pyramid/tests/test_config/test_factories.py
index 6e679397f..42bb5accc 100644
--- a/pyramid/tests/test_config/test_factories.py
+++ b/pyramid/tests/test_config/test_factories.py
@@ -23,6 +23,21 @@ class TestFactoriesMixin(unittest.TestCase):
self.assertEqual(config.registry.getUtility(IRequestFactory),
dummyfactory)
+ def test_set_response_factory(self):
+ from pyramid.interfaces import IResponseFactory
+ config = self._makeOne(autocommit=True)
+ factory = lambda r: object()
+ config.set_response_factory(factory)
+ self.assertEqual(config.registry.getUtility(IResponseFactory), factory)
+
+ def test_set_response_factory_dottedname(self):
+ from pyramid.interfaces import IResponseFactory
+ config = self._makeOne(autocommit=True)
+ config.set_response_factory(
+ 'pyramid.tests.test_config.dummyfactory')
+ self.assertEqual(config.registry.getUtility(IResponseFactory),
+ dummyfactory)
+
def test_set_root_factory(self):
from pyramid.interfaces import IRootFactory
config = self._makeOne()
@@ -111,6 +126,23 @@ class TestFactoriesMixin(unittest.TestCase):
config = self._makeOne(autocommit=True)
self.assertRaises(AttributeError, config.add_request_method)
+ def test_add_request_method_with_text_type_name(self):
+ from pyramid.interfaces import IRequestExtensions
+ from pyramid.compat import text_, PY3
+ from pyramid.exceptions import ConfigurationError
+
+ config = self._makeOne(autocommit=True)
+ def boomshaka(r): pass
+
+ def get_bad_name():
+ if PY3: # pragma: nocover
+ name = b'La Pe\xc3\xb1a'
+ else: # pragma: nocover
+ name = text_(b'La Pe\xc3\xb1a', 'utf-8')
+
+ config.add_request_method(boomshaka, name=name)
+
+ self.assertRaises(ConfigurationError, get_bad_name)
class TestDeprecatedFactoriesMixinMethods(unittest.TestCase):
def setUp(self):
@@ -120,7 +152,7 @@ class TestDeprecatedFactoriesMixinMethods(unittest.TestCase):
def tearDown(self):
from zope.deprecation import __show__
__show__.on()
-
+
def _makeOne(self, *arg, **kw):
from pyramid.config import Configurator
config = Configurator(*arg, **kw)
diff --git a/pyramid/tests/test_config/test_init.py b/pyramid/tests/test_config/test_init.py
index 1e58e4d0f..aeebe3c91 100644
--- a/pyramid/tests/test_config/test_init.py
+++ b/pyramid/tests/test_config/test_init.py
@@ -546,6 +546,18 @@ class ConfiguratorTests(unittest.TestCase):
utility = reg.getUtility(IRequestFactory)
self.assertEqual(utility, factory)
+ def test_setup_registry_response_factory(self):
+ from pyramid.registry import Registry
+ from pyramid.interfaces import IResponseFactory
+ reg = Registry()
+ config = self._makeOne(reg)
+ factory = lambda r: object()
+ config.setup_registry(response_factory=factory)
+ self.assertEqual(reg.queryUtility(IResponseFactory), None)
+ config.commit()
+ utility = reg.getUtility(IResponseFactory)
+ self.assertEqual(utility, factory)
+
def test_setup_registry_request_factory_dottedname(self):
from pyramid.registry import Registry
from pyramid.interfaces import IRequestFactory
diff --git a/pyramid/tests/test_config/test_util.py b/pyramid/tests/test_config/test_util.py
index bb61714ae..ccf7fa260 100644
--- a/pyramid/tests/test_config/test_util.py
+++ b/pyramid/tests/test_config/test_util.py
@@ -568,6 +568,13 @@ class Test_takes_one_arg(unittest.TestCase):
foo = Foo()
self.assertTrue(self._callFUT(foo.method))
+ def test_function_annotations(self):
+ def foo(bar):
+ """ """
+ # avoid SyntaxErrors in python2, this if effectively nop
+ getattr(foo, '__annotations__', {}).update({'bar': 'baz'})
+ self.assertTrue(self._callFUT(foo))
+
class TestNotted(unittest.TestCase):
def _makeOne(self, predicate):
from pyramid.config.util import Notted
diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py
index b0d03fb72..36c86f78c 100644
--- a/pyramid/tests/test_config/test_views.py
+++ b/pyramid/tests/test_config/test_views.py
@@ -1666,6 +1666,20 @@ class TestViewsConfigurationMixin(unittest.TestCase):
renderer=null_renderer)
self.assertRaises(ConfigurationConflictError, config.commit)
+ def test_add_view_class_method_no_attr(self):
+ from pyramid.renderers import null_renderer
+ from zope.interface import directlyProvides
+ from pyramid.exceptions import ConfigurationError
+
+ config = self._makeOne(autocommit=True)
+ class DummyViewClass(object):
+ def run(self): pass
+
+ def configure_view():
+ config.add_view(view=DummyViewClass.run, renderer=null_renderer)
+
+ self.assertRaises(ConfigurationError, configure_view)
+
def test_derive_view_function(self):
from pyramid.renderers import null_renderer
def view(request):
@@ -3981,7 +3995,7 @@ class TestStaticURLInfo(unittest.TestCase):
def test_add_cachebust_default(self):
config = self._makeConfig()
inst = self._makeOne()
- inst._default_cachebust = DummyCacheBuster
+ inst._default_cachebust = lambda: DummyCacheBuster('foo')
inst.add(config, 'view', 'mypackage:path', cachebust=True)
cachebust = config.registry._static_url_registrations[0][3]
subpath, kw = cachebust('some/path', {})
@@ -4000,7 +4014,7 @@ class TestStaticURLInfo(unittest.TestCase):
config = self._makeConfig()
inst = self._makeOne()
inst.add(config, 'view', 'mypackage:path',
- cachebust=DummyCacheBuster())
+ cachebust=DummyCacheBuster('foo'))
cachebust = config.registry._static_url_registrations[0][3]
subpath, kw = cachebust('some/path', {})
self.assertEqual(subpath, 'some/path')
@@ -4113,10 +4127,10 @@ class DummyMultiView:
""" """
class DummyCacheBuster(object):
- def token(self, pathspec):
- return 'foo'
- def pregenerate(self, token, subpath, kw):
- kw['x'] = token
+ def __init__(self, token):
+ self.token = token
+ def pregenerate(self, pathspec, subpath, kw):
+ kw['x'] = self.token
return subpath, kw
def parse_httpdate(s):
diff --git a/pyramid/tests/test_decorator.py b/pyramid/tests/test_decorator.py
index 9ab1b7229..0a98a512d 100644
--- a/pyramid/tests/test_decorator.py
+++ b/pyramid/tests/test_decorator.py
@@ -15,15 +15,19 @@ class TestReify(unittest.TestCase):
self.assertEqual(inst.__dict__['wrapped'], 'a')
def test___get__noinst(self):
- decorator = self._makeOne(None)
+ def wrapped(inst):
+ return 'a' # pragma: no cover
+ decorator = self._makeOne(wrapped)
result = decorator.__get__(None)
self.assertEqual(result, decorator)
- def test___doc__copied(self):
- def wrapped(inst):
- """My doc"""
- decorator = self._makeOne(wrapped)
- self.assertEqual(decorator.__doc__, "My doc")
-
+ def test_dunder_attrs_copied(self):
+ from pyramid.util import viewdefaults
+ decorator = self._makeOne(viewdefaults)
+ self.assertEqual(decorator.__doc__, viewdefaults.__doc__)
+ self.assertEqual(decorator.__name__, viewdefaults.__name__)
+ self.assertEqual(decorator.__module__, viewdefaults.__module__)
+
+
class Dummy(object):
pass
diff --git a/pyramid/tests/test_renderers.py b/pyramid/tests/test_renderers.py
index 2bddd2318..6d79cc291 100644
--- a/pyramid/tests/test_renderers.py
+++ b/pyramid/tests/test_renderers.py
@@ -182,7 +182,10 @@ class TestRendererHelper(unittest.TestCase):
from pyramid.interfaces import IResponseFactory
class ResponseFactory(object):
pass
- self.config.registry.registerUtility(ResponseFactory, IResponseFactory)
+
+ self.config.registry.registerUtility(
+ lambda r: ResponseFactory(), IResponseFactory
+ )
def test_render_to_response(self):
self._registerRendererFactory()
@@ -191,8 +194,8 @@ class TestRendererHelper(unittest.TestCase):
helper = self._makeOne('loo.foo')
response = helper.render_to_response('values', {},
request=request)
- self.assertEqual(response.body[0], 'values')
- self.assertEqual(response.body[1], {})
+ self.assertEqual(response.app_iter[0], 'values')
+ self.assertEqual(response.app_iter[1], {})
def test_get_renderer(self):
factory = self._registerRendererFactory()
@@ -209,8 +212,8 @@ class TestRendererHelper(unittest.TestCase):
request = testing.DummyRequest()
response = 'response'
response = helper.render_view(request, response, view, context)
- self.assertEqual(response.body[0], 'response')
- self.assertEqual(response.body[1],
+ self.assertEqual(response.app_iter[0], 'response')
+ self.assertEqual(response.app_iter[1],
{'renderer_info': helper,
'renderer_name': 'loo.foo',
'request': request,
@@ -287,6 +290,23 @@ class TestRendererHelper(unittest.TestCase):
response = helper._make_response(la.encode('utf-8'), request)
self.assertEqual(response.body, la.encode('utf-8'))
+ def test__make_response_result_is_iterable(self):
+ from pyramid.response import Response
+ request = testing.DummyRequest()
+ request.response = Response()
+ helper = self._makeOne('loo.foo')
+ la = text_('/La Pe\xc3\xb1a', 'utf-8')
+ response = helper._make_response([la.encode('utf-8')], request)
+ self.assertEqual(response.body, la.encode('utf-8'))
+
+ def test__make_response_result_is_other(self):
+ self._registerResponseFactory()
+ request = None
+ helper = self._makeOne('loo.foo')
+ result = object()
+ response = helper._make_response(result, request)
+ self.assertEqual(response.body, result)
+
def test__make_response_result_is_None_no_body(self):
from pyramid.response import Response
request = testing.DummyRequest()
@@ -310,7 +330,9 @@ class TestRendererHelper(unittest.TestCase):
class ResponseFactory(object):
def __init__(self):
pass
- self.config.registry.registerUtility(ResponseFactory, IResponseFactory)
+ self.config.registry.registerUtility(
+ lambda r: ResponseFactory(), IResponseFactory
+ )
request = testing.DummyRequest()
helper = self._makeOne('loo.foo')
response = helper._make_response(b'abc', request)
@@ -580,6 +602,12 @@ class TestJSONP(unittest.TestCase):
self.assertEqual(request.response.content_type,
'application/json')
+ def test_render_without_request(self):
+ renderer_factory = self._makeOne()
+ renderer = renderer_factory(None)
+ result = renderer({'a':'1'}, {})
+ self.assertEqual(result, '{"a": "1"}')
+
class Dummy:
pass
diff --git a/pyramid/tests/test_request.py b/pyramid/tests/test_request.py
index 48af98f59..f142e4536 100644
--- a/pyramid/tests/test_request.py
+++ b/pyramid/tests/test_request.py
@@ -435,7 +435,50 @@ class Test_call_app_with_subpath_as_path_info(unittest.TestCase):
self.assertEqual(request.environ['SCRIPT_NAME'], '/' + encoded)
self.assertEqual(request.environ['PATH_INFO'], '/' + encoded)
-class DummyRequest:
+class Test_apply_request_extensions(unittest.TestCase):
+ def setUp(self):
+ self.config = testing.setUp()
+
+ def tearDown(self):
+ testing.tearDown()
+
+ def _callFUT(self, request, extensions=None):
+ from pyramid.request import apply_request_extensions
+ return apply_request_extensions(request, extensions=extensions)
+
+ def test_it_with_registry(self):
+ from pyramid.interfaces import IRequestExtensions
+ extensions = Dummy()
+ extensions.methods = {'foo': lambda x, y: y}
+ extensions.descriptors = {'bar': property(lambda x: 'bar')}
+ self.config.registry.registerUtility(extensions, IRequestExtensions)
+ request = DummyRequest()
+ request.registry = self.config.registry
+ self._callFUT(request)
+ self.assertEqual(request.bar, 'bar')
+ self.assertEqual(request.foo('abc'), 'abc')
+
+ def test_it_override_extensions(self):
+ from pyramid.interfaces import IRequestExtensions
+ ignore = Dummy()
+ ignore.methods = {'x': lambda x, y, z: 'asdf'}
+ ignore.descriptors = {'bar': property(lambda x: 'asdf')}
+ self.config.registry.registerUtility(ignore, IRequestExtensions)
+ request = DummyRequest()
+ request.registry = self.config.registry
+
+ extensions = Dummy()
+ extensions.methods = {'foo': lambda x, y: y}
+ extensions.descriptors = {'bar': property(lambda x: 'bar')}
+ self._callFUT(request, extensions=extensions)
+ self.assertRaises(AttributeError, lambda: request.x)
+ self.assertEqual(request.bar, 'bar')
+ self.assertEqual(request.foo('abc'), 'abc')
+
+class Dummy(object):
+ pass
+
+class DummyRequest(object):
def __init__(self, environ=None):
if environ is None:
environ = {}
diff --git a/pyramid/tests/test_response.py b/pyramid/tests/test_response.py
index 84ec57757..ad55882c9 100644
--- a/pyramid/tests/test_response.py
+++ b/pyramid/tests/test_response.py
@@ -8,7 +8,7 @@ class TestResponse(unittest.TestCase):
def _getTargetClass(self):
from pyramid.response import Response
return Response
-
+
def test_implements_IResponse(self):
from pyramid.interfaces import IResponse
cls = self._getTargetClass()
@@ -119,7 +119,7 @@ class Test_patch_mimetypes(unittest.TestCase):
result = self._callFUT(module)
self.assertEqual(result, True)
self.assertEqual(module.initted, True)
-
+
def test_missing_init(self):
class DummyMimetypes(object):
pass
@@ -174,6 +174,17 @@ class TestResponseAdapter(unittest.TestCase):
self.assertEqual(dummy_venusian.attached,
[(foo, dec.register, 'pyramid')])
+
+class TestGetResponseFactory(unittest.TestCase):
+ def test_get_factory(self):
+ from pyramid.registry import Registry
+ from pyramid.response import Response, _get_response_factory
+
+ registry = Registry()
+ response = _get_response_factory(registry)(None)
+ self.assertTrue(isinstance(response, Response))
+
+
class Dummy(object):
pass
@@ -190,5 +201,3 @@ class DummyVenusian(object):
def attach(self, wrapped, fn, category=None):
self.attached.append((wrapped, fn, category))
-
-
diff --git a/pyramid/tests/test_router.py b/pyramid/tests/test_router.py
index c6c6eea1c..b57c248d5 100644
--- a/pyramid/tests/test_router.py
+++ b/pyramid/tests/test_router.py
@@ -317,6 +317,7 @@ class TestRouter(unittest.TestCase):
from pyramid.interfaces import IRequestExtensions
from pyramid.interfaces import IRequest
from pyramid.request import Request
+ from pyramid.util import InstancePropertyHelper
context = DummyContext()
self._registerTraverserFactory(context)
class Extensions(object):
@@ -324,11 +325,12 @@ class TestRouter(unittest.TestCase):
self.methods = {}
self.descriptors = {}
extensions = Extensions()
- L = []
+ ext_method = lambda r: 'bar'
+ name, fn = InstancePropertyHelper.make_property(ext_method, name='foo')
+ extensions.descriptors[name] = fn
request = Request.blank('/')
request.request_iface = IRequest
request.registry = self.registry
- request._set_extensions = lambda *x: L.extend(x)
def request_factory(environ):
return request
self.registry.registerUtility(extensions, IRequestExtensions)
@@ -342,7 +344,7 @@ class TestRouter(unittest.TestCase):
router.request_factory = request_factory
start_response = DummyStartResponse()
router(environ, start_response)
- self.assertEqual(L, [extensions])
+ self.assertEqual(view.request.foo, 'bar')
def test_call_view_registered_nonspecific_default_path(self):
from pyramid.interfaces import IViewClassifier
@@ -599,17 +601,19 @@ class TestRouter(unittest.TestCase):
environ = self._makeEnviron()
self._registerView(view, '', IViewClassifier, None, None)
request_events = self._registerEventListener(INewRequest)
- aftertraversal_events = self._registerEventListener(IContextFound)
+ context_found_events = self._registerEventListener(IContextFound)
response_events = self._registerEventListener(INewResponse)
router = self._makeOne()
start_response = DummyStartResponse()
result = router(environ, start_response)
self.assertEqual(len(request_events), 1)
self.assertEqual(request_events[0].request.environ, environ)
- self.assertEqual(len(aftertraversal_events), 1)
- self.assertEqual(aftertraversal_events[0].request.environ, environ)
+ self.assertEqual(len(context_found_events), 1)
+ self.assertEqual(context_found_events[0].request.environ, environ)
+ self.assertEqual(context_found_events[0].request.context, context)
self.assertEqual(len(response_events), 1)
self.assertEqual(response_events[0].response, response)
+ self.assertEqual(response_events[0].request.context, context)
self.assertEqual(result, response.app_iter)
def test_call_newrequest_evllist_exc_can_be_caught_by_exceptionview(self):
diff --git a/pyramid/tests/test_scripting.py b/pyramid/tests/test_scripting.py
index a36d1ed71..1e952062b 100644
--- a/pyramid/tests/test_scripting.py
+++ b/pyramid/tests/test_scripting.py
@@ -122,11 +122,15 @@ class Test_prepare(unittest.TestCase):
self.assertEqual(request.context, context)
def test_it_with_extensions(self):
- exts = Dummy()
+ from pyramid.util import InstancePropertyHelper
+ exts = DummyExtensions()
+ ext_method = lambda r: 'bar'
+ name, fn = InstancePropertyHelper.make_property(ext_method, 'foo')
+ exts.descriptors[name] = fn
request = DummyRequest({})
registry = request.registry = self._makeRegistry([exts, DummyFactory])
info = self._callFUT(request=request, registry=registry)
- self.assertEqual(request.extensions, exts)
+ self.assertEqual(request.foo, 'bar')
root, closer = info['root'], info['closer']
closer()
@@ -199,11 +203,13 @@ class DummyThreadLocalManager:
def pop(self):
self.popped.append(True)
-class DummyRequest:
+class DummyRequest(object):
matchdict = None
matched_route = None
def __init__(self, environ):
self.environ = environ
- def _set_extensions(self, exts):
- self.extensions = exts
+class DummyExtensions:
+ def __init__(self):
+ self.descriptors = {}
+ self.methods = {}
diff --git a/pyramid/tests/test_scripts/dummy.py b/pyramid/tests/test_scripts/dummy.py
index 366aa00b5..930b9ed64 100644
--- a/pyramid/tests/test_scripts/dummy.py
+++ b/pyramid/tests/test_scripts/dummy.py
@@ -60,7 +60,7 @@ class DummyMapper(object):
def __init__(self, *routes):
self.routes = routes
- def get_routes(self):
+ def get_routes(self, include_static=False):
return self.routes
class DummyRoute(object):
diff --git a/pyramid/tests/test_scripts/test_pcreate.py b/pyramid/tests/test_scripts/test_pcreate.py
index 020721ca7..63e5e6368 100644
--- a/pyramid/tests/test_scripts/test_pcreate.py
+++ b/pyramid/tests/test_scripts/test_pcreate.py
@@ -12,10 +12,10 @@ class TestPCreateCommand(unittest.TestCase):
from pyramid.scripts.pcreate import PCreateCommand
return PCreateCommand
- def _makeOne(self, *args):
+ def _makeOne(self, *args, **kw):
effargs = ['pcreate']
effargs.extend(args)
- cmd = self._getTargetClass()(effargs)
+ cmd = self._getTargetClass()(effargs, **kw)
cmd.out = self.out
return cmd
@@ -34,8 +34,13 @@ class TestPCreateCommand(unittest.TestCase):
out = self.out_.getvalue()
self.assertTrue(out.startswith('No scaffolds available'))
+ def test_run_no_scaffold_no_args(self):
+ cmd = self._makeOne(quiet=True)
+ result = cmd.run()
+ self.assertEqual(result, 2)
+
def test_run_no_scaffold_name(self):
- cmd = self._makeOne()
+ cmd = self._makeOne('dummy')
result = cmd.run()
self.assertEqual(result, 2)
out = self.out_.getvalue()
diff --git a/pyramid/tests/test_scripts/test_prequest.py b/pyramid/tests/test_scripts/test_prequest.py
index 37f1d3c0f..95cec0518 100644
--- a/pyramid/tests/test_scripts/test_prequest.py
+++ b/pyramid/tests/test_scripts/test_prequest.py
@@ -210,8 +210,21 @@ class TestPRequestCommand(unittest.TestCase):
self.assertEqual(self._path_info, '/')
self.assertEqual(self._spec, 'development.ini')
self.assertEqual(self._app_name, None)
+
self.assertEqual(self._out, [b'abc'])
+ def test_command_method_configures_logging(self):
+ command = self._makeOne(['', 'development.ini', '/'])
+ called_args = []
+
+ def configure_logging(app_spec):
+ called_args.append(app_spec)
+
+ command.configure_logging = configure_logging
+ command.run()
+ self.assertEqual(called_args, ['development.ini'])
+
+
class Test_main(unittest.TestCase):
def _callFUT(self, argv):
from pyramid.scripts.prequest import main
diff --git a/pyramid/tests/test_scripts/test_proutes.py b/pyramid/tests/test_scripts/test_proutes.py
index 32202af4b..e426eee73 100644
--- a/pyramid/tests/test_scripts/test_proutes.py
+++ b/pyramid/tests/test_scripts/test_proutes.py
@@ -1,6 +1,16 @@
import unittest
from pyramid.tests.test_scripts import dummy
+
+class DummyIntrospector(object):
+ def __init__(self):
+ self.relations = {}
+ self.introspectables = {}
+
+ def get(self, name, discrim):
+ pass
+
+
class TestPRoutesCommand(unittest.TestCase):
def _getTargetClass(self):
from pyramid.scripts.proutes import PRoutesCommand
@@ -10,8 +20,20 @@ class TestPRoutesCommand(unittest.TestCase):
cmd = self._getTargetClass()([])
cmd.bootstrap = (dummy.DummyBootstrap(),)
cmd.args = ('/foo/bar/myapp.ini#myapp',)
+
return cmd
+ def _makeRegistry(self):
+ from pyramid.registry import Registry
+ registry = Registry()
+ registry.introspector = DummyIntrospector()
+ return registry
+
+ def _makeConfig(self, *arg, **kw):
+ from pyramid.config import Configurator
+ config = Configurator(*arg, **kw)
+ return config
+
def test_good_args(self):
cmd = self._getTargetClass()([])
cmd.bootstrap = (dummy.DummyBootstrap(),)
@@ -19,6 +41,8 @@ class TestPRoutesCommand(unittest.TestCase):
route = dummy.DummyRoute('a', '/a')
mapper = dummy.DummyMapper(route)
cmd._get_mapper = lambda *arg: mapper
+ registry = self._makeRegistry()
+ cmd.bootstrap = (dummy.DummyBootstrap(registry=registry),)
L = []
cmd.out = lambda msg: L.append(msg)
cmd.run()
@@ -58,12 +82,15 @@ class TestPRoutesCommand(unittest.TestCase):
route = dummy.DummyRoute('a', '/a')
mapper = dummy.DummyMapper(route)
command._get_mapper = lambda *arg: mapper
+ registry = self._makeRegistry()
+ command.bootstrap = (dummy.DummyBootstrap(registry=registry),)
+
L = []
command.out = L.append
result = command.run()
self.assertEqual(result, 0)
self.assertEqual(len(L), 3)
- self.assertEqual(L[-1].split(), ['a', '/a', '<unknown>'])
+ self.assertEqual(L[-1].split(), ['a', '/a', '<unknown>', '*'])
def test_route_with_no_slash_prefix(self):
command = self._makeOne()
@@ -72,16 +99,18 @@ class TestPRoutesCommand(unittest.TestCase):
command._get_mapper = lambda *arg: mapper
L = []
command.out = L.append
+ registry = self._makeRegistry()
+ command.bootstrap = (dummy.DummyBootstrap(registry=registry),)
result = command.run()
self.assertEqual(result, 0)
self.assertEqual(len(L), 3)
- self.assertEqual(L[-1].split(), ['a', '/a', '<unknown>'])
+ self.assertEqual(L[-1].split(), ['a', '/a', '<unknown>', '*'])
def test_single_route_no_views_registered(self):
from zope.interface import Interface
- from pyramid.registry import Registry
from pyramid.interfaces import IRouteRequest
- registry = Registry()
+ registry = self._makeRegistry()
+
def view():pass
class IMyRoute(Interface):
pass
@@ -96,15 +125,15 @@ class TestPRoutesCommand(unittest.TestCase):
result = command.run()
self.assertEqual(result, 0)
self.assertEqual(len(L), 3)
- self.assertEqual(L[-1].split()[:3], ['a', '/a', 'None'])
+ self.assertEqual(L[-1].split()[:3], ['a', '/a', '<unknown>'])
def test_single_route_one_view_registered(self):
from zope.interface import Interface
- from pyramid.registry import Registry
from pyramid.interfaces import IRouteRequest
from pyramid.interfaces import IViewClassifier
from pyramid.interfaces import IView
- registry = Registry()
+ registry = self._makeRegistry()
+
def view():pass
class IMyRoute(Interface):
pass
@@ -130,11 +159,11 @@ class TestPRoutesCommand(unittest.TestCase):
def test_one_route_with_long_name_one_view_registered(self):
from zope.interface import Interface
- from pyramid.registry import Registry
from pyramid.interfaces import IRouteRequest
from pyramid.interfaces import IViewClassifier
from pyramid.interfaces import IView
- registry = Registry()
+ registry = self._makeRegistry()
+
def view():pass
class IMyRoute(Interface):
@@ -172,11 +201,11 @@ class TestPRoutesCommand(unittest.TestCase):
def test_single_route_one_view_registered_with_factory(self):
from zope.interface import Interface
- from pyramid.registry import Registry
from pyramid.interfaces import IRouteRequest
from pyramid.interfaces import IViewClassifier
from pyramid.interfaces import IView
- registry = Registry()
+ registry = self._makeRegistry()
+
def view():pass
class IMyRoot(Interface):
pass
@@ -201,12 +230,11 @@ class TestPRoutesCommand(unittest.TestCase):
def test_single_route_multiview_registered(self):
from zope.interface import Interface
- from pyramid.registry import Registry
from pyramid.interfaces import IRouteRequest
from pyramid.interfaces import IViewClassifier
from pyramid.interfaces import IMultiView
- registry = Registry()
+ registry = self._makeRegistry()
def view(): pass
@@ -235,19 +263,494 @@ class TestPRoutesCommand(unittest.TestCase):
self.assertEqual(result, 0)
self.assertEqual(len(L), 3)
compare_to = L[-1].split()[:3]
+ view_module = 'pyramid.tests.test_scripts.dummy'
+ view_str = '<pyramid.tests.test_scripts.dummy.DummyMultiView'
+ final = '%s.%s' % (view_module, view_str)
+
self.assertEqual(
compare_to,
- ['a', '/a', 'pyramid.tests.test_scripts.test_proutes.view']
+ ['a', '/a', final]
)
def test__get_mapper(self):
- from pyramid.registry import Registry
from pyramid.urldispatch import RoutesMapper
command = self._makeOne()
- registry = Registry()
+ registry = self._makeRegistry()
+
result = command._get_mapper(registry)
self.assertEqual(result.__class__, RoutesMapper)
+ def test_one_route_all_methods_view_only_post(self):
+ from pyramid.renderers import null_renderer as nr
+
+ def view1(context, request): return 'view1'
+
+ config = self._makeConfig(autocommit=True)
+ config.add_route('foo', '/a/b')
+ config.add_view(
+ route_name='foo',
+ view=view1,
+ 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.test_proutes.view1', 'POST'
+ ]
+ self.assertEqual(compare_to, expected)
+
+ def test_one_route_only_post_view_all_methods(self):
+ from pyramid.renderers import null_renderer as nr
+
+ def view1(context, request): return 'view1'
+
+ config = self._makeConfig(autocommit=True)
+ config.add_route('foo', '/a/b', request_method='POST')
+ config.add_view(
+ route_name='foo',
+ view=view1,
+ renderer=nr,
+ )
+
+ 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.test_proutes.view1', 'POST'
+ ]
+ self.assertEqual(compare_to, expected)
+
+ def test_one_route_only_post_view_post_and_get(self):
+ from pyramid.renderers import null_renderer as nr
+
+ def view1(context, request): return 'view1'
+
+ config = self._makeConfig(autocommit=True)
+ config.add_route('foo', '/a/b', request_method='POST')
+ config.add_view(
+ route_name='foo',
+ view=view1,
+ renderer=nr,
+ request_method=('POST', 'GET')
+ )
+
+ 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.test_proutes.view1', 'POST'
+ ]
+ self.assertEqual(compare_to, expected)
+
+ def test_route_request_method_mismatch(self):
+ from pyramid.renderers import null_renderer as nr
+
+ def view1(context, request): return 'view1'
+
+ config = self._makeConfig(autocommit=True)
+ config.add_route('foo', '/a/b', request_method='POST')
+ config.add_view(
+ route_name='foo',
+ view=view1,
+ renderer=nr,
+ request_method='GET'
+ )
+
+ 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.test_proutes.view1',
+ '<route', 'mismatch>'
+ ]
+ self.assertEqual(compare_to, expected)
+
+ def test_route_static_views(self):
+ from pyramid.renderers import null_renderer as nr
+ config = self._makeConfig(autocommit=True)
+ config.add_static_view('static', 'static', cache_max_age=3600)
+ config.add_static_view(name='static2', path='/var/www/static')
+ config.add_static_view(
+ name='pyramid_scaffold',
+ path='pyramid:scaffolds/starter/+package+/static'
+ )
+
+ 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), 5)
+
+ expected = [
+ ['__static/', '/static/*subpath',
+ 'pyramid.tests.test_scripts:static/', '*'],
+ ['__static2/', '/static2/*subpath', '/var/www/static/', '*'],
+ ['__pyramid_scaffold/', '/pyramid_scaffold/*subpath',
+ 'pyramid:scaffolds/starter/+package+/static/', '*'],
+ ]
+
+ for index, line in enumerate(L[2:]):
+ data = line.split()
+ self.assertEqual(data, expected[index])
+
+ def test_route_no_view(self):
+ from pyramid.renderers import null_renderer as nr
+ config = self._makeConfig(autocommit=True)
+ config.add_route('foo', '/a/b', 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',
+ '<unknown>',
+ 'POST',
+ ]
+ self.assertEqual(compare_to, expected)
+
+ def test_route_as_wsgiapp(self):
+ from pyramid.wsgi import wsgiapp2
+
+ config1 = self._makeConfig(autocommit=True)
+ def view1(context, request): return 'view1'
+ config1.add_route('foo', '/a/b', request_method='POST')
+ config1.add_view(view=view1, route_name='foo')
+
+ config2 = self._makeConfig(autocommit=True)
+ config2.add_route('foo', '/a/b', request_method='POST')
+ config2.add_view(
+ wsgiapp2(config1.make_wsgi_app()),
+ route_name='foo',
+ )
+
+ command = self._makeOne()
+ L = []
+ command.out = L.append
+ command.bootstrap = (dummy.DummyBootstrap(registry=config2.registry),)
+ result = command.run()
+ self.assertEqual(result, 0)
+ self.assertEqual(len(L), 3)
+ compare_to = L[-1].split()
+ expected = [
+ 'foo', '/a/b',
+ '<wsgiapp>',
+ 'POST',
+ ]
+ self.assertEqual(compare_to, expected)
+
+ def test_route_is_get_view_request_method_not_post(self):
+ from pyramid.renderers import null_renderer as nr
+ from pyramid.config import not_
+
+ def view1(context, request): return 'view1'
+
+ config = self._makeConfig(autocommit=True)
+ config.add_route('foo', '/a/b', request_method='GET')
+ config.add_view(
+ route_name='foo',
+ view=view1,
+ renderer=nr,
+ request_method=not_('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.test_proutes.view1',
+ 'GET'
+ ]
+ self.assertEqual(compare_to, expected)
+
+ def test_view_request_method_not_post(self):
+ from pyramid.renderers import null_renderer as nr
+ from pyramid.config import not_
+
+ def view1(context, request): return 'view1'
+
+ config = self._makeConfig(autocommit=True)
+ config.add_route('foo', '/a/b')
+ config.add_view(
+ route_name='foo',
+ view=view1,
+ renderer=nr,
+ request_method=not_('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.test_proutes.view1',
+ '!POST,*'
+ ]
+ self.assertEqual(compare_to, expected)
+
+ def test_view_glob(self):
+ from pyramid.renderers import null_renderer as nr
+ from pyramid.config import not_
+
+ def view1(context, request): return 'view1'
+ def view2(context, request): return 'view2'
+
+ config = self._makeConfig(autocommit=True)
+ config.add_route('foo', '/a/b')
+ config.add_view(
+ route_name='foo',
+ view=view1,
+ renderer=nr,
+ request_method=not_('POST')
+ )
+
+ config.add_route('bar', '/b/a')
+ config.add_view(
+ route_name='bar',
+ view=view2,
+ renderer=nr,
+ request_method=not_('POST')
+ )
+
+ command = self._makeOne()
+ command.options.glob = '*foo*'
+
+ 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.test_proutes.view1',
+ '!POST,*'
+ ]
+ self.assertEqual(compare_to, expected)
+
+ def test_good_format(self):
+ from pyramid.renderers import null_renderer as nr
+ from pyramid.config import not_
+
+ def view1(context, request): return 'view1'
+
+ config = self._makeConfig(autocommit=True)
+ config.add_route('foo', '/a/b')
+ config.add_view(
+ route_name='foo',
+ view=view1,
+ renderer=nr,
+ request_method=not_('POST')
+ )
+
+ command = self._makeOne()
+ command.options.glob = '*foo*'
+ command.options.format = 'method,name'
+ 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 = ['!POST,*', 'foo']
+
+ self.assertEqual(compare_to, expected)
+ self.assertEqual(L[0].split(), ['Method', 'Name'])
+
+ def test_bad_format(self):
+ from pyramid.renderers import null_renderer as nr
+ from pyramid.config import not_
+
+ def view1(context, request): return 'view1'
+
+ config = self._makeConfig(autocommit=True)
+ config.add_route('foo', '/a/b')
+ config.add_view(
+ route_name='foo',
+ view=view1,
+ renderer=nr,
+ request_method=not_('POST')
+ )
+
+ command = self._makeOne()
+ command.options.glob = '*foo*'
+ command.options.format = 'predicates,name,pattern'
+ L = []
+ command.out = L.append
+ command.bootstrap = (dummy.DummyBootstrap(registry=config.registry),)
+ expected = (
+ "You provided invalid formats ['predicates'], "
+ "Available formats are ['name', 'pattern', 'view', 'method']"
+ )
+ result = command.run()
+ self.assertEqual(result, 2)
+ self.assertEqual(L[0], expected)
+
+ def test_config_format_ini_newlines(self):
+ from pyramid.renderers import null_renderer as nr
+ from pyramid.config import not_
+
+ def view1(context, request): return 'view1'
+
+ config = self._makeConfig(autocommit=True)
+ config.add_route('foo', '/a/b')
+ config.add_view(
+ route_name='foo',
+ view=view1,
+ renderer=nr,
+ request_method=not_('POST')
+ )
+
+ command = self._makeOne()
+
+ L = []
+ command.out = L.append
+ command.bootstrap = (dummy.DummyBootstrap(registry=config.registry),)
+ config_factory = dummy.DummyConfigParserFactory()
+ command.ConfigParser = config_factory
+ config_factory.items = [('format', 'method\nname')]
+
+ result = command.run()
+ self.assertEqual(result, 0)
+ self.assertEqual(len(L), 3)
+ compare_to = L[-1].split()
+ expected = ['!POST,*', 'foo']
+
+ self.assertEqual(compare_to, expected)
+ self.assertEqual(L[0].split(), ['Method', 'Name'])
+
+ def test_config_format_ini_spaces(self):
+ from pyramid.renderers import null_renderer as nr
+ from pyramid.config import not_
+
+ def view1(context, request): return 'view1'
+
+ config = self._makeConfig(autocommit=True)
+ config.add_route('foo', '/a/b')
+ config.add_view(
+ route_name='foo',
+ view=view1,
+ renderer=nr,
+ request_method=not_('POST')
+ )
+
+ command = self._makeOne()
+
+ L = []
+ command.out = L.append
+ command.bootstrap = (dummy.DummyBootstrap(registry=config.registry),)
+ config_factory = dummy.DummyConfigParserFactory()
+ command.ConfigParser = config_factory
+ config_factory.items = [('format', 'method name')]
+
+ result = command.run()
+ self.assertEqual(result, 0)
+ self.assertEqual(len(L), 3)
+ compare_to = L[-1].split()
+ expected = ['!POST,*', 'foo']
+
+ self.assertEqual(compare_to, expected)
+ self.assertEqual(L[0].split(), ['Method', 'Name'])
+
+ def test_config_format_ini_commas(self):
+ from pyramid.renderers import null_renderer as nr
+ from pyramid.config import not_
+
+ def view1(context, request): return 'view1'
+
+ config = self._makeConfig(autocommit=True)
+ config.add_route('foo', '/a/b')
+ config.add_view(
+ route_name='foo',
+ view=view1,
+ renderer=nr,
+ request_method=not_('POST')
+ )
+
+ command = self._makeOne()
+
+ L = []
+ command.out = L.append
+ command.bootstrap = (dummy.DummyBootstrap(registry=config.registry),)
+ config_factory = dummy.DummyConfigParserFactory()
+ command.ConfigParser = config_factory
+ config_factory.items = [('format', 'method,name')]
+
+ result = command.run()
+ self.assertEqual(result, 0)
+ self.assertEqual(len(L), 3)
+ compare_to = L[-1].split()
+ expected = ['!POST,*', 'foo']
+
+ self.assertEqual(compare_to, expected)
+ self.assertEqual(L[0].split(), ['Method', 'Name'])
+
+ def test_static_routes_included_in_list(self):
+ from pyramid.renderers import null_renderer as nr
+
+ config = self._makeConfig(autocommit=True)
+ config.add_route('foo', 'http://example.com/bar.aspx', static=True)
+
+ 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', 'http://example.com/bar.aspx',
+ '<unknown>', '*',
+ ]
+ self.assertEqual(compare_to, expected)
+
class Test_main(unittest.TestCase):
def _callFUT(self, argv):
from pyramid.scripts.proutes import main
diff --git a/pyramid/tests/test_static.py b/pyramid/tests/test_static.py
index 2f4de249e..a3df74b44 100644
--- a/pyramid/tests/test_static.py
+++ b/pyramid/tests/test_static.py
@@ -393,13 +393,13 @@ class TestMd5AssetTokenGenerator(unittest.TestCase):
return cls()
def test_package_resource(self):
- fut = self._makeOne().token
+ fut = self._makeOne().tokenize
expected = '76d653a3a044e2f4b38bb001d283e3d9'
token = fut('pyramid.tests:fixtures/static/index.html')
self.assertEqual(token, expected)
def test_filesystem_resource(self):
- fut = self._makeOne().token
+ fut = self._makeOne().tokenize
expected = 'd5155f250bef0e9923e894dbc713c5dd'
with open(self.fspath, 'w') as f:
f.write("Are we rich yet?")
@@ -407,7 +407,7 @@ class TestMd5AssetTokenGenerator(unittest.TestCase):
self.assertEqual(token, expected)
def test_cache(self):
- fut = self._makeOne().token
+ fut = self._makeOne().tokenize
expected = 'd5155f250bef0e9923e894dbc713c5dd'
with open(self.fspath, 'w') as f:
f.write("Are we rich yet?")
@@ -425,11 +425,11 @@ class TestPathSegmentMd5CacheBuster(unittest.TestCase):
def _makeOne(self):
from pyramid.static import PathSegmentMd5CacheBuster as cls
inst = cls()
- inst.token = lambda pathspec: 'foo'
+ inst.tokenize = lambda pathspec: 'foo'
return inst
def test_token(self):
- fut = self._makeOne().token
+ fut = self._makeOne().tokenize
self.assertEqual(fut('whatever'), 'foo')
def test_pregenerate(self):
@@ -448,11 +448,11 @@ class TestQueryStringMd5CacheBuster(unittest.TestCase):
inst = cls(param)
else:
inst = cls()
- inst.token = lambda pathspec: 'foo'
+ inst.tokenize = lambda pathspec: 'foo'
return inst
def test_token(self):
- fut = self._makeOne().token
+ fut = self._makeOne().tokenize
self.assertEqual(fut('whatever'), 'foo')
def test_pregenerate(self):
@@ -490,7 +490,7 @@ class TestQueryStringConstantCacheBuster(TestQueryStringMd5CacheBuster):
return inst
def test_token(self):
- fut = self._makeOne().token
+ fut = self._makeOne().tokenize
self.assertEqual(fut('whatever'), 'foo')
def test_pregenerate(self):
diff --git a/pyramid/tests/test_testing.py b/pyramid/tests/test_testing.py
index dfcad2a0c..113f7e5f4 100644
--- a/pyramid/tests/test_testing.py
+++ b/pyramid/tests/test_testing.py
@@ -259,7 +259,9 @@ class TestDummyRequest(unittest.TestCase):
registry = Registry('this_test')
class ResponseFactory(object):
pass
- registry.registerUtility(ResponseFactory, IResponseFactory)
+ registry.registerUtility(
+ lambda r: ResponseFactory(), IResponseFactory
+ )
request = self._makeOne()
request.registry = registry
resp = request.response
diff --git a/pyramid/tests/test_util.py b/pyramid/tests/test_util.py
index a18fa8d16..459c729a0 100644
--- a/pyramid/tests/test_util.py
+++ b/pyramid/tests/test_util.py
@@ -1,9 +1,193 @@
import unittest
from pyramid.compat import PY3
+
+class Test_InstancePropertyHelper(unittest.TestCase):
+ def _makeOne(self):
+ cls = self._getTargetClass()
+ return cls()
+
+ def _getTargetClass(self):
+ from pyramid.util import InstancePropertyHelper
+ return InstancePropertyHelper
+
+ def test_callable(self):
+ def worker(obj):
+ return obj.bar
+ foo = Dummy()
+ helper = self._getTargetClass()
+ helper.set_property(foo, worker)
+ foo.bar = 1
+ self.assertEqual(1, foo.worker)
+ foo.bar = 2
+ self.assertEqual(2, foo.worker)
+
+ def test_callable_with_name(self):
+ def worker(obj):
+ return obj.bar
+ foo = Dummy()
+ helper = self._getTargetClass()
+ helper.set_property(foo, worker, name='x')
+ foo.bar = 1
+ self.assertEqual(1, foo.x)
+ foo.bar = 2
+ self.assertEqual(2, foo.x)
+
+ def test_callable_with_reify(self):
+ def worker(obj):
+ return obj.bar
+ foo = Dummy()
+ helper = self._getTargetClass()
+ helper.set_property(foo, worker, reify=True)
+ foo.bar = 1
+ self.assertEqual(1, foo.worker)
+ foo.bar = 2
+ self.assertEqual(1, foo.worker)
+
+ def test_callable_with_name_reify(self):
+ def worker(obj):
+ return obj.bar
+ foo = Dummy()
+ helper = self._getTargetClass()
+ helper.set_property(foo, worker, name='x')
+ helper.set_property(foo, worker, name='y', reify=True)
+ foo.bar = 1
+ self.assertEqual(1, foo.y)
+ self.assertEqual(1, foo.x)
+ foo.bar = 2
+ self.assertEqual(2, foo.x)
+ self.assertEqual(1, foo.y)
+
+ def test_property_without_name(self):
+ def worker(obj): pass
+ foo = Dummy()
+ helper = self._getTargetClass()
+ self.assertRaises(ValueError, helper.set_property, foo, property(worker))
+
+ def test_property_with_name(self):
+ def worker(obj):
+ return obj.bar
+ foo = Dummy()
+ helper = self._getTargetClass()
+ helper.set_property(foo, property(worker), name='x')
+ foo.bar = 1
+ self.assertEqual(1, foo.x)
+ foo.bar = 2
+ self.assertEqual(2, foo.x)
+
+ def test_property_with_reify(self):
+ def worker(obj): pass
+ foo = Dummy()
+ helper = self._getTargetClass()
+ self.assertRaises(ValueError, helper.set_property,
+ foo, property(worker), name='x', reify=True)
+
+ def test_override_property(self):
+ def worker(obj): pass
+ foo = Dummy()
+ helper = self._getTargetClass()
+ helper.set_property(foo, worker, name='x')
+ def doit():
+ foo.x = 1
+ self.assertRaises(AttributeError, doit)
+
+ def test_override_reify(self):
+ def worker(obj): pass
+ foo = Dummy()
+ helper = self._getTargetClass()
+ helper.set_property(foo, worker, name='x', reify=True)
+ foo.x = 1
+ self.assertEqual(1, foo.x)
+ foo.x = 2
+ self.assertEqual(2, foo.x)
+
+ def test_reset_property(self):
+ foo = Dummy()
+ helper = self._getTargetClass()
+ helper.set_property(foo, lambda _: 1, name='x')
+ self.assertEqual(1, foo.x)
+ helper.set_property(foo, lambda _: 2, name='x')
+ self.assertEqual(2, foo.x)
+
+ def test_reset_reify(self):
+ """ This is questionable behavior, but may as well get notified
+ if it changes."""
+ foo = Dummy()
+ helper = self._getTargetClass()
+ helper.set_property(foo, lambda _: 1, name='x', reify=True)
+ self.assertEqual(1, foo.x)
+ helper.set_property(foo, lambda _: 2, name='x', reify=True)
+ self.assertEqual(1, foo.x)
+
+ def test_make_property(self):
+ from pyramid.decorator import reify
+ helper = self._getTargetClass()
+ name, fn = helper.make_property(lambda x: 1, name='x', reify=True)
+ self.assertEqual(name, 'x')
+ self.assertTrue(isinstance(fn, reify))
+
+ def test_apply_properties_with_iterable(self):
+ foo = Dummy()
+ helper = self._getTargetClass()
+ x = helper.make_property(lambda _: 1, name='x', reify=True)
+ y = helper.make_property(lambda _: 2, name='y')
+ helper.apply_properties(foo, [x, y])
+ self.assertEqual(1, foo.x)
+ self.assertEqual(2, foo.y)
+
+ def test_apply_properties_with_dict(self):
+ foo = Dummy()
+ helper = self._getTargetClass()
+ x_name, x_fn = helper.make_property(lambda _: 1, name='x', reify=True)
+ y_name, y_fn = helper.make_property(lambda _: 2, name='y')
+ helper.apply_properties(foo, {x_name: x_fn, y_name: y_fn})
+ self.assertEqual(1, foo.x)
+ self.assertEqual(2, foo.y)
+
+ def test_make_property_unicode(self):
+ from pyramid.compat import text_
+ from pyramid.exceptions import ConfigurationError
+
+ cls = self._getTargetClass()
+ if PY3: # pragma: nocover
+ name = b'La Pe\xc3\xb1a'
+ else: # pragma: nocover
+ name = text_(b'La Pe\xc3\xb1a', 'utf-8')
+
+ def make_bad_name():
+ cls.make_property(lambda x: 1, name=name, reify=True)
+
+ self.assertRaises(ConfigurationError, make_bad_name)
+
+ def test_add_property(self):
+ helper = self._makeOne()
+ helper.add_property(lambda obj: obj.bar, name='x', reify=True)
+ helper.add_property(lambda obj: obj.bar, name='y')
+ self.assertEqual(len(helper.properties), 2)
+ foo = Dummy()
+ helper.apply(foo)
+ foo.bar = 1
+ self.assertEqual(foo.x, 1)
+ self.assertEqual(foo.y, 1)
+ foo.bar = 2
+ self.assertEqual(foo.x, 1)
+ self.assertEqual(foo.y, 2)
+
+ def test_apply_multiple_times(self):
+ helper = self._makeOne()
+ helper.add_property(lambda obj: 1, name='x')
+ foo, bar = Dummy(), Dummy()
+ helper.apply(foo)
+ self.assertEqual(foo.x, 1)
+ helper.add_property(lambda obj: 2, name='x')
+ helper.apply(bar)
+ self.assertEqual(foo.x, 1)
+ self.assertEqual(bar.x, 2)
+
class Test_InstancePropertyMixin(unittest.TestCase):
def _makeOne(self):
cls = self._getTargetClass()
+
class Foo(cls):
pass
return Foo()
@@ -109,43 +293,6 @@ class Test_InstancePropertyMixin(unittest.TestCase):
foo.set_property(lambda _: 2, name='x', reify=True)
self.assertEqual(1, foo.x)
- def test__make_property(self):
- from pyramid.decorator import reify
- cls = self._getTargetClass()
- name, fn = cls._make_property(lambda x: 1, name='x', reify=True)
- self.assertEqual(name, 'x')
- self.assertTrue(isinstance(fn, reify))
-
- def test__set_properties_with_iterable(self):
- foo = self._makeOne()
- x = foo._make_property(lambda _: 1, name='x', reify=True)
- y = foo._make_property(lambda _: 2, name='y')
- foo._set_properties([x, y])
- self.assertEqual(1, foo.x)
- self.assertEqual(2, foo.y)
-
- def test__set_properties_with_dict(self):
- foo = self._makeOne()
- x_name, x_fn = foo._make_property(lambda _: 1, name='x', reify=True)
- y_name, y_fn = foo._make_property(lambda _: 2, name='y')
- foo._set_properties({x_name: x_fn, y_name: y_fn})
- self.assertEqual(1, foo.x)
- self.assertEqual(2, foo.y)
-
- def test__set_extensions(self):
- inst = self._makeOne()
- def foo(self, result):
- return result
- n, bar = inst._make_property(lambda _: 'bar', name='bar')
- class Extensions(object):
- def __init__(self):
- self.methods = {'foo':foo}
- self.descriptors = {'bar':bar}
- extensions = Extensions()
- inst._set_extensions(extensions)
- self.assertEqual(inst.bar, 'bar')
- self.assertEqual(inst.foo('abc'), 'abc')
-
class Test_WeakOrderedSet(unittest.TestCase):
def _makeOne(self):
from pyramid.config import WeakOrderedSet
@@ -324,7 +471,7 @@ class Test_object_description(unittest.TestCase):
self.assertEqual(
self._callFUT(inst),
"object %s" % str(inst))
-
+
def test_shortened_repr(self):
inst = ['1'] * 1000
self.assertEqual(
@@ -592,7 +739,7 @@ class TestActionInfo(unittest.TestCase):
def _getTargetClass(self):
from pyramid.util import ActionInfo
return ActionInfo
-
+
def _makeOne(self, filename, lineno, function, linerepr):
return self._getTargetClass()(filename, lineno, function, linerepr)
@@ -619,7 +766,36 @@ class TestActionInfo(unittest.TestCase):
"Line 0 of file filename:\n linerepr ")
+class TestCallableName(unittest.TestCase):
+ def test_valid_ascii(self):
+ from pyramid.util import get_callable_name
+ from pyramid.compat import text_, PY3
+
+ if PY3: # pragma: nocover
+ name = b'hello world'
+ else: # pragma: nocover
+ name = text_(b'hello world', 'utf-8')
+
+ self.assertEqual(get_callable_name(name), 'hello world')
+
+ def test_invalid_ascii(self):
+ from pyramid.util import get_callable_name
+ from pyramid.compat import text_, PY3
+ from pyramid.exceptions import ConfigurationError
+
+ def get_bad_name():
+ if PY3: # pragma: nocover
+ name = b'La Pe\xc3\xb1a'
+ else: # pragma: nocover
+ name = text_(b'La Pe\xc3\xb1a', 'utf-8')
+
+ get_callable_name(name)
+
+ self.assertRaises(ConfigurationError, get_bad_name)
+
+
def dummyfunc(): pass
+
class Dummy(object):
pass
diff --git a/pyramid/urldispatch.py b/pyramid/urldispatch.py
index fe4d433c3..349742c4a 100644
--- a/pyramid/urldispatch.py
+++ b/pyramid/urldispatch.py
@@ -42,12 +42,17 @@ class Route(object):
class RoutesMapper(object):
def __init__(self):
self.routelist = []
+ self.static_routes = []
+
self.routes = {}
def has_routes(self):
return bool(self.routelist)
- def get_routes(self):
+ def get_routes(self, include_static=False):
+ if include_static is True:
+ return self.routelist + self.static_routes
+
return self.routelist
def get_route(self, name):
@@ -59,9 +64,13 @@ class RoutesMapper(object):
oldroute = self.routes[name]
if oldroute in self.routelist:
self.routelist.remove(oldroute)
+
route = Route(name, pattern, factory, predicates, pregenerator)
if not static:
self.routelist.append(route)
+ else:
+ self.static_routes.append(route)
+
self.routes[name] = route
return route
diff --git a/pyramid/util.py b/pyramid/util.py
index 6de53d559..5721a93fc 100644
--- a/pyramid/util.py
+++ b/pyramid/util.py
@@ -22,6 +22,7 @@ from pyramid.compat import (
string_types,
text_,
PY3,
+ native_
)
from pyramid.interfaces import IActionInfo
@@ -33,14 +34,21 @@ class DottedNameResolver(_DottedNameResolver):
_marker = object()
-class InstancePropertyMixin(object):
- """ Mixin that will allow an instance to add properties at
- run-time as if they had been defined via @property or @reify
- on the class itself.
+class InstancePropertyHelper(object):
+ """A helper object for assigning properties and descriptors to instances.
+ It is not normally possible to do this because descriptors must be
+ defined on the class itself.
+
+ This class is optimized for adding multiple properties at once to an
+ instance. This is done by calling :meth:`.add_property` once
+ per-property and then invoking :meth:`.apply` on target objects.
+
"""
+ def __init__(self):
+ self.properties = {}
@classmethod
- def _make_property(cls, callable, name=None, reify=False):
+ def make_property(cls, callable, name=None, reify=False):
""" Convert a callable into one suitable for adding to the
instance. This will return a 2-tuple containing the computed
(name, property) pair.
@@ -55,7 +63,7 @@ class InstancePropertyMixin(object):
raise ValueError('cannot reify a property')
elif name is not None:
fn = lambda this: callable(this)
- fn.__name__ = name
+ fn.__name__ = get_callable_name(name)
fn.__doc__ = callable.__doc__
else:
name = callable.__name__
@@ -68,25 +76,15 @@ class InstancePropertyMixin(object):
return name, fn
- def _set_properties(self, properties):
- """ Create several properties on the instance at once.
-
- This is a more efficient version of
- :meth:`pyramid.util.InstancePropertyMixin.set_property` which
- can accept multiple ``(name, property)`` pairs generated via
- :meth:`pyramid.util.InstancePropertyMixin._make_property`.
-
- ``properties`` is a sequence of two-tuples *or* a data structure
- with an ``.items()`` method which returns a sequence of two-tuples
- (presumably a dictionary). It will be used to add several
- properties to the instance in a manner that is more efficient
- than simply calling ``set_property`` repeatedly.
+ @classmethod
+ def apply_properties(cls, target, properties):
+ """Accept a list or dict of ``properties`` generated from
+ :meth:`.make_property` and apply them to a ``target`` object.
"""
attrs = dict(properties)
-
if attrs:
- parent = self.__class__
- cls = type(parent.__name__, (parent, object), attrs)
+ parent = target.__class__
+ newcls = type(parent.__name__, (parent, object), attrs)
# We assign __provides__, __implemented__ and __providedBy__ below
# to prevent a memory leak that results from from the usage of this
# instance's eventual use in an adapter lookup. Adapter lookup
@@ -105,14 +103,34 @@ class InstancePropertyMixin(object):
# attached to it
val = getattr(parent, name, _marker)
if val is not _marker:
- setattr(cls, name, val)
- self.__class__ = cls
+ setattr(newcls, name, val)
+ target.__class__ = newcls
+
+ @classmethod
+ def set_property(cls, target, callable, name=None, reify=False):
+ """A helper method to apply a single property to an instance."""
+ prop = cls.make_property(callable, name=name, reify=reify)
+ cls.apply_properties(target, [prop])
- def _set_extensions(self, extensions):
- for name, fn in iteritems_(extensions.methods):
- method = fn.__get__(self, self.__class__)
- setattr(self, name, method)
- self._set_properties(extensions.descriptors)
+ def add_property(self, callable, name=None, reify=False):
+ """Add a new property configuration.
+
+ This should be used in combination with :meth:`.apply` as a
+ more efficient version of :meth:`.set_property`.
+ """
+ name, fn = self.make_property(callable, name=name, reify=reify)
+ self.properties[name] = fn
+
+ def apply(self, target):
+ """ Apply all configured properties to the ``target`` instance."""
+ if self.properties:
+ self.apply_properties(target, self.properties)
+
+class InstancePropertyMixin(object):
+ """ Mixin that will allow an instance to add properties at
+ run-time as if they had been defined via @property or @reify
+ on the class itself.
+ """
def set_property(self, callable, name=None, reify=False):
""" Add a callable or a property descriptor to the instance.
@@ -166,8 +184,8 @@ class InstancePropertyMixin(object):
>>> foo.y # notice y keeps the original value
1
"""
- prop = self._make_property(callable, name=name, reify=reify)
- self._set_properties([prop])
+ InstancePropertyHelper.set_property(
+ self, callable, name=name, reify=reify)
class WeakOrderedSet(object):
""" Maintain a set of items.
@@ -551,3 +569,17 @@ def action_method(wrapped):
wrapper.__docobj__ = wrapped
return wrapper
+
+def get_callable_name(name):
+ """
+ Verifies that the ``name`` is ascii and will raise a ``ConfigurationError``
+ if it is not.
+ """
+ try:
+ return native_(name, 'ascii')
+ except (UnicodeEncodeError, UnicodeDecodeError):
+ msg = (
+ '`name="%s"` is invalid. `name` must be ascii because it is '
+ 'used on __name__ of the method'
+ )
+ raise ConfigurationError(msg % name)
diff --git a/rtd.txt b/rtd.txt
index b449ac73c..4aecd9933 100644
--- a/rtd.txt
+++ b/rtd.txt
@@ -1,4 +1,4 @@
+Sphinx >= 1.2.3
repoze.sphinx.autointerface
repoze.lru
pylons_sphinx_latesturl
-
diff --git a/setup.py b/setup.py
index d736dc38d..3233193e7 100644
--- a/setup.py
+++ b/setup.py
@@ -56,7 +56,7 @@ if not PY3:
tests_require.append('zope.component>=3.11.0')
docs_extras = [
- 'Sphinx',
+ 'Sphinx >= 1.2.3',
'docutils',
'repoze.sphinx.autointerface',
]
@@ -68,7 +68,7 @@ testing_extras = tests_require + [
]
setup(name='pyramid',
- version='1.6dev',
+ version='1.6.dev0',
description='The Pyramid Web Framework, a Pylons project',
long_description=README + '\n\n' + CHANGES,
classifiers=[
diff --git a/tox.ini b/tox.ini
index 714c5b6d3..202e29e30 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,18 +1,19 @@
[tox]
+skipsdist = True
envlist =
py26,py27,py32,py33,py34,pypy,pypy3,cover
[testenv]
commands =
- python setup.py dev
- python setup.py test -q
+ python setup.py -q dev
+ python setup.py -q test -q
[testenv:cover]
basepython =
python2.6
commands =
- python setup.py dev
- python setup.py nosetests --with-xunit --with-xcoverage
+ python setup.py -q dev
+ nosetests --with-xunit --with-xcoverage
deps =
nosexcover