diff options
109 files changed, 4037 insertions, 2647 deletions
diff --git a/.travis.yml b/.travis.yml index 2e737af04..ab9c3ff30 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,8 +6,8 @@ python: - pypy - 3.2 -matrix: - allow_failures: - - python: pypy - script: python setup.py test + +notifications: + email: + - pyramid-checkins@lists.repoze.org diff --git a/CHANGES.txt b/CHANGES.txt index 7c2af4451..da2399d9b 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,6 +1,7 @@ Next release ============ + Bug Fixes --------- @@ -9,9 +10,66 @@ Bug Fixes return the empty list. This was incorrect, it should have unconditionally returned ``[Everyone]``, and now does. +- Explicit url dispatch regexes can now contain colons. + https://github.com/Pylons/pyramid/issues/629 + +- On at least one 64-bit Ubuntu system under Python 3.2, using the + ``view_config`` decorator caused a ``RuntimeError: dictionary changed size + during iteration`` exception. It no longer does. See + https://github.com/Pylons/pyramid/issues/635 for more information. + +- In Mako Templates lookup, check if the uri is already adjusted and bring + it back to an asset spec. Normally occurs with inherited templates or + included components. + https://github.com/Pylons/pyramid/issues/606 + https://github.com/Pylons/pyramid/issues/607 + +- In Mako Templates lookup, check for absolute uri (using mako directories) + when mixing up inheritance with asset specs. + https://github.com/Pylons/pyramid/issues/662 + +- HTTP Accept headers were not being normalized causing potentially + conflicting view registrations to go unnoticed. Two views that only + differ in the case ('text/html' vs. 'text/HTML') will now raise an error. + https://github.com/Pylons/pyramid/pull/620 + +- Configurator.add_directive now accepts arbitrary callables like partials or + objects implementing ``__call__`` which dont have ``__name__`` and + ``__doc__`` attributes. See https://github.com/Pylons/pyramid/issues/621 + and https://github.com/Pylons/pyramid/pull/647. + +- Forward-port from 1.3 branch: when registering multiple views with an + ``accept`` predicate in a Pyramid application runing under Python 3, you + might have received a ``TypeError: unorderable types: function() < + function()`` exception. + Features -------- +- Third-party custom view, route, and subscriber predicates can now be added + for use by view authors via + ``pyramid.config.Configurator.add_view_predicate``, + ``pyramid.config.Configurator.add_route_predicate`` and + ``pyramid.config.Configurator.add_subscriber_predicate``. So, for example, + doing this:: + + config.add_view_predicate('abc', my.package.ABCPredicate) + + Might allow a view author to do this in an application that configured that + predicate:: + + @view_config(abc=1) + + Similar features exist for ``add_route``, and ``add_subscriber``. See + "Adding A Third Party View, Route, or Subscriber Predicate" in the Hooks + chapter for more information. + + Note that changes made to support the above feature now means that only + actions registered using the same "order" can conflict with one another. + It used to be the case that actions registered at different orders could + potentially conflict, but to my knowledge nothing ever depended on this + behavior (it was a bit silly). + - Custom objects can be made easily JSON-serializable in Pyramid by defining a ``__json__`` method on the object's class. This method should return values natively serializable by ``json.dumps`` (such as ints, lists, @@ -29,15 +87,183 @@ 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_property`` now causes less code to be executed at - request construction time. +- ``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 + request construction time than ``config.set_request_property`` in + version 1.3. -- Don't add a ``?`` to URLs generated by request.resource_url if the +- Don't add a ``?`` to URLs generated by ``request.resource_url`` if the ``query`` argument is provided but empty. -- Don't add a ``?`` to URLs generated by request.route_url if the +- Don't add a ``?`` to URLs generated by ``request.route_url`` if the ``_query`` argument is provided but empty. - The static view machinery now raises (rather than returns) ``HTTPNotFound`` and ``HTTPMovedPermanently`` exceptions, so these can be caught by the NotFound view (and other exception views). + +- The Mako renderer now accepts a def name in an asset spect. When the def + name is present in the asset spec, the system will render the template def + within the template and will return the result. An example asset spec is + ``package:path/to/template#defname.mako``. This will render the def named + ``defname`` inside the ``template.pt`` package instead of rendering the + entire template. The old way of returning a tuple from the view is + supported for backward compatibility, ('defname', {}). + +- The Chameleon ZPT renderer now accepts a macro name in an asset spec. When + the macro name is present in the asset spec, the system will render the + macro listed as a ``define-macro`` and return the result instead of + rendering the entire template. An example asset spec: + ``package:path/to/template#macroname.pt``. This will render the macro + defined as ``macroname`` within the ``template.pt`` template instead of the + entire templae. + +- When there is a predicate mismatch exception (seen when no view matches for + a given request due to predicates not working), the exception now contains + a textual description of the predicate which didn't match. + +- An ``add_permission`` directive method was added to the Configurator. This + directive registers a free-standing permission introspectable into the + Pyramid introspection system. Frameworks built atop Pyramid can thus use + the the ``permissions`` introspectable category data to build a + comprehensive list of permissions supported by a running system. Before + this method was added, permissions were already registered in this + introspectable category as a side effect of naming them in an ``add_view`` + call, this method just makes it possible to arrange for a permission to be + put into the ``permissions`` introspectable category without naming it + along with an associated view. Here's an example of usage of + ``add_permission``:: + + config = Configurator() + config.add_permission('view') + +- The ``UnencryptedCookieSessionFactoryConfig`` now accepts + ``signed_serialize`` and ``signed_deserialize`` hooks which may be used + 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. + +- The ``pyramid.decorator.reify`` function is now an API, and was added to + the API documentation. + +- Added the ``pyramid.testing.testConfig`` context manager, which can be used + to generate a configurator in a test, e.g. ``with testing.testConfig(...):``. + +Deprecations +------------ + +- The ``pyramid.config.Configurator.set_request_property`` has been + documentation-deprecated. The method remains usable but the more + 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 and + how to show Pyramid-generated deprecation warnings while running tests and + while running a server. + +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. + diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 65ff1b2f2..e2a6ec018 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -173,6 +173,17 @@ Contributors - Marin Rukavina, 2012/05/03 +- Lorenzo M. Catucci, 2012/06/08 + - Marc Abramowitz, 2012/06/13 - Brian Sutherland, 2012/06/16 + +- Jeff Cook, 2012/06/16 + +- Ian Wilson, 2012/06/17 + +- Roman Kozlovskyi, 2012/08/11 + +- Domen Kozar, 2012/09/11 + diff --git a/HACKING.txt b/HACKING.txt index dd735bf22..87d1422dc 100644 --- a/HACKING.txt +++ b/HACKING.txt @@ -30,11 +30,13 @@ checkout. $ env/bin/easy_install setuptools-git - Install Pyramid from the checkout into the virtualenv using ``setup.py - develop`` (running ``setup.py develop`` *must* be done while the current - working directory is the ``pyramid`` checkout directory):: + dev``. ``setup.py dev`` is an alias for "setup.py develop" which also + installs testing requirements such as nose and coverage. Running + ``setup.py dev`` *must* be done while the current working directory is the + ``pyramid`` checkout directory:: $ cd pyramid - $ ../env/bin/python setup.py develop + $ ../env/bin/python setup.py dev - At that point, you should be able to create new Pyramid projects by using ``pcreate``:: @@ -85,6 +87,9 @@ Coding Style 2 newlines between classes. But 80-column lines, in particular, are mandatory. +- Please do not remove trailing whitespace. Configure your editor to reduce + diff noise. + Running Tests -------------- @@ -110,8 +115,8 @@ Test Coverage - The codebase *must* have 100% test statement coverage after each commit. You can test coverage via ``tox -e coverage``, or alternately by installing - ``nose`` and ``coverage`` into your virtualenv, and running ``setup.py - nosetests --with-coverage``. + ``nose`` and ``coverage`` into your virtualenv (easiest via ``setup.py + dev``) , and running ``setup.py nosetests --with-coverage``. Documentation Coverage and Building HTML Documentation ------------------------------------------------------ @@ -127,13 +132,19 @@ using to develop Pyramid): 1. Run ``$yourvenv/bin/python setup.py dev docs``. This will cause Sphinx and all development requirements to be installed in your virtualenv. -2. cd to the ``docs`` directory within your Pyramid checkout and execute +2. Update all git submodules from the top-level of your Pyramid checkout, like + so: + git submodule update --init --recursive + This will checkout theme subrepositories and prevent error conditions when + HTML docs are generated. + +3. cd to the ``docs`` directory within your Pyramid checkout and execute ``make clean html SPHINXBUILD=$yourvenv/bin/sphinx-build``. The ``SPHINXBUILD=...`` hair is there in order to tell it to use the virtualenv Python, which will have both Sphinx and Pyramid (for API documentation generation) installed. -3. Open the ``docs/_build/html/index.html`` file to see the resulting HTML +4. Open the ``docs/_build/html/index.html`` file to see the resulting HTML rendering. Change Log @@ -24,17 +24,6 @@ Nice-to-Have - Modify the urldispatch chapter examples to assume a scan rather than ``add_view``. -- Context manager for creating a new configurator (replacing - ``with_package``). E.g.:: - - with config.partial(package='bar') as c: - c.add_view(...) - - or:: - - with config.partial(introspection=False) as c: - c.add_view(..) - - Introspection: * ``default root factory`` category (prevent folks from needing to searh @@ -106,30 +95,59 @@ 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.ConfigurationError`` (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 in 1.2). -- 1.5: Remove ``pyramid.requests.DeprecatedRequestMethodsMixin``. +- 1.5: Remove ``pyramid.requests.DeprecatedRequestMethodsMixin`` and code in + renderers module that looks for _response_content_type, et. al. - 1.5: Maybe? deprecate set_request_property in favor of pointing people at - set_request_method. + add_request_method, schedule removal for 1.8? - 1.6: Remove IContextURL and TraversalContextURL. @@ -152,3 +170,15 @@ Probably Bad Ideas - http://pythonguy.wordpress.com/2011/06/22/dynamic-variables-revisited/ instead of thread locals + +- Context manager for creating a new configurator (replacing + ``with_package``). E.g.:: + + with config.partial(package='bar') as c: + c.add_view(...) + + or:: + + with config.partial(introspection=False) as c: + c.add_view(..) + diff --git a/docs/api.rst b/docs/api.rst index d510c0d27..9e540b49b 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -10,10 +10,9 @@ documentation is organized alphabetically by module name. api/authorization api/authentication - api/chameleon_text - api/chameleon_zpt api/compat api/config + api/decorator api/events api/exceptions api/httpexceptions 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 cd58e74d3..5d2bce23e 100644 --- a/docs/api/config.rst +++ b/docs/api/config.rst @@ -36,9 +36,11 @@ .. automethod:: set_authentication_policy .. automethod:: set_authorization_policy .. automethod:: set_default_permission + .. automethod:: add_permission - :methodcategory:`Setting Request Properties` + :methodcategory:`Extending the Request Object` + .. automethod:: add_request_method .. automethod:: set_request_property :methodcategory:`Using I18N` @@ -66,6 +68,8 @@ .. automethod:: add_response_adapter .. automethod:: add_traverser .. automethod:: add_tween + .. automethod:: add_route_predicate + .. automethod:: add_view_predicate .. automethod:: set_request_factory .. automethod:: set_root_factory .. automethod:: set_session_factory diff --git a/docs/api/decorator.rst b/docs/api/decorator.rst new file mode 100644 index 000000000..35d9131df --- /dev/null +++ b/docs/api/decorator.rst @@ -0,0 +1,9 @@ +.. _decorator_module: + +:mod:`pyramid.decorator` +-------------------------- + +.. automodule:: pyramid.decorator + +.. autofunction:: reify + diff --git a/docs/api/registry.rst b/docs/api/registry.rst index e62e2ba6f..1d5d52248 100644 --- a/docs/api/registry.rst +++ b/docs/api/registry.rst @@ -38,3 +38,17 @@ This class is new as of :app:`Pyramid` 1.3. +.. autoclass:: Deferred + + This class is new as of :app:`Pyramid` 1.4. + +.. autofunction:: undefer + + This function is new as of :app:`Pyramid` 1.4. + +.. autoclass:: predvalseq + + This class is new as of :app:`Pyramid` 1.4. + + + 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/glossary.rst b/docs/glossary.rst index 45a79326f..34cf1b078 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -994,3 +994,10 @@ Glossary Aka ``gunicorn``, a fast :term:`WSGI` server that runs on UNIX under Python 2.5+ (although at the time of this writing does not support Python 3). See http://gunicorn.org/ for detailed information. + + predicate factory + A callable which is used by a third party during the registration of a + route, view, or subscriber predicates to extend the configuration + system. See :ref:`registering_thirdparty_predicates` for more + information. + diff --git a/docs/index.rst b/docs/index.rst index 31c2fde6b..321fe1fed 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -13,15 +13,9 @@ Here is one of the simplest :app:`Pyramid` applications you can make: .. literalinclude:: narr/helloworld.py -When saved to ``helloworld.py``, the above application can be run via: - -.. code-block:: text - - $ easy_install pyramid - $ python helloworld.py - -When you visit ``http://localhost:8080/hello/world`` in a browser, you will -see the text ``Hello, world!``. +After you install :app:`Pyramid` and run this application, when you visit +``http://localhost:8080/hello/world`` in a browser, you will see the text +``Hello, world!`` See :ref:`firstapp_chapter` for a full explanation of how this application works. Read the :ref:`html_narrative_documentation` to understand how @@ -94,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 2949dc808..ad22a82c9 100644 --- a/docs/narr/advconfig.rst +++ b/docs/narr/advconfig.rst @@ -294,6 +294,7 @@ 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_property`, @@ -413,3 +414,10 @@ constraints: the routes they imply require relative ordering. Such ordering constraints are not absolved by two-phase configuration. Routes are still added in configuration execution order. +More Information +---------------- + +For more information, see the article entitled `"A Whirlwind Rour of Advanced +Configuration Tactics" +<http://docs.pylonsproject.org/projects/pyramid_cookbook/en/latest/configuration/whirlwind_tour.html>`_ +in the Pyramid Cookbook. diff --git a/docs/narr/commandline.rst b/docs/narr/commandline.rst index af53c1f78..3bdf8c5cd 100644 --- a/docs/narr/commandline.rst +++ b/docs/narr/commandline.rst @@ -349,7 +349,7 @@ setting) orderings using the ``ptweens`` command. Tween factories will show up represented by their standard Python dotted name in the ``ptweens`` output. -For example, here's the ``pwteens`` command run against a system +For example, here's the ``ptweens`` command run against a system configured without any explicit tweens: .. code-block:: text @@ -367,7 +367,7 @@ configured without any explicit tweens: 1 pyramid.tweens.excview_tween_factory excview - - MAIN -Here's the ``pwteens`` command run against a system configured *with* +Here's the ``ptweens`` command run against a system configured *with* explicit tweens defined in its ``development.ini`` file: .. code-block:: text diff --git a/docs/narr/firstapp.rst b/docs/narr/firstapp.rst index 1ca188d7e..ccaa6e9e2 100644 --- a/docs/narr/firstapp.rst +++ b/docs/narr/firstapp.rst @@ -8,7 +8,8 @@ Creating Your First :app:`Pyramid` Application In this chapter, we will walk through the creation of a tiny :app:`Pyramid` application. After we're finished creating the application, we'll explain in -more detail how it works. +more detail how it works. It assumes you already have :app:`Pyramid` installed. +If you do not, head over to the :ref:`installing_chapter` section. .. _helloworld_imperative: @@ -126,7 +127,7 @@ defined imports and function definitions, placed within the confines of an .. literalinclude:: helloworld.py :linenos: - :lines: 8-13 + :lines: 9-15 Let's break this down piece-by-piece. @@ -135,7 +136,7 @@ Configurator Construction .. literalinclude:: helloworld.py :linenos: - :lines: 8-9 + :lines: 9-10 The ``if __name__ == '__main__':`` line in the code sample above represents a Python idiom: the code inside this if clause is not invoked unless the script @@ -168,7 +169,7 @@ Adding Configuration .. ignore-next-block .. literalinclude:: helloworld.py :linenos: - :lines: 10-11 + :lines: 11-12 First line above calls the :meth:`pyramid.config.Configurator.add_route` method, which registers a :term:`route` to match any URL path that begins @@ -188,7 +189,7 @@ WSGI Application Creation .. ignore-next-block .. literalinclude:: helloworld.py :linenos: - :lines: 12 + :lines: 13 After configuring views and ending configuration, the script creates a WSGI *application* via the :meth:`pyramid.config.Configurator.make_wsgi_app` @@ -217,7 +218,7 @@ WSGI Application Serving .. ignore-next-block .. literalinclude:: helloworld.py :linenos: - :lines: 13 + :lines: 14-15 Finally, we actually serve the application to requestors by starting up a WSGI server. We happen to use the :mod:`wsgiref` ``make_server`` server diff --git a/docs/narr/helloworld.py b/docs/narr/helloworld.py index 7c26c8cdc..c01329af9 100644 --- a/docs/narr/helloworld.py +++ b/docs/narr/helloworld.py @@ -2,14 +2,15 @@ from wsgiref.simple_server import make_server from pyramid.config import Configurator from pyramid.response import Response + def hello_world(request): - return Response('Hello %(name)s!' % request.matchdict) + return Response('Hello %(name)s!' % request.matchdict) if __name__ == '__main__': - config = Configurator() - config.add_route('hello', '/hello/{name}') - config.add_view(hello_world, route_name='hello') - app = config.make_wsgi_app() - server = make_server('0.0.0.0', 8080, app) - server.serve_forever() + config = Configurator() + config.add_route('hello', '/hello/{name}') + config.add_view(hello_world, route_name='hello') + app = config.make_wsgi_app() + server = make_server('0.0.0.0', 8080, app) + server.serve_forever() diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index a2143b3c5..96fa77a07 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -289,6 +289,36 @@ keys added to the renderer globals dictionary by all :class:`pyramid.events.BeforeRender` subscribers and renderer globals factories must be unique. +The dictionary returned from the view is accessible through the +:attr:`rendering_val` attribute of a :class:`~pyramid.events.BeforeRender` +event. + +Suppose you return ``{'mykey': 'somevalue', 'mykey2': 'somevalue2'}`` from +your view callable, like so: + +.. code-block:: python + :linenos: + + from pyramid.view import view_config + + @view_config(renderer='some_renderer') + def myview(request): + return {'mykey': 'somevalue', 'mykey2': 'somevalue2'} + +:attr:`rendering_val` can be used to access these values from the +:class:`~pyramid.events.BeforeRender` object: + +.. code-block:: python + :linenos: + + from pyramid.events import subscriber + from pyramid.events import BeforeRender + + @subscriber(BeforeRender) + def read_return(event): + # {'mykey': 'somevalue'} is returned from the view + print(event.rendering_val['mykey']) + See the API documentation for the :class:`~pyramid.events.BeforeRender` event interface at :class:`pyramid.interfaces.IBeforeRender`. @@ -1202,3 +1232,203 @@ Displaying Tween Ordering The ``ptweens`` command-line utility can be used to report the current implict and explicit tween chains used by an application. See :ref:`displaying_tweens`. + +.. _registering_thirdparty_predicates: + +Adding A Third Party View, Route, or Subscriber Predicate +--------------------------------------------------------- + +.. note:: + + Third-party view, route, and subscriber predicates are a feature new as of + Pyramid 1.4. + +.. _view_and_route_predicates: + +View and Route Predicates +~~~~~~~~~~~~~~~~~~~~~~~~~ + +View and route predicates used during configuration allow you to narrow the +set of circumstances under which a view or route will match. For example, +the ``request_method`` view predicate can be used to ensure a view callable +is only invoked when the request's method is ``POST``: + +.. code-block:: python + + @view_config(request_method='POST') + def someview(request): + ... + +Likewise, a similar predicate can be used as a *route* predicate: + +.. code-block:: python + + config.add_route('name', '/foo', request_method='POST') + +Many other built-in predicates exists (``request_param``, and others). You +can add third-party predicates to the list of available predicates by using +one of :meth:`pyramid.config.Configurator.add_view_predicate` or +:meth:`pyramid.config.Configurator.add_route_predicate`. The former adds a +view predicate, the latter a route predicate. + +When using one of those APIs, you pass a *name* and a *factory* to add a +predicate during Pyramid's configuration stage. For example: + +.. code-block:: python + + config.add_view_predicate('content_type', ContentTypePredicate) + +The above example adds a new predicate named ``content_type`` to the list of +available predicates for views. This will allow the following view +configuration statement to work: + +.. code-block:: python + :linenos: + + @view_config(content_type='File') + def aview(request): ... + +The first argument to :meth:`pyramid.config.Configurator.add_view_predicate`, +the name, is a string representing the name that is expected to be passed to +``view_config`` (or its imperative analogue ``add_view``). + +The second argument is a view or route predicate factory. A view or route +predicate factory is most often a class with a constructor (``__init__``), a +``text`` method, a ``phash`` method and a ``__call__`` method. For example: + +.. code-block:: python + :linenos: + + class ContentTypePredicate(object): + def __init__(self, val, config): + self.val = val + + def text(self): + return 'content_type = %s' % (self.val,) + + phash = text + + def __call__(self, context, request): + return getattr(context, 'content_type', None) == self.val + +The constructor of a predicate factory takes two arguments: ``val`` and +``config``. The ``val`` argument will be the argument passed to +``view_config`` (or ``add_view``). In the example above, it will be the +string ``File``. The second arg, ``config`` will be the Configurator +instance at the time of configuration. + +The ``text`` method must return a string. It should be useful to describe +the behavior of the predicate in error messages. + +The ``phash`` method must return a string or a sequence of strings. It's +most often the same as ``text``, as long as ``text`` uniquely describes the +predicate's name and the value passed to the constructor. If ``text`` is +more general, or doesn't describe things that way, ``phash`` should return a +string with the name and the value serialized. The result of ``phash`` is +not seen in output anywhere, it just informs the uniqueness constraints for +view configuration. + +The ``__call__`` method of a predicate factory must accept a resource +(``context``) and a request, and must return ``True`` or ``False``. It is +the "meat" of the predicate. + +You can use the same predicate factory as both a view predicate and as a +route predicate, but you'll need to call ``add_view_predicate`` and +``add_route_predicate`` separately with the same factory. + +.. _subscriber_predicates: + +Subscriber Predicates +~~~~~~~~~~~~~~~~~~~~~ + +Subscriber predicates work almost exactly like view and route predicates. +They narrow the set of circumstances in which a subscriber will be called. +There are several minor differences between a subscriber predicate and a +view/route predicate: + +- There are no default subscriber predicates. You must register one to use + one. + +- The ``__call__`` method of a subscriber predicate accepts a single + ``event`` object instead of a ``context`` and a ``request``. + +- Not every subscriber predicate can be used with every event type. Some + subscriber predicates will assume a certain event type. + +Here's an example of a subscriber predicate that can be used in conjunction +with a subscriber that subscribes to the :class:`pyramid.events.NewReqest` +event type. + +.. code-block:: python + :linenos: + + class RequestPathStartsWith(object): + def __init__(self, val, config): + self.val = val + + def text(self): + return 'path_startswith = %s' % (self.val,) + + phash = text + + def __call__(self, event): + return event.request.path.startswith(self.val) + +Once you've created a subscriber predicate, it may registered via +:meth:`pyramid.config.Configurator.add_subscriber_predicate`. For example: + +.. code-block:: python + + config.add_subscriber_predicate( + 'request_path_startswith', RequestPathStartsWith) + +Once a subscriber predicate is registered, you can use it in a call to +:meth:`pyramid.config.Configurator.add_subscriber` or to +:class:`pyramid.events.subscriber`. Here's an example of using the +previously registered ``request_path_startswith`` predicate in a call to +:meth:`~pyramid.config.Configurator.add_subscriber`: + +.. code-block:: python + :linenos: + + # define a subscriber in your code + + def yosubscriber(event): + event.request.yo = 'YO!' + + # and at configuration time + + config.add_subscriber(yosubscriber, NewRequest, + request_path_startswith='/add_yo') + +Here's the same subscriber/predicate/event-type combination used via +:class:`~pyramid.events.subscriber`. + +.. code-block:: python + :linenos: + + from pyramid.events import subscriber + + @subscriber(NewRequest, request_path_startswith='/add_yo') + def yosubscriber(event): + event.request.yo = 'YO!' + +In either of the above configurations, the ``yosubscriber`` callable will +only be called if the request path starts with ``/add_yo``. Otherwise the +event subscriber will not be called. + +Note that the ``request_path_startswith`` subscriber you defined can be used +with events that have a ``request`` attribute, but not ones that do not. So, +for example, the predicate can be used with subscribers registered for +:class:`pyramid.events.NewRequest` and :class:`pyramid.events.ContextFound` +events, but it cannot be used with subscribers registered for +:class:`pyramid.events.ApplicationCreated` because the latter type of event +has no ``request`` attribute. The point being: unlike route and view +predicates, not every type of subscriber predicate will necessarily be +applicable for use in every subscriber registration. It is not the +responsibility of the predicate author to make every predicate make sense for +every event type; it is the responsibility of the predicate consumer to use +predicates that make sense for a particular event type registration. + + + diff --git a/docs/narr/introduction.rst b/docs/narr/introduction.rst index b5fa6a9f7..7c0f9223f 100644 --- a/docs/narr/introduction.rst +++ b/docs/narr/introduction.rst @@ -803,7 +803,7 @@ within a function called when another user uses the See also :ref:`add_directive`. Programmatic Introspection --------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~~~ If you're building a large system that other users may plug code into, it's useful to be able to get an enumeration of what code they plugged in *at @@ -831,7 +831,7 @@ callable: See also :ref:`using_introspection`. Python 3 Compatibility ----------------------- +~~~~~~~~~~~~~~~~~~~~~~ Pyramid and most of its add-ons are Python 3 compatible. If you develop a Pyramid application today, you won't need to worry that five years from now diff --git a/docs/narr/renderers.rst b/docs/narr/renderers.rst index 57b5bc65b..63287e2cd 100644 --- a/docs/narr/renderers.rst +++ b/docs/narr/renderers.rst @@ -306,7 +306,9 @@ See :class:`pyramid.renderers.JSON` and JSONP Renderer ~~~~~~~~~~~~~~ -.. note:: This feature is new in Pyramid 1.1. +.. note:: + + This feature is new in Pyramid 1.1. :class:`pyramid.renderers.JSONP` is a `JSONP <http://en.wikipedia.org/wiki/JSONP>`_ renderer factory helper which diff --git a/docs/narr/sessions.rst b/docs/narr/sessions.rst index 6ff9e3dea..1aa1b6341 100644 --- a/docs/narr/sessions.rst +++ b/docs/narr/sessions.rst @@ -151,13 +151,12 @@ Using Alternate Session Factories --------------------------------- At the time of this writing, exactly one alternate session factory -implementation exists, named ``pyramid_beaker``. This is a session -factory that uses the `Beaker <http://beaker.groovie.org/>`_ library -as a backend. Beaker has support for file-based sessions, database -based sessions, and encrypted cookie-based sessions. See -`http://github.com/Pylons/pyramid_beaker -<http://github.com/Pylons/pyramid_beaker>`_ for more information about -``pyramid_beaker``. +implementation exists, named ``pyramid_beaker``. This is a session factory +that uses the `Beaker <http://beaker.groovie.org/>`_ library as a backend. +Beaker has support for file-based sessions, database based sessions, and +encrypted cookie-based sessions. See `the pyramid_beaker documentation +<http://docs.pylonsproject.org/projects/pyramid_beaker/en/latest/>`_ for more +information about ``pyramid_beaker``. .. index:: single: session factory (custom) diff --git a/docs/narr/templates.rst b/docs/narr/templates.rst index 9db0b1c4d..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 @@ -534,6 +520,33 @@ And ``templates/mytemplate.pt`` might look like so: </span> </html> + +Using A Chameleon Macro Name Within a Renderer Name +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Sommetime you'd like to render a macro inside of a Chameleon ZPT template +instead of the full Chameleon ZPT template. To render the content of a +``define-macro`` field inside a Chameleon ZPT template, given a Chameleon +template file named ``foo.pt`` and a macro named ``bar`` defined within it +(e.g. ``<div metal:define-macro="bar">...</div>``), you can configure the +template as a :term:`renderer` like so: + +.. code-block:: python + :linenos: + + from pyramid.view import view_config + + @view_config(renderer='foo#bar.pt') + def my_view(request): + return {'project':'my project'} + +The above will render only the ``bar`` macro defined within the ``foo.pt`` +template instead of the entire template. + +.. note:: + + This feature is new in Pyramid 1.4. + .. index:: single: Chameleon text templates @@ -573,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. @@ -714,6 +723,30 @@ This template doesn't use any advanced features of Mako, only the :term:`renderer globals`. See the `the Mako documentation <http://www.makotemplates.org/>`_ to use more advanced features. +Using A Mako def name Within a Renderer Name +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Sommetime you'd like to render a ``def`` inside of a Mako template instead of +the full Mako template. To render a def inside a Mako template, given a +:term:`Mako` template file named ``foo.mak`` and a def named ``bar``, you can +configure the template as a :term:`renderer` like so: + +.. code-block:: python + :linenos: + + from pyramid.view import view_config + + @view_config(renderer='foo#bar.mak') + def my_view(request): + return {'project':'my project'} + +The above will render the ``bar`` def from within the ``foo.mak`` template +instead of the entire template. + +.. note:: + + This feature is new in Pyramid 1.4. + .. index:: single: automatic reloading of templates single: template automatic reload diff --git a/docs/narr/testing.rst b/docs/narr/testing.rst index 89bc1a089..20017064b 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/narr/views.rst b/docs/narr/views.rst index f6ee9a8d5..9e41464a6 100644 --- a/docs/narr/views.rst +++ b/docs/narr/views.rst @@ -177,7 +177,7 @@ HTTP Exceptions ~~~~~~~~~~~~~~~ All classes documented in the :mod:`pyramid.httpexceptions` module documented -as inheriting from the :class:`pryamid.httpexceptions.HTTPException` are +as inheriting from the :class:`pyramid.httpexceptions.HTTPException` are :term:`http exception` objects. Instances of an HTTP exception object may either be *returned* or *raised* from within view code. In either case (return or raise) the instance will be used as as the view's response. diff --git a/docs/tutorials/modwsgi/index.rst b/docs/tutorials/modwsgi/index.rst index e070f8eda..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 @@ -73,9 +72,10 @@ commands and files. .. code-block:: python - from pyramid.paster import get_app - application = get_app( - '/Users/chrism/modwsgi/env/myapp/production.ini', 'main') + from pyramid.paster import get_app, setup_logging + ini_path = '/Users/chrism/modwsgi/env/myapp/production.ini' + setup_logging(ini_path) + application = get_app(ini_path, 'main') The first argument to ``get_app`` is the project configuration file name. It's best to use the ``production.ini`` file provided by your @@ -85,12 +85,15 @@ commands and files. ``application`` is important: mod_wsgi requires finding such an assignment when it opens the file. -#. Make the ``pyramid.wsgi`` script executable. + The call to ``setup_logging`` initializes the standard library's + `logging` module to allow logging within your application. + See :ref:`logging_config`. - .. 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/definingviews.rst b/docs/tutorials/wiki/definingviews.rst index 28cecb787..529603546 100644 --- a/docs/tutorials/wiki/definingviews.rst +++ b/docs/tutorials/wiki/definingviews.rst @@ -251,7 +251,7 @@ wiki page. It includes: - A ``div`` element that is replaced with the ``content`` value provided by the view (rows 45-47). ``content`` contains HTML, so the ``structure`` keyword is used - to prevent escaping it (i.e. changing ">" to >, etc.) + to prevent escaping it (i.e. changing ">" to ">", etc.) - A link that points at the "edit" URL which invokes the ``edit_page`` view for the page being viewed (rows 49-51). 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/authorization.rst b/docs/tutorials/wiki2/authorization.rst index 2ef55d15b..d7bd24a53 100644 --- a/docs/tutorials/wiki2/authorization.rst +++ b/docs/tutorials/wiki2/authorization.rst @@ -353,7 +353,7 @@ when we're done: .. literalinclude:: src/authorization/tutorial/views.py :linenos: - :emphasize-lines: 11,14-18,31,37,58,61,73,76,88,91-117,119-123 + :emphasize-lines: 11,14-18,25,31,37,58,61,73,76,88,91-117,119-123 :language: python (Only the highlighted lines need to be added.) diff --git a/docs/tutorials/wiki2/basiclayout.rst b/docs/tutorials/wiki2/basiclayout.rst index 5f4ea671c..b3184c4fc 100644 --- a/docs/tutorials/wiki2/basiclayout.rst +++ b/docs/tutorials/wiki2/basiclayout.rst @@ -100,7 +100,7 @@ used when the URL is ``/``: :language: py Since this route has a ``pattern`` equalling ``/`` it is the route that will -be matched when the URL ``/`` is visted, e.g. ``http://localhost:6543/``. +be matched when the URL ``/`` is visited, e.g. ``http://localhost:6543/``. ``main`` next calls the ``scan`` method of the configurator, which will recursively scan our ``tutorial`` package, looking for ``@view_config`` (and @@ -190,7 +190,7 @@ Next we set up a SQLAlchemy "DBSession" object: ``scoped_session`` allows us to access our database connection globally. ``sessionmaker`` creates a database session object. We pass to ``sessionmaker`` the ``extension=ZopeTransactionExtension()`` extension -option in order to allow the system to automatically manage datbase +option in order to allow the system to automatically manage database transactions. With ``ZopeTransactionExtension`` activated, our application will automatically issue a transaction commit after every request unless an exception is raised, in which case the transaction will be aborted. diff --git a/docs/tutorials/wiki2/definingviews.rst b/docs/tutorials/wiki2/definingviews.rst index efb72230e..24ac4338d 100644 --- a/docs/tutorials/wiki2/definingviews.rst +++ b/docs/tutorials/wiki2/definingviews.rst @@ -248,7 +248,7 @@ wiki page. It includes: - A ``div`` element that is replaced with the ``content`` value provided by the view (rows 45-47). ``content`` contains HTML, so the ``structure`` keyword is used - to prevent escaping it (i.e. changing ">" to >, etc.) + to prevent escaping it (i.e. changing ">" to ">", etc.) - A link that points at the "edit" URL which invokes the ``edit_page`` view for the page being viewed (rows 49-51). diff --git a/docs/tutorials/wiki2/design.rst b/docs/tutorials/wiki2/design.rst index 2e6fc0e77..deaf32ef6 100644 --- a/docs/tutorials/wiki2/design.rst +++ b/docs/tutorials/wiki2/design.rst @@ -20,7 +20,7 @@ Models We'll be using a SQLite database to hold our wiki data, and we'll be using :term:`SQLAlchemy` to access the data in this database. -Within the database, we define a single table named `tables`, whose elements +Within the database, we define a single table named `pages`, whose elements will store the wiki pages. There are two columns: `name` and `data`. URLs like ``/PageName`` will try to find an element in 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 872d3b920..21dc85528 100644 --- a/pyramid/chameleon_text.py +++ b/pyramid/chameleon_text.py @@ -1,35 +1,24 @@ import sys -from zope.deprecation import deprecated from zope.interface import implementer -from pyramid.compat import reraise - -try: - from chameleon.zpt.template import PageTextTemplateFile - # prevent pyflakes complaining about a redefinition below - PageTextTemplateFile -except ImportError: # pragma: no cover - exc_class, exc, tb = sys.exc_info() - # Chameleon doesn't work on non-CPython platforms - class PageTextTemplateFile(object): - def __init__(self, *arg, **kw): - reraise(ImportError, exc, tb) +from chameleon.zpt.template import PageTextTemplateFile 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) @implementer(ITemplateRenderer) class TextTemplateRenderer(object): - def __init__(self, path, lookup): + 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 +41,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 aa6f89e07..862e996b9 100644 --- a/pyramid/chameleon_zpt.py +++ b/pyramid/chameleon_zpt.py @@ -1,24 +1,11 @@ import sys -from zope.deprecation import deprecated from zope.interface import implementer -from pyramid.compat import reraise - -try: - from chameleon.zpt.template import PageTemplateFile - PageTemplateFile # prevent pyflakes complaining about a redefinition below -except ImportError: # pragma: no cover - exc_class, exc, tb = sys.exc_info() - # Chameleon doesn't work on non-CPython platforms - class PageTemplateFile(object): - def __init__(self, *arg, **kw): - reraise(ImportError, exc, tb) +from chameleon.zpt.template import PageTemplateFile from pyramid.interfaces import ITemplateRenderer - from pyramid.decorator import reify -from pyramid.path import caller_package from pyramid import renderers def renderer_factory(info): @@ -26,19 +13,26 @@ def renderer_factory(info): @implementer(ITemplateRenderer) class ZPTTemplateRenderer(object): - def __init__(self, path, lookup): + def __init__(self, path, lookup, macro=None): self.path = path self.lookup = lookup + self.macro = macro @reify # avoid looking up reload_templates before manager pushed def template(self): if sys.platform.startswith('java'): # pragma: no cover raise RuntimeError( 'Chameleon templates are not compatible with Jython') - return PageTemplateFile(self.path, - auto_reload=self.lookup.auto_reload, - debug=self.lookup.debug, - translate=self.lookup.translate) + tf = PageTemplateFile(self.path, + auto_reload=self.lookup.auto_reload, + debug=self.lookup.debug, + translate=self.lookup.translate) + if self.macro: + # render only the portion of the template included in a + # define-macro named the value of self.macro + macro_renderer = tf.macros[self.macro].include + tf._render = macro_renderer + return tf def implementation(self): return self.template @@ -51,92 +45,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/__init__.py b/pyramid/config/__init__.py index 52d7aca83..1dc438597 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -1,9 +1,9 @@ import inspect +import itertools import logging import operator import os import sys -import types import warnings import venusian @@ -12,6 +12,8 @@ from webob.exc import WSGIHTTPException as WebobWSGIHTTPException from pyramid.interfaces import ( IDebugLogger, IExceptionResponse, + IPredicateList, + PHASE1_CONFIG, ) from pyramid.asset import resolve_asset_spec @@ -22,7 +24,6 @@ from pyramid.compat import ( text_, reraise, string_types, - PY3, ) from pyramid.events import ApplicationCreated @@ -44,6 +45,7 @@ from pyramid.registry import ( Introspectable, Introspector, Registry, + undefer, ) from pyramid.router import Router @@ -71,6 +73,7 @@ from pyramid.config.tweens import TweensConfiguratorMixin from pyramid.config.util import ( action_method, ActionInfo, + PredicateList, ) from pyramid.config.views import ViewsConfiguratorMixin from pyramid.config.zca import ZCAConfiguratorMixin @@ -353,6 +356,9 @@ class Configurator( for name, renderer in DEFAULT_RENDERERS: self.add_renderer(name, renderer) + self.add_default_view_predicates() + self.add_default_route_predicates() + if exceptionresponse_view is not None: exceptionresponse_view = self.maybe_dotted(exceptionresponse_view) self.add_view(exceptionresponse_view, context=IExceptionResponse) @@ -486,6 +492,32 @@ class Configurator( _get_introspector, _set_introspector, _del_introspector ) + def get_predlist(self, name): + predlist = self.registry.queryUtility(IPredicateList, name=name) + if predlist is None: + predlist = PredicateList() + self.registry.registerUtility(predlist, IPredicateList, name=name) + return predlist + + def _add_predicate(self, type, name, factory, weighs_more_than=None, + weighs_less_than=None): + discriminator = ('%s predicate' % type, name) + intr = self.introspectable( + '%s predicates' % type, + discriminator, + '%s predicate named %s' % (type, name), + '%s predicate' % type) + intr['name'] = name + intr['factory'] = factory + intr['weighs_more_than'] = weighs_more_than + intr['weighs_less_than'] = weighs_less_than + def register(): + predlist = self.get_predlist(type) + predlist.add(name, factory, weighs_more_than=weighs_more_than, + weighs_less_than=weighs_less_than) + self.action(discriminator, register, introspectables=(intr,), + order=PHASE1_CONFIG) # must be registered early + @property def action_info(self): info = self.info # usually a ZCML action (ParserInfo) if self.info @@ -547,6 +579,9 @@ class Configurator( introspectables = () if autocommit: + # callables can depend on the side effects of resolving a + # deferred discriminator + undefer(discriminator) if callable is not None: callable(*args, **kw) for introspectable in introspectables: @@ -776,10 +811,9 @@ class Configurator( c, action_wrap = c if action_wrap: c = action_method(c) - if PY3: # pragma: no cover - m = types.MethodType(c, self) - else: - m = types.MethodType(c, self, self.__class__) + # Create a bound method (works on both Py2 and Py3) + # http://stackoverflow.com/a/1015405/209039 + m = c.__get__(self, self.__class__) return m def with_package(self, package): @@ -1070,73 +1104,96 @@ def resolveConflicts(actions): other conflicting actions. """ - # organize actions by discriminators - unique = {} - output = [] - for i, action in enumerate(actions): - if not isinstance(action, dict): + def orderandpos(v): + n, v = v + if not isinstance(v, dict): + # old-style tuple action + v = expand_action(*v) + return (v['order'] or 0, n) + + sactions = sorted(enumerate(actions), key=orderandpos) + + def orderonly(v): + n, v = v + if not isinstance(v, dict): # old-style tuple action - action = expand_action(*action) + v = expand_action(*v) + return v['order'] or 0 + for order, actiongroup in itertools.groupby(sactions, orderonly): # "order" is an integer grouping. Actions in a lower order will be - # executed before actions in a higher order. Within an order, - # actions are executed sequentially based on original action ordering - # ("i"). - order = action['order'] or 0 - discriminator = action['discriminator'] - - # "ainfo" is a tuple of (order, i, action) where "order" is a - # user-supplied grouping, "i" is an integer expressing the relative - # position of this action in the action list being resolved, and - # "action" is an action dictionary. The purpose of an ainfo is to - # associate an "order" and an "i" with a particular action; "order" - # and "i" exist for sorting purposes after conflict resolution. - ainfo = (order, i, action) - - if discriminator is None: - # The discriminator is None, so this action can never conflict. - # We can add it directly to the result. + # executed before actions in a higher order. All of the actions in + # one grouping will be executed (its callable, if any will be called) + # before any of the actions in the next. + + unique = {} + output = [] + + for i, action in actiongroup: + # Within an order, actions are executed sequentially based on + # original action ordering ("i"). + + if not isinstance(action, dict): + # old-style tuple action + action = expand_action(*action) + + # "ainfo" is a tuple of (order, i, action) where "order" is a + # user-supplied grouping, "i" is an integer expressing the relative + # position of this action in the action list being resolved, and + # "action" is an action dictionary. The purpose of an ainfo is to + # associate an "order" and an "i" with a particular action; "order" + # and "i" exist for sorting purposes after conflict resolution. + ainfo = (order, i, action) + + discriminator = undefer(action['discriminator']) + action['discriminator'] = discriminator + + if discriminator is None: + # The discriminator is None, so this action can never conflict. + # We can add it directly to the result. + output.append(ainfo) + continue + + L = unique.setdefault(discriminator, []) + L.append(ainfo) + + # Check for conflicts + conflicts = {} + + for discriminator, ainfos in unique.items(): + # We use (includepath, order, i) as a sort key because we need to + # sort the actions by the paths so that the shortest path with a + # given prefix comes first. The "first" action is the one with the + # shortest include path. We break sorting ties using "order", then + # "i". + def bypath(ainfo): + path, order, i = ainfo[2]['includepath'], ainfo[0], ainfo[1] + return path, order, i + + ainfos.sort(key=bypath) + ainfo, rest = ainfos[0], ainfos[1:] output.append(ainfo) - continue - - L = unique.setdefault(discriminator, []) - L.append(ainfo) - - # Check for conflicts - conflicts = {} - - for discriminator, ainfos in unique.items(): - - # We use (includepath, order, i) as a sort key because we need to - # sort the actions by the paths so that the shortest path with a - # given prefix comes first. The "first" action is the one with the - # shortest include path. We break sorting ties using "order", then - # "i". - def bypath(ainfo): - path, order, i = ainfo[2]['includepath'], ainfo[0], ainfo[1] - return path, order, i - - ainfos.sort(key=bypath) - ainfo, rest = ainfos[0], ainfos[1:] - output.append(ainfo) - _, _, action = ainfo - basepath, baseinfo, discriminator = (action['includepath'], - action['info'], - action['discriminator']) - - for _, _, action in rest: - includepath = action['includepath'] - # Test whether path is a prefix of opath - if (includepath[:len(basepath)] != basepath # not a prefix - or includepath == basepath): - L = conflicts.setdefault(discriminator, [baseinfo]) - L.append(action['info']) - - if conflicts: - raise ConfigurationConflictError(conflicts) - - # sort conflict-resolved actions by (order, i) and return them - return [ x[2] for x in sorted(output, key=operator.itemgetter(0, 1))] + _, _, action = ainfo + basepath, baseinfo, discriminator = ( + action['includepath'], + action['info'], + action['discriminator'], + ) + + for _, _, action in rest: + includepath = action['includepath'] + # Test whether path is a prefix of opath + if (includepath[:len(basepath)] != basepath # not a prefix + or includepath == basepath): + L = conflicts.setdefault(discriminator, [baseinfo]) + L.append(action['info']) + + if conflicts: + raise ConfigurationConflictError(conflicts) + + # sort conflict-resolved actions by (order, i) and yield them one by one + for a in [x[2] for x in sorted(output, key=operator.itemgetter(0, 1))]: + yield a def expand_action(discriminator, callable=None, args=(), kw=None, includepath=(), info=None, order=0, introspectables=()): diff --git a/pyramid/config/adapters.py b/pyramid/config/adapters.py index 5f15f2e46..12c4de660 100644 --- a/pyramid/config/adapters.py +++ b/pyramid/config/adapters.py @@ -1,3 +1,5 @@ +from functools import update_wrapper + from zope.interface import Interface from pyramid.interfaces import ( @@ -6,40 +8,116 @@ from pyramid.interfaces import ( IResourceURL, ) -from pyramid.config.util import action_method +from pyramid.config.util import ( + action_method, + ) + class AdaptersConfiguratorMixin(object): @action_method - def add_subscriber(self, subscriber, iface=None): + def add_subscriber(self, subscriber, iface=None, **predicates): """Add an event :term:`subscriber` for the event stream - implied by the supplied ``iface`` interface. The - ``subscriber`` argument represents a callable object (or a - :term:`dotted Python name` which identifies a callable); it - will be called with a single object ``event`` whenever - :app:`Pyramid` emits an :term:`event` associated with the - ``iface``, which may be an :term:`interface` or a class or a - :term:`dotted Python name` to a global object representing an - interface or a class. Using the default ``iface`` value, - ``None`` will cause the subscriber to be registered for all - event types. See :ref:`events_chapter` for more information - about events and subscribers.""" + implied by the supplied ``iface`` interface. + + The ``subscriber`` argument represents a callable object (or a + :term:`dotted Python name` which identifies a callable); it will be + called with a single object ``event`` whenever :app:`Pyramid` emits + an :term:`event` associated with the ``iface``, which may be an + :term:`interface` or a class or a :term:`dotted Python name` to a + global object representing an interface or a class. + + Using the default ``iface`` value, ``None`` will cause the subscriber + to be registered for all event types. See :ref:`events_chapter` for + more information about events and subscribers. + + Any number of predicate keyword arguments may be passed in + ``**predicates``. Each predicate named will narrow the set of + circumstances that the subscriber will be invoked. Each named + predicate must have been registered via + :meth:`pyramid.config.Configurator.add_subscriber_predicate` before it + can be used. See :ref:`subscriber_predicates` for more information. + + .. note:: + + THe ``**predicates`` argument is new as of Pyramid 1.4. + """ dotted = self.maybe_dotted subscriber, iface = dotted(subscriber), dotted(iface) if iface is None: iface = (Interface,) if not isinstance(iface, (tuple, list)): iface = (iface,) + def register(): - self.registry.registerHandler(subscriber, iface) - intr = self.introspectable('subscribers', - id(subscriber), - self.object_description(subscriber), - 'subscriber') + predlist = self.get_predlist('subscriber') + order, preds, phash = predlist.make(self, **predicates) + intr.update({'phash':phash, 'order':order, 'predicates':preds}) + derived_subscriber = self._derive_subscriber(subscriber, preds) + self.registry.registerHandler(derived_subscriber, iface) + + intr = self.introspectable( + 'subscribers', + id(subscriber), + self.object_description(subscriber), + 'subscriber' + ) + intr['subscriber'] = subscriber intr['interfaces'] = iface + self.action(None, register, introspectables=(intr,)) return subscriber + def _derive_subscriber(self, subscriber, predicates): + if not predicates: + return subscriber + def subscriber_wrapper(*arg): + # We need to accept *arg and pass it along because zope + # subscribers are designed poorly. Notification will always call + # an associated subscriber with all of the objects involved in + # the subscription lookup, despite the fact that the event sender + # always has the option to attach those objects to the event + # object itself (and usually does). It would be much saner if the + # registry just used extra args passed to notify to do the lookup + # but only called event subscribers with the actual event object, + # or if we had been smart enough early on to always wrap + # subscribers in something that threw away the extra args, but + # c'est la vie. + if all((predicate(*arg) for predicate in predicates)): + return subscriber(*arg) + if hasattr(subscriber, '__name__'): + update_wrapper(subscriber_wrapper, subscriber) + return subscriber_wrapper + + @action_method + def add_subscriber_predicate(self, name, factory, weighs_more_than=None, + weighs_less_than=None): + """ + Adds a subscriber predicate factory. The associated subscriber + predicate can later be named as a keyword argument to + :meth:`pyramid.config.Configurator.add_subscriber` in the + ``**predicates`` anonyous keyword argument dictionary. + + ``name`` should be the name of the predicate. It must be a valid + Python identifier (it will be used as a ``**predicates`` keyword + argument to :meth:`~pyramid.config.Configurator.add_subscriber`). + + ``factory`` should be a :term:`predicate factory`. + + See :ref:`subscriber_predicates` for more information. + + .. note:: + + This method is new as of Pyramid 1.4. + """ + self._add_predicate( + 'subscriber', + name, + factory, + weighs_more_than=weighs_more_than, + weighs_less_than=weighs_less_than + ) + @action_method def add_response_adapter(self, adapter, type_or_iface): """ When an object of type (or interface) ``type_or_iface`` is @@ -203,4 +281,3 @@ class AdaptersConfiguratorMixin(object): intr['resource_iface'] = resource_iface self.action(discriminator, register, introspectables=(intr,)) - diff --git a/pyramid/config/factories.py b/pyramid/config/factories.py index ccbf3bbe9..e46519bf5 100644 --- a/pyramid/config/factories.py +++ b/pyramid/config/factories.py @@ -1,10 +1,13 @@ +from zope.interface import implementer + +from pyramid.compat import iteritems_ from pyramid.config.util import action_method from pyramid.interfaces import ( IDefaultRootFactory, INewRequest, IRequestFactory, - IRequestProperties, + IRequestExtensions, IRootFactory, ISessionFactory, ) @@ -73,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:: @@ -93,55 +96,106 @@ class FactoriesConfiguratorMixin(object): self.action(IRequestFactory, register, introspectables=(intr,)) @action_method - def set_request_property(self, callable, name=None, reify=False): - """ Add a property to the request object. + def add_request_method(self, + callable=None, + name=None, + property=False, + reify=False): + """ Add a property or method to the request object. + + When adding a method to the request, ``callable`` may be any + function that receives the request object as the first + parameter. If ``name`` is ``None`` then it will be computed + from the name of the ``callable``. - ``callable`` can either be a callable that accepts the request - as its single positional parameter, or it can be a property - descriptor. It may also be a :term:`dotted Python name` which - refers to either a callable or a property descriptor. + When adding a property to the request, ``callable`` can either + be a callable that accepts the request as its single positional + parameter, or it can be a property descriptor. If ``name`` is + ``None``, the name of the property will be computed from the + name of the ``callable``. If the ``callable`` is a property descriptor a ``ValueError`` will be raised if ``name`` is ``None`` or ``reify`` is ``True``. - If ``name`` is None, the name of the property will be computed - from the name of the ``callable``. - See :meth:`pyramid.request.Request.set_property` for more - information on its usage. + details on ``property`` vs ``reify``. When ``reify`` is + ``True``, the value of ``property`` is assumed to also be + ``True``. + + In all cases, ``callable`` may also be a + :term:`dotted Python name` which refers to either a callable or + a property descriptor. + + If ``callable`` is ``None`` then the method is only used to + assist in conflict detection between different addons requesting + the same attribute on the request object. This is the recommended method for extending the request object and should be used in favor of providing a custom request factory via :meth:`pyramid.config.Configurator.set_request_factory`. - .. versionadded:: 1.3 + .. versionadded:: 1.4 """ - callable = self.maybe_dotted(callable) + if callable is not None: + callable = self.maybe_dotted(callable) - name, callable = InstancePropertyMixin._make_property( - callable, name=name, reify=reify) + property = property or reify + if property: + name, callable = InstancePropertyMixin._make_property( + callable, name=name, reify=reify) + elif name is None: + name = callable.__name__ def register(): - plist = self.registry.queryUtility(IRequestProperties) - - if plist is None: - plist = [] - self.registry.registerUtility(plist, IRequestProperties) - self.registry.registerHandler(_set_request_properties, - (INewRequest,)) - - plist.append((name, callable)) - - intr = self.introspectable('request properties', name, - self.object_description(callable), - 'request property') - intr['callable'] = callable - intr['reify'] = reify - self.action(('request properties', name), register, - introspectables=(intr,)) - -def _set_request_properties(event): - request = event.request - plist = request.registry.queryUtility(IRequestProperties) - request._set_properties(plist) + exts = self.registry.queryUtility(IRequestExtensions) + + if exts is None: + exts = _RequestExtensions() + self.registry.registerUtility(exts, IRequestExtensions) + + plist = exts.descriptors if property else exts.methods + plist[name] = callable + + if callable is None: + self.action(('request extensions', name), None) + elif property: + intr = self.introspectable('request extensions', name, + self.object_description(callable), + 'request property') + intr['callable'] = callable + intr['property'] = True + intr['reify'] = reify + self.action(('request extensions', name), register, + introspectables=(intr,)) + else: + intr = self.introspectable('request extensions', name, + self.object_description(callable), + 'request method') + intr['callable'] = callable + intr['property'] = False + intr['reify'] = False + self.action(('request extensions', name), register, + introspectables=(intr,)) + + @action_method + def set_request_property(self, callable, name=None, reify=False): + """ Add a property to the request object. + + .. warning:: + + 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.add_request_method( + callable, name=name, property=not reify, reify=reify) + +@implementer(IRequestExtensions) +class _RequestExtensions(object): + def __init__(self): + self.descriptors = {} + self.methods = {} + diff --git a/pyramid/config/predicates.py b/pyramid/config/predicates.py new file mode 100644 index 000000000..9e0ee28c1 --- /dev/null +++ b/pyramid/config/predicates.py @@ -0,0 +1,228 @@ +import re + +from pyramid.compat import is_nonstr_iter + +from pyramid.exceptions import ConfigurationError + +from pyramid.traversal import ( + find_interface, + traversal_path, + ) + +from pyramid.urldispatch import _compile_route + +from pyramid.util import object_description + +from .util import as_sorted_tuple + +class XHRPredicate(object): + def __init__(self, val, config): + self.val = bool(val) + + def text(self): + return 'xhr = %s' % self.val + + phash = text + + def __call__(self, context, request): + return bool(request.is_xhr) is self.val + +class RequestMethodPredicate(object): + def __init__(self, val, config): + request_method = as_sorted_tuple(val) + if 'GET' in request_method and 'HEAD' not in request_method: + # GET implies HEAD too + request_method = as_sorted_tuple(request_method + ('HEAD',)) + self.val = request_method + + def text(self): + return 'request_method = %s' % (','.join(self.val)) + + phash = text + + def __call__(self, context, request): + return request.method in self.val + +class PathInfoPredicate(object): + def __init__(self, val, config): + self.orig = val + try: + val = re.compile(val) + except re.error as why: + raise ConfigurationError(why.args[0]) + self.val = val + + def text(self): + return 'path_info = %s' % (self.orig,) + + phash = text + + def __call__(self, context, request): + return self.val.match(request.upath_info) is not None + +class RequestParamPredicate(object): + def __init__(self, val, config): + name = val + v = None + if '=' in name: + name, v = name.split('=', 1) + name, v = name.strip(), v.strip() + if v is None: + self._text = 'request_param %s' % (name,) + else: + self._text = 'request_param %s = %s' % (name, v) + self.name = name + self.val = v + + def text(self): + return self._text + + phash = text + + def __call__(self, context, request): + if self.val is None: + return self.name in request.params + return request.params.get(self.name) == self.val + + +class HeaderPredicate(object): + def __init__(self, val, config): + name = val + v = None + if ':' in name: + name, v = name.split(':', 1) + try: + v = re.compile(v) + except re.error as why: + raise ConfigurationError(why.args[0]) + if v is None: + self._text = 'header %s' % (name,) + else: + self._text = 'header %s = %s' % (name, v) + self.name = name + self.val = v + + def text(self): + return self._text + + phash = text + + def __call__(self, context, request): + if self.val is None: + return self.name in request.headers + val = request.headers.get(self.name) + if val is None: + return False + return self.val.match(val) is not None + +class AcceptPredicate(object): + def __init__(self, val, config): + self.val = val + + def text(self): + return 'accept = %s' % (self.val,) + + phash = text + + def __call__(self, context, request): + return self.val in request.accept + +class ContainmentPredicate(object): + def __init__(self, val, config): + self.val = config.maybe_dotted(val) + + def text(self): + return 'containment = %s' % (self.val,) + + phash = text + + def __call__(self, context, request): + ctx = getattr(request, 'context', context) + return find_interface(ctx, self.val) is not None + +class RequestTypePredicate(object): + def __init__(self, val, config): + self.val = val + + def text(self): + return 'request_type = %s' % (self.val,) + + phash = text + + def __call__(self, context, request): + return self.val.providedBy(request) + +class MatchParamPredicate(object): + def __init__(self, val, config): + if not is_nonstr_iter(val): + val = (val,) + val = sorted(val) + self.val = val + reqs = [ p.split('=', 1) for p in val ] + self.reqs = [ (x.strip(), y.strip()) for x, y in reqs ] + + def text(self): + return 'match_param %s' % ','.join( + ['%s=%s' % (x,y) for x, y in self.reqs] + ) + + phash = text + + def __call__(self, context, request): + for k, v in self.reqs: + if request.matchdict.get(k) != v: + return False + return True + +class CustomPredicate(object): + def __init__(self, func, config): + self.func = func + + def text(self): + return getattr( + self.func, + '__text__', + 'custom predicate: %s' % object_description(self.func) + ) + + def phash(self): + # using hash() here rather than id() is intentional: we + # want to allow custom predicates that are part of + # frameworks to be able to define custom __hash__ + # functions for custom predicates, so that the hash output + # of predicate instances which are "logically the same" + # may compare equal. + return 'custom:%r' % hash(self.func) + + def __call__(self, context, request): + return self.func(context, request) + + +class TraversePredicate(object): + # Can only be used as a *route* "predicate"; it adds 'traverse' to the + # matchdict if it's specified in the routing args. This causes the + # ResourceTreeTraverser to use the resolved traverse pattern as the + # traversal path. + def __init__(self, val, config): + _, self.tgenerate = _compile_route(val) + self.val = val + + def text(self): + return 'traverse matchdict pseudo-predicate' + + def phash(self): + # This isn't actually a predicate, it's just a infodict modifier that + # injects ``traverse`` into the matchdict. As a result, we don't + # need to update the hash. + return '' + + def __call__(self, context, request): + if 'traverse' in context: + return True + m = context['match'] + tvalue = self.tgenerate(m) # tvalue will be urlquoted string + m['traverse'] = traversal_path(tvalue) + # This isn't actually a predicate, it's just a infodict modifier that + # injects ``traverse`` into the matchdict. As a result, we just + # return True. + return True diff --git a/pyramid/config/routes.py b/pyramid/config/routes.py index ea39b6805..1a7fdfac9 100644 --- a/pyramid/config/routes.py +++ b/pyramid/config/routes.py @@ -4,19 +4,22 @@ from pyramid.interfaces import ( IRequest, IRouteRequest, IRoutesMapper, + PHASE1_CONFIG, PHASE2_CONFIG, ) from pyramid.exceptions import ConfigurationError +from pyramid.registry import predvalseq from pyramid.request import route_request_iface from pyramid.urldispatch import RoutesMapper from pyramid.config.util import ( action_method, - make_predicates, as_sorted_tuple, ) +import pyramid.config.predicates + class RoutesConfiguratorMixin(object): @action_method def add_route(self, @@ -28,7 +31,7 @@ class RoutesConfiguratorMixin(object): factory=None, for_=None, header=None, - xhr=False, + xhr=None, accept=None, path_info=None, request_method=None, @@ -44,7 +47,7 @@ class RoutesConfiguratorMixin(object): path=None, pregenerator=None, static=False, - ): + **predicates): """ Add a :term:`route configuration` to the current configuration state, as well as possibly a :term:`view configuration` to be used to specify a :term:`view callable` @@ -254,6 +257,15 @@ class RoutesConfiguratorMixin(object): :ref:`custom_route_predicates` for more information about ``info``. + predicates + + Pass a key/value pair here to use a third-party predicate + registered via + :meth:`pyramid.config.Configurator.add_view_predicate`. More than + one key/value pair can be used at the same time. See + :ref:`view_and_route_predicates` for more information about + third-party predicates. This argument is new as of Pyramid 1.4. + View-Related Arguments .. warning:: @@ -351,17 +363,6 @@ class RoutesConfiguratorMixin(object): if request_method is not None: request_method = as_sorted_tuple(request_method) - ignored, predicates, ignored = make_predicates( - xhr=xhr, - request_method=request_method, - path_info=path_info, - request_param=request_param, - header=header, - accept=accept, - traverse=traverse, - custom=custom_predicates - ) - factory = self.maybe_dotted(factory) if pattern is None: pattern = path @@ -417,8 +418,24 @@ class RoutesConfiguratorMixin(object): request_iface, IRouteRequest, name=name) def register_connect(): + pvals = predicates.copy() + pvals.update( + dict( + xhr=xhr, + request_method=request_method, + path_info=path_info, + request_param=request_param, + header=header, + accept=accept, + traverse=traverse, + custom=predvalseq(custom_predicates), + ) + ) + + predlist = self.get_predlist('route') + _, preds, _ = predlist.make(self, **pvals) route = mapper.connect( - name, pattern, factory, predicates=predicates, + name, pattern, factory, predicates=preds, pregenerator=pregenerator, static=static ) intr['object'] = route @@ -447,6 +464,47 @@ class RoutesConfiguratorMixin(object): attr=view_attr, ) + @action_method + def add_route_predicate(self, name, factory, weighs_more_than=None, + weighs_less_than=None): + """ Adds a route predicate factory. The view predicate can later be + named as a keyword argument to + :meth:`pyramid.config.Configurator.add_route`. + + ``name`` should be the name of the predicate. It must be a valid + Python identifier (it will be used as a keyword argument to + ``add_view``). + + ``factory`` should be a :term:`predicate factory`. + + See :ref:`view_and_route_predicates` for more information. + + .. note:: + + This method is new as of Pyramid 1.4. + """ + self._add_predicate( + 'route', + name, + factory, + weighs_more_than=weighs_more_than, + weighs_less_than=weighs_less_than + ) + + def add_default_route_predicates(self): + p = pyramid.config.predicates + for (name, factory) in ( + ('xhr', p.XHRPredicate), + ('request_method', p.RequestMethodPredicate), + ('path_info', p.PathInfoPredicate), + ('request_param', p.RequestParamPredicate), + ('header', p.HeaderPredicate), + ('accept', p.AcceptPredicate), + ('custom', p.CustomPredicate), + ('traverse', p.TraversePredicate), + ): + self.add_route_predicate(name, factory) + def get_routes_mapper(self): """ Return the :term:`routes mapper` object associated with this configurator's :term:`registry`.""" diff --git a/pyramid/config/security.py b/pyramid/config/security.py index e8ef1518d..567999cc4 100644 --- a/pyramid/config/security.py +++ b/pyramid/config/security.py @@ -137,3 +137,23 @@ class SecurityConfiguratorMixin(object): introspectables=(intr, perm_intr,)) + def add_permission(self, permission_name): + """ + A configurator directive which registers a free-standing + permission without associating it with a view callable. This can be + used so that the permission shows up in the introspectable data under + the ``permissions`` category (permissions mentioned via ``add_view`` + already end up in there). For example:: + + config = Configurator() + config.add_permission('view') + """ + intr = self.introspectable( + 'permissions', + permission_name, + permission_name, + 'permission' + ) + intr['value'] = permission_name + self.action(None, introspectables=(intr,)) + diff --git a/pyramid/config/tweens.py b/pyramid/config/tweens.py index 1a83f0de9..1bc6dc95c 100644 --- a/pyramid/config/tweens.py +++ b/pyramid/config/tweens.py @@ -16,7 +16,10 @@ from pyramid.tweens import ( EXCVIEW, ) -from pyramid.config.util import action_method +from pyramid.config.util import ( + action_method, + TopologicalSorter, + ) class TweensConfiguratorMixin(object): def add_tween(self, tween_factory, under=None, over=None): @@ -177,119 +180,24 @@ class TweensConfiguratorMixin(object): introspectables.append(intr) self.action(discriminator, register, introspectables=introspectables) -class CyclicDependencyError(Exception): - def __init__(self, cycles): - self.cycles = cycles - - def __str__(self): - L = [] - cycles = self.cycles - for cycle in cycles: - dependent = cycle - dependees = cycles[cycle] - L.append('%r sorts over %r' % (dependent, dependees)) - msg = 'Implicit tween ordering cycle:' + '; '.join(L) - return msg - @implementer(ITweens) class Tweens(object): def __init__(self): + self.sorter = TopologicalSorter( + default_before=None, + default_after=INGRESS, + first=INGRESS, + last=MAIN) self.explicit = [] - self.names = [] - self.req_over = set() - self.req_under = set() - self.factories = {} - self.order = [] def add_explicit(self, name, factory): self.explicit.append((name, factory)) def add_implicit(self, name, factory, under=None, over=None): - self.names.append(name) - self.factories[name] = factory - if under is None and over is None: - under = INGRESS - if under is not None: - if not is_nonstr_iter(under): - under = (under,) - self.order += [(u, name) for u in under] - self.req_under.add(name) - if over is not None: - if not is_nonstr_iter(over): - over = (over,) - self.order += [(name, o) for o in over] - self.req_over.add(name) + self.sorter.add(name, factory, after=under, before=over) def implicit(self): - order = [(INGRESS, MAIN)] - roots = [] - graph = {} - names = [INGRESS, MAIN] - names.extend(self.names) - - for a, b in self.order: - order.append((a, b)) - - def add_node(node): - if not node in graph: - roots.append(node) - graph[node] = [0] # 0 = number of arcs coming into this node - - def add_arc(fromnode, tonode): - graph[fromnode].append(tonode) - graph[tonode][0] += 1 - if tonode in roots: - roots.remove(tonode) - - for name in names: - add_node(name) - - has_over, has_under = set(), set() - for a, b in order: - if a in names and b in names: # deal with missing dependencies - add_arc(a, b) - has_over.add(a) - has_under.add(b) - - if not self.req_over.issubset(has_over): - raise ConfigurationError( - 'Detected tweens with no satisfied over dependencies: %s' - % (', '.join(sorted(self.req_over - has_over))) - ) - if not self.req_under.issubset(has_under): - raise ConfigurationError( - 'Detected tweens with no satisfied under dependencies: %s' - % (', '.join(sorted(self.req_under - has_under))) - ) - - sorted_names = [] - - while roots: - root = roots.pop(0) - sorted_names.append(root) - children = graph[root][1:] - for child in children: - arcs = graph[child][0] - arcs -= 1 - graph[child][0] = arcs - if arcs == 0: - roots.insert(0, child) - del graph[root] - - if graph: - # loop in input - cycledeps = {} - for k, v in graph.items(): - cycledeps[k] = v[1:] - raise CyclicDependencyError(cycledeps) - - result = [] - - for name in sorted_names: - if name in self.names: - result.append((name, self.factories[name])) - - return result + return self.sorter.sorted() def __call__(self, handler, registry): if self.explicit: diff --git a/pyramid/config/util.py b/pyramid/config/util.py index b8d0f2319..a4df44408 100644 --- a/pyramid/config/util.py +++ b/pyramid/config/util.py @@ -1,6 +1,7 @@ -import re import traceback +from functools import update_wrapper + from zope.interface import implementer from pyramid.interfaces import IActionInfo @@ -12,10 +13,7 @@ from pyramid.compat import ( from pyramid.exceptions import ConfigurationError -from pyramid.traversal import ( - find_interface, - traversal_path, - ) +from pyramid.registry import predvalseq from hashlib import md5 @@ -59,233 +57,11 @@ def action_method(wrapped): finally: self._ainfo.pop() return result - wrapper.__name__ = wrapped.__name__ - wrapper.__doc__ = wrapped.__doc__ - wrapper.__docobj__ = wrapped # for sphinx - return wrapper -def make_predicates(xhr=None, request_method=None, path_info=None, - request_param=None, match_param=None, header=None, - accept=None, containment=None, request_type=None, - traverse=None, custom=()): - - # PREDICATES - # ---------- - # - # Given an argument list, a predicate list is computed. - # Predicates are added to a predicate list in (presumed) - # computation expense order. All predicates associated with a - # view or route must evaluate true for the view or route to - # "match" during a request. Elsewhere in the code, we evaluate - # predicates using a generator expression. The fastest predicate - # should be evaluated first, then the next fastest, and so on, as - # if one returns false, the remainder of the predicates won't need - # to be evaluated. - # - # While we compute predicates, we also compute a predicate hash - # (aka phash) that can be used by a caller to identify identical - # predicate lists. - # - # ORDERING - # -------- - # - # A "order" is computed for the predicate list. An order is - # a scoring. - # - # Each predicate is associated with a weight value, which is a - # multiple of 2. The weight of a predicate symbolizes the - # relative potential "importance" of the predicate to all other - # predicates. A larger weight indicates greater importance. - # - # All weights for a given predicate list are bitwise ORed together - # to create a "score"; this score is then subtracted from - # MAX_ORDER and divided by an integer representing the number of - # predicates+1 to determine the order. - # - # The order represents the ordering in which a "multiview" ( a - # collection of views that share the same context/request/name - # triad but differ in other ways via predicates) will attempt to - # call its set of views. Views with lower orders will be tried - # first. The intent is to a) ensure that views with more - # predicates are always evaluated before views with fewer - # predicates and b) to ensure a stable call ordering of views that - # share the same number of predicates. Views which do not have - # any predicates get an order of MAX_ORDER, meaning that they will - # be tried very last. - - predicates = [] - weights = [] - h = md5() - - if xhr: - def xhr_predicate(context, request): - return request.is_xhr - xhr_predicate.__text__ = "xhr = True" - weights.append(1 << 1) - predicates.append(xhr_predicate) - h.update(bytes_('xhr:%r' % bool(xhr))) - - if request_method is not None: - if not is_nonstr_iter(request_method): - request_method = (request_method,) - request_method = sorted(request_method) - def request_method_predicate(context, request): - return request.method in request_method - text = "request method = %r" % request_method - request_method_predicate.__text__ = text - weights.append(1 << 2) - predicates.append(request_method_predicate) - for m in request_method: - h.update(bytes_('request_method:%r' % m)) - - if path_info is not None: - try: - path_info_val = re.compile(path_info) - except re.error as why: - raise ConfigurationError(why.args[0]) - def path_info_predicate(context, request): - return path_info_val.match(request.upath_info) is not None - text = "path_info = %s" - path_info_predicate.__text__ = text % path_info - weights.append(1 << 3) - predicates.append(path_info_predicate) - h.update(bytes_('path_info:%r' % path_info)) - - if request_param is not None: - request_param_val = None - if '=' in request_param: - request_param, request_param_val = request_param.split('=', 1) - if request_param_val is None: - text = "request_param %s" % request_param - else: - text = "request_param %s = %s" % (request_param, request_param_val) - def request_param_predicate(context, request): - if request_param_val is None: - return request_param in request.params - return request.params.get(request_param) == request_param_val - request_param_predicate.__text__ = text - weights.append(1 << 4) - predicates.append(request_param_predicate) - h.update( - bytes_('request_param:%r=%r' % (request_param, request_param_val))) - - if header is not None: - header_name = header - header_val = None - if ':' in header: - header_name, header_val = header.split(':', 1) - try: - header_val = re.compile(header_val) - except re.error as why: - raise ConfigurationError(why.args[0]) - if header_val is None: - text = "header %s" % header_name - else: - text = "header %s = %s" % (header_name, header_val) - def header_predicate(context, request): - if header_val is None: - return header_name in request.headers - val = request.headers.get(header_name) - if val is None: - return False - return header_val.match(val) is not None - header_predicate.__text__ = text - weights.append(1 << 5) - predicates.append(header_predicate) - h.update(bytes_('header:%r=%r' % (header_name, header_val))) - - if accept is not None: - def accept_predicate(context, request): - return accept in request.accept - accept_predicate.__text__ = "accept = %s" % accept - weights.append(1 << 6) - predicates.append(accept_predicate) - h.update(bytes_('accept:%r' % accept)) - - if containment is not None: - def containment_predicate(context, request): - ctx = getattr(request, 'context', context) - return find_interface(ctx, containment) is not None - containment_predicate.__text__ = "containment = %s" % containment - weights.append(1 << 7) - predicates.append(containment_predicate) - h.update(bytes_('containment:%r' % hash(containment))) - - if request_type is not None: - def request_type_predicate(context, request): - return request_type.providedBy(request) - text = "request_type = %s" - request_type_predicate.__text__ = text % request_type - weights.append(1 << 8) - predicates.append(request_type_predicate) - h.update(bytes_('request_type:%r' % hash(request_type))) - - if match_param is not None: - if not is_nonstr_iter(match_param): - match_param = (match_param,) - match_param = sorted(match_param) - text = "match_param %s" % repr(match_param) - reqs = [p.split('=', 1) for p in match_param] - def match_param_predicate(context, request): - for k, v in reqs: - if request.matchdict.get(k) != v: - return False - return True - match_param_predicate.__text__ = text - weights.append(1 << 9) - predicates.append(match_param_predicate) - for p in match_param: - h.update(bytes_('match_param:%r' % p)) - - if custom: - for num, predicate in enumerate(custom): - if getattr(predicate, '__text__', None) is None: - text = '<unknown custom predicate>' - try: - predicate.__text__ = text - except AttributeError: - # if this happens the predicate is probably a classmethod - if hasattr(predicate, '__func__'): - predicate.__func__.__text__ = text - else: # pragma: no cover ; 2.5 doesn't have __func__ - predicate.im_func.__text__ = text - predicates.append(predicate) - # using hash() here rather than id() is intentional: we - # want to allow custom predicates that are part of - # frameworks to be able to define custom __hash__ - # functions for custom predicates, so that the hash output - # of predicate instances which are "logically the same" - # may compare equal. - h.update(bytes_('custom%s:%r' % (num, hash(predicate)))) - weights.append(1 << 10) - - if traverse is not None: - # ``traverse`` can only be used as a *route* "predicate"; it - # adds 'traverse' to the matchdict if it's specified in the - # routing args. This causes the ResourceTreeTraverser to use - # the resolved traverse pattern as the traversal path. - from pyramid.urldispatch import _compile_route - _, tgenerate = _compile_route(traverse) - def traverse_predicate(context, request): - if 'traverse' in context: - return True - m = context['match'] - tvalue = tgenerate(m) # tvalue will be urlquoted string - m['traverse'] = traversal_path(tvalue) # will be seq of unicode - return True - # This isn't actually a predicate, it's just a infodict - # modifier that injects ``traverse`` into the matchdict. As a - # result, the ``traverse_predicate`` function above always - # returns True, and we don't need to update the hash or attach - # a weight to it - predicates.append(traverse_predicate) - - score = 0 - for bit in weights: - score = score | bit - order = (MAX_ORDER - score) / (len(predicates) + 1) - phash = h.hexdigest() - return order, predicates, phash + if hasattr(wrapped, '__name__'): + update_wrapper(wrapper, wrapped) + wrapper.__docobj__ = wrapped + return wrapper def as_sorted_tuple(val): if not is_nonstr_iter(val): @@ -293,3 +69,233 @@ def as_sorted_tuple(val): val = tuple(sorted(val)) return val +# under = after +# over = before + +class Singleton(object): + def __init__(self, repr): + self.repr = repr + + def __repr__(self): + return self.repr + +FIRST = Singleton('FIRST') +LAST = Singleton('LAST') + +class TopologicalSorter(object): + def __init__( + self, + default_before=LAST, + default_after=None, + first=FIRST, + last=LAST, + ): + self.names = [] + self.req_before = set() + self.req_after = set() + self.name2before = {} + self.name2after = {} + self.name2val = {} + self.order = [] + self.default_before = default_before + self.default_after = default_after + self.first = first + self.last = last + + def remove(self, name): + self.names.remove(name) + del self.name2val[name] + after = self.name2after.pop(name, []) + if after: + self.req_after.remove(name) + for u in after: + self.order.remove((u, name)) + before = self.name2before.pop(name, []) + if before: + self.req_before.remove(name) + for u in before: + self.order.remove((name, u)) + + def add(self, name, val, after=None, before=None): + if name in self.names: + self.remove(name) + self.names.append(name) + self.name2val[name] = val + if after is None and before is None: + before = self.default_before + after = self.default_after + if after is not None: + if not is_nonstr_iter(after): + after = (after,) + self.name2after[name] = after + self.order += [(u, name) for u in after] + self.req_after.add(name) + if before is not None: + if not is_nonstr_iter(before): + before = (before,) + self.name2before[name] = before + self.order += [(name, o) for o in before] + self.req_before.add(name) + + def sorted(self): + order = [(self.first, self.last)] + roots = [] + graph = {} + names = [self.first, self.last] + names.extend(self.names) + + for a, b in self.order: + order.append((a, b)) + + def add_node(node): + if not node in graph: + roots.append(node) + graph[node] = [0] # 0 = number of arcs coming into this node + + def add_arc(fromnode, tonode): + graph[fromnode].append(tonode) + graph[tonode][0] += 1 + if tonode in roots: + roots.remove(tonode) + + for name in names: + add_node(name) + + has_before, has_after = set(), set() + for a, b in order: + if a in names and b in names: # deal with missing dependencies + add_arc(a, b) + has_before.add(a) + has_after.add(b) + + if not self.req_before.issubset(has_before): + raise ConfigurationError( + 'Unsatisfied before dependencies: %s' + % (', '.join(sorted(self.req_before - has_before))) + ) + if not self.req_after.issubset(has_after): + raise ConfigurationError( + 'Unsatisfied after dependencies: %s' + % (', '.join(sorted(self.req_after - has_after))) + ) + + sorted_names = [] + + while roots: + root = roots.pop(0) + sorted_names.append(root) + children = graph[root][1:] + for child in children: + arcs = graph[child][0] + arcs -= 1 + graph[child][0] = arcs + if arcs == 0: + roots.insert(0, child) + del graph[root] + + if graph: + # loop in input + cycledeps = {} + for k, v in graph.items(): + cycledeps[k] = v[1:] + raise CyclicDependencyError(cycledeps) + + result = [] + + for name in sorted_names: + if name in self.names: + result.append((name, self.name2val[name])) + + return result + +class CyclicDependencyError(Exception): + def __init__(self, cycles): + self.cycles = cycles + + def __str__(self): + L = [] + cycles = self.cycles + for cycle in cycles: + dependent = cycle + dependees = cycles[cycle] + L.append('%r sorts before %r' % (dependent, dependees)) + msg = 'Implicit ordering cycle:' + '; '.join(L) + return msg + +class PredicateList(object): + + def __init__(self): + self.sorter = TopologicalSorter() + self.last_added = None + + def add(self, name, factory, weighs_more_than=None, weighs_less_than=None): + # Predicates should be added to a predicate list in (presumed) + # computation expense order. + ## if weighs_more_than is None and weighs_less_than is None: + ## weighs_more_than = self.last_added or FIRST + ## weighs_less_than = LAST + self.last_added = name + self.sorter.add(name, factory, after=weighs_more_than, + before=weighs_less_than) + + def make(self, config, **kw): + # Given a configurator and a list of keywords, a predicate list is + # computed. Elsewhere in the code, we evaluate predicates using a + # generator expression. All predicates associated with a view or + # route must evaluate true for the view or route to "match" during a + # request. The fastest predicate should be evaluated first, then the + # next fastest, and so on, as if one returns false, the remainder of + # the predicates won't need to be evaluated. + # + # While we compute predicates, we also compute a predicate hash (aka + # phash) that can be used by a caller to identify identical predicate + # lists. + ordered = self.sorter.sorted() + phash = md5() + weights = [] + preds = [] + for n, (name, predicate_factory) in enumerate(ordered): + vals = kw.pop(name, None) + if vals is None: # XXX should this be a sentinel other than None? + continue + if not isinstance(vals, predvalseq): + vals = (vals,) + for val in vals: + pred = predicate_factory(val, config) + hashes = pred.phash() + if not is_nonstr_iter(hashes): + hashes = [hashes] + for h in hashes: + phash.update(bytes_(h)) + weights.append(1 << n+1) + preds.append(pred) + if kw: + raise ConfigurationError('Unknown predicate values: %r' % (kw,)) + # A "order" is computed for the predicate list. An order is + # a scoring. + # + # Each predicate is associated with a weight value. The weight of a + # predicate symbolizes the relative potential "importance" of the + # predicate to all other predicates. A larger weight indicates + # greater importance. + # + # All weights for a given predicate list are bitwise ORed together + # to create a "score"; this score is then subtracted from + # MAX_ORDER and divided by an integer representing the number of + # predicates+1 to determine the order. + # + # For views, the order represents the ordering in which a "multiview" + # ( a collection of views that share the same context/request/name + # triad but differ in other ways via predicates) will attempt to call + # its set of views. Views with lower orders will be tried first. + # The intent is to a) ensure that views with more predicates are + # always evaluated before views with fewer predicates and b) to + # ensure a stable call ordering of views that share the same number + # of predicates. Views which do not have any predicates get an order + # of MAX_ORDER, meaning that they will be tried very last. + score = 0 + for bit in weights: + score = score | bit + order = (MAX_ORDER - score) / (len(preds) + 1) + return order, preds, phash.hexdigest() + diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 9e9b5321b..36896a17e 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -54,6 +54,11 @@ from pyramid.httpexceptions import ( HTTPNotFound, ) +from pyramid.registry import ( + predvalseq, + Deferred, + ) + from pyramid.security import NO_PERMISSION_REQUIRED from pyramid.static import static_view from pyramid.threadlocal import get_current_registry @@ -63,14 +68,16 @@ from pyramid.view import ( AppendSlashNotFoundViewFactory, ) -from pyramid.util import object_description +from pyramid.util import ( + object_description, + ) + +import pyramid.config.predicates from pyramid.config.util import ( DEFAULT_PHASH, MAX_ORDER, action_method, - as_sorted_tuple, - make_predicates, ) urljoin = urlparse.urljoin @@ -272,20 +279,22 @@ class ViewDeriver(object): @wraps_view def predicated_view(self, view): - predicates = self.kw.get('predicates', ()) - if not predicates: + preds = self.kw.get('predicates', ()) + if not preds: return view def predicate_wrapper(context, request): - if all((predicate(context, request) for predicate in predicates)): - return view(context, request) - view_name = getattr(view, '__name__', view) - raise PredicateMismatch( - 'predicate mismatch for view %s' % view_name) + for predicate in preds: + if not predicate(context, request): + view_name = getattr(view, '__name__', view) + raise PredicateMismatch( + 'predicate mismatch for view %s (%s)' % ( + view_name, predicate.text())) + return view(context, request) def checker(context, request): return all((predicate(context, request) for predicate in - predicates)) + preds)) predicate_wrapper.__predicated__ = checker - predicate_wrapper.__predicates__ = predicates + predicate_wrapper.__predicates__ = preds return predicate_wrapper @wraps_view @@ -564,7 +573,7 @@ class MultiView(object): return else: subset.append((order, view, phash)) - subset.sort() + subset.sort(key=operator.itemgetter(0)) accepts = set(self.accepts) accepts.add(accept) self.accepts = list(accepts) # dedupe @@ -629,13 +638,31 @@ def viewdefaults(wrapped): class ViewsConfiguratorMixin(object): @viewdefaults @action_method - def add_view(self, view=None, name="", for_=None, permission=None, - request_type=None, route_name=None, request_method=None, - request_param=None, containment=None, attr=None, - renderer=None, wrapper=None, xhr=False, accept=None, - header=None, path_info=None, custom_predicates=(), - context=None, decorator=None, mapper=None, http_cache=None, - match_param=None): + def add_view( + self, + view=None, + name="", + for_=None, + permission=None, + request_type=None, + route_name=None, + request_method=None, + request_param=None, + containment=None, + attr=None, + renderer=None, + wrapper=None, + xhr=None, + accept=None, + header=None, + path_info=None, + custom_predicates=(), + context=None, + decorator=None, + mapper=None, + http_cache=None, + match_param=None, + **predicates): """ Add a :term:`view configuration` to the current configuration state. Arguments to ``add_view`` are broken down below into *predicate* arguments and *non-predicate* @@ -658,24 +685,27 @@ class ViewsConfiguratorMixin(object): permission - The name of a :term:`permission` that the user must possess - in order to invoke the :term:`view callable`. See - :ref:`view_security_section` for more information about view - security and permissions. If ``permission`` is omitted, a - *default* permission may be used for this view registration - if one was named as the + A :term:`permission` that the user must possess in order to invoke + the :term:`view callable`. See :ref:`view_security_section` for + more information about view security and permissions. This is + often a string like ``view`` or ``edit``. + + If ``permission`` is omitted, a *default* permission may be used + for this view registration if one was named as the :class:`pyramid.config.Configurator` constructor's ``default_permission`` argument, or if - :meth:`pyramid.config.Configurator.set_default_permission` - was used prior to this view registration. Pass the string - :data:`pyramid.security.NO_PERMISSION_REQUIRED` as the - permission argument to explicitly indicate that the view should - always be executable by entirely anonymous users, regardless of - the default permission, bypassing any :term:`authorization - policy` that may be in effect. + :meth:`pyramid.config.Configurator.set_default_permission` was used + prior to this view registration. Pass the value + :data:`pyramid.security.NO_PERMISSION_REQUIRED` as the permission + argument to explicitly indicate that the view should always be + executable by entirely anonymous users, regardless of the default + permission, bypassing any :term:`authorization policy` that may be + in effect. attr + This knob is most useful when the view definition is a class. + The view machinery defaults to using the ``__call__`` method of the :term:`view callable` (or the function itself, if the view callable is a function) to obtain a response. The @@ -684,8 +714,7 @@ class ViewsConfiguratorMixin(object): class, and the class has a method named ``index`` and you wanted to use this method instead of the class' ``__call__`` method to return the response, you'd say ``attr="index"`` in the - view configuration for the view. This is - most useful when the view definition is a class. + view configuration for the view. renderer @@ -969,9 +998,16 @@ class ViewsConfiguratorMixin(object): Each custom predicate callable should accept two arguments: ``context`` and ``request`` and should return either ``True`` or ``False`` after doing arbitrary evaluation of - the context and/or the request. If all callables return - ``True``, the associated view callable will be considered - viable for a given request. + the context and/or the request. + + predicates + + Pass a key/value pair here to use a third-party predicate + registered via + :meth:`pyramid.config.Configurator.add_view_predicate`. More than + one key/value pair can be used at the same time. See + :ref:`view_and_route_predicates` for more information about + third-party predicates. This argument is new as of Pyramid 1.4. """ view = self.maybe_dotted(view) @@ -995,18 +1031,6 @@ class ViewsConfiguratorMixin(object): raise ConfigurationError( 'request_type must be an interface, not %s' % request_type) - if request_method is not None: - request_method = as_sorted_tuple(request_method) - if 'GET' in request_method and 'HEAD' not in request_method: - # GET implies HEAD too - request_method = as_sorted_tuple(request_method + ('HEAD',)) - - order, predicates, phash = make_predicates(xhr=xhr, - request_method=request_method, path_info=path_info, - request_param=request_param, header=header, accept=accept, - containment=containment, request_type=request_type, - match_param=match_param, custom=custom_predicates) - if context is None: context = for_ @@ -1021,18 +1045,42 @@ class ViewsConfiguratorMixin(object): name=renderer, package=self.package, registry = self.registry) + if accept is not None: + accept = accept.lower() + introspectables = [] - discriminator = [ - 'view', context, name, request_type, IView, containment, - request_param, request_method, route_name, attr, - xhr, accept, header, path_info, match_param] - discriminator.extend(sorted([hash(x) for x in custom_predicates])) - discriminator = tuple(discriminator) + pvals = predicates.copy() + pvals.update( + dict( + xhr=xhr, + request_method=request_method, + path_info=path_info, + request_param=request_param, + header=header, + accept=accept, + containment=containment, + request_type=request_type, + match_param=match_param, + custom=predvalseq(custom_predicates), + ) + ) + + def discrim_func(): + # We need to defer the discriminator until we know what the phash + # is. It can't be computed any sooner because thirdparty + # predicates may not yet exist when add_view is called. + order, preds, phash = predlist.make(self, **pvals) + view_intr.update({'phash':phash, 'order':order, 'predicates':preds}) + return ('view', context, name, route_name, phash) + + discriminator = Deferred(discrim_func) + if inspect.isclass(view) and attr: view_desc = 'method %r of %s' % ( attr, self.object_description(view)) else: view_desc = self.object_description(view) + view_intr = self.introspectable('views', discriminator, view_desc, @@ -1055,9 +1103,15 @@ class ViewsConfiguratorMixin(object): decorator=decorator, ) ) + view_intr.update(**predicates) introspectables.append(view_intr) + predlist = self.get_predlist('view') def register(permission=permission, renderer=renderer): + # the discrim_func above is guaranteed to have been called already + order = view_intr['order'] + preds = view_intr['predicates'] + phash = view_intr['phash'] request_iface = IRequest if route_name is not None: request_iface = self.registry.queryUtility(IRouteRequest, @@ -1082,21 +1136,28 @@ class ViewsConfiguratorMixin(object): # (reg'd in phase 1) permission = self.registry.queryUtility(IDefaultPermission) + # added by discrim_func above during conflict resolving + preds = view_intr['predicates'] + order = view_intr['order'] + phash = view_intr['phash'] + # __no_permission_required__ handled by _secure_view - deriver = ViewDeriver(registry=self.registry, - permission=permission, - predicates=predicates, - attr=attr, - renderer=renderer, - wrapper_viewname=wrapper, - viewname=name, - accept=accept, - order=order, - phash=phash, - package=self.package, - mapper=mapper, - decorator=decorator, - http_cache=http_cache) + deriver = ViewDeriver( + registry=self.registry, + permission=permission, + predicates=preds, + attr=attr, + renderer=renderer, + wrapper_viewname=wrapper, + viewname=name, + accept=accept, + order=order, + phash=phash, + package=self.package, + mapper=mapper, + decorator=decorator, + http_cache=http_cache, + ) derived_view = deriver(view) derived_view.__discriminator__ = lambda *arg: discriminator # __discriminator__ is used by superdynamic systems @@ -1201,19 +1262,25 @@ class ViewsConfiguratorMixin(object): IMultiView, name=name) if mapper: - mapper_intr = self.introspectable('view mappers', - discriminator, - 'view mapper for %s' % view_desc, - 'view mapper') + mapper_intr = self.introspectable( + 'view mappers', + discriminator, + 'view mapper for %s' % view_desc, + 'view mapper' + ) mapper_intr['mapper'] = mapper mapper_intr.relate('views', discriminator) introspectables.append(mapper_intr) if route_name: view_intr.relate('routes', route_name) # see add_route if renderer is not None and renderer.name and '.' in renderer.name: - # it's a template - tmpl_intr = self.introspectable('templates', discriminator, - renderer.name, 'template') + # the renderer is a template + tmpl_intr = self.introspectable( + 'templates', + discriminator, + renderer.name, + 'template' + ) tmpl_intr.relate('views', discriminator) tmpl_intr['name'] = renderer.name tmpl_intr['type'] = renderer.type @@ -1221,13 +1288,62 @@ class ViewsConfiguratorMixin(object): tmpl_intr.relate('renderer factories', renderer.type) introspectables.append(tmpl_intr) if permission is not None: - perm_intr = self.introspectable('permissions', permission, - permission, 'permission') + # if a permission exists, register a permission introspectable + perm_intr = self.introspectable( + 'permissions', + permission, + permission, + 'permission' + ) perm_intr['value'] = permission perm_intr.relate('views', discriminator) introspectables.append(perm_intr) self.action(discriminator, register, introspectables=introspectables) + @action_method + def add_view_predicate(self, name, factory, weighs_more_than=None, + weighs_less_than=None): + """ Adds a view predicate factory. The associated view predicate can + later be named as a keyword argument to + :meth:`pyramid.config.Configurator.add_view` in the + ``predicates`` anonyous keyword argument dictionary. + + ``name`` should be the name of the predicate. It must be a valid + Python identifier (it will be used as a keyword argument to + ``add_view`` by others). + + ``factory`` should be a :term:`predicate factory`. + + See :ref:`view_and_route_predicates` for more information. + + .. note:: + + This method is new as of Pyramid 1.4. + """ + self._add_predicate( + 'view', + name, + factory, + weighs_more_than=weighs_more_than, + weighs_less_than=weighs_less_than + ) + + def add_default_view_predicates(self): + p = pyramid.config.predicates + for (name, factory) in ( + ('xhr', p.XHRPredicate), + ('request_method', p.RequestMethodPredicate), + ('path_info', p.PathInfoPredicate), + ('request_param', p.RequestParamPredicate), + ('header', p.HeaderPredicate), + ('accept', p.AcceptPredicate), + ('containment', p.ContainmentPredicate), + ('request_type', p.RequestTypePredicate), + ('match_param', p.MatchParamPredicate), + ('custom', p.CustomPredicate), + ): + self.add_view_predicate(name, factory) + def derive_view(self, view, attr=None, renderer=None): """ Create a :term:`view callable` using the function, instance, @@ -1345,11 +1461,26 @@ class ViewsConfiguratorMixin(object): @action_method def add_forbidden_view( - self, view=None, attr=None, renderer=None, wrapper=None, - route_name=None, request_type=None, request_method=None, - request_param=None, containment=None, xhr=None, accept=None, - header=None, path_info=None, custom_predicates=(), decorator=None, - mapper=None, match_param=None): + self, + view=None, + attr=None, + renderer=None, + wrapper=None, + route_name=None, + request_type=None, + request_method=None, + request_param=None, + containment=None, + xhr=None, + accept=None, + header=None, + path_info=None, + custom_predicates=(), + decorator=None, + mapper=None, + match_param=None, + **predicates + ): """ Add a forbidden view to the current configuration state. The view will be called when Pyramid or application code raises a :exc:`pyramid.httpexceptions.HTTPForbidden` exception and the set of @@ -1366,12 +1497,23 @@ class ViewsConfiguratorMixin(object): All arguments have the same meaning as :meth:`pyramid.config.Configurator.add_view` and each predicate argument restricts the set of circumstances under which this notfound - view will be invoked. + view will be invoked. Unlike + :meth:`pyramid.config.Configurator.add_view`, this method will raise + an exception if passed ``name``, ``permission``, ``context``, + ``for_``, or ``http_cache`` keyword arguments. These argument values + make no sense in the context of a forbidden view. .. note:: This method is new as of Pyramid 1.3. """ + for arg in ('name', 'permission', 'context', 'for_', 'http_cache'): + if arg in predicates: + raise ConfigurationError( + '%s may not be used as an argument to add_forbidden_view' + % arg + ) + settings = dict( view=view, context=HTTPForbidden, @@ -1393,17 +1535,34 @@ class ViewsConfiguratorMixin(object): attr=attr, renderer=renderer, ) + settings.update(predicates) return self.add_view(**settings) set_forbidden_view = add_forbidden_view # deprecated sorta-bw-compat alias @action_method def add_notfound_view( - self, view=None, attr=None, renderer=None, wrapper=None, - route_name=None, request_type=None, request_method=None, - request_param=None, containment=None, xhr=None, accept=None, - header=None, path_info=None, custom_predicates=(), decorator=None, - mapper=None, match_param=None, append_slash=False): + self, + view=None, + attr=None, + renderer=None, + wrapper=None, + route_name=None, + request_type=None, + request_method=None, + request_param=None, + containment=None, + xhr=None, + accept=None, + header=None, + path_info=None, + custom_predicates=(), + decorator=None, + mapper=None, + match_param=None, + append_slash=False, + **predicates + ): """ Add a default notfound view to the current configuration state. The view will be called when Pyramid or application code raises an :exc:`pyramid.httpexceptions.HTTPForbidden` exception (e.g. when a @@ -1419,7 +1578,11 @@ class ViewsConfiguratorMixin(object): All arguments except ``append_slash`` have the same meaning as :meth:`pyramid.config.Configurator.add_view` and each predicate argument restricts the set of circumstances under which this notfound - view will be invoked. + view will be invoked. Unlike + :meth:`pyramid.config.Configurator.add_view`, this method will raise + an exception if passed ``name``, ``permission``, ``context``, + ``for_``, or ``http_cache`` keyword arguments. These argument values + make no sense in the context of a notfound view. If ``append_slash`` is ``True``, when this notfound view is invoked, and the current path info does not end in a slash, the notfound logic @@ -1433,6 +1596,13 @@ class ViewsConfiguratorMixin(object): This method is new as of Pyramid 1.3. """ + for arg in ('name', 'permission', 'context', 'for_', 'http_cache'): + if arg in predicates: + raise ConfigurationError( + '%s may not be used as an argument to add_notfound_view' + % arg + ) + settings = dict( view=view, context=HTTPNotFound, @@ -1452,6 +1622,7 @@ class ViewsConfiguratorMixin(object): route_name=route_name, permission=NO_PERMISSION_REQUIRED, ) + settings.update(predicates) if append_slash: view = self._derive_view(view, attr=attr, renderer=renderer) view = AppendSlashNotFoundViewFactory(view) 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/decorator.py b/pyramid/decorator.py index 98d7b36b5..82d2b1280 100644 --- a/pyramid/decorator.py +++ b/pyramid/decorator.py @@ -1,9 +1,31 @@ class reify(object): + """ Use as a class method decorator. It operates almost exactly like the + Python ``@property`` decorator, but it puts the result of the method it + decorates into the instance dict after the first call, effectively + replacing the function it decorates with an instance variable. It is, in + Python parlance, a non-data descriptor. An example: - """ Put the result of a method which uses this (non-data) - descriptor decorator in the instance dict after the first call, - effectively replacing the decorator with an instance variable.""" + .. code-block:: python + class Foo(object): + @reify + def jammy(self): + print 'jammy called' + return 1 + + And usage of Foo: + + .. code-block:: text + + >>> f = Foo() + >>> v = f.jammy + 'jammy called' + >>> print v + 1 + >>> f.jammy + 1 + >>> # jammy func not called the second time; it replaced itself with 1 + """ def __init__(self, wrapped): self.wrapped = wrapped try: diff --git a/pyramid/events.py b/pyramid/events.py index e181ef33f..836466ba2 100644 --- a/pyramid/events.py +++ b/pyramid/events.py @@ -14,9 +14,10 @@ from pyramid.interfaces import ( ) class subscriber(object): - """ Decorator activated via a :term:`scan` which treats the - function being decorated as an event subscriber for the set of - interfaces passed as ``*ifaces`` to the decorator constructor. + """ Decorator activated via a :term:`scan` which treats the function + being decorated as an event subscriber for the set of interfaces passed + as ``*ifaces`` and the set of predicate terms passed as ``**predicates`` + to the decorator constructor. For example: @@ -61,16 +62,22 @@ class subscriber(object): config = Configurator() config.scan('somepackage_containing_subscribers') + Any ``**predicate`` arguments will be passed along to + :meth:`pyramid.config.Configurator.add_subscriber`. See + :ref:`subscriber_predicates` for a description of how predicates can + narrow the set of circumstances in which a subscriber will be called. + """ venusian = venusian # for unit testing - def __init__(self, *ifaces): + def __init__(self, *ifaces, **predicates): self.ifaces = ifaces + self.predicates = predicates def register(self, scanner, name, wrapped): config = scanner.config for iface in self.ifaces or (Interface,): - config.add_subscriber(wrapped, iface) + config.add_subscriber(wrapped, iface, **self.predicates) def __call__(self, wrapped): self.venusian.attach(wrapped, self.register, category='pyramid') @@ -200,10 +207,34 @@ class BeforeRender(dict): setting an overriding value (which can be done using ``.get`` or ``__contains__`` of the event object). - The event has an additional attribute named ``rendering_val``. This is - the (non-system) value returned by a view or passed to ``render*`` as - ``value``. This feature is new in Pyramid 1.2. - + The dictionary returned from the view is accessible through the + :attr:`rendering_val` attribute of a :class:`~pyramid.events.BeforeRender` + event. + + Suppose you return ``{'mykey': 'somevalue', 'mykey2': 'somevalue2'}`` from + your view callable, like so:: + + from pyramid.view import view_config + + @view_config(renderer='some_renderer') + def myview(request): + return {'mykey': 'somevalue', 'mykey2': 'somevalue2'} + + :attr:`rendering_val` can be used to access these values from the + :class:`~pyramid.events.BeforeRender` object:: + + from pyramid.events import subscriber + from pyramid.events import BeforeRender + + @subscriber(BeforeRender) + def read_return(event): + # {'mykey': 'somevalue'} is returned from the view + print(event.rendering_val['mykey']) + + In other words, :attr:`rendering_val` is the (non-system) value returned by a + view or passed to ``render*`` as ``value``. This feature is new in Pyramid + 1.2. + For a description of the values present in the renderer globals dictionary, see :ref:`renderer_system_values`. diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index 1445ee394..042b4487b 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -513,9 +513,13 @@ class IRequestHandler(Interface): IRequest.combined = IRequest # for exception view lookups -class IRequestProperties(Interface): - """ Marker interface for storing a list of request properties which - will be added to the request object.""" +class IRequestExtensions(Interface): + """ Marker interface for storing request extensions (properties and + methods) which will be added to the request object.""" + descriptors = Attribute( + """A list of descriptors that will be added to each request.""") + methods = Attribute( + """A list of methods to be added to each request.""") class IRouteRequest(Interface): """ *internal only* interface used as in a utility lookup to find @@ -1111,6 +1115,9 @@ class IJSONAdapter(Interface): into a JSON-serializable primitive. """ +class IPredicateList(Interface): + """ Interface representing a predicate list """ + # configuration phases: a lower phase number means the actions associated # with this phase will be executed earlier than those with later phase # numbers. The default phase number is 0, FTR. diff --git a/pyramid/mako_templating.py b/pyramid/mako_templating.py index 208e54bf5..5d09cad01 100644 --- a/pyramid/mako_templating.py +++ b/pyramid/mako_templating.py @@ -1,4 +1,6 @@ import os +import posixpath +import re import sys import threading @@ -36,16 +38,28 @@ class PkgResourceTemplateLookup(TemplateLookup): isabs = os.path.isabs(uri) if (not isabs) and (':' in uri): return uri + if not(isabs) and ('$' in uri): + return uri.replace('$', ':') + if relativeto is not None: + relativeto = relativeto.replace('$', ':') + if not(':' in uri) and (':' in relativeto): + if uri.startswith('/'): + return uri + pkg, relto = relativeto.split(':') + _uri = posixpath.join(posixpath.dirname(relto), uri) + return '{0}:{1}'.format(pkg, _uri) + if not(':' in uri) and not(':' in relativeto): + return posixpath.join(posixpath.dirname(relativeto), uri) return TemplateLookup.adjust_uri(self, uri, relativeto) def get_template(self, uri): """Fetch a template from the cache, or check the filesystem for it - + In addition to the basic filesystem lookup, this subclass will use pkg_resource to load a file using the asset specification syntax. - + """ isabs = os.path.isabs(uri) if (not isabs) and (':' in uri): @@ -69,14 +83,22 @@ class PkgResourceTemplateLookup(TemplateLookup): return TemplateLookup.get_template(self, uri) -registry_lock = threading.Lock() +registry_lock = threading.Lock() class MakoRendererFactoryHelper(object): def __init__(self, settings_prefix=None): self.settings_prefix = settings_prefix def __call__(self, info): - path = info.name + p = re.compile( + r'(?P<asset>[\w_.:/]+)' + r'(?:\#(?P<defname>[\w_]+))?' + r'(\.(?P<ext>.*))' + ) + asset, defname, ext = p.match(info.name).group( + 'asset', 'defname', 'ext' + ) + path = '%s.%s' % (asset, ext) registry = info.registry settings = info.settings settings_prefix = self.settings_prefix @@ -134,14 +156,11 @@ class MakoRendererFactoryHelper(object): preprocessor=preprocessor ) - registry_lock.acquire() - try: - registry.registerUtility(lookup, IMakoLookup, + with registry_lock: + registry.registerUtility(lookup, IMakoLookup, name=settings_prefix) - finally: - registry_lock.release() - return MakoLookupTemplateRenderer(path, lookup) + return MakoLookupTemplateRenderer(path, defname, lookup) renderer_factory = MakoRendererFactoryHelper('mako.') @@ -156,10 +175,18 @@ class MakoRenderingException(Exception): @implementer(ITemplateRenderer) class MakoLookupTemplateRenderer(object): - def __init__(self, path, lookup): + """ Render a :term:`Mako` template using the template + implied by the ``path`` argument.The ``path`` argument may be a + package-relative path, an absolute path, or a :term:`asset + specification`. If a defname is defined, in the form of + package:path/to/template#defname.mako, a function named ``defname`` + inside the template will then be rendered. + """ + def __init__(self, path, defname, lookup): self.path = path + self.defname = defname self.lookup = lookup - + def implementation(self): return self.lookup.get_template(self.path) @@ -167,16 +194,19 @@ class MakoLookupTemplateRenderer(object): context = system.pop('context', None) if context is not None: system['_context'] = context - def_name = None - if isinstance(value, tuple): - def_name, value = value + if self.defname is None: + if isinstance(value, tuple): + self.defname, value = value + else: + if isinstance(value, tuple): + _, value = value try: system.update(value) except (TypeError, ValueError): raise ValueError('renderer was passed non-dictionary as value') template = self.implementation() - if def_name is not None: - template = template.get_def(def_name) + if self.defname is not None: + template = template.get_def(self.defname) try: result = template.render_unicode(**system) except: diff --git a/pyramid/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/registry.py b/pyramid/registry.py index f0f9c83ea..606251a8d 100644 --- a/pyramid/registry.py +++ b/pyramid/registry.py @@ -191,14 +191,20 @@ class Introspectable(dict): def unrelate(self, category_name, discriminator): self._relations.append((False, category_name, discriminator)) + def _assert_resolved(self): + assert undefer(self.discriminator) is self.discriminator + @property def discriminator_hash(self): + self._assert_resolved() return hash(self.discriminator) def __hash__(self): + self._assert_resolved() return hash((self.category_name,) + (self.discriminator,)) def __repr__(self): + self._assert_resolved() return '<%s category %r, discriminator %r>' % (self.__class__.__name__, self.category_name, self.discriminator) @@ -209,9 +215,11 @@ class Introspectable(dict): __bool__ = __nonzero__ # py3 def register(self, introspector, action_info): + self.discriminator = undefer(self.discriminator) self.action_info = action_info introspector.add(self) for relate, category_name, discriminator in self._relations: + discriminator = undefer(discriminator) if relate: method = introspector.relate else: @@ -221,4 +229,29 @@ class Introspectable(dict): (category_name, discriminator) ) +class Deferred(object): + """ Can be used by a third-party configuration extender to wrap a + :term:`discriminator` during configuration if an immediately hashable + discriminator cannot be computed because it relies on unresolved values. + The function should accept no arguments and should return a hashable + discriminator.""" + def __init__(self, func): + self.func = func + + def resolve(self): + return self.func() + +def undefer(v): + """ Function which accepts an object and returns it unless it is a + :class:`pyramid.registry.Deferred` instance. If it is an instance of + that class, its ``resolve`` method is called, and the result of the + method is returned.""" + if isinstance(v, Deferred): + v = v.resolve() + return v + +class predvalseq(tuple): + """ A subtype of tuple used to represent a sequence of predicate values """ + pass + global_registry = Registry('global') diff --git a/pyramid/renderers.py b/pyramid/renderers.py index e526f9997..3252c2c93 100644 --- a/pyramid/renderers.py +++ b/pyramid/renderers.py @@ -1,5 +1,6 @@ import json import os +import re import pkg_resources import threading @@ -426,18 +427,24 @@ class ChameleonRendererLookup(object): raise ValueError('Missing template file: %s' % spec) renderer = registry.queryUtility(ITemplateRenderer, name=spec) if renderer is None: - renderer = self.impl(spec, self) + renderer = self.impl(spec, self, macro=None) # cache the template - try: - self.lock.acquire() + with self.lock: registry.registerUtility(renderer, ITemplateRenderer, name=spec) - finally: - self.lock.release() else: # spec is a package:relpath asset spec renderer = registry.queryUtility(ITemplateRenderer, name=spec) if renderer is None: + p = re.compile( + r'(?P<asset>[\w_.:/]+)' + r'(?:\#(?P<defname>[\w_]+))?' + r'(\.(?P<ext>.*))' + ) + asset, macro, ext = p.match(spec).group( + 'asset', 'defname', 'ext' + ) + spec = '%s.%s' % (asset, ext) try: package_name, filename = spec.split(':', 1) except ValueError: # pragma: no cover @@ -450,16 +457,13 @@ class ChameleonRendererLookup(object): if not pkg_resources.resource_exists(package_name, filename): raise ValueError( 'Missing template asset: %s (%s)' % (spec, abspath)) - renderer = self.impl(abspath, self) + renderer = self.impl(abspath, self, macro=macro) settings = info.settings if not settings.get('reload_assets'): # cache the template - self.lock.acquire() - try: + with self.lock: registry.registerUtility(renderer, ITemplateRenderer, name=spec) - finally: - self.lock.release() return renderer @@ -470,11 +474,8 @@ def template_renderer_factory(info, impl, lock=registry_lock): lookup = registry.queryUtility(IChameleonLookup, name=info.type) if lookup is None: lookup = ChameleonRendererLookup(impl, registry) - lock.acquire() - try: + with lock: registry.registerUtility(lookup, IChameleonLookup, name=info.type) - finally: - lock.release() return lookup(info) @implementer(IRendererInfo) 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/scripts/proutes.py b/pyramid/scripts/proutes.py index 29ec9e72a..f64107d2b 100644 --- a/pyramid/scripts/proutes.py +++ b/pyramid/scripts/proutes.py @@ -15,10 +15,10 @@ class PRoutesCommand(object): route, the pattern of the route, and the view callable which will be invoked when the route is matched. - This command accepts one positional argument named "config_uri". It + This command accepts one positional argument named 'config_uri'. It specifies the PasteDeploy config file to use for the interactive - shell. The format is "inifile#name". If the name is left off, "main" - will be assumed. Example: "proutes myapp.ini". + shell. The format is 'inifile#name'. If the name is left off, 'main' + will be assumed. Example: 'proutes myapp.ini'. """ bootstrap = (bootstrap,) diff --git a/pyramid/scripts/pserve.py b/pyramid/scripts/pserve.py index ea2a4ae09..9fbf0729a 100644 --- a/pyramid/scripts/pserve.py +++ b/pyramid/scripts/pserve.py @@ -583,12 +583,8 @@ class LazyWriter(object): def open(self): if self.fileobj is None: - self.lock.acquire() - try: - if self.fileobj is None: - self.fileobj = open(self.filename, self.mode) - finally: - self.lock.release() + with self.lock: + self.fileobj = open(self.filename, self.mode) return self.fileobj def close(self): diff --git a/pyramid/scripts/pviews.py b/pyramid/scripts/pviews.py index 72a9800c3..a9db59dc1 100644 --- a/pyramid/scripts/pviews.py +++ b/pyramid/scripts/pviews.py @@ -17,11 +17,11 @@ class PViewsCommand(object): each route+predicate set, print each view that might match and its predicates. - This command accepts two positional arguments: "config_uri" specifies the + This command accepts two positional arguments: 'config_uri' specifies the PasteDeploy config file to use for the interactive shell. The format is - "inifile#name". If the name is left off, "main" will be assumed. "url" + 'inifile#name'. If the name is left off, 'main' will be assumed. 'url' specifies the path info portion of a URL that will be used to find - matching views. Example: "proutes myapp.ini#main /url" + matching views. Example: 'proutes myapp.ini#main /url' """ stdout = sys.stdout @@ -223,7 +223,7 @@ class PViewsCommand(object): self.out("%srequired permission = %s" % (indent, permission)) predicates = getattr(view_wrapper, '__predicates__', None) if predicates is not None: - predicate_text = ', '.join([p.__text__ for p in predicates]) + predicate_text = ', '.join([p.text() for p in predicates]) self.out("%sview predicates (%s)" % (indent, predicate_text)) def run(self): diff --git a/pyramid/session.py b/pyramid/session.py index 76b2b30b1..40e21ddbc 100644 --- a/pyramid/session.py +++ b/pyramid/session.py @@ -33,6 +33,53 @@ def manage_accessed(wrapped): accessed.__doc__ = wrapped.__doc__ return accessed +def signed_serialize(data, secret): + """ Serialize any pickleable structure (``data``) and sign it + using the ``secret`` (must be a string). Return the + serialization, which includes the signature as its first 40 bytes. + The ``signed_deserialize`` method will deserialize such a value. + + This function is useful for creating signed cookies. For example: + + .. code-block:: python + + cookieval = signed_serialize({'a':1}, 'secret') + response.set_cookie('signed_cookie', cookieval) + """ + pickled = pickle.dumps(data, pickle.HIGHEST_PROTOCOL) + sig = hmac.new(bytes_(secret), pickled, sha1).hexdigest() + return sig + native_(base64.b64encode(pickled)) + +def signed_deserialize(serialized, secret, hmac=hmac): + """ Deserialize the value returned from ``signed_serialize``. If + the value cannot be deserialized for any reason, a + :exc:`ValueError` exception will be raised. + + This function is useful for deserializing a signed cookie value + created by ``signed_serialize``. For example: + + .. code-block:: python + + cookieval = request.cookies['signed_cookie'] + data = signed_deserialize(cookieval, 'secret') + """ + # hmac parameterized only for unit tests + try: + input_sig, pickled = (serialized[:40], + base64.b64decode(bytes_(serialized[40:]))) + except (binascii.Error, TypeError) as e: + # Badly formed data can make base64 die + raise ValueError('Badly formed base64 data: %s' % e) + + sig = hmac.new(bytes_(secret), pickled, sha1).hexdigest() + + # Avoid timing attacks (see + # http://seb.dbzteam.org/crypto/python-oauth-timing-hmac.pdf) + if strings_differ(sig, input_sig): + raise ValueError('Invalid signature') + + return pickle.loads(pickled) + def UnencryptedCookieSessionFactoryConfig( secret, timeout=1200, @@ -43,6 +90,8 @@ def UnencryptedCookieSessionFactoryConfig( cookie_secure=False, cookie_httponly=False, cookie_on_exception=True, + signed_serialize=signed_serialize, + signed_deserialize=signed_deserialize, ): """ Configure a :term:`session factory` which will provide unencrypted @@ -89,6 +138,15 @@ def UnencryptedCookieSessionFactoryConfig( If ``True``, set a session cookie even if an exception occurs while rendering a view. Default: ``True``. + ``signed_serialize`` + A callable which takes more or less arbitrary python data structure and + a secret and returns a signed serialization in bytes. + Default: ``signed_serialize`` (using pickle). + + ``signed_deserialize`` + A callable which takes a signed and serialized data structure in bytes + and a secret and returns the original data structure if the signature + is valid. Default: ``signed_deserialize`` (using pickle). """ @implementer(ISession) @@ -225,51 +283,3 @@ def UnencryptedCookieSessionFactoryConfig( return True return UnencryptedCookieSessionFactory - -def signed_serialize(data, secret): - """ Serialize any pickleable structure (``data``) and sign it - using the ``secret`` (must be a string). Return the - serialization, which includes the signature as its first 40 bytes. - The ``signed_deserialize`` method will deserialize such a value. - - This function is useful for creating signed cookies. For example: - - .. code-block:: python - - cookieval = signed_serialize({'a':1}, 'secret') - response.set_cookie('signed_cookie', cookieval) - """ - pickled = pickle.dumps(data, pickle.HIGHEST_PROTOCOL) - sig = hmac.new(bytes_(secret), pickled, sha1).hexdigest() - return sig + native_(base64.b64encode(pickled)) - -def signed_deserialize(serialized, secret, hmac=hmac): - """ Deserialize the value returned from ``signed_serialize``. If - the value cannot be deserialized for any reason, a - :exc:`ValueError` exception will be raised. - - This function is useful for deserializing a signed cookie value - created by ``signed_serialize``. For example: - - .. code-block:: python - - cookieval = request.cookies['signed_cookie'] - data = signed_deserialize(cookieval, 'secret') - """ - # hmac parameterized only for unit tests - try: - input_sig, pickled = (serialized[:40], - base64.b64decode(bytes_(serialized[40:]))) - except (binascii.Error, TypeError) as e: - # Badly formed data can make base64 die - raise ValueError('Badly formed base64 data: %s' % e) - - sig = hmac.new(bytes_(secret), pickled, sha1).hexdigest() - - # Avoid timing attacks (see - # http://seb.dbzteam.org/crypto/python-oauth-timing-hmac.pdf) - if strings_differ(sig, input_sig): - raise ValueError('Invalid signature') - - return pickle.loads(pickled) - 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 5589f6ae2..9e8f2bff3 100644 --- a/pyramid/testing.py +++ b/pyramid/testing.py @@ -2,20 +2,14 @@ import copy import os from contextlib import contextmanager -from zope.deprecation import deprecated - from zope.interface import ( implementer, - Interface, alsoProvides, ) from pyramid.interfaces import ( IRequest, IResponseFactory, - ISecuredView, - IView, - IViewClassifier, ISession, ) @@ -27,14 +21,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 ( @@ -48,373 +40,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) - 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 +275,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 @@ -665,7 +294,7 @@ class DummyRequest(DeprecatedRequestMethodsMixin, URLMethodsMixin, request. For example, by default, the DummyRequest ``GET`` and ``POST`` attributes are of type ``dict``, unlike a normal Request's GET and POST, which are of type ``MultiDict``. If your code uses the features of - MultiDict, you should either use a"real" :class:`pyramid.request.Request` + MultiDict, you should either use a real :class:`pyramid.request.Request` or adapt your DummyRequest by replacing the attributes with ``MultiDict`` instances. @@ -825,6 +454,8 @@ def setUp(registry=None, request=None, hook_zca=True, autocommit=True, # ``render_template`` and friends went behind the back of # any existing renderer factory lookup system. config.add_renderer(name, renderer) + config.add_default_view_predicates() + config.add_default_route_predicates() config.commit() global have_zca try: diff --git a/pyramid/tests/fixtures/components.mak b/pyramid/tests/fixtures/components.mak new file mode 100644 index 000000000..cc886805c --- /dev/null +++ b/pyramid/tests/fixtures/components.mak @@ -0,0 +1,3 @@ +<%def name="comp()"> +World! +</%def>
\ No newline at end of file diff --git a/pyramid/tests/fixtures/hellocompo.mak b/pyramid/tests/fixtures/hellocompo.mak new file mode 100644 index 000000000..142676a11 --- /dev/null +++ b/pyramid/tests/fixtures/hellocompo.mak @@ -0,0 +1,3 @@ +<%namespace name="comp" file="pyramid.tests:fixtures/components.mak"/> +Namespace +Hello ${comp.comp()}
\ No newline at end of file diff --git a/pyramid/tests/fixtures/withmacro.pt b/pyramid/tests/fixtures/withmacro.pt new file mode 100644 index 000000000..8bca01e4d --- /dev/null +++ b/pyramid/tests/fixtures/withmacro.pt @@ -0,0 +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..d9f20f241 100644 --- a/pyramid/tests/test_chameleon_text.py +++ b/pyramid/tests/test_chameleon_text.py @@ -1,28 +1,32 @@ +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): @@ -45,7 +49,6 @@ class TextTemplateRendererTests(Base, unittest.TestCase): from pyramid.interfaces import ITemplateRenderer verifyClass(ITemplateRenderer, self._getTargetClass()) - @skip_on('java') def test_template_reified(self): minimal = self._getTemplatePath('minimal.txt') lookup = DummyLookup() @@ -54,7 +57,6 @@ class TextTemplateRendererTests(Base, unittest.TestCase): template = instance.template self.assertEqual(template, instance.__dict__['template']) - @skip_on('java') def test_template_with_ichameleon_translate(self): minimal = self._getTemplatePath('minimal.txt') lookup = DummyLookup() @@ -63,7 +65,6 @@ class TextTemplateRendererTests(Base, unittest.TestCase): template = instance.template self.assertEqual(template.translate, lookup.translate) - @skip_on('java') def test_template_with_debug_templates(self): minimal = self._getTemplatePath('minimal.txt') lookup = DummyLookup() @@ -73,7 +74,6 @@ class TextTemplateRendererTests(Base, unittest.TestCase): template = instance.template self.assertEqual(template.debug, True) - @skip_on('java') def test_template_with_reload_templates(self): minimal = self._getTemplatePath('minimal.txt') lookup = DummyLookup() @@ -83,7 +83,6 @@ class TextTemplateRendererTests(Base, unittest.TestCase): template = instance.template self.assertEqual(template.auto_reload, True) - @skip_on('java') def test_template_without_reload_templates(self): minimal = self._getTemplatePath('minimal.txt') lookup = DummyLookup() @@ -93,7 +92,6 @@ class TextTemplateRendererTests(Base, unittest.TestCase): template = instance.template self.assertEqual(template.auto_reload, False) - @skip_on('java') def test_call(self): minimal = self._getTemplatePath('minimal.txt') lookup = DummyLookup() @@ -102,14 +100,12 @@ class TextTemplateRendererTests(Base, unittest.TestCase): self.assertTrue(isinstance(result, binary_type)) self.assertEqual(result, b'Hello.\n') - @skip_on('java') def test_call_with_nondict_value(self): minimal = self._getTemplatePath('minimal.txt') lookup = DummyLookup() instance = self._makeOne(minimal, lookup) self.assertRaises(ValueError, instance, None, {}) - @skip_on('java') def test_call_nonminimal(self): nonminimal = self._getTemplatePath('nonminimal.txt') lookup = DummyLookup() @@ -118,7 +114,6 @@ class TextTemplateRendererTests(Base, unittest.TestCase): self.assertTrue(isinstance(result, binary_type)) self.assertEqual(result, b'Hello, Chris!\n') - @skip_on('java') def test_implementation(self): minimal = self._getTemplatePath('minimal.txt') lookup = DummyLookup() @@ -127,83 +122,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..37538e83e 100644 --- a/pyramid/tests/test_chameleon_zpt.py +++ b/pyramid/tests/test_chameleon_zpt.py @@ -1,30 +1,33 @@ +import sys import unittest -from pyramid.testing import skip_on from pyramid import testing 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 @@ -46,7 +49,6 @@ class ZPTTemplateRendererTests(Base, unittest.TestCase): from pyramid.interfaces import ITemplateRenderer verifyClass(ITemplateRenderer, self._getTargetClass()) - @skip_on('java') def test_call(self): minimal = self._getTemplatePath('minimal.pt') lookup = DummyLookup() @@ -56,7 +58,6 @@ class ZPTTemplateRendererTests(Base, unittest.TestCase): self.assertEqual(result.rstrip('\n'), '<div xmlns="http://www.w3.org/1999/xhtml">\n</div>') - @skip_on('java') def test_template_reified(self): minimal = self._getTemplatePath('minimal.pt') lookup = DummyLookup() @@ -65,7 +66,6 @@ class ZPTTemplateRendererTests(Base, unittest.TestCase): template = instance.template self.assertEqual(template, instance.__dict__['template']) - @skip_on('java') def test_template_with_ichameleon_translate(self): minimal = self._getTemplatePath('minimal.pt') lookup = DummyLookup() @@ -74,7 +74,6 @@ class ZPTTemplateRendererTests(Base, unittest.TestCase): template = instance.template self.assertEqual(template.translate, lookup.translate) - @skip_on('java') def test_template_with_debug_templates(self): minimal = self._getTemplatePath('minimal.pt') lookup = DummyLookup() @@ -84,7 +83,6 @@ class ZPTTemplateRendererTests(Base, unittest.TestCase): template = instance.template self.assertEqual(template.debug, True) - @skip_on('java') def test_template_without_debug_templates(self): minimal = self._getTemplatePath('minimal.pt') lookup = DummyLookup() @@ -94,7 +92,6 @@ class ZPTTemplateRendererTests(Base, unittest.TestCase): template = instance.template self.assertEqual(template.debug, False) - @skip_on('java') def test_template_with_reload_templates(self): minimal = self._getTemplatePath('minimal.pt') lookup = DummyLookup() @@ -104,7 +101,6 @@ class ZPTTemplateRendererTests(Base, unittest.TestCase): template = instance.template self.assertEqual(template.auto_reload, True) - @skip_on('java') def test_template_without_reload_templates(self): minimal = self._getTemplatePath('minimal.pt') lookup = DummyLookup() @@ -114,14 +110,12 @@ class ZPTTemplateRendererTests(Base, unittest.TestCase): template = instance.template self.assertEqual(template.auto_reload, False) - @skip_on('java') def test_call_with_nondict_value(self): minimal = self._getTemplatePath('minimal.pt') lookup = DummyLookup() instance = self._makeOne(minimal, lookup) self.assertRaises(ValueError, instance, None, {}) - @skip_on('java') def test_implementation(self): minimal = self._getTemplatePath('minimal.pt') lookup = DummyLookup() @@ -130,86 +124,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/__init__.py b/pyramid/tests/test_config/__init__.py index 5b40a8c09..81d9f4965 100644 --- a/pyramid/tests/test_config/__init__.py +++ b/pyramid/tests/test_config/__init__.py @@ -43,3 +43,11 @@ def dummy_extend(config, discrim): def dummy_extend2(config, discrim): config.action(discrim, None, config.registry) +from functools import partial +dummy_partial = partial(dummy_extend, discrim='partial') + +class DummyCallable(object): + def __call__(self, config, discrim): + config.action(discrim, None, config.package) +dummy_callable = DummyCallable() + diff --git a/pyramid/tests/test_config/test_adapters.py b/pyramid/tests/test_config/test_adapters.py index 83ea0f05b..d47e012dc 100644 --- a/pyramid/tests/test_config/test_adapters.py +++ b/pyramid/tests/test_config/test_adapters.py @@ -81,6 +81,121 @@ class AdaptersConfiguratorMixinTests(unittest.TestCase): config.registry.subscribers((event.object, IDummy), None) self.assertEqual(len(L), 1) + def test_add_subscriber_with_specific_type_and_predicates_True(self): + from zope.interface import implementer + from zope.interface import Interface + class IEvent(Interface): + pass + @implementer(IEvent) + class Event: + pass + L = [] + def subscriber(event): + L.append(event) + config = self._makeOne(autocommit=True) + predlist = config.get_predlist('subscriber') + jam_predicate = predicate_maker('jam') + jim_predicate = predicate_maker('jim') + predlist.add('jam', jam_predicate) + predlist.add('jim', jim_predicate) + config.add_subscriber(subscriber, IEvent, jam=True, jim=True) + event = Event() + event.jam = True + event.jim = True + config.registry.notify(event) + self.assertEqual(len(L), 1) + self.assertEqual(L[0], event) + config.registry.notify(object()) + self.assertEqual(len(L), 1) + + def test_add_subscriber_with_default_type_predicates_True(self): + from zope.interface import implementer + from zope.interface import Interface + class IEvent(Interface): + pass + @implementer(IEvent) + class Event: + pass + L = [] + def subscriber(event): + L.append(event) + config = self._makeOne(autocommit=True) + predlist = config.get_predlist('subscriber') + jam_predicate = predicate_maker('jam') + jim_predicate = predicate_maker('jim') + predlist.add('jam', jam_predicate) + predlist.add('jim', jim_predicate) + config.add_subscriber(subscriber, jam=True, jim=True) + event = Event() + event.jam = True + event.jim = True + config.registry.notify(event) + self.assertEqual(len(L), 1) + self.assertEqual(L[0], event) + config.registry.notify(object()) + self.assertEqual(len(L), 1) + + def test_add_subscriber_with_specific_type_and_predicates_False(self): + from zope.interface import implementer + from zope.interface import Interface + class IEvent(Interface): + pass + @implementer(IEvent) + class Event: + pass + L = [] + def subscriber(event): L.append(event) + config = self._makeOne(autocommit=True) + predlist = config.get_predlist('subscriber') + jam_predicate = predicate_maker('jam') + jim_predicate = predicate_maker('jim') + predlist.add('jam', jam_predicate) + predlist.add('jim', jim_predicate) + config.add_subscriber(subscriber, IEvent, jam=True, jim=True) + event = Event() + event.jam = True + event.jim = False + config.registry.notify(event) + self.assertEqual(len(L), 0) + + def test_add_subscriber_with_default_type_predicates_False(self): + from zope.interface import implementer + from zope.interface import Interface + class IEvent(Interface): + pass + @implementer(IEvent) + class Event: + pass + L = [] + def subscriber(event): L.append(event) + config = self._makeOne(autocommit=True) + predlist = config.get_predlist('subscriber') + jam_predicate = predicate_maker('jam') + jim_predicate = predicate_maker('jim') + predlist.add('jam', jam_predicate) + predlist.add('jim', jim_predicate) + config.add_subscriber(subscriber, jam=True, jim=True) + event = Event() + event.jam = False + event.jim = True + config.registry.notify(event) + self.assertEqual(len(L), 0) + + def test_add_subscriber_predicate(self): + config = self._makeOne() + L = [] + def add_predicate(type, name, factory, weighs_less_than=None, + weighs_more_than=None): + self.assertEqual(type, 'subscriber') + self.assertEqual(name, 'name') + self.assertEqual(factory, 'factory') + self.assertEqual(weighs_more_than, 1) + self.assertEqual(weighs_less_than, 2) + L.append(1) + config._add_predicate = add_predicate + config.add_subscriber_predicate('name', 'factory', 1, 2) + self.assertTrue(L) + def test_add_response_adapter(self): from pyramid.interfaces import IResponse config = self._makeOne(autocommit=True) @@ -228,4 +343,14 @@ class DummyResourceURL(object): self.resource = resource self.request = request - +def predicate_maker(name): + class Predicate(object): + def __init__(self, val, config): + self.val = val + def phash(self): + return 'phash' + text = phash + def __call__(self, event): + return getattr(event, name, None) == self.val + return Predicate + diff --git a/pyramid/tests/test_config/test_factories.py b/pyramid/tests/test_config/test_factories.py index 1dfeda34c..e89fc077e 100644 --- a/pyramid/tests/test_config/test_factories.py +++ b/pyramid/tests/test_config/test_factories.py @@ -68,39 +68,40 @@ class TestFactoriesMixin(unittest.TestCase): dummyfactory) def test_set_request_property_with_callable(self): - from pyramid.interfaces import IRequestProperties + from pyramid.interfaces import IRequestExtensions config = self._makeOne(autocommit=True) callable = lambda x: None config.set_request_property(callable, name='foo') - plist = config.registry.getUtility(IRequestProperties) - self.assertEqual(set(p[0] for p in plist), set(['foo'])) + exts = config.registry.getUtility(IRequestExtensions) + self.assertTrue('foo' in exts.descriptors) def test_set_request_property_with_unnamed_callable(self): - from pyramid.interfaces import IRequestProperties + from pyramid.interfaces import IRequestExtensions config = self._makeOne(autocommit=True) def foo(self): pass config.set_request_property(foo, reify=True) - plist = config.registry.getUtility(IRequestProperties) - self.assertEqual(set(p[0] for p in plist), set(['foo'])) + exts = config.registry.getUtility(IRequestExtensions) + self.assertTrue('foo' in exts.descriptors) def test_set_request_property_with_property(self): - from pyramid.interfaces import IRequestProperties + from pyramid.interfaces import IRequestExtensions config = self._makeOne(autocommit=True) callable = property(lambda x: None) config.set_request_property(callable, name='foo') - plist = config.registry.getUtility(IRequestProperties) - self.assertEqual(set(p[0] for p in plist), set(['foo'])) + exts = config.registry.getUtility(IRequestExtensions) + self.assertTrue('foo' in exts.descriptors) def test_set_multiple_request_properties(self): - from pyramid.interfaces import IRequestProperties + from pyramid.interfaces import IRequestExtensions config = self._makeOne() def foo(self): pass bar = property(lambda x: None) config.set_request_property(foo, reify=True) config.set_request_property(bar, name='bar') config.commit() - plist = config.registry.getUtility(IRequestProperties) - self.assertEqual(set(p[0] for p in plist), set(['foo', 'bar'])) + exts = config.registry.getUtility(IRequestExtensions) + self.assertTrue('foo' in exts.descriptors) + self.assertTrue('bar' in exts.descriptors) def test_set_multiple_request_properties_conflict(self): from pyramid.exceptions import ConfigurationConflictError @@ -111,32 +112,48 @@ 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 + def test_add_request_method_with_callable(self): + from pyramid.interfaces import IRequestExtensions + config = self._makeOne(autocommit=True) + callable = lambda x: None + config.add_request_method(callable, name='foo') + exts = config.registry.getUtility(IRequestExtensions) + self.assertTrue('foo' in exts.methods) + + def test_add_request_method_with_unnamed_callable(self): + from pyramid.interfaces import IRequestExtensions + config = self._makeOne(autocommit=True) + def foo(self): pass + config.add_request_method(foo) + exts = config.registry.getUtility(IRequestExtensions) + self.assertTrue('foo' in exts.methods) + + def test_set_multiple_request_methods_conflict(self): + from pyramid.exceptions import ConfigurationConflictError 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) - plist = event.request.plist - self.assertEqual(set(p[0] for p in plist), set(['foo', 'bar'])) - - - -class DummyRequest(object): - plist = None - - def __init__(self, registry): - self.registry = registry - - def _set_properties(self, properties): - if self.plist is None: - self.plist = [] - self.plist.extend(properties) + def foo(self): pass + def bar(self): pass + config.add_request_method(foo, name='bar') + config.add_request_method(bar, name='bar') + self.assertRaises(ConfigurationConflictError, config.commit) + + def test_add_request_method_with_None_callable(self): + from pyramid.interfaces import IRequestExtensions + config = self._makeOne(autocommit=True) + config.add_request_method(name='foo') + exts = config.registry.queryUtility(IRequestExtensions) + self.assertTrue(exts is None) + + def test_add_request_method_with_None_callable_conflict(self): + from pyramid.exceptions import ConfigurationConflictError + config = self._makeOne() + def bar(self): pass + config.add_request_method(name='foo') + config.add_request_method(bar, name='foo') + self.assertRaises(ConfigurationConflictError, config.commit) + + def test_add_request_method_with_None_callable_and_no_name(self): + config = self._makeOne(autocommit=True) + self.assertRaises(AttributeError, config.add_request_method) + diff --git a/pyramid/tests/test_config/test_init.py b/pyramid/tests/test_config/test_init.py index 37c3de275..f39906dd9 100644 --- a/pyramid/tests/test_config/test_init.py +++ b/pyramid/tests/test_config/test_init.py @@ -349,7 +349,7 @@ class ConfiguratorTests(unittest.TestCase): config.setup_registry() self.assertEqual(reg.has_listeners, True) - def test_setup_registry_registers_default_exceptionresponse_view(self): + def test_setup_registry_registers_default_exceptionresponse_views(self): from webob.exc import WSGIHTTPException from pyramid.interfaces import IExceptionResponse from pyramid.view import default_exceptionresponse_view @@ -357,6 +357,7 @@ class ConfiguratorTests(unittest.TestCase): config = self._makeOne(reg) views = [] config.add_view = lambda *arg, **kw: views.append((arg, kw)) + config.add_default_view_predicates = lambda *arg: None config._add_tween = lambda *arg, **kw: False config.setup_registry() self.assertEqual(views[0], ((default_exceptionresponse_view,), @@ -364,6 +365,16 @@ class ConfiguratorTests(unittest.TestCase): self.assertEqual(views[1], ((default_exceptionresponse_view,), {'context':WSGIHTTPException})) + def test_setup_registry_registers_default_view_predicates(self): + reg = DummyRegistry() + config = self._makeOne(reg) + vp_called = [] + config.add_view = lambda *arg, **kw: None + config.add_default_view_predicates = lambda *arg: vp_called.append(True) + config._add_tween = lambda *arg, **kw: False + config.setup_registry() + self.assertTrue(vp_called) + def test_setup_registry_registers_default_webob_iresponse_adapter(self): from webob import Response from pyramid.interfaces import IResponse @@ -1385,13 +1396,9 @@ class TestConfiguratorDeprecatedFeatures(unittest.TestCase): try: config.commit() except ConfigurationConflictError as why: - c1, c2, c3, c4, c5, c6 = _conflictFunctions(why) + c1, c2 = _conflictFunctions(why) self.assertEqual(c1, 'test_conflict_route_with_view') self.assertEqual(c2, 'test_conflict_route_with_view') - self.assertEqual(c3, 'test_conflict_route_with_view') - self.assertEqual(c4, 'test_conflict_route_with_view') - self.assertEqual(c5, 'test_conflict_route_with_view') - self.assertEqual(c6, 'test_conflict_route_with_view') else: # pragma: no cover raise AssertionError @@ -1414,6 +1421,32 @@ class TestConfigurator_add_directive(unittest.TestCase): self.assertEqual(action['callable'], None) self.assertEqual(action['args'], test_config) + def test_add_directive_with_partial(self): + from pyramid.tests import test_config + config = self.config + config.add_directive( + 'dummy_partial', 'pyramid.tests.test_config.dummy_partial') + self.assertTrue(hasattr(config, 'dummy_partial')) + config.dummy_partial() + after = config.action_state + action = after.actions[-1] + self.assertEqual(action['discriminator'], 'partial') + self.assertEqual(action['callable'], None) + self.assertEqual(action['args'], test_config) + + def test_add_directive_with_custom_callable(self): + from pyramid.tests import test_config + config = self.config + config.add_directive( + 'dummy_callable', 'pyramid.tests.test_config.dummy_callable') + self.assertTrue(hasattr(config, 'dummy_callable')) + config.dummy_callable('discrim') + after = config.action_state + action = after.actions[-1] + self.assertEqual(action['discriminator'], 'discrim') + self.assertEqual(action['callable'], None) + self.assertEqual(action['args'], test_config) + def test_extend_with_python_callable(self): from pyramid.tests import test_config config = self.config @@ -1682,6 +1715,7 @@ class Test_resolveConflicts(unittest.TestCase): (3, f, (3,), {}, ('y',)), (None, f, (5,), {}, ('y',)), ]) + result = list(result) self.assertEqual( result, [{'info': None, @@ -1743,6 +1777,7 @@ class Test_resolveConflicts(unittest.TestCase): expand_action(3, f, (3,), {}, ('y',)), expand_action(None, f, (5,), {}, ('y',)), ]) + result = list(result) self.assertEqual( result, [{'info': None, @@ -1794,32 +1829,31 @@ class Test_resolveConflicts(unittest.TestCase): def test_it_conflict(self): from pyramid.tests.test_config import dummyfactory as f - self.assertRaises( - ConfigurationConflictError, - self._callFUT, [ - (None, f), - (1, f, (2,), {}, ('x',), 'eek'), - (1, f, (3,), {}, ('y',), 'ack'), - (4, f, (4,), {}, ('y',)), - (3, f, (3,), {}, ('y',)), - (None, f, (5,), {}, ('y',)), - ] - ) + result = self._callFUT([ + (None, f), + (1, f, (2,), {}, ('x',), 'eek'), # will conflict + (1, f, (3,), {}, ('y',), 'ack'), # will conflict + (4, f, (4,), {}, ('y',)), + (3, f, (3,), {}, ('y',)), + (None, f, (5,), {}, ('y',)), + ]) + self.assertRaises(ConfigurationConflictError, list, result) def test_it_with_actions_grouped_by_order(self): from pyramid.tests.test_config import dummyfactory as f from pyramid.config import expand_action result = self._callFUT([ - expand_action(None, f), - expand_action(1, f, (1,), {}, (), 'third', 10), + expand_action(None, f), # X + expand_action(1, f, (1,), {}, (), 'third', 10), # X expand_action(1, f, (2,), {}, ('x',), 'fourth', 10), expand_action(1, f, (3,), {}, ('y',), 'fifth', 10), - expand_action(2, f, (1,), {}, (), 'sixth', 10), - expand_action(3, f, (1,), {}, (), 'seventh', 10), - expand_action(5, f, (4,), {}, ('y',), 'eighth', 99999), - expand_action(4, f, (3,), {}, (), 'first', 5), + expand_action(2, f, (1,), {}, (), 'sixth', 10), # X + expand_action(3, f, (1,), {}, (), 'seventh', 10), # X + expand_action(5, f, (4,), {}, ('y',), 'eighth', 99999), # X + expand_action(4, f, (3,), {}, (), 'first', 5), # X expand_action(4, f, (5,), {}, ('y',), 'second', 5), ]) + result = list(result) self.assertEqual(len(result), 6) # resolved actions should be grouped by (order, i) self.assertEqual( @@ -1940,10 +1974,11 @@ class DummyEvent: pass class DummyRegistry(object): - def __init__(self, adaptation=None): + def __init__(self, adaptation=None, util=None): self.utilities = [] self.adapters = [] self.adaptation = adaptation + self.util = util def subscribers(self, events, name): self.events = events return events @@ -1953,6 +1988,8 @@ class DummyRegistry(object): self.adapters.append((arg, kw)) def queryAdapter(self, *arg, **kw): return self.adaptation + def queryUtility(self, *arg, **kw): + return self.util from pyramid.interfaces import IResponse @implementer(IResponse) diff --git a/pyramid/tests/test_config/test_init.py:TestConfigurator_add_directive b/pyramid/tests/test_config/test_init.py:TestConfigurator_add_directive new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/pyramid/tests/test_config/test_init.py:TestConfigurator_add_directive diff --git a/pyramid/tests/test_config/test_predicates.py b/pyramid/tests/test_config/test_predicates.py new file mode 100644 index 000000000..e33a31458 --- /dev/null +++ b/pyramid/tests/test_config/test_predicates.py @@ -0,0 +1,268 @@ +import unittest + +from pyramid.compat import text_ + +class TestXHRPredicate(unittest.TestCase): + def _makeOne(self, val): + from pyramid.config.predicates import XHRPredicate + return XHRPredicate(val, None) + + def test___call___true(self): + inst = self._makeOne(True) + request = Dummy() + request.is_xhr = True + result = inst(None, request) + self.assertTrue(result) + + def test___call___false(self): + inst = self._makeOne(True) + request = Dummy() + request.is_xhr = False + result = inst(None, request) + self.assertFalse(result) + + def test_text(self): + inst = self._makeOne(True) + self.assertEqual(inst.text(), 'xhr = True') + + def test_phash(self): + inst = self._makeOne(True) + self.assertEqual(inst.phash(), 'xhr = True') + +class TestRequestMethodPredicate(unittest.TestCase): + def _makeOne(self, val): + from pyramid.config.predicates import RequestMethodPredicate + return RequestMethodPredicate(val, None) + + def test_ctor_get_but_no_head(self): + inst = self._makeOne('GET') + self.assertEqual(inst.val, ('GET', 'HEAD')) + + def test___call___true_single(self): + inst = self._makeOne('GET') + request = Dummy() + request.method = 'GET' + result = inst(None, request) + self.assertTrue(result) + + def test___call___true_multi(self): + inst = self._makeOne(('GET','HEAD')) + request = Dummy() + request.method = 'GET' + result = inst(None, request) + self.assertTrue(result) + + def test___call___false(self): + inst = self._makeOne(('GET','HEAD')) + request = Dummy() + request.method = 'POST' + result = inst(None, request) + self.assertFalse(result) + + def test_text(self): + inst = self._makeOne(('HEAD','GET')) + self.assertEqual(inst.text(), 'request_method = GET,HEAD') + + def test_phash(self): + inst = self._makeOne(('HEAD','GET')) + self.assertEqual(inst.phash(), 'request_method = GET,HEAD') + +class TestPathInfoPredicate(unittest.TestCase): + def _makeOne(self, val): + from pyramid.config.predicates import PathInfoPredicate + return PathInfoPredicate(val, None) + + def test_ctor_compilefail(self): + from pyramid.exceptions import ConfigurationError + self.assertRaises(ConfigurationError, self._makeOne, '\\') + + def test___call___true(self): + inst = self._makeOne(r'/\d{2}') + request = Dummy() + request.upath_info = text_('/12') + result = inst(None, request) + self.assertTrue(result) + + def test___call___false(self): + inst = self._makeOne(r'/\d{2}') + request = Dummy() + request.upath_info = text_('/n12') + result = inst(None, request) + self.assertFalse(result) + + def test_text(self): + inst = self._makeOne('/') + self.assertEqual(inst.text(), 'path_info = /') + + def test_phash(self): + inst = self._makeOne('/') + self.assertEqual(inst.phash(), 'path_info = /') + +class TestRequestParamPredicate(unittest.TestCase): + def _makeOne(self, val): + from pyramid.config.predicates import RequestParamPredicate + return RequestParamPredicate(val, None) + + def test___call___true_exists(self): + inst = self._makeOne('abc') + request = Dummy() + request.params = {'abc':1} + result = inst(None, request) + self.assertTrue(result) + + def test___call___true_withval(self): + inst = self._makeOne('abc=1') + request = Dummy() + request.params = {'abc':'1'} + result = inst(None, request) + self.assertTrue(result) + + def test___call___false(self): + inst = self._makeOne('abc') + request = Dummy() + request.params = {} + result = inst(None, request) + self.assertFalse(result) + + def test_text_exists(self): + inst = self._makeOne('abc') + self.assertEqual(inst.text(), 'request_param abc') + + def test_text_withval(self): + inst = self._makeOne('abc= 1') + self.assertEqual(inst.text(), 'request_param abc = 1') + + def test_phash_exists(self): + inst = self._makeOne('abc') + self.assertEqual(inst.phash(), 'request_param abc') + + def test_phash_withval(self): + inst = self._makeOne('abc= 1') + self.assertEqual(inst.phash(), "request_param abc = 1") + +class TestMatchParamPredicate(unittest.TestCase): + def _makeOne(self, val): + from pyramid.config.predicates import MatchParamPredicate + return MatchParamPredicate(val, None) + + def test___call___true_single(self): + inst = self._makeOne('abc=1') + request = Dummy() + request.matchdict = {'abc':'1'} + result = inst(None, request) + self.assertTrue(result) + + + def test___call___true_multi(self): + inst = self._makeOne(('abc=1', 'def=2')) + request = Dummy() + request.matchdict = {'abc':'1', 'def':'2'} + result = inst(None, request) + self.assertTrue(result) + + def test___call___false(self): + inst = self._makeOne('abc=1') + request = Dummy() + request.matchdict = {} + result = inst(None, request) + self.assertFalse(result) + + def test_text(self): + inst = self._makeOne(('def= 1', 'abc =2')) + self.assertEqual(inst.text(), 'match_param abc=2,def=1') + + def test_phash(self): + inst = self._makeOne(('def= 1', 'abc =2')) + self.assertEqual(inst.phash(), 'match_param abc=2,def=1') + +class TestCustomPredicate(unittest.TestCase): + def _makeOne(self, val): + from pyramid.config.predicates import CustomPredicate + return CustomPredicate(val, None) + + def test___call___true(self): + def func(context, request): + self.assertEqual(context, None) + self.assertEqual(request, None) + return True + inst = self._makeOne(func) + result = inst(None, None) + self.assertTrue(result) + + def test___call___false(self): + def func(context, request): + self.assertEqual(context, None) + self.assertEqual(request, None) + return False + inst = self._makeOne(func) + result = inst(None, None) + self.assertFalse(result) + + def test_text_func_has___text__(self): + pred = predicate() + pred.__text__ = 'text' + inst = self._makeOne(pred) + self.assertEqual(inst.text(), 'text') + + def test_text_func_repr(self): + pred = predicate() + inst = self._makeOne(pred) + self.assertEqual(inst.text(), 'custom predicate: object predicate') + + def test_phash(self): + pred = predicate() + inst = self._makeOne(pred) + self.assertEqual(inst.phash(), 'custom:1') + +class TestTraversePredicate(unittest.TestCase): + def _makeOne(self, val): + from pyramid.config.predicates import TraversePredicate + return TraversePredicate(val, None) + + def test___call__traverse_has_remainder_already(self): + inst = self._makeOne('/1/:a/:b') + info = {'traverse':'abc'} + request = Dummy() + result = inst(info, request) + self.assertEqual(result, True) + self.assertEqual(info, {'traverse':'abc'}) + + def test___call__traverse_matches(self): + inst = self._makeOne('/1/:a/:b') + info = {'match':{'a':'a', 'b':'b'}} + request = Dummy() + result = inst(info, request) + self.assertEqual(result, True) + self.assertEqual(info, {'match': + {'a':'a', 'b':'b', 'traverse':('1', 'a', 'b')}}) + + def test___call__traverse_matches_with_highorder_chars(self): + inst = self._makeOne(text_(b'/La Pe\xc3\xb1a/{x}', 'utf-8')) + info = {'match':{'x':text_(b'Qu\xc3\xa9bec', 'utf-8')}} + request = Dummy() + result = inst(info, request) + self.assertEqual(result, True) + self.assertEqual( + info['match']['traverse'], + (text_(b'La Pe\xc3\xb1a', 'utf-8'), + text_(b'Qu\xc3\xa9bec', 'utf-8')) + ) + + def test_text(self): + inst = self._makeOne('/abc') + self.assertEqual(inst.text(), 'traverse matchdict pseudo-predicate') + + def test_phash(self): + inst = self._makeOne('/abc') + self.assertEqual(inst.phash(), '') + +class predicate(object): + def __repr__(self): + return 'predicate' + def __hash__(self): + return 1 + +class Dummy(object): + def __init__(self, **kw): + self.__dict__.update(**kw) + diff --git a/pyramid/tests/test_config/test_routes.py b/pyramid/tests/test_config/test_routes.py index bb47d2d7e..6fb5189f6 100644 --- a/pyramid/tests/test_config/test_routes.py +++ b/pyramid/tests/test_config/test_routes.py @@ -158,7 +158,7 @@ class RoutesConfiguratorMixinTests(unittest.TestCase): def pred2(context, request): pass config.add_route('name', 'path', custom_predicates=(pred1, pred2)) route = self._assertRoute(config, 'name', 'path', 2) - self.assertEqual(route.predicates, [pred1, pred2]) + self.assertEqual(len(route.predicates), 2) def test_add_route_with_header(self): config = self._makeOne(autocommit=True) diff --git a/pyramid/tests/test_config/test_security.py b/pyramid/tests/test_config/test_security.py index d05d1d471..817f6ce02 100644 --- a/pyramid/tests/test_config/test_security.py +++ b/pyramid/tests/test_config/test_security.py @@ -89,3 +89,12 @@ class ConfiguratorSecurityMethodsTests(unittest.TestCase): self.assertEqual(config.registry.getUtility(IDefaultPermission), 'view') + def test_add_permission(self): + config = self._makeOne(autocommit=True) + config.add_permission('perm') + cat = config.registry.introspector.get_category('permissions') + self.assertEqual(len(cat), 1) + D = cat[0] + intr = D['introspectable'] + self.assertEqual(intr['value'], 'perm') + 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_config/test_tweens.py b/pyramid/tests/test_config/test_tweens.py index 0d96bf601..8853b6899 100644 --- a/pyramid/tests/test_config/test_tweens.py +++ b/pyramid/tests/test_config/test_tweens.py @@ -179,28 +179,12 @@ class TestTweens(unittest.TestCase): ('name2', 'factory2')]) def test_add_implicit(self): - from pyramid.tweens import INGRESS tweens = self._makeOne() tweens.add_implicit('name', 'factory') - self.assertEqual(tweens.names, ['name']) - self.assertEqual(tweens.factories, - {'name':'factory'}) - self.assertEqual(tweens.order, [(INGRESS, 'name')]) tweens.add_implicit('name2', 'factory2') - self.assertEqual(tweens.names, ['name', 'name2']) - self.assertEqual(tweens.factories, - {'name':'factory', 'name2':'factory2'}) - self.assertEqual(tweens.order, - [(INGRESS, 'name'), (INGRESS, 'name2')]) - tweens.add_implicit('name3', 'factory3', over='name2') - self.assertEqual(tweens.names, - ['name', 'name2', 'name3']) - self.assertEqual(tweens.factories, - {'name':'factory', 'name2':'factory2', - 'name3':'factory3'}) - self.assertEqual(tweens.order, - [(INGRESS, 'name'), (INGRESS, 'name2'), - ('name3', 'name2')]) + self.assertEqual(tweens.sorter.sorted(), + [('name2', 'factory2'), + ('name', 'factory')]) def test___call___explicit(self): tweens = self._makeOne() @@ -212,18 +196,13 @@ class TestTweens(unittest.TestCase): self.assertEqual(tweens(None, None), '123') def test___call___implicit(self): - from pyramid.tweens import INGRESS tweens = self._makeOne() def factory1(handler, registry): return handler def factory2(handler, registry): return '123' - tweens.names = ['name', 'name2'] - tweens.alias_to_name = {'name':'name', 'name2':'name2'} - tweens.name_to_alias = {'name':'name', 'name2':'name2'} - tweens.req_under = set(['name', 'name2']) - tweens.order = [(INGRESS, 'name'), (INGRESS, 'name2')] - tweens.factories = {'name':factory1, 'name2':factory2} + tweens.add_implicit('name2', factory2) + tweens.add_implicit('name1', factory1) self.assertEqual(tweens(None, None), '123') def test_implicit_ordering_1(self): @@ -413,7 +392,7 @@ class TestTweens(unittest.TestCase): self.assertRaises(ConfigurationError, tweens.implicit) def test_implicit_ordering_conflict_direct(self): - from pyramid.config.tweens import CyclicDependencyError + from pyramid.config.util import CyclicDependencyError tweens = self._makeOne() add = tweens.add_implicit add('browserid', 'browserid_factory') @@ -421,7 +400,7 @@ class TestTweens(unittest.TestCase): self.assertRaises(CyclicDependencyError, tweens.implicit) def test_implicit_ordering_conflict_indirect(self): - from pyramid.config.tweens import CyclicDependencyError + from pyramid.config.util import CyclicDependencyError tweens = self._makeOne() add = tweens.add_implicit add('browserid', 'browserid_factory') @@ -429,14 +408,3 @@ class TestTweens(unittest.TestCase): add('dbt', 'dbt_factory', under='browserid', over='auth') self.assertRaises(CyclicDependencyError, tweens.implicit) -class TestCyclicDependencyError(unittest.TestCase): - def _makeOne(self, cycles): - from pyramid.config.tweens import CyclicDependencyError - return CyclicDependencyError(cycles) - - def test___str__(self): - exc = self._makeOne({'a':['c', 'd'], 'c':['a']}) - result = str(exc) - self.assertTrue("'a' sorts over ['c', 'd']" in result) - self.assertTrue("'c' sorts over ['a']" in result) - diff --git a/pyramid/tests/test_config/test_util.py b/pyramid/tests/test_config/test_util.py index 1ad1fb3c1..13cb27526 100644 --- a/pyramid/tests/test_config/test_util.py +++ b/pyramid/tests/test_config/test_util.py @@ -1,10 +1,32 @@ import unittest from pyramid.compat import text_ -class Test__make_predicates(unittest.TestCase): +class TestPredicateList(unittest.TestCase): + + def _makeOne(self): + from pyramid.config.util import PredicateList + from pyramid.config import predicates + inst = PredicateList() + for name, factory in ( + ('xhr', predicates.XHRPredicate), + ('request_method', predicates.RequestMethodPredicate), + ('path_info', predicates.PathInfoPredicate), + ('request_param', predicates.RequestParamPredicate), + ('header', predicates.HeaderPredicate), + ('accept', predicates.AcceptPredicate), + ('containment', predicates.ContainmentPredicate), + ('request_type', predicates.RequestTypePredicate), + ('match_param', predicates.MatchParamPredicate), + ('custom', predicates.CustomPredicate), + ('traverse', predicates.TraversePredicate), + ): + inst.add(name, factory) + return inst + def _callFUT(self, **kw): - from pyramid.config.util import make_predicates - return make_predicates(**kw) + inst = self._makeOne() + config = DummyConfigurator() + return inst.make(config, **kw) def test_ordering_xhr_and_request_method_trump_only_containment(self): order1, _, _ = self._callFUT(xhr=True, request_method='GET') @@ -12,6 +34,7 @@ class Test__make_predicates(unittest.TestCase): self.assertTrue(order1 < order2) def test_ordering_number_of_predicates(self): + from pyramid.config.util import predvalseq order1, _, _ = self._callFUT( xhr='xhr', request_method='request_method', @@ -22,7 +45,7 @@ class Test__make_predicates(unittest.TestCase): accept='accept', containment='containment', request_type='request_type', - custom=(DummyCustomPredicate(),), + custom=predvalseq([DummyCustomPredicate()]), ) order2, _, _ = self._callFUT( xhr='xhr', @@ -34,7 +57,7 @@ class Test__make_predicates(unittest.TestCase): accept='accept', containment='containment', request_type='request_type', - custom=(DummyCustomPredicate(),), + custom=predvalseq([DummyCustomPredicate()]), ) order3, _, _ = self._callFUT( xhr='xhr', @@ -114,6 +137,7 @@ class Test__make_predicates(unittest.TestCase): self.assertTrue(order12 > order10) def test_ordering_importance_of_predicates(self): + from pyramid.config.util import predvalseq order1, _, _ = self._callFUT( xhr='xhr', ) @@ -142,7 +166,7 @@ class Test__make_predicates(unittest.TestCase): match_param='foo=bar', ) order10, _, _ = self._callFUT( - custom=(DummyCustomPredicate(),), + custom=predvalseq([DummyCustomPredicate()]), ) self.assertTrue(order1 > order2) self.assertTrue(order2 > order3) @@ -155,12 +179,13 @@ class Test__make_predicates(unittest.TestCase): self.assertTrue(order9 > order10) def test_ordering_importance_and_number(self): + from pyramid.config.util import predvalseq order1, _, _ = self._callFUT( xhr='xhr', request_method='request_method', ) order2, _, _ = self._callFUT( - custom=(DummyCustomPredicate(),), + custom=predvalseq([DummyCustomPredicate()]), ) self.assertTrue(order1 < order2) @@ -170,7 +195,7 @@ class Test__make_predicates(unittest.TestCase): ) order2, _, _ = self._callFUT( request_method='request_method', - custom=(DummyCustomPredicate(),), + custom=predvalseq([DummyCustomPredicate()]), ) self.assertTrue(order1 > order2) @@ -181,7 +206,7 @@ class Test__make_predicates(unittest.TestCase): ) order2, _, _ = self._callFUT( request_method='request_method', - custom=(DummyCustomPredicate(),), + custom=predvalseq([DummyCustomPredicate()]), ) self.assertTrue(order1 < order2) @@ -193,18 +218,19 @@ class Test__make_predicates(unittest.TestCase): order2, _, _ = self._callFUT( xhr='xhr', request_method='request_method', - custom=(DummyCustomPredicate(),), + custom=predvalseq([DummyCustomPredicate()]), ) self.assertTrue(order1 > order2) def test_different_custom_predicates_with_same_hash(self): + from pyramid.config.util import predvalseq class PredicateWithHash(object): def __hash__(self): return 1 a = PredicateWithHash() b = PredicateWithHash() - _, _, a_phash = self._callFUT(custom=(a,)) - _, _, b_phash = self._callFUT(custom=(b,)) + _, _, a_phash = self._callFUT(custom=predvalseq([a])) + _, _, b_phash = self._callFUT(custom=predvalseq([b])) self.assertEqual(a_phash, b_phash) def test_traverse_has_remainder_already(self): @@ -244,12 +270,14 @@ class Test__make_predicates(unittest.TestCase): ) def test_custom_predicates_can_affect_traversal(self): + from pyramid.config.util import predvalseq def custom(info, request): m = info['match'] m['dummy'] = 'foo' return True - _, predicates, _ = self._callFUT(custom=(custom,), - traverse='/1/:dummy/:a') + _, predicates, _ = self._callFUT( + custom=predvalseq([custom]), + traverse='/1/:dummy/:a') self.assertEqual(len(predicates), 2) info = {'match':{'a':'a'}} request = DummyRequest() @@ -259,6 +287,7 @@ class Test__make_predicates(unittest.TestCase): 'traverse':('1', 'foo', 'a')}}) def test_predicate_text_is_correct(self): + from pyramid.config.util import predvalseq _, predicates, _ = self._callFUT( xhr='xhr', request_method='request_method', @@ -268,23 +297,27 @@ class Test__make_predicates(unittest.TestCase): accept='accept', containment='containment', request_type='request_type', - custom=(DummyCustomPredicate(), + custom=predvalseq( + [ + DummyCustomPredicate(), DummyCustomPredicate.classmethod_predicate, - DummyCustomPredicate.classmethod_predicate_no_text), + DummyCustomPredicate.classmethod_predicate_no_text, + ] + ), match_param='foo=bar') - self.assertEqual(predicates[0].__text__, 'xhr = True') - self.assertEqual(predicates[1].__text__, - "request method = ['request_method']") - self.assertEqual(predicates[2].__text__, 'path_info = path_info') - self.assertEqual(predicates[3].__text__, 'request_param param') - self.assertEqual(predicates[4].__text__, 'header header') - self.assertEqual(predicates[5].__text__, 'accept = accept') - self.assertEqual(predicates[6].__text__, 'containment = containment') - self.assertEqual(predicates[7].__text__, 'request_type = request_type') - self.assertEqual(predicates[8].__text__, "match_param ['foo=bar']") - self.assertEqual(predicates[9].__text__, 'custom predicate') - self.assertEqual(predicates[10].__text__, 'classmethod predicate') - self.assertEqual(predicates[11].__text__, '<unknown custom predicate>') + self.assertEqual(predicates[0].text(), 'xhr = True') + self.assertEqual(predicates[1].text(), + "request_method = request_method") + self.assertEqual(predicates[2].text(), 'path_info = path_info') + self.assertEqual(predicates[3].text(), 'request_param param') + self.assertEqual(predicates[4].text(), 'header header') + self.assertEqual(predicates[5].text(), 'accept = accept') + self.assertEqual(predicates[6].text(), 'containment = containment') + self.assertEqual(predicates[7].text(), 'request_type = request_type') + self.assertEqual(predicates[8].text(), "match_param foo=bar") + self.assertEqual(predicates[9].text(), 'custom predicate') + self.assertEqual(predicates[10].text(), 'classmethod predicate') + self.assertTrue(predicates[11].text().startswith('custom predicate')) def test_match_param_from_string(self): _, predicates, _ = self._callFUT(match_param='foo=bar') @@ -328,15 +361,10 @@ class Test__make_predicates(unittest.TestCase): hash2, _, __= self._callFUT(request_method='GET') self.assertEqual(hash1, hash2) - def test_match_param_hashable(self): - # https://github.com/Pylons/pyramid/issues/425 - import pyramid.testing - def view(request): pass - config = pyramid.testing.setUp(autocommit=False) - config.add_route('foo', '/foo/{a}/{b}') - config.add_view(view, route_name='foo', match_param='a=bar') - config.add_view(view, route_name='foo', match_param=('a=bar', 'b=baz')) - config.commit() + def test_unknown_predicate(self): + from pyramid.exceptions import ConfigurationError + self.assertRaises(ConfigurationError, self._callFUT, unknown=1) + class TestActionInfo(unittest.TestCase): def _getTargetClass(self): @@ -368,6 +396,274 @@ class TestActionInfo(unittest.TestCase): self.assertEqual(str(inst), "Line 0 of file filename:\n linerepr ") +class TestTopologicalSorter(unittest.TestCase): + def _makeOne(self, *arg, **kw): + from pyramid.config.util import TopologicalSorter + return TopologicalSorter(*arg, **kw) + + def test_remove(self): + inst = self._makeOne() + inst.names.append('name') + inst.name2val['name'] = 1 + inst.req_after.add('name') + inst.req_before.add('name') + inst.name2after['name'] = ('bob',) + inst.name2before['name'] = ('fred',) + inst.order.append(('bob', 'name')) + inst.order.append(('name', 'fred')) + inst.remove('name') + self.assertFalse(inst.names) + self.assertFalse(inst.req_before) + self.assertFalse(inst.req_after) + self.assertFalse(inst.name2before) + self.assertFalse(inst.name2after) + self.assertFalse(inst.name2val) + self.assertFalse(inst.order) + + def test_add(self): + from pyramid.config.util import LAST + sorter = self._makeOne() + sorter.add('name', 'factory') + self.assertEqual(sorter.names, ['name']) + self.assertEqual(sorter.name2val, + {'name':'factory'}) + self.assertEqual(sorter.order, [('name', LAST)]) + sorter.add('name2', 'factory2') + self.assertEqual(sorter.names, ['name', 'name2']) + self.assertEqual(sorter.name2val, + {'name':'factory', 'name2':'factory2'}) + self.assertEqual(sorter.order, + [('name', LAST), ('name2', LAST)]) + sorter.add('name3', 'factory3', before='name2') + self.assertEqual(sorter.names, + ['name', 'name2', 'name3']) + self.assertEqual(sorter.name2val, + {'name':'factory', 'name2':'factory2', + 'name3':'factory3'}) + self.assertEqual(sorter.order, + [('name', LAST), ('name2', LAST), + ('name3', 'name2')]) + + def test_sorted_ordering_1(self): + sorter = self._makeOne() + sorter.add('name1', 'factory1') + sorter.add('name2', 'factory2') + self.assertEqual(sorter.sorted(), + [ + ('name1', 'factory1'), + ('name2', 'factory2'), + ]) + + def test_sorted_ordering_2(self): + from pyramid.config.util import FIRST + sorter = self._makeOne() + sorter.add('name1', 'factory1') + sorter.add('name2', 'factory2', after=FIRST) + self.assertEqual(sorter.sorted(), + [ + ('name2', 'factory2'), + ('name1', 'factory1'), + ]) + + def test_sorted_ordering_3(self): + from pyramid.config.util import FIRST + sorter = self._makeOne() + add = sorter.add + add('auth', 'auth_factory', after='browserid') + add('dbt', 'dbt_factory') + add('retry', 'retry_factory', before='txnmgr', after='exceptionview') + add('browserid', 'browserid_factory') + add('txnmgr', 'txnmgr_factory', after='exceptionview') + add('exceptionview', 'excview_factory', after=FIRST) + self.assertEqual(sorter.sorted(), + [ + ('exceptionview', 'excview_factory'), + ('retry', 'retry_factory'), + ('txnmgr', 'txnmgr_factory'), + ('dbt', 'dbt_factory'), + ('browserid', 'browserid_factory'), + ('auth', 'auth_factory'), + ]) + + def test_sorted_ordering_4(self): + from pyramid.config.util import FIRST + sorter = self._makeOne() + add = sorter.add + add('exceptionview', 'excview_factory', after=FIRST) + add('auth', 'auth_factory', after='browserid') + add('retry', 'retry_factory', before='txnmgr', after='exceptionview') + add('browserid', 'browserid_factory') + add('txnmgr', 'txnmgr_factory', after='exceptionview') + add('dbt', 'dbt_factory') + self.assertEqual(sorter.sorted(), + [ + ('exceptionview', 'excview_factory'), + ('retry', 'retry_factory'), + ('txnmgr', 'txnmgr_factory'), + ('browserid', 'browserid_factory'), + ('auth', 'auth_factory'), + ('dbt', 'dbt_factory'), + ]) + + def test_sorted_ordering_5(self): + from pyramid.config.util import LAST, FIRST + sorter = self._makeOne() + add = sorter.add + add('exceptionview', 'excview_factory') + add('auth', 'auth_factory', after=FIRST) + add('retry', 'retry_factory', before='txnmgr', after='exceptionview') + add('browserid', 'browserid_factory', after=FIRST) + add('txnmgr', 'txnmgr_factory', after='exceptionview', before=LAST) + add('dbt', 'dbt_factory') + self.assertEqual(sorter.sorted(), + [ + ('browserid', 'browserid_factory'), + ('auth', 'auth_factory'), + ('exceptionview', 'excview_factory'), + ('retry', 'retry_factory'), + ('txnmgr', 'txnmgr_factory'), + ('dbt', 'dbt_factory'), + ]) + + def test_sorted_ordering_missing_before_partial(self): + from pyramid.exceptions import ConfigurationError + sorter = self._makeOne() + add = sorter.add + add('dbt', 'dbt_factory') + add('auth', 'auth_factory', after='browserid') + add('retry', 'retry_factory', before='txnmgr', after='exceptionview') + add('browserid', 'browserid_factory') + self.assertRaises(ConfigurationError, sorter.sorted) + + def test_sorted_ordering_missing_after_partial(self): + from pyramid.exceptions import ConfigurationError + sorter = self._makeOne() + add = sorter.add + add('dbt', 'dbt_factory') + add('auth', 'auth_factory', after='txnmgr') + add('retry', 'retry_factory', before='dbt', after='exceptionview') + add('browserid', 'browserid_factory') + self.assertRaises(ConfigurationError, sorter.sorted) + + def test_sorted_ordering_missing_before_and_after_partials(self): + from pyramid.exceptions import ConfigurationError + sorter = self._makeOne() + add = sorter.add + add('dbt', 'dbt_factory') + add('auth', 'auth_factory', after='browserid') + add('retry', 'retry_factory', before='foo', after='txnmgr') + add('browserid', 'browserid_factory') + self.assertRaises(ConfigurationError, sorter.sorted) + + def test_sorted_ordering_missing_before_partial_with_fallback(self): + from pyramid.config.util import LAST + sorter = self._makeOne() + add = sorter.add + add('exceptionview', 'excview_factory', before=LAST) + add('auth', 'auth_factory', after='browserid') + add('retry', 'retry_factory', before=('txnmgr', LAST), + after='exceptionview') + add('browserid', 'browserid_factory') + add('dbt', 'dbt_factory') + self.assertEqual(sorter.sorted(), + [ + ('exceptionview', 'excview_factory'), + ('retry', 'retry_factory'), + ('browserid', 'browserid_factory'), + ('auth', 'auth_factory'), + ('dbt', 'dbt_factory'), + ]) + + def test_sorted_ordering_missing_after_partial_with_fallback(self): + from pyramid.config.util import FIRST + sorter = self._makeOne() + add = sorter.add + add('exceptionview', 'excview_factory', after=FIRST) + add('auth', 'auth_factory', after=('txnmgr','browserid')) + add('retry', 'retry_factory', after='exceptionview') + add('browserid', 'browserid_factory') + add('dbt', 'dbt_factory') + self.assertEqual(sorter.sorted(), + [ + ('exceptionview', 'excview_factory'), + ('retry', 'retry_factory'), + ('browserid', 'browserid_factory'), + ('auth', 'auth_factory'), + ('dbt', 'dbt_factory'), + ]) + + def test_sorted_ordering_with_partial_fallbacks(self): + from pyramid.config.util import LAST + sorter = self._makeOne() + add = sorter.add + add('exceptionview', 'excview_factory', before=('wontbethere', LAST)) + add('retry', 'retry_factory', after='exceptionview') + add('browserid', 'browserid_factory', before=('wont2', 'exceptionview')) + self.assertEqual(sorter.sorted(), + [ + ('browserid', 'browserid_factory'), + ('exceptionview', 'excview_factory'), + ('retry', 'retry_factory'), + ]) + + def test_sorted_ordering_with_multiple_matching_fallbacks(self): + from pyramid.config.util import LAST + sorter = self._makeOne() + add = sorter.add + add('exceptionview', 'excview_factory', before=LAST) + add('retry', 'retry_factory', after='exceptionview') + add('browserid', 'browserid_factory', before=('retry', 'exceptionview')) + self.assertEqual(sorter.sorted(), + [ + ('browserid', 'browserid_factory'), + ('exceptionview', 'excview_factory'), + ('retry', 'retry_factory'), + ]) + + def test_sorted_ordering_with_missing_fallbacks(self): + from pyramid.exceptions import ConfigurationError + from pyramid.config.util import LAST + sorter = self._makeOne() + add = sorter.add + add('exceptionview', 'excview_factory', before=LAST) + add('retry', 'retry_factory', after='exceptionview') + add('browserid', 'browserid_factory', before=('txnmgr', 'auth')) + self.assertRaises(ConfigurationError, sorter.sorted) + + def test_sorted_ordering_conflict_direct(self): + from pyramid.config.util import CyclicDependencyError + sorter = self._makeOne() + add = sorter.add + add('browserid', 'browserid_factory') + add('auth', 'auth_factory', before='browserid', after='browserid') + self.assertRaises(CyclicDependencyError, sorter.sorted) + + def test_sorted_ordering_conflict_indirect(self): + from pyramid.config.util import CyclicDependencyError + sorter = self._makeOne() + add = sorter.add + add('browserid', 'browserid_factory') + add('auth', 'auth_factory', before='browserid') + add('dbt', 'dbt_factory', after='browserid', before='auth') + self.assertRaises(CyclicDependencyError, sorter.sorted) + +class TestSingleton(unittest.TestCase): + def test_repr(self): + from pyramid.config.util import Singleton + r = repr(Singleton('ABC')) + self.assertEqual(r, 'ABC') + +class TestCyclicDependencyError(unittest.TestCase): + def _makeOne(self, cycles): + from pyramid.config.util import CyclicDependencyError + return CyclicDependencyError(cycles) + + def test___str__(self): + exc = self._makeOne({'a':['c', 'd'], 'c':['a']}) + result = str(exc) + self.assertTrue("'a' sorts before ['c', 'd']" in result) + self.assertTrue("'c' sorts before ['a']" in result) + class DummyCustomPredicate(object): def __init__(self): self.__text__ = 'custom predicate' @@ -392,3 +688,7 @@ class DummyRequest: self.params = {} self.cookies = {} +class DummyConfigurator(object): + def maybe_dotted(self, thing): + return thing + diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index 9b46f83c9..575d8c738 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -118,7 +118,8 @@ class TestViewsConfigurationMixin(unittest.TestCase): self.assertEqual(wrapper.__module__, view.__module__) self.assertEqual(wrapper.__name__, view.__name__) self.assertEqual(wrapper.__doc__, view.__doc__) - self.assertEqual(wrapper.__discriminator__(None, None)[0], 'view') + self.assertEqual(wrapper.__discriminator__(None, None).resolve()[0], + 'view') def test_add_view_view_callable_dottedname(self): from pyramid.renderers import null_renderer @@ -400,7 +401,7 @@ class TestViewsConfigurationMixin(unittest.TestCase): from pyramid.interfaces import IViewClassifier from pyramid.interfaces import IMultiView phash = md5() - phash.update(b'xhr:True') + phash.update(b'xhr = True') view = lambda *arg: 'NOT OK' view.__phash__ = phash.hexdigest() config = self._makeOne(autocommit=True) @@ -424,7 +425,7 @@ class TestViewsConfigurationMixin(unittest.TestCase): from pyramid.interfaces import IExceptionViewClassifier from pyramid.interfaces import IMultiView phash = md5() - phash.update(b'xhr:True') + phash.update(b'xhr = True') view = lambda *arg: 'NOT OK' view.__phash__ = phash.hexdigest() config = self._makeOne(autocommit=True) @@ -645,6 +646,24 @@ class TestViewsConfigurationMixin(unittest.TestCase): request.accept = DummyAccept('text/html', 'text/html') self.assertEqual(wrapper(None, request), 'OK2') + def test_add_view_mixed_case_replaces_existing_view(self): + from pyramid.renderers import null_renderer + def view(context, request): return 'OK' + def view2(context, request): return 'OK2' + def view3(context, request): return 'OK3' + config = self._makeOne(autocommit=True) + config.add_view(view=view, renderer=null_renderer) + config.add_view(view=view2, accept='text/html', renderer=null_renderer) + config.add_view(view=view3, accept='text/HTML', renderer=null_renderer) + wrapper = self._getViewCallable(config) + self.assertTrue(IMultiView.providedBy(wrapper)) + self.assertEqual(len(wrapper.media_views.items()),1) + self.assertFalse('text/HTML' in wrapper.media_views) + self.assertEqual(wrapper(None, None), 'OK') + request = DummyRequest() + request.accept = DummyAccept('text/html', 'text/html') + self.assertEqual(wrapper(None, request), 'OK3') + def test_add_views_with_accept_multiview_replaces_existing(self): from pyramid.renderers import null_renderer def view(context, request): return 'OK' @@ -970,8 +989,10 @@ class TestViewsConfigurationMixin(unittest.TestCase): wrapper = self._getViewCallable(config) self.assertTrue(IMultiView.providedBy(wrapper)) request = self._makeRequest(config) - self.assertEqual(wrapper.__discriminator__(foo, request)[5], IFoo) - self.assertEqual(wrapper.__discriminator__(bar, request)[5], IBar) + self.assertNotEqual( + wrapper.__discriminator__(foo, request), + wrapper.__discriminator__(bar, request), + ) def test_add_view_with_template_renderer(self): from pyramid.tests import test_config @@ -1217,8 +1238,8 @@ class TestViewsConfigurationMixin(unittest.TestCase): def test_add_view_with_header_badregex(self): view = lambda *arg: 'OK' config = self._makeOne() - self.assertRaises(ConfigurationError, - config.add_view, view=view, header='Host:a\\') + config.add_view(view, header='Host:a\\') + self.assertRaises(ConfigurationError, config.commit) def test_add_view_with_header_noval_match(self): from pyramid.renderers import null_renderer @@ -1323,8 +1344,8 @@ class TestViewsConfigurationMixin(unittest.TestCase): def test_add_view_with_path_info_badregex(self): view = lambda *arg: 'OK' config = self._makeOne() - self.assertRaises(ConfigurationError, - config.add_view, view=view, path_info='\\') + config.add_view(view, path_info='\\') + self.assertRaises(ConfigurationError, config.commit) def test_add_view_with_path_info_match(self): from pyramid.renderers import null_renderer @@ -1698,6 +1719,38 @@ class TestViewsConfigurationMixin(unittest.TestCase): result = view(None, request) self.assertEqual(result, 'OK') + def test_add_forbidden_view_allows_other_predicates(self): + from pyramid.renderers import null_renderer + config = self._makeOne(autocommit=True) + # doesnt blow up + config.add_view_predicate('dummy', DummyPredicate) + config.add_forbidden_view(renderer=null_renderer, dummy='abc') + + def test_add_forbidden_view_disallows_name(self): + config = self._makeOne(autocommit=True) + self.assertRaises(ConfigurationError, + config.add_forbidden_view, name='foo') + + def test_add_forbidden_view_disallows_permission(self): + config = self._makeOne(autocommit=True) + self.assertRaises(ConfigurationError, + config.add_forbidden_view, permission='foo') + + def test_add_forbidden_view_disallows_context(self): + config = self._makeOne(autocommit=True) + self.assertRaises(ConfigurationError, + config.add_forbidden_view, context='foo') + + def test_add_forbidden_view_disallows_for_(self): + config = self._makeOne(autocommit=True) + self.assertRaises(ConfigurationError, + config.add_forbidden_view, for_='foo') + + def test_add_forbidden_view_disallows_http_cache(self): + config = self._makeOne(autocommit=True) + self.assertRaises(ConfigurationError, + config.add_forbidden_view, http_cache='foo') + def test_add_notfound_view(self): from pyramid.renderers import null_renderer from zope.interface import implementedBy @@ -1713,6 +1766,38 @@ class TestViewsConfigurationMixin(unittest.TestCase): result = view(None, request) self.assertEqual(result, (None, request)) + def test_add_notfound_view_allows_other_predicates(self): + from pyramid.renderers import null_renderer + config = self._makeOne(autocommit=True) + # doesnt blow up + config.add_view_predicate('dummy', DummyPredicate) + config.add_notfound_view(renderer=null_renderer, dummy='abc') + + def test_add_notfound_view_disallows_name(self): + config = self._makeOne(autocommit=True) + self.assertRaises(ConfigurationError, + config.add_notfound_view, name='foo') + + def test_add_notfound_view_disallows_permission(self): + config = self._makeOne(autocommit=True) + self.assertRaises(ConfigurationError, + config.add_notfound_view, permission='foo') + + def test_add_notfound_view_disallows_context(self): + config = self._makeOne(autocommit=True) + self.assertRaises(ConfigurationError, + config.add_notfound_view, context='foo') + + def test_add_notfound_view_disallows_for_(self): + config = self._makeOne(autocommit=True) + self.assertRaises(ConfigurationError, + config.add_notfound_view, for_='foo') + + def test_add_notfound_view_disallows_http_cache(self): + config = self._makeOne(autocommit=True) + self.assertRaises(ConfigurationError, + config.add_notfound_view, http_cache='foo') + def test_add_notfound_view_append_slash(self): from pyramid.response import Response from pyramid.renderers import null_renderer @@ -2055,11 +2140,28 @@ class TestMultiView(unittest.TestCase): def test_add_with_phash_override_accept(self): mv = self._makeOne() - mv.add('view2', 100, accept='text/html', phash='abc') - mv.add('view3', 100, accept='text/html', phash='abc') - mv.add('view4', 99, accept='text/html', phash='def') + def view1(): pass + def view2(): pass + def view3(): pass + mv.add(view1, 100, accept='text/html', phash='abc') + mv.add(view2, 100, accept='text/html', phash='abc') + mv.add(view3, 99, accept='text/html', phash='def') self.assertEqual(mv.media_views['text/html'], - [(99, 'view4', 'def'), (100, 'view3', 'abc')]) + [(99, view3, 'def'), (100, view2, 'abc')]) + + def test_add_with_phash_override_accept2(self): + mv = self._makeOne() + def view1(): pass + def view2(): pass + def view3(): pass + mv.add(view1, 100, accept='text/html', phash='abc') + mv.add(view2, 100, accept='text/html', phash='def') + mv.add(view3, 99, accept='text/html', phash='ghi') + self.assertEqual(mv.media_views['text/html'], + [(99, view3, 'ghi'), + (100, view1, 'abc'), + (100, view2, 'def')] + ) def test_multiple_with_functions_as_views(self): # this failed on py3 at one point, because functions aren't orderable @@ -2905,6 +3007,7 @@ class TestViewDeriver(unittest.TestCase): view = lambda *arg: response def predicate1(context, request): return False + predicate1.text = lambda *arg: 'text' deriver = self._makeOne(predicates=[predicate1]) result = deriver(view) request = self._makeRequest() @@ -2912,7 +3015,8 @@ class TestViewDeriver(unittest.TestCase): try: result(None, None) except PredicateMismatch as e: - self.assertEqual(e.detail, 'predicate mismatch for view <lambda>') + self.assertEqual(e.detail, + 'predicate mismatch for view <lambda> (text)') else: # pragma: no cover raise AssertionError @@ -2921,6 +3025,7 @@ class TestViewDeriver(unittest.TestCase): def myview(request): pass def predicate1(context, request): return False + predicate1.text = lambda *arg: 'text' deriver = self._makeOne(predicates=[predicate1]) result = deriver(myview) request = self._makeRequest() @@ -2928,7 +3033,29 @@ class TestViewDeriver(unittest.TestCase): try: result(None, None) except PredicateMismatch as e: - self.assertEqual(e.detail, 'predicate mismatch for view myview') + self.assertEqual(e.detail, + 'predicate mismatch for view myview (text)') + else: # pragma: no cover + raise AssertionError + + def test_predicate_mismatch_exception_has_text_in_detail(self): + from pyramid.exceptions import PredicateMismatch + def myview(request): pass + def predicate1(context, request): + return True + predicate1.text = lambda *arg: 'pred1' + def predicate2(context, request): + return False + predicate2.text = lambda *arg: 'pred2' + deriver = self._makeOne(predicates=[predicate1, predicate2]) + result = deriver(myview) + request = self._makeRequest() + request.method = 'POST' + try: + result(None, None) + except PredicateMismatch as e: + self.assertEqual(e.detail, + 'predicate mismatch for view myview (pred2)') else: # pragma: no cover raise AssertionError @@ -2974,9 +3101,11 @@ class TestViewDeriver(unittest.TestCase): def predicate1(context, request): predicates.append(True) return True + predicate1.text = lambda *arg: 'text' def predicate2(context, request): predicates.append(True) return False + predicate2.text = lambda *arg: 'text' deriver = self._makeOne(predicates=[predicate1, predicate2]) result = deriver(view) request = self._makeRequest() @@ -3943,3 +4072,13 @@ class DummyViewDefaultsClass(object): pass def __call__(self): return 'OK' + +class DummyPredicate(object): + def __init__(self, val, config): + self.val = val + + def text(self): + return 'dummy' + + phash = text + 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_events.py b/pyramid/tests/test_events.py index f35083c02..2c72c07e8 100644 --- a/pyramid/tests/test_events.py +++ b/pyramid/tests/test_events.py @@ -9,13 +9,13 @@ class NewRequestEventTests(unittest.TestCase): def _makeOne(self, request): return self._getTargetClass()(request) - def test_class_implements(self): + def test_class_conforms_to_INewRequest(self): from pyramid.interfaces import INewRequest from zope.interface.verify import verifyClass klass = self._getTargetClass() verifyClass(INewRequest, klass) - def test_instance_implements(self): + def test_instance_conforms_to_INewRequest(self): from pyramid.interfaces import INewRequest from zope.interface.verify import verifyObject request = DummyRequest() @@ -35,13 +35,13 @@ class NewResponseEventTests(unittest.TestCase): def _makeOne(self, request, response): return self._getTargetClass()(request, response) - def test_class_implements(self): + def test_class_conforms_to_INewResponse(self): from pyramid.interfaces import INewResponse from zope.interface.verify import verifyClass klass = self._getTargetClass() verifyClass(INewResponse, klass) - def test_instance_implements(self): + def test_instance_conforms_to_INewResponse(self): from pyramid.interfaces import INewResponse from zope.interface.verify import verifyObject request = DummyRequest() @@ -57,68 +57,72 @@ class NewResponseEventTests(unittest.TestCase): self.assertEqual(inst.response, response) class ApplicationCreatedEventTests(unittest.TestCase): - def test_alias_object_implements(self): - from pyramid.events import WSGIApplicationCreatedEvent - event = WSGIApplicationCreatedEvent(object()) - from pyramid.interfaces import IWSGIApplicationCreatedEvent - from pyramid.interfaces import IApplicationCreated - from zope.interface.verify import verifyObject - verifyObject(IWSGIApplicationCreatedEvent, event) - verifyObject(IApplicationCreated, event) + def _getTargetClass(self): + from pyramid.events import ApplicationCreated + return ApplicationCreated - def test_alias_class_implements(self): - from pyramid.events import WSGIApplicationCreatedEvent - from pyramid.interfaces import IWSGIApplicationCreatedEvent + def _makeOne(self, context=object()): + return self._getTargetClass()(context) + + def test_class_conforms_to_IApplicationCreated(self): from pyramid.interfaces import IApplicationCreated from zope.interface.verify import verifyClass - verifyClass(IWSGIApplicationCreatedEvent, WSGIApplicationCreatedEvent) - verifyClass(IApplicationCreated, WSGIApplicationCreatedEvent) + verifyClass(IApplicationCreated, self._getTargetClass()) - def test_object_implements(self): - from pyramid.events import ApplicationCreated - event = ApplicationCreated(object()) + def test_object_conforms_to_IApplicationCreated(self): from pyramid.interfaces import IApplicationCreated from zope.interface.verify import verifyObject - verifyObject(IApplicationCreated, event) + verifyObject(IApplicationCreated, self._makeOne()) - def test_class_implements(self): - from pyramid.events import ApplicationCreated - from pyramid.interfaces import IApplicationCreated +class WSGIApplicationCreatedEventTests(ApplicationCreatedEventTests): + def _getTargetClass(self): + from pyramid.events import WSGIApplicationCreatedEvent + return WSGIApplicationCreatedEvent + + def test_class_conforms_to_IWSGIApplicationCreatedEvent(self): + from pyramid.interfaces import IWSGIApplicationCreatedEvent from zope.interface.verify import verifyClass - verifyClass(IApplicationCreated, ApplicationCreated) + verifyClass(IWSGIApplicationCreatedEvent, self._getTargetClass()) + + def test_object_conforms_to_IWSGIApplicationCreatedEvent(self): + from pyramid.interfaces import IWSGIApplicationCreatedEvent + from zope.interface.verify import verifyObject + verifyObject(IWSGIApplicationCreatedEvent, self._makeOne()) class ContextFoundEventTests(unittest.TestCase): - def test_alias_class_implements(self): + def _getTargetClass(self): + from pyramid.events import ContextFound + return ContextFound + + def _makeOne(self, request=None): + if request is None: + request = DummyRequest() + return self._getTargetClass()(request) + + def test_class_conforms_to_IContextFound(self): from zope.interface.verify import verifyClass - from pyramid.events import AfterTraversal - from pyramid.interfaces import IAfterTraversal from pyramid.interfaces import IContextFound - verifyClass(IAfterTraversal, AfterTraversal) - verifyClass(IContextFound, AfterTraversal) + verifyClass(IContextFound, self._getTargetClass()) - def test_alias_instance_implements(self): + def test_instance_conforms_to_IContextFound(self): from zope.interface.verify import verifyObject - from pyramid.events import AfterTraversal - from pyramid.interfaces import IAfterTraversal from pyramid.interfaces import IContextFound - request = DummyRequest() - inst = AfterTraversal(request) - verifyObject(IAfterTraversal, inst) - verifyObject(IContextFound, inst) + verifyObject(IContextFound, self._makeOne()) - def test_class_implements(self): +class AfterTraversalEventTests(ContextFoundEventTests): + def _getTargetClass(self): + from pyramid.events import AfterTraversal + return AfterTraversal + + def test_class_conforms_to_IAfterTraversal(self): from zope.interface.verify import verifyClass - from pyramid.events import ContextFound - from pyramid.interfaces import IContextFound - verifyClass(IContextFound, ContextFound) + from pyramid.interfaces import IAfterTraversal + verifyClass(IAfterTraversal, self._getTargetClass()) - def test_instance_implements(self): + def test_instance_conforms_to_IAfterTraversal(self): from zope.interface.verify import verifyObject - from pyramid.events import ContextFound - from pyramid.interfaces import IContextFound - request = DummyRequest() - inst = ContextFound(request) - verifyObject(IContextFound, inst) + from pyramid.interfaces import IAfterTraversal + verifyObject(IAfterTraversal, self._makeOne()) class TestSubscriber(unittest.TestCase): def setUp(self): @@ -127,9 +131,9 @@ class TestSubscriber(unittest.TestCase): def tearDown(self): testing.tearDown() - def _makeOne(self, *ifaces): + def _makeOne(self, *ifaces, **predicates): from pyramid.events import subscriber - return subscriber(*ifaces) + return subscriber(*ifaces, **predicates) def test_register_single(self): from zope.interface import Interface @@ -186,6 +190,16 @@ class TestSubscriber(unittest.TestCase): self.assertEqual(dummy_venusian.attached, [(foo, dec.register, 'pyramid')]) + def test_regsister_with_predicates(self): + from zope.interface import Interface + dec = self._makeOne(a=1) + def foo(): pass + config = DummyConfigurator() + scanner = Dummy() + scanner.config = config + dec.register(scanner, None, foo) + self.assertEqual(config.subscribed, [(foo, Interface, {'a':1})]) + class TestBeforeRender(unittest.TestCase): def _makeOne(self, system, val=None): from pyramid.events import BeforeRender @@ -260,8 +274,11 @@ class DummyConfigurator(object): def __init__(self): self.subscribed = [] - def add_subscriber(self, wrapped, ifaces): - self.subscribed.append((wrapped, ifaces)) + def add_subscriber(self, wrapped, ifaces, **predicates): + if not predicates: + self.subscribed.append((wrapped, ifaces)) + else: + self.subscribed.append((wrapped, ifaces, predicates)) class DummyRegistry(object): pass diff --git a/pyramid/tests/test_mako_templating.py b/pyramid/tests/test_mako_templating.py index fbb04273b..97b2c679b 100644 --- a/pyramid/tests/test_mako_templating.py +++ b/pyramid/tests/test_mako_templating.py @@ -135,7 +135,7 @@ class Test_renderer_factory(Base, unittest.TestCase): self._callFUT(info) lookup = self._getLookup() self.assertEqual(lookup.template_args['input_encoding'], 'utf-16') - + def test_with_error_handler(self): settings = {'mako.directories':self.templates_dir, 'mako.error_handler':'pyramid.tests'} @@ -315,7 +315,7 @@ class MakoLookupTemplateRendererTests(Base, unittest.TestCase): def test_instance_implements_ITemplate(self): from zope.interface.verify import verifyObject from pyramid.interfaces import ITemplateRenderer - verifyObject(ITemplateRenderer, self._makeOne(None, None)) + verifyObject(ITemplateRenderer, self._makeOne(None, None, None)) def test_class_implements_ITemplate(self): from zope.interface.verify import verifyClass @@ -324,7 +324,7 @@ class MakoLookupTemplateRendererTests(Base, unittest.TestCase): def test_call(self): lookup = DummyLookup() - instance = self._makeOne('path', lookup) + instance = self._makeOne('path', None, lookup) result = instance({}, {'system':1}) self.assertTrue(isinstance(result, text_type)) self.assertEqual(result, text_('result')) @@ -332,7 +332,7 @@ class MakoLookupTemplateRendererTests(Base, unittest.TestCase): def test_call_with_system_context(self): # lame lookup = DummyLookup() - instance = self._makeOne('path', lookup) + instance = self._makeOne('path', None, lookup) result = instance({}, {'context':1}) self.assertTrue(isinstance(result, text_type)) self.assertEqual(result, text_('result')) @@ -340,21 +340,36 @@ class MakoLookupTemplateRendererTests(Base, unittest.TestCase): def test_call_with_tuple_value(self): lookup = DummyLookup() - instance = self._makeOne('path', lookup) + instance = self._makeOne('path', None, lookup) result = instance(('fub', {}), {'context':1}) self.assertEqual(lookup.deffed, 'fub') self.assertEqual(result, text_('result')) self.assertEqual(lookup.values, {'_context':1}) + def test_call_with_defname(self): + lookup = DummyLookup() + instance = self._makeOne('path', 'defname', lookup) + result = instance({}, {'system':1}) + self.assertTrue(isinstance(result, text_type)) + self.assertEqual(result, text_('result')) + + def test_call_with_defname_with_tuple_value(self): + lookup = DummyLookup() + instance = self._makeOne('path', 'defname', lookup) + result = instance(('defname', {}), {'context':1}) + self.assertEqual(lookup.deffed, 'defname') + self.assertEqual(result, text_('result')) + self.assertEqual(lookup.values, {'_context':1}) + def test_call_with_nondict_value(self): lookup = DummyLookup() - instance = self._makeOne('path', lookup) + instance = self._makeOne('path', None, lookup) self.assertRaises(ValueError, instance, None, {}) def test_call_render_raises(self): from pyramid.mako_templating import MakoRenderingException lookup = DummyLookup(exc=NotImplementedError) - instance = self._makeOne('path', lookup) + instance = self._makeOne('path', None, lookup) try: instance({}, {}) except MakoRenderingException as e: @@ -364,11 +379,11 @@ class MakoLookupTemplateRendererTests(Base, unittest.TestCase): def test_implementation(self): lookup = DummyLookup() - instance = self._makeOne('path', lookup) + instance = self._makeOne('path', None, lookup) result = instance.implementation().render_unicode() self.assertTrue(isinstance(result, text_type)) self.assertEqual(result, text_('result')) - + class TestIntegration(unittest.TestCase): def setUp(self): import pyramid.mako_templating @@ -391,7 +406,7 @@ class TestIntegration(unittest.TestCase): self.config.add_settings({'reload_templates': True}) result = render('helloworld.mak', {'a':1}).replace('\r','') self.assertEqual(result, text_('\nHello föö\n', 'utf-8')) - + def test_render_inheritance(self): from pyramid.renderers import render result = render('helloinherit.mak', {}).replace('\r','') @@ -402,6 +417,11 @@ class TestIntegration(unittest.TestCase): result = render('hello_inherit_pkg.mak', {}).replace('\r','') self.assertEqual(result, text_('Layout\nHello World!\n')) + def test_render_namespace(self): + from pyramid.renderers import render + result = render('hellocompo.mak', {}).replace('\r','') + self.assertEqual(result, text_('\nNamespace\nHello \nWorld!\n')) + def test_render_to_response(self): from pyramid.renderers import render_to_response result = render_to_response('helloworld.mak', {'a':1}) @@ -414,7 +434,7 @@ class TestIntegration(unittest.TestCase): {'a':1}) self.assertEqual(result.ubody.replace('\r', ''), text_('\nHello föö\n', 'utf-8')) - + def test_render_with_abs_path(self): from pyramid.renderers import render result = render('/helloworld.mak', {'a':1}).replace('\r','') @@ -426,7 +446,7 @@ class TestIntegration(unittest.TestCase): self.assertEqual( result.implementation().render_unicode().replace('\r',''), text_('\nHello föö\n', 'utf-8')) - + def test_template_not_found(self): from pyramid.renderers import render from mako.exceptions import TemplateLookupException @@ -459,12 +479,42 @@ class TestPkgResourceTemplateLookup(unittest.TestCase): result = inst.adjust_uri('a:b', None) self.assertEqual(result, 'a:b') + def test_adjust_uri_asset_spec_with_modified_asset_spec(self): + inst = self._makeOne() + result = inst.adjust_uri('a$b', None) + self.assertEqual(result, 'a:b') + + def test_adjust_uri_not_asset_spec_with_relativeto_asset_spec(self): + inst = self._makeOne() + result = inst.adjust_uri('c', 'a:b') + self.assertEqual(result, 'a:c') + + def test_adjust_uri_not_asset_spec_with_relativeto_modified_asset_spec(self): + inst = self._makeOne() + result = inst.adjust_uri('c', 'a$b') + self.assertEqual(result, 'a:c') + + def test_adjust_uri_not_asset_spec_with_relativeto_not_asset_spec(self): + inst = self._makeOne() + result = inst.adjust_uri('b', '../a') + self.assertEqual(result, '../b') + + def test_adjust_uri_not_asset_spec_abs_with_relativeto_asset_spec(self): + inst = self._makeOne() + result = inst.adjust_uri('/c', 'a:b') + self.assertEqual(result, '/c') + + def test_adjust_uri_asset_spec_with_relativeto_not_asset_spec_abs(self): + inst = self._makeOne() + result = inst.adjust_uri('a:b', '/c') + self.assertEqual(result, 'a:b') + def test_get_template_not_asset_spec(self): fixturedir = self.get_fixturedir() inst = self._makeOne(directories=[fixturedir]) result = inst.get_template('helloworld.mak') self.assertFalse(result is None) - + def test_get_template_asset_spec_with_filesystem_checks(self): inst = self._makeOne(filesystem_checks=True) result = inst.get_template('pyramid.tests:fixtures/helloworld.mak') @@ -478,7 +528,7 @@ class TestPkgResourceTemplateLookup(unittest.TestCase): self.assertFalse(result is None) finally: shutil.rmtree(tmpdir, ignore_errors=True) - + def test_get_template_asset_spec_missing(self): from mako.exceptions import TopLevelLookupException fixturedir = self.get_fixturedir() @@ -490,7 +540,7 @@ class TestMakoRenderingException(unittest.TestCase): def _makeOne(self, text): from pyramid.mako_templating import MakoRenderingException return MakoRenderingException(text) - + def test_repr_and_str(self): exc = self._makeOne('text') self.assertEqual(str(exc), 'text') @@ -499,7 +549,7 @@ class TestMakoRenderingException(unittest.TestCase): class DummyLookup(object): def __init__(self, exc=None): self.exc = exc - + def get_template(self, path): self.path = path return self @@ -513,8 +563,8 @@ class DummyLookup(object): raise self.exc self.values = values return text_('result') - + class DummyRendererInfo(object): def __init__(self, kw): self.__dict__.update(kw) - + diff --git a/pyramid/tests/test_path.py b/pyramid/tests/test_path.py index 42b38d785..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) @@ -259,17 +259,15 @@ class TestPkgResourcesAssetDescriptor(unittest.TestCase): def _makeOne(self, pkg='pyramid.tests', path='test_asset.py'): return self._getTargetClass()(pkg, path) - def test_class_implements(self): + def test_class_conforms_to_IAssetDescriptor(self): from pyramid.interfaces import IAssetDescriptor from zope.interface.verify import verifyClass - klass = self._getTargetClass() - verifyClass(IAssetDescriptor, klass) + verifyClass(IAssetDescriptor, self._getTargetClass()) - def test_instance_implements(self): + def test_instance_conforms_to_IAssetDescriptor(self): from pyramid.interfaces import IAssetDescriptor from zope.interface.verify import verifyObject - inst = self._makeOne() - verifyObject(IAssetDescriptor, inst) + verifyObject(IAssetDescriptor, self._makeOne()) def test_absspec(self): inst = self._makeOne() @@ -316,17 +314,15 @@ class TestFSAssetDescriptor(unittest.TestCase): def _makeOne(self, path=os.path.join(here, 'test_asset.py')): return self._getTargetClass()(path) - def test_class_implements(self): + def test_class_conforms_to_IAssetDescriptor(self): from pyramid.interfaces import IAssetDescriptor from zope.interface.verify import verifyClass - klass = self._getTargetClass() - verifyClass(IAssetDescriptor, klass) + verifyClass(IAssetDescriptor, self._getTargetClass()) - def test_instance_implements(self): + def test_instance_conforms_to_IAssetDescriptor(self): from pyramid.interfaces import IAssetDescriptor from zope.interface.verify import verifyObject - inst = self._makeOne() - verifyObject(IAssetDescriptor, inst) + verifyObject(IAssetDescriptor, self._makeOne()) def test_absspec(self): inst = self._makeOne() @@ -399,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 495d7dc23..af9188abc 100644 --- a/pyramid/tests/test_renderers.py +++ b/pyramid/tests/test_renderers.py @@ -292,7 +292,32 @@ class TestChameleonRendererLookup(unittest.TestCase): if path.endswith('.pyc'): # pragma: no cover path = path[:-1] self.assertTrue(factory.path.startswith(path)) - self.assertEqual(factory.kw, {}) + self.assertEqual(factory.kw, {'macro':None}) + + def test___call__spec_withmacro(self): + import os + from pyramid import tests + module_name = tests.__name__ + relpath = 'fixtures/withmacro#foo.pt' + renderer = {} + factory = DummyFactory(renderer) + spec = '%s:%s' % (module_name, relpath) + info = DummyRendererInfo({ + 'name':spec, + 'package':None, + 'registry':self.config.registry, + 'settings':{}, + 'type':'type', + }) + lookup = self._makeOne(factory) + result = lookup(info) + self.assertTrue(result is renderer) + path = os.path.join( + os.path.dirname(os.path.abspath(__file__)), + 'fixtures', + 'withmacro.pt') + self.assertTrue(factory.path.startswith(path)) + self.assertEqual(factory.kw, {'macro':'foo'}) def test___call__reload_assets_true(self): import pyramid.tests @@ -508,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') @@ -529,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() @@ -627,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 6d5131013..86cfd8b09 100644 --- a/pyramid/tests/test_request.py +++ b/pyramid/tests/test_request.py @@ -18,13 +18,15 @@ class TestRequest(unittest.TestCase): def tearDown(self): testing.tearDown() - def _makeOne(self, environ): - return self._getTargetClass()(environ) - def _getTargetClass(self): from pyramid.request import Request return Request + def _makeOne(self, environ=None): + if environ is None: + environ = {} + return self._getTargetClass()(environ) + def _registerResourceURL(self): from pyramid.interfaces import IResourceURL from zope.interface import Interface @@ -36,6 +38,17 @@ class TestRequest(unittest.TestCase): DummyResourceURL, (Interface, Interface), IResourceURL) + def test_class_conforms_to_IRequest(self): + from zope.interface.verify import verifyClass + from pyramid.interfaces import IRequest + verifyClass(IRequest, self._getTargetClass()) + klass = self._getTargetClass() + + def test_instance_conforms_to_IRequest(self): + from zope.interface.verify import verifyObject + from pyramid.interfaces import IRequest + verifyObject(IRequest, self._makeOne()) + def test_charset_defaults_to_utf8(self): r = self._makeOne({'PATH_INFO':'/'}) self.assertEqual(r.charset, 'UTF-8') @@ -61,25 +74,15 @@ class TestRequest(unittest.TestCase): request.charset = None self.assertEqual(request.GET['la'], text_(b'La Pe\xf1a')) - def test_class_implements(self): - from pyramid.interfaces import IRequest - klass = self._getTargetClass() - self.assertTrue(IRequest.implementedBy(klass)) - - def test_instance_provides(self): - from pyramid.interfaces import IRequest - inst = self._makeOne({}) - self.assertTrue(IRequest.providedBy(inst)) - def test_tmpl_context(self): from pyramid.request import TemplateContext - inst = self._makeOne({}) + inst = self._makeOne() result = inst.tmpl_context self.assertEqual(result.__class__, TemplateContext) def test_session_configured(self): from pyramid.interfaces import ISessionFactory - inst = self._makeOne({}) + inst = self._makeOne() def factory(request): return 'orangejuice' self.config.registry.registerUtility(factory, ISessionFactory) @@ -88,12 +91,12 @@ class TestRequest(unittest.TestCase): self.assertEqual(inst.__dict__['session'], 'orangejuice') def test_session_not_configured(self): - inst = self._makeOne({}) + inst = self._makeOne() inst.registry = self.config.registry self.assertRaises(AttributeError, getattr, inst, 'session') def test_setattr_and_getattr_dotnotation(self): - inst = self._makeOne({}) + inst = self._makeOne() inst.foo = 1 self.assertEqual(inst.foo, 1) @@ -105,7 +108,7 @@ class TestRequest(unittest.TestCase): self.assertEqual(environ, {}) # make sure we're not using adhoc attrs def test_add_response_callback(self): - inst = self._makeOne({}) + inst = self._makeOne() self.assertEqual(inst.response_callbacks, ()) def callback(request, response): """ """ @@ -115,7 +118,7 @@ class TestRequest(unittest.TestCase): self.assertEqual(inst.response_callbacks, [callback, callback]) def test__process_response_callbacks(self): - inst = self._makeOne({}) + inst = self._makeOne() def callback1(request, response): request.called1 = True response.called1 = True @@ -132,7 +135,7 @@ class TestRequest(unittest.TestCase): self.assertEqual(inst.response_callbacks, []) def test_add_finished_callback(self): - inst = self._makeOne({}) + inst = self._makeOne() self.assertEqual(inst.finished_callbacks, ()) def callback(request): """ """ @@ -142,7 +145,7 @@ class TestRequest(unittest.TestCase): self.assertEqual(inst.finished_callbacks, [callback, callback]) def test__process_finished_callbacks(self): - inst = self._makeOne({}) + inst = self._makeOne() def callback1(request): request.called1 = True def callback2(request): @@ -219,13 +222,13 @@ class TestRequest(unittest.TestCase): ('pyramid.tests:static/foo.css', request, {}) ) def test_is_response_false(self): - request = self._makeOne({}) + request = self._makeOne() request.registry = self.config.registry self.assertEqual(request.is_response('abc'), False) def test_is_response_false_adapter_is_not_self(self): from pyramid.interfaces import IResponse - request = self._makeOne({}) + request = self._makeOne() request.registry = self.config.registry def adapter(ob): return object() @@ -237,7 +240,7 @@ class TestRequest(unittest.TestCase): def test_is_response_adapter_true(self): from pyramid.interfaces import IResponse - request = self._makeOne({}) + request = self._makeOne() request.registry = self.config.registry class Foo(object): pass @@ -277,7 +280,7 @@ class TestRequest(unittest.TestCase): self.assertRaises(ValueError, getattr, request, 'json_body') def test_set_property(self): - request = self._makeOne({}) + request = self._makeOne() opts = [2, 1] def connect(obj): return opts.pop() @@ -286,7 +289,7 @@ class TestRequest(unittest.TestCase): self.assertEqual(2, request.db) def test_set_property_reify(self): - request = self._makeOne({}) + request = self._makeOne() opts = [2, 1] def connect(obj): return opts.pop() @@ -546,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_scaffolds/test_copydir.py b/pyramid/tests/test_scaffolds/test_copydir.py index 68cefbe6e..d757b837c 100644 --- a/pyramid/tests/test_scaffolds/test_copydir.py +++ b/pyramid/tests/test_scaffolds/test_copydir.py @@ -176,6 +176,14 @@ class Test_makedirs(unittest.TestCase): self._callFUT(target, 2, None) shutil.rmtree(tmpdir) + def test_makedirs_no_parent_dir(self): + import shutil + import tempfile + tmpdir = tempfile.mkdtemp() + target = os.path.join(tmpdir, 'nonexistent_subdir', 'non2') + self._callFUT(target, 2, None) + shutil.rmtree(tmpdir) + class Test_support_functions(unittest.TestCase): def _call_html_quote(self, *arg, **kw): 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_scripts/test_pviews.py b/pyramid/tests/test_scripts/test_pviews.py index 680d48cee..6a919c31b 100644 --- a/pyramid/tests/test_scripts/test_pviews.py +++ b/pyramid/tests/test_scripts/test_pviews.py @@ -309,7 +309,7 @@ class TestPViewsCommand(unittest.TestCase): L = [] command.out = L.append def predicate(): pass - predicate.__text__ = "predicate = x" + predicate.text = lambda *arg: "predicate = x" view = dummy.DummyView(context='context', view_name='a') view.__predicates__ = [predicate] command._find_view = lambda arg1, arg2: view @@ -448,7 +448,7 @@ class TestPViewsCommand(unittest.TestCase): L = [] command.out = L.append def predicate(): pass - predicate.__text__ = "predicate = x" + predicate.text = lambda *arg: "predicate = x" view = dummy.DummyView(context='context') view.__name__ = 'view' view.__view_attr__ = 'call' diff --git a/pyramid/tests/test_session.py b/pyramid/tests/test_session.py index 6d75c7950..5143b7a95 100644 --- a/pyramid/tests/test_session.py +++ b/pyramid/tests/test_session.py @@ -205,6 +205,48 @@ class TestUnencryptedCookieSession(unittest.TestCase): self.assertTrue(token) self.assertTrue('_csrft_' in session) + def test_serialize_option(self): + from pyramid.response import Response + secret = 'secret' + request = testing.DummyRequest() + session = self._makeOne(request, + signed_serialize=dummy_signed_serialize) + session['key'] = 'value' + response = Response() + self.assertEqual(session._set_cookie(response), True) + cookie = response.headerlist[-1][1] + expected_cookieval = dummy_signed_serialize( + (session.accessed, session.created, {'key': 'value'}), secret) + response = Response() + response.set_cookie('session', expected_cookieval) + expected_cookie = response.headerlist[-1][1] + self.assertEqual(cookie, expected_cookie) + + def test_deserialize_option(self): + import time + secret = 'secret' + request = testing.DummyRequest() + accessed = time.time() + state = {'key': 'value'} + cookieval = dummy_signed_serialize((accessed, accessed, state), secret) + request.cookies['session'] = cookieval + session = self._makeOne(request, + signed_deserialize=dummy_signed_deserialize) + self.assertEqual(dict(session), state) + +def dummy_signed_serialize(data, secret): + import base64 + from pyramid.compat import pickle, bytes_ + pickled = pickle.dumps(data) + return base64.b64encode(bytes_(secret)) + base64.b64encode(pickled) + +def dummy_signed_deserialize(serialized, secret): + import base64 + from pyramid.compat import pickle, bytes_ + serialized_data = base64.b64decode( + serialized[len(base64.b64encode(bytes_(secret))):]) + return pickle.loads(serialized_data) + class Test_manage_accessed(unittest.TestCase): def _makeOne(self, wrapped): from pyramid.session import manage_accessed 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 779c66818..7f14462a2 100644 --- a/pyramid/tests/test_testing.py +++ b/pyramid/tests/test_testing.py @@ -1,282 +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.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 - 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): @@ -910,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_url.py b/pyramid/tests/test_url.py index 50deb63f3..a7a565356 100644 --- a/pyramid/tests/test_url.py +++ b/pyramid/tests/test_url.py @@ -2,10 +2,8 @@ import os import unittest import warnings -from pyramid.testing import ( - setUp, - tearDown, - ) +from pyramid import testing + from pyramid.compat import ( text_, native_, @@ -14,10 +12,10 @@ from pyramid.compat import ( class TestURLMethodsMixin(unittest.TestCase): def setUp(self): - self.config = setUp() + self.config = testing.setUp() def tearDown(self): - tearDown() + testing.tearDown() def _makeOne(self, environ=None): from pyramid.url import URLMethodsMixin diff --git a/pyramid/tests/test_urldispatch.py b/pyramid/tests/test_urldispatch.py index e15242f75..b2164717e 100644 --- a/pyramid/tests/test_urldispatch.py +++ b/pyramid/tests/test_urldispatch.py @@ -311,6 +311,14 @@ class TestCompileRoute(unittest.TestCase): self.assertEqual(matcher('foo/baz/biz/buz/bar'), None) self.assertEqual(generator({'baz':1, 'buz':2, 'bar': 'html'}), '/foo/1/biz/2.html') + + def test_custom_regex_with_colons(self): + matcher, generator = self._callFUT('foo/{baz}/biz/{buz:(?:[^/\.]+)}.{bar}') + self.assertEqual(matcher('/foo/baz/biz/buz.bar'), + {'baz':'baz', 'buz':'buz', 'bar':'bar'}) + self.assertEqual(matcher('foo/baz/biz/buz/bar'), None) + self.assertEqual(generator({'baz':1, 'buz':2, 'bar': 'html'}), + '/foo/1/biz/2.html') def test_mixed_newstyle_oldstyle_pattern_defaults_to_newstyle(self): # pattern: '\\/foo\\/(?P<baz>abc)\\/biz\\/(?P<buz>[^/]+)\\/bar$' diff --git a/pyramid/tests/test_util.py b/pyramid/tests/test_util.py index 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/tests/test_view.py b/pyramid/tests/test_view.py index a105adb70..f63e17bd8 100644 --- a/pyramid/tests/test_view.py +++ b/pyramid/tests/test_view.py @@ -3,17 +3,14 @@ import sys from zope.interface import implementer -from pyramid.testing import ( - setUp, - tearDown, - ) +from pyramid import testing class BaseTest(object): def setUp(self): - self.config = setUp() + self.config = testing.setUp() def tearDown(self): - tearDown() + testing.tearDown() def _registerView(self, reg, app, name): from pyramid.interfaces import IRequest @@ -90,7 +87,7 @@ class Test_notfound_view_config(BaseTest, unittest.TestCase): config = call_venusian(venusian) settings = config.settings self.assertEqual(len(settings), 1) - self.assertEqual(len(settings[0]), 5) + self.assertEqual(len(settings[0]), 4) self.assertEqual(settings[0]['venusian'], venusian) self.assertEqual(settings[0]['view'], None) # comes from call_venusian self.assertEqual(settings[0]['attr'], 'view') @@ -334,10 +331,10 @@ class TestIsResponse(unittest.TestCase): class TestViewConfigDecorator(unittest.TestCase): def setUp(self): - setUp() + testing.setUp() def tearDown(self): - tearDown() + testing.tearDown() def _getTargetClass(self): from pyramid.view import view_config @@ -371,6 +368,10 @@ class TestViewConfigDecorator(unittest.TestCase): self.assertEqual(decorator.mapper, 'mapper') self.assertEqual(decorator.decorator, 'decorator') self.assertEqual(decorator.match_param, 'match_param') + + def test_create_with_other_predicates(self): + decorator = self._makeOne(foo=1) + self.assertEqual(decorator.foo, 1) def test_call_function(self): decorator = self._makeOne() 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/urldispatch.py b/pyramid/urldispatch.py index cccff14ba..4182ea665 100644 --- a/pyramid/urldispatch.py +++ b/pyramid/urldispatch.py @@ -148,7 +148,9 @@ def _compile_route(route): name = pat.pop() # unicode name = name[1:-1] if ':' in name: - name, reg = name.split(':') + # reg may contain colons as well, + # so we must strictly split name into two parts + name, reg = name.split(':', 1) else: reg = '[^/]+' gen.append('%%(%s)s' % native_(name)) # native diff --git a/pyramid/util.py b/pyramid/util.py index 7d5c97814..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. @@ -280,3 +287,4 @@ def shortrepr(object, closer): if len(r) > 100: r = r[:100] + ' ... %s' % closer return r + diff --git a/pyramid/view.py b/pyramid/view.py index bb531c326..12a2efde6 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -138,15 +138,6 @@ def render_view(context, request, name='', secure=True): return None return ''.join(iterable) -class _default(object): - def __nonzero__(self): - return False - __bool__ = __nonzero__ - def __repr__(self): # pragma: no cover - return '(default)' - -default = _default() - class view_config(object): """ A function, class or method :term:`decorator` which allows a developer to create view registrations nearer to a :term:`view @@ -174,12 +165,12 @@ class view_config(object): backwards compatibility purposes, as the name :class:`pyramid.view.bfg_view`. - The following arguments are supported to + The following keyword arguments are supported to :class:`pyramid.view.view_config`: ``context``, ``permission``, ``name``, ``request_type``, ``route_name``, ``request_method``, ``request_param``, ``containment``, ``xhr``, ``accept``, ``header``, ``path_info``, ``custom_predicates``, ``decorator``, ``mapper``, ``http_cache``, - and ``match_param``. + ``match_param``, and ``predicates``. The meanings of these arguments are the same as the arguments passed to :meth:`pyramid.config.Configurator.add_view`. If any argument is left @@ -190,21 +181,11 @@ class view_config(object): """ venusian = venusian # for testing injection - def __init__(self, name=default, request_type=default, for_=default, - permission=default, route_name=default, - request_method=default, request_param=default, - containment=default, attr=default, renderer=default, - wrapper=default, xhr=default, accept=default, - header=default, path_info=default, - custom_predicates=default, context=default, - decorator=default, mapper=default, http_cache=default, - match_param=default): - L = locals() - if (context is not default) or (for_ is not default): - L['context'] = context or for_ - for k, v in L.items(): - if k not in ('self', 'L') and v is not default: - setattr(self, k, v) + def __init__(self, **settings): + if 'for_' in settings: + if settings.get('context') is None: + settings['context'] = settings['for_'] + self.__dict__.update(settings) def __call__(self, wrapped): settings = self.__dict__.copy() @@ -326,7 +307,7 @@ class notfound_view_config(object): The notfound_view_config constructor accepts most of the same arguments as the constructor of :class:`pyramid.view.view_config`. It can be used in the same places, and behaves in largely the same way, except it always - registers a not found exception view instead of a "normal" view. + registers a not found exception view instead of a 'normal' view. Example: @@ -360,17 +341,8 @@ class notfound_view_config(object): venusian = venusian - def __init__(self, request_type=default, request_method=default, - route_name=default, request_param=default, attr=default, - renderer=default, containment=default, wrapper=default, - xhr=default, accept=default, header=default, - path_info=default, custom_predicates=default, - decorator=default, mapper=default, match_param=default, - append_slash=False): - L = locals() - for k, v in L.items(): - if k not in ('self', 'L') and v is not default: - self.__dict__[k] = v + def __init__(self, **settings): + self.__dict__.update(settings) def __call__(self, wrapped): settings = self.__dict__.copy() @@ -400,7 +372,7 @@ class forbidden_view_config(object): The forbidden_view_config constructor accepts most of the same arguments as the constructor of :class:`pyramid.view.view_config`. It can be used in the same places, and behaves in largely the same way, except it always - registers a forbidden exception view instead of a "normal" view. + registers a forbidden exception view instead of a 'normal' view. Example: @@ -413,9 +385,9 @@ class forbidden_view_config(object): def notfound(request): return Response('You are not allowed', status='401 Unauthorized') - All have the same meaning as :meth:`pyramid.view.view_config` and each - predicate argument restricts the set of circumstances under which this - notfound view will be invoked. + All arguments passed to this function have the same meaning as + :meth:`pyramid.view.view_config` and each predicate argument restricts + the set of circumstances under which this notfound view will be invoked. See :ref:`changing_the_forbidden_view` for detailed usage information. @@ -426,16 +398,8 @@ class forbidden_view_config(object): venusian = venusian - def __init__(self, request_type=default, request_method=default, - route_name=default, request_param=default, attr=default, - renderer=default, containment=default, wrapper=default, - xhr=default, accept=default, header=default, - path_info=default, custom_predicates=default, - decorator=default, mapper=default, match_param=default): - L = locals() - for k, v in L.items(): - if k not in ('self', 'L') and v is not default: - self.__dict__[k] = v + def __init__(self, **settings): + self.__dict__.update(settings) def __call__(self, wrapped): settings = self.__dict__.copy() @@ -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 |
