From 8ac9e61e365e28698a36474d8cb3dc15ab358983 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 16 Dec 2011 02:39:13 -0500 Subject: garden --- TODO.txt | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/TODO.txt b/TODO.txt index a5ec323a3..f04c341e2 100644 --- a/TODO.txt +++ b/TODO.txt @@ -4,9 +4,13 @@ Pyramid TODOs Must-Have --------- -- Introspection: +- Fix deployment recipes in cookbook (discourage proxying without changing + server). + +Nice-to-Have +------------ - * Review narrative docs. +- Introspection: * ``default root factory`` category? @@ -22,13 +26,6 @@ Must-Have - Fix deployment recipes in cookbook (discourage proxying without changing server). -- Allow prequest path to have query string variables. - -Nice-to-Have ------------- - -- Implement analogue of "paster request"? - - CherryPy server testing / exploded from CherryPy itself. - Try "with transaction.manager" in an exception view with SQLA (preempt -- cgit v1.2.3 From 9003a8bba654f98933b98dacac67760cff73e1c5 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 16 Dec 2011 04:35:05 -0500 Subject: - Removed the "Running Pyramid on Google App Engine" tutorial from the main docs. It survives on in the Cookbook (http://docs.pylonsproject.org/projects/pyramid_cookbook/en/latest/gae.html). Rationale: it provides the correct info for the Python 2.5 version of GAE only, and this version of Pyramid does not support Python 2.5. --- CHANGES.txt | 6 ++ docs/index.rst | 1 - docs/latexindex.rst | 1 - docs/narr/install.rst | 11 +-- docs/tutorials/gae/index.rst | 231 ------------------------------------------- docs/whatsnew-1.3.rst | 6 ++ 6 files changed, 13 insertions(+), 243 deletions(-) delete mode 100644 docs/tutorials/gae/index.rst diff --git a/CHANGES.txt b/CHANGES.txt index 63c84d0f8..8df5ab934 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -20,6 +20,12 @@ Documentation - Added a section named "Making Your Script into a Console Script" in the "Command-Line Pyramid" chapter. +- Removed the "Running Pyramid on Google App Engine" tutorial from the main + docs. It survives on in the Cookbook + (http://docs.pylonsproject.org/projects/pyramid_cookbook/en/latest/gae.html). + Rationale: it provides the correct info for the Python 2.5 version of GAE + only, and this version of Pyramid does not support Python 2.5. + 1.3a2 (2011-12-14) ================== diff --git a/docs/index.rst b/docs/index.rst index 21e587992..be03448c9 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -109,7 +109,6 @@ applications to various platforms. tutorials/wiki2/index.rst tutorials/wiki/index.rst tutorials/bfg/index.rst - tutorials/gae/index.rst tutorials/modwsgi/index.rst Reference Material diff --git a/docs/latexindex.rst b/docs/latexindex.rst index 4db5b64b2..99ec2f54d 100644 --- a/docs/latexindex.rst +++ b/docs/latexindex.rst @@ -73,7 +73,6 @@ Tutorials tutorials/wiki/index.rst tutorials/wiki2/index.rst tutorials/bfg/index.rst - tutorials/gae/index.rst tutorials/modwsgi/index.rst .. _api_reference: diff --git a/docs/narr/install.rst b/docs/narr/install.rst index 3de4d6e27..172cfd6d3 100644 --- a/docs/narr/install.rst +++ b/docs/narr/install.rst @@ -20,7 +20,7 @@ run :app:`Pyramid`. :app:`Pyramid` is known to run on all popular UNIX-like systems such as Linux, MacOS X, and FreeBSD as well as on Windows platforms. It is also -known to run on Google's App Engine, and :term:`PyPy` (1.6+). +known to run on :term:`PyPy` (1.6+). :app:`Pyramid` installation does not require the compilation of any C code, so you need only a Python interpreter that meets the @@ -315,15 +315,6 @@ Installing :app:`Pyramid` on a Windows System c:\env> Scripts\easy_install pyramid -.. index:: - single: installing on Google App Engine - -Installing :app:`Pyramid` on Google App Engine -------------------------------------------------- - -:ref:`appengine_tutorial` documents the steps required to install a -:app:`Pyramid` application on Google App Engine. - What Gets Installed ------------------- diff --git a/docs/tutorials/gae/index.rst b/docs/tutorials/gae/index.rst deleted file mode 100644 index 3bd739480..000000000 --- a/docs/tutorials/gae/index.rst +++ /dev/null @@ -1,231 +0,0 @@ -.. _appengine_tutorial: - -Running :app:`Pyramid` on Google's App Engine -================================================ - -It is possible to run a :app:`Pyramid` application on Google's `App -Engine `_. Content from this -tutorial was contributed by YoungKing, based on the -`"appengine-monkey" tutorial for Pylons -`_. This -tutorial is written in terms of using the command line on a UNIX -system; it should be possible to perform similar actions on a Windows -system. - -#. Download Google's `App Engine SDK - `_ and install it - on your system. - -#. Use Subversion to check out the source code for - ``appengine-monkey``. - - .. code-block:: text - - $ svn co http://appengine-monkey.googlecode.com/svn/trunk/ \ - appengine-monkey - -#. Use ``appengine_homedir.py`` script in ``appengine-monkey`` to - create a :term:`virtualenv` for your application. - - .. code-block:: text - - $ export GAE_PATH=/usr/local/google_appengine - $ python2.5 /path/to/appengine-monkey/appengine-homedir.py --gae \ - $GAE_PATH pyramidapp - - Note that ``$GAE_PATH`` should be the path where you have unpacked - the App Engine SDK. (On Mac OS X at least, - ``/usr/local/google_appengine`` is indeed where the installer puts - it). - - This will set up an environment in ``pyramidapp/``, with some tools - installed in ``pyramidapp/bin``. There will also be a directory - ``pyramidapp/app/`` which is the directory you will upload to - appengine. - -#. Install :app:`Pyramid` into the virtualenv - - .. code-block:: text - - $ cd pyramidapp/ - $ bin/easy_install pyramid - - This will install :app:`Pyramid` in the environment. - -#. Create your application - - We'll use the standard way to create a :app:`Pyramid` - application, but we'll have to move some files around when we are - done. The below commands assume your current working directory is - the ``pyramidapp`` virtualenv directory you created in the third step - above: - - .. code-block:: text - - $ cd app - $ rm -rf pyramidapp - $ bin/pcreate -s starter pyramidapp - $ mv pyramidapp aside - $ mv aside/pyramidapp . - $ rm -rf aside - -#. Edit ``config.py`` - - Edit the ``APP_NAME`` and ``APP_ARGS`` settings within - ``config.py``. The ``APP_NAME`` must be ``pyramidapp:main``, and - the APP_ARGS must be ``({},)``. Any other settings in - ``config.py`` should remain the same. - - .. code-block:: python - - APP_NAME = 'pyramidapp:main' - APP_ARGS = ({},) - -#. Edit ``runner.py`` - - To prevent errors for ``import site``, add this code stanza before - ``import site`` in app/runner.py: - - .. code-block:: python - - import sys - sys.path = [path for path in sys.path if 'site-packages' not in path] - import site - - You will also need to comment out the line that starts with - ``assert sys.path`` in the file. - - .. code-block:: python - - # comment the sys.path assertion out - # assert sys.path[:len(cur_sys_path)] == cur_sys_path, ( - # "addsitedir() caused entries to be prepended to sys.path") - - For GAE development environment 1.3.0 or better, you will also need - the following somewhere near the top of the ``runner.py`` file to - fix a compatibility issue with ``appengine-monkey``: - - .. code-block:: python - - import os - os.mkdir = None - -#. Run the application. ``dev_appserver.py`` is typically installed - by the SDK in the global path but you need to be sure to run it - with Python 2.5 (or whatever version of Python your GAE SDK - expects). - - .. code-block:: text - :linenos: - - $ cd ../.. - $ python2.5 /usr/local/bin/dev_appserver.py pyramidapp/app/ - - Startup success looks something like this: - - .. code-block:: text - - [chrism@vitaminf pyramid_gae]$ python2.5 \ - /usr/local/bin/dev_appserver.py \ - pyramidapp/app/ - INFO 2009-05-03 22:23:13,887 appengine_rpc.py:157] # ... more... - Running application pyramidapp on port 8080: http://localhost:8080 - - You may need to run "Make Symlinks" from the Google App Engine - Launcher GUI application if your system doesn't already have the - ``dev_appserver.py`` script sitting around somewhere. - -#. Hack on your pyramid application, using a normal run, debug, restart - process. For tips on how to use the ``pdb`` module within Google - App Engine, `see this blog post - `_. - In particular, you can create a function like so and call it to - drop your console into a pdb trace: - - .. code-block:: python - :linenos: - - def set_trace(): - import pdb, sys - debugger = pdb.Pdb(stdin=sys.__stdin__, - stdout=sys.__stdout__) - debugger.set_trace(sys._getframe().f_back) - -#. `Sign up for a GAE account `_ - and create an application. You'll need a mobile phone to accept an - SMS in order to receive authorization. - -#. Edit the application's ID in ``app.yaml`` to match the application - name you created during GAE account setup. - - .. code-block:: yaml - - application: mycoolpyramidapp - -#. Upload the application - - .. code-block:: text - - $ python2.5 /usr/local/bin/appcfg.py update pyramidapp/app - - You almost certainly won't hit the 3000-file GAE file number limit - when invoking this command. If you do, however, it will look like - so: - - .. code-block:: text - - HTTPError: HTTP Error 400: Bad Request - Rolling back the update. - Error 400: --- begin server output --- - Max number of files and blobs is 3000. - --- end server output --- - - If you do experience this error, you will be able to get around - this by zipping libraries. You can use ``pip`` to create zipfiles - from packages. See :ref:`pip_zip` for more information about this. - - A successful upload looks like so: - - .. code-block:: text - - [chrism@vitaminf pyramidapp]$ python2.5 /usr/local/bin/appcfg.py \ - update ../pyramidapp/app/ - Scanning files on local disk. - Scanned 500 files. - # ... more output ... - Will check again in 16 seconds. - Checking if new version is ready to serve. - Closing update: new version is ready to start serving. - Uploading index definitions. - -#. Visit ``http://.appspot.com`` in a browser. - -.. _pip_zip: - -Zipping Files Via Pip ---------------------- - -If you hit the Google App Engine 3000-file limit, you may need to -create zipfile archives out of some distributions installed in your -application's virtualenv. - -First, see which packages are available for zipping: - -.. code-block:: text - - $ bin/pip zip -l - -This shows your zipped packages (by default, none) and your unzipped -packages. You can zip a package like so: - -.. code-block:: text - - $ bin/pip zip pytz-2009g-py2.5.egg - -Note that it requires the whole egg file name. For a :app:`Pyramid` app, the -following packages are good candidates to be zipped. - -- Chameleon -- zope.i18n - -Once the zipping procedure is finished you can try uploading again. diff --git a/docs/whatsnew-1.3.rst b/docs/whatsnew-1.3.rst index 8dd3c3cdb..b61893536 100644 --- a/docs/whatsnew-1.3.rst +++ b/docs/whatsnew-1.3.rst @@ -302,6 +302,12 @@ Documentation Enhancements - Added a section to the "Command-Line Pyramid" chapter named :ref:`making_a_console_script`. +- Removed the "Running Pyramid on Google App Engine" tutorial from the main + docs. It survives on in the Cookbook + (http://docs.pylonsproject.org/projects/pyramid_cookbook/en/latest/gae.html). + Rationale: it provides the correct info for the Python 2.5 version of GAE + only, and this version of Pyramid does not support Python 2.5. + Dependency Changes ------------------ -- cgit v1.2.3 From 10f1b5c35072c5499f3504dc261fb3c67a90c70d Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 16 Dec 2011 04:54:09 -0500 Subject: stamp out paste.httpserver usage --- docs/designdefense.rst | 12 +++++++----- docs/narr/advconfig.rst | 20 ++++++++++++-------- docs/narr/configuration.rst | 11 ++++++----- docs/narr/firstapp.rst | 24 +++++++++++++----------- docs/narr/hooks.rst | 5 +++-- 5 files changed, 41 insertions(+), 31 deletions(-) diff --git a/docs/designdefense.rst b/docs/designdefense.rst index 80675ce83..59b0e5a2d 100644 --- a/docs/designdefense.rst +++ b/docs/designdefense.rst @@ -1628,8 +1628,8 @@ comments take into account what we've discussed in the .. code-block:: python :linenos: - from pyramid.response import Response # explicit response objects, no TL - from paste.httpserver import serve # explicitly WSGI + from pyramid.response import Response # explicit response, no TL + from wsgiref.simple_server import make_server # explicitly WSGI def hello_world(request): # accepts a request; no request thread local reqd # explicit response object means no response threadlocal @@ -1640,7 +1640,8 @@ comments take into account what we've discussed in the config = Configurator() # no global application object. config.add_view(hello_world) # explicit non-decorator registration app = config.make_wsgi_app() # explicitly WSGI - serve(app, host='0.0.0.0') # explicitly WSGI + server = make_server('0.0.0.0', 8080, app) + server.serve_forever() # explicitly WSGI Pyramid Doesn't Offer Pluggable Apps ------------------------------------ @@ -1736,7 +1737,7 @@ If you can understand this hello world program, you can use Pyramid: .. code-block:: python :linenos: - from paste.httpserver import serve + from wsgiref.simple_server import make_server from pyramid.config import Configurator from pyramid.response import Response @@ -1747,7 +1748,8 @@ If you can understand this hello world program, you can use Pyramid: config = Configurator() config.add_view(hello_world) app = config.make_wsgi_app() - serve(app) + server = make_server('0.0.0.0', 8080, app) + server.serve_forever() Pyramid has ~ 650 pages of documentation (printed), covering topics from the very basic to the most advanced. *Nothing* is left undocumented, quite diff --git a/docs/narr/advconfig.rst b/docs/narr/advconfig.rst index 3a7bf2805..5f2175d2a 100644 --- a/docs/narr/advconfig.rst +++ b/docs/narr/advconfig.rst @@ -27,7 +27,7 @@ configured imperatively: .. code-block:: python :linenos: - from paste.httpserver import serve + from wsgiref.simple_server import make_server from pyramid.config import Configurator from pyramid.response import Response @@ -38,7 +38,8 @@ configured imperatively: config = Configurator() config.add_view(hello_world) app = config.make_wsgi_app() - serve(app, host='0.0.0.0') + server = make_server('0.0.0.0', 8080, app) + server.serve_forever() When you start this application, all will be OK. However, what happens if we try to add another view to the configuration with the same set of @@ -47,7 +48,7 @@ try to add another view to the configuration with the same set of .. code-block:: python :linenos: - from paste.httpserver import serve + from wsgiref.simple_server import make_server from pyramid.config import Configurator from pyramid.response import Response @@ -66,7 +67,8 @@ try to add another view to the configuration with the same set of config.add_view(goodbye_world, name='hello') app = config.make_wsgi_app() - serve(app, host='0.0.0.0') + server = make_server('0.0.0.0', 8080, app) + server.serve_forever() The application now has two conflicting view configuration statements. When we try to start it again, it won't start. Instead, we'll receive a traceback @@ -170,7 +172,7 @@ application that generates conflicts: .. code-block:: python :linenos: - from paste.httpserver import serve + from wsgiref.simple_server import make_server from pyramid.config import Configurator from pyramid.response import Response @@ -189,7 +191,8 @@ application that generates conflicts: config.add_view(goodbye_world, name='hello') app = config.make_wsgi_app() - serve(app, host='0.0.0.0') + server = make_server('0.0.0.0', 8080, app) + server.serve_forever() We can prevent the two ``add_view`` calls from conflicting by issuing a call to :meth:`~pyramid.config.Configurator.commit` between them: @@ -197,7 +200,7 @@ to :meth:`~pyramid.config.Configurator.commit` between them: .. code-block:: python :linenos: - from paste.httpserver import serve + from wsgiref.simple_server import make_server from pyramid.config import Configurator from pyramid.response import Response @@ -218,7 +221,8 @@ to :meth:`~pyramid.config.Configurator.commit` between them: config.add_view(goodbye_world, name='hello') app = config.make_wsgi_app() - serve(app, host='0.0.0.0') + server = make_server('0.0.0.0', 8080, app) + server.serve_forever() In the above example we've issued a call to :meth:`~pyramid.config.Configurator.commit` between the two ``add_view`` diff --git a/docs/narr/configuration.rst b/docs/narr/configuration.rst index 597d48b09..37122e382 100644 --- a/docs/narr/configuration.rst +++ b/docs/narr/configuration.rst @@ -36,7 +36,7 @@ applications, configured imperatively: .. code-block:: python :linenos: - from paste.httpserver import serve + from wsgiref.simple_server import make_server from pyramid.config import Configurator from pyramid.response import Response @@ -46,8 +46,8 @@ applications, configured imperatively: if __name__ == '__main__': config = Configurator() config.add_view(hello_world) - app = config.make_wsgi_app() - serve(app, host='0.0.0.0') + server = make_server('0.0.0.0', 8080, app) + server.serve_forever() We won't talk much about what this application does yet. Just note that the "configuration' statements take place underneath the ``if __name__ == @@ -105,7 +105,7 @@ in a package and its subpackages. For example: .. code-block:: python :linenos: - from paste.httpserver import serve + from wsgiref.simple_server import make_server from pyramid.response import Response from pyramid.view import view_config @@ -118,7 +118,8 @@ in a package and its subpackages. For example: config = Configurator() config.scan() app = config.make_wsgi_app() - serve(app, host='0.0.0.0') + server = make_server('0.0.0.0', 8080, app) + server.serve_forever() The scanning machinery imports each module and subpackage in a package or module recursively, looking for special attributes attached to objects diff --git a/docs/narr/firstapp.rst b/docs/narr/firstapp.rst index c082f616b..922b0ea82 100644 --- a/docs/narr/firstapp.rst +++ b/docs/narr/firstapp.rst @@ -54,9 +54,8 @@ The script imports the :class:`~pyramid.config.Configurator` class from the Like many other Python web frameworks, :app:`Pyramid` uses the :term:`WSGI` protocol to connect an application and a web server together. The -:mod:`paste.httpserver` server is used in this example as a WSGI server for -convenience, as the ``paste`` package is a dependency of :app:`Pyramid` -itself. +:mod:`wsgiref` server is used in this example as a WSGI server for +convenience, as it is shipped within the Python standard library. The script also imports the :class:`pyramid.response.Response` class for later use. An instance of this class will be used to create a web response. @@ -205,14 +204,17 @@ WSGI Application Serving :lines: 13 Finally, we actually serve the application to requestors by starting up a -WSGI server. We happen to use the :func:`paste.httpserver.serve` WSGI server -runner, passing it the ``app`` object (a :term:`router`) as the application -we wish to serve. We also pass in an argument ``host='0.0.0.0'``, meaning -"listen on all TCP interfaces." By default, the HTTP server listens -only on the ``127.0.0.1`` interface, which is problematic if you're running -the server on a remote system and you wish to access it with a web browser -from a local system. We don't specify a TCP port number to listen on; this -means we want to use the default TCP port, which is 8080. +WSGI server. We happen to use the :mod:`wsgiref` ``make_server`` server +maker for this purpose. We pass in as the first argument ``'0.0.0.0'``, +which means "listen on all TCP interfaces." By default, the HTTP server +listens only on the ``127.0.0.1`` interface, which is problematic if you're +running the server on a remote system and you wish to access it with a web +browser from a local system. We also specify a TCP port number to listen on, +which is 8080, passing it as the second argument. The final argument ios , +passing it the ``app`` object (a :term:`router`), which is the the +application we wish to serve. Finally, we call the server's +``serve_forever`` method, which starts the main loop in which it will wait +for requests from the outside world. When this line is invoked, it causes the server to start listening on TCP port 8080. The server will serve requests forever, or at least until we stop diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index a1ed6c7ff..fd6544416 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -812,7 +812,7 @@ performed, enabling you to set up the utility in advance: .. code-block:: python :linenos: - from paste.httpserver import serve + from wsgiref.simple_server import make_server from pyramid.config import Configurator from mypackage.interfaces import IMyUtility @@ -831,7 +831,8 @@ performed, enabling you to set up the utility in advance: config.registry.registerUtility(UtilityImplementation()) config.scan() app = config.make_wsgi_app() - serve(app, host='0.0.0.0') + server = make_server('0.0.0.0', 8080, app) + server.serve_forever() For full details, please read the `Venusian documentation `_. -- cgit v1.2.3 From 0cc7a31f6fab611a151335c5f1590d0f9e7b7d98 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 16 Dec 2011 04:57:40 -0500 Subject: have output description match reality --- docs/narr/project.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/narr/project.rst b/docs/narr/project.rst index af8714573..39f6faace 100644 --- a/docs/narr/project.rst +++ b/docs/narr/project.rst @@ -283,7 +283,7 @@ For example, on UNIX: $ ../bin/pserve development.ini --reload Starting subprocess with file monitor Starting server in PID 16601. - serving on 0.0.0.0:6543 view at http://127.0.0.1:6543 + Starting HTTP server on http://0.0.0.0:6543 For more detailed information about the startup process, see :ref:`startup_chapter`. For more information about environment variables and -- cgit v1.2.3 From ef9767780ff176aef063ca669bb729e6be15c7c3 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 16 Dec 2011 05:00:05 -0500 Subject: justify wsgiref usage --- docs/narr/project.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/narr/project.rst b/docs/narr/project.rst index 39f6faace..0c12d97e6 100644 --- a/docs/narr/project.rst +++ b/docs/narr/project.rst @@ -909,6 +909,13 @@ one of the following. compatible. You'll need to ``easy_install CherryPy`` into your Pyramid virtualenv for this server to work. +The servers described above are typically both faster and more secure than +the default WSGI server used by Pyramid. Pyramid does not depend on either +because Paste doesn't run on Python 3 (and Pyramid must) and the CherryPy +server is not distributed separately from the CherryPy web framework (and it +would be an awkward dependency to have a web framework rely on another web +framework for just its server component). + ``pserve`` is by no means the only way to start up and serve a :app:`Pyramid` application. As we saw in :ref:`firstapp_chapter`, ``pserve`` needn't be invoked at all to run a :app:`Pyramid` application. The use of ``pserve`` to -- cgit v1.2.3 From 253df134a73613574e6c895b1edde4d3bb441253 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 17 Dec 2011 00:43:16 -0500 Subject: whitespace --- docs/narr/paste.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/narr/paste.rst b/docs/narr/paste.rst index cf8f96c8a..86b047aec 100644 --- a/docs/narr/paste.rst +++ b/docs/narr/paste.rst @@ -17,7 +17,7 @@ a Pyramid application that doesn't use PasteDeploy in :ref:`firstapp_chapter`. However, all Pyramid scaffolds render PasteDeploy configuration files, to provide new developers with a standardized way of setting deployment values, and to provide new users with a standardized way -of starting, stopping, and debugging an application. +of starting, stopping, and debugging an application. This chapter is not a replacement for documentation about PasteDeploy; it only contextualizes the use of PasteDeploy within Pyramid. For detailed -- cgit v1.2.3 From 9a9f7c09bba06e72e603562139ad55d1c2d54588 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 17 Dec 2011 16:30:10 -0500 Subject: garden --- TODO.txt | 3 --- 1 file changed, 3 deletions(-) diff --git a/TODO.txt b/TODO.txt index f04c341e2..f235713b3 100644 --- a/TODO.txt +++ b/TODO.txt @@ -23,9 +23,6 @@ Nice-to-Have * introspection hiding for directives? -- Fix deployment recipes in cookbook (discourage proxying without changing - server). - - CherryPy server testing / exploded from CherryPy itself. - Try "with transaction.manager" in an exception view with SQLA (preempt -- cgit v1.2.3 From cd422c1674ec408b866aa715843b084cd0c3ff40 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 18 Dec 2011 16:42:40 -0500 Subject: garden --- TODO.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/TODO.txt b/TODO.txt index f235713b3..f31f391d1 100644 --- a/TODO.txt +++ b/TODO.txt @@ -10,6 +10,11 @@ Must-Have Nice-to-Have ------------ +- Modify the urldispatch chapter examples to assume a scan rather than + ``add_view``. + +- Decorator for append_slash_notfound_view_factory? + - Introspection: * ``default root factory`` category? -- cgit v1.2.3 From ba2a3f87fa51843a8a160f209fb576ae21d0050b Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 21 Dec 2011 01:04:26 -0500 Subject: - Add undocumented ``__discriminator__`` API to derived view callables. e.g. ``adapters.lookup(...).__discriminator__(context, request)``. It will be used by superdynamic systems that require the discriminator to be used for introspection after manual view lookup. --- CHANGES.txt | 5 +++++ pyramid/config/views.py | 13 ++++++++++++ pyramid/tests/test_config/test_views.py | 36 +++++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 8df5ab934..b5c1859c9 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -8,6 +8,11 @@ Features documented in the "Command-Line Pyramid" chapter in the section entitled "Invoking a Request". +- Add undocumented ``__discriminator__`` API to derived view callables. + e.g. ``adapters.lookup(...).__discriminator__(context, request)``. It will + be used by superdynamic systems that require the discriminator to be used + for introspection after manual view lookup. + Bug Fixes --------- diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 5efe1f2bb..3cca1c7c5 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -492,6 +492,15 @@ class MultiView(object): self.views = [] self.accepts = [] + def __discriminator__(self, context, request): + # used by introspection systems like so: + # view = adapters.lookup(....) + # view.__discriminator__(context, request) -> view's discriminator + # so that superdynamic systems can feed the discriminator to + # the introspection system to get info about it + view = self.match(context, request) + return view.__discriminator__(context, request) + def add(self, view, order, accept=None, phash=None): if phash is not None: for i, (s, v, h) in enumerate(list(self.views)): @@ -1034,6 +1043,10 @@ class ViewsConfiguratorMixin(object): decorator=decorator, http_cache=http_cache) derived_view = deriver(view) + derived_view.__discriminator__ = lambda *arg: discriminator + # __discriminator__ is used by superdynamic systems + # that require it for introspection after manual view lookup; + # see also MultiView.__discriminator__ view_intr['derived_callable'] = derived_view registered = self.registry.adapters.registered diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index 30a30a1e8..bcf1faa08 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -115,6 +115,7 @@ class TestViewsConfigurationMixin(unittest.TestCase): self.assertEqual(wrapper.__module__, view.__module__) self.assertEqual(wrapper.__name__, view.__name__) self.assertEqual(wrapper.__doc__, view.__doc__) + self.assertEqual(wrapper.__discriminator__(None, None)[0], 'view') def test_add_view_view_callable_dottedname(self): from pyramid.renderers import null_renderer @@ -875,6 +876,41 @@ class TestViewsConfigurationMixin(unittest.TestCase): request.params = {'param':'1'} self.assertEqual(wrapper(ctx, request), 'view8') + def test_add_view_multiview___discriminator__(self): + from pyramid.renderers import null_renderer + from zope.interface import Interface + class IFoo(Interface): + pass + class IBar(Interface): + pass + @implementer(IFoo) + class Foo(object): + pass + @implementer(IBar) + class Bar(object): + pass + foo = Foo() + bar = Bar() + + from pyramid.interfaces import IRequest + from pyramid.interfaces import IView + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IMultiView + view = lambda *arg: 'OK' + view.__phash__ = 'abc' + config = self._makeOne(autocommit=True) + config.registry.registerAdapter( + view, (IViewClassifier, IRequest, Interface), IView, name='') + config.add_view(view=view, renderer=null_renderer, + containment=IFoo) + config.add_view(view=view, renderer=null_renderer, + containment=IBar) + wrapper = self._getViewCallable(config) + self.assertTrue(IMultiView.providedBy(wrapper)) + request = self._makeRequest(config) + self.assertEqual(wrapper.__discriminator__(foo, request)[5], IFoo) + self.assertEqual(wrapper.__discriminator__(bar, request)[5], IBar) + def test_add_view_with_template_renderer(self): from pyramid.tests import test_config from pyramid.interfaces import ISettings -- cgit v1.2.3 From d394da5175fece8388f2fc0dfd906942faa6f2ed Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 21 Dec 2011 02:18:37 -0500 Subject: prep for 1.3a3 --- CHANGES.txt | 4 ++-- docs/conf.py | 2 +- setup.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index b5c1859c9..3feaa6332 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,5 +1,5 @@ -Next release -============ +1.3a3 (2011-12-21) +================== Features -------- diff --git a/docs/conf.py b/docs/conf.py index 5281017e7..39006f1bb 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -80,7 +80,7 @@ copyright = '%s, Agendaless Consulting' % datetime.datetime.now().year # other places throughout the built documents. # # The short X.Y version. -version = '1.3a2' +version = '1.3a3' # The full version, including alpha/beta/rc tags. release = version diff --git a/setup.py b/setup.py index b9220285f..78963289a 100644 --- a/setup.py +++ b/setup.py @@ -56,7 +56,7 @@ if not PY3: ]) setup(name='pyramid', - version='1.3a2', + version='1.3a3', description=('The Pyramid web application development framework, a ' 'Pylons project'), long_description=README + '\n\n' + CHANGES, -- cgit v1.2.3 From cd5fd7cc06939ea7dc26371f687ffaeb58a0e491 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 27 Dec 2011 09:50:39 -0600 Subject: garden --- docs/tutorials/wiki/basiclayout.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorials/wiki/basiclayout.rst b/docs/tutorials/wiki/basiclayout.rst index 56f817a85..308c8e292 100644 --- a/docs/tutorials/wiki/basiclayout.rst +++ b/docs/tutorials/wiki/basiclayout.rst @@ -10,7 +10,7 @@ The source code for this tutorial stage can be browsed via `http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki/src/basiclayout/ `_. -Appplication Configuration with ``__init__.py`` +Application Configuration with ``__init__.py`` ------------------------------------------------ A directory on disk can be turned into a Python :term:`package` by containing -- cgit v1.2.3 From 5a3ddb76a7ac4ee6f41652f774e8387a8c9b131a Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 28 Dec 2011 09:28:18 -0500 Subject: typo --- pyramid/compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/compat.py b/pyramid/compat.py index 484cb7b97..12b8c9f37 100644 --- a/pyramid/compat.py +++ b/pyramid/compat.py @@ -33,7 +33,7 @@ else: def text_(s, encoding='latin-1', errors='strict'): """ If ``s`` is an instance of ``binary_type``, return - ``s.encode(encoding, errors)``, otherwise return ``s``""" + ``s.decode(encoding, errors)``, otherwise return ``s``""" if isinstance(s, binary_type): return s.decode(encoding, errors) return s # pragma: no cover -- cgit v1.2.3 From 577db92fe4626e86e7c5cf4fe062205d6cf5d33b Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 30 Dec 2011 01:34:36 -0600 Subject: Added the InstancePropertyMixin to the Request. The new mixin allows adding properties to the request object which are lazily evaluated. --- pyramid/request.py | 3 +- pyramid/tests/test_request.py | 18 +++++++ pyramid/tests/test_util.py | 108 ++++++++++++++++++++++++++++++++++++++++++ pyramid/util.py | 83 ++++++++++++++++++++++++++++++++ 4 files changed, 211 insertions(+), 1 deletion(-) diff --git a/pyramid/request.py b/pyramid/request.py index 4005213fa..c15ed7d8e 100644 --- a/pyramid/request.py +++ b/pyramid/request.py @@ -26,6 +26,7 @@ from pyramid.compat import ( from pyramid.decorator import reify from pyramid.response import Response from pyramid.url import URLMethodsMixin +from pyramid.util import InstancePropertyMixin class TemplateContext(object): pass @@ -301,7 +302,7 @@ class CallbackMethodsMixin(object): @implementer(IRequest) class Request(BaseRequest, DeprecatedRequestMethodsMixin, URLMethodsMixin, - CallbackMethodsMixin): + CallbackMethodsMixin, InstancePropertyMixin): """ A subclass of the :term:`WebOb` Request class. An instance of this class is created by the :term:`router` and is provided to a diff --git a/pyramid/tests/test_request.py b/pyramid/tests/test_request.py index 546f670c0..10cda96d8 100644 --- a/pyramid/tests/test_request.py +++ b/pyramid/tests/test_request.py @@ -267,6 +267,24 @@ class TestRequest(unittest.TestCase): request = self._makeOne({'REQUEST_METHOD':'GET'}) self.assertRaises(ValueError, getattr, request, 'json_body') + def test_set_property(self): + request = self._makeOne({}) + opts = [2, 1] + def connect(obj): + return opts.pop() + request.set_property(connect, name='db') + self.assertEqual(1, request.db) + self.assertEqual(2, request.db) + + def test_set_property_reify(self): + request = self._makeOne({}) + opts = [2, 1] + def connect(obj): + return opts.pop() + request.set_property(connect, name='db', reify=True) + self.assertEqual(1, request.db) + self.assertEqual(1, request.db) + class TestRequestDeprecatedMethods(unittest.TestCase): def setUp(self): self.config = testing.setUp() diff --git a/pyramid/tests/test_util.py b/pyramid/tests/test_util.py index b9a9d1960..824ee329f 100644 --- a/pyramid/tests/test_util.py +++ b/pyramid/tests/test_util.py @@ -1,6 +1,114 @@ import unittest from pyramid.compat import PY3 +class Test_InstancePropertyMixin(unittest.TestCase): + def _makeOne(self): + cls = self._targetClass() + class Foo(cls): + pass + return Foo() + + def _targetClass(self): + from pyramid.util import InstancePropertyMixin + return InstancePropertyMixin + + def test_callable(self): + def worker(obj): + return obj.bar + foo = self._makeOne() + foo.set_property(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 = self._makeOne() + foo.set_property(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 = self._makeOne() + foo.set_property(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 = self._makeOne() + foo.set_property(worker, name='x') + foo.set_property(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 = self._makeOne() + self.assertRaises(ValueError, foo.set_property, property(worker)) + + def test_property_with_name(self): + def worker(obj): + return obj.bar + foo = self._makeOne() + foo.set_property(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 = self._makeOne() + self.assertRaises(ValueError, foo.set_property, + property(worker), name='x', reify=True) + + def test_override_property(self): + def worker(obj): pass + foo = self._makeOne() + foo.set_property(worker, name='x') + def doit(): + foo.x = 1 + self.assertRaises(AttributeError, doit) + + def test_override_reify(self): + def worker(obj): pass + foo = self._makeOne() + foo.set_property(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 = self._makeOne() + foo.set_property(lambda _: 1, name='x') + self.assertEqual(1, foo.x) + foo.set_property(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 = self._makeOne() + foo.set_property(lambda _: 1, name='x', reify=True) + self.assertEqual(1, foo.x) + foo.set_property(lambda _: 2, name='x', reify=True) + self.assertEqual(1, foo.x) + class Test_WeakOrderedSet(unittest.TestCase): def _makeOne(self): from pyramid.config import WeakOrderedSet diff --git a/pyramid/util.py b/pyramid/util.py index 76968bbbd..852689c4d 100644 --- a/pyramid/util.py +++ b/pyramid/util.py @@ -14,6 +14,89 @@ class DottedNameResolver(_DottedNameResolver): def __init__(self, package=None): # default to package = None for bw compat return _DottedNameResolver.__init__(self, package) +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, func, name=None, reify=False): + """ Add a callable or a property descriptor to the instance. + + Properties, unlike attributes, are lazily evaluated by executing + an underlying callable when accessed. They can be useful for + adding features to an object without any cost if those features + go unused. + + A property may also be reified via the + :class:`pyramid.decorator.reify` decorator by setting + ``reify=True``, allowing the result of the evaluation to be + cached. Thus the value of the property is only computed once for + the lifetime of the object. + + ``func`` can either be a callable that accepts the instance as + its single positional parameter, or it can be a property + descriptor. + + If the ``func`` is a property descriptor, the ``name`` parameter + must be supplied or a ``ValueError`` will be raised. Also note + that a property descriptor cannot be reified, so ``reify`` must + be ``False``. + + If ``name`` is None, the name of the property will be computed + from the name of the ``func``. + + .. code-block:: python + :linenos: + + class Foo(InstancePropertyMixin): + _x = 1 + + def _get_x(self): + return _x + + def _set_x(self, value): + self._x = value + + foo = Foo() + foo.set_property(property(_get_x, _set_x), name='x') + foo.set_property(_get_x, name='y', reify=True) + + >>> foo.x + 1 + >>> foo.y + 1 + >>> foo.x = 5 + >>> foo.x + 5 + >>> foo.y # notice y keeps the original value + 1 + """ + + is_property = isinstance(func, property) + if is_property: + fn = func + if name is None: + raise ValueError('must specify "name" for a property') + if reify: + raise ValueError('cannot reify a property') + elif name is not None: + fn = lambda this: func(this) + fn.__name__ = name + fn.__doc__ = func.__doc__ + else: + name = func.__name__ + fn = func + if reify: + import pyramid.decorator + fn = pyramid.decorator.reify(fn) + elif not is_property: + fn = property(fn) + attrs = { name: fn } + parent = self.__class__ + cls = type(parent.__name__, (parent, object), attrs) + self.__class__ = cls + class WeakOrderedSet(object): """ Maintain a set of items. -- cgit v1.2.3 From 79a11ce169305ed32c4f3fa887450320ed837e94 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 30 Dec 2011 02:07:25 -0600 Subject: Documented Request.set_property. --- docs/api/request.rst | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/docs/api/request.rst b/docs/api/request.rst index 642e6c84f..9596e5621 100644 --- a/docs/api/request.rst +++ b/docs/api/request.rst @@ -8,6 +8,10 @@ .. autoclass:: Request :members: :inherited-members: + :exclude-members: add_response_callback, add_finished_callback, + route_url, route_path, current_route_url, + current_route_path, static_url, static_path, + model_url, resource_url, set_property .. attribute:: context @@ -204,6 +208,57 @@ body associated with this request, this property will raise an exception. See also :ref:`request_json_body`. + .. method:: set_property(func, name=None, reify=False) + + .. versionadded:: 1.3 + + Add a callable or a property descriptor to the request instance. + + Properties, unlike attributes, are lazily evaluated by executing + an underlying callable when accessed. They can be useful for + adding features to an object without any cost if those features + go unused. + + A property may also be reified via the + :class:`pyramid.decorator.reify` decorator by setting + ``reify=True``, allowing the result of the evaluation to be + cached. Thus the value of the property is only computed once for + the lifetime of the object. + + ``func`` can either be a callable that accepts the request as + its single positional parameter, or it can be a property + descriptor. + + If the ``func`` is a property descriptor a ``ValueError`` will + be raised if ``name`` is ``None`` or ``reify`` is ``True``. + + If ``name`` is None, the name of the property will be computed + from the name of the ``func``. + + .. code-block:: python + :linenos: + + def _connect(request): + conn = request.registry.dbsession() + def cleanup(_): + conn.close() + request.add_finished_callback(cleanup) + return conn + + @subscriber(NewRequest) + def new_request(event): + request = event.request + request.set_property(_connect, 'db', reify=True) + + The subscriber doesn't actually connect to the database, it just + provides the API which, when accessed via ``request.db``, will + create the connection. Thanks to reify, only one connection is + made per-request even if ``request.db`` is accessed many times. + + This pattern provides a way to augment the ``request`` object + without having to subclass it, which can be useful for extension + authors. + .. note:: For information about the API of a :term:`multidict` structure (such as -- cgit v1.2.3 From b73edc8a1b99dd513cfe82c045f3e6bb502f4c69 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 30 Dec 2011 02:32:54 -0600 Subject: Updated the changelog for Request.set_property. --- CHANGES.txt | 12 ++++++++++++ docs/whatsnew-1.3.rst | 10 ++++++++++ 2 files changed, 22 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 3feaa6332..5d05b60b8 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,15 @@ +Unreleased +========== + +Features +-------- + +- New API: ``pyramid.request.Request.set_property``. Add lazy property + descriptors to a request without changing the request factory. New + properties may be reified, effectively caching the value for the lifetime + of the instance. Common use-cases for this would be to get a database + connection for the request or identify the current user. + 1.3a3 (2011-12-21) ================== diff --git a/docs/whatsnew-1.3.rst b/docs/whatsnew-1.3.rst index b61893536..acbaa1335 100644 --- a/docs/whatsnew-1.3.rst +++ b/docs/whatsnew-1.3.rst @@ -206,6 +206,16 @@ This also works for imperative view configurations that involve a class. See :ref:`view_defaults` for more information. +Extending a Request without Subclassing +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +It is now possible to extend a :class:`pyramid.request.Request` object +with property descriptors without having to create a subclass via +:meth:`pyramid.request.Request.set_property`. New properties may be +reified, effectively caching the value for the lifetime of the instance. +Common use-cases for this would be to get a database connection for the +request or identify the current user. + Minor Feature Additions ----------------------- -- cgit v1.2.3 From 2d7d36c7c3d7bd03f18d0cbec411da6bac2b05e2 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 30 Dec 2011 19:11:38 -0500 Subject: garden --- TODO.txt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/TODO.txt b/TODO.txt index 0aebca6eb..3d11470dd 100644 --- a/TODO.txt +++ b/TODO.txt @@ -7,6 +7,13 @@ Must-Have - Fix deployment recipes in cookbook (discourage proxying without changing server). +- Use waitress instead of wsgiref. + +- pyramid.config.util.ActionInfo.__str__ potentially returns Unicode under + Py2, fix. + +- Tests for view names/route patterns that contain Unicode. + Nice-to-Have ------------ -- cgit v1.2.3 From 030d10697cc52a5c26d19818140616a485f63428 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 2 Jan 2012 20:41:44 -0500 Subject: - Use the ``waitress`` WSGI server instead of ``wsgiref`` in scaffolding. --- CHANGES.txt | 2 ++ docs/narr/MyProject/development.ini | 2 +- docs/narr/MyProject/production.ini | 2 +- docs/narr/project.rst | 32 +++-------------- docs/narr/startup.rst | 4 +-- .../wiki/src/authorization/development.ini | 2 +- .../wiki/src/authorization/production.ini | 2 +- docs/tutorials/wiki/src/authorization/setup.py | 1 + .../tutorials/wiki/src/basiclayout/development.ini | 2 +- docs/tutorials/wiki/src/basiclayout/production.ini | 2 +- docs/tutorials/wiki/src/basiclayout/setup.py | 1 + docs/tutorials/wiki/src/models/development.ini | 2 +- docs/tutorials/wiki/src/models/production.ini | 2 +- docs/tutorials/wiki/src/models/setup.py | 1 + docs/tutorials/wiki/src/tests/development.ini | 2 +- docs/tutorials/wiki/src/tests/production.ini | 2 +- docs/tutorials/wiki/src/tests/setup.py | 1 + docs/tutorials/wiki/src/views/development.ini | 2 +- docs/tutorials/wiki/src/views/production.ini | 2 +- docs/tutorials/wiki/src/views/setup.py | 1 + .../wiki2/src/authorization/development.ini | 2 +- .../wiki2/src/authorization/production.ini | 2 +- docs/tutorials/wiki2/src/authorization/setup.py | 1 + .../wiki2/src/basiclayout/development.ini | 2 +- .../tutorials/wiki2/src/basiclayout/production.ini | 2 +- docs/tutorials/wiki2/src/basiclayout/setup.py | 1 + docs/tutorials/wiki2/src/models/development.ini | 2 +- docs/tutorials/wiki2/src/models/production.ini | 2 +- docs/tutorials/wiki2/src/models/setup.py | 1 + docs/tutorials/wiki2/src/tests/development.ini | 2 +- docs/tutorials/wiki2/src/tests/production.ini | 2 +- docs/tutorials/wiki2/src/tests/setup.py | 1 + docs/tutorials/wiki2/src/views/development.ini | 2 +- docs/tutorials/wiki2/src/views/production.ini | 2 +- docs/tutorials/wiki2/src/views/setup.py | 1 + docs/whatsnew-1.3.rst | 41 +++++++--------------- pyramid/scaffolds/alchemy/development.ini_tmpl | 2 +- pyramid/scaffolds/alchemy/production.ini_tmpl | 2 +- pyramid/scaffolds/alchemy/setup.py_tmpl | 1 + pyramid/scaffolds/starter/development.ini_tmpl | 2 +- pyramid/scaffolds/starter/production.ini_tmpl | 2 +- pyramid/scaffolds/starter/setup.py_tmpl | 6 +++- pyramid/scaffolds/zodb/development.ini_tmpl | 2 +- pyramid/scaffolds/zodb/production.ini_tmpl | 2 +- pyramid/scaffolds/zodb/setup.py_tmpl | 1 + 45 files changed, 67 insertions(+), 86 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 5d05b60b8..adc380c34 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -10,6 +10,8 @@ Features of the instance. Common use-cases for this would be to get a database connection for the request or identify the current user. +- Use the ``waitress`` WSGI server instead of ``wsgiref`` in scaffolding. + 1.3a3 (2011-12-21) ================== diff --git a/docs/narr/MyProject/development.ini b/docs/narr/MyProject/development.ini index d61da580f..2ccedb27b 100644 --- a/docs/narr/MyProject/development.ini +++ b/docs/narr/MyProject/development.ini @@ -10,7 +10,7 @@ pyramid.default_locale_name = en pyramid.includes = pyramid_debugtoolbar [server:main] -use = egg:pyramid#wsgiref +use = egg:waitress#main host = 0.0.0.0 port = 6543 diff --git a/docs/narr/MyProject/production.ini b/docs/narr/MyProject/production.ini index 97050e8fe..43ea1d140 100644 --- a/docs/narr/MyProject/production.ini +++ b/docs/narr/MyProject/production.ini @@ -9,7 +9,7 @@ pyramid.debug_templates = false pyramid.default_locale_name = en [server:main] -use = egg:pyramid#wsgiref +use = egg:waitress#main host = 0.0.0.0 port = 6543 diff --git a/docs/narr/project.rst b/docs/narr/project.rst index 0c12d97e6..896b65249 100644 --- a/docs/narr/project.rst +++ b/docs/narr/project.rst @@ -888,33 +888,11 @@ Using an Alternate WSGI Server The code generated by :app:`Pyramid` scaffolding assumes that you will be using the ``pserve`` command to start your application while you do -development. The default rendering of Pyramid scaffolding uses the *wsgiref* -WSGI server, which is a server that is ill-suited for production usage: its -main feature is that it works on all platforms and all systems, making it a -good choice as a default server from the perspective of Pyramid's developers. - -To use a server more suitable for production, you have a number of choices. -Replace the ``use = egg:pyramid#wsgref`` line in your ``production.ini`` with -one of the following. - -``use = egg:Paste#http`` - - ``paste.httpserver`` is Windows, UNIX, and Python 2 compatible. You'll - need to ``easy_install Paste`` into your Pyramid virtualenv for this server - to work. - -``use = egg:pyramid#cherrypy`` - - The ``CherryPy`` WSGI server is Windows, UNIX, Python 2, and Python 3 - compatible. You'll need to ``easy_install CherryPy`` into your Pyramid - virtualenv for this server to work. - -The servers described above are typically both faster and more secure than -the default WSGI server used by Pyramid. Pyramid does not depend on either -because Paste doesn't run on Python 3 (and Pyramid must) and the CherryPy -server is not distributed separately from the CherryPy web framework (and it -would be an awkward dependency to have a web framework rely on another web -framework for just its server component). +development. The default rendering of Pyramid scaffolding uses the +*waitress* WSGI server, which is a server that is suited for production +usage. It's not very fast, or very featureful: its main feature is that it +works on all platforms and all systems, making it a good choice as a default +server from the perspective of Pyramid's developers. ``pserve`` is by no means the only way to start up and serve a :app:`Pyramid` application. As we saw in :ref:`firstapp_chapter`, ``pserve`` needn't be diff --git a/docs/narr/startup.rst b/docs/narr/startup.rst index a7fc5d33c..78b119687 100644 --- a/docs/narr/startup.rst +++ b/docs/narr/startup.rst @@ -133,8 +133,8 @@ Here's a high-level time-ordered overview of what happens when you press far as ``pserve`` is concerned, it is "just another WSGI application". #. ``pserve`` starts the WSGI *server* defined within the ``[server:main]`` - section. In our case, this is the ``egg:pyramid#wsgiref`` server (``use = - egg:pyramid#wsgiref``), and it will listen on all interfaces (``host = + section. In our case, this is the Waitress server (``use = + egg:waitress#main``), and it will listen on all interfaces (``host = 0.0.0.0``), on port number 6543 (``port = 6543``). The server code itself is what prints ``serving on 0.0.0.0:6543 view at http://127.0.0.1:6543``. The server serves the application, and the application is running, waiting diff --git a/docs/tutorials/wiki/src/authorization/development.ini b/docs/tutorials/wiki/src/authorization/development.ini index 0f950e260..47566515c 100644 --- a/docs/tutorials/wiki/src/authorization/development.ini +++ b/docs/tutorials/wiki/src/authorization/development.ini @@ -13,7 +13,7 @@ tm.attempts = 3 zodbconn.uri = file://%(here)s/Data.fs?connection_cache_size=20000 [server:main] -use = egg:pyramid#wsgiref +use = egg:waitress#main host = 0.0.0.0 port = 6543 diff --git a/docs/tutorials/wiki/src/authorization/production.ini b/docs/tutorials/wiki/src/authorization/production.ini index f632f89d1..919efce1e 100644 --- a/docs/tutorials/wiki/src/authorization/production.ini +++ b/docs/tutorials/wiki/src/authorization/production.ini @@ -12,7 +12,7 @@ tm.attempts = 3 zodbconn.uri = file://%(here)s/Data.fs?connection_cache_size=20000 [server:main] -use = egg:pyramid#wsgiref +use = egg:waitress#main host = 0.0.0.0 port = 6543 diff --git a/docs/tutorials/wiki/src/authorization/setup.py b/docs/tutorials/wiki/src/authorization/setup.py index 284bc216e..31c51dbcf 100644 --- a/docs/tutorials/wiki/src/authorization/setup.py +++ b/docs/tutorials/wiki/src/authorization/setup.py @@ -12,6 +12,7 @@ requires = [ 'pyramid_tm', 'pyramid_debugtoolbar', 'ZODB3', + 'waitress', 'docutils', ] diff --git a/docs/tutorials/wiki/src/basiclayout/development.ini b/docs/tutorials/wiki/src/basiclayout/development.ini index e296ea1a1..3acff7f6d 100644 --- a/docs/tutorials/wiki/src/basiclayout/development.ini +++ b/docs/tutorials/wiki/src/basiclayout/development.ini @@ -13,7 +13,7 @@ tm.attempts = 3 zodbconn.uri = file://%(here)s/Data.fs?connection_cache_size=20000 [server:main] -use = egg:pyramid#wsgiref +use = egg:waitress#main host = 0.0.0.0 port = 6543 diff --git a/docs/tutorials/wiki/src/basiclayout/production.ini b/docs/tutorials/wiki/src/basiclayout/production.ini index f632f89d1..919efce1e 100644 --- a/docs/tutorials/wiki/src/basiclayout/production.ini +++ b/docs/tutorials/wiki/src/basiclayout/production.ini @@ -12,7 +12,7 @@ tm.attempts = 3 zodbconn.uri = file://%(here)s/Data.fs?connection_cache_size=20000 [server:main] -use = egg:pyramid#wsgiref +use = egg:waitress#main host = 0.0.0.0 port = 6543 diff --git a/docs/tutorials/wiki/src/basiclayout/setup.py b/docs/tutorials/wiki/src/basiclayout/setup.py index d5fd8891f..43600e239 100644 --- a/docs/tutorials/wiki/src/basiclayout/setup.py +++ b/docs/tutorials/wiki/src/basiclayout/setup.py @@ -12,6 +12,7 @@ requires = [ 'pyramid_tm', 'pyramid_debugtoolbar', 'ZODB3', + 'waitress', ] setup(name='tutorial', diff --git a/docs/tutorials/wiki/src/models/development.ini b/docs/tutorials/wiki/src/models/development.ini index 0f950e260..47566515c 100644 --- a/docs/tutorials/wiki/src/models/development.ini +++ b/docs/tutorials/wiki/src/models/development.ini @@ -13,7 +13,7 @@ tm.attempts = 3 zodbconn.uri = file://%(here)s/Data.fs?connection_cache_size=20000 [server:main] -use = egg:pyramid#wsgiref +use = egg:waitress#main host = 0.0.0.0 port = 6543 diff --git a/docs/tutorials/wiki/src/models/production.ini b/docs/tutorials/wiki/src/models/production.ini index f632f89d1..919efce1e 100644 --- a/docs/tutorials/wiki/src/models/production.ini +++ b/docs/tutorials/wiki/src/models/production.ini @@ -12,7 +12,7 @@ tm.attempts = 3 zodbconn.uri = file://%(here)s/Data.fs?connection_cache_size=20000 [server:main] -use = egg:pyramid#wsgiref +use = egg:waitress#main host = 0.0.0.0 port = 6543 diff --git a/docs/tutorials/wiki/src/models/setup.py b/docs/tutorials/wiki/src/models/setup.py index d5fd8891f..43600e239 100644 --- a/docs/tutorials/wiki/src/models/setup.py +++ b/docs/tutorials/wiki/src/models/setup.py @@ -12,6 +12,7 @@ requires = [ 'pyramid_tm', 'pyramid_debugtoolbar', 'ZODB3', + 'waitress', ] setup(name='tutorial', diff --git a/docs/tutorials/wiki/src/tests/development.ini b/docs/tutorials/wiki/src/tests/development.ini index 0f950e260..47566515c 100644 --- a/docs/tutorials/wiki/src/tests/development.ini +++ b/docs/tutorials/wiki/src/tests/development.ini @@ -13,7 +13,7 @@ tm.attempts = 3 zodbconn.uri = file://%(here)s/Data.fs?connection_cache_size=20000 [server:main] -use = egg:pyramid#wsgiref +use = egg:waitress#main host = 0.0.0.0 port = 6543 diff --git a/docs/tutorials/wiki/src/tests/production.ini b/docs/tutorials/wiki/src/tests/production.ini index f632f89d1..919efce1e 100644 --- a/docs/tutorials/wiki/src/tests/production.ini +++ b/docs/tutorials/wiki/src/tests/production.ini @@ -12,7 +12,7 @@ tm.attempts = 3 zodbconn.uri = file://%(here)s/Data.fs?connection_cache_size=20000 [server:main] -use = egg:pyramid#wsgiref +use = egg:waitress#main host = 0.0.0.0 port = 6543 diff --git a/docs/tutorials/wiki/src/tests/setup.py b/docs/tutorials/wiki/src/tests/setup.py index dfecb7c5c..a7ad7317c 100644 --- a/docs/tutorials/wiki/src/tests/setup.py +++ b/docs/tutorials/wiki/src/tests/setup.py @@ -12,6 +12,7 @@ requires = [ 'pyramid_tm', 'pyramid_debugtoolbar', 'ZODB3', + 'waitress', 'docutils', 'WebTest', # add this ] diff --git a/docs/tutorials/wiki/src/views/development.ini b/docs/tutorials/wiki/src/views/development.ini index e296ea1a1..3acff7f6d 100644 --- a/docs/tutorials/wiki/src/views/development.ini +++ b/docs/tutorials/wiki/src/views/development.ini @@ -13,7 +13,7 @@ tm.attempts = 3 zodbconn.uri = file://%(here)s/Data.fs?connection_cache_size=20000 [server:main] -use = egg:pyramid#wsgiref +use = egg:waitress#main host = 0.0.0.0 port = 6543 diff --git a/docs/tutorials/wiki/src/views/production.ini b/docs/tutorials/wiki/src/views/production.ini index f632f89d1..919efce1e 100644 --- a/docs/tutorials/wiki/src/views/production.ini +++ b/docs/tutorials/wiki/src/views/production.ini @@ -12,7 +12,7 @@ tm.attempts = 3 zodbconn.uri = file://%(here)s/Data.fs?connection_cache_size=20000 [server:main] -use = egg:pyramid#wsgiref +use = egg:waitress#main host = 0.0.0.0 port = 6543 diff --git a/docs/tutorials/wiki/src/views/setup.py b/docs/tutorials/wiki/src/views/setup.py index 866f12bef..a6be89b2e 100644 --- a/docs/tutorials/wiki/src/views/setup.py +++ b/docs/tutorials/wiki/src/views/setup.py @@ -12,6 +12,7 @@ requires = [ 'pyramid_tm', 'pyramid_debugtoolbar', 'ZODB3', + 'waitress', 'docutils', ] diff --git a/docs/tutorials/wiki2/src/authorization/development.ini b/docs/tutorials/wiki2/src/authorization/development.ini index 4f7493cba..2bb74454c 100644 --- a/docs/tutorials/wiki2/src/authorization/development.ini +++ b/docs/tutorials/wiki2/src/authorization/development.ini @@ -13,7 +13,7 @@ pyramid.includes = pyramid_debugtoolbar sqlalchemy.url = sqlite:///%(here)s/tutorial.db [server:main] -use = egg:pyramid#wsgiref +use = egg:waitress#main host = 0.0.0.0 port = 6543 diff --git a/docs/tutorials/wiki2/src/authorization/production.ini b/docs/tutorials/wiki2/src/authorization/production.ini index 53eaf20a1..ec6dea135 100644 --- a/docs/tutorials/wiki2/src/authorization/production.ini +++ b/docs/tutorials/wiki2/src/authorization/production.ini @@ -12,7 +12,7 @@ pyramid.includes = pyramid_tm sqlalchemy.url = sqlite:///%(here)s/tutorial.db [server:main] -use = egg:pyramid#wsgiref +use = egg:waitress#main host = 0.0.0.0 port = 6543 diff --git a/docs/tutorials/wiki2/src/authorization/setup.py b/docs/tutorials/wiki2/src/authorization/setup.py index 09769bfff..7f1ae10b1 100644 --- a/docs/tutorials/wiki2/src/authorization/setup.py +++ b/docs/tutorials/wiki2/src/authorization/setup.py @@ -15,6 +15,7 @@ requires = [ 'pyramid_debugtoolbar', 'zope.sqlalchemy', 'docutils', + 'waitress', ] if sys.version_info[:3] < (2,5,0): diff --git a/docs/tutorials/wiki2/src/basiclayout/development.ini b/docs/tutorials/wiki2/src/basiclayout/development.ini index 4f7493cba..2bb74454c 100644 --- a/docs/tutorials/wiki2/src/basiclayout/development.ini +++ b/docs/tutorials/wiki2/src/basiclayout/development.ini @@ -13,7 +13,7 @@ pyramid.includes = pyramid_debugtoolbar sqlalchemy.url = sqlite:///%(here)s/tutorial.db [server:main] -use = egg:pyramid#wsgiref +use = egg:waitress#main host = 0.0.0.0 port = 6543 diff --git a/docs/tutorials/wiki2/src/basiclayout/production.ini b/docs/tutorials/wiki2/src/basiclayout/production.ini index 53eaf20a1..ec6dea135 100644 --- a/docs/tutorials/wiki2/src/basiclayout/production.ini +++ b/docs/tutorials/wiki2/src/basiclayout/production.ini @@ -12,7 +12,7 @@ pyramid.includes = pyramid_tm sqlalchemy.url = sqlite:///%(here)s/tutorial.db [server:main] -use = egg:pyramid#wsgiref +use = egg:waitress#main host = 0.0.0.0 port = 6543 diff --git a/docs/tutorials/wiki2/src/basiclayout/setup.py b/docs/tutorials/wiki2/src/basiclayout/setup.py index 0ca918cab..44796d4ad 100644 --- a/docs/tutorials/wiki2/src/basiclayout/setup.py +++ b/docs/tutorials/wiki2/src/basiclayout/setup.py @@ -14,6 +14,7 @@ requires = [ 'pyramid_tm', 'pyramid_debugtoolbar', 'zope.sqlalchemy', + 'waitress', ] if sys.version_info[:3] < (2,5,0): diff --git a/docs/tutorials/wiki2/src/models/development.ini b/docs/tutorials/wiki2/src/models/development.ini index 4f7493cba..2bb74454c 100644 --- a/docs/tutorials/wiki2/src/models/development.ini +++ b/docs/tutorials/wiki2/src/models/development.ini @@ -13,7 +13,7 @@ pyramid.includes = pyramid_debugtoolbar sqlalchemy.url = sqlite:///%(here)s/tutorial.db [server:main] -use = egg:pyramid#wsgiref +use = egg:waitress#main host = 0.0.0.0 port = 6543 diff --git a/docs/tutorials/wiki2/src/models/production.ini b/docs/tutorials/wiki2/src/models/production.ini index 53eaf20a1..ec6dea135 100644 --- a/docs/tutorials/wiki2/src/models/production.ini +++ b/docs/tutorials/wiki2/src/models/production.ini @@ -12,7 +12,7 @@ pyramid.includes = pyramid_tm sqlalchemy.url = sqlite:///%(here)s/tutorial.db [server:main] -use = egg:pyramid#wsgiref +use = egg:waitress#main host = 0.0.0.0 port = 6543 diff --git a/docs/tutorials/wiki2/src/models/setup.py b/docs/tutorials/wiki2/src/models/setup.py index 0ca918cab..44796d4ad 100644 --- a/docs/tutorials/wiki2/src/models/setup.py +++ b/docs/tutorials/wiki2/src/models/setup.py @@ -14,6 +14,7 @@ requires = [ 'pyramid_tm', 'pyramid_debugtoolbar', 'zope.sqlalchemy', + 'waitress', ] if sys.version_info[:3] < (2,5,0): diff --git a/docs/tutorials/wiki2/src/tests/development.ini b/docs/tutorials/wiki2/src/tests/development.ini index 4f7493cba..2bb74454c 100644 --- a/docs/tutorials/wiki2/src/tests/development.ini +++ b/docs/tutorials/wiki2/src/tests/development.ini @@ -13,7 +13,7 @@ pyramid.includes = pyramid_debugtoolbar sqlalchemy.url = sqlite:///%(here)s/tutorial.db [server:main] -use = egg:pyramid#wsgiref +use = egg:waitress#main host = 0.0.0.0 port = 6543 diff --git a/docs/tutorials/wiki2/src/tests/production.ini b/docs/tutorials/wiki2/src/tests/production.ini index 53eaf20a1..ec6dea135 100644 --- a/docs/tutorials/wiki2/src/tests/production.ini +++ b/docs/tutorials/wiki2/src/tests/production.ini @@ -12,7 +12,7 @@ pyramid.includes = pyramid_tm sqlalchemy.url = sqlite:///%(here)s/tutorial.db [server:main] -use = egg:pyramid#wsgiref +use = egg:waitress#main host = 0.0.0.0 port = 6543 diff --git a/docs/tutorials/wiki2/src/tests/setup.py b/docs/tutorials/wiki2/src/tests/setup.py index f965ccc6e..d8be7539b 100644 --- a/docs/tutorials/wiki2/src/tests/setup.py +++ b/docs/tutorials/wiki2/src/tests/setup.py @@ -14,6 +14,7 @@ requires = [ 'pyramid_tm', 'pyramid_debugtoolbar', 'zope.sqlalchemy', + 'waitress', 'docutils', 'WebTest', # add this ] diff --git a/docs/tutorials/wiki2/src/views/development.ini b/docs/tutorials/wiki2/src/views/development.ini index 4f7493cba..2bb74454c 100644 --- a/docs/tutorials/wiki2/src/views/development.ini +++ b/docs/tutorials/wiki2/src/views/development.ini @@ -13,7 +13,7 @@ pyramid.includes = pyramid_debugtoolbar sqlalchemy.url = sqlite:///%(here)s/tutorial.db [server:main] -use = egg:pyramid#wsgiref +use = egg:waitress#main host = 0.0.0.0 port = 6543 diff --git a/docs/tutorials/wiki2/src/views/production.ini b/docs/tutorials/wiki2/src/views/production.ini index 53eaf20a1..ec6dea135 100644 --- a/docs/tutorials/wiki2/src/views/production.ini +++ b/docs/tutorials/wiki2/src/views/production.ini @@ -12,7 +12,7 @@ pyramid.includes = pyramid_tm sqlalchemy.url = sqlite:///%(here)s/tutorial.db [server:main] -use = egg:pyramid#wsgiref +use = egg:waitress#main host = 0.0.0.0 port = 6543 diff --git a/docs/tutorials/wiki2/src/views/setup.py b/docs/tutorials/wiki2/src/views/setup.py index 9c0e88eb0..05c7be2d7 100644 --- a/docs/tutorials/wiki2/src/views/setup.py +++ b/docs/tutorials/wiki2/src/views/setup.py @@ -14,6 +14,7 @@ requires = [ 'pyramid_tm', 'pyramid_debugtoolbar', 'zope.sqlalchemy', + 'waitress', 'docutils', ] diff --git a/docs/whatsnew-1.3.rst b/docs/whatsnew-1.3.rst index acbaa1335..e8d549e60 100644 --- a/docs/whatsnew-1.3.rst +++ b/docs/whatsnew-1.3.rst @@ -52,7 +52,7 @@ to make some changes: - We've replaced the ``paster`` command with Pyramid-specific analogues. -- We've made the default WSGI server the ``wsgiref`` server. +- We've made the default WSGI server the ``waitress`` server. Previously (in Pyramid 1.0, 1.1 and 1.2), you created a Pyramid application using ``paster create``, like so:: @@ -85,37 +85,25 @@ Analogues of ``paster pshell``, ``paster pviews``, ``paster request`` and ``paster ptweens`` also exist under the respective console script names ``pshell``, ``pviews``, ``prequest`` and ``ptweens``. -We've replaced use of the Paste ``httpserver`` with the ``wsgiref`` server in +We've replaced use of the Paste ``httpserver`` with the ``waitress`` server in the scaffolds, so once you create a project from a scaffold, its ``development.ini`` and ``production.ini`` will have the following line:: - use = egg:pyramid#wsgiref + use = egg:waitress#main Instead of this (which was the default in older versions):: use = egg:Paste#http -Using ``wsgiref`` as the default WSGI server is purely a default to make it -possible to use the same scaffolding under Python 2 and Python 3; people -running Pyramid under Python 2 can still manually install ``Paste`` and use -the Paste ``httpserver`` by replacing the former line with the latter. This is -actually recommended if you rely on proxying from Apache or Nginx to a -``pserve`` -invoked application. **The wsgiref server is not a production -quality server.** See :ref:`alternate_wsgi_server` for more information. - -New releases in every older major Pyramid series (1.0.2, 1.1.3, 1.2.5) also -have the ``egg:pyramid#wsgiref`` entry point, so scaffold-writers can depend -on it being there even in older major Pyramid versions. - .. warning:: - Previously, paste.httpserver "helped" by converting header values that weren't - strings to strings. The wsgiref server, on the other hand implements the spec - more fully. This specifically may affect you if you are modifying headers on - your response. The following error might be an indicator of this problem: - **AssertionError: Header values must be strings, please check the type of - the header being returned.** A common case would be returning unicode headers - instead of string headers. + Previously, paste.httpserver "helped" by converting header values that + weren't strings to strings. The ``waitress`` server, on the other hand + implements the spec more fully. This specifically may affect you if you + are modifying headers on your response. The following error might be an + indicator of this problem: **AssertionError: Header values must be + strings, please check the type of the header being returned.** A common + case would be returning unicode headers instead of string headers. A new :mod:`pyramid.compat` module was added which provides Python 2/3 straddling support for Pyramid add-ons and development environments. @@ -264,12 +252,9 @@ Backwards Incompatibilities Python 3. - The default WSGI server run as the result of ``pserve`` from newly rendered - scaffolding is now the ``wsgiref`` WSGI server instead of the - ``paste.httpserver`` server. ``wsgiref``, unlike the server it replaced - (``paste.httpserver``) is not a production quality server. See - :ref:`alternate_wsgi_server` for information about how to use another WSGI - server in production. Rationale: the Paste and PasteScript packages do not - run under Python 3. + scaffolding is now the ``waitress`` WSGI server instead of the + ``paste.httpserver`` server. Rationale: the Paste and PasteScript packages + do not run under Python 3. - The ``pshell`` command (see "paster pshell") no longer accepts a ``--disable-ipython`` command-line argument. Instead, it accepts a ``-p`` diff --git a/pyramid/scaffolds/alchemy/development.ini_tmpl b/pyramid/scaffolds/alchemy/development.ini_tmpl index d804a0b0e..1127aa00f 100644 --- a/pyramid/scaffolds/alchemy/development.ini_tmpl +++ b/pyramid/scaffolds/alchemy/development.ini_tmpl @@ -13,7 +13,7 @@ pyramid.includes = pyramid_debugtoolbar sqlalchemy.url = sqlite:///%(here)s/{{project}}.db [server:main] -use = egg:pyramid#wsgiref +use = egg:waitress#main host = 0.0.0.0 port = 6543 diff --git a/pyramid/scaffolds/alchemy/production.ini_tmpl b/pyramid/scaffolds/alchemy/production.ini_tmpl index 7350ce25f..9c9335d20 100644 --- a/pyramid/scaffolds/alchemy/production.ini_tmpl +++ b/pyramid/scaffolds/alchemy/production.ini_tmpl @@ -12,7 +12,7 @@ pyramid.includes = pyramid_tm sqlalchemy.url = sqlite:///%(here)s/{{project}}.db [server:main] -use = egg:pyramid#wsgiref +use = egg:waitress#main host = 0.0.0.0 port = 6543 diff --git a/pyramid/scaffolds/alchemy/setup.py_tmpl b/pyramid/scaffolds/alchemy/setup.py_tmpl index a2766547d..a7797af2f 100644 --- a/pyramid/scaffolds/alchemy/setup.py_tmpl +++ b/pyramid/scaffolds/alchemy/setup.py_tmpl @@ -14,6 +14,7 @@ requires = [ 'pyramid_tm', 'pyramid_debugtoolbar', 'zope.sqlalchemy', + 'waitress', ] if sys.version_info[:3] < (2,5,0): diff --git a/pyramid/scaffolds/starter/development.ini_tmpl b/pyramid/scaffolds/starter/development.ini_tmpl index 4d3a80286..ee53fcf1c 100644 --- a/pyramid/scaffolds/starter/development.ini_tmpl +++ b/pyramid/scaffolds/starter/development.ini_tmpl @@ -10,7 +10,7 @@ pyramid.default_locale_name = en pyramid.includes = pyramid_debugtoolbar [server:main] -use = egg:pyramid#wsgiref +use = egg:waitress#main host = 0.0.0.0 port = 6543 diff --git a/pyramid/scaffolds/starter/production.ini_tmpl b/pyramid/scaffolds/starter/production.ini_tmpl index 931cfa510..2712d2e39 100644 --- a/pyramid/scaffolds/starter/production.ini_tmpl +++ b/pyramid/scaffolds/starter/production.ini_tmpl @@ -9,7 +9,7 @@ pyramid.debug_templates = false pyramid.default_locale_name = en [server:main] -use = egg:pyramid#wsgiref +use = egg:waitress#main host = 0.0.0.0 port = 6543 diff --git a/pyramid/scaffolds/starter/setup.py_tmpl b/pyramid/scaffolds/starter/setup.py_tmpl index e63579d50..39ac6de9d 100644 --- a/pyramid/scaffolds/starter/setup.py_tmpl +++ b/pyramid/scaffolds/starter/setup.py_tmpl @@ -6,7 +6,11 @@ here = os.path.abspath(os.path.dirname(__file__)) README = open(os.path.join(here, 'README.txt')).read() CHANGES = open(os.path.join(here, 'CHANGES.txt')).read() -requires = ['pyramid', 'pyramid_debugtoolbar'] +requires = [ + 'pyramid', + 'pyramid_debugtoolbar', + 'waitress', + ] setup(name='{{project}}', version='0.0', diff --git a/pyramid/scaffolds/zodb/development.ini_tmpl b/pyramid/scaffolds/zodb/development.ini_tmpl index eb2b0d3de..6cffd5657 100644 --- a/pyramid/scaffolds/zodb/development.ini_tmpl +++ b/pyramid/scaffolds/zodb/development.ini_tmpl @@ -14,7 +14,7 @@ tm.attempts = 3 zodbconn.uri = file://%(here)s/Data.fs?connection_cache_size=20000 [server:main] -use = egg:pyramid#wsgiref +use = egg:waitress#main host = 0.0.0.0 port = 6543 diff --git a/pyramid/scaffolds/zodb/production.ini_tmpl b/pyramid/scaffolds/zodb/production.ini_tmpl index 7c91ea0df..c0f2fbbf3 100644 --- a/pyramid/scaffolds/zodb/production.ini_tmpl +++ b/pyramid/scaffolds/zodb/production.ini_tmpl @@ -13,7 +13,7 @@ tm.attempts = 3 zodbconn.uri = file://%(here)s/Data.fs?connection_cache_size=20000 [server:main] -use = egg:pyramid#wsgiref +use = egg:waitress#main host = 0.0.0.0 port = 6543 diff --git a/pyramid/scaffolds/zodb/setup.py_tmpl b/pyramid/scaffolds/zodb/setup.py_tmpl index 812b85e83..965c0178f 100644 --- a/pyramid/scaffolds/zodb/setup.py_tmpl +++ b/pyramid/scaffolds/zodb/setup.py_tmpl @@ -12,6 +12,7 @@ requires = [ 'pyramid_tm', 'pyramid_debugtoolbar', 'ZODB3', + 'waitress', ] setup(name='{{project}}', -- cgit v1.2.3 From 22c2200bf69ca49a0bfbe0e0a0241e87b9143737 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 2 Jan 2012 20:42:56 -0500 Subject: have sample scaffold depend on waitress --- docs/narr/MyProject/setup.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/narr/MyProject/setup.py b/docs/narr/MyProject/setup.py index 74879f65d..b119a954b 100644 --- a/docs/narr/MyProject/setup.py +++ b/docs/narr/MyProject/setup.py @@ -6,7 +6,11 @@ here = os.path.abspath(os.path.dirname(__file__)) README = open(os.path.join(here, 'README.txt')).read() CHANGES = open(os.path.join(here, 'CHANGES.txt')).read() -requires = ['pyramid', 'pyramid_debugtoolbar'] +requires = [ + 'pyramid', + 'pyramid_debugtoolbar', + 'waitress', + ] setup(name='MyProject', version='0.0', -- cgit v1.2.3 From 52a948e65ef923340a9c172fe6d258f2832f8799 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 3 Jan 2012 08:16:06 -0500 Subject: - The documentation of ``pyramid.events.subscriber`` indicated that using it as a decorator with no arguments like this:: @subscriber() def somefunc(event): pass Would register ``somefunc`` to receive all events sent via the registry, but this was untrue. Instead, it would receive no events at all. This has now been fixed and the code matches the documentation. See also https://github.com/Pylons/pyramid/issues/386 Closes #386 --- CHANGES.txt | 15 +++++++++++++++ pyramid/events.py | 9 ++++++--- pyramid/tests/test_events.py | 10 ++++++++++ 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index adc380c34..07c0b564d 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -12,6 +12,21 @@ Features - Use the ``waitress`` WSGI server instead of ``wsgiref`` in scaffolding. +Bug Fixes +--------- + +- The documentation of ``pyramid.events.subscriber`` indicated that using it + as a decorator with no arguments like this:: + + @subscriber() + def somefunc(event): + pass + + Would register ``somefunc`` to receive all events sent via the registry, + but this was untrue. Instead, it would receive no events at all. This has + now been fixed and the code matches the documentation. See also + https://github.com/Pylons/pyramid/issues/386 + 1.3a3 (2011-12-21) ================== diff --git a/pyramid/events.py b/pyramid/events.py index 9cb8b31ad..e181ef33f 100644 --- a/pyramid/events.py +++ b/pyramid/events.py @@ -1,6 +1,9 @@ import venusian -from zope.interface import implementer +from zope.interface import ( + implementer, + Interface + ) from pyramid.interfaces import ( IContextFound, @@ -26,7 +29,7 @@ class subscriber(object): def mysubscriber(event): event.request.foo = 1 - More than one event type can be passed as a construtor argument. The + More than one event type can be passed as a constructor argument. The decorated subscriber will be called for each event type. .. code-block:: python @@ -66,7 +69,7 @@ class subscriber(object): def register(self, scanner, name, wrapped): config = scanner.config - for iface in self.ifaces: + for iface in self.ifaces or (Interface,): config.add_subscriber(wrapped, iface) def __call__(self, wrapped): diff --git a/pyramid/tests/test_events.py b/pyramid/tests/test_events.py index 4b58a129c..f35083c02 100644 --- a/pyramid/tests/test_events.py +++ b/pyramid/tests/test_events.py @@ -155,6 +155,16 @@ class TestSubscriber(unittest.TestCase): dec.register(scanner, None, foo) self.assertEqual(config.subscribed, [(foo, IFoo), (foo, IBar)]) + def test_register_none_means_all(self): + from zope.interface import Interface + dec = self._makeOne() + def foo(): pass + config = DummyConfigurator() + scanner = Dummy() + scanner.config = config + dec.register(scanner, None, foo) + self.assertEqual(config.subscribed, [(foo, Interface)]) + def test_register_objectevent(self): from zope.interface import Interface class IFoo(Interface): pass -- cgit v1.2.3 From 5c43d5725f7d3266f56ec58b412f797a82078aa0 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 4 Jan 2012 01:16:50 -0500 Subject: untested work --- pyramid/compat.py | 18 +++++++++++ pyramid/traversal.py | 84 +++++++++++++++++++++++++++++++++------------------- 2 files changed, 71 insertions(+), 31 deletions(-) diff --git a/pyramid/compat.py b/pyramid/compat.py index 12b8c9f37..7376278ac 100644 --- a/pyramid/compat.py +++ b/pyramid/compat.py @@ -213,3 +213,21 @@ except ImportError: # pragma: no cover import json +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): + return path.encode('latin-1').decode('utf-8') +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 + 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) diff --git a/pyramid/traversal.py b/pyramid/traversal.py index cd624fd30..3489dade0 100644 --- a/pyramid/traversal.py +++ b/pyramid/traversal.py @@ -16,12 +16,12 @@ from pyramid.compat import ( PY3, native_, text_, - bytes_, ascii_native_, text_type, binary_type, - url_unquote_native, is_nonstr_iter, + decode_path_info, + unquote_bytes_to_wsgi, ) from pyramid.encode import url_quote @@ -429,33 +429,44 @@ def virtual_root(resource, request): def traversal_path(path): """ Variant of :func:`pyramid.traversal.traversal_path_info` suitable for - decoding paths that are URL-encoded.""" - path = ascii_native_(path) - path = url_unquote_native(path, 'latin-1', 'strict') + decoding paths that are URL-encoded. + + If this function is passed a Unicode object instead of a sequence of + bytes as ``path``, that Unicode object *must* directly encodeable to + ASCII. For example, u'/foo' will work but u'/' (a + Unicode object with characters that cannot be encoded to ascii) will + not. A :exc:`UnicodeEncodeError` will be raised if the Unicode cannot be + encoded directly to ASCII. + """ + # we unquote this path exactly like a PEP 3333 server would + if isinstance(path, text_type): + path = path.encode('ascii') + path = unquote_bytes_to_wsgi(path) # result will be a native string return traversal_path_info(path) -@lru_cache(1000) def traversal_path_info(path): - """ Given a ``PATH_INFO`` environ value (slash-separated path segments), - return a tuple representing that path which can be used to traverse a - resource tree. - - ``PATH_INFO`` is assumed to already be URL-decoded. It is encoded to - bytes using the Latin-1 encoding; the resulting set of bytes is - subsequently decoded to text using the UTF-8 encoding; a - :exc:`pyramid.exc.URLDecodeError` is raised if a the URL cannot be - decoded. - - The ``PATH_INFO`` is split on slashes, creating a list of segments. Each - segment subsequently decoded into Unicode. If a segment name is empty or - if it is ``.``, it is ignored. If a segment name is ``..``, the previous - segment is deleted, and the ``..`` is ignored. - - If this function is passed a Unicode object instead of a string, that - Unicode object *must* directly encodeable to ASCII. For example, u'/foo' - will work but u'/' (a Unicode object with characters - that cannot be encoded to ascii) will not. A :exc:`UnicodeError` will be - raised if the Unicode cannot be encoded directly to ASCII. + """ Given``path``, return a tuple representing that path which can be + used to traverse a resource tree. ``path`` is assumed to be an + already-URL-decoded ``str`` type as if it had come to us from an upstream + WSGI server as the ``PATH_INFO`` environment variable. + + The ``path`` is first decoded to from its WSGI representation to Unicode; + it is decoded differently depending on platform: + + - On Python 2, ``path`` is decoded to Unicode from bytes using the UTF-8 + decoding directly; a :exc:`pyramid.exc.URLDecodeError` is raised if a the + URL cannot be decoded. + + - On Python 3, as per the WSGI spec, ``path`` is first encoded to bytes + using the Latin-1 encoding; the resulting set of bytes is subsequently + decoded to text using the UTF-8 encoding; a + :exc:`pyramid.exc.URLDecodeError` is raised if a the URL cannot be + decoded. + + The ``path`` is split on slashes, creating a list of segments. If a + segment name is empty or if it is ``.``, it is ignored. If a segment + name is ``..``, the previous segment is deleted, and the ``..`` is + ignored. Examples: @@ -504,9 +515,15 @@ def traversal_path_info(path): applications in :app:`Pyramid`. """ try: - path = bytes_(path, 'latin-1').decode('utf-8') + path = decode_path_info(path) except UnicodeDecodeError as e: raise URLDecodeError(e.encoding, e.object, e.start, e.end, e.reason) + return split_path_info(path) + +@lru_cache(1000) +def split_path_info(path): + # suitable for splitting an already-unquoted-already-decoded path_info + # string path = path.strip('/') clean = [] for segment in path.split('/'): @@ -622,23 +639,28 @@ class ResourceTreeTraverser(object): path = matchdict.get('traverse', '/') or '/' if is_nonstr_iter(path): # this is a *traverse stararg (not a {traverse}) - path = '/'.join([quote_path_segment(x) for x in path]) or '/' + # routing has already decoded these elements, so we just + # need to join them + path = '/'.join(path) or '/' subpath = matchdict.get('subpath', ()) if not is_nonstr_iter(subpath): # this is not a *subpath stararg (just a {subpath}) - subpath = traversal_path_info(subpath) + # routing has already decoded this string, so we just need + # to split it + subpath = split_path_info(subpath) else: # this request did not match a route subpath = () try: - path = environ['PATH_INFO'] or '/' + path = decode_path_info(environ['PATH_INFO'] or '/') except KeyError: path = '/' if VH_ROOT_KEY in environ: - vroot_path = environ[VH_ROOT_KEY] + # HTTP_X_VHM_ROOT + vroot_path = decode_path_info(environ[VH_ROOT_KEY]) vroot_tuple = traversal_path_info(vroot_path) vpath = vroot_path + path vroot_idx = len(vroot_tuple) -1 -- cgit v1.2.3 From 6212db897aa5cb61615f9fdefc08adf4cb7f7476 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 4 Jan 2012 17:53:42 -0500 Subject: more speculative changes regarding matching --- pyramid/traversal.py | 13 +++++++++---- pyramid/urldispatch.py | 19 +++++++------------ 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/pyramid/traversal.py b/pyramid/traversal.py index 3489dade0..2443e2d1c 100644 --- a/pyramid/traversal.py +++ b/pyramid/traversal.py @@ -444,11 +444,12 @@ def traversal_path(path): path = unquote_bytes_to_wsgi(path) # result will be a native string return traversal_path_info(path) +@lru_cache(1000) def traversal_path_info(path): """ Given``path``, return a tuple representing that path which can be used to traverse a resource tree. ``path`` is assumed to be an already-URL-decoded ``str`` type as if it had come to us from an upstream - WSGI server as the ``PATH_INFO`` environment variable. + WSGI server as the ``PATH_INFO`` environ variable. The ``path`` is first decoded to from its WSGI representation to Unicode; it is decoded differently depending on platform: @@ -457,9 +458,9 @@ def traversal_path_info(path): decoding directly; a :exc:`pyramid.exc.URLDecodeError` is raised if a the URL cannot be decoded. - - On Python 3, as per the WSGI spec, ``path`` is first encoded to bytes - using the Latin-1 encoding; the resulting set of bytes is subsequently - decoded to text using the UTF-8 encoding; a + - On Python 3, as per the PEP 3333 spec, ``path`` is first encoded to + bytes using the Latin-1 encoding; the resulting set of bytes is + subsequently decoded to text using the UTF-8 encoding; a :exc:`pyramid.exc.URLDecodeError` is raised if a the URL cannot be decoded. @@ -654,9 +655,13 @@ class ResourceTreeTraverser(object): # this request did not match a route subpath = () try: + # empty if mounted under a path in mod_wsgi, for example path = decode_path_info(environ['PATH_INFO'] or '/') except KeyError: path = '/' + except UnicodeDecodeError as e: + raise URLDecodeError(e.encoding, e.object, e.start, e.end, + e.reason) if VH_ROOT_KEY in environ: # HTTP_X_VHM_ROOT diff --git a/pyramid/urldispatch.py b/pyramid/urldispatch.py index c7520b8d2..73875b675 100644 --- a/pyramid/urldispatch.py +++ b/pyramid/urldispatch.py @@ -18,8 +18,9 @@ from pyramid.compat import ( from pyramid.exceptions import URLDecodeError from pyramid.traversal import ( - traversal_path_info, quote_path_segment, + decode_path_info, + split_path_info, ) _marker = object() @@ -70,9 +71,11 @@ class RoutesMapper(object): environ = request.environ try: # empty if mounted under a path in mod_wsgi, for example - path = environ['PATH_INFO'] or '/' + path = decode_path_info(environ['PATH_INFO'] or '/') except KeyError: path = '/' + except UnicodeDecodeError as e: + raise URLDecodeError(e.encoding, e.object, e.start, e.end, e.reason) for route in self.routelist: match = route.match(path) @@ -147,17 +150,9 @@ def _compile_route(route): d = {} for k, v in m.groupdict().items(): if k == star: - d[k] = traversal_path_info(v) + d[k] = split_path_info(v) else: - try: - val = bytes_(v).decode('utf-8', 'strict') - d[k] = val - except UnicodeDecodeError as e: - raise URLDecodeError( - e.encoding, e.object, e.start, e.end, e.reason - ) - - + d[k] = v return d -- cgit v1.2.3 From 4ded431cba6729e5f2ac1879df735905006555ac Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 4 Jan 2012 19:02:04 -0500 Subject: remove dependency on pysqlite from all scaffolding on platforms lt 2.6 --- docs/tutorials/wiki2/src/authorization/setup.py | 4 ---- docs/tutorials/wiki2/src/basiclayout/setup.py | 4 ---- docs/tutorials/wiki2/src/models/setup.py | 4 ---- docs/tutorials/wiki2/src/tests/setup.py | 4 ---- docs/tutorials/wiki2/src/views/setup.py | 4 ---- docs/tutorials/wiki2/tests.rst | 2 +- pyramid/scaffolds/alchemy/setup.py_tmpl | 1 - 7 files changed, 1 insertion(+), 22 deletions(-) diff --git a/docs/tutorials/wiki2/src/authorization/setup.py b/docs/tutorials/wiki2/src/authorization/setup.py index 7f1ae10b1..964e39010 100644 --- a/docs/tutorials/wiki2/src/authorization/setup.py +++ b/docs/tutorials/wiki2/src/authorization/setup.py @@ -1,5 +1,4 @@ import os -import sys from setuptools import setup, find_packages @@ -18,9 +17,6 @@ requires = [ 'waitress', ] -if sys.version_info[:3] < (2,5,0): - requires.append('pysqlite') - setup(name='tutorial', version='0.0', description='tutorial', diff --git a/docs/tutorials/wiki2/src/basiclayout/setup.py b/docs/tutorials/wiki2/src/basiclayout/setup.py index 44796d4ad..fae1c25a8 100644 --- a/docs/tutorials/wiki2/src/basiclayout/setup.py +++ b/docs/tutorials/wiki2/src/basiclayout/setup.py @@ -1,5 +1,4 @@ import os -import sys from setuptools import setup, find_packages @@ -17,9 +16,6 @@ requires = [ 'waitress', ] -if sys.version_info[:3] < (2,5,0): - requires.append('pysqlite') - setup(name='tutorial', version='0.0', description='tutorial', diff --git a/docs/tutorials/wiki2/src/models/setup.py b/docs/tutorials/wiki2/src/models/setup.py index 44796d4ad..fae1c25a8 100644 --- a/docs/tutorials/wiki2/src/models/setup.py +++ b/docs/tutorials/wiki2/src/models/setup.py @@ -1,5 +1,4 @@ import os -import sys from setuptools import setup, find_packages @@ -17,9 +16,6 @@ requires = [ 'waitress', ] -if sys.version_info[:3] < (2,5,0): - requires.append('pysqlite') - setup(name='tutorial', version='0.0', description='tutorial', diff --git a/docs/tutorials/wiki2/src/tests/setup.py b/docs/tutorials/wiki2/src/tests/setup.py index d8be7539b..d05495fbd 100644 --- a/docs/tutorials/wiki2/src/tests/setup.py +++ b/docs/tutorials/wiki2/src/tests/setup.py @@ -1,5 +1,4 @@ import os -import sys from setuptools import setup, find_packages @@ -19,9 +18,6 @@ requires = [ 'WebTest', # add this ] -if sys.version_info[:3] < (2,5,0): - requires.append('pysqlite') - setup(name='tutorial', version='0.0', description='tutorial', diff --git a/docs/tutorials/wiki2/src/views/setup.py b/docs/tutorials/wiki2/src/views/setup.py index 05c7be2d7..834280118 100644 --- a/docs/tutorials/wiki2/src/views/setup.py +++ b/docs/tutorials/wiki2/src/views/setup.py @@ -1,5 +1,4 @@ import os -import sys from setuptools import setup, find_packages @@ -18,9 +17,6 @@ requires = [ 'docutils', ] -if sys.version_info[:3] < (2,5,0): - requires.append('pysqlite') - setup(name='tutorial', version='0.0', description='tutorial', diff --git a/docs/tutorials/wiki2/tests.rst b/docs/tutorials/wiki2/tests.rst index 25cf2c25e..92544404c 100644 --- a/docs/tutorials/wiki2/tests.rst +++ b/docs/tutorials/wiki2/tests.rst @@ -55,7 +55,7 @@ Change the ``requires`` list in ``setup.py`` to include ``WebTest``. .. literalinclude:: src/tests/setup.py :linenos: :language: python - :lines: 10-19 + :lines: 9-20 After we've added a dependency on WebTest in ``setup.py``, we need to rerun ``setup.py develop`` to get WebTest installed into our virtualenv. Assuming diff --git a/pyramid/scaffolds/alchemy/setup.py_tmpl b/pyramid/scaffolds/alchemy/setup.py_tmpl index bffbd8bfb..025ee5be3 100644 --- a/pyramid/scaffolds/alchemy/setup.py_tmpl +++ b/pyramid/scaffolds/alchemy/setup.py_tmpl @@ -1,5 +1,4 @@ import os -import sys from setuptools import setup, find_packages -- cgit v1.2.3 From 95df3ac0fc92e882997997dac97b7ba86e19e963 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 4 Jan 2012 20:01:57 -0500 Subject: more usage of traversal_path_info fixed --- pyramid/config/testing.py | 10 +++++++--- pyramid/config/util.py | 6 +++--- pyramid/traversal.py | 13 +++++++------ 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/pyramid/config/testing.py b/pyramid/config/testing.py index 3cdc1aa24..f40cf25a7 100644 --- a/pyramid/config/testing.py +++ b/pyramid/config/testing.py @@ -8,7 +8,11 @@ from pyramid.interfaces import ( ) from pyramid.renderers import RendererHelper -from pyramid.traversal import traversal_path_info + +from pyramid.traversal import ( + decode_path_info, + split_path_info, + ) from pyramid.config.util import action_method @@ -66,9 +70,9 @@ class TestingConfiguratorMixin(object): self.context = context def __call__(self, request): - path = request.environ['PATH_INFO'] + path = decode_path_info(request.environ['PATH_INFO']) ob = resources[path] - traversed = traversal_path_info(path) + traversed = split_path_info(path) return {'context':ob, 'view_name':'','subpath':(), 'traversed':traversed, 'virtual_root':ob, 'virtual_root_path':(), 'root':ob} diff --git a/pyramid/config/util.py b/pyramid/config/util.py index b0e873de3..79f13e4a0 100644 --- a/pyramid/config/util.py +++ b/pyramid/config/util.py @@ -15,7 +15,7 @@ from pyramid.exceptions import ConfigurationError from pyramid.traversal import ( find_interface, - traversal_path_info, + traversal_path, ) from hashlib import md5 @@ -268,8 +268,8 @@ def make_predicates(xhr=None, request_method=None, path_info=None, if 'traverse' in context: return True m = context['match'] - tvalue = tgenerate(m) - m['traverse'] = traversal_path_info(tvalue) + tvalue = tgenerate(m) # tvalue will be urlquoted string + m['traverse'] = traversal_path(tvalue) # will be seq of unicode return True # This isn't actually a predicate, it's just a infodict # modifier that injects ``traverse`` into the matchdict. As a diff --git a/pyramid/traversal.py b/pyramid/traversal.py index 2443e2d1c..ffc40fa60 100644 --- a/pyramid/traversal.py +++ b/pyramid/traversal.py @@ -438,11 +438,12 @@ def traversal_path(path): not. A :exc:`UnicodeEncodeError` will be raised if the Unicode cannot be encoded directly to ASCII. """ - # we unquote this path exactly like a PEP 3333 server would if isinstance(path, text_type): + # must not possess characters outside ascii path = path.encode('ascii') + # we unquote this path exactly like a PEP 3333 server would path = unquote_bytes_to_wsgi(path) # result will be a native string - return traversal_path_info(path) + return traversal_path_info(path) # result will be a tuple of unicode @lru_cache(1000) def traversal_path_info(path): @@ -516,15 +517,15 @@ def traversal_path_info(path): applications in :app:`Pyramid`. """ try: - path = decode_path_info(path) + path = decode_path_info(path) # result will be Unicode except UnicodeDecodeError as e: raise URLDecodeError(e.encoding, e.object, e.start, e.end, e.reason) - return split_path_info(path) + return split_path_info(path) # result will be tuple of Unicode @lru_cache(1000) def split_path_info(path): - # suitable for splitting an already-unquoted-already-decoded path_info - # string + # suitable for splitting an already-unquoted-already-decoded (unicode) + # path value path = path.strip('/') clean = [] for segment in path.split('/'): -- cgit v1.2.3 From a511b1423334f855e996bb06714b36aa86f861e9 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 5 Jan 2012 02:41:32 -0500 Subject: fix urldispatch matching and generation to cope with various inputs --- docs/narr/urldispatch.rst | 87 ++++++++++++++++++++++++++++++++-- pyramid/tests/test_urldispatch.py | 59 +++++++++++++++++------ pyramid/urldispatch.py | 99 ++++++++++++++++++++++++++++----------- 3 files changed, 200 insertions(+), 45 deletions(-) diff --git a/docs/narr/urldispatch.rst b/docs/narr/urldispatch.rst index 35613ea1b..7e485f8ae 100644 --- a/docs/narr/urldispatch.rst +++ b/docs/narr/urldispatch.rst @@ -235,7 +235,7 @@ When matching the following URL: .. code-block:: text - foo/La%20Pe%C3%B1a + http://example.com/foo/La%20Pe%C3%B1a The matchdict will look like so (the value is URL-decoded / UTF-8 decoded): @@ -243,6 +243,50 @@ The matchdict will look like so (the value is URL-decoded / UTF-8 decoded): {'bar':u'La Pe\xf1a'} +Literal strings in the path segment should represent the *decoded* value of +the ``PATH_INFO`` provided to Pyramid. You don't want to use a URL-encoded +value or a bytestring representing the literal's UTF-8 in the pattern. For +example, rather than this: + +.. code-block:: text + + /Foo%20Bar/{baz} + +You'll want to use something like this: + +.. code-block:: text + + /Foo Bar/{baz} + +For patterns that contain "high-order" characters in its literals, you'll +want to use a Unicode value as the pattern as opposed to any URL-encoded or +UTF-8-encoded value. For example, you might be tempted to use a bytestring +pattern like this: + +.. code-block:: text + + /La Pe\xc3\xb1a/{x} + +But that probably won't match as you expect it to. You'll want to use a +Unicode value as the pattern instead rather than raw bytestring escapes. You +can use a high-order Unicode value as the pattern by using `Python source +file encoding `_ plus the "real" +character in the Unicode pattern in the source, like so: + +.. code-block:: text + + /La Peña/{x} + +Or you can ignore source file encoding and use equivalent Unicode escape +characters in the pattern. + +.. code-block:: text + + /La Pe\xf1a/{x} + +Dynamic segment names cannot contain high-order characters, so this applies +only to literals in the pattern. + If the pattern has a ``*`` in it, the name which follows it is considered a "remainder match". A remainder match *must* come at the end of the pattern. Unlike segment replacement markers, it does not need to be preceded by a @@ -612,7 +656,6 @@ Use the :meth:`pyramid.request.Request.route_url` method to generate URLs based on route patterns. For example, if you've configured a route with the ``name`` "foo" and the ``pattern`` "{a}/{b}/{c}", you might do this. -.. ignore-next-block .. code-block:: python :linenos: @@ -620,7 +663,45 @@ based on route patterns. For example, if you've configured a route with the This would return something like the string ``http://example.com/1/2/3`` (at least if the current protocol and hostname implied ``http://example.com``). -See the :meth:`~pyramid.request.Request.route_url` API documentation for more + +To get only the *path* of a route, use the +:meth:`pyramid.request.Request.route_path` API instead of +:meth:`~pyramid.request.Request.route_url`. + +.. code-block:: python + + url = request.route_path('foo', a='1', b='2', c='3') + +This will return the string ``/1/2/3`` rather than a full URL. + +Note that URLs and paths generated by ``route_path`` and ``route_url`` are +always URL-quoted string types (which contain no non-ASCII characters). +Therefore, if you've added a route like so: + +.. code-block:: python + + config.add_route('la', u'/La Peña/{city}') + +And you later generate a URL using ``route_path`` or ``route_url`` like so: + +.. code-block:: python + + url = request.route_path('la', city=u'Québec') + +You will wind up with the path encoded to UTF-8 and URL quoted like so: + +.. code-block:: python + + /La%20Pe%C3%B1a/Qu%C3%A9bec + +.. note:: + + Generating URL-quoted URLs and paths is new as of Pyramid 1.3 (and Pyramid + 1.2 after 1.2.6). Previous versions generated unquoted URLs and paths + (which was broken). + +See the :meth:`~pyramid.request.Request.route_url` and +:meth:`~pyramid.request.Request.route_path` API documentation for more information. .. index:: diff --git a/pyramid/tests/test_urldispatch.py b/pyramid/tests/test_urldispatch.py index be823b045..856bdcb78 100644 --- a/pyramid/tests/test_urldispatch.py +++ b/pyramid/tests/test_urldispatch.py @@ -292,12 +292,6 @@ class TestCompileRoute(unittest.TestCase): self.assertEqual(matcher('foo/baz/biz/buz/bar'), None) self.assertEqual(generator({'baz':1, 'buz':2}), '/foo/1/biz/2/bar') - def test_url_decode_error(self): - from pyramid.exceptions import URLDecodeError - matcher, generator = self._callFUT('/:foo') - self.assertRaises(URLDecodeError, matcher, - native_(b'/\xff\xfe\x8b\x00')) - def test_custom_regex(self): matcher, generator = self._callFUT('foo/{baz}/biz/{buz:[^/\.]+}.{bar}') self.assertEqual(matcher('/foo/baz/biz/buz.bar'), @@ -328,7 +322,8 @@ class TestCompileRoute(unittest.TestCase): self.assertEqual(generator({'buz':2001}), '/2001') def test_custom_regex_with_embedded_squigglies3(self): - matcher, generator = self._callFUT('/{buz:(\d{2}|\d{4})-[a-zA-Z]{3,4}-\d{2}}') + matcher, generator = self._callFUT( + '/{buz:(\d{2}|\d{4})-[a-zA-Z]{3,4}-\d{2}}') self.assertEqual(matcher('/2001-Nov-15'), {'buz':'2001-Nov-15'}) self.assertEqual(matcher('/99-June-10'), {'buz':'99-June-10'}) self.assertEqual(matcher('/2-Nov-15'), None) @@ -337,6 +332,39 @@ class TestCompileRoute(unittest.TestCase): self.assertEqual(generator({'buz':'2001-Nov-15'}), '/2001-Nov-15') self.assertEqual(generator({'buz':'99-June-10'}), '/99-June-10') + def test_pattern_with_high_order_literal(self): + pattern = text_(b'/La Pe\xc3\xb1a/{x}', 'utf-8') + matcher, generator = self._callFUT(pattern) + self.assertEqual(matcher(text_(b'/La Pe\xc3\xb1a/x', 'utf-8')), + {'x':'x'}) + self.assertEqual(generator({'x':'1'}), '/La%20Pe%C3%B1a/1') + + def test_pattern_generate_with_high_order_dynamic(self): + pattern = '/{x}' + _, generator = self._callFUT(pattern) + self.assertEqual( + generator({'x':text_(b'La Pe\xc3\xb1a', 'utf-8')}), + '/La%20Pe%C3%B1a') + + def test_docs_sample_generate(self): + # sample from urldispatch.rst + pattern = text_(b'/La Pe\xc3\xb1a/{city}', 'utf-8') + _, generator = self._callFUT(pattern) + self.assertEqual( + generator({'city':text_(b'Qu\xc3\xa9bec', 'utf-8')}), + '/La%20Pe%C3%B1a/Qu%C3%A9bec') + + def test_generate_with_mixedtype_values(self): + pattern = '/{city}/{state}' + _, generator = self._callFUT(pattern) + result = generator( + {'city': text_(b'Qu\xc3\xa9bec', 'utf-8'), + 'state': b'La Pe\xc3\xb1a'} + ) + self.assertEqual(result, '/Qu%C3%A9bec/La%20Pe%C3%B1a') + # should be a native string + self.assertEqual(type(result), str) + class TestCompileRouteFunctional(unittest.TestCase): def matches(self, pattern, path, expected): from pyramid.urldispatch import _compile_route @@ -368,11 +396,11 @@ class TestCompileRouteFunctional(unittest.TestCase): self.matches('*traverse', '/zzz/abc', {'traverse':('zzz', 'abc')}) self.matches('*traverse', '/zzz/ abc', {'traverse':('zzz', ' abc')}) #'/La%20Pe%C3%B1a' - self.matches('{x}', native_(b'/La Pe\xc3\xb1a'), - {'x':text_(b'La Pe\xf1a')}) + self.matches('{x}', text_(b'/La Pe\xc3\xb1a', 'utf-8'), + {'x':text_(b'La Pe\xc3\xb1a', 'utf-8')}) # '/La%20Pe%C3%B1a/x' - self.matches('*traverse', native_(b'/La Pe\xc3\xb1a/x'), - {'traverse':(text_(b'La Pe\xf1a'), 'x')}) + self.matches('*traverse', text_(b'/La Pe\xc3\xb1a/x'), + {'traverse':(text_(b'La Pe\xc3\xb1a'), 'x')}) self.matches('/foo/{id}.html', '/foo/bar.html', {'id':'bar'}) self.matches('/{num:[0-9]+}/*traverse', '/555/abc/def', {'num':'555', 'traverse':('abc', 'def')}) @@ -394,11 +422,12 @@ class TestCompileRouteFunctional(unittest.TestCase): self.matches('*traverse', '/zzz/abc', {'traverse':('zzz', 'abc')}) self.matches('*traverse', '/zzz/ abc', {'traverse':('zzz', ' abc')}) #'/La%20Pe%C3%B1a' - self.matches(':x', native_(b'/La Pe\xc3\xb1a'), - {'x':text_(b'La Pe\xf1a')}) + # pattern, path, expected + self.matches(':x', text_(b'/La Pe\xc3\xb1a', 'utf-8'), + {'x':text_(b'La Pe\xc3\xb1a', 'utf-8')}) # '/La%20Pe%C3%B1a/x' - self.matches('*traverse', native_(b'/La Pe\xc3\xb1a/x'), - {'traverse':(text_(b'La Pe\xf1a'), 'x')}) + self.matches('*traverse', text_(b'/La Pe\xc3\xb1a/x', 'utf-8'), + {'traverse':(text_(b'La Pe\xc3\xb1a', 'utf-8'), 'x')}) self.matches('/foo/:id.html', '/foo/bar.html', {'id':'bar'}) self.matches('/foo/:id_html', '/foo/bar_html', {'id_html':'bar_html'}) self.matches('zzz/:_', '/zzz/abc', {'_':'abc'}) diff --git a/pyramid/urldispatch.py b/pyramid/urldispatch.py index 73875b675..cb0e57c4d 100644 --- a/pyramid/urldispatch.py +++ b/pyramid/urldispatch.py @@ -7,10 +7,12 @@ from pyramid.interfaces import ( ) from pyramid.compat import ( + PY3, native_, - bytes_, + text_, text_type, string_types, + binary_type, is_nonstr_iter, url_quote, ) @@ -103,72 +105,115 @@ def update_pattern(matchobj): return '{%s}' % name[1:] def _compile_route(route): + # This function really wants to consume Unicode patterns natively, but if + # someone passes us a bytestring, we allow it by converting it to Unicode + # using the ASCII decoding. We decode it using ASCII because we dont + # want to accept bytestrings with high-order characters in them here as + # we have no idea what the encoding represents. + if route.__class__ is not text_type: + route = text_(route, 'ascii') + if old_route_re.search(route) and not route_re.search(route): route = old_route_re.sub(update_pattern, route) if not route.startswith('/'): route = '/' + route - star = None + remainder = None if star_at_end.search(route): - route, star = route.rsplit('*', 1) + route, remainder = route.rsplit('*', 1) pat = route_re.split(route) + + # every element in "pat" will be Unicode (regardless of whether the + # route_re regex pattern is itself Unicode or str) pat.reverse() rpat = [] gen = [] prefix = pat.pop() # invar: always at least one element (route='/'+route) - rpat.append(re.escape(prefix)) - gen.append(prefix) + + # We want to generate URL-encoded URLs, so we url-quote the prefix, being + # careful not to quote any embedded slashes. We have to replace '%' with + # '%%' afterwards, as the strings that go into "gen" are used as string + # replacement targets. + gen.append(quote_path_segment(prefix, safe='/').replace('%', '%%')) # native + rpat.append(re.escape(prefix)) # unicode while pat: - name = pat.pop() + name = pat.pop() # unicode name = name[1:-1] if ':' in name: name, reg = name.split(':') else: reg = '[^/]+' - gen.append('%%(%s)s' % name) - name = '(?P<%s>%s)' % (name, reg) + gen.append('%%(%s)s' % native_(name)) # native + name = '(?P<%s>%s)' % (name, reg) # unicode rpat.append(name) - s = pat.pop() + s = pat.pop() # unicode if s: - rpat.append(re.escape(s)) - gen.append(s) + rpat.append(re.escape(s)) # unicode + # We want to generate URL-encoded URLs, so we url-quote this + # literal in the pattern, being careful not to quote the embedded + # slashes. We have to replace '%' with '%%' afterwards, as the + # strings that go into "gen" are used as string replacement + # targets. What is appended to gen is a native string. + gen.append(quote_path_segment(s, safe='/').replace('%', '%%')) - if star: - rpat.append('(?P<%s>.*?)' % star) - gen.append('%%(%s)s' % star) + if remainder: + rpat.append('(?P<%s>.*?)' % remainder) # unicode + gen.append('%%(%s)s' % native_(remainder)) # native - pattern = ''.join(rpat) + '$' + pattern = ''.join(rpat) + '$' # unicode match = re.compile(pattern).match def matcher(path): + # This function really wants to consume Unicode patterns natively, + # but if someone passes us a bytestring, we allow it by converting it + # to Unicode using the ASCII decoding. We decode it using ASCII + # because we dont want to accept bytestrings with high-order + # characters in them here as we have no idea what the encoding + # represents. + if path.__class__ is not text_type: + path = text_(path, 'ascii') m = match(path) if m is None: - return m + return None d = {} for k, v in m.groupdict().items(): - if k == star: - d[k] = split_path_info(v) + # k and v will be Unicode 2.6.4 and lower doesnt accept unicode + # kwargs as **kw, so we explicitly cast the keys to native + # strings in case someone wants to pass the result as **kw + nk = native_(k, 'ascii') + if k == remainder: + d[nk] = split_path_info(v) else: - d[k] = v + d[nk] = v return d - gen = ''.join(gen) def generator(dict): newdict = {} for k, v in dict.items(): - if v.__class__ is text_type: - v = native_(v, 'utf-8') - if k == star and is_nonstr_iter(v): - v = '/'.join([quote_path_segment(x) for x in v]) - elif k != star: + if PY3: + if v.__class__ is binary_type: + # url_quote below needs a native string, not bytes on Py3 + v = v.decode('utf-8') + else: + if v.__class__ is text_type: + # url_quote below needs bytes, not unicode on Py2 + v = v.encode('utf-8') + if k == remainder and is_nonstr_iter(v): + v = '/'.join([quote_path_segment(x) for x in v]) # native + elif k != remainder: if v.__class__ not in string_types: v = str(v) - v = url_quote(v, safe='') + # v may be bytes (py2) or native string (py3) + v = url_quote(v, safe='') # defaults to utf8 encoding on py3 + + # at this point, the value will be a native string newdict[k] = v - return gen % newdict + + result = gen % newdict # native string result + return result return matcher, generator -- cgit v1.2.3 From 5864016d6432d19fe9588f57d5b1ad95aac9c6c8 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 5 Jan 2012 03:39:21 -0500 Subject: add test for pattern and match value having high-order characters in a traverse= --- pyramid/tests/test_config/test_util.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/pyramid/tests/test_config/test_util.py b/pyramid/tests/test_config/test_util.py index 1180e7e29..0445485cd 100644 --- a/pyramid/tests/test_config/test_util.py +++ b/pyramid/tests/test_config/test_util.py @@ -1,4 +1,5 @@ import unittest +from pyramid.compat import text_ class Test__make_predicates(unittest.TestCase): def _callFUT(self, **kw): @@ -227,6 +228,21 @@ class Test__make_predicates(unittest.TestCase): self.assertEqual(info, {'match': {'a':'a', 'b':'b', 'traverse':('1', 'a', 'b')}}) + def test_traverse_matches_with_highorder_chars(self): + order, predicates, phash = self._callFUT( + traverse=text_('/La Pe\xc3\xb1a/{x}', 'utf-8')) + self.assertEqual(len(predicates), 1) + pred = predicates[0] + info = {'match':{'x':text_(b'Qu\xc3\xa9bec', 'utf-8')}} + request = DummyRequest() + result = pred(info, request) + self.assertEqual(result, True) + self.assertEqual( + info['match']['traverse'], + (text_(b'La Pe\xc3\xb1a', 'utf-8'), + text_(b'Qu\xc3\xa9bec', 'utf-8')) + ) + def test_custom_predicates_can_affect_traversal(self): def custom(info, request): m = info['match'] -- cgit v1.2.3 From 64497edc64a153815c54b254bf64a46ac21a6ac7 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 5 Jan 2012 03:46:18 -0600 Subject: Fixed a couple docs bugs reported by davidfung. --- docs/narr/configuration.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/narr/configuration.rst b/docs/narr/configuration.rst index 37122e382..cd3dab099 100644 --- a/docs/narr/configuration.rst +++ b/docs/narr/configuration.rst @@ -46,6 +46,7 @@ applications, configured imperatively: if __name__ == '__main__': config = Configurator() config.add_view(hello_world) + app = config.make_wsgi_app() server = make_server('0.0.0.0', 8080, app) server.serve_forever() @@ -106,6 +107,7 @@ in a package and its subpackages. For example: :linenos: from wsgiref.simple_server import make_server + from pyramid.config import Configurator from pyramid.response import Response from pyramid.view import view_config -- cgit v1.2.3 From 78535833058ba2c01f44eef311f8bacdb1ee4bc7 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 5 Jan 2012 05:54:54 -0500 Subject: - String values passed to ``route_url`` or ``route_path`` that are meant to replace "remainder" matches will now be URL-quoted except for embedded slashes. For example:: config.add_route('remain', '/foo*remainder') request.route_path('remain', remainder='abc / def') # -> '/foo/abc%20/%20def' Previously string values passed as remainder replacements were tacked on untouched, without any URL-quoting. But this doesn't really work logically if the value passed is Unicode (raw unicode cannot be placed in a URL or in a path) and it is inconsistent with the rest of the URL generation machinery if the value is a string (it won't be quoted unless by the caller). Some folks will have been relying on the older behavior to tack on query string elements and anchor portions of the URL; sorry, you'll need to change your code to use the ``_query`` and/or ``_anchor`` arguments to ``route_path`` or ``route_url`` to do this now. - If you pass a bytestring that contains non-ASCII characters to ``add_route`` as a pattern, it will now fail at startup time. Use Unicode instead. --- pyramid/tests/test_urldispatch.py | 24 ++++++++++++++++++++++++ pyramid/urldispatch.py | 23 ++++++++++++++++++----- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/pyramid/tests/test_urldispatch.py b/pyramid/tests/test_urldispatch.py index 856bdcb78..286cc1573 100644 --- a/pyramid/tests/test_urldispatch.py +++ b/pyramid/tests/test_urldispatch.py @@ -365,6 +365,30 @@ class TestCompileRoute(unittest.TestCase): # should be a native string self.assertEqual(type(result), str) + def test_highorder_pattern_utf8(self): + pattern = b'/La Pe\xc3\xb1a/{city}' + self.assertRaises(ValueError, self._callFUT, pattern) + + def test_generate_with_string_remainder_and_unicode_replacement(self): + pattern = text_(b'/abc*remainder', 'utf-8') + _, generator = self._callFUT(pattern) + result = generator( + {'remainder': text_(b'/Qu\xc3\xa9bec/La Pe\xc3\xb1a', 'utf-8')} + ) + self.assertEqual(result, '/abc/Qu%C3%A9bec/La%20Pe%C3%B1a') + # should be a native string + self.assertEqual(type(result), str) + + def test_generate_with_string_remainder_and_nonstring_replacement(self): + pattern = text_(b'/abc/*remainder', 'utf-8') + _, generator = self._callFUT(pattern) + result = generator( + {'remainder': None} + ) + self.assertEqual(result, '/abc/None') + # should be a native string + self.assertEqual(type(result), str) + class TestCompileRouteFunctional(unittest.TestCase): def matches(self, pattern, path, expected): from pyramid.urldispatch import _compile_route diff --git a/pyramid/urldispatch.py b/pyramid/urldispatch.py index cb0e57c4d..fcd6c55c9 100644 --- a/pyramid/urldispatch.py +++ b/pyramid/urldispatch.py @@ -111,7 +111,13 @@ def _compile_route(route): # want to accept bytestrings with high-order characters in them here as # we have no idea what the encoding represents. if route.__class__ is not text_type: - route = text_(route, 'ascii') + try: + route = text_(route, 'ascii') + except UnicodeDecodeError: + raise ValueError( + 'The pattern value passed to add_route must be ' + 'either a Unicode string or a plain string without ' + 'any non-ASCII characters (you provided %r).' % route) if old_route_re.search(route) and not route_re.search(route): route = old_route_re.sub(update_pattern, route) @@ -202,13 +208,20 @@ def _compile_route(route): if v.__class__ is text_type: # url_quote below needs bytes, not unicode on Py2 v = v.encode('utf-8') - if k == remainder and is_nonstr_iter(v): - v = '/'.join([quote_path_segment(x) for x in v]) # native - elif k != remainder: + + if k == remainder: + # a stararg argument + if is_nonstr_iter(v): + v = '/'.join([quote_path_segment(x) for x in v]) # native + else: + if v.__class__ not in string_types: + v = str(v) + v = quote_path_segment(v, safe='/') + else: if v.__class__ not in string_types: v = str(v) # v may be bytes (py2) or native string (py3) - v = url_quote(v, safe='') # defaults to utf8 encoding on py3 + v = quote_path_segment(v) # at this point, the value will be a native string newdict[k] = v -- cgit v1.2.3 From e3dadfd5865f58a2f816e558e8dbc636dd037197 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 5 Jan 2012 05:55:31 -0500 Subject: bring docs up to date with code --- docs/narr/urldispatch.rst | 59 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 44 insertions(+), 15 deletions(-) diff --git a/docs/narr/urldispatch.rst b/docs/narr/urldispatch.rst index 7e485f8ae..6d9dfdd92 100644 --- a/docs/narr/urldispatch.rst +++ b/docs/narr/urldispatch.rst @@ -267,11 +267,12 @@ pattern like this: /La Pe\xc3\xb1a/{x} -But that probably won't match as you expect it to. You'll want to use a -Unicode value as the pattern instead rather than raw bytestring escapes. You -can use a high-order Unicode value as the pattern by using `Python source -file encoding `_ plus the "real" -character in the Unicode pattern in the source, like so: +But this will either cause an error at startup time or it won't match +properly. You'll want to use a Unicode value as the pattern instead rather +than raw bytestring escapes. You can use a high-order Unicode value as the +pattern by using `Python source file encoding +`_ plus the "real" character in the +Unicode pattern in the source, like so: .. code-block:: text @@ -664,7 +665,7 @@ based on route patterns. For example, if you've configured a route with the This would return something like the string ``http://example.com/1/2/3`` (at least if the current protocol and hostname implied ``http://example.com``). -To get only the *path* of a route, use the +To generate only the *path* portion of a URL from a route, use the :meth:`pyramid.request.Request.route_path` API instead of :meth:`~pyramid.request.Request.route_url`. @@ -674,8 +675,14 @@ To get only the *path* of a route, use the This will return the string ``/1/2/3`` rather than a full URL. +Replacement values passed to ``route_url`` or ``route_path`` must be Unicode +or bytestrings encoded in UTF-8. One exception to this rule exists: if +you're trying to replace a "remainder" match value (a ``*stararg`` +replacement value), the value may be a tuple containing Unicode strings or +UTF-8 strings. + Note that URLs and paths generated by ``route_path`` and ``route_url`` are -always URL-quoted string types (which contain no non-ASCII characters). +always URL-quoted string types (they contain no non-ASCII characters). Therefore, if you've added a route like so: .. code-block:: python @@ -690,19 +697,41 @@ And you later generate a URL using ``route_path`` or ``route_url`` like so: You will wind up with the path encoded to UTF-8 and URL quoted like so: -.. code-block:: python +.. code-block:: text /La%20Pe%C3%B1a/Qu%C3%A9bec -.. note:: +If you have a ``*stararg`` remainder dynamic part of your route pattern: + +.. code-block:: python + + config.add_route('abc', 'a/b/c/*foo') + +And you later generate a URL using ``route_path`` or ``route_url`` using a +*string* as the replacement value: + +.. code-block:: python + + url = request.route_path('abc', foo=u'Québec/biz') - Generating URL-quoted URLs and paths is new as of Pyramid 1.3 (and Pyramid - 1.2 after 1.2.6). Previous versions generated unquoted URLs and paths - (which was broken). +The value you pass will be URL-quoted except for embedded slashes in the +result: + +.. code-block:: text + + /a/b/c/Qu%C3%A9bec/biz + +You can get a similar result by passing a tuple composed of path elements: + +.. code-block:: python + + url = request.route_path('abc', foo=(u'Québec', u'biz')) + +Each value in the tuple will be url-quoted and joined by slashes in this case: + +.. code-block:: text -See the :meth:`~pyramid.request.Request.route_url` and -:meth:`~pyramid.request.Request.route_path` API documentation for more -information. + /a/b/c/Qu%C3%A9bec/biz .. index:: single: static routes -- cgit v1.2.3 From 97bd71684fe70a69529a7991729894c16ab998dd Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 5 Jan 2012 05:55:57 -0500 Subject: bring docs up to date with code --- pyramid/url.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/url.py b/pyramid/url.py index afb602d3a..e6a508c17 100644 --- a/pyramid/url.py +++ b/pyramid/url.py @@ -67,7 +67,7 @@ class URLMethodsMixin(object): encoded to UTF-8. The resulting strings are joined with slashes and rendered into the URL. If a string is passed as a ``*remainder`` replacement value, it is tacked on to the URL - untouched. + after being URL-quoted-except-for-embedded-slashes. If a keyword argument ``_query`` is present, it will be used to compose a query string that will be tacked on to the end of the -- cgit v1.2.3 From ad9807016fd28b7da424174fdb6ed9b93641f58f Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 5 Jan 2012 06:09:08 -0500 Subject: garden --- CHANGES.txt | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 07c0b564d..93be3eab3 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -27,6 +27,51 @@ Bug Fixes now been fixed and the code matches the documentation. See also https://github.com/Pylons/pyramid/issues/386 +- Literal portions of route patterns were not URL-quoted when ``route_url`` + or ``route_path`` was used. + +- The result of ``route_path`` or ``route_url`` might have been ``unicode`` + or ``str`` depending on the input. It is now guaranteed to always be + ``str``. + +- URL matching when the pattern contained non-ASCII characters in literal + parts was indeterminate. Now the pattern supplied to ``add_route`` is + assumed to be either: a ``unicode`` value, or a ``str`` value that contains + only ASCII characters. If you now want to match the path info from a URL + that contains high order characters, you can pass the Unicode + representation of the decoded path portion in the pattern. + +- When using a ``traverse=`` route predicate, traversal would fail with a + URLDecodeError if there were any high-order characters in the traversal + pattern or in the matched dynamic segments. + +Backwards Incompatibilities +--------------------------- + +- String values passed to ``route_url`` or ``route_path`` that are meant to + replace "remainder" matches will now be URL-quoted except for embedded + slashes. For example:: + + config.add_route('remain', '/foo*remainder') + request.route_path('remain', remainder='abc / def') + # -> '/foo/abc%20/%20def' + + Previously string values passed as remainder replacements were tacked on + untouched, without any URL-quoting. But this doesn't really work logically + if the value passed is Unicode (raw unicode cannot be placed in a URL or in + a path) and it is inconsistent with the rest of the URL generation + machinery if the value is a string (it won't be quoted unless by the + caller). + + Some folks will have been relying on the older behavior to tack on query + string elements and anchor portions of the URL; sorry, you'll need to + change your code to use the ``_query`` and/or ``_anchor`` arguments to + ``route_path`` or ``route_url`` to do this now. + +- If you pass a bytestring that contains non-ASCII characters to + ``add_route`` as a pattern, it will now fail at startup time. Use Unicode + instead. + 1.3a3 (2011-12-21) ================== -- cgit v1.2.3 From 92dcb5f1a52d46fec7c043a1b5b4158f3d013c34 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 5 Jan 2012 06:32:38 -0500 Subject: - Using a dynamic segment named ``traverse`` in a route pattern like this:: config.add_route('trav_route', 'traversal/{traverse:.*}') Would cause a ``UnicodeDecodeError`` when the route was matched and the matched portion of the URL contained any high-order characters. See also https://github.com/Pylons/pyramid/issues/385 . --- CHANGES.txt | 8 ++++++ pyramid/tests/test_traversal.py | 60 ++++++++++++++++++++++++++++++++++++++--- pyramid/traversal.py | 6 ++--- 3 files changed, 67 insertions(+), 7 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 93be3eab3..6116da229 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -45,6 +45,14 @@ Bug Fixes URLDecodeError if there were any high-order characters in the traversal pattern or in the matched dynamic segments. +- Using a dynamic segment named ``traverse`` in a route pattern like this:: + + config.add_route('trav_route', 'traversal/{traverse:.*}') + + Would cause a ``UnicodeDecodeError`` when the route was matched and the + matched portion of the URL contained any high-order characters. See also + https://github.com/Pylons/pyramid/issues/385 . + Backwards Incompatibilities --------------------------- diff --git a/pyramid/tests/test_traversal.py b/pyramid/tests/test_traversal.py index 72192b23b..683679253 100644 --- a/pyramid/tests/test_traversal.py +++ b/pyramid/tests/test_traversal.py @@ -1,10 +1,13 @@ import unittest from pyramid.testing import cleanUp -from pyramid.compat import text_ -from pyramid.compat import native_ -from pyramid.compat import text_type -from pyramid.compat import url_quote +from pyramid.compat import ( + text_, + native_, + text_type, + url_quote, + PY3, + ) class TraversalPathTests(unittest.TestCase): def _callFUT(self, path): @@ -131,6 +134,28 @@ class ResourceTreeTraverserTests(unittest.TestCase): self.assertEqual(result['virtual_root'], policy.root) self.assertEqual(result['virtual_root_path'], ()) + def test_call_with_pathinfo_highorder(self): + foo = DummyContext(None, text_(b'Qu\xc3\xa9bec', 'utf-8')) + root = DummyContext(foo, 'root') + policy = self._makeOne(root) + if PY3: + path_info = b'/Qu\xc3\xa9bec'.encode('latin-1') + else: + path_info = b'/Qu\xc3\xa9bec' + environ = self._getEnviron(PATH_INFO=path_info) + request = DummyRequest(environ) + result = policy(request) + self.assertEqual(result['context'], foo) + self.assertEqual(result['view_name'], '') + self.assertEqual(result['subpath'], ()) + self.assertEqual( + result['traversed'], + (text_(b'Qu\xc3\xa9bec', 'utf-8'),) + ) + self.assertEqual(result['root'], policy.root) + self.assertEqual(result['virtual_root'], policy.root) + self.assertEqual(result['virtual_root_path'], ()) + def test_call_pathel_with_no_getitem(self): policy = self._makeOne(None) environ = self._getEnviron(PATH_INFO='/foo/bar') @@ -295,6 +320,33 @@ class ResourceTreeTraverserTests(unittest.TestCase): self.assertEqual(result['virtual_root'], policy.root) self.assertEqual(result['virtual_root_path'], ()) + def test_call_with_vh_root_highorder(self): + bar = DummyContext(None, 'bar') + foo = DummyContext(bar, text_(b'Qu\xc3\xa9bec', 'utf-8')) + root = DummyContext(foo, 'root') + policy = self._makeOne(root) + if PY3: + vhm_root = b'/Qu\xc3\xa9bec'.encode('latin-1') + else: + vhm_root = b'/Qu\xc3\xa9bec' + environ = self._getEnviron(HTTP_X_VHM_ROOT=vhm_root, + PATH_INFO='/bar') + request = DummyRequest(environ) + result = policy(request) + self.assertEqual(result['context'], bar) + self.assertEqual(result['view_name'], '') + self.assertEqual(result['subpath'], ()) + self.assertEqual( + result['traversed'], + (text_(b'Qu\xc3\xa9bec', 'utf-8'), u'bar') + ) + self.assertEqual(result['root'], policy.root) + self.assertEqual(result['virtual_root'], foo) + self.assertEqual( + result['virtual_root_path'], + (text_(b'Qu\xc3\xa9bec', 'utf-8'),) + ) + def test_non_utf8_path_segment_unicode_path_segments_fails(self): from pyramid.exceptions import URLDecodeError foo = DummyContext() diff --git a/pyramid/traversal.py b/pyramid/traversal.py index ffc40fa60..84dcd33ec 100644 --- a/pyramid/traversal.py +++ b/pyramid/traversal.py @@ -667,8 +667,8 @@ class ResourceTreeTraverser(object): if VH_ROOT_KEY in environ: # HTTP_X_VHM_ROOT vroot_path = decode_path_info(environ[VH_ROOT_KEY]) - vroot_tuple = traversal_path_info(vroot_path) - vpath = vroot_path + path + vroot_tuple = split_path_info(vroot_path) + vpath = vroot_path + path # both will (must) be unicode or asciistr vroot_idx = len(vroot_tuple) -1 else: vroot_tuple = () @@ -688,7 +688,7 @@ class ResourceTreeTraverser(object): # and this hurts readability; apologies i = 0 view_selector = self.VIEW_SELECTOR - vpath_tuple = traversal_path_info(vpath) + vpath_tuple = split_path_info(vpath) for segment in vpath_tuple: if segment[:2] == view_selector: return {'context':ob, -- cgit v1.2.3 From 403dc9f1ca3627c9456a056b7a5f6429c0c218e5 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 5 Jan 2012 07:04:59 -0500 Subject: fix test failures on python 3 --- pyramid/tests/test_config/test_util.py | 2 +- pyramid/tests/test_traversal.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyramid/tests/test_config/test_util.py b/pyramid/tests/test_config/test_util.py index 0445485cd..ebf308929 100644 --- a/pyramid/tests/test_config/test_util.py +++ b/pyramid/tests/test_config/test_util.py @@ -230,7 +230,7 @@ class Test__make_predicates(unittest.TestCase): def test_traverse_matches_with_highorder_chars(self): order, predicates, phash = self._callFUT( - traverse=text_('/La Pe\xc3\xb1a/{x}', 'utf-8')) + traverse=text_(b'/La Pe\xc3\xb1a/{x}', 'utf-8')) self.assertEqual(len(predicates), 1) pred = predicates[0] info = {'match':{'x':text_(b'Qu\xc3\xa9bec', 'utf-8')}} diff --git a/pyramid/tests/test_traversal.py b/pyramid/tests/test_traversal.py index 683679253..c9685b124 100644 --- a/pyramid/tests/test_traversal.py +++ b/pyramid/tests/test_traversal.py @@ -139,7 +139,7 @@ class ResourceTreeTraverserTests(unittest.TestCase): root = DummyContext(foo, 'root') policy = self._makeOne(root) if PY3: - path_info = b'/Qu\xc3\xa9bec'.encode('latin-1') + path_info = b'/Qu\xc3\xa9bec'.decode('latin-1') else: path_info = b'/Qu\xc3\xa9bec' environ = self._getEnviron(PATH_INFO=path_info) @@ -326,7 +326,7 @@ class ResourceTreeTraverserTests(unittest.TestCase): root = DummyContext(foo, 'root') policy = self._makeOne(root) if PY3: - vhm_root = b'/Qu\xc3\xa9bec'.encode('latin-1') + vhm_root = b'/Qu\xc3\xa9bec'.decode('latin-1') else: vhm_root = b'/Qu\xc3\xa9bec' environ = self._getEnviron(HTTP_X_VHM_ROOT=vhm_root, @@ -338,7 +338,7 @@ class ResourceTreeTraverserTests(unittest.TestCase): self.assertEqual(result['subpath'], ()) self.assertEqual( result['traversed'], - (text_(b'Qu\xc3\xa9bec', 'utf-8'), u'bar') + (text_(b'Qu\xc3\xa9bec', 'utf-8'), text_('bar')) ) self.assertEqual(result['root'], policy.root) self.assertEqual(result['virtual_root'], foo) -- cgit v1.2.3 From c52c927e930f44c25912c8e5d444489e95ea9d31 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 5 Jan 2012 07:16:58 -0500 Subject: garden --- CHANGES.txt | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 6116da229..8aafeef74 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -28,7 +28,7 @@ Bug Fixes https://github.com/Pylons/pyramid/issues/386 - Literal portions of route patterns were not URL-quoted when ``route_url`` - or ``route_path`` was used. + or ``route_path`` was used to generate a URL or path. - The result of ``route_path`` or ``route_url`` might have been ``unicode`` or ``str`` depending on the input. It is now guaranteed to always be @@ -50,9 +50,16 @@ Bug Fixes config.add_route('trav_route', 'traversal/{traverse:.*}') Would cause a ``UnicodeDecodeError`` when the route was matched and the - matched portion of the URL contained any high-order characters. See also + matched portion of the URL contained any high-order characters. See https://github.com/Pylons/pyramid/issues/385 . +- When using a ``*traverse`` stararg in a route pattern, a URL that matched + that possessed a ``@@`` in its name (signifying a view name) would be + inappropriately quoted by the traversal machinery during traversal, + resulting in the view not being found properly. See + https://github.com/Pylons/pyramid/issues/382 and + https://github.com/Pylons/pyramid/issues/375 . + Backwards Incompatibilities --------------------------- -- cgit v1.2.3 From b78b082e3778bb0e0a7c30d4cf711764407e0dcf Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 5 Jan 2012 08:34:23 -0500 Subject: coverage --- pyramid/compat.py | 6 +++--- pyramid/tests/test_traversal.py | 4 ++-- pyramid/tests/test_urldispatch.py | 6 ++++++ pyramid/urldispatch.py | 3 +-- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/pyramid/compat.py b/pyramid/compat.py index 7376278ac..948a1c3be 100644 --- a/pyramid/compat.py +++ b/pyramid/compat.py @@ -41,7 +41,7 @@ def text_(s, encoding='latin-1', errors='strict'): 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): + if isinstance(s, text_type): # pragma: no cover return s.encode(encoding, errors) return s @@ -105,10 +105,10 @@ 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'): + 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'): + def url_unquote_native(v, encoding='utf-8', errors='replace'): # pragma: no cover return native_(url_unquote_text(v, encoding, errors)) diff --git a/pyramid/tests/test_traversal.py b/pyramid/tests/test_traversal.py index c9685b124..1f9971ca5 100644 --- a/pyramid/tests/test_traversal.py +++ b/pyramid/tests/test_traversal.py @@ -138,7 +138,7 @@ class ResourceTreeTraverserTests(unittest.TestCase): foo = DummyContext(None, text_(b'Qu\xc3\xa9bec', 'utf-8')) root = DummyContext(foo, 'root') policy = self._makeOne(root) - if PY3: + if PY3: # pragma: no cover path_info = b'/Qu\xc3\xa9bec'.decode('latin-1') else: path_info = b'/Qu\xc3\xa9bec' @@ -325,7 +325,7 @@ class ResourceTreeTraverserTests(unittest.TestCase): foo = DummyContext(bar, text_(b'Qu\xc3\xa9bec', 'utf-8')) root = DummyContext(foo, 'root') policy = self._makeOne(root) - if PY3: + if PY3: # pragma: no cover vhm_root = b'/Qu\xc3\xa9bec'.decode('latin-1') else: vhm_root = b'/Qu\xc3\xa9bec' diff --git a/pyramid/tests/test_urldispatch.py b/pyramid/tests/test_urldispatch.py index 286cc1573..370f072ff 100644 --- a/pyramid/tests/test_urldispatch.py +++ b/pyramid/tests/test_urldispatch.py @@ -115,6 +115,12 @@ class RoutesMapperTests(unittest.TestCase): self.assertEqual(mapper.routelist[0].pattern, 'archives/:action/:article2') + def test___call__pathinfo_cant_be_decoded(self): + from pyramid.exceptions import URLDecodeError + mapper = self._makeOne() + request = self._getRequest(PATH_INFO=b'\xff\xfe\xe6\x00') + self.assertRaises(URLDecodeError, mapper, request) + def test___call__route_matches(self): mapper = self._makeOne() mapper.connect('foo', 'archives/:action/:article') diff --git a/pyramid/urldispatch.py b/pyramid/urldispatch.py index fcd6c55c9..bd1da8f71 100644 --- a/pyramid/urldispatch.py +++ b/pyramid/urldispatch.py @@ -14,7 +14,6 @@ from pyramid.compat import ( string_types, binary_type, is_nonstr_iter, - url_quote, ) from pyramid.exceptions import URLDecodeError @@ -200,7 +199,7 @@ def _compile_route(route): def generator(dict): newdict = {} for k, v in dict.items(): - if PY3: + if PY3: # pragma: no cover if v.__class__ is binary_type: # url_quote below needs a native string, not bytes on Py3 v = v.decode('utf-8') -- cgit v1.2.3 From 962816fb573547e59a4c376c25e11b0597eee9d6 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 5 Jan 2012 08:44:20 -0500 Subject: whatsnew --- docs/whatsnew-1.3.rst | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/whatsnew-1.3.rst b/docs/whatsnew-1.3.rst index e8d549e60..eb8617ff1 100644 --- a/docs/whatsnew-1.3.rst +++ b/docs/whatsnew-1.3.rst @@ -271,6 +271,30 @@ Backwards Incompatibilities and upgrade Pyramid itself "in-place"; it may simply break instead (particularly if you use ZCML's ``includeOverrides`` directive). +- String values passed to ``route_url`` or ``route_path`` that are meant to + replace "remainder" matches will now be URL-quoted except for embedded + slashes. For example:: + + config.add_route('remain', '/foo*remainder') + request.route_path('remain', remainder='abc / def') + # -> '/foo/abc%20/%20def' + + Previously string values passed as remainder replacements were tacked on + untouched, without any URL-quoting. But this doesn't really work logically + if the value passed is Unicode (raw unicode cannot be placed in a URL or in + a path) and it is inconsistent with the rest of the URL generation + machinery if the value is a string (it won't be quoted unless by the + caller). + + Some folks will have been relying on the older behavior to tack on query + string elements and anchor portions of the URL; sorry, you'll need to + change your code to use the ``_query`` and/or ``_anchor`` arguments to + ``route_path`` or ``route_url`` to do this now. + +- If you pass a bytestring that contains non-ASCII characters to + ``add_route`` as a pattern, it will now fail at startup time. Use Unicode + instead. + Documentation Enhancements -------------------------- -- cgit v1.2.3 From 933944c12cf09c2818ebf9f82e9c0c3efd5f1d41 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 5 Jan 2012 09:07:33 -0500 Subject: fix test on py3 --- pyramid/tests/test_urldispatch.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/pyramid/tests/test_urldispatch.py b/pyramid/tests/test_urldispatch.py index 370f072ff..d7dd2f44a 100644 --- a/pyramid/tests/test_urldispatch.py +++ b/pyramid/tests/test_urldispatch.py @@ -1,7 +1,9 @@ import unittest from pyramid import testing -from pyramid.compat import text_ -from pyramid.compat import native_ +from pyramid.compat import ( + text_, + PY3, + ) class TestRoute(unittest.TestCase): def _getTargetClass(self): @@ -118,7 +120,11 @@ class RoutesMapperTests(unittest.TestCase): def test___call__pathinfo_cant_be_decoded(self): from pyramid.exceptions import URLDecodeError mapper = self._makeOne() - request = self._getRequest(PATH_INFO=b'\xff\xfe\xe6\x00') + if PY3: + path_info = b'\xff\xfe\xe6\x00'.decode('latin-1') + else: + path_info = b'\xff\xfe\xe6\x00' + request = self._getRequest(PATH_INFO=path_info) self.assertRaises(URLDecodeError, mapper, request) def test___call__route_matches(self): -- cgit v1.2.3 From 2bf3f5c62b374bd1fa4cc87166f3073a1101ea12 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 5 Jan 2012 09:10:03 -0500 Subject: coverage --- pyramid/tests/test_urldispatch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/tests/test_urldispatch.py b/pyramid/tests/test_urldispatch.py index d7dd2f44a..e15242f75 100644 --- a/pyramid/tests/test_urldispatch.py +++ b/pyramid/tests/test_urldispatch.py @@ -120,7 +120,7 @@ class RoutesMapperTests(unittest.TestCase): def test___call__pathinfo_cant_be_decoded(self): from pyramid.exceptions import URLDecodeError mapper = self._makeOne() - if PY3: + if PY3: # pragma: no cover path_info = b'\xff\xfe\xe6\x00'.decode('latin-1') else: path_info = b'\xff\xfe\xe6\x00' -- cgit v1.2.3 From 683941663033bbac588b9ac8f1ff28eeefc511c9 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 5 Jan 2012 09:21:55 -0500 Subject: prep for 1.3a4 --- CHANGES.txt | 4 ++-- docs/conf.py | 2 +- setup.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 8aafeef74..339317beb 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,5 +1,5 @@ -Unreleased -========== +1.3a4 (2012-01-05) +================== Features -------- diff --git a/docs/conf.py b/docs/conf.py index 39006f1bb..4e0ffff55 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -80,7 +80,7 @@ copyright = '%s, Agendaless Consulting' % datetime.datetime.now().year # other places throughout the built documents. # # The short X.Y version. -version = '1.3a3' +version = '1.3a4' # The full version, including alpha/beta/rc tags. release = version diff --git a/setup.py b/setup.py index 78963289a..378ef1b38 100644 --- a/setup.py +++ b/setup.py @@ -56,7 +56,7 @@ if not PY3: ]) setup(name='pyramid', - version='1.3a3', + version='1.3a4', description=('The Pyramid web application development framework, a ' 'Pylons project'), long_description=README + '\n\n' + CHANGES, -- cgit v1.2.3 From 4c29ef5be6e7bb1418ce896a92f4c8bd6d9a3111 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 6 Jan 2012 02:16:03 -0500 Subject: - The ``pyramid.view.view_defaults`` decorator did not work properly when more than one view relied on the defaults being different for configuration conflict resolution. See https://github.com/Pylons/pyramid/issues/394. Closes #394. --- CHANGES.txt | 10 ++++++ pyramid/tests/test_config/test_views.py | 38 ++++++++++++++++++++++- pyramid/tests/test_view.py | 40 ++++++++++++------------ pyramid/view.py | 55 ++++++++++++++++----------------- 4 files changed, 95 insertions(+), 48 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 339317beb..d5ef9dd29 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,13 @@ +Next release +============ + +Bug Fixes +--------- + +- The ``pyramid.view.view_defaults`` decorator did not work properly when + more than one view relied on the defaults being different for configuration + conflict resolution. See https://github.com/Pylons/pyramid/issues/394. + 1.3a4 (2012-01-05) ================== diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index bcf1faa08..6563fe772 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -1433,11 +1433,47 @@ class TestViewsConfigurationMixin(unittest.TestCase): directlyProvides(context, IDummy) request = self._makeRequest(config) self.assertEqual(wrapper(context, request), 'OK') - context = DummyContext() request = self._makeRequest(config) self.assertRaises(PredicateMismatch, wrapper, context, request) + def test_add_view_with_view_config_and_view_defaults_doesnt_conflict(self): + from pyramid.renderers import null_renderer + class view(object): + __view_defaults__ = { + 'containment':'pyramid.tests.test_config.IDummy' + } + class view2(object): + __view_defaults__ = { + 'containment':'pyramid.tests.test_config.IFactory' + } + config = self._makeOne(autocommit=False) + config.add_view( + view=view, + renderer=null_renderer) + config.add_view( + view=view2, + renderer=null_renderer) + config.commit() # does not raise + + def test_add_view_with_view_config_and_view_defaults_conflicts(self): + from pyramid.renderers import null_renderer + class view(object): + __view_defaults__ = { + 'containment':'pyramid.tests.test_config.IDummy' + } + class view2(object): + __view_defaults__ = { + 'containment':'pyramid.tests.test_config.IDummy' + } + config = self._makeOne(autocommit=False) + config.add_view( + view=view, + renderer=null_renderer) + config.add_view( + view=view2, + renderer=null_renderer) + self.assertRaises(ConfigurationConflictError, config.commit) def test_derive_view_function(self): from pyramid.renderers import null_renderer diff --git a/pyramid/tests/test_view.py b/pyramid/tests/test_view.py index 0d00e65c6..03a111828 100644 --- a/pyramid/tests/test_view.py +++ b/pyramid/tests/test_view.py @@ -260,10 +260,7 @@ class TestViewConfigDecorator(unittest.TestCase): def test_create_defaults(self): decorator = self._makeOne() - self.assertEqual(decorator.name, '') - self.assertEqual(decorator.request_type, None) - self.assertEqual(decorator.context, None) - self.assertEqual(decorator.permission, None) + self.assertEqual(decorator.__dict__, {}) def test_create_context_trumps_for(self): decorator = self._makeOne(context='123', for_='456') @@ -274,9 +271,11 @@ class TestViewConfigDecorator(unittest.TestCase): self.assertEqual(decorator.context, '456') def test_create_nondefaults(self): - decorator = self._makeOne(name=None, request_type=None, for_=None, - permission='foo', mapper='mapper', - decorator='decorator', match_param='match_param') + decorator = self._makeOne( + name=None, request_type=None, for_=None, + permission='foo', mapper='mapper', + decorator='decorator', match_param='match_param' + ) self.assertEqual(decorator.name, None) self.assertEqual(decorator.request_type, None) self.assertEqual(decorator.context, None) @@ -295,9 +294,11 @@ class TestViewConfigDecorator(unittest.TestCase): config = call_venusian(venusian) settings = config.settings self.assertEqual(len(settings), 1) - self.assertEqual(settings[0]['permission'], None) - self.assertEqual(settings[0]['context'], None) - self.assertEqual(settings[0]['request_type'], None) + self.assertEqual(len(settings), 1) + self.assertEqual(len(settings[0]), 3) + self.assertEqual(settings[0]['venusian'], venusian) + self.assertEqual(settings[0]['view'], None) # comes from call_venusian + self.assertEqual(settings[0]['_info'], 'codeinfo') def test_call_class(self): decorator = self._makeOne() @@ -310,10 +311,11 @@ class TestViewConfigDecorator(unittest.TestCase): config = call_venusian(venusian) settings = config.settings self.assertEqual(len(settings), 1) - self.assertEqual(settings[0]['permission'], None) - self.assertEqual(settings[0]['context'], None) - self.assertEqual(settings[0]['request_type'], None) + self.assertEqual(len(settings[0]), 4) + self.assertEqual(settings[0]['venusian'], venusian) + self.assertEqual(settings[0]['view'], None) # comes from call_venusian self.assertEqual(settings[0]['attr'], 'foo') + self.assertEqual(settings[0]['_info'], 'codeinfo') def test_call_class_attr_already_set(self): decorator = self._makeOne(attr='abc') @@ -326,10 +328,11 @@ class TestViewConfigDecorator(unittest.TestCase): config = call_venusian(venusian) settings = config.settings self.assertEqual(len(settings), 1) - self.assertEqual(settings[0]['permission'], None) - self.assertEqual(settings[0]['context'], None) - self.assertEqual(settings[0]['request_type'], None) + self.assertEqual(len(settings[0]), 4) + self.assertEqual(settings[0]['venusian'], venusian) + self.assertEqual(settings[0]['view'], None) # comes from call_venusian self.assertEqual(settings[0]['attr'], 'abc') + self.assertEqual(settings[0]['_info'], 'codeinfo') def test_stacking(self): decorator1 = self._makeOne(name='1') @@ -593,7 +596,7 @@ class Test_view_defaults(unittest.TestCase): @view_defaults(route_name='ghi') class Bar(Foo): pass self.assertEqual(Bar.__view_defaults__['route_name'],'ghi') - self.assertEqual(Bar.__view_defaults__['renderer'], None) + self.assertFalse('renderer' in Bar.__view_defaults__) def test_it_inheritance_overriden_empty(self): from pyramid.view import view_defaults @@ -601,8 +604,7 @@ class Test_view_defaults(unittest.TestCase): class Foo(object): pass @view_defaults() class Bar(Foo): pass - self.assertEqual(Bar.__view_defaults__['route_name'], None) - self.assertEqual(Bar.__view_defaults__['renderer'], None) + self.assertEqual(Bar.__view_defaults__, {}) class ExceptionResponse(Exception): status = '404 Not Found' diff --git a/pyramid/view.py b/pyramid/view.py index eae56a661..a68f9ad8a 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -133,6 +133,15 @@ def render_view(context, request, name='', secure=True): return None return ''.join(iterable) +class _default(object): + def __nonzero__(self): + return False + __bool__ = __nonzero__ + def __repr__(self): # pragma: no cover + return '(default)' + +default = _default() + class view_config(object): """ A function, class or method :term:`decorator` which allows a developer to create view registrations nearer to a :term:`view @@ -168,39 +177,29 @@ class view_config(object): and ``match_param``. The meanings of these arguments are the same as the arguments passed to - :meth:`pyramid.config.Configurator.add_view`. + :meth:`pyramid.config.Configurator.add_view`. If any argument is left + out, its default will be the equivalent ``add_view`` default. See :ref:`mapping_views_using_a_decorator_section` for details about using :class:`view_config`. """ venusian = venusian # for testing injection - def __init__(self, name='', request_type=None, for_=None, permission=None, - route_name=None, request_method=None, request_param=None, - containment=None, attr=None, renderer=None, wrapper=None, - xhr=False, accept=None, header=None, path_info=None, - custom_predicates=(), context=None, decorator=None, - mapper=None, http_cache=None, match_param=None): - self.name = name - self.request_type = request_type - self.context = context or for_ - self.permission = permission - self.route_name = route_name - self.request_method = request_method - self.request_param = request_param - self.containment = containment - self.attr = attr - self.renderer = renderer - self.wrapper = wrapper - self.xhr = xhr - self.accept = accept - self.header = header - self.path_info = path_info - self.custom_predicates = custom_predicates - self.decorator = decorator - self.mapper = mapper - self.http_cache = http_cache - self.match_param = match_param + def __init__(self, name=default, request_type=default, for_=default, + permission=default, route_name=default, + request_method=default, request_param=default, + containment=default, attr=default, renderer=default, + wrapper=default, xhr=default, accept=default, + header=default, path_info=default, + custom_predicates=default, context=default, + decorator=default, mapper=default, http_cache=default, + match_param=default): + L = locals() + if (context is not default) or (for_ is not default): + L['context'] = context or for_ + for k, v in L.items(): + if k not in ('self', 'L') and v is not default: + setattr(self, k, v) def __call__(self, wrapped): settings = self.__dict__.copy() @@ -215,7 +214,7 @@ class view_config(object): # if the decorator was attached to a method in a class, or # otherwise executed at class scope, we need to set an # 'attr' into the settings if one isn't already in there - if settings['attr'] is None: + if settings.get('attr') is None: settings['attr'] = wrapped.__name__ settings['_info'] = info.codeinfo # fbo "action_method" -- cgit v1.2.3 From 3c59ce5e5b99d652938ef5111091ce8e2516b4d1 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 6 Jan 2012 04:15:21 -0500 Subject: add tests for different kinds of names --- pyramid/tests/test_config/test_views.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index 6563fe772..fdf82e7d8 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -5,7 +5,10 @@ from pyramid.tests.test_config import IDummy from pyramid.tests.test_config import dummy_view -from pyramid.compat import im_func +from pyramid.compat import ( + im_func, + text_, + ) from pyramid.exceptions import ConfigurationError from pyramid.exceptions import ConfigurationExecutionError from pyramid.exceptions import ConfigurationConflictError @@ -144,6 +147,25 @@ class TestViewsConfigurationMixin(unittest.TestCase): result = wrapper(None, None) self.assertEqual(result, 'OK') + def test_add_view_with_name(self): + from pyramid.renderers import null_renderer + view = lambda *arg: 'OK' + config = self._makeOne(autocommit=True) + config.add_view(view=view, name='abc', renderer=null_renderer) + wrapper = self._getViewCallable(config, name='abc') + result = wrapper(None, None) + self.assertEqual(result, 'OK') + + def test_add_view_with_name_unicode(self): + from pyramid.renderers import null_renderer + view = lambda *arg: 'OK' + config = self._makeOne(autocommit=True) + name = text_(b'La Pe\xc3\xb1a', 'utf-8') + config.add_view(view=view, name=name, renderer=null_renderer) + wrapper = self._getViewCallable(config, name=name) + result = wrapper(None, None) + self.assertEqual(result, 'OK') + def test_add_view_with_decorator(self): from pyramid.renderers import null_renderer def view(request): -- cgit v1.2.3 From a5d9943f643f9d4fd7a25f1a9722bf385430b768 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 6 Jan 2012 18:35:18 -0500 Subject: - The ``path_info`` route and view predicates now match against ``request.upath_info`` (Unicode) rather than ``request.path_info`` (indeterminate value based on Python 3 vs. Python 2). This has to be done to normalize matching on Python 2 and Python 3. --- CHANGES.txt | 8 ++++++++ pyramid/config/util.py | 2 +- pyramid/tests/test_config/test_routes.py | 31 +++++++++++++++++++++++++++++-- pyramid/tests/test_config/test_views.py | 4 ++-- 4 files changed, 40 insertions(+), 5 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index d5ef9dd29..65e6cceec 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -8,6 +8,14 @@ Bug Fixes more than one view relied on the defaults being different for configuration conflict resolution. See https://github.com/Pylons/pyramid/issues/394. +Backwards Incompatibilities +--------------------------- + +- The ``path_info`` route and view predicates now match against + ``request.upath_info`` (Unicode) rather than ``request.path_info`` + (indeterminate value based on Python 3 vs. Python 2). This has to be done + to normalize matching on Python 2 and Python 3. + 1.3a4 (2012-01-05) ================== diff --git a/pyramid/config/util.py b/pyramid/config/util.py index 79f13e4a0..6c1bb8368 100644 --- a/pyramid/config/util.py +++ b/pyramid/config/util.py @@ -145,7 +145,7 @@ def make_predicates(xhr=None, request_method=None, path_info=None, except re.error as why: raise ConfigurationError(why.args[0]) def path_info_predicate(context, request): - return path_info_val.match(request.path_info) is not None + return path_info_val.match(request.upath_info) is not None text = "path_info = %s" path_info_predicate.__text__ = text % path_info weights.append(1 << 3) diff --git a/pyramid/tests/test_config/test_routes.py b/pyramid/tests/test_config/test_routes.py index 140a4aa73..bb47d2d7e 100644 --- a/pyramid/tests/test_config/test_routes.py +++ b/pyramid/tests/test_config/test_routes.py @@ -2,6 +2,7 @@ import unittest from pyramid.tests.test_config import dummyfactory from pyramid.tests.test_config import DummyContext +from pyramid.compat import text_ class RoutesConfiguratorMixinTests(unittest.TestCase): def _makeOne(self, *arg, **kw): @@ -107,10 +108,36 @@ class RoutesConfiguratorMixinTests(unittest.TestCase): route = self._assertRoute(config, 'name', 'path', 1) predicate = route.predicates[0] request = self._makeRequest(config) - request.path_info = '/foo' + request.upath_info = '/foo' self.assertEqual(predicate(None, request), True) request = self._makeRequest(config) - request.path_info = '/' + request.upath_info = '/' + self.assertEqual(predicate(None, request), False) + + def test_add_route_with_path_info_highorder(self): + config = self._makeOne(autocommit=True) + config.add_route('name', 'path', + path_info=text_(b'/La Pe\xc3\xb1a', 'utf-8')) + route = self._assertRoute(config, 'name', 'path', 1) + predicate = route.predicates[0] + request = self._makeRequest(config) + request.upath_info = text_(b'/La Pe\xc3\xb1a', 'utf-8') + self.assertEqual(predicate(None, request), True) + request = self._makeRequest(config) + request.upath_info = text_('/') + self.assertEqual(predicate(None, request), False) + + def test_add_route_with_path_info_regex(self): + config = self._makeOne(autocommit=True) + config.add_route('name', 'path', + path_info=text_(br'/La Pe\w*', 'utf-8')) + route = self._assertRoute(config, 'name', 'path', 1) + predicate = route.predicates[0] + request = self._makeRequest(config) + request.upath_info = text_(b'/La Pe\xc3\xb1a', 'utf-8') + self.assertEqual(predicate(None, request), True) + request = self._makeRequest(config) + request.upath_info = text_('/') self.assertEqual(predicate(None, request), False) def test_add_route_with_request_param(self): diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index fdf82e7d8..aa4c03db6 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -1283,7 +1283,7 @@ class TestViewsConfigurationMixin(unittest.TestCase): config.add_view(view=view, path_info='/foo', renderer=null_renderer) wrapper = self._getViewCallable(config) request = self._makeRequest(config) - request.path_info = '/foo' + request.upath_info = text_(b'/foo') self.assertEqual(wrapper(None, request), 'OK') def test_add_view_with_path_info_nomatch(self): @@ -1292,7 +1292,7 @@ class TestViewsConfigurationMixin(unittest.TestCase): config.add_view(view=view, path_info='/foo') wrapper = self._getViewCallable(config) request = self._makeRequest(config) - request.path_info = '/' + request.upath_info = text_('/') self._assertNotFound(wrapper, None, request) def test_add_view_with_custom_predicates_match(self): -- cgit v1.2.3 From 442dc88ad5c03cf6b373e0d97a55f23dfa5563e3 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 8 Jan 2012 14:23:23 -0600 Subject: garden --- docs/api/renderers.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api/renderers.rst b/docs/api/renderers.rst index 15670c46e..312aa0b31 100644 --- a/docs/api/renderers.rst +++ b/docs/api/renderers.rst @@ -20,5 +20,5 @@ as a view renderer argument, Pyramid avoids converting the view callable result into a Response object. This is useful if you want to reuse the view configuration and lookup machinery outside the context of its use by - the Pyramid router (e.g. the package named ``pyramid_rpc`` does this). + the Pyramid router. -- cgit v1.2.3 From cf3a11e990adda800e284effb006f1c28335da4d Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 9 Jan 2012 03:49:46 -0500 Subject: prep for 1.3a5 --- CHANGES.txt | 4 ++-- docs/conf.py | 2 +- setup.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 65e6cceec..8950e75a2 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,5 +1,5 @@ -Next release -============ +1.3a5 (2012-01-09) +================== Bug Fixes --------- diff --git a/docs/conf.py b/docs/conf.py index 4e0ffff55..2ab56cadf 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -80,7 +80,7 @@ copyright = '%s, Agendaless Consulting' % datetime.datetime.now().year # other places throughout the built documents. # # The short X.Y version. -version = '1.3a4' +version = '1.3a5' # The full version, including alpha/beta/rc tags. release = version diff --git a/setup.py b/setup.py index 378ef1b38..91b056778 100644 --- a/setup.py +++ b/setup.py @@ -56,7 +56,7 @@ if not PY3: ]) setup(name='pyramid', - version='1.3a4', + version='1.3a5', description=('The Pyramid web application development framework, a ' 'Pylons project'), long_description=README + '\n\n' + CHANGES, -- cgit v1.2.3