diff options
50 files changed, 791 insertions, 1594 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index bb726738c..47f51575c 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,6 +1,7 @@ Next release ============ + Bug Fixes --------- @@ -86,7 +87,7 @@ Features HEAD is a variant of GET that omits the body, and WebOb has special support to return an empty body when a HEAD is used. -- ``config.set_request_method`` has been introduced to support extending +- ``config.add_request_method`` has been introduced to support extending request objects with arbitrary callables. This method expands on the previous ``config.set_request_property`` by supporting methods as well as properties. This method now causes less code to be executed at @@ -143,11 +144,118 @@ Features to influence how the sessions are marshalled (by default this is done with HMAC+pickle). +- ``pyramid.testing.DummyRequest`` now supports methods supplied by the + ``pyramid.util.InstancePropertyMixin`` class such as ``set_property``. + +- Request properties and methods added via ``config.set_request_property`` or + ``config.add_request_method`` are now available to tweens. + +- Request properties and methods added via ``config.set_request_property`` or + ``config.add_request_method`` are now available to tweens. + +- Request properties and methods added via ``config.set_request_property`` or + ``config.add_request_method`` are now available in the request object + returned from ``pyramid.paster.bootstrap``. + +- ``request.context`` of environment request during ``bootstrap`` is now the + root object if a context isn't already set on a provided request. + Deprecations ------------ - The ``pyramid.config.Configurator.set_request_property`` has been documentation-deprecated. The method remains usable but the more - featureful ``pyramid.config.Configurator.set_request_method`` should be + featureful ``pyramid.config.Configurator.add_request_method`` should be used in its place (it has all of the same capabilities but can also extend the request object with methods). + +Backwards Incompatibilities +--------------------------- + +- The Pyramid router no longer adds the values ``bfg.routes.route`` or + ``bfg.routes.matchdict`` to the request's WSGI environment dictionary. + These values were docs-deprecated in ``repoze.bfg`` 1.0 (effectively seven + minor releases ago). If your code depended on these values, use + request.matched_route and request.matchdict instead. + +- It is no longer possible to pass an environ dictionary directly to + ``pyramid.traversal.ResourceTreeTraverser.__call__`` (aka + ``ModelGraphTraverser.__call__``). Instead, you must pass a request + object. Passing an environment instead of a request has generated a + deprecation warning since Pyramid 1.1. + +- Pyramid will no longer work properly if you use the + ``webob.request.LegacyRequest`` as a request factory. Instances of the + LegacyRequest class have a ``request.path_info`` which return a string. + This Pyramid release assumes that ``request.path_info`` will + unconditionally be Unicode. + +- The functions from ``pyramid.chameleon_zpt`` and ``pyramid.chameleon_text`` + named ``get_renderer``, ``get_template``, ``render_template``, and + ``render_template_to_response`` have been removed. These have issued a + deprecation warning upon import since Pyramid 1.0. Use + ``pyramid.renderers.get_renderer()``, + ``pyramid.renderers.get_renderer().implementation()``, + ``pyramid.renderers.render()`` or ``pyramid.renderers.render_to_response`` + respectively instead of these functions. + +- The ``pyramid.configuration`` module was removed. It had been deprecated + since Pyramid 1.0 and printed a deprecation warning upon its use. Use + ``pyramid.config`` instead. + +- The ``pyramid.paster.PyramidTemplate`` API was removed. It had been + deprecated since Pyramid 1.1 and issued a warning on import. If your code + depended on this, adjust your code to import + ``pyramid.scaffolds.PyramidTemplate`` instead. + +- The ``pyramid.settings.get_settings()`` API was removed. It had been + printing a deprecation warning since Pyramid 1.0. If your code depended on + this API, use ``pyramid.threadlocal.get_current_registry().settings`` + instead or use the ``settings`` attribute of the registry available from + the request (``request.registry.settings``). + +- These APIs from the ``pyramid.testing`` module were removed. They have + been printing deprecation warnings since Pyramid 1.0: + + * ``registerDummySecurityPolicy``, use + ``pyramid.config.Configurator.testing_securitypolicy`` instead. + + * ``registerResources`` (aka ``registerModels``, use + ``pyramid.config.Configurator.testing_resources`` instead. + + * ``registerEventListener``, use + ``pyramid.config.Configurator.testing_add_subscriber`` instead. + + * ``registerTemplateRenderer`` (aka `registerDummyRenderer``), use + ``pyramid.config.Configurator.testing_add_template`` instead. + + * ``registerView``, use ``pyramid.config.Configurator.add_view`` instead. + + * ``registerUtility``, use + ``pyramid.config.Configurator.registry.registerUtility`` instead. + + * ``registerAdapter``, use + ``pyramid.config.Configurator.registry.registerAdapter`` instead. + + * ``registerSubscriber``, use + ``pyramid.config.Configurator.add_subscriber`` instead. + + * ``registerRoute``, use + ``pyramid.config.Configurator.add_route`` instead. + + * ``registerSettings``, use + ``pyramid.config.Configurator.add_settings`` instead. + +Documentation +------------- + +- Added an "Upgrading Pyramid" chapter to the narrative documentation. It + describes how to cope with deprecations and removals of Pyramid APIs. + +Dependencies +------------ + +- Pyramid now requires WebOb 1.2b3+ (the prior Pyramid release only relied on + 1.2dev+). This is to ensure that we obtain a version of WebOb that returns + ``request.path_info`` as text. + @@ -95,21 +95,49 @@ Nice-to-Have - Update App engine chapter with less creaky directions. +- Idea from Zart: + + diff --git a/pyramid/paster.py b/pyramid/paster.py + index b0e4d79..b3bd82a 100644 + --- a/pyramid/paster.py + +++ b/pyramid/paster.py + @@ -8,6 +8,7 @@ from paste.deploy import ( + from pyramid.compat import configparser + from logging.config import fileConfig + from pyramid.scripting import prepare + +from pyramid.config import Configurator + + def get_app(config_uri, name=None, loadapp=loadapp): + """ Return the WSGI application named ``name`` in the PasteDeploy + @@ -111,3 +112,10 @@ def bootstrap(config_uri, request=None): + env['app'] = app + return env + + +def make_pyramid_app(global_conf, app=None, **settings): + + """Return pyramid application configured with provided settings""" + + config = Configurator(package='pyramid', settings=settings) + + if app: + + config.include(app) + + app = config.make_wsgi_app() + + return app + diff --git a/setup.py b/setup.py + index 03ebb42..91e0e21 100644 + --- a/setup.py + +++ b/setup.py + @@ -118,6 +118,8 @@ setup(name='pyramid', + [paste.server_runner] + wsgiref = pyramid.scripts.pserve:wsgiref_server_runner + cherrypy = pyramid.scripts.pserve:cherrypy_server_runner + + [paster.app_factory] + + main = pyramid.paster:make_pyramid_app + """ + ) + + Future ------ -- 1.4: Kill off ``bfg.routes`` envvars in router. - -- 1.4: Remove ``chameleon_text`` / ``chameleon_zpt`` deprecated functions - (render_*) - -- 1.4: Remove ``pyramid.configuration`` (deprecated). - -- 1.4: Remove ``pyramid.paster.PyramidTemplate`` (deprecated). - -- 1.4: Remove ``pyramid.settings.get_settings`` (deprecated). - -- 1.5: Remove all deprecated ``pyramid.testing`` functions. +- 1.5: remove ``pyramid.view.static`` and ``pyramid.view.is_response``. - 1.5: turn ``pyramid.settings.Settings`` into a function that returns the original dict (after ``__getattr__`` deprecation period, it was deprecated @@ -117,8 +145,8 @@ Future - 1.5: Remove ``pyramid.requests.DeprecatedRequestMethodsMixin``. -- 1.6: Maybe? deprecate set_request_property in favor of pointing people at - set_request_method. +- 1.5: Maybe? deprecate set_request_property in favor of pointing people at + add_request_method, schedule removal for 1.8? - 1.6: Remove IContextURL and TraversalContextURL. diff --git a/docs/api.rst b/docs/api.rst index d510c0d27..e33fd6a74 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -10,8 +10,6 @@ documentation is organized alphabetically by module name. api/authorization api/authentication - api/chameleon_text - api/chameleon_zpt api/compat api/config api/events diff --git a/docs/api/chameleon_text.rst b/docs/api/chameleon_text.rst deleted file mode 100644 index 494f5b464..000000000 --- a/docs/api/chameleon_text.rst +++ /dev/null @@ -1,31 +0,0 @@ -.. _chameleon_text_module: - -:mod:`pyramid.chameleon_text` ----------------------------------- - -.. automodule:: pyramid.chameleon_text - - .. autofunction:: get_template - - .. autofunction:: render_template - - .. autofunction:: render_template_to_response - -These APIs will will work against template files which contain simple -``${Genshi}`` - style replacement markers. - -The API of :mod:`pyramid.chameleon_text` is identical to that of -:mod:`pyramid.chameleon_zpt`; only its import location is -different. If you need to import an API functions from this module as -well as the :mod:`pyramid.chameleon_zpt` module within the same -view file, use the ``as`` feature of the Python import statement, -e.g.: - -.. code-block:: python - :linenos: - - from pyramid.chameleon_zpt import render_template as zpt_render - from pyramid.chameleon_text import render_template as text_render - - - diff --git a/docs/api/chameleon_zpt.rst b/docs/api/chameleon_zpt.rst deleted file mode 100644 index df9a36a56..000000000 --- a/docs/api/chameleon_zpt.rst +++ /dev/null @@ -1,28 +0,0 @@ -.. _chameleon_zpt_module: - -:mod:`pyramid.chameleon_zpt` -------------------------------- - -.. automodule:: pyramid.chameleon_zpt - - .. autofunction:: get_template - - .. autofunction:: render_template - - .. autofunction:: render_template_to_response - -These APIs will work against files which supply template text which -matches the :term:`ZPT` specification. - -The API of :mod:`pyramid.chameleon_zpt` is identical to that of -:mod:`pyramid.chameleon_text`; only its import location is -different. If you need to import an API functions from this module as -well as the :mod:`pyramid.chameleon_text` module within the same -view file, use the ``as`` feature of the Python import statement, -e.g.: - -.. code-block:: python - :linenos: - - from pyramid.chameleon_zpt import render_template as zpt_render - from pyramid.chameleon_text import render_template as text_render diff --git a/docs/api/config.rst b/docs/api/config.rst index 028a55d4b..5d2bce23e 100644 --- a/docs/api/config.rst +++ b/docs/api/config.rst @@ -40,7 +40,7 @@ :methodcategory:`Extending the Request Object` - .. automethod:: set_request_method + .. automethod:: add_request_method .. automethod:: set_request_property :methodcategory:`Using I18N` diff --git a/docs/api/settings.rst b/docs/api/settings.rst index 6b12c038c..cd802e138 100644 --- a/docs/api/settings.rst +++ b/docs/api/settings.rst @@ -5,8 +5,6 @@ .. automodule:: pyramid.settings - .. autofunction:: get_settings - .. autofunction:: asbool .. autofunction:: aslist diff --git a/docs/changes.rst b/docs/changes.rst index 6294123ed..fdeaf1e99 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -1,3 +1,5 @@ +.. _changelog: + :app:`Pyramid` Change History ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/index.rst b/docs/index.rst index c84314274..321fe1fed 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -88,6 +88,7 @@ Narrative documentation in chapter form explaining how to use narr/advconfig narr/extconfig narr/scaffolding + narr/upgrading narr/threadlocals narr/zca diff --git a/docs/latexindex.rst b/docs/latexindex.rst index 99ec2f54d..604e6e7c6 100644 --- a/docs/latexindex.rst +++ b/docs/latexindex.rst @@ -85,8 +85,6 @@ API Reference api/authorization api/authentication - api/chameleon_text - api/chameleon_zpt api/config api/events api/exceptions diff --git a/docs/narr/advconfig.rst b/docs/narr/advconfig.rst index b1ea652d6..165cf7474 100644 --- a/docs/narr/advconfig.rst +++ b/docs/narr/advconfig.rst @@ -294,9 +294,9 @@ These are the methods of the configurator which provide conflict detection: :meth:`~pyramid.config.Configurator.add_view`, :meth:`~pyramid.config.Configurator.add_route`, :meth:`~pyramid.config.Configurator.add_renderer`, +:meth:`~pyramid.config.Configurator.add_request_method`, :meth:`~pyramid.config.Configurator.set_request_factory`, :meth:`~pyramid.config.Configurator.set_session_factory`, -:meth:`~pyramid.config.Configurator.set_request_method`, :meth:`~pyramid.config.Configurator.set_request_property`, :meth:`~pyramid.config.Configurator.set_root_factory`, :meth:`~pyramid.config.Configurator.set_view_mapper`, diff --git a/docs/narr/templates.rst b/docs/narr/templates.rst index 5930bb379..ba72ebfbf 100644 --- a/docs/narr/templates.rst +++ b/docs/narr/templates.rst @@ -46,20 +46,6 @@ within the body of a view callable like so: {'foo':1, 'bar':2}, request=request) -.. warning:: - - Earlier iterations of this documentation - (pre-version-1.3) encouraged the application developer to use - ZPT-specific APIs such as - :func:`pyramid.chameleon_zpt.render_template_to_response` and - :func:`pyramid.chameleon_zpt.render_template` to render templates - directly. This style of rendering still works, but at least for - purposes of this documentation, those functions are deprecated. - Application developers are encouraged instead to use the functions - available in the :mod:`pyramid.renderers` module to perform - rendering tasks. This set of functions works to render templates - for all renderer extensions registered with :app:`Pyramid`. - The ``sample_view`` :term:`view callable` function above returns a :term:`response` object which contains the body of the ``templates/foo.pt`` template. In this case, the ``templates`` @@ -79,12 +65,12 @@ prefix on Windows. .. warning:: Only :term:`Chameleon` templates support defining a renderer for a - template relative to the location of the module where the view - callable is defined. Mako templates, and other templating system - bindings work differently. In particular, Mako templates use a - "lookup path" as defined by the ``mako.directories`` configuration - file instead of treating relative paths as relative to the current - view module. See :ref:`mako_templates`. + template relative to the location of the module where the view callable is + defined. Mako templates, and other templating system bindings work + differently. In particular, Mako templates use a "lookup path" as defined + by the ``mako.directories`` configuration file instead of treating + relative paths as relative to the current view module. See + :ref:`mako_templates`. The path can alternately be a :term:`asset specification` in the form ``some.dotted.package_name:relative/path``. This makes it possible to @@ -600,10 +586,6 @@ When the template is rendered, it will show: Hello, world! -If you'd rather use templates directly within a view callable (without -the indirection of using a renderer), see :ref:`chameleon_text_module` -for the API description. - See also :ref:`built_in_renderers` for more general information about renderers, including Chameleon text renderers. diff --git a/docs/narr/testing.rst b/docs/narr/testing.rst index 5ce2c8a66..50e9d5604 100644 --- a/docs/narr/testing.rst +++ b/docs/narr/testing.rst @@ -52,7 +52,7 @@ The suggested mechanism for unit and integration testing of a :app:`Pyramid` application is the Python :mod:`unittest` module. Although this module is named :mod:`unittest`, it is actually capable of driving both unit and integration tests. A good :mod:`unittest` tutorial is available within `Dive -Into Python <http://diveintopython.nfshost.com/unit_testing/index.html>`_ by Mark +Into Python <http://www.diveintopython.net/unit_testing/index.html>`_ by Mark Pilgrim. :app:`Pyramid` provides a number of facilities that make unit, integration, diff --git a/docs/narr/upgrading.rst b/docs/narr/upgrading.rst new file mode 100644 index 000000000..92f125c0a --- /dev/null +++ b/docs/narr/upgrading.rst @@ -0,0 +1,232 @@ +Upgrading Pyramid +================= + +When a new version of Pyramid is released, it will sometimes deprecate a +feature or remove a feature that was deprecated in an older release. When +features are removed from Pyramid, applications that depend on those features +will begin to break. This chapter explains how to ensure your Pyramid +applications keep working when you upgrade the Pyramid version you're using. + +.. sidebar:: About Release Numbering + + Conventionally, application version numbering in Python is described as + ``major.minor.micro``. If your Pyramid version is "1.2.3", it means + you're running a version of Pyramid with the major version "1", the minor + version "2" and the micro version "3". A "major" release is one that + increments the first-dot number; 2.X.X might follow 1.X.X. A "minor" + release is one that increments the second-dot number; 1.3.X might follow + 1.2.X. A "micro" release is one that increments the third-dot number; + 1.2.3 might follow 1.2.2. In general, micro releases are "bugfix-only", + and contain no new features, minor releases contain new features but are + largely backwards compatible with older versions, and a major release + indicates a large set of backwards incompatibilities. + +The Pyramid core team is conservative when it comes to removing features. We +don't remove features unnecessarily, but we're human, and we make mistakes +which cause some features to be evolutionary dead ends. Though we are +willing to support dead-end features for some amount of time, some eventually +have to be removed when the cost of supporting them outweighs the benefit of +keeping them around, because each feature in Pyramid represents a certain +documentation and maintenance burden. + +Deprecation and Removal Policy +------------------------------ + +When a feature is scheduled for removal from Pyramid or any of its official +add-ons, the core development team takes these steps: + +- Using the feature will begin to generate a `DeprecationWarning`, indicating + the version in which the feature became deprecated. + +- A note is added to the documentation indicating that the feature is + deprecated. + +- A note is added to the :ref:`changelog` about the deprecation. + +When a deprecated feature is eventually removed: + +- The feature is removed. + +- A note is added to the :ref:`changelog` about the removal. + +Features are never removed in *micro* releases. They are only removed in +minor and major releases. Deprecated features are kept around for at least +*three* minor releases from the time the feature became deprecated. +Therefore, if a feature is added in Pyramid 1.0, but it's deprecated in +Pyramid 1.1, it will be kept around through all 1.1.X releases, all 1.2.X +releases and all 1.3.X releases. It will finally be removed in the first +1.4.X release. + +Sometimes features are "docs-deprecated" instead of formally deprecated. +This means that the feature will be kept around indefinitely, but it will be +removed from the documentation or a note will be added to the documentation +telling folks to use some other newer feature. This happens when the cost of +keeping an old feature around is very minimal and the support and +documentation burden is very low. For example, we might rename a function +that is an API without changing the arguments it accepts. In this case, +we'll often rename the function, and change the docs to point at the new +function name, but leave around a backwards compatibility alias to the old +function name so older code doesn't break. + +"Docs deprecated" features tend to work "forever", meaning that they won't be +removed, and they'll never generate a deprecation warning. However, such +changes are noted in the :ref:`changelog`, so it's possible to know that you +should change older spellings to newer ones to ensure that people reading +your code can find the APIs you're using in the Pyramid docs. + +Consulting the Change History +----------------------------- + +Your first line of defense against application failures caused by upgrading +to a newer Pyramid release is always to read the :ref:`changelog`. to find +the deprecations and removals for each release between the release you're +currently running and the one you wish to upgrade to. The change history +notes every deprecation within a ``Deprecation`` section and every removal +within a ``Backwards Incompatibilies`` section for each release. + +The change history often contains instructions for changing your code to +avoid deprecation warnings and how to change docs-deprecated spellings to +newer ones. You can follow along with each deprecation explanation in the +change history, simply doing a grep or other code search to your application, +using the change log examples to remediate each potential problem. + +.. _testing_under_new_release: + +Testing Your Application Under a New Pyramid Release +---------------------------------------------------- + +Once you've upgraded your application to a new Pyramid release and you've +remediated as much as possible by using the change history notes, you'll want +to run your application's tests (see :ref:`running_tests`) in such a way that +you can see DeprecationWarnings printed to the console when the tests run. + +.. code-block:: bash + + $ python -Wd setup.py test -q + +The ``-Wd`` argument is an argument that tells Python to print deprecation +warnings to the console. Note that the ``-Wd`` flag is only required for +Python 2.7 and better: Python versions 2.6 and older print deprecation +warnings to the console by default. See `the Python -W flag documentation +<http://docs.python.org/using/cmdline.html#cmdoption-W>`_ for more +information. + +As your tests run, deprecation warnings will be printed to the console +explaining the deprecation and providing instructions about how to prevent +the deprecation warning from being issued. For example: + +.. code-block:: text + + $ python -Wd setup.py test -q + # .. elided ... + running build_ext + /home/chrism/projects/pyramid/env27/myproj/myproj/views.py:3: + DeprecationWarning: static: The "pyramid.view.static" class is deprecated + as of Pyramid 1.1; use the "pyramid.static.static_view" class instead with + the "use_subpath" argument set to True. + from pyramid.view import static + . + ---------------------------------------------------------------------- + Ran 1 test in 0.014s + + OK + +In the above case, it's line #3 in the ``myproj.views`` module (``from +pyramid.view import static``) that is causing the problem: + +.. code-block:: python + :linenos: + + from pyramid.view import view_config + + from pyramid.view import static + myview = static('static', 'static') + +The deprecation warning tells me how to fix it, so I can change the code to +do things the newer way: + +.. code-block:: python + :linenos: + + from pyramid.view. import view_config + + from pyramid.static import static_view + myview = static_view('static', 'static', use_subpath=True) + +When I run the tests again, the deprecation warning is no longer printed to +my console: + +.. code-block:: text + + $ python -Wd setup.py test -q + # .. elided ... + running build_ext + . + ---------------------------------------------------------------------- + Ran 1 test in 0.014s + + OK + + +My Application Doesn't Have Any Tests or Has Few Tests +------------------------------------------------------ + +If your application has no tests, or has only moderate test coverage, running +tests won't tell you very much, because the Pyramid codepaths that generate +deprecation warnings won't be executed. + +In this circumstance, you can start your application interactively under a +server run with the ``PYTHONWARNINGS`` environment variable set to +``default``. On UNIX, you can do that via: + +.. code-block:: bash + + $ PYTHONWARNINGS=default bin/pserve development.ini + +On Windows, you need to issue two commands: + +.. code-block:: bash + + C:\> set PYTHONWARNINGS=default + C:\> Scripts/pserve.exe development.ini + +At this point, it's ensured that deprecation warnings will be printed to the +console whenever a codepath is hit that generates one. You can then click +around in your application interactively to try to generate them, and +remediate as explained in :ref:`testing_under_new_release`. + +See `the PYTHONWARNINGS environment variable documentation +<http://docs.python.org/using/cmdline.html#envvar-PYTHONWARNINGS>`_ or `the +Python -W flag documentation +<http://docs.python.org/using/cmdline.html#cmdoption-W>`_ for more +information. + +Upgrading to the Very Latest Pyramid Release +-------------------------------------------- + +When you upgrade your application to the very most recent Pyramid release, +it's advisable to upgrade step-wise through each most recent minor release, +beginning with the one that you know your application currently runs under, +and ending on the most recent release. For example, if your application is +running in production on Pyramid 1.2.1, and the most recent Pyramid 1.3 +release is Pyramid 1.3.3, and the most recent Pyramid release is 1.4.4, it's +advisable to do this: + +- Upgrade your environment to the most recent 1.2 release. For example, the + most recent 1.2 release might be 1.2.3, so upgrade to it. Then run your + application's tests under 1.2.3 as described in + :ref:`testing_under_new_release`. Note any deprecation warnings and + remediate. + +- Upgrade to the most recent 1.3 release, 1.3.3. Run your application's + tests, note any deprecation warnings and remediate. + +- Upgrade to 1.4.4. Run your application's tests, note any deprecation + warnings and remediate. + +If you skip testing your application under each minor release (for example if +you upgrade directly from 1.2.1 to 1.4.4), you might miss a deprecation +warning and waste more time trying to figure out an error caused by a feature +removal than it would take to upgrade stepwise through each minor release. + + diff --git a/docs/tutorials/modwsgi/index.rst b/docs/tutorials/modwsgi/index.rst index d11167344..a22f12610 100644 --- a/docs/tutorials/modwsgi/index.rst +++ b/docs/tutorials/modwsgi/index.rst @@ -7,12 +7,11 @@ Running a :app:`Pyramid` Application under ``mod_wsgi`` It allows :term:`WSGI` programs to be served using the Apache web server. -This guide will outline broad steps that can be used to get a -:app:`Pyramid` application running under Apache via ``mod_wsgi``. -This particular tutorial was developed under Apple's Mac OS X platform -(Snow Leopard, on a 32-bit Mac), but the instructions should be -largely the same for all systems, delta specific path information for -commands and files. +This guide will outline broad steps that can be used to get a :app:`Pyramid` +application running under Apache via ``mod_wsgi``. This particular tutorial +was developed under Apple's Mac OS X platform (Snow Leopard, on a 32-bit +Mac), but the instructions should be largely the same for all systems, delta +specific path information for commands and files. .. note:: Unfortunately these instructions almost certainly won't work for deploying a :app:`Pyramid` application on a Windows system using @@ -90,12 +89,11 @@ commands and files. `logging` module to allow logging within your application. See :ref:`logging_config`. -#. Make the ``pyramid.wsgi`` script executable. - - .. code-block:: text - - $ cd ~/modwsgi/env - $ chmod 755 pyramid.wsgi + There is no need to make the ``pyramid.wsgi`` script executable. + However, you'll need to make sure that *two* users have access to change + into the ``~/modwsgi/env`` directory: your current user (mine is + ``chrism`` and the user that Apache will run as often named ``apache`` or + ``httpd``). Make sure both of these users can "cd" into that directory. #. Edit your Apache configuration and add some stuff. I happened to create a file named ``/etc/apache2/other/modwsgi.conf`` on my own diff --git a/docs/tutorials/wiki/src/authorization/tutorial/views.py b/docs/tutorials/wiki/src/authorization/tutorial/views.py index 21f12b31d..50485d279 100644 --- a/docs/tutorials/wiki/src/authorization/tutorial/views.py +++ b/docs/tutorials/wiki/src/authorization/tutorial/views.py @@ -51,17 +51,17 @@ def view_page(context, request): renderer='templates/edit.pt', permission='edit') def add_page(context, request): - name = request.subpath[0] + pagename = request.subpath[0] if 'form.submitted' in request.params: body = request.params['body'] page = Page(body) - page.__name__ = name + page.__name__ = pagename page.__parent__ = context - context[name] = page + context[pagename] = page return HTTPFound(location = request.resource_url(page)) - save_url = request.resource_url(context, 'add_page', name) + save_url = request.resource_url(context, 'add_page', pagename) page = Page('') - page.__name__ = name + page.__name__ = pagename page.__parent__ = context return dict(page = page, save_url = save_url, diff --git a/docs/tutorials/wiki/src/tests/tutorial/views.py b/docs/tutorials/wiki/src/tests/tutorial/views.py index 21f12b31d..50485d279 100644 --- a/docs/tutorials/wiki/src/tests/tutorial/views.py +++ b/docs/tutorials/wiki/src/tests/tutorial/views.py @@ -51,17 +51,17 @@ def view_page(context, request): renderer='templates/edit.pt', permission='edit') def add_page(context, request): - name = request.subpath[0] + pagename = request.subpath[0] if 'form.submitted' in request.params: body = request.params['body'] page = Page(body) - page.__name__ = name + page.__name__ = pagename page.__parent__ = context - context[name] = page + context[pagename] = page return HTTPFound(location = request.resource_url(page)) - save_url = request.resource_url(context, 'add_page', name) + save_url = request.resource_url(context, 'add_page', pagename) page = Page('') - page.__name__ = name + page.__name__ = pagename page.__parent__ = context return dict(page = page, save_url = save_url, diff --git a/docs/tutorials/wiki/src/views/tutorial/views.py b/docs/tutorials/wiki/src/views/tutorial/views.py index 016f5b6bb..b0c15297f 100644 --- a/docs/tutorials/wiki/src/views/tutorial/views.py +++ b/docs/tutorials/wiki/src/views/tutorial/views.py @@ -35,17 +35,17 @@ def view_page(context, request): @view_config(name='add_page', context='.models.Wiki', renderer='templates/edit.pt') def add_page(context, request): - name = request.subpath[0] + pagename = request.subpath[0] if 'form.submitted' in request.params: body = request.params['body'] page = Page(body) - page.__name__ = name + page.__name__ = pagename page.__parent__ = context - context[name] = page + context[pagename] = page return HTTPFound(location = request.resource_url(page)) - save_url = request.resource_url(context, 'add_page', name) + save_url = request.resource_url(context, 'add_page', pagename) page = Page('') - page.__name__ = name + page.__name__ = pagename page.__parent__ = context return dict(page = page, save_url = save_url) diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views.py b/docs/tutorials/wiki2/src/authorization/tutorial/views.py index c7670b049..0d085b0e2 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/views.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/views.py @@ -60,14 +60,14 @@ def view_page(request): @view_config(route_name='add_page', renderer='templates/edit.pt', permission='edit') def add_page(request): - name = request.matchdict['pagename'] + pagename = request.matchdict['pagename'] if 'form.submitted' in request.params: body = request.params['body'] - page = Page(name, body) + page = Page(pagename, body) DBSession.add(page) return HTTPFound(location = request.route_url('view_page', - pagename=name)) - save_url = request.route_url('add_page', pagename=name) + pagename=pagename)) + save_url = request.route_url('add_page', pagename=pagename) page = Page('', '') return dict(page=page, save_url=save_url, logged_in=authenticated_userid(request)) @@ -75,16 +75,16 @@ def add_page(request): @view_config(route_name='edit_page', renderer='templates/edit.pt', permission='edit') def edit_page(request): - name = request.matchdict['pagename'] - page = DBSession.query(Page).filter_by(name=name).one() + pagename = request.matchdict['pagename'] + page = DBSession.query(Page).filter_by(name=pagename).one() if 'form.submitted' in request.params: page.data = request.params['body'] DBSession.add(page) return HTTPFound(location = request.route_url('view_page', - pagename=name)) + pagename=pagename)) return dict( page=page, - save_url = request.route_url('edit_page', pagename=name), + save_url = request.route_url('edit_page', pagename=pagename), logged_in=authenticated_userid(request), ) diff --git a/docs/tutorials/wiki2/src/tests/tutorial/views.py b/docs/tutorials/wiki2/src/tests/tutorial/views.py index f2a33af1e..42ac0eb7f 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/views.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/views.py @@ -61,15 +61,15 @@ def view_page(request): @view_config(route_name='add_page', renderer='templates/edit.pt', permission='edit') def add_page(request): - name = request.matchdict['pagename'] + pagename = request.matchdict['pagename'] if 'form.submitted' in request.params: session = DBSession() body = request.params['body'] - page = Page(name, body) + page = Page(pagename, body) session.add(page) return HTTPFound(location = request.route_url('view_page', - pagename=name)) - save_url = request.route_url('add_page', pagename=name) + pagename=pagename)) + save_url = request.route_url('add_page', pagename=pagename) page = Page('', '') return dict(page=page, save_url=save_url, logged_in=authenticated_userid(request)) @@ -77,17 +77,17 @@ def add_page(request): @view_config(route_name='edit_page', renderer='templates/edit.pt', permission='edit') def edit_page(request): - name = request.matchdict['pagename'] + pagename = request.matchdict['pagename'] session = DBSession() - page = session.query(Page).filter_by(name=name).one() + page = session.query(Page).filter_by(name=pagename).one() if 'form.submitted' in request.params: page.data = request.params['body'] session.add(page) return HTTPFound(location = request.route_url('view_page', - pagename=name)) + pagename=pagename)) return dict( page=page, - save_url = request.route_url('edit_page', pagename=name), + save_url = request.route_url('edit_page', pagename=pagename), logged_in=authenticated_userid(request), ) diff --git a/docs/tutorials/wiki2/src/views/tutorial/views.py b/docs/tutorials/wiki2/src/views/tutorial/views.py index c2a94a96b..5a9c75a61 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/views.py +++ b/docs/tutorials/wiki2/src/views/tutorial/views.py @@ -44,27 +44,27 @@ def view_page(request): @view_config(route_name='add_page', renderer='templates/edit.pt') def add_page(request): - name = request.matchdict['pagename'] + pagename = request.matchdict['pagename'] if 'form.submitted' in request.params: body = request.params['body'] - page = Page(name, body) + page = Page(pagename, body) DBSession.add(page) return HTTPFound(location = request.route_url('view_page', - pagename=name)) - save_url = request.route_url('add_page', pagename=name) + pagename=pagename)) + save_url = request.route_url('add_page', pagename=pagename) page = Page('', '') return dict(page=page, save_url=save_url) @view_config(route_name='edit_page', renderer='templates/edit.pt') def edit_page(request): - name = request.matchdict['pagename'] - page = DBSession.query(Page).filter_by(name=name).one() + pagename = request.matchdict['pagename'] + page = DBSession.query(Page).filter_by(name=pagename).one() if 'form.submitted' in request.params: page.data = request.params['body'] DBSession.add(page) return HTTPFound(location = request.route_url('view_page', - pagename=name)) + pagename=pagename)) return dict( page=page, - save_url = request.route_url('edit_page', pagename=name), + save_url = request.route_url('edit_page', pagename=pagename), ) diff --git a/pyramid/chameleon_text.py b/pyramid/chameleon_text.py index 20f614857..3484b7973 100644 --- a/pyramid/chameleon_text.py +++ b/pyramid/chameleon_text.py @@ -1,6 +1,5 @@ import sys -from zope.deprecation import deprecated from zope.interface import implementer from pyramid.compat import reraise @@ -20,7 +19,6 @@ from pyramid.interfaces import ITemplateRenderer from pyramid.decorator import reify from pyramid import renderers -from pyramid.path import caller_package def renderer_factory(info): return renderers.template_renderer_factory(info, TextTemplateRenderer) @@ -30,6 +28,8 @@ class TextTemplateRenderer(object): def __init__(self, path, lookup, macro=None): self.path = path self.lookup = lookup + # text template renderers have no macros, so we ignore the + # macro arg @reify # avoid looking up reload_templates before manager pushed def template(self): @@ -52,93 +52,3 @@ class TextTemplateRenderer(object): result = self.template(**system) return result -def get_renderer(path): - """ Return a callable object which can be used to render a - :term:`Chameleon` text template using the template implied by the - ``path`` argument. The ``path`` argument may be a - package-relative path, an absolute path, or a :term:`asset - specification`. - - .. warning:: - - This API is deprecated in :app:`Pyramid` 1.0. Use - :func:`pyramid.renderers.get_renderer` instead. - """ - package = caller_package() - factory = renderers.RendererHelper(path, package=package) - return factory.get_renderer() - -deprecated( - 'get_renderer', - '(pyramid.chameleon_text.get_renderer is deprecated ' - 'as of Pyramid 1.0; instead use pyramid.renderers.get_renderer)') - -def get_template(path): - """ Return the underyling object representing a :term:`Chameleon` - text template using the template implied by the ``path`` argument. - The ``path`` argument may be a package-relative path, an absolute - path, or a :term:`asset specification`. - - .. warning:: - - This API is deprecated in :app:`Pyramid` 1.0. Use - the ``implementation()`` method of a template renderer retrieved via - :func:`pyramid.renderers.get_renderer` instead. - """ - package = caller_package() - factory = renderers.RendererHelper(path, package=package) - return factory.get_renderer().implementation() - -deprecated( - 'get_template', - '(pyramid.chameleon_text.get_template is deprecated ' - 'as of Pyramid 1.0; instead use ' - 'pyramid.renderers.get_renderer().implementation())') - -def render_template(path, **kw): - """ Render a :term:`Chameleon` text template using the template - implied by the ``path`` argument. The ``path`` argument may be a - package-relative path, an absolute path, or a :term:`asset - specification`. The arguments in ``*kw`` are passed as top-level - names to the template, and so may be used within the template - itself. Returns a string. - - .. warning:: - - This API is deprecated in :app:`Pyramid` 1.0. Use - :func:`pyramid.renderers.render` instead. - """ - package = caller_package() - request = kw.pop('request', None) - renderer = renderers.RendererHelper(path, package=package) - return renderer.render(kw, None, request=request) - -deprecated( - 'render_template', - '(pyramid.chameleon_text.render_template is deprecated ' - 'as of Pyramid 1.0; instead use pyramid.renderers.render)') - -def render_template_to_response(path, **kw): - """ Render a :term:`Chameleon` text template using the template - implied by the ``path`` argument. The ``path`` argument may be a - package-relative path, an absolute path, or a :term:`asset - specification`. The arguments in ``*kw`` are passed as top-level - names to the template, and so may be used within the template - itself. Returns a :term:`Response` object with the body as the - template result. - - .. warning:: - - This API is deprecated in :app:`Pyramid` 1.0. Use - :func:`pyramid.renderers.render_to_response` instead. - """ - package = caller_package() - request = kw.pop('request', None) - renderer = renderers.RendererHelper(path, package=package) - return renderer.render_to_response(kw, None, request=request) - -deprecated( - 'render_template_to_response', - '(pyramid.chameleon_text.render_template_to_response is deprecated ' - 'as of Pyramid 1.0; instead use pyramid.renderers.render_to_response)') - diff --git a/pyramid/chameleon_zpt.py b/pyramid/chameleon_zpt.py index 9f9e09850..d9f4337fa 100644 --- a/pyramid/chameleon_zpt.py +++ b/pyramid/chameleon_zpt.py @@ -1,6 +1,5 @@ import sys -from zope.deprecation import deprecated from zope.interface import implementer from pyramid.compat import reraise @@ -18,7 +17,6 @@ except ImportError: # pragma: no cover from pyramid.interfaces import ITemplateRenderer from pyramid.decorator import reify -from pyramid.path import caller_package from pyramid import renderers def renderer_factory(info): @@ -58,92 +56,3 @@ class ZPTTemplateRenderer(object): result = self.template(**system) return result -def get_renderer(path): - """ Return a callable object which can be used to render a - :term:`Chameleon` ZPT template using the template implied by the - ``path`` argument. The ``path`` argument may be a - package-relative path, an absolute path, or a :term:`asset - specification`. - - .. warning:: - - This API is deprecated in :app:`Pyramid` 1.0. Use - :func:`pyramid.renderers.get_renderer` instead. - """ - package = caller_package() - factory = renderers.RendererHelper(name=path, package=package) - return factory.get_renderer() - -deprecated( - 'get_renderer', - '(pyramid.chameleon_zpt.get_renderer is deprecated ' - 'as of Pyramid 1.0; instead use pyramid.renderers.get_renderer)') - -def get_template(path): - """ Return the underyling object representing a :term:`Chameleon` - ZPT template using the template implied by the ``path`` argument. - The ``path`` argument may be a package-relative path, an absolute - path, or a :term:`asset specification`. - - .. warning:: - - This API is deprecated in :app:`Pyramid` 1.0. Use - the ``implementation()`` method of a template renderer retrieved via - :func:`pyramid.renderers.get_renderer` instead. - """ - package = caller_package() - factory = renderers.RendererHelper(name=path, package=package) - return factory.get_renderer().implementation() - -deprecated( - 'get_template', - '(pyramid.chameleon_zpt.get_template is deprecated ' - 'as of Pyramid 1.0; instead use ' - 'pyramid.renderers.get_renderer().implementation())') - -def render_template(path, **kw): - """ Render a :term:`Chameleon` ZPT template using the template - implied by the ``path`` argument. The ``path`` argument may be a - package-relative path, an absolute path, or a :term:`asset - specification`. The arguments in ``*kw`` are passed as top-level - names to the template, and so may be used within the template - itself. Returns a string. - - .. warning:: - - This API is deprecated in :app:`Pyramid` 1.0. Use - :func:`pyramid.renderers.render` instead. - """ - package = caller_package() - request = kw.pop('request', None) - renderer = renderers.RendererHelper(name=path, package=package) - return renderer.render(kw, None, request=request) - -deprecated( - 'render_template', - '(pyramid.chameleon_zpt.render_template is deprecated as of Pyramid 1.0; ' - 'instead use pyramid.renderers.render)') - -def render_template_to_response(path, **kw): - """ Render a :term:`Chameleon` ZPT template using the template - implied by the ``path`` argument. The ``path`` argument may be a - package-relative path, an absolute path, or a :term:`asset - specification`. The arguments in ``*kw`` are passed as top-level - names to the template, and so may be used within the template - itself. Returns a :term:`Response` object with the body as the - template result. - - .. warning:: - - This API is deprecated in :app:`Pyramid` 1.0. Use - :func:`pyramid.renderers.render_to_response` instead. - """ - package = caller_package() - request = kw.pop('request', None) - renderer = renderers.RendererHelper(name=path, package=package) - return renderer.render_to_response(kw, None, request=request) - -deprecated( - 'render_template_to_response', - '(pyramid.chameleon_zpt.render_template_to_response is deprecated; as of ' - 'Pyramid 1.0, instead use pyramid.renderers.render_to_response)') diff --git a/pyramid/config/factories.py b/pyramid/config/factories.py index e8043ed28..e46519bf5 100644 --- a/pyramid/config/factories.py +++ b/pyramid/config/factories.py @@ -76,9 +76,9 @@ class FactoriesConfiguratorMixin(object): :class:`pyramid.request.Request` class (particularly ``__call__``, and ``blank``). - See :meth:`pyramid.config.Configurator.set_request_property` + See :meth:`pyramid.config.Configurator.add_request_method` for a less intrusive way to extend the request objects with - custom properties. + custom methods and properties. .. note:: @@ -96,7 +96,7 @@ class FactoriesConfiguratorMixin(object): self.action(IRequestFactory, register, introspectables=(intr,)) @action_method - def set_request_method(self, + def add_request_method(self, callable=None, name=None, property=False, @@ -153,8 +153,6 @@ class FactoriesConfiguratorMixin(object): if exts is None: exts = _RequestExtensions() self.registry.registerUtility(exts, IRequestExtensions) - self.registry.registerHandler(_set_request_extensions, - (INewRequest,)) plist = exts.descriptors if property else exts.methods plist[name] = callable @@ -186,13 +184,13 @@ class FactoriesConfiguratorMixin(object): .. warning:: - This method has been deprecated as of Pyramid 1.4. - :meth:`pyramid.config.Configurator.set_request_method` should be + This method has been docs-deprecated as of Pyramid 1.4. + :meth:`pyramid.config.Configurator.add_request_method` should be used instead. .. versionadded:: 1.3 """ - self.set_request_method( + self.add_request_method( callable, name=name, property=not reify, reify=reify) @implementer(IRequestExtensions) @@ -201,10 +199,3 @@ class _RequestExtensions(object): self.descriptors = {} self.methods = {} -def _set_request_extensions(event): - request = event.request - exts = request.registry.queryUtility(IRequestExtensions) - for name, fn in iteritems_(exts.methods): - method = fn.__get__(request, request.__class__) - setattr(request, name, method) - request._set_properties(exts.descriptors) diff --git a/pyramid/configuration.py b/pyramid/configuration.py deleted file mode 100644 index 802c10d1f..000000000 --- a/pyramid/configuration.py +++ /dev/null @@ -1,60 +0,0 @@ -from pyramid.config import Configurator as BaseConfigurator -from pyramid.exceptions import ConfigurationError # API -from pyramid.config import DEFAULT_RENDERERS -from pyramid.path import caller_package - -from zope.deprecation import deprecated - -ConfigurationError = ConfigurationError # pyflakes - -deprecated( - 'ConfigurationError', - 'pyramid.configuration.ConfigurationError is deprecated as of ' - 'Pyramid 1.0. Use ``pyramid.config.ConfigurationError`` instead.') - -class Configurator(BaseConfigurator): - def __init__(self, - registry=None, - package=None, - settings=None, - root_factory=None, - authentication_policy=None, - authorization_policy=None, - renderers=DEFAULT_RENDERERS, - debug_logger=None, - locale_negotiator=None, - request_factory=None, - renderer_globals_factory=None, - default_permission=None, - session_factory=None, - autocommit=True, - route_prefix=None, - introspection=True, - ): - if package is None: - package = caller_package() - BaseConfigurator.__init__( - self, - registry=registry, - package=package, - settings=settings, - root_factory=root_factory, - authentication_policy=authentication_policy, - authorization_policy=authorization_policy, - renderers=renderers, - debug_logger=debug_logger, - locale_negotiator=locale_negotiator, - request_factory=request_factory, - renderer_globals_factory=renderer_globals_factory, - default_permission=default_permission, - session_factory=session_factory, - autocommit=autocommit, - route_prefix=route_prefix, - introspection=introspection, - ) - -deprecated( - 'Configurator', - 'pyramid.configuration.Configurator is deprecated as of Pyramid 1.0. Use' - '``pyramid.config.Configurator`` with ``autocommit=True`` instead.') - diff --git a/pyramid/paster.py b/pyramid/paster.py index 5102e7b9b..b0e4d7933 100644 --- a/pyramid/paster.py +++ b/pyramid/paster.py @@ -1,7 +1,5 @@ import os -import zope.deprecation - from paste.deploy import ( loadapp, appconfig, @@ -10,14 +8,6 @@ from paste.deploy import ( from pyramid.compat import configparser from logging.config import fileConfig from pyramid.scripting import prepare -from pyramid.scaffolds import PyramidTemplate # bw compat - -PyramidTemplate = PyramidTemplate # pyflakes - -zope.deprecation.deprecated( - 'PyramidTemplate', ('pyramid.paster.PyramidTemplate was moved to ' - 'pyramid.scaffolds.PyramidTemplate in Pyramid 1.1'), -) def get_app(config_uri, name=None, loadapp=loadapp): """ Return the WSGI application named ``name`` in the PasteDeploy diff --git a/pyramid/request.py b/pyramid/request.py index 37fac6a46..af3310829 100644 --- a/pyramid/request.py +++ b/pyramid/request.py @@ -454,13 +454,4 @@ def call_app_with_subpath_as_path_info(request, app): new_request.environ['SCRIPT_NAME'] = new_script_name new_request.environ['PATH_INFO'] = new_path_info - # In case downstream WSGI app is a Pyramid app, hack around existence of - # these envars until we can safely remove them (see router.py); in any - # case, even if these get removed, it might be better to not copy the - # existing environ but to create a new one instead. - if 'bfg.routes.route' in new_request.environ: - del new_request.environ['bfg.routes.route'] - if 'bfg.routes.matchdict' in new_request.environ: - del new_request.environ['bfg.routes.matchdict'] - return new_request.get_response(app) diff --git a/pyramid/router.py b/pyramid/router.py index 0c115a1ac..0cbe00f3a 100644 --- a/pyramid/router.py +++ b/pyramid/router.py @@ -6,6 +6,7 @@ from zope.interface import ( from pyramid.interfaces import ( IDebugLogger, IRequest, + IRequestExtensions, IRootFactory, IRouteRequest, IRouter, @@ -48,6 +49,7 @@ class Router(object): self.root_factory = q(IRootFactory, default=DefaultRootFactory) self.routes_mapper = q(IRoutesMapper) self.request_factory = q(IRequestFactory, default=Request) + self.request_extensions = q(IRequestExtensions) tweens = q(ITweens) if tweens is None: tweens = excview_tween_factory @@ -84,13 +86,6 @@ class Router(object): request.url) logger and logger.debug(msg) else: - # TODO: kill off bfg.routes.* environ keys - # when traverser requires request arg, and - # cant cope with environ anymore (they are - # docs-deprecated as of BFG 1.3) - environ = request.environ - environ['bfg.routes.route'] = route - environ['bfg.routes.matchdict'] = match attrs['matchdict'] = match attrs['matched_route'] = route @@ -105,7 +100,8 @@ class Router(object): request.url, route.name, request.path_info, - route.pattern, match, + route.pattern, + match, ', '.join([p.__text__ for p in route.predicates])) ) logger and logger.debug(msg) @@ -184,6 +180,9 @@ class Router(object): try: try: + extensions = self.request_extensions + if extensions is not None: + request._set_extensions(extensions) response = self.handle_request(request) has_listeners and notify(NewResponse(request, response)) @@ -198,4 +197,3 @@ class Router(object): finally: manager.pop() - diff --git a/pyramid/scripting.py b/pyramid/scripting.py index f1dc24637..00177986f 100644 --- a/pyramid/scripting.py +++ b/pyramid/scripting.py @@ -3,6 +3,7 @@ from pyramid.exceptions import ConfigurationError from pyramid.request import Request from pyramid.interfaces import ( + IRequestExtensions, IRequestFactory, IRootFactory, ) @@ -70,14 +71,18 @@ def prepare(request=None, registry=None): 'before trying to activate it.') if request is None: request = _make_request('/', registry) - request.registry = registry threadlocals = {'registry':registry, 'request':request} threadlocal_manager.push(threadlocals) + extensions = registry.queryUtility(IRequestExtensions) + if extensions is not None: + request._set_extensions(extensions) def closer(): threadlocal_manager.pop() root_factory = registry.queryUtility(IRootFactory, default=DefaultRootFactory) root = root_factory(request) + if getattr(request, 'context', None) is None: + request.context = root return {'root':root, 'closer':closer, 'registry':registry, 'request':request, 'root_factory':root_factory} diff --git a/pyramid/settings.py b/pyramid/settings.py index 86304307e..e2cb3cb3c 100644 --- a/pyramid/settings.py +++ b/pyramid/settings.py @@ -1,31 +1,5 @@ -from zope.deprecation import deprecated - -from pyramid.threadlocal import get_current_registry from pyramid.compat import string_types -def get_settings(): - """ - Return a :term:`deployment settings` object for the current application. - The object is a dictionary-like object that contains key/value pairs - based on the dictionary passed as the ``settings`` argument to the - :class:`pyramid.config.Configurator` constructor or the - :func:`pyramid.router.make_app` API. - - .. warning:: This method is deprecated as of Pyramid 1.0. Use - ``pyramid.threadlocal.get_current_registry().settings`` instead or use - the ``settings`` attribute of the registry available from the request - (``request.registry.settings``). - """ - reg = get_current_registry() - return reg.settings - -deprecated( - 'get_settings', - '(pyramid.settings.get_settings is deprecated as of Pyramid 1.0. Use' - '``pyramid.threadlocal.get_current_registry().settings`` instead or use ' - 'the ``settings`` attribute of the registry available from the request ' - '(``request.registry.settings``)).') - truthy = frozenset(('t', 'true', 'y', 'yes', 'on', '1')) def asbool(s): diff --git a/pyramid/testing.py b/pyramid/testing.py index 89eec84b0..750effb83 100644 --- a/pyramid/testing.py +++ b/pyramid/testing.py @@ -1,20 +1,14 @@ import copy import os -from zope.deprecation import deprecated - from zope.interface import ( implementer, - Interface, alsoProvides, ) from pyramid.interfaces import ( IRequest, IResponseFactory, - ISecuredView, - IView, - IViewClassifier, ISession, ) @@ -26,14 +20,12 @@ from pyramid.compat import ( from pyramid.config import Configurator from pyramid.decorator import reify -from pyramid.httpexceptions import HTTPForbidden from pyramid.response import Response from pyramid.registry import Registry from pyramid.security import ( Authenticated, Everyone, - has_permission, ) from pyramid.threadlocal import ( @@ -47,374 +39,10 @@ from pyramid.request import ( ) from pyramid.url import URLMethodsMixin +from pyramid.util import InstancePropertyMixin _marker = object() -def registerDummySecurityPolicy(userid=None, groupids=(), permissive=True): - """ Registers a pair of faux :app:`Pyramid` security policies: - a :term:`authentication policy` and a :term:`authorization - policy`. - - The behavior of the registered :term:`authorization policy` - depends on the ``permissive`` argument. If ``permissive`` is - true, a permissive :term:`authorization policy` is registered; - this policy allows all access. If ``permissive`` is false, a - nonpermissive :term:`authorization policy` is registered; this - policy denies all access. - - The behavior of the registered :term:`authentication policy` - depends on the values provided for the ``userid`` and ``groupids`` - argument. The authentication policy will return the userid - identifier implied by the ``userid`` argument and the group ids - implied by the ``groupids`` argument when the - :func:`pyramid.security.authenticated_userid` or - :func:`pyramid.security.effective_principals` APIs are used. - - This function is most useful when testing code that uses the APIs named - :func:`pyramid.security.has_permission`, - :func:`pyramid.security.authenticated_userid`, - :func:`pyramid.security.unauthenticated_userid`, - :func:`pyramid.security.effective_principals`, and - :func:`pyramid.security.principals_allowed_by_permission`. - - .. warning:: This API is deprecated as of :app:`Pyramid` 1.0. - Instead use the - :meth:`pyramid.config.Configurator.testing_securitypolicy` - method in your unit and integration tests. - """ - registry = get_current_registry() - config = Configurator(registry=registry) - result = config.testing_securitypolicy(userid=userid, groupids=groupids, - permissive=permissive) - config.commit() - return result - -deprecated('registerDummySecurityPolicy', - 'The testing.registerDummySecurityPolicy API is deprecated as of ' - 'Pyramid 1.0. Instead use the ' - 'pyramid.config.Configurator.testing_securitypolicy ' - 'method in your unit and integration tests.') - -def registerResources(resources): - """ Registers a dictionary of :term:`resource` objects that can be - resolved via the :func:`pyramid.traversal.find_resource` API. - - The :func:`pyramid.traversal.find_resource` API is called with a - path as one of its arguments. If the dictionary you register when - calling this method contains that path as a string key - (e.g. ``/foo/bar`` or ``foo/bar``), the corresponding value will - be returned to ``find_resource`` (and thus to your code) when - :func:`pyramid.traversal.find_resource` is called with an - equivalent path string or tuple. - - .. warning:: This API is deprecated as of :app:`Pyramid` 1.0. - Instead use the - :meth:`pyramid.config.Configurator.testing_resources` - method in your unit and integration tests. - - .. note:: For ancient backward compatibility purposes, this API can also - be accessed as :func:`pyramid.testing.registerModels`. - """ - registry = get_current_registry() - config = Configurator(registry=registry) - result = config.testing_resources(resources) - config.commit() - return result - -deprecated('registerResources', - 'The testing.registerResources API is deprecated as of ' - 'Pyramid 1.0. Instead use the ' - 'pyramid.config.Configurator.testing_resources ' - 'method in your unit and integration tests.') - -registerModels = registerResources - -deprecated('registerModels', - 'The testing.registerModels API is deprecated as of ' - 'Pyramid 1.0. Instead use the ' - 'pyramid.config.Configurator.testing_resources ' - 'method in your unit and integration tests.') - -def registerEventListener(event_iface=None): - """ Registers an :term:`event` listener (aka :term:`subscriber`) - listening for events of the type ``event_iface``. This method - returns a list object which is appended to by the subscriber - whenever an event is captured. - - When an event is dispatched that matches ``event_iface``, that - event will be appended to the list. You can then compare the - values in the list to expected event notifications. This method - is useful when testing code that wants to call - :meth:`pyramid.registry.Registry.notify`. - - The default value of ``event_iface`` (``None``) implies a - subscriber registered for *any* kind of event. - - .. warning:: This API is deprecated as of :app:`Pyramid` 1.0. - Instead use the - :meth:`pyramid.config.Configurator.testing_add_subscriber` - method in your unit and integration tests. - """ - registry = get_current_registry() - config = Configurator(registry=registry) - result = config.testing_add_subscriber(event_iface) - config.commit() - return result - -deprecated('registerEventListener', - 'The testing.registerEventListener API is deprecated as of ' - 'Pyramid 1.0. Instead use the ' - 'pyramid.config.Configurator.testing_add_subscriber ' - 'method in your unit and integration tests.') - -def registerTemplateRenderer(path, renderer=None): - """ Register a template renderer at ``path`` (usually a relative - filename ala ``templates/foo.pt``) and return the renderer object. - If the ``renderer`` argument is None, a 'dummy' renderer will be - used. This function is useful when testing code that calls the - :func:`pyramid.renderers.render` function or - :func:`pyramid.renderers.render_to_response` function or any - other ``render_*`` or ``get_*`` API of the - :mod:`pyramid.renderers` module. - - .. warning:: This API is deprecated as of :app:`Pyramid` 1.0. - Instead use the - :meth:`pyramid.config.Configurator.testing_add_renderer` - method in your unit and integration tests. - - """ - registry = get_current_registry() - config = Configurator(registry=registry) - result = config.testing_add_template(path, renderer) - config.commit() - return result - -deprecated('registerTemplateRenderer', - 'The testing.registerTemplateRenderer API is deprecated as of ' - 'Pyramid 1.0. Instead use the ' - 'pyramid.config.Configurator.testing_add_renderer ' - 'method in your unit and integration tests.') - -# registerDummyRenderer is a deprecated alias that should never be removed -# (too much usage in the wild) -registerDummyRenderer = registerTemplateRenderer - -deprecated('registerDummyRenderer', - 'The testing.registerDummyRenderer API is deprecated as of ' - 'Pyramid 1.0. Instead use the ' - 'pyramid.config.Configurator.testing_add_renderer ' - 'method in your unit and integration tests.') - -def registerView(name, result='', view=None, for_=(Interface, Interface), - permission=None): - """ Registers a :app:`Pyramid` :term:`view callable` under the - name implied by the ``name`` argument. The view will return a - :term:`WebOb` :term:`Response` object with the value implied by - the ``result`` argument as its ``body`` attribute. To gain more - control, if you pass in a non-``None`` ``view`` argument, this - value will be used as a view callable instead of an automatically - generated view callable (and ``result`` is not used). - - To protect the view using a :term:`permission`, pass in a - non-``None`` value as ``permission``. This permission will be - checked by any active :term:`authorization policy` when view - execution is attempted. - - This function is useful when testing code which calls - :func:`pyramid.view.render_view_to_response`. - - .. warning:: This API is deprecated as of :app:`Pyramid` 1.0. - Instead use the - :meth:`pyramid.config.Configurator.add_view` - method in your unit and integration tests. - """ - for_ = (IViewClassifier, ) + for_ - if view is None: - def view(context, request): - return Response(result) - if permission is None: - return registerAdapter(view, for_, IView, name) - else: - def _secure(context, request): - if not has_permission(permission, context, request): - raise HTTPForbidden('no permission') - else: - return view(context, request) - _secure.__call_permissive__ = view - def permitted(context, request): - return has_permission(permission, context, request) - _secure.__permitted__ = permitted - return registerAdapter(_secure, for_, ISecuredView, name) - -deprecated('registerView', - 'The registerView API is deprecated as of ' - 'Pyramid 1.0. Instead use the ' - 'pyramid.config.Configurator.add_view ' - 'method in your unit and integration tests.') - -def registerUtility(impl, iface=Interface, name=''): - """ Register a ZCA utility component. - - The ``impl`` argument specifies the implementation of the utility. - The ``iface`` argument specifies the :term:`interface` which will - be later required to look up the utility - (:class:`zope.interface.Interface`, by default). The ``name`` - argument implies the utility name; it is the empty string by - default. - - See `The ZCA book <http://www.muthukadan.net/docs/zca.html>`_ for - more information about ZCA utilities. - - .. warning:: This API is deprecated as of :app:`Pyramid` 1.0. - Instead use the :meth:`pyramid.Registry.registerUtility` - method. The ``registry`` attribute of a :term:`Configurator` - in your unit and integration tests is an instance of the - :class:`pyramid.Registry` class. - """ - reg = get_current_registry() - reg.registerUtility(impl, iface, name=name) - return impl - -deprecated('registerUtility', - 'The registerUtility API is deprecated as of ' - 'Pyramid 1.0. Instead use the ' - 'pyramid.registry.registerUtility method (via ' - 'e.g. "config.registry.registerUtility(..)" ' - 'method in your unit and integration tests.') - -def registerAdapter(impl, for_=Interface, provides=Interface, name=''): - """ Register a ZCA adapter component. - - The ``impl`` argument specifies the implementation of the - component (often a class). The ``for_`` argument implies the - ``for`` interface type used for this registration; it is - :class:`zope.interface.Interface` by default. If ``for`` is not a - tuple or list, it will be converted to a one-tuple before being - passed to underlying :meth:`pyramid.registry.registerAdapter` - API. - - The ``provides`` argument specifies the ZCA 'provides' interface, - :class:`zope.interface.Interface` by default. - - The ``name`` argument is the empty string by default; it implies - the name under which the adapter is registered. - - See `The ZCA book <http://www.muthukadan.net/docs/zca.html>`_ for - more information about ZCA adapters. - - .. warning:: This API is deprecated as of :app:`Pyramid` 1.0. - Instead use the :meth:`pyramid.Registry.registerAdapter` - method. The ``registry`` attribute of a :term:`Configurator` - in your unit and integration tests is an instance of the - :class:`pyramid.Registry` class. - """ - reg = get_current_registry() - if not isinstance(for_, (tuple, list)): - for_ = (for_,) - reg.registerAdapter(impl, for_, provides, name=name) - return impl - -deprecated('registerAdapter', - 'The registerAdapter API is deprecated as of ' - 'Pyramid 1.0. Instead use the ' - 'pyramid.registry.registerAdapter method (via ' - 'e.g. "config.registry.registerAdapter(..)" ' - 'method in your unit and integration tests.') - -def registerSubscriber(subscriber, iface=Interface): - """ Register a ZCA subscriber component. - - The ``subscriber`` argument specifies the implementation of the - subscriber component (often a function). - - The ``iface`` argument is the interface type for which the - subscriber will be registered (:class:`zope.interface.Interface` - by default). If ``iface`` is not a tuple or list, it will be - converted to a one-tuple before being passed to the underlying ZCA - :meth:`pyramid.registry.registerHandler` method. - - See `The ZCA book <http://www.muthukadan.net/docs/zca.html>`_ for - more information about ZCA subscribers. - - .. warning:: This API is deprecated as of :app:`Pyramid` 1.0. - Instead use the - :meth:`pyramid.config.Configurator.add_subscriber` - method in your unit and integration tests. - """ - registry = get_current_registry() - config = Configurator(registry) - result = config.add_subscriber(subscriber, iface=iface) - config.commit() - return result - -deprecated('registerSubscriber', - 'The testing.registerSubscriber API is deprecated as of ' - 'Pyramid 1.0. Instead use the ' - 'pyramid.config.Configurator.add_subscriber ' - 'method in your unit and integration tests.') - -def registerRoute(pattern, name, factory=None): - """ Register a new :term:`route` using a pattern - (e.g. ``:pagename``), a name (e.g. ``home``), and an optional root - factory. - - The ``pattern`` argument implies the route pattern. The ``name`` - argument implies the route name. The ``factory`` argument implies - a :term:`root factory` associated with the route. - - This API is useful for testing code that calls - e.g. :func:`pyramid.url.route_url`. - - .. warning:: This API is deprecated as of :app:`Pyramid` 1.0. - Instead use the - :meth:`pyramid.config.Configurator.add_route` - method in your unit and integration tests. - """ - reg = get_current_registry() - config = Configurator(registry=reg) - config.setup_registry() - result = config.add_route(name, pattern, factory=factory) - config.commit() - return result - -deprecated('registerRoute', - 'The testing.registerRoute API is deprecated as of ' - 'Pyramid 1.0. Instead use the ' - 'pyramid.config.Configurator.add_route ' - 'method in your unit and integration tests.') - -def registerSettings(dictarg=None, **kw): - """Register one or more 'setting' key/value pairs. A setting is - a single key/value pair in the dictionary-ish object returned from - the API :attr:`pyramid.registry.Registry.settings`. - - You may pass a dictionary:: - - registerSettings({'external_uri':'http://example.com'}) - - Or a set of key/value pairs:: - - registerSettings(external_uri='http://example.com') - - Use of this function is required when you need to test code that calls - the :attr:`pyramid.registry.Registry.settings` API and which uses return - values from that API. - - .. warning:: This API is deprecated as of :app:`Pyramid` 1.0. - Instead use the - :meth:`pyramid.config.Configurator.add_settings` - method in your unit and integration tests. - """ - registry = get_current_registry() - config = Configurator(registry=registry) - config.add_settings(dictarg, **kw) - -deprecated('registerSettings', - 'The testing.registerSettings API is deprecated as of ' - 'Pyramid 1.0. Instead use the ' - 'pyramid.config.Configurator.add_settings ' - 'method in your unit and integration tests.') - class DummyRootFactory(object): __parent__ = None __name__ = None @@ -646,7 +274,7 @@ class DummySession(dict): @implementer(IRequest) class DummyRequest(DeprecatedRequestMethodsMixin, URLMethodsMixin, - CallbackMethodsMixin): + CallbackMethodsMixin, InstancePropertyMixin): """ A DummyRequest object (incompletely) imitates a :term:`request` object. The ``params``, ``environ``, ``headers``, ``path``, and diff --git a/pyramid/tests/fixtures/withmacro.pt b/pyramid/tests/fixtures/withmacro.pt index bb80475c0..8bca01e4d 100644 --- a/pyramid/tests/fixtures/withmacro.pt +++ b/pyramid/tests/fixtures/withmacro.pt @@ -1,3 +1,6 @@ +<html> <metal:m define-macro="foo"> Hello! </metal:m> +</html> + diff --git a/pyramid/tests/test_chameleon_text.py b/pyramid/tests/test_chameleon_text.py index 8d23c8753..297e96554 100644 --- a/pyramid/tests/test_chameleon_text.py +++ b/pyramid/tests/test_chameleon_text.py @@ -1,28 +1,33 @@ +import sys import unittest from pyramid.compat import binary_type from pyramid.testing import skip_on from pyramid import testing -class Base: +class Base(object): def setUp(self): self.config = testing.setUp() - from zope.deprecation import __show__ - __show__.off() def tearDown(self): testing.tearDown() - from zope.deprecation import __show__ - __show__.on() def _getTemplatePath(self, name): import os here = os.path.abspath(os.path.dirname(__file__)) return os.path.join(here, 'fixtures', name) - def _registerUtility(self, utility, iface, name=''): - reg = self.config.registry - reg.registerUtility(utility, iface, name=name) +class Test_renderer_factory(Base, unittest.TestCase): + def _callFUT(self, info): + from pyramid.chameleon_text import renderer_factory + return renderer_factory(info) + + def test_it(self): + # this test is way too functional + from pyramid.chameleon_text import TextTemplateRenderer + info = DummyInfo() + result = self._callFUT(info) + self.assertEqual(result.__class__, TextTemplateRenderer) class TextTemplateRendererTests(Base, unittest.TestCase): def _getTargetClass(self): @@ -127,83 +132,24 @@ class TextTemplateRendererTests(Base, unittest.TestCase): self.assertTrue(isinstance(result, binary_type)) self.assertEqual(result, b'Hello.\n') -class RenderTemplateTests(Base, unittest.TestCase): - def _callFUT(self, name, **kw): - from pyramid.chameleon_text import render_template - return render_template(name, **kw) - - @skip_on('java') - def test_it(self): - minimal = self._getTemplatePath('minimal.txt') - result = self._callFUT(minimal) - self.assertTrue(isinstance(result, binary_type)) - self.assertEqual(result, b'Hello.\n') - -class RenderTemplateToResponseTests(Base, unittest.TestCase): - def _callFUT(self, name, **kw): - from pyramid.chameleon_text import render_template_to_response - return render_template_to_response(name, **kw) - - @skip_on('java') - def test_minimal(self): - minimal = self._getTemplatePath('minimal.txt') - result = self._callFUT(minimal) - from webob import Response - self.assertTrue(isinstance(result, Response)) - self.assertEqual(result.app_iter, [b'Hello.\n']) - self.assertEqual(result.status, '200 OK') - self.assertEqual(len(result.headerlist), 2) - - @skip_on('java') - def test_iresponsefactory_override(self): - from webob import Response - class Response2(Response): - pass - from pyramid.interfaces import IResponseFactory - self._registerUtility(Response2, IResponseFactory) - minimal = self._getTemplatePath('minimal.txt') - result = self._callFUT(minimal) - self.assertTrue(isinstance(result, Response2)) - -class GetRendererTests(Base, unittest.TestCase): - def _callFUT(self, name): - from pyramid.chameleon_text import get_renderer - return get_renderer(name) - - @skip_on('java') - def test_it(self): - from pyramid.interfaces import IRendererFactory - class Dummy: - template = object() - def implementation(self): pass - renderer = Dummy() - def rf(spec): - return renderer - self._registerUtility(rf, IRendererFactory, name='foo') - result = self._callFUT('foo') - self.assertTrue(result is renderer) - -class GetTemplateTests(Base, unittest.TestCase): - def _callFUT(self, name): - from pyramid.chameleon_text import get_template - return get_template(name) - - @skip_on('java') - def test_it(self): - from pyramid.interfaces import IRendererFactory - class Dummy: - template = object() - def implementation(self): - return self.template - renderer = Dummy() - def rf(spec): - return renderer - self._registerUtility(rf, IRendererFactory, name='foo') - result = self._callFUT('foo') - self.assertTrue(result is renderer.template) - class DummyLookup(object): auto_reload=True debug = True def translate(self, msg): pass + +class DummyRegistry(object): + def queryUtility(self, iface, name): + self.queried = iface, name + return None + + def registerUtility(self, impl, iface, name): + self.registered = impl, iface, name + +class DummyInfo(object): + def __init__(self): + self.registry = DummyRegistry() + self.type = '.pt' + self.name = 'fixtures/minimal.pt' + self.package = sys.modules[__name__] + self.settings = {} diff --git a/pyramid/tests/test_chameleon_zpt.py b/pyramid/tests/test_chameleon_zpt.py index e7a1499e6..5d197dac4 100644 --- a/pyramid/tests/test_chameleon_zpt.py +++ b/pyramid/tests/test_chameleon_zpt.py @@ -1,3 +1,4 @@ +import sys import unittest from pyramid.testing import skip_on @@ -7,24 +8,27 @@ from pyramid.compat import text_type class Base(object): def setUp(self): self.config = testing.setUp() - from zope.deprecation import __show__ - __show__.off() def tearDown(self): testing.tearDown() - from zope.deprecation import __show__ - __show__.on() def _getTemplatePath(self, name): import os here = os.path.abspath(os.path.dirname(__file__)) return os.path.join(here, 'fixtures', name) - def _registerUtility(self, utility, iface, name=''): - reg = self.config.registry - reg.registerUtility(utility, iface, name=name) - return reg - +class Test_renderer_factory(Base, unittest.TestCase): + def _callFUT(self, info): + from pyramid.chameleon_zpt import renderer_factory + return renderer_factory(info) + + def test_it(self): + # this test is way too functional + from pyramid.chameleon_zpt import ZPTTemplateRenderer + info = DummyInfo() + result = self._callFUT(info) + self.assertEqual(result.__class__, ZPTTemplateRenderer) + class ZPTTemplateRendererTests(Base, unittest.TestCase): def _getTargetClass(self): from pyramid.chameleon_zpt import ZPTTemplateRenderer @@ -130,86 +134,35 @@ class ZPTTemplateRendererTests(Base, unittest.TestCase): self.assertTrue(isinstance(result, text_type)) self.assertEqual(result.rstrip('\n'), '<div xmlns="http://www.w3.org/1999/xhtml">\n</div>') - - -class RenderTemplateTests(Base, unittest.TestCase): - def _callFUT(self, name, **kw): - from pyramid.chameleon_zpt import render_template - return render_template(name, **kw) - - @skip_on('java') - def test_it(self): - minimal = self._getTemplatePath('minimal.pt') - result = self._callFUT(minimal) - self.assertTrue(isinstance(result, text_type)) - self.assertEqual(result.rstrip('\n'), - '<div xmlns="http://www.w3.org/1999/xhtml">\n</div>') - -class RenderTemplateToResponseTests(Base, unittest.TestCase): - def _callFUT(self, name, **kw): - from pyramid.chameleon_zpt import render_template_to_response - return render_template_to_response(name, **kw) - - @skip_on('java') - def test_it(self): - minimal = self._getTemplatePath('minimal.pt') - result = self._callFUT(minimal) - from webob import Response - self.assertTrue(isinstance(result, Response)) - self.assertEqual(result.app_iter[0].rstrip(b'\n'), - b'<div xmlns="http://www.w3.org/1999/xhtml">\n</div>') - self.assertEqual(result.status, '200 OK') - self.assertEqual(len(result.headerlist), 2) - - @skip_on('java') - def test_iresponsefactory_override(self): - from webob import Response - class Response2(Response): - pass - from pyramid.interfaces import IResponseFactory - self._registerUtility(Response2, IResponseFactory) - minimal = self._getTemplatePath('minimal.pt') - result = self._callFUT(minimal) - self.assertTrue(isinstance(result, Response2)) - -class GetRendererTests(Base, unittest.TestCase): - def _callFUT(self, name): - from pyramid.chameleon_zpt import get_renderer - return get_renderer(name) - - @skip_on('java') - def test_it(self): - from pyramid.interfaces import IRendererFactory - class Dummy: - template = object() - def implementation(self): pass - renderer = Dummy() - def rf(spec): - return renderer - self._registerUtility(rf, IRendererFactory, name='foo') - result = self._callFUT('foo') - self.assertTrue(result is renderer) - -class GetTemplateTests(Base, unittest.TestCase): - def _callFUT(self, name): - from pyramid.chameleon_zpt import get_template - return get_template(name) - @skip_on('java') - def test_it(self): - from pyramid.interfaces import IRendererFactory - class Dummy: - template = object() - def implementation(self): - return self.template - renderer = Dummy() - def rf(spec): - return renderer - self._registerUtility(rf, IRendererFactory, name='foo') - result = self._callFUT('foo') - self.assertTrue(result is renderer.template) + def test_macro_supplied(self): + minimal = self._getTemplatePath('withmacro.pt') + lookup = DummyLookup() + instance = self._makeOne(minimal, lookup, macro='foo') + result = instance.implementation()() + self.assertEqual(result, '\n Hello!\n') + + + class DummyLookup(object): auto_reload=True debug = True def translate(self, msg): pass + +class DummyRegistry(object): + def queryUtility(self, iface, name): + self.queried = iface, name + return None + + def registerUtility(self, impl, iface, name): + self.registered = impl, iface, name + +class DummyInfo(object): + def __init__(self): + self.registry = DummyRegistry() + self.type = '.pt' + self.name = 'fixtures/minimal.pt' + self.package = sys.modules[__name__] + self.settings = {} + diff --git a/pyramid/tests/test_config/test_factories.py b/pyramid/tests/test_config/test_factories.py index 38e80416f..e89fc077e 100644 --- a/pyramid/tests/test_config/test_factories.py +++ b/pyramid/tests/test_config/test_factories.py @@ -112,49 +112,19 @@ class TestFactoriesMixin(unittest.TestCase): config.set_request_property(bar, name='bar') self.assertRaises(ConfigurationConflictError, config.commit) - def test_set_request_property_subscriber(self): - from zope.interface import implementer - from pyramid.interfaces import INewRequest - config = self._makeOne() - def foo(r): pass - config.set_request_property(foo, name='foo') - config.set_request_property(foo, name='bar', reify=True) - config.commit() - @implementer(INewRequest) - class Event(object): - request = DummyRequest(config.registry) - event = Event() - config.registry.notify(event) - exts = list(sorted(event.request.extensions)) - self.assertEqual('bar', exts[0]) - self.assertEqual('foo', exts[1]) - - def test_set_request_method_subscriber(self): - from zope.interface import implementer - from pyramid.interfaces import INewRequest - config = self._makeOne(autocommit=True) - def foo(r): return 'bar' - config.set_request_method(foo, name='foo') - @implementer(INewRequest) - class Event(object): - request = DummyRequest(config.registry) - event = Event() - config.registry.notify(event) - self.assertEqual('bar', event.request.foo()) - - def test_set_request_method_with_callable(self): + def test_add_request_method_with_callable(self): from pyramid.interfaces import IRequestExtensions config = self._makeOne(autocommit=True) callable = lambda x: None - config.set_request_method(callable, name='foo') + config.add_request_method(callable, name='foo') exts = config.registry.getUtility(IRequestExtensions) self.assertTrue('foo' in exts.methods) - def test_set_request_method_with_unnamed_callable(self): + def test_add_request_method_with_unnamed_callable(self): from pyramid.interfaces import IRequestExtensions config = self._makeOne(autocommit=True) def foo(self): pass - config.set_request_method(foo) + config.add_request_method(foo) exts = config.registry.getUtility(IRequestExtensions) self.assertTrue('foo' in exts.methods) @@ -163,38 +133,27 @@ class TestFactoriesMixin(unittest.TestCase): config = self._makeOne() def foo(self): pass def bar(self): pass - config.set_request_method(foo, name='bar') - config.set_request_method(bar, name='bar') + config.add_request_method(foo, name='bar') + config.add_request_method(bar, name='bar') self.assertRaises(ConfigurationConflictError, config.commit) - def test_set_request_method_with_None_callable(self): + def test_add_request_method_with_None_callable(self): from pyramid.interfaces import IRequestExtensions config = self._makeOne(autocommit=True) - config.set_request_method(name='foo') + config.add_request_method(name='foo') exts = config.registry.queryUtility(IRequestExtensions) self.assertTrue(exts is None) - def test_set_request_method_with_None_callable_conflict(self): + def test_add_request_method_with_None_callable_conflict(self): from pyramid.exceptions import ConfigurationConflictError config = self._makeOne() def bar(self): pass - config.set_request_method(name='foo') - config.set_request_method(bar, name='foo') + config.add_request_method(name='foo') + config.add_request_method(bar, name='foo') self.assertRaises(ConfigurationConflictError, config.commit) - def test_set_request_method_with_None_callable_and_no_name(self): + def test_add_request_method_with_None_callable_and_no_name(self): config = self._makeOne(autocommit=True) - self.assertRaises(AttributeError, config.set_request_method) - - -class DummyRequest(object): - extensions = None - - def __init__(self, registry): - self.registry = registry + self.assertRaises(AttributeError, config.add_request_method) - def _set_properties(self, properties): - if self.extensions is None: - self.extensions = [] - self.extensions.extend(properties) diff --git a/pyramid/tests/test_config/test_settings.py b/pyramid/tests/test_config/test_settings.py index 0f5239915..c74f96375 100644 --- a/pyramid/tests/test_config/test_settings.py +++ b/pyramid/tests/test_config/test_settings.py @@ -47,6 +47,15 @@ class TestSettingsConfiguratorMixin(unittest.TestCase): settings = reg.getUtility(ISettings) self.assertEqual(settings['a'], 1) + def test_add_settings_settings_None(self): + from pyramid.registry import Registry + from pyramid.interfaces import ISettings + reg = Registry() + config = self._makeOne(reg) + config.add_settings(None, a=1) + settings = reg.getUtility(ISettings) + self.assertEqual(settings['a'], 1) + class TestSettings(unittest.TestCase): def _getTargetClass(self): diff --git a/pyramid/tests/test_configuration.py b/pyramid/tests/test_configuration.py deleted file mode 100644 index 0a98bcb5c..000000000 --- a/pyramid/tests/test_configuration.py +++ /dev/null @@ -1,31 +0,0 @@ -import unittest - -class ConfiguratorTests(unittest.TestCase): - def setUp(self): - from zope.deprecation import __show__ - __show__.off() - - def tearDown(self): - from zope.deprecation import __show__ - __show__.on() - - def _makeOne(self, *arg, **kw): - from pyramid.configuration import Configurator - return Configurator(*arg, **kw) - - def test_autocommit_true(self): - config = self._makeOne() - self.assertEqual(config.autocommit, True) - - def test_package_is_not_None(self): - import pyramid - config = self._makeOne(package='pyramid') - self.assertEqual(config.package, pyramid) - - def test_with_package(self): - import pyramid - config = self._makeOne() - newconfig = config.with_package('pyramid') - self.assertEqual(newconfig.package, pyramid) - - diff --git a/pyramid/tests/test_path.py b/pyramid/tests/test_path.py index ccc56fb0d..a07ebeffa 100644 --- a/pyramid/tests/test_path.py +++ b/pyramid/tests/test_path.py @@ -138,9 +138,9 @@ class TestPackageOf(unittest.TestCase): self.assertEqual(result, tests) def test_it_module(self): - import pyramid.tests.test_configuration + import pyramid.tests.test_path from pyramid import tests - package = DummyPackageOrModule(pyramid.tests.test_configuration) + package = DummyPackageOrModule(pyramid.tests.test_path) result = self._callFUT(package) self.assertEqual(result, tests) @@ -395,7 +395,7 @@ class TestDottedNameResolver(unittest.TestCase): self.assertEqual(result, self.__class__) def test__zope_dottedname_style_resolve_relative_leading_dots(self): - import pyramid.tests.test_configuration + import pyramid.tests.test_path typ = self._makeOne() result = typ._zope_dottedname_style( '..tests.test_path.TestDottedNameResolver', pyramid.tests) diff --git a/pyramid/tests/test_renderers.py b/pyramid/tests/test_renderers.py index cc3d73fb7..af9188abc 100644 --- a/pyramid/tests/test_renderers.py +++ b/pyramid/tests/test_renderers.py @@ -533,6 +533,7 @@ class TestRendererHelper(unittest.TestCase): def renderer(*arg): def respond(*arg): return arg + renderer.respond = respond return respond self.config.registry.registerUtility(renderer, IRendererFactory, name='.foo') @@ -554,6 +555,11 @@ class TestRendererHelper(unittest.TestCase): self.assertEqual(response.body[0], 'values') self.assertEqual(response.body[1], {}) + def test_get_renderer(self): + factory = self._registerRendererFactory() + helper = self._makeOne('loo.foo') + self.assertEqual(helper.get_renderer(), factory.respond) + def test_render_view(self): self._registerRendererFactory() self._registerResponseFactory() @@ -652,6 +658,14 @@ class TestRendererHelper(unittest.TestCase): response = helper._make_response(la.encode('utf-8'), request) self.assertEqual(response.body, la.encode('utf-8')) + def test__make_response_result_is_None(self): + from pyramid.response import Response + request = testing.DummyRequest() + request.response = Response() + helper = self._makeOne('loo.foo') + response = helper._make_response(None, request) + self.assertEqual(response.body, b'') + def test__make_response_with_content_type(self): from pyramid.response import Response request = testing.DummyRequest() diff --git a/pyramid/tests/test_request.py b/pyramid/tests/test_request.py index a95d614f9..86cfd8b09 100644 --- a/pyramid/tests/test_request.py +++ b/pyramid/tests/test_request.py @@ -549,18 +549,6 @@ class Test_call_app_with_subpath_as_path_info(unittest.TestCase): self.assertEqual(request.environ['SCRIPT_NAME'], '/' + encoded) self.assertEqual(request.environ['PATH_INFO'], '/' + encoded) - def test_it_removes_bfg_routes_info(self): - request = DummyRequest({}) - request.environ['bfg.routes.route'] = True - request.environ['bfg.routes.matchdict'] = True - response = self._callFUT(request, 'app') - self.assertTrue(request.copied) - self.assertEqual(response, 'app') - self.assertEqual(request.environ['SCRIPT_NAME'], '') - self.assertEqual(request.environ['PATH_INFO'], '/') - self.assertFalse('bfg.routes.route' in request.environ) - self.assertFalse('bfg.routes.matchdict' in request.environ) - class DummyRequest: def __init__(self, environ=None): if environ is None: diff --git a/pyramid/tests/test_router.py b/pyramid/tests/test_router.py index eb9b7285d..778b27473 100644 --- a/pyramid/tests/test_router.py +++ b/pyramid/tests/test_router.py @@ -312,6 +312,38 @@ class TestRouter(unittest.TestCase): self.assertEqual(app_iter, [b'abc']) self.assertEqual(start_response.status, '200 OK') + def test_call_with_request_extensions(self): + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IRequestExtensions + from pyramid.interfaces import IRequest + from pyramid.request import Request + context = DummyContext() + self._registerTraverserFactory(context) + class Extensions(object): + def __init__(self): + self.methods = {} + self.descriptors = {} + extensions = Extensions() + L = [] + request = Request.blank('/') + request.request_iface = IRequest + request.registry = self.registry + request._set_extensions = lambda *x: L.extend(x) + def request_factory(environ): + return request + self.registry.registerUtility(extensions, IRequestExtensions) + environ = self._makeEnviron() + response = DummyResponse() + response.app_iter = ['Hello world'] + view = DummyView(response) + self._registerView(self.config.derive_view(view), '', + IViewClassifier, None, None) + router = self._makeOne() + router.request_factory = request_factory + start_response = DummyStartResponse() + router(environ, start_response) + self.assertEqual(L, [extensions]) + def test_call_view_registered_nonspecific_default_path(self): from pyramid.interfaces import IViewClassifier context = DummyContext() @@ -646,8 +678,6 @@ class TestRouter(unittest.TestCase): self.assertEqual(request.context, context) self.assertEqual(request.root, root) matchdict = {'action':'action1', 'article':'article1'} - self.assertEqual(environ['bfg.routes.matchdict'], matchdict) - self.assertEqual(environ['bfg.routes.route'].name, 'foo') self.assertEqual(request.matchdict, matchdict) self.assertEqual(request.matched_route.name, 'foo') self.assertEqual(len(logger.messages), 1) @@ -712,8 +742,6 @@ class TestRouter(unittest.TestCase): self.assertEqual(request.context, context) self.assertEqual(request.root, root) matchdict = {'action':'action1', 'article':'article1'} - self.assertEqual(environ['bfg.routes.matchdict'], matchdict) - self.assertEqual(environ['bfg.routes.route'].name, 'foo') self.assertEqual(request.matchdict, matchdict) self.assertEqual(request.matched_route.name, 'foo') self.assertTrue(IFoo.providedBy(request)) diff --git a/pyramid/tests/test_scripting.py b/pyramid/tests/test_scripting.py index a99ae53fe..1ccc7af3b 100644 --- a/pyramid/tests/test_scripting.py +++ b/pyramid/tests/test_scripting.py @@ -5,31 +5,37 @@ class Test_get_root(unittest.TestCase): from pyramid.scripting import get_root return get_root(app, request) + def _makeRegistry(self): + return DummyRegistry([DummyFactory]) + def test_it_norequest(self): - app = DummyApp(registry=dummy_registry) + registry = self._makeRegistry() + app = DummyApp(registry=registry) root, closer = self._callFUT(app) self.assertEqual(len(app.threadlocal_manager.pushed), 1) pushed = app.threadlocal_manager.pushed[0] - self.assertEqual(pushed['registry'], dummy_registry) + self.assertEqual(pushed['registry'], registry) self.assertEqual(pushed['request'].registry, app.registry) self.assertEqual(len(app.threadlocal_manager.popped), 0) closer() self.assertEqual(len(app.threadlocal_manager.popped), 1) def test_it_withrequest(self): - app = DummyApp(registry=dummy_registry) + registry = self._makeRegistry() + app = DummyApp(registry=registry) request = DummyRequest({}) root, closer = self._callFUT(app, request) self.assertEqual(len(app.threadlocal_manager.pushed), 1) pushed = app.threadlocal_manager.pushed[0] - self.assertEqual(pushed['registry'], dummy_registry) + self.assertEqual(pushed['registry'], registry) self.assertEqual(pushed['request'], request) self.assertEqual(len(app.threadlocal_manager.popped), 0) closer() self.assertEqual(len(app.threadlocal_manager.popped), 1) def test_it_requestfactory_overridden(self): - app = DummyApp(registry=dummy_registry) + registry = self._makeRegistry() + app = DummyApp(registry=registry) root, closer = self._callFUT(app) self.assertEqual(len(app.threadlocal_manager.pushed), 1) pushed = app.threadlocal_manager.pushed[0] @@ -40,8 +46,10 @@ class Test_prepare(unittest.TestCase): from pyramid.scripting import prepare return prepare(request, registry) - def _makeRegistry(self): - return DummyRegistry(DummyFactory) + def _makeRegistry(self, L=None): + if L is None: + L = [None, DummyFactory] + return DummyRegistry(L) def setUp(self): from pyramid.threadlocal import manager @@ -53,21 +61,22 @@ class Test_prepare(unittest.TestCase): self.assertRaises(ConfigurationError, self._callFUT) def test_it_norequest(self): - registry = self._makeRegistry() + registry = self._makeRegistry([DummyFactory, None, DummyFactory]) info = self._callFUT(registry=registry) - root, closer = info['root'], info['closer'] + root, closer, request = info['root'], info['closer'], info['request'] pushed = self.manager.get() self.assertEqual(pushed['registry'], registry) self.assertEqual(pushed['request'].registry, registry) self.assertEqual(root.a, (pushed['request'],)) closer() self.assertEqual(self.default, self.manager.get()) + self.assertEqual(request.context, root) def test_it_withrequest(self): request = DummyRequest({}) registry = request.registry = self._makeRegistry() info = self._callFUT(request=request) - root, closer = info['root'], info['closer'] + root, closer, request = info['root'], info['closer'], info['request'] pushed = self.manager.get() self.assertEqual(pushed['request'], request) self.assertEqual(pushed['registry'], registry) @@ -75,12 +84,13 @@ class Test_prepare(unittest.TestCase): self.assertEqual(root.a, (request,)) closer() self.assertEqual(self.default, self.manager.get()) + self.assertEqual(request.context, root) def test_it_with_request_and_registry(self): request = DummyRequest({}) registry = request.registry = self._makeRegistry() info = self._callFUT(request=request, registry=registry) - root, closer = info['root'], info['closer'] + root, closer, root = info['root'], info['closer'], info['root'] pushed = self.manager.get() self.assertEqual(pushed['request'], request) self.assertEqual(pushed['registry'], registry) @@ -88,21 +98,44 @@ class Test_prepare(unittest.TestCase): self.assertEqual(root.a, (request,)) closer() self.assertEqual(self.default, self.manager.get()) + self.assertEqual(request.context, root) + + def test_it_with_request_context_already_set(self): + request = DummyRequest({}) + context = Dummy() + request.context = context + registry = request.registry = self._makeRegistry() + info = self._callFUT(request=request, registry=registry) + root, closer, root = info['root'], info['closer'], info['root'] + closer() + self.assertEqual(request.context, context) + + def test_it_with_extensions(self): + exts = Dummy() + request = DummyRequest({}) + registry = request.registry = self._makeRegistry([exts, DummyFactory]) + info = self._callFUT(request=request, registry=registry) + self.assertEqual(request.extensions, exts) + root, closer = info['root'], info['closer'] + closer() class Test__make_request(unittest.TestCase): def _callFUT(self, path='/', registry=None): from pyramid.scripting import _make_request return _make_request(path, registry) + def _makeRegistry(self): + return DummyRegistry([DummyFactory]) + def test_it_with_registry(self): - request = self._callFUT('/', dummy_registry) + registry = self._makeRegistry() + request = self._callFUT('/', registry) self.assertEqual(request.environ['path'], '/') - self.assertEqual(request.registry, dummy_registry) + self.assertEqual(request.registry, registry) def test_it_with_no_registry(self): from pyramid.config import global_registries - # keep registry local so that global_registries is cleared after - registry = DummyRegistry(DummyFactory) + registry = self._makeRegistry() global_registries.add(registry) try: request = self._callFUT('/hello') @@ -127,13 +160,13 @@ class DummyFactory(object): self.kw = kw class DummyRegistry(object): - def __init__(self, factory=None): - self.factory = factory + def __init__(self, utilities): + self.utilities = utilities - def queryUtility(self, iface, default=None): - return self.factory or default - -dummy_registry = DummyRegistry(DummyFactory) + def queryUtility(self, iface, default=None): # pragma: no cover + if self.utilities: + return self.utilities.pop(0) + return default class DummyApp: def __init__(self, registry=None): @@ -156,6 +189,10 @@ class DummyThreadLocalManager: self.popped.append(True) class DummyRequest: + matchdict = None + matched_route = None def __init__(self, environ): self.environ = environ - + + def _set_extensions(self, exts): + self.extensions = exts diff --git a/pyramid/tests/test_settings.py b/pyramid/tests/test_settings.py index 2ef15f62a..a586cb6fd 100644 --- a/pyramid/tests/test_settings.py +++ b/pyramid/tests/test_settings.py @@ -1,30 +1,4 @@ import unittest -from pyramid import testing - -class TestGetSettings(unittest.TestCase): - def setUp(self): - from pyramid.registry import Registry - registry = Registry('testing') - self.config = testing.setUp(registry=registry) - from zope.deprecation import __show__ - __show__.off() - - def tearDown(self): - self.config.end() - from zope.deprecation import __show__ - __show__.on() - - def _callFUT(self): - from pyramid.settings import get_settings - return get_settings() - - def test_it_nosettings(self): - self.assertEqual(self._callFUT()['reload_templates'], False) - - def test_it_withsettings(self): - settings = {'a':1} - self.config.registry.settings = settings - self.assertEqual(self._callFUT(), settings) class Test_asbool(unittest.TestCase): def _callFUT(self, s): diff --git a/pyramid/tests/test_testing.py b/pyramid/tests/test_testing.py index a9e50442f..339a39cd8 100644 --- a/pyramid/tests/test_testing.py +++ b/pyramid/tests/test_testing.py @@ -1,285 +1,4 @@ import unittest -from pyramid.compat import text_ - -class TestBase(unittest.TestCase): - def setUp(self): - from pyramid.threadlocal import manager - from pyramid.registry import Registry - manager.clear() - registry = Registry('testing') - self.registry = registry - manager.push({'registry':registry, 'request':None}) - from zope.deprecation import __show__ - __show__.off() - - def tearDown(self): - from pyramid.threadlocal import manager - manager.clear() - from zope.deprecation import __show__ - __show__.on() - -class Test_registerDummySecurityPolicy(TestBase): - def test_registerDummySecurityPolicy(self): - from pyramid import testing - testing.registerDummySecurityPolicy('user', ('group1', 'group2'), - permissive=False) - from pyramid.interfaces import IAuthenticationPolicy - from pyramid.interfaces import IAuthorizationPolicy - ut = self.registry.getUtility(IAuthenticationPolicy) - from pyramid.testing import DummySecurityPolicy - self.assertTrue(isinstance(ut, DummySecurityPolicy)) - ut = self.registry.getUtility(IAuthorizationPolicy) - self.assertEqual(ut.userid, 'user') - self.assertEqual(ut.groupids, ('group1', 'group2')) - self.assertEqual(ut.permissive, False) - -class Test_registerResources(TestBase): - def test_it(self): - class Dummy: - pass - ob1 = Dummy() - ob2 = Dummy() - resources = {'/ob1':ob1, '/ob2':ob2} - from pyramid import testing - testing.registerResources(resources) - from pyramid.interfaces import ITraverser - adapter = self.registry.getAdapter(None, ITraverser) - result = adapter(DummyRequest({'PATH_INFO':'/ob1'})) - self.assertEqual(result['context'], ob1) - self.assertEqual(result['view_name'], '') - self.assertEqual(result['subpath'], ()) - self.assertEqual(result['traversed'], (text_('ob1'),)) - self.assertEqual(result['virtual_root'], ob1) - self.assertEqual(result['virtual_root_path'], ()) - result = adapter(DummyRequest({'PATH_INFO':'/ob2'})) - self.assertEqual(result['context'], ob2) - self.assertEqual(result['view_name'], '') - self.assertEqual(result['subpath'], ()) - self.assertEqual(result['traversed'], (text_('ob2'),)) - self.assertEqual(result['virtual_root'], ob2) - self.assertEqual(result['virtual_root_path'], ()) - self.assertRaises(KeyError, adapter, DummyRequest({'PATH_INFO':'/ob3'})) - from pyramid.traversal import find_resource - self.assertEqual(find_resource(None, '/ob1'), ob1) - -class Test_registerTemplateRenderer(TestBase): - def test_registerTemplateRenderer(self): - from pyramid import testing - renderer = testing.registerTemplateRenderer('templates/foo') - from pyramid.testing import DummyTemplateRenderer - self.assertTrue(isinstance(renderer, DummyTemplateRenderer)) - from pyramid.renderers import render_to_response - render_to_response('templates/foo', dict(foo=1, bar=2)) - renderer.assert_(foo=1) - renderer.assert_(bar=2) - - def test_registerTemplateRenderer_explicitrenderer(self): - from pyramid import testing - def renderer(kw, system): - self.assertEqual(kw, {'foo':1, 'bar':2}) - renderer = testing.registerTemplateRenderer('templates/foo', renderer) - from pyramid.renderers import render_to_response - render_to_response('templates/foo', dict(foo=1, bar=2)) - -class Test_registerEventListener(TestBase): - def test_registerEventListener_single(self): - from pyramid import testing - L = testing.registerEventListener(IDummy) - event = DummyEvent() - self.registry.notify(event) - self.assertEqual(len(L), 1) - self.assertEqual(L[0], event) - self.registry.notify(object()) - self.assertEqual(len(L), 1) - - def test_registerEventListener_multiple(self): - from pyramid import testing - L = testing.registerEventListener((Interface, IDummy)) - event = DummyEvent() - event.object = 'foo' - # the below is the equivalent of z.c.event.objectEventNotify(event) - self.registry.subscribers((event.object, event), None) - self.assertEqual(len(L), 2) - self.assertEqual(L[0], 'foo') - self.assertEqual(L[1], event) - - def test_registerEventListener_defaults(self): - from pyramid import testing - L = testing.registerEventListener() - event = object() - self.registry.notify(event) - self.assertEqual(L[-1], event) - event2 = object() - self.registry.notify(event2) - self.assertEqual(L[-1], event2) - -class Test_registerView(TestBase): - def test_registerView_defaults(self): - from pyramid import testing - view = testing.registerView('moo.html') - import types - self.assertTrue(isinstance(view, types.FunctionType)) - from pyramid.view import render_view_to_response - request = DummyRequest() - request.registry = self.registry - response = render_view_to_response(None, request, 'moo.html') - self.assertEqual(view(None, None).body, response.body) - - def test_registerView_withresult(self): - from pyramid import testing - view = testing.registerView('moo.html', 'yo') - import types - self.assertTrue(isinstance(view, types.FunctionType)) - from pyramid.view import render_view_to_response - request = DummyRequest() - request.registry = self.registry - response = render_view_to_response(None, request, 'moo.html') - self.assertEqual(response.body, b'yo') - - def test_registerView_custom(self): - from pyramid import testing - def view(context, request): - from webob import Response - return Response('123') - view = testing.registerView('moo.html', view=view) - import types - self.assertTrue(isinstance(view, types.FunctionType)) - from pyramid.view import render_view_to_response - request = DummyRequest() - request.registry = self.registry - response = render_view_to_response(None, request, 'moo.html') - self.assertEqual(response.body, b'123') - - def test_registerView_with_permission_denying(self): - from pyramid import testing - from pyramid.httpexceptions import HTTPForbidden - def view(context, request): - """ """ - view = testing.registerView('moo.html', view=view, permission='bar') - testing.registerDummySecurityPolicy(permissive=False) - import types - self.assertTrue(isinstance(view, types.FunctionType)) - from pyramid.view import render_view_to_response - request = DummyRequest() - request.registry = self.registry - self.assertRaises(HTTPForbidden, render_view_to_response, - None, request, 'moo.html') - - def test_registerView_with_permission_denying2(self): - from pyramid import testing - from pyramid.security import view_execution_permitted - def view(context, request): - """ """ - view = testing.registerView('moo.html', view=view, permission='bar') - testing.registerDummySecurityPolicy(permissive=False) - import types - self.assertTrue(isinstance(view, types.FunctionType)) - result = view_execution_permitted(None, None, 'moo.html') - self.assertEqual(result, False) - - def test_registerView_with_permission_allowing(self): - from pyramid import testing - def view(context, request): - from webob import Response - return Response('123') - view = testing.registerView('moo.html', view=view, permission='bar') - testing.registerDummySecurityPolicy(permissive=True) - import types - self.assertTrue(isinstance(view, types.FunctionType)) - from pyramid.view import render_view_to_response - request = DummyRequest() - request.registry = self.registry - result = render_view_to_response(None, request, 'moo.html') - self.assertEqual(result.app_iter, [b'123']) - - -class Test_registerAdapter(TestBase): - def test_registerAdapter(self): - from zope.interface import Interface - class provides(Interface): - pass - class Provider: - pass - class for_(Interface): - pass - from pyramid import testing - testing.registerAdapter(Provider, (for_, for_), provides, name='foo') - adapter = self.registry.adapters.lookup( - (for_, for_), provides, name='foo') - self.assertEqual(adapter, Provider) - - def test_registerAdapter_notlist(self): - from zope.interface import Interface - class provides(Interface): - pass - class Provider: - pass - class for_(Interface): - pass - from pyramid import testing - testing.registerAdapter(Provider, for_, provides, name='foo') - adapter = self.registry.adapters.lookup( - (for_,), provides, name='foo') - self.assertEqual(adapter, Provider) - -class Test_registerUtility(TestBase): - def test_registerUtility(self): - from zope.interface import implementer - from zope.interface import Interface - class iface(Interface): - pass - @implementer(iface) - class impl: - def __call__(self): - return 'foo' - utility = impl() - from pyramid import testing - testing.registerUtility(utility, iface, name='mudge') - self.assertEqual(self.registry.getUtility(iface, name='mudge')(), 'foo') - -class Test_registerSubscriber(TestBase): - def test_it(self): - from pyramid import testing - L = [] - def subscriber(event): - L.append(event) - testing.registerSubscriber(subscriber, iface=IDummy) - event = DummyEvent() - self.registry.notify(event) - self.assertEqual(len(L), 1) - self.assertEqual(L[0], event) - self.registry.notify(object()) - self.assertEqual(len(L), 1) - -class Test_registerRoute(TestBase): - def test_registerRoute(self): - from pyramid.config import Configurator - from pyramid.request import Request - from pyramid.interfaces import IRoutesMapper - from pyramid.testing import registerRoute - registerRoute(':pagename', 'home', DummyFactory) - mapper = self.registry.getUtility(IRoutesMapper) - self.assertEqual(len(mapper.routelist), 1) - request = Request.blank('/') - request.registry = self.registry - config = Configurator(registry=self.registry) - config.setup_registry() - self.assertEqual(request.route_url('home', pagename='abc'), - 'http://localhost/abc') - -class Test_registerSettings(TestBase): - def test_registerSettings(self): - from pyramid.interfaces import ISettings - from pyramid.testing import registerSettings - registerSettings({'a':1, 'b':2}) - settings = self.registry.getUtility(ISettings) - self.assertEqual(settings['a'], 1) - self.assertEqual(settings['b'], 2) - registerSettings(b=3, c=4) - settings = self.registry.getUtility(ISettings) - self.assertEqual(settings['a'], 1) - self.assertEqual(settings['b'], 3) - self.assertEqual(settings['c'], 4) class TestDummyRootFactory(unittest.TestCase): def _makeOne(self, environ): @@ -913,14 +632,6 @@ class IDummy(Interface): class DummyEvent: pass - -class DummyRequest: - application_url = 'http://example.com' - def __init__(self, environ=None): - if environ is None: - environ = {} - self.environ = environ - class DummyFactory: def __init__(self, environ): """ """ diff --git a/pyramid/tests/test_traversal.py b/pyramid/tests/test_traversal.py index 8e0bb2494..2e45ae1a9 100644 --- a/pyramid/tests/test_traversal.py +++ b/pyramid/tests/test_traversal.py @@ -128,10 +128,23 @@ class ResourceTreeTraverserTests(unittest.TestCase): context = DummyContext() verifyObject(ITraverser, self._makeOne(context)) - def test_call_with_no_pathinfo(self): + def test_call_with_empty_pathinfo(self): policy = self._makeOne(None) environ = self._getEnviron() - request = DummyRequest(environ) + request = DummyRequest(environ, path_info='') + result = policy(request) + self.assertEqual(result['context'], None) + self.assertEqual(result['view_name'], '') + self.assertEqual(result['subpath'], ()) + self.assertEqual(result['traversed'], ()) + self.assertEqual(result['root'], policy.root) + self.assertEqual(result['virtual_root'], policy.root) + self.assertEqual(result['virtual_root_path'], ()) + + def test_call_with_pathinfo_KeyError(self): + policy = self._makeOne(None) + environ = self._getEnviron() + request = DummyRequest(environ, toraise=KeyError) result = policy(request) self.assertEqual(result['context'], None) self.assertEqual(result['view_name'], '') @@ -142,31 +155,25 @@ class ResourceTreeTraverserTests(unittest.TestCase): self.assertEqual(result['virtual_root_path'], ()) def test_call_with_pathinfo_highorder(self): - foo = DummyContext(None, text_(b'Qu\xc3\xa9bec', 'utf-8')) + path = text_(b'/Qu\xc3\xa9bec', 'utf-8') + foo = DummyContext(None, path) root = DummyContext(foo, 'root') policy = self._makeOne(root) - if PY3: # pragma: no cover - path_info = b'/Qu\xc3\xa9bec'.decode('latin-1') - else: - path_info = b'/Qu\xc3\xa9bec' - environ = self._getEnviron(PATH_INFO=path_info) - request = DummyRequest(environ) + environ = self._getEnviron() + request = DummyRequest(environ, path_info=path) 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['traversed'], (path[1:],)) 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') - request = DummyRequest(environ) + environ = self._getEnviron() + request = DummyRequest(environ, path_info=text_('/foo/bar')) result = policy(request) self.assertEqual(result['context'], None) self.assertEqual(result['view_name'], 'foo') @@ -179,8 +186,8 @@ class ResourceTreeTraverserTests(unittest.TestCase): def test_call_withconn_getitem_emptypath_nosubpath(self): root = DummyContext() policy = self._makeOne(root) - environ = self._getEnviron(PATH_INFO='') - request = DummyRequest(environ) + environ = self._getEnviron() + request = DummyRequest(environ, path_info=text_('')) result = policy(request) self.assertEqual(result['context'], root) self.assertEqual(result['view_name'], '') @@ -194,8 +201,8 @@ class ResourceTreeTraverserTests(unittest.TestCase): foo = DummyContext() root = DummyContext(foo) policy = self._makeOne(root) - environ = self._getEnviron(PATH_INFO='/foo/bar') - request = DummyRequest(environ) + environ = self._getEnviron() + request = DummyRequest(environ, path_info=text_('/foo/bar')) result = policy(request) self.assertEqual(result['context'], foo) self.assertEqual(result['view_name'], 'bar') @@ -209,8 +216,8 @@ class ResourceTreeTraverserTests(unittest.TestCase): foo = DummyContext() root = DummyContext(foo) policy = self._makeOne(root) - environ = self._getEnviron(PATH_INFO='/foo/bar/baz/buz') - request = DummyRequest(environ) + environ = self._getEnviron() + request = DummyRequest(environ, path_info=text_('/foo/bar/baz/buz')) result = policy(request) self.assertEqual(result['context'], foo) self.assertEqual(result['view_name'], 'bar') @@ -224,8 +231,8 @@ class ResourceTreeTraverserTests(unittest.TestCase): foo = DummyContext() root = DummyContext(foo) policy = self._makeOne(root) - environ = self._getEnviron(PATH_INFO='/@@foo') - request = DummyRequest(environ) + environ = self._getEnviron() + request = DummyRequest(environ, path_info=text_('/@@foo')) result = policy(request) self.assertEqual(result['context'], root) self.assertEqual(result['view_name'], 'foo') @@ -236,14 +243,13 @@ class ResourceTreeTraverserTests(unittest.TestCase): self.assertEqual(result['virtual_root_path'], ()) def test_call_with_vh_root(self): - environ = self._getEnviron(PATH_INFO='/baz', - HTTP_X_VHM_ROOT='/foo/bar') + environ = self._getEnviron(HTTP_X_VHM_ROOT='/foo/bar') baz = DummyContext(None, 'baz') bar = DummyContext(baz, 'bar') foo = DummyContext(bar, 'foo') root = DummyContext(foo, 'root') policy = self._makeOne(root) - request = DummyRequest(environ) + request = DummyRequest(environ, path_info=text_('/baz')) result = policy(request) self.assertEqual(result['context'], baz) self.assertEqual(result['view_name'], '') @@ -256,14 +262,13 @@ class ResourceTreeTraverserTests(unittest.TestCase): (text_('foo'), text_('bar'))) def test_call_with_vh_root2(self): - environ = self._getEnviron(PATH_INFO='/bar/baz', - HTTP_X_VHM_ROOT='/foo') + environ = self._getEnviron(HTTP_X_VHM_ROOT='/foo') baz = DummyContext(None, 'baz') bar = DummyContext(baz, 'bar') foo = DummyContext(bar, 'foo') root = DummyContext(foo, 'root') policy = self._makeOne(root) - request = DummyRequest(environ) + request = DummyRequest(environ, path_info=text_('/bar/baz')) result = policy(request) self.assertEqual(result['context'], baz) self.assertEqual(result['view_name'], '') @@ -275,14 +280,13 @@ class ResourceTreeTraverserTests(unittest.TestCase): self.assertEqual(result['virtual_root_path'], (text_('foo'),)) def test_call_with_vh_root3(self): - environ = self._getEnviron(PATH_INFO='/foo/bar/baz', - HTTP_X_VHM_ROOT='/') + environ = self._getEnviron(HTTP_X_VHM_ROOT='/') baz = DummyContext() bar = DummyContext(baz) foo = DummyContext(bar) root = DummyContext(foo) policy = self._makeOne(root) - request = DummyRequest(environ) + request = DummyRequest(environ, path_info=text_('/foo/bar/baz')) result = policy(request) self.assertEqual(result['context'], baz) self.assertEqual(result['view_name'], '') @@ -294,14 +298,13 @@ class ResourceTreeTraverserTests(unittest.TestCase): self.assertEqual(result['virtual_root_path'], ()) def test_call_with_vh_root4(self): - environ = self._getEnviron(PATH_INFO='/', - HTTP_X_VHM_ROOT='/foo/bar/baz') + environ = self._getEnviron(HTTP_X_VHM_ROOT='/foo/bar/baz') baz = DummyContext(None, 'baz') bar = DummyContext(baz, 'bar') foo = DummyContext(bar, 'foo') root = DummyContext(foo, 'root') policy = self._makeOne(root) - request = DummyRequest(environ) + request = DummyRequest(environ, path_info=text_('/')) result = policy(request) self.assertEqual(result['context'], baz) self.assertEqual(result['view_name'], '') @@ -315,9 +318,8 @@ class ResourceTreeTraverserTests(unittest.TestCase): def test_call_with_vh_root_path_root(self): policy = self._makeOne(None) - environ = self._getEnviron(HTTP_X_VHM_ROOT='/', - PATH_INFO='/') - request = DummyRequest(environ) + environ = self._getEnviron(HTTP_X_VHM_ROOT='/') + request = DummyRequest(environ, path_info=text_('/')) result = policy(request) self.assertEqual(result['context'], None) self.assertEqual(result['view_name'], '') @@ -328,57 +330,48 @@ class ResourceTreeTraverserTests(unittest.TestCase): self.assertEqual(result['virtual_root_path'], ()) def test_call_with_vh_root_highorder(self): + path = text_(b'Qu\xc3\xa9bec', 'utf-8') bar = DummyContext(None, 'bar') - foo = DummyContext(bar, text_(b'Qu\xc3\xa9bec', 'utf-8')) + foo = DummyContext(bar, path) root = DummyContext(foo, 'root') policy = self._makeOne(root) if PY3: # pragma: no cover 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, - PATH_INFO='/bar') - request = DummyRequest(environ) + environ = self._getEnviron(HTTP_X_VHM_ROOT=vhm_root) + request = DummyRequest(environ, path_info=text_('/bar')) 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'), text_('bar')) + (path, text_('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'),) + (path,) ) - def test_non_utf8_path_segment_unicode_path_segments_fails(self): - from pyramid.exceptions import URLDecodeError - foo = DummyContext() - root = DummyContext(foo) - policy = self._makeOne(root) - segment = native_(text_(b'LaPe\xc3\xb1a', 'utf-8'), 'utf-16') - environ = self._getEnviron(PATH_INFO='/%s' % segment) - request = DummyRequest(environ) - self.assertRaises(URLDecodeError, policy, request) - - def test_non_utf8_path_segment_settings_unicode_path_segments_fails(self): + def test_path_info_raises_unicodedecodeerror(self): from pyramid.exceptions import URLDecodeError foo = DummyContext() root = DummyContext(foo) policy = self._makeOne(root) - segment = native_(text_(b'LaPe\xc3\xb1a', 'utf-8'), 'utf-16') - environ = self._getEnviron(PATH_INFO='/%s' % segment) - request = DummyRequest(environ) + environ = self._getEnviron() + toraise = UnicodeDecodeError('ascii', b'a', 2, 3, '5') + request = DummyRequest(environ, toraise=toraise) + request.matchdict = None self.assertRaises(URLDecodeError, policy, request) def test_withroute_nothingfancy(self): resource = DummyContext() traverser = self._makeOne(resource) - environ = {'bfg.routes.matchdict': {}} - request = DummyRequest(environ) + request = DummyRequest({}) + request.matchdict = {} result = traverser(request) self.assertEqual(result['context'], resource) self.assertEqual(result['view_name'], '') @@ -391,8 +384,9 @@ class ResourceTreeTraverserTests(unittest.TestCase): def test_withroute_with_subpath_string(self): resource = DummyContext() traverser = self._makeOne(resource) - environ = {'bfg.routes.matchdict': {'subpath':'/a/b/c'}} - request = DummyRequest(environ) + matchdict = {'subpath':'/a/b/c'} + request = DummyRequest({}) + request.matchdict = matchdict result = traverser(request) self.assertEqual(result['context'], resource) self.assertEqual(result['view_name'], '') @@ -405,8 +399,9 @@ class ResourceTreeTraverserTests(unittest.TestCase): def test_withroute_with_subpath_tuple(self): resource = DummyContext() traverser = self._makeOne(resource) - environ = {'bfg.routes.matchdict': {'subpath':('a', 'b', 'c')}} - request = DummyRequest(environ) + matchdict = {'subpath':('a', 'b', 'c')} + request = DummyRequest({}) + request.matchdict = matchdict result = traverser(request) self.assertEqual(result['context'], resource) self.assertEqual(result['view_name'], '') @@ -419,8 +414,9 @@ class ResourceTreeTraverserTests(unittest.TestCase): def test_withroute_and_traverse_string(self): resource = DummyContext() traverser = self._makeOne(resource) - environ = {'bfg.routes.matchdict': {'traverse':'foo/bar'}} - request = DummyRequest(environ) + matchdict = {'traverse':text_('foo/bar')} + request = DummyRequest({}) + request.matchdict = matchdict result = traverser(request) self.assertEqual(result['context'], resource) self.assertEqual(result['view_name'], 'foo') @@ -433,8 +429,9 @@ class ResourceTreeTraverserTests(unittest.TestCase): def test_withroute_and_traverse_tuple(self): resource = DummyContext() traverser = self._makeOne(resource) - environ = {'bfg.routes.matchdict': {'traverse':('foo', 'bar')}} - request = DummyRequest(environ) + matchdict = {'traverse':('foo', 'bar')} + request = DummyRequest({}) + request.matchdict = matchdict result = traverser(request) self.assertEqual(result['context'], resource) self.assertEqual(result['view_name'], 'foo') @@ -447,8 +444,9 @@ class ResourceTreeTraverserTests(unittest.TestCase): def test_withroute_and_traverse_empty(self): resource = DummyContext() traverser = self._makeOne(resource) - environ = {'bfg.routes.matchdict': {'traverse':''}} - request = DummyRequest(environ) + matchdict = {'traverse':''} + request = DummyRequest({}) + request.matchdict = matchdict result = traverser(request) self.assertEqual(result['context'], resource) self.assertEqual(result['view_name'], '') @@ -458,21 +456,6 @@ class ResourceTreeTraverserTests(unittest.TestCase): self.assertEqual(result['virtual_root'], resource) self.assertEqual(result['virtual_root_path'], ()) - def test_call_with_environ(self): - with warnings.catch_warnings(record=True) as w: - warnings.filterwarnings('always') - policy = self._makeOne(None) - environ = self._getEnviron() - result = policy(environ) - self.assertEqual(result['context'], None) - self.assertEqual(result['view_name'], '') - self.assertEqual(result['subpath'], ()) - self.assertEqual(result['traversed'], ()) - self.assertEqual(result['root'], policy.root) - self.assertEqual(result['virtual_root'], policy.root) - self.assertEqual(result['virtual_root_path'], ()) - self.assertEqual(len(w), 1) - class FindInterfaceTests(unittest.TestCase): def _callFUT(self, context, iface): from pyramid.traversal import find_interface @@ -1310,11 +1293,28 @@ class DummyContext(object): return '<DummyContext with name %s at id %s>'%(self.__name__, id(self)) class DummyRequest: + application_url = 'http://example.com:5432' # app_url never ends with slash - def __init__(self, environ=None): + matchdict = None + matched_route = None + + def __init__(self, environ=None, path_info=text_('/'), toraise=None): if environ is None: environ = {} self.environ = environ + self._set_path_info(path_info) + self.toraise = toraise + + def _get_path_info(self): + if self.toraise: + raise self.toraise + return self._path_info + + def _set_path_info(self, v): + self._path_info = v + + path_info = property(_get_path_info, _set_path_info) + class DummyContextURL: def __init__(self, context, request): diff --git a/pyramid/tests/test_util.py b/pyramid/tests/test_util.py index e83ad5922..3d85e18f5 100644 --- a/pyramid/tests/test_util.py +++ b/pyramid/tests/test_util.py @@ -132,6 +132,20 @@ class Test_InstancePropertyMixin(unittest.TestCase): self.assertEqual(1, foo.x) self.assertEqual(2, foo.y) + def test__set_extensions(self): + inst = self._makeOne() + def foo(self, result): + return result + n, bar = inst._make_property(lambda _: 'bar', name='bar') + class Extensions(object): + def __init__(self): + self.methods = {'foo':foo} + self.descriptors = {'bar':bar} + extensions = Extensions() + inst._set_extensions(extensions) + self.assertEqual(inst.bar, 'bar') + self.assertEqual(inst.foo('abc'), 'abc') + class Test_WeakOrderedSet(unittest.TestCase): def _makeOne(self): from pyramid.config import WeakOrderedSet diff --git a/pyramid/traversal.py b/pyramid/traversal.py index b514d4c16..6832ce69a 100644 --- a/pyramid/traversal.py +++ b/pyramid/traversal.py @@ -614,6 +614,7 @@ else: _segment_cache[(segment, safe)] = result return result +slash = text_('/') @implementer(ITraverser) class ResourceTreeTraverser(object): @@ -629,27 +630,17 @@ class ResourceTreeTraverser(object): self.root = root def __call__(self, request): - try: - environ = request.environ - except AttributeError: - # In BFG 1.0 and before, this API expected an environ - # rather than a request; some bit of code may still be - # passing us an environ. If so, deal. - environ = request - depwarn = ('Passing an environ dictionary directly to a traverser ' - 'is deprecated in Pyramid 1.1. Pass a request object ' - 'instead.') - warnings.warn(depwarn, DeprecationWarning, 2) - - if 'bfg.routes.matchdict' in environ: - matchdict = environ['bfg.routes.matchdict'] - - path = matchdict.get('traverse', '/') or '/' + environ = request.environ + matchdict = request.matchdict + + if matchdict is not None: + + path = matchdict.get('traverse', slash) or slash if is_nonstr_iter(path): # this is a *traverse stararg (not a {traverse}) # routing has already decoded these elements, so we just # need to join them - path = '/'.join(path) or '/' + path = slash.join(path) or slash subpath = matchdict.get('subpath', ()) if not is_nonstr_iter(subpath): @@ -663,9 +654,10 @@ class ResourceTreeTraverser(object): subpath = () try: # empty if mounted under a path in mod_wsgi, for example - path = decode_path_info(environ['PATH_INFO'] or '/') + path = request.path_info or slash except KeyError: - path = '/' + # if environ['PATH_INFO'] is just not there + path = slash except UnicodeDecodeError as e: raise URLDecodeError(e.encoding, e.object, e.start, e.end, e.reason) @@ -684,7 +676,7 @@ class ResourceTreeTraverser(object): root = self.root ob = vroot = root - if vpath == '/': # invariant: vpath must not be empty + if vpath == slash: # invariant: vpath must not be empty # prevent a call to traversal_path if we know it's going # to return the empty tuple vpath_tuple = () diff --git a/pyramid/util.py b/pyramid/util.py index dabd84695..6190e8156 100644 --- a/pyramid/util.py +++ b/pyramid/util.py @@ -2,6 +2,7 @@ import inspect import weakref from pyramid.compat import ( + iteritems_, integer_types, string_types, text_, @@ -74,6 +75,12 @@ class InstancePropertyMixin(object): cls = type(parent.__name__, (parent, object), attrs) self.__class__ = cls + def _set_extensions(self, extensions): + for name, fn in iteritems_(extensions.methods): + method = fn.__get__(self, self.__class__) + setattr(self, name, method) + self._set_properties(extensions.descriptors) + def set_property(self, callable, name=None, reify=False): """ Add a callable or a property descriptor to the instance. @@ -39,7 +39,7 @@ install_requires=[ 'setuptools', 'Chameleon >= 1.2.3', 'Mako >= 0.3.6', # strict_undefined - 'WebOb >= 1.2dev', # response.text / py3 compat + 'WebOb >= 1.2b3', # request.path_info is unicode 'repoze.lru >= 0.4', # py3 compat 'zope.interface >= 3.8.0', # has zope.interface.registry 'zope.deprecation >= 3.5.0', # py3 compat @@ -4,45 +4,16 @@ envlist = [testenv] commands = + python setup.py dev python setup.py test -q -deps = - zope.component - Sphinx - repoze.sphinx.autointerface - WebTest - virtualenv - venusian - -[testenv:py32] -commands = - python setup.py test -q -deps = - WebTest - virtualenv - venusian - -[testenv:py33] -commands = - python setup.py test -q -deps = - WebTest - virtualenv - venusian [testenv:cover] basepython = python2.6 commands = + python setup.py dev python setup.py nosetests --with-xunit --with-xcoverage deps = - zope.component - Sphinx - WebTest - repoze.sphinx.autointerface - virtualenv - venusian - nose - coverage nosexcover # we separate coverage into its own testenv because a) "last run wins" wrt |
