diff options
95 files changed, 1625 insertions, 603 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index d57444ad0..841d2ebec 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,12 +1,193 @@ -Next release -============ +1.4b1 (2012-11-21) +================== + +Features +-------- + +- Small microspeed enhancement which anticipates that a + ``pyramid.response.Response`` object is likely to be returned from a view. + Some code is shortcut if the class of the object returned by a view is this + class. A similar microoptimization was done to + ``pyramid.request.Request.is_response``. + +- Make it possible to use variable arguments on ``p*`` commands (``pserve``, + ``pshell``, ``pviews``, etc) in the form ``a=1 b=2`` so you can fill in + values in parameterized ``.ini`` file, e.g. ``pshell etc/development.ini + http_port=8080``. See https://github.com/Pylons/pyramid/pull/714 + +- A somewhat advanced and obscure feature of Pyramid event handlers is their + ability to handle "multi-interface" notifications. These notifications have + traditionally presented multiple objects to the subscriber callable. For + instance, if an event was sent by code like this:: + + registry.notify(event, context) + + In the past, in order to catch such an event, you were obligated to write and + register an event subscriber that mentioned both the event and the context in + its argument list:: + + @subscriber([SomeEvent, SomeContextType]) + def asubscriber(event, context): + pass + + In many subscriber callables registered this way, it was common for the logic + in the subscriber callable to completely ignore the second and following + arguments (e.g. ``context`` in the above example might be ignored), because + they usually existed as attributes of the event anyway. You could usually + get the same value by doing ``event.context`` or similar. + + The fact that you needed to put an extra argument which you usually ignored + in the subscriber callable body was only a minor annoyance until we added + "subscriber predicates", used to narrow the set of circumstances under which + a subscriber will be executed, in a prior 1.4 alpha release. Once those were + added, the annoyance was escalated, because subscriber predicates needed to + accept the same argument list and arity as the subscriber callables that they + were configured against. So, for example, if you had these two subscriber + registrations in your code:: + + @subscriber([SomeEvent, SomeContextType]) + def asubscriber(event, context): + pass + + @subscriber(SomeOtherEvent) + def asubscriber(event): + pass + + And you wanted to use a subscriber predicate:: + + @subscriber([SomeEvent, SomeContextType], mypredicate=True) + def asubscriber1(event, context): + pass + + @subscriber(SomeOtherEvent, mypredicate=True) + def asubscriber2(event): + pass + + If an existing ``mypredicate`` subscriber predicate had been written in such + a way that it accepted only one argument in its ``__call__``, you could not + use it against a subscription which named more than one interface in its + subscriber interface list. Similarly, if you had written a subscriber + predicate that accepted two arguments, you couldn't use it against a + registration that named only a single interface type. + + For example, if you created this predicate:: + + class MyPredicate(object): + # portions elided... + def __call__(self, event): + return self.val == event.context.foo + + It would not work against a multi-interface-registered subscription, so in + the above example, when you attempted to use it against ``asubscriber1``, it + would fail at runtime with a TypeError, claiming something was attempting to + call it with too many arguments. + + To hack around this limitation, you were obligated to design the + ``mypredicate`` predicate to expect to receive in its ``__call__`` either a + single ``event`` argument (a SomeOtherEvent object) *or* a pair of arguments + (a SomeEvent object and a SomeContextType object), presumably by doing + something like this:: + + class MyPredicate(object): + # portions elided... + def __call__(self, event, context=None): + return self.val == event.context.foo + + This was confusing and bad. + + In order to allow people to ignore unused arguments to subscriber callables + and to normalize the relationship between event subscribers and subscriber + predicates, we now allow both subscribers and subscriber predicates to accept + only a single ``event`` argument even if they've been subscribed for + notifications that involve multiple interfaces. Subscribers and subscriber + predicates that accept only one argument will receive the first object passed + to ``notify``; this is typically (but not always) the event object. The + other objects involved in the subscription lookup will be discarded. You can + now write an event subscriber that accepts only ``event`` even if it + subscribes to multiple interfaces:: + + @subscriber([SomeEvent, SomeContextType]) + def asubscriber(event): + # this will work! + + This prevents you from needing to match the subscriber callable parameters to + the subscription type unnecessarily, especially when you don't make use of + any argument in your subscribers except for the event object itself. + + Note, however, that if the event object is not the first + object in the call to ``notify``, you'll run into trouble. For example, if + notify is called with the context argument first:: + + registry.notify(context, event) + + You won't be able to take advantage of the event-only feature. It will + "work", but the object received by your event handler won't be the event + object, it will be the context object, which won't be very useful:: + + @subscriber([SomeContextType, SomeEvent]) + def asubscriber(event): + # bzzt! you'll be getting the context here as ``event``, and it'll + # be useless + + Existing multiple-argument subscribers continue to work without issue, so you + should continue use those if your system notifies using multiple interfaces + and the first interface is not the event interface. For example:: + + @subscriber([SomeContextType, SomeEvent]) + def asubscriber(context, event): + # this will still work! + + The event-only feature makes it possible to use a subscriber predicate that + accepts only a request argument within both multiple-interface subscriber + registrations and single-interface subscriber registrations. You needn't + make slightly different variations of predicates depending on the + subscription type arguments. Instead, just write all your subscriber + predicates so they only accept ``event`` in their ``__call__`` and they'll be + useful across all registrations for subscriptions that use an event as their + first argument, even ones which accept more than just ``event``. + + However, the same caveat applies to predicates as to subscriber callables: if + you're subscribing to a multi-interface event, and the first interface is not + the event interface, the predicate won't work properly. In such a case, + you'll need to match the predicate ``__call__`` argument ordering and + composition to the ordering of the interfaces. For example, if the + registration for the subscription uses ``[SomeContext, SomeEvent]``, you'll + need to reflect that in the ordering of the parameters of the predicate's + ``__call__`` method:: + + def __call__(self, context, event): + return event.request.path.startswith(self.val) + + tl;dr: 1) When using multi-interface subscriptions, always use the event type + as the first subscription registration argument and 2) When 1 is true, use + only ``event`` in your subscriber and subscriber predicate parameter lists, + no matter how many interfaces the subscriber is notified with. This + combination will result in the maximum amount of reusability of subscriber + predicates and the least amount of thought on your part. Drink responsibly. + +Bug Fixes +--------- + +- A failure when trying to locate the attribute ``__text__`` on route and view + predicates existed when the ``debug_routematch`` setting was true or when the + ``pviews`` command was used. See https://github.com/Pylons/pyramid/pull/727 + +Documentation +------------- + +- Sync up tutorial source files with the files that are rendered by the + scaffold that each uses. + +1.4a4 (2012-11-14) +================== Features -------- - ``pyramid.authentication.AuthTktAuthenticationPolicy`` has been updated to support newer hashing algorithms such as ``sha512``. Existing applications - should consider updating if possible. + should consider updating if possible for improved security over the default + md5 hashing. - Added an ``effective_principals`` route and view predicate. @@ -21,18 +202,11 @@ Features - Slightly better debug logging from ``pyramid.authentication.RepozeWho1AuthenticationPolicy``. -- ``pyramid.security.view_execution_permitted`` used to return `True` if no +- ``pyramid.security.view_execution_permitted`` used to return ``True`` if no view could be found. It now raises a ``TypeError`` exception in that case, as it doesn't make sense to assert that a nonexistent view is execution-permitted. See https://github.com/Pylons/pyramid/issues/299. -- Get rid of shady monkeypatching of ``pyramid.request.Request`` and - ``pyramid.response.Response`` done within the ``__init__.py`` of Pyramid. - Webob no longer relies on this being done. Instead, the ResponseClass - attribute of the Pyramid Request class is assigned to the Pyramid response - class; that's enough to satisfy WebOb and behave as it did before with the - monkeypatching. - - Allow a ``_depth`` argument to ``pyramid.view.view_config``, which will permit limited composition reuse of the decorator by other software that wants to provide custom decorators that are much like view_config. @@ -57,14 +231,30 @@ Bug Fixes attribute of the request. It no longer fails in this case. See https://github.com/Pylons/pyramid/issues/700 +- Be more tolerant of potential error conditions in ``match_param`` and + ``physical_path`` predicate implementations; instead of raising an exception, + return False. + +- ``pyramid.view.render_view`` was not functioning properly under Python 3.x + due to a byte/unicode discrepancy. See + http://github.com/Pylons/pyramid/issues/721 + Deprecations ------------ -- ``pyramid.authentication.AuthTktAuthenticationPolicy`` will emit a warning - if an application is using the policy without explicitly setting the - ``hashalg``. This is because the default is "md5" which is considered - insecure. If you really want "md5" then you must specify it explicitly to - get rid of the warning. +- ``pyramid.authentication.AuthTktAuthenticationPolicy`` will emit a warning if + an application is using the policy without explicitly passing a ``hashalg`` + argument. This is because the default is "md5" which is considered + theoretically subject to collision attacks. If you really want "md5" then you + must specify it explicitly to get rid of the warning. + +Documentation +------------- + +- All of the tutorials that use + ``pyramid.authentication.AuthTktAuthenticationPolicy`` now explicitly pass + ``sha512`` as a ``hashalg`` argument. + Internals --------- @@ -77,6 +267,13 @@ Internals because that package should never be imported from non-Pyramid code. TopologicalSorter is still not an API, but may become one. +- Get rid of shady monkeypatching of ``pyramid.request.Request`` and + ``pyramid.response.Response`` done within the ``__init__.py`` of Pyramid. + Webob no longer relies on this being done. Instead, the ResponseClass + attribute of the Pyramid Request class is assigned to the Pyramid response + class; that's enough to satisfy WebOb and behave as it did before with the + monkeypatching. + 1.4a3 (2012-10-26) ================== diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index d03da3e62..971c172f8 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -188,3 +188,7 @@ Contributors - Domen Kozar, 2012/09/11 - David Gay, 2012/09/16 + +- Robert Jackiewicz, 2012/11/12 + +- John Anderson, 2012/11/14 @@ -4,6 +4,9 @@ Pyramid TODOs Nice-to-Have ------------ +- config.set_registry_attr with conflict detection... make sure the attr is + added before a commit, but register an action so a conflict can be detected. + - Provide the presumed renderer name to the called view as an attribute of the request. @@ -175,6 +178,3 @@ Probably Bad Ideas - _fix_registry should dictify the registry being fixed. -- config.set_registry_attr (with conflict detection)... bad idea because it - won't take effect until after a commit and folks will be confused by that. - diff --git a/docs/api/paster.rst b/docs/api/paster.rst index 3f7a1c364..bde128e05 100644 --- a/docs/api/paster.rst +++ b/docs/api/paster.rst @@ -7,7 +7,7 @@ .. autofunction:: bootstrap - .. autofunction:: get_app(config_uri, name=None) + .. autofunction:: get_app(config_uri, name=None, options=None) .. autofunction:: get_appsettings(config_uri, name=None) diff --git a/docs/conf.py b/docs/conf.py index 9bda4c798..3dc09d95d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -81,7 +81,7 @@ copyright = '%s, Agendaless Consulting' % datetime.datetime.now().year # other places throughout the built documents. # # The short X.Y version. -version = '1.4a3' +version = '1.4b1' # The full version, including alpha/beta/rc tags. release = version diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index 96fa77a07..ea75e5fe4 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -180,11 +180,14 @@ as a forbidden view: config.scan() Like any other view, the forbidden view must accept at least a ``request`` -parameter, or both ``context`` and ``request``. The ``context`` (available -as ``request.context`` if you're using the request-only view argument -pattern) is the context found by the router when the view invocation was -denied. The ``request`` is the current :term:`request` representing the -denied action. +parameter, or both ``context`` and ``request``. If a forbidden view +callable accepts both ``context`` and ``request``, the HTTP Exception is passed +as context. The ``context`` as found by the router when view was +denied (that you normally would expect) is available as +``request.context``. The ``request`` is the current :term:`request` +representing the denied action. + + Here's some sample code that implements a minimal forbidden view: diff --git a/docs/narr/install.rst b/docs/narr/install.rst index e8482a289..882e76f11 100644 --- a/docs/narr/install.rst +++ b/docs/narr/install.rst @@ -243,6 +243,21 @@ Once you've got setuptools or distribute installed, you should install the :term:`virtualenv` package. To install the :term:`virtualenv` package into your setuptools-enabled Python interpreter, use the ``easy_install`` command. +.. warning:: + + Even though Python 3.3 and better comes with ``pyvenv`` out of the box, + which is similar to ``virtualenv``, we suggest using ``virtualenv`` instead. + ``virtualenv`` works on well Python 3.3. This isn't a recommendation made + for technical reasons, it's one made because it's not feasible for the + authors of this guide to explain setup using multiple virtual environment + systems. We are aiming to not need to make the installation documentation + Turing-complete. + + ``pyvenv`` will work fine. However, if you use ``pyvenv`` instead of + ``virtualenv``, you'll need to understand how to install software such as + ``distribute`` into the virtual environment manually, which this guide does + not cover. + .. code-block:: text $ easy_install virtualenv diff --git a/docs/narr/introspector.rst b/docs/narr/introspector.rst index b88f3f0c8..bd81fb56a 100644 --- a/docs/narr/introspector.rst +++ b/docs/narr/introspector.rst @@ -130,6 +130,23 @@ introspectables in categories not described here. A sequence of interfaces (or classes) that are subscribed to (the resolution of the ``ifaces`` argument passed to ``add_subscriber``). + ``derived_subscriber`` + + A wrapper around the subscriber used internally by the system so it can + call it with more than one argument if your original subscriber accepts + only one. + + ``predicates`` + + The predicate objects created as the result of passing predicate arguments + to ``add_susbcriber`` + + ``derived_predicates`` + + Wrappers around the predicate objects created as the result of passing + predicate arguments to ``add_susbcriber`` (to be used when predicates take + only one value but must be passed more than one). + ``response adapters`` Each introspectable in the ``response adapters`` category represents a call diff --git a/docs/tutorials/wiki/basiclayout.rst b/docs/tutorials/wiki/basiclayout.rst index f6050f391..033dcb28c 100644 --- a/docs/tutorials/wiki/basiclayout.rst +++ b/docs/tutorials/wiki/basiclayout.rst @@ -165,7 +165,7 @@ Configuration in ``development.ini`` The ``development.ini`` (in the tutorial :term:`project` directory, as opposed to the tutorial :term:`package` directory) looks like this: -.. literalinclude:: src/views/development.ini +.. literalinclude:: src/basiclayout/development.ini :language: ini Note the existence of an ``[app:main]`` section which specifies our WSGI diff --git a/docs/tutorials/wiki/src/authorization/development.ini b/docs/tutorials/wiki/src/authorization/development.ini index 996caa741..72bd22e54 100644 --- a/docs/tutorials/wiki/src/authorization/development.ini +++ b/docs/tutorials/wiki/src/authorization/development.ini @@ -1,5 +1,11 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html +### + [app:main] use = egg:tutorial + pyramid.reload_templates = true pyramid.debug_authorization = false pyramid.debug_notfound = false @@ -13,15 +19,26 @@ pyramid.includes = tm.attempts = 3 zodbconn.uri = file://%(here)s/Data.fs?connection_cache_size=20000 +# By default, the toolbar only appears for clients from IP addresses +# '127.0.0.1' and '::1'. +# debugtoolbar.hosts = 127.0.0.1 ::1 + +### +# wsgi server configuration +### + [server:main] use = egg:waitress#main host = 0.0.0.0 port = 6543 -# Begin logging configuration +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +### [loggers] -keys = root +keys = root, tutorial [handlers] keys = console @@ -33,6 +50,11 @@ keys = generic level = INFO handlers = console +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + [handler_console] class = StreamHandler args = (sys.stderr,) @@ -41,5 +63,3 @@ formatter = generic [formatter_generic] format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s - -# End logging configuration diff --git a/docs/tutorials/wiki/src/authorization/production.ini b/docs/tutorials/wiki/src/authorization/production.ini index ca8107802..d9bf27c42 100644 --- a/docs/tutorials/wiki/src/authorization/production.ini +++ b/docs/tutorials/wiki/src/authorization/production.ini @@ -1,5 +1,11 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html +### + [app:main] use = egg:tutorial + pyramid.reload_templates = false pyramid.debug_authorization = false pyramid.debug_notfound = false @@ -12,12 +18,19 @@ pyramid.includes = tm.attempts = 3 zodbconn.uri = file://%(here)s/Data.fs?connection_cache_size=20000 +### +# wsgi server configuration +### + [server:main] use = egg:waitress#main host = 0.0.0.0 port = 6543 -# Begin logging configuration +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +### [loggers] keys = root, tutorial @@ -45,5 +58,3 @@ formatter = generic [formatter_generic] format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s - -# End logging configuration diff --git a/docs/tutorials/wiki/src/authorization/setup.py b/docs/tutorials/wiki/src/authorization/setup.py index d83f14400..1d2d690fc 100644 --- a/docs/tutorials/wiki/src/authorization/setup.py +++ b/docs/tutorials/wiki/src/authorization/setup.py @@ -22,9 +22,8 @@ setup(name='tutorial', description='tutorial', long_description=README + '\n\n' + CHANGES, classifiers=[ - "Intended Audience :: Developers", - "Framework :: Pylons", "Programming Language :: Python", + "Framework :: Pyramid", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", ], @@ -35,8 +34,8 @@ setup(name='tutorial', packages=find_packages(), include_package_data=True, zip_safe=False, - install_requires=requires, - tests_require=requires, + install_requires = requires, + tests_require= requires, test_suite="tutorial", entry_points = """\ [paste.app_factory] diff --git a/docs/tutorials/wiki/src/authorization/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki/src/authorization/tutorial/templates/mytemplate.pt index 3597c679b..84824f605 100644 --- a/docs/tutorials/wiki/src/authorization/tutorial/templates/mytemplate.pt +++ b/docs/tutorials/wiki/src/authorization/tutorial/templates/mytemplate.pt @@ -6,9 +6,9 @@ <meta name="keywords" content="python web application" /> <meta name="description" content="pyramid web application" /> <link rel="shortcut icon" href="/static/favicon.ico" /> + <link rel="stylesheet" href="/static/pylons.css" type="text/css" media="screen" charset="utf-8" /> <link rel="stylesheet" href="http://static.pylonsproject.org/fonts/nobile/stylesheet.css" media="screen" /> <link rel="stylesheet" href="http://static.pylonsproject.org/fonts/neuton/stylesheet.css" media="screen" /> - <link rel="stylesheet" href="/static/pylons.css" type="text/css" media="screen" charset="utf-8" /> <!--[if lte IE 6]> <link rel="stylesheet" href="/static/ie6.css" type="text/css" media="screen" charset="utf-8" /> <![endif]--> @@ -41,7 +41,7 @@ <h2>Pyramid links</h2> <ul class="links"> <li> - <a href="http://pylonsproject.org">Pylons Website</a> + <a href="http://pylonsproject.org/">Pylons Website</a> </li> <li> <a href="http://docs.pylonsproject.org/projects/pyramid/current/#narrative-documentation">Narrative Documentation</a> @@ -70,7 +70,7 @@ </div> </div> <div id="footer"> - <div class="footer">© Copyright 2008-2011, Agendaless Consulting.</div> + <div class="footer">© Copyright 2008-2012, Agendaless Consulting.</div> </div> </body> </html> diff --git a/docs/tutorials/wiki/src/basiclayout/development.ini b/docs/tutorials/wiki/src/basiclayout/development.ini index f637ebaa6..72bd22e54 100644 --- a/docs/tutorials/wiki/src/basiclayout/development.ini +++ b/docs/tutorials/wiki/src/basiclayout/development.ini @@ -1,5 +1,11 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html +### + [app:main] use = egg:tutorial + pyramid.reload_templates = true pyramid.debug_authorization = false pyramid.debug_notfound = false @@ -13,15 +19,26 @@ pyramid.includes = tm.attempts = 3 zodbconn.uri = file://%(here)s/Data.fs?connection_cache_size=20000 +# By default, the toolbar only appears for clients from IP addresses +# '127.0.0.1' and '::1'. +# debugtoolbar.hosts = 127.0.0.1 ::1 + +### +# wsgi server configuration +### + [server:main] use = egg:waitress#main host = 0.0.0.0 port = 6543 -# Begin logging configuration +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +### [loggers] -keys = root +keys = root, tutorial [handlers] keys = console @@ -33,6 +50,11 @@ keys = generic level = INFO handlers = console +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + [handler_console] class = StreamHandler args = (sys.stderr,) @@ -40,6 +62,4 @@ level = NOTSET formatter = generic [formatter_generic] -format = %(asctime)s %(levelname)-5.5s [%(name)s] %(message)s - -# End logging configuration +format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s diff --git a/docs/tutorials/wiki/src/basiclayout/production.ini b/docs/tutorials/wiki/src/basiclayout/production.ini index ca8107802..d9bf27c42 100644 --- a/docs/tutorials/wiki/src/basiclayout/production.ini +++ b/docs/tutorials/wiki/src/basiclayout/production.ini @@ -1,5 +1,11 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html +### + [app:main] use = egg:tutorial + pyramid.reload_templates = false pyramid.debug_authorization = false pyramid.debug_notfound = false @@ -12,12 +18,19 @@ pyramid.includes = tm.attempts = 3 zodbconn.uri = file://%(here)s/Data.fs?connection_cache_size=20000 +### +# wsgi server configuration +### + [server:main] use = egg:waitress#main host = 0.0.0.0 port = 6543 -# Begin logging configuration +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +### [loggers] keys = root, tutorial @@ -45,5 +58,3 @@ formatter = generic [formatter_generic] format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s - -# End logging configuration diff --git a/docs/tutorials/wiki/src/basiclayout/setup.py b/docs/tutorials/wiki/src/basiclayout/setup.py index bd26f8efc..647c3a638 100644 --- a/docs/tutorials/wiki/src/basiclayout/setup.py +++ b/docs/tutorials/wiki/src/basiclayout/setup.py @@ -22,7 +22,7 @@ setup(name='tutorial', long_description=README + '\n\n' + CHANGES, classifiers=[ "Programming Language :: Python", - "Framework :: Pylons", + "Framework :: Pyramid", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", ], diff --git a/docs/tutorials/wiki/src/basiclayout/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki/src/basiclayout/tutorial/templates/mytemplate.pt index 557e071ed..84824f605 100644 --- a/docs/tutorials/wiki/src/basiclayout/tutorial/templates/mytemplate.pt +++ b/docs/tutorials/wiki/src/basiclayout/tutorial/templates/mytemplate.pt @@ -6,9 +6,9 @@ <meta name="keywords" content="python web application" /> <meta name="description" content="pyramid web application" /> <link rel="shortcut icon" href="/static/favicon.ico" /> + <link rel="stylesheet" href="/static/pylons.css" type="text/css" media="screen" charset="utf-8" /> <link rel="stylesheet" href="http://static.pylonsproject.org/fonts/nobile/stylesheet.css" media="screen" /> <link rel="stylesheet" href="http://static.pylonsproject.org/fonts/neuton/stylesheet.css" media="screen" /> - <link rel="stylesheet" href="/static/pylons.css" type="text/css" media="screen" charset="utf-8" /> <!--[if lte IE 6]> <link rel="stylesheet" href="/static/ie6.css" type="text/css" media="screen" charset="utf-8" /> <![endif]--> @@ -70,7 +70,7 @@ </div> </div> <div id="footer"> - <div class="footer">© Copyright 2008-2011, Agendaless Consulting.</div> + <div class="footer">© Copyright 2008-2012, Agendaless Consulting.</div> </div> </body> </html> diff --git a/docs/tutorials/wiki/src/models/development.ini b/docs/tutorials/wiki/src/models/development.ini index 996caa741..72bd22e54 100644 --- a/docs/tutorials/wiki/src/models/development.ini +++ b/docs/tutorials/wiki/src/models/development.ini @@ -1,5 +1,11 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html +### + [app:main] use = egg:tutorial + pyramid.reload_templates = true pyramid.debug_authorization = false pyramid.debug_notfound = false @@ -13,15 +19,26 @@ pyramid.includes = tm.attempts = 3 zodbconn.uri = file://%(here)s/Data.fs?connection_cache_size=20000 +# By default, the toolbar only appears for clients from IP addresses +# '127.0.0.1' and '::1'. +# debugtoolbar.hosts = 127.0.0.1 ::1 + +### +# wsgi server configuration +### + [server:main] use = egg:waitress#main host = 0.0.0.0 port = 6543 -# Begin logging configuration +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +### [loggers] -keys = root +keys = root, tutorial [handlers] keys = console @@ -33,6 +50,11 @@ keys = generic level = INFO handlers = console +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + [handler_console] class = StreamHandler args = (sys.stderr,) @@ -41,5 +63,3 @@ formatter = generic [formatter_generic] format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s - -# End logging configuration diff --git a/docs/tutorials/wiki/src/models/production.ini b/docs/tutorials/wiki/src/models/production.ini index ca8107802..d9bf27c42 100644 --- a/docs/tutorials/wiki/src/models/production.ini +++ b/docs/tutorials/wiki/src/models/production.ini @@ -1,5 +1,11 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html +### + [app:main] use = egg:tutorial + pyramid.reload_templates = false pyramid.debug_authorization = false pyramid.debug_notfound = false @@ -12,12 +18,19 @@ pyramid.includes = tm.attempts = 3 zodbconn.uri = file://%(here)s/Data.fs?connection_cache_size=20000 +### +# wsgi server configuration +### + [server:main] use = egg:waitress#main host = 0.0.0.0 port = 6543 -# Begin logging configuration +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +### [loggers] keys = root, tutorial @@ -45,5 +58,3 @@ formatter = generic [formatter_generic] format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s - -# End logging configuration diff --git a/docs/tutorials/wiki/src/models/setup.py b/docs/tutorials/wiki/src/models/setup.py index bd26f8efc..647c3a638 100644 --- a/docs/tutorials/wiki/src/models/setup.py +++ b/docs/tutorials/wiki/src/models/setup.py @@ -22,7 +22,7 @@ setup(name='tutorial', long_description=README + '\n\n' + CHANGES, classifiers=[ "Programming Language :: Python", - "Framework :: Pylons", + "Framework :: Pyramid", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", ], diff --git a/docs/tutorials/wiki/src/models/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki/src/models/tutorial/templates/mytemplate.pt index 3597c679b..84824f605 100644 --- a/docs/tutorials/wiki/src/models/tutorial/templates/mytemplate.pt +++ b/docs/tutorials/wiki/src/models/tutorial/templates/mytemplate.pt @@ -6,9 +6,9 @@ <meta name="keywords" content="python web application" /> <meta name="description" content="pyramid web application" /> <link rel="shortcut icon" href="/static/favicon.ico" /> + <link rel="stylesheet" href="/static/pylons.css" type="text/css" media="screen" charset="utf-8" /> <link rel="stylesheet" href="http://static.pylonsproject.org/fonts/nobile/stylesheet.css" media="screen" /> <link rel="stylesheet" href="http://static.pylonsproject.org/fonts/neuton/stylesheet.css" media="screen" /> - <link rel="stylesheet" href="/static/pylons.css" type="text/css" media="screen" charset="utf-8" /> <!--[if lte IE 6]> <link rel="stylesheet" href="/static/ie6.css" type="text/css" media="screen" charset="utf-8" /> <![endif]--> @@ -41,7 +41,7 @@ <h2>Pyramid links</h2> <ul class="links"> <li> - <a href="http://pylonsproject.org">Pylons Website</a> + <a href="http://pylonsproject.org/">Pylons Website</a> </li> <li> <a href="http://docs.pylonsproject.org/projects/pyramid/current/#narrative-documentation">Narrative Documentation</a> @@ -70,7 +70,7 @@ </div> </div> <div id="footer"> - <div class="footer">© Copyright 2008-2011, Agendaless Consulting.</div> + <div class="footer">© Copyright 2008-2012, Agendaless Consulting.</div> </div> </body> </html> diff --git a/docs/tutorials/wiki/src/tests/development.ini b/docs/tutorials/wiki/src/tests/development.ini index 996caa741..72bd22e54 100644 --- a/docs/tutorials/wiki/src/tests/development.ini +++ b/docs/tutorials/wiki/src/tests/development.ini @@ -1,5 +1,11 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html +### + [app:main] use = egg:tutorial + pyramid.reload_templates = true pyramid.debug_authorization = false pyramid.debug_notfound = false @@ -13,15 +19,26 @@ pyramid.includes = tm.attempts = 3 zodbconn.uri = file://%(here)s/Data.fs?connection_cache_size=20000 +# By default, the toolbar only appears for clients from IP addresses +# '127.0.0.1' and '::1'. +# debugtoolbar.hosts = 127.0.0.1 ::1 + +### +# wsgi server configuration +### + [server:main] use = egg:waitress#main host = 0.0.0.0 port = 6543 -# Begin logging configuration +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +### [loggers] -keys = root +keys = root, tutorial [handlers] keys = console @@ -33,6 +50,11 @@ keys = generic level = INFO handlers = console +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + [handler_console] class = StreamHandler args = (sys.stderr,) @@ -41,5 +63,3 @@ formatter = generic [formatter_generic] format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s - -# End logging configuration diff --git a/docs/tutorials/wiki/src/tests/production.ini b/docs/tutorials/wiki/src/tests/production.ini index ca8107802..d9bf27c42 100644 --- a/docs/tutorials/wiki/src/tests/production.ini +++ b/docs/tutorials/wiki/src/tests/production.ini @@ -1,5 +1,11 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html +### + [app:main] use = egg:tutorial + pyramid.reload_templates = false pyramid.debug_authorization = false pyramid.debug_notfound = false @@ -12,12 +18,19 @@ pyramid.includes = tm.attempts = 3 zodbconn.uri = file://%(here)s/Data.fs?connection_cache_size=20000 +### +# wsgi server configuration +### + [server:main] use = egg:waitress#main host = 0.0.0.0 port = 6543 -# Begin logging configuration +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +### [loggers] keys = root, tutorial @@ -45,5 +58,3 @@ formatter = generic [formatter_generic] format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s - -# End logging configuration diff --git a/docs/tutorials/wiki/src/tests/setup.py b/docs/tutorials/wiki/src/tests/setup.py index 51c638638..a665611bd 100644 --- a/docs/tutorials/wiki/src/tests/setup.py +++ b/docs/tutorials/wiki/src/tests/setup.py @@ -23,9 +23,8 @@ setup(name='tutorial', description='tutorial', long_description=README + '\n\n' + CHANGES, classifiers=[ - "Intended Audience :: Developers", - "Framework :: Pylons", "Programming Language :: Python", + "Framework :: Pyramid", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", ], @@ -36,8 +35,8 @@ setup(name='tutorial', packages=find_packages(), include_package_data=True, zip_safe=False, - install_requires=requires, - tests_require=requires, + install_requires = requires, + tests_require= requires, test_suite="tutorial", entry_points = """\ [paste.app_factory] diff --git a/docs/tutorials/wiki/src/tests/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki/src/tests/tutorial/templates/mytemplate.pt index 3597c679b..84824f605 100644 --- a/docs/tutorials/wiki/src/tests/tutorial/templates/mytemplate.pt +++ b/docs/tutorials/wiki/src/tests/tutorial/templates/mytemplate.pt @@ -6,9 +6,9 @@ <meta name="keywords" content="python web application" /> <meta name="description" content="pyramid web application" /> <link rel="shortcut icon" href="/static/favicon.ico" /> + <link rel="stylesheet" href="/static/pylons.css" type="text/css" media="screen" charset="utf-8" /> <link rel="stylesheet" href="http://static.pylonsproject.org/fonts/nobile/stylesheet.css" media="screen" /> <link rel="stylesheet" href="http://static.pylonsproject.org/fonts/neuton/stylesheet.css" media="screen" /> - <link rel="stylesheet" href="/static/pylons.css" type="text/css" media="screen" charset="utf-8" /> <!--[if lte IE 6]> <link rel="stylesheet" href="/static/ie6.css" type="text/css" media="screen" charset="utf-8" /> <![endif]--> @@ -41,7 +41,7 @@ <h2>Pyramid links</h2> <ul class="links"> <li> - <a href="http://pylonsproject.org">Pylons Website</a> + <a href="http://pylonsproject.org/">Pylons Website</a> </li> <li> <a href="http://docs.pylonsproject.org/projects/pyramid/current/#narrative-documentation">Narrative Documentation</a> @@ -70,7 +70,7 @@ </div> </div> <div id="footer"> - <div class="footer">© Copyright 2008-2011, Agendaless Consulting.</div> + <div class="footer">© Copyright 2008-2012, Agendaless Consulting.</div> </div> </body> </html> diff --git a/docs/tutorials/wiki/src/views/development.ini b/docs/tutorials/wiki/src/views/development.ini index f637ebaa6..72bd22e54 100644 --- a/docs/tutorials/wiki/src/views/development.ini +++ b/docs/tutorials/wiki/src/views/development.ini @@ -1,5 +1,11 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html +### + [app:main] use = egg:tutorial + pyramid.reload_templates = true pyramid.debug_authorization = false pyramid.debug_notfound = false @@ -13,15 +19,26 @@ pyramid.includes = tm.attempts = 3 zodbconn.uri = file://%(here)s/Data.fs?connection_cache_size=20000 +# By default, the toolbar only appears for clients from IP addresses +# '127.0.0.1' and '::1'. +# debugtoolbar.hosts = 127.0.0.1 ::1 + +### +# wsgi server configuration +### + [server:main] use = egg:waitress#main host = 0.0.0.0 port = 6543 -# Begin logging configuration +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +### [loggers] -keys = root +keys = root, tutorial [handlers] keys = console @@ -33,6 +50,11 @@ keys = generic level = INFO handlers = console +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + [handler_console] class = StreamHandler args = (sys.stderr,) @@ -40,6 +62,4 @@ level = NOTSET formatter = generic [formatter_generic] -format = %(asctime)s %(levelname)-5.5s [%(name)s] %(message)s - -# End logging configuration +format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s diff --git a/docs/tutorials/wiki/src/views/production.ini b/docs/tutorials/wiki/src/views/production.ini index ca8107802..d9bf27c42 100644 --- a/docs/tutorials/wiki/src/views/production.ini +++ b/docs/tutorials/wiki/src/views/production.ini @@ -1,5 +1,11 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html +### + [app:main] use = egg:tutorial + pyramid.reload_templates = false pyramid.debug_authorization = false pyramid.debug_notfound = false @@ -12,12 +18,19 @@ pyramid.includes = tm.attempts = 3 zodbconn.uri = file://%(here)s/Data.fs?connection_cache_size=20000 +### +# wsgi server configuration +### + [server:main] use = egg:waitress#main host = 0.0.0.0 port = 6543 -# Begin logging configuration +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +### [loggers] keys = root, tutorial @@ -45,5 +58,3 @@ formatter = generic [formatter_generic] format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s - -# End logging configuration diff --git a/docs/tutorials/wiki/src/views/setup.py b/docs/tutorials/wiki/src/views/setup.py index 6f946582c..1d2d690fc 100644 --- a/docs/tutorials/wiki/src/views/setup.py +++ b/docs/tutorials/wiki/src/views/setup.py @@ -22,9 +22,8 @@ setup(name='tutorial', description='tutorial', long_description=README + '\n\n' + CHANGES, classifiers=[ - "Intended Audience :: Developers", - "Framework :: Pylons", "Programming Language :: Python", + "Framework :: Pyramid", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", ], @@ -35,11 +34,12 @@ setup(name='tutorial', packages=find_packages(), include_package_data=True, zip_safe=False, - install_requires=requires, - tests_require=requires, + install_requires = requires, + tests_require= requires, test_suite="tutorial", entry_points = """\ [paste.app_factory] main = tutorial:main """, ) + diff --git a/docs/tutorials/wiki/src/views/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki/src/views/tutorial/templates/mytemplate.pt index 3597c679b..84824f605 100644 --- a/docs/tutorials/wiki/src/views/tutorial/templates/mytemplate.pt +++ b/docs/tutorials/wiki/src/views/tutorial/templates/mytemplate.pt @@ -6,9 +6,9 @@ <meta name="keywords" content="python web application" /> <meta name="description" content="pyramid web application" /> <link rel="shortcut icon" href="/static/favicon.ico" /> + <link rel="stylesheet" href="/static/pylons.css" type="text/css" media="screen" charset="utf-8" /> <link rel="stylesheet" href="http://static.pylonsproject.org/fonts/nobile/stylesheet.css" media="screen" /> <link rel="stylesheet" href="http://static.pylonsproject.org/fonts/neuton/stylesheet.css" media="screen" /> - <link rel="stylesheet" href="/static/pylons.css" type="text/css" media="screen" charset="utf-8" /> <!--[if lte IE 6]> <link rel="stylesheet" href="/static/ie6.css" type="text/css" media="screen" charset="utf-8" /> <![endif]--> @@ -41,7 +41,7 @@ <h2>Pyramid links</h2> <ul class="links"> <li> - <a href="http://pylonsproject.org">Pylons Website</a> + <a href="http://pylonsproject.org/">Pylons Website</a> </li> <li> <a href="http://docs.pylonsproject.org/projects/pyramid/current/#narrative-documentation">Narrative Documentation</a> @@ -70,7 +70,7 @@ </div> </div> <div id="footer"> - <div class="footer">© Copyright 2008-2011, Agendaless Consulting.</div> + <div class="footer">© Copyright 2008-2012, Agendaless Consulting.</div> </div> </body> </html> diff --git a/docs/tutorials/wiki/tests.rst b/docs/tutorials/wiki/tests.rst index ebb4d93dc..46d009d0f 100644 --- a/docs/tutorials/wiki/tests.rst +++ b/docs/tutorials/wiki/tests.rst @@ -62,7 +62,7 @@ Change the ``requires`` list in ``setup.py`` to include ``WebTest``. .. literalinclude:: src/tests/setup.py :linenos: :language: python - :lines: 9-18 + :lines: 9-19 After we've added a dependency on WebTest in ``setup.py``, we need to rerun ``setup.py develop`` to get WebTest installed into our virtualenv. Assuming diff --git a/docs/tutorials/wiki2/basiclayout.rst b/docs/tutorials/wiki2/basiclayout.rst index dbd130c36..4f73dc914 100644 --- a/docs/tutorials/wiki2/basiclayout.rst +++ b/docs/tutorials/wiki2/basiclayout.rst @@ -171,6 +171,12 @@ application. Without being processed by ``scan``, the decorator effectively does nothing. ``@view_config`` is inert without being detected via a :term:`scan`. +The sample ``my_view()`` created by the scaffold uses a ``try:`` and ``except:`` +clause, to detect if there is a problem accessing the project database and +provide an alternate error response. That response will include the text +shown at the end of the file, which will be displayed in the browser to +inform the user about possible actions to take to solve the problem. + Content Models with ``models.py`` --------------------------------- diff --git a/docs/tutorials/wiki2/src/authorization/README.txt b/docs/tutorials/wiki2/src/authorization/README.txt index 6f851e9b7..141851285 100644 --- a/docs/tutorials/wiki2/src/authorization/README.txt +++ b/docs/tutorials/wiki2/src/authorization/README.txt @@ -1 +1,14 @@ tutorial README +================== + +Getting Started +--------------- + +- cd <directory containing this file> + +- $venv/bin/python setup.py develop + +- $venv/bin/initialize_tutorial_db development.ini + +- $venv/bin/pserve development.ini + diff --git a/docs/tutorials/wiki2/src/authorization/development.ini b/docs/tutorials/wiki2/src/authorization/development.ini index eb2f878c5..a9d53b296 100644 --- a/docs/tutorials/wiki2/src/authorization/development.ini +++ b/docs/tutorials/wiki2/src/authorization/development.ini @@ -1,3 +1,8 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html +### + [app:main] use = egg:tutorial @@ -12,12 +17,23 @@ pyramid.includes = sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite +# By default, the toolbar only appears for clients from IP addresses +# '127.0.0.1' and '::1'. +# debugtoolbar.hosts = 127.0.0.1 ::1 + +### +# wsgi server configuration +### + [server:main] use = egg:waitress#main host = 0.0.0.0 port = 6543 -# Begin logging configuration +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +### [loggers] keys = root, tutorial, sqlalchemy @@ -53,5 +69,3 @@ formatter = generic [formatter_generic] format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s - -# End logging configuration diff --git a/docs/tutorials/wiki2/src/authorization/setup.py b/docs/tutorials/wiki2/src/authorization/setup.py index 2fd051927..d658cae93 100644 --- a/docs/tutorials/wiki2/src/authorization/setup.py +++ b/docs/tutorials/wiki2/src/authorization/setup.py @@ -13,8 +13,8 @@ requires = [ 'pyramid_tm', 'pyramid_debugtoolbar', 'zope.sqlalchemy', - 'docutils', 'waitress', + 'docutils', ] setup(name='tutorial', @@ -23,7 +23,7 @@ setup(name='tutorial', long_description=README + '\n\n' + CHANGES, classifiers=[ "Programming Language :: Python", - "Framework :: Pylons", + "Framework :: Pyramid", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", ], @@ -35,11 +35,12 @@ setup(name='tutorial', include_package_data=True, zip_safe=False, test_suite='tutorial', - install_requires = requires, - entry_points = """\ + install_requires=requires, + entry_points="""\ [paste.app_factory] main = tutorial:main [console_scripts] initialize_tutorial_db = tutorial.scripts.initializedb:main """, ) + diff --git a/docs/tutorials/wiki2/src/basiclayout/README.txt b/docs/tutorials/wiki2/src/basiclayout/README.txt index 6f851e9b7..141851285 100644 --- a/docs/tutorials/wiki2/src/basiclayout/README.txt +++ b/docs/tutorials/wiki2/src/basiclayout/README.txt @@ -1 +1,14 @@ tutorial README +================== + +Getting Started +--------------- + +- cd <directory containing this file> + +- $venv/bin/python setup.py develop + +- $venv/bin/initialize_tutorial_db development.ini + +- $venv/bin/pserve development.ini + diff --git a/docs/tutorials/wiki2/src/basiclayout/development.ini b/docs/tutorials/wiki2/src/basiclayout/development.ini index eb2f878c5..a9d53b296 100644 --- a/docs/tutorials/wiki2/src/basiclayout/development.ini +++ b/docs/tutorials/wiki2/src/basiclayout/development.ini @@ -1,3 +1,8 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html +### + [app:main] use = egg:tutorial @@ -12,12 +17,23 @@ pyramid.includes = sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite +# By default, the toolbar only appears for clients from IP addresses +# '127.0.0.1' and '::1'. +# debugtoolbar.hosts = 127.0.0.1 ::1 + +### +# wsgi server configuration +### + [server:main] use = egg:waitress#main host = 0.0.0.0 port = 6543 -# Begin logging configuration +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +### [loggers] keys = root, tutorial, sqlalchemy @@ -53,5 +69,3 @@ formatter = generic [formatter_generic] format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s - -# End logging configuration diff --git a/docs/tutorials/wiki2/src/basiclayout/production.ini b/docs/tutorials/wiki2/src/basiclayout/production.ini index 4684d2f7a..fa94c1b3e 100644 --- a/docs/tutorials/wiki2/src/basiclayout/production.ini +++ b/docs/tutorials/wiki2/src/basiclayout/production.ini @@ -1,3 +1,8 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html +### + [app:main] use = egg:tutorial @@ -16,7 +21,10 @@ use = egg:waitress#main host = 0.0.0.0 port = 6543 -# Begin logging configuration +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +### [loggers] keys = root, tutorial, sqlalchemy @@ -52,5 +60,3 @@ formatter = generic [formatter_generic] format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s - -# End logging configuration diff --git a/docs/tutorials/wiki2/src/basiclayout/setup.py b/docs/tutorials/wiki2/src/basiclayout/setup.py index 050de7299..1a1ad78aa 100644 --- a/docs/tutorials/wiki2/src/basiclayout/setup.py +++ b/docs/tutorials/wiki2/src/basiclayout/setup.py @@ -22,7 +22,7 @@ setup(name='tutorial', long_description=README + '\n\n' + CHANGES, classifiers=[ "Programming Language :: Python", - "Framework :: Pylons", + "Framework :: Pyramid", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", ], @@ -34,8 +34,8 @@ setup(name='tutorial', include_package_data=True, zip_safe=False, test_suite='tutorial', - install_requires = requires, - entry_points = """\ + install_requires=requires, + entry_points="""\ [paste.app_factory] main = tutorial:main [console_scripts] diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/mytemplate.pt index fbfa9870b..15ea6614f 100644 --- a/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/mytemplate.pt +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/mytemplate.pt @@ -70,7 +70,7 @@ </div> </div> <div id="footer"> - <div class="footer">© Copyright 2008-2011, Agendaless Consulting.</div> + <div class="footer">© Copyright 2008-2012, Agendaless Consulting.</div> </div> </body> </html> diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/views.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/views.py index 3e6abf2c2..daf21bb7b 100644 --- a/docs/tutorials/wiki2/src/basiclayout/tutorial/views.py +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/views.py @@ -1,5 +1,8 @@ +from pyramid.response import Response from pyramid.view import view_config +from sqlalchemy.exc import DBAPIError + from .models import ( DBSession, MyModel, @@ -7,5 +10,25 @@ from .models import ( @view_config(route_name='home', renderer='templates/mytemplate.pt') def my_view(request): - one = DBSession.query(MyModel).filter(MyModel.name=='one').first() + try: + one = DBSession.query(MyModel).filter(MyModel.name=='one').first() + except DBAPIError: + return Response(conn_err_msg, content_type='text/plain', status_int=500) return {'one':one, 'project':'tutorial'} + +conn_err_msg = """\ +Pyramid is having a problem using your SQL database. The problem +might be caused by one of the following things: + +1. You may need to run the "initialize_tutorial_db" script + to initialize your database tables. Check your virtual + environment's "bin" directory for this script and try to run it. + +2. Your database server may not be running. Check that the + database server referred to by the "sqlalchemy.url" setting in + your "development.ini" file is running. + +After you fix the problem, please restart the Pyramid application to +try it again. +""" + diff --git a/docs/tutorials/wiki2/src/models/README.txt b/docs/tutorials/wiki2/src/models/README.txt index 6f851e9b7..141851285 100644 --- a/docs/tutorials/wiki2/src/models/README.txt +++ b/docs/tutorials/wiki2/src/models/README.txt @@ -1 +1,14 @@ tutorial README +================== + +Getting Started +--------------- + +- cd <directory containing this file> + +- $venv/bin/python setup.py develop + +- $venv/bin/initialize_tutorial_db development.ini + +- $venv/bin/pserve development.ini + diff --git a/docs/tutorials/wiki2/src/models/development.ini b/docs/tutorials/wiki2/src/models/development.ini index eb2f878c5..a9d53b296 100644 --- a/docs/tutorials/wiki2/src/models/development.ini +++ b/docs/tutorials/wiki2/src/models/development.ini @@ -1,3 +1,8 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html +### + [app:main] use = egg:tutorial @@ -12,12 +17,23 @@ pyramid.includes = sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite +# By default, the toolbar only appears for clients from IP addresses +# '127.0.0.1' and '::1'. +# debugtoolbar.hosts = 127.0.0.1 ::1 + +### +# wsgi server configuration +### + [server:main] use = egg:waitress#main host = 0.0.0.0 port = 6543 -# Begin logging configuration +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +### [loggers] keys = root, tutorial, sqlalchemy @@ -53,5 +69,3 @@ formatter = generic [formatter_generic] format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s - -# End logging configuration diff --git a/docs/tutorials/wiki2/src/models/setup.py b/docs/tutorials/wiki2/src/models/setup.py index 050de7299..1a1ad78aa 100644 --- a/docs/tutorials/wiki2/src/models/setup.py +++ b/docs/tutorials/wiki2/src/models/setup.py @@ -22,7 +22,7 @@ setup(name='tutorial', long_description=README + '\n\n' + CHANGES, classifiers=[ "Programming Language :: Python", - "Framework :: Pylons", + "Framework :: Pyramid", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", ], @@ -34,8 +34,8 @@ setup(name='tutorial', include_package_data=True, zip_safe=False, test_suite='tutorial', - install_requires = requires, - entry_points = """\ + install_requires=requires, + entry_points="""\ [paste.app_factory] main = tutorial:main [console_scripts] diff --git a/docs/tutorials/wiki2/src/models/tutorial/views.py b/docs/tutorials/wiki2/src/models/tutorial/views.py index 3e6abf2c2..daf21bb7b 100644 --- a/docs/tutorials/wiki2/src/models/tutorial/views.py +++ b/docs/tutorials/wiki2/src/models/tutorial/views.py @@ -1,5 +1,8 @@ +from pyramid.response import Response from pyramid.view import view_config +from sqlalchemy.exc import DBAPIError + from .models import ( DBSession, MyModel, @@ -7,5 +10,25 @@ from .models import ( @view_config(route_name='home', renderer='templates/mytemplate.pt') def my_view(request): - one = DBSession.query(MyModel).filter(MyModel.name=='one').first() + try: + one = DBSession.query(MyModel).filter(MyModel.name=='one').first() + except DBAPIError: + return Response(conn_err_msg, content_type='text/plain', status_int=500) return {'one':one, 'project':'tutorial'} + +conn_err_msg = """\ +Pyramid is having a problem using your SQL database. The problem +might be caused by one of the following things: + +1. You may need to run the "initialize_tutorial_db" script + to initialize your database tables. Check your virtual + environment's "bin" directory for this script and try to run it. + +2. Your database server may not be running. Check that the + database server referred to by the "sqlalchemy.url" setting in + your "development.ini" file is running. + +After you fix the problem, please restart the Pyramid application to +try it again. +""" + diff --git a/docs/tutorials/wiki2/src/tests/README.txt b/docs/tutorials/wiki2/src/tests/README.txt index 6f851e9b7..141851285 100644 --- a/docs/tutorials/wiki2/src/tests/README.txt +++ b/docs/tutorials/wiki2/src/tests/README.txt @@ -1 +1,14 @@ tutorial README +================== + +Getting Started +--------------- + +- cd <directory containing this file> + +- $venv/bin/python setup.py develop + +- $venv/bin/initialize_tutorial_db development.ini + +- $venv/bin/pserve development.ini + diff --git a/docs/tutorials/wiki2/src/tests/development.ini b/docs/tutorials/wiki2/src/tests/development.ini index eb2f878c5..a9d53b296 100644 --- a/docs/tutorials/wiki2/src/tests/development.ini +++ b/docs/tutorials/wiki2/src/tests/development.ini @@ -1,3 +1,8 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html +### + [app:main] use = egg:tutorial @@ -12,12 +17,23 @@ pyramid.includes = sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite +# By default, the toolbar only appears for clients from IP addresses +# '127.0.0.1' and '::1'. +# debugtoolbar.hosts = 127.0.0.1 ::1 + +### +# wsgi server configuration +### + [server:main] use = egg:waitress#main host = 0.0.0.0 port = 6543 -# Begin logging configuration +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +### [loggers] keys = root, tutorial, sqlalchemy @@ -53,5 +69,3 @@ formatter = generic [formatter_generic] format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s - -# End logging configuration diff --git a/docs/tutorials/wiki2/src/tests/setup.py b/docs/tutorials/wiki2/src/tests/setup.py index 0f58d8a18..8a619d27b 100644 --- a/docs/tutorials/wiki2/src/tests/setup.py +++ b/docs/tutorials/wiki2/src/tests/setup.py @@ -24,7 +24,7 @@ setup(name='tutorial', long_description=README + '\n\n' + CHANGES, classifiers=[ "Programming Language :: Python", - "Framework :: Pylons", + "Framework :: Pyramid", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", ], @@ -36,11 +36,12 @@ setup(name='tutorial', include_package_data=True, zip_safe=False, test_suite='tutorial', - install_requires = requires, - entry_points = """\ + install_requires=requires, + entry_points="""\ [paste.app_factory] main = tutorial:main [console_scripts] initialize_tutorial_db = tutorial.scripts.initializedb:main """, ) + diff --git a/docs/tutorials/wiki2/src/tests/tutorial/views.py b/docs/tutorials/wiki2/src/tests/tutorial/views.py index 42ac0eb7f..0d085b0e2 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/views.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/views.py @@ -37,14 +37,13 @@ def view_wiki(request): permission='view') def view_page(request): pagename = request.matchdict['pagename'] - session = DBSession() - page = session.query(Page).filter_by(name=pagename).first() + page = DBSession.query(Page).filter_by(name=pagename).first() if page is None: return HTTPNotFound('No such page') def check(match): word = match.group(1) - exists = session.query(Page).filter_by(name=word).all() + exists = DBSession.query(Page).filter_by(name=word).all() if exists: view_url = request.route_url('view_page', pagename=word) return '<a href="%s">%s</a>' % (view_url, word) @@ -63,10 +62,9 @@ def view_page(request): def add_page(request): pagename = request.matchdict['pagename'] if 'form.submitted' in request.params: - session = DBSession() body = request.params['body'] page = Page(pagename, body) - session.add(page) + DBSession.add(page) return HTTPFound(location = request.route_url('view_page', pagename=pagename)) save_url = request.route_url('add_page', pagename=pagename) @@ -78,11 +76,10 @@ def add_page(request): permission='edit') def edit_page(request): pagename = request.matchdict['pagename'] - session = DBSession() - page = session.query(Page).filter_by(name=pagename).one() + page = DBSession.query(Page).filter_by(name=pagename).one() if 'form.submitted' in request.params: page.data = request.params['body'] - session.add(page) + DBSession.add(page) return HTTPFound(location = request.route_url('view_page', pagename=pagename)) return dict( diff --git a/docs/tutorials/wiki2/src/views/README.txt b/docs/tutorials/wiki2/src/views/README.txt index 6f851e9b7..141851285 100644 --- a/docs/tutorials/wiki2/src/views/README.txt +++ b/docs/tutorials/wiki2/src/views/README.txt @@ -1 +1,14 @@ tutorial README +================== + +Getting Started +--------------- + +- cd <directory containing this file> + +- $venv/bin/python setup.py develop + +- $venv/bin/initialize_tutorial_db development.ini + +- $venv/bin/pserve development.ini + diff --git a/docs/tutorials/wiki2/src/views/development.ini b/docs/tutorials/wiki2/src/views/development.ini index eb2f878c5..a9d53b296 100644 --- a/docs/tutorials/wiki2/src/views/development.ini +++ b/docs/tutorials/wiki2/src/views/development.ini @@ -1,3 +1,8 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html +### + [app:main] use = egg:tutorial @@ -12,12 +17,23 @@ pyramid.includes = sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite +# By default, the toolbar only appears for clients from IP addresses +# '127.0.0.1' and '::1'. +# debugtoolbar.hosts = 127.0.0.1 ::1 + +### +# wsgi server configuration +### + [server:main] use = egg:waitress#main host = 0.0.0.0 port = 6543 -# Begin logging configuration +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +### [loggers] keys = root, tutorial, sqlalchemy @@ -53,5 +69,3 @@ formatter = generic [formatter_generic] format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s - -# End logging configuration diff --git a/docs/tutorials/wiki2/src/views/setup.py b/docs/tutorials/wiki2/src/views/setup.py index 34b578e21..d658cae93 100644 --- a/docs/tutorials/wiki2/src/views/setup.py +++ b/docs/tutorials/wiki2/src/views/setup.py @@ -23,7 +23,7 @@ setup(name='tutorial', long_description=README + '\n\n' + CHANGES, classifiers=[ "Programming Language :: Python", - "Framework :: Pylons", + "Framework :: Pyramid", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", ], @@ -35,8 +35,8 @@ setup(name='tutorial', include_package_data=True, zip_safe=False, test_suite='tutorial', - install_requires = requires, - entry_points = """\ + install_requires=requires, + entry_points="""\ [paste.app_factory] main = tutorial:main [console_scripts] diff --git a/docs/whatsnew-1.3.rst b/docs/whatsnew-1.3.rst index d90897d16..f32053202 100644 --- a/docs/whatsnew-1.3.rst +++ b/docs/whatsnew-1.3.rst @@ -289,13 +289,6 @@ Minor Feature Additions not a new feature, it just provides an API for adding a resource url adapter without needing to use the ZCA API. -- The :meth:`pyramid.config.Configurator.scan` method can now be passed an - ``ignore`` argument, which can be a string, a callable, or a list - consisting of strings and/or callables. This feature allows submodules, - subpackages, and global objects from being scanned. See - http://readthedocs.org/docs/venusian/en/latest/#ignore-scan-argument for - more information about how to use the ``ignore`` argument to ``scan``. - - Better error messages when a view callable returns a value that cannot be converted to a response (for example, when a view callable returns a dictionary without a renderer defined, or doesn't return any value at all). @@ -486,6 +479,11 @@ Deprecations was designed to offer in Pylons. It will continue to exist "forever" but it will not be recommended or mentioned in the docs. +- Remove references to do-nothing ``pyramid.debug_templates`` setting in all + Pyramid-provided .ini files. This setting previously told Chameleon to render + better exceptions; now Chameleon always renders nice exceptions regardless of + the value of this setting. + Known Issues ------------ diff --git a/docs/whatsnew-1.4.rst b/docs/whatsnew-1.4.rst index 59e1f7a96..34fda5f37 100644 --- a/docs/whatsnew-1.4.rst +++ b/docs/whatsnew-1.4.rst @@ -77,6 +77,11 @@ Subrequest Support Minor Feature Additions ----------------------- +- :class:`pyramid.authentication.AuthTktAuthenticationPolicy` has been updated + to support newer hashing algorithms such as ``sha512``. Existing applications + should consider updating if possible for improved security over the default + md5 hashing. + - :meth:`pyramid.config.Configurator.add_directive` now accepts arbitrary callables like partials or objects implementing ``__call__`` which don't have ``__name__`` and ``__doc__`` attributes. See @@ -182,7 +187,6 @@ Minor Feature Additions :meth:`pyramid.config.testing_securitypolicy` now sets a ``forgotten`` value on the policy (the value ``True``) when its ``forget`` method is called. - - The DummySecurityPolicy created by :meth:`pyramid.config.testing_securitypolicy` now sets a ``remembered`` value on the policy, which is the value of the ``principal`` @@ -196,6 +200,48 @@ Minor Feature Additions view when some object is traversed to, but you can't be sure about what kind of object it will be, so you can't use the ``context`` predicate. +- Added an ``effective_principals`` route and view predicate. + +- Do not allow the userid returned from the + :func:`pyramid.security.authenticated_userid` or the userid that is one of the + list of principals returned by :func:`pyramid.security.effective_principals` + to be either of the strings ``system.Everyone`` or ``system.Authenticated`` + when any of the built-in authorization policies that live in + :mod:`pyramid.authentication` are in use. These two strings are reserved for + internal usage by Pyramid and they will no longer be accepted as valid + userids. + +- Allow a ``_depth`` argument to :class:`pyramid.view.view_config`, which will + permit limited composition reuse of the decorator by other software that + wants to provide custom decorators that are much like view_config. + +- Allow an iterable of decorators to be passed to + :meth:`pyramid.config.Configurator.add_view`. This allows views to be wrapped + by more than one decorator without requiring combining the decorators + yourself. + +- :func:`pyramid.security.view_execution_permitted` used to return `True` if no + view could be found. It now raises a :exc:`TypeError` exception in that case, + as it doesn't make sense to assert that a nonexistent view is + execution-permitted. See https://github.com/Pylons/pyramid/issues/299. + +- Small microspeed enhancement which anticipates that a + :class:`pyramid.response.Response` object is likely to be returned from a + view. Some code is shortcut if the class of the object returned by a view is + this class. A similar microoptimization was done to + :func:`pyramid.request.Request.is_response`. + +- Make it possible to use variable arguments on all ``p*`` commands + (``pserve``, ``pshell``, ``pviews``, etc) in the form ``a=1 b=2`` so you can + fill in values in parameterized ``.ini`` file, e.g. ``pshell + etc/development.ini http_port=8080``. + +- In order to allow people to ignore unused arguments to subscriber callables + and to normalize the relationship between event subscribers and subscriber + predicates, we now allow both subscribers and subscriber predicates to accept + only a single ``event`` argument even if they've been subscribed for + notifications that involve multiple interfaces. + Backwards Incompatibilities --------------------------- @@ -289,6 +335,12 @@ Deprecations used in its place (it has all of the same capabilities but can also extend the request object with methods). +- :class:`pyramid.authentication.AuthTktAuthenticationPolicy` will emit a + deprecation warning if an application is using the policy without explicitly + passing a ``hashalg`` argument. This is because the default is "md5" which is + considered theoretically subject to collision attacks. If you really want + "md5" then you must specify it explicitly to get rid of the warning. + Documentation Enhancements -------------------------- @@ -299,6 +351,10 @@ Documentation Enhancements - Added a :ref:`subrequest_chapter` chapter to the narrative documentation. +- All of the tutorials that use + :class:`pyramid.authentication.AuthTktAuthenticationPolicy` now explicitly + pass ``sha512`` as a ``hashalg`` argument. + - Many cleanups and improvements to narrative and API docs. Dependency Changes diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index 1dc438597..40edaa324 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -70,16 +70,17 @@ from pyramid.config.security import SecurityConfiguratorMixin from pyramid.config.settings import SettingsConfiguratorMixin from pyramid.config.testing import TestingConfiguratorMixin from pyramid.config.tweens import TweensConfiguratorMixin -from pyramid.config.util import ( - action_method, - ActionInfo, - PredicateList, - ) +from pyramid.config.util import PredicateList from pyramid.config.views import ViewsConfiguratorMixin from pyramid.config.zca import ZCAConfiguratorMixin from pyramid.path import DottedNameResolver +from pyramid.util import ( + action_method, + ActionInfo, + ) + empty = text_('') _marker = object() diff --git a/pyramid/config/adapters.py b/pyramid/config/adapters.py index 12c4de660..dad60660a 100644 --- a/pyramid/config/adapters.py +++ b/pyramid/config/adapters.py @@ -10,6 +10,7 @@ from pyramid.interfaces import ( from pyramid.config.util import ( action_method, + takes_one_arg, ) @@ -51,8 +52,22 @@ class AdaptersConfiguratorMixin(object): def register(): 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) + + derived_predicates = [ self._derive_predicate(p) for p in preds ] + derived_subscriber = self._derive_subscriber( + subscriber, + derived_predicates, + ) + + intr.update( + {'phash':phash, + 'order':order, + 'predicates':preds, + 'derived_predicates':derived_predicates, + 'derived_subscriber':derived_subscriber, + } + ) + self.registry.registerHandler(derived_subscriber, iface) intr = self.introspectable( @@ -68,25 +83,54 @@ class AdaptersConfiguratorMixin(object): self.action(None, register, introspectables=(intr,)) return subscriber + def _derive_predicate(self, predicate): + derived_predicate = predicate + + if eventonly(predicate): + def derived_predicate(*arg): + return predicate(arg[0]) + # seems pointless to try to fix __doc__, __module__, etc as + # predicate will invariably be an instance + + return derived_predicate + def _derive_subscriber(self, subscriber, predicates): + derived_subscriber = subscriber + + if eventonly(subscriber): + def derived_subscriber(*arg): + return subscriber(arg[0]) + if hasattr(subscriber, '__name__'): + update_wrapper(derived_subscriber, subscriber) + if not predicates: - return subscriber + return derived_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. + # We need to accept *arg and pass it along because zope subscribers + # are designed awkwardly. Notification via + # registry.adapter.subscribers 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 + # almost always does. + # + # The "eventonly" jazz sprinkled in this function and related + # functions allows users to define subscribers and predicates which + # accept only an event argument without needing to accept the rest + # of the adaptation arguments. Had I been smart enough early on to + # use .subscriptions to find the subscriber functions in order to + # call them manually with a single "event" argument instead of + # relying on .subscribers to both find and call them implicitly + # with all args, the eventonly hack would not have been required. + # At this point, though, using .subscriptions and manual execution + # is not possible without badly breaking backwards compatibility. if all((predicate(*arg) for predicate in predicates)): - return subscriber(*arg) + return derived_subscriber(*arg) + if hasattr(subscriber, '__name__'): update_wrapper(subscriber_wrapper, subscriber) + return subscriber_wrapper @action_method @@ -266,7 +310,7 @@ class AdaptersConfiguratorMixin(object): if resource_iface is None: resource_iface = Interface self.registry.registerAdapter( - adapter, + adapter, (resource_iface, Interface), IResourceURL, ) @@ -281,3 +325,5 @@ class AdaptersConfiguratorMixin(object): intr['resource_iface'] = resource_iface self.action(discriminator, register, introspectables=(intr,)) +def eventonly(callee): + return takes_one_arg(callee, argname='event') diff --git a/pyramid/config/assets.py b/pyramid/config/assets.py index c93431987..5d4682349 100644 --- a/pyramid/config/assets.py +++ b/pyramid/config/assets.py @@ -8,7 +8,7 @@ from pyramid.interfaces import IPackageOverrides from pyramid.exceptions import ConfigurationError from pyramid.threadlocal import get_current_registry -from pyramid.config.util import action_method +from pyramid.util import action_method class OverrideProvider(pkg_resources.DefaultProvider): def __init__(self, module): diff --git a/pyramid/config/factories.py b/pyramid/config/factories.py index 01b1fb22e..ef7975d92 100644 --- a/pyramid/config/factories.py +++ b/pyramid/config/factories.py @@ -1,7 +1,5 @@ from zope.interface import implementer -from pyramid.config.util import action_method - from pyramid.interfaces import ( IDefaultRootFactory, IRequestFactory, @@ -11,7 +9,11 @@ from pyramid.interfaces import ( ) from pyramid.traversal import DefaultRootFactory -from pyramid.util import InstancePropertyMixin + +from pyramid.util import ( + action_method, + InstancePropertyMixin, + ) class FactoriesConfiguratorMixin(object): @action_method diff --git a/pyramid/config/i18n.py b/pyramid/config/i18n.py index 67a7e2018..9eb59e1c7 100644 --- a/pyramid/config/i18n.py +++ b/pyramid/config/i18n.py @@ -13,8 +13,7 @@ from pyramid.exceptions import ConfigurationError from pyramid.i18n import get_localizer from pyramid.path import package_path from pyramid.threadlocal import get_current_request - -from pyramid.config.util import action_method +from pyramid.util import action_method class I18NConfiguratorMixin(object): @action_method diff --git a/pyramid/config/predicates.py b/pyramid/config/predicates.py index e31425899..ded8fbfbf 100644 --- a/pyramid/config/predicates.py +++ b/pyramid/config/predicates.py @@ -17,6 +17,8 @@ from pyramid.security import effective_principals from .util import as_sorted_tuple +_marker = object() + class XHRPredicate(object): def __init__(self, val, config): self.val = bool(val) @@ -174,6 +176,9 @@ class MatchParamPredicate(object): phash = text def __call__(self, context, request): + if not request.matchdict: + # might be None + return False for k, v in self.reqs: if request.matchdict.get(k) != v: return False @@ -266,7 +271,9 @@ class PhysicalPathPredicate(object): phash = text def __call__(self, context, request): - return resource_path_tuple(context) == self.val + if getattr(context, '__name__', _marker) is not _marker: + return resource_path_tuple(context) == self.val + return False class EffectivePrincipalsPredicate(object): def __init__(self, val, config): diff --git a/pyramid/config/rendering.py b/pyramid/config/rendering.py index 926511b7b..4f33b23d9 100644 --- a/pyramid/config/rendering.py +++ b/pyramid/config/rendering.py @@ -6,7 +6,7 @@ from pyramid.interfaces import ( PHASE1_CONFIG, ) -from pyramid.config.util import action_method +from pyramid.util import action_method from pyramid import ( renderers, diff --git a/pyramid/config/security.py b/pyramid/config/security.py index 567999cc4..6a1257b6a 100644 --- a/pyramid/config/security.py +++ b/pyramid/config/security.py @@ -7,7 +7,7 @@ from pyramid.interfaces import ( ) from pyramid.exceptions import ConfigurationError -from pyramid.config.util import action_method +from pyramid.util import action_method class SecurityConfiguratorMixin(object): @action_method diff --git a/pyramid/config/testing.py b/pyramid/config/testing.py index abbbffc10..7141a5049 100644 --- a/pyramid/config/testing.py +++ b/pyramid/config/testing.py @@ -14,7 +14,7 @@ from pyramid.traversal import ( split_path_info, ) -from pyramid.config.util import action_method +from pyramid.util import action_method class TestingConfiguratorMixin(object): # testing API diff --git a/pyramid/config/util.py b/pyramid/config/util.py index 1c6e1ca15..af0dd1641 100644 --- a/pyramid/config/util.py +++ b/pyramid/config/util.py @@ -1,68 +1,27 @@ -import traceback - -from functools import update_wrapper - -from zope.interface import implementer - -from pyramid.interfaces import IActionInfo +from hashlib import md5 +import inspect from pyramid.compat import ( bytes_, is_nonstr_iter, ) +from pyramid.compat import im_func from pyramid.exceptions import ConfigurationError from pyramid.registry import predvalseq -from pyramid.util import TopologicalSorter -from hashlib import md5 +from pyramid.util import ( + TopologicalSorter, + action_method, + ActionInfo, + ) + +action_method = action_method # support bw compat imports +ActionInfo = ActionInfo # support bw compat imports MAX_ORDER = 1 << 30 DEFAULT_PHASH = md5().hexdigest() -@implementer(IActionInfo) -class ActionInfo(object): - def __init__(self, file, line, function, src): - self.file = file - self.line = line - self.function = function - self.src = src - - def __str__(self): - srclines = self.src.split('\n') - src = '\n'.join(' %s' % x for x in srclines) - return 'Line %s of file %s:\n%s' % (self.line, self.file, src) - -def action_method(wrapped): - """ Wrapper to provide the right conflict info report data when a method - that calls Configurator.action calls another that does the same""" - def wrapper(self, *arg, **kw): - if self._ainfo is None: - self._ainfo = [] - info = kw.pop('_info', None) - # backframes for outer decorators to actionmethods - backframes = kw.pop('_backframes', 2) - if is_nonstr_iter(info) and len(info) == 4: - # _info permitted as extract_stack tuple - info = ActionInfo(*info) - if info is None: - try: - f = traceback.extract_stack(limit=3) - info = ActionInfo(*f[-backframes]) - except: # pragma: no cover - info = ActionInfo(None, 0, '', '') - self._ainfo.append(info) - try: - result = wrapped(self, *arg, **kw) - finally: - self._ainfo.pop() - return result - - if hasattr(wrapped, '__name__'): - update_wrapper(wrapper, wrapped) - wrapper.__docobj__ = wrapped - return wrapper - def as_sorted_tuple(val): if not is_nonstr_iter(val): val = (val,) @@ -85,8 +44,12 @@ class PredicateList(object): ## 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) + 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 @@ -149,3 +112,51 @@ class PredicateList(object): order = (MAX_ORDER - score) / (len(preds) + 1) return order, preds, phash.hexdigest() +def takes_one_arg(callee, attr=None, argname=None): + ismethod = False + if attr is None: + attr = '__call__' + if inspect.isroutine(callee): + fn = callee + elif inspect.isclass(callee): + try: + fn = callee.__init__ + except AttributeError: + return False + ismethod = hasattr(fn, '__call__') + else: + try: + fn = getattr(callee, attr) + except AttributeError: + return False + + try: + argspec = inspect.getargspec(fn) + except TypeError: + return False + + args = argspec[0] + + if hasattr(fn, im_func) or ismethod: + # it's an instance method (or unbound method on py2) + if not args: + return False + args = args[1:] + + if not args: + return False + + if len(args) == 1: + return True + + if argname: + + defaults = argspec[3] + if defaults is None: + defaults = () + + if args[0] == argname: + if len(args) - len(defaults) == 1: + return True + + return False diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 8a4db149e..d1b69566b 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1,7 +1,6 @@ import inspect import operator import os -from functools import wraps from zope.interface import ( Interface, @@ -60,6 +59,8 @@ from pyramid.registry import ( Deferred, ) +from pyramid.response import Response + from pyramid.security import NO_PERMISSION_REQUIRED from pyramid.static import static_view from pyramid.threadlocal import get_current_registry @@ -71,6 +72,8 @@ from pyramid.view import ( from pyramid.util import ( object_description, + viewdefaults, + action_method, ) import pyramid.config.predicates @@ -78,7 +81,7 @@ import pyramid.config.predicates from pyramid.config.util import ( DEFAULT_PHASH, MAX_ORDER, - action_method, + takes_one_arg, ) urljoin = urlparse.urljoin @@ -341,25 +344,28 @@ class ViewDeriver(object): def rendered_view(context, request): renderer = view_renderer result = view(context, request) - registry = self.registry - # this must adapt, it can't do a simple interface check - # (avoid trying to render webob responses) - response = registry.queryAdapterOrSelf(result, IResponse) - if response is None: - attrs = getattr(request, '__dict__', {}) - if 'override_renderer' in attrs: - # renderer overridden by newrequest event or other - renderer_name = attrs.pop('override_renderer') - renderer = renderers.RendererHelper( - name=renderer_name, - package=self.kw.get('package'), - registry = registry) - if '__view__' in attrs: - view_inst = attrs.pop('__view__') - else: - view_inst = getattr(view, '__original_view__', view) - response = renderer.render_view(request, result, view_inst, - context) + if result.__class__ is Response: # potential common case + response = result + else: + registry = self.registry + # this must adapt, it can't do a simple interface check + # (avoid trying to render webob responses) + response = registry.queryAdapterOrSelf(result, IResponse) + if response is None: + attrs = getattr(request, '__dict__', {}) + if 'override_renderer' in attrs: + # renderer overridden by newrequest event or other + renderer_name = attrs.pop('override_renderer') + renderer = renderers.RendererHelper( + name=renderer_name, + package=self.kw.get('package'), + registry = registry) + if '__view__' in attrs: + view_inst = attrs.pop('__view__') + else: + view_inst = getattr(view, '__original_view__', view) + response = renderer.render_view(request, result, view_inst, + context) return response return rendered_view @@ -368,21 +374,26 @@ class ViewDeriver(object): registry = self.registry def viewresult_to_response(context, request): result = view(context, request) - response = registry.queryAdapterOrSelf(result, IResponse) - if response is None: - if result is None: - append = (' You may have forgotten to return a value from ' - 'the view callable.') - elif isinstance(result, dict): - append = (' You may have forgotten to define a renderer in ' - 'the view configuration.') - else: - append = '' - msg = ('Could not convert return value of the view callable %s ' - 'into a response object. ' - 'The value returned was %r.' + append) - - raise ValueError(msg % (view_description(view), result)) + if result.__class__ is Response: # common case + response = result + else: + response = registry.queryAdapterOrSelf(result, IResponse) + if response is None: + if result is None: + append = (' You may have forgotten to return a value ' + 'from the view callable.') + elif isinstance(result, dict): + append = (' You may have forgotten to define a ' + 'renderer in the view configuration.') + else: + append = '' + + msg = ('Could not convert return value of the view ' + 'callable %s into a response object. ' + 'The value returned was %r.' + append) + + raise ValueError(msg % (view_description(view), result)) + return response return viewresult_to_response @@ -493,50 +504,7 @@ class DefaultViewMapper(object): return _attr_view def requestonly(view, attr=None): - ismethod = False - if attr is None: - attr = '__call__' - if inspect.isroutine(view): - fn = view - elif inspect.isclass(view): - try: - fn = view.__init__ - except AttributeError: - return False - ismethod = hasattr(fn, '__call__') - else: - try: - fn = getattr(view, attr) - except AttributeError: - return False - - try: - argspec = inspect.getargspec(fn) - except TypeError: - return False - - args = argspec[0] - - if hasattr(fn, im_func) or ismethod: - # it's an instance method (or unbound method on py2) - if not args: - return False - args = args[1:] - if not args: - return False - - if len(args) == 1: - return True - - defaults = argspec[3] - if defaults is None: - defaults = () - - if args[0] == 'request': - if len(args) - len(defaults) == 1: - return True - - return False + return takes_one_arg(view, attr=attr, argname='request') @implementer(IMultiView) class MultiView(object): @@ -621,21 +589,6 @@ class MultiView(object): continue raise PredicateMismatch(self.name) -def viewdefaults(wrapped): - def wrapper(self, *arg, **kw): - defaults = {} - if arg: - view = arg[0] - else: - view = kw.get('view') - view = self.maybe_dotted(view) - if inspect.isclass(view): - defaults = getattr(view, '__view_defaults__', {}).copy() - defaults.update(kw) - defaults['_backframes'] = 3 # for action_method - return wrapped(self, *arg, **defaults) - return wraps(wrapped)(wrapper) - class ViewsConfiguratorMixin(object): @viewdefaults @action_method diff --git a/pyramid/paster.py b/pyramid/paster.py index b0e4d7933..ce07d1fe0 100644 --- a/pyramid/paster.py +++ b/pyramid/paster.py @@ -9,17 +9,27 @@ from pyramid.compat import configparser from logging.config import fileConfig from pyramid.scripting import prepare -def get_app(config_uri, name=None, loadapp=loadapp): +def get_app(config_uri, name=None, options=None, loadapp=loadapp): """ Return the WSGI application named ``name`` in the PasteDeploy config file specified by ``config_uri``. + ``options``, if passed, should be a dictionary used as variable assignments + like ``{'http_port': 8080}``. This is useful if e.g. ``%(http_port)s`` is + used in the config file. + If the ``name`` is None, this will attempt to parse the name from the ``config_uri`` string expecting the format ``inifile#name``. If no name is found, the name will default to "main".""" path, section = _getpathsec(config_uri, name) config_name = 'config:%s' % path here_dir = os.getcwd() - app = loadapp(config_name, name=section, relative_to=here_dir) + if options: + kw = {'global_conf': options} + else: + kw = {} + + app = loadapp(config_name, name=section, relative_to=here_dir, **kw) + return app def get_appsettings(config_uri, name=None, appconfig=appconfig): @@ -63,7 +73,7 @@ def _getpathsec(config_uri, name): section = name return path, section -def bootstrap(config_uri, request=None): +def bootstrap(config_uri, request=None, options=None): """ Load a WSGI application from the PasteDeploy config file specified by ``config_uri``. The environment will be configured as if it is currently serving ``request``, leaving a natural environment in place @@ -103,10 +113,14 @@ def bootstrap(config_uri, request=None): for you if none is provided. You can mutate the request's ``environ`` later to setup a specific host/port/scheme/etc. + ``options`` Is passed to get_app for use as variable assignments like + {'http_port': 8080} and then use %(http_port)s in the + config file. + See :ref:`writing_a_script` for more information about how to use this function. """ - app = get_app(config_uri) + app = get_app(config_uri, options=options) env = prepare(request) env['app'] = app return env diff --git a/pyramid/request.py b/pyramid/request.py index 9e275c2c0..27ab337de 100644 --- a/pyramid/request.py +++ b/pyramid/request.py @@ -372,6 +372,8 @@ class Request(BaseRequest, DeprecatedRequestMethodsMixin, URLMethodsMixin, def is_response(self, ob): """ Return ``True`` if the object passed as ``ob`` is a valid response object, ``False`` otherwise.""" + if ob.__class__ is Response: + return True registry = self.registry adapted = registry.queryAdapterOrSelf(ob, IResponse) if adapted is None: diff --git a/pyramid/router.py b/pyramid/router.py index 0c7f61071..9b6138ea9 100644 --- a/pyramid/router.py +++ b/pyramid/router.py @@ -103,7 +103,7 @@ class Router(object): request.path_info, route.pattern, match, - ', '.join([p.__text__ for p in route.predicates])) + ', '.join([p.text() for p in route.predicates])) ) logger and logger.debug(msg) diff --git a/pyramid/scripts/__init__.py b/pyramid/scripts/__init__.py index ed88d78b4..5bb534f79 100644 --- a/pyramid/scripts/__init__.py +++ b/pyramid/scripts/__init__.py @@ -1,2 +1 @@ # package - diff --git a/pyramid/scripts/common.py b/pyramid/scripts/common.py index dfff26449..cbc172e9b 100644 --- a/pyramid/scripts/common.py +++ b/pyramid/scripts/common.py @@ -2,6 +2,21 @@ import os from pyramid.compat import configparser from logging.config import fileConfig +def parse_vars(args): + """ + Given variables like ``['a=b', 'c=d']`` turns it into ``{'a': + 'b', 'c': 'd'}`` + """ + result = {} + for arg in args: + if '=' not in arg: + raise ValueError( + 'Variable assignment %r invalid (no "=")' + % arg) + name, value = arg.split('=', 1) + result[name] = value + return result + def logging_file_config(config_file, fileConfig=fileConfig, configparser=configparser): """ diff --git a/pyramid/scripts/prequest.py b/pyramid/scripts/prequest.py index 13406372a..da6b8cc14 100644 --- a/pyramid/scripts/prequest.py +++ b/pyramid/scripts/prequest.py @@ -5,6 +5,7 @@ import textwrap from pyramid.compat import url_unquote from pyramid.request import Request from pyramid.paster import get_app +from pyramid.scripts.common import parse_vars def main(argv=sys.argv, quiet=False): command = PRequestCommand(argv, quiet) @@ -101,7 +102,9 @@ class PRequestCommand(object): name, value = item.split(':', 1) headers[name] = value.strip() - app = self.get_app(app_spec, self.options.app_name) + app = self.get_app(app_spec, self.options.app_name, + options=parse_vars(self.args[2:])) + request_method = (self.options.method or 'GET').upper() environ = { diff --git a/pyramid/scripts/proutes.py b/pyramid/scripts/proutes.py index f64107d2b..49e19deca 100644 --- a/pyramid/scripts/proutes.py +++ b/pyramid/scripts/proutes.py @@ -3,6 +3,7 @@ import sys import textwrap from pyramid.paster import bootstrap +from pyramid.scripts.common import parse_vars def main(argv=sys.argv, quiet=False): command = PRoutesCommand(argv, quiet) @@ -47,12 +48,14 @@ class PRoutesCommand(object): if not self.args: self.out('requires a config file argument') return 2 + from pyramid.interfaces import IRouteRequest from pyramid.interfaces import IViewClassifier from pyramid.interfaces import IView from zope.interface import Interface config_uri = self.args[0] - env = self.bootstrap[0](config_uri) + + env = self.bootstrap[0](config_uri, options=parse_vars(self.args[1:])) registry = env['registry'] mapper = self._get_mapper(registry) if mapper is not None: diff --git a/pyramid/scripts/pserve.py b/pyramid/scripts/pserve.py index 9fbf0729a..6c7e1d654 100644 --- a/pyramid/scripts/pserve.py +++ b/pyramid/scripts/pserve.py @@ -22,12 +22,15 @@ import threading import time import traceback -from paste.deploy import loadapp, loadserver +from paste.deploy import loadserver +from paste.deploy import loadapp from pyramid.compat import WIN from pyramid.paster import setup_logging +from pyramid.scripts.common import parse_vars + MAXFD = 1024 if WIN and not hasattr(os, 'kill'): # pragma: no cover @@ -160,6 +163,15 @@ class PServeCommand(object): if not self.quiet: print(msg) + def get_options(self): + if (len(self.args) > 1 + and self.args[1] in self.possible_subcommands): + restvars = self.args[2:] + else: + restvars = self.args[1:] + + return parse_vars(restvars) + def run(self): # pragma: no cover if self.options.stop_daemon: return self.stop_daemon() @@ -176,13 +188,12 @@ class PServeCommand(object): self.out('You must give a config file') return 2 app_spec = self.args[0] + if (len(self.args) > 1 and self.args[1] in self.possible_subcommands): cmd = self.args[1] - restvars = self.args[2:] else: cmd = None - restvars = self.args[1:] if self.options.reload: if os.environ.get(self._reloader_environ_key): @@ -218,7 +229,9 @@ class PServeCommand(object): self.options.daemon = True app_name = self.options.app_name - vars = self.parse_vars(restvars) + + vars = self.get_options() + if not self._scheme_re.search(app_spec): app_spec = 'config:' + app_spec server_name = self.options.server_name @@ -286,8 +299,9 @@ class PServeCommand(object): server = self.loadserver(server_spec, name=server_name, relative_to=base, global_conf=vars) - app = self.loadapp(app_spec, name=app_name, - relative_to=base, global_conf=vars) + + app = self.loadapp(app_spec, name=app_name, relative_to=base, + global_conf=vars) if self.verbose > 0: if hasattr(os, 'getpid'): @@ -310,27 +324,12 @@ class PServeCommand(object): serve() - def loadserver(self, server_spec, name, relative_to, **kw):# pragma:no cover - return loadserver( - server_spec, name=name, relative_to=relative_to, **kw) - def loadapp(self, app_spec, name, relative_to, **kw): # pragma: no cover return loadapp(app_spec, name=name, relative_to=relative_to, **kw) - def parse_vars(self, args): - """ - Given variables like ``['a=b', 'c=d']`` turns it into ``{'a': - 'b', 'c': 'd'}`` - """ - result = {} - for arg in args: - if '=' not in arg: - raise ValueError( - 'Variable assignment %r invalid (no "=")' - % arg) - name, value = arg.split('=', 1) - result[name] = value - return result + def loadserver(self, server_spec, name, relative_to, **kw):# pragma:no cover + return loadserver( + server_spec, name=name, relative_to=relative_to, **kw) def quote_first_command_arg(self, arg): # pragma: no cover """ diff --git a/pyramid/scripts/pshell.py b/pyramid/scripts/pshell.py index 7a21eaf98..f74402928 100644 --- a/pyramid/scripts/pshell.py +++ b/pyramid/scripts/pshell.py @@ -9,6 +9,8 @@ from pyramid.paster import bootstrap from pyramid.paster import setup_logging +from pyramid.scripts.common import parse_vars + def main(argv=sys.argv, quiet=False): command = PShellCommand(argv, quiet) return command.run() @@ -87,7 +89,7 @@ class PShellCommand(object): self.pshell_file_config(config_file) # bootstrap the environ - env = self.bootstrap[0](config_uri) + env = self.bootstrap[0](config_uri, options=parse_vars(self.args[1:])) # remove the closer from the env closer = env.pop('closer') diff --git a/pyramid/scripts/ptweens.py b/pyramid/scripts/ptweens.py index d3e17db58..5fe2fa120 100644 --- a/pyramid/scripts/ptweens.py +++ b/pyramid/scripts/ptweens.py @@ -7,6 +7,7 @@ from pyramid.interfaces import ITweens from pyramid.tweens import MAIN from pyramid.tweens import INGRESS from pyramid.paster import bootstrap +from pyramid.scripts.common import parse_vars def main(argv=sys.argv, quiet=False): command = PTweensCommand(argv, quiet) @@ -62,7 +63,7 @@ class PTweensCommand(object): self.out('Requires a config file argument') return 2 config_uri = self.args[0] - env = self.bootstrap[0](config_uri) + env = self.bootstrap[0](config_uri, options=parse_vars(self.args[1:])) registry = env['registry'] tweens = self._get_tweens(registry) if tweens is not None: diff --git a/pyramid/scripts/pviews.py b/pyramid/scripts/pviews.py index a9db59dc1..081c13e9d 100644 --- a/pyramid/scripts/pviews.py +++ b/pyramid/scripts/pviews.py @@ -4,6 +4,7 @@ import textwrap from pyramid.interfaces import IMultiView from pyramid.paster import bootstrap +from pyramid.scripts.common import parse_vars def main(argv=sys.argv, quiet=False): command = PViewsCommand(argv, quiet) @@ -187,7 +188,7 @@ class PViewsCommand(object): self.out("%sroute pattern: %s" % (indent, route.pattern)) self.out("%sroute path: %s" % (indent, route.path)) self.out("%ssubpath: %s" % (indent, '/'.join(attrs['subpath']))) - predicates = ', '.join([p.__text__ for p in route.predicates]) + predicates = ', '.join([p.text() for p in route.predicates]) if predicates != '': self.out("%sroute predicates (%s)" % (indent, predicates)) @@ -230,10 +231,12 @@ class PViewsCommand(object): if len(self.args) < 2: self.out('Command requires a config file arg and a url arg') return 2 - config_uri, url = self.args + config_uri = self.args[0] + url = self.args[1] + if not url.startswith('/'): url = '/%s' % url - env = self.bootstrap[0](config_uri) + env = self.bootstrap[0](config_uri, options=parse_vars(self.args[2:])) registry = env['registry'] view = self._find_view(url, registry) self.out('') diff --git a/pyramid/tests/pkgs/eventonly/__init__.py b/pyramid/tests/pkgs/eventonly/__init__.py new file mode 100644 index 000000000..7ae93ada6 --- /dev/null +++ b/pyramid/tests/pkgs/eventonly/__init__.py @@ -0,0 +1,64 @@ +from pyramid.view import view_config +from pyramid.events import subscriber + +class Yup(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 getattr(event.response, 'yup', False) + +class Foo(object): + def __init__(self, response): + self.response = response + +class Bar(object): + pass + +@subscriber(Foo) +def foo(event): + event.response.text += 'foo ' + +@subscriber(Foo, yup=True) +def fooyup(event): + event.response.text += 'fooyup ' + +@subscriber([Foo, Bar]) +def foobar(event): + event.response.text += 'foobar ' + +@subscriber([Foo, Bar]) +def foobar2(event, context): + event.response.text += 'foobar2 ' + +@subscriber([Foo, Bar], yup=True) +def foobaryup(event): + event.response.text += 'foobaryup ' + +@subscriber([Foo, Bar], yup=True) +def foobaryup2(event, context): + event.response.text += 'foobaryup2 ' + +@view_config(name='sendfoo') +def sendfoo(request): + response = request.response + response.yup = True + request.registry.notify(Foo(response)) + return response + +@view_config(name='sendfoobar') +def sendfoobar(request): + response = request.response + response.yup = True + request.registry.notify(Foo(response), Bar()) + return response + +def includeme(config): + config.add_subscriber_predicate('yup', Yup) + config.scan('pyramid.tests.pkgs.eventonly') + diff --git a/pyramid/tests/test_config/test_adapters.py b/pyramid/tests/test_config/test_adapters.py index d47e012dc..4cbb1bf80 100644 --- a/pyramid/tests/test_config/test_adapters.py +++ b/pyramid/tests/test_config/test_adapters.py @@ -331,6 +331,15 @@ class AdaptersConfiguratorMixinTests(unittest.TestCase): self.assertEqual(intr['adapter'], DummyResourceURL) self.assertEqual(intr['resource_iface'], DummyIface) +class Test_eventonly(unittest.TestCase): + def _callFUT(self, callee): + from pyramid.config.adapters import eventonly + return eventonly(callee) + + def test_defaults(self): + def acallable(event, a=1, b=2): pass + self.assertTrue(self._callFUT(acallable)) + class DummyTraverser(object): def __init__(self, root): self.root = root diff --git a/pyramid/tests/test_config/test_init.py b/pyramid/tests/test_config/test_init.py index 2cf9a269a..7c2880a18 100644 --- a/pyramid/tests/test_config/test_init.py +++ b/pyramid/tests/test_config/test_init.py @@ -772,7 +772,7 @@ pyramid.tests.test_config.dummy_include2""", self.assertEqual(config.action('discrim', kw={'a':1}), None) def test_action_autocommit_with_introspectables(self): - from pyramid.config.util import ActionInfo + from pyramid.util import ActionInfo config = self._makeOne(autocommit=True) intr = DummyIntrospectable() config.action('discrim', introspectables=(intr,)) diff --git a/pyramid/tests/test_config/test_predicates.py b/pyramid/tests/test_config/test_predicates.py index 91dfb0fb6..1cd6050bf 100644 --- a/pyramid/tests/test_config/test_predicates.py +++ b/pyramid/tests/test_config/test_predicates.py @@ -187,6 +187,13 @@ class TestMatchParamPredicate(unittest.TestCase): result = inst(None, request) self.assertFalse(result) + def test___call___matchdict_is_None(self): + inst = self._makeOne('abc=1') + request = Dummy() + request.matchdict = None + 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') @@ -436,6 +443,11 @@ class Test_PhysicalPathPredicate(unittest.TestCase): context.__parent__ = root self.assertFalse(inst(context, None)) + def test_it_call_context_has_no_name(self): + inst = self._makeOne('/', None) + context = Dummy() + self.assertFalse(inst(context, None)) + class Test_EffectivePrincipalsPredicate(unittest.TestCase): def setUp(self): self.config = testing.setUp() diff --git a/pyramid/tests/test_config/test_util.py b/pyramid/tests/test_config/test_util.py index 8c3cd7455..a984acfd0 100644 --- a/pyramid/tests/test_config/test_util.py +++ b/pyramid/tests/test_config/test_util.py @@ -365,36 +365,192 @@ class TestPredicateList(unittest.TestCase): from pyramid.exceptions import ConfigurationError self.assertRaises(ConfigurationError, self._callFUT, unknown=1) +class Test_takes_one_arg(unittest.TestCase): + def _callFUT(self, view, attr=None, argname=None): + from pyramid.config.util import takes_one_arg + return takes_one_arg(view, attr=attr, argname=argname) + + def test_requestonly_newstyle_class_no_init(self): + class foo(object): + """ """ + self.assertFalse(self._callFUT(foo)) + + def test_requestonly_newstyle_class_init_toomanyargs(self): + class foo(object): + def __init__(self, context, request): + """ """ + self.assertFalse(self._callFUT(foo)) + + def test_requestonly_newstyle_class_init_onearg_named_request(self): + class foo(object): + def __init__(self, request): + """ """ + self.assertTrue(self._callFUT(foo)) + + def test_newstyle_class_init_onearg_named_somethingelse(self): + class foo(object): + def __init__(self, req): + """ """ + self.assertTrue(self._callFUT(foo)) + + def test_newstyle_class_init_defaultargs_firstname_not_request(self): + class foo(object): + def __init__(self, context, request=None): + """ """ + self.assertFalse(self._callFUT(foo)) + + def test_newstyle_class_init_defaultargs_firstname_request(self): + class foo(object): + def __init__(self, request, foo=1, bar=2): + """ """ + self.assertTrue(self._callFUT(foo, argname='request')) + + def test_newstyle_class_init_firstname_request_with_secondname(self): + class foo(object): + def __init__(self, request, two): + """ """ + self.assertFalse(self._callFUT(foo)) + + def test_newstyle_class_init_noargs(self): + class foo(object): + def __init__(): + """ """ + self.assertFalse(self._callFUT(foo)) + + def test_oldstyle_class_no_init(self): + class foo: + """ """ + self.assertFalse(self._callFUT(foo)) + + def test_oldstyle_class_init_toomanyargs(self): + class foo: + def __init__(self, context, request): + """ """ + self.assertFalse(self._callFUT(foo)) + + def test_oldstyle_class_init_onearg_named_request(self): + class foo: + def __init__(self, request): + """ """ + self.assertTrue(self._callFUT(foo)) + + def test_oldstyle_class_init_onearg_named_somethingelse(self): + class foo: + def __init__(self, req): + """ """ + self.assertTrue(self._callFUT(foo)) + + def test_oldstyle_class_init_defaultargs_firstname_not_request(self): + class foo: + def __init__(self, context, request=None): + """ """ + self.assertFalse(self._callFUT(foo)) + + def test_oldstyle_class_init_defaultargs_firstname_request(self): + class foo: + def __init__(self, request, foo=1, bar=2): + """ """ + self.assertTrue(self._callFUT(foo, argname='request'), True) + + def test_oldstyle_class_init_noargs(self): + class foo: + def __init__(): + """ """ + self.assertFalse(self._callFUT(foo)) + + def test_function_toomanyargs(self): + def foo(context, request): + """ """ + self.assertFalse(self._callFUT(foo)) + + def test_function_with_attr_false(self): + def bar(context, request): + """ """ + def foo(context, request): + """ """ + foo.bar = bar + self.assertFalse(self._callFUT(foo, 'bar')) + + def test_function_with_attr_true(self): + def bar(context, request): + """ """ + def foo(request): + """ """ + foo.bar = bar + self.assertTrue(self._callFUT(foo, 'bar')) + + def test_function_onearg_named_request(self): + def foo(request): + """ """ + self.assertTrue(self._callFUT(foo)) + + def test_function_onearg_named_somethingelse(self): + def foo(req): + """ """ + self.assertTrue(self._callFUT(foo)) + + def test_function_defaultargs_firstname_not_request(self): + def foo(context, request=None): + """ """ + self.assertFalse(self._callFUT(foo)) + + def test_function_defaultargs_firstname_request(self): + def foo(request, foo=1, bar=2): + """ """ + self.assertTrue(self._callFUT(foo, argname='request')) + + def test_function_noargs(self): + def foo(): + """ """ + self.assertFalse(self._callFUT(foo)) + + def test_instance_toomanyargs(self): + class Foo: + def __call__(self, context, request): + """ """ + foo = Foo() + self.assertFalse(self._callFUT(foo)) + + def test_instance_defaultargs_onearg_named_request(self): + class Foo: + def __call__(self, request): + """ """ + foo = Foo() + self.assertTrue(self._callFUT(foo)) + + def test_instance_defaultargs_onearg_named_somethingelse(self): + class Foo: + def __call__(self, req): + """ """ + foo = Foo() + self.assertTrue(self._callFUT(foo)) + + def test_instance_defaultargs_firstname_not_request(self): + class Foo: + def __call__(self, context, request=None): + """ """ + foo = Foo() + self.assertFalse(self._callFUT(foo)) + + def test_instance_defaultargs_firstname_request(self): + class Foo: + def __call__(self, request, foo=1, bar=2): + """ """ + foo = Foo() + self.assertTrue(self._callFUT(foo, argname='request'), True) + + def test_instance_nocall(self): + class Foo: pass + foo = Foo() + self.assertFalse(self._callFUT(foo)) + + def test_method_onearg_named_request(self): + class Foo: + def method(self, request): + """ """ + foo = Foo() + self.assertTrue(self._callFUT(foo.method)) -class TestActionInfo(unittest.TestCase): - def _getTargetClass(self): - from pyramid.config.util import ActionInfo - return ActionInfo - - def _makeOne(self, filename, lineno, function, linerepr): - return self._getTargetClass()(filename, lineno, function, linerepr) - - def test_class_conforms(self): - from zope.interface.verify import verifyClass - from pyramid.interfaces import IActionInfo - verifyClass(IActionInfo, self._getTargetClass()) - - def test_instance_conforms(self): - from zope.interface.verify import verifyObject - from pyramid.interfaces import IActionInfo - verifyObject(IActionInfo, self._makeOne('f', 0, 'f', 'f')) - - def test_ctor(self): - inst = self._makeOne('filename', 10, 'function', 'src') - self.assertEqual(inst.file, 'filename') - self.assertEqual(inst.line, 10) - self.assertEqual(inst.function, 'function') - self.assertEqual(inst.src, 'src') - - def test___str__(self): - inst = self._makeOne('filename', 0, 'function', ' linerepr ') - self.assertEqual(str(inst), - "Line 0 of file filename:\n linerepr ") class DummyCustomPredicate(object): def __init__(self): diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index 8324eb2b9..4cebdce8a 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -1896,192 +1896,20 @@ class TestViewsConfigurationMixin(unittest.TestCase): from pyramid.tests import test_config self.assertEqual(result, test_config) - class Test_requestonly(unittest.TestCase): def _callFUT(self, view, attr=None): from pyramid.config.views import requestonly - return requestonly(view, attr) - - def test_requestonly_newstyle_class_no_init(self): - class foo(object): - """ """ - self.assertFalse(self._callFUT(foo)) - - def test_requestonly_newstyle_class_init_toomanyargs(self): - class foo(object): - def __init__(self, context, request): - """ """ - self.assertFalse(self._callFUT(foo)) - - def test_requestonly_newstyle_class_init_onearg_named_request(self): - class foo(object): - def __init__(self, request): - """ """ - self.assertTrue(self._callFUT(foo)) - - def test_newstyle_class_init_onearg_named_somethingelse(self): - class foo(object): - def __init__(self, req): - """ """ - self.assertTrue(self._callFUT(foo)) - - def test_newstyle_class_init_defaultargs_firstname_not_request(self): - class foo(object): - def __init__(self, context, request=None): - """ """ - self.assertFalse(self._callFUT(foo)) - - def test_newstyle_class_init_defaultargs_firstname_request(self): - class foo(object): - def __init__(self, request, foo=1, bar=2): - """ """ - self.assertTrue(self._callFUT(foo)) - - def test_newstyle_class_init_firstname_request_with_secondname(self): - class foo(object): - def __init__(self, request, two): - """ """ - self.assertFalse(self._callFUT(foo)) - - def test_newstyle_class_init_noargs(self): - class foo(object): - def __init__(): - """ """ - self.assertFalse(self._callFUT(foo)) - - def test_oldstyle_class_no_init(self): - class foo: - """ """ - self.assertFalse(self._callFUT(foo)) - - def test_oldstyle_class_init_toomanyargs(self): - class foo: - def __init__(self, context, request): - """ """ - self.assertFalse(self._callFUT(foo)) + return requestonly(view, attr=attr) - def test_oldstyle_class_init_onearg_named_request(self): - class foo: - def __init__(self, request): - """ """ - self.assertTrue(self._callFUT(foo)) - - def test_oldstyle_class_init_onearg_named_somethingelse(self): - class foo: - def __init__(self, req): - """ """ - self.assertTrue(self._callFUT(foo)) - - def test_oldstyle_class_init_defaultargs_firstname_not_request(self): - class foo: - def __init__(self, context, request=None): - """ """ - self.assertFalse(self._callFUT(foo)) - - def test_oldstyle_class_init_defaultargs_firstname_request(self): - class foo: - def __init__(self, request, foo=1, bar=2): - """ """ - self.assertTrue(self._callFUT(foo), True) - - def test_oldstyle_class_init_noargs(self): - class foo: - def __init__(): - """ """ - self.assertFalse(self._callFUT(foo)) - - def test_function_toomanyargs(self): - def foo(context, request): - """ """ - self.assertFalse(self._callFUT(foo)) - - def test_function_with_attr_false(self): - def bar(context, request): - """ """ - def foo(context, request): - """ """ - foo.bar = bar - self.assertFalse(self._callFUT(foo, 'bar')) - - def test_function_with_attr_true(self): - def bar(context, request): - """ """ - def foo(request): - """ """ - foo.bar = bar - self.assertTrue(self._callFUT(foo, 'bar')) + def test_defaults(self): + def aview(request, a=1, b=2): pass + self.assertTrue(self._callFUT(aview)) - def test_function_onearg_named_request(self): - def foo(request): - """ """ - self.assertTrue(self._callFUT(foo)) - - def test_function_onearg_named_somethingelse(self): - def foo(req): - """ """ - self.assertTrue(self._callFUT(foo)) - - def test_function_defaultargs_firstname_not_request(self): - def foo(context, request=None): - """ """ - self.assertFalse(self._callFUT(foo)) - - def test_function_defaultargs_firstname_request(self): - def foo(request, foo=1, bar=2): - """ """ - self.assertTrue(self._callFUT(foo)) - - def test_function_noargs(self): - def foo(): - """ """ - self.assertFalse(self._callFUT(foo)) - - def test_instance_toomanyargs(self): - class Foo: - def __call__(self, context, request): - """ """ - foo = Foo() - self.assertFalse(self._callFUT(foo)) - - def test_instance_defaultargs_onearg_named_request(self): - class Foo: - def __call__(self, request): - """ """ - foo = Foo() - self.assertTrue(self._callFUT(foo)) - - def test_instance_defaultargs_onearg_named_somethingelse(self): - class Foo: - def __call__(self, req): - """ """ - foo = Foo() - self.assertTrue(self._callFUT(foo)) - - def test_instance_defaultargs_firstname_not_request(self): - class Foo: - def __call__(self, context, request=None): - """ """ - foo = Foo() - self.assertFalse(self._callFUT(foo)) - - def test_instance_defaultargs_firstname_request(self): - class Foo: - def __call__(self, request, foo=1, bar=2): - """ """ - foo = Foo() - self.assertTrue(self._callFUT(foo), True) - - def test_instance_nocall(self): - class Foo: pass - foo = Foo() - self.assertFalse(self._callFUT(foo)) - - def test_method_onearg_named_request(self): - class Foo: - def method(self, request): - """ """ - foo = Foo() - self.assertTrue(self._callFUT(foo.method)) + def test_otherattr(self): + class AView(object): + def __init__(self, request, a=1, b=2): pass + def bleh(self): pass + self.assertTrue(self._callFUT(AView, 'bleh')) class Test_isexception(unittest.TestCase): def _callFUT(self, ob): @@ -2483,6 +2311,29 @@ class TestViewDeriver(unittest.TestCase): else: # pragma: no cover raise AssertionError + def test_function_returns_true_Response_no_renderer(self): + from pyramid.response import Response + r = Response('Hello') + def view(request): + return r + deriver = self._makeOne() + result = deriver(view) + self.assertFalse(result is view) + response = result(None, None) + self.assertEqual(response, r) + + def test_function_returns_true_Response_with_renderer(self): + from pyramid.response import Response + r = Response('Hello') + def view(request): + return r + renderer = object() + deriver = self._makeOne(renderer=renderer) + result = deriver(view) + self.assertFalse(result is view) + response = result(None, None) + self.assertEqual(response, r) + def test_requestonly_default_method_returns_non_adaptable(self): request = DummyRequest() class AView(object): diff --git a/pyramid/tests/test_integration.py b/pyramid/tests/test_integration.py index 9a8f842aa..bf3960b2d 100644 --- a/pyramid/tests/test_integration.py +++ b/pyramid/tests/test_integration.py @@ -174,6 +174,18 @@ class TestStaticAppBase(IntegrationBase): def test_oob_slash(self): self.testapp.get('/%2F/test_integration.py', status=404) +class TestEventOnlySubscribers(IntegrationBase, unittest.TestCase): + package = 'pyramid.tests.pkgs.eventonly' + + def test_sendfoo(self): + res = self.testapp.get('/sendfoo', status=200) + self.assertEqual(sorted(res.body.split()), [b'foo', b'fooyup']) + + def test_sendfoobar(self): + res = self.testapp.get('/sendfoobar', status=200) + self.assertEqual(sorted(res.body.split()), + [b'foobar', b'foobar2', b'foobaryup', b'foobaryup2']) + class TestStaticAppUsingAbsPath(TestStaticAppBase, unittest.TestCase): package = 'pyramid.tests.pkgs.static_abspath' diff --git a/pyramid/tests/test_paster.py b/pyramid/tests/test_paster.py index d94b46a9f..b72e0e6b6 100644 --- a/pyramid/tests/test_paster.py +++ b/pyramid/tests/test_paster.py @@ -2,14 +2,16 @@ import os import unittest class Test_get_app(unittest.TestCase): - def _callFUT(self, config_file, section_name, loadapp): + def _callFUT(self, config_file, section_name, options=None, loadapp=None): from pyramid.paster import get_app - return get_app(config_file, section_name, loadapp) + return get_app( + config_file, section_name, options=options, loadapp=loadapp + ) def test_it(self): app = DummyApp() loadapp = DummyLoadWSGI(app) - result = self._callFUT('/foo/bar/myapp.ini', 'myapp', loadapp) + result = self._callFUT('/foo/bar/myapp.ini', 'myapp', loadapp=loadapp) self.assertEqual(loadapp.config_name, 'config:/foo/bar/myapp.ini') self.assertEqual(loadapp.section_name, 'myapp') self.assertEqual(loadapp.relative_to, os.getcwd()) @@ -18,7 +20,9 @@ class Test_get_app(unittest.TestCase): def test_it_with_hash(self): app = DummyApp() loadapp = DummyLoadWSGI(app) - result = self._callFUT('/foo/bar/myapp.ini#myapp', None, loadapp) + result = self._callFUT( + '/foo/bar/myapp.ini#myapp', None, loadapp=loadapp + ) self.assertEqual(loadapp.config_name, 'config:/foo/bar/myapp.ini') self.assertEqual(loadapp.section_name, 'myapp') self.assertEqual(loadapp.relative_to, os.getcwd()) @@ -27,12 +31,30 @@ class Test_get_app(unittest.TestCase): def test_it_with_hash_and_name_override(self): app = DummyApp() loadapp = DummyLoadWSGI(app) - result = self._callFUT('/foo/bar/myapp.ini#myapp', 'yourapp', loadapp) + result = self._callFUT( + '/foo/bar/myapp.ini#myapp', 'yourapp', loadapp=loadapp + ) self.assertEqual(loadapp.config_name, 'config:/foo/bar/myapp.ini') self.assertEqual(loadapp.section_name, 'yourapp') self.assertEqual(loadapp.relative_to, os.getcwd()) self.assertEqual(result, app) + def test_it_with_options(self): + app = DummyApp() + loadapp = DummyLoadWSGI(app) + options = {'a':1} + result = self._callFUT( + '/foo/bar/myapp.ini#myapp', + 'yourapp', + loadapp=loadapp, + options=options, + ) + self.assertEqual(loadapp.config_name, 'config:/foo/bar/myapp.ini') + self.assertEqual(loadapp.section_name, 'yourapp') + self.assertEqual(loadapp.relative_to, os.getcwd()) + self.assertEqual(loadapp.kw, {'global_conf':options}) + self.assertEqual(result, app) + class Test_get_appsettings(unittest.TestCase): def _callFUT(self, config_file, section_name, appconfig): from pyramid.paster import get_appsettings @@ -132,10 +154,11 @@ class DummyLoadWSGI: def __init__(self, result): self.result = result - def __call__(self, config_name, name=None, relative_to=None): + def __call__(self, config_name, name=None, relative_to=None, **kw): self.config_name = config_name self.section_name = name self.relative_to = relative_to + self.kw = kw return self.result class DummyApp: diff --git a/pyramid/tests/test_request.py b/pyramid/tests/test_request.py index 945e36a7f..565c6377e 100644 --- a/pyramid/tests/test_request.py +++ b/pyramid/tests/test_request.py @@ -230,6 +230,13 @@ class TestRequest(unittest.TestCase): request.registry = self.config.registry self.assertEqual(request.is_response('abc'), False) + def test_is_response_true_ob_is_pyramid_response(self): + from pyramid.response import Response + r = Response('hello') + request = self._makeOne() + request.registry = self.config.registry + self.assertEqual(request.is_response(r), True) + def test_is_response_false_adapter_is_not_self(self): from pyramid.interfaces import IResponse request = self._makeOne() diff --git a/pyramid/tests/test_router.py b/pyramid/tests/test_router.py index 778b27473..65152ca05 100644 --- a/pyramid/tests/test_router.py +++ b/pyramid/tests/test_router.py @@ -24,7 +24,7 @@ class TestRouter(unittest.TestCase): if mapper is None: mapper = RoutesMapper() self.registry.registerUtility(mapper, IRoutesMapper) - mapper.connect(name, path, factory) + return mapper.connect(name, path, factory) def _registerLogger(self): from pyramid.interfaces import IDebugLogger @@ -657,7 +657,8 @@ class TestRouter(unittest.TestCase): root = object() def factory(request): return root - self._connectRoute('foo', 'archives/:action/:article', factory) + route = self._connectRoute('foo', 'archives/:action/:article', factory) + route.predicates = [DummyPredicate()] context = DummyContext() self._registerTraverserFactory(context) response = DummyResponse() @@ -686,7 +687,11 @@ class TestRouter(unittest.TestCase): "route matched for url http://localhost:8080" "/archives/action1/article1; " "route_name: 'foo', " - "path_info: ")) + "path_info: ") + ) + self.assertTrue( + "predicates: 'predicate'" in logger.messages[0] + ) def test_call_route_match_miss_debug_routematch(self): from pyramid.httpexceptions import HTTPNotFound @@ -1159,6 +1164,12 @@ class TestRouter(unittest.TestCase): start_response = DummyStartResponse() self.assertRaises(RuntimeError, router, environ, start_response) +class DummyPredicate(object): + def __call__(self, info, request): + return True + def text(self): + return 'predicate' + class DummyContext: pass diff --git a/pyramid/tests/test_scripts/test_common.py b/pyramid/tests/test_scripts/test_common.py index c3c792ca4..13ab0ae6a 100644 --- a/pyramid/tests/test_scripts/test_common.py +++ b/pyramid/tests/test_scripts/test_common.py @@ -17,6 +17,19 @@ class Test_logging_file_config(unittest.TestCase): def fileConfig(self, config_file, dict): return config_file, dict +class TestParseVars(unittest.TestCase): + def test_parse_vars_good(self): + from pyramid.scripts.common import parse_vars + vars = ['a=1', 'b=2'] + result = parse_vars(vars) + self.assertEqual(result, {'a': '1', 'b': '2'}) + + def test_parse_vars_bad(self): + from pyramid.scripts.common import parse_vars + vars = ['a'] + self.assertRaises(ValueError, parse_vars, vars) + + class DummyConfigParser(object): def read(self, x): pass diff --git a/pyramid/tests/test_scripts/test_prequest.py b/pyramid/tests/test_scripts/test_prequest.py index cf7af4218..91d2b322a 100644 --- a/pyramid/tests/test_scripts/test_prequest.py +++ b/pyramid/tests/test_scripts/test_prequest.py @@ -13,9 +13,11 @@ class TestPRequestCommand(unittest.TestCase): cmd.out = self.out return cmd - def get_app(self, spec, app_name=None): + def get_app(self, spec, app_name=None, options=None): self._spec = spec self._app_name = app_name + self._options = options or {} + def helloworld(environ, start_request): self._environ = environ self._path_info = environ['PATH_INFO'] diff --git a/pyramid/tests/test_scripts/test_proutes.py b/pyramid/tests/test_scripts/test_proutes.py index fad8c1895..25a3cd2e3 100644 --- a/pyramid/tests/test_scripts/test_proutes.py +++ b/pyramid/tests/test_scripts/test_proutes.py @@ -12,6 +12,28 @@ class TestPRoutesCommand(unittest.TestCase): cmd.args = ('/foo/bar/myapp.ini#myapp',) return cmd + def test_good_args(self): + cmd = self._getTargetClass()([]) + cmd.bootstrap = (dummy.DummyBootstrap(),) + cmd.args = ('/foo/bar/myapp.ini#myapp', 'a=1') + route = dummy.DummyRoute('a', '/a') + mapper = dummy.DummyMapper(route) + cmd._get_mapper = lambda *arg: mapper + L = [] + cmd.out = lambda msg: L.append(msg) + cmd.run() + self.assertTrue('<unknown>' in ''.join(L)) + + def test_bad_args(self): + cmd = self._getTargetClass()([]) + cmd.bootstrap = (dummy.DummyBootstrap(),) + cmd.args = ('/foo/bar/myapp.ini#myapp', 'a') + route = dummy.DummyRoute('a', '/a') + mapper = dummy.DummyMapper(route) + cmd._get_mapper = lambda *arg: mapper + + self.assertRaises(ValueError, cmd.run) + def test_no_routes(self): command = self._makeOne() mapper = dummy.DummyMapper() diff --git a/pyramid/tests/test_scripts/test_pserve.py b/pyramid/tests/test_scripts/test_pserve.py index d7b252d92..6e4b0f17d 100644 --- a/pyramid/tests/test_scripts/test_pserve.py +++ b/pyramid/tests/test_scripts/test_pserve.py @@ -22,6 +22,12 @@ class TestPServeCommand(unittest.TestCase): def out(self, msg): self.out_.write(msg) + def _get_server(*args, **kwargs): + def server(app): + return '' + + return server + def _getTargetClass(self): from pyramid.scripts.pserve import PServeCommand return PServeCommand @@ -193,16 +199,39 @@ class TestPServeCommand(unittest.TestCase): msg = 'PID in %s is not valid (deleting)' % fn self.assertEqual(self.out_.getvalue(), msg) - def test_parse_vars_good(self): - vars = ['a=1', 'b=2'] - inst = self._makeOne('development.ini') - result = inst.parse_vars(vars) + def test_get_options_with_command(self): + inst = self._makeOne() + inst.args = ['foo', 'stop', 'a=1', 'b=2'] + result = inst.get_options() self.assertEqual(result, {'a': '1', 'b': '2'}) + def test_get_options_no_command(self): + inst = self._makeOne() + inst.args = ['foo', 'a=1', 'b=2'] + result = inst.get_options() + self.assertEqual(result, {'a': '1', 'b': '2'}) + + def test_parse_vars_good(self): + from pyramid.tests.test_scripts.dummy import DummyApp + + inst = self._makeOne('development.ini', 'a=1', 'b=2') + inst.loadserver = self._get_server + + + app = DummyApp() + + def get_app(*args, **kwargs): + app.global_conf = kwargs.get('global_conf', None) + + inst.loadapp = get_app + inst.run() + + self.assertEqual(app.global_conf, {'a': '1', 'b': '2'}) + def test_parse_vars_bad(self): - vars = ['a'] - inst = self._makeOne('development.ini') - self.assertRaises(ValueError, inst.parse_vars, vars) + inst = self._makeOne('development.ini', 'a') + inst.loadserver = self._get_server + self.assertRaises(ValueError, inst.run) class Test_read_pidfile(unittest.TestCase): def _callFUT(self, filename): diff --git a/pyramid/tests/test_scripts/test_pviews.py b/pyramid/tests/test_scripts/test_pviews.py index 6a919c31b..266d1ec90 100644 --- a/pyramid/tests/test_scripts/test_pviews.py +++ b/pyramid/tests/test_scripts/test_pviews.py @@ -379,7 +379,7 @@ class TestPViewsCommand(unittest.TestCase): L = [] command.out = L.append def predicate(): pass - predicate.__text__ = "predicate = x" + predicate.text = lambda *arg: "predicate = x" route = dummy.DummyRoute('a', '/a', matchdict={}, predicate=predicate) view = dummy.DummyView(context='context', view_name='a', matched_route=route, subpath='') diff --git a/pyramid/tests/test_util.py b/pyramid/tests/test_util.py index 785950230..2ca4c4a66 100644 --- a/pyramid/tests/test_util.py +++ b/pyramid/tests/test_util.py @@ -545,6 +545,37 @@ class TestSentinel(unittest.TestCase): r = repr(Sentinel('ABC')) self.assertEqual(r, 'ABC') +class TestActionInfo(unittest.TestCase): + def _getTargetClass(self): + from pyramid.util import ActionInfo + return ActionInfo + + def _makeOne(self, filename, lineno, function, linerepr): + return self._getTargetClass()(filename, lineno, function, linerepr) + + def test_class_conforms(self): + from zope.interface.verify import verifyClass + from pyramid.interfaces import IActionInfo + verifyClass(IActionInfo, self._getTargetClass()) + + def test_instance_conforms(self): + from zope.interface.verify import verifyObject + from pyramid.interfaces import IActionInfo + verifyObject(IActionInfo, self._makeOne('f', 0, 'f', 'f')) + + def test_ctor(self): + inst = self._makeOne('filename', 10, 'function', 'src') + self.assertEqual(inst.file, 'filename') + self.assertEqual(inst.line, 10) + self.assertEqual(inst.function, 'function') + self.assertEqual(inst.src, 'src') + + def test___str__(self): + inst = self._makeOne('filename', 0, 'function', ' linerepr ') + self.assertEqual(str(inst), + "Line 0 of file filename:\n linerepr ") + + def dummyfunc(): pass class Dummy(object): diff --git a/pyramid/tests/test_view.py b/pyramid/tests/test_view.py index 0af941e0d..a78b0cbab 100644 --- a/pyramid/tests/test_view.py +++ b/pyramid/tests/test_view.py @@ -224,12 +224,29 @@ class RenderViewToIterableTests(BaseTest, unittest.TestCase): response = DummyResponse() view = make_view(response) def anotherview(context, request): - return DummyResponse('anotherview') + return DummyResponse(b'anotherview') view.__call_permissive__ = anotherview self._registerView(request.registry, view, 'registered') iterable = self._callFUT(context, request, name='registered', secure=False) - self.assertEqual(iterable, ['anotherview']) + self.assertEqual(iterable, [b'anotherview']) + + def test_verify_output_bytestring(self): + from pyramid.request import Request + from pyramid.config import Configurator + from pyramid.view import render_view + from webob.compat import text_type + config = Configurator(settings={}) + def view(request): + request.response.text = text_type('<body></body>') + return request.response + + config.add_view(name='test', view=view) + config.commit() + + r = Request({}) + r.registry = config.registry + self.assertEqual(render_view(object(), r, 'test'), b'<body></body>') def test_call_request_has_no_registry(self): request = self._makeRequest() @@ -261,7 +278,7 @@ class RenderViewTests(BaseTest, unittest.TestCase): view = make_view(response) self._registerView(request.registry, view, 'registered') s = self._callFUT(context, request, name='registered', secure=True) - self.assertEqual(s, '') + self.assertEqual(s, b'') def test_call_view_registered_insecure_no_call_permissive(self): context = self._makeContext() @@ -270,7 +287,7 @@ class RenderViewTests(BaseTest, unittest.TestCase): view = make_view(response) self._registerView(request.registry, view, 'registered') s = self._callFUT(context, request, name='registered', secure=False) - self.assertEqual(s, '') + self.assertEqual(s, b'') def test_call_view_registered_insecure_with_call_permissive(self): context = self._makeContext() @@ -278,11 +295,11 @@ class RenderViewTests(BaseTest, unittest.TestCase): response = DummyResponse() view = make_view(response) def anotherview(context, request): - return DummyResponse('anotherview') + return DummyResponse(b'anotherview') view.__call_permissive__ = anotherview self._registerView(request.registry, view, 'registered') s = self._callFUT(context, request, name='registered', secure=False) - self.assertEqual(s, 'anotherview') + self.assertEqual(s, b'anotherview') class TestIsResponse(unittest.TestCase): def setUp(self): diff --git a/pyramid/util.py b/pyramid/util.py index d83837322..ca7f5951c 100644 --- a/pyramid/util.py +++ b/pyramid/util.py @@ -1,6 +1,10 @@ +import functools import inspect +import traceback import weakref +from zope.interface import implementer + from pyramid.exceptions import ( ConfigurationError, CyclicDependencyError, @@ -15,6 +19,7 @@ from pyramid.compat import ( PY3, ) +from pyramid.interfaces import IActionInfo from pyramid.path import DottedNameResolver as _DottedNameResolver class DottedNameResolver(_DottedNameResolver): @@ -453,3 +458,65 @@ class TopologicalSorter(object): return result +def viewdefaults(wrapped): + """ Decorator for add_view-like methods which takes into account + __view_defaults__ attached to view it is passed. Not a documented API but + used by some external systems.""" + def wrapper(self, *arg, **kw): + defaults = {} + if arg: + view = arg[0] + else: + view = kw.get('view') + view = self.maybe_dotted(view) + if inspect.isclass(view): + defaults = getattr(view, '__view_defaults__', {}).copy() + defaults.update(kw) + defaults['_backframes'] = 3 # for action_method + return wrapped(self, *arg, **defaults) + return functools.wraps(wrapped)(wrapper) + +@implementer(IActionInfo) +class ActionInfo(object): + def __init__(self, file, line, function, src): + self.file = file + self.line = line + self.function = function + self.src = src + + def __str__(self): + srclines = self.src.split('\n') + src = '\n'.join(' %s' % x for x in srclines) + return 'Line %s of file %s:\n%s' % (self.line, self.file, src) + +def action_method(wrapped): + """ Wrapper to provide the right conflict info report data when a method + that calls Configurator.action calls another that does the same. Not a + documented API but used by some external systems.""" + def wrapper(self, *arg, **kw): + if self._ainfo is None: + self._ainfo = [] + info = kw.pop('_info', None) + # backframes for outer decorators to actionmethods + backframes = kw.pop('_backframes', 2) + if is_nonstr_iter(info) and len(info) == 4: + # _info permitted as extract_stack tuple + info = ActionInfo(*info) + if info is None: + try: + f = traceback.extract_stack(limit=3) + info = ActionInfo(*f[-backframes]) + except: # pragma: no cover + info = ActionInfo(None, 0, '', '') + self._ainfo.append(info) + try: + result = wrapped(self, *arg, **kw) + finally: + self._ainfo.pop() + return result + + if hasattr(wrapped, '__name__'): + functools.update_wrapper(wrapper, wrapped) + wrapper.__docobj__ = wrapped + return wrapper + diff --git a/pyramid/view.py b/pyramid/view.py index 835982e79..1a66c9e9c 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -3,6 +3,7 @@ import venusian from zope.interface import providedBy from zope.deprecation import deprecated + from pyramid.interfaces import ( IRoutesMapper, IView, @@ -93,8 +94,8 @@ def render_view_to_iterable(context, request, name='', secure=True): :exc:`ValueError` if a view function is found and called but the view function's result does not have an ``app_iter`` attribute. - You can usually get the string representation of the return value - of this function by calling ``''.join(iterable)``, or just use + You can usually get the bytestring representation of the return value of + this function by calling ``b''.join(iterable)``, or just use :func:`pyramid.view.render_view` instead. If ``secure`` is ``True``, and the view is protected by a permission, the @@ -116,7 +117,7 @@ def render_view(context, request, name='', secure=True): configuration` that matches the :term:`view name` ``name`` registered against the specified ``context`` and ``request`` and unwind the view response's ``app_iter`` (see - :ref:`the_response`) into a single string. This function will + :ref:`the_response`) into a single bytestring. This function will return ``None`` if a corresponding :term:`view callable` cannot be found (when no :term:`view configuration` matches the combination of ``name`` / ``context`` / and ``request``). Additionally, this @@ -136,7 +137,7 @@ def render_view(context, request, name='', secure=True): iterable = render_view_to_iterable(context, request, name, secure) if iterable is None: return None - return ''.join(iterable) + return b''.join(iterable) class view_config(object): """ A function, class or method :term:`decorator` which allows a @@ -68,7 +68,7 @@ testing_extras = tests_require + [ ] setup(name='pyramid', - version='1.4a3', + version='1.4b1', description=('The Pyramid web application development framework, a ' 'Pylons project'), long_description=README + '\n\n' + CHANGES, |
