From 1ffb8e3cc21603b29ccd78152f82cca7f61a09b1 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 16 May 2011 02:11:56 -0400 Subject: - Added API docs for ``pyramid.httpexceptions.abort`` and ``pyramid.httpexceptions.redirect``. - Added "HTTP Exceptions" section to Views narrative chapter including a description of ``pyramid.httpexceptions.abort``; adjusted redirect section to note ``pyramid.httpexceptions.redirect``. - A default exception view for the context ``webob.exc.HTTPException`` (aka ``pyramid.httpexceptions.HTTPException``) is now registered by default. This means that an instance of any exception class imported from ``pyramid.httpexceptions`` (such as ``HTTPFound``) can now be raised from within view code; when raised, this exception view will render the exception to a response. - New functions named ``pyramid.httpexceptions.abort`` and ``pyramid.httpexceptions.redirect`` perform the equivalent of their Pylons brethren when an HTTP exception handler is registered. These functions take advantage of the newly registered exception view for ``webob.exc.HTTPException``. - The Configurator now accepts an additional keyword argument named ``httpexception_view``. By default, this argument is populated with a default exception view function that will be used when an HTTP exception is raised. When ``None`` is passed for this value, an exception view for HTTP exceptions will not be registered. Passing ``None`` returns the behavior of raising an HTTP exception to that of Pyramid 1.0 (the exception will propagate to middleware and to the WSGI server). --- CHANGES.txt | 28 ++++ TODO.txt | 4 - docs/api/httpexceptions.rst | 4 + docs/glossary.rst | 6 + docs/narr/views.rst | 249 +++++++++++++++++++++++++---------- docs/whatsnew-1.1.rst | 35 +++++ pyramid/config.py | 25 +++- pyramid/httpexceptions.py | 33 +++++ pyramid/tests/test_config.py | 46 +++++++ pyramid/tests/test_httpexceptions.py | 79 +++++++++++ 10 files changed, 432 insertions(+), 77 deletions(-) create mode 100644 pyramid/tests/test_httpexceptions.py diff --git a/CHANGES.txt b/CHANGES.txt index 0992af9ef..756d1345c 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -37,6 +37,13 @@ Documentation - Added "What's New in Pyramid 1.1" to HTML rendering of documentation. +- Added API docs for ``pyramid.httpexceptions.abort`` and + ``pyramid.httpexceptions.redirect``. + +- Added "HTTP Exceptions" section to Views narrative chapter including a + description of ``pyramid.httpexceptions.abort``; adjusted redirect section + to note ``pyramid.httpexceptions.redirect``. + Features -------- @@ -97,6 +104,27 @@ Features section entitled "Static Routes" in the URL Dispatch narrative chapter for more information. +- A default exception view for the context ``webob.exc.HTTPException`` (aka + ``pyramid.httpexceptions.HTTPException``) is now registered by default. + This means that an instance of any exception class imported from + ``pyramid.httpexceptions`` (such as ``HTTPFound``) can now be raised from + within view code; when raised, this exception view will render the + exception to a response. + +- New functions named ``pyramid.httpexceptions.abort`` and + ``pyramid.httpexceptions.redirect`` perform the equivalent of their Pylons + brethren when an HTTP exception handler is registered. These functions + take advantage of the newly registered exception view for + ``webob.exc.HTTPException``. + +- The Configurator now accepts an additional keyword argument named + ``httpexception_view``. By default, this argument is populated with a + default exception view function that will be used when an HTTP exception is + raised. When ``None`` is passed for this value, an exception view for HTTP + exceptions will not be registered. Passing ``None`` returns the behavior + of raising an HTTP exception to that of Pyramid 1.0 (the exception will + propagate to middleware and to the WSGI server). + Bug Fixes --------- diff --git a/TODO.txt b/TODO.txt index 0f7d6342c..d85f3b7f0 100644 --- a/TODO.txt +++ b/TODO.txt @@ -4,10 +4,6 @@ Pyramid TODOs Should-Have ----------- -- Consider adding a default exception view for HTTPException and attendant - ``redirect`` and ``abort`` functions ala Pylons (promised Mike I'd enable - this in 1.1). - - Add narrative docs for wsgiapp and wsgiapp2. Nice-to-Have diff --git a/docs/api/httpexceptions.rst b/docs/api/httpexceptions.rst index 57ca8092c..73da4126b 100644 --- a/docs/api/httpexceptions.rst +++ b/docs/api/httpexceptions.rst @@ -5,6 +5,10 @@ .. automodule:: pyramid.httpexceptions + .. autofunction:: abort + + .. autofunction:: redirect + .. attribute:: status_map A mapping of integer status code to exception class (eg. the diff --git a/docs/glossary.rst b/docs/glossary.rst index e1e9e76a9..797343e5e 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -618,6 +618,12 @@ Glossary request processing. See :ref:`exception_views` for more information. + HTTP Exception + The set of exception classes defined in :mod:`pyramid.httpexceptions`. + These can be used to generate responses with various status codes when + raised or returned from a :term:`view callable`. See also + :ref:`http_exceptions`. + thread local A thread-local variable is one which is essentially a global variable in terms of how it is accessed and treated, however, each `thread diff --git a/docs/narr/views.rst b/docs/narr/views.rst index 5c9bd91af..465cd3c0d 100644 --- a/docs/narr/views.rst +++ b/docs/narr/views.rst @@ -51,14 +51,14 @@ the request object contains everything your application needs to know about the specific HTTP request being made. A view callable's ultimate responsibility is to create a :mod:`Pyramid` -:term:`Response` object. This can be done by creating the response -object in the view callable code and returning it directly, as we will -be doing in this chapter. However, if a view callable does not return a -response itself, it can be configured to use a :term:`renderer` that -converts its return value into a :term:`Response` object. Using -renderers is the common way that templates are used with view callables -to generate markup. See the :ref:`renderers_chapter` chapter for -details. +:term:`Response` object. This can be done by creating the response object in +the view callable code and returning it directly, as we will be doing in this +chapter. However, if a view callable does not return a response itself, it +can be configured to use a :term:`renderer` that converts its return value +into a :term:`Response` object. Using renderers is the common way that +templates are used with view callables to generate markup: see the +:ref:`renderers_chapter` chapter for details. In some cases, a response may +also be generated by raising an exception within a view callable. .. index:: single: view calling convention @@ -234,8 +234,8 @@ You don't need to always use :class:`~pyramid.response.Response` to represent a response. :app:`Pyramid` provides a range of different "exception" classes which can act as response objects too. For example, an instance of the class :class:`pyramid.httpexceptions.HTTPFound` is also a valid response object -(see :ref:`http_redirect`). A view can actually return any object that has -the following attributes. +(see :ref:`http_exceptions` and ref:`http_redirect`). A view can actually +return any object that has the following attributes. status The HTTP status code (including the name) for the response as a string. @@ -254,46 +254,6 @@ app_iter These attributes form the structure of the "Pyramid Response interface". -.. index:: - single: view http redirect - single: http redirect (from a view) - -.. _http_redirect: - -Using a View Callable to Do an HTTP Redirect --------------------------------------------- - -You can issue an HTTP redirect from within a view by returning a particular -kind of response. - -.. code-block:: python - :linenos: - - from pyramid.httpexceptions import HTTPFound - - def myview(request): - return HTTPFound(location='http://example.com') - -All exception types from the :mod:`pyramid.httpexceptions` module implement -the :term:`Response` interface; any can be returned as the response from a -view. See :mod:`pyramid.httpexceptions` for the documentation for the -``HTTPFound`` exception; it also includes other response types that imply -other HTTP response codes, such as ``HTTPUnauthorized`` for ``401 -Unauthorized``. - -.. note:: - - Although exception types from the :mod:`pyramid.httpexceptions` module are - in fact bona fide Python :class:`Exception` types, the :app:`Pyramid` view - machinery expects them to be *returned* by a view callable rather than - *raised*. - - It is possible, however, in Python 2.5 and above, to configure an - *exception view* to catch these exceptions, and return an appropriate - :class:`~pyramid.response.Response`. The simplest such view could just - catch and return the original exception. See :ref:`exception_views` for - more details. - .. index:: single: view exceptions @@ -304,13 +264,21 @@ Using Special Exceptions In View Callables Usually when a Python exception is raised within a view callable, :app:`Pyramid` allows the exception to propagate all the way out to the -:term:`WSGI` server which invoked the application. +:term:`WSGI` server which invoked the application. It is usually caught and +logged there. -However, for convenience, two special exceptions exist which are always -handled by :app:`Pyramid` itself. These are -:exc:`pyramid.exceptions.NotFound` and :exc:`pyramid.exceptions.Forbidden`. -Both are exception classes which accept a single positional constructor -argument: a ``message``. +However, for convenience, a special set of exceptions exists. When one of +these exceptions is raised within a view callable, it will always cause +:app:`Pyramid` to generate a response. Two categories of special exceptions +exist: internal exceptions and HTTP exceptions. + +Internal Exceptions +~~~~~~~~~~~~~~~~~~~ + +:exc:`pyramid.exceptions.NotFound` and :exc:`pyramid.exceptions.Forbidden` +are exceptions often raised by Pyramid itself when it (respectively) cannot +find a view to service a request or when authorization was forbidden by a +security policy. However, they can also be raised by application developers. If :exc:`~pyramid.exceptions.NotFound` is raised within view code, the result of the :term:`Not Found View` will be returned to the user agent which @@ -320,22 +288,100 @@ If :exc:`~pyramid.exceptions.Forbidden` is raised within view code, the result of the :term:`Forbidden View` will be returned to the user agent which performed the request. -In all cases, the message provided to the exception constructor is made -available to the view which :app:`Pyramid` invokes as +Both are exception classes which accept a single positional constructor +argument: a ``message``. In all cases, the message provided to the exception +constructor is made available to the view which :app:`Pyramid` invokes as ``request.exception.args[0]``. +An example: + +.. code-block:: python + :linenos: + + from pyramid.exceptions import NotFound + + def aview(request): + raise NotFound('not found!') + +Internal exceptions may not be *returned* in order to generate a response, +they must always be *raised*. + +.. index:: + single: HTTP exceptions + +.. _http_exceptions: + +HTTP Exceptions +~~~~~~~~~~~~~~~ + +All exception classes documented in the :mod:`pyramid.httpexceptions` module +implement the :term:`Response` interface; an instance of any of these classes +can be returned or raised from within a view. The instance will be used as +as the view's response. + +For example, the :class:`pyramid.httpexceptions.HTTPUnauthorized` exception +can be raised. This will cause a response to be generated with a ``401 +Unauthorized`` status: + +.. code-block:: python + :linenos: + + from pyramid.httpexceptions import HTTPUnauthorized + + def aview(request): + raise HTTPUnauthorized() + +A shortcut for importing and raising an HTTP exception is the +:func:`pyramid.httpexceptions.abort` function. This function accepts an HTTP +status code and raises the corresponding HTTP exception. For example, to +raise HTTPUnauthorized, instead of the above, you could do: + +.. code-block:: python + :linenos: + + from pyramid.httpexceptions import abort + + def aview(request): + abort(401) + +This is the case because ``401`` is the HTTP status code for "HTTP +Unauthorized". Therefore, ``abort(401)`` is functionally equivalent to +``raise HTTPUnauthorized()``. Other exceptions in +:mod:`pyramid.httpexceptions` can be raised via +:func:`pyramid.httpexceptions.abort` as well, as long as the status code +associated with the exception is provided to the function. + +An HTTP exception, instead of being raised, can alternately be *returned* +(HTTP exceptions are also valid response objects): + +.. code-block:: python + :linenos: + + from pyramid.httpexceptions import HTTPUnauthorized + + def aview(request): + return HTTPUnauthorized() + +Note that :class:`pyramid.exceptions.NotFound` is *not* the same as +:class:`pyramid.httpexceptions.HTTPNotFound`. If the latter is raised, the +:term:`Not Found view` will *not* be called automatically. Likewise, +:class:`pyramid.exceptions.Foribdden` is not the same exception as +:class:`pyramid.httpexceptions.HTTPForbidden`. If the latter is raised, the +:term:`Forbidden view` will not be called automatically. + .. index:: single: exception views .. _exception_views: -Exception Views ---------------- +Custom Exception Views +---------------------- -The machinery which allows the special :exc:`~pyramid.exceptions.NotFound` and -:exc:`~pyramid.exceptions.Forbidden` exceptions to be caught by specialized -views as described in :ref:`special_exceptions_in_callables` can also be used -by application developers to convert arbitrary exceptions to responses. +The machinery which allows :exc:`~pyramid.exceptions.NotFound`, +:exc:`~pyramid.exceptions.Forbidden` and HTTP exceptions to be caught by +specialized views as described in :ref:`special_exceptions_in_callables` can +also be used by application developers to convert arbitrary exceptions to +responses. To register a view that should be called whenever a particular exception is raised from with :app:`Pyramid` view code, use the exception class or one of @@ -359,6 +405,7 @@ raises a ``helloworld.exceptions.ValidationFailure`` exception: .. code-block:: python :linenos: + from pyramid.view import view_config from helloworld.exceptions import ValidationFailure @view_config(context=ValidationFailure) @@ -380,12 +427,13 @@ exception view registration: :linenos: from pyramid.view import view_config - from pyramid.exceptions import NotFound - from pyramid.httpexceptions import HTTPNotFound + from helloworld.exceptions import ValidationFailure - @view_config(context=NotFound, route_name='home') - def notfound_view(request): - return HTTPNotFound() + @view_config(context=ValidationFailure, route_name='home') + def failed_validation(exc, request): + response = Response('Failed validation: %s' % exc.msg) + response.status_int = 500 + return response The above exception view names the ``route_name`` of ``home``, meaning that it will only be called when the route matched has a name of ``home``. You @@ -407,7 +455,68 @@ exception views which have a name will be ignored. can use an exception as ``context`` for a normal view. Exception views can be configured with any view registration mechanism: -``@view_config`` decorator, ZCML, or imperative ``add_view`` styles. +``@view_config`` decorator or imperative ``add_view`` styles. + +.. index:: + single: view http redirect + single: http redirect (from a view) + +.. _http_redirect: + +Using a View Callable to Do an HTTP Redirect +-------------------------------------------- + +Two methods exist to redirect to another URL from within a view callable: a +short form and a long form. The short form should be preferred when +possible. + +Short Form +~~~~~~~~~~ + +You can issue an HTTP redirect from within a view callable by using the +:func:`pyramid.httpexceptions.redirect` function. This function raises an +:class:`pyramid.httpexceptions.HTTPFound` exception (a "302"), which is +caught by an exception handler and turned into a response. + +.. code-block:: python + :linenos: + + from pyramid.httpexceptions import redirect + + def myview(request): + redirect('http://example.com') + +Long Form +~~~~~~~~~ + +You can issue an HTTP redirect from within a view "by hand" instead of +relying on the :func:`pyramid.httpexceptions.redirect` function to do it for +you. + +To do so, you can *return* a :class:`pyramid.httpexceptions.HTTPFound` +instance. + +.. code-block:: python + :linenos: + + from pyramid.httpexceptions import HTTPFound + + def myview(request): + return HTTPFound(location='http://example.com') + +Or, alternately, you can *raise* an HTTPFound exception instead of returning +one. + +.. code-block:: python + :linenos: + + from pyramid.httpexceptions import HTTPFound + + def myview(request): + raise HTTPFound(location='http://example.com') + +The above form of generating a response by raising HTTPFound is completely +equivalent to ``redirect('http://example.com')``. .. index:: single: unicode, views, and forms diff --git a/docs/whatsnew-1.1.rst b/docs/whatsnew-1.1.rst index 992e0b637..488328519 100644 --- a/docs/whatsnew-1.1.rst +++ b/docs/whatsnew-1.1.rst @@ -18,6 +18,9 @@ The major feature additions in Pyramid 1.1 are: - Support for "static" routes. +- Default HTTP exception view and associated ``redirect`` and ``abort`` + convenience functions. + ``request.response`` ~~~~~~~~~~~~~~~~~~~~ @@ -50,6 +53,31 @@ Static Routes be useful for URL generation via ``route_url`` and ``route_path``. See the section entitled :ref:`static_route_narr` for more information. +Default HTTP Exception View +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- A default exception view for the context :exc:`webob.exc.HTTPException` + (aka :class:`pyramid.httpexceptions.HTTPException`) is now registered by + default. This means that an instance of any exception class imported from + :mod:`pyramid.httpexceptions` (such as ``HTTPFound``) can now be raised + from within view code; when raised, this exception view will render the + exception to a response. + + New convenience functions named :func:`pyramid.httpexceptions.abort` and + :func:`pyramid.httpexceptions.redirect` perform the equivalent of their + Pylons brethren when an HTTP exception handler is registered. These + functions take advantage of the newly registered exception view for + :exc:`webob.exc.HTTPException`. + + To allow for configuration of this feature, the :term:`Configurator` now + accepts an additional keyword argument named ``httpexception_view``. By + default, this argument is populated with a default exception view function + that will be used when an HTTP exception is raised. When ``None`` is + passed for this value, an exception view for HTTP exceptions will not be + registered. Passing ``None`` returns the behavior of raising an HTTP + exception to that of Pyramid 1.0 (the exception will propagate to + middleware and to the WSGI server). + Minor Feature Additions ----------------------- @@ -222,3 +250,10 @@ Documentation Enhancements - Added a section to the "URL Dispatch" narrative chapter regarding the new "static" route feature entitled :ref:`static_route_narr`. + +- Added API docs for :func:`pyramid.httpexceptions.abort` and + :func:`pyramid.httpexceptions.redirect`. + +- Added :ref:`http_exceptions` section to Views narrative chapter including a + description of :func:`pyramid.httpexceptions.abort`` and + :func:`pyramid.httpexceptions.redirect`. diff --git a/pyramid/config.py b/pyramid/config.py index 4e06a9b2e..85a66a837 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -59,6 +59,8 @@ from pyramid.exceptions import ConfigurationError from pyramid.exceptions import Forbidden from pyramid.exceptions import NotFound from pyramid.exceptions import PredicateMismatch +from pyramid.httpexceptions import HTTPException +from pyramid.httpexceptions import default_httpexception_view from pyramid.i18n import get_localizer from pyramid.log import make_stream_logger from pyramid.mako_templating import renderer_factory as mako_renderer_factory @@ -139,7 +141,8 @@ class Configurator(object): ``package``, ``settings``, ``root_factory``, ``authentication_policy``, ``authorization_policy``, ``renderers`` ``debug_logger``, ``locale_negotiator``, ``request_factory``, ``renderer_globals_factory``, - ``default_permission``, ``session_factory``, and ``autocommit``. + ``default_permission``, ``session_factory``, ``default_view_mapper``, + ``autocommit``, and ``httpexception_view``. If the ``registry`` argument is passed as a non-``None`` value, it must be an instance of the :class:`pyramid.registry.Registry` @@ -254,7 +257,17 @@ class Configurator(object): :term:`view mapper` factory for view configurations that don't otherwise specify one (see :class:`pyramid.interfaces.IViewMapperFactory`). If a default_view_mapper is not passed, a superdefault view mapper will be - used. """ + used. + + If ``httpexception_view`` is passed, it must be a :term:`view callable` + or ``None``. If it is a view callable, it will be used as an exception + view callable when an :term:`HTTP exception` is raised (any named + exception from the ``pyramid.httpexceptions`` module) by + :func:`pyramid.httpexceptions.abort`, + :func:`pyramid.httpexceptions.redirect` or 'by hand'. If it is ``None``, + no httpexception view will be registered. By default, the + ``pyramid.httpexceptions.default_httpexception_view`` function is + used. This behavior is new in Pyramid 1.1. """ manager = manager # for testing injection venusian = venusian # for testing injection @@ -277,6 +290,7 @@ class Configurator(object): session_factory=None, default_view_mapper=None, autocommit=False, + httpexception_view=default_httpexception_view, ): if package is None: package = caller_package() @@ -302,6 +316,7 @@ class Configurator(object): default_permission=default_permission, session_factory=session_factory, default_view_mapper=default_view_mapper, + httpexception_view=httpexception_view, ) def _set_settings(self, mapping): @@ -658,7 +673,8 @@ class Configurator(object): renderers=DEFAULT_RENDERERS, debug_logger=None, locale_negotiator=None, request_factory=None, renderer_globals_factory=None, default_permission=None, - session_factory=None, default_view_mapper=None): + session_factory=None, default_view_mapper=None, + httpexception_view=default_httpexception_view): """ When you pass a non-``None`` ``registry`` argument to the :term:`Configurator` constructor, no initial 'setup' is performed against the registry. This is because the registry you pass in may @@ -690,6 +706,9 @@ class Configurator(object): self.add_renderer(name, renderer) self.add_view(default_exceptionresponse_view, context=IExceptionResponse) + if httpexception_view is not None: + httpexception_view = self.maybe_dotted(httpexception_view) + self.add_view(httpexception_view, context=HTTPException) if locale_negotiator: locale_negotiator = self.maybe_dotted(locale_negotiator) registry.registerUtility(locale_negotiator, ILocaleNegotiator) diff --git a/pyramid/httpexceptions.py b/pyramid/httpexceptions.py index f56910b53..cbd87520b 100644 --- a/pyramid/httpexceptions.py +++ b/pyramid/httpexceptions.py @@ -48,3 +48,36 @@ from webob.exc import HTTPBadGateway from webob.exc import HTTPServiceUnavailable from webob.exc import HTTPGatewayTimeout from webob.exc import HTTPVersionNotSupported + +from webob.response import Response + +def abort(status_code, **kw): + """Aborts the request immediately by raising an HTTP exception. The + values in ``*kw`` will be passed to the HTTP exception constructor. + Example:: + + abort(404) # raises an HTTPNotFound exception. + """ + exc = status_map[status_code](**kw) + raise exc.exception + + +def redirect(url, code=302, **kw): + """Raises a redirect exception to the specified URL. + + Optionally, a code variable may be passed with the status code of + the redirect, ie:: + + redirect(route_url('foo', request), code=303) + + """ + exc = status_map[code] + raise exc(location=url, **kw).exception + +def default_httpexception_view(context, request): + if isinstance(context, Response): + # WSGIHTTPException, a Response (2.5+) + return context + # HTTPException, a WSGI app (2.4) + return request.get_response(context) + diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py index 97a93616d..9c8f4875b 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -203,6 +203,38 @@ class ConfiguratorTests(unittest.TestCase): self.assertEqual(config.registry.getUtility(IViewMapperFactory), mapper) + def test_ctor_httpexception_view_default(self): + from zope.interface import implementedBy + from pyramid.httpexceptions import HTTPException + from pyramid.httpexceptions import default_httpexception_view + from pyramid.interfaces import IRequest + config = self._makeOne() + view = self._getViewCallable(config, + ctx_iface=implementedBy(HTTPException), + request_iface=IRequest) + self.failUnless(view is default_httpexception_view) + + def test_ctor_httpexception_view_None(self): + from zope.interface import implementedBy + from pyramid.httpexceptions import HTTPException + from pyramid.interfaces import IRequest + config = self._makeOne(httpexception_view=None) + view = self._getViewCallable(config, + ctx_iface=implementedBy(HTTPException), + request_iface=IRequest) + self.failUnless(view is None) + + def test_ctor_httpexception_view_custom(self): + from zope.interface import implementedBy + from pyramid.httpexceptions import HTTPException + from pyramid.interfaces import IRequest + def httpexception_view(context, request): pass + config = self._makeOne(httpexception_view=httpexception_view) + view = self._getViewCallable(config, + ctx_iface=implementedBy(HTTPException), + request_iface=IRequest) + self.failUnless(view is httpexception_view) + def test_with_package_module(self): from pyramid.tests import test_configuration import pyramid.tests @@ -289,6 +321,20 @@ class ConfiguratorTests(unittest.TestCase): self.assertEqual(views[0], ((default_exceptionresponse_view,), {'context':IExceptionResponse})) + def test_setup_registry_registers_default_httpexception_view(self): + from pyramid.httpexceptions import HTTPException + from pyramid.httpexceptions import default_httpexception_view + class DummyRegistry(object): + def registerUtility(self, *arg, **kw): + pass + reg = DummyRegistry() + config = self._makeOne(reg) + views = [] + config.add_view = lambda *arg, **kw: views.append((arg, kw)) + config.setup_registry() + self.assertEqual(views[1], ((default_httpexception_view,), + {'context':HTTPException})) + def test_setup_registry_explicit_notfound_trumps_iexceptionresponse(self): from zope.interface import implementedBy from pyramid.interfaces import IRequest diff --git a/pyramid/tests/test_httpexceptions.py b/pyramid/tests/test_httpexceptions.py new file mode 100644 index 000000000..843f9485a --- /dev/null +++ b/pyramid/tests/test_httpexceptions.py @@ -0,0 +1,79 @@ +import unittest + +class Test_abort(unittest.TestCase): + def _callFUT(self, *arg, **kw): + from pyramid.httpexceptions import abort + return abort(*arg, **kw) + + def test_status_404(self): + from pyramid.httpexceptions import HTTPNotFound + self.assertRaises(HTTPNotFound().exception.__class__, + self._callFUT, 404) + + def test_status_201(self): + from pyramid.httpexceptions import HTTPCreated + self.assertRaises(HTTPCreated().exception.__class__, + self._callFUT, 201) + + def test_extra_kw(self): + from pyramid.httpexceptions import HTTPNotFound + try: + self._callFUT(404, headers=[('abc', 'def')]) + except HTTPNotFound().exception.__class__, exc: + self.assertEqual(exc.headers['abc'], 'def') + else: # pragma: no cover + raise AssertionError + +class Test_redirect(unittest.TestCase): + def _callFUT(self, *arg, **kw): + from pyramid.httpexceptions import redirect + return redirect(*arg, **kw) + + def test_default(self): + from pyramid.httpexceptions import HTTPFound + try: + self._callFUT('http://example.com') + except HTTPFound().exception.__class__, exc: + self.assertEqual(exc.location, 'http://example.com') + self.assertEqual(exc.status, '302 Found') + + def test_custom_code(self): + from pyramid.httpexceptions import HTTPMovedPermanently + try: + self._callFUT('http://example.com', 301) + except HTTPMovedPermanently().exception.__class__, exc: + self.assertEqual(exc.location, 'http://example.com') + self.assertEqual(exc.status, '301 Moved Permanently') + + def test_extra_kw(self): + from pyramid.httpexceptions import HTTPFound + try: + self._callFUT('http://example.com', headers=[('abc', 'def')]) + except HTTPFound().exception.__class__, exc: + self.assertEqual(exc.location, 'http://example.com') + self.assertEqual(exc.status, '302 Found') + self.assertEqual(exc.headers['abc'], 'def') + + +class Test_default_httpexception_view(unittest.TestCase): + def _callFUT(self, context, request): + from pyramid.httpexceptions import default_httpexception_view + return default_httpexception_view(context, request) + + def test_call_with_response(self): + from pyramid.response import Response + r = Response() + result = self._callFUT(r, None) + self.assertEqual(result, r) + + def test_call_with_nonresponse(self): + request = DummyRequest() + result = self._callFUT(None, request) + self.assertEqual(result, 'response') + +class DummyRequest(object): + def get_response(self, context): + return 'response' + + + -- cgit v1.2.3 From e1e0df9ff85d5d31b355527549e0b5654cce3af9 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 16 May 2011 02:19:37 -0400 Subject: typo --- docs/narr/views.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/narr/views.rst b/docs/narr/views.rst index 465cd3c0d..66e9919e2 100644 --- a/docs/narr/views.rst +++ b/docs/narr/views.rst @@ -365,7 +365,7 @@ An HTTP exception, instead of being raised, can alternately be *returned* Note that :class:`pyramid.exceptions.NotFound` is *not* the same as :class:`pyramid.httpexceptions.HTTPNotFound`. If the latter is raised, the :term:`Not Found view` will *not* be called automatically. Likewise, -:class:`pyramid.exceptions.Foribdden` is not the same exception as +:class:`pyramid.exceptions.Forbidden` is not the same exception as :class:`pyramid.httpexceptions.HTTPForbidden`. If the latter is raised, the :term:`Forbidden view` will not be called automatically. -- cgit v1.2.3 From 8c2a9e6d8d4f089222db7b30324774e94279b0e4 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 26 May 2011 14:13:45 -0400 Subject: work towards unifying NotFound/HTTPNotFound and Forbidden/HTTPForbidden; 2 tests fail --- pyramid/config.py | 52 ++++---- pyramid/exceptions.py | 215 ++++++++++++++++++++++++--------- pyramid/httpexceptions.py | 83 +------------ pyramid/router.py | 2 +- pyramid/testing.py | 2 +- pyramid/tests/forbiddenapp/__init__.py | 4 +- pyramid/tests/test_config.py | 45 +++---- pyramid/tests/test_exceptions.py | 114 ++++++++++++----- pyramid/tests/test_httpexceptions.py | 80 +----------- pyramid/view.py | 31 +---- 10 files changed, 302 insertions(+), 326 deletions(-) diff --git a/pyramid/config.py b/pyramid/config.py index 85a66a837..a20f93a90 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -56,11 +56,10 @@ from pyramid.compat import md5 from pyramid.compat import any from pyramid.events import ApplicationCreated from pyramid.exceptions import ConfigurationError +from pyramid.exceptions import default_exceptionresponse_view from pyramid.exceptions import Forbidden from pyramid.exceptions import NotFound from pyramid.exceptions import PredicateMismatch -from pyramid.httpexceptions import HTTPException -from pyramid.httpexceptions import default_httpexception_view from pyramid.i18n import get_localizer from pyramid.log import make_stream_logger from pyramid.mako_templating import renderer_factory as mako_renderer_factory @@ -82,7 +81,6 @@ from pyramid.traversal import find_interface from pyramid.traversal import traversal_path from pyramid.urldispatch import RoutesMapper from pyramid.util import DottedNameResolver -from pyramid.view import default_exceptionresponse_view from pyramid.view import render_view_to_response from pyramid.view import is_response @@ -142,7 +140,7 @@ class Configurator(object): ``authorization_policy``, ``renderers`` ``debug_logger``, ``locale_negotiator``, ``request_factory``, ``renderer_globals_factory``, ``default_permission``, ``session_factory``, ``default_view_mapper``, - ``autocommit``, and ``httpexception_view``. + ``autocommit``, and ``exceptionresponse_view``. If the ``registry`` argument is passed as a non-``None`` value, it must be an instance of the :class:`pyramid.registry.Registry` @@ -259,15 +257,18 @@ class Configurator(object): default_view_mapper is not passed, a superdefault view mapper will be used. - If ``httpexception_view`` is passed, it must be a :term:`view callable` - or ``None``. If it is a view callable, it will be used as an exception - view callable when an :term:`HTTP exception` is raised (any named - exception from the ``pyramid.httpexceptions`` module) by - :func:`pyramid.httpexceptions.abort`, - :func:`pyramid.httpexceptions.redirect` or 'by hand'. If it is ``None``, - no httpexception view will be registered. By default, the - ``pyramid.httpexceptions.default_httpexception_view`` function is - used. This behavior is new in Pyramid 1.1. """ + If ``exceptionresponse_view`` is passed, it must be a :term:`view + callable` or ``None``. If it is a view callable, it will be used as an + exception view callable when an :term:`exception response` is raised (any + named exception from the ``pyramid.exceptions`` module that begins with + ``HTTP`` as well as the ``NotFound`` and ``Forbidden`` exceptions) as + well as exceptions raised via :func:`pyramid.exceptions.abort`, + :func:`pyramid.exceptions.redirect`. If ``exceptionresponse_view`` is + ``None``, no exception response view will be registered, and all + raised exception responses will be bubbled up to Pyramid's caller. By + default, the ``pyramid.exceptions.default_exceptionresponse_view`` + function is used as the ``exceptionresponse_view``. This argument is new + in Pyramid 1.1. """ manager = manager # for testing injection venusian = venusian # for testing injection @@ -290,7 +291,7 @@ class Configurator(object): session_factory=None, default_view_mapper=None, autocommit=False, - httpexception_view=default_httpexception_view, + exceptionresponse_view=default_exceptionresponse_view, ): if package is None: package = caller_package() @@ -316,7 +317,7 @@ class Configurator(object): default_permission=default_permission, session_factory=session_factory, default_view_mapper=default_view_mapper, - httpexception_view=httpexception_view, + exceptionresponse_view=exceptionresponse_view, ) def _set_settings(self, mapping): @@ -674,7 +675,7 @@ class Configurator(object): locale_negotiator=None, request_factory=None, renderer_globals_factory=None, default_permission=None, session_factory=None, default_view_mapper=None, - httpexception_view=default_httpexception_view): + exceptionresponse_view=default_exceptionresponse_view): """ When you pass a non-``None`` ``registry`` argument to the :term:`Configurator` constructor, no initial 'setup' is performed against the registry. This is because the registry you pass in may @@ -704,11 +705,9 @@ class Configurator(object): authorization_policy) for name, renderer in renderers: self.add_renderer(name, renderer) - self.add_view(default_exceptionresponse_view, - context=IExceptionResponse) - if httpexception_view is not None: - httpexception_view = self.maybe_dotted(httpexception_view) - self.add_view(httpexception_view, context=HTTPException) + if exceptionresponse_view is not None: + exceptionresponse_view = self.maybe_dotted(exceptionresponse_view) + self.add_view(exceptionresponse_view, context=IExceptionResponse) if locale_negotiator: locale_negotiator = self.maybe_dotted(locale_negotiator) registry.registerUtility(locale_negotiator, ILocaleNegotiator) @@ -724,7 +723,7 @@ class Configurator(object): if session_factory is not None: self.set_session_factory(session_factory) # commit before adding default_view_mapper, as the - # default_exceptionresponse_view above requires the superdefault view + # exceptionresponse_view above requires the superdefault view # mapper self.commit() if default_view_mapper is not None: @@ -2703,7 +2702,7 @@ class MultiView(object): return view if view.__predicated__(context, request): return view - raise PredicateMismatch(self.name) + raise PredicateMismatch(self.name).exception def __permitted__(self, context, request): view = self.match(context, request) @@ -2722,7 +2721,7 @@ class MultiView(object): return view(context, request) except PredicateMismatch: continue - raise PredicateMismatch(self.name) + raise PredicateMismatch(self.name).exception def wraps_view(wrapped): def inner(self, view): @@ -2845,7 +2844,7 @@ class ViewDeriver(object): return view(context, request) msg = getattr(request, 'authdebug_message', 'Unauthorized: %s failed permission check' % view) - raise Forbidden(msg, result) + raise Forbidden(msg, result=result).exception _secured_view.__call_permissive__ = view _secured_view.__permitted__ = _permitted _secured_view.__permission__ = permission @@ -2894,7 +2893,8 @@ class ViewDeriver(object): def predicate_wrapper(context, request): if all((predicate(context, request) for predicate in predicates)): return view(context, request) - raise PredicateMismatch('predicate mismatch for view %s' % view) + raise PredicateMismatch( + 'predicate mismatch for view %s' % view).exception def checker(context, request): return all((predicate(context, request) for predicate in predicates)) diff --git a/pyramid/exceptions.py b/pyramid/exceptions.py index 771d71b88..60e3c7b9b 100644 --- a/pyramid/exceptions.py +++ b/pyramid/exceptions.py @@ -1,42 +1,91 @@ from zope.configuration.exceptions import ConfigurationError as ZCE -from zope.interface import implements - -from pyramid.decorator import reify +from zope.interface import classImplements from pyramid.interfaces import IExceptionResponse -import cgi - -class ExceptionResponse(Exception): - """ Abstract class to support behaving as a WSGI response object """ - implements(IExceptionResponse) - status = None - - def __init__(self, message=''): - Exception.__init__(self, message) # B / C - self.message = message - - @reify # defer execution until asked explicitly - def app_iter(self): - return [ - """ - - %s - -

%s

- %s - - - """ % (self.status, self.status, cgi.escape(self.message)) - ] - - @reify # defer execution until asked explicitly - def headerlist(self): - return [ - ('Content-Length', str(len(self.app_iter[0]))), - ('Content-Type', 'text/html') - ] - - -class Forbidden(ExceptionResponse): +from webob.response import Response + +# Documentation proxy import +from webob.exc import __doc__ + +# API: status_map +from webob.exc import status_map +status_map = status_map.copy() # we mutate it + +# API: parent classes +from webob.exc import HTTPException +from webob.exc import WSGIHTTPException +from webob.exc import HTTPOk +from webob.exc import HTTPRedirection +from webob.exc import HTTPError +from webob.exc import HTTPClientError +from webob.exc import HTTPServerError + +# slightly nasty import-time side effect to provide WSGIHTTPException +# with IExceptionResponse interface (used during config.py exception view +# registration) +classImplements(WSGIHTTPException, IExceptionResponse) + +# API: Child classes +from webob.exc import HTTPCreated +from webob.exc import HTTPAccepted +from webob.exc import HTTPNonAuthoritativeInformation +from webob.exc import HTTPNoContent +from webob.exc import HTTPResetContent +from webob.exc import HTTPPartialContent +from webob.exc import HTTPMultipleChoices +from webob.exc import HTTPMovedPermanently +from webob.exc import HTTPFound +from webob.exc import HTTPSeeOther +from webob.exc import HTTPNotModified +from webob.exc import HTTPUseProxy +from webob.exc import HTTPTemporaryRedirect +from webob.exc import HTTPBadRequest +from webob.exc import HTTPUnauthorized +from webob.exc import HTTPPaymentRequired +from webob.exc import HTTPMethodNotAllowed +from webob.exc import HTTPNotAcceptable +from webob.exc import HTTPProxyAuthenticationRequired +from webob.exc import HTTPRequestTimeout +from webob.exc import HTTPConflict +from webob.exc import HTTPGone +from webob.exc import HTTPLengthRequired +from webob.exc import HTTPPreconditionFailed +from webob.exc import HTTPRequestEntityTooLarge +from webob.exc import HTTPRequestURITooLong +from webob.exc import HTTPUnsupportedMediaType +from webob.exc import HTTPRequestRangeNotSatisfiable +from webob.exc import HTTPExpectationFailed +from webob.exc import HTTPInternalServerError +from webob.exc import HTTPNotImplemented +from webob.exc import HTTPBadGateway +from webob.exc import HTTPServiceUnavailable +from webob.exc import HTTPGatewayTimeout +from webob.exc import HTTPVersionNotSupported + +# API: HTTPNotFound and HTTPForbidden (redefined for bw compat) + +from webob.exc import HTTPForbidden as _HTTPForbidden +from webob.exc import HTTPNotFound as _HTTPNotFound + +class HTTPNotFound(_HTTPNotFound): + """ + Raise this exception within :term:`view` code to immediately + return the :term:`Not Found view` to the invoking user. Usually + this is a basic ``404`` page, but the Not Found view can be + customized as necessary. See :ref:`changing_the_notfound_view`. + + This exception's constructor accepts a single positional argument, which + should be a string. The value of this string will be available as the + ``message`` attribute of this exception, for availability to the + :term:`Not Found View`. + """ + def __init__(self, detail=None, headers=None, comment=None, + body_template=None, **kw): + self.message = detail # prevent 2.6.X whining + _HTTPNotFound.__init__(self, detail=detail, headers=headers, + comment=comment, body_template=body_template, + **kw) + +class HTTPForbidden(_HTTPForbidden): """ Raise this exception within :term:`view` code to immediately return the :term:`forbidden view` to the invoking user. Usually this is a basic @@ -58,25 +107,20 @@ class Forbidden(ExceptionResponse): exception as necessary to provide extended information in an error report shown to a user. """ - status = '403 Forbidden' - def __init__(self, message='', result=None): - ExceptionResponse.__init__(self, message) - self.message = message - self.result = result + def __init__(self, detail=None, headers=None, comment=None, + body_template=None, result=None, **kw): + self.message = detail # prevent 2.6.X whining + self.result = result # bw compat + _HTTPForbidden.__init__(self, detail=detail, headers=headers, + comment=comment, body_template=body_template, + **kw) -class NotFound(ExceptionResponse): - """ - Raise this exception within :term:`view` code to immediately - return the :term:`Not Found view` to the invoking user. Usually - this is a basic ``404`` page, but the Not Found view can be - customized as necessary. See :ref:`changing_the_notfound_view`. +NotFound = HTTPNotFound # bw compat +Forbidden = HTTPForbidden # bw compat - This exception's constructor accepts a single positional argument, which - should be a string. The value of this string will be available as the - ``message`` attribute of this exception, for availability to the - :term:`Not Found View`. - """ - status = '404 Not Found' +# patch our status map with subclasses +status_map[403] = HTTPForbidden +status_map[404] = HTTPNotFound class PredicateMismatch(NotFound): """ @@ -102,3 +146,66 @@ class ConfigurationError(ZCE): """ Raised when inappropriate input values are supplied to an API method of a :term:`Configurator`""" + +def abort(status_code, **kw): + """Aborts the request immediately by raising an HTTP exception. The + values in ``*kw`` will be passed to the HTTP exception constructor. + Example:: + + abort(404) # raises an HTTPNotFound exception. + """ + exc = status_map[status_code](**kw) + raise exc.exception + + +def redirect(url, code=302, **kw): + """Raises a redirect exception to the specified URL. + + Optionally, a code variable may be passed with the status code of + the redirect, ie:: + + redirect(route_url('foo', request), code=303) + + """ + exc = status_map[code] + raise exc(location=url, **kw).exception + +def is_response(ob): + """ Return ``True`` if ``ob`` implements the interface implied by + :ref:`the_response`. ``False`` if not. + + .. note:: This isn't a true interface or subclass check. Instead, it's a + duck-typing check, as response objects are not obligated to be of a + particular class or provide any particular Zope interface.""" + + # response objects aren't obligated to implement a Zope interface, + # so we do it the hard way + if ( hasattr(ob, 'app_iter') and hasattr(ob, 'headerlist') and + hasattr(ob, 'status') ): + return True + return False + +newstyle_exceptions = issubclass(Exception, object) + +if newstyle_exceptions: + # webob exceptions will be Response objects (Py 2.5+) + def default_exceptionresponse_view(context, request): + if not isinstance(context, Exception): + # backwards compat for an exception response view registered via + # config.set_notfound_view or config.set_forbidden_view + # instead of as a proper exception view + context = request.exception or context + # WSGIHTTPException, a Response (2.5+) + return context + +else: + # webob exceptions will not be Response objects (Py 2.4) + def default_exceptionresponse_view(context, request): + if not isinstance(context, Exception): + # backwards compat for an exception response view registered via + # config.set_notfound_view or config.set_forbidden_view + # instead of as a proper exception view + context = request.exception or context + # HTTPException, not a Response (2.4) + get_response = getattr(request, 'get_response', lambda c: c) # testing + return get_response(context) diff --git a/pyramid/httpexceptions.py b/pyramid/httpexceptions.py index cbd87520b..8b2a012cc 100644 --- a/pyramid/httpexceptions.py +++ b/pyramid/httpexceptions.py @@ -1,83 +1,2 @@ -from webob.exc import __doc__ -from webob.exc import status_map - -# Parent classes -from webob.exc import HTTPException -from webob.exc import WSGIHTTPException -from webob.exc import HTTPOk -from webob.exc import HTTPRedirection -from webob.exc import HTTPError -from webob.exc import HTTPClientError -from webob.exc import HTTPServerError - -# Child classes -from webob.exc import HTTPCreated -from webob.exc import HTTPAccepted -from webob.exc import HTTPNonAuthoritativeInformation -from webob.exc import HTTPNoContent -from webob.exc import HTTPResetContent -from webob.exc import HTTPPartialContent -from webob.exc import HTTPMultipleChoices -from webob.exc import HTTPMovedPermanently -from webob.exc import HTTPFound -from webob.exc import HTTPSeeOther -from webob.exc import HTTPNotModified -from webob.exc import HTTPUseProxy -from webob.exc import HTTPTemporaryRedirect -from webob.exc import HTTPBadRequest -from webob.exc import HTTPUnauthorized -from webob.exc import HTTPPaymentRequired -from webob.exc import HTTPForbidden -from webob.exc import HTTPNotFound -from webob.exc import HTTPMethodNotAllowed -from webob.exc import HTTPNotAcceptable -from webob.exc import HTTPProxyAuthenticationRequired -from webob.exc import HTTPRequestTimeout -from webob.exc import HTTPConflict -from webob.exc import HTTPGone -from webob.exc import HTTPLengthRequired -from webob.exc import HTTPPreconditionFailed -from webob.exc import HTTPRequestEntityTooLarge -from webob.exc import HTTPRequestURITooLong -from webob.exc import HTTPUnsupportedMediaType -from webob.exc import HTTPRequestRangeNotSatisfiable -from webob.exc import HTTPExpectationFailed -from webob.exc import HTTPInternalServerError -from webob.exc import HTTPNotImplemented -from webob.exc import HTTPBadGateway -from webob.exc import HTTPServiceUnavailable -from webob.exc import HTTPGatewayTimeout -from webob.exc import HTTPVersionNotSupported - -from webob.response import Response - -def abort(status_code, **kw): - """Aborts the request immediately by raising an HTTP exception. The - values in ``*kw`` will be passed to the HTTP exception constructor. - Example:: - - abort(404) # raises an HTTPNotFound exception. - """ - exc = status_map[status_code](**kw) - raise exc.exception - - -def redirect(url, code=302, **kw): - """Raises a redirect exception to the specified URL. - - Optionally, a code variable may be passed with the status code of - the redirect, ie:: - - redirect(route_url('foo', request), code=303) - - """ - exc = status_map[code] - raise exc(location=url, **kw).exception - -def default_httpexception_view(context, request): - if isinstance(context, Response): - # WSGIHTTPException, a Response (2.5+) - return context - # HTTPException, a WSGI app (2.4) - return request.get_response(context) +from pyramid.exceptions import * # bw compat diff --git a/pyramid/router.py b/pyramid/router.py index b8a8639aa..069db52bc 100644 --- a/pyramid/router.py +++ b/pyramid/router.py @@ -153,7 +153,7 @@ class Router(object): logger and logger.debug(msg) else: msg = request.path_info - raise NotFound(msg) + raise NotFound(msg).exception else: response = view_callable(context, request) diff --git a/pyramid/testing.py b/pyramid/testing.py index 36cc38830..a512ede4b 100644 --- a/pyramid/testing.py +++ b/pyramid/testing.py @@ -217,7 +217,7 @@ def registerView(name, result='', view=None, for_=(Interface, Interface), else: def _secure(context, request): if not has_permission(permission, context, request): - raise Forbidden('no permission') + raise Forbidden('no permission').exception else: return view(context, request) _secure.__call_permissive__ = view diff --git a/pyramid/tests/forbiddenapp/__init__.py b/pyramid/tests/forbiddenapp/__init__.py index ed9aa8357..614aff037 100644 --- a/pyramid/tests/forbiddenapp/__init__.py +++ b/pyramid/tests/forbiddenapp/__init__.py @@ -1,6 +1,4 @@ -from cgi import escape from webob import Response -from pyramid.httpexceptions import HTTPForbidden from pyramid.exceptions import Forbidden def x_view(request): # pragma: no cover @@ -10,7 +8,7 @@ def forbidden_view(context, request): msg = context.message result = context.result message = msg + '\n' + str(result) - resp = HTTPForbidden() + resp = Forbidden() resp.body = message return resp diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py index 9c8f4875b..7c6389253 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -204,36 +204,33 @@ class ConfiguratorTests(unittest.TestCase): mapper) def test_ctor_httpexception_view_default(self): - from zope.interface import implementedBy - from pyramid.httpexceptions import HTTPException - from pyramid.httpexceptions import default_httpexception_view + from pyramid.interfaces import IExceptionResponse + from pyramid.exceptions import default_exceptionresponse_view from pyramid.interfaces import IRequest config = self._makeOne() view = self._getViewCallable(config, - ctx_iface=implementedBy(HTTPException), + ctx_iface=IExceptionResponse, request_iface=IRequest) - self.failUnless(view is default_httpexception_view) + self.failUnless(view is default_exceptionresponse_view) - def test_ctor_httpexception_view_None(self): - from zope.interface import implementedBy - from pyramid.httpexceptions import HTTPException + def test_ctor_exceptionresponse_view_None(self): + from pyramid.interfaces import IExceptionResponse from pyramid.interfaces import IRequest - config = self._makeOne(httpexception_view=None) + config = self._makeOne(exceptionresponse_view=None) view = self._getViewCallable(config, - ctx_iface=implementedBy(HTTPException), + ctx_iface=IExceptionResponse, request_iface=IRequest) self.failUnless(view is None) - def test_ctor_httpexception_view_custom(self): - from zope.interface import implementedBy - from pyramid.httpexceptions import HTTPException + def test_ctor_exceptionresponse_view_custom(self): + from pyramid.interfaces import IExceptionResponse from pyramid.interfaces import IRequest - def httpexception_view(context, request): pass - config = self._makeOne(httpexception_view=httpexception_view) + def exceptionresponse_view(context, request): pass + config = self._makeOne(exceptionresponse_view=exceptionresponse_view) view = self._getViewCallable(config, - ctx_iface=implementedBy(HTTPException), + ctx_iface=IExceptionResponse, request_iface=IRequest) - self.failUnless(view is httpexception_view) + self.failUnless(view is exceptionresponse_view) def test_with_package_module(self): from pyramid.tests import test_configuration @@ -321,20 +318,6 @@ class ConfiguratorTests(unittest.TestCase): self.assertEqual(views[0], ((default_exceptionresponse_view,), {'context':IExceptionResponse})) - def test_setup_registry_registers_default_httpexception_view(self): - from pyramid.httpexceptions import HTTPException - from pyramid.httpexceptions import default_httpexception_view - class DummyRegistry(object): - def registerUtility(self, *arg, **kw): - pass - reg = DummyRegistry() - config = self._makeOne(reg) - views = [] - config.add_view = lambda *arg, **kw: views.append((arg, kw)) - config.setup_registry() - self.assertEqual(views[1], ((default_httpexception_view,), - {'context':HTTPException})) - def test_setup_registry_explicit_notfound_trumps_iexceptionresponse(self): from zope.interface import implementedBy from pyramid.interfaces import IRequest diff --git a/pyramid/tests/test_exceptions.py b/pyramid/tests/test_exceptions.py index 5d0fa1e1a..2e9279f66 100644 --- a/pyramid/tests/test_exceptions.py +++ b/pyramid/tests/test_exceptions.py @@ -1,37 +1,16 @@ import unittest -class TestExceptionResponse(unittest.TestCase): - def _makeOne(self, message): - from pyramid.exceptions import ExceptionResponse - return ExceptionResponse(message) - - def test_app_iter(self): - exc = self._makeOne('') - self.assertTrue('' in exc.app_iter[0]) - - def test_headerlist(self): - exc = self._makeOne('') - headerlist = exc.headerlist - headerlist.sort() - app_iter = exc.app_iter - clen = str(len(app_iter[0])) - self.assertEqual(headerlist[0], ('Content-Length', clen)) - self.assertEqual(headerlist[1], ('Content-Type', 'text/html')) - - def test_withmessage(self): - exc = self._makeOne('abc&123') - self.assertTrue('abc&123' in exc.app_iter[0]) - class TestNotFound(unittest.TestCase): def _makeOne(self, message): from pyramid.exceptions import NotFound return NotFound(message) def test_it(self): - from pyramid.exceptions import ExceptionResponse + from pyramid.interfaces import IExceptionResponse e = self._makeOne('notfound') - self.assertTrue(isinstance(e, ExceptionResponse)) + self.assertTrue(IExceptionResponse.providedBy(e)) self.assertEqual(e.status, '404 Not Found') + self.assertEqual(e.message, 'notfound') class TestForbidden(unittest.TestCase): def _makeOne(self, message): @@ -39,7 +18,88 @@ class TestForbidden(unittest.TestCase): return Forbidden(message) def test_it(self): - from pyramid.exceptions import ExceptionResponse - e = self._makeOne('unauthorized') - self.assertTrue(isinstance(e, ExceptionResponse)) + from pyramid.interfaces import IExceptionResponse + e = self._makeOne('forbidden') + self.assertTrue(IExceptionResponse.providedBy(e)) self.assertEqual(e.status, '403 Forbidden') + self.assertEqual(e.message, 'forbidden') + +class Test_abort(unittest.TestCase): + def _callFUT(self, *arg, **kw): + from pyramid.exceptions import abort + return abort(*arg, **kw) + + def test_status_404(self): + from pyramid.exceptions import HTTPNotFound + self.assertRaises(HTTPNotFound().exception.__class__, + self._callFUT, 404) + + def test_status_201(self): + from pyramid.exceptions import HTTPCreated + self.assertRaises(HTTPCreated().exception.__class__, + self._callFUT, 201) + + def test_extra_kw(self): + from pyramid.exceptions import HTTPNotFound + try: + self._callFUT(404, headers=[('abc', 'def')]) + except HTTPNotFound().exception.__class__, exc: + self.assertEqual(exc.headers['abc'], 'def') + else: # pragma: no cover + raise AssertionError + +class Test_redirect(unittest.TestCase): + def _callFUT(self, *arg, **kw): + from pyramid.exceptions import redirect + return redirect(*arg, **kw) + + def test_default(self): + from pyramid.exceptions import HTTPFound + try: + self._callFUT('http://example.com') + except HTTPFound().exception.__class__, exc: + self.assertEqual(exc.location, 'http://example.com') + self.assertEqual(exc.status, '302 Found') + + def test_custom_code(self): + from pyramid.exceptions import HTTPMovedPermanently + try: + self._callFUT('http://example.com', 301) + except HTTPMovedPermanently().exception.__class__, exc: + self.assertEqual(exc.location, 'http://example.com') + self.assertEqual(exc.status, '301 Moved Permanently') + + def test_extra_kw(self): + from pyramid.exceptions import HTTPFound + try: + self._callFUT('http://example.com', headers=[('abc', 'def')]) + except HTTPFound().exception.__class__, exc: + self.assertEqual(exc.location, 'http://example.com') + self.assertEqual(exc.status, '302 Found') + self.assertEqual(exc.headers['abc'], 'def') + + +class Test_default_exceptionresponse_view(unittest.TestCase): + def _callFUT(self, context, request): + from pyramid.exceptions import default_exceptionresponse_view + return default_exceptionresponse_view(context, request) + + def test_call_with_exception(self): + context = Exception() + result = self._callFUT(context, None) + self.assertEqual(result, context) + + def test_call_with_nonexception(self): + request = DummyRequest() + context = Exception() + request.exception = context + result = self._callFUT(None, request) + self.assertEqual(result, context) + +class DummyRequest(object): + exception = None + def get_response(self, context): + return 'response' + + + diff --git a/pyramid/tests/test_httpexceptions.py b/pyramid/tests/test_httpexceptions.py index 843f9485a..afa5a94de 100644 --- a/pyramid/tests/test_httpexceptions.py +++ b/pyramid/tests/test_httpexceptions.py @@ -1,79 +1,9 @@ import unittest -class Test_abort(unittest.TestCase): - def _callFUT(self, *arg, **kw): - from pyramid.httpexceptions import abort - return abort(*arg, **kw) - - def test_status_404(self): - from pyramid.httpexceptions import HTTPNotFound - self.assertRaises(HTTPNotFound().exception.__class__, - self._callFUT, 404) - - def test_status_201(self): - from pyramid.httpexceptions import HTTPCreated - self.assertRaises(HTTPCreated().exception.__class__, - self._callFUT, 201) - - def test_extra_kw(self): - from pyramid.httpexceptions import HTTPNotFound - try: - self._callFUT(404, headers=[('abc', 'def')]) - except HTTPNotFound().exception.__class__, exc: - self.assertEqual(exc.headers['abc'], 'def') - else: # pragma: no cover - raise AssertionError - -class Test_redirect(unittest.TestCase): - def _callFUT(self, *arg, **kw): - from pyramid.httpexceptions import redirect - return redirect(*arg, **kw) - - def test_default(self): - from pyramid.httpexceptions import HTTPFound - try: - self._callFUT('http://example.com') - except HTTPFound().exception.__class__, exc: - self.assertEqual(exc.location, 'http://example.com') - self.assertEqual(exc.status, '302 Found') - - def test_custom_code(self): - from pyramid.httpexceptions import HTTPMovedPermanently - try: - self._callFUT('http://example.com', 301) - except HTTPMovedPermanently().exception.__class__, exc: - self.assertEqual(exc.location, 'http://example.com') - self.assertEqual(exc.status, '301 Moved Permanently') +class TestIt(unittest.TestCase): + def test_bwcompat_imports(self): + from pyramid.httpexceptions import HTTPNotFound as one + from pyramid.exceptions import HTTPNotFound as two + self.assertTrue(one is two) - def test_extra_kw(self): - from pyramid.httpexceptions import HTTPFound - try: - self._callFUT('http://example.com', headers=[('abc', 'def')]) - except HTTPFound().exception.__class__, exc: - self.assertEqual(exc.location, 'http://example.com') - self.assertEqual(exc.status, '302 Found') - self.assertEqual(exc.headers['abc'], 'def') - - -class Test_default_httpexception_view(unittest.TestCase): - def _callFUT(self, context, request): - from pyramid.httpexceptions import default_httpexception_view - return default_httpexception_view(context, request) - - def test_call_with_response(self): - from pyramid.response import Response - r = Response() - result = self._callFUT(r, None) - self.assertEqual(result, r) - def test_call_with_nonresponse(self): - request = DummyRequest() - result = self._callFUT(None, request) - self.assertEqual(result, 'response') - -class DummyRequest(object): - def get_response(self, context): - return 'response' - - - diff --git a/pyramid/view.py b/pyramid/view.py index 2563f1e43..d6b666cf2 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -8,7 +8,9 @@ from pyramid.interfaces import IRoutesMapper from pyramid.interfaces import IView from pyramid.interfaces import IViewClassifier -from pyramid.httpexceptions import HTTPFound +from pyramid.exceptions import HTTPFound +from pyramid.exceptions import default_exceptionresponse_view +from pyramid.exceptions import is_response # API from pyramid.renderers import RendererHelper from pyramid.static import static_view from pyramid.threadlocal import get_current_registry @@ -130,21 +132,6 @@ def render_view(context, request, name='', secure=True): return None return ''.join(iterable) -def is_response(ob): - """ Return ``True`` if ``ob`` implements the interface implied by - :ref:`the_response`. ``False`` if not. - - .. note:: This isn't a true interface or subclass check. Instead, it's a - duck-typing check, as response objects are not obligated to be of a - particular class or provide any particular Zope interface.""" - - # response objects aren't obligated to implement a Zope interface, - # so we do it the hard way - if ( hasattr(ob, 'app_iter') and hasattr(ob, 'headerlist') and - hasattr(ob, 'status') ): - return True - return False - class view_config(object): """ A function, class or method :term:`decorator` which allows a developer to create view registrations nearer to a :term:`view @@ -243,14 +230,6 @@ deprecated( 'pyramid.view.view_config instead (API-compat, simple ' 'rename).') -def default_exceptionresponse_view(context, request): - if not isinstance(context, Exception): - # backwards compat for an exception response view registered via - # config.set_notfound_view or config.set_forbidden_view - # instead of as a proper exception view - context = request.exception or context - return context - class AppendSlashNotFoundViewFactory(object): """ There can only be one :term:`Not Found view` in any :app:`Pyramid` application. Even if you use @@ -273,7 +252,7 @@ class AppendSlashNotFoundViewFactory(object): from pyramid.exceptions import NotFound from pyramid.view import AppendSlashNotFoundViewFactory - from pyramid.httpexceptions import HTTPNotFound + from pyramid.exceptions import HTTPNotFound def notfound_view(context, request): return HTTPNotFound('nope') @@ -294,7 +273,7 @@ class AppendSlashNotFoundViewFactory(object): if not isinstance(context, Exception): # backwards compat for an append_notslash_view registered via # config.set_notfound_view instead of as a proper exception view - context = request.exception + context = getattr(request, 'exception', None) or context path = request.path registry = request.registry mapper = registry.queryUtility(IRoutesMapper) -- cgit v1.2.3 From f77703d25056236d027a1a61bf63fab3a7c1b2c2 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 28 May 2011 00:13:06 -0400 Subject: horrid workaround for no app_iter initialized --- pyramid/exceptions.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pyramid/exceptions.py b/pyramid/exceptions.py index 60e3c7b9b..e9718c6ab 100644 --- a/pyramid/exceptions.py +++ b/pyramid/exceptions.py @@ -84,6 +84,12 @@ class HTTPNotFound(_HTTPNotFound): _HTTPNotFound.__init__(self, detail=detail, headers=headers, comment=comment, body_template=body_template, **kw) + if not ('body' in kw or 'app_iter' in kw): + if not self.empty_body: + body = self.html_body(self.environ) + if isinstance(body, unicode): + body = body.encode(self.charset) + self.body = body class HTTPForbidden(_HTTPForbidden): """ @@ -114,6 +120,12 @@ class HTTPForbidden(_HTTPForbidden): _HTTPForbidden.__init__(self, detail=detail, headers=headers, comment=comment, body_template=body_template, **kw) + if not ('body' in kw or 'app_iter' in kw): + if not self.empty_body: + body = self.html_body(self.environ) + if isinstance(body, unicode): + body = body.encode(self.charset) + self.body = body NotFound = HTTPNotFound # bw compat Forbidden = HTTPForbidden # bw compat -- cgit v1.2.3 From 4184d956514ada7dccf2f99ced09cbf07a721cc3 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 28 May 2011 19:22:13 -0400 Subject: bite the bullet and replace all webob.exc classes with ones of our own --- pyramid/exceptions.py | 1144 +++++++++++++++++++++++++++++++++----- pyramid/tests/test_exceptions.py | 299 +++++++++- 2 files changed, 1308 insertions(+), 135 deletions(-) diff --git a/pyramid/exceptions.py b/pyramid/exceptions.py index e9718c6ab..53cb0e5a8 100644 --- a/pyramid/exceptions.py +++ b/pyramid/exceptions.py @@ -1,139 +1,1033 @@ +""" +HTTP Exceptions +--------------- + +This module contains Python exceptions that relate to HTTP status codes by +defining a set of classes, all subclasses of HTTPException. Each +exception, in addition to being a Python exception that can be raised and +caught, is also a ``Response`` object. + +This module defines exceptions according to RFC 2068 [1]_ : codes with +100-300 are not really errors; 400's are client errors, and 500's are +server errors. + +Exception + HTTPException + HTTPOk + * 200 - HTTPOk + * 201 - HTTPCreated + * 202 - HTTPAccepted + * 203 - HTTPNonAuthoritativeInformation + * 204 - HTTPNoContent + * 205 - HTTPResetContent + * 206 - HTTPPartialContent + HTTPRedirection + * 300 - HTTPMultipleChoices + * 301 - HTTPMovedPermanently + * 302 - HTTPFound + * 303 - HTTPSeeOther + * 304 - HTTPNotModified + * 305 - HTTPUseProxy + * 306 - Unused (not implemented, obviously) + * 307 - HTTPTemporaryRedirect + HTTPError + HTTPClientError + * 400 - HTTPBadRequest + * 401 - HTTPUnauthorized + * 402 - HTTPPaymentRequired + * 403 - HTTPForbidden + * 404 - HTTPNotFound + * 405 - HTTPMethodNotAllowed + * 406 - HTTPNotAcceptable + * 407 - HTTPProxyAuthenticationRequired + * 408 - HTTPRequestTimeout + * 409 - HTTPConflict + * 410 - HTTPGone + * 411 - HTTPLengthRequired + * 412 - HTTPPreconditionFailed + * 413 - HTTPRequestEntityTooLarge + * 414 - HTTPRequestURITooLong + * 415 - HTTPUnsupportedMediaType + * 416 - HTTPRequestRangeNotSatisfiable + * 417 - HTTPExpectationFailed + HTTPServerError + * 500 - HTTPInternalServerError + * 501 - HTTPNotImplemented + * 502 - HTTPBadGateway + * 503 - HTTPServiceUnavailable + * 504 - HTTPGatewayTimeout + * 505 - HTTPVersionNotSupported + +Each HTTP exception has the following attributes: + + ``code`` + the HTTP status code for the exception + + ``title`` + remainder of the status line (stuff after the code) + + ``explanation`` + a plain-text explanation of the error message that is + not subject to environment or header substitutions; + it is accessible in the template via %(explanation)s + + ``detail`` + a plain-text message customization that is not subject + to environment or header substitutions; accessible in + the template via %(detail)s + + ``body_template`` + a content fragment (in HTML) used for environment and + header substitution; the default template includes both + the explanation and further detail provided in the + message + +Each HTTP exception accepts the following parameters: + + ``detail`` + a plain-text override of the default ``detail`` + + ``headers`` + a list of (k,v) header pairs + + ``comment`` + a plain-text additional information which is + usually stripped/hidden for end-users + + ``body_template`` + a string.Template object containing a content fragment in HTML + that frames the explanation and further detail + +Substitution of environment variables and headers into template values is +performed if a ``request`` is passed to the exception constructor. + +The subclasses of :class:`~_HTTPMove` +(:class:`~HTTPMultipleChoices`, :class:`~HTTPMovedPermanently`, +:class:`~HTTPFound`, :class:`~HTTPSeeOther`, :class:`~HTTPUseProxy` and +:class:`~HTTPTemporaryRedirect`) are redirections that require a ``Location`` +field. Reflecting this, these subclasses have one additional keyword argument: +``location``, which indicates the location to which to redirect. + +References: + +.. [1] http://www.python.org/peps/pep-0333.html#error-handling +""" + +import types +from string import Template +from webob import Response +from webob import html_escape + from zope.configuration.exceptions import ConfigurationError as ZCE -from zope.interface import classImplements +from zope.interface import implements from pyramid.interfaces import IExceptionResponse -from webob.response import Response - -# Documentation proxy import -from webob.exc import __doc__ - -# API: status_map -from webob.exc import status_map -status_map = status_map.copy() # we mutate it - -# API: parent classes -from webob.exc import HTTPException -from webob.exc import WSGIHTTPException -from webob.exc import HTTPOk -from webob.exc import HTTPRedirection -from webob.exc import HTTPError -from webob.exc import HTTPClientError -from webob.exc import HTTPServerError - -# slightly nasty import-time side effect to provide WSGIHTTPException -# with IExceptionResponse interface (used during config.py exception view -# registration) -classImplements(WSGIHTTPException, IExceptionResponse) - -# API: Child classes -from webob.exc import HTTPCreated -from webob.exc import HTTPAccepted -from webob.exc import HTTPNonAuthoritativeInformation -from webob.exc import HTTPNoContent -from webob.exc import HTTPResetContent -from webob.exc import HTTPPartialContent -from webob.exc import HTTPMultipleChoices -from webob.exc import HTTPMovedPermanently -from webob.exc import HTTPFound -from webob.exc import HTTPSeeOther -from webob.exc import HTTPNotModified -from webob.exc import HTTPUseProxy -from webob.exc import HTTPTemporaryRedirect -from webob.exc import HTTPBadRequest -from webob.exc import HTTPUnauthorized -from webob.exc import HTTPPaymentRequired -from webob.exc import HTTPMethodNotAllowed -from webob.exc import HTTPNotAcceptable -from webob.exc import HTTPProxyAuthenticationRequired -from webob.exc import HTTPRequestTimeout -from webob.exc import HTTPConflict -from webob.exc import HTTPGone -from webob.exc import HTTPLengthRequired -from webob.exc import HTTPPreconditionFailed -from webob.exc import HTTPRequestEntityTooLarge -from webob.exc import HTTPRequestURITooLong -from webob.exc import HTTPUnsupportedMediaType -from webob.exc import HTTPRequestRangeNotSatisfiable -from webob.exc import HTTPExpectationFailed -from webob.exc import HTTPInternalServerError -from webob.exc import HTTPNotImplemented -from webob.exc import HTTPBadGateway -from webob.exc import HTTPServiceUnavailable -from webob.exc import HTTPGatewayTimeout -from webob.exc import HTTPVersionNotSupported - -# API: HTTPNotFound and HTTPForbidden (redefined for bw compat) - -from webob.exc import HTTPForbidden as _HTTPForbidden -from webob.exc import HTTPNotFound as _HTTPNotFound - -class HTTPNotFound(_HTTPNotFound): - """ - Raise this exception within :term:`view` code to immediately - return the :term:`Not Found view` to the invoking user. Usually - this is a basic ``404`` page, but the Not Found view can be - customized as necessary. See :ref:`changing_the_notfound_view`. - This exception's constructor accepts a single positional argument, which - should be a string. The value of this string will be available as the - ``message`` attribute of this exception, for availability to the - :term:`Not Found View`. +newstyle_exceptions = issubclass(Exception, object) + +def no_escape(value): + if value is None: + return '' + if not isinstance(value, basestring): + if hasattr(value, '__unicode__'): + value = unicode(value) + else: + value = str(value) + return value + +class HTTPException(Exception): + implements(IExceptionResponse) """ + Exception used on pre-Python-2.5, where new-style classes cannot be used as + an exception. + """ + + def __init__(self, message, wsgi_response): + self.message = message + Exception.__init__(self, message) + self.__dict__['wsgi_response'] = wsgi_response + + def exception(self): + return self + + exception = property(exception) + + # for old style exceptions + if not newstyle_exceptions: #pragma NO COVERAGE + def __getattr__(self, attr): + if not attr.startswith('_'): + return getattr(self.wsgi_response, attr) + else: + raise AttributeError(attr) + + def __setattr__(self, attr, value): + if attr.startswith('_') or attr in ('args',): + self.__dict__[attr] = value + else: + setattr(self.wsgi_response, attr, value) + +class WSGIHTTPException(Response, HTTPException): + + ## You should set in subclasses: + # code = 200 + # title = 'OK' + # explanation = 'why this happens' + # body_template_obj = Template('response template') + + # differences from webob.exc.WSGIHTTPException: + # - not a WSGI application (just a response) + # + # as a result: + # + # - bases plaintext vs. html result on self.content_type rather than + # on request environ + # + # - doesn't add request.environ keys to template substitutions unless + # 'request' is passed as a keyword argument. + # + # - doesn't use "strip_tags" (${br} placeholder for
, no other html + # in default body template) + # + # - sets a default app_iter if no body, app_iter, or unicode_body is + # passed + # + # - explicitly sets self.message = detail to prevent whining by Python + # 2.6.5+ Exception.message + # + code = None + title = None + explanation = '' + body_template_obj = Template('''\ +${explanation}${br}${br} +${detail} +${html_comment} +''') + + plain_template_obj = Template('''\ +${status} + +${body}''') + + html_template_obj = Template('''\ + + + ${status} + + +

${status}

+ ${body} + +''') + + ## Set this to True for responses that should have no request body + empty_body = False + _default_called = False + def __init__(self, detail=None, headers=None, comment=None, body_template=None, **kw): - self.message = detail # prevent 2.6.X whining - _HTTPNotFound.__init__(self, detail=detail, headers=headers, - comment=comment, body_template=body_template, - **kw) - if not ('body' in kw or 'app_iter' in kw): - if not self.empty_body: - body = self.html_body(self.environ) - if isinstance(body, unicode): - body = body.encode(self.charset) - self.body = body - -class HTTPForbidden(_HTTPForbidden): + status = '%s %s' % (self.code, self.title) + Response.__init__(self, status=status, **kw) + Exception.__init__(self, detail) + self.detail = self.message = detail + if headers: + self.headers.extend(headers) + self.comment = comment + if body_template is not None: + self.body_template = body_template + self.body_template_obj = Template(body_template) + + if self.empty_body: + del self.content_type + del self.content_length + elif not ('unicode_body' in kw or 'body' in kw or 'app_iter' in kw): + self.app_iter = iter(self._default_app_iter, None) + + def __str__(self): + return self.detail or self.explanation + + def _default_app_iter(self): + if self._default_called: + return None + html_comment = '' + comment = self.comment or '' + if 'html' in self.content_type or '': + escape = html_escape + page_template = self.html_template_obj + br = '
' + if comment: + html_comment = '' % escape(comment) + else: + escape = no_escape + page_template = self.plain_template_obj + br = '\n' + if comment: + html_comment = escape(comment) + args = { + 'br':br, + 'explanation': escape(self.explanation), + 'detail': escape(self.detail or ''), + 'comment': escape(comment), + 'html_comment':html_comment, + } + body_tmpl = self.body_template_obj + if WSGIHTTPException.body_template_obj is not body_tmpl: + # Custom template; add headers to args + environ = self.environ + if environ is not None: + for k, v in environ.items(): + args[k] = escape(v) + for k, v in self.headers.items(): + args[k.lower()] = escape(v) + body = body_tmpl.substitute(args) + page = page_template.substitute(status=self.status, body=body) + if isinstance(page, unicode): + page = page.encode(self.charset) + self._default_called = True + return page + + def wsgi_response(self): + return self + + wsgi_response = property(wsgi_response) + + def exception(self): + if newstyle_exceptions: + return self + else: + return HTTPException(self.detail, self) + + exception = property(exception) + +class HTTPError(WSGIHTTPException): + """ + base class for status codes in the 400's and 500's + + This is an exception which indicates that an error has occurred, + and that any work in progress should not be committed. These are + typically results in the 400's and 500's. + """ + +class HTTPRedirection(WSGIHTTPException): + """ + base class for 300's status code (redirections) + + This is an abstract base class for 3xx redirection. It indicates + that further action needs to be taken by the user agent in order + to fulfill the request. It does not necessarly signal an error + condition. + """ + +class HTTPOk(WSGIHTTPException): + """ + Base class for the 200's status code (successful responses) + + code: 200, title: OK + """ + code = 200 + title = 'OK' + +############################################################ +## 2xx success +############################################################ + +class HTTPCreated(HTTPOk): + """ + subclass of :class:`~HTTPOk` + + This indicates that request has been fulfilled and resulted in a new + resource being created. + + code: 201, title: Created + """ + code = 201 + title = 'Created' + +class HTTPAccepted(HTTPOk): + """ + subclass of :class:`~HTTPOk` + + This indicates that the request has been accepted for processing, but the + processing has not been completed. + + code: 202, title: Accepted + """ + code = 202 + title = 'Accepted' + explanation = 'The request is accepted for processing.' + +class HTTPNonAuthoritativeInformation(HTTPOk): + """ + subclass of :class:`~HTTPOk` + + This indicates that the returned metainformation in the entity-header is + not the definitive set as available from the origin server, but is + gathered from a local or a third-party copy. + + code: 203, title: Non-Authoritative Information + """ + code = 203 + title = 'Non-Authoritative Information' + +class HTTPNoContent(HTTPOk): + """ + subclass of :class:`~HTTPOk` + + This indicates that the server has fulfilled the request but does + not need to return an entity-body, and might want to return updated + metainformation. + + code: 204, title: No Content + """ + code = 204 + title = 'No Content' + empty_body = True + +class HTTPResetContent(HTTPOk): + """ + subclass of :class:`~HTTPOk` + + This indicates that the the server has fulfilled the request and + the user agent SHOULD reset the document view which caused the + request to be sent. + + code: 205, title: Reset Content + """ + code = 205 + title = 'Reset Content' + empty_body = True + +class HTTPPartialContent(HTTPOk): + """ + subclass of :class:`~HTTPOk` + + This indicates that the server has fulfilled the partial GET + request for the resource. + + code: 206, title: Partial Content + """ + code = 206 + title = 'Partial Content' + +## FIXME: add 207 Multi-Status (but it's complicated) + +############################################################ +## 3xx redirection +############################################################ + +class _HTTPMove(HTTPRedirection): + """ + redirections which require a Location field + + Since a 'Location' header is a required attribute of 301, 302, 303, + 305 and 307 (but not 304), this base class provides the mechanics to + make this easy. + + You must provide a ``location`` keyword argument. + """ + # differences from webob.exc._HTTPMove: + # + # - not a wsgi app + # + # - ${location} isn't wrapped in an tag in body + # + # - location keyword arg defaults to '' + # + # - ``add_slash`` argument is no longer accepted: code that passes + # add_slash argument will receive an exception. + explanation = 'The resource has been moved to' + body_template_obj = Template('''\ +${explanation} ${location}; +you should be redirected automatically. +${detail} +${html_comment}''') + + def __init__(self, detail=None, headers=None, comment=None, + body_template=None, location='', **kw): + super(_HTTPMove, self).__init__( + detail=detail, headers=headers, comment=comment, + body_template=body_template, location=location, **kw) + +class HTTPMultipleChoices(_HTTPMove): + """ + subclass of :class:`~_HTTPMove` + + This indicates that the requested resource corresponds to any one + of a set of representations, each with its own specific location, + and agent-driven negotiation information is being provided so that + the user can select a preferred representation and redirect its + request to that location. + + code: 300, title: Multiple Choices + """ + code = 300 + title = 'Multiple Choices' + +class HTTPMovedPermanently(_HTTPMove): + """ + subclass of :class:`~_HTTPMove` + + This indicates that the requested resource has been assigned a new + permanent URI and any future references to this resource SHOULD use + one of the returned URIs. + + code: 301, title: Moved Permanently + """ + code = 301 + title = 'Moved Permanently' + +class HTTPFound(_HTTPMove): + """ + subclass of :class:`~_HTTPMove` + + This indicates that the requested resource resides temporarily under + a different URI. + + code: 302, title: Found + """ + code = 302 + title = 'Found' + explanation = 'The resource was found at' + +# This one is safe after a POST (the redirected location will be +# retrieved with GET): +class HTTPSeeOther(_HTTPMove): + """ + subclass of :class:`~_HTTPMove` + + This indicates that the response to the request can be found under + a different URI and SHOULD be retrieved using a GET method on that + resource. + + code: 303, title: See Other + """ + code = 303 + title = 'See Other' + +class HTTPNotModified(HTTPRedirection): + """ + subclass of :class:`~HTTPRedirection` + + This indicates that if the client has performed a conditional GET + request and access is allowed, but the document has not been + modified, the server SHOULD respond with this status code. + + code: 304, title: Not Modified + """ + # FIXME: this should include a date or etag header + code = 304 + title = 'Not Modified' + empty_body = True + +class HTTPUseProxy(_HTTPMove): + """ + subclass of :class:`~_HTTPMove` + + This indicates that the requested resource MUST be accessed through + the proxy given by the Location field. + + code: 305, title: Use Proxy + """ + # Not a move, but looks a little like one + code = 305 + title = 'Use Proxy' + explanation = ( + 'The resource must be accessed through a proxy located at') + +class HTTPTemporaryRedirect(_HTTPMove): + """ + subclass of :class:`~_HTTPMove` + + This indicates that the requested resource resides temporarily + under a different URI. + + code: 307, title: Temporary Redirect + """ + code = 307 + title = 'Temporary Redirect' + +############################################################ +## 4xx client error +############################################################ + +class HTTPClientError(HTTPError): + """ + base class for the 400's, where the client is in error + + This is an error condition in which the client is presumed to be + in-error. This is an expected problem, and thus is not considered + a bug. A server-side traceback is not warranted. Unless specialized, + this is a '400 Bad Request' + """ + code = 400 + title = 'Bad Request' + explanation = ('The server could not comply with the request since\r\n' + 'it is either malformed or otherwise incorrect.\r\n') + +class HTTPBadRequest(HTTPClientError): + pass + +class HTTPUnauthorized(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the request requires user authentication. + + code: 401, title: Unauthorized """ + code = 401 + title = 'Unauthorized' + explanation = ( + 'This server could not verify that you are authorized to\r\n' + 'access the document you requested. Either you supplied the\r\n' + 'wrong credentials (e.g., bad password), or your browser\r\n' + 'does not understand how to supply the credentials required.\r\n') + +class HTTPPaymentRequired(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + code: 402, title: Payment Required + """ + code = 402 + title = 'Payment Required' + explanation = ('Access was denied for financial reasons.') + +class HTTPForbidden(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the server understood the request, but is + refusing to fulfill it. + + code: 403, title: Forbidden + Raise this exception within :term:`view` code to immediately return the :term:`forbidden view` to the invoking user. Usually this is a basic ``403`` page, but the forbidden view can be customized as necessary. See :ref:`changing_the_forbidden_view`. A ``Forbidden`` exception will be the ``context`` of a :term:`Forbidden View`. - This exception's constructor accepts two arguments. The first argument, - ``message``, should be a string. The value of this string will be used - as the ``message`` attribute of the exception object. The second - argument, ``result`` is usually an instance of + This exception's constructor treats two arguments specially. The first + argument, ``detail``, should be a string. The value of this string will + be used as the ``message`` attribute of the exception object. The second + special keyword argument, ``result`` is usually an instance of :class:`pyramid.security.Denied` or :class:`pyramid.security.ACLDenied` each of which indicates a reason for the forbidden error. However, ``result`` is also permitted to be just a plain boolean ``False`` object. The ``result`` value will be used as the ``result`` attribute of the - exception object. + exception object. It defaults to ``None``. The :term:`Forbidden View` can use the attributes of a Forbidden exception as necessary to provide extended information in an error report shown to a user. """ + # differences from webob.exc.HTTPForbidden: + # + # - accepts a ``result`` keyword argument + # + # - overrides constructor to set ``self.result`` + # + # differences from older pyramid.exceptions.Forbidden: + # + # - ``result`` must be passed as a keyword argument. + # + code = 403 + title = 'Forbidden' + explanation = ('Access was denied to this resource.') def __init__(self, detail=None, headers=None, comment=None, body_template=None, result=None, **kw): - self.message = detail # prevent 2.6.X whining - self.result = result # bw compat - _HTTPForbidden.__init__(self, detail=detail, headers=headers, - comment=comment, body_template=body_template, - **kw) - if not ('body' in kw or 'app_iter' in kw): - if not self.empty_body: - body = self.html_body(self.environ) - if isinstance(body, unicode): - body = body.encode(self.charset) - self.body = body + HTTPClientError.__init__(self, detail=detail, headers=headers, + comment=comment, body_template=body_template, + **kw) + self.result = result + +class HTTPNotFound(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the server did not find anything matching the + Request-URI. + + code: 404, title: Not Found + + Raise this exception within :term:`view` code to immediately + return the :term:`Not Found view` to the invoking user. Usually + this is a basic ``404`` page, but the Not Found view can be + customized as necessary. See :ref:`changing_the_notfound_view`. + + This exception's constructor accepts a ``detail`` argument + (the first argument), which should be a string. The value of this + string will be available as the ``message`` attribute of this exception, + for availability to the :term:`Not Found View`. + """ + code = 404 + title = 'Not Found' + explanation = ('The resource could not be found.') + +class HTTPMethodNotAllowed(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the method specified in the Request-Line is + not allowed for the resource identified by the Request-URI. + + code: 405, title: Method Not Allowed + """ + # differences from webob.exc.HTTPMethodNotAllowed: + # + # - body_template_obj not overridden (it tried to use request environ's + # REQUEST_METHOD) + code = 405 + title = 'Method Not Allowed' + +class HTTPNotAcceptable(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates the resource identified by the request is only + capable of generating response entities which have content + characteristics not acceptable according to the accept headers + sent in the request. + + code: 406, title: Not Acceptable + """ + # differences from webob.exc.HTTPNotAcceptable: + # + # - body_template_obj not overridden (it tried to use request environ's + # HTTP_ACCEPT) + code = 406 + title = 'Not Acceptable' + +class HTTPProxyAuthenticationRequired(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This is similar to 401, but indicates that the client must first + authenticate itself with the proxy. + + code: 407, title: Proxy Authentication Required + """ + code = 407 + title = 'Proxy Authentication Required' + explanation = ('Authentication with a local proxy is needed.') + +class HTTPRequestTimeout(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the client did not produce a request within + the time that the server was prepared to wait. + + code: 408, title: Request Timeout + """ + code = 408 + title = 'Request Timeout' + explanation = ('The server has waited too long for the request to ' + 'be sent by the client.') + +class HTTPConflict(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the request could not be completed due to a + conflict with the current state of the resource. + + code: 409, title: Conflict + """ + code = 409 + title = 'Conflict' + explanation = ('There was a conflict when trying to complete ' + 'your request.') + +class HTTPGone(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the requested resource is no longer available + at the server and no forwarding address is known. + + code: 410, title: Gone + """ + code = 410 + title = 'Gone' + explanation = ('This resource is no longer available. No forwarding ' + 'address is given.') + +class HTTPLengthRequired(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the the server refuses to accept the request + without a defined Content-Length. + + code: 411, title: Length Required + """ + code = 411 + title = 'Length Required' + explanation = ('Content-Length header required.') + +class HTTPPreconditionFailed(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the precondition given in one or more of the + request-header fields evaluated to false when it was tested on the + server. + + code: 412, title: Precondition Failed + """ + code = 412 + title = 'Precondition Failed' + explanation = ('Request precondition failed.') + +class HTTPRequestEntityTooLarge(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the server is refusing to process a request + because the request entity is larger than the server is willing or + able to process. + + code: 413, title: Request Entity Too Large + """ + code = 413 + title = 'Request Entity Too Large' + explanation = ('The body of your request was too large for this server.') + +class HTTPRequestURITooLong(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the server is refusing to service the request + because the Request-URI is longer than the server is willing to + interpret. + + code: 414, title: Request-URI Too Long + """ + code = 414 + title = 'Request-URI Too Long' + explanation = ('The request URI was too long for this server.') + +class HTTPUnsupportedMediaType(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the server is refusing to service the request + because the entity of the request is in a format not supported by + the requested resource for the requested method. + + code: 415, title: Unsupported Media Type + """ + # differences from webob.exc.HTTPUnsupportedMediaType: + # + # - body_template_obj not overridden (it tried to use request environ's + # CONTENT_TYPE) + code = 415 + title = 'Unsupported Media Type' + +class HTTPRequestRangeNotSatisfiable(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + The server SHOULD return a response with this status code if a + request included a Range request-header field, and none of the + range-specifier values in this field overlap the current extent + of the selected resource, and the request did not include an + If-Range request-header field. + + code: 416, title: Request Range Not Satisfiable + """ + code = 416 + title = 'Request Range Not Satisfiable' + explanation = ('The Range requested is not available.') + +class HTTPExpectationFailed(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indidcates that the expectation given in an Expect + request-header field could not be met by this server. + + code: 417, title: Expectation Failed + """ + code = 417 + title = 'Expectation Failed' + explanation = ('Expectation failed.') + +class HTTPUnprocessableEntity(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the server is unable to process the contained + instructions. Only for WebDAV. + + code: 422, title: Unprocessable Entity + """ + ## Note: from WebDAV + code = 422 + title = 'Unprocessable Entity' + explanation = 'Unable to process the contained instructions' + +class HTTPLocked(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the resource is locked. Only for WebDAV + + code: 423, title: Locked + """ + ## Note: from WebDAV + code = 423 + title = 'Locked' + explanation = ('The resource is locked') + +class HTTPFailedDependency(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the method could not be performed because the + requested action depended on another action and that action failed. + Only for WebDAV. + + code: 424, title: Failed Dependency + """ + ## Note: from WebDAV + code = 424 + title = 'Failed Dependency' + explanation = ( + 'The method could not be performed because the requested ' + 'action dependended on another action and that action failed') + +############################################################ +## 5xx Server Error +############################################################ +# Response status codes beginning with the digit "5" indicate cases in +# which the server is aware that it has erred or is incapable of +# performing the request. Except when responding to a HEAD request, the +# server SHOULD include an entity containing an explanation of the error +# situation, and whether it is a temporary or permanent condition. User +# agents SHOULD display any included entity to the user. These response +# codes are applicable to any request method. + +class HTTPServerError(HTTPError): + """ + base class for the 500's, where the server is in-error + + This is an error condition in which the server is presumed to be + in-error. This is usually unexpected, and thus requires a traceback; + ideally, opening a support ticket for the customer. Unless specialized, + this is a '500 Internal Server Error' + """ + code = 500 + title = 'Internal Server Error' + explanation = ( + 'The server has either erred or is incapable of performing\r\n' + 'the requested operation.\r\n') + +class HTTPInternalServerError(HTTPServerError): + pass + +class HTTPNotImplemented(HTTPServerError): + """ + subclass of :class:`~HTTPServerError` + + This indicates that the server does not support the functionality + required to fulfill the request. + + code: 501, title: Not Implemented + """ + # differences from webob.exc.HTTPNotAcceptable: + # + # - body_template_obj not overridden (it tried to use request environ's + # REQUEST_METHOD) + code = 501 + title = 'Not Implemented' + +class HTTPBadGateway(HTTPServerError): + """ + subclass of :class:`~HTTPServerError` + + This indicates that the server, while acting as a gateway or proxy, + received an invalid response from the upstream server it accessed + in attempting to fulfill the request. + + code: 502, title: Bad Gateway + """ + code = 502 + title = 'Bad Gateway' + explanation = ('Bad gateway.') + +class HTTPServiceUnavailable(HTTPServerError): + """ + subclass of :class:`~HTTPServerError` + + This indicates that the server is currently unable to handle the + request due to a temporary overloading or maintenance of the server. + + code: 503, title: Service Unavailable + """ + code = 503 + title = 'Service Unavailable' + explanation = ('The server is currently unavailable. ' + 'Please try again at a later time.') + +class HTTPGatewayTimeout(HTTPServerError): + """ + subclass of :class:`~HTTPServerError` + + This indicates that the server, while acting as a gateway or proxy, + did not receive a timely response from the upstream server specified + by the URI (e.g. HTTP, FTP, LDAP) or some other auxiliary server + (e.g. DNS) it needed to access in attempting to complete the request. + + code: 504, title: Gateway Timeout + """ + code = 504 + title = 'Gateway Timeout' + explanation = ('The gateway has timed out.') + +class HTTPVersionNotSupported(HTTPServerError): + """ + subclass of :class:`~HTTPServerError` + + This indicates that the server does not support, or refuses to + support, the HTTP protocol version that was used in the request + message. + + code: 505, title: HTTP Version Not Supported + """ + code = 505 + title = 'HTTP Version Not Supported' + explanation = ('The HTTP version is not supported.') + +class HTTPInsufficientStorage(HTTPServerError): + """ + subclass of :class:`~HTTPServerError` + + This indicates that the server does not have enough space to save + the resource. + + code: 507, title: Insufficient Storage + """ + code = 507 + title = 'Insufficient Storage' + explanation = ('There was not enough space to save the resource') + +__all__ = ['status_map'] +status_map={} +for name, value in globals().items(): + if (isinstance(value, (type, types.ClassType)) and + issubclass(value, HTTPException) + and not name.startswith('_')): + __all__.append(name) + if getattr(value, 'code', None): + status_map[value.code]=value + if hasattr(value, 'explanation'): + value.explanation = ' '.join(value.explanation.strip().split()) +del name, value NotFound = HTTPNotFound # bw compat Forbidden = HTTPForbidden # bw compat -# patch our status map with subclasses -status_map[403] = HTTPForbidden -status_map[404] = HTTPNotFound - class PredicateMismatch(NotFound): """ Internal exception (not an API) raised by multiviews when no @@ -197,27 +1091,15 @@ def is_response(ob): return True return False -newstyle_exceptions = issubclass(Exception, object) +def default_exceptionresponse_view(context, request): + if not isinstance(context, Exception): + # backwards compat for an exception response view registered via + # config.set_notfound_view or config.set_forbidden_view + # instead of as a proper exception view + context = request.exception or context + # WSGIHTTPException, a Response (2.5+) + return context -if newstyle_exceptions: - # webob exceptions will be Response objects (Py 2.5+) - def default_exceptionresponse_view(context, request): - if not isinstance(context, Exception): - # backwards compat for an exception response view registered via - # config.set_notfound_view or config.set_forbidden_view - # instead of as a proper exception view - context = request.exception or context - # WSGIHTTPException, a Response (2.5+) - return context - -else: - # webob exceptions will not be Response objects (Py 2.4) - def default_exceptionresponse_view(context, request): - if not isinstance(context, Exception): - # backwards compat for an exception response view registered via - # config.set_notfound_view or config.set_forbidden_view - # instead of as a proper exception view - context = request.exception or context - # HTTPException, not a Response (2.4) - get_response = getattr(request, 'get_response', lambda c: c) # testing - return get_response(context) +__all__.extend(['NotFound', 'Forbidden', 'PredicateMismatch', 'URLDecodeError', + 'ConfigurationError', 'abort', 'redirect', 'is_response', + 'default_exceptionresponse_view']) diff --git a/pyramid/tests/test_exceptions.py b/pyramid/tests/test_exceptions.py index 2e9279f66..3f303e3df 100644 --- a/pyramid/tests/test_exceptions.py +++ b/pyramid/tests/test_exceptions.py @@ -96,10 +96,301 @@ class Test_default_exceptionresponse_view(unittest.TestCase): result = self._callFUT(None, request) self.assertEqual(result, context) +class Test_no_escape(unittest.TestCase): + def _callFUT(self, val): + from pyramid.exceptions import no_escape + return no_escape(val) + + def test_null(self): + self.assertEqual(self._callFUT(None), '') + + def test_not_basestring(self): + self.assertEqual(self._callFUT(42), '42') + + def test_unicode(self): + class DummyUnicodeObject(object): + def __unicode__(self): + return u'42' + duo = DummyUnicodeObject() + self.assertEqual(self._callFUT(duo), u'42') + class DummyRequest(object): exception = None - def get_response(self, context): - return 'response' + +# from webob.request import Request +# from webob.exc import HTTPException +# from webob.exc import WSGIHTTPException +# from webob.exc import HTTPMethodNotAllowed +# from webob.exc import _HTTPMove +# from webob.exc import HTTPExceptionMiddleware + +# from nose.tools import eq_, ok_, assert_equal, assert_raises + +# def test_HTTPException(self): +# _called = [] +# _result = object() +# def _response(environ, start_response): +# _called.append((environ, start_response)) +# return _result +# environ = {} +# start_response = object() +# exc = HTTPException('testing', _response) +# ok_(exc.wsgi_response is _response) +# ok_(exc.exception is exc) +# result = exc(environ, start_response) +# ok_(result is result) +# assert_equal(_called, [(environ, start_response)]) + +# from webob.dec import wsgify +# @wsgify +# def method_not_allowed_app(req): +# if req.method != 'GET': +# raise HTTPMethodNotAllowed().exception +# return 'hello!' + + +# def test_exception_with_unicode_data(): +# req = Request.blank('/', method=u'POST') +# res = req.get_response(method_not_allowed_app) +# assert res.status_int == 405 + +# def test_WSGIHTTPException_headers(): +# exc = WSGIHTTPException(headers=[('Set-Cookie', 'a=1'), +# ('Set-Cookie', 'a=2')]) +# mixed = exc.headers.mixed() +# assert mixed['set-cookie'] == ['a=1', 'a=2'] + +# def test_WSGIHTTPException_w_body_template(): +# from string import Template +# TEMPLATE = '$foo: $bar' +# exc = WSGIHTTPException(body_template = TEMPLATE) +# assert_equal(exc.body_template, TEMPLATE) +# ok_(isinstance(exc.body_template_obj, Template)) +# eq_(exc.body_template_obj.substitute({'foo': 'FOO', 'bar': 'BAR'}), +# 'FOO: BAR') + +# def test_WSGIHTTPException_w_empty_body(): +# class EmptyOnly(WSGIHTTPException): +# empty_body = True +# exc = EmptyOnly(content_type='text/plain', content_length=234) +# ok_('content_type' not in exc.__dict__) +# ok_('content_length' not in exc.__dict__) + +# def test_WSGIHTTPException___str__(): +# exc1 = WSGIHTTPException(detail='Detail') +# eq_(str(exc1), 'Detail') +# class Explain(WSGIHTTPException): +# explanation = 'Explanation' +# eq_(str(Explain()), 'Explanation') + +# def test_WSGIHTTPException_plain_body_no_comment(): +# class Explain(WSGIHTTPException): +# code = '999' +# title = 'Testing' +# explanation = 'Explanation' +# exc = Explain(detail='Detail') +# eq_(exc.plain_body({}), +# '999 Testing\n\nExplanation\n\n Detail ') + +# def test_WSGIHTTPException_html_body_w_comment(): +# class Explain(WSGIHTTPException): +# code = '999' +# title = 'Testing' +# explanation = 'Explanation' +# exc = Explain(detail='Detail', comment='Comment') +# eq_(exc.html_body({}), +# '\n' +# ' \n' +# ' 999 Testing\n' +# ' \n' +# ' \n' +# '

999 Testing

\n' +# ' Explanation

\n' +# 'Detail\n' +# '\n\n' +# ' \n' +# '' +# ) + +# def test_WSGIHTTPException_generate_response(): +# def start_response(status, headers, exc_info=None): +# pass +# environ = { +# 'wsgi.url_scheme': 'HTTP', +# 'SERVER_NAME': 'localhost', +# 'SERVER_PORT': '80', +# 'REQUEST_METHOD': 'PUT', +# 'HTTP_ACCEPT': 'text/html' +# } +# excep = WSGIHTTPException() +# assert_equal( excep(environ,start_response), [ +# '\n' +# ' \n' +# ' None None\n' +# ' \n' +# ' \n' +# '

None None

\n' +# '

\n' +# '\n' +# '\n\n' +# ' \n' +# '' ] +# ) + +# def test_WSGIHTTPException_call_w_body(): +# def start_response(status, headers, exc_info=None): +# pass +# environ = { +# 'wsgi.url_scheme': 'HTTP', +# 'SERVER_NAME': 'localhost', +# 'SERVER_PORT': '80', +# 'REQUEST_METHOD': 'PUT' +# } +# excep = WSGIHTTPException() +# excep.body = 'test' +# assert_equal( excep(environ,start_response), ['test'] ) + + +# def test_WSGIHTTPException_wsgi_response(): +# def start_response(status, headers, exc_info=None): +# pass +# environ = { +# 'wsgi.url_scheme': 'HTTP', +# 'SERVER_NAME': 'localhost', +# 'SERVER_PORT': '80', +# 'REQUEST_METHOD': 'HEAD' +# } +# excep = WSGIHTTPException() +# assert_equal( excep.wsgi_response(environ,start_response), [] ) + +# def test_WSGIHTTPException_exception_newstyle(): +# def start_response(status, headers, exc_info=None): +# pass +# environ = { +# 'wsgi.url_scheme': 'HTTP', +# 'SERVER_NAME': 'localhost', +# 'SERVER_PORT': '80', +# 'REQUEST_METHOD': 'HEAD' +# } +# excep = WSGIHTTPException() +# exc.newstyle_exceptions = True +# assert_equal( excep.exception(environ,start_response), [] ) + +# def test_WSGIHTTPException_exception_no_newstyle(): +# def start_response(status, headers, exc_info=None): +# pass +# environ = { +# 'wsgi.url_scheme': 'HTTP', +# 'SERVER_NAME': 'localhost', +# 'SERVER_PORT': '80', +# 'REQUEST_METHOD': 'HEAD' +# } +# excep = WSGIHTTPException() +# exc.newstyle_exceptions = False +# assert_equal( excep.exception(environ,start_response), [] ) + +# def test_HTTPMove(): +# def start_response(status, headers, exc_info=None): +# pass +# environ = { +# 'wsgi.url_scheme': 'HTTP', +# 'SERVER_NAME': 'localhost', +# 'SERVER_PORT': '80', +# 'REQUEST_METHOD': 'HEAD' +# } +# m = _HTTPMove() +# assert_equal( m( environ, start_response ), [] ) + +# def test_HTTPMove_location_not_none(): +# def start_response(status, headers, exc_info=None): +# pass +# environ = { +# 'wsgi.url_scheme': 'HTTP', +# 'SERVER_NAME': 'localhost', +# 'SERVER_PORT': '80', +# 'REQUEST_METHOD': 'HEAD' +# } +# m = _HTTPMove(location='http://example.com') +# assert_equal( m( environ, start_response ), [] ) + +# def test_HTTPMove_add_slash_and_location(): +# def start_response(status, headers, exc_info=None): +# pass +# environ = { +# 'wsgi.url_scheme': 'HTTP', +# 'SERVER_NAME': 'localhost', +# 'SERVER_PORT': '80', +# 'REQUEST_METHOD': 'HEAD' +# } +# assert_raises( TypeError, _HTTPMove, location='http://example.com', add_slash=True ) + +# def test_HTTPMove_call_add_slash(): +# def start_response(status, headers, exc_info=None): +# pass +# environ = { +# 'wsgi.url_scheme': 'HTTP', +# 'SERVER_NAME': 'localhost', +# 'SERVER_PORT': '80', +# 'REQUEST_METHOD': 'HEAD' +# } +# m = _HTTPMove() +# m.add_slash = True +# assert_equal( m( environ, start_response ), [] ) + +# def test_HTTPMove_call_query_string(): +# def start_response(status, headers, exc_info=None): +# pass +# environ = { +# 'wsgi.url_scheme': 'HTTP', +# 'SERVER_NAME': 'localhost', +# 'SERVER_PORT': '80', +# 'REQUEST_METHOD': 'HEAD' +# } +# m = _HTTPMove() +# m.add_slash = True +# environ[ 'QUERY_STRING' ] = 'querystring' +# assert_equal( m( environ, start_response ), [] ) + +# def test_HTTPExceptionMiddleware_ok(): +# def app( environ, start_response ): +# return '123' +# application = app +# m = HTTPExceptionMiddleware(application) +# environ = {} +# start_response = None +# res = m( environ, start_response ) +# assert_equal( res, '123' ) - - +# def test_HTTPExceptionMiddleware_exception(): +# def wsgi_response( environ, start_response): +# return '123' +# def app( environ, start_response ): +# raise HTTPException( None, wsgi_response ) +# application = app +# m = HTTPExceptionMiddleware(application) +# environ = {} +# start_response = None +# res = m( environ, start_response ) +# assert_equal( res, '123' ) + +# def test_HTTPExceptionMiddleware_exception_exc_info_none(): +# class DummySys: +# def exc_info(self): +# return None +# def wsgi_response( environ, start_response): +# return start_response('200 OK', [], exc_info=None) +# def app( environ, start_response ): +# raise HTTPException( None, wsgi_response ) +# application = app +# m = HTTPExceptionMiddleware(application) +# environ = {} +# def start_response(status, headers, exc_info): +# pass +# try: +# from webob import exc +# old_sys = exc.sys +# sys = DummySys() +# res = m( environ, start_response ) +# assert_equal( res, None ) +# finally: +# exc.sys = old_sys -- cgit v1.2.3 From 7cac620d6834f2997bedbb1e6bbb10637f97a9b7 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 28 May 2011 19:44:41 -0400 Subject: use a generator; explain --- pyramid/exceptions.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/pyramid/exceptions.py b/pyramid/exceptions.py index 53cb0e5a8..e626efd5c 100644 --- a/pyramid/exceptions.py +++ b/pyramid/exceptions.py @@ -220,7 +220,6 @@ ${body}''') ## Set this to True for responses that should have no request body empty_body = False - _default_called = False def __init__(self, detail=None, headers=None, comment=None, body_template=None, **kw): @@ -239,14 +238,17 @@ ${body}''') del self.content_type del self.content_length elif not ('unicode_body' in kw or 'body' in kw or 'app_iter' in kw): - self.app_iter = iter(self._default_app_iter, None) + self.app_iter = self._default_app_iter() def __str__(self): return self.detail or self.explanation def _default_app_iter(self): - if self._default_called: - return None + # This is a generator which defers the creation of the response page + # body; we use a generator because we want to ensure that if + # attributes of this response are changed after it is constructed we + # use the changed values rather than the values at time of construction + # (e.g. self.content_type). html_comment = '' comment = self.comment or '' if 'html' in self.content_type or '': @@ -281,8 +283,8 @@ ${body}''') page = page_template.substitute(status=self.status, body=body) if isinstance(page, unicode): page = page.encode(self.charset) - self._default_called = True - return page + yield page + raise StopIteration def wsgi_response(self): return self -- cgit v1.2.3 From e25be5271cf54dd409cacf8089e055b0d13b59c7 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 28 May 2011 19:45:38 -0400 Subject: explain better --- pyramid/exceptions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyramid/exceptions.py b/pyramid/exceptions.py index e626efd5c..393fb376f 100644 --- a/pyramid/exceptions.py +++ b/pyramid/exceptions.py @@ -246,9 +246,9 @@ ${body}''') def _default_app_iter(self): # This is a generator which defers the creation of the response page # body; we use a generator because we want to ensure that if - # attributes of this response are changed after it is constructed we + # attributes of this response are changed after it is constructed, we # use the changed values rather than the values at time of construction - # (e.g. self.content_type). + # (e.g. self.content_type or self.charset). html_comment = '' comment = self.comment or '' if 'html' in self.content_type or '': -- cgit v1.2.3 From a90d3231d0820dec17c26f31e0045b6edd6ebbc3 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 28 May 2011 19:46:11 -0400 Subject: explain better --- pyramid/exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/exceptions.py b/pyramid/exceptions.py index 393fb376f..354679a22 100644 --- a/pyramid/exceptions.py +++ b/pyramid/exceptions.py @@ -182,7 +182,7 @@ class WSGIHTTPException(Response, HTTPException): # on request environ # # - doesn't add request.environ keys to template substitutions unless - # 'request' is passed as a keyword argument. + # 'request' is passed as a constructor keyword argument. # # - doesn't use "strip_tags" (${br} placeholder for
, no other html # in default body template) -- cgit v1.2.3 From ce9b9baba16799041e6a4e819eef894ae9ff516d Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 28 May 2011 20:08:25 -0400 Subject: move is_response back to pyramid.view --- pyramid/exceptions.py | 17 +---------------- pyramid/view.py | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/pyramid/exceptions.py b/pyramid/exceptions.py index 354679a22..8705ed1fb 100644 --- a/pyramid/exceptions.py +++ b/pyramid/exceptions.py @@ -1078,21 +1078,6 @@ def redirect(url, code=302, **kw): exc = status_map[code] raise exc(location=url, **kw).exception -def is_response(ob): - """ Return ``True`` if ``ob`` implements the interface implied by - :ref:`the_response`. ``False`` if not. - - .. note:: This isn't a true interface or subclass check. Instead, it's a - duck-typing check, as response objects are not obligated to be of a - particular class or provide any particular Zope interface.""" - - # response objects aren't obligated to implement a Zope interface, - # so we do it the hard way - if ( hasattr(ob, 'app_iter') and hasattr(ob, 'headerlist') and - hasattr(ob, 'status') ): - return True - return False - def default_exceptionresponse_view(context, request): if not isinstance(context, Exception): # backwards compat for an exception response view registered via @@ -1103,5 +1088,5 @@ def default_exceptionresponse_view(context, request): return context __all__.extend(['NotFound', 'Forbidden', 'PredicateMismatch', 'URLDecodeError', - 'ConfigurationError', 'abort', 'redirect', 'is_response', + 'ConfigurationError', 'abort', 'redirect', 'default_exceptionresponse_view']) diff --git a/pyramid/view.py b/pyramid/view.py index d6b666cf2..975464124 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -10,7 +10,6 @@ from pyramid.interfaces import IViewClassifier from pyramid.exceptions import HTTPFound from pyramid.exceptions import default_exceptionresponse_view -from pyramid.exceptions import is_response # API from pyramid.renderers import RendererHelper from pyramid.static import static_view from pyramid.threadlocal import get_current_registry @@ -312,4 +311,18 @@ See also :ref:`changing_the_notfound_view`. """ +def is_response(ob): + """ Return ``True`` if ``ob`` implements the interface implied by + :ref:`the_response`. ``False`` if not. + + .. note:: This isn't a true interface or subclass check. Instead, it's a + duck-typing check, as response objects are not obligated to be of a + particular class or provide any particular Zope interface.""" + + # response objects aren't obligated to implement a Zope interface, + # so we do it the hard way + if ( hasattr(ob, 'app_iter') and hasattr(ob, 'headerlist') and + hasattr(ob, 'status') ): + return True + return False -- cgit v1.2.3 From 3488ebdb257f2b0784def1aa90e56c6a6581bb9d Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 29 May 2011 01:24:43 -0400 Subject: preemptively drop 2.4 support --- pyramid/config.py | 8 +++---- pyramid/exceptions.py | 47 +++++++++------------------------------- pyramid/router.py | 2 +- pyramid/tests/test_exceptions.py | 14 +++++------- 4 files changed, 21 insertions(+), 50 deletions(-) diff --git a/pyramid/config.py b/pyramid/config.py index a20f93a90..1013456ec 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -2702,7 +2702,7 @@ class MultiView(object): return view if view.__predicated__(context, request): return view - raise PredicateMismatch(self.name).exception + raise PredicateMismatch(self.name) def __permitted__(self, context, request): view = self.match(context, request) @@ -2721,7 +2721,7 @@ class MultiView(object): return view(context, request) except PredicateMismatch: continue - raise PredicateMismatch(self.name).exception + raise PredicateMismatch(self.name) def wraps_view(wrapped): def inner(self, view): @@ -2844,7 +2844,7 @@ class ViewDeriver(object): return view(context, request) msg = getattr(request, 'authdebug_message', 'Unauthorized: %s failed permission check' % view) - raise Forbidden(msg, result=result).exception + raise Forbidden(msg, result=result) _secured_view.__call_permissive__ = view _secured_view.__permitted__ = _permitted _secured_view.__permission__ = permission @@ -2894,7 +2894,7 @@ class ViewDeriver(object): if all((predicate(context, request) for predicate in predicates)): return view(context, request) raise PredicateMismatch( - 'predicate mismatch for view %s' % view).exception + 'predicate mismatch for view %s' % view) def checker(context, request): return all((predicate(context, request) for predicate in predicates)) diff --git a/pyramid/exceptions.py b/pyramid/exceptions.py index 8705ed1fb..168426a9c 100644 --- a/pyramid/exceptions.py +++ b/pyramid/exceptions.py @@ -122,8 +122,6 @@ from zope.configuration.exceptions import ConfigurationError as ZCE from zope.interface import implements from pyramid.interfaces import IExceptionResponse -newstyle_exceptions = issubclass(Exception, object) - def no_escape(value): if value is None: return '' @@ -135,37 +133,10 @@ def no_escape(value): return value class HTTPException(Exception): - implements(IExceptionResponse) - """ - Exception used on pre-Python-2.5, where new-style classes cannot be used as - an exception. - """ - - def __init__(self, message, wsgi_response): - self.message = message - Exception.__init__(self, message) - self.__dict__['wsgi_response'] = wsgi_response - - def exception(self): - return self - - exception = property(exception) - - # for old style exceptions - if not newstyle_exceptions: #pragma NO COVERAGE - def __getattr__(self, attr): - if not attr.startswith('_'): - return getattr(self.wsgi_response, attr) - else: - raise AttributeError(attr) - - def __setattr__(self, attr, value): - if attr.startswith('_') or attr in ('args',): - self.__dict__[attr] = value - else: - setattr(self.wsgi_response, attr, value) + pass class WSGIHTTPException(Response, HTTPException): + implements(IExceptionResponse) ## You should set in subclasses: # code = 200 @@ -193,6 +164,10 @@ class WSGIHTTPException(Response, HTTPException): # - explicitly sets self.message = detail to prevent whining by Python # 2.6.5+ Exception.message # + # - its base class of HTTPException is no longer a Python 2.4 compatibility + # shim; it's purely a base class that inherits from Exception. This + # implies that this class' ``exception`` property always returns + # ``self`` (only for bw compat at this point). code = None title = None explanation = '' @@ -292,10 +267,8 @@ ${body}''') wsgi_response = property(wsgi_response) def exception(self): - if newstyle_exceptions: - return self - else: - return HTTPException(self.detail, self) + # bw compat + return self exception = property(exception) @@ -1063,7 +1036,7 @@ def abort(status_code, **kw): abort(404) # raises an HTTPNotFound exception. """ exc = status_map[status_code](**kw) - raise exc.exception + raise exc def redirect(url, code=302, **kw): @@ -1076,7 +1049,7 @@ def redirect(url, code=302, **kw): """ exc = status_map[code] - raise exc(location=url, **kw).exception + raise exc(location=url, **kw) def default_exceptionresponse_view(context, request): if not isinstance(context, Exception): diff --git a/pyramid/router.py b/pyramid/router.py index 069db52bc..b8a8639aa 100644 --- a/pyramid/router.py +++ b/pyramid/router.py @@ -153,7 +153,7 @@ class Router(object): logger and logger.debug(msg) else: msg = request.path_info - raise NotFound(msg).exception + raise NotFound(msg) else: response = view_callable(context, request) diff --git a/pyramid/tests/test_exceptions.py b/pyramid/tests/test_exceptions.py index 3f303e3df..e3e44c9f2 100644 --- a/pyramid/tests/test_exceptions.py +++ b/pyramid/tests/test_exceptions.py @@ -31,19 +31,17 @@ class Test_abort(unittest.TestCase): def test_status_404(self): from pyramid.exceptions import HTTPNotFound - self.assertRaises(HTTPNotFound().exception.__class__, - self._callFUT, 404) + self.assertRaises(HTTPNotFound, self._callFUT, 404) def test_status_201(self): from pyramid.exceptions import HTTPCreated - self.assertRaises(HTTPCreated().exception.__class__, - self._callFUT, 201) + self.assertRaises(HTTPCreated, self._callFUT, 201) def test_extra_kw(self): from pyramid.exceptions import HTTPNotFound try: self._callFUT(404, headers=[('abc', 'def')]) - except HTTPNotFound().exception.__class__, exc: + except HTTPNotFound, exc: self.assertEqual(exc.headers['abc'], 'def') else: # pragma: no cover raise AssertionError @@ -57,7 +55,7 @@ class Test_redirect(unittest.TestCase): from pyramid.exceptions import HTTPFound try: self._callFUT('http://example.com') - except HTTPFound().exception.__class__, exc: + except HTTPFound, exc: self.assertEqual(exc.location, 'http://example.com') self.assertEqual(exc.status, '302 Found') @@ -65,7 +63,7 @@ class Test_redirect(unittest.TestCase): from pyramid.exceptions import HTTPMovedPermanently try: self._callFUT('http://example.com', 301) - except HTTPMovedPermanently().exception.__class__, exc: + except HTTPMovedPermanently, exc: self.assertEqual(exc.location, 'http://example.com') self.assertEqual(exc.status, '301 Moved Permanently') @@ -73,7 +71,7 @@ class Test_redirect(unittest.TestCase): from pyramid.exceptions import HTTPFound try: self._callFUT('http://example.com', headers=[('abc', 'def')]) - except HTTPFound().exception.__class__, exc: + except HTTPFound, exc: self.assertEqual(exc.location, 'http://example.com') self.assertEqual(exc.status, '302 Found') self.assertEqual(exc.headers['abc'], 'def') -- cgit v1.2.3 From 9b496b8b248911ca8bd6c2bce08b73ee894809c4 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 29 May 2011 02:19:05 -0400 Subject: change docs; simplify --- pyramid/exceptions.py | 91 ++++++++++++++++++++++----------------------------- 1 file changed, 40 insertions(+), 51 deletions(-) diff --git a/pyramid/exceptions.py b/pyramid/exceptions.py index 168426a9c..c2db99627 100644 --- a/pyramid/exceptions.py +++ b/pyramid/exceptions.py @@ -2,14 +2,13 @@ HTTP Exceptions --------------- -This module contains Python exceptions that relate to HTTP status codes by -defining a set of classes, all subclasses of HTTPException. Each -exception, in addition to being a Python exception that can be raised and -caught, is also a ``Response`` object. +This module contains Pyramid HTTP exception classes. Each class relates to a +single HTTP status code. Each class is a subclass of the `~HTTPException`. +Each exception class is also a :term:`response` object. -This module defines exceptions according to RFC 2068 [1]_ : codes with -100-300 are not really errors; 400's are client errors, and 500's are -server errors. +Each exception class has a status code according to `RFC 2068 +`: codes with 100-300 are not really +errors; 400's are client errors, and 500's are server errors. Exception HTTPException @@ -69,12 +68,12 @@ Each HTTP exception has the following attributes: ``explanation`` a plain-text explanation of the error message that is not subject to environment or header substitutions; - it is accessible in the template via %(explanation)s + it is accessible in the template via ${explanation} ``detail`` a plain-text message customization that is not subject to environment or header substitutions; accessible in - the template via %(detail)s + the template via ${detail} ``body_template`` a content fragment (in HTML) used for environment and @@ -98,8 +97,9 @@ Each HTTP exception accepts the following parameters: a string.Template object containing a content fragment in HTML that frames the explanation and further detail -Substitution of environment variables and headers into template values is -performed if a ``request`` is passed to the exception constructor. +Substitution of response headers into template values is always performed. +Substitution of WSGI environment values is performed if a ``request`` is +passed to the exception's constructor. The subclasses of :class:`~_HTTPMove` (:class:`~HTTPMultipleChoices`, :class:`~HTTPMovedPermanently`, @@ -107,22 +107,18 @@ The subclasses of :class:`~_HTTPMove` :class:`~HTTPTemporaryRedirect`) are redirections that require a ``Location`` field. Reflecting this, these subclasses have one additional keyword argument: ``location``, which indicates the location to which to redirect. - -References: - -.. [1] http://www.python.org/peps/pep-0333.html#error-handling """ import types from string import Template from webob import Response -from webob import html_escape +from webob import html_escape as _html_escape from zope.configuration.exceptions import ConfigurationError as ZCE from zope.interface import implements from pyramid.interfaces import IExceptionResponse -def no_escape(value): +def _no_escape(value): if value is None: return '' if not isinstance(value, basestring): @@ -227,15 +223,15 @@ ${body}''') html_comment = '' comment = self.comment or '' if 'html' in self.content_type or '': - escape = html_escape + escape = _html_escape page_template = self.html_template_obj br = '
' if comment: html_comment = '' % escape(comment) else: - escape = no_escape + escape = _no_escape page_template = self.plain_template_obj - br = '\n' + br = '\r\n' if comment: html_comment = escape(comment) args = { @@ -262,14 +258,13 @@ ${body}''') raise StopIteration def wsgi_response(self): + # bw compat only return self - wsgi_response = property(wsgi_response) def exception(self): - # bw compat + # bw compat only return self - exception = property(exception) class HTTPError(WSGIHTTPException): @@ -407,7 +402,7 @@ class _HTTPMove(HTTPRedirection): # - location keyword arg defaults to '' # # - ``add_slash`` argument is no longer accepted: code that passes - # add_slash argument will receive an exception. + # add_slash argument to the constructor will receive an exception. explanation = 'The resource has been moved to' body_template_obj = Template('''\ ${explanation} ${location}; @@ -534,8 +529,8 @@ class HTTPClientError(HTTPError): """ code = 400 title = 'Bad Request' - explanation = ('The server could not comply with the request since\r\n' - 'it is either malformed or otherwise incorrect.\r\n') + explanation = ('The server could not comply with the request since ' + 'it is either malformed or otherwise incorrect.') class HTTPBadRequest(HTTPClientError): pass @@ -551,10 +546,10 @@ class HTTPUnauthorized(HTTPClientError): code = 401 title = 'Unauthorized' explanation = ( - 'This server could not verify that you are authorized to\r\n' - 'access the document you requested. Either you supplied the\r\n' - 'wrong credentials (e.g., bad password), or your browser\r\n' - 'does not understand how to supply the credentials required.\r\n') + 'This server could not verify that you are authorized to ' + 'access the document you requested. Either you supplied the ' + 'wrong credentials (e.g., bad password), or your browser ' + 'does not understand how to supply the credentials required.') class HTTPPaymentRequired(HTTPClientError): """ @@ -587,9 +582,9 @@ class HTTPForbidden(HTTPClientError): special keyword argument, ``result`` is usually an instance of :class:`pyramid.security.Denied` or :class:`pyramid.security.ACLDenied` each of which indicates a reason for the forbidden error. However, - ``result`` is also permitted to be just a plain boolean ``False`` object. - The ``result`` value will be used as the ``result`` attribute of the - exception object. It defaults to ``None``. + ``result`` is also permitted to be just a plain boolean ``False`` object + or ``None``. The ``result`` value will be used as the ``result`` + attribute of the exception object. It defaults to ``None``. The :term:`Forbidden View` can use the attributes of a Forbidden exception as necessary to provide extended information in an error @@ -895,8 +890,8 @@ class HTTPServerError(HTTPError): code = 500 title = 'Internal Server Error' explanation = ( - 'The server has either erred or is incapable of performing\r\n' - 'the requested operation.\r\n') + 'The server has either erred or is incapable of performing ' + 'the requested operation.') class HTTPInternalServerError(HTTPServerError): pass @@ -987,19 +982,6 @@ class HTTPInsufficientStorage(HTTPServerError): title = 'Insufficient Storage' explanation = ('There was not enough space to save the resource') -__all__ = ['status_map'] -status_map={} -for name, value in globals().items(): - if (isinstance(value, (type, types.ClassType)) and - issubclass(value, HTTPException) - and not name.startswith('_')): - __all__.append(name) - if getattr(value, 'code', None): - status_map[value.code]=value - if hasattr(value, 'explanation'): - value.explanation = ' '.join(value.explanation.strip().split()) -del name, value - NotFound = HTTPNotFound # bw compat Forbidden = HTTPForbidden # bw compat @@ -1060,6 +1042,13 @@ def default_exceptionresponse_view(context, request): # WSGIHTTPException, a Response (2.5+) return context -__all__.extend(['NotFound', 'Forbidden', 'PredicateMismatch', 'URLDecodeError', - 'ConfigurationError', 'abort', 'redirect', - 'default_exceptionresponse_view']) +status_map={} +for name, value in globals().items(): + if (isinstance(value, (type, types.ClassType)) and + issubclass(value, HTTPException) + and not name.startswith('_')): + code = getattr(value, 'code', None) + if code: + status_map[code] = value +del name, value + -- cgit v1.2.3 From c49e9f42e7932599a5a4864c9a17b494592c98c3 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 29 May 2011 03:33:16 -0400 Subject: add some tests for WSGIHTTPException --- pyramid/exceptions.py | 13 ++--- pyramid/tests/test_exceptions.py | 103 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 105 insertions(+), 11 deletions(-) diff --git a/pyramid/exceptions.py b/pyramid/exceptions.py index c2db99627..8b3c75549 100644 --- a/pyramid/exceptions.py +++ b/pyramid/exceptions.py @@ -222,7 +222,8 @@ ${body}''') # (e.g. self.content_type or self.charset). html_comment = '' comment = self.comment or '' - if 'html' in self.content_type or '': + content_type = self.content_type or '' + if 'html' in content_type: escape = _html_escape page_template = self.html_template_obj br = '
' @@ -231,7 +232,7 @@ ${body}''') else: escape = _no_escape page_template = self.plain_template_obj - br = '\r\n' + br = '\n' if comment: html_comment = escape(comment) args = { @@ -257,15 +258,11 @@ ${body}''') yield page raise StopIteration - def wsgi_response(self): - # bw compat only - return self - wsgi_response = property(wsgi_response) - + @property def exception(self): # bw compat only return self - exception = property(exception) + wsgi_response = exception # bw compat only class HTTPError(WSGIHTTPException): """ diff --git a/pyramid/tests/test_exceptions.py b/pyramid/tests/test_exceptions.py index e3e44c9f2..bbeb280f4 100644 --- a/pyramid/tests/test_exceptions.py +++ b/pyramid/tests/test_exceptions.py @@ -94,10 +94,10 @@ class Test_default_exceptionresponse_view(unittest.TestCase): result = self._callFUT(None, request) self.assertEqual(result, context) -class Test_no_escape(unittest.TestCase): +class Test__no_escape(unittest.TestCase): def _callFUT(self, val): - from pyramid.exceptions import no_escape - return no_escape(val) + from pyramid.exceptions import _no_escape + return _no_escape(val) def test_null(self): self.assertEqual(self._callFUT(None), '') @@ -112,6 +112,103 @@ class Test_no_escape(unittest.TestCase): duo = DummyUnicodeObject() self.assertEqual(self._callFUT(duo), u'42') +class TestWSGIHTTPException(unittest.TestCase): + def _getTargetClass(self): + from pyramid.exceptions import WSGIHTTPException + return WSGIHTTPException + + def _makeOne(self, *arg, **kw): + cls = self._getTargetClass() + return cls(*arg, **kw) + + def test_ctor_sets_detail(self): + exc = self._makeOne('message') + self.assertEqual(exc.detail, 'message') + + def test_ctor_sets_comment(self): + exc = self._makeOne(comment='comment') + self.assertEqual(exc.comment, 'comment') + + def test_ctor_calls_Exception_ctor(self): + exc = self._makeOne('message') + self.assertEqual(exc.message, 'message') + + def test_ctor_calls_Response_ctor(self): + exc = self._makeOne('message') + self.assertEqual(exc.status, 'None None') + + def test_ctor_extends_headers(self): + exc = self._makeOne(headers=[('X-Foo', 'foo')]) + self.assertEqual(exc.headers.get('X-Foo'), 'foo') + + def test_ctor_sets_body_template_obj(self): + exc = self._makeOne(body_template='${foo}') + self.assertEqual( + exc.body_template_obj.substitute({'foo':'foo'}), 'foo') + + def test_ctor_with_empty_body(self): + cls = self._getTargetClass() + class Subclass(cls): + empty_body = True + exc = Subclass() + self.assertEqual(exc.content_type, None) + self.assertEqual(exc.content_length, None) + + def test_ctor_with_body_doesnt_set_default_app_iter(self): + exc = self._makeOne(body='123') + self.assertEqual(exc.app_iter, ['123']) + + def test_ctor_with_unicode_body_doesnt_set_default_app_iter(self): + exc = self._makeOne(unicode_body=u'123') + self.assertEqual(exc.app_iter, ['123']) + + def test_ctor_with_app_iter_doesnt_set_default_app_iter(self): + exc = self._makeOne(app_iter=['123']) + self.assertEqual(exc.app_iter, ['123']) + + def test_ctor_with_body_sets_default_app_iter_html(self): + cls = self._getTargetClass() + class Subclass(cls): + code = '200' + title = 'OK' + explanation = 'explanation' + exc = Subclass('detail') + body = list(exc.app_iter)[0] + self.assertTrue(body.startswith(' Date: Sun, 29 May 2011 04:22:09 -0400 Subject: back up to complete coverage --- pyramid/tests/test_exceptions.py | 135 ++++++++++++++++++++++++++++++++++----- 1 file changed, 119 insertions(+), 16 deletions(-) diff --git a/pyramid/tests/test_exceptions.py b/pyramid/tests/test_exceptions.py index bbeb280f4..bf06f0b2e 100644 --- a/pyramid/tests/test_exceptions.py +++ b/pyramid/tests/test_exceptions.py @@ -117,6 +117,17 @@ class TestWSGIHTTPException(unittest.TestCase): from pyramid.exceptions import WSGIHTTPException return WSGIHTTPException + def _getTargetSubclass(self, code='200', title='OK', + explanation='explanation', empty_body=False): + cls = self._getTargetClass() + class Subclass(cls): + pass + Subclass.empty_body = empty_body + Subclass.code = code + Subclass.title = title + Subclass.explanation = explanation + return Subclass + def _makeOne(self, *arg, **kw): cls = self._getTargetClass() return cls(*arg, **kw) @@ -147,10 +158,8 @@ class TestWSGIHTTPException(unittest.TestCase): exc.body_template_obj.substitute({'foo':'foo'}), 'foo') def test_ctor_with_empty_body(self): - cls = self._getTargetClass() - class Subclass(cls): - empty_body = True - exc = Subclass() + cls = self._getTargetSubclass(empty_body=True) + exc = cls() self.assertEqual(exc.content_type, None) self.assertEqual(exc.content_length, None) @@ -167,12 +176,8 @@ class TestWSGIHTTPException(unittest.TestCase): self.assertEqual(exc.app_iter, ['123']) def test_ctor_with_body_sets_default_app_iter_html(self): - cls = self._getTargetClass() - class Subclass(cls): - code = '200' - title = 'OK' - explanation = 'explanation' - exc = Subclass('detail') + cls = self._getTargetSubclass() + exc = cls('detail') body = list(exc.app_iter)[0] self.assertTrue(body.startswith('' in body) + + def test_custom_body_template_no_environ(self): + cls = self._getTargetSubclass() + exc = cls(body_template='${location}', location='foo') + exc.content_type = 'text/plain' + body = list(exc._default_app_iter())[0] + self.assertEqual(body, '200 OK\n\nfoo') + + def test_custom_body_template_with_environ(self): + cls = self._getTargetSubclass() + from pyramid.request import Request + request = Request.blank('/') + exc = cls(body_template='${REQUEST_METHOD}', request=request) + exc.content_type = 'text/plain' + body = list(exc._default_app_iter())[0] + self.assertEqual(body, '200 OK\n\nGET') + + def test_body_template_unicode(self): + from pyramid.request import Request + cls = self._getTargetSubclass() + la = unicode('/La Pe\xc3\xb1a', 'utf-8') + request = Request.blank('/') + request.environ['unicodeval'] = la + exc = cls(body_template='${unicodeval}', request=request) + exc.content_type = 'text/plain' + body = list(exc._default_app_iter())[0] + self.assertEqual(body, '200 OK\n\n/La Pe\xc3\xb1a') + +class TestRenderAllExceptionsWithoutArguments(unittest.TestCase): + def _doit(self, content_type): + from pyramid.exceptions import status_map + L = [] + self.assertTrue(status_map) + for v in status_map.values(): + exc = v() + exc.content_type = content_type + result = list(exc.app_iter)[0] + if exc.empty_body: + self.assertEqual(result, '') + else: + self.assertTrue(exc.status in result) + L.append(result) + self.assertEqual(len(L), len(status_map)) + + def test_it_plain(self): + self._doit('text/plain') + + def test_it_html(self): + self._doit('text/html') + +class Test_HTTPMove(unittest.TestCase): + def _makeOne(self, *arg, **kw): + from pyramid.exceptions import _HTTPMove + return _HTTPMove(*arg, **kw) + + def test_it_location_not_passed(self): + exc = self._makeOne() + self.assertEqual(exc.location, '') + + def test_it_location_passed(self): + exc = self._makeOne(location='foo') + self.assertEqual(exc.location, 'foo') + +class TestHTTPForbidden(unittest.TestCase): + def _makeOne(self, *arg, **kw): + from pyramid.exceptions import HTTPForbidden + return HTTPForbidden(*arg, **kw) + def test_it_result_not_passed(self): + exc = self._makeOne() + self.assertEqual(exc.result, None) + + def test_it_result_passed(self): + exc = self._makeOne(result='foo') + self.assertEqual(exc.result, 'foo') + class DummyRequest(object): exception = None -- cgit v1.2.3 From 43378f78c0108831f3e07717c5213f1fd17a3d39 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 29 May 2011 04:22:43 -0400 Subject: remove unused comments --- pyramid/tests/test_exceptions.py | 277 --------------------------------------- 1 file changed, 277 deletions(-) diff --git a/pyramid/tests/test_exceptions.py b/pyramid/tests/test_exceptions.py index bf06f0b2e..f2e577416 100644 --- a/pyramid/tests/test_exceptions.py +++ b/pyramid/tests/test_exceptions.py @@ -315,280 +315,3 @@ class TestHTTPForbidden(unittest.TestCase): class DummyRequest(object): exception = None -# from webob.request import Request -# from webob.exc import HTTPException -# from webob.exc import WSGIHTTPException -# from webob.exc import HTTPMethodNotAllowed -# from webob.exc import _HTTPMove -# from webob.exc import HTTPExceptionMiddleware - -# from nose.tools import eq_, ok_, assert_equal, assert_raises - -# def test_HTTPException(self): -# _called = [] -# _result = object() -# def _response(environ, start_response): -# _called.append((environ, start_response)) -# return _result -# environ = {} -# start_response = object() -# exc = HTTPException('testing', _response) -# ok_(exc.wsgi_response is _response) -# ok_(exc.exception is exc) -# result = exc(environ, start_response) -# ok_(result is result) -# assert_equal(_called, [(environ, start_response)]) - -# from webob.dec import wsgify -# @wsgify -# def method_not_allowed_app(req): -# if req.method != 'GET': -# raise HTTPMethodNotAllowed().exception -# return 'hello!' - - -# def test_exception_with_unicode_data(): -# req = Request.blank('/', method=u'POST') -# res = req.get_response(method_not_allowed_app) -# assert res.status_int == 405 - -# def test_WSGIHTTPException_headers(): -# exc = WSGIHTTPException(headers=[('Set-Cookie', 'a=1'), -# ('Set-Cookie', 'a=2')]) -# mixed = exc.headers.mixed() -# assert mixed['set-cookie'] == ['a=1', 'a=2'] - -# def test_WSGIHTTPException_w_body_template(): -# from string import Template -# TEMPLATE = '$foo: $bar' -# exc = WSGIHTTPException(body_template = TEMPLATE) -# assert_equal(exc.body_template, TEMPLATE) -# ok_(isinstance(exc.body_template_obj, Template)) -# eq_(exc.body_template_obj.substitute({'foo': 'FOO', 'bar': 'BAR'}), -# 'FOO: BAR') - -# def test_WSGIHTTPException_w_empty_body(): -# class EmptyOnly(WSGIHTTPException): -# empty_body = True -# exc = EmptyOnly(content_type='text/plain', content_length=234) -# ok_('content_type' not in exc.__dict__) -# ok_('content_length' not in exc.__dict__) - -# def test_WSGIHTTPException___str__(): -# exc1 = WSGIHTTPException(detail='Detail') -# eq_(str(exc1), 'Detail') -# class Explain(WSGIHTTPException): -# explanation = 'Explanation' -# eq_(str(Explain()), 'Explanation') - -# def test_WSGIHTTPException_plain_body_no_comment(): -# class Explain(WSGIHTTPException): -# code = '999' -# title = 'Testing' -# explanation = 'Explanation' -# exc = Explain(detail='Detail') -# eq_(exc.plain_body({}), -# '999 Testing\n\nExplanation\n\n Detail ') - -# def test_WSGIHTTPException_html_body_w_comment(): -# class Explain(WSGIHTTPException): -# code = '999' -# title = 'Testing' -# explanation = 'Explanation' -# exc = Explain(detail='Detail', comment='Comment') -# eq_(exc.html_body({}), -# '\n' -# ' \n' -# ' 999 Testing\n' -# ' \n' -# ' \n' -# '

999 Testing

\n' -# ' Explanation

\n' -# 'Detail\n' -# '\n\n' -# ' \n' -# '' -# ) - -# def test_WSGIHTTPException_generate_response(): -# def start_response(status, headers, exc_info=None): -# pass -# environ = { -# 'wsgi.url_scheme': 'HTTP', -# 'SERVER_NAME': 'localhost', -# 'SERVER_PORT': '80', -# 'REQUEST_METHOD': 'PUT', -# 'HTTP_ACCEPT': 'text/html' -# } -# excep = WSGIHTTPException() -# assert_equal( excep(environ,start_response), [ -# '\n' -# ' \n' -# ' None None\n' -# ' \n' -# ' \n' -# '

None None

\n' -# '

\n' -# '\n' -# '\n\n' -# ' \n' -# '' ] -# ) - -# def test_WSGIHTTPException_call_w_body(): -# def start_response(status, headers, exc_info=None): -# pass -# environ = { -# 'wsgi.url_scheme': 'HTTP', -# 'SERVER_NAME': 'localhost', -# 'SERVER_PORT': '80', -# 'REQUEST_METHOD': 'PUT' -# } -# excep = WSGIHTTPException() -# excep.body = 'test' -# assert_equal( excep(environ,start_response), ['test'] ) - - -# def test_WSGIHTTPException_wsgi_response(): -# def start_response(status, headers, exc_info=None): -# pass -# environ = { -# 'wsgi.url_scheme': 'HTTP', -# 'SERVER_NAME': 'localhost', -# 'SERVER_PORT': '80', -# 'REQUEST_METHOD': 'HEAD' -# } -# excep = WSGIHTTPException() -# assert_equal( excep.wsgi_response(environ,start_response), [] ) - -# def test_WSGIHTTPException_exception_newstyle(): -# def start_response(status, headers, exc_info=None): -# pass -# environ = { -# 'wsgi.url_scheme': 'HTTP', -# 'SERVER_NAME': 'localhost', -# 'SERVER_PORT': '80', -# 'REQUEST_METHOD': 'HEAD' -# } -# excep = WSGIHTTPException() -# exc.newstyle_exceptions = True -# assert_equal( excep.exception(environ,start_response), [] ) - -# def test_WSGIHTTPException_exception_no_newstyle(): -# def start_response(status, headers, exc_info=None): -# pass -# environ = { -# 'wsgi.url_scheme': 'HTTP', -# 'SERVER_NAME': 'localhost', -# 'SERVER_PORT': '80', -# 'REQUEST_METHOD': 'HEAD' -# } -# excep = WSGIHTTPException() -# exc.newstyle_exceptions = False -# assert_equal( excep.exception(environ,start_response), [] ) - -# def test_HTTPMove(): -# def start_response(status, headers, exc_info=None): -# pass -# environ = { -# 'wsgi.url_scheme': 'HTTP', -# 'SERVER_NAME': 'localhost', -# 'SERVER_PORT': '80', -# 'REQUEST_METHOD': 'HEAD' -# } -# m = _HTTPMove() -# assert_equal( m( environ, start_response ), [] ) - -# def test_HTTPMove_location_not_none(): -# def start_response(status, headers, exc_info=None): -# pass -# environ = { -# 'wsgi.url_scheme': 'HTTP', -# 'SERVER_NAME': 'localhost', -# 'SERVER_PORT': '80', -# 'REQUEST_METHOD': 'HEAD' -# } -# m = _HTTPMove(location='http://example.com') -# assert_equal( m( environ, start_response ), [] ) - -# def test_HTTPMove_add_slash_and_location(): -# def start_response(status, headers, exc_info=None): -# pass -# environ = { -# 'wsgi.url_scheme': 'HTTP', -# 'SERVER_NAME': 'localhost', -# 'SERVER_PORT': '80', -# 'REQUEST_METHOD': 'HEAD' -# } -# assert_raises( TypeError, _HTTPMove, location='http://example.com', add_slash=True ) - -# def test_HTTPMove_call_add_slash(): -# def start_response(status, headers, exc_info=None): -# pass -# environ = { -# 'wsgi.url_scheme': 'HTTP', -# 'SERVER_NAME': 'localhost', -# 'SERVER_PORT': '80', -# 'REQUEST_METHOD': 'HEAD' -# } -# m = _HTTPMove() -# m.add_slash = True -# assert_equal( m( environ, start_response ), [] ) - -# def test_HTTPMove_call_query_string(): -# def start_response(status, headers, exc_info=None): -# pass -# environ = { -# 'wsgi.url_scheme': 'HTTP', -# 'SERVER_NAME': 'localhost', -# 'SERVER_PORT': '80', -# 'REQUEST_METHOD': 'HEAD' -# } -# m = _HTTPMove() -# m.add_slash = True -# environ[ 'QUERY_STRING' ] = 'querystring' -# assert_equal( m( environ, start_response ), [] ) - -# def test_HTTPExceptionMiddleware_ok(): -# def app( environ, start_response ): -# return '123' -# application = app -# m = HTTPExceptionMiddleware(application) -# environ = {} -# start_response = None -# res = m( environ, start_response ) -# assert_equal( res, '123' ) - -# def test_HTTPExceptionMiddleware_exception(): -# def wsgi_response( environ, start_response): -# return '123' -# def app( environ, start_response ): -# raise HTTPException( None, wsgi_response ) -# application = app -# m = HTTPExceptionMiddleware(application) -# environ = {} -# start_response = None -# res = m( environ, start_response ) -# assert_equal( res, '123' ) - -# def test_HTTPExceptionMiddleware_exception_exc_info_none(): -# class DummySys: -# def exc_info(self): -# return None -# def wsgi_response( environ, start_response): -# return start_response('200 OK', [], exc_info=None) -# def app( environ, start_response ): -# raise HTTPException( None, wsgi_response ) -# application = app -# m = HTTPExceptionMiddleware(application) -# environ = {} -# def start_response(status, headers, exc_info): -# pass -# try: -# from webob import exc -# old_sys = exc.sys -# sys = DummySys() -# res = m( environ, start_response ) -# assert_equal( res, None ) -# finally: -# exc.sys = old_sys -- cgit v1.2.3 From 8662ce04c7a2a2dd490761f330336e4c3e156c7d Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 29 May 2011 04:45:55 -0400 Subject: docs fixes --- pyramid/exceptions.py | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/pyramid/exceptions.py b/pyramid/exceptions.py index 8b3c75549..46367d4ef 100644 --- a/pyramid/exceptions.py +++ b/pyramid/exceptions.py @@ -3,8 +3,9 @@ HTTP Exceptions --------------- This module contains Pyramid HTTP exception classes. Each class relates to a -single HTTP status code. Each class is a subclass of the `~HTTPException`. -Each exception class is also a :term:`response` object. +single HTTP status code. Each class is a subclass of the +:class:`~HTTPException`. Each exception class is also a :term:`response` +object. Each exception class has a status code according to `RFC 2068 `: codes with 100-300 are not really @@ -76,10 +77,10 @@ Each HTTP exception has the following attributes: the template via ${detail} ``body_template`` - a content fragment (in HTML) used for environment and - header substitution; the default template includes both + a ``String.template``-format content fragment used for environment + and header substitution; the default template includes both the explanation and further detail provided in the - message + message. Each HTTP exception accepts the following parameters: @@ -94,7 +95,7 @@ Each HTTP exception accepts the following parameters: usually stripped/hidden for end-users ``body_template`` - a string.Template object containing a content fragment in HTML + a ``string.Template`` object containing a content fragment in HTML that frames the explanation and further detail Substitution of response headers into template values is always performed. @@ -108,15 +109,14 @@ The subclasses of :class:`~_HTTPMove` field. Reflecting this, these subclasses have one additional keyword argument: ``location``, which indicates the location to which to redirect. """ - import types from string import Template -from webob import Response from webob import html_escape as _html_escape - from zope.configuration.exceptions import ConfigurationError as ZCE from zope.interface import implements + from pyramid.interfaces import IExceptionResponse +from pyramid.response import Response def _no_escape(value): if value is None: @@ -1008,24 +1008,28 @@ class ConfigurationError(ZCE): def abort(status_code, **kw): - """Aborts the request immediately by raising an HTTP exception. The - values in ``*kw`` will be passed to the HTTP exception constructor. - Example:: + """Aborts the request immediately by raising an HTTP exception based on a + status code. Example:: abort(404) # raises an HTTPNotFound exception. + + The values passed as ``kw`` are provided to the exception's constructor. """ exc = status_map[status_code](**kw) raise exc def redirect(url, code=302, **kw): - """Raises a redirect exception to the specified URL. + """Raises an :class:`~HTTPFound` (302) redirect exception to the + URL specified by ``url``. Optionally, a code variable may be passed with the status code of the redirect, ie:: redirect(route_url('foo', request), code=303) + The values passed as ``kw`` are provided to the exception constructor. + """ exc = status_map[code] raise exc(location=url, **kw) -- cgit v1.2.3 From 356d0327c22d7ced5fe28f9e5cb73671fe63a69b Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 29 May 2011 04:46:42 -0400 Subject: docs fixes --- pyramid/exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/exceptions.py b/pyramid/exceptions.py index 46367d4ef..c1af43692 100644 --- a/pyramid/exceptions.py +++ b/pyramid/exceptions.py @@ -158,7 +158,7 @@ class WSGIHTTPException(Response, HTTPException): # passed # # - explicitly sets self.message = detail to prevent whining by Python - # 2.6.5+ Exception.message + # 2.6.5+ access of Exception.message # # - its base class of HTTPException is no longer a Python 2.4 compatibility # shim; it's purely a base class that inherits from Exception. This -- cgit v1.2.3 From 966b5cfe03009069d7bbe92cc047b32a5e3cd4e6 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 30 May 2011 03:23:31 -0400 Subject: - Fix older CHANGES entries. - The ``pyramid.request.Request`` class now has a ``ResponseClass`` interface which points at ``pyramid.response.Response``. - The ``pyramid.request.Response`` class now has a ``RequestClass`` interface which points at ``pyramid.response.Request``. - ``pyramid.response.Response`` is now a *subclass* of ``webob.response.Response``. It also inherits from the built-in Python ``Exception`` class and implements the ``pyramid.interfaces.IExceptionResponse`` class so it can be raised as an exception from view code. --- CHANGES.txt | 37 +++++++++++++++++++++++++------------ pyramid/__init__.py | 7 +++++-- pyramid/config.py | 13 +++++++------ pyramid/exceptions.py | 4 ++-- pyramid/response.py | 10 ++++++++-- 5 files changed, 47 insertions(+), 24 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 756d1345c..15c86c13c 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -104,12 +104,13 @@ Features section entitled "Static Routes" in the URL Dispatch narrative chapter for more information. -- A default exception view for the context ``webob.exc.HTTPException`` (aka - ``pyramid.httpexceptions.HTTPException``) is now registered by default. - This means that an instance of any exception class imported from - ``pyramid.httpexceptions`` (such as ``HTTPFound``) can now be raised from - within view code; when raised, this exception view will render the - exception to a response. +- A default exception view for the context + ``pyramid.interfaces.IExceptionResponse`` (aka + ``pyramid.response.Response`` or ``pyramid.httpexceptions.HTTPException``) + is now registered by default. This means that an instance of any exception + response class imported from ``pyramid.httpexceptions`` (such as + ``HTTPFound``) can now be raised from within view code; when raised, this + exception view will render the exception to a response. - New functions named ``pyramid.httpexceptions.abort`` and ``pyramid.httpexceptions.redirect`` perform the equivalent of their Pylons @@ -118,12 +119,18 @@ Features ``webob.exc.HTTPException``. - The Configurator now accepts an additional keyword argument named - ``httpexception_view``. By default, this argument is populated with a - default exception view function that will be used when an HTTP exception is - raised. When ``None`` is passed for this value, an exception view for HTTP - exceptions will not be registered. Passing ``None`` returns the behavior - of raising an HTTP exception to that of Pyramid 1.0 (the exception will - propagate to middleware and to the WSGI server). + ``exceptionresponse_view``. By default, this argument is populated with a + default exception view function that will be used when a response is raised + as an exception. When ``None`` is passed for this value, an exception view + for responses will not be registered. Passing ``None`` returns the + behavior of raising an HTTP exception to that of Pyramid 1.0 (the exception + will propagate to middleware and to the WSGI server). + +- The ``pyramid.request.Request`` class now has a ``ResponseClass`` interface + which points at ``pyramid.response.Response``. + +- The ``pyramid.request.Response`` class now has a ``RequestClass`` interface + which points at ``pyramid.response.Request``. Bug Fixes --------- @@ -289,6 +296,12 @@ Behavior Changes implements its own ``__getattr__``, ``__setattr__`` or ``__delattr__`` as a result. +- ``pyramid.response.Response`` is now a *subclass* of + ``webob.response.Response``. It also inherits from the built-in Python + ``Exception`` class and implements the + ``pyramid.interfaces.IExceptionResponse`` class so it can be raised as an + exception from view code. + Dependencies ------------ diff --git a/pyramid/__init__.py b/pyramid/__init__.py index 5f6a326f8..473d5e1c6 100644 --- a/pyramid/__init__.py +++ b/pyramid/__init__.py @@ -1,2 +1,5 @@ -# pyramid package - +from pyramid.request import Request +from pyramid.response import Response +Response.RequestClass = Request +Request.ResponseClass = Response +del Request, Response diff --git a/pyramid/config.py b/pyramid/config.py index 1013456ec..ce5201ed3 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -260,12 +260,13 @@ class Configurator(object): If ``exceptionresponse_view`` is passed, it must be a :term:`view callable` or ``None``. If it is a view callable, it will be used as an exception view callable when an :term:`exception response` is raised (any - named exception from the ``pyramid.exceptions`` module that begins with - ``HTTP`` as well as the ``NotFound`` and ``Forbidden`` exceptions) as - well as exceptions raised via :func:`pyramid.exceptions.abort`, - :func:`pyramid.exceptions.redirect`. If ``exceptionresponse_view`` is - ``None``, no exception response view will be registered, and all - raised exception responses will be bubbled up to Pyramid's caller. By + object that implements the :class:`pyramid.interaces.IExceptionResponse` + interface, such as a :class:`pyramid.response.Response` object or any + ``HTTP`` exception documented in :mod:`pyramid.httpexceptions` as well as + exception responses raised via :func:`pyramid.exceptions.abort`, + :func:`pyramid.exceptions.redirect`). If ``exceptionresponse_view`` is + ``None``, no exception response view will be registered, and all raised + exception responses will be bubbled up to Pyramid's caller. By default, the ``pyramid.exceptions.default_exceptionresponse_view`` function is used as the ``exceptionresponse_view``. This argument is new in Pyramid 1.1. """ diff --git a/pyramid/exceptions.py b/pyramid/exceptions.py index c1af43692..678529c1e 100644 --- a/pyramid/exceptions.py +++ b/pyramid/exceptions.py @@ -128,7 +128,8 @@ def _no_escape(value): value = str(value) return value -class HTTPException(Exception): + +class HTTPException(Exception): # bw compat pass class WSGIHTTPException(Response, HTTPException): @@ -1040,7 +1041,6 @@ def default_exceptionresponse_view(context, request): # config.set_notfound_view or config.set_forbidden_view # instead of as a proper exception view context = request.exception or context - # WSGIHTTPException, a Response (2.5+) return context status_map={} diff --git a/pyramid/response.py b/pyramid/response.py index 26f27b142..e9f5528a5 100644 --- a/pyramid/response.py +++ b/pyramid/response.py @@ -1,2 +1,8 @@ -from webob import Response -Response = Response # pyflakes +from webob import Response as _Response +from zope.interface import implements + +from pyramid.interfaces import IExceptionResponse + +class Response(_Response, Exception): + implements(IExceptionResponse) + -- cgit v1.2.3 From a7e625785f65c41e5a6dc017b31bd0d74821474e Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 31 May 2011 14:40:05 -0400 Subject: the canonical import location for HTTP exceptions/responses is now pyramid.response --- docs/glossary.rst | 4 +- docs/narr/hooks.rst | 48 +- docs/narr/renderers.rst | 31 +- docs/narr/router.rst | 53 +- docs/narr/testing.rst | 13 +- docs/narr/urldispatch.rst | 6 +- docs/narr/views.rst | 82 +- docs/narr/webob.rst | 36 +- docs/tutorials/bfg/index.rst | 2 +- docs/tutorials/wiki/authorization.rst | 2 +- docs/tutorials/wiki/definingviews.rst | 8 +- .../wiki/src/authorization/tutorial/login.py | 4 +- .../wiki/src/authorization/tutorial/views.py | 2 +- docs/tutorials/wiki/src/views/tutorial/views.py | 2 +- docs/tutorials/wiki2/definingviews.rst | 4 +- .../wiki2/src/authorization/tutorial/__init__.py | 2 +- .../wiki2/src/authorization/tutorial/login.py | 2 +- .../wiki2/src/authorization/tutorial/views.py | 2 +- docs/tutorials/wiki2/src/views/tutorial/views.py | 2 +- pyramid/config.py | 14 +- pyramid/exceptions.py | 1026 +------------------- pyramid/httpexceptions.py | 116 ++- pyramid/interfaces.py | 17 +- pyramid/response.py | 940 ++++++++++++++++++ pyramid/router.py | 4 +- pyramid/testing.py | 4 +- pyramid/tests/fixtureapp/views.py | 4 +- pyramid/tests/forbiddenapp/__init__.py | 6 +- pyramid/tests/test_config.py | 69 +- pyramid/tests/test_exceptions.py | 299 +----- pyramid/tests/test_httpexceptions.py | 2 +- pyramid/tests/test_response.py | 308 ++++++ pyramid/tests/test_router.py | 56 +- pyramid/tests/test_testing.py | 4 +- pyramid/view.py | 19 +- 35 files changed, 1618 insertions(+), 1575 deletions(-) create mode 100644 pyramid/tests/test_response.py diff --git a/docs/glossary.rst b/docs/glossary.rst index 797343e5e..20b9bfd64 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -594,7 +594,7 @@ Glossary Not Found view An :term:`exception view` invoked by :app:`Pyramid` when the - developer explicitly raises a ``pyramid.exceptions.NotFound`` + developer explicitly raises a ``pyramid.response.HTTPNotFound`` exception from within :term:`view` code or :term:`root factory` code, or when the current request doesn't match any :term:`view configuration`. :app:`Pyramid` provides a default @@ -604,7 +604,7 @@ Glossary Forbidden view An :term:`exception view` invoked by :app:`Pyramid` when the developer explicitly raises a - ``pyramid.exceptions.Forbidden`` exception from within + ``pyramid.response.HTTPForbidden`` exception from within :term:`view` code or :term:`root factory` code, or when the :term:`view configuration` and :term:`authorization policy` found for a request disallows a particular view invocation. diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index 7e3fe0a5c..d620b5672 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -21,7 +21,7 @@ configuration. The :term:`not found view` callable is a view callable like any other. The :term:`view configuration` which causes it to be a "not found" view consists -only of naming the :exc:`pyramid.exceptions.NotFound` class as the +only of naming the :exc:`pyramid.response.HTTPNotFound` class as the ``context`` of the view configuration. If your application uses :term:`imperative configuration`, you can replace @@ -31,9 +31,9 @@ method to register an "exception view": .. code-block:: python :linenos: - from pyramid.exceptions import NotFound + from pyramid.response import HTTPNotFound from helloworld.views import notfound_view - config.add_view(notfound_view, context=NotFound) + config.add_view(notfound_view, context=HTTPNotFound) Replace ``helloworld.views.notfound_view`` with a reference to the :term:`view callable` you want to use to represent the Not Found view. @@ -42,7 +42,7 @@ Like any other view, the notfound view must accept at least a ``request`` parameter, or both ``context`` and ``request``. The ``request`` is the current :term:`request` representing the denied action. The ``context`` (if used in the call signature) will be the instance of the -:exc:`~pyramid.exceptions.NotFound` exception that caused the view to be +:exc:`~pyramid.response.HTTPNotFound` exception that caused the view to be called. Here's some sample code that implements a minimal NotFound view callable: @@ -50,25 +50,25 @@ Here's some sample code that implements a minimal NotFound view callable: .. code-block:: python :linenos: - from pyramid.httpexceptions import HTTPNotFound + from pyramid.response import HTTPNotFound def notfound_view(request): return HTTPNotFound() .. note:: When a NotFound view callable is invoked, it is passed a - :term:`request`. The ``exception`` attribute of the request will - be an instance of the :exc:`~pyramid.exceptions.NotFound` - exception that caused the not found view to be called. The value - of ``request.exception.args[0]`` will be a value explaining why the - not found error was raised. This message will be different when - the ``debug_notfound`` environment setting is true than it is when - it is false. + :term:`request`. The ``exception`` attribute of the request will be an + instance of the :exc:`~pyramid.response.HTTPNotFound` exception that + caused the not found view to be called. The value of + ``request.exception.args[0]`` will be a value explaining why the not found + error was raised. This message will be different when the + ``debug_notfound`` environment setting is true than it is when it is + false. .. warning:: When a NotFound view callable accepts an argument list as described in :ref:`request_and_context_view_definitions`, the ``context`` passed as the first argument to the view callable will be the - :exc:`~pyramid.exceptions.NotFound` exception instance. If available, the - resource context will still be available as ``request.context``. + :exc:`~pyramid.response.HTTPNotFound` exception instance. If available, + the resource context will still be available as ``request.context``. .. index:: single: forbidden view @@ -85,7 +85,7 @@ the view which generates it can be overridden as necessary. The :term:`forbidden view` callable is a view callable like any other. The :term:`view configuration` which causes it to be a "not found" view consists -only of naming the :exc:`pyramid.exceptions.Forbidden` class as the +only of naming the :exc:`pyramid.response.HTTPForbidden` class as the ``context`` of the view configuration. You can replace the forbidden view by using the @@ -96,8 +96,8 @@ view": :linenos: from helloworld.views import forbidden_view - from pyramid.exceptions import Forbidden - config.add_view(forbidden_view, context=Forbidden) + from pyramid.response import HTTPForbidden + config.add_view(forbidden_view, context=HTTPForbidden) Replace ``helloworld.views.forbidden_view`` with a reference to the Python :term:`view callable` you want to use to represent the Forbidden view. @@ -121,13 +121,13 @@ Here's some sample code that implements a minimal forbidden view: return Response('forbidden') .. note:: When a forbidden view callable is invoked, it is passed a - :term:`request`. The ``exception`` attribute of the request will - be an instance of the :exc:`~pyramid.exceptions.Forbidden` - exception that caused the forbidden view to be called. The value - of ``request.exception.args[0]`` will be a value explaining why the - forbidden was raised. This message will be different when the - ``debug_authorization`` environment setting is true than it is when - it is false. + :term:`request`. The ``exception`` attribute of the request will be an + instance of the :exc:`~pyramid.response.HTTPForbidden` exception that + caused the forbidden view to be called. The value of + ``request.exception.args[0]`` will be a value explaining why the forbidden + was raised. This message will be different when the + ``debug_authorization`` environment setting is true than it is when it is + false. .. index:: single: request factory diff --git a/docs/narr/renderers.rst b/docs/narr/renderers.rst index c3533648b..c7a3d7837 100644 --- a/docs/narr/renderers.rst +++ b/docs/narr/renderers.rst @@ -73,30 +73,43 @@ When this configuration is added to an application, the which renders view return values to a :term:`JSON` response serialization. Other built-in renderers include renderers which use the :term:`Chameleon` -templating language to render a dictionary to a response. +templating language to render a dictionary to a response. Additional +renderers can be added by developers to the system as necessary (see +:ref:`adding_and_overriding_renderers`). + +Views which use a renderer can vary non-body response attributes (such as +headers and the HTTP status code) by attaching a property to the +``request.response`` attribute See :ref:`request_response_attr`. If the :term:`view callable` associated with a :term:`view configuration` returns a Response object directly (an object with the attributes ``status``, ``headerlist`` and ``app_iter``), any renderer associated with the view configuration is ignored, and the response is passed back to :app:`Pyramid` unchanged. For example, if your view callable returns an instance of the -:class:`pyramid.httpexceptions.HTTPFound` class as a response, no renderer -will be employed. +:class:`pyramid.response.HTTPFound` class as a response, no renderer will be +employed. .. code-block:: python :linenos: - from pyramid.httpexceptions import HTTPFound + from pyramid.response import HTTPFound def view(request): return HTTPFound(location='http://example.com') # any renderer avoided -Views which use a renderer can vary non-body response attributes (such as -headers and the HTTP status code) by attaching a property to the -``request.response`` attribute See :ref:`request_response_attr`. +Likewise for a "plain old response": + +.. code-block:: python + :linenos: + + from pyramid.response import Response + + def view(request): + return Response('OK') # any renderer avoided -Additional renderers can be added by developers to the system as necessary -(see :ref:`adding_and_overriding_renderers`). +Mutations to ``request.response`` in views which return a Response object +like this directly (unless that response *is* ``request.response``) will be +ignored. .. index:: single: renderers (built-in) diff --git a/docs/narr/router.rst b/docs/narr/router.rst index 11f84d4ea..44fa9835b 100644 --- a/docs/narr/router.rst +++ b/docs/narr/router.rst @@ -77,40 +77,37 @@ processing? #. A :class:`~pyramid.events.ContextFound` :term:`event` is sent to any subscribers. -#. :app:`Pyramid` looks up a :term:`view` callable using the - context, the request, and the view name. If a view callable - doesn't exist for this combination of objects (based on the type of - the context, the type of the request, and the value of the view - name, and any :term:`predicate` attributes applied to the view - configuration), :app:`Pyramid` raises a - :class:`~pyramid.exceptions.NotFound` exception, which is meant - to be caught by a surrounding exception handler. +#. :app:`Pyramid` looks up a :term:`view` callable using the context, the + request, and the view name. If a view callable doesn't exist for this + combination of objects (based on the type of the context, the type of the + request, and the value of the view name, and any :term:`predicate` + attributes applied to the view configuration), :app:`Pyramid` raises a + :class:`~pyramid.response.HTTPNotFound` exception, which is meant to be + caught by a surrounding exception handler. #. If a view callable was found, :app:`Pyramid` attempts to call the view function. -#. If an :term:`authorization policy` is in use, and the view was - protected by a :term:`permission`, :app:`Pyramid` passes the - context, the request, and the view_name to a function which - determines whether the view being asked for can be executed by the - requesting user, based on credential information in the request and - security information attached to the context. If it returns - ``True``, :app:`Pyramid` calls the view callable to obtain a - response. If it returns ``False``, it raises a - :class:`~pyramid.exceptions.Forbidden` exception, which is meant - to be called by a surrounding exception handler. +#. If an :term:`authorization policy` is in use, and the view was protected + by a :term:`permission`, :app:`Pyramid` passes the context, the request, + and the view_name to a function which determines whether the view being + asked for can be executed by the requesting user, based on credential + information in the request and security information attached to the + context. If it returns ``True``, :app:`Pyramid` calls the view callable + to obtain a response. If it returns ``False``, it raises a + :class:`~pyramid.response.HTTPForbidden` exception, which is meant to be + called by a surrounding exception handler. #. If any exception was raised within a :term:`root factory`, by - :term:`traversal`, by a :term:`view callable` or by - :app:`Pyramid` itself (such as when it raises - :class:`~pyramid.exceptions.NotFound` or - :class:`~pyramid.exceptions.Forbidden`), the router catches the - exception, and attaches it to the request as the ``exception`` - attribute. It then attempts to find a :term:`exception view` for - the exception that was caught. If it finds an exception view - callable, that callable is called, and is presumed to generate a - response. If an :term:`exception view` that matches the exception - cannot be found, the exception is reraised. + :term:`traversal`, by a :term:`view callable` or by :app:`Pyramid` itself + (such as when it raises :class:`~pyramid.response.HTTPNotFound` or + :class:`~pyramid.response.HTTPForbidden`), the router catches the + exception, and attaches it to the request as the ``exception`` attribute. + It then attempts to find a :term:`exception view` for the exception that + was caught. If it finds an exception view callable, that callable is + called, and is presumed to generate a response. If an :term:`exception + view` that matches the exception cannot be found, the exception is + reraised. #. The following steps occur only when a :term:`response` could be successfully generated by a normal :term:`view callable` or an diff --git a/docs/narr/testing.rst b/docs/narr/testing.rst index bd45388c2..862eda9f0 100644 --- a/docs/narr/testing.rst +++ b/docs/narr/testing.rst @@ -191,11 +191,11 @@ function. :linenos: from pyramid.security import has_permission - from pyramid.exceptions import Forbidden + from pyramid.response import HTTPForbidden def view_fn(request): if not has_permission('edit', request.context, request): - raise Forbidden + raise HTTPForbidden return {'greeting':'hello'} Without doing anything special during a unit test, the call to @@ -207,7 +207,7 @@ application registry is not created and populated (e.g. by initializing the configurator with an authorization policy), like when you invoke application code via a unit test, :app:`Pyramid` API functions will tend to either fail or return default results. So how do you test the branch of the code in this -view function that raises :exc:`Forbidden`? +view function that raises :exc:`HTTPForbidden`? The testing API provided by :app:`Pyramid` allows you to simulate various application registry registrations for use under a unit testing framework @@ -230,16 +230,15 @@ without needing to invoke the actual application configuration implied by its testing.tearDown() def test_view_fn_forbidden(self): - from pyramid.exceptions import Forbidden + from pyramid.response import HTTPForbidden from my.package import view_fn self.config.testing_securitypolicy(userid='hank', permissive=False) request = testing.DummyRequest() request.context = testing.DummyResource() - self.assertRaises(Forbidden, view_fn, request) + self.assertRaises(HTTPForbidden, view_fn, request) def test_view_fn_allowed(self): - from pyramid.exceptions import Forbidden from my.package import view_fn self.config.testing_securitypolicy(userid='hank', permissive=True) @@ -265,7 +264,7 @@ We call the function being tested with the manufactured request. When the function is called, :func:`pyramid.security.has_permission` will call the "dummy" authentication policy we've registered through :meth:`~pyramid.config.Configuration.testing_securitypolicy`, which denies -access. We check that the view function raises a :exc:`Forbidden` error. +access. We check that the view function raises a :exc:`HTTPForbidden` error. The second test method, named ``test_view_fn_allowed`` tests the alternate case, where the authentication policy allows access. Notice that we pass diff --git a/docs/narr/urldispatch.rst b/docs/narr/urldispatch.rst index 5df1eb3af..e5228b81e 100644 --- a/docs/narr/urldispatch.rst +++ b/docs/narr/urldispatch.rst @@ -917,7 +917,7 @@ the application's startup configuration, adding the following stanza: :linenos: config.add_view('pyramid.view.append_slash_notfound_view', - context='pyramid.exceptions.NotFound') + context='pyramid.response.HTTPNotFound') See :ref:`view_module` and :ref:`changing_the_notfound_view` for more information about the slash-appending not found view and for a more general @@ -945,14 +945,14 @@ view as the first argument to its constructor. For instance: .. code-block:: python :linenos: - from pyramid.exceptions import NotFound + from pyramid.response import HTTPNotFound from pyramid.view import AppendSlashNotFoundViewFactory def notfound_view(context, request): return HTTPNotFound('It aint there, stop trying!') custom_append_slash = AppendSlashNotFoundViewFactory(notfound_view) - config.add_view(custom_append_slash, context=NotFound) + config.add_view(custom_append_slash, context=HTTPNotFound) The ``notfound_view`` supplied must adhere to the two-argument view callable calling convention of ``(context, request)`` (``context`` will be the diff --git a/docs/narr/views.rst b/docs/narr/views.rst index 66e9919e2..73a7c2e2a 100644 --- a/docs/narr/views.rst +++ b/docs/narr/views.rst @@ -233,7 +233,7 @@ implements the :term:`Response` interface is to return a You don't need to always use :class:`~pyramid.response.Response` to represent a response. :app:`Pyramid` provides a range of different "exception" classes which can act as response objects too. For example, an instance of the class -:class:`pyramid.httpexceptions.HTTPFound` is also a valid response object +:class:`pyramid.response.HTTPFound` is also a valid response object (see :ref:`http_exceptions` and ref:`http_redirect`). A view can actually return any object that has the following attributes. @@ -275,17 +275,18 @@ exist: internal exceptions and HTTP exceptions. Internal Exceptions ~~~~~~~~~~~~~~~~~~~ -:exc:`pyramid.exceptions.NotFound` and :exc:`pyramid.exceptions.Forbidden` -are exceptions often raised by Pyramid itself when it (respectively) cannot -find a view to service a request or when authorization was forbidden by a -security policy. However, they can also be raised by application developers. +:exc:`pyramid.response.HTTPNotFound` and +:exc:`pyramid.response.HTTPForbidden` are exceptions often raised by Pyramid +itself when it (respectively) cannot find a view to service a request or when +authorization was forbidden by a security policy. However, they can also be +raised by application developers. -If :exc:`~pyramid.exceptions.NotFound` is raised within view code, the result -of the :term:`Not Found View` will be returned to the user agent which +If :exc:`~pyramid.response.HTTPNotFound` is raised within view code, the +result of the :term:`Not Found View` will be returned to the user agent which performed the request. -If :exc:`~pyramid.exceptions.Forbidden` is raised within view code, the result -of the :term:`Forbidden View` will be returned to the user agent which +If :exc:`~pyramid.response.HTTPForbidden` is raised within view code, the +result of the :term:`Forbidden View` will be returned to the user agent which performed the request. Both are exception classes which accept a single positional constructor @@ -298,13 +299,10 @@ An example: .. code-block:: python :linenos: - from pyramid.exceptions import NotFound + from pyramid.response import HTTPNotFound def aview(request): - raise NotFound('not found!') - -Internal exceptions may not be *returned* in order to generate a response, -they must always be *raised*. + raise HTTPNotFound('not found!') .. index:: single: HTTP exceptions @@ -314,32 +312,33 @@ they must always be *raised*. HTTP Exceptions ~~~~~~~~~~~~~~~ -All exception classes documented in the :mod:`pyramid.httpexceptions` module -implement the :term:`Response` interface; an instance of any of these classes -can be returned or raised from within a view. The instance will be used as -as the view's response. +All classes documented in the :mod:`pyramid.response` module as inheriting +from the :class:`pryamid.response.Response` object implement the +:term:`Response` interface; an instance of any of these classes can be +returned or raised from within a view. The instance will be used as as the +view's response. -For example, the :class:`pyramid.httpexceptions.HTTPUnauthorized` exception +For example, the :class:`pyramid.response.HTTPUnauthorized` exception can be raised. This will cause a response to be generated with a ``401 Unauthorized`` status: .. code-block:: python :linenos: - from pyramid.httpexceptions import HTTPUnauthorized + from pyramid.response import HTTPUnauthorized def aview(request): raise HTTPUnauthorized() A shortcut for importing and raising an HTTP exception is the -:func:`pyramid.httpexceptions.abort` function. This function accepts an HTTP +:func:`pyramid.response.abort` function. This function accepts an HTTP status code and raises the corresponding HTTP exception. For example, to raise HTTPUnauthorized, instead of the above, you could do: .. code-block:: python :linenos: - from pyramid.httpexceptions import abort + from pyramid.response import abort def aview(request): abort(401) @@ -347,8 +346,8 @@ raise HTTPUnauthorized, instead of the above, you could do: This is the case because ``401`` is the HTTP status code for "HTTP Unauthorized". Therefore, ``abort(401)`` is functionally equivalent to ``raise HTTPUnauthorized()``. Other exceptions in -:mod:`pyramid.httpexceptions` can be raised via -:func:`pyramid.httpexceptions.abort` as well, as long as the status code +:mod:`pyramid.response` can be raised via +:func:`pyramid.response.abort` as well, as long as the status code associated with the exception is provided to the function. An HTTP exception, instead of being raised, can alternately be *returned* @@ -357,18 +356,11 @@ An HTTP exception, instead of being raised, can alternately be *returned* .. code-block:: python :linenos: - from pyramid.httpexceptions import HTTPUnauthorized + from pyramid.response import HTTPUnauthorized def aview(request): return HTTPUnauthorized() -Note that :class:`pyramid.exceptions.NotFound` is *not* the same as -:class:`pyramid.httpexceptions.HTTPNotFound`. If the latter is raised, the -:term:`Not Found view` will *not* be called automatically. Likewise, -:class:`pyramid.exceptions.Forbidden` is not the same exception as -:class:`pyramid.httpexceptions.HTTPForbidden`. If the latter is raised, the -:term:`Forbidden view` will not be called automatically. - .. index:: single: exception views @@ -377,11 +369,11 @@ Note that :class:`pyramid.exceptions.NotFound` is *not* the same as Custom Exception Views ---------------------- -The machinery which allows :exc:`~pyramid.exceptions.NotFound`, -:exc:`~pyramid.exceptions.Forbidden` and HTTP exceptions to be caught by -specialized views as described in :ref:`special_exceptions_in_callables` can -also be used by application developers to convert arbitrary exceptions to -responses. +The machinery which allows :exc:`~pyramid.response.HTTPNotFound`, +:exc:`~pyramid.response.HTTPForbidden` and other responses to be used as +exceptions and caught by specialized views as described in +:ref:`special_exceptions_in_callables` can also be used by application +developers to convert arbitrary exceptions to responses. To register a view that should be called whenever a particular exception is raised from with :app:`Pyramid` view code, use the exception class or one of @@ -474,14 +466,14 @@ Short Form ~~~~~~~~~~ You can issue an HTTP redirect from within a view callable by using the -:func:`pyramid.httpexceptions.redirect` function. This function raises an -:class:`pyramid.httpexceptions.HTTPFound` exception (a "302"), which is -caught by an exception handler and turned into a response. +:func:`pyramid.response.redirect` function. This function raises an +:class:`pyramid.response.HTTPFound` exception (a "302"), which is caught by +the default exception response handler and turned into a response. .. code-block:: python :linenos: - from pyramid.httpexceptions import redirect + from pyramid.response import redirect def myview(request): redirect('http://example.com') @@ -490,16 +482,16 @@ Long Form ~~~~~~~~~ You can issue an HTTP redirect from within a view "by hand" instead of -relying on the :func:`pyramid.httpexceptions.redirect` function to do it for +relying on the :func:`pyramid.response.redirect` function to do it for you. -To do so, you can *return* a :class:`pyramid.httpexceptions.HTTPFound` +To do so, you can *return* a :class:`pyramid.response.HTTPFound` instance. .. code-block:: python :linenos: - from pyramid.httpexceptions import HTTPFound + from pyramid.response import HTTPFound def myview(request): return HTTPFound(location='http://example.com') @@ -510,7 +502,7 @@ one. .. code-block:: python :linenos: - from pyramid.httpexceptions import HTTPFound + from pyramid.response import HTTPFound def myview(request): raise HTTPFound(location='http://example.com') diff --git a/docs/narr/webob.rst b/docs/narr/webob.rst index 072ca1c74..6cd9418ce 100644 --- a/docs/narr/webob.rst +++ b/docs/narr/webob.rst @@ -362,11 +362,11 @@ To facilitate error responses like ``404 Not Found``, the module :mod:`webob.exc` contains classes for each kind of error response. These include boring, but appropriate error bodies. The exceptions exposed by this module, when used under :app:`Pyramid`, should be imported from the -:mod:`pyramid.httpexceptions` "facade" module. This import location is merely -a facade for the original location of these exceptions: ``webob.exc``. +:mod:`pyramid.response` module. This import location contains subclasses and +replacements that mirror those in the original ``webob.exc``. -Each class is named ``pyramid.httpexceptions.HTTP*``, where ``*`` is the reason -for the error. For instance, :class:`pyramid.httpexceptions.HTTPNotFound`. It +Each class is named ``pyramid.response.HTTP*``, where ``*`` is the reason for +the error. For instance, :class:`pyramid.response.HTTPNotFound`. It subclasses :class:`pyramid.Response`, so you can manipulate the instances in the same way. A typical example is: @@ -374,40 +374,18 @@ the same way. A typical example is: .. code-block:: python :linenos: - from pyramid.httpexceptions import HTTPNotFound - from pyramid.httpexceptions import HTTPMovedPermanently + from pyramid.response import HTTPNotFound + from pyramid.response import HTTPMovedPermanently response = HTTPNotFound('There is no such resource') # or: response = HTTPMovedPermanently(location=new_url) -These are not exceptions unless you are using Python 2.5+, because -they are new-style classes which are not allowed as exceptions until -Python 2.5. To get an exception object use ``response.exception``. -You can use this like: - -.. code-block:: python - :linenos: - - from pyramid.httpexceptions import HTTPException - from pyramid.httpexceptions import HTTPNotFound - - def aview(request): - try: - # ... stuff ... - raise HTTPNotFound('No such resource').exception - except HTTPException, e: - return request.get_response(e) - -The exceptions are still WSGI applications, but you cannot set -attributes like ``content_type``, ``charset``, etc. on these exception -objects. - More Details ++++++++++++ More details about the response object API are available in the :mod:`pyramid.response` documentation. More details about exception responses -are in the :mod:`pyramid.httpexceptions` API documentation. The `WebOb +are in the :mod:`pyramid.response` API documentation. The `WebOb documentation `_ is also useful. diff --git a/docs/tutorials/bfg/index.rst b/docs/tutorials/bfg/index.rst index e68e63b0b..e01345158 100644 --- a/docs/tutorials/bfg/index.rst +++ b/docs/tutorials/bfg/index.rst @@ -106,7 +106,7 @@ Here's how to convert a :mod:`repoze.bfg` application to a - ZCML files which contain directives that have attributes which name a ``repoze.bfg`` API module or attribute of an API module - (e.g. ``context="repoze.bfg.exceptions.NotFound"``) will be + (e.g. ``context="repoze.bfg.exeptions.NotFound"``) will be converted to :app:`Pyramid` compatible ZCML attributes (e.g. ``context="pyramid.exceptions.NotFound``). Every ZCML file beneath the top-level path (files ending with ``.zcml``) will be diff --git a/docs/tutorials/wiki/authorization.rst b/docs/tutorials/wiki/authorization.rst index e4480d6d9..3b102958e 100644 --- a/docs/tutorials/wiki/authorization.rst +++ b/docs/tutorials/wiki/authorization.rst @@ -131,7 +131,7 @@ callable. The first view configuration decorator configures the ``login`` view callable so it will be invoked when someone visits ``/login`` (when the context is a Wiki and the view name is ``login``). The second decorator (with context of -``pyramid.exceptions.Forbidden``) specifies a :term:`forbidden view`. This +``pyramid.response.HTTPForbidden``) specifies a :term:`forbidden view`. This configures our login view to be presented to the user when :app:`Pyramid` detects that a view invocation can not be authorized. Because we've configured a forbidden view, the ``login`` view callable will be invoked diff --git a/docs/tutorials/wiki/definingviews.rst b/docs/tutorials/wiki/definingviews.rst index b6c083bbf..ea8842294 100644 --- a/docs/tutorials/wiki/definingviews.rst +++ b/docs/tutorials/wiki/definingviews.rst @@ -83,10 +83,10 @@ No renderer is necessary when a view returns a response object. The ``view_wiki`` view callable always redirects to the URL of a Page resource named "FrontPage". To do so, it returns an instance of the -:class:`pyramid.httpexceptions.HTTPFound` class (instances of which implement -the WebOb :term:`response` interface). The :func:`pyramid.url.resource_url` -API. :func:`pyramid.url.resource_url` constructs a URL to the ``FrontPage`` -page resource (e.g. ``http://localhost:6543/FrontPage``), and uses it as the +:class:`pyramid.response.HTTPFound` class (instances of which implement the +WebOb :term:`response` interface). The :func:`pyramid.url.resource_url` API. +:func:`pyramid.url.resource_url` constructs a URL to the ``FrontPage`` page +resource (e.g. ``http://localhost:6543/FrontPage``), and uses it as the "location" of the HTTPFound response, forming an HTTP redirect. The ``view_page`` view function diff --git a/docs/tutorials/wiki/src/authorization/tutorial/login.py b/docs/tutorials/wiki/src/authorization/tutorial/login.py index 463db71a6..822b19b9e 100644 --- a/docs/tutorials/wiki/src/authorization/tutorial/login.py +++ b/docs/tutorials/wiki/src/authorization/tutorial/login.py @@ -1,4 +1,4 @@ -from pyramid.httpexceptions import HTTPFound +from pyramid.response import HTTPFound from pyramid.security import remember from pyramid.security import forget @@ -9,7 +9,7 @@ from tutorial.security import USERS @view_config(context='tutorial.models.Wiki', name='login', renderer='templates/login.pt') -@view_config(context='pyramid.exceptions.Forbidden', +@view_config(context='pyramid.response.HTTPForbidden', renderer='templates/login.pt') def login(request): login_url = resource_url(request.context, request, 'login') diff --git a/docs/tutorials/wiki/src/authorization/tutorial/views.py b/docs/tutorials/wiki/src/authorization/tutorial/views.py index a83e17de4..67550d58e 100644 --- a/docs/tutorials/wiki/src/authorization/tutorial/views.py +++ b/docs/tutorials/wiki/src/authorization/tutorial/views.py @@ -1,7 +1,7 @@ from docutils.core import publish_parts import re -from pyramid.httpexceptions import HTTPFound +from pyramid.response import HTTPFound from pyramid.url import resource_url from pyramid.view import view_config from pyramid.security import authenticated_userid diff --git a/docs/tutorials/wiki/src/views/tutorial/views.py b/docs/tutorials/wiki/src/views/tutorial/views.py index 42420f2fe..d72cbd3fd 100644 --- a/docs/tutorials/wiki/src/views/tutorial/views.py +++ b/docs/tutorials/wiki/src/views/tutorial/views.py @@ -1,7 +1,7 @@ from docutils.core import publish_parts import re -from pyramid.httpexceptions import HTTPFound +from pyramid.response import HTTPFound from pyramid.url import resource_url from pyramid.view import view_config diff --git a/docs/tutorials/wiki2/definingviews.rst b/docs/tutorials/wiki2/definingviews.rst index 832f90b92..32e3c0b24 100644 --- a/docs/tutorials/wiki2/definingviews.rst +++ b/docs/tutorials/wiki2/definingviews.rst @@ -90,8 +90,8 @@ path to our "FrontPage". :language: python The ``view_wiki`` function returns an instance of the -:class:`pyramid.httpexceptions.HTTPFound` class (instances of which implement -the WebOb :term:`response` interface), It will use the +:class:`pyramid.response.HTTPFound` class (instances of which implement the +WebOb :term:`response` interface), It will use the :func:`pyramid.url.route_url` API to construct a URL to the ``FrontPage`` page (e.g. ``http://localhost:6543/FrontPage``), and will use it as the "location" of the HTTPFound response, forming an HTTP redirect. diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py b/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py index 05183d3d4..42013622c 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py @@ -39,7 +39,7 @@ def main(global_config, **settings): config.add_view('tutorial.views.edit_page', route_name='edit_page', renderer='tutorial:templates/edit.pt', permission='edit') config.add_view('tutorial.login.login', - context='pyramid.exceptions.Forbidden', + context='pyramid.response.HTTPForbidden', renderer='tutorial:templates/login.pt') return config.make_wsgi_app() diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/login.py b/docs/tutorials/wiki2/src/authorization/tutorial/login.py index 7a1d1f663..2bc8a7201 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/login.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/login.py @@ -1,4 +1,4 @@ -from pyramid.httpexceptions import HTTPFound +from pyramid.response import HTTPFound from pyramid.security import remember from pyramid.security import forget from pyramid.url import route_url diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views.py b/docs/tutorials/wiki2/src/authorization/tutorial/views.py index 5abd8391e..ed441295c 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/views.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/views.py @@ -2,7 +2,7 @@ import re from docutils.core import publish_parts -from pyramid.httpexceptions import HTTPFound +from pyramid.response import HTTPFound from pyramid.security import authenticated_userid from pyramid.url import route_url diff --git a/docs/tutorials/wiki2/src/views/tutorial/views.py b/docs/tutorials/wiki2/src/views/tutorial/views.py index b8896abe7..80d817d99 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/views.py +++ b/docs/tutorials/wiki2/src/views/tutorial/views.py @@ -2,7 +2,7 @@ import re from docutils.core import publish_parts -from pyramid.httpexceptions import HTTPFound +from pyramid.response import HTTPFound from pyramid.url import route_url from tutorial.models import DBSession diff --git a/pyramid/config.py b/pyramid/config.py index ce5201ed3..ab1729c06 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -56,9 +56,9 @@ from pyramid.compat import md5 from pyramid.compat import any from pyramid.events import ApplicationCreated from pyramid.exceptions import ConfigurationError -from pyramid.exceptions import default_exceptionresponse_view -from pyramid.exceptions import Forbidden -from pyramid.exceptions import NotFound +from pyramid.response import default_exceptionresponse_view +from pyramid.response import HTTPForbidden +from pyramid.response import HTTPNotFound from pyramid.exceptions import PredicateMismatch from pyramid.i18n import get_localizer from pyramid.log import make_stream_logger @@ -1997,7 +1997,8 @@ class Configurator(object): def bwcompat_view(context, request): context = getattr(request, 'context', None) return view(context, request) - return self.add_view(bwcompat_view, context=Forbidden, wrapper=wrapper) + return self.add_view(bwcompat_view, context=HTTPForbidden, + wrapper=wrapper) @action_method def set_notfound_view(self, view=None, attr=None, renderer=None, @@ -2037,7 +2038,8 @@ class Configurator(object): def bwcompat_view(context, request): context = getattr(request, 'context', None) return view(context, request) - return self.add_view(bwcompat_view, context=NotFound, wrapper=wrapper) + return self.add_view(bwcompat_view, context=HTTPNotFound, + wrapper=wrapper) @action_method def set_request_factory(self, factory): @@ -2845,7 +2847,7 @@ class ViewDeriver(object): return view(context, request) msg = getattr(request, 'authdebug_message', 'Unauthorized: %s failed permission check' % view) - raise Forbidden(msg, result=result) + raise HTTPForbidden(msg, result=result) _secured_view.__call_permissive__ = view _secured_view.__permitted__ = _permitted _secured_view.__permission__ = permission diff --git a/pyramid/exceptions.py b/pyramid/exceptions.py index 678529c1e..2484f94a3 100644 --- a/pyramid/exceptions.py +++ b/pyramid/exceptions.py @@ -1,984 +1,7 @@ -""" -HTTP Exceptions ---------------- - -This module contains Pyramid HTTP exception classes. Each class relates to a -single HTTP status code. Each class is a subclass of the -:class:`~HTTPException`. Each exception class is also a :term:`response` -object. - -Each exception class has a status code according to `RFC 2068 -`: codes with 100-300 are not really -errors; 400's are client errors, and 500's are server errors. - -Exception - HTTPException - HTTPOk - * 200 - HTTPOk - * 201 - HTTPCreated - * 202 - HTTPAccepted - * 203 - HTTPNonAuthoritativeInformation - * 204 - HTTPNoContent - * 205 - HTTPResetContent - * 206 - HTTPPartialContent - HTTPRedirection - * 300 - HTTPMultipleChoices - * 301 - HTTPMovedPermanently - * 302 - HTTPFound - * 303 - HTTPSeeOther - * 304 - HTTPNotModified - * 305 - HTTPUseProxy - * 306 - Unused (not implemented, obviously) - * 307 - HTTPTemporaryRedirect - HTTPError - HTTPClientError - * 400 - HTTPBadRequest - * 401 - HTTPUnauthorized - * 402 - HTTPPaymentRequired - * 403 - HTTPForbidden - * 404 - HTTPNotFound - * 405 - HTTPMethodNotAllowed - * 406 - HTTPNotAcceptable - * 407 - HTTPProxyAuthenticationRequired - * 408 - HTTPRequestTimeout - * 409 - HTTPConflict - * 410 - HTTPGone - * 411 - HTTPLengthRequired - * 412 - HTTPPreconditionFailed - * 413 - HTTPRequestEntityTooLarge - * 414 - HTTPRequestURITooLong - * 415 - HTTPUnsupportedMediaType - * 416 - HTTPRequestRangeNotSatisfiable - * 417 - HTTPExpectationFailed - HTTPServerError - * 500 - HTTPInternalServerError - * 501 - HTTPNotImplemented - * 502 - HTTPBadGateway - * 503 - HTTPServiceUnavailable - * 504 - HTTPGatewayTimeout - * 505 - HTTPVersionNotSupported - -Each HTTP exception has the following attributes: - - ``code`` - the HTTP status code for the exception - - ``title`` - remainder of the status line (stuff after the code) - - ``explanation`` - a plain-text explanation of the error message that is - not subject to environment or header substitutions; - it is accessible in the template via ${explanation} - - ``detail`` - a plain-text message customization that is not subject - to environment or header substitutions; accessible in - the template via ${detail} - - ``body_template`` - a ``String.template``-format content fragment used for environment - and header substitution; the default template includes both - the explanation and further detail provided in the - message. - -Each HTTP exception accepts the following parameters: - - ``detail`` - a plain-text override of the default ``detail`` - - ``headers`` - a list of (k,v) header pairs - - ``comment`` - a plain-text additional information which is - usually stripped/hidden for end-users - - ``body_template`` - a ``string.Template`` object containing a content fragment in HTML - that frames the explanation and further detail - -Substitution of response headers into template values is always performed. -Substitution of WSGI environment values is performed if a ``request`` is -passed to the exception's constructor. - -The subclasses of :class:`~_HTTPMove` -(:class:`~HTTPMultipleChoices`, :class:`~HTTPMovedPermanently`, -:class:`~HTTPFound`, :class:`~HTTPSeeOther`, :class:`~HTTPUseProxy` and -:class:`~HTTPTemporaryRedirect`) are redirections that require a ``Location`` -field. Reflecting this, these subclasses have one additional keyword argument: -``location``, which indicates the location to which to redirect. -""" -import types -from string import Template -from webob import html_escape as _html_escape from zope.configuration.exceptions import ConfigurationError as ZCE -from zope.interface import implements - -from pyramid.interfaces import IExceptionResponse -from pyramid.response import Response - -def _no_escape(value): - if value is None: - return '' - if not isinstance(value, basestring): - if hasattr(value, '__unicode__'): - value = unicode(value) - else: - value = str(value) - return value - - -class HTTPException(Exception): # bw compat - pass - -class WSGIHTTPException(Response, HTTPException): - implements(IExceptionResponse) - - ## You should set in subclasses: - # code = 200 - # title = 'OK' - # explanation = 'why this happens' - # body_template_obj = Template('response template') - - # differences from webob.exc.WSGIHTTPException: - # - not a WSGI application (just a response) - # - # as a result: - # - # - bases plaintext vs. html result on self.content_type rather than - # on request environ - # - # - doesn't add request.environ keys to template substitutions unless - # 'request' is passed as a constructor keyword argument. - # - # - doesn't use "strip_tags" (${br} placeholder for
, no other html - # in default body template) - # - # - sets a default app_iter if no body, app_iter, or unicode_body is - # passed - # - # - explicitly sets self.message = detail to prevent whining by Python - # 2.6.5+ access of Exception.message - # - # - its base class of HTTPException is no longer a Python 2.4 compatibility - # shim; it's purely a base class that inherits from Exception. This - # implies that this class' ``exception`` property always returns - # ``self`` (only for bw compat at this point). - code = None - title = None - explanation = '' - body_template_obj = Template('''\ -${explanation}${br}${br} -${detail} -${html_comment} -''') - - plain_template_obj = Template('''\ -${status} - -${body}''') - - html_template_obj = Template('''\ - - - ${status} - - -

${status}

- ${body} - -''') - - ## Set this to True for responses that should have no request body - empty_body = False - - def __init__(self, detail=None, headers=None, comment=None, - body_template=None, **kw): - status = '%s %s' % (self.code, self.title) - Response.__init__(self, status=status, **kw) - Exception.__init__(self, detail) - self.detail = self.message = detail - if headers: - self.headers.extend(headers) - self.comment = comment - if body_template is not None: - self.body_template = body_template - self.body_template_obj = Template(body_template) - - if self.empty_body: - del self.content_type - del self.content_length - elif not ('unicode_body' in kw or 'body' in kw or 'app_iter' in kw): - self.app_iter = self._default_app_iter() - - def __str__(self): - return self.detail or self.explanation - - def _default_app_iter(self): - # This is a generator which defers the creation of the response page - # body; we use a generator because we want to ensure that if - # attributes of this response are changed after it is constructed, we - # use the changed values rather than the values at time of construction - # (e.g. self.content_type or self.charset). - html_comment = '' - comment = self.comment or '' - content_type = self.content_type or '' - if 'html' in content_type: - escape = _html_escape - page_template = self.html_template_obj - br = '
' - if comment: - html_comment = '' % escape(comment) - else: - escape = _no_escape - page_template = self.plain_template_obj - br = '\n' - if comment: - html_comment = escape(comment) - args = { - 'br':br, - 'explanation': escape(self.explanation), - 'detail': escape(self.detail or ''), - 'comment': escape(comment), - 'html_comment':html_comment, - } - body_tmpl = self.body_template_obj - if WSGIHTTPException.body_template_obj is not body_tmpl: - # Custom template; add headers to args - environ = self.environ - if environ is not None: - for k, v in environ.items(): - args[k] = escape(v) - for k, v in self.headers.items(): - args[k.lower()] = escape(v) - body = body_tmpl.substitute(args) - page = page_template.substitute(status=self.status, body=body) - if isinstance(page, unicode): - page = page.encode(self.charset) - yield page - raise StopIteration - - @property - def exception(self): - # bw compat only - return self - wsgi_response = exception # bw compat only - -class HTTPError(WSGIHTTPException): - """ - base class for status codes in the 400's and 500's - - This is an exception which indicates that an error has occurred, - and that any work in progress should not be committed. These are - typically results in the 400's and 500's. - """ - -class HTTPRedirection(WSGIHTTPException): - """ - base class for 300's status code (redirections) - - This is an abstract base class for 3xx redirection. It indicates - that further action needs to be taken by the user agent in order - to fulfill the request. It does not necessarly signal an error - condition. - """ - -class HTTPOk(WSGIHTTPException): - """ - Base class for the 200's status code (successful responses) - - code: 200, title: OK - """ - code = 200 - title = 'OK' - -############################################################ -## 2xx success -############################################################ - -class HTTPCreated(HTTPOk): - """ - subclass of :class:`~HTTPOk` - - This indicates that request has been fulfilled and resulted in a new - resource being created. - - code: 201, title: Created - """ - code = 201 - title = 'Created' - -class HTTPAccepted(HTTPOk): - """ - subclass of :class:`~HTTPOk` - - This indicates that the request has been accepted for processing, but the - processing has not been completed. - - code: 202, title: Accepted - """ - code = 202 - title = 'Accepted' - explanation = 'The request is accepted for processing.' - -class HTTPNonAuthoritativeInformation(HTTPOk): - """ - subclass of :class:`~HTTPOk` - - This indicates that the returned metainformation in the entity-header is - not the definitive set as available from the origin server, but is - gathered from a local or a third-party copy. - - code: 203, title: Non-Authoritative Information - """ - code = 203 - title = 'Non-Authoritative Information' - -class HTTPNoContent(HTTPOk): - """ - subclass of :class:`~HTTPOk` - - This indicates that the server has fulfilled the request but does - not need to return an entity-body, and might want to return updated - metainformation. - - code: 204, title: No Content - """ - code = 204 - title = 'No Content' - empty_body = True - -class HTTPResetContent(HTTPOk): - """ - subclass of :class:`~HTTPOk` - - This indicates that the the server has fulfilled the request and - the user agent SHOULD reset the document view which caused the - request to be sent. - - code: 205, title: Reset Content - """ - code = 205 - title = 'Reset Content' - empty_body = True - -class HTTPPartialContent(HTTPOk): - """ - subclass of :class:`~HTTPOk` - - This indicates that the server has fulfilled the partial GET - request for the resource. - - code: 206, title: Partial Content - """ - code = 206 - title = 'Partial Content' - -## FIXME: add 207 Multi-Status (but it's complicated) - -############################################################ -## 3xx redirection -############################################################ - -class _HTTPMove(HTTPRedirection): - """ - redirections which require a Location field - - Since a 'Location' header is a required attribute of 301, 302, 303, - 305 and 307 (but not 304), this base class provides the mechanics to - make this easy. - - You must provide a ``location`` keyword argument. - """ - # differences from webob.exc._HTTPMove: - # - # - not a wsgi app - # - # - ${location} isn't wrapped in an
tag in body - # - # - location keyword arg defaults to '' - # - # - ``add_slash`` argument is no longer accepted: code that passes - # add_slash argument to the constructor will receive an exception. - explanation = 'The resource has been moved to' - body_template_obj = Template('''\ -${explanation} ${location}; -you should be redirected automatically. -${detail} -${html_comment}''') - - def __init__(self, detail=None, headers=None, comment=None, - body_template=None, location='', **kw): - super(_HTTPMove, self).__init__( - detail=detail, headers=headers, comment=comment, - body_template=body_template, location=location, **kw) - -class HTTPMultipleChoices(_HTTPMove): - """ - subclass of :class:`~_HTTPMove` - - This indicates that the requested resource corresponds to any one - of a set of representations, each with its own specific location, - and agent-driven negotiation information is being provided so that - the user can select a preferred representation and redirect its - request to that location. - - code: 300, title: Multiple Choices - """ - code = 300 - title = 'Multiple Choices' - -class HTTPMovedPermanently(_HTTPMove): - """ - subclass of :class:`~_HTTPMove` - - This indicates that the requested resource has been assigned a new - permanent URI and any future references to this resource SHOULD use - one of the returned URIs. - - code: 301, title: Moved Permanently - """ - code = 301 - title = 'Moved Permanently' - -class HTTPFound(_HTTPMove): - """ - subclass of :class:`~_HTTPMove` - - This indicates that the requested resource resides temporarily under - a different URI. - - code: 302, title: Found - """ - code = 302 - title = 'Found' - explanation = 'The resource was found at' - -# This one is safe after a POST (the redirected location will be -# retrieved with GET): -class HTTPSeeOther(_HTTPMove): - """ - subclass of :class:`~_HTTPMove` - - This indicates that the response to the request can be found under - a different URI and SHOULD be retrieved using a GET method on that - resource. - - code: 303, title: See Other - """ - code = 303 - title = 'See Other' - -class HTTPNotModified(HTTPRedirection): - """ - subclass of :class:`~HTTPRedirection` - - This indicates that if the client has performed a conditional GET - request and access is allowed, but the document has not been - modified, the server SHOULD respond with this status code. - - code: 304, title: Not Modified - """ - # FIXME: this should include a date or etag header - code = 304 - title = 'Not Modified' - empty_body = True - -class HTTPUseProxy(_HTTPMove): - """ - subclass of :class:`~_HTTPMove` - - This indicates that the requested resource MUST be accessed through - the proxy given by the Location field. - - code: 305, title: Use Proxy - """ - # Not a move, but looks a little like one - code = 305 - title = 'Use Proxy' - explanation = ( - 'The resource must be accessed through a proxy located at') - -class HTTPTemporaryRedirect(_HTTPMove): - """ - subclass of :class:`~_HTTPMove` - - This indicates that the requested resource resides temporarily - under a different URI. - - code: 307, title: Temporary Redirect - """ - code = 307 - title = 'Temporary Redirect' - -############################################################ -## 4xx client error -############################################################ - -class HTTPClientError(HTTPError): - """ - base class for the 400's, where the client is in error - - This is an error condition in which the client is presumed to be - in-error. This is an expected problem, and thus is not considered - a bug. A server-side traceback is not warranted. Unless specialized, - this is a '400 Bad Request' - """ - code = 400 - title = 'Bad Request' - explanation = ('The server could not comply with the request since ' - 'it is either malformed or otherwise incorrect.') - -class HTTPBadRequest(HTTPClientError): - pass - -class HTTPUnauthorized(HTTPClientError): - """ - subclass of :class:`~HTTPClientError` - - This indicates that the request requires user authentication. - - code: 401, title: Unauthorized - """ - code = 401 - title = 'Unauthorized' - explanation = ( - 'This server could not verify that you are authorized to ' - 'access the document you requested. Either you supplied the ' - 'wrong credentials (e.g., bad password), or your browser ' - 'does not understand how to supply the credentials required.') - -class HTTPPaymentRequired(HTTPClientError): - """ - subclass of :class:`~HTTPClientError` - - code: 402, title: Payment Required - """ - code = 402 - title = 'Payment Required' - explanation = ('Access was denied for financial reasons.') - -class HTTPForbidden(HTTPClientError): - """ - subclass of :class:`~HTTPClientError` - This indicates that the server understood the request, but is - refusing to fulfill it. - - code: 403, title: Forbidden - - Raise this exception within :term:`view` code to immediately return the - :term:`forbidden view` to the invoking user. Usually this is a basic - ``403`` page, but the forbidden view can be customized as necessary. See - :ref:`changing_the_forbidden_view`. A ``Forbidden`` exception will be - the ``context`` of a :term:`Forbidden View`. - - This exception's constructor treats two arguments specially. The first - argument, ``detail``, should be a string. The value of this string will - be used as the ``message`` attribute of the exception object. The second - special keyword argument, ``result`` is usually an instance of - :class:`pyramid.security.Denied` or :class:`pyramid.security.ACLDenied` - each of which indicates a reason for the forbidden error. However, - ``result`` is also permitted to be just a plain boolean ``False`` object - or ``None``. The ``result`` value will be used as the ``result`` - attribute of the exception object. It defaults to ``None``. - - The :term:`Forbidden View` can use the attributes of a Forbidden - exception as necessary to provide extended information in an error - report shown to a user. - """ - # differences from webob.exc.HTTPForbidden: - # - # - accepts a ``result`` keyword argument - # - # - overrides constructor to set ``self.result`` - # - # differences from older pyramid.exceptions.Forbidden: - # - # - ``result`` must be passed as a keyword argument. - # - code = 403 - title = 'Forbidden' - explanation = ('Access was denied to this resource.') - def __init__(self, detail=None, headers=None, comment=None, - body_template=None, result=None, **kw): - HTTPClientError.__init__(self, detail=detail, headers=headers, - comment=comment, body_template=body_template, - **kw) - self.result = result - -class HTTPNotFound(HTTPClientError): - """ - subclass of :class:`~HTTPClientError` - - This indicates that the server did not find anything matching the - Request-URI. - - code: 404, title: Not Found - - Raise this exception within :term:`view` code to immediately - return the :term:`Not Found view` to the invoking user. Usually - this is a basic ``404`` page, but the Not Found view can be - customized as necessary. See :ref:`changing_the_notfound_view`. - - This exception's constructor accepts a ``detail`` argument - (the first argument), which should be a string. The value of this - string will be available as the ``message`` attribute of this exception, - for availability to the :term:`Not Found View`. - """ - code = 404 - title = 'Not Found' - explanation = ('The resource could not be found.') - -class HTTPMethodNotAllowed(HTTPClientError): - """ - subclass of :class:`~HTTPClientError` - - This indicates that the method specified in the Request-Line is - not allowed for the resource identified by the Request-URI. - - code: 405, title: Method Not Allowed - """ - # differences from webob.exc.HTTPMethodNotAllowed: - # - # - body_template_obj not overridden (it tried to use request environ's - # REQUEST_METHOD) - code = 405 - title = 'Method Not Allowed' - -class HTTPNotAcceptable(HTTPClientError): - """ - subclass of :class:`~HTTPClientError` - - This indicates the resource identified by the request is only - capable of generating response entities which have content - characteristics not acceptable according to the accept headers - sent in the request. - - code: 406, title: Not Acceptable - """ - # differences from webob.exc.HTTPNotAcceptable: - # - # - body_template_obj not overridden (it tried to use request environ's - # HTTP_ACCEPT) - code = 406 - title = 'Not Acceptable' - -class HTTPProxyAuthenticationRequired(HTTPClientError): - """ - subclass of :class:`~HTTPClientError` - - This is similar to 401, but indicates that the client must first - authenticate itself with the proxy. - - code: 407, title: Proxy Authentication Required - """ - code = 407 - title = 'Proxy Authentication Required' - explanation = ('Authentication with a local proxy is needed.') - -class HTTPRequestTimeout(HTTPClientError): - """ - subclass of :class:`~HTTPClientError` - - This indicates that the client did not produce a request within - the time that the server was prepared to wait. - - code: 408, title: Request Timeout - """ - code = 408 - title = 'Request Timeout' - explanation = ('The server has waited too long for the request to ' - 'be sent by the client.') - -class HTTPConflict(HTTPClientError): - """ - subclass of :class:`~HTTPClientError` - - This indicates that the request could not be completed due to a - conflict with the current state of the resource. - - code: 409, title: Conflict - """ - code = 409 - title = 'Conflict' - explanation = ('There was a conflict when trying to complete ' - 'your request.') - -class HTTPGone(HTTPClientError): - """ - subclass of :class:`~HTTPClientError` - - This indicates that the requested resource is no longer available - at the server and no forwarding address is known. - - code: 410, title: Gone - """ - code = 410 - title = 'Gone' - explanation = ('This resource is no longer available. No forwarding ' - 'address is given.') - -class HTTPLengthRequired(HTTPClientError): - """ - subclass of :class:`~HTTPClientError` - - This indicates that the the server refuses to accept the request - without a defined Content-Length. - - code: 411, title: Length Required - """ - code = 411 - title = 'Length Required' - explanation = ('Content-Length header required.') - -class HTTPPreconditionFailed(HTTPClientError): - """ - subclass of :class:`~HTTPClientError` - - This indicates that the precondition given in one or more of the - request-header fields evaluated to false when it was tested on the - server. - - code: 412, title: Precondition Failed - """ - code = 412 - title = 'Precondition Failed' - explanation = ('Request precondition failed.') - -class HTTPRequestEntityTooLarge(HTTPClientError): - """ - subclass of :class:`~HTTPClientError` - - This indicates that the server is refusing to process a request - because the request entity is larger than the server is willing or - able to process. - - code: 413, title: Request Entity Too Large - """ - code = 413 - title = 'Request Entity Too Large' - explanation = ('The body of your request was too large for this server.') - -class HTTPRequestURITooLong(HTTPClientError): - """ - subclass of :class:`~HTTPClientError` - - This indicates that the server is refusing to service the request - because the Request-URI is longer than the server is willing to - interpret. - - code: 414, title: Request-URI Too Long - """ - code = 414 - title = 'Request-URI Too Long' - explanation = ('The request URI was too long for this server.') - -class HTTPUnsupportedMediaType(HTTPClientError): - """ - subclass of :class:`~HTTPClientError` - - This indicates that the server is refusing to service the request - because the entity of the request is in a format not supported by - the requested resource for the requested method. - - code: 415, title: Unsupported Media Type - """ - # differences from webob.exc.HTTPUnsupportedMediaType: - # - # - body_template_obj not overridden (it tried to use request environ's - # CONTENT_TYPE) - code = 415 - title = 'Unsupported Media Type' - -class HTTPRequestRangeNotSatisfiable(HTTPClientError): - """ - subclass of :class:`~HTTPClientError` - - The server SHOULD return a response with this status code if a - request included a Range request-header field, and none of the - range-specifier values in this field overlap the current extent - of the selected resource, and the request did not include an - If-Range request-header field. - - code: 416, title: Request Range Not Satisfiable - """ - code = 416 - title = 'Request Range Not Satisfiable' - explanation = ('The Range requested is not available.') - -class HTTPExpectationFailed(HTTPClientError): - """ - subclass of :class:`~HTTPClientError` - - This indidcates that the expectation given in an Expect - request-header field could not be met by this server. - - code: 417, title: Expectation Failed - """ - code = 417 - title = 'Expectation Failed' - explanation = ('Expectation failed.') - -class HTTPUnprocessableEntity(HTTPClientError): - """ - subclass of :class:`~HTTPClientError` - - This indicates that the server is unable to process the contained - instructions. Only for WebDAV. - - code: 422, title: Unprocessable Entity - """ - ## Note: from WebDAV - code = 422 - title = 'Unprocessable Entity' - explanation = 'Unable to process the contained instructions' - -class HTTPLocked(HTTPClientError): - """ - subclass of :class:`~HTTPClientError` - - This indicates that the resource is locked. Only for WebDAV - - code: 423, title: Locked - """ - ## Note: from WebDAV - code = 423 - title = 'Locked' - explanation = ('The resource is locked') - -class HTTPFailedDependency(HTTPClientError): - """ - subclass of :class:`~HTTPClientError` - - This indicates that the method could not be performed because the - requested action depended on another action and that action failed. - Only for WebDAV. - - code: 424, title: Failed Dependency - """ - ## Note: from WebDAV - code = 424 - title = 'Failed Dependency' - explanation = ( - 'The method could not be performed because the requested ' - 'action dependended on another action and that action failed') - -############################################################ -## 5xx Server Error -############################################################ -# Response status codes beginning with the digit "5" indicate cases in -# which the server is aware that it has erred or is incapable of -# performing the request. Except when responding to a HEAD request, the -# server SHOULD include an entity containing an explanation of the error -# situation, and whether it is a temporary or permanent condition. User -# agents SHOULD display any included entity to the user. These response -# codes are applicable to any request method. - -class HTTPServerError(HTTPError): - """ - base class for the 500's, where the server is in-error - - This is an error condition in which the server is presumed to be - in-error. This is usually unexpected, and thus requires a traceback; - ideally, opening a support ticket for the customer. Unless specialized, - this is a '500 Internal Server Error' - """ - code = 500 - title = 'Internal Server Error' - explanation = ( - 'The server has either erred or is incapable of performing ' - 'the requested operation.') - -class HTTPInternalServerError(HTTPServerError): - pass - -class HTTPNotImplemented(HTTPServerError): - """ - subclass of :class:`~HTTPServerError` - - This indicates that the server does not support the functionality - required to fulfill the request. - - code: 501, title: Not Implemented - """ - # differences from webob.exc.HTTPNotAcceptable: - # - # - body_template_obj not overridden (it tried to use request environ's - # REQUEST_METHOD) - code = 501 - title = 'Not Implemented' - -class HTTPBadGateway(HTTPServerError): - """ - subclass of :class:`~HTTPServerError` - - This indicates that the server, while acting as a gateway or proxy, - received an invalid response from the upstream server it accessed - in attempting to fulfill the request. - - code: 502, title: Bad Gateway - """ - code = 502 - title = 'Bad Gateway' - explanation = ('Bad gateway.') - -class HTTPServiceUnavailable(HTTPServerError): - """ - subclass of :class:`~HTTPServerError` - - This indicates that the server is currently unable to handle the - request due to a temporary overloading or maintenance of the server. - - code: 503, title: Service Unavailable - """ - code = 503 - title = 'Service Unavailable' - explanation = ('The server is currently unavailable. ' - 'Please try again at a later time.') - -class HTTPGatewayTimeout(HTTPServerError): - """ - subclass of :class:`~HTTPServerError` - - This indicates that the server, while acting as a gateway or proxy, - did not receive a timely response from the upstream server specified - by the URI (e.g. HTTP, FTP, LDAP) or some other auxiliary server - (e.g. DNS) it needed to access in attempting to complete the request. - - code: 504, title: Gateway Timeout - """ - code = 504 - title = 'Gateway Timeout' - explanation = ('The gateway has timed out.') - -class HTTPVersionNotSupported(HTTPServerError): - """ - subclass of :class:`~HTTPServerError` - - This indicates that the server does not support, or refuses to - support, the HTTP protocol version that was used in the request - message. - - code: 505, title: HTTP Version Not Supported - """ - code = 505 - title = 'HTTP Version Not Supported' - explanation = ('The HTTP version is not supported.') - -class HTTPInsufficientStorage(HTTPServerError): - """ - subclass of :class:`~HTTPServerError` - - This indicates that the server does not have enough space to save - the resource. - - code: 507, title: Insufficient Storage - """ - code = 507 - title = 'Insufficient Storage' - explanation = ('There was not enough space to save the resource') +from pyramid.response import HTTPNotFound +from pyramid.response import HTTPForbidden NotFound = HTTPNotFound # bw compat Forbidden = HTTPForbidden # bw compat @@ -1008,48 +31,3 @@ class ConfigurationError(ZCE): method of a :term:`Configurator`""" -def abort(status_code, **kw): - """Aborts the request immediately by raising an HTTP exception based on a - status code. Example:: - - abort(404) # raises an HTTPNotFound exception. - - The values passed as ``kw`` are provided to the exception's constructor. - """ - exc = status_map[status_code](**kw) - raise exc - - -def redirect(url, code=302, **kw): - """Raises an :class:`~HTTPFound` (302) redirect exception to the - URL specified by ``url``. - - Optionally, a code variable may be passed with the status code of - the redirect, ie:: - - redirect(route_url('foo', request), code=303) - - The values passed as ``kw`` are provided to the exception constructor. - - """ - exc = status_map[code] - raise exc(location=url, **kw) - -def default_exceptionresponse_view(context, request): - if not isinstance(context, Exception): - # backwards compat for an exception response view registered via - # config.set_notfound_view or config.set_forbidden_view - # instead of as a proper exception view - context = request.exception or context - return context - -status_map={} -for name, value in globals().items(): - if (isinstance(value, (type, types.ClassType)) and - issubclass(value, HTTPException) - and not name.startswith('_')): - code = getattr(value, 'code', None) - if code: - status_map[code] = value -del name, value - diff --git a/pyramid/httpexceptions.py b/pyramid/httpexceptions.py index 8b2a012cc..dbb530b4a 100644 --- a/pyramid/httpexceptions.py +++ b/pyramid/httpexceptions.py @@ -1,2 +1,116 @@ -from pyramid.exceptions import * # bw compat +""" +HTTP Exceptions +--------------- + +This module contains Pyramid HTTP exception classes. Each class relates to a +single HTTP status code. Each class is a subclass of the +:class:`~HTTPException`. Each exception class is also a :term:`response` +object. + +Each exception class has a status code according to `RFC 2068 +`: codes with 100-300 are not really +errors; 400's are client errors, and 500's are server errors. + +Exception + HTTPException + HTTPOk + * 200 - HTTPOk + * 201 - HTTPCreated + * 202 - HTTPAccepted + * 203 - HTTPNonAuthoritativeInformation + * 204 - HTTPNoContent + * 205 - HTTPResetContent + * 206 - HTTPPartialContent + HTTPRedirection + * 300 - HTTPMultipleChoices + * 301 - HTTPMovedPermanently + * 302 - HTTPFound + * 303 - HTTPSeeOther + * 304 - HTTPNotModified + * 305 - HTTPUseProxy + * 306 - Unused (not implemented, obviously) + * 307 - HTTPTemporaryRedirect + HTTPError + HTTPClientError + * 400 - HTTPBadRequest + * 401 - HTTPUnauthorized + * 402 - HTTPPaymentRequired + * 403 - HTTPForbidden + * 404 - HTTPNotFound + * 405 - HTTPMethodNotAllowed + * 406 - HTTPNotAcceptable + * 407 - HTTPProxyAuthenticationRequired + * 408 - HTTPRequestTimeout + * 409 - HTTPConflict + * 410 - HTTPGone + * 411 - HTTPLengthRequired + * 412 - HTTPPreconditionFailed + * 413 - HTTPRequestEntityTooLarge + * 414 - HTTPRequestURITooLong + * 415 - HTTPUnsupportedMediaType + * 416 - HTTPRequestRangeNotSatisfiable + * 417 - HTTPExpectationFailed + HTTPServerError + * 500 - HTTPInternalServerError + * 501 - HTTPNotImplemented + * 502 - HTTPBadGateway + * 503 - HTTPServiceUnavailable + * 504 - HTTPGatewayTimeout + * 505 - HTTPVersionNotSupported + +Each HTTP exception has the following attributes: + + ``code`` + the HTTP status code for the exception + + ``title`` + remainder of the status line (stuff after the code) + + ``explanation`` + a plain-text explanation of the error message that is + not subject to environment or header substitutions; + it is accessible in the template via ${explanation} + + ``detail`` + a plain-text message customization that is not subject + to environment or header substitutions; accessible in + the template via ${detail} + + ``body_template`` + a ``String.template``-format content fragment used for environment + and header substitution; the default template includes both + the explanation and further detail provided in the + message. + +Each HTTP exception accepts the following parameters: + + ``detail`` + a plain-text override of the default ``detail`` + + ``headers`` + a list of (k,v) header pairs + + ``comment`` + a plain-text additional information which is + usually stripped/hidden for end-users + + ``body_template`` + a ``string.Template`` object containing a content fragment in HTML + that frames the explanation and further detail + +Substitution of response headers into template values is always performed. +Substitution of WSGI environment values is performed if a ``request`` is +passed to the exception's constructor. + +The subclasses of :class:`~_HTTPMove` +(:class:`~HTTPMultipleChoices`, :class:`~HTTPMovedPermanently`, +:class:`~HTTPFound`, :class:`~HTTPSeeOther`, :class:`~HTTPUseProxy` and +:class:`~HTTPTemporaryRedirect`) are redirections that require a ``Location`` +field. Reflecting this, these subclasses have one additional keyword argument: +``location``, which indicates the location to which to redirect. +""" + +from pyramid.response import * # API + + diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index d200d15cf..237727b41 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -55,12 +55,13 @@ class IException(Interface): # not an API """ An interface representing a generic exception """ class IExceptionResponse(IException, IResponse): - """ An interface representing a WSGI response which is also an - exception object. Register an exception view using this interface - as a ``context`` to apply the registered view for all exception - types raised by :app:`Pyramid` internally - (:class:`pyramid.exceptions.NotFound` and - :class:`pyramid.exceptions.Forbidden`).""" + """ An interface representing a WSGI response which is also an exception + object. Register an exception view using this interface as a ``context`` + to apply the registered view for all exception types raised by + :app:`Pyramid` internally (any exception that inherits from + :class:`pyramid.response.Response`, including + :class:`pyramid.response.HTTPNotFound` and + :class:`pyramid.response.HTTPForbidden`).""" class IBeforeRender(Interface): """ @@ -274,9 +275,9 @@ class IExceptionViewClassifier(Interface): class IView(Interface): def __call__(context, request): """ Must return an object that implements IResponse. May - optionally raise ``pyramid.exceptions.Forbidden`` if an + optionally raise ``pyramid.response.HTTPForbidden`` if an authorization failure is detected during view execution or - ``pyramid.exceptions.NotFound`` if the not found page is + ``pyramid.response.HTTPNotFound`` if the not found page is meant to be returned.""" class ISecuredView(IView): diff --git a/pyramid/response.py b/pyramid/response.py index e9f5528a5..41ac354f9 100644 --- a/pyramid/response.py +++ b/pyramid/response.py @@ -1,8 +1,948 @@ +import types +from string import Template + from webob import Response as _Response +from webob import html_escape as _html_escape from zope.interface import implements +from zope.configuration.exceptions import ConfigurationError as ZCE from pyramid.interfaces import IExceptionResponse class Response(_Response, Exception): implements(IExceptionResponse) +def _no_escape(value): + if value is None: + return '' + if not isinstance(value, basestring): + if hasattr(value, '__unicode__'): + value = unicode(value) + else: + value = str(value) + return value + + +class HTTPException(Exception): # bw compat + pass + +class WSGIHTTPException(Response, HTTPException): + implements(IExceptionResponse) + + ## You should set in subclasses: + # code = 200 + # title = 'OK' + # explanation = 'why this happens' + # body_template_obj = Template('response template') + + # differences from webob.exc.WSGIHTTPException: + # - not a WSGI application (just a response) + # + # as a result: + # + # - bases plaintext vs. html result on self.content_type rather than + # on request environ + # + # - doesn't add request.environ keys to template substitutions unless + # 'request' is passed as a constructor keyword argument. + # + # - doesn't use "strip_tags" (${br} placeholder for
, no other html + # in default body template) + # + # - sets a default app_iter if no body, app_iter, or unicode_body is + # passed + # + # - explicitly sets self.message = detail to prevent whining by Python + # 2.6.5+ access of Exception.message + # + # - its base class of HTTPException is no longer a Python 2.4 compatibility + # shim; it's purely a base class that inherits from Exception. This + # implies that this class' ``exception`` property always returns + # ``self`` (only for bw compat at this point). + code = None + title = None + explanation = '' + body_template_obj = Template('''\ +${explanation}${br}${br} +${detail} +${html_comment} +''') + + plain_template_obj = Template('''\ +${status} + +${body}''') + + html_template_obj = Template('''\ + + + ${status} + + +

${status}

+ ${body} + +''') + + ## Set this to True for responses that should have no request body + empty_body = False + + def __init__(self, detail=None, headers=None, comment=None, + body_template=None, **kw): + status = '%s %s' % (self.code, self.title) + Response.__init__(self, status=status, **kw) + Exception.__init__(self, detail) + self.detail = self.message = detail + if headers: + self.headers.extend(headers) + self.comment = comment + if body_template is not None: + self.body_template = body_template + self.body_template_obj = Template(body_template) + + if self.empty_body: + del self.content_type + del self.content_length + elif not ('unicode_body' in kw or 'body' in kw or 'app_iter' in kw): + self.app_iter = self._default_app_iter() + + def __str__(self): + return self.detail or self.explanation + + def _default_app_iter(self): + # This is a generator which defers the creation of the response page + # body; we use a generator because we want to ensure that if + # attributes of this response are changed after it is constructed, we + # use the changed values rather than the values at time of construction + # (e.g. self.content_type or self.charset). + html_comment = '' + comment = self.comment or '' + content_type = self.content_type or '' + if 'html' in content_type: + escape = _html_escape + page_template = self.html_template_obj + br = '
' + if comment: + html_comment = '' % escape(comment) + else: + escape = _no_escape + page_template = self.plain_template_obj + br = '\n' + if comment: + html_comment = escape(comment) + args = { + 'br':br, + 'explanation': escape(self.explanation), + 'detail': escape(self.detail or ''), + 'comment': escape(comment), + 'html_comment':html_comment, + } + body_tmpl = self.body_template_obj + if WSGIHTTPException.body_template_obj is not body_tmpl: + # Custom template; add headers to args + environ = self.environ + if environ is not None: + for k, v in environ.items(): + args[k] = escape(v) + for k, v in self.headers.items(): + args[k.lower()] = escape(v) + body = body_tmpl.substitute(args) + page = page_template.substitute(status=self.status, body=body) + if isinstance(page, unicode): + page = page.encode(self.charset) + yield page + raise StopIteration + + @property + def exception(self): + # bw compat only + return self + wsgi_response = exception # bw compat only + +class HTTPError(WSGIHTTPException): + """ + base class for status codes in the 400's and 500's + + This is an exception which indicates that an error has occurred, + and that any work in progress should not be committed. These are + typically results in the 400's and 500's. + """ + +class HTTPRedirection(WSGIHTTPException): + """ + base class for 300's status code (redirections) + + This is an abstract base class for 3xx redirection. It indicates + that further action needs to be taken by the user agent in order + to fulfill the request. It does not necessarly signal an error + condition. + """ + +class HTTPOk(WSGIHTTPException): + """ + Base class for the 200's status code (successful responses) + + code: 200, title: OK + """ + code = 200 + title = 'OK' + +############################################################ +## 2xx success +############################################################ + +class HTTPCreated(HTTPOk): + """ + subclass of :class:`~HTTPOk` + + This indicates that request has been fulfilled and resulted in a new + resource being created. + + code: 201, title: Created + """ + code = 201 + title = 'Created' + +class HTTPAccepted(HTTPOk): + """ + subclass of :class:`~HTTPOk` + + This indicates that the request has been accepted for processing, but the + processing has not been completed. + + code: 202, title: Accepted + """ + code = 202 + title = 'Accepted' + explanation = 'The request is accepted for processing.' + +class HTTPNonAuthoritativeInformation(HTTPOk): + """ + subclass of :class:`~HTTPOk` + + This indicates that the returned metainformation in the entity-header is + not the definitive set as available from the origin server, but is + gathered from a local or a third-party copy. + + code: 203, title: Non-Authoritative Information + """ + code = 203 + title = 'Non-Authoritative Information' + +class HTTPNoContent(HTTPOk): + """ + subclass of :class:`~HTTPOk` + + This indicates that the server has fulfilled the request but does + not need to return an entity-body, and might want to return updated + metainformation. + + code: 204, title: No Content + """ + code = 204 + title = 'No Content' + empty_body = True + +class HTTPResetContent(HTTPOk): + """ + subclass of :class:`~HTTPOk` + + This indicates that the the server has fulfilled the request and + the user agent SHOULD reset the document view which caused the + request to be sent. + + code: 205, title: Reset Content + """ + code = 205 + title = 'Reset Content' + empty_body = True + +class HTTPPartialContent(HTTPOk): + """ + subclass of :class:`~HTTPOk` + + This indicates that the server has fulfilled the partial GET + request for the resource. + + code: 206, title: Partial Content + """ + code = 206 + title = 'Partial Content' + +## FIXME: add 207 Multi-Status (but it's complicated) + +############################################################ +## 3xx redirection +############################################################ + +class _HTTPMove(HTTPRedirection): + """ + redirections which require a Location field + + Since a 'Location' header is a required attribute of 301, 302, 303, + 305 and 307 (but not 304), this base class provides the mechanics to + make this easy. + + You must provide a ``location`` keyword argument. + """ + # differences from webob.exc._HTTPMove: + # + # - not a wsgi app + # + # - ${location} isn't wrapped in an
tag in body + # + # - location keyword arg defaults to '' + # + # - ``add_slash`` argument is no longer accepted: code that passes + # add_slash argument to the constructor will receive an exception. + explanation = 'The resource has been moved to' + body_template_obj = Template('''\ +${explanation} ${location}; +you should be redirected automatically. +${detail} +${html_comment}''') + + def __init__(self, detail=None, headers=None, comment=None, + body_template=None, location='', **kw): + super(_HTTPMove, self).__init__( + detail=detail, headers=headers, comment=comment, + body_template=body_template, location=location, **kw) + +class HTTPMultipleChoices(_HTTPMove): + """ + subclass of :class:`~_HTTPMove` + + This indicates that the requested resource corresponds to any one + of a set of representations, each with its own specific location, + and agent-driven negotiation information is being provided so that + the user can select a preferred representation and redirect its + request to that location. + + code: 300, title: Multiple Choices + """ + code = 300 + title = 'Multiple Choices' + +class HTTPMovedPermanently(_HTTPMove): + """ + subclass of :class:`~_HTTPMove` + + This indicates that the requested resource has been assigned a new + permanent URI and any future references to this resource SHOULD use + one of the returned URIs. + + code: 301, title: Moved Permanently + """ + code = 301 + title = 'Moved Permanently' + +class HTTPFound(_HTTPMove): + """ + subclass of :class:`~_HTTPMove` + + This indicates that the requested resource resides temporarily under + a different URI. + + code: 302, title: Found + """ + code = 302 + title = 'Found' + explanation = 'The resource was found at' + +# This one is safe after a POST (the redirected location will be +# retrieved with GET): +class HTTPSeeOther(_HTTPMove): + """ + subclass of :class:`~_HTTPMove` + + This indicates that the response to the request can be found under + a different URI and SHOULD be retrieved using a GET method on that + resource. + + code: 303, title: See Other + """ + code = 303 + title = 'See Other' + +class HTTPNotModified(HTTPRedirection): + """ + subclass of :class:`~HTTPRedirection` + + This indicates that if the client has performed a conditional GET + request and access is allowed, but the document has not been + modified, the server SHOULD respond with this status code. + + code: 304, title: Not Modified + """ + # FIXME: this should include a date or etag header + code = 304 + title = 'Not Modified' + empty_body = True + +class HTTPUseProxy(_HTTPMove): + """ + subclass of :class:`~_HTTPMove` + + This indicates that the requested resource MUST be accessed through + the proxy given by the Location field. + + code: 305, title: Use Proxy + """ + # Not a move, but looks a little like one + code = 305 + title = 'Use Proxy' + explanation = ( + 'The resource must be accessed through a proxy located at') + +class HTTPTemporaryRedirect(_HTTPMove): + """ + subclass of :class:`~_HTTPMove` + + This indicates that the requested resource resides temporarily + under a different URI. + + code: 307, title: Temporary Redirect + """ + code = 307 + title = 'Temporary Redirect' + +############################################################ +## 4xx client error +############################################################ + +class HTTPClientError(HTTPError): + """ + base class for the 400's, where the client is in error + + This is an error condition in which the client is presumed to be + in-error. This is an expected problem, and thus is not considered + a bug. A server-side traceback is not warranted. Unless specialized, + this is a '400 Bad Request' + """ + code = 400 + title = 'Bad Request' + explanation = ('The server could not comply with the request since ' + 'it is either malformed or otherwise incorrect.') + +class HTTPBadRequest(HTTPClientError): + pass + +class HTTPUnauthorized(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the request requires user authentication. + + code: 401, title: Unauthorized + """ + code = 401 + title = 'Unauthorized' + explanation = ( + 'This server could not verify that you are authorized to ' + 'access the document you requested. Either you supplied the ' + 'wrong credentials (e.g., bad password), or your browser ' + 'does not understand how to supply the credentials required.') + +class HTTPPaymentRequired(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + code: 402, title: Payment Required + """ + code = 402 + title = 'Payment Required' + explanation = ('Access was denied for financial reasons.') + +class HTTPForbidden(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the server understood the request, but is + refusing to fulfill it. + + code: 403, title: Forbidden + + Raise this exception within :term:`view` code to immediately return the + :term:`forbidden view` to the invoking user. Usually this is a basic + ``403`` page, but the forbidden view can be customized as necessary. See + :ref:`changing_the_forbidden_view`. A ``Forbidden`` exception will be + the ``context`` of a :term:`Forbidden View`. + + This exception's constructor treats two arguments specially. The first + argument, ``detail``, should be a string. The value of this string will + be used as the ``message`` attribute of the exception object. The second + special keyword argument, ``result`` is usually an instance of + :class:`pyramid.security.Denied` or :class:`pyramid.security.ACLDenied` + each of which indicates a reason for the forbidden error. However, + ``result`` is also permitted to be just a plain boolean ``False`` object + or ``None``. The ``result`` value will be used as the ``result`` + attribute of the exception object. It defaults to ``None``. + + The :term:`Forbidden View` can use the attributes of a Forbidden + exception as necessary to provide extended information in an error + report shown to a user. + """ + # differences from webob.exc.HTTPForbidden: + # + # - accepts a ``result`` keyword argument + # + # - overrides constructor to set ``self.result`` + # + # differences from older ``pyramid.exceptions.Forbidden``: + # + # - ``result`` must be passed as a keyword argument. + # + code = 403 + title = 'Forbidden' + explanation = ('Access was denied to this resource.') + def __init__(self, detail=None, headers=None, comment=None, + body_template=None, result=None, **kw): + HTTPClientError.__init__(self, detail=detail, headers=headers, + comment=comment, body_template=body_template, + **kw) + self.result = result + +class HTTPNotFound(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the server did not find anything matching the + Request-URI. + + code: 404, title: Not Found + + Raise this exception within :term:`view` code to immediately + return the :term:`Not Found view` to the invoking user. Usually + this is a basic ``404`` page, but the Not Found view can be + customized as necessary. See :ref:`changing_the_notfound_view`. + + This exception's constructor accepts a ``detail`` argument + (the first argument), which should be a string. The value of this + string will be available as the ``message`` attribute of this exception, + for availability to the :term:`Not Found View`. + """ + code = 404 + title = 'Not Found' + explanation = ('The resource could not be found.') + +class HTTPMethodNotAllowed(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the method specified in the Request-Line is + not allowed for the resource identified by the Request-URI. + + code: 405, title: Method Not Allowed + """ + # differences from webob.exc.HTTPMethodNotAllowed: + # + # - body_template_obj not overridden (it tried to use request environ's + # REQUEST_METHOD) + code = 405 + title = 'Method Not Allowed' + +class HTTPNotAcceptable(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates the resource identified by the request is only + capable of generating response entities which have content + characteristics not acceptable according to the accept headers + sent in the request. + + code: 406, title: Not Acceptable + """ + # differences from webob.exc.HTTPNotAcceptable: + # + # - body_template_obj not overridden (it tried to use request environ's + # HTTP_ACCEPT) + code = 406 + title = 'Not Acceptable' + +class HTTPProxyAuthenticationRequired(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This is similar to 401, but indicates that the client must first + authenticate itself with the proxy. + + code: 407, title: Proxy Authentication Required + """ + code = 407 + title = 'Proxy Authentication Required' + explanation = ('Authentication with a local proxy is needed.') + +class HTTPRequestTimeout(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the client did not produce a request within + the time that the server was prepared to wait. + + code: 408, title: Request Timeout + """ + code = 408 + title = 'Request Timeout' + explanation = ('The server has waited too long for the request to ' + 'be sent by the client.') + +class HTTPConflict(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the request could not be completed due to a + conflict with the current state of the resource. + + code: 409, title: Conflict + """ + code = 409 + title = 'Conflict' + explanation = ('There was a conflict when trying to complete ' + 'your request.') + +class HTTPGone(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the requested resource is no longer available + at the server and no forwarding address is known. + + code: 410, title: Gone + """ + code = 410 + title = 'Gone' + explanation = ('This resource is no longer available. No forwarding ' + 'address is given.') + +class HTTPLengthRequired(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the the server refuses to accept the request + without a defined Content-Length. + + code: 411, title: Length Required + """ + code = 411 + title = 'Length Required' + explanation = ('Content-Length header required.') + +class HTTPPreconditionFailed(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the precondition given in one or more of the + request-header fields evaluated to false when it was tested on the + server. + + code: 412, title: Precondition Failed + """ + code = 412 + title = 'Precondition Failed' + explanation = ('Request precondition failed.') + +class HTTPRequestEntityTooLarge(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the server is refusing to process a request + because the request entity is larger than the server is willing or + able to process. + + code: 413, title: Request Entity Too Large + """ + code = 413 + title = 'Request Entity Too Large' + explanation = ('The body of your request was too large for this server.') + +class HTTPRequestURITooLong(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the server is refusing to service the request + because the Request-URI is longer than the server is willing to + interpret. + + code: 414, title: Request-URI Too Long + """ + code = 414 + title = 'Request-URI Too Long' + explanation = ('The request URI was too long for this server.') + +class HTTPUnsupportedMediaType(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the server is refusing to service the request + because the entity of the request is in a format not supported by + the requested resource for the requested method. + + code: 415, title: Unsupported Media Type + """ + # differences from webob.exc.HTTPUnsupportedMediaType: + # + # - body_template_obj not overridden (it tried to use request environ's + # CONTENT_TYPE) + code = 415 + title = 'Unsupported Media Type' + +class HTTPRequestRangeNotSatisfiable(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + The server SHOULD return a response with this status code if a + request included a Range request-header field, and none of the + range-specifier values in this field overlap the current extent + of the selected resource, and the request did not include an + If-Range request-header field. + + code: 416, title: Request Range Not Satisfiable + """ + code = 416 + title = 'Request Range Not Satisfiable' + explanation = ('The Range requested is not available.') + +class HTTPExpectationFailed(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indidcates that the expectation given in an Expect + request-header field could not be met by this server. + + code: 417, title: Expectation Failed + """ + code = 417 + title = 'Expectation Failed' + explanation = ('Expectation failed.') + +class HTTPUnprocessableEntity(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the server is unable to process the contained + instructions. Only for WebDAV. + + code: 422, title: Unprocessable Entity + """ + ## Note: from WebDAV + code = 422 + title = 'Unprocessable Entity' + explanation = 'Unable to process the contained instructions' + +class HTTPLocked(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the resource is locked. Only for WebDAV + + code: 423, title: Locked + """ + ## Note: from WebDAV + code = 423 + title = 'Locked' + explanation = ('The resource is locked') + +class HTTPFailedDependency(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the method could not be performed because the + requested action depended on another action and that action failed. + Only for WebDAV. + + code: 424, title: Failed Dependency + """ + ## Note: from WebDAV + code = 424 + title = 'Failed Dependency' + explanation = ( + 'The method could not be performed because the requested ' + 'action dependended on another action and that action failed') + +############################################################ +## 5xx Server Error +############################################################ +# Response status codes beginning with the digit "5" indicate cases in +# which the server is aware that it has erred or is incapable of +# performing the request. Except when responding to a HEAD request, the +# server SHOULD include an entity containing an explanation of the error +# situation, and whether it is a temporary or permanent condition. User +# agents SHOULD display any included entity to the user. These response +# codes are applicable to any request method. + +class HTTPServerError(HTTPError): + """ + base class for the 500's, where the server is in-error + + This is an error condition in which the server is presumed to be + in-error. This is usually unexpected, and thus requires a traceback; + ideally, opening a support ticket for the customer. Unless specialized, + this is a '500 Internal Server Error' + """ + code = 500 + title = 'Internal Server Error' + explanation = ( + 'The server has either erred or is incapable of performing ' + 'the requested operation.') + +class HTTPInternalServerError(HTTPServerError): + pass + +class HTTPNotImplemented(HTTPServerError): + """ + subclass of :class:`~HTTPServerError` + + This indicates that the server does not support the functionality + required to fulfill the request. + + code: 501, title: Not Implemented + """ + # differences from webob.exc.HTTPNotAcceptable: + # + # - body_template_obj not overridden (it tried to use request environ's + # REQUEST_METHOD) + code = 501 + title = 'Not Implemented' + +class HTTPBadGateway(HTTPServerError): + """ + subclass of :class:`~HTTPServerError` + + This indicates that the server, while acting as a gateway or proxy, + received an invalid response from the upstream server it accessed + in attempting to fulfill the request. + + code: 502, title: Bad Gateway + """ + code = 502 + title = 'Bad Gateway' + explanation = ('Bad gateway.') + +class HTTPServiceUnavailable(HTTPServerError): + """ + subclass of :class:`~HTTPServerError` + + This indicates that the server is currently unable to handle the + request due to a temporary overloading or maintenance of the server. + + code: 503, title: Service Unavailable + """ + code = 503 + title = 'Service Unavailable' + explanation = ('The server is currently unavailable. ' + 'Please try again at a later time.') + +class HTTPGatewayTimeout(HTTPServerError): + """ + subclass of :class:`~HTTPServerError` + + This indicates that the server, while acting as a gateway or proxy, + did not receive a timely response from the upstream server specified + by the URI (e.g. HTTP, FTP, LDAP) or some other auxiliary server + (e.g. DNS) it needed to access in attempting to complete the request. + + code: 504, title: Gateway Timeout + """ + code = 504 + title = 'Gateway Timeout' + explanation = ('The gateway has timed out.') + +class HTTPVersionNotSupported(HTTPServerError): + """ + subclass of :class:`~HTTPServerError` + + This indicates that the server does not support, or refuses to + support, the HTTP protocol version that was used in the request + message. + + code: 505, title: HTTP Version Not Supported + """ + code = 505 + title = 'HTTP Version Not Supported' + explanation = ('The HTTP version is not supported.') + +class HTTPInsufficientStorage(HTTPServerError): + """ + subclass of :class:`~HTTPServerError` + + This indicates that the server does not have enough space to save + the resource. + + code: 507, title: Insufficient Storage + """ + code = 507 + title = 'Insufficient Storage' + explanation = ('There was not enough space to save the resource') + +NotFound = HTTPNotFound # bw compat +Forbidden = HTTPForbidden # bw compat + +class PredicateMismatch(NotFound): + """ + Internal exception (not an API) raised by multiviews when no + view matches. This exception subclasses the ``NotFound`` + exception only one reason: if it reaches the main exception + handler, it should be treated like a ``NotFound`` by any exception + view registrations. + """ + +class URLDecodeError(UnicodeDecodeError): + """ + This exception is raised when :app:`Pyramid` cannot + successfully decode a URL or a URL path segment. This exception + it behaves just like the Python builtin + :exc:`UnicodeDecodeError`. It is a subclass of the builtin + :exc:`UnicodeDecodeError` exception only for identity purposes, + mostly so an exception view can be registered when a URL cannot be + decoded. + """ + +class ConfigurationError(ZCE): + """ Raised when inappropriate input values are supplied to an API + method of a :term:`Configurator`""" + + +def abort(status_code, **kw): + """Aborts the request immediately by raising an HTTP exception based on a + status code. Example:: + + abort(404) # raises an HTTPNotFound exception. + + The values passed as ``kw`` are provided to the exception's constructor. + """ + exc = status_map[status_code](**kw) + raise exc + + +def redirect(url, code=302, **kw): + """Raises an :class:`~HTTPFound` (302) redirect exception to the + URL specified by ``url``. + + Optionally, a code variable may be passed with the status code of + the redirect, ie:: + + redirect(route_url('foo', request), code=303) + + The values passed as ``kw`` are provided to the exception constructor. + + """ + exc = status_map[code] + raise exc(location=url, **kw) + +def default_exceptionresponse_view(context, request): + if not isinstance(context, Exception): + # backwards compat for an exception response view registered via + # config.set_notfound_view or config.set_forbidden_view + # instead of as a proper exception view + context = request.exception or context + return context + +status_map={} +for name, value in globals().items(): + if (isinstance(value, (type, types.ClassType)) and + issubclass(value, HTTPException) + and not name.startswith('_')): + code = getattr(value, 'code', None) + if code: + status_map[code] = value +del name, value + diff --git a/pyramid/router.py b/pyramid/router.py index b8a8639aa..9cd682623 100644 --- a/pyramid/router.py +++ b/pyramid/router.py @@ -16,7 +16,7 @@ from pyramid.interfaces import IViewClassifier from pyramid.events import ContextFound from pyramid.events import NewRequest from pyramid.events import NewResponse -from pyramid.exceptions import NotFound +from pyramid.response import HTTPNotFound from pyramid.request import Request from pyramid.threadlocal import manager from pyramid.traversal import DefaultRootFactory @@ -153,7 +153,7 @@ class Router(object): logger and logger.debug(msg) else: msg = request.path_info - raise NotFound(msg) + raise HTTPNotFound(msg) else: response = view_callable(context, request) diff --git a/pyramid/testing.py b/pyramid/testing.py index a512ede4b..4d7dd252a 100644 --- a/pyramid/testing.py +++ b/pyramid/testing.py @@ -17,7 +17,7 @@ from pyramid.interfaces import ISession from pyramid.config import Configurator from pyramid.decorator import reify -from pyramid.exceptions import Forbidden +from pyramid.response import HTTPForbidden from pyramid.response import Response from pyramid.registry import Registry from pyramid.security import Authenticated @@ -217,7 +217,7 @@ def registerView(name, result='', view=None, for_=(Interface, Interface), else: def _secure(context, request): if not has_permission(permission, context, request): - raise Forbidden('no permission').exception + raise HTTPForbidden('no permission') else: return view(context, request) _secure.__call_permissive__ = view diff --git a/pyramid/tests/fixtureapp/views.py b/pyramid/tests/fixtureapp/views.py index 9ab985e32..3125c972f 100644 --- a/pyramid/tests/fixtureapp/views.py +++ b/pyramid/tests/fixtureapp/views.py @@ -1,6 +1,6 @@ from zope.interface import Interface from webob import Response -from pyramid.exceptions import Forbidden +from pyramid.response import HTTPForbidden def fixture_view(context, request): """ """ @@ -16,7 +16,7 @@ def exception_view(context, request): def protected_view(context, request): """ """ - raise Forbidden() + raise HTTPForbidden() class IDummy(Interface): pass diff --git a/pyramid/tests/forbiddenapp/__init__.py b/pyramid/tests/forbiddenapp/__init__.py index 614aff037..9ad2dc801 100644 --- a/pyramid/tests/forbiddenapp/__init__.py +++ b/pyramid/tests/forbiddenapp/__init__.py @@ -1,5 +1,5 @@ from webob import Response -from pyramid.exceptions import Forbidden +from pyramid.response import HTTPForbidden def x_view(request): # pragma: no cover return Response('this is private!') @@ -8,7 +8,7 @@ def forbidden_view(context, request): msg = context.message result = context.result message = msg + '\n' + str(result) - resp = Forbidden() + resp = HTTPForbidden() resp.body = message return resp @@ -20,4 +20,4 @@ def includeme(config): config._set_authentication_policy(authn_policy) config._set_authorization_policy(authz_policy) config.add_view(x_view, name='x', permission='private') - config.add_view(forbidden_view, context=Forbidden) + config.add_view(forbidden_view, context=HTTPForbidden) diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py index 7c6389253..6817c5936 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -50,8 +50,8 @@ class ConfiguratorTests(unittest.TestCase): return iface def _assertNotFound(self, wrapper, *arg): - from pyramid.exceptions import NotFound - self.assertRaises(NotFound, wrapper, *arg) + from pyramid.response import HTTPNotFound + self.assertRaises(HTTPNotFound, wrapper, *arg) def _registerEventListener(self, config, event_iface=None): if event_iface is None: # pragma: no cover @@ -205,7 +205,7 @@ class ConfiguratorTests(unittest.TestCase): def test_ctor_httpexception_view_default(self): from pyramid.interfaces import IExceptionResponse - from pyramid.exceptions import default_exceptionresponse_view + from pyramid.response import default_exceptionresponse_view from pyramid.interfaces import IRequest config = self._makeOne() view = self._getViewCallable(config, @@ -321,16 +321,17 @@ class ConfiguratorTests(unittest.TestCase): def test_setup_registry_explicit_notfound_trumps_iexceptionresponse(self): from zope.interface import implementedBy from pyramid.interfaces import IRequest - from pyramid.exceptions import NotFound + from pyramid.response import HTTPNotFound from pyramid.registry import Registry reg = Registry() config = self._makeOne(reg, autocommit=True) config.setup_registry() # registers IExceptionResponse default view def myview(context, request): return 'OK' - config.add_view(myview, context=NotFound) + config.add_view(myview, context=HTTPNotFound) request = self._makeRequest(config) - view = self._getViewCallable(config, ctx_iface=implementedBy(NotFound), + view = self._getViewCallable(config, + ctx_iface=implementedBy(HTTPNotFound), request_iface=IRequest) result = view(None, request) self.assertEqual(result, 'OK') @@ -1694,14 +1695,14 @@ class ConfiguratorTests(unittest.TestCase): self._assertNotFound(wrapper, None, request) def test_add_view_with_header_val_missing(self): - from pyramid.exceptions import NotFound + from pyramid.response import HTTPNotFound view = lambda *arg: 'OK' config = self._makeOne(autocommit=True) config.add_view(view=view, header=r'Host:\d') wrapper = self._getViewCallable(config) request = self._makeRequest(config) request.headers = {'NoHost':'1'} - self.assertRaises(NotFound, wrapper, None, request) + self.assertRaises(HTTPNotFound, wrapper, None, request) def test_add_view_with_accept_match(self): view = lambda *arg: 'OK' @@ -2228,12 +2229,13 @@ class ConfiguratorTests(unittest.TestCase): def test_set_notfound_view(self): from zope.interface import implementedBy from pyramid.interfaces import IRequest - from pyramid.exceptions import NotFound + from pyramid.response import HTTPNotFound config = self._makeOne(autocommit=True) view = lambda *arg: arg config.set_notfound_view(view) request = self._makeRequest(config) - view = self._getViewCallable(config, ctx_iface=implementedBy(NotFound), + view = self._getViewCallable(config, + ctx_iface=implementedBy(HTTPNotFound), request_iface=IRequest) result = view(None, request) self.assertEqual(result, (None, request)) @@ -2241,13 +2243,14 @@ class ConfiguratorTests(unittest.TestCase): def test_set_notfound_view_request_has_context(self): from zope.interface import implementedBy from pyramid.interfaces import IRequest - from pyramid.exceptions import NotFound + from pyramid.response import HTTPNotFound config = self._makeOne(autocommit=True) view = lambda *arg: arg config.set_notfound_view(view) request = self._makeRequest(config) request.context = 'abc' - view = self._getViewCallable(config, ctx_iface=implementedBy(NotFound), + view = self._getViewCallable(config, + ctx_iface=implementedBy(HTTPNotFound), request_iface=IRequest) result = view(None, request) self.assertEqual(result, ('abc', request)) @@ -2256,7 +2259,7 @@ class ConfiguratorTests(unittest.TestCase): def test_set_notfound_view_with_renderer(self): from zope.interface import implementedBy from pyramid.interfaces import IRequest - from pyramid.exceptions import NotFound + from pyramid.response import HTTPNotFound config = self._makeOne(autocommit=True) view = lambda *arg: {} config.set_notfound_view(view, @@ -2265,7 +2268,7 @@ class ConfiguratorTests(unittest.TestCase): try: # chameleon depends on being able to find a threadlocal registry request = self._makeRequest(config) view = self._getViewCallable(config, - ctx_iface=implementedBy(NotFound), + ctx_iface=implementedBy(HTTPNotFound), request_iface=IRequest) result = view(None, request) finally: @@ -2275,7 +2278,7 @@ class ConfiguratorTests(unittest.TestCase): def test_set_forbidden_view(self): from zope.interface import implementedBy from pyramid.interfaces import IRequest - from pyramid.exceptions import Forbidden + from pyramid.response import Forbidden config = self._makeOne(autocommit=True) view = lambda *arg: 'OK' config.set_forbidden_view(view) @@ -2288,7 +2291,7 @@ class ConfiguratorTests(unittest.TestCase): def test_set_forbidden_view_request_has_context(self): from zope.interface import implementedBy from pyramid.interfaces import IRequest - from pyramid.exceptions import Forbidden + from pyramid.response import Forbidden config = self._makeOne(autocommit=True) view = lambda *arg: arg config.set_forbidden_view(view) @@ -2303,7 +2306,7 @@ class ConfiguratorTests(unittest.TestCase): def test_set_forbidden_view_with_renderer(self): from zope.interface import implementedBy from pyramid.interfaces import IRequest - from pyramid.exceptions import Forbidden + from pyramid.response import Forbidden config = self._makeOne(autocommit=True) view = lambda *arg: {} config.set_forbidden_view(view, @@ -3682,7 +3685,7 @@ class TestViewDeriver(unittest.TestCase): "None against context None): True") def test_debug_auth_permission_authpol_denied(self): - from pyramid.exceptions import Forbidden + from pyramid.response import Forbidden view = lambda *arg: 'OK' self.config.registry.settings = dict( debug_authorization=True, reload_templates=True) @@ -3810,7 +3813,7 @@ class TestViewDeriver(unittest.TestCase): self.assertEqual(predicates, [True, True]) def test_with_predicates_notall(self): - from pyramid.exceptions import NotFound + from pyramid.response import HTTPNotFound view = lambda *arg: 'OK' predicates = [] def predicate1(context, request): @@ -3823,7 +3826,7 @@ class TestViewDeriver(unittest.TestCase): result = deriver(view) request = self._makeRequest() request.method = 'POST' - self.assertRaises(NotFound, result, None, None) + self.assertRaises(HTTPNotFound, result, None, None) self.assertEqual(predicates, [True, True]) def test_with_wrapper_viewname(self): @@ -4618,14 +4621,14 @@ class TestMultiView(unittest.TestCase): self.assertEqual(mv.get_views(request), mv.views) def test_match_not_found(self): - from pyramid.exceptions import NotFound + from pyramid.response import HTTPNotFound mv = self._makeOne() context = DummyContext() request = DummyRequest() - self.assertRaises(NotFound, mv.match, context, request) + self.assertRaises(HTTPNotFound, mv.match, context, request) def test_match_predicate_fails(self): - from pyramid.exceptions import NotFound + from pyramid.response import HTTPNotFound mv = self._makeOne() def view(context, request): """ """ @@ -4633,7 +4636,7 @@ class TestMultiView(unittest.TestCase): mv.views = [(100, view, None)] context = DummyContext() request = DummyRequest() - self.assertRaises(NotFound, mv.match, context, request) + self.assertRaises(HTTPNotFound, mv.match, context, request) def test_match_predicate_succeeds(self): mv = self._makeOne() @@ -4647,11 +4650,11 @@ class TestMultiView(unittest.TestCase): self.assertEqual(result, view) def test_permitted_no_views(self): - from pyramid.exceptions import NotFound + from pyramid.response import HTTPNotFound mv = self._makeOne() context = DummyContext() request = DummyRequest() - self.assertRaises(NotFound, mv.__permitted__, context, request) + self.assertRaises(HTTPNotFound, mv.__permitted__, context, request) def test_permitted_no_match_with__permitted__(self): mv = self._makeOne() @@ -4674,11 +4677,11 @@ class TestMultiView(unittest.TestCase): self.assertEqual(result, False) def test__call__not_found(self): - from pyramid.exceptions import NotFound + from pyramid.response import HTTPNotFound mv = self._makeOne() context = DummyContext() request = DummyRequest() - self.assertRaises(NotFound, mv, context, request) + self.assertRaises(HTTPNotFound, mv, context, request) def test___call__intermediate_not_found(self): from pyramid.exceptions import PredicateMismatch @@ -4696,17 +4699,17 @@ class TestMultiView(unittest.TestCase): self.assertEqual(response, expected_response) def test___call__raise_not_found_isnt_interpreted_as_pred_mismatch(self): - from pyramid.exceptions import NotFound + from pyramid.response import HTTPNotFound mv = self._makeOne() context = DummyContext() request = DummyRequest() request.view_name = '' def view1(context, request): - raise NotFound + raise HTTPNotFound def view2(context, request): """ """ mv.views = [(100, view1, None), (99, view2, None)] - self.assertRaises(NotFound, mv, context, request) + self.assertRaises(HTTPNotFound, mv, context, request) def test___call__(self): mv = self._makeOne() @@ -4721,11 +4724,11 @@ class TestMultiView(unittest.TestCase): self.assertEqual(response, expected_response) def test__call_permissive__not_found(self): - from pyramid.exceptions import NotFound + from pyramid.response import HTTPNotFound mv = self._makeOne() context = DummyContext() request = DummyRequest() - self.assertRaises(NotFound, mv, context, request) + self.assertRaises(HTTPNotFound, mv, context, request) def test___call_permissive_has_call_permissive(self): mv = self._makeOne() diff --git a/pyramid/tests/test_exceptions.py b/pyramid/tests/test_exceptions.py index f2e577416..673fb6712 100644 --- a/pyramid/tests/test_exceptions.py +++ b/pyramid/tests/test_exceptions.py @@ -12,6 +12,11 @@ class TestNotFound(unittest.TestCase): self.assertEqual(e.status, '404 Not Found') self.assertEqual(e.message, 'notfound') + def test_response_equivalence(self): + from pyramid.exceptions import NotFound + from pyramid.response import HTTPNotFound + self.assertTrue(NotFound is HTTPNotFound) + class TestForbidden(unittest.TestCase): def _makeOne(self, message): from pyramid.exceptions import Forbidden @@ -24,294 +29,8 @@ class TestForbidden(unittest.TestCase): self.assertEqual(e.status, '403 Forbidden') self.assertEqual(e.message, 'forbidden') -class Test_abort(unittest.TestCase): - def _callFUT(self, *arg, **kw): - from pyramid.exceptions import abort - return abort(*arg, **kw) - - def test_status_404(self): - from pyramid.exceptions import HTTPNotFound - self.assertRaises(HTTPNotFound, self._callFUT, 404) - - def test_status_201(self): - from pyramid.exceptions import HTTPCreated - self.assertRaises(HTTPCreated, self._callFUT, 201) - - def test_extra_kw(self): - from pyramid.exceptions import HTTPNotFound - try: - self._callFUT(404, headers=[('abc', 'def')]) - except HTTPNotFound, exc: - self.assertEqual(exc.headers['abc'], 'def') - else: # pragma: no cover - raise AssertionError - -class Test_redirect(unittest.TestCase): - def _callFUT(self, *arg, **kw): - from pyramid.exceptions import redirect - return redirect(*arg, **kw) - - def test_default(self): - from pyramid.exceptions import HTTPFound - try: - self._callFUT('http://example.com') - except HTTPFound, exc: - self.assertEqual(exc.location, 'http://example.com') - self.assertEqual(exc.status, '302 Found') - - def test_custom_code(self): - from pyramid.exceptions import HTTPMovedPermanently - try: - self._callFUT('http://example.com', 301) - except HTTPMovedPermanently, exc: - self.assertEqual(exc.location, 'http://example.com') - self.assertEqual(exc.status, '301 Moved Permanently') - - def test_extra_kw(self): - from pyramid.exceptions import HTTPFound - try: - self._callFUT('http://example.com', headers=[('abc', 'def')]) - except HTTPFound, exc: - self.assertEqual(exc.location, 'http://example.com') - self.assertEqual(exc.status, '302 Found') - self.assertEqual(exc.headers['abc'], 'def') - - -class Test_default_exceptionresponse_view(unittest.TestCase): - def _callFUT(self, context, request): - from pyramid.exceptions import default_exceptionresponse_view - return default_exceptionresponse_view(context, request) - - def test_call_with_exception(self): - context = Exception() - result = self._callFUT(context, None) - self.assertEqual(result, context) - - def test_call_with_nonexception(self): - request = DummyRequest() - context = Exception() - request.exception = context - result = self._callFUT(None, request) - self.assertEqual(result, context) - -class Test__no_escape(unittest.TestCase): - def _callFUT(self, val): - from pyramid.exceptions import _no_escape - return _no_escape(val) - - def test_null(self): - self.assertEqual(self._callFUT(None), '') - - def test_not_basestring(self): - self.assertEqual(self._callFUT(42), '42') - - def test_unicode(self): - class DummyUnicodeObject(object): - def __unicode__(self): - return u'42' - duo = DummyUnicodeObject() - self.assertEqual(self._callFUT(duo), u'42') - -class TestWSGIHTTPException(unittest.TestCase): - def _getTargetClass(self): - from pyramid.exceptions import WSGIHTTPException - return WSGIHTTPException - - def _getTargetSubclass(self, code='200', title='OK', - explanation='explanation', empty_body=False): - cls = self._getTargetClass() - class Subclass(cls): - pass - Subclass.empty_body = empty_body - Subclass.code = code - Subclass.title = title - Subclass.explanation = explanation - return Subclass - - def _makeOne(self, *arg, **kw): - cls = self._getTargetClass() - return cls(*arg, **kw) - - def test_ctor_sets_detail(self): - exc = self._makeOne('message') - self.assertEqual(exc.detail, 'message') - - def test_ctor_sets_comment(self): - exc = self._makeOne(comment='comment') - self.assertEqual(exc.comment, 'comment') - - def test_ctor_calls_Exception_ctor(self): - exc = self._makeOne('message') - self.assertEqual(exc.message, 'message') - - def test_ctor_calls_Response_ctor(self): - exc = self._makeOne('message') - self.assertEqual(exc.status, 'None None') - - def test_ctor_extends_headers(self): - exc = self._makeOne(headers=[('X-Foo', 'foo')]) - self.assertEqual(exc.headers.get('X-Foo'), 'foo') - - def test_ctor_sets_body_template_obj(self): - exc = self._makeOne(body_template='${foo}') - self.assertEqual( - exc.body_template_obj.substitute({'foo':'foo'}), 'foo') - - def test_ctor_with_empty_body(self): - cls = self._getTargetSubclass(empty_body=True) - exc = cls() - self.assertEqual(exc.content_type, None) - self.assertEqual(exc.content_length, None) - - def test_ctor_with_body_doesnt_set_default_app_iter(self): - exc = self._makeOne(body='123') - self.assertEqual(exc.app_iter, ['123']) - - def test_ctor_with_unicode_body_doesnt_set_default_app_iter(self): - exc = self._makeOne(unicode_body=u'123') - self.assertEqual(exc.app_iter, ['123']) - - def test_ctor_with_app_iter_doesnt_set_default_app_iter(self): - exc = self._makeOne(app_iter=['123']) - self.assertEqual(exc.app_iter, ['123']) - - def test_ctor_with_body_sets_default_app_iter_html(self): - cls = self._getTargetSubclass() - exc = cls('detail') - body = list(exc.app_iter)[0] - self.assertTrue(body.startswith('' in body) - - def test_custom_body_template_no_environ(self): - cls = self._getTargetSubclass() - exc = cls(body_template='${location}', location='foo') - exc.content_type = 'text/plain' - body = list(exc._default_app_iter())[0] - self.assertEqual(body, '200 OK\n\nfoo') - - def test_custom_body_template_with_environ(self): - cls = self._getTargetSubclass() - from pyramid.request import Request - request = Request.blank('/') - exc = cls(body_template='${REQUEST_METHOD}', request=request) - exc.content_type = 'text/plain' - body = list(exc._default_app_iter())[0] - self.assertEqual(body, '200 OK\n\nGET') - - def test_body_template_unicode(self): - from pyramid.request import Request - cls = self._getTargetSubclass() - la = unicode('/La Pe\xc3\xb1a', 'utf-8') - request = Request.blank('/') - request.environ['unicodeval'] = la - exc = cls(body_template='${unicodeval}', request=request) - exc.content_type = 'text/plain' - body = list(exc._default_app_iter())[0] - self.assertEqual(body, '200 OK\n\n/La Pe\xc3\xb1a') - -class TestRenderAllExceptionsWithoutArguments(unittest.TestCase): - def _doit(self, content_type): - from pyramid.exceptions import status_map - L = [] - self.assertTrue(status_map) - for v in status_map.values(): - exc = v() - exc.content_type = content_type - result = list(exc.app_iter)[0] - if exc.empty_body: - self.assertEqual(result, '') - else: - self.assertTrue(exc.status in result) - L.append(result) - self.assertEqual(len(L), len(status_map)) - - def test_it_plain(self): - self._doit('text/plain') - - def test_it_html(self): - self._doit('text/html') - -class Test_HTTPMove(unittest.TestCase): - def _makeOne(self, *arg, **kw): - from pyramid.exceptions import _HTTPMove - return _HTTPMove(*arg, **kw) - - def test_it_location_not_passed(self): - exc = self._makeOne() - self.assertEqual(exc.location, '') - - def test_it_location_passed(self): - exc = self._makeOne(location='foo') - self.assertEqual(exc.location, 'foo') - -class TestHTTPForbidden(unittest.TestCase): - def _makeOne(self, *arg, **kw): - from pyramid.exceptions import HTTPForbidden - return HTTPForbidden(*arg, **kw) - - def test_it_result_not_passed(self): - exc = self._makeOne() - self.assertEqual(exc.result, None) - - def test_it_result_passed(self): - exc = self._makeOne(result='foo') - self.assertEqual(exc.result, 'foo') - -class DummyRequest(object): - exception = None + def test_response_equivalence(self): + from pyramid.exceptions import Forbidden + from pyramid.response import HTTPForbidden + self.assertTrue(Forbidden is HTTPForbidden) diff --git a/pyramid/tests/test_httpexceptions.py b/pyramid/tests/test_httpexceptions.py index afa5a94de..28adc9d3d 100644 --- a/pyramid/tests/test_httpexceptions.py +++ b/pyramid/tests/test_httpexceptions.py @@ -3,7 +3,7 @@ import unittest class TestIt(unittest.TestCase): def test_bwcompat_imports(self): from pyramid.httpexceptions import HTTPNotFound as one - from pyramid.exceptions import HTTPNotFound as two + from pyramid.response import HTTPNotFound as two self.assertTrue(one is two) diff --git a/pyramid/tests/test_response.py b/pyramid/tests/test_response.py new file mode 100644 index 000000000..6cc87fc0a --- /dev/null +++ b/pyramid/tests/test_response.py @@ -0,0 +1,308 @@ +import unittest + +class TestResponse(unittest.TestCase): + def _getTargetClass(self): + from pyramid.response import Response + return Response + + def test_implements_IExceptionResponse(self): + from pyramid.interfaces import IExceptionResponse + Response = self._getTargetClass() + self.failUnless(IExceptionResponse.implementedBy(Response)) + + def test_provides_IExceptionResponse(self): + from pyramid.interfaces import IExceptionResponse + response = self._getTargetClass()() + self.failUnless(IExceptionResponse.providedBy(response)) + +class Test_abort(unittest.TestCase): + def _callFUT(self, *arg, **kw): + from pyramid.response import abort + return abort(*arg, **kw) + + def test_status_404(self): + from pyramid.response import HTTPNotFound + self.assertRaises(HTTPNotFound, self._callFUT, 404) + + def test_status_201(self): + from pyramid.response import HTTPCreated + self.assertRaises(HTTPCreated, self._callFUT, 201) + + def test_extra_kw(self): + from pyramid.response import HTTPNotFound + try: + self._callFUT(404, headers=[('abc', 'def')]) + except HTTPNotFound, exc: + self.assertEqual(exc.headers['abc'], 'def') + else: # pragma: no cover + raise AssertionError + +class Test_redirect(unittest.TestCase): + def _callFUT(self, *arg, **kw): + from pyramid.response import redirect + return redirect(*arg, **kw) + + def test_default(self): + from pyramid.response import HTTPFound + try: + self._callFUT('http://example.com') + except HTTPFound, exc: + self.assertEqual(exc.location, 'http://example.com') + self.assertEqual(exc.status, '302 Found') + + def test_custom_code(self): + from pyramid.response import HTTPMovedPermanently + try: + self._callFUT('http://example.com', 301) + except HTTPMovedPermanently, exc: + self.assertEqual(exc.location, 'http://example.com') + self.assertEqual(exc.status, '301 Moved Permanently') + + def test_extra_kw(self): + from pyramid.response import HTTPFound + try: + self._callFUT('http://example.com', headers=[('abc', 'def')]) + except HTTPFound, exc: + self.assertEqual(exc.location, 'http://example.com') + self.assertEqual(exc.status, '302 Found') + self.assertEqual(exc.headers['abc'], 'def') + + +class Test_default_exceptionresponse_view(unittest.TestCase): + def _callFUT(self, context, request): + from pyramid.response import default_exceptionresponse_view + return default_exceptionresponse_view(context, request) + + def test_call_with_exception(self): + context = Exception() + result = self._callFUT(context, None) + self.assertEqual(result, context) + + def test_call_with_nonexception(self): + request = DummyRequest() + context = Exception() + request.exception = context + result = self._callFUT(None, request) + self.assertEqual(result, context) + +class Test__no_escape(unittest.TestCase): + def _callFUT(self, val): + from pyramid.response import _no_escape + return _no_escape(val) + + def test_null(self): + self.assertEqual(self._callFUT(None), '') + + def test_not_basestring(self): + self.assertEqual(self._callFUT(42), '42') + + def test_unicode(self): + class DummyUnicodeObject(object): + def __unicode__(self): + return u'42' + duo = DummyUnicodeObject() + self.assertEqual(self._callFUT(duo), u'42') + +class TestWSGIHTTPException(unittest.TestCase): + def _getTargetClass(self): + from pyramid.response import WSGIHTTPException + return WSGIHTTPException + + def _getTargetSubclass(self, code='200', title='OK', + explanation='explanation', empty_body=False): + cls = self._getTargetClass() + class Subclass(cls): + pass + Subclass.empty_body = empty_body + Subclass.code = code + Subclass.title = title + Subclass.explanation = explanation + return Subclass + + def _makeOne(self, *arg, **kw): + cls = self._getTargetClass() + return cls(*arg, **kw) + + def test_ctor_sets_detail(self): + exc = self._makeOne('message') + self.assertEqual(exc.detail, 'message') + + def test_ctor_sets_comment(self): + exc = self._makeOne(comment='comment') + self.assertEqual(exc.comment, 'comment') + + def test_ctor_calls_Exception_ctor(self): + exc = self._makeOne('message') + self.assertEqual(exc.message, 'message') + + def test_ctor_calls_Response_ctor(self): + exc = self._makeOne('message') + self.assertEqual(exc.status, 'None None') + + def test_ctor_extends_headers(self): + exc = self._makeOne(headers=[('X-Foo', 'foo')]) + self.assertEqual(exc.headers.get('X-Foo'), 'foo') + + def test_ctor_sets_body_template_obj(self): + exc = self._makeOne(body_template='${foo}') + self.assertEqual( + exc.body_template_obj.substitute({'foo':'foo'}), 'foo') + + def test_ctor_with_empty_body(self): + cls = self._getTargetSubclass(empty_body=True) + exc = cls() + self.assertEqual(exc.content_type, None) + self.assertEqual(exc.content_length, None) + + def test_ctor_with_body_doesnt_set_default_app_iter(self): + exc = self._makeOne(body='123') + self.assertEqual(exc.app_iter, ['123']) + + def test_ctor_with_unicode_body_doesnt_set_default_app_iter(self): + exc = self._makeOne(unicode_body=u'123') + self.assertEqual(exc.app_iter, ['123']) + + def test_ctor_with_app_iter_doesnt_set_default_app_iter(self): + exc = self._makeOne(app_iter=['123']) + self.assertEqual(exc.app_iter, ['123']) + + def test_ctor_with_body_sets_default_app_iter_html(self): + cls = self._getTargetSubclass() + exc = cls('detail') + body = list(exc.app_iter)[0] + self.assertTrue(body.startswith('' in body) + + def test_custom_body_template_no_environ(self): + cls = self._getTargetSubclass() + exc = cls(body_template='${location}', location='foo') + exc.content_type = 'text/plain' + body = list(exc._default_app_iter())[0] + self.assertEqual(body, '200 OK\n\nfoo') + + def test_custom_body_template_with_environ(self): + cls = self._getTargetSubclass() + from pyramid.request import Request + request = Request.blank('/') + exc = cls(body_template='${REQUEST_METHOD}', request=request) + exc.content_type = 'text/plain' + body = list(exc._default_app_iter())[0] + self.assertEqual(body, '200 OK\n\nGET') + + def test_body_template_unicode(self): + from pyramid.request import Request + cls = self._getTargetSubclass() + la = unicode('/La Pe\xc3\xb1a', 'utf-8') + request = Request.blank('/') + request.environ['unicodeval'] = la + exc = cls(body_template='${unicodeval}', request=request) + exc.content_type = 'text/plain' + body = list(exc._default_app_iter())[0] + self.assertEqual(body, '200 OK\n\n/La Pe\xc3\xb1a') + +class TestRenderAllExceptionsWithoutArguments(unittest.TestCase): + def _doit(self, content_type): + from pyramid.response import status_map + L = [] + self.assertTrue(status_map) + for v in status_map.values(): + exc = v() + exc.content_type = content_type + result = list(exc.app_iter)[0] + if exc.empty_body: + self.assertEqual(result, '') + else: + self.assertTrue(exc.status in result) + L.append(result) + self.assertEqual(len(L), len(status_map)) + + def test_it_plain(self): + self._doit('text/plain') + + def test_it_html(self): + self._doit('text/html') + +class Test_HTTPMove(unittest.TestCase): + def _makeOne(self, *arg, **kw): + from pyramid.response import _HTTPMove + return _HTTPMove(*arg, **kw) + + def test_it_location_not_passed(self): + exc = self._makeOne() + self.assertEqual(exc.location, '') + + def test_it_location_passed(self): + exc = self._makeOne(location='foo') + self.assertEqual(exc.location, 'foo') + +class TestHTTPForbidden(unittest.TestCase): + def _makeOne(self, *arg, **kw): + from pyramid.response import HTTPForbidden + return HTTPForbidden(*arg, **kw) + + def test_it_result_not_passed(self): + exc = self._makeOne() + self.assertEqual(exc.result, None) + + def test_it_result_passed(self): + exc = self._makeOne(result='foo') + self.assertEqual(exc.result, 'foo') + +class DummyRequest(object): + exception = None + diff --git a/pyramid/tests/test_router.py b/pyramid/tests/test_router.py index b869a3830..106f7c57d 100644 --- a/pyramid/tests/test_router.py +++ b/pyramid/tests/test_router.py @@ -136,37 +136,37 @@ class TestRouter(unittest.TestCase): self.assertEqual(router.request_factory, DummyRequestFactory) def test_call_traverser_default(self): - from pyramid.exceptions import NotFound + from pyramid.response import HTTPNotFound environ = self._makeEnviron() logger = self._registerLogger() router = self._makeOne() start_response = DummyStartResponse() - why = exc_raised(NotFound, router, environ, start_response) + why = exc_raised(HTTPNotFound, router, environ, start_response) self.assertTrue('/' in why[0], why) self.assertFalse('debug_notfound' in why[0]) self.assertEqual(len(logger.messages), 0) def test_traverser_raises_notfound_class(self): - from pyramid.exceptions import NotFound + from pyramid.response import HTTPNotFound environ = self._makeEnviron() context = DummyContext() - self._registerTraverserFactory(context, raise_error=NotFound) + self._registerTraverserFactory(context, raise_error=HTTPNotFound) router = self._makeOne() start_response = DummyStartResponse() - self.assertRaises(NotFound, router, environ, start_response) + self.assertRaises(HTTPNotFound, router, environ, start_response) def test_traverser_raises_notfound_instance(self): - from pyramid.exceptions import NotFound + from pyramid.response import HTTPNotFound environ = self._makeEnviron() context = DummyContext() - self._registerTraverserFactory(context, raise_error=NotFound('foo')) + self._registerTraverserFactory(context, raise_error=HTTPNotFound('foo')) router = self._makeOne() start_response = DummyStartResponse() - why = exc_raised(NotFound, router, environ, start_response) + why = exc_raised(HTTPNotFound, router, environ, start_response) self.assertTrue('foo' in why[0], why) def test_traverser_raises_forbidden_class(self): - from pyramid.exceptions import Forbidden + from pyramid.response import Forbidden environ = self._makeEnviron() context = DummyContext() self._registerTraverserFactory(context, raise_error=Forbidden) @@ -175,7 +175,7 @@ class TestRouter(unittest.TestCase): self.assertRaises(Forbidden, router, environ, start_response) def test_traverser_raises_forbidden_instance(self): - from pyramid.exceptions import Forbidden + from pyramid.response import Forbidden environ = self._makeEnviron() context = DummyContext() self._registerTraverserFactory(context, raise_error=Forbidden('foo')) @@ -185,20 +185,20 @@ class TestRouter(unittest.TestCase): self.assertTrue('foo' in why[0], why) def test_call_no_view_registered_no_isettings(self): - from pyramid.exceptions import NotFound + from pyramid.response import HTTPNotFound environ = self._makeEnviron() context = DummyContext() self._registerTraverserFactory(context) logger = self._registerLogger() router = self._makeOne() start_response = DummyStartResponse() - why = exc_raised(NotFound, router, environ, start_response) + why = exc_raised(HTTPNotFound, router, environ, start_response) self.assertTrue('/' in why[0], why) self.assertFalse('debug_notfound' in why[0]) self.assertEqual(len(logger.messages), 0) def test_call_no_view_registered_debug_notfound_false(self): - from pyramid.exceptions import NotFound + from pyramid.response import HTTPNotFound environ = self._makeEnviron() context = DummyContext() self._registerTraverserFactory(context) @@ -206,13 +206,13 @@ class TestRouter(unittest.TestCase): self._registerSettings(debug_notfound=False) router = self._makeOne() start_response = DummyStartResponse() - why = exc_raised(NotFound, router, environ, start_response) + why = exc_raised(HTTPNotFound, router, environ, start_response) self.assertTrue('/' in why[0], why) self.assertFalse('debug_notfound' in why[0]) self.assertEqual(len(logger.messages), 0) def test_call_no_view_registered_debug_notfound_true(self): - from pyramid.exceptions import NotFound + from pyramid.response import HTTPNotFound environ = self._makeEnviron() context = DummyContext() self._registerTraverserFactory(context) @@ -220,7 +220,7 @@ class TestRouter(unittest.TestCase): logger = self._registerLogger() router = self._makeOne() start_response = DummyStartResponse() - why = exc_raised(NotFound, router, environ, start_response) + why = exc_raised(HTTPNotFound, router, environ, start_response) self.assertTrue( "debug_notfound of url http://localhost:8080/; path_info: '/', " "context:" in why[0]) @@ -323,7 +323,7 @@ class TestRouter(unittest.TestCase): def test_call_view_registered_specific_fail(self): from zope.interface import Interface from zope.interface import directlyProvides - from pyramid.exceptions import NotFound + from pyramid.response import HTTPNotFound from pyramid.interfaces import IViewClassifier class IContext(Interface): pass @@ -339,12 +339,12 @@ class TestRouter(unittest.TestCase): self._registerView(view, '', IViewClassifier, IRequest, IContext) router = self._makeOne() start_response = DummyStartResponse() - self.assertRaises(NotFound, router, environ, start_response) + self.assertRaises(HTTPNotFound, router, environ, start_response) def test_call_view_raises_forbidden(self): from zope.interface import Interface from zope.interface import directlyProvides - from pyramid.exceptions import Forbidden + from pyramid.response import Forbidden class IContext(Interface): pass from pyramid.interfaces import IRequest @@ -368,17 +368,17 @@ class TestRouter(unittest.TestCase): pass from pyramid.interfaces import IRequest from pyramid.interfaces import IViewClassifier - from pyramid.exceptions import NotFound + from pyramid.response import HTTPNotFound context = DummyContext() directlyProvides(context, IContext) self._registerTraverserFactory(context, subpath=['']) response = DummyResponse() - view = DummyView(response, raise_exception=NotFound("notfound")) + view = DummyView(response, raise_exception=HTTPNotFound("notfound")) environ = self._makeEnviron() self._registerView(view, '', IViewClassifier, IRequest, IContext) router = self._makeOne() start_response = DummyStartResponse() - why = exc_raised(NotFound, router, environ, start_response) + why = exc_raised(HTTPNotFound, router, environ, start_response) self.assertEqual(why[0], 'notfound') def test_call_request_has_response_callbacks(self): @@ -566,7 +566,7 @@ class TestRouter(unittest.TestCase): "pattern: 'archives/:action/:article', ")) def test_call_route_match_miss_debug_routematch(self): - from pyramid.exceptions import NotFound + from pyramid.response import HTTPNotFound logger = self._registerLogger() self._registerSettings(debug_routematch=True) self._registerRouteRequest('foo') @@ -577,7 +577,7 @@ class TestRouter(unittest.TestCase): self._registerRootFactory(context) router = self._makeOne() start_response = DummyStartResponse() - self.assertRaises(NotFound, router, environ, start_response) + self.assertRaises(HTTPNotFound, router, environ, start_response) self.assertEqual(len(logger.messages), 1) self.assertEqual( @@ -627,11 +627,11 @@ class TestRouter(unittest.TestCase): def test_root_factory_raises_notfound(self): from pyramid.interfaces import IRootFactory - from pyramid.exceptions import NotFound + from pyramid.response import HTTPNotFound from zope.interface import Interface from zope.interface import directlyProvides def rootfactory(request): - raise NotFound('from root factory') + raise HTTPNotFound('from root factory') self.registry.registerUtility(rootfactory, IRootFactory) class IContext(Interface): pass @@ -640,12 +640,12 @@ class TestRouter(unittest.TestCase): environ = self._makeEnviron() router = self._makeOne() start_response = DummyStartResponse() - why = exc_raised(NotFound, router, environ, start_response) + why = exc_raised(HTTPNotFound, router, environ, start_response) self.assertTrue('from root factory' in why[0]) def test_root_factory_raises_forbidden(self): from pyramid.interfaces import IRootFactory - from pyramid.exceptions import Forbidden + from pyramid.response import Forbidden from zope.interface import Interface from zope.interface import directlyProvides def rootfactory(request): diff --git a/pyramid/tests/test_testing.py b/pyramid/tests/test_testing.py index 58ca2b7d9..0288884b7 100644 --- a/pyramid/tests/test_testing.py +++ b/pyramid/tests/test_testing.py @@ -150,7 +150,7 @@ class Test_registerView(TestBase): def test_registerView_with_permission_denying(self): from pyramid import testing - from pyramid.exceptions import Forbidden + from pyramid.response import HTTPForbidden def view(context, request): """ """ view = testing.registerView('moo.html', view=view, permission='bar') @@ -160,7 +160,7 @@ class Test_registerView(TestBase): from pyramid.view import render_view_to_response request = DummyRequest() request.registry = self.registry - self.assertRaises(Forbidden, render_view_to_response, + self.assertRaises(HTTPForbidden, render_view_to_response, None, request, 'moo.html') def test_registerView_with_permission_denying2(self): diff --git a/pyramid/view.py b/pyramid/view.py index 975464124..0b5c7cdc9 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -8,8 +8,8 @@ from pyramid.interfaces import IRoutesMapper from pyramid.interfaces import IView from pyramid.interfaces import IViewClassifier -from pyramid.exceptions import HTTPFound -from pyramid.exceptions import default_exceptionresponse_view +from pyramid.response import HTTPFound +from pyramid.response import default_exceptionresponse_view from pyramid.renderers import RendererHelper from pyramid.static import static_view from pyramid.threadlocal import get_current_registry @@ -48,7 +48,7 @@ def render_view_to_response(context, request, name='', secure=True): protected by a permission, the permission will be checked before calling the view function. If the permission check disallows view execution (based on the current :term:`authorization policy`), a - :exc:`pyramid.exceptions.Forbidden` exception will be raised. + :exc:`pyramid.response.HTTPForbidden` exception will be raised. The exception's ``args`` attribute explains why the view access was disallowed. @@ -92,7 +92,7 @@ def render_view_to_iterable(context, request, name='', secure=True): permission, the permission will be checked before the view function is invoked. If the permission check disallows view execution (based on the current :term:`authentication policy`), a - :exc:`pyramid.exceptions.Forbidden` exception will be raised; + :exc:`pyramid.response.HTTPForbidden` exception will be raised; its ``args`` attribute explains why the view access was disallowed. @@ -121,7 +121,7 @@ def render_view(context, request, name='', secure=True): permission, the permission will be checked before the view is invoked. If the permission check disallows view execution (based on the current :term:`authorization policy`), a - :exc:`pyramid.exceptions.Forbidden` exception will be raised; + :exc:`pyramid.response.HTTPForbidden` exception will be raised; its ``args`` attribute explains why the view access was disallowed. @@ -249,14 +249,13 @@ class AppendSlashNotFoundViewFactory(object): .. code-block:: python - from pyramid.exceptions import NotFound + from pyramid.response import HTTPNotFound from pyramid.view import AppendSlashNotFoundViewFactory - from pyramid.exceptions import HTTPNotFound def notfound_view(context, request): return HTTPNotFound('nope') custom_append_slash = AppendSlashNotFoundViewFactory(notfound_view) - config.add_view(custom_append_slash, context=NotFound) + config.add_view(custom_append_slash, context=HTTPNotFound) The ``notfound_view`` supplied must adhere to the two-argument view callable calling convention of ``(context, request)`` @@ -303,9 +302,9 @@ routes are not considered when attempting to find a matching route. Use the :meth:`pyramid.config.Configurator.add_view` method to configure this view as the Not Found view:: - from pyramid.exceptions import NotFound + from pyramid.response import HTTPNotFound from pyramid.view import append_slash_notfound_view - config.add_view(append_slash_notfound_view, context=NotFound) + config.add_view(append_slash_notfound_view, context=HTTPNotFound) See also :ref:`changing_the_notfound_view`. -- cgit v1.2.3 From 71738bc9418170cebfd532fbed6bb48ac8c3fb40 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 31 May 2011 15:30:24 -0400 Subject: broke this in the last commit --- docs/tutorials/bfg/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorials/bfg/index.rst b/docs/tutorials/bfg/index.rst index e01345158..e68e63b0b 100644 --- a/docs/tutorials/bfg/index.rst +++ b/docs/tutorials/bfg/index.rst @@ -106,7 +106,7 @@ Here's how to convert a :mod:`repoze.bfg` application to a - ZCML files which contain directives that have attributes which name a ``repoze.bfg`` API module or attribute of an API module - (e.g. ``context="repoze.bfg.exeptions.NotFound"``) will be + (e.g. ``context="repoze.bfg.exceptions.NotFound"``) will be converted to :app:`Pyramid` compatible ZCML attributes (e.g. ``context="pyramid.exceptions.NotFound``). Every ZCML file beneath the top-level path (files ending with ``.zcml``) will be -- cgit v1.2.3 From df15ed98612e7962e3122da52d8d5f5b9d8882b2 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 4 Jun 2011 18:43:25 -0400 Subject: - It is now possible to control how the Pyramid router calls the WSGI ``start_response`` callable and obtains the WSGI ``app_iter`` based on adapting the response object to the new ``pyramid.interfaces.IResponder`` interface. The default ``IResponder`` uses Pyramid 1.0's logic to do this. To override the responder:: from pyramid.interfaces import IResponder from pyramid.response import Response from myapp import MyResponder config.registry.registerAdapter(MyResponder, (Response,), IResponder, name='') This makes it possible to reuse response object implementations which have, for example, their own ``__call__`` expected to be used as a WSGI application (like ``pyramid.response.Response``), e.g.: class MyResponder(object): def __init__(self, response): """ Obtain a reference to the response """ self.response = response def __call__(self, request, start_response): """ Call start_response and return an app_iter """ app_iter = self.response(request.environ, start_response) return app_iter --- CHANGES.txt | 26 ++++++++++++++++++++++++++ docs/api/interfaces.rst | 2 ++ docs/glossary.rst | 4 ++++ docs/narr/hooks.rst | 36 ++++++++++++++++++++++++++++++++++++ pyramid/interfaces.py | 8 ++++++++ pyramid/response.py | 5 ++--- pyramid/router.py | 33 ++++++++++++++++++++++----------- pyramid/tests/test_router.py | 31 +++++++++++++++++++++++++++++++ 8 files changed, 131 insertions(+), 14 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 15c86c13c..7840bc525 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -132,6 +132,32 @@ Features - The ``pyramid.request.Response`` class now has a ``RequestClass`` interface which points at ``pyramid.response.Request``. +- It is now possible to control how the Pyramid router calls the WSGI + ``start_response`` callable and obtains the WSGI ``app_iter`` based on + adapting the response object to the new ``pyramid.interfaces.IResponder`` + interface. The default ``IResponder`` uses Pyramid 1.0's logic to do this. + To override the responder:: + + from pyramid.interfaces import IResponder + from pyramid.response import Response + from myapp import MyResponder + + config.registry.registerAdapter(MyResponder, (Response,), + IResponder, name='') + + This makes it possible to reuse response object implementations which have, + for example, their own ``__call__`` expected to be used as a WSGI + application (like ``pyramid.response.Response``), e.g.: + + class MyResponder(object): + def __init__(self, response): + """ Obtain a reference to the response """ + self.response = response + def __call__(self, request, start_response): + """ Call start_response and return an app_iter """ + app_iter = self.response(request.environ, start_response) + return app_iter + Bug Fixes --------- diff --git a/docs/api/interfaces.rst b/docs/api/interfaces.rst index ac282fbcc..3a60fa4dc 100644 --- a/docs/api/interfaces.rst +++ b/docs/api/interfaces.rst @@ -57,4 +57,6 @@ Other Interfaces .. autointerface:: IMultiDict :members: + .. autointerface:: IResponder + :members: diff --git a/docs/glossary.rst b/docs/glossary.rst index 20b9bfd64..dbab331c1 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -900,5 +900,9 @@ Glossary http://docs.python.org/distutils/index.html for more information. :term:`setuptools` is actually an *extension* of the Distutils. + exception response + A :term:`response` that is generated as the result of a raised exception + being caught by an :term:`exception view`. + diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index d620b5672..aa151d281 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -521,6 +521,42 @@ The default context URL generator is available for perusal as the class `_ of the :term:`Pylons` GitHub Pyramid repository. +.. index:: + single: IResponder + +.. _using_iresponder: + +Changing How Pyramid Treats Response Objects +-------------------------------------------- + +It is possible to control how the Pyramid :term:`router` calls the WSGI +``start_response`` callable and obtains the WSGI ``app_iter`` based on +adapting the response object to the :class: `pyramid.interfaces.IResponder` +interface. The default ``IResponder`` uses the three attributes ``status``, +``headerlist``, and ``app_iter`` attached to the response object, and calls +``start_response`` with the status and headerlist, returning the +``app_iter``. To override the responder:: + + from pyramid.interfaces import IResponder + from pyramid.response import Response + from myapp import MyResponder + + config.registry.registerAdapter(MyResponder, (Response,), + IResponder, name='') + +Overriding makes it possible to reuse response object implementations which +have, for example, their own ``__call__`` expected to be used as a WSGI +application (like :class:`pyramid.response.Response`), e.g.: + + class MyResponder(object): + def __init__(self, response): + """ Obtain a reference to the response """ + self.response = response + def __call__(self, request, start_response): + """ Call start_response and return an app_iter """ + app_iter = self.response(request.environ, start_response) + return app_iter + .. index:: single: view mapper diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index 237727b41..d5d382492 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -229,6 +229,14 @@ class IMultiDict(Interface): # docs-only interface dictionary. This is similar to the kind of dictionary often used to represent the variables in a web request. """ +class IResponder(Interface): + """ Adapter from IResponse to an IResponder. See :ref:`using_iresponder` + for usage details. New in Pyramid 1.1. + """ + def __call__(self, request, start_response): + """ Call the WSGI ``start_response`` callable passed as + ``start_response`` and return an ``app_iter``.""" + # internal interfaces class IRequest(Interface): diff --git a/pyramid/response.py b/pyramid/response.py index 41ac354f9..6e6af32c8 100644 --- a/pyramid/response.py +++ b/pyramid/response.py @@ -21,7 +21,6 @@ def _no_escape(value): value = str(value) return value - class HTTPException(Exception): # bw compat pass @@ -40,7 +39,7 @@ class WSGIHTTPException(Response, HTTPException): # as a result: # # - bases plaintext vs. html result on self.content_type rather than - # on request environ + # on request accept header # # - doesn't add request.environ keys to template substitutions unless # 'request' is passed as a constructor keyword argument. @@ -49,7 +48,7 @@ class WSGIHTTPException(Response, HTTPException): # in default body template) # # - sets a default app_iter if no body, app_iter, or unicode_body is - # passed + # passed using a template (ala the replaced version's "generate_response") # # - explicitly sets self.message = detail to prevent whining by Python # 2.6.5+ access of Exception.message diff --git a/pyramid/router.py b/pyramid/router.py index 9cd682623..92c6cc920 100644 --- a/pyramid/router.py +++ b/pyramid/router.py @@ -12,6 +12,7 @@ from pyramid.interfaces import IRoutesMapper from pyramid.interfaces import ITraverser from pyramid.interfaces import IView from pyramid.interfaces import IViewClassifier +from pyramid.interfaces import IResponder from pyramid.events import ContextFound from pyramid.events import NewRequest @@ -58,6 +59,7 @@ class Router(object): logger = self.logger manager = self.threadlocal_manager request = None + responder = default_responder threadlocals = {'registry':registry, 'request':request} manager.push(threadlocals) @@ -186,21 +188,30 @@ class Router(object): if request.response_callbacks: request._process_response_callbacks(response) - try: - headers = response.headerlist - app_iter = response.app_iter - status = response.status - except AttributeError: - raise ValueError( - 'Non-response object returned from view named %s ' - '(and no renderer): %r' % (view_name, response)) + responder = adapters.queryAdapter(response, IResponder) + if responder is None: + responder = default_responder(response) finally: if request is not None and request.finished_callbacks: request._process_finished_callbacks() - start_response(status, headers) - return app_iter - + return responder(request, start_response) + finally: manager.pop() + +def default_responder(response): + def inner(request, start_response): + try: + headers = response.headerlist + app_iter = response.app_iter + status = response.status + except AttributeError: + raise ValueError( + 'Non-response object returned from view ' + '(and no renderer): %r' % (response)) + start_response(status, headers) + return app_iter + return inner + diff --git a/pyramid/tests/test_router.py b/pyramid/tests/test_router.py index 106f7c57d..a89de7a36 100644 --- a/pyramid/tests/test_router.py +++ b/pyramid/tests/test_router.py @@ -449,6 +449,37 @@ class TestRouter(unittest.TestCase): exc_raised(NotImplementedError, router, environ, start_response) self.assertEqual(environ['called_back'], True) + def test_call_with_overridden_iresponder_factory(self): + from zope.interface import Interface + from zope.interface import directlyProvides + from pyramid.interfaces import IRequest + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IResponder + context = DummyContext() + class IFoo(Interface): + pass + directlyProvides(context, IFoo) + self._registerTraverserFactory(context, subpath=['']) + class DummyResponder(object): + def __init__(self, response): + self.response = response + def __call__(self, request, start_response): + self.response.responder_used = True + return '123' + self.registry.registerAdapter(DummyResponder, (None,), + IResponder, name='') + response = DummyResponse('200 OK') + directlyProvides(response, IFoo) + def view(context, request): + return response + environ = self._makeEnviron() + self._registerView(view, '', IViewClassifier, IRequest, Interface) + router = self._makeOne() + start_response = DummyStartResponse() + result = router(environ, start_response) + self.assertTrue(response.responder_used) + self.assertEqual(result, '123') + def test_call_request_factory_raises(self): # making sure finally doesnt barf when a request cannot be created environ = self._makeEnviron() -- cgit v1.2.3 From 99edc51a3b05309c7f5d98ff96289ec51b1d7660 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 11 Jun 2011 05:35:27 -0400 Subject: - Pyramid now expects Response objects to have a __call__ method which implements the WSGI application interface instead of the three webob attrs status, headerlist and app_iter. Backwards compatibility exists for code which returns response objects that do not have a __call__. - pyramid.response.Response is no longer an exception (and therefore cannot be raised in order to generate a response). - Changed my mind about moving stuff from pyramid.httpexceptions to pyramid.response. The stuff I moved over has been moved back to pyramid.httpexceptions. --- CHANGES.txt | 79 +- TODO.txt | 6 + docs/api/httpexceptions.rst | 6 +- docs/api/response.rst | 1 + docs/glossary.rst | 4 +- docs/narr/hooks.rst | 42 +- docs/narr/renderers.rst | 69 +- docs/narr/router.rst | 12 +- docs/narr/testing.rst | 4 +- docs/narr/urldispatch.rst | 4 +- docs/narr/views.rst | 209 ++--- docs/narr/webob.rst | 23 +- docs/tutorials/wiki/authorization.rst | 22 +- docs/tutorials/wiki/definingviews.rst | 8 +- .../wiki/src/authorization/tutorial/login.py | 4 +- .../wiki/src/authorization/tutorial/views.py | 2 +- docs/tutorials/wiki/src/views/tutorial/views.py | 2 +- docs/tutorials/wiki2/definingviews.rst | 4 +- .../wiki2/src/authorization/tutorial/__init__.py | 2 +- .../wiki2/src/authorization/tutorial/login.py | 2 +- .../wiki2/src/authorization/tutorial/views.py | 2 +- docs/tutorials/wiki2/src/views/tutorial/views.py | 2 +- docs/whatsnew-1.1.rst | 12 +- pyramid/config.py | 6 +- pyramid/exceptions.py | 6 +- pyramid/httpexceptions.py | 902 +++++++++++++++++++- pyramid/interfaces.py | 16 +- pyramid/response.py | 946 +-------------------- pyramid/router.py | 17 +- pyramid/testing.py | 2 +- pyramid/tests/fixtureapp/views.py | 2 +- pyramid/tests/forbiddenapp/__init__.py | 2 +- pyramid/tests/test_config.py | 46 +- pyramid/tests/test_exceptions.py | 15 +- pyramid/tests/test_httpexceptions.py | 278 +++++- pyramid/tests/test_response.py | 305 +------ pyramid/tests/test_router.py | 117 ++- pyramid/tests/test_testing.py | 2 +- pyramid/view.py | 44 +- 39 files changed, 1632 insertions(+), 1595 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 7840bc525..e413f0657 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -37,12 +37,10 @@ Documentation - Added "What's New in Pyramid 1.1" to HTML rendering of documentation. -- Added API docs for ``pyramid.httpexceptions.abort`` and - ``pyramid.httpexceptions.redirect``. +- Added API docs for ``pyramid.httpexceptions.responsecode``. - Added "HTTP Exceptions" section to Views narrative chapter including a - description of ``pyramid.httpexceptions.abort``; adjusted redirect section - to note ``pyramid.httpexceptions.redirect``. + description of ``pyramid.httpexceptions.responsecode``. Features -------- @@ -105,18 +103,15 @@ Features more information. - A default exception view for the context - ``pyramid.interfaces.IExceptionResponse`` (aka - ``pyramid.response.Response`` or ``pyramid.httpexceptions.HTTPException``) - is now registered by default. This means that an instance of any exception - response class imported from ``pyramid.httpexceptions`` (such as - ``HTTPFound``) can now be raised from within view code; when raised, this - exception view will render the exception to a response. - -- New functions named ``pyramid.httpexceptions.abort`` and - ``pyramid.httpexceptions.redirect`` perform the equivalent of their Pylons - brethren when an HTTP exception handler is registered. These functions - take advantage of the newly registered exception view for - ``webob.exc.HTTPException``. + ``pyramid.interfaces.IExceptionResponse`` is now registered by default. + This means that an instance of any exception response class imported from + ``pyramid.httpexceptions`` (such as ``HTTPFound``) can now be raised from + within view code; when raised, this exception view will render the + exception to a response. + +- A function named ``pyramid.httpexceptions.responsecode`` is a shortcut that + can be used to create HTTP exception response objects using an HTTP integer + status code. - The Configurator now accepts an additional keyword argument named ``exceptionresponse_view``. By default, this argument is populated with a @@ -135,28 +130,13 @@ Features - It is now possible to control how the Pyramid router calls the WSGI ``start_response`` callable and obtains the WSGI ``app_iter`` based on adapting the response object to the new ``pyramid.interfaces.IResponder`` - interface. The default ``IResponder`` uses Pyramid 1.0's logic to do this. - To override the responder:: - - from pyramid.interfaces import IResponder - from pyramid.response import Response - from myapp import MyResponder - - config.registry.registerAdapter(MyResponder, (Response,), - IResponder, name='') - - This makes it possible to reuse response object implementations which have, - for example, their own ``__call__`` expected to be used as a WSGI - application (like ``pyramid.response.Response``), e.g.: + interface. See the section in the Hooks chapter of the documentation + entitled "Changing How Pyramid Treats Response Objects". - class MyResponder(object): - def __init__(self, response): - """ Obtain a reference to the response """ - self.response = response - def __call__(self, request, start_response): - """ Call start_response and return an app_iter """ - app_iter = self.response(request.environ, start_response) - return app_iter +- The Pyramid router will now, by default, call the ``__call__`` method of + WebOb response objects when returning a WSGI response. This means that, + among other things, the ``conditional_response`` feature of WebOb response + objects will now behave properly. Bug Fixes --------- @@ -291,7 +271,7 @@ Deprecations Behavior Changes ---------------- -- A custom request factory is now required to return a response object that +- A custom request factory is now required to return a request object that has a ``response`` attribute (or "reified"/lazy property) if they the request is meant to be used in a view that uses a renderer. This ``response`` attribute should be an instance of the class @@ -323,10 +303,25 @@ Behavior Changes result. - ``pyramid.response.Response`` is now a *subclass* of - ``webob.response.Response``. It also inherits from the built-in Python - ``Exception`` class and implements the - ``pyramid.interfaces.IExceptionResponse`` class so it can be raised as an - exception from view code. + ``webob.response.Response`` (in order to directly implement the + ``pyramid.interfaces.IResponse`` interface). + +- The ``pyramid.interfaces.IResponse`` interface now includes a ``__call__`` + method which has the WSGI application call signature (and which expects an + iterable as a result). + +- The Pyramid router now, by default, expects response objects returned from + views to implement the WSGI application interface (a ``__call__`` method + that accepts ``environ`` and ``start_response``, and which returns an + ``app_iter`` iterable). If such a method exists, Pyramid will now call it + in order to satisfy the WSGI request. Backwards compatibility code in the + default responder exists which will fall back to the older behavior, but + Pyramid will raise a deprecation warning if it is reached. See the section + in the Hooks chapter of the documentation entitled "Changing How Pyramid + Treats Response Objects" to default back to the older behavior, where the + ``app_iter``, ``headerlist``, and ``status`` attributes of the object were + consulted directly (without any indirection through ``__call__``) to + silence the deprecation warnings. Dependencies ------------ diff --git a/TODO.txt b/TODO.txt index d85f3b7f0..04c6e60d7 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,6 +1,12 @@ Pyramid TODOs ============= +Must-Have +--------- + +- Depend on only __call__ interface or only 3-attr interface in builtin code + that deals with response objects. + Should-Have ----------- diff --git a/docs/api/httpexceptions.rst b/docs/api/httpexceptions.rst index 73da4126b..325d5af03 100644 --- a/docs/api/httpexceptions.rst +++ b/docs/api/httpexceptions.rst @@ -5,16 +5,14 @@ .. automodule:: pyramid.httpexceptions - .. autofunction:: abort - - .. autofunction:: redirect - .. attribute:: status_map A mapping of integer status code to exception class (eg. the integer "401" maps to :class:`pyramid.httpexceptions.HTTPUnauthorized`). + .. autofunction:: responsecode + .. autoclass:: HTTPException .. autoclass:: HTTPOk diff --git a/docs/api/response.rst b/docs/api/response.rst index c545b4977..e67b15568 100644 --- a/docs/api/response.rst +++ b/docs/api/response.rst @@ -8,3 +8,4 @@ .. autoclass:: Response :members: :inherited-members: + diff --git a/docs/glossary.rst b/docs/glossary.rst index dbab331c1..079a069b4 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -594,7 +594,7 @@ Glossary Not Found view An :term:`exception view` invoked by :app:`Pyramid` when the - developer explicitly raises a ``pyramid.response.HTTPNotFound`` + developer explicitly raises a ``pyramid.httpexceptions.HTTPNotFound`` exception from within :term:`view` code or :term:`root factory` code, or when the current request doesn't match any :term:`view configuration`. :app:`Pyramid` provides a default @@ -604,7 +604,7 @@ Glossary Forbidden view An :term:`exception view` invoked by :app:`Pyramid` when the developer explicitly raises a - ``pyramid.response.HTTPForbidden`` exception from within + ``pyramid.httpexceptions.HTTPForbidden`` exception from within :term:`view` code or :term:`root factory` code, or when the :term:`view configuration` and :term:`authorization policy` found for a request disallows a particular view invocation. diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index aa151d281..b6a781417 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -21,7 +21,7 @@ configuration. The :term:`not found view` callable is a view callable like any other. The :term:`view configuration` which causes it to be a "not found" view consists -only of naming the :exc:`pyramid.response.HTTPNotFound` class as the +only of naming the :exc:`pyramid.httpexceptions.HTTPNotFound` class as the ``context`` of the view configuration. If your application uses :term:`imperative configuration`, you can replace @@ -31,7 +31,7 @@ method to register an "exception view": .. code-block:: python :linenos: - from pyramid.response import HTTPNotFound + from pyramid.httpexceptions import HTTPNotFound from helloworld.views import notfound_view config.add_view(notfound_view, context=HTTPNotFound) @@ -42,22 +42,22 @@ Like any other view, the notfound view must accept at least a ``request`` parameter, or both ``context`` and ``request``. The ``request`` is the current :term:`request` representing the denied action. The ``context`` (if used in the call signature) will be the instance of the -:exc:`~pyramid.response.HTTPNotFound` exception that caused the view to be -called. +:exc:`~pyramid.httpexceptions.HTTPNotFound` exception that caused the view to +be called. Here's some sample code that implements a minimal NotFound view callable: .. code-block:: python :linenos: - from pyramid.response import HTTPNotFound + from pyramid.httpexceptions import HTTPNotFound def notfound_view(request): return HTTPNotFound() .. note:: When a NotFound view callable is invoked, it is passed a :term:`request`. The ``exception`` attribute of the request will be an - instance of the :exc:`~pyramid.response.HTTPNotFound` exception that + instance of the :exc:`~pyramid.httpexceptions.HTTPNotFound` exception that caused the not found view to be called. The value of ``request.exception.args[0]`` will be a value explaining why the not found error was raised. This message will be different when the @@ -67,8 +67,9 @@ Here's some sample code that implements a minimal NotFound view callable: .. warning:: When a NotFound view callable accepts an argument list as described in :ref:`request_and_context_view_definitions`, the ``context`` passed as the first argument to the view callable will be the - :exc:`~pyramid.response.HTTPNotFound` exception instance. If available, - the resource context will still be available as ``request.context``. + :exc:`~pyramid.httpexceptions.HTTPNotFound` exception instance. If + available, the resource context will still be available as + ``request.context``. .. index:: single: forbidden view @@ -85,7 +86,7 @@ the view which generates it can be overridden as necessary. The :term:`forbidden view` callable is a view callable like any other. The :term:`view configuration` which causes it to be a "not found" view consists -only of naming the :exc:`pyramid.response.HTTPForbidden` class as the +only of naming the :exc:`pyramid.httpexceptions.HTTPForbidden` class as the ``context`` of the view configuration. You can replace the forbidden view by using the @@ -96,7 +97,7 @@ view": :linenos: from helloworld.views import forbidden_view - from pyramid.response import HTTPForbidden + from pyramid.httpexceptions import HTTPForbidden config.add_view(forbidden_view, context=HTTPForbidden) Replace ``helloworld.views.forbidden_view`` with a reference to the Python @@ -122,8 +123,8 @@ Here's some sample code that implements a minimal forbidden view: .. note:: When a forbidden view callable is invoked, it is passed a :term:`request`. The ``exception`` attribute of the request will be an - instance of the :exc:`~pyramid.response.HTTPForbidden` exception that - caused the forbidden view to be called. The value of + instance of the :exc:`~pyramid.httpexceptions.HTTPForbidden` exception + that caused the forbidden view to be called. The value of ``request.exception.args[0]`` will be a value explaining why the forbidden was raised. This message will be different when the ``debug_authorization`` environment setting is true than it is when it is @@ -532,10 +533,10 @@ Changing How Pyramid Treats Response Objects It is possible to control how the Pyramid :term:`router` calls the WSGI ``start_response`` callable and obtains the WSGI ``app_iter`` based on adapting the response object to the :class: `pyramid.interfaces.IResponder` -interface. The default ``IResponder`` uses the three attributes ``status``, -``headerlist``, and ``app_iter`` attached to the response object, and calls -``start_response`` with the status and headerlist, returning the -``app_iter``. To override the responder:: +interface. The default responder uses the ``__call__`` method of a response +object, passing it the WSGI environ and the WSGI ``start_response`` callable +(the response is assumed to be a WSGI application). To override the +responder:: from pyramid.interfaces import IResponder from pyramid.response import Response @@ -545,8 +546,9 @@ interface. The default ``IResponder`` uses the three attributes ``status``, IResponder, name='') Overriding makes it possible to reuse response object implementations which -have, for example, their own ``__call__`` expected to be used as a WSGI -application (like :class:`pyramid.response.Response`), e.g.: +have, for example, the ``app_iter``, ``headerlist`` and ``status`` attributes +of an object returned as a response instead of trying to use the object's +``__call__`` method:: class MyResponder(object): def __init__(self, response): @@ -554,8 +556,8 @@ application (like :class:`pyramid.response.Response`), e.g.: self.response = response def __call__(self, request, start_response): """ Call start_response and return an app_iter """ - app_iter = self.response(request.environ, start_response) - return app_iter + start_response(self.response.status, self.response.headerlist) + return self.response.app_iter .. index:: single: view mapper diff --git a/docs/narr/renderers.rst b/docs/narr/renderers.rst index c7a3d7837..99ee14908 100644 --- a/docs/narr/renderers.rst +++ b/docs/narr/renderers.rst @@ -11,7 +11,6 @@ Response interface, :app:`Pyramid` will attempt to use a .. code-block:: python :linenos: - from pyramid.response import Response from pyramid.view import view_config @view_config(renderer='json') @@ -77,39 +76,52 @@ templating language to render a dictionary to a response. Additional renderers can be added by developers to the system as necessary (see :ref:`adding_and_overriding_renderers`). -Views which use a renderer can vary non-body response attributes (such as -headers and the HTTP status code) by attaching a property to the -``request.response`` attribute See :ref:`request_response_attr`. +Views which use a renderer and return a non-Response value can vary non-body +response attributes (such as headers and the HTTP status code) by attaching a +property to the ``request.response`` attribute See +:ref:`request_response_attr`. If the :term:`view callable` associated with a :term:`view configuration` -returns a Response object directly (an object with the attributes ``status``, -``headerlist`` and ``app_iter``), any renderer associated with the view +returns a Response object directly, any renderer associated with the view configuration is ignored, and the response is passed back to :app:`Pyramid` unchanged. For example, if your view callable returns an instance of the -:class:`pyramid.response.HTTPFound` class as a response, no renderer will be -employed. +:class:`pyramid.response.Response` class as a response, no renderer +will be employed. .. code-block:: python :linenos: - from pyramid.response import HTTPFound + from pyramid.response import Response + from pyramid.view import view_config + @view_config(renderer='json') def view(request): - return HTTPFound(location='http://example.com') # any renderer avoided + return Response('OK') # json renderer avoided -Likewise for a "plain old response": +Likewise for an :term:`HTTP exception` response: .. code-block:: python :linenos: - from pyramid.response import Response + from pyramid.httpexceptions import HTTPNotFound + from pyramid.view import view_config + @view_config(renderer='json') def view(request): - return Response('OK') # any renderer avoided + return HTTPFound(location='http://example.com') # json renderer avoided -Mutations to ``request.response`` in views which return a Response object -like this directly (unless that response *is* ``request.response``) will be -ignored. +You can of course also return the ``request.response`` attribute instead to +avoid rendering: + +.. code-block:: python + :linenos: + + from pyramid.view import view_config + + @view_config(renderer='json') + def view(request): + request.response.body = 'OK' + return request.response # json renderer avoided .. index:: single: renderers (built-in) @@ -377,6 +389,31 @@ callable that uses a renderer, assign the ``status`` attribute to the request.response.status = '404 Not Found' return {'URL':request.URL} +Note that mutations of ``request.response`` in views which return a Response +object directly will have no effect unless the response object returned *is* +``request.response``. For example, the following example calls +``request.response.set_cookie``, but this call will have no effect, because a +different Response object is returned. + +.. code-block:: python + :linenos: + + from pyramid.response import Response + + def view(request): + request.response.set_cookie('abc', '123') # this has no effect + return Response('OK') # because we're returning a different response + +If you mutate ``request.response`` and you'd like the mutations to have an +effect, you must return ``request.response``: + +.. code-block:: python + :linenos: + + def view(request): + request.response.set_cookie('abc', '123') + return request.response + For more information on attributes of the request, see the API documentation in :ref:`request_module`. For more information on the API of ``request.response``, see :class:`pyramid.response.Response`. diff --git a/docs/narr/router.rst b/docs/narr/router.rst index 44fa9835b..30d54767e 100644 --- a/docs/narr/router.rst +++ b/docs/narr/router.rst @@ -82,8 +82,8 @@ processing? combination of objects (based on the type of the context, the type of the request, and the value of the view name, and any :term:`predicate` attributes applied to the view configuration), :app:`Pyramid` raises a - :class:`~pyramid.response.HTTPNotFound` exception, which is meant to be - caught by a surrounding exception handler. + :class:`~pyramid.httpexceptions.HTTPNotFound` exception, which is meant to + be caught by a surrounding :term:`exception view`. #. If a view callable was found, :app:`Pyramid` attempts to call the view function. @@ -95,13 +95,13 @@ processing? information in the request and security information attached to the context. If it returns ``True``, :app:`Pyramid` calls the view callable to obtain a response. If it returns ``False``, it raises a - :class:`~pyramid.response.HTTPForbidden` exception, which is meant to be - called by a surrounding exception handler. + :class:`~pyramid.httpexceptions.HTTPForbidden` exception, which is meant + to be called by a surrounding :term:`exception view`. #. If any exception was raised within a :term:`root factory`, by :term:`traversal`, by a :term:`view callable` or by :app:`Pyramid` itself - (such as when it raises :class:`~pyramid.response.HTTPNotFound` or - :class:`~pyramid.response.HTTPForbidden`), the router catches the + (such as when it raises :class:`~pyramid.httpexceptions.HTTPNotFound` or + :class:`~pyramid.httpexceptions.HTTPForbidden`), the router catches the exception, and attaches it to the request as the ``exception`` attribute. It then attempts to find a :term:`exception view` for the exception that was caught. If it finds an exception view callable, that callable is diff --git a/docs/narr/testing.rst b/docs/narr/testing.rst index 862eda9f0..05e851fde 100644 --- a/docs/narr/testing.rst +++ b/docs/narr/testing.rst @@ -191,7 +191,7 @@ function. :linenos: from pyramid.security import has_permission - from pyramid.response import HTTPForbidden + from pyramid.httpexceptions import HTTPForbidden def view_fn(request): if not has_permission('edit', request.context, request): @@ -230,7 +230,7 @@ without needing to invoke the actual application configuration implied by its testing.tearDown() def test_view_fn_forbidden(self): - from pyramid.response import HTTPForbidden + from pyramid.httpexceptions import HTTPForbidden from my.package import view_fn self.config.testing_securitypolicy(userid='hank', permissive=False) diff --git a/docs/narr/urldispatch.rst b/docs/narr/urldispatch.rst index e5228b81e..f94ed3ba8 100644 --- a/docs/narr/urldispatch.rst +++ b/docs/narr/urldispatch.rst @@ -917,7 +917,7 @@ the application's startup configuration, adding the following stanza: :linenos: config.add_view('pyramid.view.append_slash_notfound_view', - context='pyramid.response.HTTPNotFound') + context='pyramid.httpexceptions.HTTPNotFound') See :ref:`view_module` and :ref:`changing_the_notfound_view` for more information about the slash-appending not found view and for a more general @@ -945,7 +945,7 @@ view as the first argument to its constructor. For instance: .. code-block:: python :linenos: - from pyramid.response import HTTPNotFound + from pyramid.httpexceptions import HTTPNotFound from pyramid.view import AppendSlashNotFoundViewFactory def notfound_view(context, request): diff --git a/docs/narr/views.rst b/docs/narr/views.rst index 73a7c2e2a..990828f80 100644 --- a/docs/narr/views.rst +++ b/docs/narr/views.rst @@ -230,29 +230,29 @@ implements the :term:`Response` interface is to return a def view(request): return Response('OK') -You don't need to always use :class:`~pyramid.response.Response` to represent -a response. :app:`Pyramid` provides a range of different "exception" classes -which can act as response objects too. For example, an instance of the class -:class:`pyramid.response.HTTPFound` is also a valid response object -(see :ref:`http_exceptions` and ref:`http_redirect`). A view can actually -return any object that has the following attributes. - -status - The HTTP status code (including the name) for the response as a string. - E.g. ``200 OK`` or ``401 Unauthorized``. - -headerlist - A sequence of tuples representing the list of headers that should be - set in the response. E.g. ``[('Content-Type', 'text/html'), - ('Content-Length', '412')]`` - -app_iter - An iterable representing the body of the response. This can be a - list, e.g. ``['Hello - world!']`` or it can be a file-like object, or any - other sort of iterable. - -These attributes form the structure of the "Pyramid Response interface". +You don't need to use :class:`~pyramid.response.Response` to represent a +response. A view can actually return any object that has a ``__call__`` +method that implements the :term:`WSGI` application call interface. For +example, an instance of the following class could be successfully returned by +a view callable as a response object: + +.. code-block:: python + :linenos: + + class SimpleResponse(object): + def __call__(self, environ, start_response): + """ Call the ``start_response`` callback and return + an iterable """ + body = 'Hello World!' + headers = [('Content-Type', 'text/plain'), + ('Content-Length', str(len(body)))] + start_response('200 OK', headers) + return [body] + +:app:`Pyramid` provides a range of different "exception" classes which can +act as response objects too. For example, an instance of the class +:class:`pyramid.httpexceptions.HTTPFound` is also a valid response object +(see :ref:`http_exceptions` and ref:`http_redirect`). .. index:: single: view exceptions @@ -269,40 +269,8 @@ logged there. However, for convenience, a special set of exceptions exists. When one of these exceptions is raised within a view callable, it will always cause -:app:`Pyramid` to generate a response. Two categories of special exceptions -exist: internal exceptions and HTTP exceptions. - -Internal Exceptions -~~~~~~~~~~~~~~~~~~~ - -:exc:`pyramid.response.HTTPNotFound` and -:exc:`pyramid.response.HTTPForbidden` are exceptions often raised by Pyramid -itself when it (respectively) cannot find a view to service a request or when -authorization was forbidden by a security policy. However, they can also be -raised by application developers. - -If :exc:`~pyramid.response.HTTPNotFound` is raised within view code, the -result of the :term:`Not Found View` will be returned to the user agent which -performed the request. - -If :exc:`~pyramid.response.HTTPForbidden` is raised within view code, the -result of the :term:`Forbidden View` will be returned to the user agent which -performed the request. - -Both are exception classes which accept a single positional constructor -argument: a ``message``. In all cases, the message provided to the exception -constructor is made available to the view which :app:`Pyramid` invokes as -``request.exception.args[0]``. - -An example: - -.. code-block:: python - :linenos: - - from pyramid.response import HTTPNotFound - - def aview(request): - raise HTTPNotFound('not found!') +:app:`Pyramid` to generate a response. These are known as :term:`HTTP +exception` objects. .. index:: single: HTTP exceptions @@ -312,54 +280,77 @@ An example: HTTP Exceptions ~~~~~~~~~~~~~~~ -All classes documented in the :mod:`pyramid.response` module as inheriting -from the :class:`pryamid.response.Response` object implement the -:term:`Response` interface; an instance of any of these classes can be -returned or raised from within a view. The instance will be used as as the -view's response. +All classes documented in the :mod:`pyramid.httpexceptions` module documented +as inheriting from the :class:`pryamid.httpexceptions.HTTPException` are +:term:`http exception` objects. An instances of an HTTP exception object may +either be *returned* or *raised* from within view code. In either case +(return or raise) the instance will be used as as the view's response. -For example, the :class:`pyramid.response.HTTPUnauthorized` exception +For example, the :class:`pyramid.httpexceptions.HTTPUnauthorized` exception can be raised. This will cause a response to be generated with a ``401 Unauthorized`` status: .. code-block:: python :linenos: - from pyramid.response import HTTPUnauthorized + from pyramid.httpexceptions import HTTPUnauthorized def aview(request): raise HTTPUnauthorized() -A shortcut for importing and raising an HTTP exception is the -:func:`pyramid.response.abort` function. This function accepts an HTTP -status code and raises the corresponding HTTP exception. For example, to -raise HTTPUnauthorized, instead of the above, you could do: +An HTTP exception, instead of being raised, can alternately be *returned* +(HTTP exceptions are also valid response objects): .. code-block:: python :linenos: - from pyramid.response import abort + from pyramid.httpexceptions import HTTPUnauthorized def aview(request): - abort(401) - -This is the case because ``401`` is the HTTP status code for "HTTP -Unauthorized". Therefore, ``abort(401)`` is functionally equivalent to -``raise HTTPUnauthorized()``. Other exceptions in -:mod:`pyramid.response` can be raised via -:func:`pyramid.response.abort` as well, as long as the status code -associated with the exception is provided to the function. + return HTTPUnauthorized() -An HTTP exception, instead of being raised, can alternately be *returned* -(HTTP exceptions are also valid response objects): +A shortcut for creating an HTTP exception is the +:func:`pyramid.httpexceptions.responsecode` function. This function accepts +an HTTP status code and returns the corresponding HTTP exception. For +example, instead of importing and constructing a +:class:`~pyramid.httpexceptions.HTTPUnauthorized` response object, you can +use the :func:`~pyramid.httpexceptions.responsecode` function to construct +and return the same object. .. code-block:: python :linenos: - from pyramid.response import HTTPUnauthorized + from pyramid.httpexceptions import responsecode def aview(request): - return HTTPUnauthorized() + raise responsecode(401) + +This is the case because ``401`` is the HTTP status code for "HTTP +Unauthorized". Therefore, ``raise responsecode(401)`` is functionally +equivalent to ``raise HTTPUnauthorized()``. Documentation which maps each +HTTP response code to its purpose and its associated HTTP exception object is +provided within :mod:`pyramid.httpexceptions`. + +How Pyramid Uses HTTP Exceptions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +HTTP exceptions are meant to be used directly by application application +developers. However, Pyramid itself will raise two HTTP exceptions at +various points during normal operations: +:exc:`pyramid.httpexceptions.HTTPNotFound` and +:exc:`pyramid.httpexceptions.HTTPForbidden`. Pyramid will raise the +:exc:`~pyramid.httpexceptions.HTTPNotFound` exception are raised when it +cannot find a view to service a request. Pyramid will raise the +:exc:`~pyramid.httpexceptions.Forbidden` exception or when authorization was +forbidden by a security policy. + +If :exc:`~pyramid.httpexceptions.HTTPNotFound` is raised by Pyramid itself or +within view code, the result of the :term:`Not Found View` will be returned +to the user agent which performed the request. + +If :exc:`~pyramid.httpexceptions.HTTPForbidden` is raised by Pyramid itself +within view code, the result of the :term:`Forbidden View` will be returned +to the user agent which performed the request. .. index:: single: exception views @@ -369,11 +360,10 @@ An HTTP exception, instead of being raised, can alternately be *returned* Custom Exception Views ---------------------- -The machinery which allows :exc:`~pyramid.response.HTTPNotFound`, -:exc:`~pyramid.response.HTTPForbidden` and other responses to be used as -exceptions and caught by specialized views as described in -:ref:`special_exceptions_in_callables` can also be used by application -developers to convert arbitrary exceptions to responses. +The machinery which allows HTTP exceptions to be raised and caught by +specialized views as described in :ref:`special_exceptions_in_callables` can +also be used by application developers to convert arbitrary exceptions to +responses. To register a view that should be called whenever a particular exception is raised from with :app:`Pyramid` view code, use the exception class or one of @@ -409,8 +399,8 @@ raises a ``helloworld.exceptions.ValidationFailure`` exception: Assuming that a :term:`scan` was run to pick up this view registration, this view callable will be invoked whenever a ``helloworld.exceptions.ValidationFailure`` is raised by your application's -view code. The same exception raised by a custom root factory or a custom -traverser is also caught and hooked. +view code. The same exception raised by a custom root factory, a custom +traverser, or a custom view or route predicate is also caught and hooked. Other normal view predicates can also be used in combination with an exception view registration: @@ -458,57 +448,34 @@ Exception views can be configured with any view registration mechanism: Using a View Callable to Do an HTTP Redirect -------------------------------------------- -Two methods exist to redirect to another URL from within a view callable: a -short form and a long form. The short form should be preferred when -possible. - -Short Form -~~~~~~~~~~ - -You can issue an HTTP redirect from within a view callable by using the -:func:`pyramid.response.redirect` function. This function raises an -:class:`pyramid.response.HTTPFound` exception (a "302"), which is caught by -the default exception response handler and turned into a response. - -.. code-block:: python - :linenos: - - from pyramid.response import redirect - - def myview(request): - redirect('http://example.com') - -Long Form -~~~~~~~~~ - -You can issue an HTTP redirect from within a view "by hand" instead of -relying on the :func:`pyramid.response.redirect` function to do it for -you. +You can issue an HTTP redirect by using the +:class:`pyramid.httpexceptions.HTTPFound` class. Raising or returning an +instance of this class will cause the client to receive a "302 Found" +response. -To do so, you can *return* a :class:`pyramid.response.HTTPFound` +To do so, you can *return* a :class:`pyramid.httpexceptions.HTTPFound` instance. .. code-block:: python :linenos: - from pyramid.response import HTTPFound + from pyramid.httpexceptions import HTTPFound def myview(request): return HTTPFound(location='http://example.com') -Or, alternately, you can *raise* an HTTPFound exception instead of returning -one. +Alternately, you can *raise* an HTTPFound exception instead of returning one. .. code-block:: python :linenos: - from pyramid.response import HTTPFound + from pyramid.httpexceptions import HTTPFound def myview(request): raise HTTPFound(location='http://example.com') -The above form of generating a response by raising HTTPFound is completely -equivalent to ``redirect('http://example.com')``. +When the instance is raised, it is caught by the default :term:`exception +response` handler and turned into a response. .. index:: single: unicode, views, and forms diff --git a/docs/narr/webob.rst b/docs/narr/webob.rst index 6cd9418ce..70ab5eea8 100644 --- a/docs/narr/webob.rst +++ b/docs/narr/webob.rst @@ -362,20 +362,21 @@ To facilitate error responses like ``404 Not Found``, the module :mod:`webob.exc` contains classes for each kind of error response. These include boring, but appropriate error bodies. The exceptions exposed by this module, when used under :app:`Pyramid`, should be imported from the -:mod:`pyramid.response` module. This import location contains subclasses and -replacements that mirror those in the original ``webob.exc``. +:mod:`pyramid.httpexceptions` module. This import location contains +subclasses and replacements that mirror those in the original ``webob.exc``. -Each class is named ``pyramid.response.HTTP*``, where ``*`` is the reason for -the error. For instance, :class:`pyramid.response.HTTPNotFound`. It -subclasses :class:`pyramid.Response`, so you can manipulate the instances in -the same way. A typical example is: +Each class is named ``pyramid.httpexceptions.HTTP*``, where ``*`` is the +reason for the error. For instance, +:class:`pyramid.httpexceptions.HTTPNotFound` subclasses +:class:`pyramid.Response`, so you can manipulate the instances in the same +way. A typical example is: .. ignore-next-block .. code-block:: python :linenos: - from pyramid.response import HTTPNotFound - from pyramid.response import HTTPMovedPermanently + from pyramid.httpexceptions import HTTPNotFound + from pyramid.httpexceptions import HTTPMovedPermanently response = HTTPNotFound('There is no such resource') # or: @@ -385,7 +386,7 @@ More Details ++++++++++++ More details about the response object API are available in the -:mod:`pyramid.response` documentation. More details about exception responses -are in the :mod:`pyramid.response` API documentation. The `WebOb -documentation `_ is also useful. +:mod:`pyramid.response` documentation. More details about exception +responses are in the :mod:`pyramid.httpexceptions` API documentation. The +`WebOb documentation `_ is also useful. diff --git a/docs/tutorials/wiki/authorization.rst b/docs/tutorials/wiki/authorization.rst index 3b102958e..de5c9486d 100644 --- a/docs/tutorials/wiki/authorization.rst +++ b/docs/tutorials/wiki/authorization.rst @@ -131,17 +131,17 @@ callable. The first view configuration decorator configures the ``login`` view callable so it will be invoked when someone visits ``/login`` (when the context is a Wiki and the view name is ``login``). The second decorator (with context of -``pyramid.response.HTTPForbidden``) specifies a :term:`forbidden view`. This -configures our login view to be presented to the user when :app:`Pyramid` -detects that a view invocation can not be authorized. Because we've -configured a forbidden view, the ``login`` view callable will be invoked -whenever one of our users tries to execute a view callable that they are not -allowed to invoke as determined by the :term:`authorization policy` in use. -In our application, for example, this means that if a user has not logged in, -and he tries to add or edit a Wiki page, he will be shown the login form. -Before being allowed to continue on to the add or edit form, he will have to -provide credentials that give him permission to add or edit via this login -form. +``pyramid.httpexceptions.HTTPForbidden``) specifies a :term:`forbidden view`. +This configures our login view to be presented to the user when +:app:`Pyramid` detects that a view invocation can not be authorized. Because +we've configured a forbidden view, the ``login`` view callable will be +invoked whenever one of our users tries to execute a view callable that they +are not allowed to invoke as determined by the :term:`authorization policy` +in use. In our application, for example, this means that if a user has not +logged in, and he tries to add or edit a Wiki page, he will be shown the +login form. Before being allowed to continue on to the add or edit form, he +will have to provide credentials that give him permission to add or edit via +this login form. Changing Existing Views ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/tutorials/wiki/definingviews.rst b/docs/tutorials/wiki/definingviews.rst index ea8842294..b6c083bbf 100644 --- a/docs/tutorials/wiki/definingviews.rst +++ b/docs/tutorials/wiki/definingviews.rst @@ -83,10 +83,10 @@ No renderer is necessary when a view returns a response object. The ``view_wiki`` view callable always redirects to the URL of a Page resource named "FrontPage". To do so, it returns an instance of the -:class:`pyramid.response.HTTPFound` class (instances of which implement the -WebOb :term:`response` interface). The :func:`pyramid.url.resource_url` API. -:func:`pyramid.url.resource_url` constructs a URL to the ``FrontPage`` page -resource (e.g. ``http://localhost:6543/FrontPage``), and uses it as the +:class:`pyramid.httpexceptions.HTTPFound` class (instances of which implement +the WebOb :term:`response` interface). The :func:`pyramid.url.resource_url` +API. :func:`pyramid.url.resource_url` constructs a URL to the ``FrontPage`` +page resource (e.g. ``http://localhost:6543/FrontPage``), and uses it as the "location" of the HTTPFound response, forming an HTTP redirect. The ``view_page`` view function diff --git a/docs/tutorials/wiki/src/authorization/tutorial/login.py b/docs/tutorials/wiki/src/authorization/tutorial/login.py index 822b19b9e..334115880 100644 --- a/docs/tutorials/wiki/src/authorization/tutorial/login.py +++ b/docs/tutorials/wiki/src/authorization/tutorial/login.py @@ -1,4 +1,4 @@ -from pyramid.response import HTTPFound +from pyramid.httpexceptions import HTTPFound from pyramid.security import remember from pyramid.security import forget @@ -9,7 +9,7 @@ from tutorial.security import USERS @view_config(context='tutorial.models.Wiki', name='login', renderer='templates/login.pt') -@view_config(context='pyramid.response.HTTPForbidden', +@view_config(context='pyramid.httpexceptions.HTTPForbidden', renderer='templates/login.pt') def login(request): login_url = resource_url(request.context, request, 'login') diff --git a/docs/tutorials/wiki/src/authorization/tutorial/views.py b/docs/tutorials/wiki/src/authorization/tutorial/views.py index 67550d58e..a83e17de4 100644 --- a/docs/tutorials/wiki/src/authorization/tutorial/views.py +++ b/docs/tutorials/wiki/src/authorization/tutorial/views.py @@ -1,7 +1,7 @@ from docutils.core import publish_parts import re -from pyramid.response import HTTPFound +from pyramid.httpexceptions import HTTPFound from pyramid.url import resource_url from pyramid.view import view_config from pyramid.security import authenticated_userid diff --git a/docs/tutorials/wiki/src/views/tutorial/views.py b/docs/tutorials/wiki/src/views/tutorial/views.py index d72cbd3fd..42420f2fe 100644 --- a/docs/tutorials/wiki/src/views/tutorial/views.py +++ b/docs/tutorials/wiki/src/views/tutorial/views.py @@ -1,7 +1,7 @@ from docutils.core import publish_parts import re -from pyramid.response import HTTPFound +from pyramid.httpexceptions import HTTPFound from pyramid.url import resource_url from pyramid.view import view_config diff --git a/docs/tutorials/wiki2/definingviews.rst b/docs/tutorials/wiki2/definingviews.rst index 32e3c0b24..832f90b92 100644 --- a/docs/tutorials/wiki2/definingviews.rst +++ b/docs/tutorials/wiki2/definingviews.rst @@ -90,8 +90,8 @@ path to our "FrontPage". :language: python The ``view_wiki`` function returns an instance of the -:class:`pyramid.response.HTTPFound` class (instances of which implement the -WebOb :term:`response` interface), It will use the +:class:`pyramid.httpexceptions.HTTPFound` class (instances of which implement +the WebOb :term:`response` interface), It will use the :func:`pyramid.url.route_url` API to construct a URL to the ``FrontPage`` page (e.g. ``http://localhost:6543/FrontPage``), and will use it as the "location" of the HTTPFound response, forming an HTTP redirect. diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py b/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py index 42013622c..4cd84eda5 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py @@ -39,7 +39,7 @@ def main(global_config, **settings): config.add_view('tutorial.views.edit_page', route_name='edit_page', renderer='tutorial:templates/edit.pt', permission='edit') config.add_view('tutorial.login.login', - context='pyramid.response.HTTPForbidden', + context='pyramid.httpexceptions.HTTPForbidden', renderer='tutorial:templates/login.pt') return config.make_wsgi_app() diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/login.py b/docs/tutorials/wiki2/src/authorization/tutorial/login.py index 2bc8a7201..7a1d1f663 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/login.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/login.py @@ -1,4 +1,4 @@ -from pyramid.response import HTTPFound +from pyramid.httpexceptions import HTTPFound from pyramid.security import remember from pyramid.security import forget from pyramid.url import route_url diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views.py b/docs/tutorials/wiki2/src/authorization/tutorial/views.py index ed441295c..5abd8391e 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/views.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/views.py @@ -2,7 +2,7 @@ import re from docutils.core import publish_parts -from pyramid.response import HTTPFound +from pyramid.httpexceptions import HTTPFound from pyramid.security import authenticated_userid from pyramid.url import route_url diff --git a/docs/tutorials/wiki2/src/views/tutorial/views.py b/docs/tutorials/wiki2/src/views/tutorial/views.py index 80d817d99..b8896abe7 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/views.py +++ b/docs/tutorials/wiki2/src/views/tutorial/views.py @@ -2,7 +2,7 @@ import re from docutils.core import publish_parts -from pyramid.response import HTTPFound +from pyramid.httpexceptions import HTTPFound from pyramid.url import route_url from tutorial.models import DBSession diff --git a/docs/whatsnew-1.1.rst b/docs/whatsnew-1.1.rst index 488328519..533ae3637 100644 --- a/docs/whatsnew-1.1.rst +++ b/docs/whatsnew-1.1.rst @@ -63,12 +63,6 @@ Default HTTP Exception View from within view code; when raised, this exception view will render the exception to a response. - New convenience functions named :func:`pyramid.httpexceptions.abort` and - :func:`pyramid.httpexceptions.redirect` perform the equivalent of their - Pylons brethren when an HTTP exception handler is registered. These - functions take advantage of the newly registered exception view for - :exc:`webob.exc.HTTPException`. - To allow for configuration of this feature, the :term:`Configurator` now accepts an additional keyword argument named ``httpexception_view``. By default, this argument is populated with a default exception view function @@ -81,6 +75,10 @@ Default HTTP Exception View Minor Feature Additions ----------------------- +- A function named :func:`pyramid.httpexceptions.responsecode` is a shortcut + that can be used to create HTTP exception response objects using an HTTP + integer status code. + - Integers and longs passed as ``elements`` to :func:`pyramid.url.resource_url` or :meth:`pyramid.request.Request.resource_url` e.g. ``resource_url(context, @@ -177,7 +175,7 @@ Deprecations and Behavior Differences expected an environ object in BFG 1.0 and before). In a future version, these methods will be removed entirely. -- A custom request factory is now required to return a response object that +- A custom request factory is now required to return a request object that has a ``response`` attribute (or "reified"/lazy property) if they the request is meant to be used in a view that uses a renderer. This ``response`` attribute should be an instance of the class diff --git a/pyramid/config.py b/pyramid/config.py index ab1729c06..91ba414b3 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -56,10 +56,10 @@ from pyramid.compat import md5 from pyramid.compat import any from pyramid.events import ApplicationCreated from pyramid.exceptions import ConfigurationError -from pyramid.response import default_exceptionresponse_view -from pyramid.response import HTTPForbidden -from pyramid.response import HTTPNotFound from pyramid.exceptions import PredicateMismatch +from pyramid.httpexceptions import default_exceptionresponse_view +from pyramid.httpexceptions import HTTPForbidden +from pyramid.httpexceptions import HTTPNotFound from pyramid.i18n import get_localizer from pyramid.log import make_stream_logger from pyramid.mako_templating import renderer_factory as mako_renderer_factory diff --git a/pyramid/exceptions.py b/pyramid/exceptions.py index 2484f94a3..151fc241f 100644 --- a/pyramid/exceptions.py +++ b/pyramid/exceptions.py @@ -1,12 +1,12 @@ from zope.configuration.exceptions import ConfigurationError as ZCE -from pyramid.response import HTTPNotFound -from pyramid.response import HTTPForbidden +from pyramid.httpexceptions import HTTPNotFound +from pyramid.httpexceptions import HTTPForbidden NotFound = HTTPNotFound # bw compat Forbidden = HTTPForbidden # bw compat -class PredicateMismatch(NotFound): +class PredicateMismatch(HTTPNotFound): """ Internal exception (not an API) raised by multiviews when no view matches. This exception subclasses the ``NotFound`` diff --git a/pyramid/httpexceptions.py b/pyramid/httpexceptions.py index dbb530b4a..a692380f8 100644 --- a/pyramid/httpexceptions.py +++ b/pyramid/httpexceptions.py @@ -110,7 +110,907 @@ field. Reflecting this, these subclasses have one additional keyword argument: ``location``, which indicates the location to which to redirect. """ -from pyramid.response import * # API +import types +from string import Template + +from zope.interface import implements + +from webob import html_escape as _html_escape + +from pyramid.interfaces import IExceptionResponse +from pyramid.response import Response + +def _no_escape(value): + if value is None: + return '' + if not isinstance(value, basestring): + if hasattr(value, '__unicode__'): + value = unicode(value) + else: + value = str(value) + return value + +class HTTPException(Exception): # bw compat + pass + +class WSGIHTTPException(Response, HTTPException): + implements(IExceptionResponse) + + ## You should set in subclasses: + # code = 200 + # title = 'OK' + # explanation = 'why this happens' + # body_template_obj = Template('response template') + + # differences from webob.exc.WSGIHTTPException: + # - not a WSGI application (just a response) + # + # as a result: + # + # - bases plaintext vs. html result on self.content_type rather than + # on request accept header + # + # - doesn't add request.environ keys to template substitutions unless + # 'request' is passed as a constructor keyword argument. + # + # - doesn't use "strip_tags" (${br} placeholder for
, no other html + # in default body template) + # + # - sets a default app_iter if no body, app_iter, or unicode_body is + # passed using a template (ala the replaced version's "generate_response") + # + # - explicitly sets self.message = detail to prevent whining by Python + # 2.6.5+ access of Exception.message + # + # - its base class of HTTPException is no longer a Python 2.4 compatibility + # shim; it's purely a base class that inherits from Exception. This + # implies that this class' ``exception`` property always returns + # ``self`` (only for bw compat at this point). + # + # - documentation improvements (Pyramid-specific docstrings where necessary) + # + code = None + title = None + explanation = '' + body_template_obj = Template('''\ +${explanation}${br}${br} +${detail} +${html_comment} +''') + + plain_template_obj = Template('''\ +${status} + +${body}''') + + html_template_obj = Template('''\ + + + ${status} + + +

${status}

+ ${body} + +''') + + ## Set this to True for responses that should have no request body + empty_body = False + + def __init__(self, detail=None, headers=None, comment=None, + body_template=None, **kw): + status = '%s %s' % (self.code, self.title) + Response.__init__(self, status=status, **kw) + Exception.__init__(self, detail) + self.detail = self.message = detail + if headers: + self.headers.extend(headers) + self.comment = comment + if body_template is not None: + self.body_template = body_template + self.body_template_obj = Template(body_template) + + if self.empty_body: + del self.content_type + del self.content_length + elif not ('unicode_body' in kw or 'body' in kw or 'app_iter' in kw): + self.app_iter = self._default_app_iter() + + def __str__(self): + return self.detail or self.explanation + + def _default_app_iter(self): + # This is a generator which defers the creation of the response page + # body; we use a generator because we want to ensure that if + # attributes of this response are changed after it is constructed, we + # use the changed values rather than the values at time of construction + # (e.g. self.content_type or self.charset). + html_comment = '' + comment = self.comment or '' + content_type = self.content_type or '' + if 'html' in content_type: + escape = _html_escape + page_template = self.html_template_obj + br = '
' + if comment: + html_comment = '' % escape(comment) + else: + escape = _no_escape + page_template = self.plain_template_obj + br = '\n' + if comment: + html_comment = escape(comment) + args = { + 'br':br, + 'explanation': escape(self.explanation), + 'detail': escape(self.detail or ''), + 'comment': escape(comment), + 'html_comment':html_comment, + } + body_tmpl = self.body_template_obj + if WSGIHTTPException.body_template_obj is not body_tmpl: + # Custom template; add headers to args + environ = self.environ + if environ is not None: + for k, v in environ.items(): + args[k] = escape(v) + for k, v in self.headers.items(): + args[k.lower()] = escape(v) + body = body_tmpl.substitute(args) + page = page_template.substitute(status=self.status, body=body) + if isinstance(page, unicode): + page = page.encode(self.charset) + yield page + raise StopIteration + + @property + def exception(self): + # bw compat only + return self + wsgi_response = exception # bw compat only + +class HTTPError(WSGIHTTPException): + """ + base class for status codes in the 400's and 500's + + This is an exception which indicates that an error has occurred, + and that any work in progress should not be committed. These are + typically results in the 400's and 500's. + """ + +class HTTPRedirection(WSGIHTTPException): + """ + base class for 300's status code (redirections) + + This is an abstract base class for 3xx redirection. It indicates + that further action needs to be taken by the user agent in order + to fulfill the request. It does not necessarly signal an error + condition. + """ + +class HTTPOk(WSGIHTTPException): + """ + Base class for the 200's status code (successful responses) + + code: 200, title: OK + """ + code = 200 + title = 'OK' + +############################################################ +## 2xx success +############################################################ + +class HTTPCreated(HTTPOk): + """ + subclass of :class:`~HTTPOk` + + This indicates that request has been fulfilled and resulted in a new + resource being created. + + code: 201, title: Created + """ + code = 201 + title = 'Created' + +class HTTPAccepted(HTTPOk): + """ + subclass of :class:`~HTTPOk` + + This indicates that the request has been accepted for processing, but the + processing has not been completed. + + code: 202, title: Accepted + """ + code = 202 + title = 'Accepted' + explanation = 'The request is accepted for processing.' + +class HTTPNonAuthoritativeInformation(HTTPOk): + """ + subclass of :class:`~HTTPOk` + + This indicates that the returned metainformation in the entity-header is + not the definitive set as available from the origin server, but is + gathered from a local or a third-party copy. + + code: 203, title: Non-Authoritative Information + """ + code = 203 + title = 'Non-Authoritative Information' + +class HTTPNoContent(HTTPOk): + """ + subclass of :class:`~HTTPOk` + + This indicates that the server has fulfilled the request but does + not need to return an entity-body, and might want to return updated + metainformation. + + code: 204, title: No Content + """ + code = 204 + title = 'No Content' + empty_body = True + +class HTTPResetContent(HTTPOk): + """ + subclass of :class:`~HTTPOk` + + This indicates that the the server has fulfilled the request and + the user agent SHOULD reset the document view which caused the + request to be sent. + + code: 205, title: Reset Content + """ + code = 205 + title = 'Reset Content' + empty_body = True + +class HTTPPartialContent(HTTPOk): + """ + subclass of :class:`~HTTPOk` + + This indicates that the server has fulfilled the partial GET + request for the resource. + + code: 206, title: Partial Content + """ + code = 206 + title = 'Partial Content' + +## FIXME: add 207 Multi-Status (but it's complicated) + +############################################################ +## 3xx redirection +############################################################ + +class _HTTPMove(HTTPRedirection): + """ + redirections which require a Location field + + Since a 'Location' header is a required attribute of 301, 302, 303, + 305 and 307 (but not 304), this base class provides the mechanics to + make this easy. + + You must provide a ``location`` keyword argument. + """ + # differences from webob.exc._HTTPMove: + # + # - not a wsgi app + # + # - ${location} isn't wrapped in an
tag in body + # + # - location keyword arg defaults to '' + # + # - ``add_slash`` argument is no longer accepted: code that passes + # add_slash argument to the constructor will receive an exception. + explanation = 'The resource has been moved to' + body_template_obj = Template('''\ +${explanation} ${location}; +you should be redirected automatically. +${detail} +${html_comment}''') + + def __init__(self, detail=None, headers=None, comment=None, + body_template=None, location='', **kw): + super(_HTTPMove, self).__init__( + detail=detail, headers=headers, comment=comment, + body_template=body_template, location=location, **kw) + +class HTTPMultipleChoices(_HTTPMove): + """ + subclass of :class:`~_HTTPMove` + + This indicates that the requested resource corresponds to any one + of a set of representations, each with its own specific location, + and agent-driven negotiation information is being provided so that + the user can select a preferred representation and redirect its + request to that location. + + code: 300, title: Multiple Choices + """ + code = 300 + title = 'Multiple Choices' + +class HTTPMovedPermanently(_HTTPMove): + """ + subclass of :class:`~_HTTPMove` + + This indicates that the requested resource has been assigned a new + permanent URI and any future references to this resource SHOULD use + one of the returned URIs. + + code: 301, title: Moved Permanently + """ + code = 301 + title = 'Moved Permanently' + +class HTTPFound(_HTTPMove): + """ + subclass of :class:`~_HTTPMove` + + This indicates that the requested resource resides temporarily under + a different URI. + + code: 302, title: Found + """ + code = 302 + title = 'Found' + explanation = 'The resource was found at' + +# This one is safe after a POST (the redirected location will be +# retrieved with GET): +class HTTPSeeOther(_HTTPMove): + """ + subclass of :class:`~_HTTPMove` + + This indicates that the response to the request can be found under + a different URI and SHOULD be retrieved using a GET method on that + resource. + + code: 303, title: See Other + """ + code = 303 + title = 'See Other' + +class HTTPNotModified(HTTPRedirection): + """ + subclass of :class:`~HTTPRedirection` + + This indicates that if the client has performed a conditional GET + request and access is allowed, but the document has not been + modified, the server SHOULD respond with this status code. + + code: 304, title: Not Modified + """ + # FIXME: this should include a date or etag header + code = 304 + title = 'Not Modified' + empty_body = True + +class HTTPUseProxy(_HTTPMove): + """ + subclass of :class:`~_HTTPMove` + + This indicates that the requested resource MUST be accessed through + the proxy given by the Location field. + + code: 305, title: Use Proxy + """ + # Not a move, but looks a little like one + code = 305 + title = 'Use Proxy' + explanation = ( + 'The resource must be accessed through a proxy located at') + +class HTTPTemporaryRedirect(_HTTPMove): + """ + subclass of :class:`~_HTTPMove` + + This indicates that the requested resource resides temporarily + under a different URI. + + code: 307, title: Temporary Redirect + """ + code = 307 + title = 'Temporary Redirect' + +############################################################ +## 4xx client error +############################################################ + +class HTTPClientError(HTTPError): + """ + base class for the 400's, where the client is in error + + This is an error condition in which the client is presumed to be + in-error. This is an expected problem, and thus is not considered + a bug. A server-side traceback is not warranted. Unless specialized, + this is a '400 Bad Request' + """ + code = 400 + title = 'Bad Request' + explanation = ('The server could not comply with the request since ' + 'it is either malformed or otherwise incorrect.') + +class HTTPBadRequest(HTTPClientError): + pass + +class HTTPUnauthorized(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the request requires user authentication. + + code: 401, title: Unauthorized + """ + code = 401 + title = 'Unauthorized' + explanation = ( + 'This server could not verify that you are authorized to ' + 'access the document you requested. Either you supplied the ' + 'wrong credentials (e.g., bad password), or your browser ' + 'does not understand how to supply the credentials required.') + +class HTTPPaymentRequired(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + code: 402, title: Payment Required + """ + code = 402 + title = 'Payment Required' + explanation = ('Access was denied for financial reasons.') + +class HTTPForbidden(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the server understood the request, but is + refusing to fulfill it. + + code: 403, title: Forbidden + + Raise this exception within :term:`view` code to immediately return the + :term:`forbidden view` to the invoking user. Usually this is a basic + ``403`` page, but the forbidden view can be customized as necessary. See + :ref:`changing_the_forbidden_view`. A ``Forbidden`` exception will be + the ``context`` of a :term:`Forbidden View`. + + This exception's constructor treats two arguments specially. The first + argument, ``detail``, should be a string. The value of this string will + be used as the ``message`` attribute of the exception object. The second + special keyword argument, ``result`` is usually an instance of + :class:`pyramid.security.Denied` or :class:`pyramid.security.ACLDenied` + each of which indicates a reason for the forbidden error. However, + ``result`` is also permitted to be just a plain boolean ``False`` object + or ``None``. The ``result`` value will be used as the ``result`` + attribute of the exception object. It defaults to ``None``. + + The :term:`Forbidden View` can use the attributes of a Forbidden + exception as necessary to provide extended information in an error + report shown to a user. + """ + # differences from webob.exc.HTTPForbidden: + # + # - accepts a ``result`` keyword argument + # + # - overrides constructor to set ``self.result`` + # + # differences from older ``pyramid.exceptions.Forbidden``: + # + # - ``result`` must be passed as a keyword argument. + # + code = 403 + title = 'Forbidden' + explanation = ('Access was denied to this resource.') + def __init__(self, detail=None, headers=None, comment=None, + body_template=None, result=None, **kw): + HTTPClientError.__init__(self, detail=detail, headers=headers, + comment=comment, body_template=body_template, + **kw) + self.result = result + +class HTTPNotFound(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the server did not find anything matching the + Request-URI. + + code: 404, title: Not Found + + Raise this exception within :term:`view` code to immediately + return the :term:`Not Found view` to the invoking user. Usually + this is a basic ``404`` page, but the Not Found view can be + customized as necessary. See :ref:`changing_the_notfound_view`. + + This exception's constructor accepts a ``detail`` argument + (the first argument), which should be a string. The value of this + string will be available as the ``message`` attribute of this exception, + for availability to the :term:`Not Found View`. + """ + code = 404 + title = 'Not Found' + explanation = ('The resource could not be found.') + +class HTTPMethodNotAllowed(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the method specified in the Request-Line is + not allowed for the resource identified by the Request-URI. + + code: 405, title: Method Not Allowed + """ + # differences from webob.exc.HTTPMethodNotAllowed: + # + # - body_template_obj not overridden (it tried to use request environ's + # REQUEST_METHOD) + code = 405 + title = 'Method Not Allowed' + +class HTTPNotAcceptable(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates the resource identified by the request is only + capable of generating response entities which have content + characteristics not acceptable according to the accept headers + sent in the request. + + code: 406, title: Not Acceptable + """ + # differences from webob.exc.HTTPNotAcceptable: + # + # - body_template_obj not overridden (it tried to use request environ's + # HTTP_ACCEPT) + code = 406 + title = 'Not Acceptable' + +class HTTPProxyAuthenticationRequired(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This is similar to 401, but indicates that the client must first + authenticate itself with the proxy. + + code: 407, title: Proxy Authentication Required + """ + code = 407 + title = 'Proxy Authentication Required' + explanation = ('Authentication with a local proxy is needed.') + +class HTTPRequestTimeout(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the client did not produce a request within + the time that the server was prepared to wait. + + code: 408, title: Request Timeout + """ + code = 408 + title = 'Request Timeout' + explanation = ('The server has waited too long for the request to ' + 'be sent by the client.') + +class HTTPConflict(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the request could not be completed due to a + conflict with the current state of the resource. + + code: 409, title: Conflict + """ + code = 409 + title = 'Conflict' + explanation = ('There was a conflict when trying to complete ' + 'your request.') + +class HTTPGone(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the requested resource is no longer available + at the server and no forwarding address is known. + + code: 410, title: Gone + """ + code = 410 + title = 'Gone' + explanation = ('This resource is no longer available. No forwarding ' + 'address is given.') + +class HTTPLengthRequired(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the the server refuses to accept the request + without a defined Content-Length. + + code: 411, title: Length Required + """ + code = 411 + title = 'Length Required' + explanation = ('Content-Length header required.') + +class HTTPPreconditionFailed(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the precondition given in one or more of the + request-header fields evaluated to false when it was tested on the + server. + + code: 412, title: Precondition Failed + """ + code = 412 + title = 'Precondition Failed' + explanation = ('Request precondition failed.') + +class HTTPRequestEntityTooLarge(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the server is refusing to process a request + because the request entity is larger than the server is willing or + able to process. + + code: 413, title: Request Entity Too Large + """ + code = 413 + title = 'Request Entity Too Large' + explanation = ('The body of your request was too large for this server.') + +class HTTPRequestURITooLong(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the server is refusing to service the request + because the Request-URI is longer than the server is willing to + interpret. + + code: 414, title: Request-URI Too Long + """ + code = 414 + title = 'Request-URI Too Long' + explanation = ('The request URI was too long for this server.') + +class HTTPUnsupportedMediaType(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the server is refusing to service the request + because the entity of the request is in a format not supported by + the requested resource for the requested method. + + code: 415, title: Unsupported Media Type + """ + # differences from webob.exc.HTTPUnsupportedMediaType: + # + # - body_template_obj not overridden (it tried to use request environ's + # CONTENT_TYPE) + code = 415 + title = 'Unsupported Media Type' + +class HTTPRequestRangeNotSatisfiable(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + The server SHOULD return a response with this status code if a + request included a Range request-header field, and none of the + range-specifier values in this field overlap the current extent + of the selected resource, and the request did not include an + If-Range request-header field. + + code: 416, title: Request Range Not Satisfiable + """ + code = 416 + title = 'Request Range Not Satisfiable' + explanation = ('The Range requested is not available.') + +class HTTPExpectationFailed(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indidcates that the expectation given in an Expect + request-header field could not be met by this server. + + code: 417, title: Expectation Failed + """ + code = 417 + title = 'Expectation Failed' + explanation = ('Expectation failed.') + +class HTTPUnprocessableEntity(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the server is unable to process the contained + instructions. Only for WebDAV. + + code: 422, title: Unprocessable Entity + """ + ## Note: from WebDAV + code = 422 + title = 'Unprocessable Entity' + explanation = 'Unable to process the contained instructions' + +class HTTPLocked(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the resource is locked. Only for WebDAV + + code: 423, title: Locked + """ + ## Note: from WebDAV + code = 423 + title = 'Locked' + explanation = ('The resource is locked') + +class HTTPFailedDependency(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the method could not be performed because the + requested action depended on another action and that action failed. + Only for WebDAV. + + code: 424, title: Failed Dependency + """ + ## Note: from WebDAV + code = 424 + title = 'Failed Dependency' + explanation = ( + 'The method could not be performed because the requested ' + 'action dependended on another action and that action failed') + +############################################################ +## 5xx Server Error +############################################################ +# Response status codes beginning with the digit "5" indicate cases in +# which the server is aware that it has erred or is incapable of +# performing the request. Except when responding to a HEAD request, the +# server SHOULD include an entity containing an explanation of the error +# situation, and whether it is a temporary or permanent condition. User +# agents SHOULD display any included entity to the user. These response +# codes are applicable to any request method. + +class HTTPServerError(HTTPError): + """ + base class for the 500's, where the server is in-error + + This is an error condition in which the server is presumed to be + in-error. This is usually unexpected, and thus requires a traceback; + ideally, opening a support ticket for the customer. Unless specialized, + this is a '500 Internal Server Error' + """ + code = 500 + title = 'Internal Server Error' + explanation = ( + 'The server has either erred or is incapable of performing ' + 'the requested operation.') + +class HTTPInternalServerError(HTTPServerError): + pass + +class HTTPNotImplemented(HTTPServerError): + """ + subclass of :class:`~HTTPServerError` + + This indicates that the server does not support the functionality + required to fulfill the request. + + code: 501, title: Not Implemented + """ + # differences from webob.exc.HTTPNotAcceptable: + # + # - body_template_obj not overridden (it tried to use request environ's + # REQUEST_METHOD) + code = 501 + title = 'Not Implemented' + +class HTTPBadGateway(HTTPServerError): + """ + subclass of :class:`~HTTPServerError` + + This indicates that the server, while acting as a gateway or proxy, + received an invalid response from the upstream server it accessed + in attempting to fulfill the request. + + code: 502, title: Bad Gateway + """ + code = 502 + title = 'Bad Gateway' + explanation = ('Bad gateway.') + +class HTTPServiceUnavailable(HTTPServerError): + """ + subclass of :class:`~HTTPServerError` + + This indicates that the server is currently unable to handle the + request due to a temporary overloading or maintenance of the server. + + code: 503, title: Service Unavailable + """ + code = 503 + title = 'Service Unavailable' + explanation = ('The server is currently unavailable. ' + 'Please try again at a later time.') + +class HTTPGatewayTimeout(HTTPServerError): + """ + subclass of :class:`~HTTPServerError` + + This indicates that the server, while acting as a gateway or proxy, + did not receive a timely response from the upstream server specified + by the URI (e.g. HTTP, FTP, LDAP) or some other auxiliary server + (e.g. DNS) it needed to access in attempting to complete the request. + + code: 504, title: Gateway Timeout + """ + code = 504 + title = 'Gateway Timeout' + explanation = ('The gateway has timed out.') + +class HTTPVersionNotSupported(HTTPServerError): + """ + subclass of :class:`~HTTPServerError` + + This indicates that the server does not support, or refuses to + support, the HTTP protocol version that was used in the request + message. + + code: 505, title: HTTP Version Not Supported + """ + code = 505 + title = 'HTTP Version Not Supported' + explanation = ('The HTTP version is not supported.') + +class HTTPInsufficientStorage(HTTPServerError): + """ + subclass of :class:`~HTTPServerError` + + This indicates that the server does not have enough space to save + the resource. + + code: 507, title: Insufficient Storage + """ + code = 507 + title = 'Insufficient Storage' + explanation = ('There was not enough space to save the resource') + +def responsecode(status_code, **kw): + """Creates an HTTP exception based on a status code. Example:: + + raise responsecode(404) # raises an HTTPNotFound exception. + + The values passed as ``kw`` are provided to the exception's constructor. + """ + exc = status_map[status_code](**kw) + return exc + +def default_exceptionresponse_view(context, request): + if not isinstance(context, Exception): + # backwards compat for an exception response view registered via + # config.set_notfound_view or config.set_forbidden_view + # instead of as a proper exception view + context = request.exception or context + return context + +status_map={} +for name, value in globals().items(): + if (isinstance(value, (type, types.ClassType)) and + issubclass(value, HTTPException) + and not name.startswith('_')): + code = getattr(value, 'code', None) + if code: + status_map[code] = value +del name, value diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index d5d382492..b8ff2d4c9 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -46,11 +46,15 @@ class IApplicationCreated(Interface): IWSGIApplicationCreatedEvent = IApplicationCreated # b /c -class IResponse(Interface): # not an API +class IResponse(Interface): status = Attribute('WSGI status code of response') headerlist = Attribute('List of response headers') app_iter = Attribute('Iterable representing the response body') + def __call__(environ, start_response): + """ WSGI call interface, should call the start_response callback + and should return an iterable """ + class IException(Interface): # not an API """ An interface representing a generic exception """ @@ -60,8 +64,8 @@ class IExceptionResponse(IException, IResponse): to apply the registered view for all exception types raised by :app:`Pyramid` internally (any exception that inherits from :class:`pyramid.response.Response`, including - :class:`pyramid.response.HTTPNotFound` and - :class:`pyramid.response.HTTPForbidden`).""" + :class:`pyramid.httpexceptions.HTTPNotFound` and + :class:`pyramid.httpexceptions.HTTPForbidden`).""" class IBeforeRender(Interface): """ @@ -282,11 +286,7 @@ class IExceptionViewClassifier(Interface): class IView(Interface): def __call__(context, request): - """ Must return an object that implements IResponse. May - optionally raise ``pyramid.response.HTTPForbidden`` if an - authorization failure is detected during view execution or - ``pyramid.response.HTTPNotFound`` if the not found page is - meant to be returned.""" + """ Must return an object that implements IResponse. """ class ISecuredView(IView): """ *Internal only* interface. Not an API. """ diff --git a/pyramid/response.py b/pyramid/response.py index 6e6af32c8..1d2ef296f 100644 --- a/pyramid/response.py +++ b/pyramid/response.py @@ -1,947 +1,7 @@ -import types -from string import Template - from webob import Response as _Response -from webob import html_escape as _html_escape from zope.interface import implements -from zope.configuration.exceptions import ConfigurationError as ZCE - -from pyramid.interfaces import IExceptionResponse - -class Response(_Response, Exception): - implements(IExceptionResponse) - -def _no_escape(value): - if value is None: - return '' - if not isinstance(value, basestring): - if hasattr(value, '__unicode__'): - value = unicode(value) - else: - value = str(value) - return value - -class HTTPException(Exception): # bw compat - pass - -class WSGIHTTPException(Response, HTTPException): - implements(IExceptionResponse) - - ## You should set in subclasses: - # code = 200 - # title = 'OK' - # explanation = 'why this happens' - # body_template_obj = Template('response template') - - # differences from webob.exc.WSGIHTTPException: - # - not a WSGI application (just a response) - # - # as a result: - # - # - bases plaintext vs. html result on self.content_type rather than - # on request accept header - # - # - doesn't add request.environ keys to template substitutions unless - # 'request' is passed as a constructor keyword argument. - # - # - doesn't use "strip_tags" (${br} placeholder for
, no other html - # in default body template) - # - # - sets a default app_iter if no body, app_iter, or unicode_body is - # passed using a template (ala the replaced version's "generate_response") - # - # - explicitly sets self.message = detail to prevent whining by Python - # 2.6.5+ access of Exception.message - # - # - its base class of HTTPException is no longer a Python 2.4 compatibility - # shim; it's purely a base class that inherits from Exception. This - # implies that this class' ``exception`` property always returns - # ``self`` (only for bw compat at this point). - code = None - title = None - explanation = '' - body_template_obj = Template('''\ -${explanation}${br}${br} -${detail} -${html_comment} -''') - - plain_template_obj = Template('''\ -${status} - -${body}''') - - html_template_obj = Template('''\ - - - ${status} - - -

${status}

- ${body} - -''') - - ## Set this to True for responses that should have no request body - empty_body = False - - def __init__(self, detail=None, headers=None, comment=None, - body_template=None, **kw): - status = '%s %s' % (self.code, self.title) - Response.__init__(self, status=status, **kw) - Exception.__init__(self, detail) - self.detail = self.message = detail - if headers: - self.headers.extend(headers) - self.comment = comment - if body_template is not None: - self.body_template = body_template - self.body_template_obj = Template(body_template) - - if self.empty_body: - del self.content_type - del self.content_length - elif not ('unicode_body' in kw or 'body' in kw or 'app_iter' in kw): - self.app_iter = self._default_app_iter() - - def __str__(self): - return self.detail or self.explanation - - def _default_app_iter(self): - # This is a generator which defers the creation of the response page - # body; we use a generator because we want to ensure that if - # attributes of this response are changed after it is constructed, we - # use the changed values rather than the values at time of construction - # (e.g. self.content_type or self.charset). - html_comment = '' - comment = self.comment or '' - content_type = self.content_type or '' - if 'html' in content_type: - escape = _html_escape - page_template = self.html_template_obj - br = '
' - if comment: - html_comment = '' % escape(comment) - else: - escape = _no_escape - page_template = self.plain_template_obj - br = '\n' - if comment: - html_comment = escape(comment) - args = { - 'br':br, - 'explanation': escape(self.explanation), - 'detail': escape(self.detail or ''), - 'comment': escape(comment), - 'html_comment':html_comment, - } - body_tmpl = self.body_template_obj - if WSGIHTTPException.body_template_obj is not body_tmpl: - # Custom template; add headers to args - environ = self.environ - if environ is not None: - for k, v in environ.items(): - args[k] = escape(v) - for k, v in self.headers.items(): - args[k.lower()] = escape(v) - body = body_tmpl.substitute(args) - page = page_template.substitute(status=self.status, body=body) - if isinstance(page, unicode): - page = page.encode(self.charset) - yield page - raise StopIteration - - @property - def exception(self): - # bw compat only - return self - wsgi_response = exception # bw compat only - -class HTTPError(WSGIHTTPException): - """ - base class for status codes in the 400's and 500's - - This is an exception which indicates that an error has occurred, - and that any work in progress should not be committed. These are - typically results in the 400's and 500's. - """ - -class HTTPRedirection(WSGIHTTPException): - """ - base class for 300's status code (redirections) - - This is an abstract base class for 3xx redirection. It indicates - that further action needs to be taken by the user agent in order - to fulfill the request. It does not necessarly signal an error - condition. - """ - -class HTTPOk(WSGIHTTPException): - """ - Base class for the 200's status code (successful responses) - - code: 200, title: OK - """ - code = 200 - title = 'OK' - -############################################################ -## 2xx success -############################################################ - -class HTTPCreated(HTTPOk): - """ - subclass of :class:`~HTTPOk` - - This indicates that request has been fulfilled and resulted in a new - resource being created. - - code: 201, title: Created - """ - code = 201 - title = 'Created' - -class HTTPAccepted(HTTPOk): - """ - subclass of :class:`~HTTPOk` - - This indicates that the request has been accepted for processing, but the - processing has not been completed. - - code: 202, title: Accepted - """ - code = 202 - title = 'Accepted' - explanation = 'The request is accepted for processing.' - -class HTTPNonAuthoritativeInformation(HTTPOk): - """ - subclass of :class:`~HTTPOk` - - This indicates that the returned metainformation in the entity-header is - not the definitive set as available from the origin server, but is - gathered from a local or a third-party copy. - - code: 203, title: Non-Authoritative Information - """ - code = 203 - title = 'Non-Authoritative Information' - -class HTTPNoContent(HTTPOk): - """ - subclass of :class:`~HTTPOk` - - This indicates that the server has fulfilled the request but does - not need to return an entity-body, and might want to return updated - metainformation. - - code: 204, title: No Content - """ - code = 204 - title = 'No Content' - empty_body = True - -class HTTPResetContent(HTTPOk): - """ - subclass of :class:`~HTTPOk` - - This indicates that the the server has fulfilled the request and - the user agent SHOULD reset the document view which caused the - request to be sent. - - code: 205, title: Reset Content - """ - code = 205 - title = 'Reset Content' - empty_body = True - -class HTTPPartialContent(HTTPOk): - """ - subclass of :class:`~HTTPOk` - - This indicates that the server has fulfilled the partial GET - request for the resource. - - code: 206, title: Partial Content - """ - code = 206 - title = 'Partial Content' - -## FIXME: add 207 Multi-Status (but it's complicated) - -############################################################ -## 3xx redirection -############################################################ - -class _HTTPMove(HTTPRedirection): - """ - redirections which require a Location field - - Since a 'Location' header is a required attribute of 301, 302, 303, - 305 and 307 (but not 304), this base class provides the mechanics to - make this easy. - - You must provide a ``location`` keyword argument. - """ - # differences from webob.exc._HTTPMove: - # - # - not a wsgi app - # - # - ${location} isn't wrapped in an
tag in body - # - # - location keyword arg defaults to '' - # - # - ``add_slash`` argument is no longer accepted: code that passes - # add_slash argument to the constructor will receive an exception. - explanation = 'The resource has been moved to' - body_template_obj = Template('''\ -${explanation} ${location}; -you should be redirected automatically. -${detail} -${html_comment}''') - - def __init__(self, detail=None, headers=None, comment=None, - body_template=None, location='', **kw): - super(_HTTPMove, self).__init__( - detail=detail, headers=headers, comment=comment, - body_template=body_template, location=location, **kw) - -class HTTPMultipleChoices(_HTTPMove): - """ - subclass of :class:`~_HTTPMove` - - This indicates that the requested resource corresponds to any one - of a set of representations, each with its own specific location, - and agent-driven negotiation information is being provided so that - the user can select a preferred representation and redirect its - request to that location. - - code: 300, title: Multiple Choices - """ - code = 300 - title = 'Multiple Choices' - -class HTTPMovedPermanently(_HTTPMove): - """ - subclass of :class:`~_HTTPMove` - - This indicates that the requested resource has been assigned a new - permanent URI and any future references to this resource SHOULD use - one of the returned URIs. - - code: 301, title: Moved Permanently - """ - code = 301 - title = 'Moved Permanently' - -class HTTPFound(_HTTPMove): - """ - subclass of :class:`~_HTTPMove` - - This indicates that the requested resource resides temporarily under - a different URI. - - code: 302, title: Found - """ - code = 302 - title = 'Found' - explanation = 'The resource was found at' - -# This one is safe after a POST (the redirected location will be -# retrieved with GET): -class HTTPSeeOther(_HTTPMove): - """ - subclass of :class:`~_HTTPMove` - - This indicates that the response to the request can be found under - a different URI and SHOULD be retrieved using a GET method on that - resource. - - code: 303, title: See Other - """ - code = 303 - title = 'See Other' - -class HTTPNotModified(HTTPRedirection): - """ - subclass of :class:`~HTTPRedirection` - - This indicates that if the client has performed a conditional GET - request and access is allowed, but the document has not been - modified, the server SHOULD respond with this status code. - - code: 304, title: Not Modified - """ - # FIXME: this should include a date or etag header - code = 304 - title = 'Not Modified' - empty_body = True - -class HTTPUseProxy(_HTTPMove): - """ - subclass of :class:`~_HTTPMove` - - This indicates that the requested resource MUST be accessed through - the proxy given by the Location field. - - code: 305, title: Use Proxy - """ - # Not a move, but looks a little like one - code = 305 - title = 'Use Proxy' - explanation = ( - 'The resource must be accessed through a proxy located at') - -class HTTPTemporaryRedirect(_HTTPMove): - """ - subclass of :class:`~_HTTPMove` - - This indicates that the requested resource resides temporarily - under a different URI. - - code: 307, title: Temporary Redirect - """ - code = 307 - title = 'Temporary Redirect' - -############################################################ -## 4xx client error -############################################################ - -class HTTPClientError(HTTPError): - """ - base class for the 400's, where the client is in error - - This is an error condition in which the client is presumed to be - in-error. This is an expected problem, and thus is not considered - a bug. A server-side traceback is not warranted. Unless specialized, - this is a '400 Bad Request' - """ - code = 400 - title = 'Bad Request' - explanation = ('The server could not comply with the request since ' - 'it is either malformed or otherwise incorrect.') - -class HTTPBadRequest(HTTPClientError): - pass - -class HTTPUnauthorized(HTTPClientError): - """ - subclass of :class:`~HTTPClientError` - - This indicates that the request requires user authentication. - - code: 401, title: Unauthorized - """ - code = 401 - title = 'Unauthorized' - explanation = ( - 'This server could not verify that you are authorized to ' - 'access the document you requested. Either you supplied the ' - 'wrong credentials (e.g., bad password), or your browser ' - 'does not understand how to supply the credentials required.') - -class HTTPPaymentRequired(HTTPClientError): - """ - subclass of :class:`~HTTPClientError` - - code: 402, title: Payment Required - """ - code = 402 - title = 'Payment Required' - explanation = ('Access was denied for financial reasons.') - -class HTTPForbidden(HTTPClientError): - """ - subclass of :class:`~HTTPClientError` - - This indicates that the server understood the request, but is - refusing to fulfill it. - - code: 403, title: Forbidden - - Raise this exception within :term:`view` code to immediately return the - :term:`forbidden view` to the invoking user. Usually this is a basic - ``403`` page, but the forbidden view can be customized as necessary. See - :ref:`changing_the_forbidden_view`. A ``Forbidden`` exception will be - the ``context`` of a :term:`Forbidden View`. - - This exception's constructor treats two arguments specially. The first - argument, ``detail``, should be a string. The value of this string will - be used as the ``message`` attribute of the exception object. The second - special keyword argument, ``result`` is usually an instance of - :class:`pyramid.security.Denied` or :class:`pyramid.security.ACLDenied` - each of which indicates a reason for the forbidden error. However, - ``result`` is also permitted to be just a plain boolean ``False`` object - or ``None``. The ``result`` value will be used as the ``result`` - attribute of the exception object. It defaults to ``None``. - - The :term:`Forbidden View` can use the attributes of a Forbidden - exception as necessary to provide extended information in an error - report shown to a user. - """ - # differences from webob.exc.HTTPForbidden: - # - # - accepts a ``result`` keyword argument - # - # - overrides constructor to set ``self.result`` - # - # differences from older ``pyramid.exceptions.Forbidden``: - # - # - ``result`` must be passed as a keyword argument. - # - code = 403 - title = 'Forbidden' - explanation = ('Access was denied to this resource.') - def __init__(self, detail=None, headers=None, comment=None, - body_template=None, result=None, **kw): - HTTPClientError.__init__(self, detail=detail, headers=headers, - comment=comment, body_template=body_template, - **kw) - self.result = result - -class HTTPNotFound(HTTPClientError): - """ - subclass of :class:`~HTTPClientError` - - This indicates that the server did not find anything matching the - Request-URI. - - code: 404, title: Not Found +from pyramid.interfaces import IResponse - Raise this exception within :term:`view` code to immediately - return the :term:`Not Found view` to the invoking user. Usually - this is a basic ``404`` page, but the Not Found view can be - customized as necessary. See :ref:`changing_the_notfound_view`. - - This exception's constructor accepts a ``detail`` argument - (the first argument), which should be a string. The value of this - string will be available as the ``message`` attribute of this exception, - for availability to the :term:`Not Found View`. - """ - code = 404 - title = 'Not Found' - explanation = ('The resource could not be found.') - -class HTTPMethodNotAllowed(HTTPClientError): - """ - subclass of :class:`~HTTPClientError` - - This indicates that the method specified in the Request-Line is - not allowed for the resource identified by the Request-URI. - - code: 405, title: Method Not Allowed - """ - # differences from webob.exc.HTTPMethodNotAllowed: - # - # - body_template_obj not overridden (it tried to use request environ's - # REQUEST_METHOD) - code = 405 - title = 'Method Not Allowed' - -class HTTPNotAcceptable(HTTPClientError): - """ - subclass of :class:`~HTTPClientError` - - This indicates the resource identified by the request is only - capable of generating response entities which have content - characteristics not acceptable according to the accept headers - sent in the request. - - code: 406, title: Not Acceptable - """ - # differences from webob.exc.HTTPNotAcceptable: - # - # - body_template_obj not overridden (it tried to use request environ's - # HTTP_ACCEPT) - code = 406 - title = 'Not Acceptable' - -class HTTPProxyAuthenticationRequired(HTTPClientError): - """ - subclass of :class:`~HTTPClientError` - - This is similar to 401, but indicates that the client must first - authenticate itself with the proxy. - - code: 407, title: Proxy Authentication Required - """ - code = 407 - title = 'Proxy Authentication Required' - explanation = ('Authentication with a local proxy is needed.') - -class HTTPRequestTimeout(HTTPClientError): - """ - subclass of :class:`~HTTPClientError` - - This indicates that the client did not produce a request within - the time that the server was prepared to wait. - - code: 408, title: Request Timeout - """ - code = 408 - title = 'Request Timeout' - explanation = ('The server has waited too long for the request to ' - 'be sent by the client.') - -class HTTPConflict(HTTPClientError): - """ - subclass of :class:`~HTTPClientError` - - This indicates that the request could not be completed due to a - conflict with the current state of the resource. - - code: 409, title: Conflict - """ - code = 409 - title = 'Conflict' - explanation = ('There was a conflict when trying to complete ' - 'your request.') - -class HTTPGone(HTTPClientError): - """ - subclass of :class:`~HTTPClientError` - - This indicates that the requested resource is no longer available - at the server and no forwarding address is known. - - code: 410, title: Gone - """ - code = 410 - title = 'Gone' - explanation = ('This resource is no longer available. No forwarding ' - 'address is given.') - -class HTTPLengthRequired(HTTPClientError): - """ - subclass of :class:`~HTTPClientError` - - This indicates that the the server refuses to accept the request - without a defined Content-Length. - - code: 411, title: Length Required - """ - code = 411 - title = 'Length Required' - explanation = ('Content-Length header required.') - -class HTTPPreconditionFailed(HTTPClientError): - """ - subclass of :class:`~HTTPClientError` - - This indicates that the precondition given in one or more of the - request-header fields evaluated to false when it was tested on the - server. - - code: 412, title: Precondition Failed - """ - code = 412 - title = 'Precondition Failed' - explanation = ('Request precondition failed.') - -class HTTPRequestEntityTooLarge(HTTPClientError): - """ - subclass of :class:`~HTTPClientError` - - This indicates that the server is refusing to process a request - because the request entity is larger than the server is willing or - able to process. - - code: 413, title: Request Entity Too Large - """ - code = 413 - title = 'Request Entity Too Large' - explanation = ('The body of your request was too large for this server.') - -class HTTPRequestURITooLong(HTTPClientError): - """ - subclass of :class:`~HTTPClientError` - - This indicates that the server is refusing to service the request - because the Request-URI is longer than the server is willing to - interpret. - - code: 414, title: Request-URI Too Long - """ - code = 414 - title = 'Request-URI Too Long' - explanation = ('The request URI was too long for this server.') - -class HTTPUnsupportedMediaType(HTTPClientError): - """ - subclass of :class:`~HTTPClientError` - - This indicates that the server is refusing to service the request - because the entity of the request is in a format not supported by - the requested resource for the requested method. +class Response(_Response): + implements(IResponse) - code: 415, title: Unsupported Media Type - """ - # differences from webob.exc.HTTPUnsupportedMediaType: - # - # - body_template_obj not overridden (it tried to use request environ's - # CONTENT_TYPE) - code = 415 - title = 'Unsupported Media Type' - -class HTTPRequestRangeNotSatisfiable(HTTPClientError): - """ - subclass of :class:`~HTTPClientError` - - The server SHOULD return a response with this status code if a - request included a Range request-header field, and none of the - range-specifier values in this field overlap the current extent - of the selected resource, and the request did not include an - If-Range request-header field. - - code: 416, title: Request Range Not Satisfiable - """ - code = 416 - title = 'Request Range Not Satisfiable' - explanation = ('The Range requested is not available.') - -class HTTPExpectationFailed(HTTPClientError): - """ - subclass of :class:`~HTTPClientError` - - This indidcates that the expectation given in an Expect - request-header field could not be met by this server. - - code: 417, title: Expectation Failed - """ - code = 417 - title = 'Expectation Failed' - explanation = ('Expectation failed.') - -class HTTPUnprocessableEntity(HTTPClientError): - """ - subclass of :class:`~HTTPClientError` - - This indicates that the server is unable to process the contained - instructions. Only for WebDAV. - - code: 422, title: Unprocessable Entity - """ - ## Note: from WebDAV - code = 422 - title = 'Unprocessable Entity' - explanation = 'Unable to process the contained instructions' - -class HTTPLocked(HTTPClientError): - """ - subclass of :class:`~HTTPClientError` - - This indicates that the resource is locked. Only for WebDAV - - code: 423, title: Locked - """ - ## Note: from WebDAV - code = 423 - title = 'Locked' - explanation = ('The resource is locked') - -class HTTPFailedDependency(HTTPClientError): - """ - subclass of :class:`~HTTPClientError` - - This indicates that the method could not be performed because the - requested action depended on another action and that action failed. - Only for WebDAV. - - code: 424, title: Failed Dependency - """ - ## Note: from WebDAV - code = 424 - title = 'Failed Dependency' - explanation = ( - 'The method could not be performed because the requested ' - 'action dependended on another action and that action failed') - -############################################################ -## 5xx Server Error -############################################################ -# Response status codes beginning with the digit "5" indicate cases in -# which the server is aware that it has erred or is incapable of -# performing the request. Except when responding to a HEAD request, the -# server SHOULD include an entity containing an explanation of the error -# situation, and whether it is a temporary or permanent condition. User -# agents SHOULD display any included entity to the user. These response -# codes are applicable to any request method. - -class HTTPServerError(HTTPError): - """ - base class for the 500's, where the server is in-error - - This is an error condition in which the server is presumed to be - in-error. This is usually unexpected, and thus requires a traceback; - ideally, opening a support ticket for the customer. Unless specialized, - this is a '500 Internal Server Error' - """ - code = 500 - title = 'Internal Server Error' - explanation = ( - 'The server has either erred or is incapable of performing ' - 'the requested operation.') - -class HTTPInternalServerError(HTTPServerError): - pass - -class HTTPNotImplemented(HTTPServerError): - """ - subclass of :class:`~HTTPServerError` - - This indicates that the server does not support the functionality - required to fulfill the request. - - code: 501, title: Not Implemented - """ - # differences from webob.exc.HTTPNotAcceptable: - # - # - body_template_obj not overridden (it tried to use request environ's - # REQUEST_METHOD) - code = 501 - title = 'Not Implemented' - -class HTTPBadGateway(HTTPServerError): - """ - subclass of :class:`~HTTPServerError` - - This indicates that the server, while acting as a gateway or proxy, - received an invalid response from the upstream server it accessed - in attempting to fulfill the request. - - code: 502, title: Bad Gateway - """ - code = 502 - title = 'Bad Gateway' - explanation = ('Bad gateway.') - -class HTTPServiceUnavailable(HTTPServerError): - """ - subclass of :class:`~HTTPServerError` - - This indicates that the server is currently unable to handle the - request due to a temporary overloading or maintenance of the server. - - code: 503, title: Service Unavailable - """ - code = 503 - title = 'Service Unavailable' - explanation = ('The server is currently unavailable. ' - 'Please try again at a later time.') - -class HTTPGatewayTimeout(HTTPServerError): - """ - subclass of :class:`~HTTPServerError` - - This indicates that the server, while acting as a gateway or proxy, - did not receive a timely response from the upstream server specified - by the URI (e.g. HTTP, FTP, LDAP) or some other auxiliary server - (e.g. DNS) it needed to access in attempting to complete the request. - - code: 504, title: Gateway Timeout - """ - code = 504 - title = 'Gateway Timeout' - explanation = ('The gateway has timed out.') - -class HTTPVersionNotSupported(HTTPServerError): - """ - subclass of :class:`~HTTPServerError` - - This indicates that the server does not support, or refuses to - support, the HTTP protocol version that was used in the request - message. - - code: 505, title: HTTP Version Not Supported - """ - code = 505 - title = 'HTTP Version Not Supported' - explanation = ('The HTTP version is not supported.') - -class HTTPInsufficientStorage(HTTPServerError): - """ - subclass of :class:`~HTTPServerError` - - This indicates that the server does not have enough space to save - the resource. - - code: 507, title: Insufficient Storage - """ - code = 507 - title = 'Insufficient Storage' - explanation = ('There was not enough space to save the resource') - -NotFound = HTTPNotFound # bw compat -Forbidden = HTTPForbidden # bw compat - -class PredicateMismatch(NotFound): - """ - Internal exception (not an API) raised by multiviews when no - view matches. This exception subclasses the ``NotFound`` - exception only one reason: if it reaches the main exception - handler, it should be treated like a ``NotFound`` by any exception - view registrations. - """ - -class URLDecodeError(UnicodeDecodeError): - """ - This exception is raised when :app:`Pyramid` cannot - successfully decode a URL or a URL path segment. This exception - it behaves just like the Python builtin - :exc:`UnicodeDecodeError`. It is a subclass of the builtin - :exc:`UnicodeDecodeError` exception only for identity purposes, - mostly so an exception view can be registered when a URL cannot be - decoded. - """ - -class ConfigurationError(ZCE): - """ Raised when inappropriate input values are supplied to an API - method of a :term:`Configurator`""" - - -def abort(status_code, **kw): - """Aborts the request immediately by raising an HTTP exception based on a - status code. Example:: - - abort(404) # raises an HTTPNotFound exception. - - The values passed as ``kw`` are provided to the exception's constructor. - """ - exc = status_map[status_code](**kw) - raise exc - - -def redirect(url, code=302, **kw): - """Raises an :class:`~HTTPFound` (302) redirect exception to the - URL specified by ``url``. - - Optionally, a code variable may be passed with the status code of - the redirect, ie:: - - redirect(route_url('foo', request), code=303) - - The values passed as ``kw`` are provided to the exception constructor. - - """ - exc = status_map[code] - raise exc(location=url, **kw) - -def default_exceptionresponse_view(context, request): - if not isinstance(context, Exception): - # backwards compat for an exception response view registered via - # config.set_notfound_view or config.set_forbidden_view - # instead of as a proper exception view - context = request.exception or context - return context - -status_map={} -for name, value in globals().items(): - if (isinstance(value, (type, types.ClassType)) and - issubclass(value, HTTPException) - and not name.startswith('_')): - code = getattr(value, 'code', None) - if code: - status_map[code] = value -del name, value - diff --git a/pyramid/router.py b/pyramid/router.py index 92c6cc920..4d2750efb 100644 --- a/pyramid/router.py +++ b/pyramid/router.py @@ -1,3 +1,5 @@ +import warnings + from zope.interface import implements from zope.interface import providedBy @@ -17,7 +19,7 @@ from pyramid.interfaces import IResponder from pyramid.events import ContextFound from pyramid.events import NewRequest from pyramid.events import NewResponse -from pyramid.response import HTTPNotFound +from pyramid.httpexceptions import HTTPNotFound from pyramid.request import Request from pyramid.threadlocal import manager from pyramid.traversal import DefaultRootFactory @@ -203,6 +205,11 @@ class Router(object): def default_responder(response): def inner(request, start_response): + # __call__ is default 1.1 response API + call = getattr(response, '__call__', None) + if call is not None: + return call(request.environ, start_response) + # start 1.0 bw compat (use headerlist, app_iter, status) try: headers = response.headerlist app_iter = response.app_iter @@ -212,6 +219,14 @@ def default_responder(response): 'Non-response object returned from view ' '(and no renderer): %r' % (response)) start_response(status, headers) + warnings.warn( + 'As of Pyramid 1.1, an object used as a response object is ' + 'required to have a "__call__" method if an IResponder adapter is ' + 'not registered for its type. See "Deprecations" in "What\'s New ' + 'in Pyramid 1.1" within the general Pyramid documentation for ' + 'further details.', + DeprecationWarning, + 3) return app_iter return inner diff --git a/pyramid/testing.py b/pyramid/testing.py index 4d7dd252a..86276df1e 100644 --- a/pyramid/testing.py +++ b/pyramid/testing.py @@ -17,7 +17,7 @@ from pyramid.interfaces import ISession from pyramid.config import Configurator from pyramid.decorator import reify -from pyramid.response import HTTPForbidden +from pyramid.httpexceptions import HTTPForbidden from pyramid.response import Response from pyramid.registry import Registry from pyramid.security import Authenticated diff --git a/pyramid/tests/fixtureapp/views.py b/pyramid/tests/fixtureapp/views.py index 3125c972f..cbfc5a574 100644 --- a/pyramid/tests/fixtureapp/views.py +++ b/pyramid/tests/fixtureapp/views.py @@ -1,6 +1,6 @@ from zope.interface import Interface from webob import Response -from pyramid.response import HTTPForbidden +from pyramid.httpexceptions import HTTPForbidden def fixture_view(context, request): """ """ diff --git a/pyramid/tests/forbiddenapp/__init__.py b/pyramid/tests/forbiddenapp/__init__.py index 9ad2dc801..7001b87f5 100644 --- a/pyramid/tests/forbiddenapp/__init__.py +++ b/pyramid/tests/forbiddenapp/__init__.py @@ -1,5 +1,5 @@ from webob import Response -from pyramid.response import HTTPForbidden +from pyramid.httpexceptions import HTTPForbidden def x_view(request): # pragma: no cover return Response('this is private!') diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py index 6817c5936..703a2577c 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -50,7 +50,7 @@ class ConfiguratorTests(unittest.TestCase): return iface def _assertNotFound(self, wrapper, *arg): - from pyramid.response import HTTPNotFound + from pyramid.httpexceptions import HTTPNotFound self.assertRaises(HTTPNotFound, wrapper, *arg) def _registerEventListener(self, config, event_iface=None): @@ -205,7 +205,7 @@ class ConfiguratorTests(unittest.TestCase): def test_ctor_httpexception_view_default(self): from pyramid.interfaces import IExceptionResponse - from pyramid.response import default_exceptionresponse_view + from pyramid.httpexceptions import default_exceptionresponse_view from pyramid.interfaces import IRequest config = self._makeOne() view = self._getViewCallable(config, @@ -321,7 +321,7 @@ class ConfiguratorTests(unittest.TestCase): def test_setup_registry_explicit_notfound_trumps_iexceptionresponse(self): from zope.interface import implementedBy from pyramid.interfaces import IRequest - from pyramid.response import HTTPNotFound + from pyramid.httpexceptions import HTTPNotFound from pyramid.registry import Registry reg = Registry() config = self._makeOne(reg, autocommit=True) @@ -1695,7 +1695,7 @@ class ConfiguratorTests(unittest.TestCase): self._assertNotFound(wrapper, None, request) def test_add_view_with_header_val_missing(self): - from pyramid.response import HTTPNotFound + from pyramid.httpexceptions import HTTPNotFound view = lambda *arg: 'OK' config = self._makeOne(autocommit=True) config.add_view(view=view, header=r'Host:\d') @@ -2229,7 +2229,7 @@ class ConfiguratorTests(unittest.TestCase): def test_set_notfound_view(self): from zope.interface import implementedBy from pyramid.interfaces import IRequest - from pyramid.response import HTTPNotFound + from pyramid.httpexceptions import HTTPNotFound config = self._makeOne(autocommit=True) view = lambda *arg: arg config.set_notfound_view(view) @@ -2243,7 +2243,7 @@ class ConfiguratorTests(unittest.TestCase): def test_set_notfound_view_request_has_context(self): from zope.interface import implementedBy from pyramid.interfaces import IRequest - from pyramid.response import HTTPNotFound + from pyramid.httpexceptions import HTTPNotFound config = self._makeOne(autocommit=True) view = lambda *arg: arg config.set_notfound_view(view) @@ -2259,7 +2259,7 @@ class ConfiguratorTests(unittest.TestCase): def test_set_notfound_view_with_renderer(self): from zope.interface import implementedBy from pyramid.interfaces import IRequest - from pyramid.response import HTTPNotFound + from pyramid.httpexceptions import HTTPNotFound config = self._makeOne(autocommit=True) view = lambda *arg: {} config.set_notfound_view(view, @@ -2278,12 +2278,13 @@ class ConfiguratorTests(unittest.TestCase): def test_set_forbidden_view(self): from zope.interface import implementedBy from pyramid.interfaces import IRequest - from pyramid.response import Forbidden + from pyramid.httpexceptions import HTTPForbidden config = self._makeOne(autocommit=True) view = lambda *arg: 'OK' config.set_forbidden_view(view) request = self._makeRequest(config) - view = self._getViewCallable(config, ctx_iface=implementedBy(Forbidden), + view = self._getViewCallable(config, + ctx_iface=implementedBy(HTTPForbidden), request_iface=IRequest) result = view(None, request) self.assertEqual(result, 'OK') @@ -2291,13 +2292,14 @@ class ConfiguratorTests(unittest.TestCase): def test_set_forbidden_view_request_has_context(self): from zope.interface import implementedBy from pyramid.interfaces import IRequest - from pyramid.response import Forbidden + from pyramid.httpexceptions import HTTPForbidden config = self._makeOne(autocommit=True) view = lambda *arg: arg config.set_forbidden_view(view) request = self._makeRequest(config) request.context = 'abc' - view = self._getViewCallable(config, ctx_iface=implementedBy(Forbidden), + view = self._getViewCallable(config, + ctx_iface=implementedBy(HTTPForbidden), request_iface=IRequest) result = view(None, request) self.assertEqual(result, ('abc', request)) @@ -2306,7 +2308,7 @@ class ConfiguratorTests(unittest.TestCase): def test_set_forbidden_view_with_renderer(self): from zope.interface import implementedBy from pyramid.interfaces import IRequest - from pyramid.response import Forbidden + from pyramid.httpexceptions import HTTPForbidden config = self._makeOne(autocommit=True) view = lambda *arg: {} config.set_forbidden_view(view, @@ -2315,7 +2317,7 @@ class ConfiguratorTests(unittest.TestCase): try: # chameleon requires a threadlocal registry request = self._makeRequest(config) view = self._getViewCallable(config, - ctx_iface=implementedBy(Forbidden), + ctx_iface=implementedBy(HTTPForbidden), request_iface=IRequest) result = view(None, request) finally: @@ -3685,7 +3687,7 @@ class TestViewDeriver(unittest.TestCase): "None against context None): True") def test_debug_auth_permission_authpol_denied(self): - from pyramid.response import Forbidden + from pyramid.httpexceptions import HTTPForbidden view = lambda *arg: 'OK' self.config.registry.settings = dict( debug_authorization=True, reload_templates=True) @@ -3700,7 +3702,7 @@ class TestViewDeriver(unittest.TestCase): request = self._makeRequest() request.view_name = 'view_name' request.url = 'url' - self.assertRaises(Forbidden, result, None, request) + self.assertRaises(HTTPForbidden, result, None, request) self.assertEqual(len(logger.messages), 1) self.assertEqual(logger.messages[0], "debug_authorization of url url (view name " @@ -3813,7 +3815,7 @@ class TestViewDeriver(unittest.TestCase): self.assertEqual(predicates, [True, True]) def test_with_predicates_notall(self): - from pyramid.response import HTTPNotFound + from pyramid.httpexceptions import HTTPNotFound view = lambda *arg: 'OK' predicates = [] def predicate1(context, request): @@ -4621,14 +4623,14 @@ class TestMultiView(unittest.TestCase): self.assertEqual(mv.get_views(request), mv.views) def test_match_not_found(self): - from pyramid.response import HTTPNotFound + from pyramid.httpexceptions import HTTPNotFound mv = self._makeOne() context = DummyContext() request = DummyRequest() self.assertRaises(HTTPNotFound, mv.match, context, request) def test_match_predicate_fails(self): - from pyramid.response import HTTPNotFound + from pyramid.httpexceptions import HTTPNotFound mv = self._makeOne() def view(context, request): """ """ @@ -4650,7 +4652,7 @@ class TestMultiView(unittest.TestCase): self.assertEqual(result, view) def test_permitted_no_views(self): - from pyramid.response import HTTPNotFound + from pyramid.httpexceptions import HTTPNotFound mv = self._makeOne() context = DummyContext() request = DummyRequest() @@ -4677,7 +4679,7 @@ class TestMultiView(unittest.TestCase): self.assertEqual(result, False) def test__call__not_found(self): - from pyramid.response import HTTPNotFound + from pyramid.httpexceptions import HTTPNotFound mv = self._makeOne() context = DummyContext() request = DummyRequest() @@ -4699,7 +4701,7 @@ class TestMultiView(unittest.TestCase): self.assertEqual(response, expected_response) def test___call__raise_not_found_isnt_interpreted_as_pred_mismatch(self): - from pyramid.response import HTTPNotFound + from pyramid.httpexceptions import HTTPNotFound mv = self._makeOne() context = DummyContext() request = DummyRequest() @@ -4724,7 +4726,7 @@ class TestMultiView(unittest.TestCase): self.assertEqual(response, expected_response) def test__call_permissive__not_found(self): - from pyramid.response import HTTPNotFound + from pyramid.httpexceptions import HTTPNotFound mv = self._makeOne() context = DummyContext() request = DummyRequest() diff --git a/pyramid/tests/test_exceptions.py b/pyramid/tests/test_exceptions.py index 673fb6712..50182ee5c 100644 --- a/pyramid/tests/test_exceptions.py +++ b/pyramid/tests/test_exceptions.py @@ -1,5 +1,16 @@ import unittest +class TestBWCompat(unittest.TestCase): + def test_bwcompat_notfound(self): + from pyramid.exceptions import NotFound as one + from pyramid.httpexceptions import HTTPNotFound as two + self.assertTrue(one is two) + + def test_bwcompat_forbidden(self): + from pyramid.exceptions import Forbidden as one + from pyramid.httpexceptions import HTTPForbidden as two + self.assertTrue(one is two) + class TestNotFound(unittest.TestCase): def _makeOne(self, message): from pyramid.exceptions import NotFound @@ -14,7 +25,7 @@ class TestNotFound(unittest.TestCase): def test_response_equivalence(self): from pyramid.exceptions import NotFound - from pyramid.response import HTTPNotFound + from pyramid.httpexceptions import HTTPNotFound self.assertTrue(NotFound is HTTPNotFound) class TestForbidden(unittest.TestCase): @@ -31,6 +42,6 @@ class TestForbidden(unittest.TestCase): def test_response_equivalence(self): from pyramid.exceptions import Forbidden - from pyramid.response import HTTPForbidden + from pyramid.httpexceptions import HTTPForbidden self.assertTrue(Forbidden is HTTPForbidden) diff --git a/pyramid/tests/test_httpexceptions.py b/pyramid/tests/test_httpexceptions.py index 28adc9d3d..629bbe225 100644 --- a/pyramid/tests/test_httpexceptions.py +++ b/pyramid/tests/test_httpexceptions.py @@ -1,9 +1,277 @@ import unittest -class TestIt(unittest.TestCase): - def test_bwcompat_imports(self): - from pyramid.httpexceptions import HTTPNotFound as one - from pyramid.response import HTTPNotFound as two - self.assertTrue(one is two) +class Test_responsecode(unittest.TestCase): + def _callFUT(self, *arg, **kw): + from pyramid.httpexceptions import responsecode + return responsecode(*arg, **kw) + + def test_status_404(self): + from pyramid.httpexceptions import HTTPNotFound + self.assertEqual(self._callFUT(404).__class__, HTTPNotFound) + + def test_status_201(self): + from pyramid.httpexceptions import HTTPCreated + self.assertEqual(self._callFUT(201).__class__, HTTPCreated) + + def test_extra_kw(self): + resp = self._callFUT(404, headers=[('abc', 'def')]) + self.assertEqual(resp.headers['abc'], 'def') + +class Test_default_exceptionresponse_view(unittest.TestCase): + def _callFUT(self, context, request): + from pyramid.httpexceptions import default_exceptionresponse_view + return default_exceptionresponse_view(context, request) + + def test_call_with_exception(self): + context = Exception() + result = self._callFUT(context, None) + self.assertEqual(result, context) + + def test_call_with_nonexception(self): + request = DummyRequest() + context = Exception() + request.exception = context + result = self._callFUT(None, request) + self.assertEqual(result, context) + +class Test__no_escape(unittest.TestCase): + def _callFUT(self, val): + from pyramid.httpexceptions import _no_escape + return _no_escape(val) + + def test_null(self): + self.assertEqual(self._callFUT(None), '') + + def test_not_basestring(self): + self.assertEqual(self._callFUT(42), '42') + + def test_unicode(self): + class DummyUnicodeObject(object): + def __unicode__(self): + return u'42' + duo = DummyUnicodeObject() + self.assertEqual(self._callFUT(duo), u'42') + +class TestWSGIHTTPException(unittest.TestCase): + def _getTargetClass(self): + from pyramid.httpexceptions import WSGIHTTPException + return WSGIHTTPException + + def _getTargetSubclass(self, code='200', title='OK', + explanation='explanation', empty_body=False): + cls = self._getTargetClass() + class Subclass(cls): + pass + Subclass.empty_body = empty_body + Subclass.code = code + Subclass.title = title + Subclass.explanation = explanation + return Subclass + + def _makeOne(self, *arg, **kw): + cls = self._getTargetClass() + return cls(*arg, **kw) + + def test_implements_IResponse(self): + from pyramid.interfaces import IResponse + cls = self._getTargetClass() + self.failUnless(IResponse.implementedBy(cls)) + + def test_provides_IResponse(self): + from pyramid.interfaces import IResponse + inst = self._getTargetClass()() + self.failUnless(IResponse.providedBy(inst)) + + def test_implements_IExceptionResponse(self): + from pyramid.interfaces import IExceptionResponse + cls = self._getTargetClass() + self.failUnless(IExceptionResponse.implementedBy(cls)) + + def test_provides_IExceptionResponse(self): + from pyramid.interfaces import IExceptionResponse + inst = self._getTargetClass()() + self.failUnless(IExceptionResponse.providedBy(inst)) + + def test_ctor_sets_detail(self): + exc = self._makeOne('message') + self.assertEqual(exc.detail, 'message') + + def test_ctor_sets_comment(self): + exc = self._makeOne(comment='comment') + self.assertEqual(exc.comment, 'comment') + + def test_ctor_calls_Exception_ctor(self): + exc = self._makeOne('message') + self.assertEqual(exc.message, 'message') + + def test_ctor_calls_Response_ctor(self): + exc = self._makeOne('message') + self.assertEqual(exc.status, 'None None') + + def test_ctor_extends_headers(self): + exc = self._makeOne(headers=[('X-Foo', 'foo')]) + self.assertEqual(exc.headers.get('X-Foo'), 'foo') + + def test_ctor_sets_body_template_obj(self): + exc = self._makeOne(body_template='${foo}') + self.assertEqual( + exc.body_template_obj.substitute({'foo':'foo'}), 'foo') + + def test_ctor_with_empty_body(self): + cls = self._getTargetSubclass(empty_body=True) + exc = cls() + self.assertEqual(exc.content_type, None) + self.assertEqual(exc.content_length, None) + + def test_ctor_with_body_doesnt_set_default_app_iter(self): + exc = self._makeOne(body='123') + self.assertEqual(exc.app_iter, ['123']) + + def test_ctor_with_unicode_body_doesnt_set_default_app_iter(self): + exc = self._makeOne(unicode_body=u'123') + self.assertEqual(exc.app_iter, ['123']) + + def test_ctor_with_app_iter_doesnt_set_default_app_iter(self): + exc = self._makeOne(app_iter=['123']) + self.assertEqual(exc.app_iter, ['123']) + + def test_ctor_with_body_sets_default_app_iter_html(self): + cls = self._getTargetSubclass() + exc = cls('detail') + body = list(exc.app_iter)[0] + self.assertTrue(body.startswith('' in body) + + def test_custom_body_template_no_environ(self): + cls = self._getTargetSubclass() + exc = cls(body_template='${location}', location='foo') + exc.content_type = 'text/plain' + body = list(exc._default_app_iter())[0] + self.assertEqual(body, '200 OK\n\nfoo') + + def test_custom_body_template_with_environ(self): + cls = self._getTargetSubclass() + from pyramid.request import Request + request = Request.blank('/') + exc = cls(body_template='${REQUEST_METHOD}', request=request) + exc.content_type = 'text/plain' + body = list(exc._default_app_iter())[0] + self.assertEqual(body, '200 OK\n\nGET') + + def test_body_template_unicode(self): + from pyramid.request import Request + cls = self._getTargetSubclass() + la = unicode('/La Pe\xc3\xb1a', 'utf-8') + request = Request.blank('/') + request.environ['unicodeval'] = la + exc = cls(body_template='${unicodeval}', request=request) + exc.content_type = 'text/plain' + body = list(exc._default_app_iter())[0] + self.assertEqual(body, '200 OK\n\n/La Pe\xc3\xb1a') + +class TestRenderAllExceptionsWithoutArguments(unittest.TestCase): + def _doit(self, content_type): + from pyramid.httpexceptions import status_map + L = [] + self.assertTrue(status_map) + for v in status_map.values(): + exc = v() + exc.content_type = content_type + result = list(exc.app_iter)[0] + if exc.empty_body: + self.assertEqual(result, '') + else: + self.assertTrue(exc.status in result) + L.append(result) + self.assertEqual(len(L), len(status_map)) + + def test_it_plain(self): + self._doit('text/plain') + + def test_it_html(self): + self._doit('text/html') + +class Test_HTTPMove(unittest.TestCase): + def _makeOne(self, *arg, **kw): + from pyramid.httpexceptions import _HTTPMove + return _HTTPMove(*arg, **kw) + + def test_it_location_not_passed(self): + exc = self._makeOne() + self.assertEqual(exc.location, '') + + def test_it_location_passed(self): + exc = self._makeOne(location='foo') + self.assertEqual(exc.location, 'foo') + +class TestHTTPForbidden(unittest.TestCase): + def _makeOne(self, *arg, **kw): + from pyramid.httpexceptions import HTTPForbidden + return HTTPForbidden(*arg, **kw) + + def test_it_result_not_passed(self): + exc = self._makeOne() + self.assertEqual(exc.result, None) + + def test_it_result_passed(self): + exc = self._makeOne(result='foo') + self.assertEqual(exc.result, 'foo') +class DummyRequest(object): + exception = None diff --git a/pyramid/tests/test_response.py b/pyramid/tests/test_response.py index 6cc87fc0a..46eb298d1 100644 --- a/pyramid/tests/test_response.py +++ b/pyramid/tests/test_response.py @@ -5,304 +5,13 @@ class TestResponse(unittest.TestCase): from pyramid.response import Response return Response - def test_implements_IExceptionResponse(self): - from pyramid.interfaces import IExceptionResponse - Response = self._getTargetClass() - self.failUnless(IExceptionResponse.implementedBy(Response)) - - def test_provides_IExceptionResponse(self): - from pyramid.interfaces import IExceptionResponse - response = self._getTargetClass()() - self.failUnless(IExceptionResponse.providedBy(response)) - -class Test_abort(unittest.TestCase): - def _callFUT(self, *arg, **kw): - from pyramid.response import abort - return abort(*arg, **kw) - - def test_status_404(self): - from pyramid.response import HTTPNotFound - self.assertRaises(HTTPNotFound, self._callFUT, 404) - - def test_status_201(self): - from pyramid.response import HTTPCreated - self.assertRaises(HTTPCreated, self._callFUT, 201) - - def test_extra_kw(self): - from pyramid.response import HTTPNotFound - try: - self._callFUT(404, headers=[('abc', 'def')]) - except HTTPNotFound, exc: - self.assertEqual(exc.headers['abc'], 'def') - else: # pragma: no cover - raise AssertionError - -class Test_redirect(unittest.TestCase): - def _callFUT(self, *arg, **kw): - from pyramid.response import redirect - return redirect(*arg, **kw) - - def test_default(self): - from pyramid.response import HTTPFound - try: - self._callFUT('http://example.com') - except HTTPFound, exc: - self.assertEqual(exc.location, 'http://example.com') - self.assertEqual(exc.status, '302 Found') - - def test_custom_code(self): - from pyramid.response import HTTPMovedPermanently - try: - self._callFUT('http://example.com', 301) - except HTTPMovedPermanently, exc: - self.assertEqual(exc.location, 'http://example.com') - self.assertEqual(exc.status, '301 Moved Permanently') - - def test_extra_kw(self): - from pyramid.response import HTTPFound - try: - self._callFUT('http://example.com', headers=[('abc', 'def')]) - except HTTPFound, exc: - self.assertEqual(exc.location, 'http://example.com') - self.assertEqual(exc.status, '302 Found') - self.assertEqual(exc.headers['abc'], 'def') - - -class Test_default_exceptionresponse_view(unittest.TestCase): - def _callFUT(self, context, request): - from pyramid.response import default_exceptionresponse_view - return default_exceptionresponse_view(context, request) - - def test_call_with_exception(self): - context = Exception() - result = self._callFUT(context, None) - self.assertEqual(result, context) - - def test_call_with_nonexception(self): - request = DummyRequest() - context = Exception() - request.exception = context - result = self._callFUT(None, request) - self.assertEqual(result, context) - -class Test__no_escape(unittest.TestCase): - def _callFUT(self, val): - from pyramid.response import _no_escape - return _no_escape(val) - - def test_null(self): - self.assertEqual(self._callFUT(None), '') - - def test_not_basestring(self): - self.assertEqual(self._callFUT(42), '42') - - def test_unicode(self): - class DummyUnicodeObject(object): - def __unicode__(self): - return u'42' - duo = DummyUnicodeObject() - self.assertEqual(self._callFUT(duo), u'42') - -class TestWSGIHTTPException(unittest.TestCase): - def _getTargetClass(self): - from pyramid.response import WSGIHTTPException - return WSGIHTTPException - - def _getTargetSubclass(self, code='200', title='OK', - explanation='explanation', empty_body=False): + def test_implements_IResponse(self): + from pyramid.interfaces import IResponse cls = self._getTargetClass() - class Subclass(cls): - pass - Subclass.empty_body = empty_body - Subclass.code = code - Subclass.title = title - Subclass.explanation = explanation - return Subclass - - def _makeOne(self, *arg, **kw): - cls = self._getTargetClass() - return cls(*arg, **kw) - - def test_ctor_sets_detail(self): - exc = self._makeOne('message') - self.assertEqual(exc.detail, 'message') - - def test_ctor_sets_comment(self): - exc = self._makeOne(comment='comment') - self.assertEqual(exc.comment, 'comment') - - def test_ctor_calls_Exception_ctor(self): - exc = self._makeOne('message') - self.assertEqual(exc.message, 'message') - - def test_ctor_calls_Response_ctor(self): - exc = self._makeOne('message') - self.assertEqual(exc.status, 'None None') - - def test_ctor_extends_headers(self): - exc = self._makeOne(headers=[('X-Foo', 'foo')]) - self.assertEqual(exc.headers.get('X-Foo'), 'foo') + self.failUnless(IResponse.implementedBy(cls)) - def test_ctor_sets_body_template_obj(self): - exc = self._makeOne(body_template='${foo}') - self.assertEqual( - exc.body_template_obj.substitute({'foo':'foo'}), 'foo') - - def test_ctor_with_empty_body(self): - cls = self._getTargetSubclass(empty_body=True) - exc = cls() - self.assertEqual(exc.content_type, None) - self.assertEqual(exc.content_length, None) - - def test_ctor_with_body_doesnt_set_default_app_iter(self): - exc = self._makeOne(body='123') - self.assertEqual(exc.app_iter, ['123']) - - def test_ctor_with_unicode_body_doesnt_set_default_app_iter(self): - exc = self._makeOne(unicode_body=u'123') - self.assertEqual(exc.app_iter, ['123']) - - def test_ctor_with_app_iter_doesnt_set_default_app_iter(self): - exc = self._makeOne(app_iter=['123']) - self.assertEqual(exc.app_iter, ['123']) - - def test_ctor_with_body_sets_default_app_iter_html(self): - cls = self._getTargetSubclass() - exc = cls('detail') - body = list(exc.app_iter)[0] - self.assertTrue(body.startswith('' in body) - - def test_custom_body_template_no_environ(self): - cls = self._getTargetSubclass() - exc = cls(body_template='${location}', location='foo') - exc.content_type = 'text/plain' - body = list(exc._default_app_iter())[0] - self.assertEqual(body, '200 OK\n\nfoo') - - def test_custom_body_template_with_environ(self): - cls = self._getTargetSubclass() - from pyramid.request import Request - request = Request.blank('/') - exc = cls(body_template='${REQUEST_METHOD}', request=request) - exc.content_type = 'text/plain' - body = list(exc._default_app_iter())[0] - self.assertEqual(body, '200 OK\n\nGET') - - def test_body_template_unicode(self): - from pyramid.request import Request - cls = self._getTargetSubclass() - la = unicode('/La Pe\xc3\xb1a', 'utf-8') - request = Request.blank('/') - request.environ['unicodeval'] = la - exc = cls(body_template='${unicodeval}', request=request) - exc.content_type = 'text/plain' - body = list(exc._default_app_iter())[0] - self.assertEqual(body, '200 OK\n\n/La Pe\xc3\xb1a') - -class TestRenderAllExceptionsWithoutArguments(unittest.TestCase): - def _doit(self, content_type): - from pyramid.response import status_map - L = [] - self.assertTrue(status_map) - for v in status_map.values(): - exc = v() - exc.content_type = content_type - result = list(exc.app_iter)[0] - if exc.empty_body: - self.assertEqual(result, '') - else: - self.assertTrue(exc.status in result) - L.append(result) - self.assertEqual(len(L), len(status_map)) - - def test_it_plain(self): - self._doit('text/plain') - - def test_it_html(self): - self._doit('text/html') - -class Test_HTTPMove(unittest.TestCase): - def _makeOne(self, *arg, **kw): - from pyramid.response import _HTTPMove - return _HTTPMove(*arg, **kw) - - def test_it_location_not_passed(self): - exc = self._makeOne() - self.assertEqual(exc.location, '') - - def test_it_location_passed(self): - exc = self._makeOne(location='foo') - self.assertEqual(exc.location, 'foo') - -class TestHTTPForbidden(unittest.TestCase): - def _makeOne(self, *arg, **kw): - from pyramid.response import HTTPForbidden - return HTTPForbidden(*arg, **kw) - - def test_it_result_not_passed(self): - exc = self._makeOne() - self.assertEqual(exc.result, None) - - def test_it_result_passed(self): - exc = self._makeOne(result='foo') - self.assertEqual(exc.result, 'foo') - -class DummyRequest(object): - exception = None + def test_provides_IResponse(self): + from pyramid.interfaces import IResponse + inst = self._getTargetClass()() + self.failUnless(IResponse.providedBy(inst)) diff --git a/pyramid/tests/test_router.py b/pyramid/tests/test_router.py index a89de7a36..765a26751 100644 --- a/pyramid/tests/test_router.py +++ b/pyramid/tests/test_router.py @@ -2,6 +2,19 @@ import unittest from pyramid import testing +def hide_warnings(wrapped): + import warnings + def wrapper(*arg, **kw): + warnings.filterwarnings('ignore') + try: + wrapped(*arg, **kw) + finally: + warnings.resetwarnings() + wrapper.__name__ = wrapped.__name__ + wrapper.__doc__ = wrapped.__doc__ + return wrapper + + class TestRouter(unittest.TestCase): def setUp(self): testing.setUp() @@ -136,7 +149,7 @@ class TestRouter(unittest.TestCase): self.assertEqual(router.request_factory, DummyRequestFactory) def test_call_traverser_default(self): - from pyramid.response import HTTPNotFound + from pyramid.httpexceptions import HTTPNotFound environ = self._makeEnviron() logger = self._registerLogger() router = self._makeOne() @@ -147,7 +160,7 @@ class TestRouter(unittest.TestCase): self.assertEqual(len(logger.messages), 0) def test_traverser_raises_notfound_class(self): - from pyramid.response import HTTPNotFound + from pyramid.httpexceptions import HTTPNotFound environ = self._makeEnviron() context = DummyContext() self._registerTraverserFactory(context, raise_error=HTTPNotFound) @@ -156,7 +169,7 @@ class TestRouter(unittest.TestCase): self.assertRaises(HTTPNotFound, router, environ, start_response) def test_traverser_raises_notfound_instance(self): - from pyramid.response import HTTPNotFound + from pyramid.httpexceptions import HTTPNotFound environ = self._makeEnviron() context = DummyContext() self._registerTraverserFactory(context, raise_error=HTTPNotFound('foo')) @@ -166,26 +179,27 @@ class TestRouter(unittest.TestCase): self.assertTrue('foo' in why[0], why) def test_traverser_raises_forbidden_class(self): - from pyramid.response import Forbidden + from pyramid.httpexceptions import HTTPForbidden environ = self._makeEnviron() context = DummyContext() - self._registerTraverserFactory(context, raise_error=Forbidden) + self._registerTraverserFactory(context, raise_error=HTTPForbidden) router = self._makeOne() start_response = DummyStartResponse() - self.assertRaises(Forbidden, router, environ, start_response) + self.assertRaises(HTTPForbidden, router, environ, start_response) def test_traverser_raises_forbidden_instance(self): - from pyramid.response import Forbidden + from pyramid.httpexceptions import HTTPForbidden environ = self._makeEnviron() context = DummyContext() - self._registerTraverserFactory(context, raise_error=Forbidden('foo')) + self._registerTraverserFactory(context, + raise_error=HTTPForbidden('foo')) router = self._makeOne() start_response = DummyStartResponse() - why = exc_raised(Forbidden, router, environ, start_response) + why = exc_raised(HTTPForbidden, router, environ, start_response) self.assertTrue('foo' in why[0], why) def test_call_no_view_registered_no_isettings(self): - from pyramid.response import HTTPNotFound + from pyramid.httpexceptions import HTTPNotFound environ = self._makeEnviron() context = DummyContext() self._registerTraverserFactory(context) @@ -198,7 +212,7 @@ class TestRouter(unittest.TestCase): self.assertEqual(len(logger.messages), 0) def test_call_no_view_registered_debug_notfound_false(self): - from pyramid.response import HTTPNotFound + from pyramid.httpexceptions import HTTPNotFound environ = self._makeEnviron() context = DummyContext() self._registerTraverserFactory(context) @@ -212,7 +226,7 @@ class TestRouter(unittest.TestCase): self.assertEqual(len(logger.messages), 0) def test_call_no_view_registered_debug_notfound_true(self): - from pyramid.response import HTTPNotFound + from pyramid.httpexceptions import HTTPNotFound environ = self._makeEnviron() context = DummyContext() self._registerTraverserFactory(context) @@ -323,7 +337,7 @@ class TestRouter(unittest.TestCase): def test_call_view_registered_specific_fail(self): from zope.interface import Interface from zope.interface import directlyProvides - from pyramid.response import HTTPNotFound + from pyramid.httpexceptions import HTTPNotFound from pyramid.interfaces import IViewClassifier class IContext(Interface): pass @@ -344,7 +358,7 @@ class TestRouter(unittest.TestCase): def test_call_view_raises_forbidden(self): from zope.interface import Interface from zope.interface import directlyProvides - from pyramid.response import Forbidden + from pyramid.httpexceptions import HTTPForbidden class IContext(Interface): pass from pyramid.interfaces import IRequest @@ -353,12 +367,13 @@ class TestRouter(unittest.TestCase): directlyProvides(context, IContext) self._registerTraverserFactory(context, subpath=['']) response = DummyResponse() - view = DummyView(response, raise_exception=Forbidden("unauthorized")) + view = DummyView(response, + raise_exception=HTTPForbidden("unauthorized")) environ = self._makeEnviron() self._registerView(view, '', IViewClassifier, IRequest, IContext) router = self._makeOne() start_response = DummyStartResponse() - why = exc_raised(Forbidden, router, environ, start_response) + why = exc_raised(HTTPForbidden, router, environ, start_response) self.assertEqual(why[0], 'unauthorized') def test_call_view_raises_notfound(self): @@ -368,7 +383,7 @@ class TestRouter(unittest.TestCase): pass from pyramid.interfaces import IRequest from pyramid.interfaces import IViewClassifier - from pyramid.response import HTTPNotFound + from pyramid.httpexceptions import HTTPNotFound context = DummyContext() directlyProvides(context, IContext) self._registerTraverserFactory(context, subpath=['']) @@ -597,7 +612,7 @@ class TestRouter(unittest.TestCase): "pattern: 'archives/:action/:article', ")) def test_call_route_match_miss_debug_routematch(self): - from pyramid.response import HTTPNotFound + from pyramid.httpexceptions import HTTPNotFound logger = self._registerLogger() self._registerSettings(debug_routematch=True) self._registerRouteRequest('foo') @@ -658,7 +673,7 @@ class TestRouter(unittest.TestCase): def test_root_factory_raises_notfound(self): from pyramid.interfaces import IRootFactory - from pyramid.response import HTTPNotFound + from pyramid.httpexceptions import HTTPNotFound from zope.interface import Interface from zope.interface import directlyProvides def rootfactory(request): @@ -676,11 +691,11 @@ class TestRouter(unittest.TestCase): def test_root_factory_raises_forbidden(self): from pyramid.interfaces import IRootFactory - from pyramid.response import Forbidden + from pyramid.httpexceptions import HTTPForbidden from zope.interface import Interface from zope.interface import directlyProvides def rootfactory(request): - raise Forbidden('from root factory') + raise HTTPForbidden('from root factory') self.registry.registerUtility(rootfactory, IRootFactory) class IContext(Interface): pass @@ -689,7 +704,7 @@ class TestRouter(unittest.TestCase): environ = self._makeEnviron() router = self._makeOne() start_response = DummyStartResponse() - why = exc_raised(Forbidden, router, environ, start_response) + why = exc_raised(HTTPForbidden, router, environ, start_response) self.assertTrue('from root factory' in why[0]) def test_root_factory_exception_propagating(self): @@ -1057,6 +1072,52 @@ class TestRouter(unittest.TestCase): start_response = DummyStartResponse() self.assertRaises(RuntimeError, router, environ, start_response) +class Test_default_responder(unittest.TestCase): + def _makeOne(self, response): + from pyramid.router import default_responder + return default_responder(response) + + def test_has_call(self): + response = DummyResponse() + response.app_iter = ['123'] + response.headerlist = [('a', '1')] + responder = self._makeOne(response) + request = DummyRequest({'a':'1'}) + start_response = DummyStartResponse() + app_iter = responder(request, start_response) + self.assertEqual(app_iter, response.app_iter) + self.assertEqual(start_response.status, response.status) + self.assertEqual(start_response.headers, response.headerlist) + self.assertEqual(response.environ, request.environ) + + @hide_warnings + def test_without_call_success(self): + response = DummyResponseWithoutCall() + response.app_iter = ['123'] + response.headerlist = [('a', '1')] + responder = self._makeOne(response) + request = DummyRequest({'a':'1'}) + start_response = DummyStartResponse() + app_iter = responder(request, start_response) + self.assertEqual(app_iter, response.app_iter) + self.assertEqual(start_response.status, response.status) + self.assertEqual(start_response.headers, response.headerlist) + + @hide_warnings + def test_without_call_exception(self): + response = DummyResponseWithoutCall() + del response.status + responder = self._makeOne(response) + request = DummyRequest({'a':'1'}) + start_response = DummyStartResponse() + self.assertRaises(ValueError, responder, request, start_response) + + +class DummyRequest(object): + def __init__(self, environ=None): + if environ is None: environ = {} + self.environ = environ + class DummyContext: pass @@ -1085,12 +1146,20 @@ class DummyStartResponse: def __call__(self, status, headers): self.status = status self.headers = headers - -class DummyResponse: + +class DummyResponseWithoutCall: headerlist = () app_iter = () def __init__(self, status='200 OK'): self.status = status + +class DummyResponse(DummyResponseWithoutCall): + environ = None + + def __call__(self, environ, start_response): + self.environ = environ + start_response(self.status, self.headerlist) + return self.app_iter class DummyThreadLocalManager: def __init__(self): diff --git a/pyramid/tests/test_testing.py b/pyramid/tests/test_testing.py index 0288884b7..159a88ebd 100644 --- a/pyramid/tests/test_testing.py +++ b/pyramid/tests/test_testing.py @@ -150,7 +150,7 @@ class Test_registerView(TestBase): def test_registerView_with_permission_denying(self): from pyramid import testing - from pyramid.response import HTTPForbidden + from pyramid.httpexceptions import HTTPForbidden def view(context, request): """ """ view = testing.registerView('moo.html', view=view, permission='bar') diff --git a/pyramid/view.py b/pyramid/view.py index 0b5c7cdc9..9a4be7580 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -8,8 +8,8 @@ from pyramid.interfaces import IRoutesMapper from pyramid.interfaces import IView from pyramid.interfaces import IViewClassifier -from pyramid.response import HTTPFound -from pyramid.response import default_exceptionresponse_view +from pyramid.httpexceptions import HTTPFound +from pyramid.httpexceptions import default_exceptionresponse_view from pyramid.renderers import RendererHelper from pyramid.static import static_view from pyramid.threadlocal import get_current_registry @@ -45,12 +45,12 @@ def render_view_to_response(context, request, name='', secure=True): ``name`` / ``context`` / and ``request``). If `secure`` is ``True``, and the :term:`view callable` found is - protected by a permission, the permission will be checked before - calling the view function. If the permission check disallows view - execution (based on the current :term:`authorization policy`), a - :exc:`pyramid.response.HTTPForbidden` exception will be raised. - The exception's ``args`` attribute explains why the view access - was disallowed. + protected by a permission, the permission will be checked before calling + the view function. If the permission check disallows view execution + (based on the current :term:`authorization policy`), a + :exc:`pyramid.httpexceptions.HTTPForbidden` exception will be raised. + The exception's ``args`` attribute explains why the view access was + disallowed. If ``secure`` is ``False``, no permission checking is done.""" provides = [IViewClassifier] + map(providedBy, (request, context)) @@ -88,13 +88,12 @@ def render_view_to_iterable(context, request, name='', secure=True): of this function by calling ``''.join(iterable)``, or just use :func:`pyramid.view.render_view` instead. - If ``secure`` is ``True``, and the view is protected by a - permission, the permission will be checked before the view - function is invoked. If the permission check disallows view - execution (based on the current :term:`authentication policy`), a - :exc:`pyramid.response.HTTPForbidden` exception will be raised; - its ``args`` attribute explains why the view access was - disallowed. + If ``secure`` is ``True``, and the view is protected by a permission, the + permission will be checked before the view function is invoked. If the + permission check disallows view execution (based on the current + :term:`authentication policy`), a + :exc:`pyramid.httpexceptions.HTTPForbidden` exception will be raised; its + ``args`` attribute explains why the view access was disallowed. If ``secure`` is ``False``, no permission checking is done.""" @@ -117,12 +116,11 @@ def render_view(context, request, name='', secure=True): ``app_iter`` attribute. This function will return ``None`` if a corresponding view cannot be found. - If ``secure`` is ``True``, and the view is protected by a - permission, the permission will be checked before the view is - invoked. If the permission check disallows view execution (based - on the current :term:`authorization policy`), a - :exc:`pyramid.response.HTTPForbidden` exception will be raised; - its ``args`` attribute explains why the view access was + If ``secure`` is ``True``, and the view is protected by a permission, the + permission will be checked before the view is invoked. If the permission + check disallows view execution (based on the current :term:`authorization + policy`), a :exc:`pyramid.httpexceptions.HTTPForbidden` exception will be + raised; its ``args`` attribute explains why the view access was disallowed. If ``secure`` is ``False``, no permission checking is done.""" @@ -249,7 +247,7 @@ class AppendSlashNotFoundViewFactory(object): .. code-block:: python - from pyramid.response import HTTPNotFound + from pyramid.httpexceptions import HTTPNotFound from pyramid.view import AppendSlashNotFoundViewFactory def notfound_view(context, request): return HTTPNotFound('nope') @@ -302,7 +300,7 @@ routes are not considered when attempting to find a matching route. Use the :meth:`pyramid.config.Configurator.add_view` method to configure this view as the Not Found view:: - from pyramid.response import HTTPNotFound + from pyramid.httpexceptions import HTTPNotFound from pyramid.view import append_slash_notfound_view config.add_view(append_slash_notfound_view, context=HTTPNotFound) -- cgit v1.2.3 From fc048afce7c58a1e794b495b438f4ee76f084b69 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 11 Jun 2011 05:42:43 -0400 Subject: todo --- TODO.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/TODO.txt b/TODO.txt index 04c6e60d7..1a178a23d 100644 --- a/TODO.txt +++ b/TODO.txt @@ -7,6 +7,11 @@ Must-Have - Depend on only __call__ interface or only 3-attr interface in builtin code that deals with response objects. +- Figure out what to do with ``is_response``. + +- Docs mention ``exception.args[0]`` as a way to get messages; check that + this works. + Should-Have ----------- -- cgit v1.2.3 From f0d77e8f3cec1ff90a2029fe143580fd42cf81aa Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 11 Jun 2011 05:45:15 -0400 Subject: todo --- TODO.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/TODO.txt b/TODO.txt index 1a178a23d..4b82208bd 100644 --- a/TODO.txt +++ b/TODO.txt @@ -4,6 +4,8 @@ Pyramid TODOs Must-Have --------- +- To subclass or not subclass http exceptions. + - Depend on only __call__ interface or only 3-attr interface in builtin code that deals with response objects. -- cgit v1.2.3 From d868fff7597c5a05acd1f5c024fc45dde9880413 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 13 Jun 2011 06:17:00 -0400 Subject: - Remove IResponder abstraction in favor of more general IResponse abstraction. - It is now possible to return an arbitrary object from a Pyramid view callable even if a renderer is not used, as long as a suitable adapter to ``pyramid.interfaces.IResponse`` is registered for the type of the returned object. See the section in the Hooks chapter of the documentation entitled "Changing How Pyramid Treats View Responses". - The Pyramid router now, by default, expects response objects returned from view callables to implement the ``pyramid.interfaces.IResponse`` interface. Unlike the Pyramid 1.0 version of this interface, objects which implement IResponse now must define a ``__call__`` method that accepts ``environ`` and ``start_response``, and which returns an ``app_iter`` iterable, among other things. Previously, it was possible to return any object which had the three WebOb ``app_iter``, ``headerlist``, and ``status`` attributes as a response, so this is a backwards incompatibility. It is possible to get backwards compatibility back by registering an adapter to IResponse from the type of object you're now returning from view callables. See the section in the Hooks chapter of the documentation entitled "Changing How Pyramid Treats View Responses". - The ``pyramid.interfaces.IResponse`` interface is now much more extensive. Previously it defined only ``app_iter``, ``status`` and ``headerlist``; now it is basically intended to directly mirror the ``webob.Response`` API, which has many methods and attributes. - Documentation changes to support above. --- CHANGES.txt | 42 ++++++----- TODO.txt | 19 ++++- docs/api/interfaces.rst | 2 +- docs/api/request.rst | 33 ++++---- docs/designdefense.rst | 4 +- docs/glossary.rst | 12 +-- docs/narr/assets.rst | 2 +- docs/narr/hooks.rst | 134 ++++++++++++++++++++++++--------- docs/narr/renderers.rst | 2 +- docs/narr/router.rst | 6 +- docs/narr/views.rst | 36 ++++----- docs/narr/webob.rst | 66 ++++++++-------- docs/tutorials/wiki/definingviews.rst | 10 ++- docs/tutorials/wiki2/definingviews.rst | 3 +- pyramid/config.py | 39 ++++++++-- pyramid/interfaces.py | 8 -- pyramid/registry.py | 19 +++++ pyramid/renderers.py | 4 +- pyramid/request.py | 2 + pyramid/response.py | 2 +- pyramid/router.py | 47 +++--------- pyramid/session.py | 14 +--- pyramid/tests/test_config.py | 78 ++++++++++++++++--- pyramid/tests/test_router.py | 122 +++++++----------------------- pyramid/tests/test_session.py | 15 +--- pyramid/tests/test_view.py | 23 +++++- pyramid/view.py | 6 ++ 27 files changed, 411 insertions(+), 339 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index e413f0657..5e8df1a0b 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -127,11 +127,11 @@ Features - The ``pyramid.request.Response`` class now has a ``RequestClass`` interface which points at ``pyramid.response.Request``. -- It is now possible to control how the Pyramid router calls the WSGI - ``start_response`` callable and obtains the WSGI ``app_iter`` based on - adapting the response object to the new ``pyramid.interfaces.IResponder`` - interface. See the section in the Hooks chapter of the documentation - entitled "Changing How Pyramid Treats Response Objects". +- It is now possible to return an arbitrary object from a Pyramid view + callable even if a renderer is not used, as long as a suitable adapter to + ``pyramid.interfaces.IResponse`` is registered for the type of the returned + object. See the section in the Hooks chapter of the documentation entitled + "Changing How Pyramid Treats View Responses". - The Pyramid router will now, by default, call the ``__call__`` method of WebOb response objects when returning a WSGI response. This means that, @@ -306,22 +306,26 @@ Behavior Changes ``webob.response.Response`` (in order to directly implement the ``pyramid.interfaces.IResponse`` interface). -- The ``pyramid.interfaces.IResponse`` interface now includes a ``__call__`` - method which has the WSGI application call signature (and which expects an - iterable as a result). +Backwards Incompatibilities +--------------------------- - The Pyramid router now, by default, expects response objects returned from - views to implement the WSGI application interface (a ``__call__`` method - that accepts ``environ`` and ``start_response``, and which returns an - ``app_iter`` iterable). If such a method exists, Pyramid will now call it - in order to satisfy the WSGI request. Backwards compatibility code in the - default responder exists which will fall back to the older behavior, but - Pyramid will raise a deprecation warning if it is reached. See the section - in the Hooks chapter of the documentation entitled "Changing How Pyramid - Treats Response Objects" to default back to the older behavior, where the - ``app_iter``, ``headerlist``, and ``status`` attributes of the object were - consulted directly (without any indirection through ``__call__``) to - silence the deprecation warnings. + view callables to implement the ``pyramid.interfaces.IResponse`` interface. + Unlike the Pyramid 1.0 version of this interface, objects which implement + IResponse now must define a ``__call__`` method that accepts ``environ`` + and ``start_response``, and which returns an ``app_iter`` iterable, among + other things. Previously, it was possible to return any object which had + the three WebOb ``app_iter``, ``headerlist``, and ``status`` attributes as + a response, so this is a backwards incompatibility. It is possible to get + backwards compatibility back by registering an adapter to IResponse from + the type of object you're now returning from view callables. See the + section in the Hooks chapter of the documentation entitled "Changing How + Pyramid Treats View Responses". + +- The ``pyramid.interfaces.IResponse`` interface is now much more extensive. + Previously it defined only ``app_iter``, ``status`` and ``headerlist``; now + it is basically intended to directly mirror the ``webob.Response`` API, + which has many methods and attributes. Dependencies ------------ diff --git a/TODO.txt b/TODO.txt index 4b82208bd..ca2433d3c 100644 --- a/TODO.txt +++ b/TODO.txt @@ -6,14 +6,27 @@ Must-Have - To subclass or not subclass http exceptions. -- Depend on only __call__ interface or only 3-attr interface in builtin code - that deals with response objects. +- Flesh out IResponse interface. Attributes Used internally: unicode_body / + body / content_type / charset / cache_expires / headers/ + default_content_type / set_cookie / headerlist / app_iter / status / + __call__. -- Figure out what to do with ``is_response``. +- Deprecate view.is_response? + +- Move is_response to response.py? + +- Make sure registering IResponse adapter for webob.Response doesn't make it + impossible to register an IResponse adapter for an interface that a + webob.Response happens to implement. + +- Run whatsitdoing tests. - Docs mention ``exception.args[0]`` as a way to get messages; check that this works. +- Deprecate response_foo attrs on request at attribute set time rather than + lookup time. + Should-Have ----------- diff --git a/docs/api/interfaces.rst b/docs/api/interfaces.rst index 3a60fa4dc..51a1963b5 100644 --- a/docs/api/interfaces.rst +++ b/docs/api/interfaces.rst @@ -57,6 +57,6 @@ Other Interfaces .. autointerface:: IMultiDict :members: - .. autointerface:: IResponder + .. autointerface:: IResponse :members: diff --git a/docs/api/request.rst b/docs/api/request.rst index 8cb424658..27ce395ac 100644 --- a/docs/api/request.rst +++ b/docs/api/request.rst @@ -107,7 +107,9 @@ return {'text':'Value that will be used by the renderer'} Mutations to this response object will be preserved in the response sent - to the client after rendering. + to the client after rendering. For more information about using + ``request.response`` in conjunction with a renderer, see + :ref:`request_response_attr`. Non-renderer code can also make use of request.response instead of creating a response "by hand". For example, in view code:: @@ -162,20 +164,21 @@ .. attribute:: response_* - .. warning:: As of Pyramid 1.1, assignment to ``response_*`` attrs are - deprecated. Assigning to one will cause a deprecation warning to be - emitted. Instead of assigning ``response_*`` attributes to the - request, use API of the the :attr:`pyramid.request.Request.response` - object (exposed to view code as ``request.response``) to influence - response behavior. - - You can set attributes on a :class:`pyramid.request.Request` which will - influence the behavor of *rendered* responses (views which use a - :term:`renderer` and which don't directly return a response). These - attributes begin with ``response_``, such as ``response_headerlist``. If - you need to influence response values from a view that uses a renderer - (such as the status code, a header, the content type, etc) see, - :ref:`response_prefixed_attrs`. + In Pyramid 1.0, you could set attributes on a + :class:`pyramid.request.Request` which influenced the behavor of + *rendered* responses (views which use a :term:`renderer` and which + don't directly return a response). These attributes began with + ``response_``, such as ``response_headerlist``. If you needed to + influence response values from a view that uses a renderer (such as the + status code, a header, the content type, etc) you would set these + attributes. See :ref:`response_prefixed_attrs` for further discussion. + As of Pyramid 1.1, assignment to ``response_*`` attrs are deprecated. + Assigning to one is still supported but will cause a deprecation + warning to be emitted, and eventually the feature will be removed. For + new code, instead of assigning ``response_*`` attributes to the + request, use API of the the :attr:`pyramid.request.Request.response` + object (exposed to view code as ``request.response``) to influence + rendered response behavior. .. note:: diff --git a/docs/designdefense.rst b/docs/designdefense.rst index 136b9c5de..de6c0af33 100644 --- a/docs/designdefense.rst +++ b/docs/designdefense.rst @@ -428,7 +428,7 @@ allowing people to define "custom" view predicates: :linenos: from pyramid.view import view_config - from webob import Response + from pyramid.response import Response def subpath(context, request): return request.subpath and request.subpath[0] == 'abc' @@ -1497,7 +1497,7 @@ comments take into account what we've discussed in the .. code-block:: python :linenos: - from webob import Response # explicit response objects, no TL + from pyramid.response import Response # explicit response objects, no TL from paste.httpserver import serve # explicitly WSGI def hello_world(request): # accepts a request; no request thread local reqd diff --git a/docs/glossary.rst b/docs/glossary.rst index 079a069b4..d3ba9a545 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -16,12 +16,12 @@ Glossary positional argument, returns a ``WebOb`` compatible request. response - An object that has three attributes: ``app_iter`` (representing an - iterable body), ``headerlist`` (representing the http headers sent - to the user agent), and ``status`` (representing the http status - string sent to the user agent). This is the interface defined for - ``WebOb`` response objects. See :ref:`webob_chapter` for - information about response objects. + An object returned by a :term:`view callable` that represents response + data returned to the requesting user agent. It must implements the + :class:`pyramid.interfaces.IResponse` interface. A response object is + typically an instance of the :class:`pyramid.response.Response` class or + a subclass such as :class:`pyramid.httpexceptions.HTTPFound`. See + :ref:`webob_chapter` for information about response objects. Repoze "Repoze" is essentially a "brand" of software developed by `Agendaless diff --git a/docs/narr/assets.rst b/docs/narr/assets.rst index 8d0e7058c..0d50b0106 100644 --- a/docs/narr/assets.rst +++ b/docs/narr/assets.rst @@ -358,7 +358,7 @@ do so, do things "by hand". First define the view callable. :linenos: import os - from webob import Response + from pyramid.response import Response def favicon_view(request): here = os.path.dirname(__file__) diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index b6a781417..0db8ce5e0 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -523,41 +523,103 @@ The default context URL generator is available for perusal as the class :term:`Pylons` GitHub Pyramid repository. .. index:: - single: IResponder - -.. _using_iresponder: - -Changing How Pyramid Treats Response Objects --------------------------------------------- - -It is possible to control how the Pyramid :term:`router` calls the WSGI -``start_response`` callable and obtains the WSGI ``app_iter`` based on -adapting the response object to the :class: `pyramid.interfaces.IResponder` -interface. The default responder uses the ``__call__`` method of a response -object, passing it the WSGI environ and the WSGI ``start_response`` callable -(the response is assumed to be a WSGI application). To override the -responder:: - - from pyramid.interfaces import IResponder - from pyramid.response import Response - from myapp import MyResponder - - config.registry.registerAdapter(MyResponder, (Response,), - IResponder, name='') - -Overriding makes it possible to reuse response object implementations which -have, for example, the ``app_iter``, ``headerlist`` and ``status`` attributes -of an object returned as a response instead of trying to use the object's -``__call__`` method:: - - class MyResponder(object): - def __init__(self, response): - """ Obtain a reference to the response """ - self.response = response - def __call__(self, request, start_response): - """ Call start_response and return an app_iter """ - start_response(self.response.status, self.response.headerlist) - return self.response.app_iter + single: IResponse + +.. _using_iresponse: + +Changing How Pyramid Treats View Responses +------------------------------------------ + +It is possible to control how Pyramid treats the result of calling a view +callable on a per-type basis by using a hook involving +:class:`pyramid.interfaces.IResponse`. + +.. note:: This feature is new as of Pyramid 1.1. + +Pyramid, in various places, adapts the result of calling a view callable to +the :class:`~pyramid.interfaces.IResponse` interface to ensure that the +object returned by the view callable is a "true" response object. The vast +majority of time, the result of this adaptation is the result object itself, +as view callables written by "civilians" who read the narrative documentation +contained in this manual will always return something that implements the +:class:`~pyramid.interfaces.IResponse` interface. Most typically, this will +be an instance of the :class:`pyramid.response.Response` class or a subclass. +If a civilian returns a non-Response object from a view callable that isn't +configured to use a :term:`renderer`, he will typically expect the router to +raise an error. However, you can hook Pyramid in such a way that users can +return arbitrary values from a view callable by providing an adapter which +converts the arbitrary return value into something that implements +:class:`~pyramid.interfaces.IResponse`. + +For example, if you'd like to allow view callables to return bare string +objects (without requiring a a :term:`renderer` to convert a string to a +response object), you can register an adapter which converts the string to a +Response: + +.. code-block:: python + :linenos: + + from pyramid.interfaces import IResponse + from pyramid.response import Response + + def string_response_adapter(s): + response = Response(s) + return response + + # config is an instance of pyramid.config.Configurator + + config.registry.registerAdapter(string_response_adapter, (str,), + IResponse) + +Likewise, if you want to be able to return a simplified kind of response +object from view callables, you can use the IResponse hook to register an +adapter to the more complex IResponse interface: + +.. code-block:: python + :linenos: + + from pyramid.interfaces import IResponse + from pyramid.response import Response + + class SimpleResponse(object): + def __init__(self, body): + self.body = body + + def simple_response_adapter(simple_response): + response = Response(simple_response.body) + return response + + # config is an instance of pyramid.config.Configurator + + config.registry.registerAdapter(simple_response_adapter, + (SimpleResponse,), + IResponse) + +If you want to implement your own Response object instead of using the +:class:`pyramid.response.Response` object in any capacity at all, you'll have +to make sure the object implements every attribute and method outlined in +:class:`pyramid.interfaces.IResponse` *and* you'll have to ensure that it's +marked up with ``zope.interface.implements(IResponse)``: + + from pyramid.interfaces import IResponse + from zope.interface import implements + + class MyResponse(object): + implements(IResponse) + # ... an implementation of every method and attribute + # documented in IResponse should follow ... + +When an alternate response object implementation is returned by a view +callable, if that object asserts that it implements +:class:`~pyramid.interfaces.IResponse` (via +``zope.interface.implements(IResponse)``) , an adapter needn't be registered +for the object; Pyramid will use it directly. + +An IResponse adapter for ``webob.Response`` (as opposed to +:class:`pyramid.response.Response`) is registered by Pyramid by default at +startup time, as by their nature, instances of this class (and instances of +subclasses of the class) will natively provide IResponse. The adapter +registered for ``webob.Response`` simply returns the response object. .. index:: single: view mapper @@ -628,7 +690,7 @@ A user might make use of these framework components like so: # user application - from webob import Response + from pyramid.response import Response from pyramid.config import Configurator import pyramid_handlers from paste.httpserver import serve diff --git a/docs/narr/renderers.rst b/docs/narr/renderers.rst index 99ee14908..c4a37c23d 100644 --- a/docs/narr/renderers.rst +++ b/docs/narr/renderers.rst @@ -416,7 +416,7 @@ effect, you must return ``request.response``: For more information on attributes of the request, see the API documentation in :ref:`request_module`. For more information on the API of -``request.response``, see :class:`pyramid.response.Response`. +``request.response``, see :attr:`pyramid.request.Request.response`. .. _response_prefixed_attrs: diff --git a/docs/narr/router.rst b/docs/narr/router.rst index 30d54767e..0812f7ec7 100644 --- a/docs/narr/router.rst +++ b/docs/narr/router.rst @@ -115,9 +115,9 @@ processing? any :term:`response callback` functions attached via :meth:`~pyramid.request.Request.add_response_callback`. A :class:`~pyramid.events.NewResponse` :term:`event` is then sent to any - subscribers. The response object's ``app_iter``, ``status``, and - ``headerlist`` attributes are then used to generate a WSGI response. The - response is sent back to the upstream WSGI server. + subscribers. The response object's ``__call__`` method is then used to + generate a WSGI response. The response is sent back to the upstream WSGI + server. #. :app:`Pyramid` will attempt to execute any :term:`finished callback` functions attached via diff --git a/docs/narr/views.rst b/docs/narr/views.rst index 990828f80..e3d0a37e5 100644 --- a/docs/narr/views.rst +++ b/docs/narr/views.rst @@ -230,29 +230,19 @@ implements the :term:`Response` interface is to return a def view(request): return Response('OK') -You don't need to use :class:`~pyramid.response.Response` to represent a -response. A view can actually return any object that has a ``__call__`` -method that implements the :term:`WSGI` application call interface. For -example, an instance of the following class could be successfully returned by -a view callable as a response object: - -.. code-block:: python - :linenos: - - class SimpleResponse(object): - def __call__(self, environ, start_response): - """ Call the ``start_response`` callback and return - an iterable """ - body = 'Hello World!' - headers = [('Content-Type', 'text/plain'), - ('Content-Length', str(len(body)))] - start_response('200 OK', headers) - return [body] - -:app:`Pyramid` provides a range of different "exception" classes which can -act as response objects too. For example, an instance of the class -:class:`pyramid.httpexceptions.HTTPFound` is also a valid response object -(see :ref:`http_exceptions` and ref:`http_redirect`). +:app:`Pyramid` provides a range of different "exception" classes which +inherit from :class:`pyramid.response.Response`. For example, an instance of +the class :class:`pyramid.httpexceptions.HTTPFound` is also a valid response +object because it inherits from :class:`~pyramid.response.Response`. For +examples, see :ref:`http_exceptions` and ref:`http_redirect`. + +You can also return objects from view callables that aren't instances of (or +instances of classes which are subclasses of) +:class:`pyramid.response.Response` in various circumstances. This can be +helpful when writing tests and when attempting to share code between view +callables. See :ref:`renderers_chapter` for the common way to allow for +this. A much less common way to allow for view callables to return +non-Response objects is documented in :ref:`using_iresponse`. .. index:: single: view exceptions diff --git a/docs/narr/webob.rst b/docs/narr/webob.rst index 70ab5eea8..0ff8e1de7 100644 --- a/docs/narr/webob.rst +++ b/docs/narr/webob.rst @@ -10,15 +10,15 @@ Request and Response Objects .. note:: This chapter is adapted from a portion of the :term:`WebOb` documentation, originally written by Ian Bicking. -:app:`Pyramid` uses the :term:`WebOb` package to supply +:app:`Pyramid` uses the :term:`WebOb` package as a basis for its :term:`request` and :term:`response` object implementations. The -:term:`request` object that is passed to a :app:`Pyramid` -:term:`view` is an instance of the :class:`pyramid.request.Request` -class, which is a subclass of :class:`webob.Request`. The -:term:`response` returned from a :app:`Pyramid` :term:`view` -:term:`renderer` is an instance of the :mod:`webob.Response` class. -Users can also return an instance of :mod:`webob.Response` directly -from a view as necessary. +:term:`request` object that is passed to a :app:`Pyramid` :term:`view` is an +instance of the :class:`pyramid.request.Request` class, which is a subclass +of :class:`webob.Request`. The :term:`response` returned from a +:app:`Pyramid` :term:`view` :term:`renderer` is an instance of the +:mod:`pyramid.response.Response` class, which is a subclass of the +:class:`webob.Response` class. Users can also return an instance of +:class:`pyramid.response.Response` directly from a view as necessary. WebOb is a project separate from :app:`Pyramid` with a separate set of authors and a fully separate `set of documentation @@ -26,16 +26,15 @@ authors and a fully separate `set of documentation standard WebOb request, which is documented in the :ref:`request_module` API documentation. -WebOb provides objects for HTTP requests and responses. Specifically -it does this by wrapping the `WSGI `_ request -environment and response status/headers/app_iter (body). +WebOb provides objects for HTTP requests and responses. Specifically it does +this by wrapping the `WSGI `_ request environment and +response status, header list, and app_iter (body) values. -WebOb request and response objects provide many conveniences for -parsing WSGI requests and forming WSGI responses. WebOb is a nice way -to represent "raw" WSGI requests and responses; however, we won't -cover that use case in this document, as users of :app:`Pyramid` -don't typically need to use the WSGI-related features of WebOb -directly. The `reference documentation +WebOb request and response objects provide many conveniences for parsing WSGI +requests and forming WSGI responses. WebOb is a nice way to represent "raw" +WSGI requests and responses; however, we won't cover that use case in this +document, as users of :app:`Pyramid` don't typically need to use the +WSGI-related features of WebOb directly. The `reference documentation `_ shows many examples of creating requests and using response objects in this manner, however. @@ -170,9 +169,9 @@ of the request. I'll show various values for an example URL Methods +++++++ -There are `several methods -`_ but -only a few you'll use often: +There are methods of request objects documented in +:class:`pyramid.request.Request` but you'll find that you won't use very many +of them. Here are a couple that might be useful: ``Request.blank(base_url)``: Creates a new request with blank information, based at the given @@ -183,9 +182,9 @@ only a few you'll use often: subrequests). ``req.get_response(wsgi_application)``: - This method calls the given WSGI application with this request, - and returns a `Response`_ object. You can also use this for - subrequests, or testing. + This method calls the given WSGI application with this request, and + returns a :class:`pyramid.response.Response` object. You can also use + this for subrequests, or testing. .. index:: single: request (and unicode) @@ -259,8 +258,10 @@ Response ~~~~~~~~ The :app:`Pyramid` response object can be imported as -:class:`pyramid.response.Response`. This import location is merely a facade -for its original location: ``webob.Response``. +:class:`pyramid.response.Response`. This class is a subclass of the +``webob.Response`` class. The subclass does not add or change any +functionality, so the WebOb Response documentation will be completely +relevant for this class as well. A response object has three fundamental parts: @@ -283,8 +284,8 @@ A response object has three fundamental parts: ``response.body_file`` (a file-like object; writing to it appends to ``app_iter``). -Everything else in the object derives from this underlying state. -Here's the highlights: +Everything else in the object typically derives from this underlying state. +Here are some highlights: ``response.content_type`` The content type *not* including the ``charset`` parameter. @@ -359,11 +360,12 @@ Exception Responses +++++++++++++++++++ To facilitate error responses like ``404 Not Found``, the module -:mod:`webob.exc` contains classes for each kind of error response. These -include boring, but appropriate error bodies. The exceptions exposed by this -module, when used under :app:`Pyramid`, should be imported from the -:mod:`pyramid.httpexceptions` module. This import location contains -subclasses and replacements that mirror those in the original ``webob.exc``. +:mod:`pyramid.httpexceptions` contains classes for each kind of error +response. These include boring, but appropriate error bodies. The +exceptions exposed by this module, when used under :app:`Pyramid`, should be +imported from the :mod:`pyramid.httpexceptions` module. This import location +contains subclasses and replacements that mirror those in the ``webob.exc`` +module. Each class is named ``pyramid.httpexceptions.HTTP*``, where ``*`` is the reason for the error. For instance, diff --git a/docs/tutorials/wiki/definingviews.rst b/docs/tutorials/wiki/definingviews.rst index b6c083bbf..92a3da09c 100644 --- a/docs/tutorials/wiki/definingviews.rst +++ b/docs/tutorials/wiki/definingviews.rst @@ -84,10 +84,12 @@ No renderer is necessary when a view returns a response object. The ``view_wiki`` view callable always redirects to the URL of a Page resource named "FrontPage". To do so, it returns an instance of the :class:`pyramid.httpexceptions.HTTPFound` class (instances of which implement -the WebOb :term:`response` interface). The :func:`pyramid.url.resource_url` -API. :func:`pyramid.url.resource_url` constructs a URL to the ``FrontPage`` -page resource (e.g. ``http://localhost:6543/FrontPage``), and uses it as the -"location" of the HTTPFound response, forming an HTTP redirect. +the :class:`pyramid.interfaces.IResponse` interface like +:class:`pyramid.response.Response` does). The +:func:`pyramid.url.resource_url` API. :func:`pyramid.url.resource_url` +constructs a URL to the ``FrontPage`` page resource +(e.g. ``http://localhost:6543/FrontPage``), and uses it as the "location" of +the HTTPFound response, forming an HTTP redirect. The ``view_page`` view function ------------------------------- diff --git a/docs/tutorials/wiki2/definingviews.rst b/docs/tutorials/wiki2/definingviews.rst index 832f90b92..43cbc3483 100644 --- a/docs/tutorials/wiki2/definingviews.rst +++ b/docs/tutorials/wiki2/definingviews.rst @@ -91,7 +91,8 @@ path to our "FrontPage". The ``view_wiki`` function returns an instance of the :class:`pyramid.httpexceptions.HTTPFound` class (instances of which implement -the WebOb :term:`response` interface), It will use the +the :class:`pyramid.interfaces.IResponse` interface like +:class:`pyramid.response.Response` does), It will use the :func:`pyramid.url.route_url` API to construct a URL to the ``FrontPage`` page (e.g. ``http://localhost:6543/FrontPage``), and will use it as the "location" of the HTTPFound response, forming an HTTP redirect. diff --git a/pyramid/config.py b/pyramid/config.py index 91ba414b3..fab75f56d 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -19,6 +19,7 @@ from zope.interface import implementedBy from zope.interface.interfaces import IInterface from zope.interface import implements from zope.interface import classProvides +from zope.interface import providedBy from pyramid.interfaces import IAuthenticationPolicy from pyramid.interfaces import IAuthorizationPolicy @@ -36,6 +37,7 @@ from pyramid.interfaces import IRendererFactory from pyramid.interfaces import IRendererGlobalsFactory from pyramid.interfaces import IRequest from pyramid.interfaces import IRequestFactory +from pyramid.interfaces import IResponse from pyramid.interfaces import IRootFactory from pyramid.interfaces import IRouteRequest from pyramid.interfaces import IRoutesMapper @@ -82,7 +84,6 @@ from pyramid.traversal import traversal_path from pyramid.urldispatch import RoutesMapper from pyramid.util import DottedNameResolver from pyramid.view import render_view_to_response -from pyramid.view import is_response DEFAULT_RENDERERS = ( ('.mak', mako_renderer_factory), @@ -417,7 +418,8 @@ class Configurator(object): def _fix_registry(self): """ Fix up a ZCA component registry that is not a pyramid.registry.Registry by adding analogues of ``has_listeners``, - and ``notify`` through monkey-patching.""" + ``notify``, ``queryAdapterOrSelf``, and ``registerSelfAdapter`` + through monkey-patching.""" _registry = self.registry @@ -429,6 +431,24 @@ class Configurator(object): if not hasattr(_registry, 'has_listeners'): _registry.has_listeners = True + if not hasattr(_registry, 'queryAdapterOrSelf'): + def queryAdapterOrSelf(object, interface, name=u'', default=None): + provides = providedBy(object) + if not interface in provides: + return _registry.queryAdapter(object, interface, name=name, + default=default) + return object + _registry.queryAdapterOrSelf = queryAdapterOrSelf + + if not hasattr(_registry, 'registerSelfAdapter'): + def registerSelfAdapter(required=None, provided=None, + name=u'', info=u'', event=True): + return _registry.registerAdapter(lambda x: x, + required=required, + provided=provided, name=name, + info=info, event=event) + _registry.registerSelfAdapter = registerSelfAdapter + def _make_context(self, autocommit=False): context = PyramidConfigurationMachine() registerCommonDirectives(context) @@ -697,6 +717,9 @@ class Configurator(object): self._fix_registry() self._set_settings(settings) self._set_root_factory(root_factory) + # cope with WebOb response objects that aren't decorated with IResponse + from webob import Response as WebobResponse + registry.registerSelfAdapter((WebobResponse,), IResponse) debug_logger = self.maybe_dotted(debug_logger) if debug_logger is None: debug_logger = make_stream_logger('pyramid.debug', sys.stderr) @@ -2942,22 +2965,24 @@ class ViewDeriver(object): def _rendered_view(context, request): renderer = static_renderer - response = wrapped_view(context, request) - if not is_response(response): + result = wrapped_view(context, request) + registry = self.kw['registry'] + 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 = RendererHelper(name=renderer_name, package=self.kw.get('package'), - registry = self.kw['registry']) + registry = registry) if '__view__' in attrs: view_inst = attrs.pop('__view__') else: view_inst = getattr(wrapped_view, '__original_view__', wrapped_view) - return renderer.render_view(request, response, view_inst, - context) + response = renderer.render_view(request, result, view_inst, + context) return response return _rendered_view diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index b8ff2d4c9..dea7174fb 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -233,14 +233,6 @@ class IMultiDict(Interface): # docs-only interface dictionary. This is similar to the kind of dictionary often used to represent the variables in a web request. """ -class IResponder(Interface): - """ Adapter from IResponse to an IResponder. See :ref:`using_iresponder` - for usage details. New in Pyramid 1.1. - """ - def __call__(self, request, start_response): - """ Call the WSGI ``start_response`` callable passed as - ``start_response`` and return an ``app_iter``.""" - # internal interfaces class IRequest(Interface): diff --git a/pyramid/registry.py b/pyramid/registry.py index 37e230dc3..26f84d493 100644 --- a/pyramid/registry.py +++ b/pyramid/registry.py @@ -1,4 +1,5 @@ from zope.component.registry import Components +from zope.interface import providedBy from pyramid.interfaces import ISettings @@ -28,6 +29,24 @@ class Registry(Components, dict): self.has_listeners = True return result + def registerSelfAdapter(self, required=None, provided=None, name=u'', + info=u'', event=True): + # registerAdapter analogue which always returns the object itself + # when required is matched + return self.registerAdapter(lambda x: x, required=required, + provided=provided, name=name, + info=info, event=event) + + def queryAdapterOrSelf(self, object, interface, name=u'', default=None): + # queryAdapter analogue which returns the object if it implements + # the interface, otherwise it will return an adaptation to the + # interface + provides = providedBy(object) + if not interface in provides: + return self.queryAdapter(object, interface, name=name, + default=default) + return object + def registerHandler(self, *arg, **kw): result = Components.registerHandler(self, *arg, **kw) self.has_listeners = True diff --git a/pyramid/renderers.py b/pyramid/renderers.py index a6dce9b3a..6865067dd 100644 --- a/pyramid/renderers.py +++ b/pyramid/renderers.py @@ -316,9 +316,7 @@ class RendererHelper(object): 'context':context, 'request':request } - return self.render_to_response(response, system, - request=request) - + return self.render_to_response(response, system, request=request) def render(self, value, system_values, request=None): renderer = self.renderer diff --git a/pyramid/request.py b/pyramid/request.py index d387a0b2f..b69440ac6 100644 --- a/pyramid/request.py +++ b/pyramid/request.py @@ -5,12 +5,14 @@ from zope.interface.interface import InterfaceClass from webob import BaseRequest from pyramid.interfaces import IRequest +from pyramid.interfaces import IResponse from pyramid.interfaces import ISessionFactory from pyramid.interfaces import IResponseFactory from pyramid.exceptions import ConfigurationError from pyramid.decorator import reify from pyramid.response import Response +from pyramid.threadlocal import get_current_registry from pyramid.url import resource_url from pyramid.url import route_url from pyramid.url import static_url diff --git a/pyramid/response.py b/pyramid/response.py index 1d2ef296f..68496e386 100644 --- a/pyramid/response.py +++ b/pyramid/response.py @@ -4,4 +4,4 @@ from pyramid.interfaces import IResponse class Response(_Response): implements(IResponse) - + diff --git a/pyramid/router.py b/pyramid/router.py index 4d2750efb..48640b39d 100644 --- a/pyramid/router.py +++ b/pyramid/router.py @@ -1,5 +1,3 @@ -import warnings - from zope.interface import implements from zope.interface import providedBy @@ -14,7 +12,7 @@ from pyramid.interfaces import IRoutesMapper from pyramid.interfaces import ITraverser from pyramid.interfaces import IView from pyramid.interfaces import IViewClassifier -from pyramid.interfaces import IResponder +from pyramid.interfaces import IResponse from pyramid.events import ContextFound from pyramid.events import NewRequest @@ -61,7 +59,6 @@ class Router(object): logger = self.logger manager = self.threadlocal_manager request = None - responder = default_responder threadlocals = {'registry':registry, 'request':request} manager.push(threadlocals) @@ -159,7 +156,7 @@ class Router(object): msg = request.path_info raise HTTPNotFound(msg) else: - response = view_callable(context, request) + result = view_callable(context, request) # handle exceptions raised during root finding and view-exec except Exception, why: @@ -181,52 +178,26 @@ class Router(object): # repoze.bfg.message docs-deprecated in Pyramid 1.0 environ['repoze.bfg.message'] = msg - response = view_callable(why, request) + result = view_callable(why, request) # process the response + response = registry.queryAdapterOrSelf(result, IResponse) + if response is None: + raise ValueError( + 'Could not convert view return value "%s" into a ' + 'response' % (result,)) has_listeners and registry.notify(NewResponse(request,response)) if request.response_callbacks: request._process_response_callbacks(response) - responder = adapters.queryAdapter(response, IResponder) - if responder is None: - responder = default_responder(response) - finally: if request is not None and request.finished_callbacks: request._process_finished_callbacks() - return responder(request, start_response) + return response(request.environ, start_response) finally: manager.pop() -def default_responder(response): - def inner(request, start_response): - # __call__ is default 1.1 response API - call = getattr(response, '__call__', None) - if call is not None: - return call(request.environ, start_response) - # start 1.0 bw compat (use headerlist, app_iter, status) - try: - headers = response.headerlist - app_iter = response.app_iter - status = response.status - except AttributeError: - raise ValueError( - 'Non-response object returned from view ' - '(and no renderer): %r' % (response)) - start_response(status, headers) - warnings.warn( - 'As of Pyramid 1.1, an object used as a response object is ' - 'required to have a "__call__" method if an IResponder adapter is ' - 'not registered for its type. See "Deprecations" in "What\'s New ' - 'in Pyramid 1.1" within the general Pyramid documentation for ' - 'further details.', - DeprecationWarning, - 3) - return app_iter - return inner - diff --git a/pyramid/session.py b/pyramid/session.py index 5772c80d0..bb3226a1e 100644 --- a/pyramid/session.py +++ b/pyramid/session.py @@ -8,8 +8,6 @@ try: except ImportError: # pragma: no cover import pickle -from webob import Response - import base64 import binascii import hmac @@ -213,17 +211,7 @@ def UnencryptedCookieSessionFactoryConfig( 'Cookie value is too long to store (%s bytes)' % len(cookieval) ) - if hasattr(response, 'set_cookie'): - # ``response`` is a "real" webob response - set_cookie = response.set_cookie - else: - # ``response`` is not a "real" webob response, cope - def set_cookie(*arg, **kw): - tmp_response = Response() - tmp_response.set_cookie(*arg, **kw) - response.headerlist.append( - tmp_response.headerlist[-1]) - set_cookie( + response.set_cookie( self._cookie_name, value=cookieval, max_age = self._cookie_max_age, diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py index 703a2577c..49bfab396 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -289,27 +289,58 @@ class ConfiguratorTests(unittest.TestCase): result = config.absolute_asset_spec('templates') self.assertEqual(result, 'pyramid.tests:templates') - def test_setup_registry_fixed(self): - class DummyRegistry(object): - def subscribers(self, events, name): - self.events = events - return events - def registerUtility(self, *arg, **kw): - pass + def test__fix_registry_has_listeners(self): reg = DummyRegistry() config = self._makeOne(reg) - config.add_view = lambda *arg, **kw: False - config.setup_registry() + config._fix_registry() self.assertEqual(reg.has_listeners, True) + + def test__fix_registry_notify(self): + reg = DummyRegistry() + config = self._makeOne(reg) + config._fix_registry() self.assertEqual(reg.notify(1), None) self.assertEqual(reg.events, (1,)) + def test__fix_registry_queryAdapterOrSelf(self): + from zope.interface import Interface + class IFoo(Interface): + pass + class Foo(object): + implements(IFoo) + class Bar(object): + pass + adaptation = () + foo = Foo() + bar = Bar() + reg = DummyRegistry(adaptation) + config = self._makeOne(reg) + config._fix_registry() + self.assertTrue(reg.queryAdapterOrSelf(foo, IFoo) is foo) + self.assertTrue(reg.queryAdapterOrSelf(bar, IFoo) is adaptation) + + def test__fix_registry_registerSelfAdapter(self): + reg = DummyRegistry() + config = self._makeOne(reg) + config._fix_registry() + reg.registerSelfAdapter('required', 'provided', name='abc') + self.assertEqual(len(reg.adapters), 1) + args, kw = reg.adapters[0] + self.assertEqual(args[0]('abc'), 'abc') + self.assertEqual(kw, + {'info': u'', 'provided': 'provided', + 'required': 'required', 'name': 'abc', 'event': True}) + + def test_setup_registry_calls_fix_registry(self): + reg = DummyRegistry() + config = self._makeOne(reg) + config.add_view = lambda *arg, **kw: False + config.setup_registry() + self.assertEqual(reg.has_listeners, True) + def test_setup_registry_registers_default_exceptionresponse_view(self): from pyramid.interfaces import IExceptionResponse from pyramid.view import default_exceptionresponse_view - class DummyRegistry(object): - def registerUtility(self, *arg, **kw): - pass reg = DummyRegistry() config = self._makeOne(reg) views = [] @@ -318,6 +349,15 @@ class ConfiguratorTests(unittest.TestCase): self.assertEqual(views[0], ((default_exceptionresponse_view,), {'context':IExceptionResponse})) + def test_setup_registry_registers_default_webob_iresponse_adapter(self): + from webob import Response + from pyramid.interfaces import IResponse + config = self._makeOne() + config.setup_registry() + response = Response() + self.assertTrue( + config.registry.queryAdapter(response, IResponse) is response) + def test_setup_registry_explicit_notfound_trumps_iexceptionresponse(self): from zope.interface import implementedBy from pyramid.interfaces import IRequest @@ -5134,3 +5174,17 @@ def dummy_extend(config, discrim): def dummy_extend2(config, discrim): config.action(discrim, None, config.registry) +class DummyRegistry(object): + def __init__(self, adaptation=None): + self.utilities = [] + self.adapters = [] + self.adaptation = adaptation + def subscribers(self, events, name): + self.events = events + return events + def registerUtility(self, *arg, **kw): + self.utilities.append((arg, kw)) + def registerAdapter(self, *arg, **kw): + self.adapters.append((arg, kw)) + def queryAdapter(self, *arg, **kw): + return self.adaptation diff --git a/pyramid/tests/test_router.py b/pyramid/tests/test_router.py index 765a26751..5fd2cf01e 100644 --- a/pyramid/tests/test_router.py +++ b/pyramid/tests/test_router.py @@ -2,19 +2,6 @@ import unittest from pyramid import testing -def hide_warnings(wrapped): - import warnings - def wrapper(*arg, **kw): - warnings.filterwarnings('ignore') - try: - wrapped(*arg, **kw) - finally: - warnings.resetwarnings() - wrapper.__name__ = wrapped.__name__ - wrapper.__doc__ = wrapped.__doc__ - return wrapper - - class TestRouter(unittest.TestCase): def setUp(self): testing.setUp() @@ -249,7 +236,7 @@ class TestRouter(unittest.TestCase): self.assertTrue("view_name: ''" in message) self.assertTrue("subpath: []" in message) - def test_call_view_returns_nonresponse(self): + def test_call_view_returns_non_iresponse(self): from pyramid.interfaces import IViewClassifier context = DummyContext() self._registerTraverserFactory(context) @@ -260,6 +247,24 @@ class TestRouter(unittest.TestCase): start_response = DummyStartResponse() self.assertRaises(ValueError, router, environ, start_response) + def test_call_view_returns_adapted_response(self): + from pyramid.response import Response + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IResponse + context = DummyContext() + self._registerTraverserFactory(context) + environ = self._makeEnviron() + view = DummyView('abc') + self._registerView(view, '', IViewClassifier, None, None) + router = self._makeOne() + start_response = DummyStartResponse() + def make_response(s): + return Response(s) + router.registry.registerAdapter(make_response, (str,), IResponse) + app_iter = router(environ, start_response) + self.assertEqual(app_iter, ['abc']) + self.assertEqual(start_response.status, '200 OK') + def test_call_view_registered_nonspecific_default_path(self): from pyramid.interfaces import IViewClassifier context = DummyContext() @@ -464,37 +469,6 @@ class TestRouter(unittest.TestCase): exc_raised(NotImplementedError, router, environ, start_response) self.assertEqual(environ['called_back'], True) - def test_call_with_overridden_iresponder_factory(self): - from zope.interface import Interface - from zope.interface import directlyProvides - from pyramid.interfaces import IRequest - from pyramid.interfaces import IViewClassifier - from pyramid.interfaces import IResponder - context = DummyContext() - class IFoo(Interface): - pass - directlyProvides(context, IFoo) - self._registerTraverserFactory(context, subpath=['']) - class DummyResponder(object): - def __init__(self, response): - self.response = response - def __call__(self, request, start_response): - self.response.responder_used = True - return '123' - self.registry.registerAdapter(DummyResponder, (None,), - IResponder, name='') - response = DummyResponse('200 OK') - directlyProvides(response, IFoo) - def view(context, request): - return response - environ = self._makeEnviron() - self._registerView(view, '', IViewClassifier, IRequest, Interface) - router = self._makeOne() - start_response = DummyStartResponse() - result = router(environ, start_response) - self.assertTrue(response.responder_used) - self.assertEqual(result, '123') - def test_call_request_factory_raises(self): # making sure finally doesnt barf when a request cannot be created environ = self._makeEnviron() @@ -875,7 +849,7 @@ class TestRouter(unittest.TestCase): result = router(environ, start_response) self.assertEqual(result, ["Hello, world"]) - def test_exception_view_returns_non_response(self): + def test_exception_view_returns_non_iresponse(self): from pyramid.interfaces import IRequest from pyramid.interfaces import IViewClassifier from pyramid.interfaces import IExceptionViewClassifier @@ -1072,52 +1046,6 @@ class TestRouter(unittest.TestCase): start_response = DummyStartResponse() self.assertRaises(RuntimeError, router, environ, start_response) -class Test_default_responder(unittest.TestCase): - def _makeOne(self, response): - from pyramid.router import default_responder - return default_responder(response) - - def test_has_call(self): - response = DummyResponse() - response.app_iter = ['123'] - response.headerlist = [('a', '1')] - responder = self._makeOne(response) - request = DummyRequest({'a':'1'}) - start_response = DummyStartResponse() - app_iter = responder(request, start_response) - self.assertEqual(app_iter, response.app_iter) - self.assertEqual(start_response.status, response.status) - self.assertEqual(start_response.headers, response.headerlist) - self.assertEqual(response.environ, request.environ) - - @hide_warnings - def test_without_call_success(self): - response = DummyResponseWithoutCall() - response.app_iter = ['123'] - response.headerlist = [('a', '1')] - responder = self._makeOne(response) - request = DummyRequest({'a':'1'}) - start_response = DummyStartResponse() - app_iter = responder(request, start_response) - self.assertEqual(app_iter, response.app_iter) - self.assertEqual(start_response.status, response.status) - self.assertEqual(start_response.headers, response.headerlist) - - @hide_warnings - def test_without_call_exception(self): - response = DummyResponseWithoutCall() - del response.status - responder = self._makeOne(response) - request = DummyRequest({'a':'1'}) - start_response = DummyStartResponse() - self.assertRaises(ValueError, responder, request, start_response) - - -class DummyRequest(object): - def __init__(self, environ=None): - if environ is None: environ = {} - self.environ = environ - class DummyContext: pass @@ -1147,14 +1075,16 @@ class DummyStartResponse: self.status = status self.headers = headers -class DummyResponseWithoutCall: +from pyramid.interfaces import IResponse +from zope.interface import implements + +class DummyResponse(object): + implements(IResponse) headerlist = () app_iter = () + environ = None def __init__(self, status='200 OK'): self.status = status - -class DummyResponse(DummyResponseWithoutCall): - environ = None def __call__(self, environ, start_response): self.environ = environ diff --git a/pyramid/tests/test_session.py b/pyramid/tests/test_session.py index 17a437af6..5c6454a38 100644 --- a/pyramid/tests/test_session.py +++ b/pyramid/tests/test_session.py @@ -90,16 +90,8 @@ class TestUnencryptedCookieSession(unittest.TestCase): self.assertEqual(session._set_cookie(response), True) self.assertEqual(response.headerlist[-1][0], 'Set-Cookie') - def test__set_cookie_other_kind_of_response(self): - request = testing.DummyRequest() - request.exception = None - session = self._makeOne(request) - session['abc'] = 'x' - response = DummyResponse() - self.assertEqual(session._set_cookie(response), True) - self.assertEqual(len(response.headerlist), 1) - def test__set_cookie_options(self): + from pyramid.response import Response request = testing.DummyRequest() request.exception = None session = self._makeOne(request, @@ -110,10 +102,9 @@ class TestUnencryptedCookieSession(unittest.TestCase): cookie_httponly = True, ) session['abc'] = 'x' - response = DummyResponse() + response = Response() self.assertEqual(session._set_cookie(response), True) - self.assertEqual(len(response.headerlist), 1) - cookieval= response.headerlist[0][1] + cookieval= response.headerlist[-1][1] val, domain, path, secure, httponly = [x.strip() for x in cookieval.split(';')] self.assertTrue(val.startswith('abc=')) diff --git a/pyramid/tests/test_view.py b/pyramid/tests/test_view.py index b1d48b98b..0ea9a11a0 100644 --- a/pyramid/tests/test_view.py +++ b/pyramid/tests/test_view.py @@ -120,6 +120,19 @@ class RenderViewToIterableTests(BaseTest, unittest.TestCase): secure=True) self.assertEqual(iterable, ()) + def test_call_view_returns_iresponse_adaptable(self): + from pyramid.response import Response + request = self._makeRequest() + context = self._makeContext() + view = make_view('123') + self._registerView(request.registry, view, 'registered') + def str_response(s): + return Response(s) + request.registry.registerAdapter(str_response, (str,), IResponse) + iterable = self._callFUT(context, request, name='registered', + secure=True) + self.assertEqual(iterable, ['123']) + def test_call_view_registered_insecure_no_call_permissive(self): context = self._makeContext() request = self._makeRequest() @@ -536,9 +549,15 @@ def make_view(response): class DummyRequest: exception = None -class DummyResponse: - status = '200 OK' +from pyramid.interfaces import IResponse +from zope.interface import implements + +class DummyResponse(object): + implements(IResponse) headerlist = () + app_iter = () + status = '200 OK' + environ = None def __init__(self, body=None): if body is None: self.app_iter = () diff --git a/pyramid/view.py b/pyramid/view.py index 9a4be7580..a89df8859 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -4,6 +4,7 @@ import venusian from zope.interface import providedBy from zope.deprecation import deprecated +from pyramid.interfaces import IResponse from pyramid.interfaces import IRoutesMapper from pyramid.interfaces import IView from pyramid.interfaces import IViewClassifier @@ -100,6 +101,11 @@ def render_view_to_iterable(context, request, name='', secure=True): response = render_view_to_response(context, request, name, secure) if response is None: return None + try: + reg = request.registry + except AttributeError: + reg = get_current_registry() + response = reg.queryAdapterOrSelf(response, IResponse) return response.app_iter def render_view(context, request, name='', secure=True): -- cgit v1.2.3 From c209f8013189745983c628b1ddf3438858600a15 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 13 Jun 2011 06:25:58 -0400 Subject: name argument makes no sense here --- pyramid/config.py | 4 ++-- pyramid/registry.py | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/pyramid/config.py b/pyramid/config.py index fab75f56d..dbeb0788c 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -432,10 +432,10 @@ class Configurator(object): _registry.has_listeners = True if not hasattr(_registry, 'queryAdapterOrSelf'): - def queryAdapterOrSelf(object, interface, name=u'', default=None): + def queryAdapterOrSelf(object, interface, default=None): provides = providedBy(object) if not interface in provides: - return _registry.queryAdapter(object, interface, name=name, + return _registry.queryAdapter(object, interface, default=default) return object _registry.queryAdapterOrSelf = queryAdapterOrSelf diff --git a/pyramid/registry.py b/pyramid/registry.py index 26f84d493..4c6262ce1 100644 --- a/pyramid/registry.py +++ b/pyramid/registry.py @@ -37,14 +37,13 @@ class Registry(Components, dict): provided=provided, name=name, info=info, event=event) - def queryAdapterOrSelf(self, object, interface, name=u'', default=None): + def queryAdapterOrSelf(self, object, interface, default=None): # queryAdapter analogue which returns the object if it implements # the interface, otherwise it will return an adaptation to the # interface provides = providedBy(object) if not interface in provides: - return self.queryAdapter(object, interface, name=name, - default=default) + return self.queryAdapter(object, interface, default=default) return object def registerHandler(self, *arg, **kw): -- cgit v1.2.3 From 51fb07becd49a187da84542981f38f820efe8f8b Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 13 Jun 2011 23:04:45 -0400 Subject: flesh out IResponse interface --- pyramid/interfaces.py | 209 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 204 insertions(+), 5 deletions(-) diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index dea7174fb..fee8d549d 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -47,13 +47,212 @@ class IApplicationCreated(Interface): IWSGIApplicationCreatedEvent = IApplicationCreated # b /c class IResponse(Interface): - status = Attribute('WSGI status code of response') - headerlist = Attribute('List of response headers') - app_iter = Attribute('Iterable representing the response body') + """ Represents a WSGI response using the WebOb response interface. Some + attribute and method documentation of this interface references `RFC 2616 + `_. + + This interface is most famously implemented by + :class:`pyramid.response.Response` and the HTTP exception classes in + :mod:`pyramid.httpexceptions`.""" + + RequestClass = Attribute( + """ Alias for :class:`pyramid.request.Request` """) def __call__(environ, start_response): - """ WSGI call interface, should call the start_response callback - and should return an iterable """ + """ :term:`WSGI` call interface, should call the start_response + callback and should return an iterable""" + + accept_ranges = Attribute( + """Gets and sets and deletes the Accept-Ranges header. For more + information on Accept-Ranges see RFC 2616, section 14.5""") + + age = Attribute( + """Gets and sets and deletes the Age header. Converts using int. + For more information on Age see RFC 2616, section 14.6.""") + + allow = Attribute( + """Gets and sets and deletes the Allow header. Converts using + list. For more information on Allow see RFC 2616, Section 14.7.""") + + app_iter = Attribute( + """Returns the app_iter of the response. + + If body was set, this will create an app_iter from that body + (a single-item list)""") + + def app_iter_range(start, stop): + """ Return a new app_iter built from the response app_iter that + serves up only the given start:stop range. """ + + body = Attribute( + """The body of the response, as a str. This will read in the entire + app_iter if necessary.""") + + body_file = Attribute( + """A file-like object that can be used to write to the body. If you + passed in a list app_iter, that app_iter will be modified by writes.""") + + cache_control = Attribute( + """Get/set/modify the Cache-Control header (RFC 2616 section 14.9)""") + + cache_expires = Attribute( + """ Get/set the Cache-Control and Expires headers. This sets the + response to expire in the number of seconds passed when set. """) + + charset = Attribute( + """Get/set the charset (in the Content-Type)""") + + def conditional_response_app(environ, start_response): + """ Like the normal __call__ interface, but checks conditional + headers: + + - If-Modified-Since (304 Not Modified; only on GET, HEAD) + + - If-None-Match (304 Not Modified; only on GET, HEAD) + + - Range (406 Partial Content; only on GET, HEAD)""" + + content_disposition = Attribute( + """Gets and sets and deletes the Content-Disposition header. + For more information on Content-Disposition see RFC 2616 section + 19.5.1.""") + + content_encoding = Attribute( + """Gets and sets and deletes the Content-Encoding header. For more + information about Content-Encoding see RFC 2616 section 14.11.""") + + content_language = Attribute( + """Gets and sets and deletes the Content-Language header. Converts + using list. For more information about Content-Language see RFC 2616 + section 14.12.""") + + content_length = Attribute( + """Gets and sets and deletes the Content-Length header. For more + information on Content-Length see RFC 2616 section 14.17. + Converts using int. """) + + content_location = Attribute( + """Gets and sets and deletes the Content-Location header. For more + information on Content-Location see RFC 2616 section 14.14.""") + + content_md5 = Attribute( + """Gets and sets and deletes the Content-MD5 header. For more + information on Content-MD5 see RFC 2616 section 14.14.""") + + content_range = Attribute( + """Gets and sets and deletes the Content-Range header. For more + information on Content-Range see section 14.16. Converts using + ContentRange object.""") + + content_type = Attribute( + """Get/set the Content-Type header (or None), without the charset + or any parameters. If you include parameters (or ; at all) when + setting the content_type, any existing parameters will be deleted; + otherwise they will be preserved.""") + + content_type_params = Attribute( + """A dictionary of all the parameters in the content type. This is + not a view, set to change, modifications of the dict would not + be applied otherwise.""") + + def copy(): + """ Makes a copy of the response and returns the copy. """ + + date = Attribute( + """Gets and sets and deletes the Date header. For more information on + Date see RFC 2616 section 14.18. Converts using HTTP date.""") + + def delete_cookie(key, path='/', domain=None): + """ Delete a cookie from the client. Note that path and domain must + match how the cookie was originally set. This sets the cookie to the + empty string, and max_age=0 so that it should expire immediately. """ + + def encode_content(encoding='gzip', lazy=False): + """ Encode the content with the given encoding (only gzip and + identity are supported).""" + + environ = Attribute( + """Get/set the request environ associated with this response, + if any.""") + + etag = Attribute( + """ Gets and sets and deletes the ETag header. For more information + on ETag see RFC 2616 section 14.19. Converts using Entity tag.""") + + expires = Attribute( + """ Gets and sets and deletes the Expires header. For more + information on Expires see RFC 2616 section 14.21. Converts using + HTTP date.""") + + headerlist = Attribute( + """ The list of response headers. """) + + headers = Attribute( + """ The headers in a dictionary-like object """) + + last_modified = Attribute( + """ Gets and sets and deletes the Last-Modified header. For more + information on Last-Modified see RFC 2616 section 14.29. Converts + using HTTP date.""") + + location = Attribute( + """ Gets and sets and deletes the Location header. For more + information on Location see RFC 2616 section 14.30.""") + + def md5_etag(body=None, set_content_md5=False): + """ Generate an etag for the response object using an MD5 hash of the + body (the body parameter, or self.body if not given). Sets self.etag. + If set_content_md5 is True sets self.content_md5 as well """ + + def merge_cookies(resp): + """ Merge the cookies that were set on this response with the given + resp object (which can be any WSGI application). If the resp is a + webob.Response object, then the other object will be modified + in-place. """ + + pragma = Attribute( + """ Gets and sets and deletes the Pragma header. For more information + on Pragma see RFC 2616 section 14.32. """) + + request = Attribute( + """ Return the request associated with this response if any. """) + + retry_after = Attribute( + """ Gets and sets and deletes the Retry-After header. For more + information on Retry-After see RFC 2616 section 14.37. Converts + using HTTP date or delta seconds.""") + + server = Attribute( + """ Gets and sets and deletes the Server header. For more information + on Server see RFC216 section 14.38. """) + + def set_cookie(key, value='', max_age=None, path='/', domain=None, + secure=False, httponly=False, comment=None, expires=None, + overwrite=False): + """ Set (add) a cookie for the response """ + + status = Attribute( + """ The status string. """) + + status_int = Attribute( + """ The status as an integer """) + + unicode_body = Attribute( + """ Get/set the unicode value of the body (using the charset of + the Content-Type)""") + + def unset_cookie(key, strict=True): + """ Unset a cookie with the given name (remove it from the + response).""" + + vary = Attribute( + """Gets and sets and deletes the Vary header. For more information + on Vary see section 14.44. Converts using list.""") + + www_authenticate = Attribute( + """ Gets and sets and deletes the WWW-Authenticate header. For more + information on WWW-Authenticate see RFC 2616 section 14.47. Converts + using 'parse_auth' and 'serialize_auth'. """) class IException(Interface): # not an API """ An interface representing a generic exception """ -- cgit v1.2.3 From 8b1057beec5bf237e162ac3d3ab648cc7c22b392 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 13 Jun 2011 23:05:03 -0400 Subject: render nicer docs --- pyramid/httpexceptions.py | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/pyramid/httpexceptions.py b/pyramid/httpexceptions.py index a692380f8..f3df574a0 100644 --- a/pyramid/httpexceptions.py +++ b/pyramid/httpexceptions.py @@ -8,8 +8,8 @@ single HTTP status code. Each class is a subclass of the object. Each exception class has a status code according to `RFC 2068 -`: codes with 100-300 are not really -errors; 400's are client errors, and 500's are server errors. +`_: codes with 100-300 are not really +errors; 400s are client errors, and 500s are server errors. Exception HTTPException @@ -131,7 +131,7 @@ def _no_escape(value): return value class HTTPException(Exception): # bw compat - pass + """ Base class for all :term:`exception response` objects.""" class WSGIHTTPException(Response, HTTPException): implements(IExceptionResponse) @@ -271,16 +271,15 @@ ${body}''') class HTTPError(WSGIHTTPException): """ - base class for status codes in the 400's and 500's + base class for exceptions with status codes in the 400s and 500s This is an exception which indicates that an error has occurred, - and that any work in progress should not be committed. These are - typically results in the 400's and 500's. + and that any work in progress should not be committed. """ class HTTPRedirection(WSGIHTTPException): """ - base class for 300's status code (redirections) + base class for exceptions with status codes in the 300s (redirections) This is an abstract base class for 3xx redirection. It indicates that further action needs to be taken by the user agent in order @@ -290,7 +289,8 @@ class HTTPRedirection(WSGIHTTPException): class HTTPOk(WSGIHTTPException): """ - Base class for the 200's status code (successful responses) + Base class for exceptions with status codes in the 200s (successful + responses) code: 200, title: OK """ @@ -522,7 +522,7 @@ class HTTPTemporaryRedirect(_HTTPMove): class HTTPClientError(HTTPError): """ - base class for the 400's, where the client is in error + base class for the 400s, where the client is in error This is an error condition in which the client is presumed to be in-error. This is an expected problem, and thus is not considered @@ -882,12 +882,10 @@ class HTTPFailedDependency(HTTPClientError): class HTTPServerError(HTTPError): """ - base class for the 500's, where the server is in-error + base class for the 500s, where the server is in-error This is an error condition in which the server is presumed to be - in-error. This is usually unexpected, and thus requires a traceback; - ideally, opening a support ticket for the customer. Unless specialized, - this is a '500 Internal Server Error' + in-error. Unless specialized, this is a '500 Internal Server Error'. """ code = 500 title = 'Internal Server Error' -- cgit v1.2.3 From 3f4f67e76c2e1338377babd983e4071f52235132 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 13 Jun 2011 23:05:38 -0400 Subject: garden --- TODO.txt | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/TODO.txt b/TODO.txt index ca2433d3c..9d9bbf1eb 100644 --- a/TODO.txt +++ b/TODO.txt @@ -6,15 +6,12 @@ Must-Have - To subclass or not subclass http exceptions. -- Flesh out IResponse interface. Attributes Used internally: unicode_body / - body / content_type / charset / cache_expires / headers/ - default_content_type / set_cookie / headerlist / app_iter / status / - __call__. - - Deprecate view.is_response? - Move is_response to response.py? +- Create add_response_adapter Configurator API? + - Make sure registering IResponse adapter for webob.Response doesn't make it impossible to register an IResponse adapter for an interface that a webob.Response happens to implement. -- cgit v1.2.3 From 1a6fc7062f803b9f15b7677db9a9257a4f00bfcb Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 14 Jun 2011 02:36:07 -0400 Subject: - Added new add_response_adapter method to Configurator. - Fix Configurator docstring wrt exception responses. - Speed up registry.queryAdapterOrSelf --- CHANGES.txt | 6 ++++-- docs/api/config.rst | 2 ++ docs/narr/hooks.rst | 13 ++++--------- pyramid/config.py | 42 ++++++++++++++++++++++++++++++------------ pyramid/registry.py | 3 +-- pyramid/router.py | 2 +- pyramid/tests/test_config.py | 28 ++++++++++++++++++++++++++++ 7 files changed, 70 insertions(+), 26 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 5e8df1a0b..c7ca3794d 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -130,8 +130,10 @@ Features - It is now possible to return an arbitrary object from a Pyramid view callable even if a renderer is not used, as long as a suitable adapter to ``pyramid.interfaces.IResponse`` is registered for the type of the returned - object. See the section in the Hooks chapter of the documentation entitled - "Changing How Pyramid Treats View Responses". + object by using the new + ``pyramid.config.Configurator.add_response_adapter`` API. See the section + in the Hooks chapter of the documentation entitled "Changing How Pyramid + Treats View Responses". - The Pyramid router will now, by default, call the ``__call__`` method of WebOb response objects when returning a WSGI response. This means that, diff --git a/docs/api/config.rst b/docs/api/config.rst index 2b9d7bcef..274ee0292 100644 --- a/docs/api/config.rst +++ b/docs/api/config.rst @@ -40,6 +40,8 @@ .. automethod:: add_renderer(name, factory) + .. automethod:: add_response_adapter + .. automethod:: add_route .. automethod:: add_static_view(name, path, cache_max_age=3600, permission='__no_permission_required__') diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index 0db8ce5e0..8e5b93ed4 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -532,7 +532,7 @@ Changing How Pyramid Treats View Responses It is possible to control how Pyramid treats the result of calling a view callable on a per-type basis by using a hook involving -:class:`pyramid.interfaces.IResponse`. +:method:`pyramid.config.Configurator.add_response_adapter`. .. note:: This feature is new as of Pyramid 1.1. @@ -559,7 +559,6 @@ Response: .. code-block:: python :linenos: - from pyramid.interfaces import IResponse from pyramid.response import Response def string_response_adapter(s): @@ -568,8 +567,7 @@ Response: # config is an instance of pyramid.config.Configurator - config.registry.registerAdapter(string_response_adapter, (str,), - IResponse) + config.add_response_adapter(string_response_adapter, str) Likewise, if you want to be able to return a simplified kind of response object from view callables, you can use the IResponse hook to register an @@ -578,7 +576,6 @@ adapter to the more complex IResponse interface: .. code-block:: python :linenos: - from pyramid.interfaces import IResponse from pyramid.response import Response class SimpleResponse(object): @@ -591,14 +588,12 @@ adapter to the more complex IResponse interface: # config is an instance of pyramid.config.Configurator - config.registry.registerAdapter(simple_response_adapter, - (SimpleResponse,), - IResponse) + config.add_response_adapter(simple_response_adapter, SimpleResponse) If you want to implement your own Response object instead of using the :class:`pyramid.response.Response` object in any capacity at all, you'll have to make sure the object implements every attribute and method outlined in -:class:`pyramid.interfaces.IResponse` *and* you'll have to ensure that it's +:class:`pyramid.interfaces.IResponse` and you'll have to ensure that it's marked up with ``zope.interface.implements(IResponse)``: from pyramid.interfaces import IResponse diff --git a/pyramid/config.py b/pyramid/config.py index dbeb0788c..70b5cd639 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -19,7 +19,6 @@ from zope.interface import implementedBy from zope.interface.interfaces import IInterface from zope.interface import implements from zope.interface import classProvides -from zope.interface import providedBy from pyramid.interfaces import IAuthenticationPolicy from pyramid.interfaces import IAuthorizationPolicy @@ -260,15 +259,11 @@ class Configurator(object): If ``exceptionresponse_view`` is passed, it must be a :term:`view callable` or ``None``. If it is a view callable, it will be used as an - exception view callable when an :term:`exception response` is raised (any - object that implements the :class:`pyramid.interaces.IExceptionResponse` - interface, such as a :class:`pyramid.response.Response` object or any - ``HTTP`` exception documented in :mod:`pyramid.httpexceptions` as well as - exception responses raised via :func:`pyramid.exceptions.abort`, - :func:`pyramid.exceptions.redirect`). If ``exceptionresponse_view`` is - ``None``, no exception response view will be registered, and all raised - exception responses will be bubbled up to Pyramid's caller. By - default, the ``pyramid.exceptions.default_exceptionresponse_view`` + exception view callable when an :term:`exception response` is raised. If + ``exceptionresponse_view`` is ``None``, no exception response view will + be registered, and all raised exception responses will be bubbled up to + Pyramid's caller. By + default, the ``pyramid.httpexceptions.default_exceptionresponse_view`` function is used as the ``exceptionresponse_view``. This argument is new in Pyramid 1.1. """ @@ -433,8 +428,7 @@ class Configurator(object): if not hasattr(_registry, 'queryAdapterOrSelf'): def queryAdapterOrSelf(object, interface, default=None): - provides = providedBy(object) - if not interface in provides: + if not interface.providedBy(object): return _registry.queryAdapter(object, interface, default=default) return object @@ -893,6 +887,30 @@ class Configurator(object): self.action(None, register) return subscriber + @action_method + def add_response_adapter(self, adapter, type_or_iface): + """ When an object of type (or interface) ``type_or_iface`` is + returned from a view callable, Pyramid will use the adapter + ``adapter`` to convert it into an object which implements the + :class:`pyramid.interfaces.IResponse` interface. If ``adapter`` is + None, an object returned of type (or interface) ``type_or_iface`` + will itself be used as a response object. + + ``adapter`` and ``type_or_interface`` may be Python objects or + strings representing dotted names to importable Python global + objects. + + See :ref:`using_iresponse` for more information.""" + adapter = self.maybe_dotted(adapter) + type_or_iface = self.maybe_dotted(type_or_iface) + def register(): + reg = self.registry + if adapter is None: + reg.registerSelfAdapter((type_or_iface,), IResponse) + else: + reg.registerAdapter(adapter, (type_or_iface,), IResponse) + self.action((IResponse, type_or_iface), register) + def add_settings(self, settings=None, **kw): """Augment the ``settings`` argument passed in to the Configurator constructor with one or more 'setting' key/value pairs. A setting is diff --git a/pyramid/registry.py b/pyramid/registry.py index 4c6262ce1..5db0a11e2 100644 --- a/pyramid/registry.py +++ b/pyramid/registry.py @@ -41,8 +41,7 @@ class Registry(Components, dict): # queryAdapter analogue which returns the object if it implements # the interface, otherwise it will return an adaptation to the # interface - provides = providedBy(object) - if not interface in provides: + if not interface.providedBy(object): return self.queryAdapter(object, interface, default=default) return object diff --git a/pyramid/router.py b/pyramid/router.py index 48640b39d..8e33332df 100644 --- a/pyramid/router.py +++ b/pyramid/router.py @@ -185,7 +185,7 @@ class Router(object): if response is None: raise ValueError( 'Could not convert view return value "%s" into a ' - 'response' % (result,)) + 'response object' % (result,)) has_listeners and registry.notify(NewResponse(request,response)) diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py index 49bfab396..cc4a037c2 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -2628,6 +2628,34 @@ class ConfiguratorTests(unittest.TestCase): self.assertEqual(config.registry.getUtility(IRendererFactory, 'name'), pyramid.tests) + def test_add_response_adapter(self): + from pyramid.interfaces import IResponse + config = self._makeOne(autocommit=True) + class Adapter(object): + def __init__(self, other): + self.other = other + config.add_response_adapter(Adapter, str) + result = config.registry.queryAdapter('foo', IResponse) + self.assertTrue(result.other, 'foo') + + def test_add_response_adapter_self(self): + from pyramid.interfaces import IResponse + config = self._makeOne(autocommit=True) + class Adapter(object): + pass + config.add_response_adapter(None, Adapter) + adapter = Adapter() + result = config.registry.queryAdapter(adapter, IResponse) + self.assertTrue(result is adapter) + + def test_add_response_adapter_dottednames(self): + from pyramid.interfaces import IResponse + config = self._makeOne(autocommit=True) + config.add_response_adapter('pyramid.response.Response', + 'types.StringType') + result = config.registry.queryAdapter('foo', IResponse) + self.assertTrue(result.body, 'foo') + def test_scan_integration(self): import os from zope.interface import alsoProvides -- cgit v1.2.3 From 92099080859976ce78882de477ddc2c01bc880b2 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 14 Jun 2011 03:37:08 -0400 Subject: - New method named ``pyramid.request.Request.is_response``. This method should be used instead of the ``pyramid.view.is_response`` function, which has been deprecated. - Deprecated ``pyramid.view.is_response`` function in favor of (newly-added) ``pyramid.request.Request.is_response`` method. Determining if an object is truly a valid response object now requires access to the registry, which is only easily available as a request attribute. The ``pyramid.view.is_response`` function will still work until it is removed, but now may return an incorrect answer under some (very uncommon) circumstances. --- CHANGES.txt | 12 ++++++++++++ TODO.txt | 12 ------------ pyramid/request.py | 9 +++++++++ pyramid/tests/test_request.py | 28 ++++++++++++++++++++++++++++ pyramid/tests/test_view.py | 8 ++++++++ pyramid/view.py | 13 +++++++------ 6 files changed, 64 insertions(+), 18 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index c7ca3794d..ea4bedc7e 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -140,6 +140,10 @@ Features among other things, the ``conditional_response`` feature of WebOb response objects will now behave properly. +- New method named ``pyramid.request.Request.is_response``. This method + should be used instead of the ``pyramid.view.is_response`` function, which + has been deprecated. + Bug Fixes --------- @@ -270,6 +274,14 @@ Deprecations 1.0 and before). In a future version, these methods will be removed entirely. +- Deprecated ``pyramid.view.is_response`` function in favor of (newly-added) + ``pyramid.request.Request.is_response`` method. Determining if an object + is truly a valid response object now requires access to the registry, which + is only easily available as a request attribute. The + ``pyramid.view.is_response`` function will still work until it is removed, + but now may return an incorrect answer under some (very uncommon) + circumstances. + Behavior Changes ---------------- diff --git a/TODO.txt b/TODO.txt index 9d9bbf1eb..27ab9ffb5 100644 --- a/TODO.txt +++ b/TODO.txt @@ -6,18 +6,6 @@ Must-Have - To subclass or not subclass http exceptions. -- Deprecate view.is_response? - -- Move is_response to response.py? - -- Create add_response_adapter Configurator API? - -- Make sure registering IResponse adapter for webob.Response doesn't make it - impossible to register an IResponse adapter for an interface that a - webob.Response happens to implement. - -- Run whatsitdoing tests. - - Docs mention ``exception.args[0]`` as a way to get messages; check that this works. diff --git a/pyramid/request.py b/pyramid/request.py index b69440ac6..06dbddd29 100644 --- a/pyramid/request.py +++ b/pyramid/request.py @@ -323,6 +323,15 @@ class Request(BaseRequest): default=Response) return response_factory() + def is_response(self, ob): + """ Return ``True`` if the object passed as ``ob`` is a valid + response object, ``False`` otherwise.""" + registry = self.registry + adapted = registry.queryAdapterOrSelf(ob, IResponse) + if adapted is None: + return False + return adapted is ob + # b/c dict interface for "root factory" code that expects a bare # environ. Explicitly omitted dict methods: clear (unnecessary), # copy (implemented by WebOb), fromkeys (unnecessary); deprecated diff --git a/pyramid/tests/test_request.py b/pyramid/tests/test_request.py index e35856ce0..90c55b0f0 100644 --- a/pyramid/tests/test_request.py +++ b/pyramid/tests/test_request.py @@ -203,7 +203,35 @@ class TestRequest(unittest.TestCase): self.assertEqual(result, 'abc') self.assertEqual(info.args, ('pyramid.tests:static/foo.css', request, {}) ) + + def test_is_response_false(self): + request = self._makeOne({}) + request.registry = self.config.registry + self.assertEqual(request.is_response('abc'), False) + + def test_is_response_false_adapter_is_not_self(self): + from pyramid.interfaces import IResponse + request = self._makeOne({}) + request.registry = self.config.registry + def adapter(ob): + return object() + class Foo(object): + pass + foo = Foo() + request.registry.registerAdapter(adapter, (Foo,), IResponse) + self.assertEqual(request.is_response(foo), False) + def test_is_response_adapter_true(self): + from pyramid.interfaces import IResponse + request = self._makeOne({}) + request.registry = self.config.registry + class Foo(object): + pass + foo = Foo() + def adapter(ob): + return ob + request.registry.registerAdapter(adapter, (Foo,), IResponse) + self.assertEqual(request.is_response(foo), True) class TestRequestDeprecatedMethods(unittest.TestCase): def setUp(self): diff --git a/pyramid/tests/test_view.py b/pyramid/tests/test_view.py index 0ea9a11a0..b42224d4c 100644 --- a/pyramid/tests/test_view.py +++ b/pyramid/tests/test_view.py @@ -198,6 +198,14 @@ class RenderViewTests(BaseTest, unittest.TestCase): self.assertEqual(s, 'anotherview') class TestIsResponse(unittest.TestCase): + def setUp(self): + from zope.deprecation import __show__ + __show__.off() + + def tearDown(self): + from zope.deprecation import __show__ + __show__.on() + def _callFUT(self, *arg, **kw): from pyramid.view import is_response return is_response(*arg, **kw) diff --git a/pyramid/view.py b/pyramid/view.py index a89df8859..afa10fd0f 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -318,14 +318,15 @@ def is_response(ob): """ Return ``True`` if ``ob`` implements the interface implied by :ref:`the_response`. ``False`` if not. - .. note:: This isn't a true interface or subclass check. Instead, it's a - duck-typing check, as response objects are not obligated to be of a - particular class or provide any particular Zope interface.""" - - # response objects aren't obligated to implement a Zope interface, - # so we do it the hard way + .. warning:: This function is deprecated as of :app:`Pyramid` 1.1. New + code should not use it. Instead, new code should use the + :func:`pyramid.request.Request.is_response` method.""" if ( hasattr(ob, 'app_iter') and hasattr(ob, 'headerlist') and hasattr(ob, 'status') ): return True return False +deprecated( + 'is_response', + 'pyramid.view.is_response is deprecated as of Pyramid 1.1. Use ' + 'pyramid.request.Request.is_response instead.') -- cgit v1.2.3 From 53d11e7793317eee0f756b1e77b853ae7e1e6726 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 14 Jun 2011 04:31:26 -0400 Subject: - Move default app_iter generation logic into __call__ for exception responses. - Add note about why we've created a shadow exception hierarchy parallel to that of webob.exc. --- CHANGES.txt | 19 +++++++++ pyramid/httpexceptions.py | 43 ++++++++------------ pyramid/tests/test_httpexceptions.py | 77 +++++++++++++++++++++++++----------- 3 files changed, 89 insertions(+), 50 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index ea4bedc7e..a2976d1a2 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -320,6 +320,25 @@ Behavior Changes ``webob.response.Response`` (in order to directly implement the ``pyramid.interfaces.IResponse`` interface). +- The "exception response" objects importable from ``pyramid.httpexceptions`` + (e.g. ``HTTPNotFound``) are no longer just import aliases for classes that + actually live in ``webob.exc``. Instead, we've defined our own exception + classes within the module that mirror and emulate the ``webob.exc`` + exception response objects almost entirely. We do this in order to a) + allow the exception responses to subclass ``pyramid.response.Response``, + which speeds up response generation slightly due to the way the Pyramid + router works, b) allows us to provide alternate __call__ logic which also + speeds up response generation, c) allows the exception classes to provide + for the proper value of ``self.RequestClass`` (pyramid.request.Request), d) + allows us freedom from having to think about backwards compatibility code + present in ``webob.exc`` having to do with Python 2.4, which we no longer + support, e) We change the behavior of two classes (HTTPNotFound and + HTTPForbidden) in the module so that they can be used internally for + notfound and forbidden exceptions, f) allows us to influence the docstrings + of the exception classes to provide Pyramid-specific documentation, and g) + allows us to silence a stupid deprecation warning under Python 2.6 when the + response objects are used as exceptions (related to ``self.message``). + Backwards Incompatibilities --------------------------- diff --git a/pyramid/httpexceptions.py b/pyramid/httpexceptions.py index f3df574a0..6d689988e 100644 --- a/pyramid/httpexceptions.py +++ b/pyramid/httpexceptions.py @@ -143,21 +143,16 @@ class WSGIHTTPException(Response, HTTPException): # body_template_obj = Template('response template') # differences from webob.exc.WSGIHTTPException: - # - not a WSGI application (just a response) # - # as a result: - # - # - bases plaintext vs. html result on self.content_type rather than - # on request accept header - # - # - doesn't add request.environ keys to template substitutions unless - # 'request' is passed as a constructor keyword argument. + # - bases plaintext vs. html result on self.content_type rather than + # on request accept header # # - doesn't use "strip_tags" (${br} placeholder for
, no other html # in default body template) # - # - sets a default app_iter if no body, app_iter, or unicode_body is - # passed using a template (ala the replaced version's "generate_response") + # - sets a default app_iter onto self during __call__ using a template if + # no body, app_iter, or unicode_body is set onto the response (instead of + # the replaced version's "generate_response") # # - explicitly sets self.message = detail to prevent whining by Python # 2.6.5+ access of Exception.message @@ -213,18 +208,11 @@ ${body}''') if self.empty_body: del self.content_type del self.content_length - elif not ('unicode_body' in kw or 'body' in kw or 'app_iter' in kw): - self.app_iter = self._default_app_iter() def __str__(self): return self.detail or self.explanation - def _default_app_iter(self): - # This is a generator which defers the creation of the response page - # body; we use a generator because we want to ensure that if - # attributes of this response are changed after it is constructed, we - # use the changed values rather than the values at time of construction - # (e.g. self.content_type or self.charset). + def _default_app_iter(self, environ): html_comment = '' comment = self.comment or '' content_type = self.content_type or '' @@ -250,24 +238,27 @@ ${body}''') body_tmpl = self.body_template_obj if WSGIHTTPException.body_template_obj is not body_tmpl: # Custom template; add headers to args - environ = self.environ - if environ is not None: - for k, v in environ.items(): - args[k] = escape(v) + for k, v in environ.items(): + args[k] = escape(v) for k, v in self.headers.items(): args[k.lower()] = escape(v) body = body_tmpl.substitute(args) page = page_template.substitute(status=self.status, body=body) if isinstance(page, unicode): page = page.encode(self.charset) - yield page - raise StopIteration + return [page] @property - def exception(self): + def wsgi_response(self): # bw compat only return self - wsgi_response = exception # bw compat only + + exception = wsgi_response # bw compat only + + def __call__(self, environ, start_response): + if not self.body and not self.empty_body: + self.app_iter = self._default_app_iter(environ) + return Response.__call__(self, environ, start_response) class HTTPError(WSGIHTTPException): """ diff --git a/pyramid/tests/test_httpexceptions.py b/pyramid/tests/test_httpexceptions.py index 629bbe225..60bde460e 100644 --- a/pyramid/tests/test_httpexceptions.py +++ b/pyramid/tests/test_httpexceptions.py @@ -138,7 +138,9 @@ class TestWSGIHTTPException(unittest.TestCase): def test_ctor_with_body_sets_default_app_iter_html(self): cls = self._getTargetSubclass() exc = cls('detail') - body = list(exc.app_iter)[0] + environ = _makeEnviron() + start_response = DummyStartResponse() + body = list(exc(environ, start_response))[0] self.assertTrue(body.startswith('' in body) - def test_custom_body_template_no_environ(self): + def test_custom_body_template(self): cls = self._getTargetSubclass() - exc = cls(body_template='${location}', location='foo') + exc = cls(body_template='${REQUEST_METHOD}') exc.content_type = 'text/plain' - body = list(exc._default_app_iter())[0] - self.assertEqual(body, '200 OK\n\nfoo') - - def test_custom_body_template_with_environ(self): - cls = self._getTargetSubclass() - from pyramid.request import Request - request = Request.blank('/') - exc = cls(body_template='${REQUEST_METHOD}', request=request) - exc.content_type = 'text/plain' - body = list(exc._default_app_iter())[0] + environ = _makeEnviron() + start_response = DummyStartResponse() + body = list(exc(environ, start_response))[0] self.assertEqual(body, '200 OK\n\nGET') def test_body_template_unicode(self): - from pyramid.request import Request cls = self._getTargetSubclass() la = unicode('/La Pe\xc3\xb1a', 'utf-8') - request = Request.blank('/') - request.environ['unicodeval'] = la - exc = cls(body_template='${unicodeval}', request=request) + environ = _makeEnviron(unicodeval=la) + exc = cls(body_template='${unicodeval}') exc.content_type = 'text/plain' - body = list(exc._default_app_iter())[0] + start_response = DummyStartResponse() + body = list(exc(environ, start_response))[0] self.assertEqual(body, '200 OK\n\n/La Pe\xc3\xb1a') class TestRenderAllExceptionsWithoutArguments(unittest.TestCase): @@ -230,9 +244,11 @@ class TestRenderAllExceptionsWithoutArguments(unittest.TestCase): L = [] self.assertTrue(status_map) for v in status_map.values(): + environ = _makeEnviron() + start_response = DummyStartResponse() exc = v() exc.content_type = content_type - result = list(exc.app_iter)[0] + result = list(exc(environ, start_response))[0] if exc.empty_body: self.assertEqual(result, '') else: @@ -275,3 +291,16 @@ class TestHTTPForbidden(unittest.TestCase): class DummyRequest(object): exception = None +class DummyStartResponse(object): + def __call__(self, status, headerlist): + self.status = status + self.headerlist = headerlist + +def _makeEnviron(**kw): + environ = {'REQUEST_METHOD':'GET', + 'wsgi.url_scheme':'http', + 'SERVER_NAME':'localhost', + 'SERVER_PORT':'80'} + environ.update(kw) + return environ + -- cgit v1.2.3 From cecfc9e459166f3de13141954a61eaa2d6c905f2 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 14 Jun 2011 05:29:45 -0400 Subject: garden: --- TODO.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/TODO.txt b/TODO.txt index 27ab9ffb5..fb72d42b5 100644 --- a/TODO.txt +++ b/TODO.txt @@ -4,7 +4,8 @@ Pyramid TODOs Must-Have --------- -- To subclass or not subclass http exceptions. +- Copy exception templates from webob.exc into pyramid.httpexceptions and + ensure they all work. - Docs mention ``exception.args[0]`` as a way to get messages; check that this works. -- cgit v1.2.3 From 5484e3e9be61b82b55e6e1e94365cfb3cd4d3a94 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 14 Jun 2011 06:17:39 -0400 Subject: we no longer support 2.4 --- CHANGES.txt | 3 +++ tox.ini | 13 +------------ 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index c58ff755b..6a41f30fe 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -361,6 +361,9 @@ Behavior Changes Backwards Incompatibilities --------------------------- +- Pyramid no longer supports Python 2.4. Python 2.5 or better is required to + run Pyramid 1.1+. + - The Pyramid router now, by default, expects response objects returned from view callables to implement the ``pyramid.interfaces.IResponse`` interface. Unlike the Pyramid 1.0 version of this interface, objects which implement diff --git a/tox.ini b/tox.ini index 73a487ac8..e404b29f4 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] envlist = - py24,py25,py26,py27,jython,pypy,cover + py25,py26,py27,jython,pypy,cover [testenv] commands = @@ -11,17 +11,6 @@ deps = repoze.sphinx.autointerface virtualenv -[testenv:py24] -# Chameleon 2 doesnt work under py2.4 -commands = - python setup.py test -q -deps = - Sphinx - WebTest - repoze.sphinx.autointerface - virtualenv - Chameleon<=1.999 - [testenv:jython] commands = jython setup.py test -q -- cgit v1.2.3 From 4fd3e66d5ae48acf53534a21ebf900e74f714541 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 18 Jun 2011 23:09:53 -0400 Subject: fix rendering --- docs/narr/hooks.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index 8426f11fd..e5b85dfbf 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -532,7 +532,7 @@ Changing How Pyramid Treats View Responses It is possible to control how Pyramid treats the result of calling a view callable on a per-type basis by using a hook involving -:method:`pyramid.config.Configurator.add_response_adapter`. +:meth:`pyramid.config.Configurator.add_response_adapter`. .. note:: This feature is new as of Pyramid 1.1. -- cgit v1.2.3 From 1e5e3181206de1e61e8eb4bd595cffda5603a316 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 18 Jun 2011 23:14:29 -0400 Subject: move defense from changes to design defense document --- CHANGES.txt | 17 +++-------------- docs/designdefense.rst | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 14 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 6a41f30fe..9a25a6b04 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -343,20 +343,9 @@ Behavior Changes (e.g. ``HTTPNotFound``) are no longer just import aliases for classes that actually live in ``webob.exc``. Instead, we've defined our own exception classes within the module that mirror and emulate the ``webob.exc`` - exception response objects almost entirely. We do this in order to a) - allow the exception responses to subclass ``pyramid.response.Response``, - which speeds up response generation slightly due to the way the Pyramid - router works, b) allows us to provide alternate __call__ logic which also - speeds up response generation, c) allows the exception classes to provide - for the proper value of ``self.RequestClass`` (pyramid.request.Request), d) - allows us freedom from having to think about backwards compatibility code - present in ``webob.exc`` having to do with Python 2.4, which we no longer - support, e) We change the behavior of two classes (HTTPNotFound and - HTTPForbidden) in the module so that they can be used internally for - notfound and forbidden exceptions, f) allows us to influence the docstrings - of the exception classes to provide Pyramid-specific documentation, and g) - allows us to silence a stupid deprecation warning under Python 2.6 when the - response objects are used as exceptions (related to ``self.message``). + exception response objects almost entirely. See the "Design Defense" doc + section named "Pyramid Uses its Own HTTP Exception Classes" for more + information. Backwards Incompatibilities --------------------------- diff --git a/docs/designdefense.rst b/docs/designdefense.rst index 77711016d..cfd395dd9 100644 --- a/docs/designdefense.rst +++ b/docs/designdefense.rst @@ -1011,6 +1011,47 @@ which returns Zope3-security-proxy-wrapped objects for each traversed object (including the :term:`context` and the :term:`root`). This would have the effect of creating a more Zope3-like environment without much effort. +Pyramid Uses its Own HTTP Exception Class Hierarchy Rather ``webob.exc`` +------------------------------------------------------------------------ + +.. note:: This defense is new as of Pyramid 1.1. + +The HTTP exception classes defined in :mod:`pyramid.httpexceptions` are very +much like the ones defined in ``webob.exc`` +(e.g. :class:`~pyramid.httpexceptions.HTTPNotFound`, +:class:`~pyramid.httpexceptions.HTTPForbidden`, etc). They have the same +names and largely the same behavior and all have a very similar +implementation, but not the same identity. Here's why they have a separate +identity: + +- Making them separate allows the HTTP exception classes to subclass + :class:`pyramid.response.Response`. This speeds up response generation + slightly due to the way the Pyramid router works. The same speedup could + be gained by monkeypatching ``webob.response.Response`` but it's usually + the case that monkeypatching turns out to be evil and wrong. + +- Making them separate allows them to provide alternate ``__call__`` logic + which also speeds up response generation. + +- Making them separate allows the exception classes to provide for the proper + value of ``RequestClass`` (:class:`pyramid.request.Request`). + +- Making them separate allows us freedom from having to think about backwards + compatibility code present in ``webob.exc`` having to do with Python 2.4, + which we no longer support in Pyramid 1.1+. + +- We change the behavior of two classes + (:class:`~pyramid.httpexceptions.HTTPNotFound` and + :class:`~pyramid.httpexceptions.HTTPForbidden`) in the module so that they + can be used by Pyramid internally for notfound and forbidden exceptions. + +- Making them separate allows us to influence the docstrings of the exception + classes to provide Pyramid-specific documentation. + +- Making them separate allows us to silence a stupid deprecation warning + under Python 2.6 when the response objects are used as exceptions (related + to ``self.message``). + .. _simpler_traversal_model: Pyramid has Simpler Traversal Machinery than Does Zope -- cgit v1.2.3 From c8289eaadb144dbc743b40e61318ae39b6f504f2 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 19 Jun 2011 01:54:29 -0400 Subject: format deprecating warnings properly --- pyramid/testing.py | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/pyramid/testing.py b/pyramid/testing.py index 86276df1e..27942baf5 100644 --- a/pyramid/testing.py +++ b/pyramid/testing.py @@ -69,9 +69,9 @@ def registerDummySecurityPolicy(userid=None, groupids=(), permissive=True): deprecated('registerDummySecurityPolicy', 'The testing.registerDummySecurityPolicy API is deprecated as of ' - 'Pyramid 1.0. Instead use the' + 'Pyramid 1.0. Instead use the ' 'pyramid.config.Configurator.testing_securitypolicy ' - 'method in your unit and integration tests. ') + 'method in your unit and integration tests.') def registerResources(resources): """ Registers a dictionary of :term:`resource` objects that can be @@ -101,17 +101,17 @@ def registerResources(resources): deprecated('registerResources', 'The testing.registerResources API is deprecated as of ' - 'Pyramid 1.0. Instead use the' + 'Pyramid 1.0. Instead use the ' 'pyramid.config.Configurator.testing_resources ' - 'method in your unit and integration tests. ') + 'method in your unit and integration tests.') registerModels = registerResources deprecated('registerModels', 'The testing.registerModels API is deprecated as of ' - 'Pyramid 1.0. Instead use the' + 'Pyramid 1.0. Instead use the ' 'pyramid.config.Configurator.testing_resources ' - 'method in your unit and integration tests. ') + 'method in your unit and integration tests.') def registerEventListener(event_iface=None): """ Registers an :term:`event` listener (aka :term:`subscriber`) @@ -143,9 +143,9 @@ def registerEventListener(event_iface=None): deprecated('registerEventListener', 'The testing.registerEventListener API is deprecated as of ' - 'Pyramid 1.0. Instead use the' + 'Pyramid 1.0. Instead use the ' 'pyramid.config.Configurator.testing_add_subscriber ' - 'method in your unit and integration tests. ') + 'method in your unit and integration tests.') def registerTemplateRenderer(path, renderer=None): """ Register a template renderer at ``path`` (usually a relative @@ -171,9 +171,9 @@ def registerTemplateRenderer(path, renderer=None): deprecated('registerTemplateRenderer', 'The testing.registerTemplateRenderer API is deprecated as of ' - 'Pyramid 1.0. Instead use the' + 'Pyramid 1.0. Instead use the ' 'pyramid.config.Configurator.testing_add_renderer ' - 'method in your unit and integration tests. ') + 'method in your unit and integration tests.') # registerDummyRenderer is a deprecated alias that should never be removed # (too much usage in the wild) @@ -181,9 +181,9 @@ registerDummyRenderer = registerTemplateRenderer deprecated('registerDummyRenderer', 'The testing.registerDummyRenderer API is deprecated as of ' - 'Pyramid 1.0. Instead use the' + 'Pyramid 1.0. Instead use the ' 'pyramid.config.Configurator.testing_add_renderer ' - 'method in your unit and integration tests. ') + 'method in your unit and integration tests.') def registerView(name, result='', view=None, for_=(Interface, Interface), permission=None): @@ -228,9 +228,9 @@ def registerView(name, result='', view=None, for_=(Interface, Interface), deprecated('registerView', 'The registerView API is deprecated as of ' - 'Pyramid 1.0. Instead use the' + 'Pyramid 1.0. Instead use the ' 'pyramid.config.Configurator.add_view ' - 'method in your unit and integration tests. ') + 'method in your unit and integration tests.') def registerUtility(impl, iface=Interface, name=''): """ Register a ZCA utility component. @@ -257,10 +257,10 @@ def registerUtility(impl, iface=Interface, name=''): deprecated('registerUtility', 'The registerUtility API is deprecated as of ' - 'Pyramid 1.0. Instead use the' + 'Pyramid 1.0. Instead use the ' 'pyramid.registry.registerUtility method (via ' 'e.g. "config.registry.registerUtility(..)" ' - 'method in your unit and integration tests. ') + 'method in your unit and integration tests.') def registerAdapter(impl, for_=Interface, provides=Interface, name=''): """ Register a ZCA adapter component. @@ -296,10 +296,10 @@ def registerAdapter(impl, for_=Interface, provides=Interface, name=''): deprecated('registerAdapter', 'The registerAdapter API is deprecated as of ' - 'Pyramid 1.0. Instead use the' + 'Pyramid 1.0. Instead use the ' 'pyramid.registry.registerAdapter method (via ' 'e.g. "config.registry.registerAdapter(..)" ' - 'method in your unit and integration tests. ') + 'method in your unit and integration tests.') def registerSubscriber(subscriber, iface=Interface): """ Register a ZCA subscriber component. @@ -329,9 +329,9 @@ def registerSubscriber(subscriber, iface=Interface): deprecated('registerSubscriber', 'The testing.registerSubscriber API is deprecated as of ' - 'Pyramid 1.0. Instead use the' + 'Pyramid 1.0. Instead use the ' 'pyramid.config.Configurator.add_subscriber ' - 'method in your unit and integration tests. ') + 'method in your unit and integration tests.') def registerRoute(pattern, name, factory=None): """ Register a new :term:`route` using a pattern @@ -358,9 +358,9 @@ def registerRoute(pattern, name, factory=None): deprecated('registerRoute', 'The testing.registerRoute API is deprecated as of ' - 'Pyramid 1.0. Instead use the' + 'Pyramid 1.0. Instead use the ' 'pyramid.config.Configurator.add_route ' - 'method in your unit and integration tests. ') + 'method in your unit and integration tests.') def registerSettings(dictarg=None, **kw): """Register one or more 'setting' key/value pairs. A setting is @@ -390,9 +390,9 @@ def registerSettings(dictarg=None, **kw): deprecated('registerSettings', 'The testing.registerSettings API is deprecated as of ' - 'Pyramid 1.0. Instead use the' + 'Pyramid 1.0. Instead use the ' 'pyramid.config.Configurator.add_settings ' - 'method in your unit and integration tests. ') + 'method in your unit and integration tests.') class DummyRootFactory(object): __parent__ = None -- cgit v1.2.3 From d0a5f0654e0468f9d50a4c1b98f9d316253ad64d Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 19 Jun 2011 20:19:27 -0400 Subject: - Base exception response content type again on accept header. - The ``pyramid.httpexceptions`` classes named ``HTTPFound``, ``HTTPMultipleChoices``, ``HTTPMovedPermanently``, ``HTTPSeeOther``, ``HTTPUseProxy``, and ``HTTPTemporaryRedirect`` now accept ``location`` as their first positional argument rather than ``detail``. This means that you can do, e.g. ``return pyramid.httpexceptions.HTTPFound('http://foo')`` rather than ``return pyramid.httpexceptions.HTTPFound(location='http//foo')`` (the latter will of course continue to work). --- CHANGES.txt | 9 ++++++ TODO.txt | 2 ++ pyramid/httpexceptions.py | 63 +++++++++++++++++++----------------- pyramid/tests/test_httpexceptions.py | 19 ++++++----- 4 files changed, 55 insertions(+), 38 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 9a25a6b04..dd3673173 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -371,6 +371,15 @@ Backwards Incompatibilities it is basically intended to directly mirror the ``webob.Response`` API, which has many methods and attributes. +- The ``pyramid.httpexceptions`` classes named ``HTTPFound``, + ``HTTPMultipleChoices``, ``HTTPMovedPermanently``, ``HTTPSeeOther``, + ``HTTPUseProxy``, and ``HTTPTemporaryRedirect`` now accept ``location`` as + their first positional argument rather than ``detail``. This means that + you can do, e.g. ``return pyramid.httpexceptions.HTTPFound('http://foo')`` + rather than ``return + pyramid.httpexceptions.HTTPFound(location='http//foo')`` (the latter will + of course continue to work). + Dependencies ------------ diff --git a/TODO.txt b/TODO.txt index fb72d42b5..f24b49263 100644 --- a/TODO.txt +++ b/TODO.txt @@ -4,6 +4,8 @@ Pyramid TODOs Must-Have --------- +- Grep for IExceptionResponse, forgot what it does. + - Copy exception templates from webob.exc into pyramid.httpexceptions and ensure they all work. diff --git a/pyramid/httpexceptions.py b/pyramid/httpexceptions.py index 6d689988e..68c40b098 100644 --- a/pyramid/httpexceptions.py +++ b/pyramid/httpexceptions.py @@ -144,15 +144,10 @@ class WSGIHTTPException(Response, HTTPException): # differences from webob.exc.WSGIHTTPException: # - # - bases plaintext vs. html result on self.content_type rather than - # on request accept header - # # - doesn't use "strip_tags" (${br} placeholder for
, no other html # in default body template) # - # - sets a default app_iter onto self during __call__ using a template if - # no body, app_iter, or unicode_body is set onto the response (instead of - # the replaced version's "generate_response") + # - __call__ never generates a new Response, it always mutates self # # - explicitly sets self.message = detail to prevent whining by Python # 2.6.5+ access of Exception.message @@ -160,7 +155,7 @@ class WSGIHTTPException(Response, HTTPException): # - its base class of HTTPException is no longer a Python 2.4 compatibility # shim; it's purely a base class that inherits from Exception. This # implies that this class' ``exception`` property always returns - # ``self`` (only for bw compat at this point). + # ``self`` (it exists only for bw compat at this point). # # - documentation improvements (Pyramid-specific docstrings where necessary) # @@ -212,17 +207,19 @@ ${body}''') def __str__(self): return self.detail or self.explanation - def _default_app_iter(self, environ): + def _set_default_attrs(self, environ): html_comment = '' comment = self.comment or '' - content_type = self.content_type or '' - if 'html' in content_type: + accept = environ.get('HTTP_ACCEPT', '') + if accept and 'html' in accept or '*/*' in accept: + self.content_type = 'text/html' escape = _html_escape page_template = self.html_template_obj br = '
' if comment: html_comment = '' % escape(comment) else: + self.content_type = 'text/plain' escape = _no_escape page_template = self.plain_template_obj br = '\n' @@ -246,7 +243,7 @@ ${body}''') page = page_template.substitute(status=self.status, body=body) if isinstance(page, unicode): page = page.encode(self.charset) - return [page] + self.app_iter = [page] @property def wsgi_response(self): @@ -256,8 +253,15 @@ ${body}''') exception = wsgi_response # bw compat only def __call__(self, environ, start_response): + # differences from webob.exc.WSGIHTTPException + # + # - does not try to deal with HEAD requests + # + # - does not manufacture a new response object when generating + # the default response + # if not self.body and not self.empty_body: - self.app_iter = self._default_app_iter(environ) + self._set_default_attrs(environ) return Response.__call__(self, environ, start_response) class HTTPError(WSGIHTTPException): @@ -388,23 +392,25 @@ class _HTTPMove(HTTPRedirection): """ # differences from webob.exc._HTTPMove: # - # - not a wsgi app - # # - ${location} isn't wrapped in an
tag in body # # - location keyword arg defaults to '' # + # - location isn't prepended with req.path_url when adding it as + # a header + # + # - ``location`` is first keyword (and positional) argument + # # - ``add_slash`` argument is no longer accepted: code that passes # add_slash argument to the constructor will receive an exception. explanation = 'The resource has been moved to' body_template_obj = Template('''\ -${explanation} ${location}; -you should be redirected automatically. +${explanation} ${location}; you should be redirected automatically. ${detail} ${html_comment}''') - def __init__(self, detail=None, headers=None, comment=None, - body_template=None, location='', **kw): + def __init__(self, location='', detail=None, headers=None, comment=None, + body_template=None, **kw): super(_HTTPMove, self).__init__( detail=detail, headers=headers, comment=comment, body_template=body_template, location=location, **kw) @@ -637,10 +643,12 @@ class HTTPMethodNotAllowed(HTTPClientError): """ # differences from webob.exc.HTTPMethodNotAllowed: # - # - body_template_obj not overridden (it tried to use request environ's - # REQUEST_METHOD) + # - body_template_obj uses ${br} instead of
code = 405 title = 'Method Not Allowed' + body_template_obj = Template('''\ +The method ${REQUEST_METHOD} is not allowed for this resource. ${br}${br} +${detail}''') class HTTPNotAcceptable(HTTPClientError): """ @@ -655,8 +663,7 @@ class HTTPNotAcceptable(HTTPClientError): """ # differences from webob.exc.HTTPNotAcceptable: # - # - body_template_obj not overridden (it tried to use request environ's - # HTTP_ACCEPT) + # - "template" attribute left off (useless, bug in webob?) code = 406 title = 'Not Acceptable' @@ -782,8 +789,7 @@ class HTTPUnsupportedMediaType(HTTPClientError): """ # differences from webob.exc.HTTPUnsupportedMediaType: # - # - body_template_obj not overridden (it tried to use request environ's - # CONTENT_TYPE) + # - "template_obj" attribute left off (useless, bug in webob?) code = 415 title = 'Unsupported Media Type' @@ -898,8 +904,7 @@ class HTTPNotImplemented(HTTPServerError): """ # differences from webob.exc.HTTPNotAcceptable: # - # - body_template_obj not overridden (it tried to use request environ's - # REQUEST_METHOD) + # - "template" attr left off (useless, bug in webob?) code = 501 title = 'Not Implemented' @@ -992,6 +997,7 @@ def default_exceptionresponse_view(context, request): return context status_map={} +code = None for name, value in globals().items(): if (isinstance(value, (type, types.ClassType)) and issubclass(value, HTTPException) @@ -999,7 +1005,4 @@ for name, value in globals().items(): code = getattr(value, 'code', None) if code: status_map[code] = value -del name, value - - - +del name, value, code diff --git a/pyramid/tests/test_httpexceptions.py b/pyramid/tests/test_httpexceptions.py index 60bde460e..644051fcb 100644 --- a/pyramid/tests/test_httpexceptions.py +++ b/pyramid/tests/test_httpexceptions.py @@ -139,6 +139,7 @@ class TestWSGIHTTPException(unittest.TestCase): cls = self._getTargetSubclass() exc = cls('detail') environ = _makeEnviron() + environ['HTTP_ACCEPT'] = 'text/html' start_response = DummyStartResponse() body = list(exc(environ, start_response))[0] self.assertTrue(body.startswith('' in body) + + def test__default_app_iter_with_comment_html2(self): + cls = self._getTargetSubclass() + exc = cls(comment='comment & comment') + environ = _makeEnviron() + environ['HTTP_ACCEPT'] = 'text/html' start_response = DummyStartResponse() body = list(exc(environ, start_response))[0] self.assertTrue('' in body) @@ -222,7 +227,6 @@ class TestWSGIHTTPException(unittest.TestCase): def test_custom_body_template(self): cls = self._getTargetSubclass() exc = cls(body_template='${REQUEST_METHOD}') - exc.content_type = 'text/plain' environ = _makeEnviron() start_response = DummyStartResponse() body = list(exc(environ, start_response))[0] @@ -233,7 +237,6 @@ class TestWSGIHTTPException(unittest.TestCase): la = unicode('/La Pe\xc3\xb1a', 'utf-8') environ = _makeEnviron(unicodeval=la) exc = cls(body_template='${unicodeval}') - exc.content_type = 'text/plain' start_response = DummyStartResponse() body = list(exc(environ, start_response))[0] self.assertEqual(body, '200 OK\n\n/La Pe\xc3\xb1a') -- cgit v1.2.3 From 08ffc90d071214820cb4e3514925986f796e8780 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 19 Jun 2011 20:26:41 -0400 Subject: coverage --- pyramid/tests/test_view.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/pyramid/tests/test_view.py b/pyramid/tests/test_view.py index b42224d4c..ce00454fc 100644 --- a/pyramid/tests/test_view.py +++ b/pyramid/tests/test_view.py @@ -1,14 +1,15 @@ import unittest import sys -from pyramid.testing import cleanUp +from pyramid.testing import setUp +from pyramid.testing import tearDown class BaseTest(object): def setUp(self): - cleanUp() + self.config = setUp() def tearDown(self): - cleanUp() + tearDown() def _registerView(self, reg, app, name): from pyramid.interfaces import IRequest @@ -156,6 +157,18 @@ class RenderViewToIterableTests(BaseTest, unittest.TestCase): secure=False) self.assertEqual(iterable, ['anotherview']) + def test_call_request_has_no_registry(self): + request = self._makeRequest() + del request.registry + registry = self.config.registry + context = self._makeContext() + response = DummyResponse() + view = make_view(response) + self._registerView(registry, view, 'registered') + iterable = self._callFUT(context, request, name='registered', + secure=True) + self.assertEqual(iterable, ()) + class RenderViewTests(BaseTest, unittest.TestCase): def _callFUT(self, *arg, **kw): from pyramid.view import render_view @@ -244,10 +257,10 @@ class TestIsResponse(unittest.TestCase): class TestViewConfigDecorator(unittest.TestCase): def setUp(self): - cleanUp() + setUp() def tearDown(self): - cleanUp() + tearDown() def _getTargetClass(self): from pyramid.view import view_config -- cgit v1.2.3 From 366a5ce3f0c7ac1c02b58523721ab7dff2be133f Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 19 Jun 2011 21:31:28 -0400 Subject: failUnless -> assertTrue to appease the unittest naming nazis --- pyramid/tests/test_config.py | 6 +++--- pyramid/tests/test_httpexceptions.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py index cc4a037c2..2e223076d 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -211,7 +211,7 @@ class ConfiguratorTests(unittest.TestCase): view = self._getViewCallable(config, ctx_iface=IExceptionResponse, request_iface=IRequest) - self.failUnless(view is default_exceptionresponse_view) + self.assertTrue(view is default_exceptionresponse_view) def test_ctor_exceptionresponse_view_None(self): from pyramid.interfaces import IExceptionResponse @@ -220,7 +220,7 @@ class ConfiguratorTests(unittest.TestCase): view = self._getViewCallable(config, ctx_iface=IExceptionResponse, request_iface=IRequest) - self.failUnless(view is None) + self.assertTrue(view is None) def test_ctor_exceptionresponse_view_custom(self): from pyramid.interfaces import IExceptionResponse @@ -230,7 +230,7 @@ class ConfiguratorTests(unittest.TestCase): view = self._getViewCallable(config, ctx_iface=IExceptionResponse, request_iface=IRequest) - self.failUnless(view is exceptionresponse_view) + self.assertTrue(view is exceptionresponse_view) def test_with_package_module(self): from pyramid.tests import test_configuration diff --git a/pyramid/tests/test_httpexceptions.py b/pyramid/tests/test_httpexceptions.py index 644051fcb..9ea9c76e4 100644 --- a/pyramid/tests/test_httpexceptions.py +++ b/pyramid/tests/test_httpexceptions.py @@ -75,22 +75,22 @@ class TestWSGIHTTPException(unittest.TestCase): def test_implements_IResponse(self): from pyramid.interfaces import IResponse cls = self._getTargetClass() - self.failUnless(IResponse.implementedBy(cls)) + self.assertTrue(IResponse.implementedBy(cls)) def test_provides_IResponse(self): from pyramid.interfaces import IResponse inst = self._getTargetClass()() - self.failUnless(IResponse.providedBy(inst)) + self.assertTrue(IResponse.providedBy(inst)) def test_implements_IExceptionResponse(self): from pyramid.interfaces import IExceptionResponse cls = self._getTargetClass() - self.failUnless(IExceptionResponse.implementedBy(cls)) + self.assertTrue(IExceptionResponse.implementedBy(cls)) def test_provides_IExceptionResponse(self): from pyramid.interfaces import IExceptionResponse inst = self._getTargetClass()() - self.failUnless(IExceptionResponse.providedBy(inst)) + self.assertTrue(IExceptionResponse.providedBy(inst)) def test_ctor_sets_detail(self): exc = self._makeOne('message') -- cgit v1.2.3 From 4275ebb943e5549ceb51577b2e5b1ae7a3ee5b20 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 19 Jun 2011 21:43:46 -0400 Subject: - Copy exception templates from webob.exc into pyramid.httpexceptions and ensure they all work. --- TODO.txt | 3 --- pyramid/tests/test_httpexceptions.py | 30 +++++++++++++++++++++++++++++- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/TODO.txt b/TODO.txt index f24b49263..adefbc573 100644 --- a/TODO.txt +++ b/TODO.txt @@ -6,9 +6,6 @@ Must-Have - Grep for IExceptionResponse, forgot what it does. -- Copy exception templates from webob.exc into pyramid.httpexceptions and - ensure they all work. - - Docs mention ``exception.args[0]`` as a way to get messages; check that this works. diff --git a/pyramid/tests/test_httpexceptions.py b/pyramid/tests/test_httpexceptions.py index 9ea9c76e4..705b988e6 100644 --- a/pyramid/tests/test_httpexceptions.py +++ b/pyramid/tests/test_httpexceptions.py @@ -278,6 +278,19 @@ class Test_HTTPMove(unittest.TestCase): exc = self._makeOne(location='foo') self.assertEqual(exc.location, 'foo') + def test_it_location_firstarg(self): + exc = self._makeOne('foo') + self.assertEqual(exc.location, 'foo') + + def test_it_call_with_default_body_tmpl(self): + exc = self._makeOne(location='foo') + environ = _makeEnviron() + start_response = DummyStartResponse() + app_iter = exc(environ, start_response) + self.assertEqual(app_iter[0], + ('None None\n\nThe resource has been moved to foo; ' + 'you should be redirected automatically.\n\n')) + class TestHTTPForbidden(unittest.TestCase): def _makeOne(self, *arg, **kw): from pyramid.httpexceptions import HTTPForbidden @@ -290,7 +303,22 @@ class TestHTTPForbidden(unittest.TestCase): def test_it_result_passed(self): exc = self._makeOne(result='foo') self.assertEqual(exc.result, 'foo') - + +class TestHTTPMethodNotAllowed(unittest.TestCase): + def _makeOne(self, *arg, **kw): + from pyramid.httpexceptions import HTTPMethodNotAllowed + return HTTPMethodNotAllowed(*arg, **kw) + + def test_it_with_default_body_tmpl(self): + exc = self._makeOne() + environ = _makeEnviron() + start_response = DummyStartResponse() + app_iter = exc(environ, start_response) + self.assertEqual(app_iter[0], + ('405 Method Not Allowed\n\nThe method GET is not ' + 'allowed for this resource. \n\n\n')) + + class DummyRequest(object): exception = None -- cgit v1.2.3 From 22bac63ecd35deac5f580c2d081b11c2caea4045 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 20 Jun 2011 00:37:34 -0400 Subject: note backwards incompat related to ISettings --- CHANGES.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index dd3673173..30001a7b5 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -563,6 +563,11 @@ Deprecations Backwards Incompatibilities --------------------------- +- Using ``testing.setUp`` now registers an ISettings utility as a side + effect. Some test code which queries for this utility after + ``testing.setUp`` via queryAdapter will expect a return value of ``None``. + This code will need to be changed. + - When a ``pyramid.exceptions.Forbidden`` error is raised, its status code now ``403 Forbidden``. It was previously ``401 Unauthorized``, for backwards compatibility purposes with ``repoze.bfg``. This change will -- cgit v1.2.3 From d69ae60b9a195c7cb72122b59335ba886bfffe50 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 20 Jun 2011 00:37:59 -0400 Subject: - Register the default exception view for context of webob.exc.WSGIHTTPException (convenience). - Use ``exc.message`` in docs rather than ``exc.args[0]`` now that we control this. --- TODO.txt | 5 ----- docs/narr/hooks.rst | 7 ++++--- pyramid/config.py | 3 +++ pyramid/httpexceptions.py | 2 +- pyramid/tests/test_config.py | 3 +++ 5 files changed, 11 insertions(+), 9 deletions(-) diff --git a/TODO.txt b/TODO.txt index adefbc573..8a014a245 100644 --- a/TODO.txt +++ b/TODO.txt @@ -4,11 +4,6 @@ Pyramid TODOs Must-Have --------- -- Grep for IExceptionResponse, forgot what it does. - -- Docs mention ``exception.args[0]`` as a way to get messages; check that - this works. - - Deprecate response_foo attrs on request at attribute set time rather than lookup time. diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index e5b85dfbf..1c8a64fd7 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -59,7 +59,7 @@ Here's some sample code that implements a minimal NotFound view callable: :term:`request`. The ``exception`` attribute of the request will be an instance of the :exc:`~pyramid.httpexceptions.HTTPNotFound` exception that caused the not found view to be called. The value of - ``request.exception.args[0]`` will be a value explaining why the not found + ``request.exception.message`` will be a value explaining why the not found error was raised. This message will be different when the ``debug_notfound`` environment setting is true than it is when it is false. @@ -125,8 +125,9 @@ Here's some sample code that implements a minimal forbidden view: :term:`request`. The ``exception`` attribute of the request will be an instance of the :exc:`~pyramid.httpexceptions.HTTPForbidden` exception that caused the forbidden view to be called. The value of - ``request.exception.args[0]`` will be a value explaining why the forbidden - was raised. This message will be different when the + ``request.exception.message`` will be a value explaining why the forbidden + was raised and ``request.exception.result`` will be extended information + about the forbidden exception. These messages will be different when the ``debug_authorization`` environment setting is true than it is when it is false. diff --git a/pyramid/config.py b/pyramid/config.py index 70b5cd639..1e4cbb350 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -713,6 +713,8 @@ class Configurator(object): self._set_root_factory(root_factory) # cope with WebOb response objects that aren't decorated with IResponse from webob import Response as WebobResponse + # cope with WebOb exc objects not decoratored with IExceptionResponse + from webob.exc import WSGIHTTPException as WebobWSGIHTTPException registry.registerSelfAdapter((WebobResponse,), IResponse) debug_logger = self.maybe_dotted(debug_logger) if debug_logger is None: @@ -726,6 +728,7 @@ class Configurator(object): if exceptionresponse_view is not None: exceptionresponse_view = self.maybe_dotted(exceptionresponse_view) self.add_view(exceptionresponse_view, context=IExceptionResponse) + self.add_view(exceptionresponse_view,context=WebobWSGIHTTPException) if locale_negotiator: locale_negotiator = self.maybe_dotted(locale_negotiator) registry.registerUtility(locale_negotiator, ILocaleNegotiator) diff --git a/pyramid/httpexceptions.py b/pyramid/httpexceptions.py index 68c40b098..b86e4badf 100644 --- a/pyramid/httpexceptions.py +++ b/pyramid/httpexceptions.py @@ -994,7 +994,7 @@ def default_exceptionresponse_view(context, request): # config.set_notfound_view or config.set_forbidden_view # instead of as a proper exception view context = request.exception or context - return context + return context # assumed to be an IResponse status_map={} code = None diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py index 2e223076d..12146895d 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -339,6 +339,7 @@ class ConfiguratorTests(unittest.TestCase): self.assertEqual(reg.has_listeners, True) def test_setup_registry_registers_default_exceptionresponse_view(self): + from webob.exc import WSGIHTTPException from pyramid.interfaces import IExceptionResponse from pyramid.view import default_exceptionresponse_view reg = DummyRegistry() @@ -348,6 +349,8 @@ class ConfiguratorTests(unittest.TestCase): config.setup_registry() self.assertEqual(views[0], ((default_exceptionresponse_view,), {'context':IExceptionResponse})) + self.assertEqual(views[1], ((default_exceptionresponse_view,), + {'context':WSGIHTTPException})) def test_setup_registry_registers_default_webob_iresponse_adapter(self): from webob import Response -- cgit v1.2.3 From f8f08bac0ea9edcac40fae2b3ad56e6a1ac7f47f Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 20 Jun 2011 00:57:30 -0400 Subject: responsecode -> exception_response --- CHANGES.txt | 10 +++++----- docs/api/httpexceptions.rst | 2 +- docs/narr/views.rst | 19 +++++++++++-------- docs/whatsnew-1.1.rst | 13 ++++++------- pyramid/httpexceptions.py | 2 +- pyramid/tests/test_httpexceptions.py | 6 +++--- 6 files changed, 27 insertions(+), 25 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 30001a7b5..ba392c7c6 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -39,10 +39,10 @@ Documentation - Added API docs for ``pyramid.authentication.SessionAuthenticationPolicy``. -- Added API docs for ``pyramid.httpexceptions.responsecode``. +- Added API docs for ``pyramid.httpexceptions.exception_response``. - Added "HTTP Exceptions" section to Views narrative chapter including a - description of ``pyramid.httpexceptions.responsecode``. + description of ``pyramid.httpexceptions.exception_response``. Features -------- @@ -121,9 +121,9 @@ Features within view code; when raised, this exception view will render the exception to a response. -- A function named ``pyramid.httpexceptions.responsecode`` is a shortcut that - can be used to create HTTP exception response objects using an HTTP integer - status code. +- A function named ``pyramid.httpexceptions.exception_response`` is a + shortcut that can be used to create HTTP exception response objects using + an HTTP integer status code. - The Configurator now accepts an additional keyword argument named ``exceptionresponse_view``. By default, this argument is populated with a diff --git a/docs/api/httpexceptions.rst b/docs/api/httpexceptions.rst index 325d5af03..89be17a2d 100644 --- a/docs/api/httpexceptions.rst +++ b/docs/api/httpexceptions.rst @@ -11,7 +11,7 @@ integer "401" maps to :class:`pyramid.httpexceptions.HTTPUnauthorized`). - .. autofunction:: responsecode + .. autofunction:: exception_response .. autoclass:: HTTPException diff --git a/docs/narr/views.rst b/docs/narr/views.rst index e3d0a37e5..cbd8fcfb7 100644 --- a/docs/narr/views.rst +++ b/docs/narr/views.rst @@ -300,27 +300,30 @@ An HTTP exception, instead of being raised, can alternately be *returned* return HTTPUnauthorized() A shortcut for creating an HTTP exception is the -:func:`pyramid.httpexceptions.responsecode` function. This function accepts -an HTTP status code and returns the corresponding HTTP exception. For -example, instead of importing and constructing a +:func:`pyramid.httpexceptions.exception_response` function. This function +accepts an HTTP status code and returns the corresponding HTTP exception. +For example, instead of importing and constructing a :class:`~pyramid.httpexceptions.HTTPUnauthorized` response object, you can -use the :func:`~pyramid.httpexceptions.responsecode` function to construct -and return the same object. +use the :func:`~pyramid.httpexceptions.exception_response` function to +construct and return the same object. .. code-block:: python :linenos: - from pyramid.httpexceptions import responsecode + from pyramid.httpexceptions import exception_response def aview(request): - raise responsecode(401) + raise exception_response(401) This is the case because ``401`` is the HTTP status code for "HTTP -Unauthorized". Therefore, ``raise responsecode(401)`` is functionally +Unauthorized". Therefore, ``raise exception_response(401)`` is functionally equivalent to ``raise HTTPUnauthorized()``. Documentation which maps each HTTP response code to its purpose and its associated HTTP exception object is provided within :mod:`pyramid.httpexceptions`. +.. note:: The :func:`~pyramid.httpexceptions.exception_response` function is + new as of Pyramid 1.1. + How Pyramid Uses HTTP Exceptions ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/whatsnew-1.1.rst b/docs/whatsnew-1.1.rst index 172a20343..cee701b49 100644 --- a/docs/whatsnew-1.1.rst +++ b/docs/whatsnew-1.1.rst @@ -18,8 +18,7 @@ The major feature additions in Pyramid 1.1 are: - Support for "static" routes. -- Default HTTP exception view and associated ``redirect`` and ``abort`` - convenience functions. +- Default HTTP exception view. ``request.response`` ~~~~~~~~~~~~~~~~~~~~ @@ -56,8 +55,8 @@ Static Routes Default HTTP Exception View ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -- A default exception view for the context :exc:`webob.exc.HTTPException` - (aka :class:`pyramid.httpexceptions.HTTPException`) is now registered by +- A default exception view for the context + :class:`pyramid.interfaces.IExceptionResponse` is now registered by default. This means that an instance of any exception class imported from :mod:`pyramid.httpexceptions` (such as ``HTTPFound``) can now be raised from within view code; when raised, this exception view will render the @@ -79,9 +78,9 @@ Minor Feature Additions :class:`pyramid.authentication.SessionAuthenticationPolicy`, which uses a session to store credentials. -- A function named :func:`pyramid.httpexceptions.responsecode` is a shortcut - that can be used to create HTTP exception response objects using an HTTP - integer status code. +- A function named :func:`pyramid.httpexceptions.exception_response` is a + shortcut that can be used to create HTTP exception response objects using + an HTTP integer status code. - Integers and longs passed as ``elements`` to :func:`pyramid.url.resource_url` or diff --git a/pyramid/httpexceptions.py b/pyramid/httpexceptions.py index b86e4badf..44b854929 100644 --- a/pyramid/httpexceptions.py +++ b/pyramid/httpexceptions.py @@ -978,7 +978,7 @@ class HTTPInsufficientStorage(HTTPServerError): title = 'Insufficient Storage' explanation = ('There was not enough space to save the resource') -def responsecode(status_code, **kw): +def exception_response(status_code, **kw): """Creates an HTTP exception based on a status code. Example:: raise responsecode(404) # raises an HTTPNotFound exception. diff --git a/pyramid/tests/test_httpexceptions.py b/pyramid/tests/test_httpexceptions.py index 705b988e6..203d442f7 100644 --- a/pyramid/tests/test_httpexceptions.py +++ b/pyramid/tests/test_httpexceptions.py @@ -1,9 +1,9 @@ import unittest -class Test_responsecode(unittest.TestCase): +class Test_exception_response(unittest.TestCase): def _callFUT(self, *arg, **kw): - from pyramid.httpexceptions import responsecode - return responsecode(*arg, **kw) + from pyramid.httpexceptions import exception_response + return exception_response(*arg, **kw) def test_status_404(self): from pyramid.httpexceptions import HTTPNotFound -- cgit v1.2.3 From 8cbbd98daa3ef4f43cfc46359c4e5bae85b1185c Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 20 Jun 2011 01:19:18 -0400 Subject: bring whatsnew up to date --- CHANGES.txt | 4 +- docs/designdefense.rst | 6 ++- docs/whatsnew-1.1.rst | 110 +++++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 107 insertions(+), 13 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index ba392c7c6..16a6f9650 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -136,8 +136,8 @@ Features - The ``pyramid.request.Request`` class now has a ``ResponseClass`` interface which points at ``pyramid.response.Response``. -- The ``pyramid.request.Response`` class now has a ``RequestClass`` interface - which points at ``pyramid.response.Request``. +- The ``pyramid.response.Response`` class now has a ``RequestClass`` + interface which points at ``pyramid.request.Request``. - It is now possible to return an arbitrary object from a Pyramid view callable even if a renderer is not used, as long as a suitable adapter to diff --git a/docs/designdefense.rst b/docs/designdefense.rst index cfd395dd9..ce3c507c5 100644 --- a/docs/designdefense.rst +++ b/docs/designdefense.rst @@ -1011,8 +1011,10 @@ which returns Zope3-security-proxy-wrapped objects for each traversed object (including the :term:`context` and the :term:`root`). This would have the effect of creating a more Zope3-like environment without much effort. -Pyramid Uses its Own HTTP Exception Class Hierarchy Rather ``webob.exc`` ------------------------------------------------------------------------- +.. _http_exception_hierarchy: + +Pyramid Uses its Own HTTP Exception Class Hierarchy Rather Than ``webob.exc`` +----------------------------------------------------------------------------- .. note:: This defense is new as of Pyramid 1.1. diff --git a/docs/whatsnew-1.1.rst b/docs/whatsnew-1.1.rst index cee701b49..07114f5d1 100644 --- a/docs/whatsnew-1.1.rst +++ b/docs/whatsnew-1.1.rst @@ -55,7 +55,7 @@ Static Routes Default HTTP Exception View ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -- A default exception view for the context +- A default exception view for the interface :class:`pyramid.interfaces.IExceptionResponse` is now registered by default. This means that an instance of any exception class imported from :mod:`pyramid.httpexceptions` (such as ``HTTPFound``) can now be raised @@ -63,10 +63,10 @@ Default HTTP Exception View exception to a response. To allow for configuration of this feature, the :term:`Configurator` now - accepts an additional keyword argument named ``httpexception_view``. By - default, this argument is populated with a default exception view function - that will be used when an HTTP exception is raised. When ``None`` is - passed for this value, an exception view for HTTP exceptions will not be + accepts an additional keyword argument named ``exceptionresponse_view``. + By default, this argument is populated with a default exception view + function that will be used when an HTTP exception is raised. When ``None`` + is passed for this value, an exception view for HTTP exceptions will not be registered. Passing ``None`` returns the behavior of raising an HTTP exception to that of Pyramid 1.0 (the exception will propagate to middleware and to the WSGI server). @@ -116,6 +116,69 @@ Minor Feature Additions although typically nonsensical). Allowing the nonsensical configuration made the code more understandable and required fewer tests. +- The :class:`pyramid.request.Request` class now has a ``ResponseClass`` + attribute which points at :class:`pyramid.response.Response`. + +- The :class:`pyramid.response.Response` class now has a ``RequestClass`` + interface which points at :class:`pyramid.request.Request`. + +- It is now possible to return an arbitrary object from a Pyramid view + callable even if a renderer is not used, as long as a suitable adapter to + :class:`pyramid.interfaces.IResponse` is registered for the type of the + returned object by using the new + :meth:`pyramid.config.Configurator.add_response_adapter` API. See the + section in the Hooks chapter of the documentation entitled + :ref:`using_iresponse`. + +- The Pyramid router will now, by default, call the ``__call__`` method of + response objects when returning a WSGI response. This means that, among + other things, the ``conditional_response`` feature response objects + inherited from WebOb will now behave properly. + +- New method named :meth:`pyramid.request.Request.is_response`. This method + should be used instead of the :func:`pyramid.view.is_response` function, + which has been deprecated. + +- :class:`pyramid.exceptions.NotFound` is now just an alias for + :class:`pyramid.httpexceptions.HTTPNotFound`. + +- :class:`pyramid.exceptions.NotFound` is now just an alias for + :class:`pyramid.httpexceptions.HTTPNotFound`. + +Backwards Incompatibilities +--------------------------- + +- Pyramid no longer supports Python 2.4. Python 2.5 or better is required to + run Pyramid 1.1+. Pyramid, however, does not work under any version of + Python 3 yet. + +- The Pyramid router now, by default, expects response objects returned from + view callables to implement the :class:`pyramid.interfaces.IResponse` + interface. Unlike the Pyramid 1.0 version of this interface, objects which + implement IResponse now must define a ``__call__`` method that accepts + ``environ`` and ``start_response``, and which returns an ``app_iter`` + iterable, among other things. Previously, it was possible to return any + object which had the three WebOb ``app_iter``, ``headerlist``, and + ``status`` attributes as a response, so this is a backwards + incompatibility. It is possible to get backwards compatibility back by + registering an adapter to IResponse from the type of object you're now + returning from view callables. See the section in the Hooks chapter of the + documentation entitled :ref:`using_iresponse`. + +- The :class:`pyramid.interfaces.IResponse` interface is now much more + extensive. Previously it defined only ``app_iter``, ``status`` and + ``headerlist``; now it is basically intended to directly mirror the + ``webob.Response`` API, which has many methods and attributes. + +- The :mod:`pyramid.httpexceptions` classes named ``HTTPFound``, + ``HTTPMultipleChoices``, ``HTTPMovedPermanently``, ``HTTPSeeOther``, + ``HTTPUseProxy``, and ``HTTPTemporaryRedirect`` now accept ``location`` as + their first positional argument rather than ``detail``. This means that + you can do, e.g. ``return pyramid.httpexceptions.HTTPFound('http://foo')`` + rather than ``return + pyramid.httpexceptions.HTTPFound(location='http//foo')`` (the latter will + of course continue to work). + Deprecations and Behavior Differences ------------------------------------- @@ -218,6 +281,34 @@ Deprecations and Behavior Differences :class:`pyramid.request.Request` no longer implements its own ``__getattr__``, ``__setattr__`` or ``__delattr__`` as a result. +- Deprecated :func:`pyramid.view.is_response` function in favor of + (newly-added) :meth:`pyramid.request.Request.is_response` method. + Determining if an object is truly a valid response object now requires + access to the registry, which is only easily available as a request + attribute. The :func:`pyramid.view.is_response` function will still work + until it is removed, but now may return an incorrect answer under some + (very uncommon) circumstances. + +- :class:`pyramid.response.Response` is now a *subclass* of + ``webob.response.Response`` (in order to directly implement the + :class:`pyramid.interfaces.IResponse` interface, to speed up response + generation). + +- The "exception response" objects importable from ``pyramid.httpexceptions`` + (e.g. ``HTTPNotFound``) are no longer just import aliases for classes that + actually live in ``webob.exc``. Instead, we've defined our own exception + classes within the module that mirror and emulate the ``webob.exc`` + exception response objects almost entirely. See + :ref:`http_exception_hierarchy` in the Design Defense chapter for more + information. + +- When visiting a URL that represented a static view which resolved to a + subdirectory, the ``index.html`` of that subdirectory would not be served + properly. Instead, a redirect to ``/subdir`` would be issued. This has + been fixed, and now visiting a subdirectory that contains an ``index.html`` + within a static view returns the index.html properly. See also + https://github.com/Pylons/pyramid/issues/67. + Dependency Changes ------------------ @@ -261,9 +352,10 @@ Documentation Enhancements - Added a section to the "URL Dispatch" narrative chapter regarding the new "static" route feature entitled :ref:`static_route_narr`. -- Added API docs for :func:`pyramid.httpexceptions.abort` and - :func:`pyramid.httpexceptions.redirect`. +- Added API docs for :func:`pyramid.httpexceptions.exception_response`. - Added :ref:`http_exceptions` section to Views narrative chapter including a - description of :func:`pyramid.httpexceptions.abort`` and - :func:`pyramid.httpexceptions.redirect`. + description of :func:`pyramid.httpexceptions.exception_response`. + +- Added API docs for + :class:`pyramid.authentication.SessionAuthenticationPolicy`. -- cgit v1.2.3 From 032e777b50c2565d64a995bf8abbfa332784d855 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 20 Jun 2011 01:19:41 -0400 Subject: bring whatsnew up to date --- docs/whatsnew-1.1.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/whatsnew-1.1.rst b/docs/whatsnew-1.1.rst index 07114f5d1..f5a8f88a4 100644 --- a/docs/whatsnew-1.1.rst +++ b/docs/whatsnew-1.1.rst @@ -142,8 +142,8 @@ Minor Feature Additions - :class:`pyramid.exceptions.NotFound` is now just an alias for :class:`pyramid.httpexceptions.HTTPNotFound`. -- :class:`pyramid.exceptions.NotFound` is now just an alias for - :class:`pyramid.httpexceptions.HTTPNotFound`. +- :class:`pyramid.exceptions.Forbidden` is now just an alias for + :class:`pyramid.httpexceptions.HTTPForbidden`. Backwards Incompatibilities --------------------------- -- cgit v1.2.3 From 83549e631fe28e8e3b0b0274d0e631f1ebebd205 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 20 Jun 2011 01:31:27 -0400 Subject: prep for 1.1a1 --- CHANGES.txt | 4 ++-- docs/conf.py | 2 +- setup.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 16a6f9650..413f76729 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,5 +1,5 @@ -Next release -============ +1.1a1 (2011-06-20) +================== Documentation ------------- diff --git a/docs/conf.py b/docs/conf.py index a610351ff..65cd35b94 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -93,7 +93,7 @@ copyright = '%s, Agendaless Consulting' % datetime.datetime.now().year # other places throughout the built documents. # # The short X.Y version. -version = '1.1a0' +version = '1.1a1' # The full version, including alpha/beta/rc tags. release = version diff --git a/setup.py b/setup.py index 6f3fb4eea..f77ae9393 100644 --- a/setup.py +++ b/setup.py @@ -53,7 +53,7 @@ if sys.version_info[:2] < (2, 6): install_requires.append('simplejson') setup(name='pyramid', - version='1.1a0', + version='1.1a1', description=('The Pyramid web application development framework, a ' 'Pylons project'), long_description=README + '\n\n' + CHANGES, -- cgit v1.2.3 From 837c257c60942bdb8f1a71a1a12faf8afe1a71d5 Mon Sep 17 00:00:00 2001 From: Carlos de la Guardia Date: Mon, 20 Jun 2011 07:42:41 -0700 Subject: Removed superfluous word --- docs/whatsnew-1.1.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/whatsnew-1.1.rst b/docs/whatsnew-1.1.rst index f5a8f88a4..1638fe9db 100644 --- a/docs/whatsnew-1.1.rst +++ b/docs/whatsnew-1.1.rst @@ -40,8 +40,8 @@ The major feature additions in Pyramid 1.1 are: - A new paster command named ``paster pviews`` was added. This command prints a summary of potentially matching views for a given path. See - documentation the section entitled :ref:`displaying_matching_views` for - more information. + the section entitled :ref:`displaying_matching_views` for more + information. Static Routes ~~~~~~~~~~~~~ -- cgit v1.2.3 From 92cf3d14c15c0461953000719aafab83ae991f6d Mon Sep 17 00:00:00 2001 From: Carlos de la Guardia Date: Mon, 20 Jun 2011 08:05:06 -0700 Subject: typo --- docs/whatsnew-1.1.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/whatsnew-1.1.rst b/docs/whatsnew-1.1.rst index 1638fe9db..681f05699 100644 --- a/docs/whatsnew-1.1.rst +++ b/docs/whatsnew-1.1.rst @@ -273,7 +273,7 @@ Deprecations and Behavior Differences - Previously, :class:`pyramid.request.Request` inherited from :class:`webob.request.Request` and implemented ``__getattr__``, - ``__setattr__`` and ``__delattr__`` itself in order to overidde "adhoc + ``__setattr__`` and ``__delattr__`` itself in order to override "adhoc attr" WebOb behavior where attributes of the request are stored in the environ. Now, :class:`pyramid.request.Request inherits from (the more recent) :class:`webob.request.BaseRequest`` instead of -- cgit v1.2.3 From 654e3da9d426cdb70426f1eb2a0fce0181934b41 Mon Sep 17 00:00:00 2001 From: Carlos de la Guardia Date: Mon, 20 Jun 2011 08:08:08 -0700 Subject: corrected missing apostrophe and then extra apostrophe --- docs/whatsnew-1.1.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/whatsnew-1.1.rst b/docs/whatsnew-1.1.rst index 681f05699..b57657d11 100644 --- a/docs/whatsnew-1.1.rst +++ b/docs/whatsnew-1.1.rst @@ -275,8 +275,8 @@ Deprecations and Behavior Differences :class:`webob.request.Request` and implemented ``__getattr__``, ``__setattr__`` and ``__delattr__`` itself in order to override "adhoc attr" WebOb behavior where attributes of the request are stored in the - environ. Now, :class:`pyramid.request.Request inherits from (the more - recent) :class:`webob.request.BaseRequest`` instead of + environ. Now, :class:`pyramid.request.Request` inherits from (the more + recent) :class:`webob.request.BaseRequest` instead of :class:`webob.request.Request`, which provides the same behavior. :class:`pyramid.request.Request` no longer implements its own ``__getattr__``, ``__setattr__`` or ``__delattr__`` as a result. -- cgit v1.2.3 From c724f039d58f2c126594b7a644a0de1b97e17910 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 21 Jun 2011 02:18:19 -0400 Subject: - The pyramid Router attempted to set a value into the key ``environ['repoze.bfg.message']`` when it caught a view-related exception for backwards compatibility with :mod:`repoze.bfg` during error handling. It did this by using code that looked like so:: # "why" is an exception object try: msg = why[0] except: msg = '' environ['repoze.bfg.message'] = msg Use of the value ``environ['repoze.bfg.message']`` was docs-deprecated in Pyramid 1.0. Our standing policy is to not remove features after a deprecation for two full major releases, so this code was originally slated to be removed in Pyramid 1.2. However, computing the ``repoze.bfg.message`` value was the source of at least one bug found in the wild (https://github.com/Pylons/pyramid/issues/199), and there isn't a foolproof way to both preserve backwards compatibility and to fix the bug. Therefore, the code which sets the value has been removed in this release. Code in exception views which relies on this value's presence in the environment should now use the ``exception`` attribute of the request (e.g. ``request.exception[0]``) to retrieve the message instead of relying on ``request.environ['repoze.bfg.message']``. Closes #199. --- CHANGES.txt | 32 ++++++++++++++++++++++++++++++++ pyramid/router.py | 8 -------- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 413f76729..29d3d472c 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,35 @@ +Next release +============ + +Backwards Incompatibilities +--------------------------- + +- The pyramid Router attempted to set a value into the key + ``environ['repoze.bfg.message']`` when it caught a view-related exception + for backwards compatibility with :mod:`repoze.bfg` during error handling. + It did this by using code that looked like so:: + + # "why" is an exception object + try: + msg = why[0] + except: + msg = '' + + environ['repoze.bfg.message'] = msg + + Use of the value ``environ['repoze.bfg.message']`` was docs-deprecated in + Pyramid 1.0. Our standing policy is to not remove features after a + deprecation for two full major releases, so this code was originally slated + to be removed in Pyramid 1.2. However, computing the + ``repoze.bfg.message`` value was the source of at least one bug found in + the wild (https://github.com/Pylons/pyramid/issues/199), and there isn't a + foolproof way to both preserve backwards compatibility and to fix the bug. + Therefore, the code which sets the value has been removed in this release. + Code in exception views which relies on this value's presence in the + environment should now use the ``exception`` attribute of the request + (e.g. ``request.exception[0]``) to retrieve the message instead of relying + on ``request.environ['repoze.bfg.message']``. + 1.1a1 (2011-06-20) ================== diff --git a/pyramid/router.py b/pyramid/router.py index 8e33332df..ef04497f9 100644 --- a/pyramid/router.py +++ b/pyramid/router.py @@ -170,14 +170,6 @@ class Router(object): if view_callable is None: raise - try: - msg = why[0] - except: - msg = '' - - # repoze.bfg.message docs-deprecated in Pyramid 1.0 - environ['repoze.bfg.message'] = msg - result = view_callable(why, request) # process the response -- cgit v1.2.3 From e01f0747fbad12836471bbc24953f8f06985ba52 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 21 Jun 2011 05:21:07 -0400 Subject: avoid some getattrs --- pyramid/router.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/pyramid/router.py b/pyramid/router.py index ef04497f9..458237a8c 100644 --- a/pyramid/router.py +++ b/pyramid/router.py @@ -56,8 +56,11 @@ class Router(object): registry = self.registry adapters = registry.adapters has_listeners = registry.has_listeners + notify = registry.notify logger = self.logger manager = self.threadlocal_manager + routes_mapper = self.routes_mapper + debug_routematch = self.debug_routematch request = None threadlocals = {'registry':registry, 'request':request} manager.push(threadlocals) @@ -75,14 +78,14 @@ class Router(object): request_iface = IRequest try: # matches except Exception (exception view execution) - has_listeners and registry.notify(NewRequest(request)) + has_listeners and notify(NewRequest(request)) # find the root object root_factory = self.root_factory - if self.routes_mapper is not None: - info = self.routes_mapper(request) + if routes_mapper is not None: + info = routes_mapper(request) match, route = info['match'], info['route'] if route is None: - if self.debug_routematch: + if debug_routematch: msg = ('no route matched for url %s' % request.url) logger and logger.debug(msg) @@ -96,7 +99,7 @@ class Router(object): attrs['matchdict'] = match attrs['matched_route'] = route - if self.debug_routematch: + if debug_routematch: msg = ( 'route matched for url %s; ' 'route_name: %r, ' @@ -131,7 +134,7 @@ class Router(object): tdict['traversed'], tdict['virtual_root'], tdict['virtual_root_path']) attrs.update(tdict) - has_listeners and registry.notify(ContextFound(request)) + has_listeners and notify(ContextFound(request)) # find a view callable context_iface = providedBy(context) @@ -179,7 +182,7 @@ class Router(object): 'Could not convert view return value "%s" into a ' 'response object' % (result,)) - has_listeners and registry.notify(NewResponse(request,response)) + has_listeners and notify(NewResponse(request, response)) if request.response_callbacks: request._process_response_callbacks(response) -- cgit v1.2.3 From fe5e070166d98a7be35b72a4f66177c0222b34b3 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 21 Jun 2011 10:29:11 -0500 Subject: Fixed a bw-compat issue with PyramidTemplate being moved. --- pyramid/paster.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pyramid/paster.py b/pyramid/paster.py index 28d3535df..b1f0a6c8b 100644 --- a/pyramid/paster.py +++ b/pyramid/paster.py @@ -2,11 +2,19 @@ import os import sys from code import interact +import zope.deprecation + from paste.deploy import loadapp from paste.script.command import Command from pyramid.scripting import get_root +from pyramid.scaffolds import PyramidTemplate # bw compat +zope.deprecation.deprecated( + 'PyramidTemplate', ('pyramid.paster.PyramidTemplate was moved to ' + 'pyramid.scaffolds.PyramidTemplate in Pyramid 1.1'), +) + def get_app(config_file, name, loadapp=loadapp): """ Return the WSGI application named ``name`` in the PasteDeploy config file ``config_file``""" -- cgit v1.2.3 From d74d5350422dd99eb41cefb391729dafd0e1c637 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 21 Jun 2011 14:38:49 -0400 Subject: garden --- CHANGES.txt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 29d3d472c..104700cfd 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,6 +1,13 @@ Next release ============ +Bug Fixes +--------- + +- 1.1a1 broke Akhet by not providing a backwards compatibility import shim + for ``pyramid.paster.PyramidTemplate``. Now one has been added, although a + deprecation warning is raised. + Backwards Incompatibilities --------------------------- -- cgit v1.2.3 From fc950d607e942dd6ae755354a8ac0d30040a5848 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 21 Jun 2011 19:44:34 -0400 Subject: changes suggested by sluggo --- docs/whatsnew-1.1.rst | 47 +++++++++++++++++++++++++++++++---------------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/docs/whatsnew-1.1.rst b/docs/whatsnew-1.1.rst index b57657d11..7c2aad821 100644 --- a/docs/whatsnew-1.1.rst +++ b/docs/whatsnew-1.1.rst @@ -7,6 +7,15 @@ incompatibilities between the two versions and deprecations added to Pyramid 1.1, as well as software dependency changes and notable documentation additions. +Terminology Changes +------------------- + +The term "template" used by the Pyramid documentation used to refer to both +"paster templates" and "rendered templates" (templates created by a rendering +engine. i.e. Mako, Chameleon, Jinja, etc.). "Paster templates" will now be +refered to as "scaffolds", whereas the name for "rendered templates" will +remain as "templates." + Major Feature Additions ----------------------- @@ -23,17 +32,28 @@ The major feature additions in Pyramid 1.1 are: ``request.response`` ~~~~~~~~~~~~~~~~~~~~ -- Accessing the ``response`` attribute of a :class:`pyramid.request.Request` - object (e.g. ``request.response`` within a view) now produces a new - :class:`pyramid.response.Response` object. This feature is meant to be - used mainly when a view configured with a renderer needs to set response - attributes: all renderers will use the Response object implied by - ``request.response`` as the response object returned to the router. - - ``request.response`` can also be used by code in a view that does not use a - renderer, however the response object that is produced by - ``request.response`` must be returned when a renderer is not in play (it is - not a "global" response). +- Instances of the :class:`pyramid.request.Request` class now have a + ``response`` attribute. + + The object passed to a view callable as ``request`` is an instance of + :class:`pyramid.request.Request`. ``request.response`` is an instance of + the class :class:`pyramid.request.Response`. View callables that are + configured with a :term:`renderer` will return this response object to the + Pyramid router. Therefore, code in a renderer-using view callable can set + response attributes such as ``request.response.content_type`` (before they + return, e.g. a dictionary to the renderer) and this will influence the HTTP + return value of the view callable. + + ``request.response`` can also be used in view callable code that is not + configured to use a renderer. For example, a view callable might do + ``request.response.body = '123'; return request.response``. However, the + response object that is produced by ``request.response`` must be *returned* + when a renderer is not in play in order to have any effect on the HTTP + response (it is not a "global" response, and modifications to it are not + somehow merged into a separately returned response object). + + The ``request.response`` object is lazily created, so its introduction does + not negatively impact performance. ``paster pviews`` ~~~~~~~~~~~~~~~~~ @@ -321,11 +341,6 @@ Dependency Changes Documentation Enhancements -------------------------- -- The term "template" used to refer to both "paster templates" and "rendered - templates" (templates created by a rendering engine. i.e. Mako, Chameleon, - Jinja, etc.). "Paster templates" will now be refered to as "scaffolds", - whereas the name for "rendered templates" will remain as "templates." - - The :ref:`bfg_wiki_tutorial` was updated slightly. - The :ref:`bfg_sql_wiki_tutorial` was updated slightly. -- cgit v1.2.3 From 40369d7f54adb84aa7c4e56498b18b6dda75938a Mon Sep 17 00:00:00 2001 From: Carlos de la Guardia Date: Tue, 21 Jun 2011 18:07:25 -0700 Subject: fix typo --- docs/whatsnew-1.1.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/whatsnew-1.1.rst b/docs/whatsnew-1.1.rst index 7c2aad821..cf8086fbe 100644 --- a/docs/whatsnew-1.1.rst +++ b/docs/whatsnew-1.1.rst @@ -241,7 +241,7 @@ Deprecations and Behavior Differences spelled:: config.add_route('home', '/') - config.add_view('mypackage.views.myview', route_name='home') + config.add_view('mypackage.views.myview', route_name='home', renderer='some/renderer.pt') This deprecation was done to reduce confusion observed in IRC, as well as -- cgit v1.2.3 From bf7544c683803c4490e7c82e25e31d36261fb973 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 22 Jun 2011 00:29:36 -0400 Subject: suggestions from sluggo --- CHANGES.txt | 14 ++++++++------ docs/whatsnew-1.1.rst | 14 ++++++++------ 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 104700cfd..f4cd65b36 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -351,12 +351,14 @@ Behavior Changes - The JSON and string renderer factories now assign to ``request.response.content_type`` rather than - ``request.response_content_type``. Each renderer factory determines - whether it should change the content type of the response by comparing the - response's content type against the response's default content type; if the - content type is not the default content type (usually ``text/html``), the - renderer changes the content type (to ``application/json`` or - ``text/plain`` for JSON and string renderers respectively). + ``request.response_content_type``. + +- Each built-in renderer factory now determines whether it should change the + content type of the response by comparing the response's content type + against the response's default content type; if the content type is the + default content type (usually ``text/html``), the renderer changes the + content type (to ``application/json`` or ``text/plain`` for JSON and string + renderers respectively). - The ``pyramid.wsgi.wsgiapp2`` now uses a slightly different method of figuring out how to "fix" ``SCRIPT_NAME`` and ``PATH_INFO`` for the diff --git a/docs/whatsnew-1.1.rst b/docs/whatsnew-1.1.rst index 7c2aad821..66f398e27 100644 --- a/docs/whatsnew-1.1.rst +++ b/docs/whatsnew-1.1.rst @@ -278,12 +278,14 @@ Deprecations and Behavior Differences - The JSON and string renderer factories now assign to ``request.response.content_type`` rather than - ``request.response_content_type``. Each renderer factory determines - whether it should change the content type of the response by comparing the - response's content type against the response's default content type; if the - content type is not the default content type (usually ``text/html``), the - renderer changes the content type (to ``application/json`` or - ``text/plain`` for JSON and string renderers respectively). + ``request.response_content_type``. + +- Each built-in renderer factory now determines whether it should change the + content type of the response by comparing the response's content type + against the response's default content type; if the content type is the + default content type (usually ``text/html``), the renderer changes the + content type (to ``application/json`` or ``text/plain`` for JSON and string + renderers respectively). - The :func:`pyramid.wsgi.wsgiapp2` now uses a slightly different method of figuring out how to "fix" ``SCRIPT_NAME`` and ``PATH_INFO`` for the -- cgit v1.2.3 From 6ed33ec54b0dae6ae2a38a0c7a6d383e2ac1967e Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 22 Jun 2011 03:08:03 -0400 Subject: - If multiple specs were provided in a single call to ``config.add_translation_dirs``, the directories were inserted into the beginning of the directory list in the wrong order: they were inserted in the reverse of the order they were provided in the ``*specs`` list (items later in the list trumped ones earlier in the list). This is now fixed. Note however, that later calls to ``config.add_translation_dirs`` continue to insert directories into the beginning of the list of translation directories created by earlier calls. This means that the same translation found in a directory added via ``add_translation_dirs`` later in the configuration process will be found before one added earlier via a separate call to ``add_translation_dirs`` in the configuration process. --- CHANGES.txt | 13 +++++++++ pyramid/config.py | 11 +++++++- pyramid/tests/localeapp/locale2/GARBAGE | 1 + pyramid/tests/localeapp/locale2/be/LC_MESSAGES | 1 + .../localeapp/locale2/de/LC_MESSAGES/deformsite.mo | Bin 0 -> 543 bytes .../localeapp/locale2/de/LC_MESSAGES/deformsite.po | 31 +++++++++++++++++++++ .../localeapp/locale2/en/LC_MESSAGES/deformsite.mo | Bin 0 -> 543 bytes .../localeapp/locale2/en/LC_MESSAGES/deformsite.po | 31 +++++++++++++++++++++ pyramid/tests/localeapp/locale3/GARBAGE | 1 + pyramid/tests/localeapp/locale3/be/LC_MESSAGES | 1 + .../localeapp/locale3/de/LC_MESSAGES/deformsite.mo | Bin 0 -> 543 bytes .../localeapp/locale3/de/LC_MESSAGES/deformsite.po | 31 +++++++++++++++++++++ .../localeapp/locale3/en/LC_MESSAGES/deformsite.mo | Bin 0 -> 543 bytes .../localeapp/locale3/en/LC_MESSAGES/deformsite.po | 31 +++++++++++++++++++++ pyramid/tests/test_config.py | 26 +++++++++++++++++ 15 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 pyramid/tests/localeapp/locale2/GARBAGE create mode 100644 pyramid/tests/localeapp/locale2/be/LC_MESSAGES create mode 100644 pyramid/tests/localeapp/locale2/de/LC_MESSAGES/deformsite.mo create mode 100644 pyramid/tests/localeapp/locale2/de/LC_MESSAGES/deformsite.po create mode 100644 pyramid/tests/localeapp/locale2/en/LC_MESSAGES/deformsite.mo create mode 100644 pyramid/tests/localeapp/locale2/en/LC_MESSAGES/deformsite.po create mode 100644 pyramid/tests/localeapp/locale3/GARBAGE create mode 100644 pyramid/tests/localeapp/locale3/be/LC_MESSAGES create mode 100644 pyramid/tests/localeapp/locale3/de/LC_MESSAGES/deformsite.mo create mode 100644 pyramid/tests/localeapp/locale3/de/LC_MESSAGES/deformsite.po create mode 100644 pyramid/tests/localeapp/locale3/en/LC_MESSAGES/deformsite.mo create mode 100644 pyramid/tests/localeapp/locale3/en/LC_MESSAGES/deformsite.po diff --git a/CHANGES.txt b/CHANGES.txt index f4cd65b36..f03dcd067 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -8,6 +8,19 @@ Bug Fixes for ``pyramid.paster.PyramidTemplate``. Now one has been added, although a deprecation warning is raised. +- If multiple specs were provided in a single call to + ``config.add_translation_dirs``, the directories were inserted into the + beginning of the directory list in the wrong order: they were inserted in + the reverse of the order they were provided in the ``*specs`` list (items + later in the list trumped ones earlier in the list). This is now fixed. + + Note however, that later calls to ``config.add_translation_dirs`` continue + to insert directories into the beginning of the list of translation + directories created by earlier calls. This means that the same translation + found in a directory added via ``add_translation_dirs`` later in the + configuration process will be found before one added earlier via a separate + call to ``add_translation_dirs`` in the configuration process. + Backwards Incompatibilities --------------------------- diff --git a/pyramid/config.py b/pyramid/config.py index 1e4cbb350..77f5b5b34 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -2245,8 +2245,17 @@ class Configurator(object): config.add_translation_dirs('/usr/share/locale', 'some.package:locale') + Later calls to ``add_translation_dir`` insert directories into the + beginning of the list of translation directories created by earlier + calls. This means that the same translation found in a directory + added later in the configuration process will be found before one + added earlier in the configuration process. However, if multiple + specs are provided in a single call to ``add_translation_dirs``, the + directories will be inserted into the beginning of the directory list + in the order they're provided in the ``*specs`` list argument (items + earlier in the list trump ones later in the list). """ - for spec in specs: + for spec in specs[::-1]: # reversed package_name, filename = self._split_spec(spec) if package_name is None: # absolute filename diff --git a/pyramid/tests/localeapp/locale2/GARBAGE b/pyramid/tests/localeapp/locale2/GARBAGE new file mode 100644 index 000000000..032c55584 --- /dev/null +++ b/pyramid/tests/localeapp/locale2/GARBAGE @@ -0,0 +1 @@ +Garbage file. diff --git a/pyramid/tests/localeapp/locale2/be/LC_MESSAGES b/pyramid/tests/localeapp/locale2/be/LC_MESSAGES new file mode 100644 index 000000000..909cf6a3b --- /dev/null +++ b/pyramid/tests/localeapp/locale2/be/LC_MESSAGES @@ -0,0 +1 @@ +busted. diff --git a/pyramid/tests/localeapp/locale2/de/LC_MESSAGES/deformsite.mo b/pyramid/tests/localeapp/locale2/de/LC_MESSAGES/deformsite.mo new file mode 100644 index 000000000..2924a5eb5 Binary files /dev/null and b/pyramid/tests/localeapp/locale2/de/LC_MESSAGES/deformsite.mo differ diff --git a/pyramid/tests/localeapp/locale2/de/LC_MESSAGES/deformsite.po b/pyramid/tests/localeapp/locale2/de/LC_MESSAGES/deformsite.po new file mode 100644 index 000000000..17f87bc19 --- /dev/null +++ b/pyramid/tests/localeapp/locale2/de/LC_MESSAGES/deformsite.po @@ -0,0 +1,31 @@ +# German translations for deformsite. +# Copyright (C) 2010 ORGANIZATION +# This file is distributed under the same license as the deformsite project. +# FIRST AUTHOR , 2010. +# +msgid "" +msgstr "" +"Project-Id-Version: deformsite 0.0\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2010-04-22 14:17+0400\n" +"PO-Revision-Date: 2010-04-22 14:17-0400\n" +"Last-Translator: FULL NAME \n" +"Language-Team: de \n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 0.9.5\n" + +#: deformsite/__init__.py:458 +msgid "Approve" +msgstr "Genehmigen" + +#: deformsite/__init__.py:459 +msgid "Show approval" +msgstr "Zeigen Genehmigung" + +#: deformsite/__init__.py:466 +msgid "Submit" +msgstr "Beugen" + diff --git a/pyramid/tests/localeapp/locale2/en/LC_MESSAGES/deformsite.mo b/pyramid/tests/localeapp/locale2/en/LC_MESSAGES/deformsite.mo new file mode 100644 index 000000000..2924a5eb5 Binary files /dev/null and b/pyramid/tests/localeapp/locale2/en/LC_MESSAGES/deformsite.mo differ diff --git a/pyramid/tests/localeapp/locale2/en/LC_MESSAGES/deformsite.po b/pyramid/tests/localeapp/locale2/en/LC_MESSAGES/deformsite.po new file mode 100644 index 000000000..17f87bc19 --- /dev/null +++ b/pyramid/tests/localeapp/locale2/en/LC_MESSAGES/deformsite.po @@ -0,0 +1,31 @@ +# German translations for deformsite. +# Copyright (C) 2010 ORGANIZATION +# This file is distributed under the same license as the deformsite project. +# FIRST AUTHOR , 2010. +# +msgid "" +msgstr "" +"Project-Id-Version: deformsite 0.0\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2010-04-22 14:17+0400\n" +"PO-Revision-Date: 2010-04-22 14:17-0400\n" +"Last-Translator: FULL NAME \n" +"Language-Team: de \n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 0.9.5\n" + +#: deformsite/__init__.py:458 +msgid "Approve" +msgstr "Genehmigen" + +#: deformsite/__init__.py:459 +msgid "Show approval" +msgstr "Zeigen Genehmigung" + +#: deformsite/__init__.py:466 +msgid "Submit" +msgstr "Beugen" + diff --git a/pyramid/tests/localeapp/locale3/GARBAGE b/pyramid/tests/localeapp/locale3/GARBAGE new file mode 100644 index 000000000..032c55584 --- /dev/null +++ b/pyramid/tests/localeapp/locale3/GARBAGE @@ -0,0 +1 @@ +Garbage file. diff --git a/pyramid/tests/localeapp/locale3/be/LC_MESSAGES b/pyramid/tests/localeapp/locale3/be/LC_MESSAGES new file mode 100644 index 000000000..909cf6a3b --- /dev/null +++ b/pyramid/tests/localeapp/locale3/be/LC_MESSAGES @@ -0,0 +1 @@ +busted. diff --git a/pyramid/tests/localeapp/locale3/de/LC_MESSAGES/deformsite.mo b/pyramid/tests/localeapp/locale3/de/LC_MESSAGES/deformsite.mo new file mode 100644 index 000000000..2924a5eb5 Binary files /dev/null and b/pyramid/tests/localeapp/locale3/de/LC_MESSAGES/deformsite.mo differ diff --git a/pyramid/tests/localeapp/locale3/de/LC_MESSAGES/deformsite.po b/pyramid/tests/localeapp/locale3/de/LC_MESSAGES/deformsite.po new file mode 100644 index 000000000..17f87bc19 --- /dev/null +++ b/pyramid/tests/localeapp/locale3/de/LC_MESSAGES/deformsite.po @@ -0,0 +1,31 @@ +# German translations for deformsite. +# Copyright (C) 2010 ORGANIZATION +# This file is distributed under the same license as the deformsite project. +# FIRST AUTHOR , 2010. +# +msgid "" +msgstr "" +"Project-Id-Version: deformsite 0.0\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2010-04-22 14:17+0400\n" +"PO-Revision-Date: 2010-04-22 14:17-0400\n" +"Last-Translator: FULL NAME \n" +"Language-Team: de \n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 0.9.5\n" + +#: deformsite/__init__.py:458 +msgid "Approve" +msgstr "Genehmigen" + +#: deformsite/__init__.py:459 +msgid "Show approval" +msgstr "Zeigen Genehmigung" + +#: deformsite/__init__.py:466 +msgid "Submit" +msgstr "Beugen" + diff --git a/pyramid/tests/localeapp/locale3/en/LC_MESSAGES/deformsite.mo b/pyramid/tests/localeapp/locale3/en/LC_MESSAGES/deformsite.mo new file mode 100644 index 000000000..2924a5eb5 Binary files /dev/null and b/pyramid/tests/localeapp/locale3/en/LC_MESSAGES/deformsite.mo differ diff --git a/pyramid/tests/localeapp/locale3/en/LC_MESSAGES/deformsite.po b/pyramid/tests/localeapp/locale3/en/LC_MESSAGES/deformsite.po new file mode 100644 index 000000000..17f87bc19 --- /dev/null +++ b/pyramid/tests/localeapp/locale3/en/LC_MESSAGES/deformsite.po @@ -0,0 +1,31 @@ +# German translations for deformsite. +# Copyright (C) 2010 ORGANIZATION +# This file is distributed under the same license as the deformsite project. +# FIRST AUTHOR , 2010. +# +msgid "" +msgstr "" +"Project-Id-Version: deformsite 0.0\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2010-04-22 14:17+0400\n" +"PO-Revision-Date: 2010-04-22 14:17-0400\n" +"Last-Translator: FULL NAME \n" +"Language-Team: de \n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 0.9.5\n" + +#: deformsite/__init__.py:458 +msgid "Approve" +msgstr "Genehmigen" + +#: deformsite/__init__.py:459 +msgid "Show approval" +msgstr "Zeigen Genehmigung" + +#: deformsite/__init__.py:466 +msgid "Submit" +msgstr "Beugen" + diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py index 12146895d..dd8bf109f 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -2499,6 +2499,32 @@ class ConfiguratorTests(unittest.TestCase): result = config.registry.getUtility(ITranslationDirectories) self.assertEqual(result, [locale, 'abc']) + def test_add_translation_dirs_multiple_specs(self): + import os + from pyramid.interfaces import ITranslationDirectories + config = self._makeOne(autocommit=True) + config.add_translation_dirs('pyramid.tests.localeapp:locale', + 'pyramid.tests.localeapp:locale2') + here = os.path.dirname(__file__) + locale = os.path.join(here, 'localeapp', 'locale') + locale2 = os.path.join(here, 'localeapp', 'locale2') + self.assertEqual(config.registry.getUtility(ITranslationDirectories), + [locale, locale2]) + + def test_add_translation_dirs_multiple_specs_multiple_calls(self): + import os + from pyramid.interfaces import ITranslationDirectories + config = self._makeOne(autocommit=True) + config.add_translation_dirs('pyramid.tests.localeapp:locale', + 'pyramid.tests.localeapp:locale2') + config.add_translation_dirs('pyramid.tests.localeapp:locale3') + here = os.path.dirname(__file__) + locale = os.path.join(here, 'localeapp', 'locale') + locale2 = os.path.join(here, 'localeapp', 'locale2') + locale3 = os.path.join(here, 'localeapp', 'locale3') + self.assertEqual(config.registry.getUtility(ITranslationDirectories), + [locale3, locale, locale2]) + def test_add_translation_dirs_registers_chameleon_translate(self): from pyramid.interfaces import IChameleonTranslate from pyramid.threadlocal import manager -- cgit v1.2.3 From 4f11dc142a1e515bce106a6ebf09e22433d0a845 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 22 Jun 2011 03:31:23 -0400 Subject: take robert forkels advice --- CHANGES.txt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index f03dcd067..cade195e7 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -12,14 +12,14 @@ Bug Fixes ``config.add_translation_dirs``, the directories were inserted into the beginning of the directory list in the wrong order: they were inserted in the reverse of the order they were provided in the ``*specs`` list (items - later in the list trumped ones earlier in the list). This is now fixed. + later in the list were added before ones earlier in the list). This is now + fixed. Note however, that later calls to ``config.add_translation_dirs`` continue to insert directories into the beginning of the list of translation - directories created by earlier calls. This means that the same translation - found in a directory added via ``add_translation_dirs`` later in the - configuration process will be found before one added earlier via a separate - call to ``add_translation_dirs`` in the configuration process. + directories created by earlier calls. This means that messages defined in + catalogs added earlier via ``add_translation_dirs`` take precedence over + the ones in catalogs added later. Backwards Incompatibilities --------------------------- -- cgit v1.2.3 From b0c075ce8eeb7cb95188c1f41feca7af42bb46c7 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 22 Jun 2011 03:32:55 -0400 Subject: leave undefined --- CHANGES.txt | 6 ------ 1 file changed, 6 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index cade195e7..6522297ec 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -15,12 +15,6 @@ Bug Fixes later in the list were added before ones earlier in the list). This is now fixed. - Note however, that later calls to ``config.add_translation_dirs`` continue - to insert directories into the beginning of the list of translation - directories created by earlier calls. This means that messages defined in - catalogs added earlier via ``add_translation_dirs`` take precedence over - the ones in catalogs added later. - Backwards Incompatibilities --------------------------- -- cgit v1.2.3 From e7e35e87343ce43a8b368076089a95e18aba665f Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 22 Jun 2011 17:21:30 -0400 Subject: garden --- TODO.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/TODO.txt b/TODO.txt index 8a014a245..b52ad4eec 100644 --- a/TODO.txt +++ b/TODO.txt @@ -7,6 +7,9 @@ Must-Have - Deprecate response_foo attrs on request at attribute set time rather than lookup time. +- Investigate mod_wsgi tutorial to make sure it still works (2 reports say + no; application package not found). + Should-Have ----------- -- cgit v1.2.3 From cc85e7a96ccbb1671514adb1a1b1992fd1f02461 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 22 Jun 2011 19:39:45 -0400 Subject: prep for 1.1a2 --- CHANGES.txt | 10 +++++----- RELEASING.txt | 2 +- docs/conf.py | 2 +- docs/whatsnew-1.1.rst | 26 ++++++++++++++++++++++++++ setup.py | 2 +- 5 files changed, 34 insertions(+), 8 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 6522297ec..0bb15c35b 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,12 +1,12 @@ -Next release -============ +1.1a2 (2011-06-22) +================== Bug Fixes --------- - 1.1a1 broke Akhet by not providing a backwards compatibility import shim for ``pyramid.paster.PyramidTemplate``. Now one has been added, although a - deprecation warning is raised. + deprecation warning is emitted when Akhet imports it. - If multiple specs were provided in a single call to ``config.add_translation_dirs``, the directories were inserted into the @@ -20,8 +20,8 @@ Backwards Incompatibilities - The pyramid Router attempted to set a value into the key ``environ['repoze.bfg.message']`` when it caught a view-related exception - for backwards compatibility with :mod:`repoze.bfg` during error handling. - It did this by using code that looked like so:: + for backwards compatibility with applications written for ``repoze.bfg`` + during error handling. It did this by using code that looked like so:: # "why" is an exception object try: diff --git a/RELEASING.txt b/RELEASING.txt index 115edec5f..645083acf 100644 --- a/RELEASING.txt +++ b/RELEASING.txt @@ -13,7 +13,7 @@ Releasing Pyramid communicate with contributors). - Copy relevant changes (delta bug fixes) from CHANGES.txt to - docs/whatsnew-X.X. + docs/whatsnew-X.X (if it's a major release). - Make sure docs render OK:: diff --git a/docs/conf.py b/docs/conf.py index 65cd35b94..34262191f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -93,7 +93,7 @@ copyright = '%s, Agendaless Consulting' % datetime.datetime.now().year # other places throughout the built documents. # # The short X.Y version. -version = '1.1a1' +version = '1.1a2' # The full version, including alpha/beta/rc tags. release = version diff --git a/docs/whatsnew-1.1.rst b/docs/whatsnew-1.1.rst index faf8b8b68..180380608 100644 --- a/docs/whatsnew-1.1.rst +++ b/docs/whatsnew-1.1.rst @@ -199,6 +199,32 @@ Backwards Incompatibilities pyramid.httpexceptions.HTTPFound(location='http//foo')`` (the latter will of course continue to work). +- The pyramid Router attempted to set a value into the key + ``environ['repoze.bfg.message']`` when it caught a view-related exception + for backwards compatibility with applications written for :mod:`repoze.bfg` + during error handling. It did this by using code that looked like so:: + + # "why" is an exception object + try: + msg = why[0] + except: + msg = '' + + environ['repoze.bfg.message'] = msg + + Use of the value ``environ['repoze.bfg.message']`` was docs-deprecated in + Pyramid 1.0. Our standing policy is to not remove features after a + deprecation for two full major releases, so this code was originally slated + to be removed in Pyramid 1.2. However, computing the + ``repoze.bfg.message`` value was the source of at least one bug found in + the wild (https://github.com/Pylons/pyramid/issues/199), and there isn't a + foolproof way to both preserve backwards compatibility and to fix the bug. + Therefore, the code which sets the value has been removed in this release. + Code in exception views which relies on this value's presence in the + environment should now use the ``exception`` attribute of the request + (e.g. ``request.exception[0]``) to retrieve the message instead of relying + on ``request.environ['repoze.bfg.message']``. + Deprecations and Behavior Differences ------------------------------------- diff --git a/setup.py b/setup.py index f77ae9393..5c3232fca 100644 --- a/setup.py +++ b/setup.py @@ -53,7 +53,7 @@ if sys.version_info[:2] < (2, 6): install_requires.append('simplejson') setup(name='pyramid', - version='1.1a1', + version='1.1a2', description=('The Pyramid web application development framework, a ' 'Pylons project'), long_description=README + '\n\n' + CHANGES, -- cgit v1.2.3 From f2924f2ac6d08488ce62c1de6bdee9ba00e2cc35 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 23 Jun 2011 12:41:29 -0500 Subject: A fix for classmethod-based custom predicates with no __text__ property. --- pyramid/config.py | 10 +++++++++- pyramid/tests/test_config.py | 15 ++++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/pyramid/config.py b/pyramid/config.py index 77f5b5b34..418e393f4 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -2692,7 +2692,15 @@ def _make_predicates(xhr=None, request_method=None, path_info=None, if custom: for num, predicate in enumerate(custom): if getattr(predicate, '__text__', None) is None: - predicate.__text__ = "" + text = '' + try: + predicate.__text__ = text + except AttributeError: + # if this happens the predicate is probably a classmethod + if hasattr(predicate, '__func__'): + predicate.__func__.__text__ = text + else: # 2.5 doesn't have __func__ + predicate.im_func.__text__ = text predicates.append(predicate) # using hash() here rather than id() is intentional: we # want to allow custom predicates that are part of diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py index dd8bf109f..5c102132c 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -4626,7 +4626,9 @@ class Test__make_predicates(unittest.TestCase): accept='accept', containment='containment', request_type='request_type', - custom=(DummyCustomPredicate(),)) + custom=(DummyCustomPredicate(), + DummyCustomPredicate.classmethod_predicate, + DummyCustomPredicate.classmethod_predicate_no_text)) self.assertEqual(predicates[0].__text__, 'xhr = True') self.assertEqual(predicates[1].__text__, 'request method = request_method') @@ -4637,6 +4639,8 @@ class Test__make_predicates(unittest.TestCase): self.assertEqual(predicates[6].__text__, 'containment = containment') self.assertEqual(predicates[7].__text__, 'request_type = request_type') self.assertEqual(predicates[8].__text__, 'custom predicate') + self.assertEqual(predicates[9].__text__, 'classmethod predicate') + self.assertEqual(predicates[10].__text__, '') class TestMultiView(unittest.TestCase): def _getTargetClass(self): @@ -5214,6 +5218,15 @@ class DummyCustomPredicate(object): def __init__(self): self.__text__ = 'custom predicate' + def classmethod_predicate(*args): + pass + classmethod_predicate.__text__ = 'classmethod predicate' + classmethod_predicate = classmethod(classmethod_predicate) + + @classmethod + def classmethod_predicate_no_text(*args): + pass + def dummy_view(request): return 'OK' -- cgit v1.2.3 From dfd219842b7ddb68fc5ff29d2c7bd55ac56b5e29 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 23 Jun 2011 15:46:19 -0400 Subject: coverage --- pyramid/config.py | 2 +- pyramid/tests/test_config.py | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/pyramid/config.py b/pyramid/config.py index 418e393f4..3681e48ee 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -2699,7 +2699,7 @@ def _make_predicates(xhr=None, request_method=None, path_info=None, # if this happens the predicate is probably a classmethod if hasattr(predicate, '__func__'): predicate.__func__.__text__ = text - else: # 2.5 doesn't have __func__ + else: # # pragma: no cover ; 2.5 doesn't have __func__ predicate.im_func.__text__ = text predicates.append(predicate) # using hash() here rather than id() is intentional: we diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py index 5c102132c..41811782a 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -5218,14 +5218,12 @@ class DummyCustomPredicate(object): def __init__(self): self.__text__ = 'custom predicate' - def classmethod_predicate(*args): - pass + def classmethod_predicate(*args): pass classmethod_predicate.__text__ = 'classmethod predicate' classmethod_predicate = classmethod(classmethod_predicate) @classmethod - def classmethod_predicate_no_text(*args): - pass + def classmethod_predicate_no_text(*args): pass # pragma: no cover def dummy_view(request): return 'OK' -- cgit v1.2.3 From 05fd08652293809fc6fd344cd1d4fe3a90cc2201 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 23 Jun 2011 15:47:13 -0400 Subject: garden --- CHANGES.txt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 0bb15c35b..3c1d16fa5 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,14 @@ +Next release +============ + +Bug fixes +--------- + +- Pyramid would raise an AttributeError in the Configurator when attempting + to set a ``__text__`` attribute on a custom predicate that was actually a + classmethod. See https://github.com/Pylons/pyramid/pull/217 . + + 1.1a2 (2011-06-22) ================== -- cgit v1.2.3 From 8f6e3785e262c2c06756493bf3eb425e9e4a37d8 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 26 Jun 2011 02:56:33 -0400 Subject: appeasement --- pyramid/tests/test_response.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyramid/tests/test_response.py b/pyramid/tests/test_response.py index 46eb298d1..6cb2fd6f4 100644 --- a/pyramid/tests/test_response.py +++ b/pyramid/tests/test_response.py @@ -8,10 +8,10 @@ class TestResponse(unittest.TestCase): def test_implements_IResponse(self): from pyramid.interfaces import IResponse cls = self._getTargetClass() - self.failUnless(IResponse.implementedBy(cls)) + self.assertTrue(IResponse.implementedBy(cls)) def test_provides_IResponse(self): from pyramid.interfaces import IResponse inst = self._getTargetClass()() - self.failUnless(IResponse.providedBy(inst)) + self.assertTrue(IResponse.providedBy(inst)) -- cgit v1.2.3 From d8c55c0157b37594a5266ad92b8d203b5f6cb0ca Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 26 Jun 2011 02:59:19 -0400 Subject: - Accessing or setting deprecated response_* attrs on request (e.g. ``response_content_type``) now issues a deprecation warning at access time rather than at rendering time. --- CHANGES.txt | 3 ++ TODO.txt | 3 -- pyramid/renderers.py | 26 ++++---------- pyramid/request.py | 79 ++++++++++++++++++++++++++++++++++++++++- pyramid/tests/test_renderers.py | 27 +++----------- pyramid/tests/test_request.py | 53 +++++++++++++++++++++++---- 6 files changed, 140 insertions(+), 51 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 3c1d16fa5..b95211d09 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -8,6 +8,9 @@ Bug fixes to set a ``__text__`` attribute on a custom predicate that was actually a classmethod. See https://github.com/Pylons/pyramid/pull/217 . +- Accessing or setting deprecated response_* attrs on request + (e.g. ``response_content_type``) now issues a deprecation warning at access + time rather than at rendering time. 1.1a2 (2011-06-22) ================== diff --git a/TODO.txt b/TODO.txt index b52ad4eec..ba9a72d03 100644 --- a/TODO.txt +++ b/TODO.txt @@ -4,9 +4,6 @@ Pyramid TODOs Must-Have --------- -- Deprecate response_foo attrs on request at attribute set time rather than - lookup time. - - Investigate mod_wsgi tutorial to make sure it still works (2 reports say no; application package not found). diff --git a/pyramid/renderers.py b/pyramid/renderers.py index 6865067dd..b8d02ace3 100644 --- a/pyramid/renderers.py +++ b/pyramid/renderers.py @@ -365,36 +365,24 @@ class RendererHelper(object): response.body = result if request is not None: - # deprecated mechanism to set up request.response_* attrs + # deprecated mechanism to set up request.response_* attrs, see + # pyramid.request.Request attrs = request.__dict__ - content_type = attrs.get('response_content_type', None) + content_type = attrs.get('_response_content_type', None) if content_type is not None: response.content_type = content_type - deprecate_req_attr('Setting', 'content_type', - 'set', 'content_type') - headerlist = attrs.get('response_headerlist', None) + headerlist = attrs.get('_response_headerlist', None) if headerlist is not None: for k, v in headerlist: response.headers.add(k, v) - deprecate_req_attr('Setting or mutating', 'headerlist', - 'set or mutate', 'headerlist') - status = attrs.get('response_status', None) + status = attrs.get('_response_status', None) if status is not None: response.status = status - deprecate_req_attr('Setting', 'status', 'set', 'status') - charset = attrs.get('response_charset', None) + charset = attrs.get('_response_charset', None) if charset is not None: response.charset = charset - deprecate_req_attr('Setting', 'charset', 'set', 'charset') - cache_for = attrs.get('response_cache_for', None) + cache_for = attrs.get('_response_cache_for', None) if cache_for is not None: response.cache_expires = cache_for - deprecate_req_attr('Setting', 'cache_for', - 'set', 'cache_expires') - return response -def deprecate_req_attr(*args): - depwarn = ('%s "request.response_%s" is deprecated as of Pyramid 1.1; %s ' - '"request.response.%s" instead.') - warnings.warn(depwarn % args, DeprecationWarning, 3) diff --git a/pyramid/request.py b/pyramid/request.py index 06dbddd29..b9dd2dfe9 100644 --- a/pyramid/request.py +++ b/pyramid/request.py @@ -1,4 +1,5 @@ from zope.deprecation import deprecate +from zope.deprecation.deprecation import deprecated from zope.interface import implements from zope.interface.interface import InterfaceClass @@ -12,7 +13,6 @@ from pyramid.interfaces import IResponseFactory from pyramid.exceptions import ConfigurationError from pyramid.decorator import reify from pyramid.response import Response -from pyramid.threadlocal import get_current_registry from pyramid.url import resource_url from pyramid.url import route_url from pyramid.url import static_url @@ -409,6 +409,83 @@ class Request(BaseRequest): def values(self): return self.environ.values() + # 1.0 deprecated bw compat code for using response_* values + + rr_dep = ('Accessing and setting "request.response_%s" is ' + 'deprecated as of Pyramid 1.1; access or set ' + '"request.response.%s" instead.') + + # response_content_type + def _response_content_type_get(self): + return self._response_content_type + def _response_content_type_set(self, value): + self._response_content_type = value + def _response_content_type_del(self): + del self._response_content_type + response_content_type = property(_response_content_type_get, + _response_content_type_set, + _response_content_type_del) + response_content_type = deprecated( + response_content_type, + rr_dep % ('content_type', 'content_type')) + + # response_headerlist + def _response_headerlist_get(self): + return self._response_headerlist + def _response_headerlist_set(self, value): + self._response_headerlist = value + def _response_headerlist_del(self): + del self._response_headerlist + response_headerlist = property(_response_headerlist_get, + _response_headerlist_set, + _response_headerlist_del) + response_headerlist = deprecated( + response_headerlist, + rr_dep % ('headerlist', 'headerlist')) + + # response_status + def _response_status_get(self): + return self._response_status + def _response_status_set(self, value): + self._response_status = value + def _response_status_del(self): + del self._response_status + response_status = property(_response_status_get, + _response_status_set, + _response_status_del) + + response_status = deprecated( + response_status, + rr_dep % ('status', 'status')) + + # response_charset + def _response_charset_get(self): + return self._response_charset + def _response_charset_set(self, value): + self._response_charset = value + def _response_charset_del(self): + del self._response_charset + response_charset = property(_response_charset_get, + _response_charset_set, + _response_charset_del) + response_charset = deprecated( + response_charset, + rr_dep % ('charset', 'charset')) + + # response_cache_for + def _response_cache_for_get(self): + return self._response_cache_for + def _response_cache_for_set(self, value): + self._response_cache_for = value + def _response_cache_for_del(self): + del self._response_cache_for + response_cache_for = property(_response_cache_for_get, + _response_cache_for_set, + _response_cache_for_del) + response_cache_for = deprecated( + response_cache_for, + rr_dep % ('cache_for', 'cache_expires')) + def route_request_iface(name, bases=()): iface = InterfaceClass('%s_IRequest' % name, bases=bases) # for exception view lookups diff --git a/pyramid/tests/test_renderers.py b/pyramid/tests/test_renderers.py index 0dbb0d821..68369d570 100644 --- a/pyramid/tests/test_renderers.py +++ b/pyramid/tests/test_renderers.py @@ -3,18 +3,6 @@ import unittest from pyramid.testing import cleanUp from pyramid import testing -def hide_warnings(wrapped): - import warnings - def wrapper(*arg, **kw): - warnings.filterwarnings('ignore') - try: - wrapped(*arg, **kw) - finally: - warnings.resetwarnings() - wrapper.__name__ = wrapped.__name__ - wrapper.__doc__ = wrapped.__doc__ - return wrapper - class TestTemplateRendererFactory(unittest.TestCase): def setUp(self): self.config = cleanUp() @@ -619,24 +607,22 @@ class TestRendererHelper(unittest.TestCase): response = helper._make_response(la.encode('utf-8'), request) self.assertEqual(response.body, la.encode('utf-8')) - @hide_warnings def test__make_response_with_content_type(self): from pyramid.response import Response request = testing.DummyRequest() request.response = Response() - attrs = {'response_content_type':'text/nonsense'} + attrs = {'_response_content_type':'text/nonsense'} request.__dict__.update(attrs) helper = self._makeOne('loo.foo') response = helper._make_response('abc', request) self.assertEqual(response.content_type, 'text/nonsense') self.assertEqual(response.body, 'abc') - @hide_warnings def test__make_response_with_headerlist(self): from pyramid.response import Response request = testing.DummyRequest() request.response = Response() - attrs = {'response_headerlist':[('a', '1'), ('b', '2')]} + attrs = {'_response_headerlist':[('a', '1'), ('b', '2')]} request.__dict__.update(attrs) helper = self._makeOne('loo.foo') response = helper._make_response('abc', request) @@ -647,35 +633,32 @@ class TestRendererHelper(unittest.TestCase): ('b', '2')]) self.assertEqual(response.body, 'abc') - @hide_warnings def test__make_response_with_status(self): from pyramid.response import Response request = testing.DummyRequest() request.response = Response() - attrs = {'response_status':'406 You Lose'} + attrs = {'_response_status':'406 You Lose'} request.__dict__.update(attrs) helper = self._makeOne('loo.foo') response = helper._make_response('abc', request) self.assertEqual(response.status, '406 You Lose') self.assertEqual(response.body, 'abc') - @hide_warnings def test__make_response_with_charset(self): from pyramid.response import Response request = testing.DummyRequest() request.response = Response() - attrs = {'response_charset':'UTF-16'} + attrs = {'_response_charset':'UTF-16'} request.__dict__.update(attrs) helper = self._makeOne('loo.foo') response = helper._make_response('abc', request) self.assertEqual(response.charset, 'UTF-16') - @hide_warnings def test__make_response_with_cache_for(self): from pyramid.response import Response request = testing.DummyRequest() request.response = Response() - attrs = {'response_cache_for':100} + attrs = {'_response_cache_for':100} request.__dict__.update(attrs) helper = self._makeOne('loo.foo') response = helper._make_response('abc', request) diff --git a/pyramid/tests/test_request.py b/pyramid/tests/test_request.py index 90c55b0f0..76426b8a8 100644 --- a/pyramid/tests/test_request.py +++ b/pyramid/tests/test_request.py @@ -236,20 +236,21 @@ class TestRequest(unittest.TestCase): class TestRequestDeprecatedMethods(unittest.TestCase): def setUp(self): self.config = testing.setUp() - self.config.begin() - import warnings - warnings.filterwarnings('ignore') + from zope.deprecation import __show__ + __show__.off() def tearDown(self): testing.tearDown() - import warnings - warnings.resetwarnings() + from zope.deprecation import __show__ + __show__.on() def _getTargetClass(self): from pyramid.request import Request return Request - def _makeOne(self, environ): + def _makeOne(self, environ=None): + if environ is None: + environ = {} return self._getTargetClass()(environ) def test___contains__(self): @@ -349,6 +350,46 @@ class TestRequestDeprecatedMethods(unittest.TestCase): result = inst.values() self.assertEqual(result, environ.values()) + def test_response_content_type(self): + inst = self._makeOne() + self.assertFalse(hasattr(inst, 'response_content_type')) + inst.response_content_type = 'abc' + self.assertEqual(inst.response_content_type, 'abc') + del inst.response_content_type + self.assertFalse(hasattr(inst, 'response_content_type')) + + def test_response_headerlist(self): + inst = self._makeOne() + self.assertFalse(hasattr(inst, 'response_headerlist')) + inst.response_headerlist = 'abc' + self.assertEqual(inst.response_headerlist, 'abc') + del inst.response_headerlist + self.assertFalse(hasattr(inst, 'response_headerlist')) + + def test_response_status(self): + inst = self._makeOne() + self.assertFalse(hasattr(inst, 'response_status')) + inst.response_status = 'abc' + self.assertEqual(inst.response_status, 'abc') + del inst.response_status + self.assertFalse(hasattr(inst, 'response_status')) + + def test_response_charset(self): + inst = self._makeOne() + self.assertFalse(hasattr(inst, 'response_charset')) + inst.response_charset = 'abc' + self.assertEqual(inst.response_charset, 'abc') + del inst.response_charset + self.assertFalse(hasattr(inst, 'response_charset')) + + def test_response_cache_for(self): + inst = self._makeOne() + self.assertFalse(hasattr(inst, 'response_cache_for')) + inst.response_cache_for = 'abc' + self.assertEqual(inst.response_cache_for, 'abc') + del inst.response_cache_for + self.assertFalse(hasattr(inst, 'response_cache_for')) + class Test_route_request_iface(unittest.TestCase): def _callFUT(self, name): from pyramid.request import route_request_iface -- cgit v1.2.3 From 311cccf0792233b9312324b0dd56623b929fef96 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 26 Jun 2011 03:46:32 -0400 Subject: try to replicate github #213 --- pyramid/tests/test_url.py | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/pyramid/tests/test_url.py b/pyramid/tests/test_url.py index 69cee8a41..cb8326114 100644 --- a/pyramid/tests/test_url.py +++ b/pyramid/tests/test_url.py @@ -1,13 +1,14 @@ import unittest -from pyramid.testing import cleanUp +from pyramid.testing import setUp +from pyramid.testing import tearDown class ResourceURLTests(unittest.TestCase): def setUp(self): - cleanUp() + setUp() def tearDown(self): - cleanUp() + tearDown() def _callFUT(self, resource, request, *elements, **kw): from pyramid.url import resource_url @@ -146,10 +147,10 @@ class ResourceURLTests(unittest.TestCase): class TestRouteUrl(unittest.TestCase): def setUp(self): - cleanUp() + self.config = setUp() def tearDown(self): - cleanUp() + tearDown() def _callFUT(self, *arg, **kw): from pyramid.url import route_url @@ -264,12 +265,25 @@ class TestRouteUrl(unittest.TestCase): self.assertEqual(result, 'http://example2.com/1/2/3/element1?q=1#anchor') + def test_integration_with_real_request(self): + # to try to replicate https://github.com/Pylons/pyramid/issues/213 + from pyramid.interfaces import IRoutesMapper + from pyramid.request import Request + request = Request.blank('/') + request.registry = self.config.registry + mapper = DummyRoutesMapper(route=DummyRoute('/1/2/3')) + request.registry.registerUtility(mapper, IRoutesMapper) + result = self._callFUT('flub', request, 'extra1', 'extra2') + self.assertEqual(result, + 'http://localhost/1/2/3/extra1/extra2') + + class TestCurrentRouteUrl(unittest.TestCase): def setUp(self): - cleanUp() + setUp() def tearDown(self): - cleanUp() + tearDown() def _callFUT(self, *arg, **kw): from pyramid.url import current_route_url @@ -307,10 +321,10 @@ class TestCurrentRouteUrl(unittest.TestCase): class TestRoutePath(unittest.TestCase): def setUp(self): - cleanUp() + setUp() def tearDown(self): - cleanUp() + tearDown() def _callFUT(self, *arg, **kw): from pyramid.url import route_path @@ -339,10 +353,10 @@ class TestRoutePath(unittest.TestCase): class TestStaticUrl(unittest.TestCase): def setUp(self): - cleanUp() + setUp() def tearDown(self): - cleanUp() + tearDown() def _callFUT(self, *arg, **kw): from pyramid.url import static_url -- cgit v1.2.3 From 8bd6cf291b91977f22d8e153328cc13d38d00ff2 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 26 Jun 2011 03:46:47 -0400 Subject: - Added ``mako.preprocessor`` config file parameter; allows for a Mako preprocessor to be specified as a Python callable or Python dotted name. See https://github.com/Pylons/pyramid/pull/183 for rationale. Closes #183. --- CHANGES.txt | 7 +++++++ docs/narr/environment.rst | 29 ++++++++++++++++++++++++----- docs/whatsnew-1.1.rst | 4 ++++ pyramid/mako_templating.py | 8 +++++++- pyramid/tests/test_mako_templating.py | 15 +++++++++++++++ 5 files changed, 57 insertions(+), 6 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index b95211d09..81004b00e 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,6 +1,13 @@ Next release ============ +Features +-------- + +- Added ``mako.preprocessor`` config file parameter; allows for a Mako + preprocessor to be specified as a Python callable or Python dotted name. + See https://github.com/Pylons/pyramid/pull/183 for rationale. + Bug fixes --------- diff --git a/docs/narr/environment.rst b/docs/narr/environment.rst index 3b938c09c..a57b316e1 100644 --- a/docs/narr/environment.rst +++ b/docs/narr/environment.rst @@ -227,11 +227,11 @@ should be changed accordingly. Mako Error Handler ++++++++++++++++++ -Python callable which is called whenever Mako compile or runtime exceptions -occur. The callable is passed the current context as well as the exception. If -the callable returns True, the exception is considered to be handled, else it -is re-raised after the function completes. Is used to provide custom -error-rendering functions. +A callable (or a :term:`dotted Python name` which names a callable) which is +called whenever Mako compile or runtime exceptions occur. The callable is +passed the current context as well as the exception. If the callable returns +True, the exception is considered to be handled, else it is re-raised after +the function completes. Is used to provide custom error-rendering functions. +-----------------------------+ | Config File Setting Name | @@ -290,6 +290,25 @@ default, this is ``false``. | | +-----------------------------+ +Mako Preprocessor ++++++++++++++++++ + +A callable (or a :term:`dotted Python name` which names a callable) which is +called to preprocess the source before the template is called. The callable +will be passed the full template source before it is parsed. The return +result of the callable will be used as the template source code. + +.. note:: This feature is new in Pyramid 1.1. + ++-----------------------------+ +| Config File Setting Name | ++=============================+ +| ``mako.preprocessor`` | +| | +| | +| | ++-----------------------------+ + Examples -------- diff --git a/docs/whatsnew-1.1.rst b/docs/whatsnew-1.1.rst index 180380608..4d7567886 100644 --- a/docs/whatsnew-1.1.rst +++ b/docs/whatsnew-1.1.rst @@ -165,6 +165,10 @@ Minor Feature Additions - :class:`pyramid.exceptions.Forbidden` is now just an alias for :class:`pyramid.httpexceptions.HTTPForbidden`. +- Added ``mako.preprocessor`` config file parameter; allows for a Mako + preprocessor to be specified as a Python callable or Python dotted name. + See https://github.com/Pylons/pyramid/pull/183 for rationale. + Backwards Incompatibilities --------------------------- diff --git a/pyramid/mako_templating.py b/pyramid/mako_templating.py index fea8066d4..3055e2cfb 100644 --- a/pyramid/mako_templating.py +++ b/pyramid/mako_templating.py @@ -69,6 +69,7 @@ def renderer_factory(info): default_filters = settings.get('mako.default_filters', 'h') imports = settings.get('mako.imports', None) strict_undefined = settings.get('mako.strict_undefined', 'false') + preprocessor = settings.get('mako.preprocessor', None) if directories is None: raise ConfigurationError( 'Mako template used without a ``mako.directories`` setting') @@ -87,6 +88,10 @@ def renderer_factory(info): if not hasattr(imports, '__iter__'): imports = filter(None, imports.splitlines()) strict_undefined = asbool(strict_undefined) + if preprocessor is not None: + dotted = DottedNameResolver(info.package) + preprocessor = dotted.maybe_resolve(preprocessor) + lookup = PkgResourceTemplateLookup(directories=directories, module_directory=module_directory, @@ -95,7 +100,8 @@ def renderer_factory(info): default_filters=default_filters, imports=imports, filesystem_checks=reload_templates, - strict_undefined=strict_undefined) + strict_undefined=strict_undefined, + preprocessor=preprocessor) registry_lock.acquire() try: registry.registerUtility(lookup, IMakoLookup) diff --git a/pyramid/tests/test_mako_templating.py b/pyramid/tests/test_mako_templating.py index 6b2adbe09..c63895216 100644 --- a/pyramid/tests/test_mako_templating.py +++ b/pyramid/tests/test_mako_templating.py @@ -139,6 +139,21 @@ class Test_renderer_factory(Base, unittest.TestCase): lookup = self.config.registry.getUtility(IMakoLookup) self.assertEqual(lookup.template_args['error_handler'], pyramid.tests) + def test_with_preprocessor(self): + from pyramid.mako_templating import IMakoLookup + settings = {'mako.directories':self.templates_dir, + 'mako.preprocessor':'pyramid.tests'} + import pyramid.tests + info = DummyRendererInfo({ + 'name':'helloworld.mak', + 'package':None, + 'registry':self.config.registry, + 'settings':settings, + }) + self._callFUT(info) + lookup = self.config.registry.getUtility(IMakoLookup) + self.assertEqual(lookup.template_args['preprocessor'], pyramid.tests) + def test_with_default_filters(self): from pyramid.mako_templating import IMakoLookup settings = {'mako.directories':self.templates_dir, -- cgit v1.2.3 From 1ba6fe34b9f37594d926cc63b6219ad61dbeebb6 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 26 Jun 2011 04:00:52 -0400 Subject: prep for 1.1a3 --- CHANGES.txt | 4 ++-- docs/conf.py | 2 +- setup.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 81004b00e..d25b1be66 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,5 +1,5 @@ -Next release -============ +1.1a3 (2011-06-26) +================== Features -------- diff --git a/docs/conf.py b/docs/conf.py index 34262191f..02f0339b5 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -93,7 +93,7 @@ copyright = '%s, Agendaless Consulting' % datetime.datetime.now().year # other places throughout the built documents. # # The short X.Y version. -version = '1.1a2' +version = '1.1a3' # The full version, including alpha/beta/rc tags. release = version diff --git a/setup.py b/setup.py index 5c3232fca..91d456a89 100644 --- a/setup.py +++ b/setup.py @@ -53,7 +53,7 @@ if sys.version_info[:2] < (2, 6): install_requires.append('simplejson') setup(name='pyramid', - version='1.1a2', + version='1.1a3', description=('The Pyramid web application development framework, a ' 'Pylons project'), long_description=README + '\n\n' + CHANGES, -- cgit v1.2.3 From 82ec1b881a61c94b6fb3066a2ac6b686eb0b2bd9 Mon Sep 17 00:00:00 2001 From: Blaise Laflamme Date: Sun, 26 Jun 2011 20:49:26 -0400 Subject: added github url option in docs conf for ribbon --- docs/conf.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index 02f0339b5..86a92badd 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -140,6 +140,10 @@ sys.path.append(os.path.abspath('_themes')) html_theme_path = ['_themes'] html_theme = 'pyramid' +html_theme_options = { + 'github_url': 'https://github.com/Pylons/pyramid' +} + # The style sheet to use for HTML and HTML Help pages. A file of that name # must exist either in Sphinx' static/ path, or in one of the custom paths # given in html_static_path. -- cgit v1.2.3 From 9395f0747ab5cee6b97674251c586dd662d0dd6d Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 26 Jun 2011 21:00:57 -0400 Subject: - ``pyramid.testing.DummyRequest`` now raises deprecation warnings when attributes deprecated for ``pyramid.request.Request`` are accessed (like ``response_content_type``). This is for the benefit of folks running unit tests which use DummyRequest instead of a "real" request, so they know things are deprecated without necessarily needing a functional test suite. --- CHANGES.txt | 12 ++ pyramid/request.py | 313 +++++++++++++++++++++++++++-------------------------- pyramid/testing.py | 3 +- 3 files changed, 172 insertions(+), 156 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index d25b1be66..1f0549895 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,15 @@ +Next release +============ + +Bug Fixes +--------- + +- ``pyramid.testing.DummyRequest`` now raises deprecation warnings when + attributes deprecated for ``pyramid.request.Request`` are accessed (like + ``response_content_type``). This is for the benefit of folks running unit + tests which use DummyRequest instead of a "real" request, so they know + things are deprecated without necessarily needing a functional test suite. + 1.1a3 (2011-06-26) ================== diff --git a/pyramid/request.py b/pyramid/request.py index b9dd2dfe9..cc5137869 100644 --- a/pyramid/request.py +++ b/pyramid/request.py @@ -21,7 +21,164 @@ from pyramid.url import route_path class TemplateContext(object): pass -class Request(BaseRequest): +class DeprecatedRequestMethods(object): + + # b/c dict interface for "root factory" code that expects a bare + # environ. Explicitly omitted dict methods: clear (unnecessary), + # copy (implemented by WebOb), fromkeys (unnecessary); deprecated + # as of Pyramid 1.1. + + dictlike = ('Use of the request as a dict-like object is deprecated as ' + 'of Pyramid 1.1. Use dict-like methods of "request.environ" ' + 'instead.') + + @deprecate(dictlike) + def __contains__(self, k): + return self.environ.__contains__(k) + + @deprecate(dictlike) + def __delitem__(self, k): + return self.environ.__delitem__(k) + + @deprecate(dictlike) + def __getitem__(self, k): + return self.environ.__getitem__(k) + + @deprecate(dictlike) + def __iter__(self): + return iter(self.environ) + + @deprecate(dictlike) + def __setitem__(self, k, v): + self.environ[k] = v + + @deprecate(dictlike) + def get(self, k, default=None): + return self.environ.get(k, default) + + @deprecate(dictlike) + def has_key(self, k): + return k in self.environ + + @deprecate(dictlike) + def items(self): + return self.environ.items() + + @deprecate(dictlike) + def iteritems(self): + return self.environ.iteritems() + + @deprecate(dictlike) + def iterkeys(self): + return self.environ.iterkeys() + + @deprecate(dictlike) + def itervalues(self): + return self.environ.itervalues() + + @deprecate(dictlike) + def keys(self): + return self.environ.keys() + + @deprecate(dictlike) + def pop(self, k): + return self.environ.pop(k) + + @deprecate(dictlike) + def popitem(self): + return self.environ.popitem() + + @deprecate(dictlike) + def setdefault(self, v, default): + return self.environ.setdefault(v, default) + + @deprecate(dictlike) + def update(self, v, **kw): + return self.environ.update(v, **kw) + + @deprecate(dictlike) + def values(self): + return self.environ.values() + + # 1.0 deprecated bw compat code for using response_* values + + rr_dep = ('Accessing and setting "request.response_%s" is ' + 'deprecated as of Pyramid 1.1; access or set ' + '"request.response.%s" instead.') + + # response_content_type + def _response_content_type_get(self): + return self._response_content_type + def _response_content_type_set(self, value): + self._response_content_type = value + def _response_content_type_del(self): + del self._response_content_type + response_content_type = property(_response_content_type_get, + _response_content_type_set, + _response_content_type_del) + response_content_type = deprecated( + response_content_type, + rr_dep % ('content_type', 'content_type')) + + # response_headerlist + def _response_headerlist_get(self): + return self._response_headerlist + def _response_headerlist_set(self, value): + self._response_headerlist = value + def _response_headerlist_del(self): + del self._response_headerlist + response_headerlist = property(_response_headerlist_get, + _response_headerlist_set, + _response_headerlist_del) + response_headerlist = deprecated( + response_headerlist, + rr_dep % ('headerlist', 'headerlist')) + + # response_status + def _response_status_get(self): + return self._response_status + def _response_status_set(self, value): + self._response_status = value + def _response_status_del(self): + del self._response_status + response_status = property(_response_status_get, + _response_status_set, + _response_status_del) + + response_status = deprecated( + response_status, + rr_dep % ('status', 'status')) + + # response_charset + def _response_charset_get(self): + return self._response_charset + def _response_charset_set(self, value): + self._response_charset = value + def _response_charset_del(self): + del self._response_charset + response_charset = property(_response_charset_get, + _response_charset_set, + _response_charset_del) + response_charset = deprecated( + response_charset, + rr_dep % ('charset', 'charset')) + + # response_cache_for + def _response_cache_for_get(self): + return self._response_cache_for + def _response_cache_for_set(self, value): + self._response_cache_for = value + def _response_cache_for_del(self): + del self._response_cache_for + response_cache_for = property(_response_cache_for_get, + _response_cache_for_set, + _response_cache_for_del) + response_cache_for = deprecated( + response_cache_for, + rr_dep % ('cache_for', 'cache_expires')) + + +class Request(BaseRequest, DeprecatedRequestMethods): """ A subclass of the :term:`WebOb` Request class. An instance of this class is created by the :term:`router` and is provided to a @@ -332,160 +489,6 @@ class Request(BaseRequest): return False return adapted is ob - # b/c dict interface for "root factory" code that expects a bare - # environ. Explicitly omitted dict methods: clear (unnecessary), - # copy (implemented by WebOb), fromkeys (unnecessary); deprecated - # as of Pyramid 1.1. - - dictlike = ('Use of the request as a dict-like object is deprecated as ' - 'of Pyramid 1.1. Use dict-like methods of "request.environ" ' - 'instead.') - - @deprecate(dictlike) - def __contains__(self, k): - return self.environ.__contains__(k) - - @deprecate(dictlike) - def __delitem__(self, k): - return self.environ.__delitem__(k) - - @deprecate(dictlike) - def __getitem__(self, k): - return self.environ.__getitem__(k) - - @deprecate(dictlike) - def __iter__(self): - return iter(self.environ) - - @deprecate(dictlike) - def __setitem__(self, k, v): - self.environ[k] = v - - @deprecate(dictlike) - def get(self, k, default=None): - return self.environ.get(k, default) - - @deprecate(dictlike) - def has_key(self, k): - return k in self.environ - - @deprecate(dictlike) - def items(self): - return self.environ.items() - - @deprecate(dictlike) - def iteritems(self): - return self.environ.iteritems() - - @deprecate(dictlike) - def iterkeys(self): - return self.environ.iterkeys() - - @deprecate(dictlike) - def itervalues(self): - return self.environ.itervalues() - - @deprecate(dictlike) - def keys(self): - return self.environ.keys() - - @deprecate(dictlike) - def pop(self, k): - return self.environ.pop(k) - - @deprecate(dictlike) - def popitem(self): - return self.environ.popitem() - - @deprecate(dictlike) - def setdefault(self, v, default): - return self.environ.setdefault(v, default) - - @deprecate(dictlike) - def update(self, v, **kw): - return self.environ.update(v, **kw) - - @deprecate(dictlike) - def values(self): - return self.environ.values() - - # 1.0 deprecated bw compat code for using response_* values - - rr_dep = ('Accessing and setting "request.response_%s" is ' - 'deprecated as of Pyramid 1.1; access or set ' - '"request.response.%s" instead.') - - # response_content_type - def _response_content_type_get(self): - return self._response_content_type - def _response_content_type_set(self, value): - self._response_content_type = value - def _response_content_type_del(self): - del self._response_content_type - response_content_type = property(_response_content_type_get, - _response_content_type_set, - _response_content_type_del) - response_content_type = deprecated( - response_content_type, - rr_dep % ('content_type', 'content_type')) - - # response_headerlist - def _response_headerlist_get(self): - return self._response_headerlist - def _response_headerlist_set(self, value): - self._response_headerlist = value - def _response_headerlist_del(self): - del self._response_headerlist - response_headerlist = property(_response_headerlist_get, - _response_headerlist_set, - _response_headerlist_del) - response_headerlist = deprecated( - response_headerlist, - rr_dep % ('headerlist', 'headerlist')) - - # response_status - def _response_status_get(self): - return self._response_status - def _response_status_set(self, value): - self._response_status = value - def _response_status_del(self): - del self._response_status - response_status = property(_response_status_get, - _response_status_set, - _response_status_del) - - response_status = deprecated( - response_status, - rr_dep % ('status', 'status')) - - # response_charset - def _response_charset_get(self): - return self._response_charset - def _response_charset_set(self, value): - self._response_charset = value - def _response_charset_del(self): - del self._response_charset - response_charset = property(_response_charset_get, - _response_charset_set, - _response_charset_del) - response_charset = deprecated( - response_charset, - rr_dep % ('charset', 'charset')) - - # response_cache_for - def _response_cache_for_get(self): - return self._response_cache_for - def _response_cache_for_set(self, value): - self._response_cache_for = value - def _response_cache_for_del(self): - del self._response_cache_for - response_cache_for = property(_response_cache_for_get, - _response_cache_for_set, - _response_cache_for_del) - response_cache_for = deprecated( - response_cache_for, - rr_dep % ('cache_for', 'cache_expires')) - def route_request_iface(name, bases=()): iface = InterfaceClass('%s_IRequest' % name, bases=bases) # for exception view lookups diff --git a/pyramid/testing.py b/pyramid/testing.py index 27942baf5..5b0f37f45 100644 --- a/pyramid/testing.py +++ b/pyramid/testing.py @@ -25,6 +25,7 @@ from pyramid.security import Everyone from pyramid.security import has_permission from pyramid.threadlocal import get_current_registry from pyramid.threadlocal import manager +from pyramid.request import DeprecatedRequestMethods _marker = object() @@ -619,7 +620,7 @@ class DummySession(dict): def get_csrf_token(self): return self.get('_csrft_', None) -class DummyRequest(object): +class DummyRequest(DeprecatedRequestMethods): """ A DummyRequest object (incompletely) imitates a :term:`request` object. The ``params``, ``environ``, ``headers``, ``path``, and -- cgit v1.2.3 From ddfac18fed90d785a9a7d3666a1de91e13f8dced Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 28 Jun 2011 08:45:03 -0400 Subject: unused import --- pyramid/renderers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyramid/renderers.py b/pyramid/renderers.py index b8d02ace3..64c522eb4 100644 --- a/pyramid/renderers.py +++ b/pyramid/renderers.py @@ -1,7 +1,6 @@ import os import pkg_resources import threading -import warnings from zope.interface import implements -- cgit v1.2.3 From 1655257130720a29ba9d1d9c3ee862d1708a02a1 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 30 Jun 2011 18:53:39 -0400 Subject: garden --- TODO.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/TODO.txt b/TODO.txt index ba9a72d03..b15fac342 100644 --- a/TODO.txt +++ b/TODO.txt @@ -7,6 +7,8 @@ Must-Have - Investigate mod_wsgi tutorial to make sure it still works (2 reports say no; application package not found). +- Deprecate ``renderer_global_factory`` arg to Configurator and related. + Should-Have ----------- -- cgit v1.2.3 From dc7bcb4b633718267a2509a580faf45efe338630 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 30 Jun 2011 18:56:56 -0400 Subject: garden --- TODO.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/TODO.txt b/TODO.txt index b15fac342..c74e9e405 100644 --- a/TODO.txt +++ b/TODO.txt @@ -9,6 +9,8 @@ Must-Have - Deprecate ``renderer_global_factory`` arg to Configurator and related. +- Maybe add ``add_renderer_globals`` method to Configurator. + Should-Have ----------- -- cgit v1.2.3 From a0635d41cbbc4df147f61e35aae017fa7fc629c2 Mon Sep 17 00:00:00 2001 From: Niall O'Higgins Date: Thu, 30 Jun 2011 16:07:14 -0700 Subject: Zap word "either" describing request_method. By my reading, the word "either" implies there is an alternative to one of the listed strings (perhaps that you can pass some other kind of value such as a sequence of allowed request methods - which is not the case). --- pyramid/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/config.py b/pyramid/config.py index 3681e48ee..d27d35464 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -1157,7 +1157,7 @@ class Configurator(object): request_method - This value can either be one of the strings ``GET``, + This value can be one of the strings ``GET``, ``POST``, ``PUT``, ``DELETE``, or ``HEAD`` representing an HTTP ``REQUEST_METHOD``. A view declaration with this argument ensures that the view will only be called when the -- cgit v1.2.3 From c1f3d0fd89eb7f62a7de365ca5c0ef5600ffd900 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 1 Jul 2011 00:59:11 -0400 Subject: Add JSONP renderer --- CHANGES.txt | 6 ++++ docs/api/renderers.rst | 2 ++ docs/narr/renderers.rst | 68 +++++++++++++++++++++++++++++++++++++++++ docs/whatsnew-1.1.rst | 3 ++ pyramid/renderers.py | 68 ++++++++++++++++++++++++++++++++++++++++- pyramid/tests/test_renderers.py | 25 +++++++++++++++ 6 files changed, 171 insertions(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index 1f0549895..9bf8197ab 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -10,6 +10,12 @@ Bug Fixes tests which use DummyRequest instead of a "real" request, so they know things are deprecated without necessarily needing a functional test suite. +Features +-------- + +- Add JSONP renderer (see "JSONP renderer" in the Renderers chapter of the + documentation). + 1.1a3 (2011-06-26) ================== diff --git a/docs/api/renderers.rst b/docs/api/renderers.rst index 459639a46..c13694219 100644 --- a/docs/api/renderers.rst +++ b/docs/api/renderers.rst @@ -11,3 +11,5 @@ .. autofunction:: render_to_response +.. autoclass:: JSONP + diff --git a/docs/narr/renderers.rst b/docs/narr/renderers.rst index 18cc8e539..f329a7af9 100644 --- a/docs/narr/renderers.rst +++ b/docs/narr/renderers.rst @@ -228,6 +228,74 @@ Views which use the JSON renderer can vary non-body response attributes by using the api of the ``request.response`` attribute. See :ref:`request_response_attr`. +.. _jsonp_renderer: + +JSONP Renderer +-------------- + +.. note:: This feature is new in Pyramid 1.1. + +:class:`pyramid.renderers.JSONP` is a `JSONP +`_ renderer factory helper which +implements a hybrid json/jsonp renderer. JSONP is useful for making +cross-domain AJAX requests. + +Unlike other renderers, a JSONP renderer needs to be configured at startup +time "by hand". Configure a JSONP renderer using the +:meth:`pyramid.config.Configurator.add_renderer` method: + +.. code-block:: python + + from pyramid.config import Configurator + + config = Configurator() + config.add_renderer('jsonp', JSONP(param_name='callback')) + +Once this renderer is registered via +:meth:`~pyramid.config.Configurator.add_renderer` as above, you can use +``jsonp`` as the ``renderer=`` parameter to ``@view_config`` or +:meth:`pyramid.config.Configurator.add_view``: + +.. code-block:: python + + from pyramid.view import view_config + + @view_config(renderer='jsonp') + def myview(request): + return {'greeting':'Hello world'} + +When a view is called that uses a JSONP renderer: + +- If there is a parameter in the request's HTTP query string (aka + ``request.GET``) that matches the ``param_name`` of the registered JSONP + renderer (by default, ``callback``), the renderer will return a JSONP + response. + +- If there is no callback parameter in the request's query string, the + renderer will return a 'plain' JSON response. + +Javscript library AJAX functionality will help you make JSONP requests. +For example, JQuery has a `getJSON function +`_, and has equivalent (but more +complicated) functionality in its `ajax function +`_. + +For example (Javascript): + +.. code-block:: javascript + + var api_url = 'http://api.geonames.org/timezoneJSON' + + '?lat=38.301733840000004' + + '&lng=-77.45869621' + + '&username=fred' + + '&callback=?'; + jqhxr = $.getJSON(api_url); + +The string ``callback=?`` above in the the ``url`` param to the JQuery +``getAjax`` function indicates to jQuery that the query should be made as +a JSONP request; the ``callback`` parameter will be automatically filled +in for you and used. + .. index:: pair: renderer; chameleon diff --git a/docs/whatsnew-1.1.rst b/docs/whatsnew-1.1.rst index 4d7567886..9895858cd 100644 --- a/docs/whatsnew-1.1.rst +++ b/docs/whatsnew-1.1.rst @@ -94,6 +94,9 @@ Default HTTP Exception View Minor Feature Additions ----------------------- +- A `JSONP `_ renderer. See + :ref:`jsonp_renderer` for more details. + - New authentication policy: :class:`pyramid.authentication.SessionAuthenticationPolicy`, which uses a session to store credentials. diff --git a/pyramid/renderers.py b/pyramid/renderers.py index 64c522eb4..b201d32c2 100644 --- a/pyramid/renderers.py +++ b/pyramid/renderers.py @@ -127,7 +127,6 @@ def get_renderer(renderer_name, package=None): helper = RendererHelper(name=renderer_name, package=package) return helper.renderer - # concrete renderer factory implementations (also API) def json_renderer_factory(info): @@ -154,6 +153,73 @@ def string_renderer_factory(info): return value return _render +class JSONP(object): + """ `JSONP `_ renderer factory helper + which implements a hybrid json/jsonp renderer. JSONP is useful for + making cross-domain AJAX requests. + + Configure a JSONP renderer using the + :meth:`pyramid.config.Configurator.add_renderer` API at application + startup time: + + .. code-block:: python + + from pyramid.config import Configurator + + config = Configurator() + config.add_renderer('jsonp', JSONP(param_name='callback')) + + Once this renderer is registered via + :meth:`~pyramid.config.Configurator.add_renderer` as above, you can use + ``jsonp`` as the ``renderer=`` parameter to ``@view_config`` or + :meth:`pyramid.config.Configurator.add_view``: + + .. code-block:: python + + from pyramid.view import view_config + + @view_config(renderer='jsonp') + def myview(request): + return {'greeting':'Hello world'} + + When a view is called that uses the JSONP renderer: + + - If there is a parameter in the request's HTTP query string that matches + the ``param_name`` of the registered JSONP renderer (by default, + ``callback``), the renderer will return a JSONP response. + + - If there is no callback parameter in the request's query string, the + renderer will return a 'plain' JSON response. + + .. note:: This feature is new in Pyramid 1.1. + + See also: :ref:`jsonp_renderer`. + """ + + def __init__(self, param_name='callback'): + self.param_name = param_name + + def __call__(self, info): + """ Returns JSONP-encoded string with content-type + ``application/javascript`` if query parameter matching + ``self.param_name`` is present in request.GET; otherwise returns + plain-JSON encoded string with content-type ``application/json``""" + def _render(value, system): + request = system['request'] + val = json.dumps(value) + callback = request.GET.get(self.param_name) + if callback is None: + ct = 'application/json' + body = val + else: + ct = 'application/javascript' + body = '%s(%s)' % (callback, val) + response = request.response + if response.content_type == response.default_content_type: + response.content_type = ct + return body + return _render + # utility functions, not API class ChameleonRendererLookup(object): diff --git a/pyramid/tests/test_renderers.py b/pyramid/tests/test_renderers.py index 68369d570..18b4caa61 100644 --- a/pyramid/tests/test_renderers.py +++ b/pyramid/tests/test_renderers.py @@ -798,6 +798,31 @@ class Test_get_renderer(unittest.TestCase): result = self._callFUT('abc/def.pt', package=pyramid.tests) self.assertEqual(result, renderer) +class TestJSONP(unittest.TestCase): + def _makeOne(self, param_name='callback'): + from pyramid.renderers import JSONP + return JSONP(param_name) + + def test_render_to_jsonp(self): + renderer_factory = self._makeOne() + renderer = renderer_factory(None) + request = testing.DummyRequest() + request.GET['callback'] = 'callback' + result = renderer({'a':'1'}, {'request':request}) + self.assertEqual(result, 'callback({"a": "1"})') + self.assertEqual(request.response.content_type, + 'application/javascript') + + def test_render_to_json(self): + renderer_factory = self._makeOne() + renderer = renderer_factory(None) + request = testing.DummyRequest() + result = renderer({'a':'1'}, {'request':request}) + self.assertEqual(result, '{"a": "1"}') + self.assertEqual(request.response.content_type, + 'application/json') + + class Dummy: pass -- cgit v1.2.3 From 6579f51fec8506332cdd61d06ed20178f0bdfbcb Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 1 Jul 2011 01:02:55 -0400 Subject: typo --- docs/whatsnew-1.1.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/whatsnew-1.1.rst b/docs/whatsnew-1.1.rst index 9895858cd..7d0f666d3 100644 --- a/docs/whatsnew-1.1.rst +++ b/docs/whatsnew-1.1.rst @@ -95,7 +95,7 @@ Minor Feature Additions ----------------------- - A `JSONP `_ renderer. See - :ref:`jsonp_renderer` for more details. + :ref:`jsonp_renderer` for more details. - New authentication policy: :class:`pyramid.authentication.SessionAuthenticationPolicy`, which uses a -- cgit v1.2.3 From b7f33b5fdd062e007723d0eb60001442f35c0bf7 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 1 Jul 2011 01:24:39 -0400 Subject: - Deprecated the ``set_renderer_globals_factory`` method of the Configurator and the ``renderer_globals`` Configurator constructor parameter. --- CHANGES.txt | 6 ++++ docs/api/config.rst | 2 +- docs/narr/hooks.rst | 7 ++-- docs/whatsnew-1.1.rst | 6 ++++ pyramid/config.py | 28 ++++++++++++++-- pyramid/tests/test_config.py | 78 ++++++++++++++++++++++++++++---------------- 6 files changed, 93 insertions(+), 34 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 9bf8197ab..d2b97ece6 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -16,6 +16,12 @@ Features - Add JSONP renderer (see "JSONP renderer" in the Renderers chapter of the documentation). +Deprecations +------------ + +- Deprecated the ``set_renderer_globals_factory`` method of the Configurator + and the ``renderer_globals`` Configurator constructor parameter. + 1.1a3 (2011-06-26) ================== diff --git a/docs/api/config.rst b/docs/api/config.rst index 274ee0292..71ef4a746 100644 --- a/docs/api/config.rst +++ b/docs/api/config.rst @@ -74,7 +74,7 @@ .. automethod:: set_request_factory - .. automethod:: set_renderer_globals_factory + .. automethod:: set_renderer_globals_factory(factory) .. automethod:: set_view_mapper diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index 1c8a64fd7..56c566a4c 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -183,6 +183,10 @@ already constructed a :term:`configurator` it can also be registered via the Adding Renderer Globals ----------------------- +.. warning:: this feature is deprecated as of Pyramid 1.1. A non-deprecated + mechanism which allows event subscribers to add renderer global values + is documented in :ref:`beforerender_event`. + Whenever :app:`Pyramid` handles a request to perform a rendering (after a view with a ``renderer=`` configuration attribute is invoked, or when any of the methods beginning with ``render`` within the :mod:`pyramid.renderers` @@ -227,9 +231,6 @@ already constructed a :term:`configurator` it can also be registered via the config = Configurator() config.set_renderer_globals_factory(renderer_globals_factory) -Another mechanism which allows event subscribers to add renderer global values -exists in :ref:`beforerender_event`. - .. index:: single: before render event diff --git a/docs/whatsnew-1.1.rst b/docs/whatsnew-1.1.rst index 7d0f666d3..3252cba22 100644 --- a/docs/whatsnew-1.1.rst +++ b/docs/whatsnew-1.1.rst @@ -364,6 +364,12 @@ Deprecations and Behavior Differences within a static view returns the index.html properly. See also https://github.com/Pylons/pyramid/issues/67. +- Deprecated the + :meth:`pyramid.config.Configurator.set_renderer_globals_factory` method and + the ``renderer_globals`` Configurator constructor parameter. Users should + use convert code using this feature to use a BeforeRender event als + :ref:`beforerender_event`. + Dependency Changes ------------------ diff --git a/pyramid/config.py b/pyramid/config.py index d27d35464..bf3793c26 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -215,6 +215,10 @@ class Configurator(object): See :ref:`adding_renderer_globals`. By default, it is ``None``, which means use no renderer globals factory. + .. warning:: as of Pyramid 1.1, ``renderer_globals_factory`` is + deprecated. Instead, use a BeforeRender event subscriber as per + :ref:`beforerender_event`. + If ``default_permission`` is passed, it should be a :term:`permission` string to be used as the default permission for all view configuration registrations performed against this @@ -736,9 +740,18 @@ class Configurator(object): request_factory = self.maybe_dotted(request_factory) self.set_request_factory(request_factory) if renderer_globals_factory: + warnings.warn( + 'Passing ``renderer_globals_factory`` as a Configurator ' + 'constructor parameter is deprecated as of Pyramid 1.1. ' + 'Use a BeforeRender event subscriber as documented in the ' + '"Hooks" chapter of the Pyramid narrative documentation ' + 'instead', + DeprecationWarning, + 2) renderer_globals_factory = self.maybe_dotted( renderer_globals_factory) - self.set_renderer_globals_factory(renderer_globals_factory) + self.set_renderer_globals_factory(renderer_globals_factory, + warn=False) if default_permission: self.set_default_permission(default_permission) if session_factory is not None: @@ -2105,7 +2118,7 @@ class Configurator(object): self.action(IRequestFactory, register) @action_method - def set_renderer_globals_factory(self, factory): + def set_renderer_globals_factory(self, factory, warn=True): """ The object passed as ``factory`` should be an callable (or a :term:`dotted Python name` which refers to an callable) that will be used by the :app:`Pyramid` rendering machinery as a @@ -2118,10 +2131,21 @@ class Configurator(object): dictionary, and therefore will be made available to the code which uses the renderer. + .. warning:: This method is deprecated as of Pyramid 1.1. + .. note:: Using the ``renderer_globals_factory`` argument to the :class:`pyramid.config.Configurator` constructor can be used to achieve the same purpose. """ + if warn: + warnings.warn( + 'Calling the ``set_renderer_globals`` method of a Configurator ' + 'is deprecated as of Pyramid 1.1. Use a BeforeRender event ' + 'subscriber as documented in the "Hooks" chapter of the ' + 'Pyramid narrative documentation instead', + DeprecationWarning, + 3) + factory = self.maybe_dotted(factory) def register(): self.registry.registerUtility(factory, IRendererGlobalsFactory) diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py index 41811782a..20fdd93e8 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -521,24 +521,34 @@ class ConfiguratorTests(unittest.TestCase): self.assertEqual(utility, pyramid.tests) def test_setup_registry_renderer_globals_factory(self): - from pyramid.registry import Registry - from pyramid.interfaces import IRendererGlobalsFactory - reg = Registry() - config = self._makeOne(reg) - factory = object() - config.setup_registry(renderer_globals_factory=factory) - utility = reg.getUtility(IRendererGlobalsFactory) - self.assertEqual(utility, factory) + import warnings + warnings.filterwarnings('ignore') + try: + from pyramid.registry import Registry + from pyramid.interfaces import IRendererGlobalsFactory + reg = Registry() + config = self._makeOne(reg) + factory = object() + config.setup_registry(renderer_globals_factory=factory) + utility = reg.getUtility(IRendererGlobalsFactory) + self.assertEqual(utility, factory) + finally: + warnings.resetwarnings() def test_setup_registry_renderer_globals_factory_dottedname(self): - from pyramid.registry import Registry - from pyramid.interfaces import IRendererGlobalsFactory - reg = Registry() - config = self._makeOne(reg) - import pyramid.tests - config.setup_registry(renderer_globals_factory='pyramid.tests') - utility = reg.getUtility(IRendererGlobalsFactory) - self.assertEqual(utility, pyramid.tests) + import warnings + warnings.filterwarnings('ignore') + try: + from pyramid.registry import Registry + from pyramid.interfaces import IRendererGlobalsFactory + reg = Registry() + config = self._makeOne(reg) + import pyramid.tests + config.setup_registry(renderer_globals_factory='pyramid.tests') + utility = reg.getUtility(IRendererGlobalsFactory) + self.assertEqual(utility, pyramid.tests) + finally: + warnings.resetwarnings() def test_setup_registry_alternate_renderers(self): from pyramid.registry import Registry @@ -2415,20 +2425,32 @@ class ConfiguratorTests(unittest.TestCase): dummyfactory) def test_set_renderer_globals_factory(self): - from pyramid.interfaces import IRendererGlobalsFactory - config = self._makeOne(autocommit=True) - factory = object() - config.set_renderer_globals_factory(factory) - self.assertEqual(config.registry.getUtility(IRendererGlobalsFactory), - factory) + import warnings + warnings.filterwarnings('ignore') + try: + from pyramid.interfaces import IRendererGlobalsFactory + config = self._makeOne(autocommit=True) + factory = object() + config.set_renderer_globals_factory(factory) + self.assertEqual( + config.registry.getUtility(IRendererGlobalsFactory), + factory) + finally: + warnings.resetwarnings() def test_set_renderer_globals_factory_dottedname(self): - from pyramid.interfaces import IRendererGlobalsFactory - config = self._makeOne(autocommit=True) - config.set_renderer_globals_factory( - 'pyramid.tests.test_config.dummyfactory') - self.assertEqual(config.registry.getUtility(IRendererGlobalsFactory), - dummyfactory) + import warnings + warnings.filterwarnings('ignore') + try: + from pyramid.interfaces import IRendererGlobalsFactory + config = self._makeOne(autocommit=True) + config.set_renderer_globals_factory( + 'pyramid.tests.test_config.dummyfactory') + self.assertEqual( + config.registry.getUtility(IRendererGlobalsFactory), + dummyfactory) + finally: + warnings.resetwarnings() def test_set_default_permission(self): from pyramid.interfaces import IDefaultPermission -- cgit v1.2.3 From 371a670b02052dcbf42629f228373cae805b0dd3 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 1 Jul 2011 01:26:13 -0400 Subject: garden --- TODO.txt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/TODO.txt b/TODO.txt index c74e9e405..0b83e01a6 100644 --- a/TODO.txt +++ b/TODO.txt @@ -4,21 +4,21 @@ Pyramid TODOs Must-Have --------- -- Investigate mod_wsgi tutorial to make sure it still works (2 reports say - no; application package not found). - -- Deprecate ``renderer_global_factory`` arg to Configurator and related. - -- Maybe add ``add_renderer_globals`` method to Configurator. +- Github issues fixes. Should-Have ----------- +- Investigate mod_wsgi tutorial to make sure it still works (2 reports say + no; application package not found). + - Add narrative docs for wsgiapp and wsgiapp2. Nice-to-Have ------------ +- Maybe add ``add_renderer_globals`` method to Configurator. + - Speed up startup time (defer _bootstrap and registerCommonDirectives() until needed by ZCML, as well as unfound speedups). -- cgit v1.2.3 From 05a1b4a37df7e855747f66174a472b0de3ce1c9e Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 1 Jul 2011 02:08:03 -0400 Subject: - The Wiki2 tutorial "Tests" chapter had two bugs: it did not tell the user to depend on WebTest, and 2 tests failed as the result of changes to Pyramid itself. These issues have been fixed. --- CHANGES.txt | 7 ++ docs/tutorials/wiki2/src/tests/CHANGES.txt | 4 ++ docs/tutorials/wiki2/src/tests/MANIFEST.in | 2 + docs/tutorials/wiki2/src/tests/README.txt | 4 ++ docs/tutorials/wiki2/src/tests/development.ini | 58 ++++++++++++++++ docs/tutorials/wiki2/src/tests/production.ini | 77 +++++++++++++++++++++ docs/tutorials/wiki2/src/tests/setup.cfg | 27 ++++++++ docs/tutorials/wiki2/src/tests/setup.py | 49 +++++++++++++ .../tutorials/wiki2/src/tests/tutorial/__init__.py | 45 ++++++++++++ docs/tutorials/wiki2/src/tests/tutorial/login.py | 38 ++++++++++ docs/tutorials/wiki2/src/tests/tutorial/models.py | 50 +++++++++++++ .../tutorials/wiki2/src/tests/tutorial/security.py | 8 +++ .../wiki2/src/tests/tutorial/static/favicon.ico | Bin 0 -> 1406 bytes .../wiki2/src/tests/tutorial/static/footerbg.png | Bin 0 -> 333 bytes .../wiki2/src/tests/tutorial/static/headerbg.png | Bin 0 -> 203 bytes .../wiki2/src/tests/tutorial/static/ie6.css | 8 +++ .../wiki2/src/tests/tutorial/static/middlebg.png | Bin 0 -> 2797 bytes .../wiki2/src/tests/tutorial/static/pylons.css | 65 +++++++++++++++++ .../src/tests/tutorial/static/pyramid-small.png | Bin 0 -> 7044 bytes .../wiki2/src/tests/tutorial/static/pyramid.png | Bin 0 -> 33055 bytes .../src/tests/tutorial/static/transparent.gif | Bin 0 -> 49 bytes .../wiki2/src/tests/tutorial/templates/edit.pt | 62 +++++++++++++++++ .../wiki2/src/tests/tutorial/templates/login.pt | 58 ++++++++++++++++ .../src/tests/tutorial/templates/mytemplate.pt | 75 ++++++++++++++++++++ .../wiki2/src/tests/tutorial/templates/view.pt | 65 +++++++++++++++++ docs/tutorials/wiki2/src/tests/tutorial/tests.py | 6 +- docs/tutorials/wiki2/src/tests/tutorial/views.py | 72 +++++++++++++++++++ docs/tutorials/wiki2/tests.rst | 13 +++- 28 files changed, 788 insertions(+), 5 deletions(-) create mode 100644 docs/tutorials/wiki2/src/tests/CHANGES.txt create mode 100644 docs/tutorials/wiki2/src/tests/MANIFEST.in create mode 100644 docs/tutorials/wiki2/src/tests/README.txt create mode 100644 docs/tutorials/wiki2/src/tests/development.ini create mode 100644 docs/tutorials/wiki2/src/tests/production.ini create mode 100644 docs/tutorials/wiki2/src/tests/setup.cfg create mode 100644 docs/tutorials/wiki2/src/tests/setup.py create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/__init__.py create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/login.py create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/models.py create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/security.py create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/static/favicon.ico create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/static/footerbg.png create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/static/headerbg.png create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/static/ie6.css create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/static/middlebg.png create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/static/pylons.css create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/static/pyramid-small.png create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/static/pyramid.png create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/static/transparent.gif create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/templates/edit.pt create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/templates/login.pt create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/templates/mytemplate.pt create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/templates/view.pt create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/views.py diff --git a/CHANGES.txt b/CHANGES.txt index d2b97ece6..5279f1c9c 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -22,6 +22,13 @@ Deprecations - Deprecated the ``set_renderer_globals_factory`` method of the Configurator and the ``renderer_globals`` Configurator constructor parameter. +Documentation +------------- + +- The Wiki2 tutorial "Tests" chapter had two bugs: it did not tell the user + to depend on WebTest, and 2 tests failed as the result of changes to + Pyramid itself. These issues have been fixed. + 1.1a3 (2011-06-26) ================== diff --git a/docs/tutorials/wiki2/src/tests/CHANGES.txt b/docs/tutorials/wiki2/src/tests/CHANGES.txt new file mode 100644 index 000000000..35a34f332 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/CHANGES.txt @@ -0,0 +1,4 @@ +0.0 +--- + +- Initial version diff --git a/docs/tutorials/wiki2/src/tests/MANIFEST.in b/docs/tutorials/wiki2/src/tests/MANIFEST.in new file mode 100644 index 000000000..81beba1b1 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/MANIFEST.in @@ -0,0 +1,2 @@ +include *.txt *.ini *.cfg *.rst +recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml diff --git a/docs/tutorials/wiki2/src/tests/README.txt b/docs/tutorials/wiki2/src/tests/README.txt new file mode 100644 index 000000000..d41f7f90f --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/README.txt @@ -0,0 +1,4 @@ +tutorial README + + + diff --git a/docs/tutorials/wiki2/src/tests/development.ini b/docs/tutorials/wiki2/src/tests/development.ini new file mode 100644 index 000000000..3b615f635 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/development.ini @@ -0,0 +1,58 @@ +[app:tutorial] +use = egg:tutorial +reload_templates = true +debug_authorization = false +debug_notfound = false +debug_routematch = false +debug_templates = true +default_locale_name = en +sqlalchemy.url = sqlite:///%(here)s/tutorial.db + +[pipeline:main] +pipeline = + egg:WebError#evalerror + tm + tutorial + +[filter:tm] +use = egg:repoze.tm2#tm +commit_veto = repoze.tm:default_commit_veto + +[server:main] +use = egg:Paste#http +host = 0.0.0.0 +port = 6543 + +# Begin logging configuration + +[loggers] +keys = root, sqlalchemy + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[logger_sqlalchemy] +level = INFO +handlers = +qualname = sqlalchemy.engine +# "level = INFO" logs SQL queries. +# "level = DEBUG" logs SQL queries and results. +# "level = WARN" logs neither. (Recommended for production systems.) + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +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/production.ini b/docs/tutorials/wiki2/src/tests/production.ini new file mode 100644 index 000000000..0fdc38811 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/production.ini @@ -0,0 +1,77 @@ +[app:tutorial] +use = egg:tutorial +reload_templates = false +debug_authorization = false +debug_notfound = false +debug_routematch = false +debug_templates = false +default_locale_name = en +sqlalchemy.url = sqlite:///%(here)s/tutorial.db + +[filter:weberror] +use = egg:WebError#error_catcher +debug = false +;error_log = +;show_exceptions_in_wsgi_errors = true +;smtp_server = localhost +;error_email = janitor@example.com +;smtp_username = janitor +;smtp_password = "janitor's password" +;from_address = paste@localhost +;error_subject_prefix = "Pyramid Error" +;smtp_use_tls = +;error_message = + +[filter:tm] +use = egg:repoze.tm2#tm +commit_veto = repoze.tm:default_commit_veto + +[pipeline:main] +pipeline = + weberror + tm + tutorial + +[server:main] +use = egg:Paste#http +host = 0.0.0.0 +port = 6543 + +# Begin logging configuration + +[loggers] +keys = root, tutorial, sqlalchemy + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console + +[logger_tutorial] +level = WARN +handlers = +qualname = tutorial + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine +# "level = INFO" logs SQL queries. +# "level = DEBUG" logs SQL queries and results. +# "level = WARN" logs neither. (Recommended for production systems.) + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +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.cfg b/docs/tutorials/wiki2/src/tests/setup.cfg new file mode 100644 index 000000000..23b2ad983 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/setup.cfg @@ -0,0 +1,27 @@ +[nosetests] +match=^test +nocapture=1 +cover-package=tutorial +with-coverage=1 +cover-erase=1 + +[compile_catalog] +directory = tutorial/locale +domain = tutorial +statistics = true + +[extract_messages] +add_comments = TRANSLATORS: +output_file = tutorial/locale/tutorial.pot +width = 80 + +[init_catalog] +domain = tutorial +input_file = tutorial/locale/tutorial.pot +output_dir = tutorial/locale + +[update_catalog] +domain = tutorial +input_file = tutorial/locale/tutorial.pot +output_dir = tutorial/locale +previous = true diff --git a/docs/tutorials/wiki2/src/tests/setup.py b/docs/tutorials/wiki2/src/tests/setup.py new file mode 100644 index 000000000..7828f185d --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/setup.py @@ -0,0 +1,49 @@ +import os +import sys + +from setuptools import setup, find_packages + +here = os.path.abspath(os.path.dirname(__file__)) +README = open(os.path.join(here, 'README.txt')).read() +CHANGES = open(os.path.join(here, 'CHANGES.txt')).read() + +requires = [ + 'pyramid', + 'SQLAlchemy', + 'transaction', + 'repoze.tm2>=1.0b1', + 'zope.sqlalchemy', + 'WebError', + 'docutils', + 'WebTest', # add this + ] + +if sys.version_info[:3] < (2,5,0): + requires.append('pysqlite') + +setup(name='tutorial', + version='0.0', + description='tutorial', + long_description=README + '\n\n' + CHANGES, + classifiers=[ + "Programming Language :: Python", + "Framework :: Pylons", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", + ], + author='', + author_email='', + url='', + keywords='web wsgi bfg pylons pyramid', + packages=find_packages(), + include_package_data=True, + zip_safe=False, + test_suite='tutorial', + install_requires = requires, + entry_points = """\ + [paste.app_factory] + main = tutorial:main + """, + paster_plugins=['pyramid'], + ) + diff --git a/docs/tutorials/wiki2/src/tests/tutorial/__init__.py b/docs/tutorials/wiki2/src/tests/tutorial/__init__.py new file mode 100644 index 000000000..4cd84eda5 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/__init__.py @@ -0,0 +1,45 @@ +from pyramid.config import Configurator +from pyramid.authentication import AuthTktAuthenticationPolicy +from pyramid.authorization import ACLAuthorizationPolicy + +from sqlalchemy import engine_from_config + +from tutorial.models import initialize_sql +from tutorial.security import groupfinder + +def main(global_config, **settings): + """ This function returns a WSGI application. + """ + engine = engine_from_config(settings, 'sqlalchemy.') + initialize_sql(engine) + authn_policy = AuthTktAuthenticationPolicy( + 'sosecret', callback=groupfinder) + authz_policy = ACLAuthorizationPolicy() + config = Configurator(settings=settings, + root_factory='tutorial.models.RootFactory', + authentication_policy=authn_policy, + authorization_policy=authz_policy) + config.add_static_view('static', 'tutorial:static') + + config.add_route('view_wiki', '/') + config.add_route('login', '/login') + config.add_route('logout', '/logout') + config.add_route('view_page', '/{pagename}') + config.add_route('add_page', '/add_page/{pagename}') + config.add_route('edit_page', '/{pagename}/edit_page') + + config.add_view('tutorial.views.view_wiki', route_name='view_wiki') + config.add_view('tutorial.login.login', route_name='login', + renderer='tutorial:templates/login.pt') + config.add_view('tutorial.login.logout', route_name='logout') + config.add_view('tutorial.views.view_page', route_name='view_page', + renderer='tutorial:templates/view.pt') + config.add_view('tutorial.views.add_page', route_name='add_page', + renderer='tutorial:templates/edit.pt', permission='edit') + config.add_view('tutorial.views.edit_page', route_name='edit_page', + renderer='tutorial:templates/edit.pt', permission='edit') + config.add_view('tutorial.login.login', + context='pyramid.httpexceptions.HTTPForbidden', + renderer='tutorial:templates/login.pt') + return config.make_wsgi_app() + diff --git a/docs/tutorials/wiki2/src/tests/tutorial/login.py b/docs/tutorials/wiki2/src/tests/tutorial/login.py new file mode 100644 index 000000000..7a1d1f663 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/login.py @@ -0,0 +1,38 @@ +from pyramid.httpexceptions import HTTPFound +from pyramid.security import remember +from pyramid.security import forget +from pyramid.url import route_url + +from tutorial.security import USERS + +def login(request): + login_url = route_url('login', request) + referrer = request.url + if referrer == login_url: + referrer = '/' # never use the login form itself as came_from + came_from = request.params.get('came_from', referrer) + message = '' + login = '' + password = '' + if 'form.submitted' in request.params: + login = request.params['login'] + password = request.params['password'] + if USERS.get(login) == password: + headers = remember(request, login) + return HTTPFound(location = came_from, + headers = headers) + message = 'Failed login' + + return dict( + message = message, + url = request.application_url + '/login', + came_from = came_from, + login = login, + password = password, + ) + +def logout(request): + headers = forget(request) + return HTTPFound(location = route_url('view_wiki', request), + headers = headers) + diff --git a/docs/tutorials/wiki2/src/tests/tutorial/models.py b/docs/tutorials/wiki2/src/tests/tutorial/models.py new file mode 100644 index 000000000..53c6d1122 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/models.py @@ -0,0 +1,50 @@ +import transaction + +from pyramid.security import Allow +from pyramid.security import Everyone + +from sqlalchemy import Column +from sqlalchemy import Integer +from sqlalchemy import Text + +from sqlalchemy.exc import IntegrityError +from sqlalchemy.ext.declarative import declarative_base + +from sqlalchemy.orm import scoped_session +from sqlalchemy.orm import sessionmaker + +from zope.sqlalchemy import ZopeTransactionExtension + +DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension())) +Base = declarative_base() + +class Page(Base): + """ The SQLAlchemy declarative model class for a Page object. """ + __tablename__ = 'pages' + id = Column(Integer, primary_key=True) + name = Column(Text, unique=True) + data = Column(Text) + + def __init__(self, name, data): + self.name = name + self.data = data + +def initialize_sql(engine): + DBSession.configure(bind=engine) + Base.metadata.bind = engine + Base.metadata.create_all(engine) + try: + transaction.begin() + session = DBSession() + page = Page('FrontPage', 'This is the front page') + session.add(page) + transaction.commit() + except IntegrityError: + # already created + pass + +class RootFactory(object): + __acl__ = [ (Allow, Everyone, 'view'), + (Allow, 'group:editors', 'edit') ] + def __init__(self, request): + pass diff --git a/docs/tutorials/wiki2/src/tests/tutorial/security.py b/docs/tutorials/wiki2/src/tests/tutorial/security.py new file mode 100644 index 000000000..cfd13071e --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/security.py @@ -0,0 +1,8 @@ +USERS = {'editor':'editor', + 'viewer':'viewer'} +GROUPS = {'editor':['group:editors']} + +def groupfinder(userid, request): + if userid in USERS: + return GROUPS.get(userid, []) + diff --git a/docs/tutorials/wiki2/src/tests/tutorial/static/favicon.ico b/docs/tutorials/wiki2/src/tests/tutorial/static/favicon.ico new file mode 100644 index 000000000..71f837c9e Binary files /dev/null and b/docs/tutorials/wiki2/src/tests/tutorial/static/favicon.ico differ diff --git a/docs/tutorials/wiki2/src/tests/tutorial/static/footerbg.png b/docs/tutorials/wiki2/src/tests/tutorial/static/footerbg.png new file mode 100644 index 000000000..1fbc873da Binary files /dev/null and b/docs/tutorials/wiki2/src/tests/tutorial/static/footerbg.png differ diff --git a/docs/tutorials/wiki2/src/tests/tutorial/static/headerbg.png b/docs/tutorials/wiki2/src/tests/tutorial/static/headerbg.png new file mode 100644 index 000000000..0596f2020 Binary files /dev/null and b/docs/tutorials/wiki2/src/tests/tutorial/static/headerbg.png differ diff --git a/docs/tutorials/wiki2/src/tests/tutorial/static/ie6.css b/docs/tutorials/wiki2/src/tests/tutorial/static/ie6.css new file mode 100644 index 000000000..b7c8493d8 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/static/ie6.css @@ -0,0 +1,8 @@ +* html img, +* html .png{position:relative;behavior:expression((this.runtimeStyle.behavior="none")&&(this.pngSet?this.pngSet=true:(this.nodeName == "IMG" && this.src.toLowerCase().indexOf('.png')>-1?(this.runtimeStyle.backgroundImage = "none", +this.runtimeStyle.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + this.src + "',sizingMethod='image')", +this.src = "static/transparent.gif"):(this.origBg = this.origBg? this.origBg :this.currentStyle.backgroundImage.toString().replace('url("','').replace('")',''), +this.runtimeStyle.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + this.origBg + "',sizingMethod='crop')", +this.runtimeStyle.backgroundImage = "none")),this.pngSet=true) +);} +#wrap{display:table;height:100%} diff --git a/docs/tutorials/wiki2/src/tests/tutorial/static/middlebg.png b/docs/tutorials/wiki2/src/tests/tutorial/static/middlebg.png new file mode 100644 index 000000000..2369cfb7d Binary files /dev/null and b/docs/tutorials/wiki2/src/tests/tutorial/static/middlebg.png differ diff --git a/docs/tutorials/wiki2/src/tests/tutorial/static/pylons.css b/docs/tutorials/wiki2/src/tests/tutorial/static/pylons.css new file mode 100644 index 000000000..fd1914d8d --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/static/pylons.css @@ -0,0 +1,65 @@ +html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,font,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td{margin:0;padding:0;border:0;outline:0;font-size:100%;/* 16px */ +vertical-align:baseline;background:transparent;} +body{line-height:1;} +ol,ul{list-style:none;} +blockquote,q{quotes:none;} +blockquote:before,blockquote:after,q:before,q:after{content:'';content:none;} +:focus{outline:0;} +ins{text-decoration:none;} +del{text-decoration:line-through;} +table{border-collapse:collapse;border-spacing:0;} +sub{vertical-align:sub;font-size:smaller;line-height:normal;} +sup{vertical-align:super;font-size:smaller;line-height:normal;} +ul,menu,dir{display:block;list-style-type:disc;margin:1em 0;padding-left:40px;} +ol{display:block;list-style-type:decimal-leading-zero;margin:1em 0;padding-left:40px;} +li{display:list-item;} +ul ul,ul ol,ul dir,ul menu,ul dl,ol ul,ol ol,ol dir,ol menu,ol dl,dir ul,dir ol,dir dir,dir menu,dir dl,menu ul,menu ol,menu dir,menu menu,menu dl,dl ul,dl ol,dl dir,dl menu,dl dl{margin-top:0;margin-bottom:0;} +ol ul,ul ul,menu ul,dir ul,ol menu,ul menu,menu menu,dir menu,ol dir,ul dir,menu dir,dir dir{list-style-type:circle;} +ol ol ul,ol ul ul,ol menu ul,ol dir ul,ol ol menu,ol ul menu,ol menu menu,ol dir menu,ol ol dir,ol ul dir,ol menu dir,ol dir dir,ul ol ul,ul ul ul,ul menu ul,ul dir ul,ul ol menu,ul ul menu,ul menu menu,ul dir menu,ul ol dir,ul ul dir,ul menu dir,ul dir dir,menu ol ul,menu ul ul,menu menu ul,menu dir ul,menu ol menu,menu ul menu,menu menu menu,menu dir menu,menu ol dir,menu ul dir,menu menu dir,menu dir dir,dir ol ul,dir ul ul,dir menu ul,dir dir ul,dir ol menu,dir ul menu,dir menu menu,dir dir menu,dir ol dir,dir ul dir,dir menu dir,dir dir dir{list-style-type:square;} +.hidden{display:none;} +p{line-height:1.5em;} +h1{font-size:1.75em;line-height:1.7em;font-family:helvetica,verdana;} +h2{font-size:1.5em;line-height:1.7em;font-family:helvetica,verdana;} +h3{font-size:1.25em;line-height:1.7em;font-family:helvetica,verdana;} +h4{font-size:1em;line-height:1.7em;font-family:helvetica,verdana;} +html,body{width:100%;height:100%;} +body{margin:0;padding:0;background-color:#ffffff;position:relative;font:16px/24px "Nobile","Lucida Grande",Lucida,Verdana,sans-serif;} +a{color:#1b61d6;text-decoration:none;} +a:hover{color:#e88f00;text-decoration:underline;} +body h1, +body h2, +body h3, +body h4, +body h5, +body h6{font-family:"Neuton","Lucida Grande",Lucida,Verdana,sans-serif;font-weight:normal;color:#373839;font-style:normal;} +#wrap{min-height:100%;} +#header,#footer{width:100%;color:#ffffff;height:40px;position:absolute;text-align:center;line-height:40px;overflow:hidden;font-size:12px;vertical-align:middle;} +#header{background:#000000;top:0;font-size:14px;} +#footer{bottom:0;background:#000000 url(footerbg.png) repeat-x 0 top;position:relative;margin-top:-40px;clear:both;} +.header,.footer{width:750px;margin-right:auto;margin-left:auto;} +.wrapper{width:100%} +#top,#top-small,#bottom{width:100%;} +#top{color:#000000;height:230px;background:#ffffff url(headerbg.png) repeat-x 0 top;position:relative;} +#top-small{color:#000000;height:60px;background:#ffffff url(headerbg.png) repeat-x 0 top;position:relative;} +#bottom{color:#222;background-color:#ffffff;} +.top,.top-small,.middle,.bottom{width:750px;margin-right:auto;margin-left:auto;} +.top{padding-top:40px;} +.top-small{padding-top:10px;} +#middle{width:100%;height:100px;background:url(middlebg.png) repeat-x;border-top:2px solid #ffffff;border-bottom:2px solid #b2b2b2;} +.app-welcome{margin-top:25px;} +.app-name{color:#000000;font-weight:bold;} +.bottom{padding-top:50px;} +#left{width:350px;float:left;padding-right:25px;} +#right{width:350px;float:right;padding-left:25px;} +.align-left{text-align:left;} +.align-right{text-align:right;} +.align-center{text-align:center;} +ul.links{margin:0;padding:0;} +ul.links li{list-style-type:none;font-size:14px;} +form{border-style:none;} +fieldset{border-style:none;} +input{color:#222;border:1px solid #ccc;font-family:sans-serif;font-size:12px;line-height:16px;} +input[type=text],input[type=password]{width:205px;} +input[type=submit]{background-color:#ddd;font-weight:bold;} +/*Opera Fix*/ +body:before{content:"";height:100%;float:left;width:0;margin-top:-32767px;} diff --git a/docs/tutorials/wiki2/src/tests/tutorial/static/pyramid-small.png b/docs/tutorials/wiki2/src/tests/tutorial/static/pyramid-small.png new file mode 100644 index 000000000..a5bc0ade7 Binary files /dev/null and b/docs/tutorials/wiki2/src/tests/tutorial/static/pyramid-small.png differ diff --git a/docs/tutorials/wiki2/src/tests/tutorial/static/pyramid.png b/docs/tutorials/wiki2/src/tests/tutorial/static/pyramid.png new file mode 100644 index 000000000..347e05549 Binary files /dev/null and b/docs/tutorials/wiki2/src/tests/tutorial/static/pyramid.png differ diff --git a/docs/tutorials/wiki2/src/tests/tutorial/static/transparent.gif b/docs/tutorials/wiki2/src/tests/tutorial/static/transparent.gif new file mode 100644 index 000000000..0341802e5 Binary files /dev/null and b/docs/tutorials/wiki2/src/tests/tutorial/static/transparent.gif differ diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/edit.pt b/docs/tutorials/wiki2/src/tests/tutorial/templates/edit.pt new file mode 100644 index 000000000..ca28b9fa5 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/templates/edit.pt @@ -0,0 +1,62 @@ + + + + ${page.name} - Pyramid tutorial wiki (based on + TurboGears 20-Minute Wiki) + + + + + + + + +
+
+
+
+ pyramid +
+
+
+
+
+
+
+ +
+
+ +
+ +
+
- - -
-
-
- Editing Page Name Goes - Here
- You can return to the - FrontPage.
+
+
- -
-
-
-
-
- +
+
+ +
+ +
+
- - -
-
-
- Editing Page Name - Goes Here
- You can return to the - FrontPage.
-
-
-
-
-
- +
+
+ +
+ +
+
- - -
-
-
- Editing Page Name - Goes Here
- You can return to the - FrontPage.
-
-
-
-
-
- diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.pt b/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.pt index 64e592ea9..331d52d2a 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.pt +++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.pt @@ -1,58 +1,74 @@ - - - - Login - Pyramid tutorial wiki (based on TurboGears - 20-Minute Wiki) - - - - - - - - -
-
-
-
- pyramid + + + + + + + + + + + Login - Pyramid tutorial wiki (based on + TurboGears 20-Minute Wiki) + + + + + + + + + + + + +
+
+
+
+ +
+
+
+

+ + Login + + +

+ + +
+ + +
+
+ + +
+
+ +
+ +
+
-
-
-
-
-
- Login
- +
+
- -
-
-
-
-
- -
-
- -
-
- - + + + + + + + diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.pt b/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.pt index 4e5772de0..02cb8e73b 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.pt +++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.pt @@ -33,6 +33,9 @@
+

+ Logout +

Page text goes here.
@@ -48,11 +51,6 @@

You can return to the FrontPage.

-

- - Logout - -

diff --git a/docs/tutorials/wiki2/tests.rst b/docs/tutorials/wiki2/tests.rst index 9aca0c5b7..c171a0e6e 100644 --- a/docs/tutorials/wiki2/tests.rst +++ b/docs/tutorials/wiki2/tests.rst @@ -6,8 +6,6 @@ We will now add tests for the models and the views and a few functional tests in the ``tests.py``. Tests ensure that an application works, and that it continues to work after changes are made in the future. - - Testing the Models ================== @@ -37,7 +35,7 @@ can, and so on. Viewing the results of all our edits to ``tests.py`` ==================================================== -Once we're done with the ``tests.py`` module, it will look a lot like: +Open the ``tutorial/tests.py`` module, and edit it as follows: .. literalinclude:: src/tests/tutorial/tests.py :linenos: -- cgit v1.2.3 From ca6ec2fb9f52e0bac9eed622da1cea50b5a45339 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Mon, 25 May 2015 11:20:51 -0700 Subject: grammar --- docs/tutorials/wiki2/tests.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/tutorials/wiki2/tests.rst b/docs/tutorials/wiki2/tests.rst index c171a0e6e..e96fb0bae 100644 --- a/docs/tutorials/wiki2/tests.rst +++ b/docs/tutorials/wiki2/tests.rst @@ -3,8 +3,8 @@ Adding Tests ============ We will now add tests for the models and the views and a few functional -tests in the ``tests.py``. Tests ensure that an application works, and -that it continues to work after changes are made in the future. +tests in ``tests.py``. Tests ensure that an application works, and +that it continues to work when changes are made in the future. Testing the Models ================== @@ -17,12 +17,11 @@ Testing the Views ================= We'll modify our ``tests.py`` file, adding tests for each view -function we added above. As a result, we'll *delete* the +function we added previously. As a result, we'll *delete* the ``ViewTests`` class that the ``alchemy`` scaffold provided, and add four other test classes: ``ViewWikiTests``, ``ViewPageTests``, ``AddPageTests``, and ``EditPageTests``. These test the -``view_wiki``, ``view_page``, ``add_page``, and ``edit_page`` views -respectively. +``view_wiki``, ``view_page``, ``add_page``, and ``edit_page`` views. Functional tests ================ @@ -35,7 +34,8 @@ can, and so on. Viewing the results of all our edits to ``tests.py`` ==================================================== -Open the ``tutorial/tests.py`` module, and edit it as follows: +Open the ``tutorial/tests.py`` module, and edit it such that it appears as +follows: .. literalinclude:: src/tests/tutorial/tests.py :linenos: -- cgit v1.2.3 From 2c34a669aea2a8c51e91037689f199e4e543a3c0 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Mon, 25 May 2015 11:37:20 -0700 Subject: mention ZODB; punctuation --- docs/tutorials/wiki/index.rst | 8 ++++---- docs/tutorials/wiki2/index.rst | 3 +-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/tutorials/wiki/index.rst b/docs/tutorials/wiki/index.rst index 981d135c7..89c026dac 100644 --- a/docs/tutorials/wiki/index.rst +++ b/docs/tutorials/wiki/index.rst @@ -3,10 +3,10 @@ ZODB + Traversal Wiki Tutorial ============================== -This tutorial introduces a :term:`traversal` -based :app:`Pyramid` -application to a developer familiar with Python. It will be most familiar to -developers with previous :term:`Zope` experience. When we're done with the -tutorial, the developer will have created a basic Wiki application with +This tutorial introduces a :term:`ZODB` and :term:`traversal`-based +:app:`Pyramid` application to a developer familiar with Python. It will be +most familiar to developers with previous :term:`Zope` experience. When the +is finished, the developer will have created a basic Wiki application with authentication. For cut and paste purposes, the source code for all stages of this diff --git a/docs/tutorials/wiki2/index.rst b/docs/tutorials/wiki2/index.rst index 0a614cb23..0a3873dcd 100644 --- a/docs/tutorials/wiki2/index.rst +++ b/docs/tutorials/wiki2/index.rst @@ -3,7 +3,7 @@ SQLAlchemy + URL Dispatch Wiki Tutorial ======================================= -This tutorial introduces a :term:`SQLAlchemy` and :term:`url dispatch` -based +This tutorial introduces a :term:`SQLAlchemy` and :term:`url dispatch`-based :app:`Pyramid` application to a developer familiar with Python. When the tutorial is finished, the developer will have created a basic Wiki application with authentication. @@ -26,4 +26,3 @@ which corresponds to the same location if you have Pyramid sources. tests distributing - -- cgit v1.2.3 From 3e6b601d9bc557b5b698d5fd4d6eb20b151a424f Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Mon, 25 May 2015 13:53:05 -0700 Subject: cherry pick from 1.5-branch --- docs/tutorials/wiki/design.rst | 37 +++-- docs/tutorials/wiki/installation.rst | 266 ++++++++++++++++++++++++---------- docs/tutorials/wiki2/background.rst | 8 +- docs/tutorials/wiki2/design.rst | 21 +-- docs/tutorials/wiki2/installation.rst | 83 +++++++---- 5 files changed, 279 insertions(+), 136 deletions(-) diff --git a/docs/tutorials/wiki/design.rst b/docs/tutorials/wiki/design.rst index 28380bd66..49c30d29a 100644 --- a/docs/tutorials/wiki/design.rst +++ b/docs/tutorials/wiki/design.rst @@ -2,22 +2,22 @@ Design ========== -Following is a quick overview of our wiki application, to help -us understand the changes that we will be doing next in our -default files generated by the ``zodb`` scaffold. +Following is a quick overview of the design of our wiki application, to help +us understand the changes that we will be making as we work through the +tutorial. Overall ------- -We choose to use ``reStructuredText`` markup in the wiki text. -Translation from reStructuredText to HTML is provided by the -widely used ``docutils`` Python module. We will add this module -in the dependency list on the project ``setup.py`` file. +We choose to use :term:`reStructuredText` markup in the wiki text. Translation +from reStructuredText to HTML is provided by the widely used ``docutils`` +Python module. We will add this module in the dependency list on the project +``setup.py`` file. Models ------ -The root resource, named *Wiki*, will be a mapping of wiki page +The root resource named ``Wiki`` will be a mapping of wiki page names to page resources. The page resources will be instances of a *Page* class and they store the text content. @@ -29,9 +29,9 @@ To add a page to the wiki, a new instance of the page resource is created and its name and reference are added to the Wiki mapping. -A page named *FrontPage* containing the text *This is the front -page*, will be created when the storage is initialized, and will -be used as the wiki home page. +A page named ``FrontPage`` containing the text *This is the front page*, will +be created when the storage is initialized, and will be used as the wiki home +page. Views ----- @@ -57,14 +57,13 @@ use to do this are below. corresponding passwords. - GROUPS, a dictionary mapping :term:`userids ` to a - list of groups to which they belong to. + list of groups to which they belong. -- ``groupfinder``, an *authorization callback* that looks up - USERS and GROUPS. It will be provided in a new - *security.py* file. +- ``groupfinder``, an *authorization callback* that looks up USERS and + GROUPS. It will be provided in a new ``security.py`` file. -- An :term:`ACL` is attached to the root :term:`resource`. Each - row below details an :term:`ACE`: +- An :term:`ACL` is attached to the root :term:`resource`. Each row below + details an :term:`ACE`: +----------+----------------+----------------+ | Action | Principal | Permission | @@ -125,7 +124,7 @@ listed in the following table: | | | | authenticate. | | | | | | | | | | | | | | - If authentication | | | -| | | | successful, | | | +| | | | succeeds, | | | | | | | redirect to the | | | | | | | page that we | | | | | | | came from. | | | @@ -145,6 +144,6 @@ listed in the following table: when there is no view name. .. [2] Pyramid will return a default 404 Not Found page if the page *PageName* does not exist yet. -.. [3] pyramid.exceptions.Forbidden is reached when a +.. [3] ``pyramid.exceptions.Forbidden`` is reached when a user tries to invoke a view that is not authorized by the authorization policy. diff --git a/docs/tutorials/wiki/installation.rst b/docs/tutorials/wiki/installation.rst index b51254b92..20df389c6 100644 --- a/docs/tutorials/wiki/installation.rst +++ b/docs/tutorials/wiki/installation.rst @@ -2,125 +2,218 @@ Installation ============ -Preparation -=========== +Before you begin +================ -Follow the steps in :ref:`installing_chapter`, but name the virtualenv -directory ``pyramidtut``. +This tutorial assumes that you have already followed the steps in +:ref:`installing_chapter`, except **do not create a virtualenv or install +Pyramid**. Thereby you will satisfy the following requirements. -Preparation, UNIX ------------------ +* Python interpreter is installed on your operating system +* :term:`setuptools` or :term:`distribute` is installed +* :term:`virtualenv` is installed +Create directory to contain the project +--------------------------------------- -#. Switch to the ``pyramidtut`` directory: +We need a workspace for our project files. - .. code-block:: text +On UNIX +^^^^^^^ - $ cd pyramidtut +.. code-block:: text + + $ mkdir ~/pyramidtut + +On Windows +^^^^^^^^^^ + +.. code-block:: text + + c:\> mkdir pyramidtut -#. Install tutorial dependencies: +Create and use a virtual Python environment +------------------------------------------- - .. code-block:: text +Next let's create a `virtualenv` workspace for our project. We will +use the `VENV` environment variable instead of the absolute path of the +virtual environment. - $ $VENV/bin/easy_install docutils pyramid_tm pyramid_zodbconn \ - pyramid_debugtoolbar nose coverage +On UNIX +^^^^^^^ -Preparation, Windows --------------------- +.. code-block:: text + + $ export VENV=~/pyramidtut + $ virtualenv $VENV + New python executable in /home/foo/env/bin/python + Installing setuptools.............done. + +On Windows +^^^^^^^^^^ +.. code-block:: text + + c:\> set VENV=c:\pyramidtut -#. Switch to the ``pyramidtut`` directory: +Versions of Python use different paths, so you will need to adjust the +path to the command for your Python version. - .. code-block:: text +Python 2.7: - c:\> cd pyramidtut +.. code-block:: text -#. Install tutorial dependencies: + c:\> c:\Python27\Scripts\virtualenv %VENV% - .. code-block:: text +Python 3.2: + +.. code-block:: text + + c:\> c:\Python32\Scripts\virtualenv %VENV% + +Install Pyramid and tutorial dependencies into the virtual Python environment +----------------------------------------------------------------------------- + +On UNIX +^^^^^^^ + +.. code-block:: text + + $ $VENV/bin/easy_install docutils pyramid_tm pyramid_zodbconn \ + pyramid_debugtoolbar nose coverage + +On Windows +^^^^^^^^^^ + +.. code-block:: text - c:\pyramidtut> %VENV%\Scripts\easy_install docutils pyramid_tm \ - pyramid_zodbconn pyramid_debugtoolbar nose coverage + c:\> %VENV%\Scripts\easy_install docutils pyramid_tm pyramid_zodbconn \ + pyramid_debugtoolbar nose coverage + +Change Directory to Your Virtual Python Environment +--------------------------------------------------- + +Change directory to the ``pyramidtut`` directory. + +On UNIX +^^^^^^^ + +.. code-block:: text + + $ cd pyramidtut + +On Windows +^^^^^^^^^^ + +.. code-block:: text + + c:\> cd pyramidtut .. _making_a_project: -Make a Project -============== +Making a project +================ + +Your next step is to create a project. For this tutorial, we will use +the :term:`scaffold` named ``zodb``, which generates an application +that uses :term:`ZODB` and :term:`traversal`. -Your next step is to create a project. For this tutorial, we will use the -:term:`scaffold` named ``zodb``, which generates an application -that uses :term:`ZODB` and :term:`traversal`. :app:`Pyramid` -supplies a variety of scaffolds to generate sample projects. +:app:`Pyramid` supplies a variety of scaffolds to generate sample +projects. We will use `pcreate`—a script that comes with Pyramid to +quickly and easily generate scaffolds, usually with a single command—to +create the scaffold for our project. -The below instructions assume your current working directory is the -"virtualenv" named "pyramidtut". +By passing `zodb` into the `pcreate` command, the script creates +the files needed to use ZODB. By passing in our application name +`tutorial`, the script inserts that application name into all the +required files. -On UNIX: +The below instructions assume your current working directory is "pyramidtut". + +On UNIX +------- .. code-block:: text - $ $VENV/bin/pcreate -s zodb tutorial + $ $VENV/bin/pcreate -s zodb tutorial -On Windows: +On Windows +---------- .. code-block:: text c:\pyramidtut> %VENV%\Scripts\pcreate -s zodb tutorial -.. note:: You don't have to call it `tutorial` -- the code uses - relative paths for imports and finding templates and static - resources. +.. note:: If you are using Windows, the ``zodb`` + scaffold may not deal gracefully with installation into a + location that contains spaces in the path. If you experience + startup problems, try putting both the virtualenv and the project + into directories that do not contain spaces in their paths. -.. note:: If you are using Windows, the ``zodb`` scaffold - doesn't currently deal gracefully with installation into a location - that contains spaces in the path. If you experience startup - problems, try putting both the virtualenv and the project into - directories that do not contain spaces in their paths. +.. _installing_project_in_dev_mode_zodb: -Install the Project in "Development Mode" -========================================= +Installing the project in development mode +========================================== In order to do development on the project easily, you must "register" the project as a development egg in your workspace using the -``setup.py develop`` command. In order to do so, cd to the "tutorial" +``setup.py develop`` command. In order to do so, cd to the `tutorial` directory you created in :ref:`making_a_project`, and run the -"setup.py develop" command using virtualenv Python interpreter. +``setup.py develop`` command using the virtualenv Python interpreter. -On UNIX: +On UNIX +------- .. code-block:: text - $ cd tutorial - $ $VENV/bin/python setup.py develop + $ cd tutorial + $ $VENV/bin/python setup.py develop -On Windows: +On Windows +---------- .. code-block:: text - C:\pyramidtut> cd tutorial - C:\pyramidtut\tutorial> %VENV%\Scripts\python setup.py develop + c:\pyramidtut> cd tutorial + c:\pyramidtut\tutorial> %VENV%\Scripts\python setup.py develop + +The console will show `setup.py` checking for packages and installing +missing packages. Success executing this command will show a line like +the following:: + + Finished processing dependencies for tutorial==0.0 .. _running_tests: -Run the Tests +Run the tests ============= After you've installed the project in development mode, you may run the tests for the project. -On UNIX: +On UNIX +------- .. code-block:: text - $ $VENV/bin/python setup.py test -q + $ $VENV/bin/python setup.py test -q -On Windows: +On Windows +---------- .. code-block:: text - c:\pyramidtut\tutorial> %VENV%\Scripts\python setup.py test -q + c:\pyramidtut\tutorial> %VENV%\Scripts\python setup.py test -q -Expose Test Coverage Information +For a successful test run, you should see output that ends like this:: + + . + ---------------------------------------------------------------------- + Ran 1 test in 0.094s + + OK + +Expose test coverage information ================================ You can run the ``nosetests`` command to see test coverage @@ -129,48 +222,73 @@ test`` does but provides additional "coverage" information, exposing which lines of your project are "covered" (or not covered) by the tests. -On UNIX: +On UNIX +------- .. code-block:: text - $ $VENV/bin/nosetests --cover-package=tutorial --cover-erase --with-coverage + $ $VENV/bin/nosetests --cover-package=tutorial --cover-erase --with-coverage -On Windows: +On Windows +---------- .. code-block:: text - c:\pyramidtut\tutorial> %VENV%\Scripts\nosetests --cover-package=tutorial ^ - --cover-erase --with-coverage + c:\pyramidtut\tutorial> %VENV%\Scripts\nosetests --cover-package=tutorial \ + --cover-erase --with-coverage + +If successful, you will see output something like this:: -Looks like the code in the ``zodb`` scaffold for ZODB projects is -missing some test coverage, particularly in the file named -``models.py``. + . + Name Stmts Miss Cover Missing + -------------------------------------------------- + tutorial.py 12 7 42% 7-8, 14-18 + tutorial/models.py 10 6 40% 9-14 + tutorial/views.py 4 0 100% + -------------------------------------------------- + TOTAL 26 13 50% + ---------------------------------------------------------------------- + Ran 1 test in 0.392s + + OK + +Looks like our package doesn't quite have 100% test coverage. .. _wiki-start-the-application: -Start the Application +Start the application ===================== Start the application. -On UNIX: +On UNIX +------- .. code-block:: text - $ $VENV/bin/pserve development.ini --reload + $ $VENV/bin/pserve development.ini --reload -On Windows: +On Windows +---------- .. code-block:: text - c:\pyramidtut\tutorial> %VENV%\Scripts\pserve development.ini --reload + c:\pyramidtut\tutorial> %VENV%\Scripts\pserve development.ini --reload .. note:: Your OS firewall, if any, may pop up a dialog asking for authorization to allow python to accept incoming network connections. -Visit the Application in a Browser +If successful, you will see something like this on your console:: + + Starting subprocess with file monitor + Starting server in PID 95736. + serving on http://0.0.0.0:6543 + +This means the server is ready to accept requests. + +Visit the application in a browser ================================== In a browser, visit `http://localhost:6543/ `_. You @@ -181,7 +299,7 @@ page. You can read more about the purpose of the icon at :ref:`debug_toolbar`. It allows you to get information about your application while you develop. -Decisions the ``zodb`` Scaffold Has Made For You +Decisions the ``zodb`` scaffold has made for you ================================================ Creating a project using the ``zodb`` scaffold makes the following @@ -189,11 +307,11 @@ assumptions: - you are willing to use :term:`ZODB` as persistent storage -- you are willing to use :term:`traversal` to map URLs to code. +- you are willing to use :term:`traversal` to map URLs to code .. note:: :app:`Pyramid` supports any persistent storage mechanism (e.g., a SQL - database or filesystem files). :app:`Pyramid` also supports an additional + database or filesystem files). It also supports an additional mechanism to map URLs to code (:term:`URL dispatch`). However, for the purposes of this tutorial, we'll only be using traversal and ZODB. diff --git a/docs/tutorials/wiki2/background.rst b/docs/tutorials/wiki2/background.rst index 1f9582903..b8afb8305 100644 --- a/docs/tutorials/wiki2/background.rst +++ b/docs/tutorials/wiki2/background.rst @@ -2,10 +2,12 @@ Background ========== -This tutorial presents a :app:`Pyramid` application that uses technologies -which will be familiar to someone with SQL database experience. It uses +This version of the :app:`Pyramid` wiki tutorial presents a +:app:`Pyramid` application that uses technologies which will be +familiar to someone with SQL database experience. It uses :term:`SQLAlchemy` as a persistence mechanism and :term:`url dispatch` to map -URLs to code. +URLs to code. It can also be followed by people without any prior +Python web framework experience. To code along with this tutorial, the developer will need a UNIX machine with development tools (Mac OS X with XCode, any Linux or BSD diff --git a/docs/tutorials/wiki2/design.rst b/docs/tutorials/wiki2/design.rst index fa291cdc0..e9f361e7d 100644 --- a/docs/tutorials/wiki2/design.rst +++ b/docs/tutorials/wiki2/design.rst @@ -9,7 +9,7 @@ tutorial. Overall ------- -We choose to use :term:`reStructuredText` markup in the wiki text. Translation +We choose to use :term:`reStructuredText` markup in the wiki text. Translation from reStructuredText to HTML is provided by the widely used ``docutils`` Python module. We will add this module in the dependency list on the project ``setup.py`` file. @@ -37,8 +37,8 @@ Views ----- There will be three views to handle the normal operations of adding, -editing and viewing wiki pages, plus one view for the wiki front page. -Two templates will be used, one for viewing, and one for both for adding +editing, and viewing wiki pages, plus one view for the wiki front page. +Two templates will be used, one for viewing, and one for both adding and editing wiki pages. The default templating systems in :app:`Pyramid` are @@ -53,13 +53,14 @@ Security We'll eventually be adding security to our application. The components we'll use to do this are below. -- USERS, a dictionary mapping users names (the user's :term:`userids - `) to their corresponding passwords. +- USERS, a dictionary mapping :term:`userids ` to their + corresponding passwords. -- GROUPS, a dictionary mapping user names to a list of groups they belong to. +- GROUPS, a dictionary mapping :term:`userids ` to a + list of groups to which they belong. - ``groupfinder``, an *authorization callback* that looks up USERS and - GROUPS. It will be provided in a new *security.py* file. + GROUPS. It will be provided in a new ``security.py`` file. - An :term:`ACL` is attached to the root :term:`resource`. Each row below details an :term:`ACE`: @@ -101,7 +102,7 @@ listed in the following table: | | with existing | | | | | | content. | | | | | | | | | | -| | If the form is | | | | +| | If the form was | | | | | | submitted, redirect | | | | | | to /PageName | | | | +----------------------+-----------------------+-------------+------------+------------+ @@ -111,7 +112,7 @@ listed in the following table: | | the edit form | | | | | | without content. | | | | | | | | | | -| | If the form is | | | | +| | If the form was | | | | | | submitted, | | | | | | redirect to | | | | | | /PageName | | | | @@ -119,7 +120,7 @@ listed in the following table: | /login | Display login form, | login | login.pt | | | | Forbidden [3]_ | | | | | | | | | | -| | If the form is | | | | +| | If the form was | | | | | | submitted, | | | | | | authenticate. | | | | | | | | | | diff --git a/docs/tutorials/wiki2/installation.rst b/docs/tutorials/wiki2/installation.rst index 9b0389feb..1385ab8c7 100644 --- a/docs/tutorials/wiki2/installation.rst +++ b/docs/tutorials/wiki2/installation.rst @@ -2,22 +2,41 @@ Installation ============ -Before You Begin +Before you begin ================ This tutorial assumes that you have already followed the steps in -:ref:`installing_chapter`, thereby satisfying the following -requirements. +:ref:`installing_chapter`, except **do not create a virtualenv or install +Pyramid**. Thereby you will satisfy the following requirements. * Python interpreter is installed on your operating system * :term:`setuptools` or :term:`distribute` is installed * :term:`virtualenv` is installed -Create and Use a Virtual Python Environment +Create directory to contain the project +--------------------------------------- + +We need a workspace for our project files. + +On UNIX +^^^^^^^ + +.. code-block:: text + + $ mkdir ~/pyramidtut + +On Windows +^^^^^^^^^^ + +.. code-block:: text + + c:\> mkdir pyramidtut + +Create and use a virtual Python environment ------------------------------------------- Next let's create a `virtualenv` workspace for our project. We will -use the `VENV` environment variable instead of absolute path of the +use the `VENV` environment variable instead of the absolute path of the virtual environment. On UNIX @@ -33,8 +52,6 @@ On UNIX On Windows ^^^^^^^^^^ -Set the `VENV` environment variable. - .. code-block:: text c:\> set VENV=c:\pyramidtut @@ -54,7 +71,7 @@ Python 3.2: c:\> c:\Python32\Scripts\virtualenv %VENV% -Install Pyramid Into the Virtual Python Environment +Install Pyramid into the virtual Python environment --------------------------------------------------- On UNIX @@ -69,9 +86,9 @@ On Windows .. code-block:: text - c:\env> %VENV%\Scripts\easy_install pyramid + c:\> %VENV%\Scripts\easy_install pyramid -Install SQLite3 and Its Development Packages +Install SQLite3 and its development packages -------------------------------------------- If you used a package manager to install your Python or if you compiled @@ -87,7 +104,7 @@ the Debian system and apt-get, the command would be the following: $ sudo apt-get install libsqlite3-dev -Change Directory to Your Virtual Python Environment +Change directory to your virtual Python environment --------------------------------------------------- Change directory to the ``pyramidtut`` directory. @@ -108,7 +125,7 @@ On Windows .. _sql_making_a_project: -Making a Project +Making a project ================ Your next step is to create a project. For this tutorial we will use @@ -117,7 +134,7 @@ that uses :term:`SQLAlchemy` and :term:`URL dispatch`. :app:`Pyramid` supplies a variety of scaffolds to generate sample projects. We will use `pcreate`—a script that comes with Pyramid to -quickly and easily generate scaffolds usually with a single command—to +quickly and easily generate scaffolds, usually with a single command—to create the scaffold for our project. By passing `alchemy` into the `pcreate` command, the script creates @@ -126,8 +143,7 @@ the files needed to use SQLAlchemy. By passing in our application name required files. For example, `pcreate` creates the ``initialize_tutorial_db`` in the ``pyramidtut/bin`` directory. -The below instructions assume your current working directory is the -"virtualenv" named "pyramidtut". +The below instructions assume your current working directory is "pyramidtut". On UNIX ------- @@ -141,7 +157,7 @@ On Windows .. code-block:: text - c:\pyramidtut> %VENV%\pcreate -s alchemy tutorial + c:\pyramidtut> %VENV%\Scripts\pcreate -s alchemy tutorial .. note:: If you are using Windows, the ``alchemy`` scaffold may not deal gracefully with installation into a @@ -151,7 +167,7 @@ On Windows .. _installing_project_in_dev_mode: -Installing the Project in Development Mode +Installing the project in development mode ========================================== In order to do development on the project easily, you must "register" @@ -184,8 +200,8 @@ the following:: .. _sql_running_tests: -Running the Tests -================= +Run the tests +============= After you've installed the project in development mode, you may run the tests for the project. @@ -212,8 +228,8 @@ For a successful test run, you should see output that ends like this:: OK -Exposing Test Coverage Information -================================== +Expose test coverage information +================================ You can run the ``nosetests`` command to see test coverage information. This runs the tests in the same way that ``setup.py @@ -274,10 +290,9 @@ If successful, you will see output something like this:: Looks like our package doesn't quite have 100% test coverage. - .. _initialize_db_wiki2: -Initializing the Database +Initializing the database ========================= We need to use the ``initialize_tutorial_db`` :term:`console @@ -333,8 +348,8 @@ directory. This will be a SQLite database with a single table defined in it .. _wiki2-start-the-application: -Starting the Application -======================== +Start the application +===================== Start the application. @@ -352,6 +367,11 @@ On Windows c:\pyramidtut\tutorial> %VENV%\Scripts\pserve development.ini --reload +.. note:: + + Your OS firewall, if any, may pop up a dialog asking for authorization + to allow python to accept incoming network connections. + If successful, you will see something like this on your console:: Starting subprocess with file monitor @@ -360,19 +380,22 @@ If successful, you will see something like this on your console:: This means the server is ready to accept requests. -At this point, when you visit ``http://localhost:6543/`` in your web browser, -you will see the generated application's default page. +Visit the application in a browser +================================== + +In a browser, visit `http://localhost:6543/ `_. You +will see the generated application's default page. One thing you'll notice is the "debug toolbar" icon on right hand side of the page. You can read more about the purpose of the icon at :ref:`debug_toolbar`. It allows you to get information about your application while you develop. -Decisions the ``alchemy`` Scaffold Has Made for You +Decisions the ``alchemy`` scaffold has made for you ================================================================= -Creating a project using the ``alchemy`` scaffold makes -the following assumptions: +Creating a project using the ``alchemy`` scaffold makes the following +assumptions: - you are willing to use :term:`SQLAlchemy` as a database access tool -- cgit v1.2.3 From 34a913dd693695ade475000bfe61eb6e828a1dc8 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Mon, 25 May 2015 14:55:00 -0700 Subject: grammar, caps, minor tweaks --- docs/tutorials/wiki/basiclayout.rst | 49 +++++++++++++++++++----------------- docs/tutorials/wiki2/basiclayout.rst | 9 ++++--- 2 files changed, 31 insertions(+), 27 deletions(-) diff --git a/docs/tutorials/wiki/basiclayout.rst b/docs/tutorials/wiki/basiclayout.rst index cdf52b73e..0484ebf17 100644 --- a/docs/tutorials/wiki/basiclayout.rst +++ b/docs/tutorials/wiki/basiclayout.rst @@ -2,25 +2,27 @@ Basic Layout ============ -The starter files generated by the ``zodb`` scaffold are basic, but +The starter files generated by the ``zodb`` scaffold are very basic, but they provide a good orientation for the high-level patterns common to most -:term:`traversal` -based :app:`Pyramid` (and :term:`ZODB` -based) projects. +:term:`traversal`-based (and :term:`ZODB`-based) :app:`Pyramid` projects. -Application Configuration with ``__init__.py`` ------------------------------------------------- +Application configuration with ``__init__.py`` +---------------------------------------------- A directory on disk can be turned into a Python :term:`package` by containing an ``__init__.py`` file. Even if empty, this marks a directory as a Python -package. Our application uses ``__init__.py`` both as a package marker and -to contain application configuration code. +package. We use ``__init__.py`` both as a marker, indicating the directory +in which it's contained is a package, and to contain application configuration +code. When you run the application using the ``pserve`` command using the -``development.ini`` generated config file, the application configuration -points at a Setuptools *entry point* described as ``egg:tutorial``. In our -application, because the application's ``setup.py`` file says so, this entry -point happens to be the ``main`` function within the file named -``__init__.py``: +``development.ini`` generated configuration file, the application +configuration points at a Setuptools *entry point* described as +``egg:tutorial``. In our application, because the application's ``setup.py`` +file says so, this entry point happens to be the ``main`` function within the +file named ``__init__.py``. Let's take a look at the code and describe what +it does: .. literalinclude:: src/basiclayout/tutorial/__init__.py :linenos: @@ -28,17 +30,19 @@ point happens to be the ``main`` function within the file named #. *Lines 1-3*. Perform some dependency imports. -#. *Lines 6-8*. Define a root factory for our Pyramid application. +#. *Lines 6-8*. Define a :term:`root factory` for our Pyramid application. -#. *Line 14*. We construct a :term:`Configurator` with a :term:`root - factory` and the settings keywords parsed by :term:`PasteDeploy`. The root +#. *Line 11*. ``__init__.py`` defines a function named ``main``. + +#. *Line 14*. We construct a :term:`Configurator` with a root + factory and the settings keywords parsed by :term:`PasteDeploy`. The root factory is named ``root_factory``. #. *Line 15*. Include support for the :term:`Chameleon` template rendering bindings, allowing us to use the ``.pt`` templates. -#. *Line 16*. Register a "static view" which answers requests whose URL path - start with ``/static`` using the +#. *Line 16*. Register a "static view", which answers requests whose URL + paths start with ``/static``, using the :meth:`pyramid.config.Configurator.add_static_view` method. This statement registers a view that will serve up static assets, such as CSS and image files, for us, in this case, at @@ -63,7 +67,7 @@ point happens to be the ``main`` function within the file named :meth:`pyramid.config.Configurator.make_wsgi_app` method to return a :term:`WSGI` application. -Resources and Models with ``models.py`` +Resources and models with ``models.py`` --------------------------------------- :app:`Pyramid` uses the word :term:`resource` to describe objects arranged @@ -93,13 +97,12 @@ Here is the source for ``models.py``: root* object. It is called on *every request* to the :app:`Pyramid` application. It also performs bootstrapping by *creating* an application root (inside the ZODB root object) if one - does not already exist. It is used by the "root_factory" we've defined + does not already exist. It is used by the ``root_factory`` we've defined in our ``__init__.py``. - We do so by first seeing if the database has the persistent - application root. If not, we make an instance, store it, and - commit the transaction. We then return the application root - object. + Bootstrapping is done by first seeing if the database has the persistent + application root. If not, we make an instance, store it, and commit the + transaction. We then return the application root object. Views With ``views.py`` ----------------------- @@ -171,6 +174,6 @@ opposed to the tutorial :term:`package` directory) looks like this: Note the existence of a ``[app:main]`` section which specifies our WSGI application. Our ZODB database settings are specified as the ``zodbconn.uri`` setting within this section. This value, and the other -values within this section are passed as ``**settings`` to the ``main`` +values within this section, are passed as ``**settings`` to the ``main`` function we defined in ``__init__.py`` when the server is started via ``pserve``. diff --git a/docs/tutorials/wiki2/basiclayout.rst b/docs/tutorials/wiki2/basiclayout.rst index 90157aa9e..623882516 100644 --- a/docs/tutorials/wiki2/basiclayout.rst +++ b/docs/tutorials/wiki2/basiclayout.rst @@ -7,13 +7,14 @@ they provide a good orientation for the high-level patterns common to most :term:`URL dispatch`-based :app:`Pyramid` projects. -Application Configuration with ``__init__.py`` +Application configuration with ``__init__.py`` ---------------------------------------------- A directory on disk can be turned into a Python :term:`package` by containing an ``__init__.py`` file. Even if empty, this marks a directory as a Python -package. We use ``__init__.py``, both as a marker indicating the directory -it's contained within is a package, and to contain configuration code. +package. We use ``__init__.py`` both as a marker, indicating the directory +in which it's contained is a package, and to contain application configuration +code. Open ``tutorial/tutorial/__init__.py``. It should already contain the following: @@ -136,7 +137,7 @@ Finally, ``main`` is finished configuring things, so it uses the :lines: 21 :language: py -View Declarations via ``views.py`` +View declarations via ``views.py`` ---------------------------------- The main function of a web framework is mapping each URL pattern to code (a -- cgit v1.2.3 From dd6232d43ca974e95742275312a06863c2bd0744 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Mon, 25 May 2015 15:52:53 -0700 Subject: wikis consistency, grammar, wrapping --- docs/tutorials/wiki/definingmodels.rst | 25 ++++++++--------- docs/tutorials/wiki2/definingmodels.rst | 50 ++++++++++++++++----------------- 2 files changed, 36 insertions(+), 39 deletions(-) diff --git a/docs/tutorials/wiki/definingmodels.rst b/docs/tutorials/wiki/definingmodels.rst index 49372179f..859e902ab 100644 --- a/docs/tutorials/wiki/definingmodels.rst +++ b/docs/tutorials/wiki/definingmodels.rst @@ -15,7 +15,7 @@ single instance of the "Wiki" class will serve as a container for "Page" objects, which will be instances of the "Page" class. -Delete the Database +Delete the database ------------------- In the next step, we're going to remove the ``MyModel`` Python model @@ -32,12 +32,19 @@ Edit ``models.py`` .. note:: - There is nothing automagically special about the filename ``models.py``. A + There is nothing special about the filename ``models.py``. A project may have many models throughout its codebase in arbitrarily named - files. Files implementing models often have ``model`` in their filenames, + files. Files implementing models often have ``model`` in their filenames or they may live in a Python subpackage of your application package named ``models``, but this is only by convention. +Open ``tutorial/tutorial/models.py`` file and edit it to look like the +following: + +.. literalinclude:: src/models/tutorial/models.py + :linenos: + :language: python + The first thing we want to do is remove the ``MyModel`` class from the generated ``models.py`` file. The ``MyModel`` class is only a sample and we're not going to use it. @@ -70,17 +77,7 @@ front page) into the Wiki within the ``appmaker``. This will provide :term:`traversal` a :term:`resource tree` to work against when it attempts to resolve URLs to resources. -Look at the Result of Our Edits to ``models.py`` ------------------------------------------------- - -The result of all of our edits to ``models.py`` will end up looking -something like this: - -.. literalinclude:: src/models/tutorial/models.py - :linenos: - :language: python - -View the Application in a Browser +View the application in a browser --------------------------------- We can't. At this point, our system is in a "non-runnable" state; we'll need diff --git a/docs/tutorials/wiki2/definingmodels.rst b/docs/tutorials/wiki2/definingmodels.rst index fee440fe3..0fabb5f53 100644 --- a/docs/tutorials/wiki2/definingmodels.rst +++ b/docs/tutorials/wiki2/definingmodels.rst @@ -7,18 +7,18 @@ be to define a :term:`domain model` constructor representing a wiki page. We'll do this inside our ``models.py`` file. -Making Edits to ``models.py`` ------------------------------ +Edit ``models.py`` +------------------ .. note:: There is nothing special about the filename ``models.py``. A - project may have many models throughout its codebase in arbitrarily-named + project may have many models throughout its codebase in arbitrarily named files. Files implementing models often have ``model`` in their filenames - (or they may live in a Python subpackage of your application package named - ``models``) , but this is only by convention. + or they may live in a Python subpackage of your application package named + ``models``, but this is only by convention. -Open ``tutorial/tutorial/models.py`` file and edit it to look like the +Open ``tutorial/tutorial/models.py`` file and edit it to look like the following: .. literalinclude:: src/models/tutorial/models.py @@ -45,29 +45,29 @@ this class inherits from an instance of As you can see, our ``Page`` class has a class level attribute ``__tablename__`` which equals the string ``'pages'``. This means that SQLAlchemy will store our wiki data in a SQL table named ``pages``. Our -``Page`` class will also have class-level attributes named ``id``, ``name`` and -``data`` (all instances of :class:`sqlalchemy.schema.Column`). -These will map to columns in the ``pages`` table. -The ``id`` attribute will be the primary key in the table. -The ``name`` attribute will be a text attribute, each value of -which needs to be unique within the column. The ``data`` attribute is a text -attribute that will hold the body of each page. +``Page`` class will also have class-level attributes named ``id``, ``name`` +and ``data`` (all instances of :class:`sqlalchemy.schema.Column`). These will +map to columns in the ``pages`` table. The ``id`` attribute will be the +primary key in the table. The ``name`` attribute will be a text attribute, +each value of which needs to be unique within the column. The ``data`` +attribute is a text attribute that will hold the body of each page. Changing ``scripts/initializedb.py`` ------------------------------------ We haven't looked at the details of this file yet, but within the ``scripts`` -directory of your ``tutorial`` package is a file named ``initializedb.py``. Code -in this file is executed whenever we run the ``initialize_tutorial_db`` command, -as we did in the installation step of this tutorial. +directory of your ``tutorial`` package is a file named ``initializedb.py``. +Code in this file is executed whenever we run the ``initialize_tutorial_db`` +command, as we did in the installation step of this tutorial. -Since we've changed our model, we need to make changes to our ``initializedb.py`` -script. In particular, we'll replace our import of ``MyModel`` with one of -``Page`` and we'll change the very end of the script to create a ``Page`` -rather than a ``MyModel`` and add it to our ``DBSession``. +Since we've changed our model, we need to make changes to our +``initializedb.py`` script. In particular, we'll replace our import of +``MyModel`` with one of ``Page`` and we'll change the very end of the script +to create a ``Page`` rather than a ``MyModel`` and add it to our +``DBSession``. -Open ``tutorial/tutorial/scripts/initializedb.py`` and edit it to look like the -following: +Open ``tutorial/tutorial/scripts/initializedb.py`` and edit it to look like +the following: .. literalinclude:: src/models/tutorial/scripts/initializedb.py :linenos: @@ -82,9 +82,9 @@ Installing the Project and re-initializing the Database ------------------------------------------------------- Because our model has changed, in order to reinitialize the database, we need -to rerun the ``initialize_tutorial_db`` command to pick up the changes you've made -to both the models.py file and to the initializedb.py file. -See :ref:`initialize_db_wiki2` for instructions. +to rerun the ``initialize_tutorial_db`` command to pick up the changes you've +made to both the models.py file and to the initializedb.py file. See +:ref:`initialize_db_wiki2` for instructions. Success will look something like this:: -- cgit v1.2.3 From 1204f0f285daf28e08dbfbae683a0aa8f27bd205 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Mon, 25 May 2015 16:01:14 -0700 Subject: update static assets, mytemplate.pt, caps --- .../src/basiclayout/tutorial/static/favicon.ico | Bin 1406 -> 0 bytes .../src/basiclayout/tutorial/static/footerbg.png | Bin 333 -> 0 bytes .../src/basiclayout/tutorial/static/headerbg.png | Bin 203 -> 0 bytes .../wiki/src/basiclayout/tutorial/static/ie6.css | 8 - .../src/basiclayout/tutorial/static/middlebg.png | Bin 2797 -> 0 bytes .../src/basiclayout/tutorial/static/pylons.css | 372 --------------------- .../basiclayout/tutorial/static/pyramid-16x16.png | Bin 0 -> 1319 bytes .../basiclayout/tutorial/static/pyramid-small.png | Bin 7044 -> 0 bytes .../src/basiclayout/tutorial/static/pyramid.png | Bin 33055 -> 12901 bytes .../wiki/src/basiclayout/tutorial/static/theme.css | 154 +++++++++ .../src/basiclayout/tutorial/static/theme.min.css | 1 + .../basiclayout/tutorial/static/transparent.gif | Bin 49 -> 0 bytes .../basiclayout/tutorial/templates/mytemplate.pt | 127 ++++--- docs/tutorials/wiki2/definingmodels.rst | 6 +- 14 files changed, 218 insertions(+), 450 deletions(-) delete mode 100644 docs/tutorials/wiki/src/basiclayout/tutorial/static/favicon.ico delete mode 100644 docs/tutorials/wiki/src/basiclayout/tutorial/static/footerbg.png delete mode 100644 docs/tutorials/wiki/src/basiclayout/tutorial/static/headerbg.png delete mode 100644 docs/tutorials/wiki/src/basiclayout/tutorial/static/ie6.css delete mode 100644 docs/tutorials/wiki/src/basiclayout/tutorial/static/middlebg.png delete mode 100644 docs/tutorials/wiki/src/basiclayout/tutorial/static/pylons.css create mode 100644 docs/tutorials/wiki/src/basiclayout/tutorial/static/pyramid-16x16.png delete mode 100644 docs/tutorials/wiki/src/basiclayout/tutorial/static/pyramid-small.png create mode 100644 docs/tutorials/wiki/src/basiclayout/tutorial/static/theme.css create mode 100644 docs/tutorials/wiki/src/basiclayout/tutorial/static/theme.min.css delete mode 100644 docs/tutorials/wiki/src/basiclayout/tutorial/static/transparent.gif diff --git a/docs/tutorials/wiki/src/basiclayout/tutorial/static/favicon.ico b/docs/tutorials/wiki/src/basiclayout/tutorial/static/favicon.ico deleted file mode 100644 index 71f837c9e..000000000 Binary files a/docs/tutorials/wiki/src/basiclayout/tutorial/static/favicon.ico and /dev/null differ diff --git a/docs/tutorials/wiki/src/basiclayout/tutorial/static/footerbg.png b/docs/tutorials/wiki/src/basiclayout/tutorial/static/footerbg.png deleted file mode 100644 index 1fbc873da..000000000 Binary files a/docs/tutorials/wiki/src/basiclayout/tutorial/static/footerbg.png and /dev/null differ diff --git a/docs/tutorials/wiki/src/basiclayout/tutorial/static/headerbg.png b/docs/tutorials/wiki/src/basiclayout/tutorial/static/headerbg.png deleted file mode 100644 index 0596f2020..000000000 Binary files a/docs/tutorials/wiki/src/basiclayout/tutorial/static/headerbg.png and /dev/null differ diff --git a/docs/tutorials/wiki/src/basiclayout/tutorial/static/ie6.css b/docs/tutorials/wiki/src/basiclayout/tutorial/static/ie6.css deleted file mode 100644 index b7c8493d8..000000000 --- a/docs/tutorials/wiki/src/basiclayout/tutorial/static/ie6.css +++ /dev/null @@ -1,8 +0,0 @@ -* html img, -* html .png{position:relative;behavior:expression((this.runtimeStyle.behavior="none")&&(this.pngSet?this.pngSet=true:(this.nodeName == "IMG" && this.src.toLowerCase().indexOf('.png')>-1?(this.runtimeStyle.backgroundImage = "none", -this.runtimeStyle.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + this.src + "',sizingMethod='image')", -this.src = "static/transparent.gif"):(this.origBg = this.origBg? this.origBg :this.currentStyle.backgroundImage.toString().replace('url("','').replace('")',''), -this.runtimeStyle.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + this.origBg + "',sizingMethod='crop')", -this.runtimeStyle.backgroundImage = "none")),this.pngSet=true) -);} -#wrap{display:table;height:100%} diff --git a/docs/tutorials/wiki/src/basiclayout/tutorial/static/middlebg.png b/docs/tutorials/wiki/src/basiclayout/tutorial/static/middlebg.png deleted file mode 100644 index 2369cfb7d..000000000 Binary files a/docs/tutorials/wiki/src/basiclayout/tutorial/static/middlebg.png and /dev/null differ diff --git a/docs/tutorials/wiki/src/basiclayout/tutorial/static/pylons.css b/docs/tutorials/wiki/src/basiclayout/tutorial/static/pylons.css deleted file mode 100644 index 4b1c017cd..000000000 --- a/docs/tutorials/wiki/src/basiclayout/tutorial/static/pylons.css +++ /dev/null @@ -1,372 +0,0 @@ -html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td -{ - margin: 0; - padding: 0; - border: 0; - outline: 0; - font-size: 100%; /* 16px */ - vertical-align: baseline; - background: transparent; -} - -body -{ - line-height: 1; -} - -ol, ul -{ - list-style: none; -} - -blockquote, q -{ - quotes: none; -} - -blockquote:before, blockquote:after, q:before, q:after -{ - content: ''; - content: none; -} - -:focus -{ - outline: 0; -} - -ins -{ - text-decoration: none; -} - -del -{ - text-decoration: line-through; -} - -table -{ - border-collapse: collapse; - border-spacing: 0; -} - -sub -{ - vertical-align: sub; - font-size: smaller; - line-height: normal; -} - -sup -{ - vertical-align: super; - font-size: smaller; - line-height: normal; -} - -ul, menu, dir -{ - display: block; - list-style-type: disc; - margin: 1em 0; - padding-left: 40px; -} - -ol -{ - display: block; - list-style-type: decimal-leading-zero; - margin: 1em 0; - padding-left: 40px; -} - -li -{ - display: list-item; -} - -ul ul, ul ol, ul dir, ul menu, ul dl, ol ul, ol ol, ol dir, ol menu, ol dl, dir ul, dir ol, dir dir, dir menu, dir dl, menu ul, menu ol, menu dir, menu menu, menu dl, dl ul, dl ol, dl dir, dl menu, dl dl -{ - margin-top: 0; - margin-bottom: 0; -} - -ol ul, ul ul, menu ul, dir ul, ol menu, ul menu, menu menu, dir menu, ol dir, ul dir, menu dir, dir dir -{ - list-style-type: circle; -} - -ol ol ul, ol ul ul, ol menu ul, ol dir ul, ol ol menu, ol ul menu, ol menu menu, ol dir menu, ol ol dir, ol ul dir, ol menu dir, ol dir dir, ul ol ul, ul ul ul, ul menu ul, ul dir ul, ul ol menu, ul ul menu, ul menu menu, ul dir menu, ul ol dir, ul ul dir, ul menu dir, ul dir dir, menu ol ul, menu ul ul, menu menu ul, menu dir ul, menu ol menu, menu ul menu, menu menu menu, menu dir menu, menu ol dir, menu ul dir, menu menu dir, menu dir dir, dir ol ul, dir ul ul, dir menu ul, dir dir ul, dir ol menu, dir ul menu, dir menu menu, dir dir menu, dir ol dir, dir ul dir, dir menu dir, dir dir dir -{ - list-style-type: square; -} - -.hidden -{ - display: none; -} - -p -{ - line-height: 1.5em; -} - -h1 -{ - font-size: 1.75em; - line-height: 1.7em; - font-family: helvetica, verdana; -} - -h2 -{ - font-size: 1.5em; - line-height: 1.7em; - font-family: helvetica, verdana; -} - -h3 -{ - font-size: 1.25em; - line-height: 1.7em; - font-family: helvetica, verdana; -} - -h4 -{ - font-size: 1em; - line-height: 1.7em; - font-family: helvetica, verdana; -} - -html, body -{ - width: 100%; - height: 100%; -} - -body -{ - margin: 0; - padding: 0; - background-color: #fff; - position: relative; - font: 16px/24px NobileRegular, "Lucida Grande", Lucida, Verdana, sans-serif; -} - -a -{ - color: #1b61d6; - text-decoration: none; -} - -a:hover -{ - color: #e88f00; - text-decoration: underline; -} - -body h1, body h2, body h3, body h4, body h5, body h6 -{ - font-family: NeutonRegular, "Lucida Grande", Lucida, Verdana, sans-serif; - font-weight: 400; - color: #373839; - font-style: normal; -} - -#wrap -{ - min-height: 100%; -} - -#header, #footer -{ - width: 100%; - color: #fff; - height: 40px; - position: absolute; - text-align: center; - line-height: 40px; - overflow: hidden; - font-size: 12px; - vertical-align: middle; -} - -#header -{ - background: #000; - top: 0; - font-size: 14px; -} - -#footer -{ - bottom: 0; - background: #000 url(footerbg.png) repeat-x 0 top; - position: relative; - margin-top: -40px; - clear: both; -} - -.header, .footer -{ - width: 750px; - margin-right: auto; - margin-left: auto; -} - -.wrapper -{ - width: 100%; -} - -#top, #top-small, #bottom -{ - width: 100%; -} - -#top -{ - color: #000; - height: 230px; - background: #fff url(headerbg.png) repeat-x 0 top; - position: relative; -} - -#top-small -{ - color: #000; - height: 60px; - background: #fff url(headerbg.png) repeat-x 0 top; - position: relative; -} - -#bottom -{ - color: #222; - background-color: #fff; -} - -.top, .top-small, .middle, .bottom -{ - width: 750px; - margin-right: auto; - margin-left: auto; -} - -.top -{ - padding-top: 40px; -} - -.top-small -{ - padding-top: 10px; -} - -#middle -{ - width: 100%; - height: 100px; - background: url(middlebg.png) repeat-x; - border-top: 2px solid #fff; - border-bottom: 2px solid #b2b2b2; -} - -.app-welcome -{ - margin-top: 25px; -} - -.app-name -{ - color: #000; - font-weight: 700; -} - -.bottom -{ - padding-top: 50px; -} - -#left -{ - width: 350px; - float: left; - padding-right: 25px; -} - -#right -{ - width: 350px; - float: right; - padding-left: 25px; -} - -.align-left -{ - text-align: left; -} - -.align-right -{ - text-align: right; -} - -.align-center -{ - text-align: center; -} - -ul.links -{ - margin: 0; - padding: 0; -} - -ul.links li -{ - list-style-type: none; - font-size: 14px; -} - -form -{ - border-style: none; -} - -fieldset -{ - border-style: none; -} - -input -{ - color: #222; - border: 1px solid #ccc; - font-family: sans-serif; - font-size: 12px; - line-height: 16px; -} - -input[type=text], input[type=password] -{ - width: 205px; -} - -input[type=submit] -{ - background-color: #ddd; - font-weight: 700; -} - -/*Opera Fix*/ -body:before -{ - content: ""; - height: 100%; - float: left; - width: 0; - margin-top: -32767px; -} diff --git a/docs/tutorials/wiki/src/basiclayout/tutorial/static/pyramid-16x16.png b/docs/tutorials/wiki/src/basiclayout/tutorial/static/pyramid-16x16.png new file mode 100644 index 000000000..979203112 Binary files /dev/null and b/docs/tutorials/wiki/src/basiclayout/tutorial/static/pyramid-16x16.png differ diff --git a/docs/tutorials/wiki/src/basiclayout/tutorial/static/pyramid-small.png b/docs/tutorials/wiki/src/basiclayout/tutorial/static/pyramid-small.png deleted file mode 100644 index a5bc0ade7..000000000 Binary files a/docs/tutorials/wiki/src/basiclayout/tutorial/static/pyramid-small.png and /dev/null differ diff --git a/docs/tutorials/wiki/src/basiclayout/tutorial/static/pyramid.png b/docs/tutorials/wiki/src/basiclayout/tutorial/static/pyramid.png index 347e05549..4ab837be9 100644 Binary files a/docs/tutorials/wiki/src/basiclayout/tutorial/static/pyramid.png and b/docs/tutorials/wiki/src/basiclayout/tutorial/static/pyramid.png differ diff --git a/docs/tutorials/wiki/src/basiclayout/tutorial/static/theme.css b/docs/tutorials/wiki/src/basiclayout/tutorial/static/theme.css new file mode 100644 index 000000000..0f4b1a4d4 --- /dev/null +++ b/docs/tutorials/wiki/src/basiclayout/tutorial/static/theme.css @@ -0,0 +1,154 @@ +@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700); +body { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; + color: #ffffff; + background: #bc2131; +} +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; +} +p { + font-weight: 300; +} +.font-normal { + font-weight: 400; +} +.font-semi-bold { + font-weight: 600; +} +.font-bold { + font-weight: 700; +} +.starter-template { + margin-top: 250px; +} +.starter-template .content { + margin-left: 10px; +} +.starter-template .content h1 { + margin-top: 10px; + font-size: 60px; +} +.starter-template .content h1 .smaller { + font-size: 40px; + color: #f2b7bd; +} +.starter-template .content .lead { + font-size: 25px; + color: #f2b7bd; +} +.starter-template .content .lead .font-normal { + color: #ffffff; +} +.starter-template .links { + float: right; + right: 0; + margin-top: 125px; +} +.starter-template .links ul { + display: block; + padding: 0; + margin: 0; +} +.starter-template .links ul li { + list-style: none; + display: inline; + margin: 0 10px; +} +.starter-template .links ul li:first-child { + margin-left: 0; +} +.starter-template .links ul li:last-child { + margin-right: 0; +} +.starter-template .links ul li.current-version { + color: #f2b7bd; + font-weight: 400; +} +.starter-template .links ul li a, a { + color: #f2b7bd; + text-decoration: underline; +} +.starter-template .links ul li a:hover, a:hover { + color: #ffffff; + text-decoration: underline; +} +.starter-template .links ul li .icon-muted { + color: #eb8b95; + margin-right: 5px; +} +.starter-template .links ul li:hover .icon-muted { + color: #ffffff; +} +.starter-template .copyright { + margin-top: 10px; + font-size: 0.9em; + color: #f2b7bd; + text-transform: lowercase; + float: right; + right: 0; +} +@media (max-width: 1199px) { + .starter-template .content h1 { + font-size: 45px; + } + .starter-template .content h1 .smaller { + font-size: 30px; + } + .starter-template .content .lead { + font-size: 20px; + } +} +@media (max-width: 991px) { + .starter-template { + margin-top: 0; + } + .starter-template .logo { + margin: 40px auto; + } + .starter-template .content { + margin-left: 0; + text-align: center; + } + .starter-template .content h1 { + margin-bottom: 20px; + } + .starter-template .links { + float: none; + text-align: center; + margin-top: 60px; + } + .starter-template .copyright { + float: none; + text-align: center; + } +} +@media (max-width: 767px) { + .starter-template .content h1 .smaller { + font-size: 25px; + display: block; + } + .starter-template .content .lead { + font-size: 16px; + } + .starter-template .links { + margin-top: 40px; + } + .starter-template .links ul li { + display: block; + margin: 0; + } + .starter-template .links ul li .icon-muted { + display: none; + } + .starter-template .copyright { + margin-top: 20px; + } +} diff --git a/docs/tutorials/wiki/src/basiclayout/tutorial/static/theme.min.css b/docs/tutorials/wiki/src/basiclayout/tutorial/static/theme.min.css new file mode 100644 index 000000000..2f924bcc5 --- /dev/null +++ b/docs/tutorials/wiki/src/basiclayout/tutorial/static/theme.min.css @@ -0,0 +1 @@ +@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);body{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300;color:#fff;background:#bc2131}h1,h2,h3,h4,h5,h6{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300}p{font-weight:300}.font-normal{font-weight:400}.font-semi-bold{font-weight:600}.font-bold{font-weight:700}.starter-template{margin-top:250px}.starter-template .content{margin-left:10px}.starter-template .content h1{margin-top:10px;font-size:60px}.starter-template .content h1 .smaller{font-size:40px;color:#f2b7bd}.starter-template .content .lead{font-size:25px;color:#f2b7bd}.starter-template .content .lead .font-normal{color:#fff}.starter-template .links{float:right;right:0;margin-top:125px}.starter-template .links ul{display:block;padding:0;margin:0}.starter-template .links ul li{list-style:none;display:inline;margin:0 10px}.starter-template .links ul li:first-child{margin-left:0}.starter-template .links ul li:last-child{margin-right:0}.starter-template .links ul li.current-version{color:#f2b7bd;font-weight:400}.starter-template .links ul li a{color:#fff}.starter-template .links ul li a:hover{text-decoration:underline}.starter-template .links ul li .icon-muted{color:#eb8b95;margin-right:5px}.starter-template .links ul li:hover .icon-muted{color:#fff}.starter-template .copyright{margin-top:10px;font-size:.9em;color:#f2b7bd;text-transform:lowercase;float:right;right:0}@media (max-width:1199px){.starter-template .content h1{font-size:45px}.starter-template .content h1 .smaller{font-size:30px}.starter-template .content .lead{font-size:20px}}@media (max-width:991px){.starter-template{margin-top:0}.starter-template .logo{margin:40px auto}.starter-template .content{margin-left:0;text-align:center}.starter-template .content h1{margin-bottom:20px}.starter-template .links{float:none;text-align:center;margin-top:60px}.starter-template .copyright{float:none;text-align:center}}@media (max-width:767px){.starter-template .content h1 .smaller{font-size:25px;display:block}.starter-template .content .lead{font-size:16px}.starter-template .links{margin-top:40px}.starter-template .links ul li{display:block;margin:0}.starter-template .links ul li .icon-muted{display:none}.starter-template .copyright{margin-top:20px}} \ No newline at end of file diff --git a/docs/tutorials/wiki/src/basiclayout/tutorial/static/transparent.gif b/docs/tutorials/wiki/src/basiclayout/tutorial/static/transparent.gif deleted file mode 100644 index 0341802e5..000000000 Binary files a/docs/tutorials/wiki/src/basiclayout/tutorial/static/transparent.gif and /dev/null differ diff --git a/docs/tutorials/wiki/src/basiclayout/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki/src/basiclayout/tutorial/templates/mytemplate.pt index 13b41f823..29dacef19 100644 --- a/docs/tutorials/wiki/src/basiclayout/tutorial/templates/mytemplate.pt +++ b/docs/tutorials/wiki/src/basiclayout/tutorial/templates/mytemplate.pt @@ -1,73 +1,66 @@ - - - - The Pyramid Web Framework - - - - - - - - - - -
-
-
-
pyramid
-
-
-
-
-

- Welcome to ${project}, an application generated by
- the Pyramid Web Framework. -

-
-
-
-
-
-

Search documentation

-
- - -
+ + + + + + + + + + + Alchemy Scaffold for The Pyramid Web Framework + + + + + + + + + + + + + +
+
+
+
+ +
+
+
+

Pyramid ZODB scaffold

+

Welcome to ${project}, an application generated by
the Pyramid Web Framework.

+
+
-
-
- + + + + + + + diff --git a/docs/tutorials/wiki2/definingmodels.rst b/docs/tutorials/wiki2/definingmodels.rst index 0fabb5f53..b2d9bf83a 100644 --- a/docs/tutorials/wiki2/definingmodels.rst +++ b/docs/tutorials/wiki2/definingmodels.rst @@ -78,7 +78,7 @@ Only the highlighted lines need to be changed, as well as removing the lines referencing ``pyramid.scripts.common`` and ``options`` under the ``main`` function. -Installing the Project and re-initializing the Database +Installing the project and re-initializing the database ------------------------------------------------------- Because our model has changed, in order to reinitialize the database, we need @@ -111,8 +111,8 @@ Success will look something like this:: 2015-05-24 15:34:14,549 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ('FrontPage', 'This is the front page') 2015-05-24 15:34:14,550 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT -Viewing the Application in a Browser ------------------------------------- +View the application in a browser +--------------------------------- We can't. At this point, our system is in a "non-runnable" state; we'll need to change view-related files in the next chapter to be able to start the -- cgit v1.2.3 From 8f0758a0a4ea5b9b4da25fe7ba8ca814ea7de66b Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Mon, 25 May 2015 16:06:13 -0700 Subject: update template and static assets to current --- .../wiki/src/models/tutorial/static/favicon.ico | Bin 1406 -> 0 bytes .../wiki/src/models/tutorial/static/footerbg.png | Bin 333 -> 0 bytes .../wiki/src/models/tutorial/static/headerbg.png | Bin 203 -> 0 bytes .../wiki/src/models/tutorial/static/ie6.css | 8 - .../wiki/src/models/tutorial/static/middlebg.png | Bin 2797 -> 0 bytes .../wiki/src/models/tutorial/static/pylons.css | 372 --------------------- .../src/models/tutorial/static/pyramid-16x16.png | Bin 0 -> 1319 bytes .../src/models/tutorial/static/pyramid-small.png | Bin 7044 -> 0 bytes .../wiki/src/models/tutorial/static/pyramid.png | Bin 33055 -> 12901 bytes .../wiki/src/models/tutorial/static/theme.css | 154 +++++++++ .../wiki/src/models/tutorial/static/theme.min.css | 1 + .../src/models/tutorial/static/transparent.gif | Bin 49 -> 0 bytes .../src/models/tutorial/templates/mytemplate.pt | 127 ++++--- 13 files changed, 215 insertions(+), 447 deletions(-) delete mode 100644 docs/tutorials/wiki/src/models/tutorial/static/favicon.ico delete mode 100644 docs/tutorials/wiki/src/models/tutorial/static/footerbg.png delete mode 100644 docs/tutorials/wiki/src/models/tutorial/static/headerbg.png delete mode 100644 docs/tutorials/wiki/src/models/tutorial/static/ie6.css delete mode 100644 docs/tutorials/wiki/src/models/tutorial/static/middlebg.png delete mode 100644 docs/tutorials/wiki/src/models/tutorial/static/pylons.css create mode 100644 docs/tutorials/wiki/src/models/tutorial/static/pyramid-16x16.png delete mode 100644 docs/tutorials/wiki/src/models/tutorial/static/pyramid-small.png create mode 100644 docs/tutorials/wiki/src/models/tutorial/static/theme.css create mode 100644 docs/tutorials/wiki/src/models/tutorial/static/theme.min.css delete mode 100644 docs/tutorials/wiki/src/models/tutorial/static/transparent.gif diff --git a/docs/tutorials/wiki/src/models/tutorial/static/favicon.ico b/docs/tutorials/wiki/src/models/tutorial/static/favicon.ico deleted file mode 100644 index 71f837c9e..000000000 Binary files a/docs/tutorials/wiki/src/models/tutorial/static/favicon.ico and /dev/null differ diff --git a/docs/tutorials/wiki/src/models/tutorial/static/footerbg.png b/docs/tutorials/wiki/src/models/tutorial/static/footerbg.png deleted file mode 100644 index 1fbc873da..000000000 Binary files a/docs/tutorials/wiki/src/models/tutorial/static/footerbg.png and /dev/null differ diff --git a/docs/tutorials/wiki/src/models/tutorial/static/headerbg.png b/docs/tutorials/wiki/src/models/tutorial/static/headerbg.png deleted file mode 100644 index 0596f2020..000000000 Binary files a/docs/tutorials/wiki/src/models/tutorial/static/headerbg.png and /dev/null differ diff --git a/docs/tutorials/wiki/src/models/tutorial/static/ie6.css b/docs/tutorials/wiki/src/models/tutorial/static/ie6.css deleted file mode 100644 index b7c8493d8..000000000 --- a/docs/tutorials/wiki/src/models/tutorial/static/ie6.css +++ /dev/null @@ -1,8 +0,0 @@ -* html img, -* html .png{position:relative;behavior:expression((this.runtimeStyle.behavior="none")&&(this.pngSet?this.pngSet=true:(this.nodeName == "IMG" && this.src.toLowerCase().indexOf('.png')>-1?(this.runtimeStyle.backgroundImage = "none", -this.runtimeStyle.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + this.src + "',sizingMethod='image')", -this.src = "static/transparent.gif"):(this.origBg = this.origBg? this.origBg :this.currentStyle.backgroundImage.toString().replace('url("','').replace('")',''), -this.runtimeStyle.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + this.origBg + "',sizingMethod='crop')", -this.runtimeStyle.backgroundImage = "none")),this.pngSet=true) -);} -#wrap{display:table;height:100%} diff --git a/docs/tutorials/wiki/src/models/tutorial/static/middlebg.png b/docs/tutorials/wiki/src/models/tutorial/static/middlebg.png deleted file mode 100644 index 2369cfb7d..000000000 Binary files a/docs/tutorials/wiki/src/models/tutorial/static/middlebg.png and /dev/null differ diff --git a/docs/tutorials/wiki/src/models/tutorial/static/pylons.css b/docs/tutorials/wiki/src/models/tutorial/static/pylons.css deleted file mode 100644 index 4b1c017cd..000000000 --- a/docs/tutorials/wiki/src/models/tutorial/static/pylons.css +++ /dev/null @@ -1,372 +0,0 @@ -html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td -{ - margin: 0; - padding: 0; - border: 0; - outline: 0; - font-size: 100%; /* 16px */ - vertical-align: baseline; - background: transparent; -} - -body -{ - line-height: 1; -} - -ol, ul -{ - list-style: none; -} - -blockquote, q -{ - quotes: none; -} - -blockquote:before, blockquote:after, q:before, q:after -{ - content: ''; - content: none; -} - -:focus -{ - outline: 0; -} - -ins -{ - text-decoration: none; -} - -del -{ - text-decoration: line-through; -} - -table -{ - border-collapse: collapse; - border-spacing: 0; -} - -sub -{ - vertical-align: sub; - font-size: smaller; - line-height: normal; -} - -sup -{ - vertical-align: super; - font-size: smaller; - line-height: normal; -} - -ul, menu, dir -{ - display: block; - list-style-type: disc; - margin: 1em 0; - padding-left: 40px; -} - -ol -{ - display: block; - list-style-type: decimal-leading-zero; - margin: 1em 0; - padding-left: 40px; -} - -li -{ - display: list-item; -} - -ul ul, ul ol, ul dir, ul menu, ul dl, ol ul, ol ol, ol dir, ol menu, ol dl, dir ul, dir ol, dir dir, dir menu, dir dl, menu ul, menu ol, menu dir, menu menu, menu dl, dl ul, dl ol, dl dir, dl menu, dl dl -{ - margin-top: 0; - margin-bottom: 0; -} - -ol ul, ul ul, menu ul, dir ul, ol menu, ul menu, menu menu, dir menu, ol dir, ul dir, menu dir, dir dir -{ - list-style-type: circle; -} - -ol ol ul, ol ul ul, ol menu ul, ol dir ul, ol ol menu, ol ul menu, ol menu menu, ol dir menu, ol ol dir, ol ul dir, ol menu dir, ol dir dir, ul ol ul, ul ul ul, ul menu ul, ul dir ul, ul ol menu, ul ul menu, ul menu menu, ul dir menu, ul ol dir, ul ul dir, ul menu dir, ul dir dir, menu ol ul, menu ul ul, menu menu ul, menu dir ul, menu ol menu, menu ul menu, menu menu menu, menu dir menu, menu ol dir, menu ul dir, menu menu dir, menu dir dir, dir ol ul, dir ul ul, dir menu ul, dir dir ul, dir ol menu, dir ul menu, dir menu menu, dir dir menu, dir ol dir, dir ul dir, dir menu dir, dir dir dir -{ - list-style-type: square; -} - -.hidden -{ - display: none; -} - -p -{ - line-height: 1.5em; -} - -h1 -{ - font-size: 1.75em; - line-height: 1.7em; - font-family: helvetica, verdana; -} - -h2 -{ - font-size: 1.5em; - line-height: 1.7em; - font-family: helvetica, verdana; -} - -h3 -{ - font-size: 1.25em; - line-height: 1.7em; - font-family: helvetica, verdana; -} - -h4 -{ - font-size: 1em; - line-height: 1.7em; - font-family: helvetica, verdana; -} - -html, body -{ - width: 100%; - height: 100%; -} - -body -{ - margin: 0; - padding: 0; - background-color: #fff; - position: relative; - font: 16px/24px NobileRegular, "Lucida Grande", Lucida, Verdana, sans-serif; -} - -a -{ - color: #1b61d6; - text-decoration: none; -} - -a:hover -{ - color: #e88f00; - text-decoration: underline; -} - -body h1, body h2, body h3, body h4, body h5, body h6 -{ - font-family: NeutonRegular, "Lucida Grande", Lucida, Verdana, sans-serif; - font-weight: 400; - color: #373839; - font-style: normal; -} - -#wrap -{ - min-height: 100%; -} - -#header, #footer -{ - width: 100%; - color: #fff; - height: 40px; - position: absolute; - text-align: center; - line-height: 40px; - overflow: hidden; - font-size: 12px; - vertical-align: middle; -} - -#header -{ - background: #000; - top: 0; - font-size: 14px; -} - -#footer -{ - bottom: 0; - background: #000 url(footerbg.png) repeat-x 0 top; - position: relative; - margin-top: -40px; - clear: both; -} - -.header, .footer -{ - width: 750px; - margin-right: auto; - margin-left: auto; -} - -.wrapper -{ - width: 100%; -} - -#top, #top-small, #bottom -{ - width: 100%; -} - -#top -{ - color: #000; - height: 230px; - background: #fff url(headerbg.png) repeat-x 0 top; - position: relative; -} - -#top-small -{ - color: #000; - height: 60px; - background: #fff url(headerbg.png) repeat-x 0 top; - position: relative; -} - -#bottom -{ - color: #222; - background-color: #fff; -} - -.top, .top-small, .middle, .bottom -{ - width: 750px; - margin-right: auto; - margin-left: auto; -} - -.top -{ - padding-top: 40px; -} - -.top-small -{ - padding-top: 10px; -} - -#middle -{ - width: 100%; - height: 100px; - background: url(middlebg.png) repeat-x; - border-top: 2px solid #fff; - border-bottom: 2px solid #b2b2b2; -} - -.app-welcome -{ - margin-top: 25px; -} - -.app-name -{ - color: #000; - font-weight: 700; -} - -.bottom -{ - padding-top: 50px; -} - -#left -{ - width: 350px; - float: left; - padding-right: 25px; -} - -#right -{ - width: 350px; - float: right; - padding-left: 25px; -} - -.align-left -{ - text-align: left; -} - -.align-right -{ - text-align: right; -} - -.align-center -{ - text-align: center; -} - -ul.links -{ - margin: 0; - padding: 0; -} - -ul.links li -{ - list-style-type: none; - font-size: 14px; -} - -form -{ - border-style: none; -} - -fieldset -{ - border-style: none; -} - -input -{ - color: #222; - border: 1px solid #ccc; - font-family: sans-serif; - font-size: 12px; - line-height: 16px; -} - -input[type=text], input[type=password] -{ - width: 205px; -} - -input[type=submit] -{ - background-color: #ddd; - font-weight: 700; -} - -/*Opera Fix*/ -body:before -{ - content: ""; - height: 100%; - float: left; - width: 0; - margin-top: -32767px; -} diff --git a/docs/tutorials/wiki/src/models/tutorial/static/pyramid-16x16.png b/docs/tutorials/wiki/src/models/tutorial/static/pyramid-16x16.png new file mode 100644 index 000000000..979203112 Binary files /dev/null and b/docs/tutorials/wiki/src/models/tutorial/static/pyramid-16x16.png differ diff --git a/docs/tutorials/wiki/src/models/tutorial/static/pyramid-small.png b/docs/tutorials/wiki/src/models/tutorial/static/pyramid-small.png deleted file mode 100644 index a5bc0ade7..000000000 Binary files a/docs/tutorials/wiki/src/models/tutorial/static/pyramid-small.png and /dev/null differ diff --git a/docs/tutorials/wiki/src/models/tutorial/static/pyramid.png b/docs/tutorials/wiki/src/models/tutorial/static/pyramid.png index 347e05549..4ab837be9 100644 Binary files a/docs/tutorials/wiki/src/models/tutorial/static/pyramid.png and b/docs/tutorials/wiki/src/models/tutorial/static/pyramid.png differ diff --git a/docs/tutorials/wiki/src/models/tutorial/static/theme.css b/docs/tutorials/wiki/src/models/tutorial/static/theme.css new file mode 100644 index 000000000..0f4b1a4d4 --- /dev/null +++ b/docs/tutorials/wiki/src/models/tutorial/static/theme.css @@ -0,0 +1,154 @@ +@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700); +body { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; + color: #ffffff; + background: #bc2131; +} +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; +} +p { + font-weight: 300; +} +.font-normal { + font-weight: 400; +} +.font-semi-bold { + font-weight: 600; +} +.font-bold { + font-weight: 700; +} +.starter-template { + margin-top: 250px; +} +.starter-template .content { + margin-left: 10px; +} +.starter-template .content h1 { + margin-top: 10px; + font-size: 60px; +} +.starter-template .content h1 .smaller { + font-size: 40px; + color: #f2b7bd; +} +.starter-template .content .lead { + font-size: 25px; + color: #f2b7bd; +} +.starter-template .content .lead .font-normal { + color: #ffffff; +} +.starter-template .links { + float: right; + right: 0; + margin-top: 125px; +} +.starter-template .links ul { + display: block; + padding: 0; + margin: 0; +} +.starter-template .links ul li { + list-style: none; + display: inline; + margin: 0 10px; +} +.starter-template .links ul li:first-child { + margin-left: 0; +} +.starter-template .links ul li:last-child { + margin-right: 0; +} +.starter-template .links ul li.current-version { + color: #f2b7bd; + font-weight: 400; +} +.starter-template .links ul li a, a { + color: #f2b7bd; + text-decoration: underline; +} +.starter-template .links ul li a:hover, a:hover { + color: #ffffff; + text-decoration: underline; +} +.starter-template .links ul li .icon-muted { + color: #eb8b95; + margin-right: 5px; +} +.starter-template .links ul li:hover .icon-muted { + color: #ffffff; +} +.starter-template .copyright { + margin-top: 10px; + font-size: 0.9em; + color: #f2b7bd; + text-transform: lowercase; + float: right; + right: 0; +} +@media (max-width: 1199px) { + .starter-template .content h1 { + font-size: 45px; + } + .starter-template .content h1 .smaller { + font-size: 30px; + } + .starter-template .content .lead { + font-size: 20px; + } +} +@media (max-width: 991px) { + .starter-template { + margin-top: 0; + } + .starter-template .logo { + margin: 40px auto; + } + .starter-template .content { + margin-left: 0; + text-align: center; + } + .starter-template .content h1 { + margin-bottom: 20px; + } + .starter-template .links { + float: none; + text-align: center; + margin-top: 60px; + } + .starter-template .copyright { + float: none; + text-align: center; + } +} +@media (max-width: 767px) { + .starter-template .content h1 .smaller { + font-size: 25px; + display: block; + } + .starter-template .content .lead { + font-size: 16px; + } + .starter-template .links { + margin-top: 40px; + } + .starter-template .links ul li { + display: block; + margin: 0; + } + .starter-template .links ul li .icon-muted { + display: none; + } + .starter-template .copyright { + margin-top: 20px; + } +} diff --git a/docs/tutorials/wiki/src/models/tutorial/static/theme.min.css b/docs/tutorials/wiki/src/models/tutorial/static/theme.min.css new file mode 100644 index 000000000..2f924bcc5 --- /dev/null +++ b/docs/tutorials/wiki/src/models/tutorial/static/theme.min.css @@ -0,0 +1 @@ +@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);body{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300;color:#fff;background:#bc2131}h1,h2,h3,h4,h5,h6{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300}p{font-weight:300}.font-normal{font-weight:400}.font-semi-bold{font-weight:600}.font-bold{font-weight:700}.starter-template{margin-top:250px}.starter-template .content{margin-left:10px}.starter-template .content h1{margin-top:10px;font-size:60px}.starter-template .content h1 .smaller{font-size:40px;color:#f2b7bd}.starter-template .content .lead{font-size:25px;color:#f2b7bd}.starter-template .content .lead .font-normal{color:#fff}.starter-template .links{float:right;right:0;margin-top:125px}.starter-template .links ul{display:block;padding:0;margin:0}.starter-template .links ul li{list-style:none;display:inline;margin:0 10px}.starter-template .links ul li:first-child{margin-left:0}.starter-template .links ul li:last-child{margin-right:0}.starter-template .links ul li.current-version{color:#f2b7bd;font-weight:400}.starter-template .links ul li a{color:#fff}.starter-template .links ul li a:hover{text-decoration:underline}.starter-template .links ul li .icon-muted{color:#eb8b95;margin-right:5px}.starter-template .links ul li:hover .icon-muted{color:#fff}.starter-template .copyright{margin-top:10px;font-size:.9em;color:#f2b7bd;text-transform:lowercase;float:right;right:0}@media (max-width:1199px){.starter-template .content h1{font-size:45px}.starter-template .content h1 .smaller{font-size:30px}.starter-template .content .lead{font-size:20px}}@media (max-width:991px){.starter-template{margin-top:0}.starter-template .logo{margin:40px auto}.starter-template .content{margin-left:0;text-align:center}.starter-template .content h1{margin-bottom:20px}.starter-template .links{float:none;text-align:center;margin-top:60px}.starter-template .copyright{float:none;text-align:center}}@media (max-width:767px){.starter-template .content h1 .smaller{font-size:25px;display:block}.starter-template .content .lead{font-size:16px}.starter-template .links{margin-top:40px}.starter-template .links ul li{display:block;margin:0}.starter-template .links ul li .icon-muted{display:none}.starter-template .copyright{margin-top:20px}} \ No newline at end of file diff --git a/docs/tutorials/wiki/src/models/tutorial/static/transparent.gif b/docs/tutorials/wiki/src/models/tutorial/static/transparent.gif deleted file mode 100644 index 0341802e5..000000000 Binary files a/docs/tutorials/wiki/src/models/tutorial/static/transparent.gif and /dev/null differ diff --git a/docs/tutorials/wiki/src/models/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki/src/models/tutorial/templates/mytemplate.pt index 13b41f823..29dacef19 100644 --- a/docs/tutorials/wiki/src/models/tutorial/templates/mytemplate.pt +++ b/docs/tutorials/wiki/src/models/tutorial/templates/mytemplate.pt @@ -1,73 +1,66 @@ - - - - The Pyramid Web Framework - - - - - - - - - - -
-
-
-
pyramid
-
-
-
-
-

- Welcome to ${project}, an application generated by
- the Pyramid Web Framework. -

-
-
-
-
-
-

Search documentation

-
- - -
+ + + + + + + + + + + Alchemy Scaffold for The Pyramid Web Framework + + + + + + + + + + + + + +
+
+
+
+ +
+
+
+

Pyramid ZODB scaffold

+

Welcome to ${project}, an application generated by
the Pyramid Web Framework.

+
+
-
-
- + + + + + + + -- cgit v1.2.3 From a940e1ccaa6c3ca81b1dec6ba2e505af9b792222 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Tue, 26 May 2015 13:12:01 -0700 Subject: - clean up and make defininingviews consistent between two wiki tutorials - sort imports to be consistent with scaffold setup.py - update templates, static assets --- docs/tutorials/wiki/definingviews.rst | 289 +++++++++------- docs/tutorials/wiki/src/authorization/setup.py | 4 +- .../authorization/tutorial/templates/mytemplate.pt | 127 ++++--- docs/tutorials/wiki/src/basiclayout/setup.py | 4 +- .../basiclayout/tutorial/templates/mytemplate.pt | 2 +- docs/tutorials/wiki/src/models/setup.py | 4 +- .../src/models/tutorial/templates/mytemplate.pt | 2 +- docs/tutorials/wiki/src/tests/setup.py | 4 +- .../src/tests/tutorial/templates/mytemplate.pt | 127 ++++--- docs/tutorials/wiki/src/views/setup.py | 4 +- .../wiki/src/views/tutorial/static/favicon.ico | Bin 1406 -> 0 bytes .../wiki/src/views/tutorial/static/footerbg.png | Bin 333 -> 0 bytes .../wiki/src/views/tutorial/static/headerbg.png | Bin 203 -> 0 bytes .../wiki/src/views/tutorial/static/ie6.css | 8 - .../wiki/src/views/tutorial/static/middlebg.png | Bin 2797 -> 0 bytes .../wiki/src/views/tutorial/static/pylons.css | 372 --------------------- .../src/views/tutorial/static/pyramid-16x16.png | Bin 0 -> 1319 bytes .../src/views/tutorial/static/pyramid-small.png | Bin 7044 -> 0 bytes .../wiki/src/views/tutorial/static/pyramid.png | Bin 33055 -> 12901 bytes .../wiki/src/views/tutorial/static/theme.css | 154 +++++++++ .../wiki/src/views/tutorial/static/theme.min.css | 1 + .../wiki/src/views/tutorial/static/transparent.gif | Bin 49 -> 0 bytes .../wiki/src/views/tutorial/templates/edit.pt | 115 ++++--- .../src/views/tutorial/templates/mytemplate.pt | 130 ++++--- .../wiki/src/views/tutorial/templates/view.pt | 120 +++---- docs/tutorials/wiki2/definingviews.rst | 163 ++++----- 26 files changed, 721 insertions(+), 909 deletions(-) delete mode 100644 docs/tutorials/wiki/src/views/tutorial/static/favicon.ico delete mode 100644 docs/tutorials/wiki/src/views/tutorial/static/footerbg.png delete mode 100644 docs/tutorials/wiki/src/views/tutorial/static/headerbg.png delete mode 100644 docs/tutorials/wiki/src/views/tutorial/static/ie6.css delete mode 100644 docs/tutorials/wiki/src/views/tutorial/static/middlebg.png delete mode 100644 docs/tutorials/wiki/src/views/tutorial/static/pylons.css create mode 100644 docs/tutorials/wiki/src/views/tutorial/static/pyramid-16x16.png delete mode 100644 docs/tutorials/wiki/src/views/tutorial/static/pyramid-small.png create mode 100644 docs/tutorials/wiki/src/views/tutorial/static/theme.css create mode 100644 docs/tutorials/wiki/src/views/tutorial/static/theme.min.css delete mode 100644 docs/tutorials/wiki/src/views/tutorial/static/transparent.gif diff --git a/docs/tutorials/wiki/definingviews.rst b/docs/tutorials/wiki/definingviews.rst index e06468267..2ebddd4c8 100644 --- a/docs/tutorials/wiki/definingviews.rst +++ b/docs/tutorials/wiki/definingviews.rst @@ -17,7 +17,7 @@ assumed to return a :term:`response` object. interchangeably as necessary. In :term:`traversal` based applications, URLs are mapped to a context :term:`resource`, and since our :term:`resource tree` also represents our application's - "domain model", we're often interested in the context, because + "domain model", we're often interested in the context because it represents the persistent storage of our application. For this reason, in this tutorial we define views as callables that accept ``context`` in the callable argument list. If you do @@ -35,35 +35,80 @@ Declaring Dependencies in Our ``setup.py`` File The view code in our application will depend on a package which is not a dependency of the original "tutorial" application. The original "tutorial" application was generated by the ``pcreate`` command; it doesn't know -about our custom application requirements. We need to add a dependency on -the ``docutils`` package to our ``tutorial`` package's ``setup.py`` file by -assigning this dependency to the ``install_requires`` parameter in the -``setup`` function. +about our custom application requirements. -Our resulting ``setup.py`` should look like so: +We need to add a dependency on the ``docutils`` package to our ``tutorial`` +package's ``setup.py`` file by assigning this dependency to the ``requires`` +parameter in the ``setup()`` function. + +Open ``tutorial/setup.py`` and edit it to look like the following: .. literalinclude:: src/views/setup.py :linenos: + :emphasize-lines: 20 :language: python -.. note:: After these new dependencies are added, you will need to - rerun ``python setup.py develop`` inside the root of the - ``tutorial`` package to obtain and register the newly added - dependency package. +Only the highlighted line needs to be added. + +Running ``setup.py develop`` +============================ + +Since a new software dependency was added, you will need to run ``python +setup.py develop`` again inside the root of the ``tutorial`` package to obtain +and register the newly added dependency distribution. + +Make sure your current working directory is the root of the project (the +directory in which ``setup.py`` lives) and execute the following command. + +On UNIX: + +.. code-block:: text + + $ cd tutorial + $ $VENV/bin/python setup.py develop + +On Windows: + +.. code-block:: text -Adding View Functions -===================== + c:\pyramidtut> cd tutorial + c:\pyramidtut\tutorial> %VENV%\Scripts\python setup.py develop + +Success executing this command will end with a line to the console something +like:: + + Finished processing dependencies for tutorial==0.0 + +Adding view functions in ``views.py`` +===================================== + +It's time for a major change. Open ``tutorial/tutorial/views.py`` and edit it +to look like the following: + +.. literalinclude:: src/views/tutorial/views.py + :linenos: + :language: python -We're going to add four :term:`view callable` functions to our ``views.py`` -module. One view named ``view_wiki`` will display the wiki itself (it will -answer on the root URL), another named ``view_page`` will display an -individual page, another named ``add_page`` will allow a page to be added, -and a final view named ``edit_page`` will allow a page to be edited. +We added some imports and created a regular expression to find "WikiWords". + +We got rid of the ``my_view`` view function and its decorator that was added +when we originally rendered the ``zodb`` scaffold. It was only an example and +isn't relevant to our application. + +Then we added four :term:`view callable` functions to our ``views.py`` +module: + +* ``view_wiki()`` - Displays the wiki itself. It will answer on the root URL. +* ``view_page()`` - Displays an individual page. +* ``add_page()`` - Allows the user to add a page. +* ``edit_page()`` - Allows the user to edit a page. + +We'll describe each one briefly in the following sections. .. note:: There is nothing special about the filename ``views.py``. A project may - have many view callables throughout its codebase in arbitrarily-named + have many view callables throughout its codebase in arbitrarily named files. Files implementing view callables often have ``view`` in their filenames (or may live in a Python subpackage of your application package named ``views``), but this is only by convention. @@ -71,44 +116,55 @@ and a final view named ``edit_page`` will allow a page to be edited. The ``view_wiki`` view function ------------------------------- -Here is the code for the ``view_wiki`` view function and its decorator, which -will be added to ``views.py``: +Following is the code for the ``view_wiki`` view function and its decorator: .. literalinclude:: src/views/tutorial/views.py :lines: 12-14 + :lineno-start: 12 + :linenos: :language: python -The ``view_wiki`` function will be configured to respond as the default view -callable for a Wiki resource. We'll provide it with a ``@view_config`` -decorator which names the class ``tutorial.models.Wiki`` as its context. -This means that when a Wiki resource is the context, and no :term:`view name` -exists in the request, this view will be used. The view configuration -associated with ``view_wiki`` does not use a ``renderer`` because the view -callable always returns a :term:`response` object rather than a dictionary. -No renderer is necessary when a view returns a response object. - -The ``view_wiki`` view callable always redirects to the URL of a Page -resource named "FrontPage". To do so, it returns an instance of the +.. note:: In our code, we use an *import* that is *relative* to our package + named ``tutorial``, meaning we can omit the name of the package in the + ``import`` and ``context`` statements. In our narrative, however, we refer + to a *class* and thus we use the *absolute* form, meaning that the name of + the package is included. + +``view_wiki()`` is the :term:`default view` that gets called when a request is +made to the root URL of our wiki. It always redirects to an URL which +represents the path to our "FrontPage". + +We provide it with a ``@view_config`` decorator which names the class +``tutorial.models.Wiki`` as its context. This means that when a Wiki resource +is the context and no :term:`view name` exists in the request, then this view +will be used. The view configuration associated with ``view_wiki`` does not +use a ``renderer`` because the view callable always returns a :term:`response` +object rather than a dictionary. No renderer is necessary when a view returns +a response object. + +The ``view_wiki`` view callable always redirects to the URL of a Page resource +named "FrontPage". To do so, it returns an instance of the :class:`pyramid.httpexceptions.HTTPFound` class (instances of which implement -the :class:`pyramid.interfaces.IResponse` interface like -:class:`pyramid.response.Response` does). -:meth:`pyramid.request.Request.resource_url` constructs a URL to the +the :class:`pyramid.interfaces.IResponse` interface, like +:class:`pyramid.response.Response` does). It uses the +:meth:`pyramid.request.Request.route_url` API to construct an URL to the ``FrontPage`` page resource (i.e., ``http://localhost:6543/FrontPage``), and -uses it as the "location" of the HTTPFound response, forming an HTTP +uses it as the "location" of the ``HTTPFound`` response, forming an HTTP redirect. The ``view_page`` view function ------------------------------- -Here is the code for the ``view_page`` view function and its decorator, which -will be added to ``views.py``: +Here is the code for the ``view_page`` view function and its decorator: .. literalinclude:: src/views/tutorial/views.py :lines: 16-33 + :lineno-start: 16 + :linenos: :language: python -The ``view_page`` function will be configured to respond as the default view -of a Page resource. We'll provide it with a ``@view_config`` decorator which +The ``view_page`` function is configured to respond as the default view +of a Page resource. We provide it with a ``@view_config`` decorator which names the class ``tutorial.models.Page`` as its context. This means that when a Page resource is the context, and no :term:`view name` exists in the request, this view will be used. We inform :app:`Pyramid` this view will use @@ -116,9 +172,9 @@ the ``templates/view.pt`` template file as a ``renderer``. The ``view_page`` function generates the :term:`reStructuredText` body of a page (stored as the ``data`` attribute of the context passed to the view; the -context will be a Page resource) as HTML. Then it substitutes an HTML anchor -for each *WikiWord* reference in the rendered HTML using a compiled regular -expression. +context will be a ``Page`` resource) as HTML. Then it substitutes an HTML +anchor for each *WikiWord* reference in the rendered HTML using a compiled +regular expression. The curried function named ``check`` is used as the first argument to ``wikiwords.sub``, indicating that it should be called to provide a value for @@ -133,8 +189,8 @@ As a result, the ``content`` variable is now a fully formed bit of HTML containing various view and add links for WikiWords based on the content of our current page resource. -We then generate an edit URL (because it's easier to do here than in the -template), and we wrap up a number of arguments in a dictionary and return +We then generate an edit URL because it's easier to do here than in the +template, and we wrap up a number of arguments in a dictionary and return it. The arguments we wrap into a dictionary include ``page``, ``content``, and @@ -153,22 +209,23 @@ callable. In the ``view_wiki`` view callable, we unconditionally return a The ``add_page`` view function ------------------------------ -Here is the code for the ``add_page`` view function and its decorator, which -will be added to ``views.py``: +Here is the code for the ``add_page`` view function and its decorator: .. literalinclude:: src/views/tutorial/views.py :lines: 35-50 + :lineno-start: 35 + :linenos: :language: python -The ``add_page`` function will be configured to respond when the context -resource is a Wiki and the :term:`view name` is ``add_page``. We'll provide -it with a ``@view_config`` decorator which names the string ``add_page`` as -its :term:`view name` (via name=), the class ``tutorial.models.Wiki`` as its -context, and the renderer named ``templates/edit.pt``. This means that when -a Wiki resource is the context, and a :term:`view name` named ``add_page`` +The ``add_page`` function is configured to respond when the context resource +is a Wiki and the :term:`view name` is ``add_page``. We provide it with a +``@view_config`` decorator which names the string ``add_page`` as its +:term:`view name` (via ``name=``), the class ``tutorial.models.Wiki`` as its +context, and the renderer named ``templates/edit.pt``. This means that when a +Wiki resource is the context, and a :term:`view name` named ``add_page`` exists as the result of traversal, this view will be used. We inform -:app:`Pyramid` this view will use the ``templates/edit.pt`` template file as -a ``renderer``. We share the same template between add and edit views, thus +:app:`Pyramid` this view will use the ``templates/edit.pt`` template file as a +``renderer``. We share the same template between add and edit views, thus ``edit.pt`` instead of ``add.pt``. The ``add_page`` function will be invoked when a user clicks on a WikiWord @@ -181,7 +238,7 @@ Page resource). The request :term:`subpath` in :app:`Pyramid` is the sequence of names that are found *after* the :term:`view name` in the URL segments given in the ``PATH_INFO`` of the WSGI request as the result of :term:`traversal`. If our -add view is invoked via, e.g. ``http://localhost:6543/add_page/SomeName``, +add view is invoked via, e.g., ``http://localhost:6543/add_page/SomeName``, the :term:`subpath` will be a tuple: ``('SomeName',)``. The add view takes the zeroth element of the subpath (the wiki page name), @@ -198,7 +255,7 @@ order to satisfy the edit form's desire to have *some* page object exposed as ``page``, and we'll render the template to a response. If the view rendering *is* a result of a form submission (if the expression -``'form.submitted' in request.params`` is ``True``), we scrape the page body +``'form.submitted' in request.params`` is ``True``), we grab the page body from the form data, create a Page object using the name in the subpath and the page body, and save it into "our context" (the Wiki) using the ``__setitem__`` method of the context. We then redirect back to the @@ -207,15 +264,16 @@ the page body, and save it into "our context" (the Wiki) using the The ``edit_page`` view function ------------------------------- -Here is the code for the ``edit_page`` view function and its decorator, which -will be added to ``views.py``: +Here is the code for the ``edit_page`` view function and its decorator: .. literalinclude:: src/views/tutorial/views.py :lines: 52-60 + :lineno-start: 52 + :linenos: :language: python -The ``edit_page`` function will be configured to respond when the context is -a Page resource and the :term:`view name` is ``edit_page``. We'll provide it +The ``edit_page`` function is configured to respond when the context is +a Page resource and the :term:`view name` is ``edit_page``. We provide it with a ``@view_config`` decorator which names the string ``edit_page`` as its :term:`view name` (via ``name=``), the class ``tutorial.models.Page`` as its context, and the renderer named ``templates/edit.pt``. This means that when @@ -240,26 +298,16 @@ If the view execution *is* a result of a form submission (if the expression attribute of the page context. It then redirects to the default view of the context (the page), which will always be the ``view_page`` view. -Viewing the Result of all Our Edits to ``views.py`` -=================================================== - -The result of all of our edits to ``views.py`` will leave it looking like -this: - -.. literalinclude:: src/views/tutorial/views.py - :linenos: - :language: python - -Adding Templates +Adding templates ================ The ``view_page``, ``add_page`` and ``edit_page`` views that we've added -reference a :term:`template`. Each template is a :term:`Chameleon` :term:`ZPT` -template. These templates will live in the ``templates`` directory of our -tutorial package. Chameleon templates must have a ``.pt`` extension to be -recognized as such. +reference a :term:`template`. Each template is a :term:`Chameleon` +:term:`ZPT` template. These templates will live in the ``templates`` +directory of our tutorial package. Chameleon templates must have a ``.pt`` +extension to be recognized as such. -The ``view.pt`` Template +The ``view.pt`` template ------------------------ Create ``tutorial/tutorial/templates/view.pt`` and add the following @@ -267,20 +315,18 @@ content: .. literalinclude:: src/views/tutorial/templates/view.pt :linenos: - :language: xml + :language: html This template is used by ``view_page()`` for displaying a single wiki page. It includes: -- A ``div`` element that is replaced with the ``content`` - value provided by the view (rows 45-47). ``content`` - contains HTML, so the ``structure`` keyword is used - to prevent escaping it (i.e. changing ">" to ">", etc.) -- A link that points - at the "edit" URL which invokes the ``edit_page`` view for - the page being viewed (rows 49-51). +- A ``div`` element that is replaced with the ``content`` value provided by + the view (lines 36-38). ``content`` contains HTML, so the ``structure`` + keyword is used to prevent escaping it (i.e., changing ">" to ">", etc.) +- A link that points at the "edit" URL which invokes the ``edit_page`` view + for the page being viewed (lines 40-42). -The ``edit.pt`` Template +The ``edit.pt`` template ------------------------ Create ``tutorial/tutorial/templates/edit.pt`` and add the following @@ -288,66 +334,59 @@ content: .. literalinclude:: src/views/tutorial/templates/edit.pt :linenos: - :language: xml + :language: html -This template is used by ``add_page()`` and ``edit_page()`` for adding -and editing a wiki page. It displays -a page containing a form that includes: +This template is used by ``add_page()`` and ``edit_page()`` for adding and +editing a wiki page. It displays a page containing a form that includes: - A 10 row by 60 column ``textarea`` field named ``body`` that is filled - with any existing page data when it is rendered (rows 46-47). -- A submit button that has the name ``form.submitted`` (row 48). + with any existing page data when it is rendered (line 45). +- A submit button that has the name ``form.submitted`` (line 48). -The form POSTs back to the "save_url" argument supplied -by the view (row 45). The view will use the ``body`` and -``form.submitted`` values. +The form POSTs back to the ``save_url`` argument supplied by the view (line +43). The view will use the ``body`` and ``form.submitted`` values. -.. note:: Our templates use a ``request`` object that - none of our tutorial views return in their dictionary. - ``request`` is one of several - names that are available "by default" in a template when a template - renderer is used. See :ref:`renderer_system_values` for - information about other names that are available by default - when a template is used as a renderer. +.. note:: Our templates use a ``request`` object that none of our tutorial + views return in their dictionary. ``request`` is one of several names that + are available "by default" in a template when a template renderer is used. + See :ref:`renderer_system_values` for information about other names that + are available by default when a template is used as a renderer. Static Assets ------------- -Our templates name a single static asset named ``pylons.css``. We don't need -to create this file within our package's ``static`` directory because it was -provided at the time we created the project. This file is a little too long to -replicate within the body of this guide, however it is available `online -`_. +Our templates name static assets, including CSS and images. We don't need +to create these files within our package's ``static`` directory because they +were provided at the time we created the project. -This CSS file will be accessed via -e.g. ``/static/pylons.css`` by virtue of the call to +As an example, the CSS file will be accessed via +``http://localhost:6543/static/theme.css`` by virtue of the call to the ``add_static_view`` directive we've made in the ``__init__.py`` file. Any number and type of static assets can be placed in this directory (or -subdirectories) and are just referred to by URL. +subdirectories) and are just referred to by URL or by using the convenience +method ``static_url``, e.g., +``request.static_url(':static/foo.css')`` within templates. Viewing the Application in a Browser ==================================== We can finally examine our application in a browser (See :ref:`wiki-start-the-application`). Launch a browser and visit -each of the following URLs, check that the result is as expected: +each of the following URLs, checking that the result is as expected: -- ``http://localhost:6543/`` invokes the ``view_wiki`` - view. This always redirects to the ``view_page`` view of the ``FrontPage`` - Page resource. +- ``http://localhost:6543/`` invokes the ``view_wiki`` view. This always + redirects to the ``view_page`` view of the ``FrontPage`` Page resource. -- ``http://localhost:6543/FrontPage/`` invokes - the ``view_page`` view of the front page resource. This is - because it's the :term:`default view` (a view without a ``name``) for Page - resources. +- ``http://localhost:6543/FrontPage/`` invokes the ``view_page`` view of the + front page resource. This is because it's the :term:`default view` (a view + without a ``name``) for Page resources. -- ``http://localhost:6543/FrontPage/edit_page`` - invokes the edit view for the ``FrontPage`` Page resource. +- ``http://localhost:6543/FrontPage/edit_page`` invokes the edit view for the + ``FrontPage`` Page resource. -- ``http://localhost:6543/add_page/SomePageName`` - invokes the add view for a Page. +- ``http://localhost:6543/add_page/SomePageName`` invokes the add view for a + Page. -- To generate an error, visit ``http://localhost:6543/add_page`` which - will generate an ``IndexErrorr: tuple index out of range`` error. - You'll see an interactive traceback - facility provided by :term:`pyramid_debugtoolbar`. +- To generate an error, visit ``http://localhost:6543/add_page`` which will + generate an ``IndexErrorr: tuple index out of range`` error. You'll see an + interactive traceback facility provided by :term:`pyramid_debugtoolbar`. diff --git a/docs/tutorials/wiki/src/authorization/setup.py b/docs/tutorials/wiki/src/authorization/setup.py index 5ab4f73cd..e2e96379d 100644 --- a/docs/tutorials/wiki/src/authorization/setup.py +++ b/docs/tutorials/wiki/src/authorization/setup.py @@ -11,10 +11,10 @@ with open(os.path.join(here, 'CHANGES.txt')) as f: requires = [ 'pyramid', 'pyramid_chameleon', + 'pyramid_debugtoolbar', + 'pyramid_tm', 'pyramid_zodbconn', 'transaction', - 'pyramid_tm', - 'pyramid_debugtoolbar', 'ZODB3', 'waitress', 'docutils', diff --git a/docs/tutorials/wiki/src/authorization/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki/src/authorization/tutorial/templates/mytemplate.pt index 13b41f823..1b30f42b6 100644 --- a/docs/tutorials/wiki/src/authorization/tutorial/templates/mytemplate.pt +++ b/docs/tutorials/wiki/src/authorization/tutorial/templates/mytemplate.pt @@ -1,73 +1,66 @@ - - - - The Pyramid Web Framework - - - - - - - - - - -
-
-
-
pyramid
-
-
-
-
-

- Welcome to ${project}, an application generated by
- the Pyramid Web Framework. -

-
-
-
-
-
-

Search documentation

-
- - -
+ + + + + + + + + + + ZODB Scaffold for The Pyramid Web Framework + + + + + + + + + + + + + +
+
+
+
+ +
+
+
+

Pyramid ZODB scaffold

+

Welcome to ${project}, an application generated by
the Pyramid Web Framework.

+
+
-
-
- + + + + + + + diff --git a/docs/tutorials/wiki/src/basiclayout/setup.py b/docs/tutorials/wiki/src/basiclayout/setup.py index da79881ab..58a454f80 100644 --- a/docs/tutorials/wiki/src/basiclayout/setup.py +++ b/docs/tutorials/wiki/src/basiclayout/setup.py @@ -11,10 +11,10 @@ with open(os.path.join(here, 'CHANGES.txt')) as f: requires = [ 'pyramid', 'pyramid_chameleon', + 'pyramid_debugtoolbar', + 'pyramid_tm', 'pyramid_zodbconn', 'transaction', - 'pyramid_tm', - 'pyramid_debugtoolbar', 'ZODB3', 'waitress', ] diff --git a/docs/tutorials/wiki/src/basiclayout/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki/src/basiclayout/tutorial/templates/mytemplate.pt index 29dacef19..1b30f42b6 100644 --- a/docs/tutorials/wiki/src/basiclayout/tutorial/templates/mytemplate.pt +++ b/docs/tutorials/wiki/src/basiclayout/tutorial/templates/mytemplate.pt @@ -8,7 +8,7 @@ - Alchemy Scaffold for The Pyramid Web Framework + ZODB Scaffold for The Pyramid Web Framework diff --git a/docs/tutorials/wiki/src/models/setup.py b/docs/tutorials/wiki/src/models/setup.py index da79881ab..58a454f80 100644 --- a/docs/tutorials/wiki/src/models/setup.py +++ b/docs/tutorials/wiki/src/models/setup.py @@ -11,10 +11,10 @@ with open(os.path.join(here, 'CHANGES.txt')) as f: requires = [ 'pyramid', 'pyramid_chameleon', + 'pyramid_debugtoolbar', + 'pyramid_tm', 'pyramid_zodbconn', 'transaction', - 'pyramid_tm', - 'pyramid_debugtoolbar', 'ZODB3', 'waitress', ] diff --git a/docs/tutorials/wiki/src/models/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki/src/models/tutorial/templates/mytemplate.pt index 29dacef19..1b30f42b6 100644 --- a/docs/tutorials/wiki/src/models/tutorial/templates/mytemplate.pt +++ b/docs/tutorials/wiki/src/models/tutorial/templates/mytemplate.pt @@ -8,7 +8,7 @@ - Alchemy Scaffold for The Pyramid Web Framework + ZODB Scaffold for The Pyramid Web Framework diff --git a/docs/tutorials/wiki/src/tests/setup.py b/docs/tutorials/wiki/src/tests/setup.py index 2e7ed2398..b67b702cf 100644 --- a/docs/tutorials/wiki/src/tests/setup.py +++ b/docs/tutorials/wiki/src/tests/setup.py @@ -11,10 +11,10 @@ with open(os.path.join(here, 'CHANGES.txt')) as f: requires = [ 'pyramid', 'pyramid_chameleon', + 'pyramid_debugtoolbar', + 'pyramid_tm', 'pyramid_zodbconn', 'transaction', - 'pyramid_tm', - 'pyramid_debugtoolbar', 'ZODB3', 'waitress', 'docutils', diff --git a/docs/tutorials/wiki/src/tests/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki/src/tests/tutorial/templates/mytemplate.pt index 13b41f823..1b30f42b6 100644 --- a/docs/tutorials/wiki/src/tests/tutorial/templates/mytemplate.pt +++ b/docs/tutorials/wiki/src/tests/tutorial/templates/mytemplate.pt @@ -1,73 +1,66 @@ - - - - The Pyramid Web Framework - - - - - - - - - - -
-
-
-
pyramid
-
-
-
-
-

- Welcome to ${project}, an application generated by
- the Pyramid Web Framework. -

-
-
-
-
-
-

Search documentation

-
- - -
+ + + + + + + + + + + ZODB Scaffold for The Pyramid Web Framework + + + + + + + + + + + + + +
+
+
+
+ +
+
+
+

Pyramid ZODB scaffold

+

Welcome to ${project}, an application generated by
the Pyramid Web Framework.

+
+
-
-
- + + + + + + + diff --git a/docs/tutorials/wiki/src/views/setup.py b/docs/tutorials/wiki/src/views/setup.py index 5ab4f73cd..e2e96379d 100644 --- a/docs/tutorials/wiki/src/views/setup.py +++ b/docs/tutorials/wiki/src/views/setup.py @@ -11,10 +11,10 @@ with open(os.path.join(here, 'CHANGES.txt')) as f: requires = [ 'pyramid', 'pyramid_chameleon', + 'pyramid_debugtoolbar', + 'pyramid_tm', 'pyramid_zodbconn', 'transaction', - 'pyramid_tm', - 'pyramid_debugtoolbar', 'ZODB3', 'waitress', 'docutils', diff --git a/docs/tutorials/wiki/src/views/tutorial/static/favicon.ico b/docs/tutorials/wiki/src/views/tutorial/static/favicon.ico deleted file mode 100644 index 71f837c9e..000000000 Binary files a/docs/tutorials/wiki/src/views/tutorial/static/favicon.ico and /dev/null differ diff --git a/docs/tutorials/wiki/src/views/tutorial/static/footerbg.png b/docs/tutorials/wiki/src/views/tutorial/static/footerbg.png deleted file mode 100644 index 1fbc873da..000000000 Binary files a/docs/tutorials/wiki/src/views/tutorial/static/footerbg.png and /dev/null differ diff --git a/docs/tutorials/wiki/src/views/tutorial/static/headerbg.png b/docs/tutorials/wiki/src/views/tutorial/static/headerbg.png deleted file mode 100644 index 0596f2020..000000000 Binary files a/docs/tutorials/wiki/src/views/tutorial/static/headerbg.png and /dev/null differ diff --git a/docs/tutorials/wiki/src/views/tutorial/static/ie6.css b/docs/tutorials/wiki/src/views/tutorial/static/ie6.css deleted file mode 100644 index b7c8493d8..000000000 --- a/docs/tutorials/wiki/src/views/tutorial/static/ie6.css +++ /dev/null @@ -1,8 +0,0 @@ -* html img, -* html .png{position:relative;behavior:expression((this.runtimeStyle.behavior="none")&&(this.pngSet?this.pngSet=true:(this.nodeName == "IMG" && this.src.toLowerCase().indexOf('.png')>-1?(this.runtimeStyle.backgroundImage = "none", -this.runtimeStyle.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + this.src + "',sizingMethod='image')", -this.src = "static/transparent.gif"):(this.origBg = this.origBg? this.origBg :this.currentStyle.backgroundImage.toString().replace('url("','').replace('")',''), -this.runtimeStyle.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + this.origBg + "',sizingMethod='crop')", -this.runtimeStyle.backgroundImage = "none")),this.pngSet=true) -);} -#wrap{display:table;height:100%} diff --git a/docs/tutorials/wiki/src/views/tutorial/static/middlebg.png b/docs/tutorials/wiki/src/views/tutorial/static/middlebg.png deleted file mode 100644 index 2369cfb7d..000000000 Binary files a/docs/tutorials/wiki/src/views/tutorial/static/middlebg.png and /dev/null differ diff --git a/docs/tutorials/wiki/src/views/tutorial/static/pylons.css b/docs/tutorials/wiki/src/views/tutorial/static/pylons.css deleted file mode 100644 index 4b1c017cd..000000000 --- a/docs/tutorials/wiki/src/views/tutorial/static/pylons.css +++ /dev/null @@ -1,372 +0,0 @@ -html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td -{ - margin: 0; - padding: 0; - border: 0; - outline: 0; - font-size: 100%; /* 16px */ - vertical-align: baseline; - background: transparent; -} - -body -{ - line-height: 1; -} - -ol, ul -{ - list-style: none; -} - -blockquote, q -{ - quotes: none; -} - -blockquote:before, blockquote:after, q:before, q:after -{ - content: ''; - content: none; -} - -:focus -{ - outline: 0; -} - -ins -{ - text-decoration: none; -} - -del -{ - text-decoration: line-through; -} - -table -{ - border-collapse: collapse; - border-spacing: 0; -} - -sub -{ - vertical-align: sub; - font-size: smaller; - line-height: normal; -} - -sup -{ - vertical-align: super; - font-size: smaller; - line-height: normal; -} - -ul, menu, dir -{ - display: block; - list-style-type: disc; - margin: 1em 0; - padding-left: 40px; -} - -ol -{ - display: block; - list-style-type: decimal-leading-zero; - margin: 1em 0; - padding-left: 40px; -} - -li -{ - display: list-item; -} - -ul ul, ul ol, ul dir, ul menu, ul dl, ol ul, ol ol, ol dir, ol menu, ol dl, dir ul, dir ol, dir dir, dir menu, dir dl, menu ul, menu ol, menu dir, menu menu, menu dl, dl ul, dl ol, dl dir, dl menu, dl dl -{ - margin-top: 0; - margin-bottom: 0; -} - -ol ul, ul ul, menu ul, dir ul, ol menu, ul menu, menu menu, dir menu, ol dir, ul dir, menu dir, dir dir -{ - list-style-type: circle; -} - -ol ol ul, ol ul ul, ol menu ul, ol dir ul, ol ol menu, ol ul menu, ol menu menu, ol dir menu, ol ol dir, ol ul dir, ol menu dir, ol dir dir, ul ol ul, ul ul ul, ul menu ul, ul dir ul, ul ol menu, ul ul menu, ul menu menu, ul dir menu, ul ol dir, ul ul dir, ul menu dir, ul dir dir, menu ol ul, menu ul ul, menu menu ul, menu dir ul, menu ol menu, menu ul menu, menu menu menu, menu dir menu, menu ol dir, menu ul dir, menu menu dir, menu dir dir, dir ol ul, dir ul ul, dir menu ul, dir dir ul, dir ol menu, dir ul menu, dir menu menu, dir dir menu, dir ol dir, dir ul dir, dir menu dir, dir dir dir -{ - list-style-type: square; -} - -.hidden -{ - display: none; -} - -p -{ - line-height: 1.5em; -} - -h1 -{ - font-size: 1.75em; - line-height: 1.7em; - font-family: helvetica, verdana; -} - -h2 -{ - font-size: 1.5em; - line-height: 1.7em; - font-family: helvetica, verdana; -} - -h3 -{ - font-size: 1.25em; - line-height: 1.7em; - font-family: helvetica, verdana; -} - -h4 -{ - font-size: 1em; - line-height: 1.7em; - font-family: helvetica, verdana; -} - -html, body -{ - width: 100%; - height: 100%; -} - -body -{ - margin: 0; - padding: 0; - background-color: #fff; - position: relative; - font: 16px/24px NobileRegular, "Lucida Grande", Lucida, Verdana, sans-serif; -} - -a -{ - color: #1b61d6; - text-decoration: none; -} - -a:hover -{ - color: #e88f00; - text-decoration: underline; -} - -body h1, body h2, body h3, body h4, body h5, body h6 -{ - font-family: NeutonRegular, "Lucida Grande", Lucida, Verdana, sans-serif; - font-weight: 400; - color: #373839; - font-style: normal; -} - -#wrap -{ - min-height: 100%; -} - -#header, #footer -{ - width: 100%; - color: #fff; - height: 40px; - position: absolute; - text-align: center; - line-height: 40px; - overflow: hidden; - font-size: 12px; - vertical-align: middle; -} - -#header -{ - background: #000; - top: 0; - font-size: 14px; -} - -#footer -{ - bottom: 0; - background: #000 url(footerbg.png) repeat-x 0 top; - position: relative; - margin-top: -40px; - clear: both; -} - -.header, .footer -{ - width: 750px; - margin-right: auto; - margin-left: auto; -} - -.wrapper -{ - width: 100%; -} - -#top, #top-small, #bottom -{ - width: 100%; -} - -#top -{ - color: #000; - height: 230px; - background: #fff url(headerbg.png) repeat-x 0 top; - position: relative; -} - -#top-small -{ - color: #000; - height: 60px; - background: #fff url(headerbg.png) repeat-x 0 top; - position: relative; -} - -#bottom -{ - color: #222; - background-color: #fff; -} - -.top, .top-small, .middle, .bottom -{ - width: 750px; - margin-right: auto; - margin-left: auto; -} - -.top -{ - padding-top: 40px; -} - -.top-small -{ - padding-top: 10px; -} - -#middle -{ - width: 100%; - height: 100px; - background: url(middlebg.png) repeat-x; - border-top: 2px solid #fff; - border-bottom: 2px solid #b2b2b2; -} - -.app-welcome -{ - margin-top: 25px; -} - -.app-name -{ - color: #000; - font-weight: 700; -} - -.bottom -{ - padding-top: 50px; -} - -#left -{ - width: 350px; - float: left; - padding-right: 25px; -} - -#right -{ - width: 350px; - float: right; - padding-left: 25px; -} - -.align-left -{ - text-align: left; -} - -.align-right -{ - text-align: right; -} - -.align-center -{ - text-align: center; -} - -ul.links -{ - margin: 0; - padding: 0; -} - -ul.links li -{ - list-style-type: none; - font-size: 14px; -} - -form -{ - border-style: none; -} - -fieldset -{ - border-style: none; -} - -input -{ - color: #222; - border: 1px solid #ccc; - font-family: sans-serif; - font-size: 12px; - line-height: 16px; -} - -input[type=text], input[type=password] -{ - width: 205px; -} - -input[type=submit] -{ - background-color: #ddd; - font-weight: 700; -} - -/*Opera Fix*/ -body:before -{ - content: ""; - height: 100%; - float: left; - width: 0; - margin-top: -32767px; -} diff --git a/docs/tutorials/wiki/src/views/tutorial/static/pyramid-16x16.png b/docs/tutorials/wiki/src/views/tutorial/static/pyramid-16x16.png new file mode 100644 index 000000000..979203112 Binary files /dev/null and b/docs/tutorials/wiki/src/views/tutorial/static/pyramid-16x16.png differ diff --git a/docs/tutorials/wiki/src/views/tutorial/static/pyramid-small.png b/docs/tutorials/wiki/src/views/tutorial/static/pyramid-small.png deleted file mode 100644 index a5bc0ade7..000000000 Binary files a/docs/tutorials/wiki/src/views/tutorial/static/pyramid-small.png and /dev/null differ diff --git a/docs/tutorials/wiki/src/views/tutorial/static/pyramid.png b/docs/tutorials/wiki/src/views/tutorial/static/pyramid.png index 347e05549..4ab837be9 100644 Binary files a/docs/tutorials/wiki/src/views/tutorial/static/pyramid.png and b/docs/tutorials/wiki/src/views/tutorial/static/pyramid.png differ diff --git a/docs/tutorials/wiki/src/views/tutorial/static/theme.css b/docs/tutorials/wiki/src/views/tutorial/static/theme.css new file mode 100644 index 000000000..0f4b1a4d4 --- /dev/null +++ b/docs/tutorials/wiki/src/views/tutorial/static/theme.css @@ -0,0 +1,154 @@ +@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700); +body { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; + color: #ffffff; + background: #bc2131; +} +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; +} +p { + font-weight: 300; +} +.font-normal { + font-weight: 400; +} +.font-semi-bold { + font-weight: 600; +} +.font-bold { + font-weight: 700; +} +.starter-template { + margin-top: 250px; +} +.starter-template .content { + margin-left: 10px; +} +.starter-template .content h1 { + margin-top: 10px; + font-size: 60px; +} +.starter-template .content h1 .smaller { + font-size: 40px; + color: #f2b7bd; +} +.starter-template .content .lead { + font-size: 25px; + color: #f2b7bd; +} +.starter-template .content .lead .font-normal { + color: #ffffff; +} +.starter-template .links { + float: right; + right: 0; + margin-top: 125px; +} +.starter-template .links ul { + display: block; + padding: 0; + margin: 0; +} +.starter-template .links ul li { + list-style: none; + display: inline; + margin: 0 10px; +} +.starter-template .links ul li:first-child { + margin-left: 0; +} +.starter-template .links ul li:last-child { + margin-right: 0; +} +.starter-template .links ul li.current-version { + color: #f2b7bd; + font-weight: 400; +} +.starter-template .links ul li a, a { + color: #f2b7bd; + text-decoration: underline; +} +.starter-template .links ul li a:hover, a:hover { + color: #ffffff; + text-decoration: underline; +} +.starter-template .links ul li .icon-muted { + color: #eb8b95; + margin-right: 5px; +} +.starter-template .links ul li:hover .icon-muted { + color: #ffffff; +} +.starter-template .copyright { + margin-top: 10px; + font-size: 0.9em; + color: #f2b7bd; + text-transform: lowercase; + float: right; + right: 0; +} +@media (max-width: 1199px) { + .starter-template .content h1 { + font-size: 45px; + } + .starter-template .content h1 .smaller { + font-size: 30px; + } + .starter-template .content .lead { + font-size: 20px; + } +} +@media (max-width: 991px) { + .starter-template { + margin-top: 0; + } + .starter-template .logo { + margin: 40px auto; + } + .starter-template .content { + margin-left: 0; + text-align: center; + } + .starter-template .content h1 { + margin-bottom: 20px; + } + .starter-template .links { + float: none; + text-align: center; + margin-top: 60px; + } + .starter-template .copyright { + float: none; + text-align: center; + } +} +@media (max-width: 767px) { + .starter-template .content h1 .smaller { + font-size: 25px; + display: block; + } + .starter-template .content .lead { + font-size: 16px; + } + .starter-template .links { + margin-top: 40px; + } + .starter-template .links ul li { + display: block; + margin: 0; + } + .starter-template .links ul li .icon-muted { + display: none; + } + .starter-template .copyright { + margin-top: 20px; + } +} diff --git a/docs/tutorials/wiki/src/views/tutorial/static/theme.min.css b/docs/tutorials/wiki/src/views/tutorial/static/theme.min.css new file mode 100644 index 000000000..2f924bcc5 --- /dev/null +++ b/docs/tutorials/wiki/src/views/tutorial/static/theme.min.css @@ -0,0 +1 @@ +@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);body{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300;color:#fff;background:#bc2131}h1,h2,h3,h4,h5,h6{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300}p{font-weight:300}.font-normal{font-weight:400}.font-semi-bold{font-weight:600}.font-bold{font-weight:700}.starter-template{margin-top:250px}.starter-template .content{margin-left:10px}.starter-template .content h1{margin-top:10px;font-size:60px}.starter-template .content h1 .smaller{font-size:40px;color:#f2b7bd}.starter-template .content .lead{font-size:25px;color:#f2b7bd}.starter-template .content .lead .font-normal{color:#fff}.starter-template .links{float:right;right:0;margin-top:125px}.starter-template .links ul{display:block;padding:0;margin:0}.starter-template .links ul li{list-style:none;display:inline;margin:0 10px}.starter-template .links ul li:first-child{margin-left:0}.starter-template .links ul li:last-child{margin-right:0}.starter-template .links ul li.current-version{color:#f2b7bd;font-weight:400}.starter-template .links ul li a{color:#fff}.starter-template .links ul li a:hover{text-decoration:underline}.starter-template .links ul li .icon-muted{color:#eb8b95;margin-right:5px}.starter-template .links ul li:hover .icon-muted{color:#fff}.starter-template .copyright{margin-top:10px;font-size:.9em;color:#f2b7bd;text-transform:lowercase;float:right;right:0}@media (max-width:1199px){.starter-template .content h1{font-size:45px}.starter-template .content h1 .smaller{font-size:30px}.starter-template .content .lead{font-size:20px}}@media (max-width:991px){.starter-template{margin-top:0}.starter-template .logo{margin:40px auto}.starter-template .content{margin-left:0;text-align:center}.starter-template .content h1{margin-bottom:20px}.starter-template .links{float:none;text-align:center;margin-top:60px}.starter-template .copyright{float:none;text-align:center}}@media (max-width:767px){.starter-template .content h1 .smaller{font-size:25px;display:block}.starter-template .content .lead{font-size:16px}.starter-template .links{margin-top:40px}.starter-template .links ul li{display:block;margin:0}.starter-template .links ul li .icon-muted{display:none}.starter-template .copyright{margin-top:20px}} \ No newline at end of file diff --git a/docs/tutorials/wiki/src/views/tutorial/static/transparent.gif b/docs/tutorials/wiki/src/views/tutorial/static/transparent.gif deleted file mode 100644 index 0341802e5..000000000 Binary files a/docs/tutorials/wiki/src/views/tutorial/static/transparent.gif and /dev/null differ diff --git a/docs/tutorials/wiki/src/views/tutorial/templates/edit.pt b/docs/tutorials/wiki/src/views/tutorial/templates/edit.pt index 24ed2e592..b23f45d56 100644 --- a/docs/tutorials/wiki/src/views/tutorial/templates/edit.pt +++ b/docs/tutorials/wiki/src/views/tutorial/templates/edit.pt @@ -1,58 +1,69 @@ - - - - ${page.__name__} - Pyramid tutorial wiki (based on +<!DOCTYPE html> +<html lang="${request.locale_name}"> + <head> + <meta charset="utf-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <meta name="description" content="pyramid web application"> + <meta name="author" content="Pylons Project"> + <link rel="shortcut icon" href="${request.static_url('tutorial:static/pyramid-16x16.png')}"> + + <title>${page.__name__} - Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki) - - - - - - - - -
-
-
-
- pyramid + + + + + + + + + + + + +
+
+
+
+ +
+
+
+

+ Editing + Page Name Goes Here +

+

You can return to the + FrontPage. +

+
+
+ +
+
+ +
+
+
+
-
-
-
-
-
- Editing Page Name Goes - Here
- You can return to the - FrontPage.
+
+
- -
-
-
-
-
- +
+
+ +
+ +
+
-
-
-
-
-
- Editing Page Name - Goes Here
- You can return to the - FrontPage.
-
-
-
-
-
- +
+
+ +
+ +
+
-
-
-
-
-
- Editing Page Name - Goes Here
- You can return to the - FrontPage.
-
-
-
-
-
- -
-
- -
- -
-
-
-
- -
-
-
- - - - - - - - diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki2/src/views/tutorial/templates/mytemplate.pt deleted file mode 100644 index c9b0cec21..000000000 --- a/docs/tutorials/wiki2/src/views/tutorial/templates/mytemplate.pt +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - - - - - Alchemy Scaffold for The Pyramid Web Framework - - - - - - - - - - - - - -
-
-
-
- -
-
-
-

Pyramid Alchemy scaffold

-

Welcome to ${project}, an application generated by
the Pyramid Web Framework.

-
-
-
- -
- -
-
-
- - - - - - - - diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/view.pt b/docs/tutorials/wiki2/src/views/tutorial/templates/view.pt deleted file mode 100644 index 0f564b16c..000000000 --- a/docs/tutorials/wiki2/src/views/tutorial/templates/view.pt +++ /dev/null @@ -1,69 +0,0 @@ - - - - - - - - - - - ${page.name} - Pyramid tutorial wiki (based on - TurboGears 20-Minute Wiki) - - - - - - - - - - - - -
-
-
-
- -
-
-
-
- Page text goes here. -
-

- - Edit this page - -

-

- Viewing - Page Name Goes Here -

-

You can return to the - FrontPage. -

-
-
-
-
- -
-
-
- - - - - - - - diff --git a/docs/tutorials/wiki2/src/views/tutorial/tests.py b/docs/tutorials/wiki2/src/views/tutorial/tests.py index 9f01d2da5..b947e3bb1 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/tests.py +++ b/docs/tutorials/wiki2/src/views/tutorial/tests.py @@ -3,144 +3,63 @@ import transaction from pyramid import testing -def _initTestingDB(): - from sqlalchemy import create_engine - from tutorial.models import ( - DBSession, - Page, - Base - ) - engine = create_engine('sqlite://') - Base.metadata.create_all(engine) - DBSession.configure(bind=engine) - with transaction.manager: - model = Page(name='FrontPage', data='This is the front page') - DBSession.add(model) - return DBSession - -def _registerRoutes(config): - config.add_route('view_page', '{pagename}') - config.add_route('edit_page', '{pagename}/edit_page') - config.add_route('add_page', 'add_page/{pagename}') - -class ViewWikiTests(unittest.TestCase): + +def dummy_request(dbsession): + return testing.DummyRequest(dbsession=dbsession) + + +class BaseTest(unittest.TestCase): def setUp(self): - self.config = testing.setUp() - self.session = _initTestingDB() + self.config = testing.setUp(settings={ + 'sqlalchemy.url': 'sqlite:///:memory:' + }) + self.config.include('.models.meta') + settings = self.config.get_settings() - def tearDown(self): - self.session.remove() - testing.tearDown() + from .models.meta import ( + get_session, + get_engine, + get_dbmaker, + ) - def _callFUT(self, request): - from tutorial.views import view_wiki - return view_wiki(request) + self.engine = get_engine(settings) + dbmaker = get_dbmaker(self.engine) - def test_it(self): - _registerRoutes(self.config) - request = testing.DummyRequest() - response = self._callFUT(request) - self.assertEqual(response.location, 'http://example.com/FrontPage') + self.session = get_session(transaction.manager, dbmaker) -class ViewPageTests(unittest.TestCase): - def setUp(self): - self.session = _initTestingDB() - self.config = testing.setUp() + def init_database(self): + from .models.meta import Base + Base.metadata.create_all(self.engine) def tearDown(self): - self.session.remove() - testing.tearDown() - - def _callFUT(self, request): - from tutorial.views import view_page - return view_page(request) - - def test_it(self): - from tutorial.models import Page - request = testing.DummyRequest() - request.matchdict['pagename'] = 'IDoExist' - page = Page(name='IDoExist', data='Hello CruelWorld IDoExist') - self.session.add(page) - _registerRoutes(self.config) - info = self._callFUT(request) - self.assertEqual(info['page'], page) - self.assertEqual( - info['content'], - '
\n' - '

Hello ' - 'CruelWorld ' - '' - 'IDoExist' - '

\n
\n') - self.assertEqual(info['edit_url'], - 'http://example.com/IDoExist/edit_page') - - -class AddPageTests(unittest.TestCase): - def setUp(self): - self.session = _initTestingDB() - self.config = testing.setUp() + from .models.meta import Base - def tearDown(self): - self.session.remove() testing.tearDown() + transaction.abort() + Base.metadata.create_all(self.engine) + + +class TestMyViewSuccessCondition(BaseTest): - def _callFUT(self, request): - from tutorial.views import add_page - return add_page(request) - - def test_it_notsubmitted(self): - _registerRoutes(self.config) - request = testing.DummyRequest() - request.matchdict = {'pagename':'AnotherPage'} - info = self._callFUT(request) - self.assertEqual(info['page'].data,'') - self.assertEqual(info['save_url'], - 'http://example.com/add_page/AnotherPage') - - def test_it_submitted(self): - from tutorial.models import Page - _registerRoutes(self.config) - request = testing.DummyRequest({'form.submitted':True, - 'body':'Hello yo!'}) - request.matchdict = {'pagename':'AnotherPage'} - self._callFUT(request) - page = self.session.query(Page).filter_by(name='AnotherPage').one() - self.assertEqual(page.data, 'Hello yo!') - -class EditPageTests(unittest.TestCase): def setUp(self): - self.session = _initTestingDB() - self.config = testing.setUp() + super(TestMyViewSuccessCondition, self).setUp() + self.init_database() - def tearDown(self): - self.session.remove() - testing.tearDown() + from .models.mymodel import MyModel + + model = MyModel(name='one', value=55) + self.session.add(model) + + def test_passing_view(self): + from .views.default import my_view + info = my_view(dummy_request(self.session)) + self.assertEqual(info['one'].name, 'one') + self.assertEqual(info['project'], 'tutorial') + + +class TestMyViewFailureCondition(BaseTest): - def _callFUT(self, request): - from tutorial.views import edit_page - return edit_page(request) - - def test_it_notsubmitted(self): - from tutorial.models import Page - _registerRoutes(self.config) - request = testing.DummyRequest() - request.matchdict = {'pagename':'abc'} - page = Page(name='abc', data='hello') - self.session.add(page) - info = self._callFUT(request) - self.assertEqual(info['page'], page) - self.assertEqual(info['save_url'], - 'http://example.com/abc/edit_page') - - def test_it_submitted(self): - from tutorial.models import Page - _registerRoutes(self.config) - request = testing.DummyRequest({'form.submitted':True, - 'body':'Hello yo!'}) - request.matchdict = {'pagename':'abc'} - page = Page(name='abc', data='hello') - self.session.add(page) - response = self._callFUT(request) - self.assertEqual(response.location, 'http://example.com/abc') - self.assertEqual(page.data, 'Hello yo!') + def test_failing_view(self): + from .views.default import my_view + info = my_view(dummy_request(self.session)) + self.assertEqual(info.status_int, 500) diff --git a/docs/tutorials/wiki2/src/views/tutorial/views.py b/docs/tutorials/wiki2/src/views/tutorial/views.py deleted file mode 100644 index a3707dab5..000000000 --- a/docs/tutorials/wiki2/src/views/tutorial/views.py +++ /dev/null @@ -1,72 +0,0 @@ -import cgi -import re -from docutils.core import publish_parts - -from pyramid.httpexceptions import ( - HTTPFound, - HTTPNotFound, - ) - -from pyramid.view import view_config - -from .models import ( - DBSession, - Page, - ) - -# regular expression used to find WikiWords -wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)") - -@view_config(route_name='view_wiki') -def view_wiki(request): - return HTTPFound(location = request.route_url('view_page', - pagename='FrontPage')) - -@view_config(route_name='view_page', renderer='templates/view.pt') -def view_page(request): - pagename = request.matchdict['pagename'] - 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 = DBSession.query(Page).filter_by(name=word).all() - if exists: - view_url = request.route_url('view_page', pagename=word) - return '%s' % (view_url, cgi.escape(word)) - else: - add_url = request.route_url('add_page', pagename=word) - return '%s' % (add_url, cgi.escape(word)) - - content = publish_parts(page.data, writer_name='html')['html_body'] - content = wikiwords.sub(check, content) - edit_url = request.route_url('edit_page', pagename=pagename) - return dict(page=page, content=content, edit_url=edit_url) - -@view_config(route_name='add_page', renderer='templates/edit.pt') -def add_page(request): - pagename = request.matchdict['pagename'] - if 'form.submitted' in request.params: - body = request.params['body'] - page = Page(name=pagename, data=body) - DBSession.add(page) - return HTTPFound(location = request.route_url('view_page', - pagename=pagename)) - save_url = request.route_url('add_page', pagename=pagename) - page = Page(name='', data='') - return dict(page=page, save_url=save_url) - -@view_config(route_name='edit_page', renderer='templates/edit.pt') -def edit_page(request): - pagename = request.matchdict['pagename'] - page = DBSession.query(Page).filter_by(name=pagename).one() - if 'form.submitted' in request.params: - page.data = request.params['body'] - DBSession.add(page) - return HTTPFound(location = request.route_url('view_page', - pagename=pagename)) - return dict( - page=page, - save_url = request.route_url('edit_page', pagename=pagename), - ) -- cgit v1.2.3 From 429529a09992554229ef7274bfc4f43394256105 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Fri, 13 Nov 2015 03:46:13 -0800 Subject: update wiki2/src/views and wiki2/definingviews.rst --- docs/tutorials/wiki2/definingviews.rst | 113 +++++++++++++++++---------------- 1 file changed, 58 insertions(+), 55 deletions(-) diff --git a/docs/tutorials/wiki2/definingviews.rst b/docs/tutorials/wiki2/definingviews.rst index 8a96e433c..08fa8f16b 100644 --- a/docs/tutorials/wiki2/definingviews.rst +++ b/docs/tutorials/wiki2/definingviews.rst @@ -64,8 +64,8 @@ like this:: Finished processing dependencies for tutorial==0.0 -Adding view functions in ``views.py`` -===================================== +Adding view functions in ``views/default.py`` +============================================= It's time for a major change. Open ``tutorial/tutorial/views/default.py`` and edit it to look like the following: @@ -73,18 +73,19 @@ edit it to look like the following: .. literalinclude:: src/views/tutorial/views/default.py :linenos: :language: python - :emphasize-lines: 1-7,14,16-72 + :emphasize-lines: 1-9,12-70 The highlighted lines need to be added or edited. -We added some imports and created a regular expression to find "WikiWords". +We added some imports, and created a regular expression to find "WikiWords". We got rid of the ``my_view`` view function and its decorator that was added when we originally rendered the ``alchemy`` scaffold. It was only an example -and isn't relevant to our application. +and isn't relevant to our application. We also delated the ``db_err_msg`` +string. -Then we added four :term:`view callable` functions to our ``views.py`` -module: +Then we added four :term:`view callable` functions to our ``views/default.py`` +module: * ``view_wiki()`` - Displays the wiki itself. It will answer on the root URL. * ``view_page()`` - Displays an individual page. @@ -95,20 +96,20 @@ We'll describe each one briefly in the following sections. .. note:: - There is nothing special about the filename ``views.py``. A project may - have many view callables throughout its codebase in arbitrarily named - files. Files implementing view callables often have ``view`` in their - filenames (or may live in a Python subpackage of your application package - named ``views``), but this is only by convention. + There is nothing special about the filename ``default.py``. A project may + have many view callables throughout its codebase in arbitrarily named files. + Files implementing view callables often have ``view`` in their filenames (or + may live in a Python subpackage of your application package named ``views``, + as in our case), but this is only by convention. The ``view_wiki`` view function ------------------------------- Following is the code for the ``view_wiki`` view function and its decorator: -.. literalinclude:: src/views/tutorial/views.py - :lines: 20-24 - :lineno-start: 20 +.. literalinclude:: src/views/tutorial/views/default.py + :lines: 17-20 + :lineno-start: 17 :linenos: :language: python @@ -130,9 +131,9 @@ The ``view_page`` view function Here is the code for the ``view_page`` view function and its decorator: -.. literalinclude:: src/views/tutorial/views.py - :lines: 25-45 - :lineno-start: 25 +.. literalinclude:: src/views/tutorial/views/default.py + :lines: 22-42 + :lineno-start: 22 :linenos: :language: python @@ -158,17 +159,17 @@ template, and we return a dictionary with a number of arguments. The fact that ``view_page()`` returns a dictionary (as opposed to a :term:`response` object) is a cue to :app:`Pyramid` that it should try to use a :term:`renderer` associated with the view configuration to render a response. In our case, the -renderer used will be the ``templates/view.pt`` template, as indicated in the -``@view_config`` decorator that is applied to ``view_page()``. +renderer used will be the ``templates/view.jinja2`` template, as indicated in +the ``@view_config`` decorator that is applied to ``view_page()``. The ``add_page`` view function ------------------------------ Here is the code for the ``add_page`` view function and its decorator: -.. literalinclude:: src/views/tutorial/views.py - :lines: 47-58 - :lineno-start: 47 +.. literalinclude:: src/views/tutorial/views/default.py + :lines: 44-55 + :lineno-start: 44 :linenos: :language: python @@ -189,15 +190,15 @@ If the view execution *is* a result of a form submission (i.e., the expression ``'form.submitted' in request.params`` is ``True``), we grab the page body from the form data, create a Page object with this page body and the name taken from ``matchdict['pagename']``, and save it into the database using -``DBSession.add``. We then redirect back to the ``view_page`` view for the -newly created page. +``request.dbession.add``. We then redirect back to the ``view_page`` view for +the newly created page. If the view execution is *not* a result of a form submission (i.e., the expression ``'form.submitted' in request.params`` is ``False``), the view callable renders a template. To do so, it generates a ``save_url`` which the template uses as the form post URL during rendering. We're lazy here, so -we're going to use the same template (``templates/edit.pt``) for the add -view as well as the page edit view. To do so we create a dummy Page object +we're going to use the same template (``templates/edit.jinja2``) for the add +view as well as the page edit view. To do so we create a dummy ``Page`` object in order to satisfy the edit form's desire to have *some* page object exposed as ``page``. :app:`Pyramid` will render the template associated with this view to a response. @@ -207,14 +208,14 @@ The ``edit_page`` view function Here is the code for the ``edit_page`` view function and its decorator: -.. literalinclude:: src/views/tutorial/views.py - :lines: 60-72 - :lineno-start: 60 +.. literalinclude:: src/views/tutorial/views/default.py + :lines: 57-69 + :lineno-start: 57 :linenos: :language: python ``edit_page()`` is invoked when a user clicks the "Edit this -Page" button on the view form. It renders an edit form but it also acts as +Page" button on the view form. It renders an edit form, but it also acts as the handler for the form it renders. The ``matchdict`` attribute of the request passed to the ``edit_page`` view will have a ``'pagename'`` key matching the name of the page the user wants to edit. @@ -234,49 +235,51 @@ Adding templates ================ The ``view_page``, ``add_page`` and ``edit_page`` views that we've added -reference a :term:`template`. Each template is a :term:`Chameleon` -:term:`ZPT` template. These templates will live in the ``templates`` -directory of our tutorial package. Chameleon templates must have a ``.pt`` -extension to be recognized as such. +reference a :term:`template`. Each template is a :term:`Jinja2` template. +These templates will live in the ``templates`` directory of our tutorial +package. Jinja2 templates must have a ``.jinja2`` extension to be recognized +as such. -The ``view.pt`` template ------------------------- +The ``view.jinja2`` template +---------------------------- -Create ``tutorial/tutorial/templates/view.pt`` and add the following +Create ``tutorial/tutorial/templates/view.jinja2`` and add the following content: -.. literalinclude:: src/views/tutorial/templates/view.pt +.. literalinclude:: src/views/tutorial/templates/view.jinja2 :linenos: + :emphasize-lines: 36,38-40 :language: html This template is used by ``view_page()`` for displaying a single wiki page. It includes: -- A ``div`` element that is replaced with the ``content`` value provided by - the view (lines 36-38). ``content`` contains HTML, so the ``structure`` - keyword is used to prevent escaping it (i.e., changing ">" to ">", etc.) -- A link that points at the "edit" URL which invokes the ``edit_page`` view - for the page being viewed (lines 40-42). +- A variable that is replaced with the ``content`` value provided by the view + (line 36). ``content`` contains HTML, so the ``|safe`` filter is used to + prevent escaping it (e.g., changing ">" to ">"). +- A link that points at the "edit" URL which invokes the ``edit_page`` view for + the page being viewed (lines 38-40). -The ``edit.pt`` template ------------------------- +The ``edit.jinja2`` template +---------------------------- -Create ``tutorial/tutorial/templates/edit.pt`` and add the following +Create ``tutorial/tutorial/templates/edit.jinja2`` and add the following content: -.. literalinclude:: src/views/tutorial/templates/edit.pt +.. literalinclude:: src/views/tutorial/templates/edit.jinja2 :linenos: + :emphasize-lines: 42,44,47 :language: html This template is used by ``add_page()`` and ``edit_page()`` for adding and editing a wiki page. It displays a page containing a form that includes: -- A 10 row by 60 column ``textarea`` field named ``body`` that is filled - with any existing page data when it is rendered (line 45). -- A submit button that has the name ``form.submitted`` (line 48). +- A 10-row by 60-column ``textarea`` field named ``body`` that is filled with + any existing page data when it is rendered (line 44). +- A submit button that has the name ``form.submitted`` (line 47). The form POSTs back to the ``save_url`` argument supplied by the view (line -43). The view will use the ``body`` and ``form.submitted`` values. +42). The view will use the ``body`` and ``form.submitted`` values. .. note:: Our templates use a ``request`` object that none of our tutorial views return in their dictionary. ``request`` is one of several names that @@ -284,7 +287,7 @@ The form POSTs back to the ``save_url`` argument supplied by the view (line See :ref:`renderer_system_values` for information about other names that are available by default when a template is used as a renderer. -Static Assets +Static assets ------------- Our templates name static assets, including CSS and images. We don't need @@ -299,7 +302,7 @@ subdirectories) and are just referred to by URL or by using the convenience method ``static_url``, e.g., ``request.static_url(':static/foo.css')`` within templates. -Adding Routes to ``__init__.py`` +Adding routes to ``__init__.py`` ================================ The ``__init__.py`` file contains @@ -340,7 +343,7 @@ something like: .. literalinclude:: src/views/tutorial/__init__.py :linenos: - :emphasize-lines: 19-22 + :emphasize-lines: 11-14 :language: python The highlighted lines are the ones that need to be added or edited. -- cgit v1.2.3 From 3525fa106fe2ca4b4bed93e28f72b13e3fa943b3 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Fri, 13 Nov 2015 03:54:14 -0800 Subject: correct line numbers in and remove obsolete WIP block wiki2/basiclayout.rst --- docs/tutorials/wiki2/basiclayout.rst | 45 ++++++++++-------------------------- 1 file changed, 12 insertions(+), 33 deletions(-) diff --git a/docs/tutorials/wiki2/basiclayout.rst b/docs/tutorials/wiki2/basiclayout.rst index abe9e4202..1ba7d3958 100644 --- a/docs/tutorials/wiki2/basiclayout.rst +++ b/docs/tutorials/wiki2/basiclayout.rst @@ -36,6 +36,7 @@ the ``main`` function we've defined in our ``__init__.py``: .. literalinclude:: src/basiclayout/tutorial/__init__.py :pyobject: main + :lineno-start: 4 :linenos: :language: py @@ -47,6 +48,7 @@ The next step of ``main`` is to construct a :term:`Configurator` object: .. literalinclude:: src/basiclayout/tutorial/__init__.py :lines: 7 + :lineno-start: 7 :language: py ``settings`` is passed to the Configurator as a keyword argument with the @@ -60,6 +62,7 @@ with the ``.jinja2`` extension within our project. .. literalinclude:: src/basiclayout/tutorial/__init__.py :lines: 8 + :lineno-start: 8 :language: py Next include the module ``meta`` from the package ``models`` using a dotted @@ -67,6 +70,7 @@ Python path. .. literalinclude:: src/basiclayout/tutorial/__init__.py :lines: 9 + :lineno-start: 9 :language: py ``main`` now calls :meth:`pyramid.config.Configurator.add_static_view` with @@ -74,6 +78,7 @@ two arguments: ``static`` (the name), and ``static`` (the path): .. literalinclude:: src/basiclayout/tutorial/__init__.py :lines: 10 + :lineno-start: 10 :language: py This registers a static resource view which will match any URL that starts @@ -92,6 +97,7 @@ used when the URL is ``/``: .. literalinclude:: src/basiclayout/tutorial/__init__.py :lines: 11 + :lineno-start: 11 :language: py Since this route has a ``pattern`` equaling ``/``, it is the route that will @@ -106,6 +112,7 @@ application URLs to be mapped to some code. .. literalinclude:: src/basiclayout/tutorial/__init__.py :lines: 12 + :lineno-start: 12 :language: py Finally ``main`` is finished configuring things, so it uses the @@ -114,6 +121,7 @@ Finally ``main`` is finished configuring things, so it uses the .. literalinclude:: src/basiclayout/tutorial/__init__.py :lines: 13 + :lineno-start: 13 :language: py @@ -166,39 +174,6 @@ to inform the user about possible actions to take to solve the problem. Content models with the ``models`` package ------------------------------------------ -.. START moved from Application configuration with ``__init__.py``. This - section is a WIP, and needs to be updated using the new models package. - -The main function first creates a :term:`SQLAlchemy` database engine using -:func:`sqlalchemy.engine_from_config` from the ``sqlalchemy.`` prefixed -settings in the ``development.ini`` file's ``[app:main]`` section. -This will be a URI (something like ``sqlite://``): - - .. literalinclude:: src/basiclayout/tutorial/__init__.py - :lines: 13 - :language: py - -``main`` then initializes our SQLAlchemy session object, passing it the -engine: - - .. literalinclude:: src/basiclayout/tutorial/__init__.py - :lines: 14 - :language: py - -``main`` subsequently initializes our SQLAlchemy declarative ``Base`` object, -assigning the engine we created to the ``bind`` attribute of it's -``metadata`` object. This allows table definitions done imperatively -(instead of declaratively, via a class statement) to work. We won't use any -such tables in our application, but if you add one later, long after you've -forgotten about this tutorial, you won't be left scratching your head when it -doesn't work. - - .. literalinclude:: src/basiclayout/tutorial/__init__.py - :lines: 15 - :language: py - -.. END moved from Application configuration with ``__init__.py`` - In a SQLAlchemy-based application, a *model* object is an object composed by querying the SQL database. The ``models`` package is where the ``alchemy`` scaffold put the classes that implement our models. @@ -246,6 +221,7 @@ configures various database settings by calling subsequently defined functions. .. literalinclude:: src/basiclayout/tutorial/models/meta.py :pyobject: includeme + :lineno-start: 19 :linenos: :language: py @@ -256,6 +232,7 @@ unless an exception is raised, in which case the transaction will be aborted. .. literalinclude:: src/basiclayout/tutorial/models/meta.py :pyobject: get_session + :lineno-start: 32 :linenos: :language: py @@ -266,6 +243,7 @@ URI, something like ``sqlite://``. .. literalinclude:: src/basiclayout/tutorial/models/meta.py :pyobject: get_engine + :lineno-start: 39 :linenos: :language: py @@ -276,6 +254,7 @@ creating a session with the database engine. .. literalinclude:: src/basiclayout/tutorial/models/meta.py :pyobject: get_dbmaker + :lineno-start: 43 :linenos: :language: py -- cgit v1.2.3 From 93e886be234fd187c4ddc5e376fd6c51060500a7 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 13 Nov 2015 12:51:00 -0600 Subject: print out every file that has a syntax error --- pyramid/scripts/pserve.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/pyramid/scripts/pserve.py b/pyramid/scripts/pserve.py index 4a6e917a7..1e580d897 100644 --- a/pyramid/scripts/pserve.py +++ b/pyramid/scripts/pserve.py @@ -18,6 +18,7 @@ import py_compile import re import subprocess import sys +import tempfile import textwrap import threading import time @@ -847,8 +848,16 @@ class Monitor(object): # pragma: no cover self.syntax_error_files = set() self.pending_reload = False self.file_callbacks = list(self.global_file_callbacks) + temp_pyc_fp = tempfile.NamedTemporaryFile(delete=False) + self.temp_pyc = temp_pyc_fp.name + temp_pyc_fp.close() def _exit(self): + try: + os.unlink(self.temp_pyc) + except IOError: + # not worried if the tempfile can't be removed + pass # use os._exit() here and not sys.exit() since within a # thread sys.exit() just closes the given thread and # won't kill the process; note os._exit does not call @@ -879,6 +888,7 @@ class Monitor(object): # pragma: no cover continue if filename is not None: filenames.append(filename) + new_changes = False for filename in filenames: try: stat = os.stat(filename) @@ -896,7 +906,7 @@ class Monitor(object): # pragma: no cover old_mtime = self.module_mtimes.get(filename) self.module_mtimes[filename] = mtime if old_mtime is not None and old_mtime < mtime: - self.pending_reload = True + new_changes = True if pyc: filename = filename[:-1] is_valid = True @@ -904,6 +914,11 @@ class Monitor(object): # pragma: no cover is_valid = self.check_syntax(filename) if is_valid: print("%s changed ..." % filename) + if new_changes: + self.pending_reload = True + if self.syntax_error_files: + for filename in sorted(self.syntax_error_files): + print("%s has a SyntaxError; NOT reloading." % filename) if self.pending_reload and not self.syntax_error_files: self.pending_reload = False return False @@ -913,9 +928,9 @@ class Monitor(object): # pragma: no cover # check if a file has syntax errors. # If so, track it until it's fixed. try: - py_compile.compile(filename, doraise=True) - except py_compile.PyCompileError: - print("%s has a SyntaxError; NOT reloading." % filename) + py_compile.compile(filename, cfile=self.temp_pyc, doraise=True) + except py_compile.PyCompileError as ex: + print(ex.msg) self.syntax_error_files.add(filename) return False else: -- cgit v1.2.3 From 9a2761e8739781dde57bbf607926a0c227d9b40f Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 13 Nov 2015 13:34:38 -0600 Subject: add changelog for #2044 --- CHANGES.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 739eb870d..9ff26420b 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -155,6 +155,9 @@ Features docstrings instead of the default ``str(obj)`` when possible. See https://github.com/Pylons/pyramid/pull/1929 +- ``pserve --reload`` will no longer crash on syntax errors!!! + See https://github.com/Pylons/pyramid/pull/2044 + Bug Fixes --------- -- cgit v1.2.3 From 031b5b69f4ae3e2187e3b9f1f7e582d71fac45b1 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 13 Nov 2015 15:09:43 -0600 Subject: daemon option may not be set on windows --- pyramid/scripts/pserve.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyramid/scripts/pserve.py b/pyramid/scripts/pserve.py index b7d4359df..efad0cc68 100644 --- a/pyramid/scripts/pserve.py +++ b/pyramid/scripts/pserve.py @@ -229,7 +229,10 @@ class PServeCommand(object): cmd = None if self.options.reload: - if self.options.daemon or cmd in ('start', 'stop', 'restart'): + if ( + getattr(self.options, 'daemon', False) or + cmd in ('start', 'stop', 'restart') + ): self.out( 'Error: Cannot use reloading while running as a dameon.') return 2 -- cgit v1.2.3 From 63163f7bd71fba313bdab162201cb00a8dcc7c9b Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Fri, 13 Nov 2015 13:31:40 -0800 Subject: minor grammar, .rst syntax, rewrap 79 cols --- docs/narr/upgrading.rst | 163 +++++++++++++++++++++++------------------------- 1 file changed, 79 insertions(+), 84 deletions(-) diff --git a/docs/narr/upgrading.rst b/docs/narr/upgrading.rst index eb3194a65..db9b5e090 100644 --- a/docs/narr/upgrading.rst +++ b/docs/narr/upgrading.rst @@ -12,26 +12,26 @@ applications keep working when you upgrade the Pyramid version you're using. .. sidebar:: About Release Numbering Conventionally, application version numbering in Python is described as - ``major.minor.micro``. If your Pyramid version is "1.2.3", it means - you're running a version of Pyramid with the major version "1", the minor - version "2" and the micro version "3". A "major" release is one that - increments the first-dot number; 2.X.X might follow 1.X.X. A "minor" - release is one that increments the second-dot number; 1.3.X might follow - 1.2.X. A "micro" release is one that increments the third-dot number; - 1.2.3 might follow 1.2.2. In general, micro releases are "bugfix-only", - and contain no new features, minor releases contain new features but are - largely backwards compatible with older versions, and a major release - indicates a large set of backwards incompatibilities. + ``major.minor.micro``. If your Pyramid version is "1.2.3", it means you're + running a version of Pyramid with the major version "1", the minor version + "2" and the micro version "3". A "major" release is one that increments the + first-dot number; 2.X.X might follow 1.X.X. A "minor" release is one that + increments the second-dot number; 1.3.X might follow 1.2.X. A "micro" + release is one that increments the third-dot number; 1.2.3 might follow + 1.2.2. In general, micro releases are "bugfix-only", and contain no new + features, minor releases contain new features but are largely backwards + compatible with older versions, and a major release indicates a large set of + backwards incompatibilities. The Pyramid core team is conservative when it comes to removing features. We -don't remove features unnecessarily, but we're human, and we make mistakes -which cause some features to be evolutionary dead ends. Though we are -willing to support dead-end features for some amount of time, some eventually -have to be removed when the cost of supporting them outweighs the benefit of -keeping them around, because each feature in Pyramid represents a certain -documentation and maintenance burden. - -Deprecation and Removal Policy +don't remove features unnecessarily, but we're human and we make mistakes which +cause some features to be evolutionary dead ends. Though we are willing to +support dead-end features for some amount of time, some eventually have to be +removed when the cost of supporting them outweighs the benefit of keeping them +around, because each feature in Pyramid represents a certain documentation and +maintenance burden. + +Deprecation and removal policy ------------------------------ When a feature is scheduled for removal from Pyramid or any of its official @@ -51,50 +51,49 @@ When a deprecated feature is eventually removed: - A note is added to the :ref:`changelog` about the removal. -Features are never removed in *micro* releases. They are only removed in -minor and major releases. Deprecated features are kept around for at least -*three* minor releases from the time the feature became deprecated. -Therefore, if a feature is added in Pyramid 1.0, but it's deprecated in -Pyramid 1.1, it will be kept around through all 1.1.X releases, all 1.2.X -releases and all 1.3.X releases. It will finally be removed in the first -1.4.X release. - -Sometimes features are "docs-deprecated" instead of formally deprecated. -This means that the feature will be kept around indefinitely, but it will be -removed from the documentation or a note will be added to the documentation -telling folks to use some other newer feature. This happens when the cost of -keeping an old feature around is very minimal and the support and -documentation burden is very low. For example, we might rename a function -that is an API without changing the arguments it accepts. In this case, -we'll often rename the function, and change the docs to point at the new -function name, but leave around a backwards compatibility alias to the old -function name so older code doesn't break. +Features are never removed in *micro* releases. They are only removed in minor +and major releases. Deprecated features are kept around for at least *three* +minor releases from the time the feature became deprecated. Therefore, if a +feature is added in Pyramid 1.0, but it's deprecated in Pyramid 1.1, it will be +kept around through all 1.1.X releases, all 1.2.X releases and all 1.3.X +releases. It will finally be removed in the first 1.4.X release. + +Sometimes features are "docs-deprecated" instead of formally deprecated. This +means that the feature will be kept around indefinitely, but it will be removed +from the documentation or a note will be added to the documentation telling +folks to use some other newer feature. This happens when the cost of keeping +an old feature around is very minimal and the support and documentation burden +is very low. For example, we might rename a function that is an API without +changing the arguments it accepts. In this case, we'll often rename the +function, and change the docs to point at the new function name, but leave +around a backwards compatibility alias to the old function name so older code +doesn't break. "Docs deprecated" features tend to work "forever", meaning that they won't be removed, and they'll never generate a deprecation warning. However, such changes are noted in the :ref:`changelog`, so it's possible to know that you -should change older spellings to newer ones to ensure that people reading -your code can find the APIs you're using in the Pyramid docs. +should change older spellings to newer ones to ensure that people reading your +code can find the APIs you're using in the Pyramid docs. -Consulting the Change History +Consulting the change history ----------------------------- -Your first line of defense against application failures caused by upgrading -to a newer Pyramid release is always to read the :ref:`changelog`. to find -the deprecations and removals for each release between the release you're -currently running and the one you wish to upgrade to. The change history -notes every deprecation within a ``Deprecation`` section and every removal -within a ``Backwards Incompatibilies`` section for each release. +Your first line of defense against application failures caused by upgrading to +a newer Pyramid release is always to read the :ref:`changelog` to find the +deprecations and removals for each release between the release you're currently +running and the one to which you wish to upgrade. The change history notes +every deprecation within a ``Deprecation`` section and every removal within a +``Backwards Incompatibilies`` section for each release. -The change history often contains instructions for changing your code to -avoid deprecation warnings and how to change docs-deprecated spellings to -newer ones. You can follow along with each deprecation explanation in the -change history, simply doing a grep or other code search to your application, -using the change log examples to remediate each potential problem. +The change history often contains instructions for changing your code to avoid +deprecation warnings and how to change docs-deprecated spellings to newer ones. +You can follow along with each deprecation explanation in the change history, +simply doing a grep or other code search to your application, using the change +log examples to remediate each potential problem. .. _testing_under_new_release: -Testing Your Application Under a New Pyramid Release +Testing your application under a new Pyramid release ---------------------------------------------------- Once you've upgraded your application to a new Pyramid release and you've @@ -106,25 +105,24 @@ you can see DeprecationWarnings printed to the console when the tests run. $ python -Wd setup.py test -q -The ``-Wd`` argument is an argument that tells Python to print deprecation -warnings to the console. Note that the ``-Wd`` flag is only required for -Python 2.7 and better: Python versions 2.6 and older print deprecation -warnings to the console by default. See `the Python -W flag documentation -`_ for more -information. +The ``-Wd`` argument tells Python to print deprecation warnings to the console. +Note that the ``-Wd`` flag is only required for Python 2.7 and better: Python +versions 2.6 and older print deprecation warnings to the console by default. +See `the Python -W flag documentation +`_ for more information. As your tests run, deprecation warnings will be printed to the console -explaining the deprecation and providing instructions about how to prevent -the deprecation warning from being issued. For example: +explaining the deprecation and providing instructions about how to prevent the +deprecation warning from being issued. For example: -.. code-block:: text +.. code-block:: bash $ python -Wd setup.py test -q # .. elided ... running build_ext - /home/chrism/projects/pyramid/env27/myproj/myproj/views.py:3: - DeprecationWarning: static: The "pyramid.view.static" class is deprecated - as of Pyramid 1.1; use the "pyramid.static.static_view" class instead with + /home/chrism/projects/pyramid/env27/myproj/myproj/views.py:3: + DeprecationWarning: static: The "pyramid.view.static" class is deprecated + as of Pyramid 1.1; use the "pyramid.static.static_view" class instead with the "use_subpath" argument set to True. from pyramid.view import static . @@ -144,8 +142,8 @@ pyramid.view import static``) that is causing the problem: from pyramid.view import static myview = static('static', 'static') -The deprecation warning tells me how to fix it, so I can change the code to -do things the newer way: +The deprecation warning tells me how to fix it, so I can change the code to do +things the newer way: .. code-block:: python :linenos: @@ -155,10 +153,10 @@ do things the newer way: from pyramid.static import static_view myview = static_view('static', 'static', use_subpath=True) -When I run the tests again, the deprecation warning is no longer printed to -my console: +When I run the tests again, the deprecation warning is no longer printed to my +console: -.. code-block:: text +.. code-block:: bash $ python -Wd setup.py test -q # .. elided ... @@ -170,7 +168,7 @@ my console: OK -My Application Doesn't Have Any Tests or Has Few Tests +My application doesn't have any tests or has few tests ------------------------------------------------------ If your application has no tests, or has only moderate test coverage, running @@ -178,8 +176,8 @@ tests won't tell you very much, because the Pyramid codepaths that generate deprecation warnings won't be executed. In this circumstance, you can start your application interactively under a -server run with the ``PYTHONWARNINGS`` environment variable set to -``default``. On UNIX, you can do that via: +server run with the ``PYTHONWARNINGS`` environment variable set to ``default``. +On UNIX, you can do that via: .. code-block:: bash @@ -194,16 +192,15 @@ On Windows, you need to issue two commands: At this point, it's ensured that deprecation warnings will be printed to the console whenever a codepath is hit that generates one. You can then click -around in your application interactively to try to generate them, and -remediate as explained in :ref:`testing_under_new_release`. +around in your application interactively to try to generate them, and remediate +as explained in :ref:`testing_under_new_release`. See `the PYTHONWARNINGS environment variable documentation `_ or `the Python -W flag documentation -`_ for more -information. +`_ for more information. -Upgrading to the Very Latest Pyramid Release +Upgrading to the very latest Pyramid release -------------------------------------------- When you upgrade your application to the most recent Pyramid release, @@ -220,15 +217,13 @@ advisable to do this: :ref:`testing_under_new_release`. Note any deprecation warnings and remediate. -- Upgrade to the most recent 1.3 release, 1.3.3. Run your application's - tests, note any deprecation warnings and remediate. +- Upgrade to the most recent 1.3 release, 1.3.3. Run your application's tests, + note any deprecation warnings, and remediate. - Upgrade to 1.4.4. Run your application's tests, note any deprecation - warnings and remediate. + warnings, and remediate. If you skip testing your application under each minor release (for example if -you upgrade directly from 1.2.1 to 1.4.4), you might miss a deprecation -warning and waste more time trying to figure out an error caused by a feature -removal than it would take to upgrade stepwise through each minor release. - - +you upgrade directly from 1.2.1 to 1.4.4), you might miss a deprecation warning +and waste more time trying to figure out an error caused by a feature removal +than it would take to upgrade stepwise through each minor release. -- cgit v1.2.3 From 2f6864759d66b8a9cea845d8e6b628b5bede5336 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sat, 14 Nov 2015 03:15:59 -0800 Subject: minor changes to narrative flow --- docs/tutorials/wiki2/basiclayout.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/tutorials/wiki2/basiclayout.rst b/docs/tutorials/wiki2/basiclayout.rst index 1ba7d3958..1426e55a5 100644 --- a/docs/tutorials/wiki2/basiclayout.rst +++ b/docs/tutorials/wiki2/basiclayout.rst @@ -44,7 +44,7 @@ When you invoke the ``pserve development.ini`` command, the ``main`` function above is executed. It accepts some settings and returns a :term:`WSGI` application. (See :ref:`startup_chapter` for more about ``pserve``.) -The next step of ``main`` is to construct a :term:`Configurator` object: +Next in ``main``, construct a :term:`Configurator` object: .. literalinclude:: src/basiclayout/tutorial/__init__.py :lines: 7 @@ -279,8 +279,8 @@ The ``MyModel`` class has a ``__tablename__`` attribute. This informs SQLAlchemy which table to use to store the data representing instances of this class. -The Index import and the Index object creation is not required for this -tutorial, and will be removed in the next step. - That's about all there is to it regarding models, views, and initialization code in our stock application. + +The Index import and the Index object creation is not required for this +tutorial, and will be removed in the next step. -- cgit v1.2.3 From 73c256efdbe0b5663c726623b95c05e800939fbc Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sat, 14 Nov 2015 03:51:45 -0800 Subject: commit new wiki2/src/views/tutorial files --- .../wiki2/src/views/tutorial/models/__init__.py | 7 +++ .../wiki2/src/views/tutorial/models/meta.py | 46 +++++++++++++++ .../wiki2/src/views/tutorial/models/mymodel.py | 14 +++++ .../wiki2/src/views/tutorial/templates/edit.jinja2 | 68 +++++++++++++++++++++ .../src/views/tutorial/templates/layout.jinja2 | 66 +++++++++++++++++++++ .../src/views/tutorial/templates/mytemplate.jinja2 | 8 +++ .../wiki2/src/views/tutorial/templates/view.jinja2 | 66 +++++++++++++++++++++ .../wiki2/src/views/tutorial/views/__init__.py | 0 .../wiki2/src/views/tutorial/views/default.py | 69 ++++++++++++++++++++++ 9 files changed, 344 insertions(+) create mode 100644 docs/tutorials/wiki2/src/views/tutorial/models/__init__.py create mode 100644 docs/tutorials/wiki2/src/views/tutorial/models/meta.py create mode 100644 docs/tutorials/wiki2/src/views/tutorial/models/mymodel.py create mode 100644 docs/tutorials/wiki2/src/views/tutorial/templates/edit.jinja2 create mode 100644 docs/tutorials/wiki2/src/views/tutorial/templates/layout.jinja2 create mode 100644 docs/tutorials/wiki2/src/views/tutorial/templates/mytemplate.jinja2 create mode 100644 docs/tutorials/wiki2/src/views/tutorial/templates/view.jinja2 create mode 100644 docs/tutorials/wiki2/src/views/tutorial/views/__init__.py create mode 100644 docs/tutorials/wiki2/src/views/tutorial/views/default.py diff --git a/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py new file mode 100644 index 000000000..7b1c62867 --- /dev/null +++ b/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py @@ -0,0 +1,7 @@ +from sqlalchemy.orm import configure_mappers +# import all models classes here for sqlalchemy mappers +# to pick up +from .mymodel import Page # flake8: noqa + +# run configure mappers to ensure we avoid any race conditions +configure_mappers() diff --git a/docs/tutorials/wiki2/src/views/tutorial/models/meta.py b/docs/tutorials/wiki2/src/views/tutorial/models/meta.py new file mode 100644 index 000000000..b72b45f9f --- /dev/null +++ b/docs/tutorials/wiki2/src/views/tutorial/models/meta.py @@ -0,0 +1,46 @@ +from sqlalchemy import engine_from_config +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker +from sqlalchemy.schema import MetaData +import zope.sqlalchemy + +NAMING_CONVENTION = { + "ix": 'ix_%(column_0_label)s', + "uq": "uq_%(table_name)s_%(column_0_name)s", + "ck": "ck_%(table_name)s_%(constraint_name)s", + "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", + "pk": "pk_%(table_name)s" +} + +metadata = MetaData(naming_convention=NAMING_CONVENTION) +Base = declarative_base(metadata=metadata) + + +def includeme(config): + settings = config.get_settings() + dbmaker = get_dbmaker(get_engine(settings)) + + config.add_request_method( + lambda r: get_session(r.tm, dbmaker), + 'dbsession', + reify=True + ) + + config.include('pyramid_tm') + + +def get_session(transaction_manager, dbmaker): + dbsession = dbmaker() + zope.sqlalchemy.register(dbsession, + transaction_manager=transaction_manager) + return dbsession + + +def get_engine(settings, prefix='sqlalchemy.'): + return engine_from_config(settings, prefix) + + +def get_dbmaker(engine): + dbmaker = sessionmaker() + dbmaker.configure(bind=engine) + return dbmaker diff --git a/docs/tutorials/wiki2/src/views/tutorial/models/mymodel.py b/docs/tutorials/wiki2/src/views/tutorial/models/mymodel.py new file mode 100644 index 000000000..45571d78e --- /dev/null +++ b/docs/tutorials/wiki2/src/views/tutorial/models/mymodel.py @@ -0,0 +1,14 @@ +from .meta import Base +from sqlalchemy import ( + Column, + Integer, + Text, +) + + +class Page(Base): + """ The SQLAlchemy declarative model class for a Page object. """ + __tablename__ = 'pages' + id = Column(Integer, primary_key=True) + name = Column(Text, unique=True) + data = Column(Integer) diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/edit.jinja2 b/docs/tutorials/wiki2/src/views/tutorial/templates/edit.jinja2 new file mode 100644 index 000000000..ad4cd17e1 --- /dev/null +++ b/docs/tutorials/wiki2/src/views/tutorial/templates/edit.jinja2 @@ -0,0 +1,68 @@ + + + + + + + + + + + {{page.name}} - Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki) + + + + + + + + + + + + + +
+
+
+
+ +
+
+
+

+ Editing {% if page.name %}{{page.name}}{% else %}Page Name Goes Here{% endif %} +

+

You can return to the + FrontPage. +

+
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+
+ + + + + + + + diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/layout.jinja2 b/docs/tutorials/wiki2/src/views/tutorial/templates/layout.jinja2 new file mode 100644 index 000000000..ff624c65b --- /dev/null +++ b/docs/tutorials/wiki2/src/views/tutorial/templates/layout.jinja2 @@ -0,0 +1,66 @@ + + + + + + + + + + + Alchemy Scaffold for The Pyramid Web Framework + + + + + + + + + + + + + +
+
+
+
+ +
+
+ {% block content %} +

No content

+ {% endblock content %} +
+
+
+ +
+
+ +
+
+
+ + + + + + + + diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/mytemplate.jinja2 b/docs/tutorials/wiki2/src/views/tutorial/templates/mytemplate.jinja2 new file mode 100644 index 000000000..bb622bf5a --- /dev/null +++ b/docs/tutorials/wiki2/src/views/tutorial/templates/mytemplate.jinja2 @@ -0,0 +1,8 @@ +{% extends "layout.jinja2" %} + +{% block content %} +
+

Pyramid Alchemy scaffold

+

Welcome to {{project}}, an application generated by
the Pyramid Web Framework 1.7.dev0.

+
+{% endblock content %} diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/view.jinja2 b/docs/tutorials/wiki2/src/views/tutorial/templates/view.jinja2 new file mode 100644 index 000000000..5d47dc52f --- /dev/null +++ b/docs/tutorials/wiki2/src/views/tutorial/templates/view.jinja2 @@ -0,0 +1,66 @@ + + + + + + + + + + + {{page.name}} - Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki) + + + + + + + + + + + + + +
+
+
+
+ +
+
+
+ {{ content|safe }} +

+ + Edit this page + +

+

+ Viewing {% if page.name %}{{page.name}}{% else %}Page Name Goes Here{% endif %} +

+

You can return to the + FrontPage. +

+
+
+
+
+ +
+
+
+ + + + + + + + diff --git a/docs/tutorials/wiki2/src/views/tutorial/views/__init__.py b/docs/tutorials/wiki2/src/views/tutorial/views/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs/tutorials/wiki2/src/views/tutorial/views/default.py b/docs/tutorials/wiki2/src/views/tutorial/views/default.py new file mode 100644 index 000000000..3e5c61a72 --- /dev/null +++ b/docs/tutorials/wiki2/src/views/tutorial/views/default.py @@ -0,0 +1,69 @@ +import cgi +import re +from docutils.core import publish_parts + +from pyramid.httpexceptions import ( + HTTPFound, + HTTPNotFound, + ) + +from pyramid.view import view_config + +from ..models.mymodel import Page + +# regular expression used to find WikiWords +wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)") + +@view_config(route_name='view_wiki') +def view_wiki(request): + return HTTPFound(location=request.route_url('view_page', + pagename='FrontPage')) + +@view_config(route_name='view_page', renderer='templates/view.jinja2') +def view_page(request): + pagename = request.matchdict['pagename'] + page = request.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 = request.dbsession.query(Page).filter_by(name=word).all() + if exists: + view_url = request.route_url('view_page', pagename=word) + return '%s' % (view_url, cgi.escape(word)) + else: + add_url = request.route_url('add_page', pagename=word) + return '%s' % (add_url, cgi.escape(word)) + + content = publish_parts(page.data, writer_name='html')['html_body'] + content = wikiwords.sub(check, content) + edit_url = request.route_url('edit_page', pagename=pagename) + return dict(page=page, content=content, edit_url=edit_url) + +@view_config(route_name='add_page', renderer='templates/edit.jinja2') +def add_page(request): + pagename = request.matchdict['pagename'] + if 'form.submitted' in request.params: + body = request.params['body'] + page = Page(name=pagename, data=body) + request.dbsession.add(page) + return HTTPFound(location = request.route_url('view_page', + pagename=pagename)) + save_url = request.route_url('add_page', pagename=pagename) + page = Page(name='', data='') + return dict(page=page, save_url=save_url) + +@view_config(route_name='edit_page', renderer='templates/edit.jinja2') +def edit_page(request): + pagename = request.matchdict['pagename'] + page = request.dbsession.query(Page).filter_by(name=pagename).one() + if 'form.submitted' in request.params: + page.data = request.params['body'] + request.dbsession.add(page) + return HTTPFound(location = request.route_url('view_page', + pagename=pagename)) + return dict( + page=page, + save_url = request.route_url('edit_page', pagename=pagename), + ) -- cgit v1.2.3 From a47f067ca7491b797402604fd78a5a1132964f4d Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sun, 15 Nov 2015 05:36:13 -0800 Subject: wiki2/authorization.rst - progress on models and security from module to subpackage --- docs/tutorials/wiki2/authorization.rst | 51 ++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/docs/tutorials/wiki2/authorization.rst b/docs/tutorials/wiki2/authorization.rst index 1d810b05b..98e6110f3 100644 --- a/docs/tutorials/wiki2/authorization.rst +++ b/docs/tutorials/wiki2/authorization.rst @@ -18,22 +18,22 @@ require permission, instead of a default "403 Forbidden" page. We will implement the access control with the following steps: -* Add users and groups (``security.py``, a new module). -* Add an :term:`ACL` (``models.py`` and ``__init__.py``). +* Add users and groups (``security/default.py``, a new subpackage). +* Add an :term:`ACL` (``models/mymodel.py`` and ``__init__.py``). * Add an :term:`authentication policy` and an :term:`authorization policy` (``__init__.py``). * Add :term:`permission` declarations to the ``edit_page`` and ``add_page`` - views (``views.py``). + views (``views/default.py``). Then we will add the login and logout feature: * Add routes for /login and /logout (``__init__.py``). -* Add ``login`` and ``logout`` views (``views.py``). -* Add a login template (``login.pt``). +* Add ``login`` and ``logout`` views (``views/default.py``). +* Add a login template (``login.jinja2``). * Make the existing views return a ``logged_in`` flag to the renderer - (``views.py``). + (``views/default.py``). * Add a "Logout" link to be shown when logged in and viewing or editing a page - (``view.pt``, ``edit.pt``). + (``view.jinja2``, ``edit.jinja2``). Access control @@ -42,10 +42,10 @@ Access control Add users and groups ~~~~~~~~~~~~~~~~~~~~ -Create a new ``tutorial/tutorial/security.py`` module with the +Create a new ``tutorial/tutorial/security/default.py`` subpackage with the following content: -.. literalinclude:: src/authorization/tutorial/security.py +.. literalinclude:: src/authorization/tutorial/security/default.py :linenos: :language: python @@ -68,20 +68,21 @@ database, but here we use "dummy" data to represent user and groups sources. Add an ACL ~~~~~~~~~~ -Open ``tutorial/tutorial/models.py`` and add the following import -statement at the head: +Open ``tutorial/tutorial/models/mymodel.py`` and add the following import +statement just after the ``Base`` import at the top: -.. literalinclude:: src/authorization/tutorial/models.py - :lines: 1-4 +.. literalinclude:: src/authorization/tutorial/models/mymodel.py + :lines: 3-6 :linenos: + :lineno-start: 3 :language: python Add the following class definition at the end: -.. literalinclude:: src/authorization/tutorial/models.py - :lines: 33-37 +.. literalinclude:: src/authorization/tutorial/models/mymodel.py + :lines: 22-26 :linenos: - :lineno-start: 33 + :lineno-start: 22 :language: python We import :data:`~pyramid.security.Allow`, an action that means that @@ -90,9 +91,9 @@ permission is allowed, and :data:`~pyramid.security.Everyone`, a special :term:`ACE` entries that make up the ACL. The ACL is a list that needs to be named `__acl__` and be an attribute of a -class. We define an :term:`ACL` with two :term:`ACE` entries: the first entry -allows any user the `view` permission. The second entry allows the -``group:editors`` principal the `edit` permission. +class. We define an :term:`ACL` with two :term:`ACE` entries. The first entry +allows any user (``Everyone``) the `view` permission. The second entry allows +the ``group:editors`` principal the `edit` permission. The ``RootFactory`` class that contains the ACL is a :term:`root factory`. We need to associate it to our :app:`Pyramid` application, so the ACL is provided @@ -103,11 +104,11 @@ Open ``tutorial/tutorial/__init__.py`` and add a ``root_factory`` parameter to our :term:`Configurator` constructor, that points to the class we created above: +.. TODO update the lines to include, linenos, lineno-start + .. literalinclude:: src/authorization/tutorial/__init__.py :lines: 24-25 - :linenos: :emphasize-lines: 2 - :lineno-start: 16 :language: python Only the highlighted line needs to be added. @@ -336,11 +337,11 @@ Our ``tutorial/tutorial/__init__.py`` will look like this when we're done: Only the highlighted lines need to be added or edited. -Our ``tutorial/tutorial/models.py`` will look like this when we're done: +Our ``tutorial/tutorial/models/mymodel.py`` will look like this when we're done: -.. literalinclude:: src/authorization/tutorial/models.py +.. literalinclude:: src/authorization/tutorial/models/mymodel.py :linenos: - :emphasize-lines: 1-4,33-37 + :emphasize-lines: 3-6,22-26 :language: python Only the highlighted lines need to be added or edited. @@ -404,3 +405,5 @@ following URLs, checking that the result is as expected: the login form with the ``editor`` credentials), we'll see a Logout link in the upper right hand corner. When we click it, we're logged out, and redirected back to the front page. + +.. TODO update the lines to include in src/authorization/tutorial/__init__.py -- cgit v1.2.3 From bdc3f28699c5fc6b127a1aa502b8372b149429f2 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sun, 15 Nov 2015 05:46:47 -0800 Subject: minor grammar, .rst syntax fixes, rewrap 79 cols --- docs/narr/threadlocals.rst | 226 +++++++++++++++++++++------------------------ 1 file changed, 106 insertions(+), 120 deletions(-) diff --git a/docs/narr/threadlocals.rst b/docs/narr/threadlocals.rst index afe56de3e..7437a3a76 100644 --- a/docs/narr/threadlocals.rst +++ b/docs/narr/threadlocals.rst @@ -8,26 +8,24 @@ Thread Locals ============= -A :term:`thread local` variable is a variable that appears to be a -"global" variable to an application which uses it. However, unlike a -true global variable, one thread or process serving the application -may receive a different value than another thread or process when that -variable is "thread local". +A :term:`thread local` variable is a variable that appears to be a "global" +variable to an application which uses it. However, unlike a true global +variable, one thread or process serving the application may receive a different +value than another thread or process when that variable is "thread local". -When a request is processed, :app:`Pyramid` makes two :term:`thread -local` variables available to the application: a "registry" and a -"request". +When a request is processed, :app:`Pyramid` makes two :term:`thread local` +variables available to the application: a "registry" and a "request". Why and How :app:`Pyramid` Uses Thread Local Variables ---------------------------------------------------------- +------------------------------------------------------ -How are thread locals beneficial to :app:`Pyramid` and application -developers who use :app:`Pyramid`? Well, usually they're decidedly -**not**. Using a global or a thread local variable in any application -usually makes it a lot harder to understand for a casual reader. Use -of a thread local or a global is usually just a way to avoid passing -some value around between functions, which is itself usually a very -bad idea, at least if code readability counts as an important concern. +How are thread locals beneficial to :app:`Pyramid` and application developers +who use :app:`Pyramid`? Well, usually they're decidedly **not**. Using a +global or a thread local variable in any application usually makes it a lot +harder to understand for a casual reader. Use of a thread local or a global is +usually just a way to avoid passing some value around between functions, which +is itself usually a very bad idea, at least if code readability counts as an +important concern. For historical reasons, however, thread local variables are indeed consulted by various :app:`Pyramid` API functions. For example, the implementation of the @@ -40,119 +38,107 @@ application registry, from which it looks up the authentication policy; it then uses the authentication policy to retrieve the authenticated user id. This is how :app:`Pyramid` allows arbitrary authentication policies to be "plugged in". -When they need to do so, :app:`Pyramid` internals use two API -functions to retrieve the :term:`request` and :term:`application -registry`: :func:`~pyramid.threadlocal.get_current_request` and -:func:`~pyramid.threadlocal.get_current_registry`. The former -returns the "current" request; the latter returns the "current" -registry. Both ``get_current_*`` functions retrieve an object from a -thread-local data structure. These API functions are documented in -:ref:`threadlocal_module`. - -These values are thread locals rather than true globals because one -Python process may be handling multiple simultaneous requests or even -multiple :app:`Pyramid` applications. If they were true globals, -:app:`Pyramid` could not handle multiple simultaneous requests or -allow more than one :app:`Pyramid` application instance to exist in -a single Python process. - -Because one :app:`Pyramid` application is permitted to call -*another* :app:`Pyramid` application from its own :term:`view` code -(perhaps as a :term:`WSGI` app with help from the -:func:`pyramid.wsgi.wsgiapp2` decorator), these variables are -managed in a *stack* during normal system operations. The stack -instance itself is a :class:`threading.local`. +When they need to do so, :app:`Pyramid` internals use two API functions to +retrieve the :term:`request` and :term:`application registry`: +:func:`~pyramid.threadlocal.get_current_request` and +:func:`~pyramid.threadlocal.get_current_registry`. The former returns the +"current" request; the latter returns the "current" registry. Both +``get_current_*`` functions retrieve an object from a thread-local data +structure. These API functions are documented in :ref:`threadlocal_module`. + +These values are thread locals rather than true globals because one Python +process may be handling multiple simultaneous requests or even multiple +:app:`Pyramid` applications. If they were true globals, :app:`Pyramid` could +not handle multiple simultaneous requests or allow more than one :app:`Pyramid` +application instance to exist in a single Python process. + +Because one :app:`Pyramid` application is permitted to call *another* +:app:`Pyramid` application from its own :term:`view` code (perhaps as a +:term:`WSGI` app with help from the :func:`pyramid.wsgi.wsgiapp2` decorator), +these variables are managed in a *stack* during normal system operations. The +stack instance itself is a :class:`threading.local`. During normal operations, the thread locals stack is managed by a -:term:`Router` object. At the beginning of a request, the Router -pushes the application's registry and the request on to the stack. At -the end of a request, the stack is popped. The topmost request and -registry on the stack are considered "current". Therefore, when the -system is operating normally, the very definition of "current" is -defined entirely by the behavior of a pyramid :term:`Router`. +:term:`Router` object. At the beginning of a request, the Router pushes the +application's registry and the request on to the stack. At the end of a +request, the stack is popped. The topmost request and registry on the stack +are considered "current". Therefore, when the system is operating normally, +the very definition of "current" is defined entirely by the behavior of a +pyramid :term:`Router`. However, during unit testing, no Router code is ever invoked, and the -definition of "current" is defined by the boundary between calls to -the :meth:`pyramid.config.Configurator.begin` and -:meth:`pyramid.config.Configurator.end` methods (or between -calls to the :func:`pyramid.testing.setUp` and -:func:`pyramid.testing.tearDown` functions). These functions push -and pop the threadlocal stack when the system is under test. See -:ref:`test_setup_and_teardown` for the definitions of these functions. - -Scripts which use :app:`Pyramid` machinery but never actually start -a WSGI server or receive requests via HTTP such as scripts which use -the :mod:`pyramid.scripting` API will never cause any Router code -to be executed. However, the :mod:`pyramid.scripting` APIs also -push some values on to the thread locals stack as a matter of course. -Such scripts should expect the -:func:`~pyramid.threadlocal.get_current_request` function to always -return ``None``, and should expect the -:func:`~pyramid.threadlocal.get_current_registry` function to return -exactly the same :term:`application registry` for every request. +definition of "current" is defined by the boundary between calls to the +:meth:`pyramid.config.Configurator.begin` and +:meth:`pyramid.config.Configurator.end` methods (or between calls to the +:func:`pyramid.testing.setUp` and :func:`pyramid.testing.tearDown` functions). +These functions push and pop the threadlocal stack when the system is under +test. See :ref:`test_setup_and_teardown` for the definitions of these +functions. + +Scripts which use :app:`Pyramid` machinery but never actually start a WSGI +server or receive requests via HTTP, such as scripts which use the +:mod:`pyramid.scripting` API, will never cause any Router code to be executed. +However, the :mod:`pyramid.scripting` APIs also push some values on to the +thread locals stack as a matter of course. Such scripts should expect the +:func:`~pyramid.threadlocal.get_current_request` function to always return +``None``, and should expect the +:func:`~pyramid.threadlocal.get_current_registry` function to return exactly +the same :term:`application registry` for every request. Why You Shouldn't Abuse Thread Locals ------------------------------------- You probably should almost never use the :func:`~pyramid.threadlocal.get_current_request` or -:func:`~pyramid.threadlocal.get_current_registry` functions, except -perhaps in tests. In particular, it's almost always a mistake to use -``get_current_request`` or ``get_current_registry`` in application -code because its usage makes it possible to write code that can be -neither easily tested nor scripted. Inappropriate usage is defined as -follows: +:func:`~pyramid.threadlocal.get_current_registry` functions, except perhaps in +tests. In particular, it's almost always a mistake to use +``get_current_request`` or ``get_current_registry`` in application code because +its usage makes it possible to write code that can be neither easily tested nor +scripted. Inappropriate usage is defined as follows: - ``get_current_request`` should never be called within the body of a - :term:`view callable`, or within code called by a view callable. - View callables already have access to the request (it's passed in to - each as ``request``). - -- ``get_current_request`` should never be called in :term:`resource` code. - If a resource needs access to the request, it should be passed the request - by a :term:`view callable`. - -- ``get_current_request`` function should never be called because it's - "easier" or "more elegant" to think about calling it than to pass a - request through a series of function calls when creating some API - design. Your application should instead almost certainly pass data - derived from the request around rather than relying on being able to - call this function to obtain the request in places that actually - have no business knowing about it. Parameters are *meant* to be - passed around as function arguments, this is why they exist. Don't - try to "save typing" or create "nicer APIs" by using this function - in the place where a request is required; this will only lead to - sadness later. - -- Neither ``get_current_request`` nor ``get_current_registry`` should - ever be called within application-specific forks of third-party - library code. The library you've forked almost certainly has - nothing to do with :app:`Pyramid`, and making it dependent on - :app:`Pyramid` (rather than making your :app:`pyramid` - application depend upon it) means you're forming a dependency in the - wrong direction. - -Use of the :func:`~pyramid.threadlocal.get_current_request` function -in application code *is* still useful in very limited circumstances. -As a rule of thumb, usage of ``get_current_request`` is useful -**within code which is meant to eventually be removed**. For -instance, you may find yourself wanting to deprecate some API that -expects to be passed a request object in favor of one that does not -expect to be passed a request object. But you need to keep -implementations of the old API working for some period of time while -you deprecate the older API. So you write a "facade" implementation -of the new API which calls into the code which implements the older -API. Since the new API does not require the request, your facade -implementation doesn't have local access to the request when it needs -to pass it into the older API implementation. After some period of -time, the older implementation code is disused and the hack that uses -``get_current_request`` is removed. This would be an appropriate -place to use the ``get_current_request``. - -Use of the :func:`~pyramid.threadlocal.get_current_registry` -function should be limited to testing scenarios. The registry made -current by use of the -:meth:`pyramid.config.Configurator.begin` method during a -test (or via :func:`pyramid.testing.setUp`) when you do not pass -one in is available to you via this API. - + :term:`view callable`, or within code called by a view callable. View + callables already have access to the request (it's passed in to each as + ``request``). + +- ``get_current_request`` should never be called in :term:`resource` code. If a + resource needs access to the request, it should be passed the request by a + :term:`view callable`. + +- ``get_current_request`` function should never be called because it's "easier" + or "more elegant" to think about calling it than to pass a request through a + series of function calls when creating some API design. Your application + should instead, almost certainly, pass around data derived from the request + rather than relying on being able to call this function to obtain the request + in places that actually have no business knowing about it. Parameters are + *meant* to be passed around as function arguments; this is why they exist. + Don't try to "save typing" or create "nicer APIs" by using this function in + the place where a request is required; this will only lead to sadness later. + +- Neither ``get_current_request`` nor ``get_current_registry`` should ever be + called within application-specific forks of third-party library code. The + library you've forked almost certainly has nothing to do with :app:`Pyramid`, + and making it dependent on :app:`Pyramid` (rather than making your + :app:`pyramid` application depend upon it) means you're forming a dependency + in the wrong direction. + +Use of the :func:`~pyramid.threadlocal.get_current_request` function in +application code *is* still useful in very limited circumstances. As a rule of +thumb, usage of ``get_current_request`` is useful **within code which is meant +to eventually be removed**. For instance, you may find yourself wanting to +deprecate some API that expects to be passed a request object in favor of one +that does not expect to be passed a request object. But you need to keep +implementations of the old API working for some period of time while you +deprecate the older API. So you write a "facade" implementation of the new API +which calls into the code which implements the older API. Since the new API +does not require the request, your facade implementation doesn't have local +access to the request when it needs to pass it into the older API +implementation. After some period of time, the older implementation code is +disused and the hack that uses ``get_current_request`` is removed. This would +be an appropriate place to use the ``get_current_request``. + +Use of the :func:`~pyramid.threadlocal.get_current_registry` function should be +limited to testing scenarios. The registry made current by use of the +:meth:`pyramid.config.Configurator.begin` method during a test (or via +:func:`pyramid.testing.setUp`) when you do not pass one in is available to you +via this API. -- cgit v1.2.3 From b9f8dcfdb745c81a437549922f04d03ae7f45614 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 15 Nov 2015 19:53:54 -0600 Subject: add .exe to the script being invoked if missing on windows fixes #2126 --- pyramid/scripts/pserve.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/pyramid/scripts/pserve.py b/pyramid/scripts/pserve.py index efad0cc68..95752a3be 100644 --- a/pyramid/scripts/pserve.py +++ b/pyramid/scripts/pserve.py @@ -428,6 +428,19 @@ a real process manager for your processes like Systemd, Circus, or Supervisor. arg = win32api.GetShortPathName(arg) return arg + def find_script_path(self, name): # pragma: no cover + """ + Return the path to the script being invoked by the python interpreter. + + There's an issue on Windows when running the executable from + a console_script causing the script name (sys.argv[0]) to + not end with .exe or .py and thus cannot be run via popen. + """ + if sys.platform == 'win32': + if not name.endswith('.exe') and not name.endswith('.py'): + name += '.exe' + return name + def daemonize(self): # pragma: no cover pid = live_pidfile(self.options.pid_file) if pid: @@ -573,7 +586,10 @@ a real process manager for your processes like Systemd, Circus, or Supervisor. else: self.out('Starting subprocess with monitor parent') while 1: - args = [self.quote_first_command_arg(sys.executable)] + sys.argv + args = [ + self.quote_first_command_arg(sys.executable), + self.find_script_path(sys.argv[0]), + ] + sys.argv[1:] new_environ = os.environ.copy() if reloader: new_environ[self._reloader_environ_key] = 'true' -- cgit v1.2.3 From a8c0d7161f12a24bc439495b5f1af78da3dfcf17 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 15 Nov 2015 20:01:03 -0600 Subject: update changelog --- CHANGES.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 9ff26420b..e9dc975a7 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -247,6 +247,10 @@ Bug Fixes been supported but would work and fail in weird ways. See https://github.com/Pylons/pyramid/pull/2119 +- Fix an issue on Windows when running ``pserve --reload`` in which the + process failed to fork because it could not find the pserve script to + run. See https://github.com/Pylons/pyramid/pull/2137 + Deprecations ------------ -- cgit v1.2.3 From 1e1111ba774c4cb12b075338e921283047bd3600 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 12 Nov 2015 10:45:47 -0600 Subject: support asset specs in ManifestCacheBuster --- pyramid/static.py | 20 +++++++++++++++----- pyramid/tests/test_static.py | 14 ++++++++++++++ 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/pyramid/static.py b/pyramid/static.py index 35d5e1047..c2c8c89e5 100644 --- a/pyramid/static.py +++ b/pyramid/static.py @@ -19,7 +19,10 @@ from pkg_resources import ( from repoze.lru import lru_cache -from pyramid.asset import resolve_asset_spec +from pyramid.asset import ( + abspath_from_asset_spec, + resolve_asset_spec, +) from pyramid.compat import text_ @@ -211,7 +214,11 @@ class ManifestCacheBuster(object): uses a supplied manifest file to map an asset path to a cache-busted version of the path. - The file is expected to conform to the following simple JSON format: + The ``manifest_spec`` can be an absolute path or a :term:`asset spec` + pointing to a package-relative file. + + The manifest file is expected to conform to the following simple JSON + format: .. code-block:: json @@ -247,8 +254,10 @@ class ManifestCacheBuster(object): exists = staticmethod(exists) # testing getmtime = staticmethod(getmtime) # testing - def __init__(self, manifest_path, reload=False): - self.manifest_path = manifest_path + def __init__(self, manifest_spec, reload=False): + package_name = caller_package().__name__ + self.manifest_path = abspath_from_asset_spec( + manifest_spec, package_name) self.reload = reload self._mtime = None @@ -260,7 +269,8 @@ class ManifestCacheBuster(object): Return a mapping parsed from the ``manifest_path``. Subclasses may override this method to use something other than - ``json.loads``. + ``json.loads`` to load any type of file format and return a conforming + dictionary. """ with open(self.manifest_path, 'rb') as fp: diff --git a/pyramid/tests/test_static.py b/pyramid/tests/test_static.py index ac30e9e50..4a07c2cb1 100644 --- a/pyramid/tests/test_static.py +++ b/pyramid/tests/test_static.py @@ -423,6 +423,20 @@ class TestManifestCacheBuster(unittest.TestCase): fut('foo', ('css', 'main.css'), {}), (['css', 'main-test.css'], {})) + def test_it_with_relspec(self): + fut = self._makeOne('fixtures/manifest.json').pregenerate + self.assertEqual(fut('foo', ('bar',), {}), (['bar'], {})) + self.assertEqual( + fut('foo', ('css', 'main.css'), {}), + (['css', 'main-test.css'], {})) + + def test_it_with_absspec(self): + fut = self._makeOne('pyramid.tests:fixtures/manifest.json').pregenerate + self.assertEqual(fut('foo', ('bar',), {}), (['bar'], {})) + self.assertEqual( + fut('foo', ('css', 'main.css'), {}), + (['css', 'main-test.css'], {})) + def test_reload(self): manifest_path = os.path.join(here, 'fixtures', 'manifest.json') new_manifest_path = os.path.join(here, 'fixtures', 'manifest2.json') -- cgit v1.2.3 From 104870609e23edd1dba4d64869a068e883767552 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 15 Nov 2015 21:59:56 -0600 Subject: update docs to use asset specs with ManifestCacheBuster --- docs/narr/assets.rst | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/narr/assets.rst b/docs/narr/assets.rst index d36fa49c0..0f819570c 100644 --- a/docs/narr/assets.rst +++ b/docs/narr/assets.rst @@ -500,15 +500,12 @@ The following code would set up a cachebuster: .. code-block:: python :linenos: - from pyramid.path import AssetResolver from pyramid.static import ManifestCacheBuster - resolver = AssetResolver() - manifest = resolver.resolve('myapp:static/manifest.json') config.add_static_view( name='http://mycdn.example.com/', path='mypackage:static', - cachebust=ManifestCacheBuster(manifest.abspath())) + cachebust=ManifestCacheBuster('myapp:static/manifest.json')) A simpler approach is to use the :class:`~pyramid.static.QueryStringConstantCacheBuster` to generate a global -- cgit v1.2.3 From a81ac719ce8a61305f20d05e10b3397b31ec8951 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sun, 15 Nov 2015 23:46:30 -0800 Subject: wrap content with

--- docs/tutorials/wiki2/src/views/tutorial/templates/view.jinja2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/view.jinja2 b/docs/tutorials/wiki2/src/views/tutorial/templates/view.jinja2 index 5d47dc52f..36bb96870 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/templates/view.jinja2 +++ b/docs/tutorials/wiki2/src/views/tutorial/templates/view.jinja2 @@ -33,7 +33,7 @@

- {{ content|safe }} +

{{ content|safe }}

Edit this page -- cgit v1.2.3 From 4040cf7ef5a9843e25db69b3a17b3796f3a39fb8 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Mon, 16 Nov 2015 00:17:20 -0800 Subject: - complete rewrite of wiki2/authorization.rst - add wiki2/src/authorization/ files - improve tag in views/tutorial/templates/edit.jinja2 --- docs/tutorials/wiki2/authorization.rst | 137 ++++++++-------- .../wiki2/src/authorization/development.ini | 4 +- .../wiki2/src/authorization/production.ini | 14 +- docs/tutorials/wiki2/src/authorization/setup.py | 2 +- .../wiki2/src/authorization/tutorial/__init__.py | 18 +-- .../wiki2/src/authorization/tutorial/models.py | 37 ----- .../src/authorization/tutorial/models/__init__.py | 7 + .../src/authorization/tutorial/models/meta.py | 46 ++++++ .../src/authorization/tutorial/models/mymodel.py | 26 +++ .../authorization/tutorial/scripts/initializedb.py | 25 +-- .../wiki2/src/authorization/tutorial/security.py | 7 - .../authorization/tutorial/security/__init__.py | 1 + .../src/authorization/tutorial/security/default.py | 7 + .../authorization/tutorial/static/theme.min.css | 2 +- .../authorization/tutorial/templates/edit.jinja2 | 73 +++++++++ .../src/authorization/tutorial/templates/edit.pt | 72 --------- .../authorization/tutorial/templates/layout.jinja2 | 66 ++++++++ .../authorization/tutorial/templates/login.jinja2 | 74 +++++++++ .../src/authorization/tutorial/templates/login.pt | 74 --------- .../tutorial/templates/mytemplate.jinja2 | 8 + .../authorization/tutorial/templates/mytemplate.pt | 66 -------- .../authorization/tutorial/templates/view.jinja2 | 71 +++++++++ .../src/authorization/tutorial/templates/view.pt | 72 --------- .../wiki2/src/authorization/tutorial/tests.py | 175 ++++++--------------- .../wiki2/src/authorization/tutorial/views.py | 124 --------------- .../src/authorization/tutorial/views/__init__.py | 0 .../src/authorization/tutorial/views/default.py | 120 ++++++++++++++ .../wiki2/src/views/tutorial/templates/edit.jinja2 | 2 +- 28 files changed, 648 insertions(+), 682 deletions(-) delete mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/models.py create mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py create mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/models/meta.py create mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/models/mymodel.py delete mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/security.py create mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/security/__init__.py create mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/security/default.py create mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja2 delete mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.pt create mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/templates/layout.jinja2 create mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/templates/login.jinja2 delete mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/templates/login.pt create mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/templates/mytemplate.jinja2 delete mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/templates/mytemplate.pt create mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/templates/view.jinja2 delete mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/templates/view.pt delete mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/views.py create mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/views/__init__.py create mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/views/default.py diff --git a/docs/tutorials/wiki2/authorization.rst b/docs/tutorials/wiki2/authorization.rst index 98e6110f3..e40433497 100644 --- a/docs/tutorials/wiki2/authorization.rst +++ b/docs/tutorials/wiki2/authorization.rst @@ -104,10 +104,8 @@ Open ``tutorial/tutorial/__init__.py`` and add a ``root_factory`` parameter to our :term:`Configurator` constructor, that points to the class we created above: -.. TODO update the lines to include, linenos, lineno-start - .. literalinclude:: src/authorization/tutorial/__init__.py - :lines: 24-25 + :lines: 13-14 :emphasize-lines: 2 :language: python @@ -128,18 +126,18 @@ Open ``tutorial/tutorial/__init__.py`` and add the highlighted import statements: .. literalinclude:: src/authorization/tutorial/__init__.py - :lines: 1-7 + :lines: 1-5 :linenos: - :emphasize-lines: 2-3,7 + :emphasize-lines: 2-5 :language: python Now add those policies to the configuration: .. literalinclude:: src/authorization/tutorial/__init__.py - :lines: 21-27 + :lines: 7-16 :linenos: - :lineno-start: 21 - :emphasize-lines: 1-3,6-7 + :lineno-start: 7 + :emphasize-lines: 4-6,9-10 :language: python Only the highlighted lines need to be added. @@ -152,47 +150,50 @@ ticket that may be included in the request. We are also enabling an Note that the :class:`pyramid.authentication.AuthTktAuthenticationPolicy` constructor accepts two arguments: ``secret`` and ``callback``. ``secret`` is a string representing an encryption key used by the "authentication ticket" -machinery represented by this policy: it is required. The ``callback`` is the +machinery represented by this policy; it is required. The ``callback`` is the ``groupfinder()`` function that we created before. + Add permission declarations ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Open ``tutorial/tutorial/views.py`` and add a ``permission='edit'`` parameter -to the ``@view_config`` decorators for ``add_page()`` and ``edit_page()``: -.. literalinclude:: src/authorization/tutorial/views.py - :lines: 60-61 +Open ``tutorial/tutorial/views/default.py`` and add a ``permission='view'`` +parameter to the ``@view_config`` decorator for ``view_wiki()`` and +``view_page()`` as follows: + +.. literalinclude:: src/authorization/tutorial/views/default.py + :lines: 27-29 :emphasize-lines: 1-2 :language: python -.. literalinclude:: src/authorization/tutorial/views.py - :lines: 75-76 +.. literalinclude:: src/authorization/tutorial/views/default.py + :lines: 33-35 :emphasize-lines: 1-2 :language: python Only the highlighted lines, along with their preceding commas, need to be edited and added. -The result is that only users who possess the ``edit`` permission at the time -of the request may invoke those two views. +This allows anyone to invoke these two views. -Add a ``permission='view'`` parameter to the ``@view_config`` decorator for -``view_wiki()`` and ``view_page()`` as follows: +Add a ``permission='edit'`` parameter to the ``@view_config`` decorators for +``add_page()`` and ``edit_page()``: -.. literalinclude:: src/authorization/tutorial/views.py - :lines: 30-31 +.. literalinclude:: src/authorization/tutorial/views/default.py + :lines: 57-59 :emphasize-lines: 1-2 :language: python -.. literalinclude:: src/authorization/tutorial/views.py - :lines: 36-37 +.. literalinclude:: src/authorization/tutorial/views/default.py + :lines: 72-74 :emphasize-lines: 1-2 :language: python Only the highlighted lines, along with their preceding commas, need to be edited and added. -This allows anyone to invoke these two views. +The result is that only users who possess the ``edit`` permission at the time +of the request may invoke those two views. We are done with the changes needed to control access. The changes that follow will add the login and logout feature. @@ -206,7 +207,7 @@ Go back to ``tutorial/tutorial/__init__.py`` and add these two routes as highlighted: .. literalinclude:: src/authorization/tutorial/__init__.py - :lines: 30-33 + :lines: 20-23 :emphasize-lines: 2-3 :language: python @@ -214,7 +215,7 @@ highlighted: ``view_page`` route definition: .. literalinclude:: src/authorization/tutorial/__init__.py - :lines: 33 + :lines: 23 :language: python This is because ``view_page``'s route definition uses a catch-all @@ -234,11 +235,11 @@ We'll also add a ``logout`` view callable to our application and provide a link to it. This view will clear the credentials of the logged in user and redirect back to the front page. -Add the following import statements to the head of -``tutorial/tutorial/views.py``: +Add the following import statements to ``tutorial/tutorial/views/default.py`` +after the import from ``pyramid.httpexceptions``: -.. literalinclude:: src/authorization/tutorial/views.py - :lines: 9-19 +.. literalinclude:: src/authorization/tutorial/views/default.py + :lines: 10-20 :emphasize-lines: 1-11 :language: python @@ -251,18 +252,18 @@ cookie. Now add the ``login`` and ``logout`` views at the end of the file: -.. literalinclude:: src/authorization/tutorial/views.py - :lines: 91-123 +.. literalinclude:: src/authorization/tutorial/views/default.py + :lines: 88-121 :language: python ``login()`` has two decorators: - a ``@view_config`` decorator which associates it with the ``login`` route - and makes it visible when we visit ``/login``, + and makes it visible when we visit ``/login``, and - a ``@forbidden_view_config`` decorator which turns it into a :term:`forbidden view`. ``login()`` will be invoked when a user tries to execute a view callable for which they lack authorization. For example, if - a user has not logged in and tries to add or edit a Wiki page, they will be + a user has not logged in and tries to add or edit a wiki page, they will be shown the login form before being allowed to continue. The order of these two :term:`view configuration` decorators is unimportant. @@ -270,36 +271,36 @@ The order of these two :term:`view configuration` decorators is unimportant. ``logout()`` is decorated with a ``@view_config`` decorator which associates it with the ``logout`` route. It will be invoked when we visit ``/logout``. -Add the ``login.pt`` Template -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Add the ``login.jinja2`` template +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Create ``tutorial/tutorial/templates/login.pt`` with the following content: +Create ``tutorial/tutorial/templates/login.jinja2`` with the following content: -.. literalinclude:: src/authorization/tutorial/templates/login.pt +.. literalinclude:: src/authorization/tutorial/templates/login.jinja2 :language: html The above template is referenced in the login view that we just added in -``views.py``. +``views/default.py``. Return a ``logged_in`` flag to the renderer ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Open ``tutorial/tutorial/views.py`` again. Add a ``logged_in`` parameter to -the return value of ``view_page()``, ``edit_page()``, and ``add_page()`` as -follows: +Open ``tutorial/tutorial/views/default.py`` again. Add a ``logged_in`` +parameter to the return value of ``view_page()``, ``add_page()``, and +``edit_page()`` as follows: -.. literalinclude:: src/authorization/tutorial/views.py - :lines: 57-58 +.. literalinclude:: src/authorization/tutorial/views/default.py + :lines: 54-55 :emphasize-lines: 1-2 :language: python -.. literalinclude:: src/authorization/tutorial/views.py - :lines: 72-73 +.. literalinclude:: src/authorization/tutorial/views/default.py + :lines: 69-70 :emphasize-lines: 1-2 :language: python -.. literalinclude:: src/authorization/tutorial/views.py - :lines: 85-89 +.. literalinclude:: src/authorization/tutorial/views/default.py + :lines: 82-86 :emphasize-lines: 3-4 :language: python @@ -311,19 +312,19 @@ the user is not authenticated, or a userid if the user is authenticated. Add a "Logout" link when logged in ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Open ``tutorial/tutorial/templates/edit.pt`` and -``tutorial/tutorial/templates/view.pt`` and add the following code as +Open ``tutorial/tutorial/templates/edit.jinja2`` and +``tutorial/tutorial/templates/view.jinja2`` and add the following code as indicated by the highlighted lines. -.. literalinclude:: src/authorization/tutorial/templates/edit.pt - :lines: 34-38 - :emphasize-lines: 3-5 +.. literalinclude:: src/authorization/tutorial/templates/edit.jinja2 + :lines: 34-40 + :emphasize-lines: 3-7 :language: html -The attribute ``tal:condition="logged_in"`` will make the element be included -when ``logged_in`` is any user id. The link will invoke the logout view. The -above element will not be included if ``logged_in`` is ``None``, such as when -a user is not authenticated. +The attribute ``logged_in`` will make the element be included when +``logged_in`` is any user id. The link will invoke the logout view. The above +element will not be included if ``logged_in`` is ``None``, such as when a user +is not authenticated. Reviewing our changes --------------------- @@ -332,7 +333,7 @@ Our ``tutorial/tutorial/__init__.py`` will look like this when we're done: .. literalinclude:: src/authorization/tutorial/__init__.py :linenos: - :emphasize-lines: 2-3,7,21-23,25-27,31-32 + :emphasize-lines: 2-3,5,10-12,14-16,21-22 :language: python Only the highlighted lines need to be added or edited. @@ -346,31 +347,31 @@ Our ``tutorial/tutorial/models/mymodel.py`` will look like this when we're done: Only the highlighted lines need to be added or edited. -Our ``tutorial/tutorial/views.py`` will look like this when we're done: +Our ``tutorial/tutorial/views/default.py`` will look like this when we're done: -.. literalinclude:: src/authorization/tutorial/views.py +.. literalinclude:: src/authorization/tutorial/views/default.py :linenos: - :emphasize-lines: 9-11,14-19,25,31,37,58,61,73,76,88,91-117,119-123 + :emphasize-lines: 10-20,27-28,33-34,54-55,57-58,69-70,72-73,84-85,88-121 :language: python Only the highlighted lines need to be added or edited. -Our ``tutorial/tutorial/templates/edit.pt`` template will look like this when +Our ``tutorial/tutorial/templates/edit.jinja2`` template will look like this when we're done: -.. literalinclude:: src/authorization/tutorial/templates/edit.pt +.. literalinclude:: src/authorization/tutorial/templates/edit.jinja2 :linenos: - :emphasize-lines: 36-38 + :emphasize-lines: 36-40 :language: html Only the highlighted lines need to be added or edited. -Our ``tutorial/tutorial/templates/view.pt`` template will look like this when +Our ``tutorial/tutorial/templates/view.jinja2`` template will look like this when we're done: -.. literalinclude:: src/authorization/tutorial/templates/view.pt +.. literalinclude:: src/authorization/tutorial/templates/view.jinja2 :linenos: - :emphasize-lines: 36-38 + :emphasize-lines: 36-40 :language: html Only the highlighted lines need to be added or edited. @@ -405,5 +406,3 @@ following URLs, checking that the result is as expected: the login form with the ``editor`` credentials), we'll see a Logout link in the upper right hand corner. When we click it, we're logged out, and redirected back to the front page. - -.. TODO update the lines to include in src/authorization/tutorial/__init__.py diff --git a/docs/tutorials/wiki2/src/authorization/development.ini b/docs/tutorials/wiki2/src/authorization/development.ini index a9d53b296..99c4ff0fe 100644 --- a/docs/tutorials/wiki2/src/authorization/development.ini +++ b/docs/tutorials/wiki2/src/authorization/development.ini @@ -27,7 +27,7 @@ sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite [server:main] use = egg:waitress#main -host = 0.0.0.0 +host = 127.0.0.1 port = 6543 ### @@ -68,4 +68,4 @@ level = NOTSET formatter = generic [formatter_generic] -format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/tutorials/wiki2/src/authorization/production.ini b/docs/tutorials/wiki2/src/authorization/production.ini index 4684d2f7a..97acfbd7d 100644 --- a/docs/tutorials/wiki2/src/authorization/production.ini +++ b/docs/tutorials/wiki2/src/authorization/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 @@ -51,6 +59,4 @@ level = NOTSET formatter = generic [formatter_generic] -format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s - -# End logging configuration +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/tutorials/wiki2/src/authorization/setup.py b/docs/tutorials/wiki2/src/authorization/setup.py index 09bd63d33..d4e5a4072 100644 --- a/docs/tutorials/wiki2/src/authorization/setup.py +++ b/docs/tutorials/wiki2/src/authorization/setup.py @@ -10,7 +10,7 @@ with open(os.path.join(here, 'CHANGES.txt')) as f: requires = [ 'pyramid', - 'pyramid_chameleon', + 'pyramid_jinja2', 'pyramid_debugtoolbar', 'pyramid_tm', 'SQLAlchemy', diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py b/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py index 2ada42171..084fee19f 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py @@ -2,30 +2,20 @@ from pyramid.config import Configurator from pyramid.authentication import AuthTktAuthenticationPolicy from pyramid.authorization import ACLAuthorizationPolicy -from sqlalchemy import engine_from_config - -from tutorial.security import groupfinder - -from .models import ( - DBSession, - Base, - ) - +from security.default import groupfinder def main(global_config, **settings): """ This function returns a Pyramid WSGI application. """ - engine = engine_from_config(settings, 'sqlalchemy.') - DBSession.configure(bind=engine) - Base.metadata.bind = engine authn_policy = AuthTktAuthenticationPolicy( 'sosecret', callback=groupfinder, hashalg='sha512') authz_policy = ACLAuthorizationPolicy() config = Configurator(settings=settings, - root_factory='tutorial.models.RootFactory') + root_factory='tutorial.models.mymodel.RootFactory') config.set_authentication_policy(authn_policy) config.set_authorization_policy(authz_policy) - config.include('pyramid_chameleon') + config.include('pyramid_jinja2') + config.include('.models.meta') config.add_static_view('static', 'static', cache_max_age=3600) config.add_route('view_wiki', '/') config.add_route('login', '/login') diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models.py b/docs/tutorials/wiki2/src/authorization/tutorial/models.py deleted file mode 100644 index 4f7e1e024..000000000 --- a/docs/tutorials/wiki2/src/authorization/tutorial/models.py +++ /dev/null @@ -1,37 +0,0 @@ -from pyramid.security import ( - Allow, - Everyone, - ) - -from sqlalchemy import ( - Column, - Integer, - Text, - ) - -from sqlalchemy.ext.declarative import declarative_base - -from sqlalchemy.orm import ( - scoped_session, - sessionmaker, - ) - -from zope.sqlalchemy import ZopeTransactionExtension - -DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension())) -Base = declarative_base() - - -class Page(Base): - """ The SQLAlchemy declarative model class for a Page object. """ - __tablename__ = 'pages' - id = Column(Integer, primary_key=True) - name = Column(Text, unique=True) - data = Column(Text) - - -class RootFactory(object): - __acl__ = [ (Allow, Everyone, 'view'), - (Allow, 'group:editors', 'edit') ] - def __init__(self, request): - pass diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py new file mode 100644 index 000000000..7b1c62867 --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py @@ -0,0 +1,7 @@ +from sqlalchemy.orm import configure_mappers +# import all models classes here for sqlalchemy mappers +# to pick up +from .mymodel import Page # flake8: noqa + +# run configure mappers to ensure we avoid any race conditions +configure_mappers() diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models/meta.py b/docs/tutorials/wiki2/src/authorization/tutorial/models/meta.py new file mode 100644 index 000000000..b72b45f9f --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/models/meta.py @@ -0,0 +1,46 @@ +from sqlalchemy import engine_from_config +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker +from sqlalchemy.schema import MetaData +import zope.sqlalchemy + +NAMING_CONVENTION = { + "ix": 'ix_%(column_0_label)s', + "uq": "uq_%(table_name)s_%(column_0_name)s", + "ck": "ck_%(table_name)s_%(constraint_name)s", + "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", + "pk": "pk_%(table_name)s" +} + +metadata = MetaData(naming_convention=NAMING_CONVENTION) +Base = declarative_base(metadata=metadata) + + +def includeme(config): + settings = config.get_settings() + dbmaker = get_dbmaker(get_engine(settings)) + + config.add_request_method( + lambda r: get_session(r.tm, dbmaker), + 'dbsession', + reify=True + ) + + config.include('pyramid_tm') + + +def get_session(transaction_manager, dbmaker): + dbsession = dbmaker() + zope.sqlalchemy.register(dbsession, + transaction_manager=transaction_manager) + return dbsession + + +def get_engine(settings, prefix='sqlalchemy.'): + return engine_from_config(settings, prefix) + + +def get_dbmaker(engine): + dbmaker = sessionmaker() + dbmaker.configure(bind=engine) + return dbmaker diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models/mymodel.py b/docs/tutorials/wiki2/src/authorization/tutorial/models/mymodel.py new file mode 100644 index 000000000..03e2f90ca --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/models/mymodel.py @@ -0,0 +1,26 @@ +from .meta import Base + +from pyramid.security import ( + Allow, + Everyone, + ) + +from sqlalchemy import ( + Column, + Integer, + Text, + ) + + +class Page(Base): + """ The SQLAlchemy declarative model class for a Page object. """ + __tablename__ = 'pages' + id = Column(Integer, primary_key=True) + name = Column(Text, unique=True) + data = Column(Integer) + +class RootFactory(object): + __acl__ = [ (Allow, Everyone, 'view'), + (Allow, 'group:editors', 'edit') ] + def __init__(self, request): + pass \ No newline at end of file diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/authorization/tutorial/scripts/initializedb.py index 23a5f13f4..4aac4a848 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/scripts/initializedb.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/scripts/initializedb.py @@ -2,36 +2,41 @@ import os import sys import transaction -from sqlalchemy import engine_from_config - from pyramid.paster import ( get_appsettings, setup_logging, ) -from ..models import ( - DBSession, - Page, +from ..models.meta import ( Base, + get_session, + get_engine, + get_dbmaker, ) +from ..models.mymodel import Page def usage(argv): cmd = os.path.basename(argv[0]) - print('usage: %s <config_uri>\n' + print('usage: %s <config_uri> [var=value]\n' '(example: "%s development.ini")' % (cmd, cmd)) sys.exit(1) def main(argv=sys.argv): - if len(argv) != 2: + if len(argv) < 2: usage(argv) config_uri = argv[1] setup_logging(config_uri) settings = get_appsettings(config_uri) - engine = engine_from_config(settings, 'sqlalchemy.') - DBSession.configure(bind=engine) + + engine = get_engine(settings) + dbmaker = get_dbmaker(engine) + + dbsession = get_session(transaction.manager, dbmaker) + Base.metadata.create_all(engine) + with transaction.manager: model = Page(name='FrontPage', data='This is the front page') - DBSession.add(model) + dbsession.add(model) diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/security.py b/docs/tutorials/wiki2/src/authorization/tutorial/security.py deleted file mode 100644 index d88c9c71f..000000000 --- a/docs/tutorials/wiki2/src/authorization/tutorial/security.py +++ /dev/null @@ -1,7 +0,0 @@ -USERS = {'editor':'editor', - 'viewer':'viewer'} -GROUPS = {'editor':['group:editors']} - -def groupfinder(userid, request): - if userid in USERS: - return GROUPS.get(userid, []) diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/security/__init__.py b/docs/tutorials/wiki2/src/authorization/tutorial/security/__init__.py new file mode 100644 index 000000000..5bb534f79 --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/security/__init__.py @@ -0,0 +1 @@ +# package diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/security/default.py b/docs/tutorials/wiki2/src/authorization/tutorial/security/default.py new file mode 100644 index 000000000..d88c9c71f --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/security/default.py @@ -0,0 +1,7 @@ +USERS = {'editor':'editor', + 'viewer':'viewer'} +GROUPS = {'editor':['group:editors']} + +def groupfinder(userid, request): + if userid in USERS: + return GROUPS.get(userid, []) diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/static/theme.min.css b/docs/tutorials/wiki2/src/authorization/tutorial/static/theme.min.css index 2f924bcc5..0d25de5b6 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/static/theme.min.css +++ b/docs/tutorials/wiki2/src/authorization/tutorial/static/theme.min.css @@ -1 +1 @@ -@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);body{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300;color:#fff;background:#bc2131}h1,h2,h3,h4,h5,h6{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300}p{font-weight:300}.font-normal{font-weight:400}.font-semi-bold{font-weight:600}.font-bold{font-weight:700}.starter-template{margin-top:250px}.starter-template .content{margin-left:10px}.starter-template .content h1{margin-top:10px;font-size:60px}.starter-template .content h1 .smaller{font-size:40px;color:#f2b7bd}.starter-template .content .lead{font-size:25px;color:#f2b7bd}.starter-template .content .lead .font-normal{color:#fff}.starter-template .links{float:right;right:0;margin-top:125px}.starter-template .links ul{display:block;padding:0;margin:0}.starter-template .links ul li{list-style:none;display:inline;margin:0 10px}.starter-template .links ul li:first-child{margin-left:0}.starter-template .links ul li:last-child{margin-right:0}.starter-template .links ul li.current-version{color:#f2b7bd;font-weight:400}.starter-template .links ul li a{color:#fff}.starter-template .links ul li a:hover{text-decoration:underline}.starter-template .links ul li .icon-muted{color:#eb8b95;margin-right:5px}.starter-template .links ul li:hover .icon-muted{color:#fff}.starter-template .copyright{margin-top:10px;font-size:.9em;color:#f2b7bd;text-transform:lowercase;float:right;right:0}@media (max-width:1199px){.starter-template .content h1{font-size:45px}.starter-template .content h1 .smaller{font-size:30px}.starter-template .content .lead{font-size:20px}}@media (max-width:991px){.starter-template{margin-top:0}.starter-template .logo{margin:40px auto}.starter-template .content{margin-left:0;text-align:center}.starter-template .content h1{margin-bottom:20px}.starter-template .links{float:none;text-align:center;margin-top:60px}.starter-template .copyright{float:none;text-align:center}}@media (max-width:767px){.starter-template .content h1 .smaller{font-size:25px;display:block}.starter-template .content .lead{font-size:16px}.starter-template .links{margin-top:40px}.starter-template .links ul li{display:block;margin:0}.starter-template .links ul li .icon-muted{display:none}.starter-template .copyright{margin-top:20px}} \ No newline at end of file +@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);body{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300;color:#fff;background:#bc2131}h1,h2,h3,h4,h5,h6{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300}p{font-weight:300}.font-normal{font-weight:400}.font-semi-bold{font-weight:600}.font-bold{font-weight:700}.starter-template{margin-top:250px}.starter-template .content{margin-left:10px}.starter-template .content h1{margin-top:10px;font-size:60px}.starter-template .content h1 .smaller{font-size:40px;color:#f2b7bd}.starter-template .content .lead{font-size:25px;color:#f2b7bd}.starter-template .content .lead .font-normal{color:#fff}.starter-template .links{float:right;right:0;margin-top:125px}.starter-template .links ul{display:block;padding:0;margin:0}.starter-template .links ul li{list-style:none;display:inline;margin:0 10px}.starter-template .links ul li:first-child{margin-left:0}.starter-template .links ul li:last-child{margin-right:0}.starter-template .links ul li.current-version{color:#f2b7bd;font-weight:400}.starter-template .links ul li a,a{color:#f2b7bd;text-decoration:underline}.starter-template .links ul li a:hover,a:hover{color:#fff;text-decoration:underline}.starter-template .links ul li .icon-muted{color:#eb8b95;margin-right:5px}.starter-template .links ul li:hover .icon-muted{color:#fff}.starter-template .copyright{margin-top:10px;font-size:.9em;color:#f2b7bd;text-transform:lowercase;float:right;right:0}@media (max-width:1199px){.starter-template .content h1{font-size:45px}.starter-template .content h1 .smaller{font-size:30px}.starter-template .content .lead{font-size:20px}}@media (max-width:991px){.starter-template{margin-top:0}.starter-template .logo{margin:40px auto}.starter-template .content{margin-left:0;text-align:center}.starter-template .content h1{margin-bottom:20px}.starter-template .links{float:none;text-align:center;margin-top:60px}.starter-template .copyright{float:none;text-align:center}}@media (max-width:767px){.starter-template .content h1 .smaller{font-size:25px;display:block}.starter-template .content .lead{font-size:16px}.starter-template .links{margin-top:40px}.starter-template .links ul li{display:block;margin:0}.starter-template .links ul li .icon-muted{display:none}.starter-template .copyright{margin-top:20px}} diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja2 new file mode 100644 index 000000000..c4f3a2c93 --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja2 @@ -0,0 +1,73 @@ +<!DOCTYPE html> +<html lang="{{request.locale_name}}"> + <head> + <meta charset="utf-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <meta name="description" content="pyramid web application"> + <meta name="author" content="Pylons Project"> + <link rel="shortcut icon" href="{{request.static_url('tutorial:static/pyramid-16x16.png')}}"> + + <title>Edit{% if page.name %} {{page.name}}{% endif %} - Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki) + + + + + + + + + + + + + +

+
+
+
+ +
+
+
+ {% if logged_in %} +

+ Logout +

+ {% endif %} +

+ Editing {% if page.name %}{{page.name}}{% else %}Page Name Goes Here{% endif %} +

+

You can return to the + FrontPage. +

+
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+
+ + + + + + + + diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.pt b/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.pt deleted file mode 100644 index ed355434d..000000000 --- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.pt +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - - - - - ${page.name} - Pyramid tutorial wiki (based on - TurboGears 20-Minute Wiki) - - - - - - - - - - - - -
-
-
-
- -
-
-
-

- Logout -

-

- Editing Page Name Goes - Here -

-

You can return to the - FrontPage. -

-
-
- -
-
- -
-
-
-
-
-
- -
-
-
- - - - - - - - diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/layout.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/layout.jinja2 new file mode 100644 index 000000000..ff624c65b --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/layout.jinja2 @@ -0,0 +1,66 @@ + + + + + + + + + + + Alchemy Scaffold for The Pyramid Web Framework + + + + + + + + + + + + + +
+
+
+
+ +
+
+ {% block content %} +

No content

+ {% endblock content %} +
+
+
+ +
+
+ +
+
+
+ + + + + + + + diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.jinja2 new file mode 100644 index 000000000..a80a2a165 --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.jinja2 @@ -0,0 +1,74 @@ + + + + + + + + + + + Login - Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki) + + + + + + + + + + + + + +
+
+
+
+ +
+
+
+

+ + Login +
+ {{ message }} +

+
+ +
+ + +
+
+ + +
+
+ +
+
+
+
+
+
+ +
+
+
+ + + + + + + + diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.pt b/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.pt deleted file mode 100644 index 4a938e9bb..000000000 --- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.pt +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - - - - - - Login - Pyramid tutorial wiki (based on - TurboGears 20-Minute Wiki) - - - - - - - - - - - - -
-
-
-
- -
-
-
-

- - Login -
- -

-
- -
- - -
-
- - -
-
- -
-
-
-
-
-
- -
-
-
- - - - - - - - diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/mytemplate.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/mytemplate.jinja2 new file mode 100644 index 000000000..bb622bf5a --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/mytemplate.jinja2 @@ -0,0 +1,8 @@ +{% extends "layout.jinja2" %} + +{% block content %} +
+

Pyramid Alchemy scaffold

+

Welcome to {{project}}, an application generated by
the Pyramid Web Framework 1.7.dev0.

+
+{% endblock content %} diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki2/src/authorization/tutorial/templates/mytemplate.pt deleted file mode 100644 index c9b0cec21..000000000 --- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/mytemplate.pt +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - - - - - Alchemy Scaffold for The Pyramid Web Framework - - - - - - - - - - - - - -
-
-
-
- -
-
-
-

Pyramid Alchemy scaffold

-

Welcome to ${project}, an application generated by
the Pyramid Web Framework.

-
-
-
- -
- -
-
-
- - - - - - - - diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.jinja2 new file mode 100644 index 000000000..a7afc66fc --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.jinja2 @@ -0,0 +1,71 @@ + + + + + + + + + + + {{page.name}} - Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki) + + + + + + + + + + + + + +
+
+
+
+ +
+
+
+ {% if logged_in %} +

+ Logout +

+ {% endif %} +

{{ content|safe }}

+

+ + Edit this page + +

+

+ Viewing {% if page.name %}{{page.name}}{% else %}Page Name Goes Here{% endif %} +

+

You can return to the + FrontPage. +

+
+
+
+
+ +
+
+
+ + + + + + + + diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.pt b/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.pt deleted file mode 100644 index 02cb8e73b..000000000 --- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.pt +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - - - - - ${page.name} - Pyramid tutorial wiki (based on - TurboGears 20-Minute Wiki) - - - - - - - - - - - - -
-
-
-
- -
-
-
-

- Logout -

-
- Page text goes here. -
-

- - Edit this page - -

-

- Viewing - Page Name Goes Here -

-

You can return to the - FrontPage. -

-
-
-
-
- -
-
-
- - - - - - - - diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/tests.py b/docs/tutorials/wiki2/src/authorization/tutorial/tests.py index 9f01d2da5..b947e3bb1 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/tests.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/tests.py @@ -3,144 +3,63 @@ import transaction from pyramid import testing -def _initTestingDB(): - from sqlalchemy import create_engine - from tutorial.models import ( - DBSession, - Page, - Base - ) - engine = create_engine('sqlite://') - Base.metadata.create_all(engine) - DBSession.configure(bind=engine) - with transaction.manager: - model = Page(name='FrontPage', data='This is the front page') - DBSession.add(model) - return DBSession - -def _registerRoutes(config): - config.add_route('view_page', '{pagename}') - config.add_route('edit_page', '{pagename}/edit_page') - config.add_route('add_page', 'add_page/{pagename}') - -class ViewWikiTests(unittest.TestCase): + +def dummy_request(dbsession): + return testing.DummyRequest(dbsession=dbsession) + + +class BaseTest(unittest.TestCase): def setUp(self): - self.config = testing.setUp() - self.session = _initTestingDB() + self.config = testing.setUp(settings={ + 'sqlalchemy.url': 'sqlite:///:memory:' + }) + self.config.include('.models.meta') + settings = self.config.get_settings() - def tearDown(self): - self.session.remove() - testing.tearDown() + from .models.meta import ( + get_session, + get_engine, + get_dbmaker, + ) - def _callFUT(self, request): - from tutorial.views import view_wiki - return view_wiki(request) + self.engine = get_engine(settings) + dbmaker = get_dbmaker(self.engine) - def test_it(self): - _registerRoutes(self.config) - request = testing.DummyRequest() - response = self._callFUT(request) - self.assertEqual(response.location, 'http://example.com/FrontPage') + self.session = get_session(transaction.manager, dbmaker) -class ViewPageTests(unittest.TestCase): - def setUp(self): - self.session = _initTestingDB() - self.config = testing.setUp() + def init_database(self): + from .models.meta import Base + Base.metadata.create_all(self.engine) def tearDown(self): - self.session.remove() - testing.tearDown() - - def _callFUT(self, request): - from tutorial.views import view_page - return view_page(request) - - def test_it(self): - from tutorial.models import Page - request = testing.DummyRequest() - request.matchdict['pagename'] = 'IDoExist' - page = Page(name='IDoExist', data='Hello CruelWorld IDoExist') - self.session.add(page) - _registerRoutes(self.config) - info = self._callFUT(request) - self.assertEqual(info['page'], page) - self.assertEqual( - info['content'], - '
\n' - '

Hello ' - 'CruelWorld ' - '' - 'IDoExist' - '

\n
\n') - self.assertEqual(info['edit_url'], - 'http://example.com/IDoExist/edit_page') - - -class AddPageTests(unittest.TestCase): - def setUp(self): - self.session = _initTestingDB() - self.config = testing.setUp() + from .models.meta import Base - def tearDown(self): - self.session.remove() testing.tearDown() + transaction.abort() + Base.metadata.create_all(self.engine) + + +class TestMyViewSuccessCondition(BaseTest): - def _callFUT(self, request): - from tutorial.views import add_page - return add_page(request) - - def test_it_notsubmitted(self): - _registerRoutes(self.config) - request = testing.DummyRequest() - request.matchdict = {'pagename':'AnotherPage'} - info = self._callFUT(request) - self.assertEqual(info['page'].data,'') - self.assertEqual(info['save_url'], - 'http://example.com/add_page/AnotherPage') - - def test_it_submitted(self): - from tutorial.models import Page - _registerRoutes(self.config) - request = testing.DummyRequest({'form.submitted':True, - 'body':'Hello yo!'}) - request.matchdict = {'pagename':'AnotherPage'} - self._callFUT(request) - page = self.session.query(Page).filter_by(name='AnotherPage').one() - self.assertEqual(page.data, 'Hello yo!') - -class EditPageTests(unittest.TestCase): def setUp(self): - self.session = _initTestingDB() - self.config = testing.setUp() + super(TestMyViewSuccessCondition, self).setUp() + self.init_database() - def tearDown(self): - self.session.remove() - testing.tearDown() + from .models.mymodel import MyModel + + model = MyModel(name='one', value=55) + self.session.add(model) + + def test_passing_view(self): + from .views.default import my_view + info = my_view(dummy_request(self.session)) + self.assertEqual(info['one'].name, 'one') + self.assertEqual(info['project'], 'tutorial') + + +class TestMyViewFailureCondition(BaseTest): - def _callFUT(self, request): - from tutorial.views import edit_page - return edit_page(request) - - def test_it_notsubmitted(self): - from tutorial.models import Page - _registerRoutes(self.config) - request = testing.DummyRequest() - request.matchdict = {'pagename':'abc'} - page = Page(name='abc', data='hello') - self.session.add(page) - info = self._callFUT(request) - self.assertEqual(info['page'], page) - self.assertEqual(info['save_url'], - 'http://example.com/abc/edit_page') - - def test_it_submitted(self): - from tutorial.models import Page - _registerRoutes(self.config) - request = testing.DummyRequest({'form.submitted':True, - 'body':'Hello yo!'}) - request.matchdict = {'pagename':'abc'} - page = Page(name='abc', data='hello') - self.session.add(page) - response = self._callFUT(request) - self.assertEqual(response.location, 'http://example.com/abc') - self.assertEqual(page.data, 'Hello yo!') + def test_failing_view(self): + from .views.default import my_view + info = my_view(dummy_request(self.session)) + self.assertEqual(info.status_int, 500) diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views.py b/docs/tutorials/wiki2/src/authorization/tutorial/views.py deleted file mode 100644 index e954d5a31..000000000 --- a/docs/tutorials/wiki2/src/authorization/tutorial/views.py +++ /dev/null @@ -1,124 +0,0 @@ -import re -from docutils.core import publish_parts - -from pyramid.httpexceptions import ( - HTTPFound, - HTTPNotFound, - ) - -from pyramid.view import ( - view_config, - forbidden_view_config, - ) - -from pyramid.security import ( - remember, - forget, - ) - -from .security import USERS - -from .models import ( - DBSession, - Page, - ) - - -# regular expression used to find WikiWords -wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)") - -@view_config(route_name='view_wiki', - permission='view') -def view_wiki(request): - return HTTPFound(location = request.route_url('view_page', - pagename='FrontPage')) - -@view_config(route_name='view_page', renderer='templates/view.pt', - permission='view') -def view_page(request): - pagename = request.matchdict['pagename'] - 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 = DBSession.query(Page).filter_by(name=word).all() - if exists: - view_url = request.route_url('view_page', pagename=word) - return '%s' % (view_url, word) - else: - add_url = request.route_url('add_page', pagename=word) - return '%s' % (add_url, word) - - content = publish_parts(page.data, writer_name='html')['html_body'] - content = wikiwords.sub(check, content) - edit_url = request.route_url('edit_page', pagename=pagename) - return dict(page=page, content=content, edit_url=edit_url, - logged_in=request.authenticated_userid) - -@view_config(route_name='add_page', renderer='templates/edit.pt', - permission='edit') -def add_page(request): - pagename = request.matchdict['pagename'] - if 'form.submitted' in request.params: - body = request.params['body'] - page = Page(name=pagename, data=body) - DBSession.add(page) - return HTTPFound(location = request.route_url('view_page', - pagename=pagename)) - save_url = request.route_url('add_page', pagename=pagename) - page = Page(name='', data='') - return dict(page=page, save_url=save_url, - logged_in=request.authenticated_userid) - -@view_config(route_name='edit_page', renderer='templates/edit.pt', - permission='edit') -def edit_page(request): - pagename = request.matchdict['pagename'] - page = DBSession.query(Page).filter_by(name=pagename).one() - if 'form.submitted' in request.params: - page.data = request.params['body'] - DBSession.add(page) - return HTTPFound(location = request.route_url('view_page', - pagename=pagename)) - return dict( - page=page, - save_url=request.route_url('edit_page', pagename=pagename), - logged_in=request.authenticated_userid - ) - -@view_config(route_name='login', renderer='templates/login.pt') -@forbidden_view_config(renderer='templates/login.pt') -def login(request): - login_url = request.route_url('login') - referrer = request.url - if referrer == login_url: - referrer = '/' # never use the login form itself as came_from - came_from = request.params.get('came_from', referrer) - message = '' - login = '' - password = '' - if 'form.submitted' in request.params: - login = request.params['login'] - password = request.params['password'] - if USERS.get(login) == password: - headers = remember(request, login) - return HTTPFound(location = came_from, - headers = headers) - message = 'Failed login' - - return dict( - message = message, - url = request.application_url + '/login', - came_from = came_from, - login = login, - password = password, - ) - -@view_config(route_name='logout') -def logout(request): - headers = forget(request) - return HTTPFound(location = request.route_url('view_wiki'), - headers = headers) - diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views/__init__.py b/docs/tutorials/wiki2/src/authorization/tutorial/views/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py b/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py new file mode 100644 index 000000000..f35f041a4 --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py @@ -0,0 +1,120 @@ +import cgi +import re +from docutils.core import publish_parts + +from pyramid.httpexceptions import ( + HTTPFound, + HTTPNotFound, + ) + +from pyramid.view import ( + view_config, + forbidden_view_config, + ) + +from pyramid.security import ( + remember, + forget, + ) + +from ..security.default import USERS + +from ..models.mymodel import Page + +# regular expression used to find WikiWords +wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)") + +@view_config(route_name='view_wiki', + permission='view') +def view_wiki(request): + return HTTPFound(location=request.route_url('view_page', + pagename='FrontPage')) + +@view_config(route_name='view_page', renderer='templates/view.jinja2', + permission='view') +def view_page(request): + pagename = request.matchdict['pagename'] + page = request.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 = request.dbsession.query(Page).filter_by(name=word).all() + if exists: + view_url = request.route_url('view_page', pagename=word) + return '%s' % (view_url, cgi.escape(word)) + else: + add_url = request.route_url('add_page', pagename=word) + return '%s' % (add_url, cgi.escape(word)) + + content = publish_parts(page.data, writer_name='html')['html_body'] + content = wikiwords.sub(check, content) + edit_url = request.route_url('edit_page', pagename=pagename) + return dict(page=page, content=content, edit_url=edit_url, + logged_in=request.authenticated_userid) + +@view_config(route_name='add_page', renderer='templates/edit.jinja2', + permission='edit') +def add_page(request): + pagename = request.matchdict['pagename'] + if 'form.submitted' in request.params: + body = request.params['body'] + page = Page(name=pagename, data=body) + request.dbsession.add(page) + return HTTPFound(location = request.route_url('view_page', + pagename=pagename)) + save_url = request.route_url('add_page', pagename=pagename) + page = Page(name='', data='') + return dict(page=page, save_url=save_url, + logged_in=request.authenticated_userid) + +@view_config(route_name='edit_page', renderer='templates/edit.jinja2', + permission='edit') +def edit_page(request): + pagename = request.matchdict['pagename'] + page = request.dbsession.query(Page).filter_by(name=pagename).one() + if 'form.submitted' in request.params: + page.data = request.params['body'] + request.dbsession.add(page) + return HTTPFound(location = request.route_url('view_page', + pagename=pagename)) + return dict( + page=page, + save_url = request.route_url('edit_page', pagename=pagename), + logged_in=request.authenticated_userid + ) + +@view_config(route_name='login', renderer='templates/login.jinja2') +@forbidden_view_config(renderer='templates/login.jinja2') +def login(request): + login_url = request.route_url('login') + referrer = request.url + if referrer == login_url: + referrer = '/' # never use the login form itself as came_from + came_from = request.params.get('came_from', referrer) + message = '' + login = '' + password = '' + if 'form.submitted' in request.params: + login = request.params['login'] + password = request.params['password'] + if USERS.get(login) == password: + headers = remember(request, login) + return HTTPFound(location = came_from, + headers = headers) + message = 'Failed login' + + return dict( + message = message, + url = request.application_url + '/login', + came_from = came_from, + login = login, + password = password, + ) + +@view_config(route_name='logout') +def logout(request): + headers = forget(request) + return HTTPFound(location = request.route_url('view_wiki'), + headers = headers) diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/edit.jinja2 b/docs/tutorials/wiki2/src/views/tutorial/templates/edit.jinja2 index ad4cd17e1..b3aadfc2e 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/templates/edit.jinja2 +++ b/docs/tutorials/wiki2/src/views/tutorial/templates/edit.jinja2 @@ -8,7 +8,7 @@ - {{page.name}} - Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki) + Edit{% if page.name %} {{page.name}}{% endif %} - Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki) -- cgit v1.2.3 From 6fcd98abe5bf951eb993b1a6b24126a5681221a5 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Mon, 16 Nov 2015 00:49:58 -0800 Subject: - update wiki2/design.rst to use jinja templates - minor grammar - .rst fixes - rewrap to 79 columns --- docs/tutorials/wiki2/design.rst | 180 +++++++++++++++++++--------------------- 1 file changed, 87 insertions(+), 93 deletions(-) diff --git a/docs/tutorials/wiki2/design.rst b/docs/tutorials/wiki2/design.rst index e9f361e7d..8e3bb4c13 100644 --- a/docs/tutorials/wiki2/design.rst +++ b/docs/tutorials/wiki2/design.rst @@ -1,10 +1,9 @@ -========== +====== Design -========== +====== -Following is a quick overview of the design of our wiki application, to help -us understand the changes that we will be making as we work through the -tutorial. +Following is a quick overview of the design of our wiki application to help us +understand the changes that we will be making as we work through the tutorial. Overall ------- @@ -17,17 +16,17 @@ Python module. We will add this module in the dependency list on the project Models ------ -We'll be using a SQLite database to hold our wiki data, and we'll be using +We'll be using an SQLite database to hold our wiki data, and we'll be using :term:`SQLAlchemy` to access the data in this database. Within the database, we define a single table named `pages`, whose elements will store the wiki pages. There are two columns: `name` and `data`. -URLs like ``/PageName`` will try to find an element in -the table that has a corresponding name. +URLs like ``/PageName`` will try to find an element in the table that has a +corresponding name. -To add a page to the wiki, a new row is created and the text -is stored in `data`. +To add a page to the wiki, a new row is created and the text is stored in +`data`. A page named ``FrontPage`` containing the text *This is the front page*, will be created when the storage is initialized, and will be used as the wiki home @@ -36,16 +35,14 @@ page. Views ----- -There will be three views to handle the normal operations of adding, -editing, and viewing wiki pages, plus one view for the wiki front page. -Two templates will be used, one for viewing, and one for both adding -and editing wiki pages. +There will be three views to handle the normal operations of adding, editing, +and viewing wiki pages, plus one view for the wiki front page. Two templates +will be used, one for viewing, and one for both adding and editing wiki pages. -The default templating systems in :app:`Pyramid` are -:term:`Chameleon` and :term:`Mako`. Chameleon is a variant of -:term:`ZPT`, which is an XML-based templating language. Mako is a -non-XML-based templating language. Because we had to pick one, -we chose Chameleon for this tutorial. +As of version 1.5 :app:`Pyramid` no longer ships with templating systems. In +this tutorial, we will use :term:`Jinja2`. Jinja2 is a modern and +designer-friendly templating language for Python, modeled after Django's +templates. Security -------- @@ -53,14 +50,14 @@ Security We'll eventually be adding security to our application. The components we'll use to do this are below. -- USERS, a dictionary mapping :term:`userids ` to their - corresponding passwords. +- USERS, a dictionary mapping :term:`userids ` to their corresponding + passwords. -- GROUPS, a dictionary mapping :term:`userids ` to a - list of groups to which they belong. +- GROUPS, a dictionary mapping :term:`userids ` to a list of groups to + which they belong. -- ``groupfinder``, an *authorization callback* that looks up USERS and - GROUPS. It will be provided in a new ``security.py`` file. +- ``groupfinder``, an *authorization callback* that looks up USERS and GROUPS. + It will be provided in a new ``security/default.py`` subpackage and file. - An :term:`ACL` is attached to the root :term:`resource`. Each row below details an :term:`ACE`: @@ -76,75 +73,72 @@ use to do this are below. - Permission declarations are added to the views to assert the security policies as each request is handled. -Two additional views and one template will handle the login and -logout tasks. +Two additional views and one template will handle the login and logout tasks. Summary ------- -The URL, actions, template and permission associated to each view are -listed in the following table: - -+----------------------+-----------------------+-------------+------------+------------+ -| URL | Action | View | Template | Permission | -| | | | | | -+======================+=======================+=============+============+============+ -| / | Redirect to | view_wiki | | | -| | /FrontPage | | | | -+----------------------+-----------------------+-------------+------------+------------+ -| /PageName | Display existing | view_page | view.pt | view | -| | page [2]_ | [1]_ | | | -| | | | | | -| | | | | | -| | | | | | -+----------------------+-----------------------+-------------+------------+------------+ -| /PageName/edit_page | Display edit form | edit_page | edit.pt | edit | -| | with existing | | | | -| | content. | | | | -| | | | | | -| | If the form was | | | | -| | submitted, redirect | | | | -| | to /PageName | | | | -+----------------------+-----------------------+-------------+------------+------------+ -| /add_page/PageName | Create the page | add_page | edit.pt | edit | -| | *PageName* in | | | | -| | storage, display | | | | -| | the edit form | | | | -| | without content. | | | | -| | | | | | -| | If the form was | | | | -| | submitted, | | | | -| | redirect to | | | | -| | /PageName | | | | -+----------------------+-----------------------+-------------+------------+------------+ -| /login | Display login form, | login | login.pt | | -| | Forbidden [3]_ | | | | -| | | | | | -| | If the form was | | | | -| | submitted, | | | | -| | authenticate. | | | | -| | | | | | -| | - If authentication | | | | -| | succeeds, | | | | -| | redirect to the | | | | -| | page that we | | | | -| | came from. | | | | -| | | | | | -| | - If authentication | | | | -| | fails, display | | | | -| | login form with | | | | -| | "login failed" | | | | -| | message. | | | | -| | | | | | -+----------------------+-----------------------+-------------+------------+------------+ -| /logout | Redirect to | logout | | | -| | /FrontPage | | | | -+----------------------+-----------------------+-------------+------------+------------+ - -.. [1] This is the default view for a Page context - when there is no view name. -.. [2] Pyramid will return a default 404 Not Found page - if the page *PageName* does not exist yet. -.. [3] ``pyramid.exceptions.Forbidden`` is reached when a - user tries to invoke a view that is - not authorized by the authorization policy. +The URL, actions, template, and permission associated to each view are listed +in the following table: + ++----------------------+-----------------------+-------------+----------------+------------+ +| URL | Action | View | Template | Permission | +| | | | | | ++======================+=======================+=============+================+============+ +| / | Redirect to | view_wiki | | | +| | /FrontPage | | | | ++----------------------+-----------------------+-------------+----------------+------------+ +| /PageName | Display existing | view_page | view.jinja2 | view | +| | page [2]_ | [1]_ | | | +| | | | | | +| | | | | | +| | | | | | ++----------------------+-----------------------+-------------+----------------+------------+ +| /PageName/edit_page | Display edit form | edit_page | edit.jinja2 | edit | +| | with existing | | | | +| | content. | | | | +| | | | | | +| | If the form was | | | | +| | submitted, redirect | | | | +| | to /PageName | | | | ++----------------------+-----------------------+-------------+----------------+------------+ +| /add_page/PageName | Create the page | add_page | edit.jinja2 | edit | +| | *PageName* in | | | | +| | storage, display | | | | +| | the edit form | | | | +| | without content. | | | | +| | | | | | +| | If the form was | | | | +| | submitted, | | | | +| | redirect to | | | | +| | /PageName | | | | ++----------------------+-----------------------+-------------+----------------+------------+ +| /login | Display login form, | login | login.jinja2 | | +| | Forbidden [3]_ | | | | +| | | | | | +| | If the form was | | | | +| | submitted, | | | | +| | authenticate. | | | | +| | | | | | +| | - If authentication | | | | +| | succeeds, | | | | +| | redirect to the | | | | +| | page that we | | | | +| | came from. | | | | +| | | | | | +| | - If authentication | | | | +| | fails, display | | | | +| | login form with | | | | +| | "login failed" | | | | +| | message. | | | | +| | | | | | ++----------------------+-----------------------+-------------+----------------+------------+ +| /logout | Redirect to | logout | | | +| | /FrontPage | | | | ++----------------------+-----------------------+-------------+----------------+------------+ + +.. [1] This is the default view for a Page context when there is no view name. +.. [2] Pyramid will return a default 404 Not Found page if the page *PageName* + does not exist yet. +.. [3] ``pyramid.exceptions.Forbidden`` is reached when a user tries to invoke + a view that is not authorized by the authorization policy. -- cgit v1.2.3 From fe4465b91296ca7d41c4f7c8006cbd8d50851a09 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Fri, 20 Nov 2015 17:48:38 -0800 Subject: wiki2/tests - commit progress --- docs/tutorials/wiki2/tests.rst | 77 +++++++++++++++++++++++++++++------------- 1 file changed, 53 insertions(+), 24 deletions(-) diff --git a/docs/tutorials/wiki2/tests.rst b/docs/tutorials/wiki2/tests.rst index 9db95334a..64025421f 100644 --- a/docs/tutorials/wiki2/tests.rst +++ b/docs/tutorials/wiki2/tests.rst @@ -2,25 +2,38 @@ Adding Tests ============ -We will now add tests for the models and the views and a few functional tests -in ``tests.py``. Tests ensure that an application works, and that it -continues to work when changes are made in the future. +We will now add tests for the models and views as well as a few functional +tests in a new ``tests`` subpackage. Tests ensure that an application works, +and that it continues to work when changes are made in the future. + +The file ``tests.py`` was generated as part of the ``alchemy`` scaffold, but it +is a common practice to put tests into a ``tests`` subpackage, especially as +projects grow in size and complexity. Each module in the test subpackage +should contain tests for its corresponding module in our application. Each +corresponding pair of modules should have the same names, except the test +module should have the prefix "test_". + +We will move parts of ``tests.py`` into appropriate new files in the ``tests`` +subpackage, and add several new tests. + Test the models =============== -To test the model class ``Page`` we'll add a new ``PageModelTests`` class to -our ``tests.py`` file that was generated as part of the ``alchemy`` scaffold. +To test the model class ``Page``, we'll add a new ``PageModelTests`` class to +a new file ``tests/test_models.py`` + Test the views ============== -We'll modify our ``tests.py`` file, adding tests for each view function we -added previously. As a result, we'll *delete* the ``ViewTests`` class that -the ``alchemy`` scaffold provided, and add four other test classes: -``ViewWikiTests``, ``ViewPageTests``, ``AddPageTests``, and ``EditPageTests``. -These test the ``view_wiki``, ``view_page``, ``add_page``, and ``edit_page`` -views. +We'll create a new ``tests/test_views.py`` file, adding tests for each view +function we previously added to our application. As a result, we'll *delete* +the ``ViewTests`` class that the ``alchemy`` scaffold provided, and add four +other test classes: ``ViewWikiTests``, ``ViewPageTests``, ``AddPageTests``, and +``EditPageTests``. These test the ``view_wiki``, ``view_page``, ``add_page``, +and ``edit_page`` views. + Functional tests ================ @@ -30,23 +43,41 @@ tested in the unit tests, like logging in, logging out, checking that the ``viewer`` user cannot add or edit pages, but the ``editor`` user can, and so on. -View the results of all our edits to ``tests.py`` -================================================= -Open the ``tutorial/tests.py`` module, and edit it such that it appears as +View the results of all our edits to ``tests`` subpackage +========================================================= + +Open ``tutorial/tests/test_models.py``, and edit it such that it appears as follows: -.. literalinclude:: src/tests/tutorial/tests.py +.. literalinclude:: src/tests/tutorial/tests/test_models.py :linenos: :language: python +Open ``tutorial/tests/test_views.py``, and edit it such that it appears as +follows: + +.. literalinclude:: src/tests/tutorial/tests/test_views.py + :linenos: + :language: python + +Open ``tutorial/tests/test_.py``, and edit it such that it appears as +follows: + +.. literalinclude:: src/tests/tutorial/tests/test_.py + :linenos: + :language: python + + Running the tests ================= We can run these tests by using ``setup.py test`` in the same way we did in -:ref:`running_tests`. However, first we must edit our ``setup.py`` to -include a dependency on WebTest, which we've used in our ``tests.py``. -Change the ``requires`` list in ``setup.py`` to include ``WebTest``. +:ref:`running_tests`. However, first we must edit our ``setup.py`` to include +a dependency on `WebTest +`_, which we've used +in our ``tests.py``. Change the ``requires`` list in ``setup.py`` to include +``WebTest``. .. literalinclude:: src/tests/setup.py :linenos: @@ -56,12 +87,11 @@ Change the ``requires`` list in ``setup.py`` to include ``WebTest``. After we've added a dependency on WebTest in ``setup.py``, we need to run ``setup.py develop`` to get WebTest installed into our virtualenv. Assuming -our shell's current working directory is the "tutorial" distribution -directory: +our shell's current working directory is the "tutorial" distribution directory: On UNIX: -.. code-block:: text +.. code-block:: bash $ $VENV/bin/python setup.py develop @@ -71,12 +101,11 @@ On Windows: c:\pyramidtut\tutorial> %VENV%\Scripts\python setup.py develop -Once that command has completed successfully, we can run the tests -themselves: +Once that command has completed successfully, we can run the tests themselves: On UNIX: -.. code-block:: text +.. code-block:: bash $ $VENV/bin/python setup.py test -q -- cgit v1.2.3 From 3fe24b103996bc5254a4734e15a5320da84bd76e Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Fri, 20 Nov 2015 19:00:44 -0800 Subject: spell out "specification" so that sphinx will not emit a warning for not being able to find "asset spec" --- pyramid/static.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyramid/static.py b/pyramid/static.py index c2c8c89e5..c7a5c7ba5 100644 --- a/pyramid/static.py +++ b/pyramid/static.py @@ -214,8 +214,8 @@ class ManifestCacheBuster(object): uses a supplied manifest file to map an asset path to a cache-busted version of the path. - The ``manifest_spec`` can be an absolute path or a :term:`asset spec` - pointing to a package-relative file. + The ``manifest_spec`` can be an absolute path or a :term:`asset + specification` pointing to a package-relative file. The manifest file is expected to conform to the following simple JSON format: -- cgit v1.2.3 From a3f0a397300aa290ec213760e11f85f40faa1bd7 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Fri, 20 Nov 2015 19:21:44 -0800 Subject: use intersphinx links to webob objects --- docs/designdefense.rst | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/docs/designdefense.rst b/docs/designdefense.rst index 1ed4f65a4..ee6d5a317 100644 --- a/docs/designdefense.rst +++ b/docs/designdefense.rst @@ -907,23 +907,22 @@ creating a more Zope3-like environment without much effort. .. _http_exception_hierarchy: -Pyramid Uses its Own HTTP Exception Class Hierarchy Rather Than ``webob.exc`` ------------------------------------------------------------------------------ +Pyramid uses its own HTTP exception class hierarchy rather than :mod:`webob.exc` +-------------------------------------------------------------------------------- .. versionadded:: 1.1 The HTTP exception classes defined in :mod:`pyramid.httpexceptions` are very -much like the ones defined in ``webob.exc`` -(e.g. :class:`~pyramid.httpexceptions.HTTPNotFound`, -:class:`~pyramid.httpexceptions.HTTPForbidden`, etc). They have the same -names and largely the same behavior and all have a very similar -implementation, but not the same identity. Here's why they have a separate -identity: +much like the ones defined in :mod:`webob.exc`, (e.g., +:class:`~pyramid.httpexceptions.HTTPNotFound` or +:class:`~pyramid.httpexceptions.HTTPForbidden`). They have the same names and +largely the same behavior, and all have a very similar implementation, but not +the same identity. Here's why they have a separate identity: - Making them separate allows the HTTP exception classes to subclass :class:`pyramid.response.Response`. This speeds up response generation - slightly due to the way the Pyramid router works. The same speedup could - be gained by monkeypatching ``webob.response.Response`` but it's usually + slightly due to the way the Pyramid router works. The same speedup could be + gained by monkeypatching :class:`webob.response.Response`, but it's usually the case that monkeypatching turns out to be evil and wrong. - Making them separate allows them to provide alternate ``__call__`` logic @@ -933,7 +932,7 @@ identity: value of ``RequestClass`` (:class:`pyramid.request.Request`). - Making them separate allows us freedom from having to think about backwards - compatibility code present in ``webob.exc`` having to do with Python 2.4, + compatibility code present in :mod:`webob.exc` having to do with Python 2.4, which we no longer support in Pyramid 1.1+. - We change the behavior of two classes @@ -944,9 +943,9 @@ identity: - Making them separate allows us to influence the docstrings of the exception classes to provide Pyramid-specific documentation. -- Making them separate allows us to silence a stupid deprecation warning - under Python 2.6 when the response objects are used as exceptions (related - to ``self.message``). +- Making them separate allows us to silence a stupid deprecation warning under + Python 2.6 when the response objects are used as exceptions (related to + ``self.message``). .. _simpler_traversal_model: -- cgit v1.2.3 From 84a168ef5fd4bdd487226f43b8c0e16237e82e18 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Fri, 20 Nov 2015 19:38:26 -0800 Subject: add optional step to update the Pyramid Dash docset --- RELEASING.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/RELEASING.txt b/RELEASING.txt index 87ff62c53..0ed9d5692 100644 --- a/RELEASING.txt +++ b/RELEASING.txt @@ -38,6 +38,9 @@ Releasing Pyramid - Change CHANGES.txt heading to reflect the new version number. +- (Optional) Update the Pyramid Dash docset. + https://github.com/Kapeli/Dash-User-Contributions/tree/master/docsets/Pyramid + - Make sure PyPI long description renders (requires ``collective.dist`` installed into your Python):: -- cgit v1.2.3 From ee9c620963553a3a959cdfc517f1e0818a21e9c0 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 23 Nov 2015 12:59:55 -0600 Subject: expose the PickleSerializer --- docs/api/session.rst | 1 + pyramid/session.py | 18 +++++++++++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/docs/api/session.rst b/docs/api/session.rst index dde9d20e9..474e2bb32 100644 --- a/docs/api/session.rst +++ b/docs/api/session.rst @@ -17,4 +17,5 @@ .. autofunction:: BaseCookieSessionFactory + .. autoclass:: PickleSerializer diff --git a/pyramid/session.py b/pyramid/session.py index fa85fe69c..51f9de620 100644 --- a/pyramid/session.py +++ b/pyramid/session.py @@ -133,13 +133,25 @@ def check_csrf_token(request, return True class PickleSerializer(object): - """ A Webob cookie serializer that uses the pickle protocol to dump Python - data to bytes.""" + """ A serializer that uses the pickle protocol to dump Python + data to bytes. + + This is the default serializer used by Pyramid. + + ``protocol`` may be specified to control the version of pickle used. + Defaults to :attr:`pickle.HIGHEST_PROTOCOL`. + + """ + def __init__(self, protocol=pickle.HIGHEST_PROTOCOL): + self.protocol = protocol + def loads(self, bstruct): + """Accept bytes and return a Python object.""" return pickle.loads(bstruct) def dumps(self, appstruct): - return pickle.dumps(appstruct, pickle.HIGHEST_PROTOCOL) + """Accept a Python object and return bytes.""" + return pickle.dumps(appstruct, self.protocol) def BaseCookieSessionFactory( serializer, -- cgit v1.2.3 From a708d359ff123084ea64b2e53c3ad32a74711219 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Wed, 25 Nov 2015 18:52:54 -0600 Subject: remove py2-docs from tox.ini and reorder so coverage is last --- HACKING.txt | 2 +- builddocs.sh | 2 +- tox.ini | 87 ++++++++++++++++++++++++++++-------------------------------- 3 files changed, 43 insertions(+), 48 deletions(-) diff --git a/HACKING.txt b/HACKING.txt index d0f9a769e..c838fda22 100644 --- a/HACKING.txt +++ b/HACKING.txt @@ -217,7 +217,7 @@ changed to reflect the bug fix, ideally in the same commit that fixes the bug or adds the feature. To build and review docs, use the following steps. 1. In the main Pyramid checkout directory, run ``./builddocs.sh`` (which just - turns around and runs ``tox -e py2-docs,py3-docs``):: + turns around and runs ``tox -e docs``):: $ ./builddocs.sh diff --git a/builddocs.sh b/builddocs.sh index eaf02fc1d..0859fe268 100755 --- a/builddocs.sh +++ b/builddocs.sh @@ -1,3 +1,3 @@ #!/bin/bash -tox -epy2-docs,py3-docs +tox -e docs diff --git a/tox.ini b/tox.ini index 20a9ee5b1..626931faf 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] envlist = - py26,py27,py32,py33,py34,py35,pypy,pypy3,pep8, - {py2,py3}-docs, + py26,py27,py32,py33,py34,py35,pypy,pypy3, + docs,pep8, {py2,py3}-cover,coverage, [testenv] @@ -23,49 +23,6 @@ commands = pip install pyramid[testing] nosetests --with-xunit --xunit-file=nosetests-{envname}.xml {posargs:} -# we separate coverage into its own testenv because a) "last run wins" wrt -# cobertura jenkins reporting and b) pypy and jython can't handle any -# combination of versions of coverage and nosexcover that i can find. -[testenv:py2-cover] -commands = - pip install pyramid[testing] - coverage run --source=pyramid {envbindir}/nosetests - coverage xml -o coverage-py2.xml -setenv = - COVERAGE_FILE=.coverage.py2 - -[testenv:py3-cover] -commands = - pip install pyramid[testing] - coverage run --source=pyramid {envbindir}/nosetests - coverage xml -o coverage-py3.xml -setenv = - COVERAGE_FILE=.coverage.py3 - -[testenv:coverage] -basepython = python3.4 -commands = - coverage erase - coverage combine - coverage xml - coverage report --show-missing --fail-under=100 -deps = - coverage -setenv = - COVERAGE_FILE=.coverage - -[testenv:py2-docs] -whitelist_externals = make -commands = - pip install pyramid[docs] - make -C docs html epub BUILDDIR={envdir} "SPHINXOPTS=-W -E" - -[testenv:py3-docs] -whitelist_externals = make -commands = - pip install pyramid[docs] - make -C docs html epub BUILDDIR={envdir} "SPHINXOPTS=-W -E" - [testenv:py26-scaffolds] basepython = python2.6 commands = @@ -109,8 +66,46 @@ commands = deps = virtualenv [testenv:pep8] -basepython = python3.4 +basepython = python3.5 commands = flake8 pyramid/ deps = flake8 + +[testenv:docs] +basepython = python3.5 +whitelist_externals = make +commands = + pip install pyramid[docs] + make -C docs html epub BUILDDIR={envdir} "SPHINXOPTS=-W -E" + +# we separate coverage into its own testenv because a) "last run wins" wrt +# cobertura jenkins reporting and b) pypy and jython can't handle any +# combination of versions of coverage and nosexcover that i can find. +[testenv:py2-cover] +commands = + pip install pyramid[testing] + coverage run --source=pyramid {envbindir}/nosetests + coverage xml -o coverage-py2.xml +setenv = + COVERAGE_FILE=.coverage.py2 + +[testenv:py3-cover] +commands = + pip install pyramid[testing] + coverage run --source=pyramid {envbindir}/nosetests + coverage xml -o coverage-py3.xml +setenv = + COVERAGE_FILE=.coverage.py3 + +[testenv:coverage] +basepython = python3.5 +commands = + coverage erase + coverage combine + coverage xml + coverage report --show-missing --fail-under=100 +deps = + coverage +setenv = + COVERAGE_FILE=.coverage -- cgit v1.2.3 From 0030fba497a48e596167ceffb6dd499d67c91765 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Wed, 25 Nov 2015 18:54:39 -0600 Subject: add docs to travis builds --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 79d9fa09d..2163eb8fd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,6 +22,8 @@ matrix: env: TOXENV=pypy3 - python: 3.5 env: TOXENV=py2-cover,py3-cover,coverage + - python: 3.5 + env: TOXENV=docs - python: 3.5 env: TOXENV=pep8 -- cgit v1.2.3 From a398ea813923040eea7cb9bcaced55b982149708 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sat, 28 Nov 2015 19:46:34 -0800 Subject: revert optional step for Dash docset --- RELEASING.txt | 3 --- 1 file changed, 3 deletions(-) diff --git a/RELEASING.txt b/RELEASING.txt index 0ed9d5692..87ff62c53 100644 --- a/RELEASING.txt +++ b/RELEASING.txt @@ -38,9 +38,6 @@ Releasing Pyramid - Change CHANGES.txt heading to reflect the new version number. -- (Optional) Update the Pyramid Dash docset. - https://github.com/Kapeli/Dash-User-Contributions/tree/master/docsets/Pyramid - - Make sure PyPI long description renders (requires ``collective.dist`` installed into your Python):: -- cgit v1.2.3 From 6e29b425182ccc4abc87fcfb32e20b60b15d4bdf Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 1 Dec 2015 01:59:14 -0600 Subject: initial work on config.add_cache_buster --- pyramid/config/views.py | 108 ++++++++++++++++++-------------- pyramid/interfaces.py | 45 ++++--------- pyramid/static.py | 10 ++- pyramid/tests/test_config/test_views.py | 66 +++++++++---------- 4 files changed, 106 insertions(+), 123 deletions(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index e386bc4e1..67a70145c 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1,3 +1,4 @@ +import bisect import inspect import operator import os @@ -1855,18 +1856,7 @@ class ViewsConfiguratorMixin(object): ``Expires`` and ``Cache-Control`` headers for static assets served. Note that this argument has no effect when the ``name`` is a *url prefix*. By default, this argument is ``None``, meaning that no - particular Expires or Cache-Control headers are set in the response, - unless ``cachebust`` is specified. - - The ``cachebust`` keyword argument may be set to cause - :meth:`~pyramid.request.Request.static_url` to use cache busting when - generating URLs. See :ref:`cache_busting` for general information - about cache busting. The value of the ``cachebust`` argument must - be an object which implements - :class:`~pyramid.interfaces.ICacheBuster`. If the ``cachebust`` - argument is provided, the default for ``cache_max_age`` is modified - to be ten years. ``cache_max_age`` may still be explicitly provided - to override this default. + particular Expires or Cache-Control headers are set in the response. The ``permission`` keyword argument is used to specify the :term:`permission` required by a user to execute the static view. By @@ -1946,11 +1936,32 @@ class ViewsConfiguratorMixin(object): See :ref:`static_assets_section` for more information. """ spec = self._make_spec(path) + info = self._get_static_info() + info.add(self, name, spec, **kw) + + def add_cache_buster(self, path, cachebust): + """ + The ``cachebust`` keyword argument may be set to cause + :meth:`~pyramid.request.Request.static_url` to use cache busting when + generating URLs. See :ref:`cache_busting` for general information + about cache busting. The value of the ``cachebust`` argument must + be an object which implements + :class:`~pyramid.interfaces.ICacheBuster`. If the ``cachebust`` + argument is provided, the default for ``cache_max_age`` is modified + to be ten years. ``cache_max_age`` may still be explicitly provided + to override this default. + + """ + spec = self._make_spec(path) + info = self._get_static_info() + info.add_cache_buster(self, spec, cachebust) + + def _get_static_info(self): info = self.registry.queryUtility(IStaticURLInfo) if info is None: info = StaticURLInfo() self.registry.registerUtility(info, IStaticURLInfo) - info.add(self, name, spec, **kw) + return info def isexception(o): if IInterface.providedBy(o): @@ -1964,26 +1975,18 @@ def isexception(o): @implementer(IStaticURLInfo) class StaticURLInfo(object): - def _get_registrations(self, registry): - try: - reg = registry._static_url_registrations - except AttributeError: - reg = registry._static_url_registrations = [] - return reg + def __init__(self): + self.registrations = [] + self.cache_busters = [] def generate(self, path, request, **kw): - try: - registry = request.registry - except AttributeError: # bw compat (for tests) - registry = get_current_registry() - registrations = self._get_registrations(registry) - for (url, spec, route_name, cachebust) in registrations: + for (url, spec, route_name) in self.registrations: if path.startswith(spec): subpath = path[len(spec):] if WIN: # pragma: no cover subpath = subpath.replace('\\', '/') # windows - if cachebust: - subpath, kw = cachebust(subpath, kw) + # translate spec into overridden spec and lookup cache buster + # to modify subpath, kw if url is None: kw['subpath'] = subpath return request.route_url(route_name, **kw) @@ -2023,19 +2026,6 @@ class StaticURLInfo(object): # make sure it ends with a slash name = name + '/' - if config.registry.settings.get('pyramid.prevent_cachebust'): - cb = None - else: - cb = extra.pop('cachebust', None) - if cb: - def cachebust(subpath, kw): - subpath_tuple = tuple(subpath.split('/')) - subpath_tuple, kw = cb.pregenerate( - spec + subpath, subpath_tuple, kw) - return '/'.join(subpath_tuple), kw - else: - cachebust = None - if url_parse(name).netloc: # it's a URL # url, spec, route_name @@ -2044,14 +2034,11 @@ class StaticURLInfo(object): else: # it's a view name url = None - ten_years = 10 * 365 * 24 * 60 * 60 # more or less - default = ten_years if cb else None - cache_max_age = extra.pop('cache_max_age', default) + cache_max_age = extra.pop('cache_max_age', None) # create a view - cb_match = getattr(cb, 'match', None) view = static_view(spec, cache_max_age=cache_max_age, - use_subpath=True, cachebust_match=cb_match) + use_subpath=True) # Mutate extra to allow factory, etc to be passed through here. # Treat permission specially because we'd like to default to @@ -2083,7 +2070,7 @@ class StaticURLInfo(object): ) def register(): - registrations = self._get_registrations(config.registry) + registrations = self.registrations names = [t[0] for t in registrations] @@ -2092,7 +2079,7 @@ class StaticURLInfo(object): registrations.pop(idx) # url, spec, route_name - registrations.append((url, spec, route_name, cachebust)) + registrations.append((url, spec, route_name)) intr = config.introspectable('static views', name, @@ -2102,3 +2089,30 @@ class StaticURLInfo(object): intr['spec'] = spec config.action(None, callable=register, introspectables=(intr,)) + + def add_cache_buster(self, config, spec, cachebust): + def register(): + cache_busters = self.cache_busters + + specs = [t[0] for t in cache_busters] + if spec in specs: + idx = specs.index(spec) + cache_busters.pop(idx) + + lengths = [len(t[0]) for t in cache_busters] + new_idx = bisect.bisect_left(lengths, len(spec)) + cache_busters.insert(new_idx, (spec, cachebust)) + + intr = config.introspectable('cache busters', + spec, + 'cache buster for %r' % spec, + 'cache buster') + intr['cachebust'] = cachebust + intr['spec'] = spec + + config.action(None, callable=register, introspectables=(intr,)) + + def _find_cache_buster(self, registry, spec): + for base_spec, cachebust in self.cache_busters: + if base_spec.startswith(spec): + pass diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index 90534593c..bdf5bdfbe 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -584,6 +584,9 @@ class IStaticURLInfo(Interface): def generate(path, request, **kw): """ Generate a URL for the given path """ + def add_cache_buster(config, spec, cache_buster): + """ Add a new cache buster to a particular set of assets """ + class IResponseFactory(Interface): """ A utility which generates a response """ def __call__(request): @@ -1186,45 +1189,23 @@ class IPredicateList(Interface): class ICacheBuster(Interface): """ - Instances of ``ICacheBuster`` may be provided as arguments to - :meth:`~pyramid.config.Configurator.add_static_view`. Instances of - ``ICacheBuster`` provide mechanisms for generating a cache bust token for - a static asset, modifying a static asset URL to include a cache bust token, - and, optionally, unmodifying a static asset URL in order to look up an - asset. See :ref:`cache_busting`. + A cache buster modifies the URL generation machinery for + :meth:`~pyramid.request.Request.static_url`. See :ref:`cache_busting`. .. versionadded:: 1.6 """ - def pregenerate(pathspec, subpath, kw): + def __call__(pathspec, subpath, kw): """ Modifies a subpath and/or keyword arguments from which a static asset URL will be computed during URL generation. The ``pathspec`` argument is the path specification for the resource to be cache busted. - The ``subpath`` argument is a tuple of path elements that represent the - portion of the asset URL which is used to find the asset. The ``kw`` - argument is a dict of keywords that are to be passed eventually to - :meth:`~pyramid.request.Request.route_url` for URL generation. The - return value should be a two-tuple of ``(subpath, kw)`` which are - versions of the same arguments modified to include the cache bust token - in the generated URL. - """ - - def match(subpath): - """ - Performs the logical inverse of - :meth:`~pyramid.interfaces.ICacheBuster.pregenerate` by taking a - subpath from a cache busted URL and removing the cache bust token, so - that :app:`Pyramid` can find the underlying asset. - - ``subpath`` is the subpath portion of the URL for an incoming request - for a static asset. The return value should be the same tuple with the - cache busting token elided. - - If the cache busting scheme in use doesn't specifically modify the path - portion of the generated URL (e.g. it adds a query string), a method - which implements this interface may not be necessary. It is - permissible for an instance of - :class:`~pyramid.interfaces.ICacheBuster` to omit this method. + The ``subpath`` argument is a path of ``/``-delimited segments that + represent the portion of the asset URL which is used to find the asset. + The ``kw`` argument is a dict of keywords that are to be passed + eventually to :meth:`~pyramid.request.Request.static_url` for URL + generation. The return value should be a two-tuple of + ``(subpath, kw)`` which are versions of the same arguments modified + to include the cache bust token in the generated URL. """ # configuration phases: a lower phase number means the actions associated diff --git a/pyramid/static.py b/pyramid/static.py index c7a5c7ba5..cda98bea4 100644 --- a/pyramid/static.py +++ b/pyramid/static.py @@ -179,7 +179,7 @@ class QueryStringCacheBuster(object): def __init__(self, param='x'): self.param = param - def pregenerate(self, pathspec, subpath, kw): + def __call__(self, pathspec, subpath, kw): token = self.tokenize(pathspec) query = kw.setdefault('_query', {}) if isinstance(query, dict): @@ -289,8 +289,6 @@ class ManifestCacheBuster(object): self._mtime = mtime return self._manifest - def pregenerate(self, pathspec, subpath, kw): - path = '/'.join(subpath) - path = self.manifest.get(path, path) - new_subpath = path.split('/') - return (new_subpath, kw) + def __call__(self, pathspec, subpath, kw): + subpath = self.manifest.get(subpath, subpath) + return (subpath, kw) diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index acfb81962..020ed131d 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -3865,22 +3865,11 @@ class TestStaticURLInfo(unittest.TestCase): def _makeOne(self): return self._getTargetClass()() - def _makeConfig(self, registrations=None): - config = DummyConfig() - registry = DummyRegistry() - if registrations is not None: - registry._static_url_registrations = registrations - config.registry = registry - return config - def _makeRequest(self): request = DummyRequest() request.registry = DummyRegistry() return request - def _assertRegistrations(self, config, expected): - self.assertEqual(config.registry._static_url_registrations, expected) - def test_verifyClass(self): from pyramid.interfaces import IStaticURLInfo from zope.interface.verify import verifyClass @@ -4002,12 +3991,12 @@ class TestStaticURLInfo(unittest.TestCase): 'http://example.com/abc%20def#La%20Pe%C3%B1a') def test_generate_url_cachebust(self): - def cachebust(subpath, kw): + def cachebust(request, subpath, kw): kw['foo'] = 'bar' return 'foo' + '/' + subpath, kw inst = self._makeOne() - registrations = [(None, 'package:path/', '__viewname', cachebust)] - inst._get_registrations = lambda *x: registrations + inst.registrations = [(None, 'package:path/', '__viewname', cachebust)] + inst.cache_busters = [('package:path/', cachebust)] request = self._makeRequest() def route_url(n, **kw): self.assertEqual(n, '__viewname') @@ -4016,88 +4005,88 @@ class TestStaticURLInfo(unittest.TestCase): inst.generate('package:path/abc', request) def test_add_already_exists(self): + config = DummyConfig() inst = self._makeOne() - config = self._makeConfig( - [('http://example.com/', 'package:path/', None)]) + inst.registrations = [('http://example.com/', 'package:path/', None)] inst.add(config, 'http://example.com', 'anotherpackage:path') expected = [ - ('http://example.com/', 'anotherpackage:path/', None, None)] - self._assertRegistrations(config, expected) + ('http://example.com/', 'anotherpackage:path/', None, None)] + self.assertEqual(inst.registrations, expected) def test_add_package_root(self): + config = DummyConfig() inst = self._makeOne() - config = self._makeConfig() inst.add(config, 'http://example.com', 'package:') - expected = [('http://example.com/', 'package:', None, None)] - self._assertRegistrations(config, expected) + expected = [('http://example.com/', 'package:', None, None)] + self.assertEqual(inst.registrations, expected) def test_add_url_withendslash(self): + config = DummyConfig() inst = self._makeOne() - config = self._makeConfig() inst.add(config, 'http://example.com/', 'anotherpackage:path') expected = [ ('http://example.com/', 'anotherpackage:path/', None, None)] - self._assertRegistrations(config, expected) + self.assertEqual(inst.registrations, expected) def test_add_url_noendslash(self): + config = DummyConfig() inst = self._makeOne() - config = self._makeConfig() inst.add(config, 'http://example.com', 'anotherpackage:path') expected = [ ('http://example.com/', 'anotherpackage:path/', None, None)] - self._assertRegistrations(config, expected) + self.assertEqual(inst.registrations, expected) def test_add_url_noscheme(self): + config = DummyConfig() inst = self._makeOne() - config = self._makeConfig() inst.add(config, '//example.com', 'anotherpackage:path') expected = [('//example.com/', 'anotherpackage:path/', None, None)] - self._assertRegistrations(config, expected) + self.assertEqual(inst.registrations, expected) def test_add_viewname(self): from pyramid.security import NO_PERMISSION_REQUIRED from pyramid.static import static_view - config = self._makeConfig() + config = DummyConfig() inst = self._makeOne() inst.add(config, 'view', 'anotherpackage:path', cache_max_age=1) expected = [(None, 'anotherpackage:path/', '__view/', None)] - self._assertRegistrations(config, expected) + self.assertEqual(inst.registrations, expected) self.assertEqual(config.route_args, ('__view/', 'view/*subpath')) self.assertEqual(config.view_kw['permission'], NO_PERMISSION_REQUIRED) self.assertEqual(config.view_kw['view'].__class__, static_view) def test_add_viewname_with_route_prefix(self): - config = self._makeConfig() + config = DummyConfig() config.route_prefix = '/abc' inst = self._makeOne() inst.add(config, 'view', 'anotherpackage:path',) expected = [(None, 'anotherpackage:path/', '__/abc/view/', None)] - self._assertRegistrations(config, expected) + self.assertEqual(inst.registrations, expected) self.assertEqual(config.route_args, ('__/abc/view/', 'view/*subpath')) def test_add_viewname_with_permission(self): - config = self._makeConfig() + config = DummyConfig() inst = self._makeOne() inst.add(config, 'view', 'anotherpackage:path', cache_max_age=1, permission='abc') self.assertEqual(config.view_kw['permission'], 'abc') def test_add_viewname_with_context(self): - config = self._makeConfig() + config = DummyConfig() inst = self._makeOne() inst.add(config, 'view', 'anotherpackage:path', cache_max_age=1, context=DummyContext) self.assertEqual(config.view_kw['context'], DummyContext) def test_add_viewname_with_for_(self): - config = self._makeConfig() + config = DummyConfig() inst = self._makeOne() inst.add(config, 'view', 'anotherpackage:path', cache_max_age=1, for_=DummyContext) self.assertEqual(config.view_kw['context'], DummyContext) def test_add_viewname_with_renderer(self): - config = self._makeConfig() + config = DummyConfig() inst = self._makeOne() inst.add(config, 'view', 'anotherpackage:path', cache_max_age=1, renderer='mypackage:templates/index.pt') @@ -4105,7 +4094,7 @@ class TestStaticURLInfo(unittest.TestCase): 'mypackage:templates/index.pt') def test_add_cachebust_prevented(self): - config = self._makeConfig() + config = DummyConfig() config.registry.settings['pyramid.prevent_cachebust'] = True inst = self._makeOne() inst.add(config, 'view', 'mypackage:path', cachebust=True) @@ -4113,7 +4102,7 @@ class TestStaticURLInfo(unittest.TestCase): self.assertEqual(cachebust, None) def test_add_cachebust_custom(self): - config = self._makeConfig() + config = DummyConfig() inst = self._makeOne() inst.add(config, 'view', 'mypackage:path', cachebust=DummyCacheBuster('foo')) @@ -4236,7 +4225,8 @@ class DummyMultiView: class DummyCacheBuster(object): def __init__(self, token): self.token = token - def pregenerate(self, pathspec, subpath, kw): + + def __call__(self, pathspec, subpath, kw): kw['x'] = self.token return subpath, kw -- cgit v1.2.3 From 9b12c01168cb756ec36351d7414cad95e87f6581 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Tue, 1 Dec 2015 23:18:00 -0700 Subject: Add documentation why we add a naming convention --- docs/quick_tour/sqla_demo/sqla_demo/models/meta.py | 3 +++ pyramid/scaffolds/alchemy/+package+/models/meta.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/docs/quick_tour/sqla_demo/sqla_demo/models/meta.py b/docs/quick_tour/sqla_demo/sqla_demo/models/meta.py index b72b45f9f..80ececd8c 100644 --- a/docs/quick_tour/sqla_demo/sqla_demo/models/meta.py +++ b/docs/quick_tour/sqla_demo/sqla_demo/models/meta.py @@ -4,6 +4,9 @@ from sqlalchemy.orm import sessionmaker from sqlalchemy.schema import MetaData import zope.sqlalchemy +# Recommended naming convention used by Alembic, as various different database +# providers will autogenerate vastly different names making migrations more +# difficult. See: http://alembic.readthedocs.org/en/latest/naming.html NAMING_CONVENTION = { "ix": 'ix_%(column_0_label)s', "uq": "uq_%(table_name)s_%(column_0_name)s", diff --git a/pyramid/scaffolds/alchemy/+package+/models/meta.py b/pyramid/scaffolds/alchemy/+package+/models/meta.py index b72b45f9f..80ececd8c 100644 --- a/pyramid/scaffolds/alchemy/+package+/models/meta.py +++ b/pyramid/scaffolds/alchemy/+package+/models/meta.py @@ -4,6 +4,9 @@ from sqlalchemy.orm import sessionmaker from sqlalchemy.schema import MetaData import zope.sqlalchemy +# Recommended naming convention used by Alembic, as various different database +# providers will autogenerate vastly different names making migrations more +# difficult. See: http://alembic.readthedocs.org/en/latest/naming.html NAMING_CONVENTION = { "ix": 'ix_%(column_0_label)s', "uq": "uq_%(table_name)s_%(column_0_name)s", -- cgit v1.2.3 From f10fb24a3663cb070b28b8020fde957d16563400 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Wed, 2 Dec 2015 04:18:10 -0800 Subject: - rewrite wiki2/tests.rst (removed an obsolete reference to testing models, per 2885a7b96545c037109d7999319f74869a640050) - add wiki2/src/tests/ files. special thanks to @ppaez --- docs/tutorials/wiki2/src/tests/development.ini | 4 +- docs/tutorials/wiki2/src/tests/production.ini | 14 +- docs/tutorials/wiki2/src/tests/setup.py | 4 +- .../tutorials/wiki2/src/tests/tutorial/__init__.py | 18 +- docs/tutorials/wiki2/src/tests/tutorial/models.py | 37 ---- .../wiki2/src/tests/tutorial/models/__init__.py | 7 + .../wiki2/src/tests/tutorial/models/meta.py | 46 ++++ .../wiki2/src/tests/tutorial/models/mymodel.py | 26 +++ .../src/tests/tutorial/scripts/initializedb.py | 25 ++- .../tutorials/wiki2/src/tests/tutorial/security.py | 7 - .../wiki2/src/tests/tutorial/security/__init__.py | 1 + .../wiki2/src/tests/tutorial/security/default.py | 7 + .../wiki2/src/tests/tutorial/static/theme.min.css | 2 +- .../wiki2/src/tests/tutorial/templates/edit.jinja2 | 73 +++++++ .../wiki2/src/tests/tutorial/templates/edit.pt | 74 ------- .../src/tests/tutorial/templates/layout.jinja2 | 66 ++++++ .../src/tests/tutorial/templates/login.jinja2 | 74 +++++++ .../wiki2/src/tests/tutorial/templates/login.pt | 54 ----- .../src/tests/tutorial/templates/mytemplate.jinja2 | 8 + .../src/tests/tutorial/templates/mytemplate.pt | 66 ------ .../wiki2/src/tests/tutorial/templates/view.jinja2 | 71 +++++++ .../wiki2/src/tests/tutorial/templates/view.pt | 74 ------- docs/tutorials/wiki2/src/tests/tutorial/tests.py | 235 --------------------- .../wiki2/src/tests/tutorial/tests/__init__.py | 1 + .../src/tests/tutorial/tests/test_functional.py | 141 +++++++++++++ .../wiki2/src/tests/tutorial/tests/test_views.py | 168 +++++++++++++++ docs/tutorials/wiki2/src/tests/tutorial/views.py | 123 ----------- .../wiki2/src/tests/tutorial/views/__init__.py | 0 .../wiki2/src/tests/tutorial/views/default.py | 120 +++++++++++ docs/tutorials/wiki2/tests.rst | 26 +-- 30 files changed, 851 insertions(+), 721 deletions(-) delete mode 100644 docs/tutorials/wiki2/src/tests/tutorial/models.py create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/models/__init__.py create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/models/meta.py create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/models/mymodel.py delete mode 100644 docs/tutorials/wiki2/src/tests/tutorial/security.py create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/security/__init__.py create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/security/default.py create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/templates/edit.jinja2 delete mode 100644 docs/tutorials/wiki2/src/tests/tutorial/templates/edit.pt create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/templates/layout.jinja2 create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/templates/login.jinja2 delete mode 100644 docs/tutorials/wiki2/src/tests/tutorial/templates/login.pt create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/templates/mytemplate.jinja2 delete mode 100644 docs/tutorials/wiki2/src/tests/tutorial/templates/mytemplate.pt create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/templates/view.jinja2 delete mode 100644 docs/tutorials/wiki2/src/tests/tutorial/templates/view.pt delete mode 100644 docs/tutorials/wiki2/src/tests/tutorial/tests.py create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/tests/__init__.py create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/tests/test_views.py delete mode 100644 docs/tutorials/wiki2/src/tests/tutorial/views.py create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/views/__init__.py create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/views/default.py diff --git a/docs/tutorials/wiki2/src/tests/development.ini b/docs/tutorials/wiki2/src/tests/development.ini index a9d53b296..99c4ff0fe 100644 --- a/docs/tutorials/wiki2/src/tests/development.ini +++ b/docs/tutorials/wiki2/src/tests/development.ini @@ -27,7 +27,7 @@ sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite [server:main] use = egg:waitress#main -host = 0.0.0.0 +host = 127.0.0.1 port = 6543 ### @@ -68,4 +68,4 @@ level = NOTSET formatter = generic [formatter_generic] -format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/tutorials/wiki2/src/tests/production.ini b/docs/tutorials/wiki2/src/tests/production.ini index 4684d2f7a..97acfbd7d 100644 --- a/docs/tutorials/wiki2/src/tests/production.ini +++ b/docs/tutorials/wiki2/src/tests/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 @@ -51,6 +59,4 @@ level = NOTSET formatter = generic [formatter_generic] -format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s - -# End logging configuration +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/tutorials/wiki2/src/tests/setup.py b/docs/tutorials/wiki2/src/tests/setup.py index d8486e462..f640b4399 100644 --- a/docs/tutorials/wiki2/src/tests/setup.py +++ b/docs/tutorials/wiki2/src/tests/setup.py @@ -10,7 +10,7 @@ with open(os.path.join(here, 'CHANGES.txt')) as f: requires = [ 'pyramid', - 'pyramid_chameleon', + 'pyramid_jinja2', 'pyramid_debugtoolbar', 'pyramid_tm', 'SQLAlchemy', @@ -18,7 +18,7 @@ requires = [ 'zope.sqlalchemy', 'waitress', 'docutils', - 'WebTest', # add this + 'WebTest', ] setup(name='tutorial', diff --git a/docs/tutorials/wiki2/src/tests/tutorial/__init__.py b/docs/tutorials/wiki2/src/tests/tutorial/__init__.py index cee89184b..084fee19f 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/__init__.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/__init__.py @@ -2,30 +2,20 @@ from pyramid.config import Configurator from pyramid.authentication import AuthTktAuthenticationPolicy from pyramid.authorization import ACLAuthorizationPolicy -from sqlalchemy import engine_from_config - -from tutorial.security import groupfinder - -from .models import ( - DBSession, - Base, - ) - +from security.default import groupfinder def main(global_config, **settings): """ This function returns a Pyramid WSGI application. """ - engine = engine_from_config(settings, 'sqlalchemy.') - DBSession.configure(bind=engine) - Base.metadata.bind = engine authn_policy = AuthTktAuthenticationPolicy( 'sosecret', callback=groupfinder, hashalg='sha512') authz_policy = ACLAuthorizationPolicy() config = Configurator(settings=settings, - root_factory='tutorial.models.RootFactory') - config.include('pyramid_chameleon') + root_factory='tutorial.models.mymodel.RootFactory') config.set_authentication_policy(authn_policy) config.set_authorization_policy(authz_policy) + config.include('pyramid_jinja2') + config.include('.models.meta') config.add_static_view('static', 'static', cache_max_age=3600) config.add_route('view_wiki', '/') config.add_route('login', '/login') diff --git a/docs/tutorials/wiki2/src/tests/tutorial/models.py b/docs/tutorials/wiki2/src/tests/tutorial/models.py deleted file mode 100644 index 4f7e1e024..000000000 --- a/docs/tutorials/wiki2/src/tests/tutorial/models.py +++ /dev/null @@ -1,37 +0,0 @@ -from pyramid.security import ( - Allow, - Everyone, - ) - -from sqlalchemy import ( - Column, - Integer, - Text, - ) - -from sqlalchemy.ext.declarative import declarative_base - -from sqlalchemy.orm import ( - scoped_session, - sessionmaker, - ) - -from zope.sqlalchemy import ZopeTransactionExtension - -DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension())) -Base = declarative_base() - - -class Page(Base): - """ The SQLAlchemy declarative model class for a Page object. """ - __tablename__ = 'pages' - id = Column(Integer, primary_key=True) - name = Column(Text, unique=True) - data = Column(Text) - - -class RootFactory(object): - __acl__ = [ (Allow, Everyone, 'view'), - (Allow, 'group:editors', 'edit') ] - def __init__(self, request): - pass diff --git a/docs/tutorials/wiki2/src/tests/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/tests/tutorial/models/__init__.py new file mode 100644 index 000000000..7b1c62867 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/models/__init__.py @@ -0,0 +1,7 @@ +from sqlalchemy.orm import configure_mappers +# import all models classes here for sqlalchemy mappers +# to pick up +from .mymodel import Page # flake8: noqa + +# run configure mappers to ensure we avoid any race conditions +configure_mappers() diff --git a/docs/tutorials/wiki2/src/tests/tutorial/models/meta.py b/docs/tutorials/wiki2/src/tests/tutorial/models/meta.py new file mode 100644 index 000000000..b72b45f9f --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/models/meta.py @@ -0,0 +1,46 @@ +from sqlalchemy import engine_from_config +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker +from sqlalchemy.schema import MetaData +import zope.sqlalchemy + +NAMING_CONVENTION = { + "ix": 'ix_%(column_0_label)s', + "uq": "uq_%(table_name)s_%(column_0_name)s", + "ck": "ck_%(table_name)s_%(constraint_name)s", + "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", + "pk": "pk_%(table_name)s" +} + +metadata = MetaData(naming_convention=NAMING_CONVENTION) +Base = declarative_base(metadata=metadata) + + +def includeme(config): + settings = config.get_settings() + dbmaker = get_dbmaker(get_engine(settings)) + + config.add_request_method( + lambda r: get_session(r.tm, dbmaker), + 'dbsession', + reify=True + ) + + config.include('pyramid_tm') + + +def get_session(transaction_manager, dbmaker): + dbsession = dbmaker() + zope.sqlalchemy.register(dbsession, + transaction_manager=transaction_manager) + return dbsession + + +def get_engine(settings, prefix='sqlalchemy.'): + return engine_from_config(settings, prefix) + + +def get_dbmaker(engine): + dbmaker = sessionmaker() + dbmaker.configure(bind=engine) + return dbmaker diff --git a/docs/tutorials/wiki2/src/tests/tutorial/models/mymodel.py b/docs/tutorials/wiki2/src/tests/tutorial/models/mymodel.py new file mode 100644 index 000000000..03e2f90ca --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/models/mymodel.py @@ -0,0 +1,26 @@ +from .meta import Base + +from pyramid.security import ( + Allow, + Everyone, + ) + +from sqlalchemy import ( + Column, + Integer, + Text, + ) + + +class Page(Base): + """ The SQLAlchemy declarative model class for a Page object. """ + __tablename__ = 'pages' + id = Column(Integer, primary_key=True) + name = Column(Text, unique=True) + data = Column(Integer) + +class RootFactory(object): + __acl__ = [ (Allow, Everyone, 'view'), + (Allow, 'group:editors', 'edit') ] + def __init__(self, request): + pass \ No newline at end of file diff --git a/docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py index 23a5f13f4..4aac4a848 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py @@ -2,36 +2,41 @@ import os import sys import transaction -from sqlalchemy import engine_from_config - from pyramid.paster import ( get_appsettings, setup_logging, ) -from ..models import ( - DBSession, - Page, +from ..models.meta import ( Base, + get_session, + get_engine, + get_dbmaker, ) +from ..models.mymodel import Page def usage(argv): cmd = os.path.basename(argv[0]) - print('usage: %s \n' + print('usage: %s [var=value]\n' '(example: "%s development.ini")' % (cmd, cmd)) sys.exit(1) def main(argv=sys.argv): - if len(argv) != 2: + if len(argv) < 2: usage(argv) config_uri = argv[1] setup_logging(config_uri) settings = get_appsettings(config_uri) - engine = engine_from_config(settings, 'sqlalchemy.') - DBSession.configure(bind=engine) + + engine = get_engine(settings) + dbmaker = get_dbmaker(engine) + + dbsession = get_session(transaction.manager, dbmaker) + Base.metadata.create_all(engine) + with transaction.manager: model = Page(name='FrontPage', data='This is the front page') - DBSession.add(model) + dbsession.add(model) diff --git a/docs/tutorials/wiki2/src/tests/tutorial/security.py b/docs/tutorials/wiki2/src/tests/tutorial/security.py deleted file mode 100644 index d88c9c71f..000000000 --- a/docs/tutorials/wiki2/src/tests/tutorial/security.py +++ /dev/null @@ -1,7 +0,0 @@ -USERS = {'editor':'editor', - 'viewer':'viewer'} -GROUPS = {'editor':['group:editors']} - -def groupfinder(userid, request): - if userid in USERS: - return GROUPS.get(userid, []) diff --git a/docs/tutorials/wiki2/src/tests/tutorial/security/__init__.py b/docs/tutorials/wiki2/src/tests/tutorial/security/__init__.py new file mode 100644 index 000000000..5bb534f79 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/security/__init__.py @@ -0,0 +1 @@ +# package diff --git a/docs/tutorials/wiki2/src/tests/tutorial/security/default.py b/docs/tutorials/wiki2/src/tests/tutorial/security/default.py new file mode 100644 index 000000000..d88c9c71f --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/security/default.py @@ -0,0 +1,7 @@ +USERS = {'editor':'editor', + 'viewer':'viewer'} +GROUPS = {'editor':['group:editors']} + +def groupfinder(userid, request): + if userid in USERS: + return GROUPS.get(userid, []) diff --git a/docs/tutorials/wiki2/src/tests/tutorial/static/theme.min.css b/docs/tutorials/wiki2/src/tests/tutorial/static/theme.min.css index 2f924bcc5..0d25de5b6 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/static/theme.min.css +++ b/docs/tutorials/wiki2/src/tests/tutorial/static/theme.min.css @@ -1 +1 @@ -@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);body{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300;color:#fff;background:#bc2131}h1,h2,h3,h4,h5,h6{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300}p{font-weight:300}.font-normal{font-weight:400}.font-semi-bold{font-weight:600}.font-bold{font-weight:700}.starter-template{margin-top:250px}.starter-template .content{margin-left:10px}.starter-template .content h1{margin-top:10px;font-size:60px}.starter-template .content h1 .smaller{font-size:40px;color:#f2b7bd}.starter-template .content .lead{font-size:25px;color:#f2b7bd}.starter-template .content .lead .font-normal{color:#fff}.starter-template .links{float:right;right:0;margin-top:125px}.starter-template .links ul{display:block;padding:0;margin:0}.starter-template .links ul li{list-style:none;display:inline;margin:0 10px}.starter-template .links ul li:first-child{margin-left:0}.starter-template .links ul li:last-child{margin-right:0}.starter-template .links ul li.current-version{color:#f2b7bd;font-weight:400}.starter-template .links ul li a{color:#fff}.starter-template .links ul li a:hover{text-decoration:underline}.starter-template .links ul li .icon-muted{color:#eb8b95;margin-right:5px}.starter-template .links ul li:hover .icon-muted{color:#fff}.starter-template .copyright{margin-top:10px;font-size:.9em;color:#f2b7bd;text-transform:lowercase;float:right;right:0}@media (max-width:1199px){.starter-template .content h1{font-size:45px}.starter-template .content h1 .smaller{font-size:30px}.starter-template .content .lead{font-size:20px}}@media (max-width:991px){.starter-template{margin-top:0}.starter-template .logo{margin:40px auto}.starter-template .content{margin-left:0;text-align:center}.starter-template .content h1{margin-bottom:20px}.starter-template .links{float:none;text-align:center;margin-top:60px}.starter-template .copyright{float:none;text-align:center}}@media (max-width:767px){.starter-template .content h1 .smaller{font-size:25px;display:block}.starter-template .content .lead{font-size:16px}.starter-template .links{margin-top:40px}.starter-template .links ul li{display:block;margin:0}.starter-template .links ul li .icon-muted{display:none}.starter-template .copyright{margin-top:20px}} \ No newline at end of file +@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);body{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300;color:#fff;background:#bc2131}h1,h2,h3,h4,h5,h6{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300}p{font-weight:300}.font-normal{font-weight:400}.font-semi-bold{font-weight:600}.font-bold{font-weight:700}.starter-template{margin-top:250px}.starter-template .content{margin-left:10px}.starter-template .content h1{margin-top:10px;font-size:60px}.starter-template .content h1 .smaller{font-size:40px;color:#f2b7bd}.starter-template .content .lead{font-size:25px;color:#f2b7bd}.starter-template .content .lead .font-normal{color:#fff}.starter-template .links{float:right;right:0;margin-top:125px}.starter-template .links ul{display:block;padding:0;margin:0}.starter-template .links ul li{list-style:none;display:inline;margin:0 10px}.starter-template .links ul li:first-child{margin-left:0}.starter-template .links ul li:last-child{margin-right:0}.starter-template .links ul li.current-version{color:#f2b7bd;font-weight:400}.starter-template .links ul li a,a{color:#f2b7bd;text-decoration:underline}.starter-template .links ul li a:hover,a:hover{color:#fff;text-decoration:underline}.starter-template .links ul li .icon-muted{color:#eb8b95;margin-right:5px}.starter-template .links ul li:hover .icon-muted{color:#fff}.starter-template .copyright{margin-top:10px;font-size:.9em;color:#f2b7bd;text-transform:lowercase;float:right;right:0}@media (max-width:1199px){.starter-template .content h1{font-size:45px}.starter-template .content h1 .smaller{font-size:30px}.starter-template .content .lead{font-size:20px}}@media (max-width:991px){.starter-template{margin-top:0}.starter-template .logo{margin:40px auto}.starter-template .content{margin-left:0;text-align:center}.starter-template .content h1{margin-bottom:20px}.starter-template .links{float:none;text-align:center;margin-top:60px}.starter-template .copyright{float:none;text-align:center}}@media (max-width:767px){.starter-template .content h1 .smaller{font-size:25px;display:block}.starter-template .content .lead{font-size:16px}.starter-template .links{margin-top:40px}.starter-template .links ul li{display:block;margin:0}.starter-template .links ul li .icon-muted{display:none}.starter-template .copyright{margin-top:20px}} diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/edit.jinja2 b/docs/tutorials/wiki2/src/tests/tutorial/templates/edit.jinja2 new file mode 100644 index 000000000..c4f3a2c93 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/templates/edit.jinja2 @@ -0,0 +1,73 @@ + + + + + + + + + + + Edit{% if page.name %} {{page.name}}{% endif %} - Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki) + + + + + + + + + + + + + +
+
+
+
+ +
+
+
+ {% if logged_in %} +

+ Logout +

+ {% endif %} +

+ Editing {% if page.name %}{{page.name}}{% else %}Page Name Goes Here{% endif %} +

+

You can return to the + FrontPage. +

+
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+
+ + + + + + + + diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/edit.pt b/docs/tutorials/wiki2/src/tests/tutorial/templates/edit.pt deleted file mode 100644 index 50e55c850..000000000 --- a/docs/tutorials/wiki2/src/tests/tutorial/templates/edit.pt +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - - - - - - ${page.name} - Pyramid tutorial wiki (based on - TurboGears 20-Minute Wiki) - - - - - - - - - - - - -
-
-
-
- -
-
-
-

- Editing Page Name Goes - Here -

-

You can return to the - FrontPage. -

-

- - Logout - -

-
-
- -
-
- -
-
-
-
-
-
- -
-
-
- - - - - - - - diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/layout.jinja2 b/docs/tutorials/wiki2/src/tests/tutorial/templates/layout.jinja2 new file mode 100644 index 000000000..ff624c65b --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/templates/layout.jinja2 @@ -0,0 +1,66 @@ + + + + + + + + + + + Alchemy Scaffold for The Pyramid Web Framework + + + + + + + + + + + + + +
+
+
+
+ +
+
+ {% block content %} +

No content

+ {% endblock content %} +
+
+
+ +
+
+ +
+
+
+ + + + + + + + diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/login.jinja2 b/docs/tutorials/wiki2/src/tests/tutorial/templates/login.jinja2 new file mode 100644 index 000000000..a80a2a165 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/templates/login.jinja2 @@ -0,0 +1,74 @@ + + + + + + + + + + + Login - Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki) + + + + + + + + + + + + + +
+
+
+
+ +
+
+
+

+ + Login +
+ {{ message }} +

+
+ +
+ + +
+
+ + +
+
+ +
+
+
+
+
+
+ +
+
+
+ + + + + + + + diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/login.pt b/docs/tutorials/wiki2/src/tests/tutorial/templates/login.pt deleted file mode 100644 index 5f8e9b98c..000000000 --- a/docs/tutorials/wiki2/src/tests/tutorial/templates/login.pt +++ /dev/null @@ -1,54 +0,0 @@ - - - - Login - Pyramid tutorial wiki (based on TurboGears - 20-Minute Wiki) - - - - - - - - -
-
-
-
- pyramid -
-
-
-
-
-
- Login
- -
- -
-
-
-
-
- -
-
- -
-
-
-
- - diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/mytemplate.jinja2 b/docs/tutorials/wiki2/src/tests/tutorial/templates/mytemplate.jinja2 new file mode 100644 index 000000000..bb622bf5a --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/templates/mytemplate.jinja2 @@ -0,0 +1,8 @@ +{% extends "layout.jinja2" %} + +{% block content %} +
+

Pyramid Alchemy scaffold

+

Welcome to {{project}}, an application generated by
the Pyramid Web Framework 1.7.dev0.

+
+{% endblock content %} diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/mytemplate.pt b/docs/tutorials/wiki2/src/tests/tutorial/templates/mytemplate.pt deleted file mode 100644 index c9b0cec21..000000000 --- a/docs/tutorials/wiki2/src/tests/tutorial/templates/mytemplate.pt +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - - - - - Alchemy Scaffold for The Pyramid Web Framework - - - - - - - - - - - - - -
-
-
-
- -
-
-
-

Pyramid Alchemy scaffold

-

Welcome to ${project}, an application generated by
the Pyramid Web Framework.

-
-
-
- -
- -
-
-
- - - - - - - - diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/view.jinja2 b/docs/tutorials/wiki2/src/tests/tutorial/templates/view.jinja2 new file mode 100644 index 000000000..a7afc66fc --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/templates/view.jinja2 @@ -0,0 +1,71 @@ + + + + + + + + + + + {{page.name}} - Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki) + + + + + + + + + + + + + +
+
+
+
+ +
+
+
+ {% if logged_in %} +

+ Logout +

+ {% endif %} +

{{ content|safe }}

+

+ + Edit this page + +

+

+ Viewing {% if page.name %}{{page.name}}{% else %}Page Name Goes Here{% endif %} +

+

You can return to the + FrontPage. +

+
+
+
+
+ +
+
+
+ + + + + + + + diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/view.pt b/docs/tutorials/wiki2/src/tests/tutorial/templates/view.pt deleted file mode 100644 index 4e5772de0..000000000 --- a/docs/tutorials/wiki2/src/tests/tutorial/templates/view.pt +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - - - - - - ${page.name} - Pyramid tutorial wiki (based on - TurboGears 20-Minute Wiki) - - - - - - - - - - - - -
-
-
-
- -
-
-
-
- Page text goes here. -
-

- - Edit this page - -

-

- Viewing - Page Name Goes Here -

-

You can return to the - FrontPage. -

-

- - Logout - -

-
-
-
-
- -
-
-
- - - - - - - - diff --git a/docs/tutorials/wiki2/src/tests/tutorial/tests.py b/docs/tutorials/wiki2/src/tests/tutorial/tests.py deleted file mode 100644 index c50e05b6d..000000000 --- a/docs/tutorials/wiki2/src/tests/tutorial/tests.py +++ /dev/null @@ -1,235 +0,0 @@ -import unittest -import transaction - -from pyramid import testing - - -def _initTestingDB(): - from sqlalchemy import create_engine - from tutorial.models import ( - DBSession, - Page, - Base - ) - engine = create_engine('sqlite://') - Base.metadata.create_all(engine) - DBSession.configure(bind=engine) - with transaction.manager: - model = Page(name='FrontPage', data='This is the front page') - DBSession.add(model) - return DBSession - - -def _registerRoutes(config): - config.add_route('view_page', '{pagename}') - config.add_route('edit_page', '{pagename}/edit_page') - config.add_route('add_page', 'add_page/{pagename}') - - -class ViewWikiTests(unittest.TestCase): - def setUp(self): - self.config = testing.setUp() - - def tearDown(self): - testing.tearDown() - - def _callFUT(self, request): - from tutorial.views import view_wiki - return view_wiki(request) - - def test_it(self): - _registerRoutes(self.config) - request = testing.DummyRequest() - response = self._callFUT(request) - self.assertEqual(response.location, 'http://example.com/FrontPage') - - -class ViewPageTests(unittest.TestCase): - def setUp(self): - self.session = _initTestingDB() - self.config = testing.setUp() - - def tearDown(self): - self.session.remove() - testing.tearDown() - - def _callFUT(self, request): - from tutorial.views import view_page - return view_page(request) - - def test_it(self): - from tutorial.models import Page - request = testing.DummyRequest() - request.matchdict['pagename'] = 'IDoExist' - page = Page(name='IDoExist', data='Hello CruelWorld IDoExist') - self.session.add(page) - _registerRoutes(self.config) - info = self._callFUT(request) - self.assertEqual(info['page'], page) - self.assertEqual( - info['content'], - '
\n' - '

Hello ' - 'CruelWorld ' - '' - 'IDoExist' - '

\n
\n') - self.assertEqual(info['edit_url'], - 'http://example.com/IDoExist/edit_page') - - -class AddPageTests(unittest.TestCase): - def setUp(self): - self.session = _initTestingDB() - self.config = testing.setUp() - - def tearDown(self): - self.session.remove() - testing.tearDown() - - def _callFUT(self, request): - from tutorial.views import add_page - return add_page(request) - - def test_it_notsubmitted(self): - _registerRoutes(self.config) - request = testing.DummyRequest() - request.matchdict = {'pagename':'AnotherPage'} - info = self._callFUT(request) - self.assertEqual(info['page'].data,'') - self.assertEqual(info['save_url'], - 'http://example.com/add_page/AnotherPage') - - def test_it_submitted(self): - from tutorial.models import Page - _registerRoutes(self.config) - request = testing.DummyRequest({'form.submitted':True, - 'body':'Hello yo!'}) - request.matchdict = {'pagename':'AnotherPage'} - self._callFUT(request) - page = self.session.query(Page).filter_by(name='AnotherPage').one() - self.assertEqual(page.data, 'Hello yo!') - - -class EditPageTests(unittest.TestCase): - def setUp(self): - self.session = _initTestingDB() - self.config = testing.setUp() - - def tearDown(self): - self.session.remove() - testing.tearDown() - - def _callFUT(self, request): - from tutorial.views import edit_page - return edit_page(request) - - def test_it_notsubmitted(self): - from tutorial.models import Page - _registerRoutes(self.config) - request = testing.DummyRequest() - request.matchdict = {'pagename':'abc'} - page = Page(name='abc', data='hello') - self.session.add(page) - info = self._callFUT(request) - self.assertEqual(info['page'], page) - self.assertEqual(info['save_url'], - 'http://example.com/abc/edit_page') - - def test_it_submitted(self): - from tutorial.models import Page - _registerRoutes(self.config) - request = testing.DummyRequest({'form.submitted':True, - 'body':'Hello yo!'}) - request.matchdict = {'pagename':'abc'} - page = Page(name='abc', data='hello') - self.session.add(page) - response = self._callFUT(request) - self.assertEqual(response.location, 'http://example.com/abc') - self.assertEqual(page.data, 'Hello yo!') - - -class FunctionalTests(unittest.TestCase): - - viewer_login = '/login?login=viewer&password=viewer' \ - '&came_from=FrontPage&form.submitted=Login' - viewer_wrong_login = '/login?login=viewer&password=incorrect' \ - '&came_from=FrontPage&form.submitted=Login' - editor_login = '/login?login=editor&password=editor' \ - '&came_from=FrontPage&form.submitted=Login' - - def setUp(self): - from tutorial import main - settings = { 'sqlalchemy.url': 'sqlite://'} - app = main({}, **settings) - from webtest import TestApp - self.testapp = TestApp(app) - _initTestingDB() - - def tearDown(self): - del self.testapp - from tutorial.models import DBSession - DBSession.remove() - - def test_root(self): - res = self.testapp.get('/', status=302) - self.assertEqual(res.location, 'http://localhost/FrontPage') - - def test_FrontPage(self): - res = self.testapp.get('/FrontPage', status=200) - self.assertTrue(b'FrontPage' in res.body) - - def test_unexisting_page(self): - self.testapp.get('/SomePage', status=404) - - def test_successful_log_in(self): - res = self.testapp.get(self.viewer_login, status=302) - self.assertEqual(res.location, 'http://localhost/FrontPage') - - def test_failed_log_in(self): - res = self.testapp.get(self.viewer_wrong_login, status=200) - self.assertTrue(b'login' in res.body) - - def test_logout_link_present_when_logged_in(self): - self.testapp.get(self.viewer_login, status=302) - res = self.testapp.get('/FrontPage', status=200) - self.assertTrue(b'Logout' in res.body) - - def test_logout_link_not_present_after_logged_out(self): - self.testapp.get(self.viewer_login, status=302) - self.testapp.get('/FrontPage', status=200) - res = self.testapp.get('/logout', status=302) - self.assertTrue(b'Logout' not in res.body) - - def test_anonymous_user_cannot_edit(self): - res = self.testapp.get('/FrontPage/edit_page', status=200) - self.assertTrue(b'Login' in res.body) - - def test_anonymous_user_cannot_add(self): - res = self.testapp.get('/add_page/NewPage', status=200) - self.assertTrue(b'Login' in res.body) - - def test_viewer_user_cannot_edit(self): - self.testapp.get(self.viewer_login, status=302) - res = self.testapp.get('/FrontPage/edit_page', status=200) - self.assertTrue(b'Login' in res.body) - - def test_viewer_user_cannot_add(self): - self.testapp.get(self.viewer_login, status=302) - res = self.testapp.get('/add_page/NewPage', status=200) - self.assertTrue(b'Login' in res.body) - - def test_editors_member_user_can_edit(self): - self.testapp.get(self.editor_login, status=302) - res = self.testapp.get('/FrontPage/edit_page', status=200) - self.assertTrue(b'Editing' in res.body) - - def test_editors_member_user_can_add(self): - self.testapp.get(self.editor_login, status=302) - res = self.testapp.get('/add_page/NewPage', status=200) - self.assertTrue(b'Editing' in res.body) - - def test_editors_member_user_can_view(self): - self.testapp.get(self.editor_login, status=302) - res = self.testapp.get('/FrontPage', status=200) - self.assertTrue(b'FrontPage' in res.body) diff --git a/docs/tutorials/wiki2/src/tests/tutorial/tests/__init__.py b/docs/tutorials/wiki2/src/tests/tutorial/tests/__init__.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/tests/__init__.py @@ -0,0 +1 @@ + diff --git a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py new file mode 100644 index 000000000..339c60bc2 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py @@ -0,0 +1,141 @@ +import unittest + +from pyramid import testing + + +def dummy_request(dbsession): + return testing.DummyRequest(dbsession=dbsession) + + +def _register_routes(config): + config.add_route('view_page', '{pagename}') + config.add_route('edit_page', '{pagename}/edit_page') + config.add_route('add_page', 'add_page/{pagename}') + + +class FunctionalTests(unittest.TestCase): + + viewer_login = '/login?login=viewer&password=viewer' \ + '&came_from=FrontPage&form.submitted=Login' + viewer_wrong_login = '/login?login=viewer&password=incorrect' \ + '&came_from=FrontPage&form.submitted=Login' + editor_login = '/login?login=editor&password=editor' \ + '&came_from=FrontPage&form.submitted=Login' + + engine = None + + @staticmethod + def setup_database(): + import transaction + from tutorial.models.mymodel import Page + from tutorial.models.meta import ( + Base, + ) + import tutorial.models.meta + + + def initialize_db(dbsession, engine): + Base.metadata.create_all(engine) + with transaction.manager: + model = Page(name='FrontPage', data='This is the front page') + dbsession.add(model) + + def wrap_get_session(transaction_manager, dbmaker): + dbsession = get_session(transaction_manager, dbmaker) + initialize_db(dbsession, engine) + tutorial.models.meta.get_session = get_session + tutorial.models.meta.get_engine = get_engine + return dbsession + + def wrap_get_engine(settings): + global engine + engine = get_engine(settings) + return engine + + get_session = tutorial.models.meta.get_session + tutorial.models.meta.get_session = wrap_get_session + + get_engine = tutorial.models.meta.get_engine + tutorial.models.meta.get_engine = wrap_get_engine + + @classmethod + def setUpClass(cls): + cls.setup_database() + + from webtest import TestApp + from tutorial import main + settings = {'sqlalchemy.url': 'sqlite://'} + app = main({}, **settings) + cls.testapp = TestApp(app) + + @classmethod + def tearDownClass(cls): + from tutorial.models.meta import Base + Base.metadata.drop_all(engine) + + def tearDown(self): + import transaction + transaction.abort() + + def test_root(self): + res = self.testapp.get('/', status=302) + self.assertEqual(res.location, 'http://localhost/FrontPage') + + def test_FrontPage(self): + res = self.testapp.get('/FrontPage', status=200) + self.assertTrue(b'FrontPage' in res.body) + + def test_unexisting_page(self): + self.testapp.get('/SomePage', status=404) + + def test_successful_log_in(self): + res = self.testapp.get(self.viewer_login, status=302) + self.assertEqual(res.location, 'http://localhost/FrontPage') + + def test_failed_log_in(self): + res = self.testapp.get(self.viewer_wrong_login, status=200) + self.assertTrue(b'login' in res.body) + + def test_logout_link_present_when_logged_in(self): + self.testapp.get(self.viewer_login, status=302) + res = self.testapp.get('/FrontPage', status=200) + self.assertTrue(b'Logout' in res.body) + + def test_logout_link_not_present_after_logged_out(self): + self.testapp.get(self.viewer_login, status=302) + self.testapp.get('/FrontPage', status=200) + res = self.testapp.get('/logout', status=302) + self.assertTrue(b'Logout' not in res.body) + + def test_anonymous_user_cannot_edit(self): + res = self.testapp.get('/FrontPage/edit_page', status=200) + self.assertTrue(b'Login' in res.body) + + def test_anonymous_user_cannot_add(self): + res = self.testapp.get('/add_page/NewPage', status=200) + self.assertTrue(b'Login' in res.body) + + def test_viewer_user_cannot_edit(self): + self.testapp.get(self.viewer_login, status=302) + res = self.testapp.get('/FrontPage/edit_page', status=200) + self.assertTrue(b'Login' in res.body) + + def test_viewer_user_cannot_add(self): + self.testapp.get(self.viewer_login, status=302) + res = self.testapp.get('/add_page/NewPage', status=200) + self.assertTrue(b'Login' in res.body) + + def test_editors_member_user_can_edit(self): + self.testapp.get(self.editor_login, status=302) + res = self.testapp.get('/FrontPage/edit_page', status=200) + self.assertTrue(b'Editing' in res.body) + + def test_editors_member_user_can_add(self): + self.testapp.get(self.editor_login, status=302) + res = self.testapp.get('/add_page/NewPage', status=200) + self.assertTrue(b'Editing' in res.body) + + def test_editors_member_user_can_view(self): + self.testapp.get(self.editor_login, status=302) + res = self.testapp.get('/FrontPage', status=200) + self.assertTrue(b'FrontPage' in res.body) diff --git a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_views.py b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_views.py new file mode 100644 index 000000000..d70311e38 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_views.py @@ -0,0 +1,168 @@ +import unittest +import transaction + +from pyramid import testing + + +def dummy_request(dbsession): + return testing.DummyRequest(dbsession=dbsession) + + +def _register_routes(config): + config.add_route('view_page', '{pagename}') + config.add_route('edit_page', '{pagename}/edit_page') + config.add_route('add_page', 'add_page/{pagename}') + + +class BaseTest(unittest.TestCase): + def setUp(self): + self.config = testing.setUp(settings={ + 'sqlalchemy.url': 'sqlite:///:memory:' + }) + self.config.include('..models.meta') + _register_routes(self.config) + settings = self.config.get_settings() + + from ..models.meta import ( + get_session, + get_engine, + get_dbmaker, + ) + + self.engine = get_engine(settings) + dbmaker = get_dbmaker(self.engine) + + self.session = get_session(transaction.manager, dbmaker) + + self.init_database() + + def init_database(self): + from ..models.meta import Base + Base.metadata.create_all(self.engine) + + def tearDown(self): + testing.tearDown() + transaction.abort() + + +class ViewWikiTests(unittest.TestCase): + + def setUp(self): + self.config = testing.setUp() + _register_routes(self.config) + + def tearDown(self): + testing.tearDown() + + def _callFUT(self, request): + from tutorial.views.default import view_wiki + return view_wiki(request) + + def test_it(self): + request = testing.DummyRequest() + response = self._callFUT(request) + self.assertEqual(response.location, 'http://example.com/FrontPage') + + +class ViewPageTests(BaseTest): + def setUp(self): + super(ViewPageTests, self).setUp() + + def tearDown(self): + transaction.abort() + testing.tearDown() + + def _callFUT(self, request): + from tutorial.views.default import view_page + return view_page(request) + + def test_it(self): + # add a page to the db + from ..models.mymodel import Page + page = Page(name='IDoExist', data='Hello CruelWorld IDoExist') + self.session.add(page) + + # create a request asking for the page we've created + request = dummy_request(self.session) + request.matchdict['pagename'] = 'IDoExist' + + # call the view we're testing and check its behavior + info = self._callFUT(request) + self.assertEqual(info['page'], page) + self.assertEqual( + info['content'], + '
\n' + '

Hello ' + 'CruelWorld ' + '' + 'IDoExist' + '

\n
\n') + self.assertEqual(info['edit_url'], + 'http://example.com/IDoExist/edit_page') + + +class AddPageTests(BaseTest): + def setUp(self): + super(AddPageTests, self).setUp() + + def tearDown(self): + transaction.abort() + testing.tearDown() + + def _callFUT(self, request): + from tutorial.views.default import add_page + return add_page(request) + + def test_it_notsubmitted(self): + request = dummy_request(self.session) + request.matchdict = {'pagename': 'AnotherPage'} + info = self._callFUT(request) + self.assertEqual(info['page'].data, '') + self.assertEqual(info['save_url'], + 'http://example.com/add_page/AnotherPage') + + def test_it_submitted(self): + from ..models.mymodel import Page + request = testing.DummyRequest({'form.submitted': True, + 'body': 'Hello yo!'}, + dbsession=self.session) + request.matchdict = {'pagename': 'AnotherPage'} + self._callFUT(request) + page = self.session.query(Page).filter_by(name='AnotherPage').one() + self.assertEqual(page.data, 'Hello yo!') + + +class EditPageTests(BaseTest): + def setUp(self): + super(EditPageTests, self).setUp() + + def tearDown(self): + transaction.abort() + testing.tearDown() + + def _callFUT(self, request): + from tutorial.views.default import edit_page + return edit_page(request) + + def test_it_notsubmitted(self): + from ..models.mymodel import Page + request = dummy_request(self.session) + request.matchdict = {'pagename': 'abc'} + page = Page(name='abc', data='hello') + self.session.add(page) + info = self._callFUT(request) + self.assertEqual(info['page'], page) + self.assertEqual(info['save_url'], + 'http://example.com/abc/edit_page') + + def test_it_submitted(self): + from ..models.mymodel import Page + request = testing.DummyRequest({'form.submitted': True, + 'body': 'Hello yo!'}, + dbsession=self.session) + request.matchdict = {'pagename': 'abc'} + page = Page(name='abc', data='hello') + self.session.add(page) + response = self._callFUT(request) + self.assertEqual(response.location, 'http://example.com/abc') + self.assertEqual(page.data, 'Hello yo!') diff --git a/docs/tutorials/wiki2/src/tests/tutorial/views.py b/docs/tutorials/wiki2/src/tests/tutorial/views.py deleted file mode 100644 index 41bea4785..000000000 --- a/docs/tutorials/wiki2/src/tests/tutorial/views.py +++ /dev/null @@ -1,123 +0,0 @@ -import re -from docutils.core import publish_parts - -from pyramid.httpexceptions import ( - HTTPFound, - HTTPNotFound, - ) - -from pyramid.view import ( - view_config, - forbidden_view_config, - ) - -from pyramid.security import ( - remember, - forget, - ) - -from .security import USERS - -from .models import ( - DBSession, - Page, - ) - - -# regular expression used to find WikiWords -wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)") - -@view_config(route_name='view_wiki', - permission='view') -def view_wiki(request): - return HTTPFound(location = request.route_url('view_page', - pagename='FrontPage')) - -@view_config(route_name='view_page', renderer='templates/view.pt', - permission='view') -def view_page(request): - pagename = request.matchdict['pagename'] - 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 = DBSession.query(Page).filter_by(name=word).all() - if exists: - view_url = request.route_url('view_page', pagename=word) - return '%s' % (view_url, word) - else: - add_url = request.route_url('add_page', pagename=word) - return '%s' % (add_url, word) - - content = publish_parts(page.data, writer_name='html')['html_body'] - content = wikiwords.sub(check, content) - edit_url = request.route_url('edit_page', pagename=pagename) - return dict(page=page, content=content, edit_url=edit_url, - logged_in=request.authenticated_userid) - -@view_config(route_name='add_page', renderer='templates/edit.pt', - permission='edit') -def add_page(request): - pagename = request.matchdict['pagename'] - if 'form.submitted' in request.params: - body = request.params['body'] - page = Page(name=pagename, data=body) - DBSession.add(page) - return HTTPFound(location = request.route_url('view_page', - pagename=pagename)) - save_url = request.route_url('add_page', pagename=pagename) - page = Page(name='', data='') - return dict(page=page, save_url=save_url, - logged_in=request.authenticated_userid) - -@view_config(route_name='edit_page', renderer='templates/edit.pt', - permission='edit') -def edit_page(request): - pagename = request.matchdict['pagename'] - page = DBSession.query(Page).filter_by(name=pagename).one() - if 'form.submitted' in request.params: - page.data = request.params['body'] - DBSession.add(page) - return HTTPFound(location = request.route_url('view_page', - pagename=pagename)) - return dict( - page=page, - save_url=request.route_url('edit_page', pagename=pagename), - logged_in=request.authenticated_userid - ) - -@view_config(route_name='login', renderer='templates/login.pt') -@forbidden_view_config(renderer='templates/login.pt') -def login(request): - login_url = request.route_url('login') - referrer = request.url - if referrer == login_url: - referrer = '/' # never use the login form itself as came_from - came_from = request.params.get('came_from', referrer) - message = '' - login = '' - password = '' - if 'form.submitted' in request.params: - login = request.params['login'] - password = request.params['password'] - if USERS.get(login) == password: - headers = remember(request, login) - return HTTPFound(location = came_from, - headers = headers) - message = 'Failed login' - - return dict( - message = message, - url = request.application_url + '/login', - came_from = came_from, - login = login, - password = password, - ) - -@view_config(route_name='logout') -def logout(request): - headers = forget(request) - return HTTPFound(location = request.route_url('view_wiki'), - headers = headers) diff --git a/docs/tutorials/wiki2/src/tests/tutorial/views/__init__.py b/docs/tutorials/wiki2/src/tests/tutorial/views/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs/tutorials/wiki2/src/tests/tutorial/views/default.py b/docs/tutorials/wiki2/src/tests/tutorial/views/default.py new file mode 100644 index 000000000..f35f041a4 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/views/default.py @@ -0,0 +1,120 @@ +import cgi +import re +from docutils.core import publish_parts + +from pyramid.httpexceptions import ( + HTTPFound, + HTTPNotFound, + ) + +from pyramid.view import ( + view_config, + forbidden_view_config, + ) + +from pyramid.security import ( + remember, + forget, + ) + +from ..security.default import USERS + +from ..models.mymodel import Page + +# regular expression used to find WikiWords +wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)") + +@view_config(route_name='view_wiki', + permission='view') +def view_wiki(request): + return HTTPFound(location=request.route_url('view_page', + pagename='FrontPage')) + +@view_config(route_name='view_page', renderer='templates/view.jinja2', + permission='view') +def view_page(request): + pagename = request.matchdict['pagename'] + page = request.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 = request.dbsession.query(Page).filter_by(name=word).all() + if exists: + view_url = request.route_url('view_page', pagename=word) + return '%s' % (view_url, cgi.escape(word)) + else: + add_url = request.route_url('add_page', pagename=word) + return '%s' % (add_url, cgi.escape(word)) + + content = publish_parts(page.data, writer_name='html')['html_body'] + content = wikiwords.sub(check, content) + edit_url = request.route_url('edit_page', pagename=pagename) + return dict(page=page, content=content, edit_url=edit_url, + logged_in=request.authenticated_userid) + +@view_config(route_name='add_page', renderer='templates/edit.jinja2', + permission='edit') +def add_page(request): + pagename = request.matchdict['pagename'] + if 'form.submitted' in request.params: + body = request.params['body'] + page = Page(name=pagename, data=body) + request.dbsession.add(page) + return HTTPFound(location = request.route_url('view_page', + pagename=pagename)) + save_url = request.route_url('add_page', pagename=pagename) + page = Page(name='', data='') + return dict(page=page, save_url=save_url, + logged_in=request.authenticated_userid) + +@view_config(route_name='edit_page', renderer='templates/edit.jinja2', + permission='edit') +def edit_page(request): + pagename = request.matchdict['pagename'] + page = request.dbsession.query(Page).filter_by(name=pagename).one() + if 'form.submitted' in request.params: + page.data = request.params['body'] + request.dbsession.add(page) + return HTTPFound(location = request.route_url('view_page', + pagename=pagename)) + return dict( + page=page, + save_url = request.route_url('edit_page', pagename=pagename), + logged_in=request.authenticated_userid + ) + +@view_config(route_name='login', renderer='templates/login.jinja2') +@forbidden_view_config(renderer='templates/login.jinja2') +def login(request): + login_url = request.route_url('login') + referrer = request.url + if referrer == login_url: + referrer = '/' # never use the login form itself as came_from + came_from = request.params.get('came_from', referrer) + message = '' + login = '' + password = '' + if 'form.submitted' in request.params: + login = request.params['login'] + password = request.params['password'] + if USERS.get(login) == password: + headers = remember(request, login) + return HTTPFound(location = came_from, + headers = headers) + message = 'Failed login' + + return dict( + message = message, + url = request.application_url + '/login', + came_from = came_from, + login = login, + password = password, + ) + +@view_config(route_name='logout') +def logout(request): + headers = forget(request) + return HTTPFound(location = request.route_url('view_wiki'), + headers = headers) diff --git a/docs/tutorials/wiki2/tests.rst b/docs/tutorials/wiki2/tests.rst index 64025421f..fe3fdaf2c 100644 --- a/docs/tutorials/wiki2/tests.rst +++ b/docs/tutorials/wiki2/tests.rst @@ -11,17 +11,12 @@ is a common practice to put tests into a ``tests`` subpackage, especially as projects grow in size and complexity. Each module in the test subpackage should contain tests for its corresponding module in our application. Each corresponding pair of modules should have the same names, except the test -module should have the prefix "test_". +module should have the prefix ``test_``. We will move parts of ``tests.py`` into appropriate new files in the ``tests`` subpackage, and add several new tests. - -Test the models -=============== - -To test the model class ``Page``, we'll add a new ``PageModelTests`` class to -a new file ``tests/test_models.py`` +Start by creating a new directory and a new empty file ``tests/__init__.py``. Test the views @@ -47,13 +42,6 @@ can, and so on. View the results of all our edits to ``tests`` subpackage ========================================================= -Open ``tutorial/tests/test_models.py``, and edit it such that it appears as -follows: - -.. literalinclude:: src/tests/tutorial/tests/test_models.py - :linenos: - :language: python - Open ``tutorial/tests/test_views.py``, and edit it such that it appears as follows: @@ -61,10 +49,10 @@ follows: :linenos: :language: python -Open ``tutorial/tests/test_.py``, and edit it such that it appears as +Open ``tutorial/tests/test_functional.py``, and edit it such that it appears as follows: -.. literalinclude:: src/tests/tutorial/tests/test_.py +.. literalinclude:: src/tests/tutorial/tests/test_functional.py :linenos: :language: python @@ -119,8 +107,10 @@ The expected result should look like the following: .. code-block:: text - ...................... + .................... ---------------------------------------------------------------------- - Ran 21 tests in 2.700s + Ran 20 tests in 0.524s OK + + Process finished with exit code 0 -- cgit v1.2.3 From 022a6e0f05b72c679aada6b3c9313c4fd5b31b80 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Wed, 2 Dec 2015 04:30:47 -0800 Subject: - add comment to NAMING_CONVENTION per 9b12c01168cb756ec36351d7414cad95e87f6581 --- docs/tutorials/wiki2/src/authorization/tutorial/models/meta.py | 3 +++ docs/tutorials/wiki2/src/basiclayout/tutorial/models/meta.py | 3 +++ docs/tutorials/wiki2/src/models/tutorial/models/meta.py | 3 +++ docs/tutorials/wiki2/src/tests/tutorial/models/meta.py | 3 +++ docs/tutorials/wiki2/src/views/tutorial/models/meta.py | 3 +++ 5 files changed, 15 insertions(+) diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models/meta.py b/docs/tutorials/wiki2/src/authorization/tutorial/models/meta.py index b72b45f9f..80ececd8c 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/models/meta.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/models/meta.py @@ -4,6 +4,9 @@ from sqlalchemy.orm import sessionmaker from sqlalchemy.schema import MetaData import zope.sqlalchemy +# Recommended naming convention used by Alembic, as various different database +# providers will autogenerate vastly different names making migrations more +# difficult. See: http://alembic.readthedocs.org/en/latest/naming.html NAMING_CONVENTION = { "ix": 'ix_%(column_0_label)s', "uq": "uq_%(table_name)s_%(column_0_name)s", diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/models/meta.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/meta.py index b72b45f9f..80ececd8c 100644 --- a/docs/tutorials/wiki2/src/basiclayout/tutorial/models/meta.py +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/meta.py @@ -4,6 +4,9 @@ from sqlalchemy.orm import sessionmaker from sqlalchemy.schema import MetaData import zope.sqlalchemy +# Recommended naming convention used by Alembic, as various different database +# providers will autogenerate vastly different names making migrations more +# difficult. See: http://alembic.readthedocs.org/en/latest/naming.html NAMING_CONVENTION = { "ix": 'ix_%(column_0_label)s', "uq": "uq_%(table_name)s_%(column_0_name)s", diff --git a/docs/tutorials/wiki2/src/models/tutorial/models/meta.py b/docs/tutorials/wiki2/src/models/tutorial/models/meta.py index b72b45f9f..80ececd8c 100644 --- a/docs/tutorials/wiki2/src/models/tutorial/models/meta.py +++ b/docs/tutorials/wiki2/src/models/tutorial/models/meta.py @@ -4,6 +4,9 @@ from sqlalchemy.orm import sessionmaker from sqlalchemy.schema import MetaData import zope.sqlalchemy +# Recommended naming convention used by Alembic, as various different database +# providers will autogenerate vastly different names making migrations more +# difficult. See: http://alembic.readthedocs.org/en/latest/naming.html NAMING_CONVENTION = { "ix": 'ix_%(column_0_label)s', "uq": "uq_%(table_name)s_%(column_0_name)s", diff --git a/docs/tutorials/wiki2/src/tests/tutorial/models/meta.py b/docs/tutorials/wiki2/src/tests/tutorial/models/meta.py index b72b45f9f..80ececd8c 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/models/meta.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/models/meta.py @@ -4,6 +4,9 @@ from sqlalchemy.orm import sessionmaker from sqlalchemy.schema import MetaData import zope.sqlalchemy +# Recommended naming convention used by Alembic, as various different database +# providers will autogenerate vastly different names making migrations more +# difficult. See: http://alembic.readthedocs.org/en/latest/naming.html NAMING_CONVENTION = { "ix": 'ix_%(column_0_label)s', "uq": "uq_%(table_name)s_%(column_0_name)s", diff --git a/docs/tutorials/wiki2/src/views/tutorial/models/meta.py b/docs/tutorials/wiki2/src/views/tutorial/models/meta.py index b72b45f9f..80ececd8c 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/models/meta.py +++ b/docs/tutorials/wiki2/src/views/tutorial/models/meta.py @@ -4,6 +4,9 @@ from sqlalchemy.orm import sessionmaker from sqlalchemy.schema import MetaData import zope.sqlalchemy +# Recommended naming convention used by Alembic, as various different database +# providers will autogenerate vastly different names making migrations more +# difficult. See: http://alembic.readthedocs.org/en/latest/naming.html NAMING_CONVENTION = { "ix": 'ix_%(column_0_label)s', "uq": "uq_%(table_name)s_%(column_0_name)s", -- cgit v1.2.3 From 86d90fb8c22a50e68077542d8f3f0f488e72a5d8 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Wed, 2 Dec 2015 04:38:58 -0800 Subject: - adjust linenos for NAMING_CONVENTION comment --- docs/tutorials/wiki2/basiclayout.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/tutorials/wiki2/basiclayout.rst b/docs/tutorials/wiki2/basiclayout.rst index 1426e55a5..e3d0a0a3c 100644 --- a/docs/tutorials/wiki2/basiclayout.rst +++ b/docs/tutorials/wiki2/basiclayout.rst @@ -211,8 +211,8 @@ will inherit from the ``Base`` class so they can be associated with our particular database connection. .. literalinclude:: src/basiclayout/tutorial/models/meta.py - :lines: 15-16 - :lineno-start: 15 + :lines: 18-19 + :lineno-start: 18 :linenos: :language: py @@ -221,7 +221,7 @@ configures various database settings by calling subsequently defined functions. .. literalinclude:: src/basiclayout/tutorial/models/meta.py :pyobject: includeme - :lineno-start: 19 + :lineno-start: 22 :linenos: :language: py @@ -232,7 +232,7 @@ unless an exception is raised, in which case the transaction will be aborted. .. literalinclude:: src/basiclayout/tutorial/models/meta.py :pyobject: get_session - :lineno-start: 32 + :lineno-start: 35 :linenos: :language: py @@ -243,7 +243,7 @@ URI, something like ``sqlite://``. .. literalinclude:: src/basiclayout/tutorial/models/meta.py :pyobject: get_engine - :lineno-start: 39 + :lineno-start: 42 :linenos: :language: py @@ -254,7 +254,7 @@ creating a session with the database engine. .. literalinclude:: src/basiclayout/tutorial/models/meta.py :pyobject: get_dbmaker - :lineno-start: 43 + :lineno-start: 46 :linenos: :language: py -- cgit v1.2.3 From 690fbb9dc7a1797fa81c217182766b150fc649b6 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Thu, 3 Dec 2015 21:22:53 -0800 Subject: - add updated tutorial and documentation files due to scaffold enhancements --- docs/whatsnew-1.6.rst | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/whatsnew-1.6.rst b/docs/whatsnew-1.6.rst index b99ebeec4..141a58f78 100644 --- a/docs/whatsnew-1.6.rst +++ b/docs/whatsnew-1.6.rst @@ -130,11 +130,11 @@ Feature Additions extensions by going through Pyramid's router. See https://github.com/Pylons/pyramid/pull/1581 - - Make it possible to subclass ``pyramid.request.Request`` and also use ``pyramid.request.Request.add_request.method``. See https://github.com/Pylons/pyramid/issues/1529 + Deprecations ------------ @@ -151,13 +151,14 @@ Scaffolding Enhancements debugging. See https://github.com/Pylons/pyramid/pull/1326 - Update scaffold generating machinery to return the version of pyramid and - pyramid docs for use in scaffolds. Updated ``starter``, ``alchemy`` and + pyramid docs for use in scaffolds. Updated ``starter``, ``alchemy``, and ``zodb`` templates to have links to correctly versioned documentation and reflect which pyramid was used to generate the scaffold. - Removed non-ascii copyright symbol from templates, as this was causing the scaffolds to fail for project generation. + Documentation Enhancements -------------------------- @@ -168,3 +169,8 @@ Documentation Enhancements - Improve and clarify the documentation on what Pyramid defines as a ``principal`` and a ``userid`` in its security APIs. See https://github.com/Pylons/pyramid/pull/1399 + +- Updated the Quick Tour and SQLAlchemy + URL Dispatch Wiki Tutorial narrative + documentation and source files for each step using the updated scaffold. For + a list of updated files, see + https://github.com/Pylons/pyramid/pull/2024#issue-112676079 -- cgit v1.2.3 From acfdc7085b836045e41568731e5f3223f34d635d Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Fri, 4 Dec 2015 02:17:44 -0800 Subject: - add emphasize lines, minor grammar, rewrap 79 columns --- docs/narr/zca.rst | 257 +++++++++++++++++++++++++----------------------------- 1 file changed, 121 insertions(+), 136 deletions(-) diff --git a/docs/narr/zca.rst b/docs/narr/zca.rst index b0e9b1709..784886563 100644 --- a/docs/narr/zca.rst +++ b/docs/narr/zca.rst @@ -9,17 +9,16 @@ .. _zca_chapter: Using the Zope Component Architecture in :app:`Pyramid` -========================================================== +======================================================= -Under the hood, :app:`Pyramid` uses a :term:`Zope Component -Architecture` component registry as its :term:`application registry`. -The Zope Component Architecture is referred to colloquially as the -"ZCA." +Under the hood, :app:`Pyramid` uses a :term:`Zope Component Architecture` +component registry as its :term:`application registry`. The Zope Component +Architecture is referred to colloquially as the "ZCA." The ``zope.component`` API used to access data in a traditional Zope -application can be opaque. For example, here is a typical "unnamed -utility" lookup using the :func:`zope.component.getUtility` global API -as it might appear in a traditional Zope application: +application can be opaque. For example, here is a typical "unnamed utility" +lookup using the :func:`zope.component.getUtility` global API as it might +appear in a traditional Zope application: .. code-block:: python :linenos: @@ -28,23 +27,21 @@ as it might appear in a traditional Zope application: from zope.component import getUtility settings = getUtility(ISettings) -After this code runs, ``settings`` will be a Python dictionary. But -it's unlikely that any "civilian" will be able to figure this out just -by reading the code casually. When the ``zope.component.getUtility`` -API is used by a developer, the conceptual load on a casual reader of -code is high. +After this code runs, ``settings`` will be a Python dictionary. But it's +unlikely that any "civilian" will be able to figure this out just by reading +the code casually. When the ``zope.component.getUtility`` API is used by a +developer, the conceptual load on a casual reader of code is high. -While the ZCA is an excellent tool with which to build a *framework* -such as :app:`Pyramid`, it is not always the best tool with which -to build an *application* due to the opacity of the ``zope.component`` -APIs. Accordingly, :app:`Pyramid` tends to hide the presence of the -ZCA from application developers. You needn't understand the ZCA to -create a :app:`Pyramid` application; its use is effectively only a -framework implementation detail. +While the ZCA is an excellent tool with which to build a *framework* such as +:app:`Pyramid`, it is not always the best tool with which to build an +*application* due to the opacity of the ``zope.component`` APIs. Accordingly, +:app:`Pyramid` tends to hide the presence of the ZCA from application +developers. You needn't understand the ZCA to create a :app:`Pyramid` +application; its use is effectively only a framework implementation detail. -However, developers who are already used to writing :term:`Zope` -applications often still wish to use the ZCA while building a -:app:`Pyramid` application; :app:`Pyramid` makes this possible. +However, developers who are already used to writing :term:`Zope` applications +often still wish to use the ZCA while building a :app:`Pyramid` application. +:app:`Pyramid` makes this possible. .. index:: single: get_current_registry @@ -52,87 +49,81 @@ applications often still wish to use the ZCA while building a single: getSiteManager single: ZCA global API -Using the ZCA Global API in a :app:`Pyramid` Application ------------------------------------------------------------ - -:term:`Zope` uses a single ZCA registry -- the "global" ZCA registry --- for all Zope applications that run in the same Python process, -effectively making it impossible to run more than one Zope application -in a single process. - -However, for ease of deployment, it's often useful to be able to run more -than a single application per process. For example, use of a -:term:`PasteDeploy` "composite" allows you to run separate individual WSGI -applications in the same process, each answering requests for some URL -prefix. This makes it possible to run, for example, a TurboGears application -at ``/turbogears`` and a :app:`Pyramid` application at ``/pyramid``, both -served up using the same :term:`WSGI` server within a single Python process. - -Most production Zope applications are relatively large, making it -impractical due to memory constraints to run more than one Zope -application per Python process. However, a :app:`Pyramid` application -may be very small and consume very little memory, so it's a reasonable -goal to be able to run more than one :app:`Pyramid` application per -process. - -In order to make it possible to run more than one :app:`Pyramid` -application in a single process, :app:`Pyramid` defaults to using a -separate ZCA registry *per application*. - -While this services a reasonable goal, it causes some issues when -trying to use patterns which you might use to build a typical -:term:`Zope` application to build a :app:`Pyramid` application. -Without special help, ZCA "global" APIs such as -:func:`zope.component.getUtility` and :func:`zope.component.getSiteManager` -will use the ZCA "global" registry. Therefore, these APIs -will appear to fail when used in a :app:`Pyramid` application, -because they'll be consulting the ZCA global registry rather than the -component registry associated with your :app:`Pyramid` application. - -There are three ways to fix this: by disusing the ZCA global API -entirely, by using -:meth:`pyramid.config.Configurator.hook_zca` or by passing -the ZCA global registry to the :term:`Configurator` constructor at -startup time. We'll describe all three methods in this section. +Using the ZCA global API in a :app:`Pyramid` application +-------------------------------------------------------- + +:term:`Zope` uses a single ZCA registry—the "global" ZCA registry—for all Zope +applications that run in the same Python process, effectively making it +impossible to run more than one Zope application in a single process. + +However, for ease of deployment, it's often useful to be able to run more than +a single application per process. For example, use of a :term:`PasteDeploy` +"composite" allows you to run separate individual WSGI applications in the same +process, each answering requests for some URL prefix. This makes it possible +to run, for example, a TurboGears application at ``/turbogears`` and a +:app:`Pyramid` application at ``/pyramid``, both served up using the same +:term:`WSGI` server within a single Python process. + +Most production Zope applications are relatively large, making it impractical +due to memory constraints to run more than one Zope application per Python +process. However, a :app:`Pyramid` application may be very small and consume +very little memory, so it's a reasonable goal to be able to run more than one +:app:`Pyramid` application per process. + +In order to make it possible to run more than one :app:`Pyramid` application in +a single process, :app:`Pyramid` defaults to using a separate ZCA registry *per +application*. + +While this services a reasonable goal, it causes some issues when trying to use +patterns which you might use to build a typical :term:`Zope` application to +build a :app:`Pyramid` application. Without special help, ZCA "global" APIs +such as :func:`zope.component.getUtility` and +:func:`zope.component.getSiteManager` will use the ZCA "global" registry. +Therefore, these APIs will appear to fail when used in a :app:`Pyramid` +application, because they'll be consulting the ZCA global registry rather than +the component registry associated with your :app:`Pyramid` application. + +There are three ways to fix this: by disusing the ZCA global API entirely, by +using :meth:`pyramid.config.Configurator.hook_zca` or by passing the ZCA global +registry to the :term:`Configurator` constructor at startup time. We'll +describe all three methods in this section. .. index:: single: request.registry .. _disusing_the_global_zca_api: -Disusing the Global ZCA API +Disusing the global ZCA API +++++++++++++++++++++++++++ ZCA "global" API functions such as ``zope.component.getSiteManager``, ``zope.component.getUtility``, :func:`zope.component.getAdapter`, and :func:`zope.component.getMultiAdapter` aren't strictly necessary. Every -component registry has a method API that offers the same -functionality; it can be used instead. For example, presuming the -``registry`` value below is a Zope Component Architecture component -registry, the following bit of code is equivalent to -``zope.component.getUtility(IFoo)``: +component registry has a method API that offers the same functionality; it can +be used instead. For example, presuming the ``registry`` value below is a Zope +Component Architecture component registry, the following bit of code is +equivalent to ``zope.component.getUtility(IFoo)``: .. code-block:: python registry.getUtility(IFoo) -The full method API is documented in the ``zope.component`` package, -but it largely mirrors the "global" API almost exactly. +The full method API is documented in the ``zope.component`` package, but it +largely mirrors the "global" API almost exactly. -If you are willing to disuse the "global" ZCA APIs and use the method -interface of a registry instead, you need only know how to obtain the -:app:`Pyramid` component registry. +If you are willing to disuse the "global" ZCA APIs and use the method interface +of a registry instead, you need only know how to obtain the :app:`Pyramid` +component registry. There are two ways of doing so: -- use the :func:`pyramid.threadlocal.get_current_registry` - function within :app:`Pyramid` view or resource code. This will - always return the "current" :app:`Pyramid` application registry. +- use the :func:`pyramid.threadlocal.get_current_registry` function within + :app:`Pyramid` view or resource code. This will always return the "current" + :app:`Pyramid` application registry. -- use the attribute of the :term:`request` object named ``registry`` - in your :app:`Pyramid` view code, eg. ``request.registry``. This - is the ZCA component registry related to the running - :app:`Pyramid` application. +- use the attribute of the :term:`request` object named ``registry`` in your + :app:`Pyramid` view code, e.g., ``request.registry``. This is the ZCA + component registry related to the running :app:`Pyramid` application. See :ref:`threadlocals_chapter` for more information about :func:`pyramid.threadlocal.get_current_registry`. @@ -142,7 +133,7 @@ See :ref:`threadlocals_chapter` for more information about .. _hook_zca: -Enabling the ZCA Global API by Using ``hook_zca`` +Enabling the ZCA global API by using ``hook_zca`` +++++++++++++++++++++++++++++++++++++++++++++++++ Consider the following bit of idiomatic :app:`Pyramid` startup code: @@ -157,34 +148,31 @@ Consider the following bit of idiomatic :app:`Pyramid` startup code: config.include('some.other.package') return config.make_wsgi_app() -When the ``app`` function above is run, a :term:`Configurator` is -constructed. When the configurator is created, it creates a *new* -:term:`application registry` (a ZCA component registry). A new -registry is constructed whenever the ``registry`` argument is omitted -when a :term:`Configurator` constructor is called, or when a -``registry`` argument with a value of ``None`` is passed to a -:term:`Configurator` constructor. - -During a request, the application registry created by the Configurator -is "made current". This means calls to -:func:`~pyramid.threadlocal.get_current_registry` in the thread -handling the request will return the component registry associated -with the application. - -As a result, application developers can use ``get_current_registry`` -to get the registry and thus get access to utilities and such, as per -:ref:`disusing_the_global_zca_api`. But they still cannot use the -global ZCA API. Without special treatment, the ZCA global APIs will -always return the global ZCA registry (the one in -``zope.component.globalregistry.base``). - -To "fix" this and make the ZCA global APIs use the "current" -:app:`Pyramid` registry, you need to call -:meth:`~pyramid.config.Configurator.hook_zca` within your setup code. -For example: +When the ``app`` function above is run, a :term:`Configurator` is constructed. +When the configurator is created, it creates a *new* :term:`application +registry` (a ZCA component registry). A new registry is constructed whenever +the ``registry`` argument is omitted, when a :term:`Configurator` constructor +is called, or when a ``registry`` argument with a value of ``None`` is passed +to a :term:`Configurator` constructor. + +During a request, the application registry created by the Configurator is "made +current". This means calls to +:func:`~pyramid.threadlocal.get_current_registry` in the thread handling the +request will return the component registry associated with the application. + +As a result, application developers can use ``get_current_registry`` to get the +registry and thus get access to utilities and such, as per +:ref:`disusing_the_global_zca_api`. But they still cannot use the global ZCA +API. Without special treatment, the ZCA global APIs will always return the +global ZCA registry (the one in ``zope.component.globalregistry.base``). + +To "fix" this and make the ZCA global APIs use the "current" :app:`Pyramid` +registry, you need to call :meth:`~pyramid.config.Configurator.hook_zca` within +your setup code. For example: .. code-block:: python :linenos: + :emphasize-lines: 5 from pyramid.config import Configurator @@ -194,9 +182,9 @@ For example: config.include('some.other.application') return config.make_wsgi_app() -We've added a line to our original startup code, line number 6, which -calls ``config.hook_zca()``. The effect of this line under the hood -is that an analogue of the following code is executed: +We've added a line to our original startup code, line number 5, which calls +``config.hook_zca()``. The effect of this line under the hood is that an +analogue of the following code is executed: .. code-block:: python :linenos: @@ -205,17 +193,15 @@ is that an analogue of the following code is executed: from pyramid.threadlocal import get_current_registry getSiteManager.sethook(get_current_registry) -This causes the ZCA global API to start using the :app:`Pyramid` -application registry in threads which are running a :app:`Pyramid` -request. +This causes the ZCA global API to start using the :app:`Pyramid` application +registry in threads which are running a :app:`Pyramid` request. -Calling ``hook_zca`` is usually sufficient to "fix" the problem of -being able to use the global ZCA API within a :app:`Pyramid` -application. However, it also means that a Zope application that is -running in the same process may start using the :app:`Pyramid` -global registry instead of the Zope global registry, effectively -inverting the original problem. In such a case, follow the steps in -the next section, :ref:`using_the_zca_global_registry`. +Calling ``hook_zca`` is usually sufficient to "fix" the problem of being able +to use the global ZCA API within a :app:`Pyramid` application. However, it +also means that a Zope application that is running in the same process may +start using the :app:`Pyramid` global registry instead of the Zope global +registry, effectively inverting the original problem. In such a case, follow +the steps in the next section, :ref:`using_the_zca_global_registry`. .. index:: single: get_current_registry @@ -224,14 +210,15 @@ the next section, :ref:`using_the_zca_global_registry`. .. _using_the_zca_global_registry: -Enabling the ZCA Global API by Using The ZCA Global Registry +Enabling the ZCA global API by using the ZCA global registry ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -You can tell your :app:`Pyramid` application to use the ZCA global -registry at startup time instead of constructing a new one: +You can tell your :app:`Pyramid` application to use the ZCA global registry at +startup time instead of constructing a new one: .. code-block:: python :linenos: + :emphasize-lines: 5-7 from zope.component import getGlobalSiteManager from pyramid.config import Configurator @@ -243,16 +230,14 @@ registry at startup time instead of constructing a new one: config.include('some.other.application') return config.make_wsgi_app() -Lines 5, 6, and 7 above are the interesting ones. Line 5 retrieves -the global ZCA component registry. Line 6 creates a -:term:`Configurator`, passing the global ZCA registry into its -constructor as the ``registry`` argument. Line 7 "sets up" the global -registry with Pyramid-specific registrations; this is code that is -normally executed when a registry is constructed rather than created, +Lines 5, 6, and 7 above are the interesting ones. Line 5 retrieves the global +ZCA component registry. Line 6 creates a :term:`Configurator`, passing the +global ZCA registry into its constructor as the ``registry`` argument. Line 7 +"sets up" the global registry with Pyramid-specific registrations; this is code +that is normally executed when a registry is constructed rather than created, but we must call it "by hand" when we pass an explicit registry. -At this point, :app:`Pyramid` will use the ZCA global registry -rather than creating a new application-specific registry; since by -default the ZCA global API will use this registry, things will work as -you might expect a Zope app to when you use the global ZCA API. - +At this point, :app:`Pyramid` will use the ZCA global registry rather than +creating a new application-specific registry. Since by default the ZCA global +API will use this registry, things will work as you might expect in a Zope app +when you use the global ZCA API. -- cgit v1.2.3 From a26e3298ddd73ad782132f9b1098e02f7ed55c42 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Fri, 4 Dec 2015 02:39:39 -0800 Subject: update references to references to python-distribute.org (see #2141 for discussion) --- docs/narr/project.rst | 4 ++-- docs/narr/scaffolding.rst | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/narr/project.rst b/docs/narr/project.rst index 25f3931e9..ec40bc67c 100644 --- a/docs/narr/project.rst +++ b/docs/narr/project.rst @@ -680,8 +680,8 @@ testing, packaging, and distributing your application. ``setup.py`` is the de facto standard which Python developers use to distribute their reusable code. You can read more about ``setup.py`` files and their usage in the `Setuptools documentation - `_ and `The Hitchhiker's - Guide to Packaging `_. + `_ and `Python Packaging + User Guide `_. Our generated ``setup.py`` looks like this: diff --git a/docs/narr/scaffolding.rst b/docs/narr/scaffolding.rst index 8677359de..164ceb3bf 100644 --- a/docs/narr/scaffolding.rst +++ b/docs/narr/scaffolding.rst @@ -22,10 +22,10 @@ found by the ``pcreate`` command. To create a scaffold template, create a Python :term:`distribution` to house the scaffold which includes a ``setup.py`` that relies on the ``setuptools`` -package. See `Creating a Package -`_ for more information about -how to do this. For example, we'll pretend the distribution you create is -named ``CoolExtension``, and it has a package directory within it named +package. See `Packaging and Distributing Projects +`_ for more information +about how to do this. For example, we'll pretend the distribution you create +is named ``CoolExtension``, and it has a package directory within it named ``coolextension``. Once you've created the distribution, put a "scaffolds" directory within your -- cgit v1.2.3 From 1fefda3e6d104e6a308c2daa898a85116f4a3794 Mon Sep 17 00:00:00 2001 From: Ismail Date: Mon, 7 Dec 2015 14:09:11 +0000 Subject: Fix minor typo --- pyramid/config/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index e386bc4e1..a6899abbf 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1809,7 +1809,7 @@ class ViewsConfiguratorMixin(object): signatures than the ones supported by :app:`Pyramid` as described in its narrative documentation. - The ``mapper`` should argument be an object implementing + The ``mapper`` argument should be an object implementing :class:`pyramid.interfaces.IViewMapperFactory` or a :term:`dotted Python name` to such an object. The provided ``mapper`` will become the default view mapper to be used by all subsequent :term:`view -- cgit v1.2.3 From 62222d69b7b6ef573d7f52529b15285af4111f20 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 7 Dec 2015 21:11:11 -0600 Subject: support getting the file path from a FSAssetSource even if it doesn't exist --- pyramid/config/assets.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyramid/config/assets.py b/pyramid/config/assets.py index bbdf18ced..d05314384 100644 --- a/pyramid/config/assets.py +++ b/pyramid/config/assets.py @@ -262,12 +262,15 @@ class FSAssetSource(object): def __init__(self, prefix): self.prefix = prefix - def get_filename(self, resource_name): + def get_path(self, resource_name): if resource_name: path = os.path.join(self.prefix, resource_name) else: path = self.prefix + return path + def get_filename(self, resource_name): + path = self.get_path(resource_name) if os.path.exists(path): return path -- cgit v1.2.3 From d0bd5fb326d8999e3a40e6e2d121aa69cfe05476 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 7 Dec 2015 22:58:06 -0600 Subject: add a first cut at an add_cache_buster api --- pyramid/config/views.py | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 67a70145c..f496dfb7d 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1,5 +1,5 @@ -import bisect import inspect +import posixpath import operator import os import warnings @@ -21,6 +21,7 @@ from pyramid.interfaces import ( IException, IExceptionViewClassifier, IMultiView, + IPackageOverrides, IRendererFactory, IRequest, IResponse, @@ -1987,6 +1988,8 @@ class StaticURLInfo(object): subpath = subpath.replace('\\', '/') # windows # translate spec into overridden spec and lookup cache buster # to modify subpath, kw + subpath, kw = self._bust_asset_path( + request.registry, spec, subpath, kw) if url is None: kw['subpath'] = subpath return request.route_url(route_name, **kw) @@ -2099,9 +2102,7 @@ class StaticURLInfo(object): idx = specs.index(spec) cache_busters.pop(idx) - lengths = [len(t[0]) for t in cache_busters] - new_idx = bisect.bisect_left(lengths, len(spec)) - cache_busters.insert(new_idx, (spec, cachebust)) + cache_busters.insert(0, (spec, cachebust)) intr = config.introspectable('cache busters', spec, @@ -2112,7 +2113,24 @@ class StaticURLInfo(object): config.action(None, callable=register, introspectables=(intr,)) - def _find_cache_buster(self, registry, spec): + def _bust_asset_path(self, registry, spec, subpath, kw): + pkg_name, pkg_subpath = spec.split(':') + absspec = rawspec = '{0}:{1}{2}'.format(pkg_name, pkg_subpath, subpath) + overrides = registry.queryUtility(IPackageOverrides, name=pkg_name) + if overrides is not None: + resource_name = posixpath.join(pkg_subpath, subpath) + sources = overrides.filtered_sources(resource_name) + for source, filtered_path in sources: + rawspec = source.get_path(filtered_path) + if hasattr(source, 'pkg_name'): + rawspec = '{0}:{1}'.format(source.pkg_name, rawspec) + break + for base_spec, cachebust in self.cache_busters: - if base_spec.startswith(spec): - pass + if ( + base_spec == rawspec or + (base_spec.endswith('/') and rawspec.startswith(base_spec)) + ): + subpath, kw = cachebust(absspec, subpath, kw) + break + return subpath, kw -- cgit v1.2.3 From 73630eae045549b792c4e3ef77920357f89c6874 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 7 Dec 2015 23:16:26 -0600 Subject: sort by length such that longer paths are tested first --- pyramid/config/views.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index f496dfb7d..16b150a9e 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1,3 +1,4 @@ +import bisect import inspect import posixpath import operator @@ -2102,7 +2103,9 @@ class StaticURLInfo(object): idx = specs.index(spec) cache_busters.pop(idx) - cache_busters.insert(0, (spec, cachebust)) + lengths = [len(t[0]) for t in cache_busters] + new_idx = bisect.bisect_left(lengths, len(spec)) + cache_busters.insert(new_idx, (spec, cachebust)) intr = config.introspectable('cache busters', spec, @@ -2126,7 +2129,7 @@ class StaticURLInfo(object): rawspec = '{0}:{1}'.format(source.pkg_name, rawspec) break - for base_spec, cachebust in self.cache_busters: + for base_spec, cachebust in reversed(self.cache_busters): if ( base_spec == rawspec or (base_spec.endswith('/') and rawspec.startswith(base_spec)) -- cgit v1.2.3 From 54d00fd7ff0fcfd19799cbffedb860d08604b83c Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 7 Dec 2015 23:21:22 -0600 Subject: support os.sep on windows --- pyramid/config/views.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 16b150a9e..304ce2d43 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -2132,7 +2132,11 @@ class StaticURLInfo(object): for base_spec, cachebust in reversed(self.cache_busters): if ( base_spec == rawspec or - (base_spec.endswith('/') and rawspec.startswith(base_spec)) + ( + base_spec.endswith(os.sep) + if os.path.isabs(base_spec) + else base_spec.endswith('/') + ) and rawspec.startswith(base_spec) ): subpath, kw = cachebust(absspec, subpath, kw) break -- cgit v1.2.3 From d4177a51aff1a46d1c2223db6ed7afd99964e8ad Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Tue, 8 Dec 2015 01:41:06 -0800 Subject: update narrative docs and literalinclude source files that use the starter scaffold to reflect its current state --- docs/narr/MyProject/development.ini | 6 +- .../MyProject/myproject/templates/mytemplate.pt | 13 +-- docs/narr/MyProject/myproject/tests.py | 1 + docs/narr/MyProject/production.ini | 2 +- docs/narr/MyProject/setup.py | 45 ++++------- docs/narr/project.rst | 94 ++++++++++++---------- 6 files changed, 81 insertions(+), 80 deletions(-) diff --git a/docs/narr/MyProject/development.ini b/docs/narr/MyProject/development.ini index a9a26e56b..749e574eb 100644 --- a/docs/narr/MyProject/development.ini +++ b/docs/narr/MyProject/development.ini @@ -11,7 +11,7 @@ pyramid.debug_authorization = false pyramid.debug_notfound = false pyramid.debug_routematch = false pyramid.default_locale_name = en -pyramid.includes = +pyramid.includes = pyramid_debugtoolbar # By default, the toolbar only appears for clients from IP addresses @@ -24,7 +24,7 @@ pyramid.includes = [server:main] use = egg:waitress#main -host = 0.0.0.0 +host = 127.0.0.1 port = 6543 ### @@ -57,4 +57,4 @@ level = NOTSET formatter = generic [formatter_generic] -format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/narr/MyProject/myproject/templates/mytemplate.pt b/docs/narr/MyProject/myproject/templates/mytemplate.pt index e6b00a145..65d7f0609 100644 --- a/docs/narr/MyProject/myproject/templates/mytemplate.pt +++ b/docs/narr/MyProject/myproject/templates/mytemplate.pt @@ -8,12 +8,12 @@ - Starter Template for The Pyramid Web Framework + Starter Scaffold for The Pyramid Web Framework - + @@ -33,19 +33,20 @@
-

Pyramid starter template

-

Welcome to ${project}, an application generated by
the Pyramid Web Framework.

+

Pyramid Starter scaffold

+

Welcome to ${project}, an application generated by
the Pyramid Web Framework 1.6b2.

diff --git a/docs/narr/MyProject/myproject/tests.py b/docs/narr/MyProject/myproject/tests.py index 8c60407e5..63d00910c 100644 --- a/docs/narr/MyProject/myproject/tests.py +++ b/docs/narr/MyProject/myproject/tests.py @@ -16,6 +16,7 @@ class ViewTests(unittest.TestCase): info = my_view(request) self.assertEqual(info['project'], 'MyProject') + class ViewIntegrationTests(unittest.TestCase): def setUp(self): """ This sets up the application registry with the diff --git a/docs/narr/MyProject/production.ini b/docs/narr/MyProject/production.ini index 9eae9e03f..3ccbe6619 100644 --- a/docs/narr/MyProject/production.ini +++ b/docs/narr/MyProject/production.ini @@ -51,4 +51,4 @@ level = NOTSET formatter = generic [formatter_generic] -format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/narr/MyProject/setup.py b/docs/narr/MyProject/setup.py index 9f34540a7..8c019af51 100644 --- a/docs/narr/MyProject/setup.py +++ b/docs/narr/MyProject/setup.py @@ -1,42 +1,30 @@ -"""Setup for the MyProject package. - -""" import os -from setuptools import setup, find_packages - - -HERE = os.path.abspath(os.path.dirname(__file__)) - -with open(os.path.join(HERE, 'README.txt')) as fp: - README = fp.read() - - -with open(os.path.join(HERE, 'CHANGES.txt')) as fp: - CHANGES = fp.read() +from setuptools import setup, find_packages +here = os.path.abspath(os.path.dirname(__file__)) +with open(os.path.join(here, 'README.txt')) as f: + README = f.read() +with open(os.path.join(here, 'CHANGES.txt')) as f: + CHANGES = f.read() -REQUIRES = [ +requires = [ 'pyramid', 'pyramid_chameleon', 'pyramid_debugtoolbar', 'waitress', ] -TESTS_REQUIRE = [ - 'webtest' - ] - setup(name='MyProject', version='0.0', description='MyProject', long_description=README + '\n\n' + CHANGES, classifiers=[ - 'Programming Language :: Python', - 'Framework :: Pyramid', - 'Topic :: Internet :: WWW/HTTP', - 'Topic :: Internet :: WWW/HTTP :: WSGI :: Application', - ], + "Programming Language :: Python", + "Framework :: Pyramid", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", + ], author='', author_email='', url='', @@ -44,10 +32,11 @@ setup(name='MyProject', packages=find_packages(), include_package_data=True, zip_safe=False, - install_requires=REQUIRES, - tests_require=TESTS_REQUIRE, - test_suite='myproject', + install_requires=requires, + tests_require=requires, + test_suite="myproject", entry_points="""\ [paste.app_factory] main = myproject:main - """) + """, + ) diff --git a/docs/narr/project.rst b/docs/narr/project.rst index ec40bc67c..4785b60c4 100644 --- a/docs/narr/project.rst +++ b/docs/narr/project.rst @@ -77,7 +77,7 @@ The below example uses the ``pcreate`` command to create a project with the On UNIX: -.. code-block:: text +.. code-block:: bash $ $VENV/bin/pcreate -s starter MyProject @@ -90,7 +90,7 @@ Or on Windows: Here's sample output from a run of ``pcreate`` on UNIX for a project we name ``MyProject``: -.. code-block:: text +.. code-block:: bash $ $VENV/bin/pcreate -s starter MyProject Creating template pyramid @@ -158,7 +158,7 @@ created project directory. On UNIX: -.. code-block:: text +.. code-block:: bash $ cd MyProject $ $VENV/bin/python setup.py develop @@ -172,7 +172,7 @@ Or on Windows: Elided output from a run of this command on UNIX is shown below: -.. code-block:: text +.. code-block:: bash $ cd MyProject $ $VENV/bin/python setup.py develop @@ -198,7 +198,7 @@ directory of your virtualenv). On UNIX: -.. code-block:: text +.. code-block:: bash $ $VENV/bin/python setup.py test -q @@ -210,7 +210,7 @@ Or on Windows: Here's sample output from a test run on UNIX: -.. code-block:: text +.. code-block:: bash $ $VENV/bin/python setup.py test -q running test @@ -221,11 +221,23 @@ Here's sample output from a test run on UNIX: writing dependency_links to MyProject.egg-info/dependency_links.txt writing entry points to MyProject.egg-info/entry_points.txt reading manifest file 'MyProject.egg-info/SOURCES.txt' + reading manifest template 'MANIFEST.in' + warning: no files found matching '*.cfg' + warning: no files found matching '*.rst' + warning: no files found matching '*.ico' under directory 'myproject' + warning: no files found matching '*.gif' under directory 'myproject' + warning: no files found matching '*.jpg' under directory 'myproject' + warning: no files found matching '*.txt' under directory 'myproject' + warning: no files found matching '*.mak' under directory 'myproject' + warning: no files found matching '*.mako' under directory 'myproject' + warning: no files found matching '*.js' under directory 'myproject' + warning: no files found matching '*.html' under directory 'myproject' + warning: no files found matching '*.xml' under directory 'myproject' writing manifest file 'MyProject.egg-info/SOURCES.txt' running build_ext - .. + . ---------------------------------------------------------------------- - Ran 1 test in 0.108s + Ran 1 test in 0.008s OK @@ -256,7 +268,7 @@ file. In our case, this file is named ``development.ini``. On UNIX: -.. code-block:: text +.. code-block:: bash $ $VENV/bin/pserve development.ini @@ -268,38 +280,38 @@ On Windows: Here's sample output from a run of ``pserve`` on UNIX: -.. code-block:: text +.. code-block:: bash $ $VENV/bin/pserve development.ini - Starting server in PID 16601. - serving on http://0.0.0.0:6543 - -When you use ``pserve`` to start the application implied by the default -rendering of a scaffold, it will respond to requests on *all* IP addresses -possessed by your system, not just requests to ``localhost``. This is what the -``0.0.0.0`` in ``serving on http://0.0.0.0:6543`` means. The server will -respond to requests made to ``127.0.0.1`` and on any external IP address. For -example, your system might be configured to have an external IP address -``192.168.1.50``. If that's the case, if you use a browser running on the same -system as Pyramid, it will be able to access the application via -``http://127.0.0.1:6543/`` as well as via ``http://192.168.1.50:6543/``. -However, *other people* on other computers on the same network will also be -able to visit your Pyramid application in their browser by visiting -``http://192.168.1.50:6543/``. - -If you want to restrict access such that only a browser running on the same -machine as Pyramid will be able to access your Pyramid application, edit the + Starting server in PID 16208. + serving on http://127.0.0.1:6543 + +Access is restricted such that only a browser running on the same machine as +Pyramid will be able to access your Pyramid application. However, if you want +to open access to other machines on the same network, then edit the ``development.ini`` file, and replace the ``host`` value in the -``[server:main]`` section. Change it from ``0.0.0.0`` to ``127.0.0.1``. For +``[server:main]`` section, changing it from ``127.0.0.1`` to ``0.0.0.0``. For example: .. code-block:: ini [server:main] use = egg:waitress#main - host = 127.0.0.1 + host = 0.0.0.0 port = 6543 +Now when you use ``pserve`` to start the application, it will respond to +requests on *all* IP addresses possessed by your system, not just requests to +``localhost``. This is what the ``0.0.0.0`` in +``serving on http://0.0.0.0:6543`` means. The server will respond to requests +made to ``127.0.0.1`` and on any external IP address. For example, your system +might be configured to have an external IP address ``192.168.1.50``. If that's +the case, if you use a browser running on the same system as Pyramid, it will +be able to access the application via ``http://127.0.0.1:6543/`` as well as via +``http://192.168.1.50:6543/``. However, *other people* on other computers on +the same network will also be able to visit your Pyramid application in their +browser by visiting ``http://192.168.1.50:6543/``. + You can change the port on which the server runs on by changing the same portion of the ``development.ini`` file. For example, you can change the ``port = 6543`` line in the ``development.ini`` file's ``[server:main]`` @@ -347,7 +359,7 @@ For example, on UNIX: $ $VENV/bin/pserve development.ini --reload Starting subprocess with file monitor Starting server in PID 16601. - serving on http://0.0.0.0:6543 + serving on http://127.0.0.1:6543 Now if you make a change to any of your project's ``.py`` files or ``.ini`` files, you'll see the server restart automatically: @@ -357,7 +369,7 @@ files, you'll see the server restart automatically: development.ini changed; reloading... -------------------- Restarting -------------------- Starting server in PID 16602. - serving on http://0.0.0.0:6543 + serving on http://127.0.0.1:6543 Changes to template files (such as ``.pt`` or ``.mak`` files) won't cause the server to restart. Changes to template files don't require a server restart as @@ -579,18 +591,16 @@ file. The name ``main`` is a convention used by PasteDeploy signifying that it is the default application. The ``[server:main]`` section of the configuration file configures a WSGI -server which listens on TCP port 6543. It is configured to listen on all -interfaces (``0.0.0.0``). This means that any remote system which has TCP -access to your system can see your Pyramid application. +server which listens on TCP port 6543. It is configured to listen on localhost +only (``127.0.0.1``). .. _MyProject_ini_logging: -The sections that live between the markers ``# Begin logging configuration`` -and ``# End logging configuration`` represent Python's standard library -:mod:`logging` module configuration for your application. The sections between -these two markers are passed to the `logging module's config file configuration -engine `_ when -the ``pserve`` or ``pshell`` commands are executed. The default configuration +The sections after ``# logging configuration`` represent Python's standard +library :mod:`logging` module configuration for your application. These +sections are passed to the `logging module's config file configuration engine +`_ when the +``pserve`` or ``pshell`` commands are executed. The default configuration sends application logging output to the standard error output of your terminal. For more information about logging configuration, see :ref:`logging_chapter`. @@ -912,7 +922,7 @@ The ``tests.py`` module includes unit tests for your application. .. literalinclude:: MyProject/myproject/tests.py :language: python - :lines: 1-18 + :lines: 1-17 :linenos: This sample ``tests.py`` file has a single unit test defined within it. This -- cgit v1.2.3 From bf8014ec3458b46c427706988a8ca26380707cd7 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Tue, 8 Dec 2015 02:43:14 -0800 Subject: update narrative docs and literalinclude source files that use the starter scaffold to reflect its current state --- docs/narr/startup.rst | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/docs/narr/startup.rst b/docs/narr/startup.rst index 485f6b181..3e168eaea 100644 --- a/docs/narr/startup.rst +++ b/docs/narr/startup.rst @@ -6,15 +6,15 @@ Startup When you cause a :app:`Pyramid` application to start up in a console window, you'll see something much like this show up on the console: -.. code-block:: text +.. code-block:: bash - $ pserve development.ini - Starting server in PID 16601. - serving on 0.0.0.0:6543 view at http://127.0.0.1:6543 + $ $VENV/bin/pserve development.ini + Starting server in PID 16305. + serving on http://127.0.0.1:6543 This chapter explains what happens between the time you press the "Return" key on your keyboard after typing ``pserve development.ini`` and the time the line -``serving on 0.0.0.0:6543 ...`` is output to your console. +``serving on http://127.0.0.1:6543`` is output to your console. .. index:: single: startup process @@ -92,11 +92,11 @@ Here's a high-level time-ordered overview of what happens when you press In this case, the ``myproject.__init__:main`` function referred to by the entry point URI ``egg:MyProject`` (see :ref:`MyProject_ini` for more information about entry point URIs, and how they relate to callables) will - receive the key/value pairs ``{'pyramid.reload_templates':'true', - 'pyramid.debug_authorization':'false', 'pyramid.debug_notfound':'false', - 'pyramid.debug_routematch':'false', 'pyramid.debug_templates':'true', - 'pyramid.default_locale_name':'en'}``. See :ref:`environment_chapter` for - the meanings of these keys. + receive the key/value pairs ``{pyramid.reload_templates = true, + pyramid.debug_authorization = false, pyramid.debug_notfound = false, + pyramid.debug_routematch = false, pyramid.default_locale_name = en, and + pyramid.includes = pyramid_debugtoolbar}``. See :ref:`environment_chapter` + for the meanings of these keys. #. The ``main`` function first constructs a :class:`~pyramid.config.Configurator` instance, passing the ``settings`` @@ -131,10 +131,9 @@ Here's a high-level time-ordered overview of what happens when you press #. ``pserve`` starts the WSGI *server* defined within the ``[server:main]`` section. In our case, this is the Waitress server (``use = egg:waitress#main``), and it will listen on all interfaces (``host = - 0.0.0.0``), on port number 6543 (``port = 6543``). The server code itself - is what prints ``serving on 0.0.0.0:6543 view at http://127.0.0.1:6543``. - The server serves the application, and the application is running, waiting - to receive requests. + 127.0.0.1``), on port number 6543 (``port = 6543``). The server code itself + is what prints ``serving on http://127.0.0.1:6543``. The server serves the + application, and the application is running, waiting to receive requests. .. seealso:: Logging configuration is described in the :ref:`logging_chapter` chapter. -- cgit v1.2.3 From aecb4722640bc49a8e479f5eb5f332346535be8d Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 8 Dec 2015 09:22:35 -0600 Subject: allow disabling the cache buster --- pyramid/config/views.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 304ce2d43..1115ccffc 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1982,15 +1982,16 @@ class StaticURLInfo(object): self.cache_busters = [] def generate(self, path, request, **kw): + disable_cache_buster = ( + request.registry.settings['pyramid.prevent_cachebust']) for (url, spec, route_name) in self.registrations: if path.startswith(spec): subpath = path[len(spec):] if WIN: # pragma: no cover subpath = subpath.replace('\\', '/') # windows - # translate spec into overridden spec and lookup cache buster - # to modify subpath, kw - subpath, kw = self._bust_asset_path( - request.registry, spec, subpath, kw) + if not disable_cache_buster: + subpath, kw = self._bust_asset_path( + request.registry, spec, subpath, kw) if url is None: kw['subpath'] = subpath return request.route_url(route_name, **kw) -- cgit v1.2.3 From eedef93f0c4c52ea11320bcd49386262fa7293a1 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 8 Dec 2015 11:12:38 -0600 Subject: update the cache busting narrative to use add_cache_buster --- docs/narr/assets.rst | 103 +++++++++++++++++++++++++++++++++++---------------- 1 file changed, 72 insertions(+), 31 deletions(-) diff --git a/docs/narr/assets.rst b/docs/narr/assets.rst index 0f819570c..8b41f9b8a 100644 --- a/docs/narr/assets.rst +++ b/docs/narr/assets.rst @@ -366,8 +366,7 @@ resource and requests the asset, regardless of any caching policy set for the resource's old URL. :app:`Pyramid` can be configured to produce cache busting URLs for static -assets by passing the optional argument, ``cachebust`` to -:meth:`~pyramid.config.Configurator.add_static_view`: +assets using :meth:`~pyramid.config.Configurator.add_cache_buster`: .. code-block:: python :linenos: @@ -376,14 +375,18 @@ assets by passing the optional argument, ``cachebust`` to from pyramid.static import QueryStringConstantCacheBuster # config is an instance of pyramid.config.Configurator - config.add_static_view( - name='static', path='mypackage:folder/static', - cachebust=QueryStringConstantCacheBuster(str(int(time.time()))), - ) + config.add_static_view(name='static', path='mypackage:folder/static/') + config.add_cache_buster( + 'mypackage:folder/static/', + QueryStringConstantCacheBuster(str(int(time.time())))) + +.. note:: + The trailing slash on the ``add_cache_buster`` call is important to + indicate that it is overriding every asset in the folder and not just a + file named ``static``. -Setting the ``cachebust`` argument instructs :app:`Pyramid` to use a cache -busting scheme which adds the curent time for a static asset to the query -string in the asset's URL: +Adding the cachebuster instructs :app:`Pyramid` to add the current time for +a static asset to the query string in the asset's URL: .. code-block:: python :linenos: @@ -392,10 +395,7 @@ string in the asset's URL: # Returns: 'http://www.example.com/static/js/myapp.js?x=1445318121' When the web server restarts, the time constant will change and therefore so -will its URL. Supplying the ``cachebust`` argument also causes the static -view to set headers instructing clients to cache the asset for ten years, -unless the ``cache_max_age`` argument is also passed, in which case that -value is used. +will its URL. .. note:: @@ -410,7 +410,7 @@ Disabling the Cache Buster It can be useful in some situations (e.g., development) to globally disable all configured cache busters without changing calls to -:meth:`~pyramid.config.Configurator.add_static_view`. To do this set the +:meth:`~pyramid.config.Configurator.add_cache_buster`. To do this set the ``PYRAMID_PREVENT_CACHEBUST`` environment variable or the ``pyramid.prevent_cachebust`` configuration value to a true value. @@ -419,9 +419,9 @@ configured cache busters without changing calls to Customizing the Cache Buster ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The ``cachebust`` option to -:meth:`~pyramid.config.Configurator.add_static_view` may be set to any object -that implements the interface :class:`~pyramid.interfaces.ICacheBuster`. +Calls to :meth:`~pyramid.config.Configurator.add_cache_buster` may use +any object that implements the interface +:class:`~pyramid.interfaces.ICacheBuster`. :app:`Pyramid` ships with a very simplistic :class:`~pyramid.static.QueryStringConstantCacheBuster`, which adds an @@ -461,8 +461,30 @@ the hash of the current commit: def tokenize(self, pathspec): return self.sha1 -Choosing a Cache Buster -~~~~~~~~~~~~~~~~~~~~~~~ +A simple cache buster that modifies the path segment can be constructed as +well: + +.. code-block:: python + :linenos: + + import posixpath + + def cache_buster(spec, subpath, kw): + base_subpath, ext = posixpath.splitext(subpath) + new_subpath = base_subpath + '-asdf' + ext + return new_subpath, kw + +The caveat with this approach is that modifying the path segment +changes the file name, and thus must match what is actually on the +filesystem in order for :meth:`~pyramid.config.Configurator.add_static_view` +to find the file. It's better to use the +:class:`~pyramid.static.ManifestCacheBuster` for these situations, as +described in the next section. + +.. _path_segment_cache_busters: + +Path Segments and Choosing a Cache Buster +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Many caching HTTP proxies will fail to cache a resource if the URL contains a query string. Therefore, in general, you should prefer a cache busting @@ -504,8 +526,11 @@ The following code would set up a cachebuster: config.add_static_view( name='http://mycdn.example.com/', - path='mypackage:static', - cachebust=ManifestCacheBuster('myapp:static/manifest.json')) + path='mypackage:static') + + config.add_cache_buster( + 'mypackage:static/', + ManifestCacheBuster('myapp:static/manifest.json')) A simpler approach is to use the :class:`~pyramid.static.QueryStringConstantCacheBuster` to generate a global @@ -524,8 +549,11 @@ start up as a cachebust token: config.add_static_view( name='http://mycdn.example.com/', - path='mypackage:static', - cachebust=QueryStringConstantCacheBuster(str(int(time.time())))) + path='mypackage:static') + + config.add_cache_buster( + 'mypackage:static/', + QueryStringConstantCacheBuster(str(int(time.time())))) .. index:: single: static assets view @@ -536,25 +564,38 @@ CSS and JavaScript source and cache busting Often one needs to refer to images and other static assets inside CSS and JavaScript files. If cache busting is active, the final static asset URL is not available until the static assets have been assembled. These URLs cannot be -handwritten. Thus, when having static asset references in CSS and JavaScript, -one needs to perform one of the following tasks: +handwritten. Below is an example of how to integrate the cache buster into +the entire stack. Remember, it is just an example and should be modified to +fit your specific tools. -* Process the files by using a precompiler which rewrites URLs to their final - cache busted form. Then, you can use the +* First, process the files by using a precompiler which rewrites URLs to their + final cache-busted form. Then, you can use the :class:`~pyramid.static.ManifestCacheBuster` to synchronize your asset pipeline with :app:`Pyramid`, allowing the pipeline to have full control over the final URLs of your assets. -* Templatize JS and CSS, and call ``request.static_url()`` inside their - template code. +Now that you are able to generate static URLs within :app:`Pyramid`, +you'll need to handle URLs that are out of our control. To do this you may +use some of the following options to get started: -* Pass static URL references to CSS and JavaScript via other means. +* Configure your asset pipeline to rewrite URL references inline in + CSS and JavaScript. This is the best approach because then the files + may be hosted by :app:`Pyramid` or an external CDN without haven't to + change anything. They really are static. + +* Templatize JS and CSS, and call ``request.static_url()`` inside their + template code. While this approach may work in certain scenarios, it is not + recommended because your static assets will not really be static and are now + dependent on :app:`Pyramid` to be served correctly. See + :ref:`advanced static` for more information on this approach. If your CSS and JavaScript assets use URLs to reference other assets it is recommended that you implement an external asset pipeline that can rewrite the generated static files with new URLs containing cache busting tokens. The machinery inside :app:`Pyramid` will not help with this step as it has very -little knowledge of the asset types your application may use. +little knowledge of the asset types your application may use. The integration +into :app:`Pyramid` is simply for linking those assets into your HTML and +other dynamic content. .. _advanced_static: -- cgit v1.2.3 From ffad12b0ac1ee24ad12d6d1a2f300da1ec004010 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 8 Dec 2015 11:24:35 -0600 Subject: pass the raw asset spec into the cache buster --- pyramid/config/views.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 1115ccffc..44003127a 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -2119,7 +2119,7 @@ class StaticURLInfo(object): def _bust_asset_path(self, registry, spec, subpath, kw): pkg_name, pkg_subpath = spec.split(':') - absspec = rawspec = '{0}:{1}{2}'.format(pkg_name, pkg_subpath, subpath) + rawspec = None overrides = registry.queryUtility(IPackageOverrides, name=pkg_name) if overrides is not None: resource_name = posixpath.join(pkg_subpath, subpath) @@ -2130,6 +2130,9 @@ class StaticURLInfo(object): rawspec = '{0}:{1}'.format(source.pkg_name, rawspec) break + if rawspec is None: + rawspec = '{0}:{1}{2}'.format(pkg_name, pkg_subpath, subpath) + for base_spec, cachebust in reversed(self.cache_busters): if ( base_spec == rawspec or @@ -2139,6 +2142,6 @@ class StaticURLInfo(object): else base_spec.endswith('/') ) and rawspec.startswith(base_spec) ): - subpath, kw = cachebust(absspec, subpath, kw) + subpath, kw = cachebust(rawspec, subpath, kw) break return subpath, kw -- cgit v1.2.3 From edad4ceca802324194000a98f3b07da7cedee546 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 8 Dec 2015 15:56:30 -0600 Subject: tweak ManifestCacheBuster to allow overriding parse_manifest without touching the file loading logic --- pyramid/static.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/pyramid/static.py b/pyramid/static.py index cda98bea4..9559cd881 100644 --- a/pyramid/static.py +++ b/pyramid/static.py @@ -227,7 +227,7 @@ class ManifestCacheBuster(object): "images/background.png": "images/background-a8169106.png", } - Specifically, it is a JSON-serialized dictionary where the keys are the + By default, it is a JSON-serialized dictionary where the keys are the source asset paths used in calls to :meth:`~pyramid.request.Request.static_url`. For example:: @@ -236,6 +236,9 @@ class ManifestCacheBuster(object): >>> request.static_url('myapp:static/css/main.css') "http://www.example.com/static/css/main-678b7c80.css" + The file format and location can be changed by subclassing and overriding + :meth:`.parse_manifest`. + If a path is not found in the manifest it will pass through unchanged. If ``reload`` is ``True`` then the manifest file will be reloaded when @@ -244,11 +247,6 @@ class ManifestCacheBuster(object): If the manifest file cannot be found on disk it will be treated as an empty mapping unless ``reload`` is ``False``. - The default implementation assumes the requested (possibly cache-busted) - path is the actual filename on disk. Subclasses may override the ``match`` - method to alter this behavior. For example, to strip the cache busting - token from the path. - .. versionadded:: 1.6 """ exists = staticmethod(exists) # testing @@ -262,20 +260,23 @@ class ManifestCacheBuster(object): self._mtime = None if not reload: - self._manifest = self.parse_manifest() + self._manifest = self.get_manifest() - def parse_manifest(self): + def get_manifest(self): + with open(self.manifest_path, 'rb') as fp: + return self.parse_manifest(fp.read()) + + def parse_manifest(self, content): """ - Return a mapping parsed from the ``manifest_path``. + Parse the ``content`` read from the ``manifest_path`` into a + dictionary mapping. Subclasses may override this method to use something other than ``json.loads`` to load any type of file format and return a conforming dictionary. """ - with open(self.manifest_path, 'rb') as fp: - content = fp.read().decode('utf-8') - return json.loads(content) + return json.loads(content.decode('utf-8')) @property def manifest(self): @@ -285,7 +286,7 @@ class ManifestCacheBuster(object): return {} mtime = self.getmtime(self.manifest_path) if self._mtime is None or mtime > self._mtime: - self._manifest = self.parse_manifest() + self._manifest = self.get_manifest() self._mtime = mtime return self._manifest -- cgit v1.2.3 From b2fc4ace7fdb1dd2e90d6d3cc82f7b7b923ffa68 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 8 Dec 2015 13:19:56 -0600 Subject: update docs on pathspec arg to ICacheBuster --- pyramid/interfaces.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index bdf5bdfbe..153fdad03 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -1204,8 +1204,16 @@ class ICacheBuster(Interface): The ``kw`` argument is a dict of keywords that are to be passed eventually to :meth:`~pyramid.request.Request.static_url` for URL generation. The return value should be a two-tuple of - ``(subpath, kw)`` which are versions of the same arguments modified - to include the cache bust token in the generated URL. + ``(subpath, kw)`` where ``subpath`` is the relative URL from where the + file is served and ``kw`` is the same input argument. The return value + should be modified to include the cache bust token in the generated + URL. + + The ``pathspec`` refers to original location of the file, ignoring any + calls to :meth:`pyramid.config.Configurator.override_asset`. For + example, with a call ``request.static_url('myapp:static/foo.png'), the + ``pathspec`` may be ``themepkg:bar.png``, assuming a call to + ``config.override_asset('myapp:static/foo.png', 'themepkg:bar.png')``. """ # configuration phases: a lower phase number means the actions associated -- cgit v1.2.3 From 6923cae7f493c39b17367a3935a26065d4795ea6 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 8 Dec 2015 16:38:56 -0600 Subject: support cache busting only full folders --- pyramid/config/views.py | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 44003127a..ed7ae42ce 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1943,15 +1943,18 @@ class ViewsConfiguratorMixin(object): def add_cache_buster(self, path, cachebust): """ - The ``cachebust`` keyword argument may be set to cause + Add a cache buster to a set of files on disk. + + The ``path`` should be the path on disk where the static files + reside. This can be an absolute path, a package-relative path, or a + :term:`asset specification`. + + The ``cachebust`` argument may be set to cause :meth:`~pyramid.request.Request.static_url` to use cache busting when generating URLs. See :ref:`cache_busting` for general information about cache busting. The value of the ``cachebust`` argument must be an object which implements - :class:`~pyramid.interfaces.ICacheBuster`. If the ``cachebust`` - argument is provided, the default for ``cache_max_age`` is modified - to be ten years. ``cache_max_age`` may still be explicitly provided - to override this default. + :class:`~pyramid.interfaces.ICacheBuster`. """ spec = self._make_spec(path) @@ -2096,6 +2099,15 @@ class StaticURLInfo(object): config.action(None, callable=register, introspectables=(intr,)) def add_cache_buster(self, config, spec, cachebust): + # ensure the spec always has a trailing slash as we only support + # adding cache busters to folders, not files + if os.path.isabs(spec): # FBO windows + sep = os.sep + else: + sep = '/' + if not spec.endswith(sep) and not spec.endswith(':'): + spec = spec + sep + def register(): cache_busters = self.cache_busters @@ -2134,14 +2146,7 @@ class StaticURLInfo(object): rawspec = '{0}:{1}{2}'.format(pkg_name, pkg_subpath, subpath) for base_spec, cachebust in reversed(self.cache_busters): - if ( - base_spec == rawspec or - ( - base_spec.endswith(os.sep) - if os.path.isabs(base_spec) - else base_spec.endswith('/') - ) and rawspec.startswith(base_spec) - ): + if rawspec.startswith(base_spec): subpath, kw = cachebust(rawspec, subpath, kw) break return subpath, kw -- cgit v1.2.3 From 5e3439059daa94543f9437a280fed8d804cc7596 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Wed, 9 Dec 2015 20:34:54 -0600 Subject: fix broken tests --- pyramid/config/views.py | 35 ++++--- pyramid/tests/test_config/test_views.py | 173 ++++++++++++++++++++++---------- pyramid/tests/test_static.py | 74 +++++++------- 3 files changed, 178 insertions(+), 104 deletions(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index ed7ae42ce..e5bf1203e 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -38,6 +38,7 @@ from pyramid.interfaces import ( from pyramid import renderers +from pyramid.asset import resolve_asset_spec from pyramid.compat import ( string_types, urlparse, @@ -1985,14 +1986,12 @@ class StaticURLInfo(object): self.cache_busters = [] def generate(self, path, request, **kw): - disable_cache_buster = ( - request.registry.settings['pyramid.prevent_cachebust']) for (url, spec, route_name) in self.registrations: if path.startswith(spec): subpath = path[len(spec):] if WIN: # pragma: no cover subpath = subpath.replace('\\', '/') # windows - if not disable_cache_buster: + if self.cache_busters: subpath, kw = self._bust_asset_path( request.registry, spec, subpath, kw) if url is None: @@ -2099,6 +2098,9 @@ class StaticURLInfo(object): config.action(None, callable=register, introspectables=(intr,)) def add_cache_buster(self, config, spec, cachebust): + if config.registry.settings.get('pyramid.prevent_cachebust'): + return + # ensure the spec always has a trailing slash as we only support # adding cache busters to folders, not files if os.path.isabs(spec): # FBO windows @@ -2130,20 +2132,25 @@ class StaticURLInfo(object): config.action(None, callable=register, introspectables=(intr,)) def _bust_asset_path(self, registry, spec, subpath, kw): - pkg_name, pkg_subpath = spec.split(':') + pkg_name, pkg_subpath = resolve_asset_spec(spec) rawspec = None - overrides = registry.queryUtility(IPackageOverrides, name=pkg_name) - if overrides is not None: - resource_name = posixpath.join(pkg_subpath, subpath) - sources = overrides.filtered_sources(resource_name) - for source, filtered_path in sources: - rawspec = source.get_path(filtered_path) - if hasattr(source, 'pkg_name'): - rawspec = '{0}:{1}'.format(source.pkg_name, rawspec) - break + + if pkg_name is not None: + overrides = registry.queryUtility(IPackageOverrides, name=pkg_name) + if overrides is not None: + resource_name = posixpath.join(pkg_subpath, subpath) + sources = overrides.filtered_sources(resource_name) + for source, filtered_path in sources: + rawspec = source.get_path(filtered_path) + if hasattr(source, 'pkg_name'): + rawspec = '{0}:{1}'.format(source.pkg_name, rawspec) + break + + if rawspec is None: + rawspec = '{0}:{1}{2}'.format(pkg_name, pkg_subpath, subpath) if rawspec is None: - rawspec = '{0}:{1}{2}'.format(pkg_name, pkg_subpath, subpath) + rawspec = pkg_subpath + subpath for base_spec, cachebust in reversed(self.cache_busters): if rawspec.startswith(base_spec): diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index 020ed131d..eda8d8b05 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -1,3 +1,4 @@ +import os import unittest from pyramid import testing @@ -3887,49 +3888,35 @@ class TestStaticURLInfo(unittest.TestCase): def test_generate_registration_miss(self): inst = self._makeOne() - registrations = [ - (None, 'spec', 'route_name', None), - ('http://example.com/foo/', 'package:path/', None, None)] - inst._get_registrations = lambda *x: registrations + inst.registrations = [ + (None, 'spec', 'route_name'), + ('http://example.com/foo/', 'package:path/', None)] request = self._makeRequest() result = inst.generate('package:path/abc', request) self.assertEqual(result, 'http://example.com/foo/abc') - def test_generate_registration_no_registry_on_request(self): - inst = self._makeOne() - registrations = [ - ('http://example.com/foo/', 'package:path/', None, None)] - inst._get_registrations = lambda *x: registrations - request = self._makeRequest() - del request.registry - result = inst.generate('package:path/abc', request) - self.assertEqual(result, 'http://example.com/foo/abc') - def test_generate_slash_in_name1(self): inst = self._makeOne() - registrations = [ - ('http://example.com/foo/', 'package:path/', None, None)] - inst._get_registrations = lambda *x: registrations + inst.registrations = [('http://example.com/foo/', 'package:path/', None)] request = self._makeRequest() result = inst.generate('package:path/abc', request) self.assertEqual(result, 'http://example.com/foo/abc') def test_generate_slash_in_name2(self): inst = self._makeOne() - registrations = [ - ('http://example.com/foo/', 'package:path/', None, None)] - inst._get_registrations = lambda *x: registrations + inst.registrations = [('http://example.com/foo/', 'package:path/', None)] request = self._makeRequest() result = inst.generate('package:path/', request) self.assertEqual(result, 'http://example.com/foo/') def test_generate_quoting(self): + from pyramid.interfaces import IStaticURLInfo config = testing.setUp() try: config.add_static_view('images', path='mypkg:templates') - inst = self._makeOne() request = testing.DummyRequest() request.registry = config.registry + inst = config.registry.getUtility(IStaticURLInfo) result = inst.generate('mypkg:templates/foo%2Fbar', request) self.assertEqual(result, 'http://example.com/images/foo%252Fbar') finally: @@ -3937,8 +3924,7 @@ class TestStaticURLInfo(unittest.TestCase): def test_generate_route_url(self): inst = self._makeOne() - registrations = [(None, 'package:path/', '__viewname/', None)] - inst._get_registrations = lambda *x: registrations + inst.registrations = [(None, 'package:path/', '__viewname/')] def route_url(n, **kw): self.assertEqual(n, '__viewname/') self.assertEqual(kw, {'subpath':'abc', 'a':1}) @@ -3950,8 +3936,7 @@ class TestStaticURLInfo(unittest.TestCase): def test_generate_url_unquoted_local(self): inst = self._makeOne() - registrations = [(None, 'package:path/', '__viewname/', None)] - inst._get_registrations = lambda *x: registrations + inst.registrations = [(None, 'package:path/', '__viewname/')] def route_url(n, **kw): self.assertEqual(n, '__viewname/') self.assertEqual(kw, {'subpath':'abc def', 'a':1}) @@ -3963,16 +3948,15 @@ class TestStaticURLInfo(unittest.TestCase): def test_generate_url_quoted_remote(self): inst = self._makeOne() - registrations = [('http://example.com/', 'package:path/', None, None)] - inst._get_registrations = lambda *x: registrations + inst.registrations = [('http://example.com/', 'package:path/', None)] request = self._makeRequest() result = inst.generate('package:path/abc def', request, a=1) self.assertEqual(result, 'http://example.com/abc%20def') def test_generate_url_with_custom_query(self): inst = self._makeOne() - registrations = [('http://example.com/', 'package:path/', None, None)] - inst._get_registrations = lambda *x: registrations + registrations = [('http://example.com/', 'package:path/', None)] + inst.registrations = registrations request = self._makeRequest() result = inst.generate('package:path/abc def', request, a=1, _query='(openlayers)') @@ -3981,12 +3965,10 @@ class TestStaticURLInfo(unittest.TestCase): def test_generate_url_with_custom_anchor(self): inst = self._makeOne() - registrations = [('http://example.com/', 'package:path/', None, None)] - inst._get_registrations = lambda *x: registrations + inst.registrations = [('http://example.com/', 'package:path/', None)] request = self._makeRequest() uc = text_(b'La Pe\xc3\xb1a', 'utf-8') - result = inst.generate('package:path/abc def', request, a=1, - _anchor=uc) + result = inst.generate('package:path/abc def', request, a=1, _anchor=uc) self.assertEqual(result, 'http://example.com/abc%20def#La%20Pe%C3%B1a') @@ -3995,52 +3977,102 @@ class TestStaticURLInfo(unittest.TestCase): kw['foo'] = 'bar' return 'foo' + '/' + subpath, kw inst = self._makeOne() - inst.registrations = [(None, 'package:path/', '__viewname', cachebust)] + inst.registrations = [(None, 'package:path/', '__viewname')] inst.cache_busters = [('package:path/', cachebust)] request = self._makeRequest() + called = [False] def route_url(n, **kw): + called[0] = True self.assertEqual(n, '__viewname') - self.assertEqual(kw, {'subpath':'foo/abc', 'foo':'bar'}) + self.assertEqual(kw, {'subpath': 'foo/abc', 'foo': 'bar'}) request.route_url = route_url inst.generate('package:path/abc', request) + self.assertTrue(called[0]) + + def test_generate_url_cachebust_abspath(self): + here = os.path.dirname(__file__) + os.sep + def cachebust(pathspec, subpath, kw): + kw['foo'] = 'bar' + return 'foo' + '/' + subpath, kw + inst = self._makeOne() + inst.registrations = [(None, here, '__viewname')] + inst.cache_busters = [(here, cachebust)] + request = self._makeRequest() + called = [False] + def route_url(n, **kw): + called[0] = True + self.assertEqual(n, '__viewname') + self.assertEqual(kw, {'subpath': 'foo/abc', 'foo': 'bar'}) + request.route_url = route_url + inst.generate(here + 'abc', request) + self.assertTrue(called[0]) + + def test_generate_url_cachebust_nomatch(self): + def fake_cb(*a, **kw): raise AssertionError + inst = self._makeOne() + inst.registrations = [(None, 'package:path/', '__viewname')] + inst.cache_busters = [('package:path2/', fake_cb)] + request = self._makeRequest() + called = [False] + def route_url(n, **kw): + called[0] = True + self.assertEqual(n, '__viewname') + self.assertEqual(kw, {'subpath': 'abc'}) + request.route_url = route_url + inst.generate('package:path/abc', request) + self.assertTrue(called[0]) + + def test_generate_url_cachebust_with_overrides(self): + config = testing.setUp() + try: + config.add_static_view('static', 'path') + config.override_asset( + 'pyramid.tests.test_config:path/', + 'pyramid.tests.test_config:other_path/') + def cb(pathspec, subpath, kw): + kw['_query'] = {'x': 'foo'} + return subpath, kw + config.add_cache_buster('other_path', cb) + request = testing.DummyRequest() + result = request.static_url('path/foo.png') + self.assertEqual(result, 'http://example.com/static/foo.png?x=foo') + finally: + testing.tearDown() def test_add_already_exists(self): config = DummyConfig() inst = self._makeOne() inst.registrations = [('http://example.com/', 'package:path/', None)] inst.add(config, 'http://example.com', 'anotherpackage:path') - expected = [ - ('http://example.com/', 'anotherpackage:path/', None, None)] + expected = [('http://example.com/', 'anotherpackage:path/', None)] self.assertEqual(inst.registrations, expected) def test_add_package_root(self): config = DummyConfig() inst = self._makeOne() inst.add(config, 'http://example.com', 'package:') - expected = [('http://example.com/', 'package:', None, None)] + expected = [('http://example.com/', 'package:', None)] self.assertEqual(inst.registrations, expected) def test_add_url_withendslash(self): config = DummyConfig() inst = self._makeOne() inst.add(config, 'http://example.com/', 'anotherpackage:path') - expected = [ - ('http://example.com/', 'anotherpackage:path/', None, None)] + expected = [('http://example.com/', 'anotherpackage:path/', None)] self.assertEqual(inst.registrations, expected) def test_add_url_noendslash(self): config = DummyConfig() inst = self._makeOne() inst.add(config, 'http://example.com', 'anotherpackage:path') - expected = [ - ('http://example.com/', 'anotherpackage:path/', None, None)] + expected = [('http://example.com/', 'anotherpackage:path/', None)] self.assertEqual(inst.registrations, expected) def test_add_url_noscheme(self): config = DummyConfig() inst = self._makeOne() inst.add(config, '//example.com', 'anotherpackage:path') - expected = [('//example.com/', 'anotherpackage:path/', None, None)] + expected = [('//example.com/', 'anotherpackage:path/', None)] self.assertEqual(inst.registrations, expected) def test_add_viewname(self): @@ -4049,7 +4081,7 @@ class TestStaticURLInfo(unittest.TestCase): config = DummyConfig() inst = self._makeOne() inst.add(config, 'view', 'anotherpackage:path', cache_max_age=1) - expected = [(None, 'anotherpackage:path/', '__view/', None)] + expected = [(None, 'anotherpackage:path/', '__view/')] self.assertEqual(inst.registrations, expected) self.assertEqual(config.route_args, ('__view/', 'view/*subpath')) self.assertEqual(config.view_kw['permission'], NO_PERMISSION_REQUIRED) @@ -4060,7 +4092,7 @@ class TestStaticURLInfo(unittest.TestCase): config.route_prefix = '/abc' inst = self._makeOne() inst.add(config, 'view', 'anotherpackage:path',) - expected = [(None, 'anotherpackage:path/', '__/abc/view/', None)] + expected = [(None, 'anotherpackage:path/', '__/abc/view/')] self.assertEqual(inst.registrations, expected) self.assertEqual(config.route_args, ('__/abc/view/', 'view/*subpath')) @@ -4097,20 +4129,47 @@ class TestStaticURLInfo(unittest.TestCase): config = DummyConfig() config.registry.settings['pyramid.prevent_cachebust'] = True inst = self._makeOne() - inst.add(config, 'view', 'mypackage:path', cachebust=True) - cachebust = config.registry._static_url_registrations[0][3] - self.assertEqual(cachebust, None) + cachebust = DummyCacheBuster('foo') + inst.add_cache_buster(config, 'mypackage:path', cachebust) + self.assertEqual(inst.cache_busters, []) - def test_add_cachebust_custom(self): + def test_add_cachebuster(self): config = DummyConfig() inst = self._makeOne() - inst.add(config, 'view', 'mypackage:path', - cachebust=DummyCacheBuster('foo')) - cachebust = config.registry._static_url_registrations[0][3] - subpath, kw = cachebust('some/path', {}) + inst.add_cache_buster(config, 'mypackage:path', DummyCacheBuster('foo')) + cachebust = inst.cache_busters[-1][1] + subpath, kw = cachebust('mypackage:some/path', 'some/path', {}) self.assertEqual(subpath, 'some/path') self.assertEqual(kw['x'], 'foo') + def test_add_cachebuster_abspath(self): + here = os.path.dirname(__file__) + config = DummyConfig() + inst = self._makeOne() + cb = DummyCacheBuster('foo') + inst.add_cache_buster(config, here, cb) + self.assertEqual(inst.cache_busters, [(here + '/', cb)]) + + def test_add_cachebuster_overwrite(self): + config = DummyConfig() + inst = self._makeOne() + cb1 = DummyCacheBuster('foo') + cb2 = DummyCacheBuster('bar') + inst.add_cache_buster(config, 'mypackage:path/', cb1) + inst.add_cache_buster(config, 'mypackage:path', cb2) + self.assertEqual(inst.cache_busters, + [('mypackage:path/', cb2)]) + + def test_add_cachebuster_for_more_specific_path(self): + config = DummyConfig() + inst = self._makeOne() + cb1 = DummyCacheBuster('foo') + cb2 = DummyCacheBuster('bar') + inst.add_cache_buster(config, 'mypackage:path', cb1) + inst.add_cache_buster(config, 'mypackage:path/sub', cb2) + self.assertEqual(inst.cache_busters, + [('mypackage:path/', cb1), ('mypackage:path/sub/', cb2)]) + class Test_view_description(unittest.TestCase): def _callFUT(self, view): from pyramid.config.views import view_description @@ -4130,9 +4189,14 @@ class Test_view_description(unittest.TestCase): class DummyRegistry: + utility = None + def __init__(self): self.settings = {} + def queryUtility(self, type_or_iface, name=None, default=None): + return self.utility or default + from zope.interface import implementer from pyramid.interfaces import ( IResponse, @@ -4193,6 +4257,9 @@ class DummySecurityPolicy: return self.permitted class DummyConfig: + def __init__(self): + self.registry = DummyRegistry() + route_prefix = '' def add_route(self, *args, **kw): self.route_args = args diff --git a/pyramid/tests/test_static.py b/pyramid/tests/test_static.py index 4a07c2cb1..73f242add 100644 --- a/pyramid/tests/test_static.py +++ b/pyramid/tests/test_static.py @@ -385,29 +385,29 @@ class TestQueryStringConstantCacheBuster(unittest.TestCase): fut = self._makeOne().tokenize self.assertEqual(fut('whatever'), 'foo') - def test_pregenerate(self): - fut = self._makeOne().pregenerate + def test_it(self): + fut = self._makeOne() self.assertEqual( - fut('foo', ('bar',), {}), - (('bar',), {'_query': {'x': 'foo'}})) + fut('foo', 'bar', {}), + ('bar', {'_query': {'x': 'foo'}})) - def test_pregenerate_change_param(self): - fut = self._makeOne('y').pregenerate + def test_change_param(self): + fut = self._makeOne('y') self.assertEqual( - fut('foo', ('bar',), {}), - (('bar',), {'_query': {'y': 'foo'}})) + fut('foo', 'bar', {}), + ('bar', {'_query': {'y': 'foo'}})) - def test_pregenerate_query_is_already_tuples(self): - fut = self._makeOne().pregenerate + def test_query_is_already_tuples(self): + fut = self._makeOne() self.assertEqual( - fut('foo', ('bar',), {'_query': [('a', 'b')]}), - (('bar',), {'_query': (('a', 'b'), ('x', 'foo'))})) + fut('foo', 'bar', {'_query': [('a', 'b')]}), + ('bar', {'_query': (('a', 'b'), ('x', 'foo'))})) - def test_pregenerate_query_is_tuple_of_tuples(self): - fut = self._makeOne().pregenerate + def test_query_is_tuple_of_tuples(self): + fut = self._makeOne() self.assertEqual( - fut('foo', ('bar',), {'_query': (('a', 'b'),)}), - (('bar',), {'_query': (('a', 'b'), ('x', 'foo'))})) + fut('foo', 'bar', {'_query': (('a', 'b'),)}), + ('bar', {'_query': (('a', 'b'), ('x', 'foo'))})) class TestManifestCacheBuster(unittest.TestCase): @@ -417,55 +417,55 @@ class TestManifestCacheBuster(unittest.TestCase): def test_it(self): manifest_path = os.path.join(here, 'fixtures', 'manifest.json') - fut = self._makeOne(manifest_path).pregenerate - self.assertEqual(fut('foo', ('bar',), {}), (['bar'], {})) + fut = self._makeOne(manifest_path) + self.assertEqual(fut('foo', 'bar', {}), ('bar', {})) self.assertEqual( - fut('foo', ('css', 'main.css'), {}), - (['css', 'main-test.css'], {})) + fut('foo', 'css/main.css', {}), + ('css/main-test.css', {})) def test_it_with_relspec(self): - fut = self._makeOne('fixtures/manifest.json').pregenerate - self.assertEqual(fut('foo', ('bar',), {}), (['bar'], {})) + fut = self._makeOne('fixtures/manifest.json') + self.assertEqual(fut('foo', 'bar', {}), ('bar', {})) self.assertEqual( - fut('foo', ('css', 'main.css'), {}), - (['css', 'main-test.css'], {})) + fut('foo', 'css/main.css', {}), + ('css/main-test.css', {})) def test_it_with_absspec(self): - fut = self._makeOne('pyramid.tests:fixtures/manifest.json').pregenerate - self.assertEqual(fut('foo', ('bar',), {}), (['bar'], {})) + fut = self._makeOne('pyramid.tests:fixtures/manifest.json') + self.assertEqual(fut('foo', 'bar', {}), ('bar', {})) self.assertEqual( - fut('foo', ('css', 'main.css'), {}), - (['css', 'main-test.css'], {})) + fut('foo', 'css/main.css', {}), + ('css/main-test.css', {})) def test_reload(self): manifest_path = os.path.join(here, 'fixtures', 'manifest.json') new_manifest_path = os.path.join(here, 'fixtures', 'manifest2.json') inst = self._makeOne('foo', reload=True) inst.getmtime = lambda *args, **kwargs: 0 - fut = inst.pregenerate + fut = inst # test without a valid manifest self.assertEqual( - fut('foo', ('css', 'main.css'), {}), - (['css', 'main.css'], {})) + fut('foo', 'css/main.css', {}), + ('css/main.css', {})) # swap to a real manifest, setting mtime to 0 inst.manifest_path = manifest_path self.assertEqual( - fut('foo', ('css', 'main.css'), {}), - (['css', 'main-test.css'], {})) + fut('foo', 'css/main.css', {}), + ('css/main-test.css', {})) # ensure switching the path doesn't change the result inst.manifest_path = new_manifest_path self.assertEqual( - fut('foo', ('css', 'main.css'), {}), - (['css', 'main-test.css'], {})) + fut('foo', 'css/main.css', {}), + ('css/main-test.css', {})) # update mtime, should cause a reload inst.getmtime = lambda *args, **kwargs: 1 self.assertEqual( - fut('foo', ('css', 'main.css'), {}), - (['css', 'main-678b7c80.css'], {})) + fut('foo', 'css/main.css', {}), + ('css/main-678b7c80.css', {})) def test_invalid_manifest(self): self.assertRaises(IOError, lambda: self._makeOne('foo')) -- cgit v1.2.3 From 3175c990bc02805b729594996f6360b81f5f0ebc Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Wed, 9 Dec 2015 23:01:35 -0600 Subject: fix broken ref in docs --- docs/narr/assets.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/narr/assets.rst b/docs/narr/assets.rst index 8b41f9b8a..0e3f6af11 100644 --- a/docs/narr/assets.rst +++ b/docs/narr/assets.rst @@ -587,7 +587,7 @@ use some of the following options to get started: template code. While this approach may work in certain scenarios, it is not recommended because your static assets will not really be static and are now dependent on :app:`Pyramid` to be served correctly. See - :ref:`advanced static` for more information on this approach. + :ref:`advanced_static` for more information on this approach. If your CSS and JavaScript assets use URLs to reference other assets it is recommended that you implement an external asset pipeline that can rewrite the -- cgit v1.2.3 From 4350699a208dc9304ae8c8dd165251f227ff5189 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Wed, 9 Dec 2015 23:02:28 -0600 Subject: remove unused import --- pyramid/config/views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index e5bf1203e..1fcdcb136 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -69,7 +69,6 @@ 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 from pyramid.url import parse_url_overrides -- cgit v1.2.3 From fb9b9f106699b38bc49c14c751d1b948a3c08533 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Fri, 11 Dec 2015 03:33:37 -0800 Subject: add developing and contributing section - wrap to 79 columns --- README.rst | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index e99133441..fed4534a0 100644 --- a/README.rst +++ b/README.rst @@ -16,9 +16,9 @@ Pyramid :target: https://webchat.freenode.net/?channels=pyramid :alt: IRC Freenode -Pyramid is a small, fast, down-to-earth, open source Python web framework. -It makes real-world web application development and -deployment more fun, more predictable, and more productive. +Pyramid is a small, fast, down-to-earth, open source Python web framework. It +makes real-world web application development and deployment more fun, more +predictable, and more productive. Pyramid is produced by the `Pylons Project `_. @@ -28,6 +28,13 @@ Support and Documentation See the `Pylons Project website `_ to view documentation, report bugs, and obtain support. +Developing and Contributing +--------------------------- + +See `HACKING.txt` and `contributing.md` for guidelines for running tests, +adding features, coding style, and updating documentation when developing in or +contributing to Pyramid. + License ------- -- cgit v1.2.3 From 4bd36e8669fece803ff8bb209e655ab16fb8c63b Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Fri, 11 Dec 2015 03:44:01 -0800 Subject: use double backticks for inline code formatting of filename --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index fed4534a0..2237d9950 100644 --- a/README.rst +++ b/README.rst @@ -31,7 +31,7 @@ documentation, report bugs, and obtain support. Developing and Contributing --------------------------- -See `HACKING.txt` and `contributing.md` for guidelines for running tests, +See ``HACKING.txt`` and ``contributing.md`` for guidelines for running tests, adding features, coding style, and updating documentation when developing in or contributing to Pyramid. -- cgit v1.2.3 From 3241405cefdb2cc545d689140728a5d22be7d18a Mon Sep 17 00:00:00 2001 From: Sri Sanketh Uppalapati Date: Sat, 12 Dec 2015 16:44:54 +0530 Subject: Update url.py --- pyramid/url.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/pyramid/url.py b/pyramid/url.py index b004c40ec..210163fa5 100644 --- a/pyramid/url.py +++ b/pyramid/url.py @@ -96,19 +96,17 @@ class URLMethodsMixin(object): if scheme == 'http': if port is None: port = '80' - url = scheme + '://' - if port is not None: - port = str(port) if host is None: host = e.get('HTTP_HOST') - if host is None: - host = e['SERVER_NAME'] + if host is None: + host = e['SERVER_NAME'] if port is None: if ':' in host: host, port = host.split(':', 1) else: port = e['SERVER_PORT'] else: + port=str(port) if ':' in host: host, _ = host.split(':', 1) if scheme == 'https': @@ -117,7 +115,7 @@ class URLMethodsMixin(object): elif scheme == 'http': if port == '80': port = None - url += host + url = scheme + '://' + host if port: url += ':%s' % port -- cgit v1.2.3 From 55983f327b39bbf265b1ace1baac5a1f58c0f2fe Mon Sep 17 00:00:00 2001 From: Sri Sanketh Uppalapati Date: Sat, 12 Dec 2015 16:55:17 +0530 Subject: Update url.py --- pyramid/url.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/url.py b/pyramid/url.py index 210163fa5..fd62f0057 100644 --- a/pyramid/url.py +++ b/pyramid/url.py @@ -106,7 +106,7 @@ class URLMethodsMixin(object): else: port = e['SERVER_PORT'] else: - port=str(port) + port = str(port) if ':' in host: host, _ = host.split(':', 1) if scheme == 'https': -- cgit v1.2.3 From 7682837de754c2515ff617a87afecae2498cfa6e Mon Sep 17 00:00:00 2001 From: Sri Sanketh Uppalapati Date: Sat, 12 Dec 2015 17:02:31 +0530 Subject: Update url.py --- pyramid/url.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyramid/url.py b/pyramid/url.py index fd62f0057..812514638 100644 --- a/pyramid/url.py +++ b/pyramid/url.py @@ -98,8 +98,6 @@ class URLMethodsMixin(object): port = '80' if host is None: host = e.get('HTTP_HOST') - if host is None: - host = e['SERVER_NAME'] if port is None: if ':' in host: host, port = host.split(':', 1) -- cgit v1.2.3 From b4fcff0471e16b6dab3a685df970bca712c5cb3b Mon Sep 17 00:00:00 2001 From: Sri Sanketh Uppalapati Date: Sat, 12 Dec 2015 17:06:29 +0530 Subject: Update url.py --- pyramid/url.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyramid/url.py b/pyramid/url.py index 812514638..fd62f0057 100644 --- a/pyramid/url.py +++ b/pyramid/url.py @@ -98,6 +98,8 @@ class URLMethodsMixin(object): port = '80' if host is None: host = e.get('HTTP_HOST') + if host is None: + host = e['SERVER_NAME'] if port is None: if ':' in host: host, port = host.split(':', 1) -- cgit v1.2.3 From 3b75ea56485ee2b5f10f757fce31c00ca54dbc86 Mon Sep 17 00:00:00 2001 From: Sri Sanketh Uppalapati Date: Sat, 12 Dec 2015 17:23:43 +0530 Subject: Update CONTRIBUTORS.txt --- CONTRIBUTORS.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 4edf1b4e9..1f3597e84 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -256,3 +256,5 @@ Contributors - Amos Latteier, 2015/10/22 - Rami Chousein, 2015/10/28 + +- Sri Sanketh Uppalapati, 2015/12/12 -- cgit v1.2.3 From 4d19b84cb8134a0e7f030064e5d944defaa6970a Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 14 Dec 2015 00:17:39 -0600 Subject: new default behavior matches virtual specs, old behavior hidden behind explicit=True --- pyramid/config/views.py | 67 ++++++++++++++++++++++---------- pyramid/interfaces.py | 26 +++++++++---- pyramid/static.py | 10 ++--- pyramid/tests/test_config/test_views.py | 69 ++++++++++++++++++++++++--------- pyramid/tests/test_static.py | 2 +- 5 files changed, 123 insertions(+), 51 deletions(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 1fcdcb136..759276351 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1,4 +1,3 @@ -import bisect import inspect import posixpath import operator @@ -1941,7 +1940,7 @@ class ViewsConfiguratorMixin(object): info = self._get_static_info() info.add(self, name, spec, **kw) - def add_cache_buster(self, path, cachebust): + def add_cache_buster(self, path, cachebust, explicit=False): """ Add a cache buster to a set of files on disk. @@ -1956,10 +1955,16 @@ class ViewsConfiguratorMixin(object): be an object which implements :class:`~pyramid.interfaces.ICacheBuster`. + If ``explicit`` is set to ``True`` then the ``path`` for the cache + buster will be matched based on the ``rawspec`` instead of the + ``pathspec`` as defined in the + :class:`~pyramid.interfaces.ICacheBuster` interface. + Default: ``False``. + """ spec = self._make_spec(path) info = self._get_static_info() - info.add_cache_buster(self, spec, cachebust) + info.add_cache_buster(self, spec, cachebust, explicit=explicit) def _get_static_info(self): info = self.registry.queryUtility(IStaticURLInfo) @@ -1992,7 +1997,7 @@ class StaticURLInfo(object): subpath = subpath.replace('\\', '/') # windows if self.cache_busters: subpath, kw = self._bust_asset_path( - request.registry, spec, subpath, kw) + request, spec, subpath, kw) if url is None: kw['subpath'] = subpath return request.route_url(route_name, **kw) @@ -2096,7 +2101,7 @@ class StaticURLInfo(object): config.action(None, callable=register, introspectables=(intr,)) - def add_cache_buster(self, config, spec, cachebust): + def add_cache_buster(self, config, spec, cachebust, explicit=False): if config.registry.settings.get('pyramid.prevent_cachebust'): return @@ -2112,29 +2117,46 @@ class StaticURLInfo(object): def register(): cache_busters = self.cache_busters - specs = [t[0] for t in cache_busters] - if spec in specs: - idx = specs.index(spec) - cache_busters.pop(idx) + # find duplicate cache buster (old_idx) + # and insertion location (new_idx) + new_idx, old_idx = len(cache_busters), None + for idx, (spec_, cb_, explicit_) in enumerate(cache_busters): + # if we find an identical (spec, explicit) then use it + if spec == spec_ and explicit == explicit_: + old_idx = new_idx = idx + break + + # past all explicit==False specs then add to the end + elif not explicit and explicit_: + new_idx = idx + break + + # explicit matches and spec is shorter + elif explicit == explicit_ and len(spec) < len(spec_): + new_idx = idx + break - lengths = [len(t[0]) for t in cache_busters] - new_idx = bisect.bisect_left(lengths, len(spec)) - cache_busters.insert(new_idx, (spec, cachebust)) + if old_idx is not None: + cache_busters.pop(old_idx) + cache_busters.insert(new_idx, (spec, cachebust, explicit)) intr = config.introspectable('cache busters', spec, 'cache buster for %r' % spec, 'cache buster') intr['cachebust'] = cachebust - intr['spec'] = spec + intr['path'] = spec + intr['explicit'] = explicit config.action(None, callable=register, introspectables=(intr,)) - def _bust_asset_path(self, registry, spec, subpath, kw): + def _bust_asset_path(self, request, spec, subpath, kw): + registry = request.registry pkg_name, pkg_subpath = resolve_asset_spec(spec) rawspec = None if pkg_name is not None: + pathspec = '{0}:{1}{2}'.format(pkg_name, pkg_subpath, subpath) overrides = registry.queryUtility(IPackageOverrides, name=pkg_name) if overrides is not None: resource_name = posixpath.join(pkg_subpath, subpath) @@ -2145,14 +2167,19 @@ class StaticURLInfo(object): rawspec = '{0}:{1}'.format(source.pkg_name, rawspec) break - if rawspec is None: - rawspec = '{0}:{1}{2}'.format(pkg_name, pkg_subpath, subpath) + else: + pathspec = pkg_subpath + subpath if rawspec is None: - rawspec = pkg_subpath + subpath + rawspec = pathspec - for base_spec, cachebust in reversed(self.cache_busters): - if rawspec.startswith(base_spec): - subpath, kw = cachebust(rawspec, subpath, kw) + kw['pathspec'] = pathspec + kw['rawspec'] = rawspec + for spec_, cachebust, explicit in reversed(self.cache_busters): + if ( + (explicit and rawspec.startswith(spec_)) or + (not explicit and pathspec.startswith(spec_)) + ): + subpath, kw = cachebust(request, subpath, kw) break return subpath, kw diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index 153fdad03..bbdc5121d 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -1194,11 +1194,11 @@ class ICacheBuster(Interface): .. versionadded:: 1.6 """ - def __call__(pathspec, subpath, kw): + def __call__(request, subpath, kw): """ Modifies a subpath and/or keyword arguments from which a static asset - URL will be computed during URL generation. The ``pathspec`` argument - is the path specification for the resource to be cache busted. + URL will be computed during URL generation. + The ``subpath`` argument is a path of ``/``-delimited segments that represent the portion of the asset URL which is used to find the asset. The ``kw`` argument is a dict of keywords that are to be passed @@ -1209,10 +1209,22 @@ class ICacheBuster(Interface): should be modified to include the cache bust token in the generated URL. - The ``pathspec`` refers to original location of the file, ignoring any - calls to :meth:`pyramid.config.Configurator.override_asset`. For - example, with a call ``request.static_url('myapp:static/foo.png'), the - ``pathspec`` may be ``themepkg:bar.png``, assuming a call to + The ``kw`` dictionary contains extra arguments passed to + :meth:`~pyramid.request.Request.static_url` as well as some extra + items that may be usful including: + + - ``pathspec`` is the path specification for the resource + to be cache busted. + + - ``rawspec`` is the original location of the file, ignoring + any calls to :meth:`pyramid.config.Configurator.override_asset`. + + The ``pathspec`` and ``rawspec`` values are only different in cases + where an asset has been mounted into a virtual location using + :meth:`pyramid.config.Configurator.override_asset`. For example, with + a call to ``request.static_url('myapp:static/foo.png'), the + ``pathspec`` is ``myapp:static/foo.png`` whereas the ``rawspec`` may + be ``themepkg:bar.png``, assuming a call to ``config.override_asset('myapp:static/foo.png', 'themepkg:bar.png')``. """ diff --git a/pyramid/static.py b/pyramid/static.py index 9559cd881..4054d5be0 100644 --- a/pyramid/static.py +++ b/pyramid/static.py @@ -172,15 +172,15 @@ class QueryStringCacheBuster(object): to the query string and defaults to ``'x'``. To use this class, subclass it and provide a ``tokenize`` method which - accepts a ``pathspec`` and returns a token. + accepts ``request, pathspec, kw`` and returns a token. .. versionadded:: 1.6 """ def __init__(self, param='x'): self.param = param - def __call__(self, pathspec, subpath, kw): - token = self.tokenize(pathspec) + def __call__(self, request, subpath, kw): + token = self.tokenize(request, subpath, kw) query = kw.setdefault('_query', {}) if isinstance(query, dict): query[self.param] = token @@ -205,7 +205,7 @@ class QueryStringConstantCacheBuster(QueryStringCacheBuster): super(QueryStringConstantCacheBuster, self).__init__(param=param) self._token = token - def tokenize(self, pathspec): + def tokenize(self, request, subpath, kw): return self._token class ManifestCacheBuster(object): @@ -290,6 +290,6 @@ class ManifestCacheBuster(object): self._mtime = mtime return self._manifest - def __call__(self, pathspec, subpath, kw): + def __call__(self, request, subpath, kw): subpath = self.manifest.get(subpath, subpath) return (subpath, kw) diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index eda8d8b05..e89d43c9a 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -3978,13 +3978,15 @@ class TestStaticURLInfo(unittest.TestCase): return 'foo' + '/' + subpath, kw inst = self._makeOne() inst.registrations = [(None, 'package:path/', '__viewname')] - inst.cache_busters = [('package:path/', cachebust)] + inst.cache_busters = [('package:path/', cachebust, False)] request = self._makeRequest() called = [False] def route_url(n, **kw): called[0] = True self.assertEqual(n, '__viewname') - self.assertEqual(kw, {'subpath': 'foo/abc', 'foo': 'bar'}) + self.assertEqual(kw, {'subpath': 'foo/abc', 'foo': 'bar', + 'pathspec': 'package:path/abc', + 'rawspec': 'package:path/abc'}) request.route_url = route_url inst.generate('package:path/abc', request) self.assertTrue(called[0]) @@ -3996,13 +3998,15 @@ class TestStaticURLInfo(unittest.TestCase): return 'foo' + '/' + subpath, kw inst = self._makeOne() inst.registrations = [(None, here, '__viewname')] - inst.cache_busters = [(here, cachebust)] + inst.cache_busters = [(here, cachebust, False)] request = self._makeRequest() called = [False] def route_url(n, **kw): called[0] = True self.assertEqual(n, '__viewname') - self.assertEqual(kw, {'subpath': 'foo/abc', 'foo': 'bar'}) + self.assertEqual(kw, {'subpath': 'foo/abc', 'foo': 'bar', + 'pathspec': here + 'abc', + 'rawspec': here + 'abc'}) request.route_url = route_url inst.generate(here + 'abc', request) self.assertTrue(called[0]) @@ -4011,13 +4015,15 @@ class TestStaticURLInfo(unittest.TestCase): def fake_cb(*a, **kw): raise AssertionError inst = self._makeOne() inst.registrations = [(None, 'package:path/', '__viewname')] - inst.cache_busters = [('package:path2/', fake_cb)] + inst.cache_busters = [('package:path2/', fake_cb, False)] request = self._makeRequest() called = [False] def route_url(n, **kw): called[0] = True self.assertEqual(n, '__viewname') - self.assertEqual(kw, {'subpath': 'abc'}) + self.assertEqual(kw, {'subpath': 'abc', + 'pathspec': 'package:path/abc', + 'rawspec': 'package:path/abc'}) request.route_url = route_url inst.generate('package:path/abc', request) self.assertTrue(called[0]) @@ -4025,17 +4031,22 @@ class TestStaticURLInfo(unittest.TestCase): def test_generate_url_cachebust_with_overrides(self): config = testing.setUp() try: + request = testing.DummyRequest() config.add_static_view('static', 'path') config.override_asset( 'pyramid.tests.test_config:path/', 'pyramid.tests.test_config:other_path/') - def cb(pathspec, subpath, kw): - kw['_query'] = {'x': 'foo'} - return subpath, kw - config.add_cache_buster('other_path', cb) - request = testing.DummyRequest() + def cb(val): + def cb_(request, subpath, kw): + kw['_query'] = {'x': val} + return subpath, kw + return cb_ + config.add_cache_buster('path', cb('foo')) result = request.static_url('path/foo.png') self.assertEqual(result, 'http://example.com/static/foo.png?x=foo') + config.add_cache_buster('other_path', cb('bar'), explicit=True) + result = request.static_url('path/foo.png') + self.assertEqual(result, 'http://example.com/static/foo.png?x=bar') finally: testing.tearDown() @@ -4138,7 +4149,7 @@ class TestStaticURLInfo(unittest.TestCase): inst = self._makeOne() inst.add_cache_buster(config, 'mypackage:path', DummyCacheBuster('foo')) cachebust = inst.cache_busters[-1][1] - subpath, kw = cachebust('mypackage:some/path', 'some/path', {}) + subpath, kw = cachebust(None, 'some/path', {}) self.assertEqual(subpath, 'some/path') self.assertEqual(kw['x'], 'foo') @@ -4148,7 +4159,7 @@ class TestStaticURLInfo(unittest.TestCase): inst = self._makeOne() cb = DummyCacheBuster('foo') inst.add_cache_buster(config, here, cb) - self.assertEqual(inst.cache_busters, [(here + '/', cb)]) + self.assertEqual(inst.cache_busters, [(here + '/', cb, False)]) def test_add_cachebuster_overwrite(self): config = DummyConfig() @@ -4158,17 +4169,39 @@ class TestStaticURLInfo(unittest.TestCase): inst.add_cache_buster(config, 'mypackage:path/', cb1) inst.add_cache_buster(config, 'mypackage:path', cb2) self.assertEqual(inst.cache_busters, - [('mypackage:path/', cb2)]) + [('mypackage:path/', cb2, False)]) + + def test_add_cachebuster_overwrite_explicit(self): + config = DummyConfig() + inst = self._makeOne() + cb1 = DummyCacheBuster('foo') + cb2 = DummyCacheBuster('bar') + inst.add_cache_buster(config, 'mypackage:path/', cb1) + inst.add_cache_buster(config, 'mypackage:path', cb2, True) + self.assertEqual(inst.cache_busters, + [('mypackage:path/', cb1, False), + ('mypackage:path/', cb2, True)]) def test_add_cachebuster_for_more_specific_path(self): config = DummyConfig() inst = self._makeOne() cb1 = DummyCacheBuster('foo') cb2 = DummyCacheBuster('bar') + cb3 = DummyCacheBuster('baz') + cb4 = DummyCacheBuster('xyz') + cb5 = DummyCacheBuster('w') inst.add_cache_buster(config, 'mypackage:path', cb1) - inst.add_cache_buster(config, 'mypackage:path/sub', cb2) - self.assertEqual(inst.cache_busters, - [('mypackage:path/', cb1), ('mypackage:path/sub/', cb2)]) + inst.add_cache_buster(config, 'mypackage:path/sub', cb2, True) + inst.add_cache_buster(config, 'mypackage:path/sub/other', cb3) + inst.add_cache_buster(config, 'mypackage:path/sub/other', cb4, True) + inst.add_cache_buster(config, 'mypackage:path/sub/less', cb5, True) + self.assertEqual( + inst.cache_busters, + [('mypackage:path/', cb1, False), + ('mypackage:path/sub/other/', cb3, False), + ('mypackage:path/sub/', cb2, True), + ('mypackage:path/sub/less/', cb5, True), + ('mypackage:path/sub/other/', cb4, True)]) class Test_view_description(unittest.TestCase): def _callFUT(self, view): @@ -4293,7 +4326,7 @@ class DummyCacheBuster(object): def __init__(self, token): self.token = token - def __call__(self, pathspec, subpath, kw): + def __call__(self, request, subpath, kw): kw['x'] = self.token return subpath, kw diff --git a/pyramid/tests/test_static.py b/pyramid/tests/test_static.py index 73f242add..2ca86bc44 100644 --- a/pyramid/tests/test_static.py +++ b/pyramid/tests/test_static.py @@ -383,7 +383,7 @@ class TestQueryStringConstantCacheBuster(unittest.TestCase): def test_token(self): fut = self._makeOne().tokenize - self.assertEqual(fut('whatever'), 'foo') + self.assertEqual(fut(None, 'whatever', None), 'foo') def test_it(self): fut = self._makeOne() -- cgit v1.2.3 From 312aa1b996f786dc18d8189a7a6d9813ad609645 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Mon, 14 Dec 2015 02:26:32 -0800 Subject: - Remove broken integration test example from testing and source file, per #2172 - Update functional test with explicit instructions and to sync with actual starter scaffold --- docs/narr/MyProject/myproject/tests.py | 26 ------------ docs/narr/testing.rst | 73 +++++++++++++++++++--------------- 2 files changed, 42 insertions(+), 57 deletions(-) diff --git a/docs/narr/MyProject/myproject/tests.py b/docs/narr/MyProject/myproject/tests.py index 63d00910c..37df08a2a 100644 --- a/docs/narr/MyProject/myproject/tests.py +++ b/docs/narr/MyProject/myproject/tests.py @@ -17,32 +17,6 @@ class ViewTests(unittest.TestCase): self.assertEqual(info['project'], 'MyProject') -class ViewIntegrationTests(unittest.TestCase): - def setUp(self): - """ This sets up the application registry with the - registrations your application declares in its ``includeme`` - function. - """ - self.config = testing.setUp() - self.config.include('myproject') - - def tearDown(self): - """ Clear out the application registry """ - testing.tearDown() - - def test_my_view(self): - from myproject.views import my_view - request = testing.DummyRequest() - result = my_view(request) - self.assertEqual(result.status, '200 OK') - body = result.app_iter[0] - self.assertTrue('Welcome to' in body) - self.assertEqual(len(result.headerlist), 2) - self.assertEqual(result.headerlist[0], - ('Content-Type', 'text/html; charset=UTF-8')) - self.assertEqual(result.headerlist[1], ('Content-Length', - str(len(body)))) - class FunctionalTests(unittest.TestCase): def setUp(self): from myproject import main diff --git a/docs/narr/testing.rst b/docs/narr/testing.rst index c05ee41ad..a3f62058b 100644 --- a/docs/narr/testing.rst +++ b/docs/narr/testing.rst @@ -348,26 +348,6 @@ code's integration with the rest of :app:`Pyramid`. See also :ref:`including_configuration` -Let's demonstrate this by showing an integration test for a view. - -Given the following view definition, which assumes that your application's -:term:`package` name is ``myproject``, and within that :term:`package` there -exists a module ``views``, which in turn contains a :term:`view` function named -``my_view``: - - .. literalinclude:: MyProject/myproject/views.py - :linenos: - :lines: 1-6 - :language: python - -You'd then create a ``tests`` module within your ``myproject`` package, -containing the following test code: - - .. literalinclude:: MyProject/myproject/tests.py - :linenos: - :pyobject: ViewIntegrationTests - :language: python - Writing unit tests that use the :class:`~pyramid.config.Configurator` API to set up the right "mock" registrations is often preferred to creating integration tests. Unit tests will run faster (because they do less for each @@ -388,22 +368,53 @@ package, which provides APIs for invoking HTTP(S) requests to your application. Regardless of which testing :term:`package` you use, ensure to add a ``tests_require`` dependency on that package to your application's -``setup.py`` file: +``setup.py`` file. Using the project ``MyProject`` generated by the starter +scaffold as described in :doc:`project`, we would insert the following code immediately following the +``requires`` block in the file ``MyProject/setup.py``. - .. literalinclude:: MyProject/setup.py - :linenos: - :emphasize-lines: 26-28,48 - :language: python +.. code-block:: ini + :linenos: + :lineno-start: 11 + :emphasize-lines: 8- + + requires = [ + 'pyramid', + 'pyramid_chameleon', + 'pyramid_debugtoolbar', + 'waitress', + ] + + test_requires = [ + 'webtest', + ] + +Remember to change the dependency. + +.. code-block:: ini + :linenos: + :lineno-start: 39 + :emphasize-lines: 2 + + install_requires=requires, + tests_require=test_requires, + test_suite="myproject", + +As always, whenever you change your dependencies, make sure to run the +following command. + +.. code-block:: bash + + $VENV/bin/python setup.py develop -Let us assume your :term:`package` is named ``myproject`` which contains a -``views`` module, which in turn contains a :term:`view` function ``my_view`` -that returns a HTML body when the root URL is invoked: +In your ``MyPackage`` project, your :term:`package` is named ``myproject`` +which contains a ``views`` module, which in turn contains a :term:`view` +function ``my_view`` that returns an HTML body when the root URL is invoked: .. literalinclude:: MyProject/myproject/views.py :linenos: :language: python -Then the following example functional test demonstrates invoking the above +The following example functional test demonstrates invoking the above :term:`view`: .. literalinclude:: MyProject/myproject/tests.py @@ -414,9 +425,9 @@ Then the following example functional test demonstrates invoking the above When this test is run, each test method creates a "real" :term:`WSGI` application using the ``main`` function in your ``myproject.__init__`` module, using :term:`WebTest` to wrap that WSGI application. It assigns the result to -``self.testapp``. In the test named ``test_root``. The ``TestApp``'s ``GET`` +``self.testapp``. In the test named ``test_root``, the ``TestApp``'s ``GET`` method is used to invoke the root URL. Finally, an assertion is made that the -returned HTML contains the text ``MyProject``. +returned HTML contains the text ``Pyramid``. See the :term:`WebTest` documentation for further information about the methods available to a :class:`webtest.app.TestApp` instance. -- cgit v1.2.3 From 93d2aa696288098b046915bb718604ee9a4d7de7 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Wed, 16 Dec 2015 16:26:20 -0600 Subject: test travis irc notifications --- .travis.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.travis.yml b/.travis.yml index 2163eb8fd..6cc2e9ad4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -36,3 +36,8 @@ script: notifications: email: - pyramid-checkins@lists.repoze.org + irc: + channels: + - "chat.freenode.net#pyramid" + template: + - "%{repository}/%{branch} (%{commit} - %{author}): %{message}" -- cgit v1.2.3 From e40ccc3b4cc54fdef4fa49a131a95cd9f5bed80e Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Wed, 16 Dec 2015 16:30:39 -0600 Subject: use default irc template --- .travis.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6cc2e9ad4..5c53b43f6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -39,5 +39,3 @@ notifications: irc: channels: - "chat.freenode.net#pyramid" - template: - - "%{repository}/%{branch} (%{commit} - %{author}): %{message}" -- cgit v1.2.3 From ca573ea73db05d7ea9bdbb51eb7db26ab602ccf2 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Wed, 16 Dec 2015 19:50:13 -0600 Subject: defer prevent check until register --- pyramid/config/views.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 759276351..acaf9462b 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -2102,9 +2102,6 @@ class StaticURLInfo(object): config.action(None, callable=register, introspectables=(intr,)) def add_cache_buster(self, config, spec, cachebust, explicit=False): - if config.registry.settings.get('pyramid.prevent_cachebust'): - return - # ensure the spec always has a trailing slash as we only support # adding cache busters to folders, not files if os.path.isabs(spec): # FBO windows @@ -2115,6 +2112,9 @@ class StaticURLInfo(object): spec = spec + sep def register(): + if config.registry.settings.get('pyramid.prevent_cachebust'): + return + cache_busters = self.cache_busters # find duplicate cache buster (old_idx) @@ -2138,6 +2138,7 @@ class StaticURLInfo(object): if old_idx is not None: cache_busters.pop(old_idx) + cache_busters.insert(new_idx, (spec, cachebust, explicit)) intr = config.introspectable('cache busters', -- cgit v1.2.3 From 313ff3c28fbd3b784e4c87daf8ae8a4cf713262b Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Wed, 16 Dec 2015 21:30:56 -0600 Subject: update docs to support explicit asset overrides --- docs/narr/assets.rst | 100 +++++++++++++++++++++++++++++++++------------------ 1 file changed, 65 insertions(+), 35 deletions(-) diff --git a/docs/narr/assets.rst b/docs/narr/assets.rst index 0e3f6af11..b28e6b5b3 100644 --- a/docs/narr/assets.rst +++ b/docs/narr/assets.rst @@ -380,11 +380,6 @@ assets using :meth:`~pyramid.config.Configurator.add_cache_buster`: 'mypackage:folder/static/', QueryStringConstantCacheBuster(str(int(time.time())))) -.. note:: - The trailing slash on the ``add_cache_buster`` call is important to - indicate that it is overriding every asset in the folder and not just a - file named ``static``. - Adding the cachebuster instructs :app:`Pyramid` to add the current time for a static asset to the query string in the asset's URL: @@ -451,12 +446,13 @@ the hash of the current commit: from an egg repository like PYPI, you can use this cachebuster to get the current commit's SHA1 to use as the cache bust token. """ - def __init__(self, param='x'): + def __init__(self, param='x', repo_path=None): super(GitCacheBuster, self).__init__(param=param) - here = os.path.dirname(os.path.abspath(__file__)) + if repo_path is None: + repo_path = os.path.dirname(os.path.abspath(__file__)) self.sha1 = subprocess.check_output( ['git', 'rev-parse', 'HEAD'], - cwd=here).strip() + cwd=repo_path).strip() def tokenize(self, pathspec): return self.sha1 @@ -469,10 +465,14 @@ well: import posixpath - def cache_buster(spec, subpath, kw): - base_subpath, ext = posixpath.splitext(subpath) - new_subpath = base_subpath + '-asdf' + ext - return new_subpath, kw + class PathConstantCacheBuster(object): + def __init__(self, token): + self.token = token + + def __call__(self, request, subpath, kw): + base_subpath, ext = posixpath.splitext(subpath) + new_subpath = base_subpath + self.token + ext + return new_subpath, kw The caveat with this approach is that modifying the path segment changes the file name, and thus must match what is actually on the @@ -532,29 +532,6 @@ The following code would set up a cachebuster: 'mypackage:static/', ManifestCacheBuster('myapp:static/manifest.json')) -A simpler approach is to use the -:class:`~pyramid.static.QueryStringConstantCacheBuster` to generate a global -token that will bust all of the assets at once. The advantage of this strategy -is that it is simple and by using the query string there doesn't need to be -any shared information between your application and the static assets. - -The following code would set up a cachebuster that just uses the time at -start up as a cachebust token: - -.. code-block:: python - :linenos: - - import time - from pyramid.static import QueryStringConstantCacheBuster - - config.add_static_view( - name='http://mycdn.example.com/', - path='mypackage:static') - - config.add_cache_buster( - 'mypackage:static/', - QueryStringConstantCacheBuster(str(int(time.time())))) - .. index:: single: static assets view @@ -834,3 +811,56 @@ when an override is used. As of Pyramid 1.6, it is also possible to override an asset by supplying an absolute path to a file or directory. This may be useful if the assets are not distributed as part of a Python package. + +Cache Busting and Asset Overrides +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Overriding static assets that are being hosted using +:meth:`pyramid.config.Configurator.add_static_view` can affect your cache +busting strategy when using any cache busters that are asset-aware such as +:class:`pyramid.static.ManifestCacheBuster`. What sets asset-aware cache +busters apart is that they have logic tied to specific assets. For example, +a manifest is only generated for a specific set of pre-defined assets. Now, +imagine you have overridden an asset defined in this manifest with a new, +unknown version. By default, the cache buster will be invoked for an asset +it has never seen before and will likely end up returning a cache busting +token for the original asset rather than the asset that will actually end up +being served! In order to get around this issue it's possible to attach a +different :class:`pyramid.interfaces.ICacheBuster` implementation to the +new assets. This would cause the original assets to be served by their +manifest, and the new assets served by their own cache buster. To do this, +:meth:`pyramid.config.Configurator.add_cache_buster` supports an ``explicit`` +option. For example: + +.. code-block:: python + :linenos: + + from pyramid.static import ManifestCacheBuster + + # define a static view for myapp:static assets + config.add_static_view('static', 'myapp:static') + + # setup a cache buster for your app based on the myapp:static assets + my_cb = ManifestCacheBuster('myapp:static/manifest.json') + config.add_cache_buster('myapp:static', my_cb) + + # override an asset + config.override_asset( + to_override='myapp:static/background.png', + override_with='theme:static/background.png') + + # override the cache buster for theme:static assets + theme_cb = ManifestCacheBuster('theme:static/manifest.json') + config.add_cache_buster('theme:static', theme_cb, explicit=True) + +In the above example there is a default cache buster, ``my_cb`` for all assets +served from the ``myapp:static`` folder. This would also affect +``theme:static/background.png`` when generating URLs via +``request.static_url('myapp:static/background.png')``. + +The ``theme_cb`` is defined explicitly for any assets loaded from the +``theme:static`` folder. Explicit cache busters have priority and thus +``theme_cb`` would be invoked for +``request.static_url('myapp:static/background.png')`` but ``my_cb`` would be +used for any other assets like +``request.static_url('myapp:static/favicon.ico')``. -- cgit v1.2.3 From 1dea1477ef7b06fbc1a5de4b434f6b8e6a9d9905 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Thu, 17 Dec 2015 00:11:35 -0800 Subject: progress through "Pyramid Uses a ZCA Registry" - minor grammar, wrap 79 columns --- docs/designdefense.rst | 142 ++++++++++++++++++++++++------------------------- 1 file changed, 69 insertions(+), 73 deletions(-) diff --git a/docs/designdefense.rst b/docs/designdefense.rst index ee6d5a317..478289c2b 100644 --- a/docs/designdefense.rst +++ b/docs/designdefense.rst @@ -7,98 +7,94 @@ From time to time, challenges to various aspects of :app:`Pyramid` design are lodged. To give context to discussions that follow, we detail some of the design decisions and trade-offs here. In some cases, we acknowledge that the framework can be made better and we describe future steps which will be taken -to improve it; in some cases we just file the challenge as noted, as -obviously you can't please everyone all of the time. +to improve it. In others we just file the challenge as noted, as obviously you +can't please everyone all of the time. Pyramid Provides More Than One Way to Do It ------------------------------------------- A canon of Python popular culture is "TIOOWTDI" ("there is only one way to do -it", a slighting, tongue-in-cheek reference to Perl's "TIMTOWTDI", which is -an acronym for "there is more than one way to do it"). - -:app:`Pyramid` is, for better or worse, a "TIMTOWTDI" system. For example, -it includes more than one way to resolve a URL to a :term:`view callable`: -via :term:`url dispatch` or :term:`traversal`. Multiple methods of -configuration exist: :term:`imperative configuration`, :term:`configuration -decoration`, and :term:`ZCML` (optionally via :term:`pyramid_zcml`). It works -with multiple different kinds of persistence and templating systems. And so -on. However, the existence of most of these overlapping ways to do things -are not without reason and purpose: we have a number of audiences to serve, -and we believe that TIMTOWTI at the web framework level actually *prevents* a -much more insidious and harmful set of duplication at higher levels in the -Python web community. - -:app:`Pyramid` began its life as :mod:`repoze.bfg`, written by a team of -people with many years of prior :term:`Zope` experience. The idea of +it", a slighting, tongue-in-cheek reference to Perl's "TIMTOWTDI", which is an +acronym for "there is more than one way to do it"). + +:app:`Pyramid` is, for better or worse, a "TIMTOWTDI" system. For example, it +includes more than one way to resolve a URL to a :term:`view callable`: via +:term:`url dispatch` or :term:`traversal`. Multiple methods of configuration +exist: :term:`imperative configuration`, :term:`configuration decoration`, and +:term:`ZCML` (optionally via :term:`pyramid_zcml`). It works with multiple +different kinds of persistence and templating systems. And so on. However, the +existence of most of these overlapping ways to do things are not without reason +and purpose: we have a number of audiences to serve, and we believe that +TIMTOWTDI at the web framework level actually *prevents* a much more insidious +and harmful set of duplication at higher levels in the Python web community. + +:app:`Pyramid` began its life as :mod:`repoze.bfg`, written by a team of people +with many years of prior :term:`Zope` experience. The idea of :term:`traversal` and the way :term:`view lookup` works was stolen entirely from Zope. The authorization subsystem provided by :app:`Pyramid` is a derivative of Zope's. The idea that an application can be *extended* without forking is also a Zope derivative. Implementations of these features were *required* to allow the :app:`Pyramid` -authors to build the bread-and-butter CMS-type systems for customers in the -way in which they were accustomed. No other system, save for Zope itself, -had such features, and Zope itself was beginning to show signs of its age. -We were becoming hampered by consequences of its early design mistakes. -Zope's lack of documentation was also difficult to work around: it was hard -to hire smart people to work on Zope applications, because there was no -comprehensive documentation set to point them at which explained "it all" in -one consumable place, and it was too large and self-inconsistent to document -properly. Before :mod:`repoze.bfg` went under development, its authors -obviously looked around for other frameworks that fit the bill. But no -non-Zope framework did. So we embarked on building :mod:`repoze.bfg`. +authors to build the bread-and-butter CMS-type systems for customers in the way +in which they were accustomed. No other system, save for Zope itself, had such +features, and Zope itself was beginning to show signs of its age. We were +becoming hampered by consequences of its early design mistakes. Zope's lack of +documentation was also difficult to work around. It was hard to hire smart +people to work on Zope applications because there was no comprehensive +documentation set which explained "it all" in one consumable place, and it was +too large and self-inconsistent to document properly. Before :mod:`repoze.bfg` +went under development, its authors obviously looked around for other +frameworks that fit the bill. But no non-Zope framework did. So we embarked on +building :mod:`repoze.bfg`. As the result of our research, however, it became apparent that, despite the -fact that no *one* framework had all the features we required, lots of -existing frameworks had good, and sometimes very compelling ideas. In -particular, :term:`URL dispatch` is a more direct mechanism to map URLs to -code. +fact that no *one* framework had all the features we required, lots of existing +frameworks had good, and sometimes very compelling ideas. In particular, +:term:`URL dispatch` is a more direct mechanism to map URLs to code. So, although we couldn't find a framework, save for Zope, that fit our needs, and while we incorporated a lot of Zope ideas into BFG, we also emulated the features we found compelling in other frameworks (such as :term:`url -dispatch`). After the initial public release of BFG, as time went on, -features were added to support people allergic to various Zope-isms in the -system, such as the ability to configure the application using -:term:`imperative configuration` and :term:`configuration decoration` rather -than solely using :term:`ZCML`, and the elimination of the required use of -:term:`interface` objects. It soon became clear that we had a system that -was very generic, and was beginning to appeal to non-Zope users as well as -ex-Zope users. +dispatch`). After the initial public release of BFG, as time went on, features +were added to support people allergic to various Zope-isms in the system, such +as the ability to configure the application using :term:`imperative +configuration` and :term:`configuration decoration`, rather than solely using +:term:`ZCML`, and the elimination of the required use of :term:`interface` +objects. It soon became clear that we had a system that was very generic, and +was beginning to appeal to non-Zope users as well as ex-Zope users. As the result of this generalization, it became obvious BFG shared 90% of its -featureset with the featureset of Pylons 1, and thus had a very similar -target market. Because they were so similar, choosing between the two -systems was an exercise in frustration for an otherwise non-partisan -developer. It was also strange for the Pylons and BFG development -communities to be in competition for the same set of users, given how similar -the two frameworks were. So the Pylons and BFG teams began to work together -to form a plan to merge. The features missing from BFG (notably :term:`view -handler` classes, flash messaging, and other minor missing bits), were added, -to provide familiarity to ex-Pylons users. The result is :app:`Pyramid`. - -The Python web framework space is currently notoriously balkanized. We're -truly hoping that the amalgamation of components in :app:`Pyramid` will -appeal to at least two currently very distinct sets of users: Pylons and BFG -users. By unifying the best concepts from Pylons and BFG into a single -codebase and leaving the bad concepts from their ancestors behind, we'll be -able to consolidate our efforts better, share more code, and promote our -efforts as a unit rather than competing pointlessly. We hope to be able to -shortcut the pack mentality which results in a *much larger* duplication of -effort, represented by competing but incredibly similar applications and -libraries, each built upon a specific low level stack that is incompatible -with the other. We'll also shrink the choice of credible Python web -frameworks down by at least one. We're also hoping to attract users from -other communities (such as Zope's and TurboGears') by providing the features -they require, while allowing enough flexibility to do things in a familiar -fashion. Some overlap of functionality to achieve these goals is expected -and unavoidable, at least if we aim to prevent pointless duplication at -higher levels. If we've done our job well enough, the various audiences will -be able to coexist and cooperate rather than firing at each other across some -imaginary web framework DMZ. - -Pyramid Uses A Zope Component Architecture ("ZCA") Registry +feature set with the feature set of Pylons 1, and thus had a very similar +target market. Because they were so similar, choosing between the two systems +was an exercise in frustration for an otherwise non-partisan developer. It was +also strange for the Pylons and BFG development communities to be in +competition for the same set of users, given how similar the two frameworks +were. So the Pylons and BFG teams began to work together to form a plan to +merge. The features missing from BFG (notably :term:`view handler` classes, +flash messaging, and other minor missing bits), were added to provide +familiarity to ex-Pylons users. The result is :app:`Pyramid`. + +The Python web framework space is currently notoriously balkanized. We're truly +hoping that the amalgamation of components in :app:`Pyramid` will appeal to at +least two currently very distinct sets of users: Pylons and BFG users. By +unifying the best concepts from Pylons and BFG into a single codebase, and +leaving the bad concepts from their ancestors behind, we'll be able to +consolidate our efforts better, share more code, and promote our efforts as a +unit rather than competing pointlessly. We hope to be able to shortcut the pack +mentality which results in a *much larger* duplication of effort, represented +by competing but incredibly similar applications and libraries, each built upon +a specific low level stack that is incompatible with the other. We'll also +shrink the choice of credible Python web frameworks down by at least one. We're +also hoping to attract users from other communities (such as Zope's and +TurboGears') by providing the features they require, while allowing enough +flexibility to do things in a familiar fashion. Some overlap of functionality +to achieve these goals is expected and unavoidable, at least if we aim to +prevent pointless duplication at higher levels. If we've done our job well +enough, the various audiences will be able to coexist and cooperate rather than +firing at each other across some imaginary web framework DMZ. + +Pyramid Uses a Zope Component Architecture ("ZCA") Registry ----------------------------------------------------------- :app:`Pyramid` uses a :term:`Zope Component Architecture` (ZCA) "component -- cgit v1.2.3 From 897d1deeab710233565f97d216e4d112b2a466e3 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 17 Dec 2015 20:52:19 -0600 Subject: grammar updates from stevepiercy --- docs/narr/assets.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/narr/assets.rst b/docs/narr/assets.rst index b28e6b5b3..054c58247 100644 --- a/docs/narr/assets.rst +++ b/docs/narr/assets.rst @@ -557,7 +557,7 @@ use some of the following options to get started: * Configure your asset pipeline to rewrite URL references inline in CSS and JavaScript. This is the best approach because then the files - may be hosted by :app:`Pyramid` or an external CDN without haven't to + may be hosted by :app:`Pyramid` or an external CDN without having to change anything. They really are static. * Templatize JS and CSS, and call ``request.static_url()`` inside their @@ -825,7 +825,7 @@ imagine you have overridden an asset defined in this manifest with a new, unknown version. By default, the cache buster will be invoked for an asset it has never seen before and will likely end up returning a cache busting token for the original asset rather than the asset that will actually end up -being served! In order to get around this issue it's possible to attach a +being served! In order to get around this issue, it's possible to attach a different :class:`pyramid.interfaces.ICacheBuster` implementation to the new assets. This would cause the original assets to be served by their manifest, and the new assets served by their own cache buster. To do this, @@ -853,14 +853,14 @@ option. For example: theme_cb = ManifestCacheBuster('theme:static/manifest.json') config.add_cache_buster('theme:static', theme_cb, explicit=True) -In the above example there is a default cache buster, ``my_cb`` for all assets -served from the ``myapp:static`` folder. This would also affect +In the above example there is a default cache buster, ``my_cb``, for all +assets served from the ``myapp:static`` folder. This would also affect ``theme:static/background.png`` when generating URLs via ``request.static_url('myapp:static/background.png')``. The ``theme_cb`` is defined explicitly for any assets loaded from the ``theme:static`` folder. Explicit cache busters have priority and thus ``theme_cb`` would be invoked for -``request.static_url('myapp:static/background.png')`` but ``my_cb`` would be -used for any other assets like +``request.static_url('myapp:static/background.png')``, but ``my_cb`` would +be used for any other assets like ``request.static_url('myapp:static/favicon.ico')``. -- cgit v1.2.3 From bc8fa64a416ce52ec5cc9fd819ce1a3caa427a19 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 17 Dec 2015 21:18:34 -0600 Subject: update changelog for #2171 --- CHANGES.txt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index e9dc975a7..3c3dd6e79 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -53,11 +53,12 @@ Features See https://github.com/Pylons/pyramid/pull/1471 - Cache busting for static resources has been added and is available via a new - argument to ``pyramid.config.Configurator.add_static_view``: ``cachebust``. - Core APIs are shipped for both cache busting via query strings and - path segments and may be extended to fit into custom asset pipelines. + ``pyramid.config.Configurator.add_cache_buster`` API. Core APIs are shipped + for both cache busting via query strings and via asset manifests for + integrating into custom asset pipelines. See https://github.com/Pylons/pyramid/pull/1380 and - https://github.com/Pylons/pyramid/pull/1583 + https://github.com/Pylons/pyramid/pull/1583 and + https://github.com/Pylons/pyramid/pull/2171 - Add ``pyramid.config.Configurator.root_package`` attribute and init parameter to assist with includeable packages that wish to resolve -- cgit v1.2.3 From f7171cc18d2452797e1497158bed21d779c54355 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 17 Dec 2015 21:53:34 -0600 Subject: ensure IAssetDescriptor.abspath always returns an abspath fixes #2184 --- pyramid/path.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyramid/path.py b/pyramid/path.py index b79c5a6ac..ceb0a0cf3 100644 --- a/pyramid/path.py +++ b/pyramid/path.py @@ -396,7 +396,8 @@ class PkgResourcesAssetDescriptor(object): return '%s:%s' % (self.pkg_name, self.path) def abspath(self): - return self.pkg_resources.resource_filename(self.pkg_name, self.path) + return os.path.abspath( + self.pkg_resources.resource_filename(self.pkg_name, self.path)) def stream(self): return self.pkg_resources.resource_stream(self.pkg_name, self.path) -- cgit v1.2.3 From 010218070ef62a31e3880acf2994ea217797332a Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 17 Dec 2015 22:19:46 -0600 Subject: add changelog for #2187 --- CHANGES.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 3c3dd6e79..0f88a95da 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -252,6 +252,11 @@ Bug Fixes process failed to fork because it could not find the pserve script to run. See https://github.com/Pylons/pyramid/pull/2137 +- Ensure that ``IAssetDescriptor.abspath`` always returns an absolute path. + There were cases depending on the process CWD that a relative path would + be returned. See https://github.com/Pylons/pyramid/issues/2187 + + Deprecations ------------ -- cgit v1.2.3 From 43fb2c233a6ebbf5c26cf66a0b1ddb16d89a1026 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 18 Dec 2015 09:50:52 -0600 Subject: deprecate pserve --user and --group options This completes the deprecation of all process management options from pserve. --- pyramid/scripts/pserve.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/pyramid/scripts/pserve.py b/pyramid/scripts/pserve.py index 95752a3be..5aaaffec9 100644 --- a/pyramid/scripts/pserve.py +++ b/pyramid/scripts/pserve.py @@ -212,8 +212,9 @@ class PServeCommand(object): self.options.set_user = self.options.set_group = None # @@: Is this the right stage to set the user at? - self.change_user_group( - self.options.set_user, self.options.set_group) + if self.options.set_user or self.options.set_group: + self.change_user_group( + self.options.set_user, self.options.set_group) if not self.args: self.out('You must give a config file') @@ -624,11 +625,16 @@ a real process manager for your processes like Systemd, Circus, or Supervisor. self.out('%s %s %s' % ('-' * 20, 'Restarting', '-' * 20)) def change_user_group(self, user, group): # pragma: no cover - if not user and not group: - return import pwd import grp + self.out('''\ +The --user and --group options have been deprecated in Pyramid 1.6. They will +be removed in a future release per Pyramid's deprecation policy. Please +consider using a real process manager for your processes like Systemd, Circus, +or Supervisor, all of which support process security. +''') + uid = gid = None if group: try: -- cgit v1.2.3 From 7593b05e4b4bd0bc85c5f46964b4e4a55286ad49 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sat, 19 Dec 2015 00:15:28 -0600 Subject: update changelog for #2189 --- CHANGES.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index 0f88a95da..32c4995b8 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -263,7 +263,10 @@ Deprecations - The ``pserve`` command's daemonization features have been deprecated as well as ``--monitor-restart``. This includes the ``[start,stop,restart,status]`` subcommands as well as the ``--daemon``, ``--stop-daemon``, ``--pid-file``, - and ``--status`` flags. + ``--status``, ``--user`` and ``--group`` flags. + See https://github.com/Pylons/pyramid/pull/2120 + and https://github.com/Pylons/pyramid/pull/2189 + and https://github.com/Pylons/pyramid/pull/1641 Please use a real process manager in the future instead of relying on the ``pserve`` to daemonize itself. Many options exist including your Operating -- cgit v1.2.3 From 50dd2e4c7d8f5ab8de79c490e304b44916183f77 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sun, 20 Dec 2015 14:00:12 -0800 Subject: add sphinxcontrib-programoutput configuration to render command line output --- docs/conf.py | 9 +++++---- setup.py | 1 + 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 8a9bac6ed..073811eca 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -53,12 +53,13 @@ extensions = [ 'sphinx.ext.doctest', 'repoze.sphinx.autointerface', 'sphinx.ext.viewcode', - 'sphinx.ext.intersphinx' + 'sphinx.ext.intersphinx', + 'sphinxcontrib.programoutput', ] # Looks for objects in external projects intersphinx_mapping = { - 'colander': ( 'http://docs.pylonsproject.org/projects/colander/en/latest', None), + 'colander': ('http://docs.pylonsproject.org/projects/colander/en/latest', None), 'cookbook': ('http://docs.pylonsproject.org/projects/pyramid-cookbook/en/latest/', None), 'deform': ('http://docs.pylonsproject.org/projects/deform/en/latest', None), 'jinja2': ('http://docs.pylonsproject.org/projects/pyramid-jinja2/en/latest/', None), @@ -66,8 +67,8 @@ intersphinx_mapping = { 'python': ('http://docs.python.org', None), 'python3': ('http://docs.python.org/3', None), 'sqla': ('http://docs.sqlalchemy.org/en/latest', None), - 'tm': ('http://docs.pylonsproject.org/projects/pyramid_tm/en/latest/', None), - 'toolbar': ('http://docs.pylonsproject.org/projects/pyramid-debugtoolbar/en/latest', None), + 'tm': ('http://docs.pylonsproject.org/projects/pyramid_tm/en/latest/', None), + 'toolbar': ('http://docs.pylonsproject.org/projects/pyramid-debugtoolbar/en/latest', None), 'tstring': ('http://docs.pylonsproject.org/projects/translationstring/en/latest', None), 'tutorials': ('http://docs.pylonsproject.org/projects/pyramid-tutorials/en/latest/', None), 'venusian': ('http://docs.pylonsproject.org/projects/venusian/en/latest', None), diff --git a/setup.py b/setup.py index c81956e7f..9bdfcd90e 100644 --- a/setup.py +++ b/setup.py @@ -61,6 +61,7 @@ docs_extras = [ 'repoze.sphinx.autointerface', 'pylons_sphinx_latesturl', 'pylons-sphinx-themes', + 'sphinxcontrib-programoutput', ] testing_extras = tests_require + [ -- cgit v1.2.3 From 5ff3d2dfdbf936d115e3486696401ad7dbffedc3 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Mon, 21 Dec 2015 01:24:34 -0800 Subject: - add p* scripts - add links to p* scripts - add a blank line to keep indices and labels better visually related to the subsequent heading --- docs/index.rst | 13 +++++++++++++ docs/narr/commandline.rst | 33 +++++++++++++++++++++++++++++++++ docs/narr/project.rst | 6 ++++++ docs/pscripts/index.rst | 12 ++++++++++++ docs/pscripts/pcreate.rst | 14 ++++++++++++++ docs/pscripts/pdistreport.rst | 14 ++++++++++++++ docs/pscripts/prequest.rst | 14 ++++++++++++++ docs/pscripts/proutes.rst | 14 ++++++++++++++ docs/pscripts/pserve.rst | 14 ++++++++++++++ docs/pscripts/pshell.rst | 14 ++++++++++++++ docs/pscripts/ptweens.rst | 14 ++++++++++++++ docs/pscripts/pviews.rst | 14 ++++++++++++++ 12 files changed, 176 insertions(+) create mode 100644 docs/pscripts/index.rst create mode 100644 docs/pscripts/pcreate.rst create mode 100644 docs/pscripts/pdistreport.rst create mode 100644 docs/pscripts/prequest.rst create mode 100644 docs/pscripts/proutes.rst create mode 100644 docs/pscripts/pserve.rst create mode 100644 docs/pscripts/pshell.rst create mode 100644 docs/pscripts/ptweens.rst create mode 100644 docs/pscripts/pviews.rst diff --git a/docs/index.rst b/docs/index.rst index e792b9905..8c8a0a18d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -162,6 +162,19 @@ Comprehensive reference material for every public API exposed by api/* +``p*`` Scripts Documentation +============================ + +``p*`` scripts included with :app:`Pyramid`:. + +.. toctree:: + :maxdepth: 1 + :glob: + + pscripts/index + pscripts/* + + Change History ============== diff --git a/docs/narr/commandline.rst b/docs/narr/commandline.rst index eb79dffb6..34b12e1e9 100644 --- a/docs/narr/commandline.rst +++ b/docs/narr/commandline.rst @@ -6,6 +6,7 @@ Command-Line Pyramid Your :app:`Pyramid` application can be controlled and inspected using a variety of command-line utilities. These utilities are documented in this chapter. + .. index:: pair: matching views; printing single: pviews @@ -15,6 +16,8 @@ of command-line utilities. These utilities are documented in this chapter. Displaying Matching Views for a Given URL ----------------------------------------- +.. seealso:: See also the output of :ref:`pviews --help `. + For a big application with several views, it can be hard to keep the view configuration details in your head, even if you defined all the views yourself. You can use the ``pviews`` command in a terminal window to print a summary of @@ -114,6 +117,8 @@ found* message. The Interactive Shell --------------------- +.. seealso:: See also the output of :ref:`pshell --help `. + Once you've installed your program for development using ``setup.py develop``, you can use an interactive Python shell to execute expressions in a Python environment exactly like the one that will be used when your application runs @@ -179,6 +184,7 @@ hash after the filename: Press ``Ctrl-D`` to exit the interactive shell (or ``Ctrl-Z`` on Windows). + .. index:: pair: pshell; extending @@ -261,6 +267,7 @@ request is configured to generate urls from the host >>> request.route_url('home') 'https://www.example.com/' + .. _ipython_or_bpython: Alternative Shells @@ -317,6 +324,7 @@ arguments, ``env`` and ``help``, which would look like this: ``ipython`` and ``bpython`` have been moved into their respective packages ``pyramid_ipython`` and ``pyramid_bpython``. + Setting a Default Shell ~~~~~~~~~~~~~~~~~~~~~~~ @@ -331,6 +339,7 @@ specify a list of preferred shells. .. versionadded:: 1.6 + .. index:: pair: routes; printing single: proutes @@ -340,6 +349,8 @@ specify a list of preferred shells. Displaying All Application Routes --------------------------------- +.. seealso:: See also the output of :ref:`proutes --help `. + You can use the ``proutes`` command in a terminal window to print a summary of routes related to your application. Much like the ``pshell`` command (see :ref:`interactive_shell`), the ``proutes`` command accepts one argument with @@ -421,6 +432,8 @@ include. The current available formats are ``name``, ``pattern``, ``view``, and Displaying "Tweens" ------------------- +.. seealso:: See also the output of :ref:`ptweens --help `. + A :term:`tween` is a bit of code that sits between the main Pyramid application request handler and the WSGI application which calls it. A user can get a representation of both the implicit tween ordering (the ordering specified by @@ -497,6 +510,7 @@ used: See :ref:`registering_tweens` for more information about tweens. + .. index:: single: invoking a request single: prequest @@ -506,6 +520,8 @@ See :ref:`registering_tweens` for more information about tweens. Invoking a Request ------------------ +.. seealso:: See also the output of :ref:`prequest --help `. + You can use the ``prequest`` command-line utility to send a request to your application and see the response body without starting a server. @@ -555,6 +571,7 @@ of the ``prequest`` process is used as the ``POST`` body:: $ $VENV/bin/prequest -mPOST development.ini / < somefile + Using Custom Arguments to Python when Running ``p*`` Scripts ------------------------------------------------------------ @@ -566,11 +583,22 @@ Python interpreter at runtime. For example:: python -3 -m pyramid.scripts.pserve development.ini + +.. index:: + single: pdistreport + single: distributions, showing installed + single: showing installed distributions + +.. _showing_distributions: + Showing All Installed Distributions and Their Versions ------------------------------------------------------ .. versionadded:: 1.5 +.. seealso:: See also the output of :ref:`pdistreport --help + `. + You can use the ``pdistreport`` command to show the :app:`Pyramid` version in use, the Python version in use, and all installed versions of Python distributions in your Python environment:: @@ -590,6 +618,7 @@ pastebin when you are having problems and need someone with more familiarity with Python packaging and distribution than you have to look at your environment. + .. _writing_a_script: Writing a Script @@ -702,6 +731,7 @@ The above example specifies the ``another`` ``app``, ``pipeline``, or object present in the ``env`` dictionary returned by :func:`pyramid.paster.bootstrap` will be a :app:`Pyramid` :term:`router`. + Changing the Request ~~~~~~~~~~~~~~~~~~~~ @@ -742,6 +772,7 @@ Now you can readily use Pyramid's APIs for generating URLs: env['request'].route_url('verify', code='1337') # will return 'https://example.com/prefix/verify/1337' + Cleanup ~~~~~~~ @@ -757,6 +788,7 @@ callback: env['closer']() + Setting Up Logging ~~~~~~~~~~~~~~~~~~ @@ -773,6 +805,7 @@ use the following command: See :ref:`logging_chapter` for more information on logging within :app:`Pyramid`. + .. index:: single: console script diff --git a/docs/narr/project.rst b/docs/narr/project.rst index 4785b60c4..5103bb6b8 100644 --- a/docs/narr/project.rst +++ b/docs/narr/project.rst @@ -53,15 +53,19 @@ The included scaffolds are these: ``alchemy`` URL mapping via :term:`URL dispatch` and persistence via :term:`SQLAlchemy` + .. index:: single: creating a project single: project + single: pcreate .. _creating_a_project: Creating the Project -------------------- +.. seealso:: See also the output of :ref:`pcreate --help `. + In :ref:`installing_chapter`, you created a virtual Python environment via the ``virtualenv`` command. To start a :app:`Pyramid` :term:`project`, use the ``pcreate`` command installed within the virtualenv. We'll choose the @@ -262,6 +266,8 @@ single sample test exists. Running the Project Application ------------------------------- +.. seealso:: See also the output of :ref:`pserve --help `. + Once a project is installed for development, you can run the application it represents using the ``pserve`` command against the generated configuration file. In our case, this file is named ``development.ini``. diff --git a/docs/pscripts/index.rst b/docs/pscripts/index.rst new file mode 100644 index 000000000..1f54546d7 --- /dev/null +++ b/docs/pscripts/index.rst @@ -0,0 +1,12 @@ +.. _pscripts_documentation: + +``p*`` Scripts Documentation +============================ + +``p*`` scripts included with :app:`Pyramid`:. + +.. toctree:: + :maxdepth: 1 + :glob: + + * diff --git a/docs/pscripts/pcreate.rst b/docs/pscripts/pcreate.rst new file mode 100644 index 000000000..4e7f89572 --- /dev/null +++ b/docs/pscripts/pcreate.rst @@ -0,0 +1,14 @@ +.. index:: + single: pcreate; --help + +.. _pcreate_script: + +``pcreate`` +----------- + +.. program-output:: pcreate --help + :cwd: ../../env/bin + :prompt: + :shell: + +.. seealso:: :ref:`creating_a_project` diff --git a/docs/pscripts/pdistreport.rst b/docs/pscripts/pdistreport.rst new file mode 100644 index 000000000..37d12d848 --- /dev/null +++ b/docs/pscripts/pdistreport.rst @@ -0,0 +1,14 @@ +.. index:: + single: pdistreport; --help + +.. _pdistreport_script: + +``pdistreport`` +--------------- + +.. program-output:: pdistreport --help + :cwd: ../../env/bin + :prompt: + :shell: + +.. seealso:: :ref:`showing_distributions` diff --git a/docs/pscripts/prequest.rst b/docs/pscripts/prequest.rst new file mode 100644 index 000000000..a03d5b0e0 --- /dev/null +++ b/docs/pscripts/prequest.rst @@ -0,0 +1,14 @@ +.. index:: + single: prequest; --help + +.. _prequest_script: + +``prequest`` +------------ + +.. program-output:: prequest --help + :cwd: ../../env/bin + :prompt: + :shell: + +.. seealso:: :ref:`invoking_a_request` diff --git a/docs/pscripts/proutes.rst b/docs/pscripts/proutes.rst new file mode 100644 index 000000000..8f3f34e16 --- /dev/null +++ b/docs/pscripts/proutes.rst @@ -0,0 +1,14 @@ +.. index:: + single: proutes; --help + +.. _proutes_script: + +``proutes`` +----------- + +.. program-output:: proutes --help + :cwd: ../../env/bin + :prompt: + :shell: + +.. seealso:: :ref:`displaying_application_routes` diff --git a/docs/pscripts/pserve.rst b/docs/pscripts/pserve.rst new file mode 100644 index 000000000..4e41d6e2b --- /dev/null +++ b/docs/pscripts/pserve.rst @@ -0,0 +1,14 @@ +.. index:: + single: pserve; --help + +.. _pserve_script: + +``pserve`` +---------- + +.. program-output:: pserve --help + :cwd: ../../env/bin + :prompt: + :shell: + +.. seealso:: :ref:`running_the_project_application` diff --git a/docs/pscripts/pshell.rst b/docs/pscripts/pshell.rst new file mode 100644 index 000000000..fd85a11a9 --- /dev/null +++ b/docs/pscripts/pshell.rst @@ -0,0 +1,14 @@ +.. index:: + single: pshell; --help + +.. _pshell_script: + +``pshell`` +---------- + +.. program-output:: pshell --help + :cwd: ../../env/bin + :prompt: + :shell: + +.. seealso:: :ref:`interactive_shell` diff --git a/docs/pscripts/ptweens.rst b/docs/pscripts/ptweens.rst new file mode 100644 index 000000000..e8c0d1ad0 --- /dev/null +++ b/docs/pscripts/ptweens.rst @@ -0,0 +1,14 @@ +.. index:: + single: ptweens; --help + +.. _ptweens_script: + +``ptweens`` +----------- + +.. program-output:: ptweens --help + :cwd: ../../env/bin + :prompt: + :shell: + +.. seealso:: :ref:`displaying_tweens` diff --git a/docs/pscripts/pviews.rst b/docs/pscripts/pviews.rst new file mode 100644 index 000000000..a1c2f5c2b --- /dev/null +++ b/docs/pscripts/pviews.rst @@ -0,0 +1,14 @@ +.. index:: + single: pviews; --help + +.. _pviews_script: + +``pviews`` +---------- + +.. program-output:: pviews --help + :cwd: ../../env/bin + :prompt: + :shell: + +.. seealso:: :ref:`displaying_matching_views` -- cgit v1.2.3 From d8101d6e99012bd3a7fcf1806e7d922c8d1371d9 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Mon, 21 Dec 2015 21:37:52 -0800 Subject: attempt to get travis to pass on programoutput rst directive --- docs/pscripts/pcreate.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/pscripts/pcreate.rst b/docs/pscripts/pcreate.rst index 4e7f89572..b5ec3f4e2 100644 --- a/docs/pscripts/pcreate.rst +++ b/docs/pscripts/pcreate.rst @@ -7,7 +7,6 @@ ----------- .. program-output:: pcreate --help - :cwd: ../../env/bin :prompt: :shell: -- cgit v1.2.3 From cdb9f7a18725a992f8ad0bfc920c028082222b47 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Mon, 21 Dec 2015 21:42:32 -0800 Subject: remove :cwd: argument from programoutput rst directive so that travis will pass --- docs/pscripts/pdistreport.rst | 1 - docs/pscripts/prequest.rst | 1 - docs/pscripts/proutes.rst | 1 - docs/pscripts/pserve.rst | 1 - docs/pscripts/pshell.rst | 1 - docs/pscripts/ptweens.rst | 1 - docs/pscripts/pviews.rst | 1 - 7 files changed, 7 deletions(-) diff --git a/docs/pscripts/pdistreport.rst b/docs/pscripts/pdistreport.rst index 37d12d848..1c53fb6e9 100644 --- a/docs/pscripts/pdistreport.rst +++ b/docs/pscripts/pdistreport.rst @@ -7,7 +7,6 @@ --------------- .. program-output:: pdistreport --help - :cwd: ../../env/bin :prompt: :shell: diff --git a/docs/pscripts/prequest.rst b/docs/pscripts/prequest.rst index a03d5b0e0..a15827767 100644 --- a/docs/pscripts/prequest.rst +++ b/docs/pscripts/prequest.rst @@ -7,7 +7,6 @@ ------------ .. program-output:: prequest --help - :cwd: ../../env/bin :prompt: :shell: diff --git a/docs/pscripts/proutes.rst b/docs/pscripts/proutes.rst index 8f3f34e16..09ed013e1 100644 --- a/docs/pscripts/proutes.rst +++ b/docs/pscripts/proutes.rst @@ -7,7 +7,6 @@ ----------- .. program-output:: proutes --help - :cwd: ../../env/bin :prompt: :shell: diff --git a/docs/pscripts/pserve.rst b/docs/pscripts/pserve.rst index 4e41d6e2b..d33d4a484 100644 --- a/docs/pscripts/pserve.rst +++ b/docs/pscripts/pserve.rst @@ -7,7 +7,6 @@ ---------- .. program-output:: pserve --help - :cwd: ../../env/bin :prompt: :shell: diff --git a/docs/pscripts/pshell.rst b/docs/pscripts/pshell.rst index fd85a11a9..cfd84d4f8 100644 --- a/docs/pscripts/pshell.rst +++ b/docs/pscripts/pshell.rst @@ -7,7 +7,6 @@ ---------- .. program-output:: pshell --help - :cwd: ../../env/bin :prompt: :shell: diff --git a/docs/pscripts/ptweens.rst b/docs/pscripts/ptweens.rst index e8c0d1ad0..02e23e49a 100644 --- a/docs/pscripts/ptweens.rst +++ b/docs/pscripts/ptweens.rst @@ -7,7 +7,6 @@ ----------- .. program-output:: ptweens --help - :cwd: ../../env/bin :prompt: :shell: diff --git a/docs/pscripts/pviews.rst b/docs/pscripts/pviews.rst index a1c2f5c2b..b4de5c054 100644 --- a/docs/pscripts/pviews.rst +++ b/docs/pscripts/pviews.rst @@ -7,7 +7,6 @@ ---------- .. program-output:: pviews --help - :cwd: ../../env/bin :prompt: :shell: -- cgit v1.2.3 From f829b1c82595849e0f7f685f8c559d021748a0d2 Mon Sep 17 00:00:00 2001 From: kpinc Date: Tue, 22 Dec 2015 09:47:12 -0600 Subject: Expound. Punctuation. --- docs/pscripts/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/pscripts/index.rst b/docs/pscripts/index.rst index 1f54546d7..857e0564f 100644 --- a/docs/pscripts/index.rst +++ b/docs/pscripts/index.rst @@ -3,7 +3,7 @@ ``p*`` Scripts Documentation ============================ -``p*`` scripts included with :app:`Pyramid`:. +Command line programs (``p*`` scripts) included with :app:`Pyramid`. .. toctree:: :maxdepth: 1 -- cgit v1.2.3 From 091941ddfaf2e2349b7884f15ce3d79339a8e835 Mon Sep 17 00:00:00 2001 From: kpinc Date: Tue, 22 Dec 2015 09:56:42 -0600 Subject: Update prequest.py --- pyramid/scripts/prequest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/scripts/prequest.py b/pyramid/scripts/prequest.py index 61e422c64..e07f9d10e 100644 --- a/pyramid/scripts/prequest.py +++ b/pyramid/scripts/prequest.py @@ -14,7 +14,7 @@ def main(argv=sys.argv, quiet=False): class PRequestCommand(object): description = """\ - Run a request for the described application. + Submit a HTTP request to a web application. This command makes an artifical request to a web application that uses a PasteDeploy (.ini) configuration file for the server and application. -- cgit v1.2.3 From d9da2b29861d071b9fc044319421799a3d522bcc Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Wed, 23 Dec 2015 02:31:05 -0800 Subject: Add documentation of command line programs/p* scripts --- CHANGES.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 32c4995b8..77f5d94ac 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -301,6 +301,9 @@ Docs ``principal`` and a ``userid`` in its security APIs. See https://github.com/Pylons/pyramid/pull/1399 +- Add documentation of command line programs (``p*`` scripts). See + https://github.com/Pylons/pyramid/pull/2191 + Scaffolds --------- -- cgit v1.2.3 From 169dba5c7aa02db2e48cecff8b8126b767fdf327 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Wed, 23 Dec 2015 03:19:58 -0800 Subject: - Add Python compatibility to history, hacking, releasing - Use 1.X for version number --- HACKING.txt | 8 ++++---- HISTORY.txt | 4 ++++ RELEASING.txt | 14 +++++++------- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/HACKING.txt b/HACKING.txt index c838fda22..5bbdce0c6 100644 --- a/HACKING.txt +++ b/HACKING.txt @@ -124,10 +124,10 @@ In order to add a feature to Pyramid: - The feature must be documented in both the API and narrative documentation (in ``docs/``). -- The feature must work fully on the following CPython versions: 2.6, - 2.7, 3.2, and 3.3 on both UNIX and Windows. +- The feature must work fully on the following CPython versions: 2.6, 2.7, 3.2, + 3.3, 3.4, and 3.5 on both UNIX and Windows. -- The feature must work on the latest version of PyPy. +- The feature must work on the latest version of PyPy and PyPy3. - The feature must not cause installation or runtime failure on App Engine. If it doesn't cause installation or runtime failure, but doesn't actually @@ -199,7 +199,7 @@ Running Tests Alternately:: - $ tox -e{py26,py27,py32,py33,py34,pypy,pypy3}-scaffolds, + $ tox -e{py26,py27,py32,py33,py34,py35,pypy,pypy3}-scaffolds, Test Coverage ------------- diff --git a/HISTORY.txt b/HISTORY.txt index c30bb2711..68ddb3a90 100644 --- a/HISTORY.txt +++ b/HISTORY.txt @@ -1,6 +1,8 @@ 1.5 (2014-04-08) ================ +- Python 3.4 compatibility. + - Avoid crash in ``pserve --reload`` under Py3k, when iterating over possibly mutated ``sys.modules``. @@ -1130,6 +1132,8 @@ Bug Fixes Features -------- +- Python 3.3 compatibility. + - Configurator.add_directive now accepts arbitrary callables like partials or objects implementing ``__call__`` which dont have ``__name__`` and ``__doc__`` attributes. See https://github.com/Pylons/pyramid/issues/621 diff --git a/RELEASING.txt b/RELEASING.txt index 87ff62c53..fa4ebab5b 100644 --- a/RELEASING.txt +++ b/RELEASING.txt @@ -15,7 +15,7 @@ Releasing Pyramid - Run tests on Windows if feasible. -- Make sure all scaffold tests pass (Py 2.6, 2.7, 3.2, 3.3, 3.4, pypy, and +- Make sure all scaffold tests pass (Py 2.6, 2.7, 3.2, 3.3, 3.4, 3.5, pypy, and pypy3 on UNIX; this doesn't work on Windows): $ ./scaffoldtests.sh @@ -69,22 +69,22 @@ Releasing Pyramid Announcement template ---------------------- -Pyramid 1.1.X has been released. +Pyramid 1.X.X has been released. Here are the changes: <> -A "What's New In Pyramid 1.1" document exists at -http://docs.pylonsproject.org/projects/pyramid/1.1/whatsnew-1.1.html . +A "What's New In Pyramid 1.X" document exists at +http://docs.pylonsproject.org/projects/pyramid/1.X/whatsnew-1.X.html . -You will be able to see the 1.1 release documentation (across all +You will be able to see the 1.X release documentation (across all alphas and betas, as well as when it eventually gets to final release) -at http://docs.pylonsproject.org/projects/pyramid/1.1/ . +at http://docs.pylonsproject.org/projects/pyramid/1.X/ . You can install it via PyPI: - easy_install Pyramid==1.1a4 + easy_install Pyramid==1.X Enjoy, and please report any issues you find to the issue tracker at https://github.com/Pylons/pyramid/issues -- cgit v1.2.3 From b7057f3dcac875d71916d6807d157ff6f3ede662 Mon Sep 17 00:00:00 2001 From: Bastien Date: Sat, 26 Dec 2015 14:33:23 -0800 Subject: Update glossary.rst fixed typo (cherry picked from commit 0a5c9a2) --- docs/glossary.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/glossary.rst b/docs/glossary.rst index b4bb36421..60e861597 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -273,7 +273,7 @@ Glossary (Allow, 'bob', 'read'), (Deny, 'fred', 'write')]``. If an ACL is attached to a resource instance, and that resource is findable via the context resource, it will be consulted any active security policy to - determine wither a particular request can be fulfilled given the + determine whether a particular request can be fulfilled given the :term:`authentication` information in the request. authentication -- cgit v1.2.3 From e8609bb3437a4642ce33aa13ca65e84003b397ca Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Wed, 30 Dec 2015 02:08:11 -0800 Subject: - minor grammar in "problems", rewrap to 79 columns --- docs/designdefense.rst | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/designdefense.rst b/docs/designdefense.rst index 478289c2b..bfde25246 100644 --- a/docs/designdefense.rst +++ b/docs/designdefense.rst @@ -142,7 +142,7 @@ dictionary API, but that's not very important in this context. That's problem number two. Third of all, what does the ``getUtility`` function do? It's performing a -lookup for the ``ISettings`` "utility" that should return.. well, a utility. +lookup for the ``ISettings`` "utility" that should return... well, a utility. Note how we've already built up a dependency on the understanding of an :term:`interface` and the concept of "utility" to answer this question: a bad sign so far. Note also that the answer is circular, a *really* bad sign. @@ -152,12 +152,12 @@ registry" of course. What's a component registry? Problem number four. Fifth, assuming you buy that there's some magical registry hanging around, where *is* this registry? *Homina homina*... "around"? That's sort of the -best answer in this context (a more specific answer would require knowledge -of internals). Can there be more than one registry? Yes. So *which* -registry does it find the registration in? Well, the "current" registry of -course. In terms of :app:`Pyramid`, the current registry is a thread local -variable. Using an API that consults a thread local makes understanding how -it works non-local. +best answer in this context (a more specific answer would require knowledge of +internals). Can there be more than one registry? Yes. So in *which* registry +does it find the registration? Well, the "current" registry of course. In +terms of :app:`Pyramid`, the current registry is a thread local variable. +Using an API that consults a thread local makes understanding how it works +non-local. You've now bought in to the fact that there's a registry that is just hanging around. But how does the registry get populated? Why, via code that calls @@ -166,10 +166,10 @@ registration of ``ISettings`` is made by the framework itself under the hood: it's not present in any user configuration. This is extremely hard to comprehend. Problem number six. -Clearly there's some amount of cognitive load here that needs to be borne by -a reader of code that extends the :app:`Pyramid` framework due to its use of -the ZCA, even if he or she is already an expert Python programmer and whom is -an expert in the domain of web applications. This is suboptimal. +Clearly there's some amount of cognitive load here that needs to be borne by a +reader of code that extends the :app:`Pyramid` framework due to its use of the +ZCA, even if they are already an expert Python programmer and an expert in the +domain of web applications. This is suboptimal. Ameliorations +++++++++++++ -- cgit v1.2.3 From 752d6575d7b86eeb1a7b16eac9476a7620897438 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sat, 2 Jan 2016 22:10:01 -0800 Subject: - minor grammar in "problems", rewrap to 79 columns --- docs/whatsnew-1.5.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/whatsnew-1.5.rst b/docs/whatsnew-1.5.rst index 1d863c937..8a769e88e 100644 --- a/docs/whatsnew-1.5.rst +++ b/docs/whatsnew-1.5.rst @@ -136,6 +136,8 @@ Feature Additions The feature additions in Pyramid 1.5 follow. +- Python 3.4 compatibility. + - Add ``pdistreport`` script, which prints the Python version in use, the Pyramid version in use, and the version number and location of all Python distributions currently installed. -- cgit v1.2.3 From 2901e968390f70b4bfa777dd4348dc9d3eb6210c Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 3 Jan 2016 00:22:44 -0600 Subject: add a note about serving cache busted assets --- docs/narr/assets.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/narr/assets.rst b/docs/narr/assets.rst index 054c58247..58f547fc9 100644 --- a/docs/narr/assets.rst +++ b/docs/narr/assets.rst @@ -532,6 +532,14 @@ The following code would set up a cachebuster: 'mypackage:static/', ManifestCacheBuster('myapp:static/manifest.json')) +It's important to note that the cache buster only handles generating +cache-busted URLs for static assets. It does **NOT** provide any solutions for +serving those assets. For example, if you generated a URL for +``css/main-678b7c80.css`` then that URL needs to be valid either by +configuring ``add_static_view`` properly to point to the location of the files +or some other mechanism such as the files existing on your CDN or rewriting +the incoming URL to remove the cache bust tokens. + .. index:: single: static assets view -- cgit v1.2.3 From 41aeac4b9442a03c961b85b896ae84a8da3dbc9c Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 3 Jan 2016 01:06:35 -0600 Subject: fix grammar on whatsnew document titles --- docs/whatsnew-1.0.rst | 2 +- docs/whatsnew-1.1.rst | 2 +- docs/whatsnew-1.2.rst | 2 +- docs/whatsnew-1.3.rst | 2 +- docs/whatsnew-1.4.rst | 2 +- docs/whatsnew-1.5.rst | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/whatsnew-1.0.rst b/docs/whatsnew-1.0.rst index 9541f0a28..0ed6e21fc 100644 --- a/docs/whatsnew-1.0.rst +++ b/docs/whatsnew-1.0.rst @@ -1,4 +1,4 @@ -What's New In Pyramid 1.0 +What's New in Pyramid 1.0 ========================= This article explains the new features in Pyramid version 1.0 as compared to diff --git a/docs/whatsnew-1.1.rst b/docs/whatsnew-1.1.rst index 99737b6d8..a5c7f3393 100644 --- a/docs/whatsnew-1.1.rst +++ b/docs/whatsnew-1.1.rst @@ -1,4 +1,4 @@ -What's New In Pyramid 1.1 +What's New in Pyramid 1.1 ========================= This article explains the new features in Pyramid version 1.1 as compared to diff --git a/docs/whatsnew-1.2.rst b/docs/whatsnew-1.2.rst index a9fc38908..9ff933ace 100644 --- a/docs/whatsnew-1.2.rst +++ b/docs/whatsnew-1.2.rst @@ -1,4 +1,4 @@ -What's New In Pyramid 1.2 +What's New in Pyramid 1.2 ========================= This article explains the new features in :app:`Pyramid` version 1.2 as diff --git a/docs/whatsnew-1.3.rst b/docs/whatsnew-1.3.rst index 2606c3df3..1a299e126 100644 --- a/docs/whatsnew-1.3.rst +++ b/docs/whatsnew-1.3.rst @@ -1,4 +1,4 @@ -What's New In Pyramid 1.3 +What's New in Pyramid 1.3 ========================= This article explains the new features in :app:`Pyramid` version 1.3 as diff --git a/docs/whatsnew-1.4.rst b/docs/whatsnew-1.4.rst index 505b9d798..fce889854 100644 --- a/docs/whatsnew-1.4.rst +++ b/docs/whatsnew-1.4.rst @@ -1,4 +1,4 @@ -What's New In Pyramid 1.4 +What's New in Pyramid 1.4 ========================= This article explains the new features in :app:`Pyramid` version 1.4 as diff --git a/docs/whatsnew-1.5.rst b/docs/whatsnew-1.5.rst index 8a769e88e..a477ce5ec 100644 --- a/docs/whatsnew-1.5.rst +++ b/docs/whatsnew-1.5.rst @@ -1,4 +1,4 @@ -What's New In Pyramid 1.5 +What's New in Pyramid 1.5 ========================= This article explains the new features in :app:`Pyramid` version 1.5 as -- cgit v1.2.3 From 5558386fd1a6181b2e8ad06659049c055a7ed023 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 3 Jan 2016 01:09:52 -0600 Subject: copy whatsnew-1.6 from 1.6-branch --- docs/whatsnew-1.6.rst | 204 +++++++++++++++++++++++++++++++++----------------- 1 file changed, 135 insertions(+), 69 deletions(-) diff --git a/docs/whatsnew-1.6.rst b/docs/whatsnew-1.6.rst index b99ebeec4..bdfcf34ab 100644 --- a/docs/whatsnew-1.6.rst +++ b/docs/whatsnew-1.6.rst @@ -1,40 +1,62 @@ -What's New In Pyramid 1.6 +What's New in Pyramid 1.6 ========================= This article explains the new features in :app:`Pyramid` version 1.6 as -compared to its predecessor, :app:`Pyramid` 1.5. It also documents backwards +compared to its predecessor, :app:`Pyramid` 1.5. It also documents backwards incompatibilities between the two versions and deprecations added to :app:`Pyramid` 1.6, as well as software dependency changes and notable documentation additions. + Backwards Incompatibilities --------------------------- +- IPython and BPython support have been removed from pshell in the core. To + continue using them on Pyramid 1.6+, you must install the binding packages + explicitly. One way to do this is by adding ``pyramid_ipython`` (or + ``pyramid_bpython``) to the ``install_requires`` section of your package's + ``setup.py`` file, then re-running ``setup.py develop``:: + + setup( + #... + install_requires=[ + 'pyramid_ipython', # new dependency + 'pyramid', + #... + ], + ) + - ``request.response`` will no longer be mutated when using the - :func:`~pyramid.renderers.render_to_response` API. It is now necessary - to pass in - a ``response=`` argument to :func:`~pyramid.renderers.render_to_response` if - you wish to supply the renderer with a custom response object for it to - use. If you do not pass one then a response object will be created using the - current response factory. Almost all renderers mutate the - ``request.response`` response object (for example, the JSON renderer sets - ``request.response.content_type`` to ``application/json``). However, when - invoking ``render_to_response`` it is not expected that the response object - being returned would be the same one used later in the request. The response - object returned from ``render_to_response`` is now explicitly different from - ``request.response``. This does not change the API of a renderer. See + :func:`~pyramid.renderers.render_to_response` API. It is now necessary to + pass in a ``response=`` argument to + :func:`~pyramid.renderers.render_to_response` if you wish to supply the + renderer with a custom response object. If you do not pass one, then a + response object will be created using the current response factory. Almost + all renderers mutate the ``request.response`` response object (for example, + the JSON renderer sets ``request.response.content_type`` to + ``application/json``). However, when invoking ``render_to_response``, it is + not expected that the response object being returned would be the same one + used later in the request. The response object returned from + ``render_to_response`` is now explicitly different from ``request.response``. + This does not change the API of a renderer. See https://github.com/Pylons/pyramid/pull/1563 Feature Additions ----------------- -- Cache busting for static assets has been added and is available via a new - argument to :meth:`pyramid.config.Configurator.add_static_view`: - ``cachebust``. Core APIs are shipped for both cache busting via query - strings and path segments and may be extended to fit into custom asset - pipelines. See https://github.com/Pylons/pyramid/pull/1380 and - https://github.com/Pylons/pyramid/pull/1583 +- Python 3.5 and pypy3 compatibility. + +- ``pserve --reload`` will no longer crash on syntax errors. See + https://github.com/Pylons/pyramid/pull/2044 + +- Cache busting for static resources has been added and is available via a new + :meth:`pyramid.config.Configurator.add_cache_buster` API. Core APIs are + shipped for both cache busting via query strings and via asset manifests for + integrating into custom asset pipelines. See + https://github.com/Pylons/pyramid/pull/1380 and + https://github.com/Pylons/pyramid/pull/1583 and + https://github.com/Pylons/pyramid/pull/2171 - Assets can now be overidden by an absolute path on the filesystem when using the :meth:`~pyramid.config.Configurator.override_asset` API. This makes it @@ -47,99 +69,129 @@ Feature Additions ``config.add_static_view('myapp:static', 'static')`` and ``config.override_asset(to_override='myapp:static/', override_with='/abs/path/')``. The ``myapp:static`` asset spec is completely - made up and does not need to exist - it is used for generating urls via - ``request.static_url('myapp:static/foo.png')``. See + made up and does not need to exist—it is used for generating URLs via + ``request.static_url('myapp:static/foo.png')``. See https://github.com/Pylons/pyramid/issues/1252 - Added :meth:`~pyramid.config.Configurator.set_response_factory` and the ``response_factory`` keyword argument to the constructor of :class:`~pyramid.config.Configurator` for defining a factory that will return - a custom ``Response`` class. See https://github.com/Pylons/pyramid/pull/1499 + a custom ``Response`` class. See https://github.com/Pylons/pyramid/pull/1499 -- Add :attr:`pyramid.config.Configurator.root_package` attribute and init - parameter to assist with includeable packages that wish to resolve - resources relative to the package in which the configurator was created. - This is especially useful for addons that need to load asset specs from - settings, in which case it is may be natural for a developer to define - imports or assets relative to the top-level package. - See https://github.com/Pylons/pyramid/pull/1337 +- Added :attr:`pyramid.config.Configurator.root_package` attribute and init + parameter to assist with includible packages that wish to resolve resources + relative to the package in which the configurator was created. This is + especially useful for add-ons that need to load asset specs from settings, in + which case it may be natural for a developer to define imports or assets + relative to the top-level package. See + https://github.com/Pylons/pyramid/pull/1337 - Overall improvments for the ``proutes`` command. Added ``--format`` and ``--glob`` arguments to the command, introduced the ``method`` column for displaying available request methods, and improved the ``view`` - output by showing the module instead of just ``__repr__``. - See https://github.com/Pylons/pyramid/pull/1488 + output by showing the module instead of just ``__repr__``. See + https://github.com/Pylons/pyramid/pull/1488 - ``pserve`` can now take a ``-b`` or ``--browser`` option to open the server URL in a web browser. See https://github.com/Pylons/pyramid/pull/1533 -- Support keyword-only arguments and function annotations in views in - Python 3. See https://github.com/Pylons/pyramid/pull/1556 +- Support keyword-only arguments and function annotations in views in Python 3. + See https://github.com/Pylons/pyramid/pull/1556 - The ``append_slash`` argument of :meth:`~pyramid.config.Configurator.add_notfound_view()` will now accept anything that implements the :class:`~pyramid.interfaces.IResponse` interface and will use that as the response class instead of the default - :class:`~pyramid.httpexceptions.HTTPFound`. See + :class:`~pyramid.httpexceptions.HTTPFound`. See https://github.com/Pylons/pyramid/pull/1610 - The :class:`~pyramid.config.Configurator` has grown the ability to allow - actions to call other actions during a commit-cycle. This enables much more + actions to call other actions during a commit cycle. This enables much more logic to be placed into actions, such as the ability to invoke other actions or group them for improved conflict detection. We have also exposed and - documented the config phases that Pyramid uses in order to further assist in - building conforming addons. See https://github.com/Pylons/pyramid/pull/1513 + documented the configuration phases that Pyramid uses in order to further + assist in building conforming add-ons. See + https://github.com/Pylons/pyramid/pull/1513 - Allow an iterator to be returned from a renderer. Previously it was only - possible to return bytes or unicode. - See https://github.com/Pylons/pyramid/pull/1417 + possible to return bytes or unicode. See + https://github.com/Pylons/pyramid/pull/1417 - Improve robustness to timing attacks in the :class:`~pyramid.authentication.AuthTktCookieHelper` and the :class:`~pyramid.session.SignedCookieSessionFactory` classes by using the - stdlib's ``hmac.compare_digest`` if it is available (such as Python 2.7.7+ and - 3.3+). See https://github.com/Pylons/pyramid/pull/1457 + stdlib's ``hmac.compare_digest`` if it is available (such as Python 2.7.7+ + and 3.3+). See https://github.com/Pylons/pyramid/pull/1457 -- Improve the readability of the ``pcreate`` shell script output. - See https://github.com/Pylons/pyramid/pull/1453 +- Improve the readability of the ``pcreate`` shell script output. See + https://github.com/Pylons/pyramid/pull/1453 -- Make it simple to define notfound and forbidden views that wish to use the - default exception-response view but with altered predicates and other - configuration options. The ``view`` argument is now optional in +- Make it simple to define ``notfound`` and ``forbidden`` views that wish to + use the default exception-response view, but with altered predicates and + other configuration options. The ``view`` argument is now optional in :meth:`~pyramid.config.Configurator.add_notfound_view` and :meth:`~pyramid.config.Configurator.add_forbidden_view` See https://github.com/Pylons/pyramid/issues/494 - The ``pshell`` script will now load a ``PYTHONSTARTUP`` file if one is - defined in the environment prior to launching the interpreter. - See https://github.com/Pylons/pyramid/pull/1448 + defined in the environment prior to launching the interpreter. See + https://github.com/Pylons/pyramid/pull/1448 -- Add new HTTP exception objects for status codes - ``428 Precondition Required``, ``429 Too Many Requests`` and - ``431 Request Header Fields Too Large`` in ``pyramid.httpexceptions``. - See https://github.com/Pylons/pyramid/pull/1372/files +- Add new HTTP exception objects for status codes ``428 Precondition + Required``, ``429 Too Many Requests`` and ``431 Request Header Fields Too + Large`` in ``pyramid.httpexceptions``. See + https://github.com/Pylons/pyramid/pull/1372/files - ``pcreate`` when run without a scaffold argument will now print information - on the missing flag, as well as a list of available scaffolds. See + on the missing flag, as well as a list of available scaffolds. See https://github.com/Pylons/pyramid/pull/1566 and https://github.com/Pylons/pyramid/issues/1297 +- ``pcreate`` will now ask for confirmation if invoked with an argument for a + project name that already exists or is importable in the current environment. + See https://github.com/Pylons/pyramid/issues/1357 and + https://github.com/Pylons/pyramid/pull/1837 + - Add :func:`pyramid.request.apply_request_extensions` function which can be used in testing to apply any request extensions configured via ``config.add_request_method``. Previously it was only possible to test the - extensions by going through Pyramid's router. See + extensions by going through Pyramid's router. See https://github.com/Pylons/pyramid/pull/1581 - - Make it possible to subclass ``pyramid.request.Request`` and also use - ``pyramid.request.Request.add_request.method``. See + ``pyramid.request.Request.add_request.method``. See https://github.com/Pylons/pyramid/issues/1529 +- Additional shells for ``pshell`` can now be registered as entry points. See + https://github.com/Pylons/pyramid/pull/1891 and + https://github.com/Pylons/pyramid/pull/2012 + +- The variables injected into ``pshell`` are now displayed with their + docstrings instead of the default ``str(obj)`` when possible. See + https://github.com/Pylons/pyramid/pull/1929 + + Deprecations ------------ +- The ``pserve`` command's daemonization features, as well as + ``--monitor-restart``, have been deprecated. This includes the + ``[start,stop,restart,status]`` subcommands, as well as the ``--daemon``, + ``--stop-daemon``, ``--pid-file``, ``--status``, ``--user``, and ``--group`` + flags. See https://github.com/Pylons/pyramid/pull/2120 and + https://github.com/Pylons/pyramid/pull/2189 and + https://github.com/Pylons/pyramid/pull/1641 + + Please use a real process manager in the future instead of relying on + ``pserve`` to daemonize itself. Many options exist, including your operating + system's services, such as Systemd or Upstart, as well as Python-based + solutions like Circus and Supervisor. + + See https://github.com/Pylons/pyramid/pull/1641 and + https://github.com/Pylons/pyramid/pull/2120 + - The ``principal`` argument to :func:`pyramid.security.remember` was renamed - to ``userid``. Using ``principal`` as the argument name still works and will + to ``userid``. Using ``principal`` as the argument name still works and will continue to work for the next few releases, but a deprecation warning is printed. @@ -150,21 +202,35 @@ Scaffolding Enhancements - Added line numbers to the log formatters in the scaffolds to assist with debugging. See https://github.com/Pylons/pyramid/pull/1326 -- Update scaffold generating machinery to return the version of pyramid and - pyramid docs for use in scaffolds. Updated ``starter``, ``alchemy`` and - ``zodb`` templates to have links to correctly versioned documentation and - reflect which pyramid was used to generate the scaffold. +- Updated scaffold generating machinery to return the version of :app:`Pyramid` + and its documentation for use in scaffolds. Updated ``starter``, ``alchemy`` + and ``zodb`` templates to have links to correctly versioned documentation, + and to reflect which :app:`Pyramid` was used to generate the scaffold. + +- Removed non-ASCII copyright symbol from templates, as this was causing the + scaffolds to fail for project generation. -- Removed non-ascii copyright symbol from templates, as this was - causing the scaffolds to fail for project generation. Documentation Enhancements -------------------------- -- Removed logging configuration from Quick Tutorial ini files except for - scaffolding- and logging-related chapters to avoid needing to explain it too +- Removed logging configuration from Quick Tutorial ``ini`` files, except for + scaffolding- and logging-related chapters, to avoid needing to explain it too early. -- Improve and clarify the documentation on what Pyramid defines as a - ``principal`` and a ``userid`` in its security APIs. - See https://github.com/Pylons/pyramid/pull/1399 +- Improve and clarify the documentation on what :app:`Pyramid` defines as a + ``principal`` and a ``userid`` in its security APIs. See + https://github.com/Pylons/pyramid/pull/1399 + +- Moved the documentation for ``accept`` on + :meth:`pyramid.config.Configurator.add_view` to no longer be part of the + predicate list. See https://github.com/Pylons/pyramid/issues/1391 for a bug + report stating ``not_`` was failing on ``accept``. Discussion with @mcdonc + led to the conclusion that it should not be documented as a predicate. + See https://github.com/Pylons/pyramid/pull/1487 for this PR. + +- Clarify a previously-implied detail of the ``ISession.invalidate`` API + documentation. + +- Add documentation of command line programs (``p*`` scripts). See + https://github.com/Pylons/pyramid/pull/2191 -- cgit v1.2.3 From 6f63aa652c2210a5f084644afad1bb44fa7494c2 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sun, 3 Jan 2016 00:41:44 -0800 Subject: attempt to install a patched release of sphinx 1.3.3. See https://github.com/sphinx-doc/sphinx/issues/2189 --- rtd.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/rtd.txt b/rtd.txt index 142b6ca35..62b070a83 100644 --- a/rtd.txt +++ b/rtd.txt @@ -1 +1,2 @@ -e .[docs] +git+https://github.com/sphinx-doc/sphinx.git@222a51e0cf0080b74f3a8a233590c707ee4eec32#egg=sphinx-222a51e0cf0080b74f3a8a233590c707ee4eec32 -- cgit v1.2.3 From 0713ac1b2b346ea5adb88e3b66381393368434af Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sun, 3 Jan 2016 01:08:00 -0800 Subject: undo patch of sphinx --- rtd.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/rtd.txt b/rtd.txt index 62b070a83..142b6ca35 100644 --- a/rtd.txt +++ b/rtd.txt @@ -1,2 +1 @@ -e .[docs] -git+https://github.com/sphinx-doc/sphinx.git@222a51e0cf0080b74f3a8a233590c707ee4eec32#egg=sphinx-222a51e0cf0080b74f3a8a233590c707ee4eec32 -- cgit v1.2.3 From 8a80b1094cf0ba762b30a9bae56831d4daf69e3c Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Tue, 5 Jan 2016 04:33:29 -0800 Subject: update links to tutorials in cookbook --- docs/index.rst | 6 +++--- docs/quick_tour.rst | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 8c8a0a18d..9a34f088b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -39,9 +39,9 @@ speed right away. format, with somewhat deeper treatment of each topic and with working code. * Like learning by example? Visit the official :ref:`html_tutorials` as well as - the community-contributed :ref:`Pyramid tutorials - `, which include a :ref:`Todo List Application - in One File `. + the community-contributed :ref:`Pyramid Tutorials + ` and :ref:`Pyramid Cookbook + `. * For help getting Pyramid set up, try :ref:`installing_chapter`. diff --git a/docs/quick_tour.rst b/docs/quick_tour.rst index be5be2e36..56ca53a69 100644 --- a/docs/quick_tour.rst +++ b/docs/quick_tour.rst @@ -98,7 +98,7 @@ one that we will revisit regurlarly in this *Quick Tour*. .. seealso:: See also: :ref:`Quick Tutorial Hello World `, :ref:`firstapp_chapter`, and - :ref:`Single File Tasks tutorial ` + :ref:`Todo List Application in One File ` Handling web requests and responses =================================== -- cgit v1.2.3 From 791d5947e37a52e9d34355a9c4fc9d9f1cd359e4 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Tue, 5 Jan 2016 04:47:30 -0800 Subject: remove unused "awesome" directory from quick_tour. it had references to pylonshq.com, which no longer exists. --- docs/quick_tour/awesome/CHANGES.txt | 4 - docs/quick_tour/awesome/MANIFEST.in | 2 - docs/quick_tour/awesome/README.txt | 4 - docs/quick_tour/awesome/awesome/__init__.py | 23 ------ docs/quick_tour/awesome/awesome/locale/awesome.pot | 21 ----- .../awesome/locale/de/LC_MESSAGES/awesome.mo | Bin 460 -> 0 bytes .../awesome/locale/de/LC_MESSAGES/awesome.po | 21 ----- .../awesome/locale/fr/LC_MESSAGES/awesome.mo | Bin 461 -> 0 bytes .../awesome/locale/fr/LC_MESSAGES/awesome.po | 21 ----- docs/quick_tour/awesome/awesome/models.py | 8 -- docs/quick_tour/awesome/awesome/static/favicon.ico | Bin 1406 -> 0 bytes docs/quick_tour/awesome/awesome/static/logo.png | Bin 6641 -> 0 bytes docs/quick_tour/awesome/awesome/static/pylons.css | 73 ----------------- .../awesome/awesome/templates/mytemplate.jinja2 | 87 --------------------- docs/quick_tour/awesome/awesome/tests.py | 21 ----- docs/quick_tour/awesome/awesome/views.py | 6 -- docs/quick_tour/awesome/development.ini | 49 ------------ docs/quick_tour/awesome/message-extraction.ini | 3 - docs/quick_tour/awesome/setup.py | 36 --------- 19 files changed, 379 deletions(-) delete mode 100644 docs/quick_tour/awesome/CHANGES.txt delete mode 100644 docs/quick_tour/awesome/MANIFEST.in delete mode 100644 docs/quick_tour/awesome/README.txt delete mode 100644 docs/quick_tour/awesome/awesome/__init__.py delete mode 100644 docs/quick_tour/awesome/awesome/locale/awesome.pot delete mode 100644 docs/quick_tour/awesome/awesome/locale/de/LC_MESSAGES/awesome.mo delete mode 100644 docs/quick_tour/awesome/awesome/locale/de/LC_MESSAGES/awesome.po delete mode 100644 docs/quick_tour/awesome/awesome/locale/fr/LC_MESSAGES/awesome.mo delete mode 100644 docs/quick_tour/awesome/awesome/locale/fr/LC_MESSAGES/awesome.po delete mode 100644 docs/quick_tour/awesome/awesome/models.py delete mode 100644 docs/quick_tour/awesome/awesome/static/favicon.ico delete mode 100644 docs/quick_tour/awesome/awesome/static/logo.png delete mode 100644 docs/quick_tour/awesome/awesome/static/pylons.css delete mode 100644 docs/quick_tour/awesome/awesome/templates/mytemplate.jinja2 delete mode 100644 docs/quick_tour/awesome/awesome/tests.py delete mode 100644 docs/quick_tour/awesome/awesome/views.py delete mode 100644 docs/quick_tour/awesome/development.ini delete mode 100644 docs/quick_tour/awesome/message-extraction.ini delete mode 100644 docs/quick_tour/awesome/setup.py diff --git a/docs/quick_tour/awesome/CHANGES.txt b/docs/quick_tour/awesome/CHANGES.txt deleted file mode 100644 index ffa255da8..000000000 --- a/docs/quick_tour/awesome/CHANGES.txt +++ /dev/null @@ -1,4 +0,0 @@ -0.0 ---- - -- Initial version diff --git a/docs/quick_tour/awesome/MANIFEST.in b/docs/quick_tour/awesome/MANIFEST.in deleted file mode 100644 index e78395da8..000000000 --- a/docs/quick_tour/awesome/MANIFEST.in +++ /dev/null @@ -1,2 +0,0 @@ -include *.txt *.ini *.cfg *.rst -recursive-include awesome *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml diff --git a/docs/quick_tour/awesome/README.txt b/docs/quick_tour/awesome/README.txt deleted file mode 100644 index f695286d9..000000000 --- a/docs/quick_tour/awesome/README.txt +++ /dev/null @@ -1,4 +0,0 @@ -awesome README - - - diff --git a/docs/quick_tour/awesome/awesome/__init__.py b/docs/quick_tour/awesome/awesome/__init__.py deleted file mode 100644 index 408033997..000000000 --- a/docs/quick_tour/awesome/awesome/__init__.py +++ /dev/null @@ -1,23 +0,0 @@ -from pyramid.config import Configurator -from pyramid_jinja2 import renderer_factory -from awesome.models import get_root - -def main(global_config, **settings): - """ This function returns a WSGI application. - - It is usually called by the PasteDeploy framework during - ``paster serve``. - """ - settings = dict(settings) - settings.setdefault('jinja2.i18n.domain', 'awesome') - - config = Configurator(root_factory=get_root, settings=settings) - config.add_translation_dirs('locale/') - config.include('pyramid_jinja2') - - config.add_static_view('static', 'static') - config.add_view('awesome.views.my_view', - context='awesome.models.MyModel', - renderer="mytemplate.jinja2") - - return config.make_wsgi_app() diff --git a/docs/quick_tour/awesome/awesome/locale/awesome.pot b/docs/quick_tour/awesome/awesome/locale/awesome.pot deleted file mode 100644 index 9c9460cb2..000000000 --- a/docs/quick_tour/awesome/awesome/locale/awesome.pot +++ /dev/null @@ -1,21 +0,0 @@ -# Translations template for PROJECT. -# Copyright (C) 2011 ORGANIZATION -# This file is distributed under the same license as the PROJECT project. -# FIRST AUTHOR , 2011. -# -#, fuzzy -msgid "" -msgstr "" -"Project-Id-Version: PROJECT VERSION\n" -"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2011-05-12 09:14-0330\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME \n" -"Language-Team: LANGUAGE \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=utf-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 0.9.6\n" - -msgid "Hello!" -msgstr "" diff --git a/docs/quick_tour/awesome/awesome/locale/de/LC_MESSAGES/awesome.mo b/docs/quick_tour/awesome/awesome/locale/de/LC_MESSAGES/awesome.mo deleted file mode 100644 index 40bf0c271..000000000 Binary files a/docs/quick_tour/awesome/awesome/locale/de/LC_MESSAGES/awesome.mo and /dev/null differ diff --git a/docs/quick_tour/awesome/awesome/locale/de/LC_MESSAGES/awesome.po b/docs/quick_tour/awesome/awesome/locale/de/LC_MESSAGES/awesome.po deleted file mode 100644 index 0df243dba..000000000 --- a/docs/quick_tour/awesome/awesome/locale/de/LC_MESSAGES/awesome.po +++ /dev/null @@ -1,21 +0,0 @@ -# Translations template for PROJECT. -# Copyright (C) 2011 ORGANIZATION -# This file is distributed under the same license as the PROJECT project. -# FIRST AUTHOR , 2011. -# -#, fuzzy -msgid "" -msgstr "" -"Project-Id-Version: PROJECT VERSION\n" -"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2011-05-12 09:14-0330\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME \n" -"Language-Team: LANGUAGE \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=utf-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 0.9.6\n" - -msgid "Hello!" -msgstr "Hallo!" diff --git a/docs/quick_tour/awesome/awesome/locale/fr/LC_MESSAGES/awesome.mo b/docs/quick_tour/awesome/awesome/locale/fr/LC_MESSAGES/awesome.mo deleted file mode 100644 index 4fc438bfe..000000000 Binary files a/docs/quick_tour/awesome/awesome/locale/fr/LC_MESSAGES/awesome.mo and /dev/null differ diff --git a/docs/quick_tour/awesome/awesome/locale/fr/LC_MESSAGES/awesome.po b/docs/quick_tour/awesome/awesome/locale/fr/LC_MESSAGES/awesome.po deleted file mode 100644 index dc0aae5d7..000000000 --- a/docs/quick_tour/awesome/awesome/locale/fr/LC_MESSAGES/awesome.po +++ /dev/null @@ -1,21 +0,0 @@ -# Translations template for PROJECT. -# Copyright (C) 2011 ORGANIZATION -# This file is distributed under the same license as the PROJECT project. -# FIRST AUTHOR , 2011. -# -#, fuzzy -msgid "" -msgstr "" -"Project-Id-Version: PROJECT VERSION\n" -"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2011-05-12 09:14-0330\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME \n" -"Language-Team: LANGUAGE \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=utf-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 0.9.6\n" - -msgid "Hello!" -msgstr "Bonjour!" diff --git a/docs/quick_tour/awesome/awesome/models.py b/docs/quick_tour/awesome/awesome/models.py deleted file mode 100644 index edd361c9c..000000000 --- a/docs/quick_tour/awesome/awesome/models.py +++ /dev/null @@ -1,8 +0,0 @@ -class MyModel(object): - pass - -root = MyModel() - - -def get_root(request): - return root diff --git a/docs/quick_tour/awesome/awesome/static/favicon.ico b/docs/quick_tour/awesome/awesome/static/favicon.ico deleted file mode 100644 index 71f837c9e..000000000 Binary files a/docs/quick_tour/awesome/awesome/static/favicon.ico and /dev/null differ diff --git a/docs/quick_tour/awesome/awesome/static/logo.png b/docs/quick_tour/awesome/awesome/static/logo.png deleted file mode 100644 index 88f5d9865..000000000 Binary files a/docs/quick_tour/awesome/awesome/static/logo.png and /dev/null differ diff --git a/docs/quick_tour/awesome/awesome/static/pylons.css b/docs/quick_tour/awesome/awesome/static/pylons.css deleted file mode 100644 index 42e2e320e..000000000 --- a/docs/quick_tour/awesome/awesome/static/pylons.css +++ /dev/null @@ -1,73 +0,0 @@ -html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,font,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td{margin:0;padding:0;border:0;outline:0;font-size:100%;/* 16px */ -vertical-align:baseline;background:transparent;} -body{line-height:1;} -ol,ul{list-style:none;} -blockquote,q{quotes:none;} -blockquote:before,blockquote:after,q:before,q:after{content:'';content:none;} -/* remember to define focus styles! */ -:focus{outline:0;} -/* remember to highlight inserts somehow! */ -ins{text-decoration:none;} -del{text-decoration:line-through;} -/* tables still need 'cellspacing="0"' in the markup */ -table{border-collapse:collapse;border-spacing:0;} -/* restyling */ -sub{vertical-align:sub;font-size:smaller;line-height:normal;} -sup{vertical-align:super;font-size:smaller;line-height:normal;} -/* lists */ -ul,menu,dir{display:block;list-style-type:disc;margin:1em 0;padding-left:40px;} -ol{display:block;list-style-type:decimal-leading-zero;margin:1em 0;padding-left:40px;} -li{display:list-item;} -/* nested lists have no top/bottom margins */ -ul ul,ul ol,ul dir,ul menu,ul dl,ol ul,ol ol,ol dir,ol menu,ol dl,dir ul,dir ol,dir dir,dir menu,dir dl,menu ul,menu ol,menu dir,menu menu,menu dl,dl ul,dl ol,dl dir,dl menu,dl dl{margin-top:0;margin-bottom:0;} -/* 2 deep unordered lists use a circle */ -ol ul,ul ul,menu ul,dir ul,ol menu,ul menu,menu menu,dir menu,ol dir,ul dir,menu dir,dir dir{list-style-type:circle;} -/* 3 deep (or more) unordered lists use a square */ -ol ol ul,ol ul ul,ol menu ul,ol dir ul,ol ol menu,ol ul menu,ol menu menu,ol dir menu,ol ol dir,ol ul dir,ol menu dir,ol dir dir,ul ol ul,ul ul ul,ul menu ul,ul dir ul,ul ol menu,ul ul menu,ul menu menu,ul dir menu,ul ol dir,ul ul dir,ul menu dir,ul dir dir,menu ol ul,menu ul ul,menu menu ul,menu dir ul,menu ol menu,menu ul menu,menu menu menu,menu dir menu,menu ol dir,menu ul dir,menu menu dir,menu dir dir,dir ol ul,dir ul ul,dir menu ul,dir dir ul,dir ol menu,dir ul menu,dir menu menu,dir dir menu,dir ol dir,dir ul dir,dir menu dir,dir dir dir{list-style-type:square;} -.hidden{display:none;} -p{line-height:1.5em;} -h1{font-size:1.75em;/* 28px */ -line-height:1.7em;font-family:helvetica,verdana;} -h2{font-size:1.5em;/* 24px */ -line-height:1.7em;font-family:helvetica,verdana;} -h3{font-size:1.25em;/* 20px */ -line-height:1.7em;font-family:helvetica,verdana;} -h4{font-size:1em;line-height:1.7em;font-family:helvetica,verdana;} -html,body{width:100%;height:100%;} -body{margin:0;padding:0;background-color:#ffffff;position:relative;font:16px/24px "Nobile","Lucida Grande",Lucida,Verdana,sans-serif;} -a{color:#1b61d6;text-decoration:none;} -a:hover{color:#e88f00;text-decoration:underline;} -body h1, -body h2, -body h3, -body h4, -body h5, -body h6{font-family:"Nobile","Lucida Grande",Lucida,Verdana,sans-serif;font-weight:normal;color:#144fb2;font-style:normal;} -#wrap {min-height: 100%;} -#header,#footer{width:100%;color:#ffffff;height:40px;position:absolute;text-align:center;line-height:40px;overflow:hidden;font-size:12px;} -#header{background-color:#e88f00;top:0;font-size:14px;} -#footer{background-color:#000000;bottom:0;position: relative;margin-top:-40px;clear:both;} -.header,.footer{width:700px;margin-right:auto;margin-left:auto;} -.wrapper{width:100%} -#top,#bottom{width:100%;} -#top{color:#888;background-color:#eee;height:300px;border-bottom:2px solid #ddd;} -#bottom{color:#222;background-color:#ffffff;overflow:auto;padding-bottom:80px;} -.top,.bottom{width:700px;margin-right:auto;margin-left:auto;} -.top{padding-top:100px;} -.app-welcome{margin-top:25px;} -.app-name{color:#000000;font-weight:bold;} -.bottom{padding-top:50px;} -#left{width:325px;float:left;padding-right:25px;} -#right{width:325px;float:right;padding-left:25px;} -.align-left{text-align:left;} -.align-right{text-align:right;} -.align-center{text-align:center;} -ul.links{margin:0;padding:0;} -ul.links li{list-style-type:none;font-size:14px;} -form{border-style:none;} -fieldset{border-style:none;} -input{color:#222;border:1px solid #ccc;font-family:sans-serif;font-size:12px;line-height:16px;} -input[type=text]{} -input[type=submit]{background-color:#ddd;font-weight:bold;} -/*Opera Fix*/ -body:before {content:"";height:100%;float:left;width:0;margin-top:-32767px;} diff --git a/docs/quick_tour/awesome/awesome/templates/mytemplate.jinja2 b/docs/quick_tour/awesome/awesome/templates/mytemplate.jinja2 deleted file mode 100644 index 8bf676041..000000000 --- a/docs/quick_tour/awesome/awesome/templates/mytemplate.jinja2 +++ /dev/null @@ -1,87 +0,0 @@ - - - - The Pyramid Web Framework - - - - - - - - - - -
- -
-
- Logo -

- Welcome to {{project}}, an application generated by
- the Pyramid web framework. -

-
-
-
-
-

{% trans %}Hello!{% endtrans %}

-

Request performed with {{ request.locale_name }} locale.

-
-
-
-

Search Pyramid documentation

-
- - -
-
- -
-
-
- - - diff --git a/docs/quick_tour/awesome/awesome/tests.py b/docs/quick_tour/awesome/awesome/tests.py deleted file mode 100644 index ac222e25b..000000000 --- a/docs/quick_tour/awesome/awesome/tests.py +++ /dev/null @@ -1,21 +0,0 @@ -import unittest -from pyramid import testing -from pyramid.i18n import TranslationStringFactory - -_ = TranslationStringFactory('awesome') - - -class ViewTests(unittest.TestCase): - - def setUp(self): - testing.setUp() - - def tearDown(self): - testing.tearDown() - - def test_my_view(self): - from awesome.views import my_view - request = testing.DummyRequest() - response = my_view(request) - self.assertEqual(response['project'], 'awesome') - diff --git a/docs/quick_tour/awesome/awesome/views.py b/docs/quick_tour/awesome/awesome/views.py deleted file mode 100644 index 67b282f87..000000000 --- a/docs/quick_tour/awesome/awesome/views.py +++ /dev/null @@ -1,6 +0,0 @@ -from pyramid.i18n import TranslationStringFactory - -_ = TranslationStringFactory('awesome') - -def my_view(request): - return {'project':'awesome'} diff --git a/docs/quick_tour/awesome/development.ini b/docs/quick_tour/awesome/development.ini deleted file mode 100644 index a473d32f1..000000000 --- a/docs/quick_tour/awesome/development.ini +++ /dev/null @@ -1,49 +0,0 @@ -[app:awesome] -use = egg:awesome -reload_templates = true -debug_authorization = false -debug_notfound = false -debug_routematch = false -debug_templates = true -default_locale_name = en -jinja2.directories = awesome:templates - -[pipeline:main] -pipeline = - awesome - -[server:main] -use = egg:pyramid#wsgiref -host = 0.0.0.0 -port = 6543 - -# Begin logging configuration - -[loggers] -keys = root, awesome - -[handlers] -keys = console - -[formatters] -keys = generic - -[logger_root] -level = INFO -handlers = console - -[logger_awesome] -level = DEBUG -handlers = -qualname = awesome - -[handler_console] -class = StreamHandler -args = (sys.stderr,) -level = NOTSET -formatter = generic - -[formatter_generic] -format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s - -# End logging configuration diff --git a/docs/quick_tour/awesome/message-extraction.ini b/docs/quick_tour/awesome/message-extraction.ini deleted file mode 100644 index 0c3d54bc1..000000000 --- a/docs/quick_tour/awesome/message-extraction.ini +++ /dev/null @@ -1,3 +0,0 @@ -[python: **.py] -[jinja2: **.jinja2] -encoding = utf-8 diff --git a/docs/quick_tour/awesome/setup.py b/docs/quick_tour/awesome/setup.py deleted file mode 100644 index 32d666317..000000000 --- a/docs/quick_tour/awesome/setup.py +++ /dev/null @@ -1,36 +0,0 @@ -import os - -from setuptools import setup, find_packages - -here = os.path.abspath(os.path.dirname(__file__)) -README = open(os.path.join(here, 'README.txt')).read() -CHANGES = open(os.path.join(here, 'CHANGES.txt')).read() - -requires=['pyramid>=1.0.2', 'pyramid_jinja2'] - -setup(name='awesome', - version='0.0', - description='awesome', - long_description=README + '\n\n' + CHANGES, - classifiers=[ - "Programming Language :: Python", - "Framework :: Pylons", - "Topic :: Internet :: WWW/HTTP", - "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", - ], - author='', - author_email='', - url='', - keywords='web pyramid pylons', - packages=find_packages(), - include_package_data=True, - zip_safe=False, - install_requires=requires, - tests_require=requires, - test_suite="awesome", - entry_points = """\ - [paste.app_factory] - main = awesome:main - """, - paster_plugins=['pyramid'], - ) -- cgit v1.2.3 From f8309dd390a2045e3ccf8a1d029b6ff8bb0c0a23 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Thu, 7 Jan 2016 00:13:27 -0800 Subject: add trypyramid.com to update for releases --- RELEASING.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/RELEASING.txt b/RELEASING.txt index fa4ebab5b..61420ce8b 100644 --- a/RELEASING.txt +++ b/RELEASING.txt @@ -55,6 +55,9 @@ Releasing Pyramid - Edit Pylons/pylonshq/templates/home/inside.rst for major updates only. +- Edit Pylons/trypyramid.com/src/templates/resources.html for major updates + only. + - Edit Pylons/pylonsrtd/pylonsrtd/docs/pyramid.rst for all updates. - Edit `http://wiki.python.org/moin/WebFrameworks -- cgit v1.2.3 From 6860b286dcf604eaef4940ab8e8e10e8cbcc1a58 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Fri, 8 Jan 2016 06:22:05 -0800 Subject: update glossary with cookbook rst syntax. closes #2215 --- docs/glossary.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/glossary.rst b/docs/glossary.rst index 60e861597..60f03f000 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -960,10 +960,10 @@ Glossary users transition from Pylons and those preferring a more Pylons-like API. The scaffold has been retired but the demo plays a similar role. - Pyramid Cookbook - Additional documentation for Pyramid which presents topical, - practical uses of Pyramid: - http://docs.pylonsproject.org/projects/pyramid_cookbook/en/latest. + Pyramid Community Cookbook + Additional, community-based documentation for Pyramid which presents + topical, practical uses of Pyramid: + :ref:`Pyramid Community Cookbook ` distutils The standard system for packaging and distributing Python packages. See -- cgit v1.2.3 From 99098dddf8baa23727f410f2fa6b543c0641fa21 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Fri, 8 Jan 2016 06:32:19 -0800 Subject: revert glossary entry name for tox on 3.5. I'll get this on a massive update on another pass. See https://github.com/Pylons/pyramid_cookbook/issues/155 --- docs/glossary.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/glossary.rst b/docs/glossary.rst index 60f03f000..82423a59d 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -960,7 +960,7 @@ Glossary users transition from Pylons and those preferring a more Pylons-like API. The scaffold has been retired but the demo plays a similar role. - Pyramid Community Cookbook + Pyramid Cookbook Additional, community-based documentation for Pyramid which presents topical, practical uses of Pyramid: :ref:`Pyramid Community Cookbook ` -- cgit v1.2.3 From 2b80947c9b032896b917535db5cf4dbe125e75b2 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sat, 9 Jan 2016 23:13:53 -0800 Subject: Minor grammar, rewrap to 79 columns, in Ameliorations section --- docs/designdefense.rst | 91 ++++++++++++++++++++++++-------------------------- 1 file changed, 44 insertions(+), 47 deletions(-) diff --git a/docs/designdefense.rst b/docs/designdefense.rst index bfde25246..2ce9dc12c 100644 --- a/docs/designdefense.rst +++ b/docs/designdefense.rst @@ -175,11 +175,11 @@ Ameliorations +++++++++++++ First, the primary amelioration: :app:`Pyramid` *does not expect application -developers to understand ZCA concepts or any of its APIs*. If an -*application* developer needs to understand a ZCA concept or API during the -creation of a :app:`Pyramid` application, we've failed on some axis. +developers to understand ZCA concepts or any of its APIs*. If an *application* +developer needs to understand a ZCA concept or API during the creation of a +:app:`Pyramid` application, we've failed on some axis. -Instead, the framework hides the presence of the ZCA registry behind +Instead the framework hides the presence of the ZCA registry behind special-purpose API functions that *do* use ZCA APIs. Take for example the ``pyramid.security.authenticated_userid`` function, which returns the userid present in the current request or ``None`` if no userid is present in the @@ -191,10 +191,9 @@ current request. The application developer calls it like so: from pyramid.security import authenticated_userid userid = authenticated_userid(request) -He now has the current user id. +They now have the current user id. -Under its hood however, the implementation of ``authenticated_userid`` -is this: +Under its hood however, the implementation of ``authenticated_userid`` is this: .. code-block:: python :linenos: @@ -211,58 +210,56 @@ is this: return policy.authenticated_userid(request) Using such wrappers, we strive to always hide the ZCA API from application -developers. Application developers should just never know about the ZCA API: -they should call a Python function with some object germane to the domain as -an argument, and it should return a result. A corollary that follows is -that any reader of an application that has been written using :app:`Pyramid` -needn't understand the ZCA API either. +developers. Application developers should just never know about the ZCA API; +they should call a Python function with some object germane to the domain as an +argument, and it should return a result. A corollary that follows is that any +reader of an application that has been written using :app:`Pyramid` needn't +understand the ZCA API either. Hiding the ZCA API from application developers and code readers is a form of enhancing domain specificity. No application developer wants to need to -understand the small, detailed mechanics of how a web framework does its -thing. People want to deal in concepts that are closer to the domain they're -working in: for example, web developers want to know about *users*, not -*utilities*. :app:`Pyramid` uses the ZCA as an implementation detail, not as -a feature which is exposed to end users. +understand the small, detailed mechanics of how a web framework does its thing. +People want to deal in concepts that are closer to the domain they're working +in. For example, web developers want to know about *users*, not *utilities*. +:app:`Pyramid` uses the ZCA as an implementation detail, not as a feature which +is exposed to end users. However, unlike application developers, *framework developers*, including people who want to override :app:`Pyramid` functionality via preordained -framework plugpoints like traversal or view lookup *must* understand the ZCA +framework plugpoints like traversal or view lookup, *must* understand the ZCA registry API. :app:`Pyramid` framework developers were so concerned about conceptual load -issues of the ZCA registry API for framework developers that a `replacement -registry implementation `_ -named :mod:`repoze.component` was actually developed. Though this package -has a registry implementation which is fully functional and well-tested, and -its API is much nicer than the ZCA registry API, work on it was largely -abandoned and it is not used in :app:`Pyramid`. We continued to use a ZCA -registry within :app:`Pyramid` because it ultimately proved a better fit. +issues of the ZCA registry API that a `replacement registry implementation +`_ named :mod:`repoze.component` +was actually developed. Though this package has a registry implementation +which is fully functional and well-tested, and its API is much nicer than the +ZCA registry API, work on it was largely abandoned, and it is not used in +:app:`Pyramid`. We continued to use a ZCA registry within :app:`Pyramid` +because it ultimately proved a better fit. .. note:: - We continued using ZCA registry rather than disusing it in - favor of using the registry implementation in - :mod:`repoze.component` largely because the ZCA concept of - interfaces provides for use of an interface hierarchy, which is - useful in a lot of scenarios (such as context type inheritance). - Coming up with a marker type that was something like an interface - that allowed for this functionality seemed like it was just - reinventing the wheel. - -Making framework developers and extenders understand the ZCA registry API is -a trade-off. We (the :app:`Pyramid` developers) like the features that the -ZCA registry gives us, and we have long-ago borne the weight of understanding -what it does and how it works. The authors of :app:`Pyramid` understand the -ZCA deeply and can read code that uses it as easily as any other code. + We continued using ZCA registry rather than disusing it in favor of using + the registry implementation in :mod:`repoze.component` largely because the + ZCA concept of interfaces provides for use of an interface hierarchy, which + is useful in a lot of scenarios (such as context type inheritance). Coming + up with a marker type that was something like an interface that allowed for + this functionality seemed like it was just reinventing the wheel. + +Making framework developers and extenders understand the ZCA registry API is a +trade-off. We (the :app:`Pyramid` developers) like the features that the ZCA +registry gives us, and we have long-ago borne the weight of understanding what +it does and how it works. The authors of :app:`Pyramid` understand the ZCA +deeply and can read code that uses it as easily as any other code. But we recognize that developers who might want to extend the framework are not -as comfortable with the ZCA registry API as the original developers are with -it. So, for the purposes of being kind to third-party :app:`Pyramid` -framework developers in, we've drawn some lines in the sand. +as comfortable with the ZCA registry API as the original developers. So for +the purpose of being kind to third-party :app:`Pyramid` framework developers, +we've drawn some lines in the sand. -In all core code, We've made use of ZCA global API functions such as -``zope.component.getUtility`` and ``zope.component.getAdapter`` the exception +In all core code, we've made use of ZCA global API functions, such as +``zope.component.getUtility`` and ``zope.component.getAdapter``, the exception instead of the rule. So instead of: .. code-block:: python @@ -282,9 +279,9 @@ instead of the rule. So instead of: registry = get_current_registry() policy = registry.getUtility(IAuthenticationPolicy) -While the latter is more verbose, it also arguably makes it more obvious -what's going on. All of the :app:`Pyramid` core code uses this pattern -rather than the ZCA global API. +While the latter is more verbose, it also arguably makes it more obvious what's +going on. All of the :app:`Pyramid` core code uses this pattern rather than +the ZCA global API. Rationale +++++++++ -- cgit v1.2.3 From 362e4dd4c131ba19bdcc3f14df8b26f802806544 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Tue, 12 Jan 2016 07:49:55 -0800 Subject: bump Sphinx to 1.3.4. Fixes #2209. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 9bdfcd90e..daccd3258 100644 --- a/setup.py +++ b/setup.py @@ -56,7 +56,7 @@ if not PY3: tests_require.append('zope.component>=3.11.0') docs_extras = [ - 'Sphinx >= 1.3.1', + 'Sphinx >= 1.3.4', 'docutils', 'repoze.sphinx.autointerface', 'pylons_sphinx_latesturl', -- cgit v1.2.3 From b77ce845c66b271aba0909b12ccef06d6cc45e07 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Tue, 12 Jan 2016 08:18:06 -0800 Subject: use lower case "s" for sphinx --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index daccd3258..f7c0fe85a 100644 --- a/setup.py +++ b/setup.py @@ -56,7 +56,7 @@ if not PY3: tests_require.append('zope.component>=3.11.0') docs_extras = [ - 'Sphinx >= 1.3.4', + 'sphinx >= 1.3.4', 'docutils', 'repoze.sphinx.autointerface', 'pylons_sphinx_latesturl', -- cgit v1.2.3 From 5c5035530e4e41fdb5fdc9a5767bcd7a9c58413f Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Tue, 12 Jan 2016 08:23:29 -0800 Subject: Try upper case "S" for Sphinx --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f7c0fe85a..daccd3258 100644 --- a/setup.py +++ b/setup.py @@ -56,7 +56,7 @@ if not PY3: tests_require.append('zope.component>=3.11.0') docs_extras = [ - 'sphinx >= 1.3.4', + 'Sphinx >= 1.3.4', 'docutils', 'repoze.sphinx.autointerface', 'pylons_sphinx_latesturl', -- cgit v1.2.3 From 848bc1a519635e6b20318e3ce6d68670c4459958 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Tue, 12 Jan 2016 09:00:33 -0800 Subject: try bump to sphinx 1.3.4 in rtd.txt --- rtd.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/rtd.txt b/rtd.txt index 142b6ca35..320000ee1 100644 --- a/rtd.txt +++ b/rtd.txt @@ -1 +1,2 @@ -e .[docs] +sphinx>=1.3.4 -- cgit v1.2.3 From abe3ac6009348e6f3ef6a23ef2ac8ebb6771bbb3 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Tue, 12 Jan 2016 13:30:02 -0800 Subject: maybe == ? --- rtd.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rtd.txt b/rtd.txt index 320000ee1..b24f45c39 100644 --- a/rtd.txt +++ b/rtd.txt @@ -1,2 +1,2 @@ -e .[docs] -sphinx>=1.3.4 +sphinx==1.3.4 -- cgit v1.2.3 From 0ff361debf15b7ae48df08aa9da4168e7d49cd73 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Tue, 12 Jan 2016 13:47:02 -0800 Subject: hail mary --- rtd.txt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/rtd.txt b/rtd.txt index b24f45c39..17552ec08 100644 --- a/rtd.txt +++ b/rtd.txt @@ -1,2 +1,6 @@ --e .[docs] -sphinx==1.3.4 +Sphinx >= 1.3.4 +docutils +repoze.sphinx.autointerface +pylons_sphinx_latesturl +pylons-sphinx-themes +sphinxcontrib-programoutput -- cgit v1.2.3 From 4fdda90942f90165db6a1cefefa44aa441b27e3b Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Tue, 12 Jan 2016 13:56:15 -0800 Subject: note to self: check the branch and be consistent when viewing docs on RTD --- rtd.txt | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/rtd.txt b/rtd.txt index 17552ec08..142b6ca35 100644 --- a/rtd.txt +++ b/rtd.txt @@ -1,6 +1 @@ -Sphinx >= 1.3.4 -docutils -repoze.sphinx.autointerface -pylons_sphinx_latesturl -pylons-sphinx-themes -sphinxcontrib-programoutput +-e .[docs] -- cgit v1.2.3 From bc37a59c05c13ae637052dbfee5eed83b31648de Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 12 Jan 2016 23:29:08 -0600 Subject: expect py3 and special-case py2 behavior previously py3 was the special case, but as we move toward the future we want the py2 codebase to be considered legacy with py3 as the default --- pyramid/compat.py | 181 ++++++++++++++-------------- pyramid/i18n.py | 14 +-- pyramid/interfaces.py | 4 +- pyramid/scripts/pserve.py | 4 +- pyramid/session.py | 4 +- pyramid/tests/test_config/test_adapters.py | 8 +- pyramid/tests/test_config/test_factories.py | 8 +- pyramid/tests/test_path.py | 8 +- pyramid/tests/test_request.py | 8 +- pyramid/tests/test_scripts/test_pserve.py | 8 +- pyramid/tests/test_traversal.py | 8 +- pyramid/tests/test_urldispatch.py | 8 +- pyramid/tests/test_util.py | 30 ++--- pyramid/traversal.py | 20 +-- pyramid/urldispatch.py | 12 +- pyramid/util.py | 8 +- 16 files changed, 167 insertions(+), 166 deletions(-) diff --git a/pyramid/compat.py b/pyramid/compat.py index e9edda359..16ddef38e 100644 --- a/pyramid/compat.py +++ b/pyramid/compat.py @@ -21,22 +21,23 @@ except ImportError: # pragma: no cover import pickle # True if we are running on Python 3. +PY2 = sys.version_info[0] == 2 PY3 = sys.version_info[0] == 3 -if PY3: - string_types = str, - integer_types = int, - class_types = type, - text_type = str - binary_type = bytes - long = int -else: +if PY2: string_types = basestring, integer_types = (int, long) class_types = (type, types.ClassType) text_type = unicode binary_type = str long = long +else: + string_types = str, + integer_types = int, + class_types = type, + text_type = str + binary_type = bytes + long = int def text_(s, encoding='latin-1', errors='strict'): """ If ``s`` is an instance of ``binary_type``, return @@ -52,16 +53,16 @@ def bytes_(s, encoding='latin-1', errors='strict'): return s.encode(encoding, errors) return s -if PY3: +if PY2: def ascii_native_(s): if isinstance(s, text_type): s = s.encode('ascii') - return str(s, 'ascii', 'strict') + return str(s) else: def ascii_native_(s): if isinstance(s, text_type): s = s.encode('ascii') - return str(s) + return str(s, 'ascii', 'strict') ascii_native_.__doc__ = """ Python 3: If ``s`` is an instance of ``text_type``, return @@ -72,20 +73,20 @@ Python 2: If ``s`` is an instance of ``text_type``, return """ -if PY3: +if PY2: def native_(s, encoding='latin-1', errors='strict'): """ If ``s`` is an instance of ``text_type``, return - ``s``, otherwise return ``str(s, encoding, errors)``""" + ``s.encode(encoding, errors)``, otherwise return ``str(s)``""" if isinstance(s, text_type): - return s - return str(s, encoding, errors) + return s.encode(encoding, errors) + return str(s) else: def native_(s, encoding='latin-1', errors='strict'): """ If ``s`` is an instance of ``text_type``, return - ``s.encode(encoding, errors)``, otherwise return ``str(s)``""" + ``s``, otherwise return ``str(s, encoding, errors)``""" if isinstance(s, text_type): - return s.encode(encoding, errors) - return str(s) + return s + return str(s, encoding, errors) native_.__doc__ = """ Python 3: If ``s`` is an instance of ``text_type``, return ``s``, otherwise @@ -95,17 +96,7 @@ Python 2: If ``s`` is an instance of ``text_type``, return ``s.encode(encoding, errors)``, otherwise return ``str(s)`` """ -if PY3: - from urllib import parse - urlparse = parse - from urllib.parse import quote as url_quote - from urllib.parse import quote_plus as url_quote_plus - from urllib.parse import unquote as url_unquote - from urllib.parse import urlencode as url_encode - from urllib.request import urlopen as url_open - url_unquote_text = url_unquote - url_unquote_native = url_unquote -else: +if PY2: import urlparse from urllib import quote as url_quote from urllib import quote_plus as url_quote_plus @@ -119,22 +110,19 @@ else: def url_unquote_native(v, encoding='utf-8', errors='replace'): # pragma: no cover return native_(url_unquote_text(v, encoding, errors)) +else: + from urllib import parse + urlparse = parse + from urllib.parse import quote as url_quote + from urllib.parse import quote_plus as url_quote_plus + from urllib.parse import unquote as url_unquote + from urllib.parse import urlencode as url_encode + from urllib.request import urlopen as url_open + url_unquote_text = url_unquote + url_unquote_native = url_unquote -if PY3: # pragma: no cover - import builtins - exec_ = getattr(builtins, "exec") - - def reraise(tp, value, tb=None): - if value is None: - value = tp - if value.__traceback__ is not tb: - raise value.with_traceback(tb) - raise value - - del builtins - -else: # pragma: no cover +if PY2: # pragma: no cover def exec_(code, globs=None, locs=None): """Execute code in a namespace.""" if globs is None: @@ -151,48 +139,61 @@ else: # pragma: no cover raise tp, value, tb """) +else: # pragma: no cover + import builtins + exec_ = getattr(builtins, "exec") + + def reraise(tp, value, tb=None): + if value is None: + value = tp + if value.__traceback__ is not tb: + raise value.with_traceback(tb) + raise value + + del builtins + -if PY3: # pragma: no cover +if PY2: # pragma: no cover def iteritems_(d): - return d.items() + return d.iteritems() def itervalues_(d): - return d.values() + return d.itervalues() def iterkeys_(d): - return d.keys() + return d.iterkeys() else: # pragma: no cover def iteritems_(d): - return d.iteritems() + return d.items() def itervalues_(d): - return d.itervalues() + return d.values() def iterkeys_(d): - return d.iterkeys() + return d.keys() -if PY3: +if PY2: + map_ = map +else: def map_(*arg): return list(map(*arg)) -else: - map_ = map -if PY3: +if PY2: def is_nonstr_iter(v): - if isinstance(v, str): - return False return hasattr(v, '__iter__') else: def is_nonstr_iter(v): + if isinstance(v, str): + return False return hasattr(v, '__iter__') -if PY3: - im_func = '__func__' - im_self = '__self__' -else: +if PY2: im_func = 'im_func' im_self = 'im_self' +else: + im_func = '__func__' + im_self = '__self__' try: import configparser @@ -204,65 +205,65 @@ try: except ImportError: from Cookie import SimpleCookie -if PY3: - from html import escape -else: +if PY2: from cgi import escape - -if PY3: - input_ = input else: - input_ = raw_input + from html import escape -if PY3: - from inspect import getfullargspec as getargspec +if PY2: + input_ = raw_input else: - from inspect import getargspec + input_ = input -if PY3: - from io import StringIO as NativeIO +if PY2: + from inspect import getargspec else: + from inspect import getfullargspec as getargspec + +if PY2: from io import BytesIO as NativeIO +else: + from io import StringIO as NativeIO # "json" is not an API; it's here to support older pyramid_debugtoolbar # versions which attempt to import it import json -if PY3: +if PY2: + def decode_path_info(path): + return path.decode('utf-8') +else: # see PEP 3333 for why we encode WSGI PATH_INFO to latin-1 before # decoding it to utf-8 def decode_path_info(path): return path.encode('latin-1').decode('utf-8') -else: - def decode_path_info(path): - return path.decode('utf-8') -if PY3: - # see PEP 3333 for why we decode the path to latin-1 - from urllib.parse import unquote_to_bytes +if PY2: + from urlparse import unquote as unquote_to_bytes def unquote_bytes_to_wsgi(bytestring): - return unquote_to_bytes(bytestring).decode('latin-1') + return unquote_to_bytes(bytestring) else: - from urlparse import unquote as unquote_to_bytes + # see PEP 3333 for why we decode the path to latin-1 + from urllib.parse import unquote_to_bytes def unquote_bytes_to_wsgi(bytestring): - return unquote_to_bytes(bytestring) + return unquote_to_bytes(bytestring).decode('latin-1') def is_bound_method(ob): return inspect.ismethod(ob) and getattr(ob, im_self, None) is not None # support annotations and keyword-only arguments in PY3 -if PY3: # pragma: no cover - from inspect import getfullargspec as getargspec -else: +if PY2: from inspect import getargspec - -if PY3: # pragma: no cover - from itertools import zip_longest else: + from inspect import getfullargspec as getargspec + +if PY2: from itertools import izip_longest as zip_longest +else: + from itertools import zip_longest def is_unbound_method(fn): """ @@ -275,9 +276,9 @@ def is_unbound_method(fn): spec = getargspec(fn) has_self = len(spec.args) > 0 and spec.args[0] == 'self' - if PY3 and inspect.isfunction(fn) and has_self: # pragma: no cover + if PY2 and inspect.ismethod(fn): return True - elif inspect.ismethod(fn): + elif inspect.isfunction(fn) and has_self: return True return False diff --git a/pyramid/i18n.py b/pyramid/i18n.py index 458f6168d..79209d342 100644 --- a/pyramid/i18n.py +++ b/pyramid/i18n.py @@ -8,7 +8,7 @@ from translationstring import ( TranslationStringFactory, # API ) -from pyramid.compat import PY3 +from pyramid.compat import PY2 from pyramid.decorator import reify from pyramid.interfaces import ( @@ -332,10 +332,10 @@ class Translations(gettext.GNUTranslations, object): """Like ``ugettext()``, but look the message up in the specified domain. """ - if PY3: - return self._domains.get(domain, self).gettext(message) - else: + if PY2: return self._domains.get(domain, self).ugettext(message) + else: + return self._domains.get(domain, self).gettext(message) def dngettext(self, domain, singular, plural, num): """Like ``ngettext()``, but look the message up in the specified @@ -353,11 +353,11 @@ class Translations(gettext.GNUTranslations, object): """Like ``ungettext()`` but look the message up in the specified domain. """ - if PY3: - return self._domains.get(domain, self).ngettext( + if PY2: + return self._domains.get(domain, self).ungettext( singular, plural, num) else: - return self._domains.get(domain, self).ungettext( + return self._domains.get(domain, self).ngettext( singular, plural, num) class LocalizerRequestMixin(object): diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index bbdc5121d..9e5cbb6d3 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -5,7 +5,7 @@ from zope.interface import ( Interface, ) -from pyramid.compat import PY3 +from pyramid.compat import PY2 # public API interfaces @@ -311,7 +311,7 @@ class IDict(Interface): def values(): """ Return a list of values from the dictionary """ - if not PY3: + if PY2: def iterkeys(): """ Return an iterator of keys from the dictionary """ diff --git a/pyramid/scripts/pserve.py b/pyramid/scripts/pserve.py index 5aaaffec9..155b82bdc 100644 --- a/pyramid/scripts/pserve.py +++ b/pyramid/scripts/pserve.py @@ -29,7 +29,7 @@ from paste.deploy import loadserver from paste.deploy import loadapp from paste.deploy.loadwsgi import loadcontext, SERVER -from pyramid.compat import PY3 +from pyramid.compat import PY2 from pyramid.compat import WIN from pyramid.paster import setup_logging @@ -1111,7 +1111,7 @@ def cherrypy_server_runner( server = wsgiserver.CherryPyWSGIServer(bind_addr, app, server_name=server_name, **kwargs) if ssl_pem is not None: - if not PY3: + if PY2: server.ssl_certificate = server.ssl_private_key = ssl_pem else: # creates wsgiserver.ssl_builtin as side-effect diff --git a/pyramid/session.py b/pyramid/session.py index 51f9de620..b3be68705 100644 --- a/pyramid/session.py +++ b/pyramid/session.py @@ -12,7 +12,7 @@ from webob.cookies import SignedSerializer from pyramid.compat import ( pickle, - PY3, + PY2, text_, bytes_, native_, @@ -325,7 +325,7 @@ def BaseCookieSessionFactory( __len__ = manage_accessed(dict.__len__) __iter__ = manage_accessed(dict.__iter__) - if not PY3: + if PY2: iteritems = manage_accessed(dict.iteritems) itervalues = manage_accessed(dict.itervalues) iterkeys = manage_accessed(dict.iterkeys) diff --git a/pyramid/tests/test_config/test_adapters.py b/pyramid/tests/test_config/test_adapters.py index b3b7576a3..ab5d6ef61 100644 --- a/pyramid/tests/test_config/test_adapters.py +++ b/pyramid/tests/test_config/test_adapters.py @@ -1,6 +1,6 @@ import unittest -from pyramid.compat import PY3 +from pyramid.compat import PY2 from pyramid.tests.test_config import IDummy class AdaptersConfiguratorMixinTests(unittest.TestCase): @@ -219,10 +219,10 @@ class AdaptersConfiguratorMixinTests(unittest.TestCase): def test_add_response_adapter_dottednames(self): from pyramid.interfaces import IResponse config = self._makeOne(autocommit=True) - if PY3: - str_name = 'builtins.str' - else: + if PY2: str_name = '__builtin__.str' + else: + str_name = 'builtins.str' config.add_response_adapter('pyramid.response.Response', str_name) result = config.registry.queryAdapter('foo', IResponse) self.assertTrue(result.body, b'foo') diff --git a/pyramid/tests/test_config/test_factories.py b/pyramid/tests/test_config/test_factories.py index 42bb5accc..452d762f8 100644 --- a/pyramid/tests/test_config/test_factories.py +++ b/pyramid/tests/test_config/test_factories.py @@ -128,17 +128,17 @@ class TestFactoriesMixin(unittest.TestCase): def test_add_request_method_with_text_type_name(self): from pyramid.interfaces import IRequestExtensions - from pyramid.compat import text_, PY3 + from pyramid.compat import text_, PY2 from pyramid.exceptions import ConfigurationError config = self._makeOne(autocommit=True) def boomshaka(r): pass def get_bad_name(): - if PY3: # pragma: nocover - name = b'La Pe\xc3\xb1a' - else: # pragma: nocover + if PY2: name = text_(b'La Pe\xc3\xb1a', 'utf-8') + else: + name = b'La Pe\xc3\xb1a' config.add_request_method(boomshaka, name=name) diff --git a/pyramid/tests/test_path.py b/pyramid/tests/test_path.py index f85373fd9..563ece6d6 100644 --- a/pyramid/tests/test_path.py +++ b/pyramid/tests/test_path.py @@ -1,6 +1,6 @@ import unittest import os -from pyramid.compat import PY3 +from pyramid.compat import PY2 here = os.path.abspath(os.path.dirname(__file__)) @@ -376,10 +376,10 @@ class TestDottedNameResolver(unittest.TestCase): def test_zope_dottedname_style_resolve_builtin(self): typ = self._makeOne() - if PY3: - result = typ._zope_dottedname_style('builtins.str', None) - else: + if PY2: result = typ._zope_dottedname_style('__builtin__.str', None) + else: + result = typ._zope_dottedname_style('builtins.str', None) self.assertEqual(result, str) def test_zope_dottedname_style_resolve_absolute(self): diff --git a/pyramid/tests/test_request.py b/pyramid/tests/test_request.py index c528b9174..c79c84d63 100644 --- a/pyramid/tests/test_request.py +++ b/pyramid/tests/test_request.py @@ -3,7 +3,7 @@ import unittest from pyramid import testing from pyramid.compat import ( - PY3, + PY2, text_, bytes_, native_, @@ -310,10 +310,10 @@ class TestRequest(unittest.TestCase): b'/\xe6\xb5\x81\xe8\xa1\x8c\xe8\xb6\x8b\xe5\x8a\xbf', 'utf-8' ) - if PY3: - body = bytes(json.dumps({'a':inp}), 'utf-16') - else: + if PY2: body = json.dumps({'a':inp}).decode('utf-8').encode('utf-16') + else: + body = bytes(json.dumps({'a':inp}), 'utf-16') request.body = body request.content_type = 'application/json; charset=utf-16' self.assertEqual(request.json_body, {'a':inp}) diff --git a/pyramid/tests/test_scripts/test_pserve.py b/pyramid/tests/test_scripts/test_pserve.py index 2d4c4e1c0..bc21665aa 100644 --- a/pyramid/tests/test_scripts/test_pserve.py +++ b/pyramid/tests/test_scripts/test_pserve.py @@ -3,11 +3,11 @@ import os import tempfile import unittest -from pyramid.compat import PY3 -if PY3: - import builtins as __builtin__ -else: +from pyramid.compat import PY2 +if PY2: import __builtin__ +else: + import builtins as __builtin__ class TestPServeCommand(unittest.TestCase): def setUp(self): diff --git a/pyramid/tests/test_traversal.py b/pyramid/tests/test_traversal.py index aa3f1ad16..0decd04d6 100644 --- a/pyramid/tests/test_traversal.py +++ b/pyramid/tests/test_traversal.py @@ -8,7 +8,7 @@ from pyramid.compat import ( native_, text_type, url_quote, - PY3, + PY2, ) with warnings.catch_warnings(record=True) as w: @@ -335,10 +335,10 @@ class ResourceTreeTraverserTests(unittest.TestCase): foo = DummyContext(bar, path) root = DummyContext(foo, 'root') policy = self._makeOne(root) - if PY3: - vhm_root = b'/Qu\xc3\xa9bec'.decode('latin-1') - else: + if PY2: vhm_root = b'/Qu\xc3\xa9bec' + else: + vhm_root = b'/Qu\xc3\xa9bec'.decode('latin-1') environ = self._getEnviron(HTTP_X_VHM_ROOT=vhm_root) request = DummyRequest(environ, path_info=text_('/bar')) result = policy(request) diff --git a/pyramid/tests/test_urldispatch.py b/pyramid/tests/test_urldispatch.py index 20a3a4fc8..2d20b24c3 100644 --- a/pyramid/tests/test_urldispatch.py +++ b/pyramid/tests/test_urldispatch.py @@ -2,7 +2,7 @@ import unittest from pyramid import testing from pyramid.compat import ( text_, - PY3, + PY2, ) class TestRoute(unittest.TestCase): @@ -120,10 +120,10 @@ class RoutesMapperTests(unittest.TestCase): def test___call__pathinfo_cant_be_decoded(self): from pyramid.exceptions import URLDecodeError mapper = self._makeOne() - if PY3: - path_info = b'\xff\xfe\xe6\x00'.decode('latin-1') - else: + if PY2: path_info = b'\xff\xfe\xe6\x00' + else: + path_info = b'\xff\xfe\xe6\x00'.decode('latin-1') request = self._getRequest(PATH_INFO=path_info) self.assertRaises(URLDecodeError, mapper, request) diff --git a/pyramid/tests/test_util.py b/pyramid/tests/test_util.py index 2bf6a710f..0be99e949 100644 --- a/pyramid/tests/test_util.py +++ b/pyramid/tests/test_util.py @@ -1,5 +1,5 @@ import unittest -from pyramid.compat import PY3 +from pyramid.compat import PY2 class Test_InstancePropertyHelper(unittest.TestCase): @@ -149,10 +149,10 @@ class Test_InstancePropertyHelper(unittest.TestCase): from pyramid.exceptions import ConfigurationError cls = self._getTargetClass() - if PY3: # pragma: nocover - name = b'La Pe\xc3\xb1a' - else: # pragma: nocover + if PY2: name = text_(b'La Pe\xc3\xb1a', 'utf-8') + else: + name = b'La Pe\xc3\xb1a' def make_bad_name(): cls.make_property(lambda x: 1, name=name, reify=True) @@ -431,10 +431,10 @@ class Test_object_description(unittest.TestCase): self.assertEqual(self._callFUT(('a', 'b')), "('a', 'b')") def test_set(self): - if PY3: - self.assertEqual(self._callFUT(set(['a'])), "{'a'}") - else: + if PY2: self.assertEqual(self._callFUT(set(['a'])), "set(['a'])") + else: + self.assertEqual(self._callFUT(set(['a'])), "{'a'}") def test_list(self): self.assertEqual(self._callFUT(['a']), "['a']") @@ -769,25 +769,25 @@ class TestActionInfo(unittest.TestCase): class TestCallableName(unittest.TestCase): def test_valid_ascii(self): from pyramid.util import get_callable_name - from pyramid.compat import text_, PY3 + from pyramid.compat import text_ - if PY3: # pragma: nocover - name = b'hello world' - else: # pragma: nocover + if PY2: name = text_(b'hello world', 'utf-8') + else: + name = b'hello world' self.assertEqual(get_callable_name(name), 'hello world') def test_invalid_ascii(self): from pyramid.util import get_callable_name - from pyramid.compat import text_, PY3 + from pyramid.compat import text_ from pyramid.exceptions import ConfigurationError def get_bad_name(): - if PY3: # pragma: nocover - name = b'La Pe\xc3\xb1a' - else: # pragma: nocover + if PY2: name = text_(b'La Pe\xc3\xb1a', 'utf-8') + else: + name = b'La Pe\xc3\xb1a' get_callable_name(name) diff --git a/pyramid/traversal.py b/pyramid/traversal.py index db73d13fc..963a76bb5 100644 --- a/pyramid/traversal.py +++ b/pyramid/traversal.py @@ -15,7 +15,7 @@ from pyramid.interfaces import ( ) from pyramid.compat import ( - PY3, + PY2, native_, text_, ascii_native_, @@ -575,7 +575,7 @@ the ``safe`` argument to this function. This corresponds to the """ -if PY3: +if PY2: # special-case on Python 2 for speed? unchecked def quote_path_segment(segment, safe=''): """ %s """ % quote_path_segment_doc @@ -587,9 +587,10 @@ if PY3: try: return _segment_cache[(segment, safe)] except KeyError: - if segment.__class__ not in (text_type, binary_type): - segment = str(segment) - result = url_quote(native_(segment, 'utf-8'), safe) + if segment.__class__ is text_type: #isinstance slighly slower (~15%) + result = url_quote(segment.encode('utf-8'), safe) + else: + result = url_quote(str(segment), safe) # we don't need a lock to mutate _segment_cache, as the below # will generate exactly one Python bytecode (STORE_SUBSCR) _segment_cache[(segment, safe)] = result @@ -605,15 +606,14 @@ else: try: return _segment_cache[(segment, safe)] except KeyError: - if segment.__class__ is text_type: #isinstance slighly slower (~15%) - result = url_quote(segment.encode('utf-8'), safe) - else: - result = url_quote(str(segment), safe) + if segment.__class__ not in (text_type, binary_type): + segment = str(segment) + result = url_quote(native_(segment, 'utf-8'), safe) # we don't need a lock to mutate _segment_cache, as the below # will generate exactly one Python bytecode (STORE_SUBSCR) _segment_cache[(segment, safe)] = result return result - + slash = text_('/') @implementer(ITraverser) diff --git a/pyramid/urldispatch.py b/pyramid/urldispatch.py index 4a8828810..c88ad9590 100644 --- a/pyramid/urldispatch.py +++ b/pyramid/urldispatch.py @@ -7,7 +7,7 @@ from pyramid.interfaces import ( ) from pyramid.compat import ( - PY3, + PY2, native_, text_, text_type, @@ -210,14 +210,14 @@ def _compile_route(route): def generator(dict): newdict = {} for k, v in dict.items(): - if PY3: - if v.__class__ is binary_type: - # url_quote below needs a native string, not bytes on Py3 - v = v.decode('utf-8') - else: + if PY2: if v.__class__ is text_type: # url_quote below needs bytes, not unicode on Py2 v = v.encode('utf-8') + else: + if v.__class__ is binary_type: + # url_quote below needs a native string, not bytes on Py3 + v = v.decode('utf-8') if k == remainder: # a stararg argument diff --git a/pyramid/util.py b/pyramid/util.py index 8fcd84f07..0a73cedaf 100644 --- a/pyramid/util.py +++ b/pyramid/util.py @@ -20,7 +20,7 @@ from pyramid.compat import ( integer_types, string_types, text_, - PY3, + PY2, native_ ) @@ -310,10 +310,10 @@ def object_description(object): if isinstance(object, (bool, float, type(None))): return text_(str(object)) if isinstance(object, set): - if PY3: - return shortrepr(object, '}') - else: + if PY2: return shortrepr(object, ')') + else: + return shortrepr(object, '}') if isinstance(object, tuple): return shortrepr(object, ')') if isinstance(object, list): -- cgit v1.2.3 From 1f7305442e2aa824af4223df6b844cc988034492 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Wed, 13 Jan 2016 09:22:50 -0800 Subject: add python 3.5 --- docs/narr/install.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/narr/install.rst b/docs/narr/install.rst index 26d458727..164442262 100644 --- a/docs/narr/install.rst +++ b/docs/narr/install.rst @@ -15,8 +15,8 @@ You will need `Python `_ version 2.6 or better to run .. sidebar:: Python Versions As of this writing, :app:`Pyramid` has been tested under Python 2.6, Python - 2.7, Python 3.2, Python 3.3, Python 3.4, PyPy, and PyPy3. :app:`Pyramid` - does not run under any version of Python before 2.6. + 2.7, Python 3.2, Python 3.3, Python 3.4, Python 3.5, PyPy, and PyPy3. + :app:`Pyramid` does not run under any version of Python before 2.6. :app:`Pyramid` is known to run on all popular UNIX-like systems such as Linux, Mac OS X, and FreeBSD as well as on Windows platforms. It is also known to run -- cgit v1.2.3 From 384007c4e6e1c0c397b9c643c8c34bdf0ddf4b07 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Wed, 13 Jan 2016 13:24:26 -0800 Subject: update for python 3.5 --- docs/narr/install.rst | 2 +- docs/narr/introduction.rst | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/narr/install.rst b/docs/narr/install.rst index 164442262..381e325df 100644 --- a/docs/narr/install.rst +++ b/docs/narr/install.rst @@ -46,7 +46,7 @@ Alternatively, you can use the `homebrew `_ package manager. # for python 2.7 $ brew install python - # for python 3.4 + # for python 3.5 $ brew install python3 If you use an installer for your Python, then you can skip to the section diff --git a/docs/narr/introduction.rst b/docs/narr/introduction.rst index 7906dd85d..40f465b0a 100644 --- a/docs/narr/introduction.rst +++ b/docs/narr/introduction.rst @@ -861,10 +861,10 @@ tests, as measured by the ``coverage`` tool available on PyPI. It also has greater than 95% decision/condition coverage as measured by the ``instrumental`` tool available on PyPI. It is automatically tested by the Jenkins tool on Python 2.6, Python 2.7, Python 3.2, Python 3.3, Python 3.4, -PyPy, and PyPy3 after each commit to its GitHub repository. Official Pyramid -add-ons are held to a similar testing standard. We still find bugs in Pyramid -and its official add-ons, but we've noticed we find a lot more of them while -working on other projects that don't have a good testing regime. +Python 3.5, PyPy, and PyPy3 after each commit to its GitHub repository. +Official Pyramid add-ons are held to a similar testing standard. We still find +bugs in Pyramid and its official add-ons, but we've noticed we find a lot more +of them while working on other projects that don't have a good testing regime. Example: http://jenkins.pylonsproject.org/ -- cgit v1.2.3 From 34515f33b3e391dd1c0c727bf5ef4af586b57889 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Thu, 14 Jan 2016 02:55:04 -0800 Subject: Rename Cookbook to Pyramid Community Cookbook - use .rst intersphinx labels for pages instead of broken URLs --- docs/glossary.rst | 2 +- docs/index.rst | 2 +- docs/narr/advconfig.rst | 7 +++---- docs/narr/i18n.rst | 6 +++--- docs/narr/introduction.rst | 3 +-- docs/whatsnew-1.3.rst | 9 +++++---- 6 files changed, 14 insertions(+), 15 deletions(-) diff --git a/docs/glossary.rst b/docs/glossary.rst index 82423a59d..60f03f000 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -960,7 +960,7 @@ Glossary users transition from Pylons and those preferring a more Pylons-like API. The scaffold has been retired but the demo plays a similar role. - Pyramid Cookbook + Pyramid Community Cookbook Additional, community-based documentation for Pyramid which presents topical, practical uses of Pyramid: :ref:`Pyramid Community Cookbook ` diff --git a/docs/index.rst b/docs/index.rst index 9a34f088b..ba6ca1e49 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -40,7 +40,7 @@ speed right away. * Like learning by example? Visit the official :ref:`html_tutorials` as well as the community-contributed :ref:`Pyramid Tutorials - ` and :ref:`Pyramid Cookbook + ` and :ref:`Pyramid Community Cookbook `. * For help getting Pyramid set up, try :ref:`installing_chapter`. diff --git a/docs/narr/advconfig.rst b/docs/narr/advconfig.rst index ba9bd1352..bdcdf45a4 100644 --- a/docs/narr/advconfig.rst +++ b/docs/narr/advconfig.rst @@ -417,7 +417,6 @@ added in configuration execution order. More Information ---------------- -For more information, see the article `"A Whirlwind Tour of Advanced -Configuration Tactics" -`_ -in the Pyramid Cookbook. +For more information, see the article :ref:`A Whirlwind Tour of Advanced +Configuration Tactics ` in the Pyramid Community +Cookbook. diff --git a/docs/narr/i18n.rst b/docs/narr/i18n.rst index bb0bbe511..ecc48aa2b 100644 --- a/docs/narr/i18n.rst +++ b/docs/narr/i18n.rst @@ -666,9 +666,9 @@ can always use the more manual translation facility described in Mako Pyramid i18n Support ------------------------- -There exists a recipe within the :term:`Pyramid Cookbook` named ":ref:`Mako -Internationalization `" which explains how to add idiomatic -i18n support to :term:`Mako` templates. +There exists a recipe within the :term:`Pyramid Community Cookbook` named +:ref:`Mako Internationalization ` which explains how to add +idiomatic i18n support to :term:`Mako` templates. .. index:: single: localization deployment settings diff --git a/docs/narr/introduction.rst b/docs/narr/introduction.rst index 40f465b0a..422db557e 100644 --- a/docs/narr/introduction.rst +++ b/docs/narr/introduction.rst @@ -892,8 +892,7 @@ also maintain a "cookbook" of recipes, which are usually demonstrations of common integration scenarios too specific to add to the official narrative docs. In any case, the Pyramid documentation is comprehensive. -Example: The Pyramid Cookbook at -http://docs.pylonsproject.org/projects/pyramid-cookbook/en/latest/. +Example: The :ref:`Pyramid Community Cookbook `. .. index:: single: Pylons Project diff --git a/docs/whatsnew-1.3.rst b/docs/whatsnew-1.3.rst index 1a299e126..dd3e1b8dd 100644 --- a/docs/whatsnew-1.3.rst +++ b/docs/whatsnew-1.3.rst @@ -523,10 +523,11 @@ Documentation Enhancements :ref:`making_a_console_script`. - Removed the "Running Pyramid on Google App Engine" tutorial from the main - docs. It survives on in the Cookbook - (http://docs.pylonsproject.org/projects/pyramid_cookbook/en/latest/deployment/gae.html). - Rationale: it provides the correct info for the Python 2.5 version of GAE - only, and this version of Pyramid does not support Python 2.5. + docs. It survives on in the Pyramid Community Cookbook as + :ref:`Pyramid on Google's App Engine (using appengine-monkey) + `. Rationale: it provides the correct info for + the Python 2.5 version of GAE only, and this version of Pyramid does not + support Python 2.5. - Updated the :ref:`changing_the_forbidden_view` section, replacing explanations of registering a view using ``add_view`` or ``view_config`` -- cgit v1.2.3 From 49cd2f840b441a3948c486b54ac823501a157643 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 14 Jan 2016 10:52:06 -0600 Subject: add comment about PY2 vs PY3 --- pyramid/compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/compat.py b/pyramid/compat.py index 16ddef38e..2bfbabb8e 100644 --- a/pyramid/compat.py +++ b/pyramid/compat.py @@ -20,7 +20,7 @@ try: except ImportError: # pragma: no cover import pickle -# True if we are running on Python 3. +# PY3 is left as bw-compat but PY2 should be used for most checks. PY2 = sys.version_info[0] == 2 PY3 = sys.version_info[0] == 3 -- cgit v1.2.3 From 1d837417cff0b3e8be066252213952316edf6681 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Fri, 15 Jan 2016 23:58:29 -0800 Subject: minor grammar, rewrap 79 columns, up through traversal/url dispatch debate --- docs/designdefense.rst | 97 +++++++++++++++++++++++++------------------------- 1 file changed, 49 insertions(+), 48 deletions(-) diff --git a/docs/designdefense.rst b/docs/designdefense.rst index 2ce9dc12c..35de8cb03 100644 --- a/docs/designdefense.rst +++ b/docs/designdefense.rst @@ -306,13 +306,12 @@ the ZCA registry: view that is only found when the context is some class of object, or when the context implements some :term:`interface`. -- Singularity. There's only one place where "application configuration" - lives in a :app:`Pyramid` application: in a component registry. The - component registry answers questions made to it by the framework at runtime - based on the configuration of *an application*. Note: "an application" is - not the same as "a process", multiple independently configured copies of - the same :app:`Pyramid` application are capable of running in the same - process space. +- Singularity. There's only one place where "application configuration" lives + in a :app:`Pyramid` application: in a component registry. The component + registry answers questions made to it by the framework at runtime based on + the configuration of *an application*. Note: "an application" is not the + same as "a process"; multiple independently configured copies of the same + :app:`Pyramid` application are capable of running in the same process space. - Composability. A ZCA component registry can be populated imperatively, or there's an existing mechanism to populate a registry via the use of a @@ -330,10 +329,9 @@ the ZCA registry: (non-Zope) frameworks. - Testability. Judicious use of the ZCA registry in framework code makes - testing that code slightly easier. Instead of using monkeypatching or - other facilities to register mock objects for testing, we inject - dependencies via ZCA registrations and then use lookups in the code find - our mock objects. + testing that code slightly easier. Instead of using monkeypatching or other + facilities to register mock objects for testing, we inject dependencies via + ZCA registrations, then use lookups in the code to find our mock objects. - Speed. The ZCA registry is very fast for a specific set of complex lookup scenarios that :app:`Pyramid` uses, having been optimized through the years @@ -347,17 +345,17 @@ Conclusion ++++++++++ If you only *develop applications* using :app:`Pyramid`, there's not much to -complain about here. You just should never need to understand the ZCA -registry API: use documented :app:`Pyramid` APIs instead. However, you may -be an application developer who doesn't read API documentation because it's -unmanly. Instead you read the raw source code, and because you haven't read -the documentation, you don't know what functions, classes, and methods even -*form* the :app:`Pyramid` API. As a result, you've now written code that -uses internals and you've painted yourself into a conceptual corner as a -result of needing to wrestle with some ZCA-using implementation detail. If -this is you, it's extremely hard to have a lot of sympathy for you. You'll -either need to get familiar with how we're using the ZCA registry or you'll -need to use only the documented APIs; that's why we document them as APIs. +complain about here. You just should never need to understand the ZCA registry +API; use documented :app:`Pyramid` APIs instead. However, you may be an +application developer who doesn't read API documentation. Instead you +read the raw source code, and because you haven't read the API documentation, +you don't know what functions, classes, and methods even *form* the +:app:`Pyramid` API. As a result, you've now written code that uses internals, +and you've painted yourself into a conceptual corner, needing to wrestle with +some ZCA-using implementation detail. If this is you, it's extremely hard to +have a lot of sympathy for you. You'll either need to get familiar with how +we're using the ZCA registry or you'll need to use only the documented APIs; +that's why we document them as APIs. If you *extend* or *develop* :app:`Pyramid` (create new directives, use some of the more obscure hooks as described in :ref:`hooks_chapter`, or work on @@ -366,6 +364,7 @@ at least some ZCA concepts. In some places it's used unabashedly, and will be forever. We know it's quirky, but it's also useful and fundamentally understandable if you take the time to do some reading about it. + .. _zcml_encouragement: Pyramid "Encourages Use of ZCML" @@ -381,15 +380,16 @@ completely optional. No ZCML is required at all to use :app:`Pyramid`, nor any other sort of frameworky declarative frontend to application configuration. -Pyramid Does Traversal, And I Don't Like Traversal + +Pyramid Does Traversal, and I Don't Like Traversal -------------------------------------------------- In :app:`Pyramid`, :term:`traversal` is the act of resolving a URL path to a -:term:`resource` object in a resource tree. Some people are uncomfortable -with this notion, and believe it is wrong. Thankfully, if you use -:app:`Pyramid`, and you don't want to model your application in terms of a -resource tree, you needn't use it at all. Instead, use :term:`URL dispatch` -to map URL paths to views. +:term:`resource` object in a resource tree. Some people are uncomfortable with +this notion, and believe it is wrong. Thankfully if you use :app:`Pyramid` and +you don't want to model your application in terms of a resource tree, you +needn't use it at all. Instead use :term:`URL dispatch` to map URL paths to +views. The idea that some folks believe traversal is unilaterally wrong is understandable. The people who believe it is wrong almost invariably have @@ -424,7 +424,8 @@ URL pattern matching. But the point is ultimately moot. If you don't want to use traversal, you needn't. Use URL dispatch instead. -Pyramid Does URL Dispatch, And I Don't Like URL Dispatch + +Pyramid Does URL Dispatch, and I Don't Like URL Dispatch -------------------------------------------------------- In :app:`Pyramid`, :term:`url dispatch` is the act of resolving a URL path to @@ -446,31 +447,31 @@ I'll argue that URL dispatch is ultimately useful, even if you want to use traversal as well. You can actually *combine* URL dispatch and traversal in :app:`Pyramid` (see :ref:`hybrid_chapter`). One example of such a usage: if you want to emulate something like Zope 2's "Zope Management Interface" UI on -top of your object graph (or any administrative interface), you can register -a route like ``config.add_route('manage', '/manage/*traverse')`` and then -associate "management" views in your code by using the ``route_name`` -argument to a ``view`` configuration, -e.g. ``config.add_view('.some.callable', context=".some.Resource", -route_name='manage')``. If you wire things up this way someone then walks up -to for example, ``/manage/ob1/ob2``, they might be presented with a -management interface, but walking up to ``/ob1/ob2`` would present them with -the default object view. There are other tricks you can pull in these hybrid -configurations if you're clever (and maybe masochistic) too. - -Also, if you are a URL dispatch hater, if you should ever be asked to write -an application that must use some legacy relational database structure, you -might find that using URL dispatch comes in handy for one-off associations -between views and URL paths. Sometimes it's just pointless to add a node to -the object graph that effectively represents the entry point for some bit of -code. You can just use a route and be done with it. If a route matches, a -view associated with the route will be called; if no route matches, -:app:`Pyramid` falls back to using traversal. +top of your object graph (or any administrative interface), you can register a +route like ``config.add_route('manage', '/manage/*traverse')`` and then +associate "management" views in your code by using the ``route_name`` argument +to a ``view`` configuration, e.g., ``config.add_view('.some.callable', +context=".some.Resource", route_name='manage')``. If you wire things up this +way, someone then walks up to, for example, ``/manage/ob1/ob2``, they might be +presented with a management interface, but walking up to ``/ob1/ob2`` would +present them with the default object view. There are other tricks you can pull +in these hybrid configurations if you're clever (and maybe masochistic) too. + +Also, if you are a URL dispatch hater, if you should ever be asked to write an +application that must use some legacy relational database structure, you might +find that using URL dispatch comes in handy for one-off associations between +views and URL paths. Sometimes it's just pointless to add a node to the object +graph that effectively represents the entry point for some bit of code. You +can just use a route and be done with it. If a route matches, a view +associated with the route will be called. If no route matches, :app:`Pyramid` +falls back to using traversal. But the point is ultimately moot. If you use :app:`Pyramid`, and you really don't want to use URL dispatch, you needn't use it at all. Instead, use :term:`traversal` exclusively to map URL paths to views, just like you do in :term:`Zope`. + Pyramid Views Do Not Accept Arbitrary Keyword Arguments ------------------------------------------------------- -- cgit v1.2.3 From 0f451b89f7981234614c8437d002bee48d788a51 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sat, 16 Jan 2016 16:34:29 -0600 Subject: update whatsnew fixes #2211 --- docs/whatsnew-1.6.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/whatsnew-1.6.rst b/docs/whatsnew-1.6.rst index bdfcf34ab..f5c307b5d 100644 --- a/docs/whatsnew-1.6.rst +++ b/docs/whatsnew-1.6.rst @@ -41,6 +41,18 @@ Backwards Incompatibilities This does not change the API of a renderer. See https://github.com/Pylons/pyramid/pull/1563 +- In an effort to combat a common issue it is now a + :class:`~pyramid.exceptions.ConfigurationError` to register a view + callable that is actually an unbound method when using the default view + mapper. As unbound methods do not exist in PY3+ possible errors are detected + by checking if the first parameter is named ``self``. For example, + `config.add_view(ViewClass.some_method, ...)` should actually be + `config.add_view(ViewClass, attr='some_method)'`. This was always an issue + in Pyramid on PY2 but the backward incompatibility is on PY3+ where you may + not use a function with the first parameter named ``self``. In this case + it looks too much like a common error and the exception will be raised. + See https://github.com/Pylons/pyramid/pull/1498 + Feature Additions ----------------- -- cgit v1.2.3 From 51573a705f7798403f7bce5ef1bc5f614d2690f5 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sun, 17 Jan 2016 22:27:38 -0800 Subject: minor grammar, rewrap 79 columns, up through arbitrary keywords in views --- docs/designdefense.rst | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/docs/designdefense.rst b/docs/designdefense.rst index 35de8cb03..566f10015 100644 --- a/docs/designdefense.rst +++ b/docs/designdefense.rst @@ -477,11 +477,10 @@ Pyramid Views Do Not Accept Arbitrary Keyword Arguments Many web frameworks (Zope, TurboGears, Pylons 1.X, Django) allow for their variant of a :term:`view callable` to accept arbitrary keyword or positional -arguments, which are filled in using values present in the ``request.POST`` -or ``request.GET`` dictionaries or by values present in the route match -dictionary. For example, a Django view will accept positional arguments -which match information in an associated "urlconf" such as -``r'^polls/(?P\d+)/$``: +arguments, which are filled in using values present in the ``request.POST``, +``request.GET``, or route match dictionaries. For example, a Django view will +accept positional arguments which match information in an associated "urlconf" +such as ``r'^polls/(?P\d+)/$``: .. code-block:: python :linenos: @@ -489,8 +488,8 @@ which match information in an associated "urlconf" such as def aview(request, poll_id): return HttpResponse(poll_id) -Zope, likewise allows you to add arbitrary keyword and positional -arguments to any method of a resource object found via traversal: +Zope likewise allows you to add arbitrary keyword and positional arguments to +any method of a resource object found via traversal: .. code-block:: python :linenos: @@ -507,13 +506,13 @@ match the names of the positional and keyword arguments in the request, and the method is called (if possible) with its argument list filled with values mentioned therein. TurboGears and Pylons 1.X operate similarly. -Out of the box, :app:`Pyramid` is configured to have none of these features. -By default, :app:`Pyramid` view callables always accept only ``request`` and -no other arguments. The rationale: this argument specification matching done -aggressively can be costly, and :app:`Pyramid` has performance as one of its -main goals, so we've decided to make people, by default, obtain information -by interrogating the request object within the view callable body instead of -providing magic to do unpacking into the view argument list. +Out of the box, :app:`Pyramid` is configured to have none of these features. By +default :app:`Pyramid` view callables always accept only ``request`` and no +other arguments. The rationale is, this argument specification matching when +done aggressively can be costly, and :app:`Pyramid` has performance as one of +its main goals. Therefore we've decided to make people, by default, obtain +information by interrogating the request object within the view callable body +instead of providing magic to do unpacking into the view argument list. However, as of :app:`Pyramid` 1.0a9, user code can influence the way view callables are expected to be called, making it possible to compose a system -- cgit v1.2.3 From b53bff96bd9ae1c71937b8e3870ac7c0eff16767 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Mon, 18 Jan 2016 11:31:23 -0800 Subject: minor grammar, rewrap 79 columns, filesize counts, up through Pyramid Is Too Big --- docs/designdefense.rst | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/docs/designdefense.rst b/docs/designdefense.rst index 566f10015..c58cc5403 100644 --- a/docs/designdefense.rst +++ b/docs/designdefense.rst @@ -550,7 +550,7 @@ you're building a simple bespoke web application: sources using :meth:`pyramid.config.Configurator.include`. - View and subscriber registrations made using :term:`interface` objects - instead of class objects (e.g. :ref:`using_resource_interfaces`). + instead of class objects (e.g., :ref:`using_resource_interfaces`). - A declarative :term:`authorization` system. @@ -576,42 +576,41 @@ make unit testing and implementation substitutability easier. In a bespoke web application, usually there's a single canonical deployment, and therefore no possibility of multiple code forks. Extensibility is not -required; the code is just changed in-place. Security requirements are often -less granular. Using the features listed above will often be overkill for -such an application. +required; the code is just changed in place. Security requirements are often +less granular. Using the features listed above will often be overkill for such +an application. If you don't like these features, it doesn't mean you can't or shouldn't use -:app:`Pyramid`. They are all optional, and a lot of time has been spent -making sure you don't need to know about them up-front. You can build -"Pylons-1.X-style" applications using :app:`Pyramid` that are purely bespoke -by ignoring the features above. You may find these features handy later -after building a bespoke web application that suddenly becomes popular and -requires extensibility because it must be deployed in multiple locations. +:app:`Pyramid`. They are all optional, and a lot of time has been spent making +sure you don't need to know about them up front. You can build "Pylons 1.X +style" applications using :app:`Pyramid` that are purely bespoke by ignoring +the features above. You may find these features handy later after building a +bespoke web application that suddenly becomes popular and requires +extensibility because it must be deployed in multiple locations. Pyramid Is Too Big ------------------ -"The :app:`Pyramid` compressed tarball is larger than 2MB. It must be -enormous!" +"The :app:`Pyramid` compressed tarball is larger than 2MB. It must beenormous!" -No. We just ship it with docs, test code, and scaffolding. Here's a -breakdown of what's included in subdirectories of the package tree: +No. We just ship it with docs, test code, and scaffolding. Here's a breakdown +of what's included in subdirectories of the package tree: docs/ - 4.9MB + 3.6MB pyramid/tests/ - 2.0MB + 1.3MB pyramid/scaffolds/ - 460KB + 133KB pyramid/ (except for ``pyramd/tests`` and ``pyramid/scaffolds``) - 844KB + 812KB Of the approximately 34K lines of Python code in the package, the code that actually has a chance of executing during normal operation, excluding -- cgit v1.2.3 From 5cf8c3677454303bf4e1acecf4c16809edde2a75 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Mon, 18 Jan 2016 14:53:05 -0800 Subject: overhaul quick_tour from Quick project startup with scaffolds to Sessions with updated pyramid_jinja2 scaffold --- docs/quick_tour.rst | 319 +++++++++++---------- docs/quick_tour/package/MANIFEST.in | 2 +- docs/quick_tour/package/development.ini | 63 ++-- docs/quick_tour/package/hello_world/__init__.py | 14 +- docs/quick_tour/package/hello_world/init.py | 34 --- docs/quick_tour/package/hello_world/models.py | 8 - docs/quick_tour/package/hello_world/resources.py | 8 + .../quick_tour/package/hello_world/static/logo.png | Bin 6641 -> 0 bytes .../package/hello_world/static/pylons.css | 73 ----- .../package/hello_world/static/pyramid-16x16.png | Bin 0 -> 1319 bytes .../package/hello_world/static/pyramid.png | Bin 0 -> 12901 bytes .../package/hello_world/static/theme.css | 153 ++++++++++ .../hello_world/templates/mytemplate.jinja2 | 158 +++++----- docs/quick_tour/package/hello_world/views.py | 10 +- docs/quick_tour/package/setup.cfg | 28 ++ docs/quick_tour/package/setup.py | 27 +- 16 files changed, 480 insertions(+), 417 deletions(-) delete mode 100644 docs/quick_tour/package/hello_world/init.py delete mode 100644 docs/quick_tour/package/hello_world/models.py create mode 100644 docs/quick_tour/package/hello_world/resources.py delete mode 100644 docs/quick_tour/package/hello_world/static/logo.png delete mode 100644 docs/quick_tour/package/hello_world/static/pylons.css create mode 100644 docs/quick_tour/package/hello_world/static/pyramid-16x16.png create mode 100644 docs/quick_tour/package/hello_world/static/pyramid.png create mode 100644 docs/quick_tour/package/hello_world/static/theme.css create mode 100644 docs/quick_tour/package/setup.cfg diff --git a/docs/quick_tour.rst b/docs/quick_tour.rst index 56ca53a69..82209f623 100644 --- a/docs/quick_tour.rst +++ b/docs/quick_tour.rst @@ -457,42 +457,41 @@ have much more to offer: Quick project startup with scaffolds ==================================== -So far we have done all of our *Quick Tour* as a single Python file. -No Python packages, no structure. Most Pyramid projects, though, -aren't developed this way. +So far we have done all of our *Quick Tour* as a single Python file. No Python +packages, no structure. Most Pyramid projects, though, aren't developed this +way. -To ease the process of getting started, Pyramid provides *scaffolds* -that generate sample projects from templates in Pyramid and Pyramid -add-ons. Pyramid's ``pcreate`` command can list the available scaffolds: +To ease the process of getting started, Pyramid provides *scaffolds* that +generate sample projects from templates in Pyramid and Pyramid add-ons. +Pyramid's ``pcreate`` command can list the available scaffolds: .. code-block:: bash $ pcreate --list Available scaffolds: alchemy: Pyramid SQLAlchemy project using url dispatch - pyramid_jinja2_starter: pyramid jinja2 starter project + pyramid_jinja2_starter: Pyramid Jinja2 starter project starter: Pyramid starter project zodb: Pyramid ZODB project using traversal -The ``pyramid_jinja2`` add-on gave us a scaffold that we can use. From -the parent directory of where we want our Python package to be generated, -let's use that scaffold to make our project: +The ``pyramid_jinja2`` add-on gave us a scaffold that we can use. From the +parent directory of where we want our Python package to be generated, let's use +that scaffold to make our project: .. code-block:: bash $ pcreate --scaffold pyramid_jinja2_starter hello_world -We next use the normal Python command to set up our package for -development: +We next use the normal Python command to set up our package for development: .. code-block:: bash $ cd hello_world $ python ./setup.py develop -We are moving in the direction of a full-featured Pyramid project, -with a proper setup for Python standards (packaging) and Pyramid -configuration. This includes a new way of running your application: +We are moving in the direction of a full-featured Pyramid project, with a +proper setup for Python standards (packaging) and Pyramid configuration. This +includes a new way of running your application: .. code-block:: bash @@ -508,28 +507,27 @@ Let's look at ``pserve`` and configuration in more depth. Application running with ``pserve`` =================================== -Prior to scaffolds, our project mixed a number of operational details -into our code. Why should my main code care which HTTP server I want and -what port number to run on? +Prior to scaffolds, our project mixed a number of operational details into our +code. Why should my main code care which HTTP server I want and what port +number to run on? -``pserve`` is Pyramid's application runner, separating operational -details from your code. When you install Pyramid, a small command -program called ``pserve`` is written to your ``bin`` directory. This -program is an executable Python module. It's very small, getting most -of its brains via import. +``pserve`` is Pyramid's application runner, separating operational details from +your code. When you install Pyramid, a small command program called ``pserve`` +is written to your ``bin`` directory. This program is an executable Python +module. It's very small, getting most of its brains via import. -You can run ``pserve`` with ``--help`` to see some of its options. -Doing so reveals that you can ask ``pserve`` to watch your development -files and reload the server when they change: +You can run ``pserve`` with ``--help`` to see some of its options. Doing so +reveals that you can ask ``pserve`` to watch your development files and reload +the server when they change: .. code-block:: bash $ pserve development.ini --reload -The ``pserve`` command has a number of other options and operations. -Most of the work, though, comes from your project's wiring, as -expressed in the configuration file you supply to ``pserve``. Let's -take a look at this configuration file. +The ``pserve`` command has a number of other options and operations. Most of +the work, though, comes from your project's wiring, as expressed in the +configuration file you supply to ``pserve``. Let's take a look at this +configuration file. .. seealso:: See also: :ref:`what_is_this_pserve_thing` @@ -537,21 +535,18 @@ take a look at this configuration file. Configuration with ``.ini`` files ================================= -Earlier in *Quick Tour* we first met Pyramid's configuration system. -At that point we did all configuration in Python code. For example, -the port number chosen for our HTTP server was right there in Python -code. Our scaffold has moved this decision and more into the -``development.ini`` file: +Earlier in *Quick Tour* we first met Pyramid's configuration system. At that +point we did all configuration in Python code. For example, the port number +chosen for our HTTP server was right there in Python code. Our scaffold has +moved this decision and more into the ``development.ini`` file: .. literalinclude:: quick_tour/package/development.ini :language: ini -Let's take a quick high-level look. First the ``.ini`` file is divided -into sections: - -- ``[app:hello_world]`` configures our WSGI app +Let's take a quick high-level look. First the ``.ini`` file is divided into +sections: -- ``[pipeline:main]`` sets up our WSGI "pipeline" +- ``[app:main]`` configures our WSGI app - ``[server:main]`` holds our WSGI server settings @@ -559,23 +554,23 @@ into sections: We have a few decisions made for us in this configuration: -#. *Choice of web server:* ``use = egg:pyramid#wsgiref`` tells ``pserve`` to - use the ``wsgiref`` server that is wrapped in the Pyramid package. +#. *Choice of web server:* ``use = egg:hello_world`` tells ``pserve`` to + use the ``waitress`` server. -#. *Port number:* ``port = 6543`` tells ``wsgiref`` to listen on port 6543. +#. *Port number:* ``port = 6543`` tells ``waitress`` to listen on port 6543. #. *WSGI app:* What package has our WSGI application in it? - ``use = egg:hello_world`` in the app section tells the - configuration what application to load. + ``use = egg:hello_world`` in the app section tells the configuration what + application to load. #. *Easier development by automatic template reloading:* In development mode, you shouldn't have to restart the server when editing a Jinja2 template. - ``reload_templates = true`` sets this policy, which might be different in - production. + ``pyramid.reload_templates = true`` sets this policy, which might be + different in production. -Additionally the ``development.ini`` generated by this scaffold wired -up Python's standard logging. We'll now see in the console, for example, -a log on every request that comes in, as well as traceback information. +Additionally the ``development.ini`` generated by this scaffold wired up +Python's standard logging. We'll now see in the console, for example, a log on +every request that comes in, as well as traceback information. .. seealso:: See also: :ref:`Quick Tutorial Application Configuration `, @@ -587,76 +582,77 @@ Easier development with ``debugtoolbar`` ======================================== As we introduce the basics, we also want to show how to be productive in -development and debugging. For example, we just discussed template -reloading and earlier we showed ``--reload`` for application reloading. +development and debugging. For example, we just discussed template reloading +and earlier we showed ``--reload`` for application reloading. -``pyramid_debugtoolbar`` is a popular Pyramid add-on which makes -several tools available in your browser. Adding it to your project -illustrates several points about configuration. +``pyramid_debugtoolbar`` is a popular Pyramid add-on which makes several tools +available in your browser. Adding it to your project illustrates several points +about configuration. -First change your ``setup.py`` to say: +The scaffold ``pyramid_jinja2_starter`` is already configured to include the +add-on ``pyramid_debugtoolbar`` in its ``setup.py``: .. literalinclude:: quick_tour/package/setup.py - :start-after: Start Requires - :end-before: End Requires + :language: python + :linenos: + :lineno-start: 11 + :lines: 11-16 -...and rerun your setup: +It was installed when you previously ran: .. code-block:: bash $ python ./setup.py develop -The Python package ``pyramid_debugtoolbar`` is now installed into our -environment. The package is a Pyramid add-on, which means we need to include -its configuration into our web application. We could do this with imperative -configuration, as we did above for the ``pyramid_jinja2`` add-on: +The ``pyramid_debugtoolbar`` package is a Pyramid add-on, which means we need +to include its configuration into our web application. The ``pyramid_jinja2`` +add-on already took care of this for us in its ``__init__.py``: .. literalinclude:: quick_tour/package/hello_world/__init__.py - :start-after: Start Include - :end-before: End Include + :language: python + :linenos: + :lineno-start: 16 + :lines: 19 -Now that we have a configuration file, we can use the -``pyramid.includes`` facility and place this in our -``development.ini`` instead: +And it uses the ``pyramid.includes`` facility in our ``development.ini``: .. literalinclude:: quick_tour/package/development.ini :language: ini - :start-after: Start Includes - :end-before: End Includes - -You'll now see an attractive (and -collapsible) menu in the right of your browser, providing introspective -access to debugging information. Even better, if your web application -generates an error, you will see a nice traceback on the screen. When -you want to disable this toolbar, there's no need to change code: you can -remove it from ``pyramid.includes`` in the relevant ``.ini`` -configuration file. + :linenos: + :lineno-start: 15 + :lines: 15-16 + +You'll now see a Pyramid logo on the right side of your browser window, which +when clicked opens a new window that provides introspective access to debugging +information. Even better, if your web application generates an error, you will +see a nice traceback on the screen. When you want to disable this toolbar, +there's no need to change code: you can remove it from ``pyramid.includes`` in +the relevant ``.ini`` configuration file. .. seealso:: See also: - :ref:`Quick Tutorial - pyramid_debugtoolbar ` and + :ref:`Quick Tutorial pyramid_debugtoolbar ` and :ref:`pyramid_debugtoolbar ` Unit tests and ``nose`` ======================= -Yikes! We got this far and we haven't yet discussed tests. This is -particularly egregious, as Pyramid has had a deep commitment to full test -coverage since before its release. +Yikes! We got this far and we haven't yet discussed tests. This is particularly +egregious, as Pyramid has had a deep commitment to full test coverage since +before its release. -Our ``pyramid_jinja2_starter`` scaffold generated a ``tests.py`` module -with one unit test in it. To run it, let's install the handy ``nose`` -test runner by editing ``setup.py``. While we're at it, we'll throw in -the ``coverage`` tool which yells at us for code that isn't tested: +Our ``pyramid_jinja2_starter`` scaffold generated a ``tests.py`` module with +one unit test in it. To run it, let's install the handy ``nose`` test runner by +editing ``setup.py``. While we're at it, we'll throw in the ``coverage`` tool +which yells at us for code that isn't tested. Edit line 36 so it becomes the +following: .. code-block:: python + :linenos: + :lineno-start: 36 - setup(name='hello_world', - # Some lines removed... - extras_require={ + tests_require={ 'testing': ['nose', 'coverage'], - } - ) + }, We changed ``setup.py`` which means we need to rerun ``python ./setup.py develop``. We can now run all our tests: @@ -667,124 +663,139 @@ We changed ``setup.py`` which means we need to rerun . Name Stmts Miss Cover Missing --------------------------------------------------- - hello_world 12 8 33% 11-23 - hello_world.models 5 1 80% 8 - hello_world.tests 14 0 100% - hello_world.views 4 0 100% + hello_world 11 8 27% 11-23 + hello_world.models 5 1 80% 8 + hello_world.tests 14 0 100% + hello_world.views 4 0 100% --------------------------------------------------- - TOTAL 35 9 74% + TOTAL 34 9 74% ---------------------------------------------------------------------- - Ran 1 test in 0.931s + Ran 1 test in 0.009s OK Our unit test passed. What did our test look like? .. literalinclude:: quick_tour/package/hello_world/tests.py + :linenos: -Pyramid supplies helpers for test writing, which we use in the -test setup and teardown. Our one test imports the view, -makes a dummy request, and sees if the view returns what we expected. +Pyramid supplies helpers for test writing, which we use in the test setup and +teardown. Our one test imports the view, makes a dummy request, and sees if the +view returns what we expected. .. seealso:: See also: - :ref:`Quick Tutorial Unit Testing `, - :ref:`Quick Tutorial Functional Testing `, - and + :ref:`Quick Tutorial Unit Testing `, :ref:`Quick + Tutorial Functional Testing `, and :ref:`testing_chapter` Logging ======= -It's important to know what is going on inside our web application. -In development we might need to collect some output. In production -we might need to detect situations when other people use the site. We -need *logging*. +It's important to know what is going on inside our web application. In +development we might need to collect some output. In production we might need +to detect situations when other people use the site. We need *logging*. -Fortunately Pyramid uses the normal Python approach to logging. The -scaffold generated in your ``development.ini`` has a number of lines that -configure the logging for you to some reasonable defaults. You then see -messages sent by Pyramid (for example, when a new request comes in). +Fortunately Pyramid uses the normal Python approach to logging. The scaffold +generated in your ``development.ini`` has a number of lines that configure the +logging for you to some reasonable defaults. You then see messages sent by +Pyramid (for example, when a new request comes in). -Maybe you would like to log messages in your code? In your Python -module, import and set up the logging: +Maybe you would like to log messages in your code? In your Python module, +import and set up the logging: .. literalinclude:: quick_tour/package/hello_world/views.py - :start-after: Start Logging 1 - :end-before: End Logging 1 + :language: python + :linenos: + :lineno-start: 3 + :lines: 3-4 You can now, in your code, log messages: .. literalinclude:: quick_tour/package/hello_world/views.py - :start-after: Start Logging 2 - :end-before: End Logging 2 + :language: python + :linenos: + :lineno-start: 9 + :lines: 9-10 + :emphasize-lines: 2 -This will log ``Some Message`` at a ``debug`` log level -to the application-configured logger in your ``development.ini``. What -controls that? These sections in the configuration file: +This will log ``Some Message`` at a ``debug`` log level to the +application-configured logger in your ``development.ini``. What controls that? +These emphasized sections in the configuration file: .. literalinclude:: quick_tour/package/development.ini :language: ini - :start-after: Start Sphinx Include - :end-before: End Sphinx Include + :linenos: + :lineno-start: 36 + :lines: 36-52 + :emphasize-lines: 1-2,14-17 -Our application, a package named ``hello_world``, is set up as a logger -and configured to log messages at a ``DEBUG`` or higher level. When you -visit http://localhost:6543, your console will now show:: +Our application, a package named ``hello_world``, is set up as a logger and +configured to log messages at a ``DEBUG`` or higher level. When you visit +http://localhost:6543, your console will now show:: - 2013-08-09 10:42:42,968 DEBUG [hello_world.views][MainThread] Some Message + 2016-01-18 13:55:55,040 DEBUG [hello_world.views:10][waitress] Some Message .. seealso:: See also: - :ref:`Quick Tutorial Logging ` and - :ref:`logging_chapter` + :ref:`Quick Tutorial Logging ` and :ref:`logging_chapter`. Sessions ======== -When people use your web application, they frequently perform a task -that requires semi-permanent data to be saved. For example, a shopping -cart. This is called a :term:`session`. +When people use your web application, they frequently perform a task that +requires semi-permanent data to be saved. For example, a shopping cart. This is +called a :term:`session`. -Pyramid has basic built-in support for sessions. Third party packages such as -``pyramid_redis_sessions`` provide richer session support. Or you can create -your own custom sessioning engine. Let's take a look at the -:doc:`built-in sessioning support <../narr/sessions>`. In our -``__init__.py`` we first import the kind of sessioning we want: +Pyramid has basic built-in support for sessions. Third party packages such as +``pyramid_redis_sessions`` provide richer session support. Or you can create +your own custom sessioning engine. Let's take a look at the :doc:`built-in +sessioning support <../narr/sessions>`. In our ``__init__.py`` we first import +the kind of sessioning we want: .. literalinclude:: quick_tour/package/hello_world/__init__.py - :start-after: Start Sphinx Include 1 - :end-before: End Sphinx Include 1 + :language: python + :linenos: + :lineno-start: 2 + :lines: 2-3 + :emphasize-lines: 2 .. warning:: - As noted in the session docs, this example implementation is - not intended for use in settings with security implications. + As noted in the session docs, this example implementation is not intended + for use in settings with security implications. Now make a "factory" and pass it to the :term:`configurator`'s ``session_factory`` argument: .. literalinclude:: quick_tour/package/hello_world/__init__.py - :start-after: Start Sphinx Include 2 - :end-before: End Sphinx Include 2 + :language: python + :linenos: + :lineno-start: 13 + :lines: 13-17 + :emphasize-lines: 3-5 -Pyramid's :term:`request` object now has a ``session`` attribute -that we can use in our view code: +Pyramid's :term:`request` object now has a ``session`` attribute that we can +use in our view code in ``views.py``: .. literalinclude:: quick_tour/package/hello_world/views.py - :start-after: Start Sphinx Include 1 - :end-before: End Sphinx Include 1 + :language: python + :linenos: + :lineno-start: 9 + :lines: 9-15 + :emphasize-lines: 3-7 -With this, each reload will increase the counter displayed in our -Jinja2 template: +We need to update our Jinja2 template to show counter increment in the session: .. literalinclude:: quick_tour/package/hello_world/templates/mytemplate.jinja2 :language: jinja - :start-after: Start Sphinx Include 1 - :end-before: End Sphinx Include 1 + :linenos: + :lineno-start: 40 + :lines: 40-42 + :emphasize-lines: 3 .. seealso:: See also: - :ref:`Quick Tutorial Sessions `, - :ref:`sessions_chapter`, :ref:`flash_messages`, - :ref:`session_module`, and :term:`pyramid_redis_sessions`. + :ref:`Quick Tutorial Sessions `, :ref:`sessions_chapter`, + :ref:`flash_messages`, :ref:`session_module`, and + :term:`pyramid_redis_sessions`. Databases ========= diff --git a/docs/quick_tour/package/MANIFEST.in b/docs/quick_tour/package/MANIFEST.in index 18fbd855c..1d0352f7d 100644 --- a/docs/quick_tour/package/MANIFEST.in +++ b/docs/quick_tour/package/MANIFEST.in @@ -1,2 +1,2 @@ include *.txt *.ini *.cfg *.rst -recursive-include hello_world *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml +recursive-include hello_world *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.jinja2 *.js *.html *.xml diff --git a/docs/quick_tour/package/development.ini b/docs/quick_tour/package/development.ini index a3a73e885..20f9817a9 100644 --- a/docs/quick_tour/package/development.ini +++ b/docs/quick_tour/package/development.ini @@ -1,37 +1,41 @@ -# Start Includes -[app:hello_world] -pyramid.includes = pyramid_debugtoolbar -# End Includes +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.6-branch/narr/environment.html +### + +[app:main] use = egg:hello_world -reload_templates = true -debug_authorization = false -debug_notfound = false -debug_routematch = false -debug_templates = true -default_locale_name = en -jinja2.directories = hello_world:templates - -[pipeline:main] -pipeline = - hello_world + +pyramid.reload_templates = true +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.debug_templates = true +pyramid.default_locale_name = en +pyramid.includes = + pyramid_debugtoolbar + +# 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:pyramid#wsgiref -host = 0.0.0.0 +use = egg:waitress#main +host = 127.0.0.1 port = 6543 -# Begin logging configuration +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.6-branch/narr/logging.html +### -# Start Sphinx Include [loggers] keys = root, hello_world -[logger_hello_world] -level = DEBUG -handlers = -qualname = hello_world -# End Sphinx Include - [handlers] keys = console @@ -42,6 +46,11 @@ keys = generic level = INFO handlers = console +[logger_hello_world] +level = DEBUG +handlers = +qualname = hello_world + [handler_console] class = StreamHandler args = (sys.stderr,) @@ -49,6 +58,4 @@ level = NOTSET formatter = generic [formatter_generic] -format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s - -# End logging configuration +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/quick_tour/package/hello_world/__init__.py b/docs/quick_tour/package/hello_world/__init__.py index 4a4fbec30..97f93d5a8 100644 --- a/docs/quick_tour/package/hello_world/__init__.py +++ b/docs/quick_tour/package/hello_world/__init__.py @@ -1,10 +1,7 @@ from pyramid.config import Configurator -from pyramid_jinja2 import renderer_factory -# Start Sphinx Include 1 +from hello_world.resources import get_root from pyramid.session import SignedCookieSessionFactory -# End Sphinx Include 1 -from hello_world.models import get_root def main(global_config, **settings): """ This function returns a WSGI application. @@ -15,20 +12,15 @@ def main(global_config, **settings): settings = dict(settings) settings.setdefault('jinja2.i18n.domain', 'hello_world') - # Start Sphinx Include 2 my_session_factory = SignedCookieSessionFactory('itsaseekreet') config = Configurator(root_factory=get_root, settings=settings, session_factory=my_session_factory) - # End Sphinx Include 2 config.add_translation_dirs('locale/') - # Start Include config.include('pyramid_jinja2') - # End Include - config.add_static_view('static', 'static') config.add_view('hello_world.views.my_view', - context='hello_world.models.MyModel', - renderer="mytemplate.jinja2") + context='hello_world.resources.MyResource', + renderer="templates/mytemplate.jinja2") return config.make_wsgi_app() diff --git a/docs/quick_tour/package/hello_world/init.py b/docs/quick_tour/package/hello_world/init.py deleted file mode 100644 index 5b5f6a118..000000000 --- a/docs/quick_tour/package/hello_world/init.py +++ /dev/null @@ -1,34 +0,0 @@ -from pyramid.config import Configurator -from pyramid_jinja2 import renderer_factory -# Start Sphinx 1 -from pyramid.session import SignedCookieSessionFactory -# End Sphinx 1 - -from hello_world.models import get_root - -def main(global_config, **settings): - """ This function returns a WSGI application. - - It is usually called by the PasteDeploy framework during - ``paster serve``. - """ - settings = dict(settings) - settings.setdefault('jinja2.i18n.domain', 'hello_world') - - config = Configurator(root_factory=get_root, settings=settings) - config.add_translation_dirs('locale/') - # Start Include - config.include('pyramid_jinja2') - # End Include - - # Start Sphinx Include 2 - my_session_factory = SignedCookieSessionFactory('itsaseekreet') - config = Configurator(session_factory=my_session_factory) - # End Sphinx Include 2 - - config.add_static_view('static', 'static') - config.add_view('hello_world.views.my_view', - context='hello_world.models.MyModel', - renderer="mytemplate.jinja2") - - return config.make_wsgi_app() diff --git a/docs/quick_tour/package/hello_world/models.py b/docs/quick_tour/package/hello_world/models.py deleted file mode 100644 index edd361c9c..000000000 --- a/docs/quick_tour/package/hello_world/models.py +++ /dev/null @@ -1,8 +0,0 @@ -class MyModel(object): - pass - -root = MyModel() - - -def get_root(request): - return root diff --git a/docs/quick_tour/package/hello_world/resources.py b/docs/quick_tour/package/hello_world/resources.py new file mode 100644 index 000000000..e89c2f363 --- /dev/null +++ b/docs/quick_tour/package/hello_world/resources.py @@ -0,0 +1,8 @@ +class MyResource(object): + pass + +root = MyResource() + + +def get_root(request): + return root diff --git a/docs/quick_tour/package/hello_world/static/logo.png b/docs/quick_tour/package/hello_world/static/logo.png deleted file mode 100644 index 88f5d9865..000000000 Binary files a/docs/quick_tour/package/hello_world/static/logo.png and /dev/null differ diff --git a/docs/quick_tour/package/hello_world/static/pylons.css b/docs/quick_tour/package/hello_world/static/pylons.css deleted file mode 100644 index 42e2e320e..000000000 --- a/docs/quick_tour/package/hello_world/static/pylons.css +++ /dev/null @@ -1,73 +0,0 @@ -html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,font,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td{margin:0;padding:0;border:0;outline:0;font-size:100%;/* 16px */ -vertical-align:baseline;background:transparent;} -body{line-height:1;} -ol,ul{list-style:none;} -blockquote,q{quotes:none;} -blockquote:before,blockquote:after,q:before,q:after{content:'';content:none;} -/* remember to define focus styles! */ -:focus{outline:0;} -/* remember to highlight inserts somehow! */ -ins{text-decoration:none;} -del{text-decoration:line-through;} -/* tables still need 'cellspacing="0"' in the markup */ -table{border-collapse:collapse;border-spacing:0;} -/* restyling */ -sub{vertical-align:sub;font-size:smaller;line-height:normal;} -sup{vertical-align:super;font-size:smaller;line-height:normal;} -/* lists */ -ul,menu,dir{display:block;list-style-type:disc;margin:1em 0;padding-left:40px;} -ol{display:block;list-style-type:decimal-leading-zero;margin:1em 0;padding-left:40px;} -li{display:list-item;} -/* nested lists have no top/bottom margins */ -ul ul,ul ol,ul dir,ul menu,ul dl,ol ul,ol ol,ol dir,ol menu,ol dl,dir ul,dir ol,dir dir,dir menu,dir dl,menu ul,menu ol,menu dir,menu menu,menu dl,dl ul,dl ol,dl dir,dl menu,dl dl{margin-top:0;margin-bottom:0;} -/* 2 deep unordered lists use a circle */ -ol ul,ul ul,menu ul,dir ul,ol menu,ul menu,menu menu,dir menu,ol dir,ul dir,menu dir,dir dir{list-style-type:circle;} -/* 3 deep (or more) unordered lists use a square */ -ol ol ul,ol ul ul,ol menu ul,ol dir ul,ol ol menu,ol ul menu,ol menu menu,ol dir menu,ol ol dir,ol ul dir,ol menu dir,ol dir dir,ul ol ul,ul ul ul,ul menu ul,ul dir ul,ul ol menu,ul ul menu,ul menu menu,ul dir menu,ul ol dir,ul ul dir,ul menu dir,ul dir dir,menu ol ul,menu ul ul,menu menu ul,menu dir ul,menu ol menu,menu ul menu,menu menu menu,menu dir menu,menu ol dir,menu ul dir,menu menu dir,menu dir dir,dir ol ul,dir ul ul,dir menu ul,dir dir ul,dir ol menu,dir ul menu,dir menu menu,dir dir menu,dir ol dir,dir ul dir,dir menu dir,dir dir dir{list-style-type:square;} -.hidden{display:none;} -p{line-height:1.5em;} -h1{font-size:1.75em;/* 28px */ -line-height:1.7em;font-family:helvetica,verdana;} -h2{font-size:1.5em;/* 24px */ -line-height:1.7em;font-family:helvetica,verdana;} -h3{font-size:1.25em;/* 20px */ -line-height:1.7em;font-family:helvetica,verdana;} -h4{font-size:1em;line-height:1.7em;font-family:helvetica,verdana;} -html,body{width:100%;height:100%;} -body{margin:0;padding:0;background-color:#ffffff;position:relative;font:16px/24px "Nobile","Lucida Grande",Lucida,Verdana,sans-serif;} -a{color:#1b61d6;text-decoration:none;} -a:hover{color:#e88f00;text-decoration:underline;} -body h1, -body h2, -body h3, -body h4, -body h5, -body h6{font-family:"Nobile","Lucida Grande",Lucida,Verdana,sans-serif;font-weight:normal;color:#144fb2;font-style:normal;} -#wrap {min-height: 100%;} -#header,#footer{width:100%;color:#ffffff;height:40px;position:absolute;text-align:center;line-height:40px;overflow:hidden;font-size:12px;} -#header{background-color:#e88f00;top:0;font-size:14px;} -#footer{background-color:#000000;bottom:0;position: relative;margin-top:-40px;clear:both;} -.header,.footer{width:700px;margin-right:auto;margin-left:auto;} -.wrapper{width:100%} -#top,#bottom{width:100%;} -#top{color:#888;background-color:#eee;height:300px;border-bottom:2px solid #ddd;} -#bottom{color:#222;background-color:#ffffff;overflow:auto;padding-bottom:80px;} -.top,.bottom{width:700px;margin-right:auto;margin-left:auto;} -.top{padding-top:100px;} -.app-welcome{margin-top:25px;} -.app-name{color:#000000;font-weight:bold;} -.bottom{padding-top:50px;} -#left{width:325px;float:left;padding-right:25px;} -#right{width:325px;float:right;padding-left:25px;} -.align-left{text-align:left;} -.align-right{text-align:right;} -.align-center{text-align:center;} -ul.links{margin:0;padding:0;} -ul.links li{list-style-type:none;font-size:14px;} -form{border-style:none;} -fieldset{border-style:none;} -input{color:#222;border:1px solid #ccc;font-family:sans-serif;font-size:12px;line-height:16px;} -input[type=text]{} -input[type=submit]{background-color:#ddd;font-weight:bold;} -/*Opera Fix*/ -body:before {content:"";height:100%;float:left;width:0;margin-top:-32767px;} diff --git a/docs/quick_tour/package/hello_world/static/pyramid-16x16.png b/docs/quick_tour/package/hello_world/static/pyramid-16x16.png new file mode 100644 index 000000000..979203112 Binary files /dev/null and b/docs/quick_tour/package/hello_world/static/pyramid-16x16.png differ diff --git a/docs/quick_tour/package/hello_world/static/pyramid.png b/docs/quick_tour/package/hello_world/static/pyramid.png new file mode 100644 index 000000000..4ab837be9 Binary files /dev/null and b/docs/quick_tour/package/hello_world/static/pyramid.png differ diff --git a/docs/quick_tour/package/hello_world/static/theme.css b/docs/quick_tour/package/hello_world/static/theme.css new file mode 100644 index 000000000..e3cf3f290 --- /dev/null +++ b/docs/quick_tour/package/hello_world/static/theme.css @@ -0,0 +1,153 @@ +@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700); +body { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; + color: #ffffff; + background: #bc2131; +} +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; +} +p { + font-weight: 300; +} +.font-normal { + font-weight: 400; +} +.font-semi-bold { + font-weight: 600; +} +.font-bold { + font-weight: 700; +} +.starter-template { + margin-top: 250px; +} +.starter-template .content { + margin-left: 10px; +} +.starter-template .content h1 { + margin-top: 10px; + font-size: 60px; +} +.starter-template .content h1 .smaller { + font-size: 40px; + color: #f2b7bd; +} +.starter-template .content .lead { + font-size: 25px; + color: #f2b7bd; +} +.starter-template .content .lead .font-normal { + color: #ffffff; +} +.starter-template .links { + float: right; + right: 0; + margin-top: 125px; +} +.starter-template .links ul { + display: block; + padding: 0; + margin: 0; +} +.starter-template .links ul li { + list-style: none; + display: inline; + margin: 0 10px; +} +.starter-template .links ul li:first-child { + margin-left: 0; +} +.starter-template .links ul li:last-child { + margin-right: 0; +} +.starter-template .links ul li.current-version { + color: #f2b7bd; + font-weight: 400; +} +.starter-template .links ul li a { + color: #ffffff; +} +.starter-template .links ul li a:hover { + text-decoration: underline; +} +.starter-template .links ul li .icon-muted { + color: #eb8b95; + margin-right: 5px; +} +.starter-template .links ul li:hover .icon-muted { + color: #ffffff; +} +.starter-template .copyright { + margin-top: 10px; + font-size: 0.9em; + color: #f2b7bd; + text-transform: lowercase; + float: right; + right: 0; +} +@media (max-width: 1199px) { + .starter-template .content h1 { + font-size: 45px; + } + .starter-template .content h1 .smaller { + font-size: 30px; + } + .starter-template .content .lead { + font-size: 20px; + } +} +@media (max-width: 991px) { + .starter-template { + margin-top: 0; + } + .starter-template .logo { + margin: 40px auto; + } + .starter-template .content { + margin-left: 0; + text-align: center; + } + .starter-template .content h1 { + margin-bottom: 20px; + } + .starter-template .links { + float: none; + text-align: center; + margin-top: 60px; + } + .starter-template .copyright { + float: none; + text-align: center; + } +} +@media (max-width: 767px) { + .starter-template .content h1 .smaller { + font-size: 25px; + display: block; + } + .starter-template .content .lead { + font-size: 16px; + } + .starter-template .links { + margin-top: 40px; + } + .starter-template .links ul li { + display: block; + margin: 0; + } + .starter-template .links ul li .icon-muted { + display: none; + } + .starter-template .copyright { + margin-top: 20px; + } +} + diff --git a/docs/quick_tour/package/hello_world/templates/mytemplate.jinja2 b/docs/quick_tour/package/hello_world/templates/mytemplate.jinja2 index 25a28ed7a..a6089aebc 100644 --- a/docs/quick_tour/package/hello_world/templates/mytemplate.jinja2 +++ b/docs/quick_tour/package/hello_world/templates/mytemplate.jinja2 @@ -1,90 +1,72 @@ - - - - The Pyramid Web Framework - - - - - - - - - - -
- -
-
- Logo -

- Welcome to {{project}}, an application generated by
- the Pyramid Web Framework. -

-
-
-
-
-

{% trans %}Hello!{% endtrans %}

- -

Counter: {{ request.session.counter }}

- -

Request performed with {{ request.locale_name }} locale.

+ + + + + + + + + + + Starter Scaffold for Pyramid Jinja2 + + + + + + + + + + + + + +
+
+
+
+
-
-
-

Search Pyramid documentation

-
- - -
-
- -
-
-
- - +
+
+

+ Pyramid + Jinja2 scaffold +

+

+ {% trans %}Hello{% endtrans %} to {{project}}, an application generated by
the Pyramid Web Framework 1.6.

+

Counter: {{ request.session.counter }}

+
+
+
+
+ +
+
+ +
+
+
+ + + + + + + diff --git a/docs/quick_tour/package/hello_world/views.py b/docs/quick_tour/package/hello_world/views.py index 109c260ad..9f7953c8e 100644 --- a/docs/quick_tour/package/hello_world/views.py +++ b/docs/quick_tour/package/hello_world/views.py @@ -1,22 +1,16 @@ -# Start Logging 1 +from pyramid.i18n import TranslationStringFactory + import logging log = logging.getLogger(__name__) -# End Logging 1 - -from pyramid.i18n import TranslationStringFactory _ = TranslationStringFactory('hello_world') def my_view(request): - # Start Logging 2 log.debug('Some Message') - # End Logging 2 - # Start Sphinx Include 1 session = request.session if 'counter' in session: session['counter'] += 1 else: session['counter'] = 0 - # End Sphinx Include 1 return {'project': 'hello_world'} diff --git a/docs/quick_tour/package/setup.cfg b/docs/quick_tour/package/setup.cfg new file mode 100644 index 000000000..186e796fc --- /dev/null +++ b/docs/quick_tour/package/setup.cfg @@ -0,0 +1,28 @@ +[nosetests] +match = ^test +nocapture = 1 +cover-package = hello_world +with-coverage = 1 +cover-erase = 1 + +[compile_catalog] +directory = hello_world/locale +domain = hello_world +statistics = true + +[extract_messages] +add_comments = TRANSLATORS: +output_file = hello_world/locale/hello_world.pot +width = 80 +mapping_file = message-extraction.ini + +[init_catalog] +domain = hello_world +input_file = hello_world/locale/hello_world.pot +output_dir = hello_world/locale + +[update_catalog] +domain = hello_world +input_file = hello_world/locale/hello_world.pot +output_dir = hello_world/locale +previous = true diff --git a/docs/quick_tour/package/setup.py b/docs/quick_tour/package/setup.py index f118ed5fb..61ed3c406 100644 --- a/docs/quick_tour/package/setup.py +++ b/docs/quick_tour/package/setup.py @@ -3,12 +3,17 @@ import os from setuptools import setup, find_packages here = os.path.abspath(os.path.dirname(__file__)) -README = open(os.path.join(here, 'README.txt')).read() -CHANGES = open(os.path.join(here, 'CHANGES.txt')).read() +with open(os.path.join(here, 'README.txt')) as f: + README = f.read() +with open(os.path.join(here, 'CHANGES.txt')) as f: + CHANGES = f.read() -# Start Requires -requires = ['pyramid>=1.0.2', 'pyramid_jinja2', 'pyramid_debugtoolbar'] -# End Requires +requires = [ + 'pyramid', + 'pyramid_jinja2', + 'pyramid_debugtoolbar', + 'waitress', +] setup(name='hello_world', version='0.0', @@ -16,7 +21,7 @@ setup(name='hello_world', long_description=README + '\n\n' + CHANGES, classifiers=[ "Programming Language :: Python", - "Framework :: Pylons", + "Framework :: Pyramid", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", ], @@ -28,14 +33,12 @@ setup(name='hello_world', include_package_data=True, zip_safe=False, install_requires=requires, - tests_require=requires, + tests_require={ + 'testing': ['nose', 'coverage'], + }, test_suite="hello_world", entry_points="""\ [paste.app_factory] main = hello_world:main """, - paster_plugins=['pyramid'], - extras_require={ - 'testing': ['nose', ], - } -) \ No newline at end of file + ) -- cgit v1.2.3 From b89295d7cfc26b24719692fa96aec24a8e1bd7ad Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Tue, 19 Jan 2016 11:13:11 -0800 Subject: workaround for #2251. See also https://github.com/sphinx-doc/sphinx/issues/2253 --- docs/quick_tour/requests/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quick_tour/requests/app.py b/docs/quick_tour/requests/app.py index 7ac81eb50..815714464 100644 --- a/docs/quick_tour/requests/app.py +++ b/docs/quick_tour/requests/app.py @@ -4,7 +4,7 @@ from pyramid.response import Response def hello_world(request): - # Some parameters from a request such as /?name=lisa + """ Some parameters from a request such as /?name=lisa """ url = request.url name = request.params.get('name', 'No Name Provided') -- cgit v1.2.3 From 78fb6c668362cd050bbdac4e1639cce4a01e35df Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Wed, 20 Jan 2016 00:16:29 -0700 Subject: Remove Python 3.2 from travis --- .travis.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5c53b43f6..39f0ca435 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,8 +8,6 @@ matrix: env: TOXENV=py26 - python: 2.7 env: TOXENV=py27 - - python: 3.2 - env: TOXENV=py32 - python: 3.3 env: TOXENV=py33 - python: 3.4 -- cgit v1.2.3 From ddbd96d56431583a372d3aaeed9dc262b142b2a9 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Wed, 20 Jan 2016 00:16:51 -0700 Subject: Increase lowest Py3 version to Python 3.3 --- setup.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index daccd3258..211c0a4ec 100644 --- a/setup.py +++ b/setup.py @@ -18,12 +18,13 @@ import sys from setuptools import setup, find_packages py_version = sys.version_info[:2] +is_pypy = '__pypy__' in sys.builtin_module_names PY3 = py_version[0] == 3 if PY3: - if py_version < (3, 2): - raise RuntimeError('On Python 3, Pyramid requires Python 3.2 or better') + if py_version < (3, 3) and not is_pypy: # PyPy3 masquerades as Python 3.2... + raise RuntimeError('On Python 3, Pyramid requires Python 3.3 or better') else: if py_version < (2, 6): raise RuntimeError('On Python 2, Pyramid requires Python 2.6 or better') @@ -81,7 +82,6 @@ setup(name='pyramid', "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.2", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", -- cgit v1.2.3 From b74126bcfd716dad9851c50464da6f01481df5e5 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Wed, 20 Jan 2016 00:17:01 -0700 Subject: Remove Python 3.2 from tox --- tox.ini | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 626931faf..096600aec 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] envlist = - py26,py27,py32,py33,py34,py35,pypy,pypy3, + py26,py27,py33,py34,py35,pypy,pypy3, docs,pep8, {py2,py3}-cover,coverage, @@ -10,7 +10,6 @@ envlist = basepython = py26: python2.6 py27: python2.7 - py32: python3.2 py33: python3.3 py34: python3.4 py35: python3.5 -- cgit v1.2.3 From d49c69250b406597a4796d158cf6540469ade414 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Wed, 20 Jan 2016 00:18:41 -0800 Subject: Do not use trailing . on versionadded rst directive. It automatically adds a . --- docs/narr/hooks.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index 6aa1a99c2..7ff119b53 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -262,7 +262,7 @@ already constructed a :term:`configurator`, it can also be registered via the Adding Methods or Properties to a Request Object ------------------------------------------------ -.. versionadded:: 1.4. +.. versionadded:: 1.4 Since each Pyramid application can only have one :term:`request` factory, :ref:`changing the request factory ` is not that -- cgit v1.2.3 From 4064028cadc63ed1aceb14e6c88827b88b12f839 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Wed, 20 Jan 2016 00:38:09 -0800 Subject: Update docs to reflect dropping Python 3.2 support --- docs/narr/install.rst | 4 ++-- docs/narr/introduction.rst | 10 +++++----- docs/quick_tutorial/requirements.rst | 2 +- docs/tutorials/wiki/installation.rst | 4 ++-- docs/tutorials/wiki2/installation.rst | 4 ++-- docs/whatsnew-1.3.rst | 2 +- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/narr/install.rst b/docs/narr/install.rst index 381e325df..c4e3e2c5f 100644 --- a/docs/narr/install.rst +++ b/docs/narr/install.rst @@ -15,8 +15,8 @@ You will need `Python `_ version 2.6 or better to run .. sidebar:: Python Versions As of this writing, :app:`Pyramid` has been tested under Python 2.6, Python - 2.7, Python 3.2, Python 3.3, Python 3.4, Python 3.5, PyPy, and PyPy3. - :app:`Pyramid` does not run under any version of Python before 2.6. + 2.7, Python 3.3, Python 3.4, Python 3.5, PyPy, and PyPy3. :app:`Pyramid` + does not run under any version of Python before 2.6. :app:`Pyramid` is known to run on all popular UNIX-like systems such as Linux, Mac OS X, and FreeBSD as well as on Windows platforms. It is also known to run diff --git a/docs/narr/introduction.rst b/docs/narr/introduction.rst index 422db557e..8db52dc21 100644 --- a/docs/narr/introduction.rst +++ b/docs/narr/introduction.rst @@ -860,11 +860,11 @@ Every release of Pyramid has 100% statement coverage via unit and integration tests, as measured by the ``coverage`` tool available on PyPI. It also has greater than 95% decision/condition coverage as measured by the ``instrumental`` tool available on PyPI. It is automatically tested by the -Jenkins tool on Python 2.6, Python 2.7, Python 3.2, Python 3.3, Python 3.4, -Python 3.5, PyPy, and PyPy3 after each commit to its GitHub repository. -Official Pyramid add-ons are held to a similar testing standard. We still find -bugs in Pyramid and its official add-ons, but we've noticed we find a lot more -of them while working on other projects that don't have a good testing regime. +Jenkins tool on Python 2.6, Python 2.7, Python 3.3, Python 3.4, Python 3.5, +PyPy, and PyPy3 after each commit to its GitHub repository. Official Pyramid +add-ons are held to a similar testing standard. We still find bugs in Pyramid +and its official add-ons, but we've noticed we find a lot more of them while +working on other projects that don't have a good testing regime. Example: http://jenkins.pylonsproject.org/ diff --git a/docs/quick_tutorial/requirements.rst b/docs/quick_tutorial/requirements.rst index a737ede0e..6d5a965cd 100644 --- a/docs/quick_tutorial/requirements.rst +++ b/docs/quick_tutorial/requirements.rst @@ -19,7 +19,7 @@ make an isolated environment, and setup packaging tools.) This *Quick Tutorial* is based on: -* **Python 3.3**. Pyramid fully supports Python 3.2+ and Python 2.6+. +* **Python 3.3**. Pyramid fully supports Python 3.3+ and Python 2.6+. This tutorial uses **Python 3.3** but runs fine under Python 2.7. * **pyvenv**. We believe in virtual environments. For this tutorial, diff --git a/docs/tutorials/wiki/installation.rst b/docs/tutorials/wiki/installation.rst index 20df389c6..ff5cac4c9 100644 --- a/docs/tutorials/wiki/installation.rst +++ b/docs/tutorials/wiki/installation.rst @@ -65,11 +65,11 @@ Python 2.7: c:\> c:\Python27\Scripts\virtualenv %VENV% -Python 3.2: +Python 3.3: .. code-block:: text - c:\> c:\Python32\Scripts\virtualenv %VENV% + c:\> c:\Python33\Scripts\virtualenv %VENV% Install Pyramid and tutorial dependencies into the virtual Python environment ----------------------------------------------------------------------------- diff --git a/docs/tutorials/wiki2/installation.rst b/docs/tutorials/wiki2/installation.rst index 1385ab8c7..0715805e6 100644 --- a/docs/tutorials/wiki2/installation.rst +++ b/docs/tutorials/wiki2/installation.rst @@ -65,11 +65,11 @@ Python 2.7: c:\> c:\Python27\Scripts\virtualenv %VENV% -Python 3.2: +Python 3.3: .. code-block:: text - c:\> c:\Python32\Scripts\virtualenv %VENV% + c:\> c:\Python33\Scripts\virtualenv %VENV% Install Pyramid into the virtual Python environment --------------------------------------------------- diff --git a/docs/whatsnew-1.3.rst b/docs/whatsnew-1.3.rst index dd3e1b8dd..8de69c450 100644 --- a/docs/whatsnew-1.3.rst +++ b/docs/whatsnew-1.3.rst @@ -18,7 +18,7 @@ Python 3 Compatibility .. image:: python-3.png Pyramid continues to run on Python 2, but Pyramid is now also Python 3 -compatible. To use Pyramid under Python 3, Python 3.2 or better is required. +compatible. To use Pyramid under Python 3, Python 3.3 or better is required. Many Pyramid add-ons are already Python 3 compatible. For example, ``pyramid_debugtoolbar``, ``pyramid_jinja2``, ``pyramid_exclog``, -- cgit v1.2.3 From 0e1ad7e807a0638943fe8a050da551266567cb06 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Wed, 20 Jan 2016 00:43:08 -0800 Subject: fix underline for heading --- docs/tutorials/wiki2/installation.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorials/wiki2/installation.rst b/docs/tutorials/wiki2/installation.rst index 1385ab8c7..906f00cd7 100644 --- a/docs/tutorials/wiki2/installation.rst +++ b/docs/tutorials/wiki2/installation.rst @@ -392,7 +392,7 @@ page. You can read more about the purpose of the icon at application while you develop. Decisions the ``alchemy`` scaffold has made for you -================================================================= +=================================================== Creating a project using the ``alchemy`` scaffold makes the following assumptions: -- cgit v1.2.3 From 8f364675b6cd9527a482462191c18f8b3fc22d83 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Thu, 21 Jan 2016 05:09:10 -0800 Subject: minor grammar fixes. --- docs/quick_tour.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/quick_tour.rst b/docs/quick_tour.rst index 82209f623..f5f28f86a 100644 --- a/docs/quick_tour.rst +++ b/docs/quick_tour.rst @@ -43,11 +43,11 @@ production support for Python 3 in October 2011). .. note:: - Why ``easy_install`` and not ``pip``? Some distributions on which Pyramid - depends upon have optional C extensions for performance. ``pip`` cannot + Why ``easy_install`` and not ``pip``? Some distributions upon which + Pyramid depends have optional C extensions for performance. ``pip`` cannot install some binary Python distributions. With ``easy_install``, Windows users are able to obtain binary Python distributions, so they get the - benefit of the C extensions without needing a C compiler. Also, there can + benefit of the C extensions without needing a C compiler. Also there can be issues when ``pip`` and ``easy_install`` are used side-by-side in the same environment, so we chose to recommend ``easy_install`` for the sake of reducing the complexity of these instructions. -- cgit v1.2.3 From 257ac062342d5b2cd18b47737cf9fb94aa528b8a Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Fri, 22 Jan 2016 00:29:38 -0800 Subject: Overhaul Quick Tour: start to "Quick project startup with scaffolds" --- docs/quick_tour.rst | 439 +++++++++++---------- docs/quick_tour/hello_world/app.py | 2 +- docs/quick_tour/jinja2/app.py | 2 +- docs/quick_tour/jinja2/views.py | 2 - docs/quick_tour/json/app.py | 4 +- docs/quick_tour/json/hello_world.jinja2 | 4 +- docs/quick_tour/json/hello_world.pt | 17 - docs/quick_tour/json/views.py | 4 +- docs/quick_tour/requests/app.py | 2 +- docs/quick_tour/routing/app.py | 2 - docs/quick_tour/routing/views.py | 2 - docs/quick_tour/static_assets/app.py | 4 +- docs/quick_tour/static_assets/hello_world.jinja2 | 2 +- docs/quick_tour/static_assets/hello_world.pt | 16 - .../static_assets/hello_world_static.jinja2 | 10 + docs/quick_tour/static_assets/views.py | 2 +- docs/quick_tour/templating/app.py | 3 +- docs/quick_tour/templating/views.py | 2 - docs/quick_tour/view_classes/app.py | 6 +- docs/quick_tour/view_classes/hello.jinja2 | 8 +- docs/quick_tour/view_classes/views.py | 2 - docs/quick_tour/views/app.py | 2 +- 22 files changed, 256 insertions(+), 281 deletions(-) delete mode 100644 docs/quick_tour/json/hello_world.pt delete mode 100644 docs/quick_tour/static_assets/hello_world.pt create mode 100644 docs/quick_tour/static_assets/hello_world_static.jinja2 diff --git a/docs/quick_tour.rst b/docs/quick_tour.rst index f5f28f86a..220fd4bca 100644 --- a/docs/quick_tour.rst +++ b/docs/quick_tour.rst @@ -5,19 +5,20 @@ Quick Tour of Pyramid ===================== Pyramid lets you start small and finish big. This *Quick Tour* of Pyramid is -for those who want to evaluate Pyramid, whether you are new to Python -web frameworks, or a pro in a hurry. For more detailed treatment of -each topic, give the :ref:`quick_tutorial` a try. +for those who want to evaluate Pyramid, whether you are new to Python web +frameworks, or a pro in a hurry. For more detailed treatment of each topic, +give the :ref:`quick_tutorial` a try. + Installation ============ -Once you have a standard Python environment setup, getting started with -Pyramid is a breeze. Unfortunately "standard" is not so simple in Python. -For this Quick Tour, it means: `Python `_, -a `virtual environment `_ (or -`virtualenv for Python 2.7 `_), and -`setuptools `_. +Once you have a standard Python environment setup, getting started with Pyramid +is a breeze. Unfortunately "standard" is not so simple in Python. For this +Quick Tour, it means `Python `_, a `virtual +environment `_ (or `virtualenv +for Python 2.7 `_), and `setuptools +`_. As an example, for Python 3.3+ on Linux: @@ -37,9 +38,9 @@ For Windows: c:\\> env33\\Scripts\\python ez_setup.py c:\\> env33\\Scripts\\easy_install "pyramid==\ |release|\ " -Of course Pyramid runs fine on Python 2.6+, as do the examples in this -*Quick Tour*. We're just showing Python 3 a little love (Pyramid had -production support for Python 3 in October 2011). +Of course Pyramid runs fine on Python 2.6+, as do the examples in this *Quick +Tour*. We're just showing Python 3 a little love (Pyramid had production +support for Python 3 in October 2011). .. note:: @@ -54,15 +55,15 @@ production support for Python 3 in October 2011). .. seealso:: See also: :ref:`Quick Tutorial section on Requirements `, - :ref:`installing_unix`, - :ref:`Before You Install `, and - :ref:`Installing Pyramid on a Windows System ` + :ref:`installing_unix`, :ref:`Before You Install `, and + :ref:`Installing Pyramid on a Windows System `. + Hello World =========== -Microframeworks have shown that learning starts best from a very small -first step. Here's a tiny application in Pyramid: +Microframeworks have shown that learning starts best from a very small first +step. Here's a tiny application in Pyramid: .. literalinclude:: quick_tour/hello_world/app.py :linenos: @@ -79,65 +80,63 @@ World!`` message. New to Python web programming? If so, some lines in the module merit explanation: -#. *Line 10*. The ``if __name__ == '__main__':`` is Python's way of - saying "Start here when running from the command line". +#. *Line 10*. ``if __name__ == '__main__':`` is Python's way of saying "Start + here when running from the command line". -#. *Lines 11-13*. Use Pyramid's :term:`configurator` to connect - :term:`view` code to a particular URL :term:`route`. +#. *Lines 11-13*. Use Pyramid's :term:`configurator` to connect :term:`view` + code to a particular URL :term:`route`. -#. *Lines 6-7*. Implement the view code that generates the - :term:`response`. +#. *Lines 6-7*. Implement the view code that generates the :term:`response`. #. *Lines 14-16*. Publish a :term:`WSGI` app using an HTTP server. -As shown in this example, the :term:`configurator` plays a central role -in Pyramid development. Building an application from loosely-coupled -parts via :doc:`../narr/configuration` is a central idea in Pyramid, -one that we will revisit regurlarly in this *Quick Tour*. +As shown in this example, the :term:`configurator` plays a central role in +Pyramid development. Building an application from loosely-coupled parts via +:doc:`../narr/configuration` is a central idea in Pyramid, one that we will +revisit regurlarly in this *Quick Tour*. .. seealso:: See also: :ref:`Quick Tutorial Hello World `, - :ref:`firstapp_chapter`, and - :ref:`Todo List Application in One File ` + :ref:`firstapp_chapter`, and :ref:`Todo List Application in One File + `. + Handling web requests and responses =================================== -Developing for the web means processing web requests. As this is a -critical part of a web application, web developers need a robust, -mature set of software for web requests. +Developing for the web means processing web requests. As this is a critical +part of a web application, web developers need a robust, mature set of software +for web requests. -Pyramid has always fit nicely into the existing world of Python web -development (virtual environments, packaging, scaffolding, one of the first to -embrace Python 3, etc.). Pyramid turned to the well-regarded :term:`WebOb` -Python library for request and response handling. In our example above, -Pyramid hands ``hello_world`` a ``request`` that is :ref:`based on WebOb -`. +Pyramid has always fit nicely into the existing world of Python web development +(virtual environments, packaging, scaffolding, one of the first to embrace +Python 3, etc.). Pyramid turned to the well-regarded :term:`WebOb` Python +library for request and response handling. In our example above, Pyramid hands +``hello_world`` a ``request`` that is :ref:`based on WebOb `. Let's see some features of requests and responses in action: .. literalinclude:: quick_tour/requests/app.py :pyobject: hello_world -In this Pyramid view, we get the URL being visited from ``request.url``. Also, +In this Pyramid view, we get the URL being visited from ``request.url``. Also if you visited http://localhost:6543/?name=alice in a browser, the name is included in the body of the response:: URL http://localhost:6543/?name=alice with name: alice -Finally, we set the response's content type and return the Response. +Finally we set the response's content type, and return the Response. .. seealso:: See also: - :ref:`Quick Tutorial Request and Response ` - and - :ref:`webob_chapter` + :ref:`Quick Tutorial Request and Response ` and + :ref:`webob_chapter`. + Views ===== -For the examples above, the ``hello_world`` function is a "view". In -Pyramid, views are the primary way to accept web requests and return -responses. +For the examples above, the ``hello_world`` function is a "view". In Pyramid +views are the primary way to accept web requests and return responses. So far our examples place everything in one file: @@ -149,169 +148,169 @@ So far our examples place everything in one file: - the WSGI application launcher -Let's move the views out to their own ``views.py`` module and change -the ``app.py`` to scan that module, looking for decorators that set up -the views. +Let's move the views out to their own ``views.py`` module and change the +``app.py`` to scan that module, looking for decorators that set up the views. -First, our revised ``app.py``: +First our revised ``app.py``: .. literalinclude:: quick_tour/views/app.py :linenos: -We added some more routes, but we also removed the view code. -Our views and their registrations (via decorators) are now in a module -``views.py``, which is scanned via ``config.scan('views')``. +We added some more routes, but we also removed the view code. Our views and +their registrations (via decorators) are now in a module ``views.py``, which is +scanned via ``config.scan('views')``. -We now have a ``views.py`` module that is focused on handling requests -and responses: +We now have a ``views.py`` module that is focused on handling requests and +responses: .. literalinclude:: quick_tour/views/views.py :linenos: We have four views, each leading to the other. If you start at -http://localhost:6543/, you get a response with a link to the next -view. The ``hello_view`` (available at the URL ``/howdy``) has a link -to the ``redirect_view``, which issues a redirect to the final -view. - -Earlier we saw ``config.add_view`` as one way to configure a view. This -section introduces ``@view_config``. Pyramid's configuration supports -:term:`imperative configuration`, such as the ``config.add_view`` in -the previous example. You can also use :term:`declarative -configuration`, in which a Python :term:`decorator` is placed on the -line above the view. Both approaches result in the same final -configuration, thus usually it is simply a matter of taste. +http://localhost:6543/, you get a response with a link to the next view. The +``hello_view`` (available at the URL ``/howdy``) has a link to the +``redirect_view``, which issues a redirect to the final view. + +Earlier we saw ``config.add_view`` as one way to configure a view. This section +introduces ``@view_config``. Pyramid's configuration supports :term:`imperative +configuration`, such as the ``config.add_view`` in the previous example. You +can also use :term:`declarative configuration` in which a Python +:term:`decorator` is placed on the line above the view. Both approaches result +in the same final configuration, thus usually it is simply a matter of taste. .. seealso:: See also: - :ref:`Quick Tutorial Views `, - :doc:`../narr/views`, - :doc:`../narr/viewconfig`, and - :ref:`debugging_view_configuration` + :ref:`Quick Tutorial Views `, :doc:`../narr/views`, + :doc:`../narr/viewconfig`, and :ref:`debugging_view_configuration`. + Routing ======= -Writing web applications usually means sophisticated URL design. We -just saw some Pyramid machinery for requests and views. Let's look at -features that help in routing. +Writing web applications usually means sophisticated URL design. We just saw +some Pyramid machinery for requests and views. Let's look at features that help +with routing. Above we saw the basics of routing URLs to views in Pyramid: -- Your project's "setup" code registers a route name to be used when - matching part of the URL +- Your project's "setup" code registers a route name to be used when matching + part of the URL. -- Elsewhere a view is configured to be called for that route name +- Elsewhere a view is configured to be called for that route name. .. note:: - Why do this twice? Other Python web frameworks let you create a - route and associate it with a view in one step. As - illustrated in :ref:`routes_need_ordering`, multiple routes might - match the same URL pattern. Rather than provide ways to help guess, - Pyramid lets you be explicit in ordering. Pyramid also gives - facilities to avoid the problem. + Why do this twice? Other Python web frameworks let you create a route and + associate it with a view in one step. As illustrated in + :ref:`routes_need_ordering`, multiple routes might match the same URL + pattern. Rather than provide ways to help guess, Pyramid lets you be + explicit in ordering. Pyramid also gives facilities to avoid the problem. -What if we want part of the URL to be available as data in my view? This -route declaration: +What if we want part of the URL to be available as data in my view? We can use +this route declaration, for example: .. literalinclude:: quick_tour/routing/app.py - :start-after: Start Route 1 - :end-before: End Route 1 + :linenos: + :lines: 6 + :lineno-start: 6 -With this, URLs such as ``/howdy/amy/smith`` will assign ``amy`` to -``first`` and ``smith`` to ``last``. We can then use this data in our -view: +With this, URLs such as ``/howdy/amy/smith`` will assign ``amy`` to ``first`` +and ``smith`` to ``last``. We can then use this data in our view: .. literalinclude:: quick_tour/routing/views.py - :start-after: Start Route 1 - :end-before: End Route 1 + :linenos: + :lines: 5-8 + :lineno-start: 5 + :emphasize-lines: 3 -``request.matchdict`` contains values from the URL that match the -"replacement patterns" (the curly braces) in the route declaration. -This information can then be used in your view. +``request.matchdict`` contains values from the URL that match the "replacement +patterns" (the curly braces) in the route declaration. This information can +then be used in your view. .. seealso:: See also: - :ref:`Quick Tutorial Routing `, - :doc:`../narr/urldispatch`, - :ref:`debug_routematch_section`, and - :doc:`../narr/router` + :ref:`Quick Tutorial Routing `, :doc:`../narr/urldispatch`, + :ref:`debug_routematch_section`, and :doc:`../narr/router`. + Templating ========== -Ouch. We have been making our own ``Response`` and filling the response -body with HTML. You usually won't embed an HTML string directly in -Python, but instead, will use a templating language. +Ouch. We have been making our own ``Response`` and filling the response body +with HTML. You usually won't embed an HTML string directly in Python, but +instead you will use a templating language. -Pyramid doesn't mandate a particular database system, form library, -etc. It encourages replaceability. This applies equally to templating, -which is fortunate: developers have strong views about template -languages. That said, the Pylons Project officially supports bindings for -Chameleon, Jinja2, and Mako, so in this step, let's use Chameleon. +Pyramid doesn't mandate a particular database system, form library, and so on. +It encourages replaceability. This applies equally to templating, which is +fortunate: developers have strong views about template languages. That said, +the Pylons Project officially supports bindings for Chameleon, Jinja2, and +Mako. In this step let's use Chameleon. Let's add ``pyramid_chameleon``, a Pyramid :term:`add-on` which enables -Chameleon as a :term:`renderer` in our Pyramid applications: +Chameleon as a :term:`renderer` in our Pyramid application: .. code-block:: bash $ easy_install pyramid_chameleon -With the package installed, we can include the template bindings into -our configuration: +With the package installed, we can include the template bindings into our +configuration in ``app.py``: -.. code-block:: python +.. literalinclude:: quick_tour/templating/app.py + :linenos: + :lines: 6-8 + :lineno-start: 6 + :emphasize-lines: 2 - config.include('pyramid_chameleon') - -Now lets change our views.py file: +Now lets change our ``views.py`` file: .. literalinclude:: quick_tour/templating/views.py - :start-after: Start View 1 - :end-before: End View 1 + :linenos: + :emphasize-lines: 4,6 -Ahh, that looks better. We have a view that is focused on Python code. -Our ``@view_config`` decorator specifies a :term:`renderer` that points -to our template file. Our view then simply returns data which is then -supplied to our template: +Ahh, that looks better. We have a view that is focused on Python code. Our +``@view_config`` decorator specifies a :term:`renderer` that points to our +template file. Our view then simply returns data which is then supplied to our +template ``hello_world.pt``: .. literalinclude:: quick_tour/templating/hello_world.pt :language: html -Since our view returned ``dict(name=request.matchdict['name'])``, -we can use ``name`` as a variable in our template via -``${name}``. +Since our view returned ``dict(name=request.matchdict['name'])``, we can use +``name`` as a variable in our template via ``${name}``. .. seealso:: See also: :ref:`Quick Tutorial Templating `, - :doc:`../narr/templates`, - :ref:`debugging_templates`, and - :ref:`available_template_system_bindings` + :doc:`../narr/templates`, :ref:`debugging_templates`, and + :ref:`available_template_system_bindings`. -Templating with ``jinja2`` -========================== -We just said Pyramid doesn't prefer one templating language over -another. Time to prove it. Jinja2 is a popular templating system, -modeled after Django's templates. Let's add ``pyramid_jinja2``, -a Pyramid :term:`add-on` which enables Jinja2 as a :term:`renderer` in -our Pyramid applications: +Templating with Jinja2 +====================== + +We just said Pyramid doesn't prefer one templating language over another. Time +to prove it. Jinja2 is a popular templating system, modeled after Django's +templates. Let's add ``pyramid_jinja2``, a Pyramid :term:`add-on` which enables +Jinja2 as a :term:`renderer` in our Pyramid applications: .. code-block:: bash $ easy_install pyramid_jinja2 -With the package installed, we can include the template bindings into -our configuration: - -.. code-block:: python +With the package installed, we can include the template bindings into our +configuration: - config.include('pyramid_jinja2') +.. literalinclude:: quick_tour/jinja2/app.py + :linenos: + :lines: 6-8 + :lineno-start: 6 + :emphasize-lines: 2 The only change in our view is to point the renderer at the ``.jinja2`` file: .. literalinclude:: quick_tour/jinja2/views.py - :start-after: Start View 1 - :end-before: End View 1 + :linenos: + :lines: 4-6 + :lineno-start: 4 + :emphasize-lines: 1 Our Jinja2 template is very similar to our previous template: @@ -319,54 +318,60 @@ Our Jinja2 template is very similar to our previous template: :language: html Pyramid's templating add-ons register a new kind of renderer into your -application. The renderer registration maps to different kinds of -filename extensions. In this case, changing the extension from ``.pt`` -to ``.jinja2`` passed the view response through the ``pyramid_jinja2`` -renderer. +application. The renderer registration maps to different kinds of filename +extensions. In this case, changing the extension from ``.pt`` to ``.jinja2`` +passed the view response through the ``pyramid_jinja2`` renderer. .. seealso:: See also: - :ref:`Quick Tutorial Jinja2 `, - `Jinja2 homepage `_, and - :ref:`pyramid_jinja2 Overview ` + :ref:`Quick Tutorial Jinja2 `, `Jinja2 homepage + `_, and :ref:`pyramid_jinja2 Overview + `. + Static assets ============= -Of course the Web is more than just markup. You need static assets: -CSS, JS, and images. Let's point our web app at a directory where -Pyramid will serve some static assets. First another call to the +Of course the Web is more than just markup. You need static assets: CSS, JS, +and images. Let's point our web app at a directory from which Pyramid will +serve some static assets. First let's make another call to the :term:`configurator`: .. literalinclude:: quick_tour/static_assets/app.py - :start-after: Start Static 1 - :end-before: End Static 1 + :linenos: + :lines: 6-8 + :lineno-start: 6 + :emphasize-lines: 2 This tells our WSGI application to map requests under -http://localhost:6543/static/ to files and directories inside a -``static`` directory alongside our Python module. +http://localhost:6543/static/ to files and directories inside a ``static`` +directory alongside our Python module. Next make a directory named ``static``, and place ``app.css`` inside: .. literalinclude:: quick_tour/static_assets/static/app.css :language: css -All we need to do now is point to it in the ```` of our Jinja2 -template: +All we need to do now is point to it in the ```` of our Jinja2 template, +``hello_world.jinja2``: -.. literalinclude:: quick_tour/static_assets/hello_world.pt - :language: html - :start-after: Start Link 1 - :end-before: End Link 1 +.. literalinclude:: quick_tour/static_assets/hello_world.jinja2 + :language: jinja + :linenos: + :lines: 4-6 + :lineno-start: 4 + :emphasize-lines: 2 -This link presumes that our CSS is at a URL starting with ``/static/``. -What if the site is later moved under ``/somesite/static/``? Or perhaps -a web developer changes the arrangement on disk? Pyramid provides a helper -to allow flexibility on URL generation: +This link presumes that our CSS is at a URL starting with ``/static/``. What if +the site is later moved under ``/somesite/static/``? Or perhaps a web developer +changes the arrangement on disk? Pyramid provides a helper to allow flexibility +on URL generation: -.. literalinclude:: quick_tour/static_assets/hello_world.pt - :language: html - :start-after: Start Link 2 - :end-before: End Link 2 +.. literalinclude:: quick_tour/static_assets/hello_world_static.jinja2 + :language: jinja + :linenos: + :lines: 4-6 + :lineno-start: 4 + :emphasize-lines: 2 By using ``request.static_url`` to generate the full URL to the static assets, you both ensure you stay in sync with the configuration and @@ -374,38 +379,48 @@ gain refactoring flexibility later. .. seealso:: See also: :ref:`Quick Tutorial Static Assets `, - :doc:`../narr/assets`, - :ref:`preventing_http_caching`, and - :ref:`influencing_http_caching` + :doc:`../narr/assets`, :ref:`preventing_http_caching`, and + :ref:`influencing_http_caching`. + Returning JSON ============== -Modern web apps are more than rendered HTML. Dynamic pages now use -JavaScript to update the UI in the browser by requesting server data as -JSON. Pyramid supports this with a JSON renderer: +Modern web apps are more than rendered HTML. Dynamic pages now use JavaScript +to update the UI in the browser by requesting server data as JSON. Pyramid +supports this with a JSON renderer: .. literalinclude:: quick_tour/json/views.py - :start-after: Start View 1 - :end-before: End View 2 + :linenos: + :lines: 9- + :lineno-start: 9 + +This wires up a view that returns some data through the JSON :term:`renderer`, +which calls Python's JSON support to serialize the data into JSON, and sets the +appropriate HTTP headers. + +We also need to add a route to ``app.py`` so that our app will know how to +respond to a request for ``hello.json``. -This wires up a view that returns some data through the JSON -:term:`renderer`, which calls Python's JSON support to serialize the data -into JSON and set the appropriate HTTP headers. +.. literalinclude:: quick_tour/json/app.py + :linenos: + :lines: 6-8 + :lineno-start: 6 + :emphasize-lines: 2 .. seealso:: See also: - :ref:`Quick Tutorial JSON `, - :ref:`views_which_use_a_renderer`, - :ref:`json_renderer`, and - :ref:`adding_and_overriding_renderers` + :ref:`Quick Tutorial JSON `, :ref:`views_which_use_a_renderer`, + :ref:`json_renderer`, and :ref:`adding_and_overriding_renderers`. + View classes ============ -So far our views have been simple, free-standing functions. Many times -your views are related: different ways to look at or work on the same -data, or a REST API that handles multiple operations. Grouping these -together as a :ref:`view class ` makes sense. +So far our views have been simple, free-standing functions. Many times your +views are related. They may have different ways to look at or work on the same +data, or they may be a REST API that handles multiple operations. Grouping +these together as a :ref:`view class ` makes sense and achieves +the following goals. - Group views @@ -413,46 +428,46 @@ together as a :ref:`view class ` makes sense. - Share some state and helpers -The following shows a "Hello World" example with three operations: view -a form, save a change, or press the delete button: +The following shows a "Hello World" example with three operations: view a form, +save a change, or press the delete button in our ``views.py``: .. literalinclude:: quick_tour/view_classes/views.py - :start-after: Start View 1 - :end-before: End View 1 + :linenos: + :lines: 7- + :lineno-start: 7 -As you can see, the three views are logically grouped together. -Specifically: +As you can see, the three views are logically grouped together. Specifically: -- The first view is returned when you go to ``/howdy/amy``. This URL is - mapped to the ``hello`` route that we centrally set using the optional +- The first view is returned when you go to ``/howdy/amy``. This URL is mapped + to the ``hello`` route that we centrally set using the optional ``@view_defaults``. - The second view is returned when the form data contains a field with - ``form.edit``, such as clicking on - ````. This rule - is specified in the ``@view_config`` for that view. + ``form.edit``, such as clicking on ````. This rule is specified in the ``@view_config`` for that + view. -- The third view is returned when clicking on a button such - as ````. +- The third view is returned when clicking on a button such as ````. -Only one route is needed, stated in one place atop the view class. Also, -the assignment of ``name`` is done in the ``__init__`` function. Our -templates can then use ``{{ view.name }}``. +Only one route is needed, stated in one place atop the view class. Also, the +assignment of ``name`` is done in the ``__init__`` function. Our templates can +then use ``{{ view.name }}``. -Pyramid view classes, combined with built-in and custom predicates, -have much more to offer: +Pyramid view classes, combined with built-in and custom predicates, have much +more to offer: - All the same view configuration parameters as function views -- One route leading to multiple views, based on information in the - request or data such as ``request_param``, ``request_method``, - ``accept``, ``header``, ``xhr``, ``containment``, and - ``custom_predicates`` +- One route leading to multiple views, based on information in the request or + data such as ``request_param``, ``request_method``, ``accept``, ``header``, + ``xhr``, ``containment``, and ``custom_predicates`` .. seealso:: See also: - :ref:`Quick Tutorial View Classes `, - :ref:`Quick Tutorial More View Classes `, and - :ref:`class_as_view` + :ref:`Quick Tutorial View Classes `, :ref:`Quick + Tutorial More View Classes `, and + :ref:`class_as_view`. + Quick project startup with scaffolds ==================================== diff --git a/docs/quick_tour/hello_world/app.py b/docs/quick_tour/hello_world/app.py index df5a6cf18..75d22ac96 100644 --- a/docs/quick_tour/hello_world/app.py +++ b/docs/quick_tour/hello_world/app.py @@ -13,4 +13,4 @@ if __name__ == '__main__': config.add_view(hello_world, route_name='hello') app = config.make_wsgi_app() server = make_server('0.0.0.0', 6543, app) - server.serve_forever() \ No newline at end of file + server.serve_forever() diff --git a/docs/quick_tour/jinja2/app.py b/docs/quick_tour/jinja2/app.py index 83af219db..b7632807b 100644 --- a/docs/quick_tour/jinja2/app.py +++ b/docs/quick_tour/jinja2/app.py @@ -8,4 +8,4 @@ if __name__ == '__main__': config.scan('views') app = config.make_wsgi_app() server = make_server('0.0.0.0', 6543, app) - server.serve_forever() \ No newline at end of file + server.serve_forever() diff --git a/docs/quick_tour/jinja2/views.py b/docs/quick_tour/jinja2/views.py index 916cdc720..7dbb45287 100644 --- a/docs/quick_tour/jinja2/views.py +++ b/docs/quick_tour/jinja2/views.py @@ -1,8 +1,6 @@ from pyramid.view import view_config -# Start View 1 @view_config(route_name='hello', renderer='hello_world.jinja2') -# End View 1 def hello_world(request): return dict(name=request.matchdict['name']) diff --git a/docs/quick_tour/json/app.py b/docs/quick_tour/json/app.py index 950cb478f..40faddd00 100644 --- a/docs/quick_tour/json/app.py +++ b/docs/quick_tour/json/app.py @@ -1,8 +1,6 @@ from wsgiref.simple_server import make_server - from pyramid.config import Configurator - if __name__ == '__main__': config = Configurator() config.add_route('hello', '/howdy/{name}') @@ -12,4 +10,4 @@ if __name__ == '__main__': config.scan('views') app = config.make_wsgi_app() server = make_server('0.0.0.0', 6543, app) - server.serve_forever() \ No newline at end of file + server.serve_forever() diff --git a/docs/quick_tour/json/hello_world.jinja2 b/docs/quick_tour/json/hello_world.jinja2 index f6862e618..4fb9be074 100644 --- a/docs/quick_tour/json/hello_world.jinja2 +++ b/docs/quick_tour/json/hello_world.jinja2 @@ -1,8 +1,8 @@ - Quick Glance - + Hello World +

Hello {{ name }}!

diff --git a/docs/quick_tour/json/hello_world.pt b/docs/quick_tour/json/hello_world.pt deleted file mode 100644 index 711054aa9..000000000 --- a/docs/quick_tour/json/hello_world.pt +++ /dev/null @@ -1,17 +0,0 @@ - - - - Quick Glance - - - - - - - - -

Hello ${name}!

- - \ No newline at end of file diff --git a/docs/quick_tour/json/views.py b/docs/quick_tour/json/views.py index 583e220c7..22aa8aad6 100644 --- a/docs/quick_tour/json/views.py +++ b/docs/quick_tour/json/views.py @@ -1,13 +1,11 @@ from pyramid.view import view_config -@view_config(route_name='hello', renderer='hello_world.pt') +@view_config(route_name='hello', renderer='hello_world.jinja2') def hello_world(request): return dict(name=request.matchdict['name']) -# Start View 1 @view_config(route_name='hello_json', renderer='json') def hello_json(request): return [1, 2, 3] - # End View 1 \ No newline at end of file diff --git a/docs/quick_tour/requests/app.py b/docs/quick_tour/requests/app.py index 815714464..621b0693e 100644 --- a/docs/quick_tour/requests/app.py +++ b/docs/quick_tour/requests/app.py @@ -21,4 +21,4 @@ if __name__ == '__main__': config.add_view(hello_world, route_name='hello') app = config.make_wsgi_app() server = make_server('0.0.0.0', 6543, app) - server.serve_forever() \ No newline at end of file + server.serve_forever() diff --git a/docs/quick_tour/routing/app.py b/docs/quick_tour/routing/app.py index 04a8a6344..12b547bfe 100644 --- a/docs/quick_tour/routing/app.py +++ b/docs/quick_tour/routing/app.py @@ -3,9 +3,7 @@ from pyramid.config import Configurator if __name__ == '__main__': config = Configurator() - # Start Route 1 config.add_route('hello', '/howdy/{first}/{last}') - # End Route 1 config.scan('views') app = config.make_wsgi_app() server = make_server('0.0.0.0', 6543, app) diff --git a/docs/quick_tour/routing/views.py b/docs/quick_tour/routing/views.py index 8cb8d3780..8a3bd230e 100644 --- a/docs/quick_tour/routing/views.py +++ b/docs/quick_tour/routing/views.py @@ -2,9 +2,7 @@ from pyramid.response import Response from pyramid.view import view_config -# Start Route 1 @view_config(route_name='hello') def hello_world(request): body = '

Hi %(first)s %(last)s!

' % request.matchdict return Response(body) - # End Route 1 \ No newline at end of file diff --git a/docs/quick_tour/static_assets/app.py b/docs/quick_tour/static_assets/app.py index 9c808972f..1849c0a5a 100644 --- a/docs/quick_tour/static_assets/app.py +++ b/docs/quick_tour/static_assets/app.py @@ -4,11 +4,9 @@ from pyramid.config import Configurator if __name__ == '__main__': config = Configurator() config.add_route('hello', '/howdy/{name}') - # Start Static 1 config.add_static_view(name='static', path='static') - # End Static 1 config.include('pyramid_jinja2') config.scan('views') app = config.make_wsgi_app() server = make_server('0.0.0.0', 6543, app) - server.serve_forever() \ No newline at end of file + server.serve_forever() diff --git a/docs/quick_tour/static_assets/hello_world.jinja2 b/docs/quick_tour/static_assets/hello_world.jinja2 index f6862e618..0fb2ce296 100644 --- a/docs/quick_tour/static_assets/hello_world.jinja2 +++ b/docs/quick_tour/static_assets/hello_world.jinja2 @@ -1,7 +1,7 @@ - Quick Glance + Hello World diff --git a/docs/quick_tour/static_assets/hello_world.pt b/docs/quick_tour/static_assets/hello_world.pt deleted file mode 100644 index 1797146eb..000000000 --- a/docs/quick_tour/static_assets/hello_world.pt +++ /dev/null @@ -1,16 +0,0 @@ - - - Quick Glance - - - - - - - - -

Hello ${name}!

- - \ No newline at end of file diff --git a/docs/quick_tour/static_assets/hello_world_static.jinja2 b/docs/quick_tour/static_assets/hello_world_static.jinja2 new file mode 100644 index 000000000..4fb9be074 --- /dev/null +++ b/docs/quick_tour/static_assets/hello_world_static.jinja2 @@ -0,0 +1,10 @@ + + + + Hello World + + + +

Hello {{ name }}!

+ + \ No newline at end of file diff --git a/docs/quick_tour/static_assets/views.py b/docs/quick_tour/static_assets/views.py index 90730ae32..7dbb45287 100644 --- a/docs/quick_tour/static_assets/views.py +++ b/docs/quick_tour/static_assets/views.py @@ -1,6 +1,6 @@ from pyramid.view import view_config -@view_config(route_name='hello', renderer='hello_world.pt') +@view_config(route_name='hello', renderer='hello_world.jinja2') def hello_world(request): return dict(name=request.matchdict['name']) diff --git a/docs/quick_tour/templating/app.py b/docs/quick_tour/templating/app.py index 6d1a29f4e..52b7faf55 100644 --- a/docs/quick_tour/templating/app.py +++ b/docs/quick_tour/templating/app.py @@ -4,7 +4,8 @@ from pyramid.config import Configurator if __name__ == '__main__': config = Configurator() config.add_route('hello', '/howdy/{name}') + config.include('pyramid_chameleon') config.scan('views') app = config.make_wsgi_app() server = make_server('0.0.0.0', 6543, app) - server.serve_forever() \ No newline at end of file + server.serve_forever() diff --git a/docs/quick_tour/templating/views.py b/docs/quick_tour/templating/views.py index 6c7846efa..90730ae32 100644 --- a/docs/quick_tour/templating/views.py +++ b/docs/quick_tour/templating/views.py @@ -1,8 +1,6 @@ from pyramid.view import view_config -# Start View 1 @view_config(route_name='hello', renderer='hello_world.pt') def hello_world(request): return dict(name=request.matchdict['name']) - # End View 1 \ No newline at end of file diff --git a/docs/quick_tour/view_classes/app.py b/docs/quick_tour/view_classes/app.py index 468c8c29e..40faddd00 100644 --- a/docs/quick_tour/view_classes/app.py +++ b/docs/quick_tour/view_classes/app.py @@ -3,11 +3,11 @@ from pyramid.config import Configurator if __name__ == '__main__': config = Configurator() - # Start Routes 1 config.add_route('hello', '/howdy/{name}') - # End Routes 1 + config.add_route('hello_json', 'hello.json') + config.add_static_view(name='static', path='static') config.include('pyramid_jinja2') config.scan('views') app = config.make_wsgi_app() server = make_server('0.0.0.0', 6543, app) - server.serve_forever() \ No newline at end of file + server.serve_forever() diff --git a/docs/quick_tour/view_classes/hello.jinja2 b/docs/quick_tour/view_classes/hello.jinja2 index 3446b96ce..fc3058067 100644 --- a/docs/quick_tour/view_classes/hello.jinja2 +++ b/docs/quick_tour/view_classes/hello.jinja2 @@ -5,13 +5,11 @@

Hello {{ view.name }}!

-
- - - + + +
- \ No newline at end of file diff --git a/docs/quick_tour/view_classes/views.py b/docs/quick_tour/view_classes/views.py index 62556142e..10ff238c7 100644 --- a/docs/quick_tour/view_classes/views.py +++ b/docs/quick_tour/view_classes/views.py @@ -4,7 +4,6 @@ from pyramid.view import ( ) -# Start View 1 # One route, at /howdy/amy, so don't repeat on each @view_config @view_defaults(route_name='hello') class HelloWorldViews: @@ -29,4 +28,3 @@ class HelloWorldViews: def delete_view(self): print('Deleted') return dict() - # End View 1 \ No newline at end of file diff --git a/docs/quick_tour/views/app.py b/docs/quick_tour/views/app.py index 54dc9ed4b..e8df6eff2 100644 --- a/docs/quick_tour/views/app.py +++ b/docs/quick_tour/views/app.py @@ -10,4 +10,4 @@ if __name__ == '__main__': config.scan('views') app = config.make_wsgi_app() server = make_server('0.0.0.0', 6543, app) - server.serve_forever() \ No newline at end of file + server.serve_forever() -- cgit v1.2.3 From 70074c141afd0bbc96fee264c6a929fb7a519273 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 22 Jan 2016 17:05:58 -0600 Subject: add changelog for #2256 --- CHANGES.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 77f5d94ac..d31e471f5 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,9 @@ +unreleased +========== + +- Dropped Python 3.2 support. + See https://github.com/Pylons/pyramid/pull/2256 + 1.6 (2015-04-14) ================ -- cgit v1.2.3 From 5adb45a47e4ebfff5d2ea28833ef29a7f3ddbb25 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Fri, 22 Jan 2016 15:15:34 -0800 Subject: Add Python support policy (see https://github.com/Pylons/pyramid/pull/2256#issuecomment-174029882) --- docs/narr/upgrading.rst | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/narr/upgrading.rst b/docs/narr/upgrading.rst index db9b5e090..cacfba92a 100644 --- a/docs/narr/upgrading.rst +++ b/docs/narr/upgrading.rst @@ -75,6 +75,27 @@ changes are noted in the :ref:`changelog`, so it's possible to know that you should change older spellings to newer ones to ensure that people reading your code can find the APIs you're using in the Pyramid docs. + +Python support policy +~~~~~~~~~~~~~~~~~~~~~ + +At the time of a Pyramid version release, each supports all versions of Python +through the end of their lifespans. The end-of-life for a given version of +Python is when security updates are no longer released. + +- `Python 3.2 Lifespan `_ + ends February 2016. +- `Python 3.3 Lifespan `_ + ends September 2017. +- `Python 3.4 Lifespan `_ TBD. +- `Python 3.5 Lifespan `_ TBD. +- `Python 3.6 Lifespan `_ + December 2021. + +To determine the Python support for a specific release of Pyramid, view its +``tox.ini`` file at the root of the repository's version. + + Consulting the change history ----------------------------- -- cgit v1.2.3 From 6a936654276b83ccd379c739e3c39d5a25457ab3 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sat, 23 Jan 2016 05:04:45 -0800 Subject: Complete overhaul of Quick Tour - Databases and Forms --- docs/quick_tour.rst | 92 ++++++------ docs/quick_tour/sqla_demo/README.txt | 6 +- docs/quick_tour/sqla_demo/development.ini | 2 +- docs/quick_tour/sqla_demo/production.ini | 2 +- docs/quick_tour/sqla_demo/setup.cfg | 27 ++++ docs/quick_tour/sqla_demo/setup.py | 11 +- docs/quick_tour/sqla_demo/sqla_demo.sqlite | Bin 3072 -> 0 bytes docs/quick_tour/sqla_demo/sqla_demo/__init__.py | 1 + docs/quick_tour/sqla_demo/sqla_demo/models.py | 10 +- .../sqla_demo/sqla_demo/scripts/initializedb.py | 9 +- .../sqla_demo/sqla_demo/static/pyramid-16x16.png | Bin 0 -> 1319 bytes .../sqla_demo/sqla_demo/static/pyramid.png | Bin 33055 -> 12901 bytes .../sqla_demo/sqla_demo/static/theme.css | 154 +++++++++++++++++++++ .../sqla_demo/sqla_demo/static/theme.min.css | 1 + .../sqla_demo/sqla_demo/templates/mytemplate.pt | 131 ++++++++---------- docs/quick_tour/sqla_demo/sqla_demo/tests.py | 26 +++- docs/quick_tour/sqla_demo/sqla_demo/views.py | 5 +- 17 files changed, 338 insertions(+), 139 deletions(-) create mode 100644 docs/quick_tour/sqla_demo/setup.cfg delete mode 100644 docs/quick_tour/sqla_demo/sqla_demo.sqlite create mode 100644 docs/quick_tour/sqla_demo/sqla_demo/static/pyramid-16x16.png create mode 100644 docs/quick_tour/sqla_demo/sqla_demo/static/theme.css create mode 100644 docs/quick_tour/sqla_demo/sqla_demo/static/theme.min.css diff --git a/docs/quick_tour.rst b/docs/quick_tour.rst index 220fd4bca..a7c184776 100644 --- a/docs/quick_tour.rst +++ b/docs/quick_tour.rst @@ -812,17 +812,16 @@ We need to update our Jinja2 template to show counter increment in the session: :ref:`flash_messages`, :ref:`session_module`, and :term:`pyramid_redis_sessions`. + Databases ========= -Web applications mean data. Data means databases. Frequently SQL -databases. SQL databases frequently mean an "ORM" -(object-relational mapper.) In Python, ORM usually leads to the -mega-quality *SQLAlchemy*, a Python package that greatly eases working -with databases. +Web applications mean data. Data means databases. Frequently SQL databases. SQL +databases frequently mean an "ORM" (object-relational mapper.) In Python, ORM +usually leads to the mega-quality *SQLAlchemy*, a Python package that greatly +eases working with databases. -Pyramid and SQLAlchemy are great friends. That friendship includes a -scaffold! +Pyramid and SQLAlchemy are great friends. That friendship includes a scaffold! .. code-block:: bash @@ -830,50 +829,53 @@ scaffold! $ cd sqla_demo $ python setup.py develop -We now have a working sample SQLAlchemy application with all -dependencies installed. The sample project provides a console script to -initialize a SQLite database with tables. Let's run it and then start -the application: +We now have a working sample SQLAlchemy application with all dependencies +installed. The sample project provides a console script to initialize a SQLite +database with tables. Let's run it, then start the application: .. code-block:: bash $ initialize_sqla_demo_db development.ini $ pserve development.ini -The ORM eases the mapping of database structures into a programming -language. SQLAlchemy uses "models" for this mapping. The scaffold -generated a sample model: +The ORM eases the mapping of database structures into a programming language. +SQLAlchemy uses "models" for this mapping. The scaffold generated a sample +model: .. literalinclude:: quick_tour/sqla_demo/sqla_demo/models.py - :start-after: Start Sphinx Include - :end-before: End Sphinx Include + :language: python + :linenos: + :lineno-start: 21 + :lines: 21- -View code, which mediates the logic between web requests and the rest -of the system, can then easily get at the data thanks to SQLAlchemy: +View code, which mediates the logic between web requests and the rest of the +system, can then easily get at the data thanks to SQLAlchemy: .. literalinclude:: quick_tour/sqla_demo/sqla_demo/views.py - :start-after: Start Sphinx Include - :end-before: End Sphinx Include + :language: python + :linenos: + :lineno-start: 12 + :lines: 12-18 + :emphasize-lines: 4 .. seealso:: See also: - :ref:`Quick Tutorial Databases `, - `SQLAlchemy `_, - :ref:`making_a_console_script`, - :ref:`bfg_sql_wiki_tutorial`, and - :ref:`Application Transactions With pyramid_tm ` + :ref:`Quick Tutorial Databases `, `SQLAlchemy + `_, :ref:`making_a_console_script`, + :ref:`bfg_sql_wiki_tutorial`, and :ref:`Application Transactions with + pyramid_tm `. + Forms ===== -Developers have lots of opinions about web forms, and thus there are many -form libraries for Python. Pyramid doesn't directly bundle a form -library, but *Deform* is a popular choice for forms, -along with its related *Colander* schema system. +Developers have lots of opinions about web forms, thus there are many form +libraries for Python. Pyramid doesn't directly bundle a form library, but +*Deform* is a popular choice for forms, along with its related *Colander* +schema system. -As an example, imagine we want a form that edits a wiki page. The form -should have two fields on it, one of them a required title and the -other a rich text editor for the body. With Deform we can express this -as a Colander schema: +As an example, imagine we want a form that edits a wiki page. The form should +have two fields on it, one of them a required title and the other a rich text +editor for the body. With Deform we can express this as a Colander schema: .. code-block:: python @@ -884,8 +886,8 @@ as a Colander schema: widget=deform.widget.RichTextWidget() ) -With this in place, we can render the HTML for a form, -perhaps with form data from an existing page: +With this in place, we can render the HTML for a form, perhaps with form data +from an existing page: .. code-block:: python @@ -909,20 +911,18 @@ We'd like to handle form submission, validation, and saving: page['title'] = appstruct['title'] page['body'] = appstruct['body'] -Deform and Colander provide a very flexible combination for forms, -widgets, schemas, and validation. Recent versions of Deform also -include a :ref:`retail mode ` for gaining Deform -features on custom forms. +Deform and Colander provide a very flexible combination for forms, widgets, +schemas, and validation. Recent versions of Deform also include a :ref:`retail +mode ` for gaining Deform features on custom forms. -Also the ``deform_bootstrap`` Pyramid add-on restyles the stock Deform -widgets using attractive CSS from Twitter Bootstrap and more powerful widgets -from Chosen. +Also the ``deform_bootstrap`` Pyramid add-on restyles the stock Deform widgets +using attractive CSS from Twitter Bootstrap and more powerful widgets from +Chosen. .. seealso:: See also: - :ref:`Quick Tutorial Forms `, - :ref:`Deform `, - :ref:`Colander `, and - `deform_bootstrap `_ + :ref:`Quick Tutorial Forms `, :ref:`Deform `, + :ref:`Colander `, and `deform_bootstrap + `_. Conclusion ========== diff --git a/docs/quick_tour/sqla_demo/README.txt b/docs/quick_tour/sqla_demo/README.txt index f35d3aec5..c7f9d6474 100644 --- a/docs/quick_tour/sqla_demo/README.txt +++ b/docs/quick_tour/sqla_demo/README.txt @@ -6,9 +6,9 @@ Getting Started - cd -- $venv/bin/python setup.py develop +- $VENV/bin/python setup.py develop -- $venv/bin/initialize_sqla_demo_db development.ini +- $VENV/bin/initialize_sqla_demo_db development.ini -- $venv/bin/pserve development.ini +- $VENV/bin/pserve development.ini diff --git a/docs/quick_tour/sqla_demo/development.ini b/docs/quick_tour/sqla_demo/development.ini index 174468abf..cdf20638e 100644 --- a/docs/quick_tour/sqla_demo/development.ini +++ b/docs/quick_tour/sqla_demo/development.ini @@ -68,4 +68,4 @@ level = NOTSET formatter = generic [formatter_generic] -format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/quick_tour/sqla_demo/production.ini b/docs/quick_tour/sqla_demo/production.ini index dc0ba304f..38f3b6318 100644 --- a/docs/quick_tour/sqla_demo/production.ini +++ b/docs/quick_tour/sqla_demo/production.ini @@ -59,4 +59,4 @@ level = NOTSET formatter = generic [formatter_generic] -format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/quick_tour/sqla_demo/setup.cfg b/docs/quick_tour/sqla_demo/setup.cfg new file mode 100644 index 000000000..9f91cd122 --- /dev/null +++ b/docs/quick_tour/sqla_demo/setup.cfg @@ -0,0 +1,27 @@ +[nosetests] +match=^test +nocapture=1 +cover-package=sqla_demo +with-coverage=1 +cover-erase=1 + +[compile_catalog] +directory = sqla_demo/locale +domain = sqla_demo +statistics = true + +[extract_messages] +add_comments = TRANSLATORS: +output_file = sqla_demo/locale/sqla_demo.pot +width = 80 + +[init_catalog] +domain = sqla_demo +input_file = sqla_demo/locale/sqla_demo.pot +output_dir = sqla_demo/locale + +[update_catalog] +domain = sqla_demo +input_file = sqla_demo/locale/sqla_demo.pot +output_dir = sqla_demo/locale +previous = true diff --git a/docs/quick_tour/sqla_demo/setup.py b/docs/quick_tour/sqla_demo/setup.py index ac2eed035..a9a8842e2 100644 --- a/docs/quick_tour/sqla_demo/setup.py +++ b/docs/quick_tour/sqla_demo/setup.py @@ -3,15 +3,18 @@ import os from setuptools import setup, find_packages here = os.path.abspath(os.path.dirname(__file__)) -README = open(os.path.join(here, 'README.txt')).read() -CHANGES = open(os.path.join(here, 'CHANGES.txt')).read() +with open(os.path.join(here, 'README.txt')) as f: + README = f.read() +with open(os.path.join(here, 'CHANGES.txt')) as f: + CHANGES = f.read() requires = [ 'pyramid', + 'pyramid_chameleon', + 'pyramid_debugtoolbar', + 'pyramid_tm', 'SQLAlchemy', 'transaction', - 'pyramid_tm', - 'pyramid_debugtoolbar', 'zope.sqlalchemy', 'waitress', ] diff --git a/docs/quick_tour/sqla_demo/sqla_demo.sqlite b/docs/quick_tour/sqla_demo/sqla_demo.sqlite deleted file mode 100644 index fa6adb104..000000000 Binary files a/docs/quick_tour/sqla_demo/sqla_demo.sqlite and /dev/null differ diff --git a/docs/quick_tour/sqla_demo/sqla_demo/__init__.py b/docs/quick_tour/sqla_demo/sqla_demo/__init__.py index aac7c5e69..867049e4f 100644 --- a/docs/quick_tour/sqla_demo/sqla_demo/__init__.py +++ b/docs/quick_tour/sqla_demo/sqla_demo/__init__.py @@ -14,6 +14,7 @@ def main(global_config, **settings): DBSession.configure(bind=engine) Base.metadata.bind = engine config = Configurator(settings=settings) + config.include('pyramid_chameleon') config.add_static_view('static', 'static', cache_max_age=3600) config.add_route('home', '/') config.scan() diff --git a/docs/quick_tour/sqla_demo/sqla_demo/models.py b/docs/quick_tour/sqla_demo/sqla_demo/models.py index 3dfb40e58..a0d3e7b71 100644 --- a/docs/quick_tour/sqla_demo/sqla_demo/models.py +++ b/docs/quick_tour/sqla_demo/sqla_demo/models.py @@ -1,5 +1,6 @@ from sqlalchemy import ( Column, + Index, Integer, Text, ) @@ -16,14 +17,11 @@ from zope.sqlalchemy import ZopeTransactionExtension DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension())) Base = declarative_base() -# Start Sphinx Include + class MyModel(Base): __tablename__ = 'models' id = Column(Integer, primary_key=True) - name = Column(Text, unique=True) + name = Column(Text) value = Column(Integer) - def __init__(self, name, value): - self.name = name - self.value = value - # End Sphinx Include +Index('my_index', MyModel.name, unique=True, mysql_length=255) diff --git a/docs/quick_tour/sqla_demo/sqla_demo/scripts/initializedb.py b/docs/quick_tour/sqla_demo/sqla_demo/scripts/initializedb.py index 66feb3008..7dfdece15 100644 --- a/docs/quick_tour/sqla_demo/sqla_demo/scripts/initializedb.py +++ b/docs/quick_tour/sqla_demo/sqla_demo/scripts/initializedb.py @@ -9,6 +9,8 @@ from pyramid.paster import ( setup_logging, ) +from pyramid.scripts.common import parse_vars + from ..models import ( DBSession, MyModel, @@ -18,17 +20,18 @@ from ..models import ( def usage(argv): cmd = os.path.basename(argv[0]) - print('usage: %s \n' + print('usage: %s [var=value]\n' '(example: "%s development.ini")' % (cmd, cmd)) sys.exit(1) def main(argv=sys.argv): - if len(argv) != 2: + if len(argv) < 2: usage(argv) config_uri = argv[1] + options = parse_vars(argv[2:]) setup_logging(config_uri) - settings = get_appsettings(config_uri) + settings = get_appsettings(config_uri, options=options) engine = engine_from_config(settings, 'sqlalchemy.') DBSession.configure(bind=engine) Base.metadata.create_all(engine) diff --git a/docs/quick_tour/sqla_demo/sqla_demo/static/pyramid-16x16.png b/docs/quick_tour/sqla_demo/sqla_demo/static/pyramid-16x16.png new file mode 100644 index 000000000..979203112 Binary files /dev/null and b/docs/quick_tour/sqla_demo/sqla_demo/static/pyramid-16x16.png differ diff --git a/docs/quick_tour/sqla_demo/sqla_demo/static/pyramid.png b/docs/quick_tour/sqla_demo/sqla_demo/static/pyramid.png index 347e05549..4ab837be9 100644 Binary files a/docs/quick_tour/sqla_demo/sqla_demo/static/pyramid.png and b/docs/quick_tour/sqla_demo/sqla_demo/static/pyramid.png differ diff --git a/docs/quick_tour/sqla_demo/sqla_demo/static/theme.css b/docs/quick_tour/sqla_demo/sqla_demo/static/theme.css new file mode 100644 index 000000000..0f4b1a4d4 --- /dev/null +++ b/docs/quick_tour/sqla_demo/sqla_demo/static/theme.css @@ -0,0 +1,154 @@ +@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700); +body { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; + color: #ffffff; + background: #bc2131; +} +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; +} +p { + font-weight: 300; +} +.font-normal { + font-weight: 400; +} +.font-semi-bold { + font-weight: 600; +} +.font-bold { + font-weight: 700; +} +.starter-template { + margin-top: 250px; +} +.starter-template .content { + margin-left: 10px; +} +.starter-template .content h1 { + margin-top: 10px; + font-size: 60px; +} +.starter-template .content h1 .smaller { + font-size: 40px; + color: #f2b7bd; +} +.starter-template .content .lead { + font-size: 25px; + color: #f2b7bd; +} +.starter-template .content .lead .font-normal { + color: #ffffff; +} +.starter-template .links { + float: right; + right: 0; + margin-top: 125px; +} +.starter-template .links ul { + display: block; + padding: 0; + margin: 0; +} +.starter-template .links ul li { + list-style: none; + display: inline; + margin: 0 10px; +} +.starter-template .links ul li:first-child { + margin-left: 0; +} +.starter-template .links ul li:last-child { + margin-right: 0; +} +.starter-template .links ul li.current-version { + color: #f2b7bd; + font-weight: 400; +} +.starter-template .links ul li a, a { + color: #f2b7bd; + text-decoration: underline; +} +.starter-template .links ul li a:hover, a:hover { + color: #ffffff; + text-decoration: underline; +} +.starter-template .links ul li .icon-muted { + color: #eb8b95; + margin-right: 5px; +} +.starter-template .links ul li:hover .icon-muted { + color: #ffffff; +} +.starter-template .copyright { + margin-top: 10px; + font-size: 0.9em; + color: #f2b7bd; + text-transform: lowercase; + float: right; + right: 0; +} +@media (max-width: 1199px) { + .starter-template .content h1 { + font-size: 45px; + } + .starter-template .content h1 .smaller { + font-size: 30px; + } + .starter-template .content .lead { + font-size: 20px; + } +} +@media (max-width: 991px) { + .starter-template { + margin-top: 0; + } + .starter-template .logo { + margin: 40px auto; + } + .starter-template .content { + margin-left: 0; + text-align: center; + } + .starter-template .content h1 { + margin-bottom: 20px; + } + .starter-template .links { + float: none; + text-align: center; + margin-top: 60px; + } + .starter-template .copyright { + float: none; + text-align: center; + } +} +@media (max-width: 767px) { + .starter-template .content h1 .smaller { + font-size: 25px; + display: block; + } + .starter-template .content .lead { + font-size: 16px; + } + .starter-template .links { + margin-top: 40px; + } + .starter-template .links ul li { + display: block; + margin: 0; + } + .starter-template .links ul li .icon-muted { + display: none; + } + .starter-template .copyright { + margin-top: 20px; + } +} diff --git a/docs/quick_tour/sqla_demo/sqla_demo/static/theme.min.css b/docs/quick_tour/sqla_demo/sqla_demo/static/theme.min.css new file mode 100644 index 000000000..0d25de5b6 --- /dev/null +++ b/docs/quick_tour/sqla_demo/sqla_demo/static/theme.min.css @@ -0,0 +1 @@ +@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);body{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300;color:#fff;background:#bc2131}h1,h2,h3,h4,h5,h6{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300}p{font-weight:300}.font-normal{font-weight:400}.font-semi-bold{font-weight:600}.font-bold{font-weight:700}.starter-template{margin-top:250px}.starter-template .content{margin-left:10px}.starter-template .content h1{margin-top:10px;font-size:60px}.starter-template .content h1 .smaller{font-size:40px;color:#f2b7bd}.starter-template .content .lead{font-size:25px;color:#f2b7bd}.starter-template .content .lead .font-normal{color:#fff}.starter-template .links{float:right;right:0;margin-top:125px}.starter-template .links ul{display:block;padding:0;margin:0}.starter-template .links ul li{list-style:none;display:inline;margin:0 10px}.starter-template .links ul li:first-child{margin-left:0}.starter-template .links ul li:last-child{margin-right:0}.starter-template .links ul li.current-version{color:#f2b7bd;font-weight:400}.starter-template .links ul li a,a{color:#f2b7bd;text-decoration:underline}.starter-template .links ul li a:hover,a:hover{color:#fff;text-decoration:underline}.starter-template .links ul li .icon-muted{color:#eb8b95;margin-right:5px}.starter-template .links ul li:hover .icon-muted{color:#fff}.starter-template .copyright{margin-top:10px;font-size:.9em;color:#f2b7bd;text-transform:lowercase;float:right;right:0}@media (max-width:1199px){.starter-template .content h1{font-size:45px}.starter-template .content h1 .smaller{font-size:30px}.starter-template .content .lead{font-size:20px}}@media (max-width:991px){.starter-template{margin-top:0}.starter-template .logo{margin:40px auto}.starter-template .content{margin-left:0;text-align:center}.starter-template .content h1{margin-bottom:20px}.starter-template .links{float:none;text-align:center;margin-top:60px}.starter-template .copyright{float:none;text-align:center}}@media (max-width:767px){.starter-template .content h1 .smaller{font-size:25px;display:block}.starter-template .content .lead{font-size:16px}.starter-template .links{margin-top:40px}.starter-template .links ul li{display:block;margin:0}.starter-template .links ul li .icon-muted{display:none}.starter-template .copyright{margin-top:20px}} diff --git a/docs/quick_tour/sqla_demo/sqla_demo/templates/mytemplate.pt b/docs/quick_tour/sqla_demo/sqla_demo/templates/mytemplate.pt index 321c0f5fb..99df4a8b7 100644 --- a/docs/quick_tour/sqla_demo/sqla_demo/templates/mytemplate.pt +++ b/docs/quick_tour/sqla_demo/sqla_demo/templates/mytemplate.pt @@ -1,76 +1,67 @@ - - - - The Pyramid Web Framework - - - - - - - - - - -
-
-
-
pyramid
-
-
-
-
-

- Welcome to ${project}, an application generated by
- the Pyramid web framework. -

-
-
-
-
-
-

Search documentation

-
- - -
+ + + + + + + + + + + Alchemy Scaffold for The Pyramid Web Framework + + + + + + + + + + + + + +
+
+
+
+ +
+
+
+

Pyramid Alchemy scaffold

+

Welcome to ${project}, an application generated by
the Pyramid Web Framework 1.6.

+
+
-
-
- - + + + + + + + diff --git a/docs/quick_tour/sqla_demo/sqla_demo/tests.py b/docs/quick_tour/sqla_demo/sqla_demo/tests.py index 6fef6d695..be288d580 100644 --- a/docs/quick_tour/sqla_demo/sqla_demo/tests.py +++ b/docs/quick_tour/sqla_demo/sqla_demo/tests.py @@ -6,7 +6,7 @@ from pyramid import testing from .models import DBSession -class TestMyView(unittest.TestCase): +class TestMyViewSuccessCondition(unittest.TestCase): def setUp(self): self.config = testing.setUp() from sqlalchemy import create_engine @@ -25,9 +25,31 @@ class TestMyView(unittest.TestCase): DBSession.remove() testing.tearDown() - def test_it(self): + def test_passing_view(self): from .views import my_view request = testing.DummyRequest() info = my_view(request) self.assertEqual(info['one'].name, 'one') self.assertEqual(info['project'], 'sqla_demo') + + +class TestMyViewFailureCondition(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + from sqlalchemy import create_engine + engine = create_engine('sqlite://') + from .models import ( + Base, + MyModel, + ) + DBSession.configure(bind=engine) + + def tearDown(self): + DBSession.remove() + testing.tearDown() + + def test_failing_view(self): + from .views import my_view + request = testing.DummyRequest() + info = my_view(request) + self.assertEqual(info.status_int, 500) \ No newline at end of file diff --git a/docs/quick_tour/sqla_demo/sqla_demo/views.py b/docs/quick_tour/sqla_demo/sqla_demo/views.py index 768a7e42e..964f76441 100644 --- a/docs/quick_tour/sqla_demo/sqla_demo/views.py +++ b/docs/quick_tour/sqla_demo/sqla_demo/views.py @@ -12,19 +12,18 @@ from .models import ( @view_config(route_name='home', renderer='templates/mytemplate.pt') def my_view(request): try: - # Start Sphinx Include one = DBSession.query(MyModel).filter(MyModel.name == 'one').first() - # End Sphinx Include except DBAPIError: return Response(conn_err_msg, content_type='text/plain', status_int=500) return {'one': one, 'project': 'sqla_demo'} + 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_sqla_demo_db" script - to initialize your database tables. Check your virtual + 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 -- cgit v1.2.3 From 1779c2a7c7653142cc3164e83b1f0fe43850f97b Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sun, 24 Jan 2016 00:15:46 -0800 Subject: Bump sphinx to 1.3.5; revert comment syntax. Closes #2251. --- docs/quick_tour/requests/app.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/quick_tour/requests/app.py b/docs/quick_tour/requests/app.py index 621b0693e..f55264cff 100644 --- a/docs/quick_tour/requests/app.py +++ b/docs/quick_tour/requests/app.py @@ -4,7 +4,7 @@ from pyramid.response import Response def hello_world(request): - """ Some parameters from a request such as /?name=lisa """ + # Some parameters from a request such as /?name=lisa url = request.url name = request.params.get('name', 'No Name Provided') diff --git a/setup.py b/setup.py index 211c0a4ec..e878b9932 100644 --- a/setup.py +++ b/setup.py @@ -57,7 +57,7 @@ if not PY3: tests_require.append('zope.component>=3.11.0') docs_extras = [ - 'Sphinx >= 1.3.4', + 'Sphinx >= 1.3.5', 'docutils', 'repoze.sphinx.autointerface', 'pylons_sphinx_latesturl', -- cgit v1.2.3 From 3cdd3cedb9a120228f5bb2c0a9d53c0017b55cd9 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sun, 24 Jan 2016 01:02:42 -0800 Subject: Use proper syntax in code samples to allow highlighting and avoid errors. See https://github.com/sphinx-doc/sphinx/issues/2264 --- docs/narr/project.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/narr/project.rst b/docs/narr/project.rst index 5103bb6b8..923fde436 100644 --- a/docs/narr/project.rst +++ b/docs/narr/project.rst @@ -447,7 +447,7 @@ commenting out a line. For example, instead of: :linenos: [app:main] - ... + # ... elided configuration pyramid.includes = pyramid_debugtoolbar @@ -457,7 +457,7 @@ Put a hash mark at the beginning of the ``pyramid_debugtoolbar`` line: :linenos: [app:main] - ... + # ... elided configuration pyramid.includes = # pyramid_debugtoolbar -- cgit v1.2.3 From 22f221099e785cb763e9659da029933ca9fc11e6 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sun, 24 Jan 2016 01:10:13 -0800 Subject: Use proper syntax names in code samples to allow highlighting and avoid errors. See https://github.com/sphinx-doc/sphinx/issues/2264 --- docs/narr/webob.rst | 2 +- docs/quick_tutorial/requirements.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/narr/webob.rst b/docs/narr/webob.rst index f18cf1dfb..cfcf532bc 100644 --- a/docs/narr/webob.rst +++ b/docs/narr/webob.rst @@ -269,7 +269,7 @@ to a :app:`Pyramid` application: When such a request reaches a view in your application, the ``request.json_body`` attribute will be available in the view callable body. -.. code-block:: javascript +.. code-block:: python @view_config(renderer='string') def aview(request): diff --git a/docs/quick_tutorial/requirements.rst b/docs/quick_tutorial/requirements.rst index 6d5a965cd..f855dcb55 100644 --- a/docs/quick_tutorial/requirements.rst +++ b/docs/quick_tutorial/requirements.rst @@ -109,7 +109,7 @@ For Linux, the commands to do so are as follows: For Windows: -.. code-block:: posh +.. code-block:: ps1con # Windows c:\> cd \ -- cgit v1.2.3 From 65bb52bafa5d44b378f01dfb47816a952bf93a66 Mon Sep 17 00:00:00 2001 From: Svintsov Dmitry Date: Sun, 24 Jan 2016 16:36:43 +0500 Subject: fix indent in docs --- docs/narr/install.rst | 4 ++-- docs/narr/subrequest.rst | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/narr/install.rst b/docs/narr/install.rst index c4e3e2c5f..767b16fc0 100644 --- a/docs/narr/install.rst +++ b/docs/narr/install.rst @@ -292,7 +292,7 @@ After you've got your virtualenv installed, you may install :app:`Pyramid` itself using the following commands: .. parsed-literal:: - + $ $VENV/bin/easy_install "pyramid==\ |release|\ " The ``easy_install`` command will take longer than the previous ones to @@ -371,7 +371,7 @@ You can use Pyramid on Windows under Python 2 or 3. installed: .. parsed-literal:: - + c:\\env> %VENV%\\Scripts\\easy_install "pyramid==\ |release|\ " What Gets Installed diff --git a/docs/narr/subrequest.rst b/docs/narr/subrequest.rst index 02ae14aa5..daa3cc43f 100644 --- a/docs/narr/subrequest.rst +++ b/docs/narr/subrequest.rst @@ -17,7 +17,7 @@ application. Here's an example application which uses a subrequest: .. code-block:: python - :linenos: + :linenos: from wsgiref.simple_server import make_server from pyramid.config import Configurator @@ -61,8 +61,8 @@ adapter when found and invoked via object: .. code-block:: python - :linenos: - :emphasize-lines: 11 + :linenos: + :emphasize-lines: 11 from wsgiref.simple_server import make_server from pyramid.config import Configurator @@ -106,8 +106,8 @@ exception, the exception will be raised to the caller of :term:`exception view` configured: .. code-block:: python - :linenos: - :emphasize-lines: 11-16 + :linenos: + :emphasize-lines: 11-16 from wsgiref.simple_server import make_server from pyramid.config import Configurator @@ -175,8 +175,8 @@ We can cause the subrequest to be run through the tween stack by passing :meth:`~pyramid.request.Request.invoke_subrequest`, like this: .. code-block:: python - :linenos: - :emphasize-lines: 7 + :linenos: + :emphasize-lines: 7 from wsgiref.simple_server import make_server from pyramid.config import Configurator -- cgit v1.2.3 From 41aa6080791c082c2eeffa2540d53bddea09decd Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Mon, 25 Jan 2016 00:50:01 -0800 Subject: minor grammar and rst fixes, rewrap to 79 columns --- docs/designdefense.rst | 101 +++++++++++++++++++++++++------------------------ 1 file changed, 51 insertions(+), 50 deletions(-) diff --git a/docs/designdefense.rst b/docs/designdefense.rst index c58cc5403..f4decebe6 100644 --- a/docs/designdefense.rst +++ b/docs/designdefense.rst @@ -630,7 +630,8 @@ dependencies by forcing us to make better packaging decisions. Removing Chameleon and Mako templating system dependencies in the Pyramid core in 1.5 let us shed most of the remainder of them. -Pyramid "Cheats" To Obtain Speed + +Pyramid "Cheats" to Obtain Speed -------------------------------- Complaints have been lodged by other web framework authors at various times @@ -639,10 +640,11 @@ mechanism is our use (transitively) of the C extensions provided by :mod:`zope.interface` to do fast lookups. Another claimed cheating mechanism is the religious avoidance of extraneous function calls. -If there's such a thing as cheating to get better performance, we want to -cheat as much as possible. We optimize :app:`Pyramid` aggressively. This -comes at a cost: the core code has sections that could be expressed more -readably. As an amelioration, we've commented these sections liberally. +If there's such a thing as cheating to get better performance, we want to cheat +as much as possible. We optimize :app:`Pyramid` aggressively. This comes at a +cost. The core code has sections that could be expressed with more readability. +As an amelioration, we've commented these sections liberally. + Pyramid Gets Its Terminology Wrong ("MVC") ------------------------------------------ @@ -655,8 +657,8 @@ existing "MVC" framework uses its terminology. For example, you probably expect that models are ORM models, controllers are classes that have methods that map to URLs, and views are templates. :app:`Pyramid` indeed has each of these concepts, and each probably *works* almost exactly like your existing -"MVC" web framework. We just don't use the MVC terminology, as we can't -square its usage in the web framework space with historical reality. +"MVC" web framework. We just don't use the MVC terminology, as we can't square +its usage in the web framework space with historical reality. People very much want to give web applications the same properties as common desktop GUI platforms by using similar terminology, and to provide some frame @@ -665,60 +667,59 @@ hang together. But in the opinion of the author, "MVC" doesn't match the web very well in general. Quoting from the `Model-View-Controller Wikipedia entry `_: -.. code-block:: text + Though MVC comes in different flavors, control flow is generally as + follows: - Though MVC comes in different flavors, control flow is generally as - follows: + The user interacts with the user interface in some way (for example, + presses a mouse button). - The user interacts with the user interface in some way (for - example, presses a mouse button). + The controller handles the input event from the user interface, often via + a registered handler or callback and converts the event into appropriate + user action, understandable for the model. - The controller handles the input event from the user interface, - often via a registered handler or callback and converts the event - into appropriate user action, understandable for the model. + The controller notifies the model of the user action, possibly resulting + in a change in the model's state. (For example, the controller updates the + user's shopping cart.)[5] - The controller notifies the model of the user action, possibly - resulting in a change in the model's state. (For example, the - controller updates the user's shopping cart.)[5] + A view queries the model in order to generate an appropriate user + interface (for example, the view lists the shopping cart's contents). Note + that the view gets its own data from the model. - A view queries the model in order to generate an appropriate - user interface (for example, the view lists the shopping cart's - contents). Note that the view gets its own data from the model. + The controller may (in some implementations) issue a general instruction + to the view to render itself. In others, the view is automatically + notified by the model of changes in state (Observer) which require a + screen update. - The controller may (in some implementations) issue a general - instruction to the view to render itself. In others, the view is - automatically notified by the model of changes in state - (Observer) which require a screen update. - - The user interface waits for further user interactions, which - restarts the cycle. + The user interface waits for further user interactions, which restarts the + cycle. To the author, it seems as if someone edited this Wikipedia definition, tortuously couching concepts in the most generic terms possible in order to -account for the use of the term "MVC" by current web frameworks. I doubt -such a broad definition would ever be agreed to by the original authors of -the MVC pattern. But *even so*, it seems most MVC web frameworks fail to -meet even this falsely generic definition. +account for the use of the term "MVC" by current web frameworks. I doubt such +a broad definition would ever be agreed to by the original authors of the MVC +pattern. But *even so*, it seems most MVC web frameworks fail to meet even +this falsely generic definition. For example, do your templates (views) always query models directly as is -claimed in "note that the view gets its own data from the model"? Probably -not. My "controllers" tend to do this, massaging the data for easier use by -the "view" (template). What do you do when your "controller" returns JSON? Do -your controllers use a template to generate JSON? If not, what's the "view" -then? Most MVC-style GUI web frameworks have some sort of event system -hooked up that lets the view detect when the model changes. The web just has -no such facility in its current form: it's effectively pull-only. - -So, in the interest of not mistaking desire with reality, and instead of -trying to jam the square peg that is the web into the round hole of "MVC", we -just punt and say there are two things: resources and views. The resource -tree represents a site structure, the view presents a resource. The -templates are really just an implementation detail of any given view: a view -doesn't need a template to return a response. There's no "controller": it -just doesn't exist. The "model" is either represented by the resource tree -or by a "domain model" (like a SQLAlchemy model) that is separate from the -framework entirely. This seems to us like more reasonable terminology, given -the current constraints of the web. +claimed in "note that the view gets its own data from the model"? Probably not. +My "controllers" tend to do this, massaging the data for easier use by the +"view" (template). What do you do when your "controller" returns JSON? Do your +controllers use a template to generate JSON? If not, what's the "view" then? +Most MVC-style GUI web frameworks have some sort of event system hooked up that +lets the view detect when the model changes. The web just has no such facility +in its current form; it's effectively pull-only. + +So, in the interest of not mistaking desire with reality, and instead of trying +to jam the square peg that is the web into the round hole of "MVC", we just +punt and say there are two things: resources and views. The resource tree +represents a site structure, the view presents a resource. The templates are +really just an implementation detail of any given view. A view doesn't need a +template to return a response. There's no "controller"; it just doesn't exist. +The "model" is either represented by the resource tree or by a "domain model" +(like an SQLAlchemy model) that is separate from the framework entirely. This +seems to us like more reasonable terminology, given the current constraints of +the web. + .. _apps_are_extensible: -- cgit v1.2.3 From 628dac551663d6e676f6a3b35cb81375709c4525 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Tue, 26 Jan 2016 00:03:41 -0800 Subject: minor grammar and rst fixes, rewrap to 79 columns, in section "Pyramid Applications Are Extensible" --- docs/designdefense.rst | 183 ++++++++++++++++++++++++------------------------- 1 file changed, 90 insertions(+), 93 deletions(-) diff --git a/docs/designdefense.rst b/docs/designdefense.rst index f4decebe6..f757a8e70 100644 --- a/docs/designdefense.rst +++ b/docs/designdefense.rst @@ -723,125 +723,122 @@ the web. .. _apps_are_extensible: -Pyramid Applications are Extensible; I Don't Believe In Application Extensibility +Pyramid Applications Are Extensible; I Don't Believe in Application Extensibility --------------------------------------------------------------------------------- Any :app:`Pyramid` application written obeying certain constraints is *extensible*. This feature is discussed in the :app:`Pyramid` documentation -chapters named :ref:`extending_chapter` and :ref:`advconfig_narr`. It is -made possible by the use of the :term:`Zope Component Architecture` and -within :app:`Pyramid`. +chapters named :ref:`extending_chapter` and :ref:`advconfig_narr`. It is made +possible by the use of the :term:`Zope Component Architecture` within +:app:`Pyramid`. -"Extensible", in this context, means: +"Extensible" in this context means: -- The behavior of an application can be overridden or extended in a - particular *deployment* of the application without requiring that - the deployer modify the source of the original application. +- The behavior of an application can be overridden or extended in a particular + *deployment* of the application without requiring that the deployer modify + the source of the original application. -- The original developer is not required to anticipate any - extensibility plugpoints at application creation time to allow - fundamental application behavior to be overriden or extended. +- The original developer is not required to anticipate any extensibility + plug points at application creation time to allow fundamental application + behavior to be overridden or extended. - The original developer may optionally choose to anticipate an - application-specific set of plugpoints, which may be hooked by - a deployer. If he chooses to use the facilities provided by the - ZCA, the original developer does not need to think terribly hard - about the mechanics of introducing such a plugpoint. + application-specific set of plug points, which may be hooked by a deployer. + If they choose to use the facilities provided by the ZCA, the original + developer does not need to think terribly hard about the mechanics of + introducing such a plug point. Many developers seem to believe that creating extensible applications is not -worth it. They instead suggest that modifying the source of a given -application for each deployment to override behavior is more reasonable. -Much discussion about version control branching and merging typically ensues. - -It's clear that making every application extensible isn't required. The -majority of web applications only have a single deployment, and thus needn't -be extensible at all. However, some web applications have multiple -deployments, and some have *many* deployments. For example, a generic -content management system (CMS) may have basic functionality that needs to be -extended for a particular deployment. That CMS system may be deployed for -many organizations at many places. Some number of deployments of this CMS -may be deployed centrally by a third party and managed as a group. It's -easier to be able to extend such a system for each deployment via preordained -plugpoints than it is to continually keep each software branch of the system -in sync with some upstream source: the upstream developers may change code in -such a way that your changes to the same codebase conflict with theirs in -fiddly, trivial ways. Merging such changes repeatedly over the lifetime of a -deployment can be difficult and time consuming, and it's often useful to be -able to modify an application for a particular deployment in a less invasive -way. +worth it. They instead suggest that modifying the source of a given application +for each deployment to override behavior is more reasonable. Much discussion +about version control branching and merging typically ensues. + +It's clear that making every application extensible isn't required. The +majority of web applications only have a single deployment, and thus needn't be +extensible at all. However some web applications have multiple deployments, and +others have *many* deployments. For example, a generic content management +system (CMS) may have basic functionality that needs to be extended for a +particular deployment. That CMS may be deployed for many organizations at many +places. Some number of deployments of this CMS may be deployed centrally by a +third party and managed as a group. It's easier to be able to extend such a +system for each deployment via preordained plug points than it is to +continually keep each software branch of the system in sync with some upstream +source. The upstream developers may change code in such a way that your changes +to the same codebase conflict with theirs in fiddly, trivial ways. Merging such +changes repeatedly over the lifetime of a deployment can be difficult and time +consuming, and it's often useful to be able to modify an application for a +particular deployment in a less invasive way. If you don't want to think about :app:`Pyramid` application extensibility at -all, you needn't. You can ignore extensibility entirely. However, if you -follow the set of rules defined in :ref:`extending_chapter`, you don't need -to *make* your application extensible: any application you write in the -framework just *is* automatically extensible at a basic level. The -mechanisms that deployers use to extend it will be necessarily coarse: -typically, views, routes, and resources will be capable of being -overridden. But for most minor (and even some major) customizations, these -are often the only override plugpoints necessary: if the application doesn't -do exactly what the deployment requires, it's often possible for a deployer -to override a view, route, or resource and quickly make it do what he or she -wants it to do in ways *not necessarily anticipated by the original -developer*. Here are some example scenarios demonstrating the benefits of -such a feature. - -- If a deployment needs a different styling, the deployer may override the - main template and the CSS in a separate Python package which defines - overrides. - -- If a deployment needs an application page to do something differently, or - to expose more or different information, the deployer may override the - view that renders the page within a separate Python package. +all, you needn't. You can ignore extensibility entirely. However if you follow +the set of rules defined in :ref:`extending_chapter`, you don't need to *make* +your application extensible. Any application you write in the framework just +*is* automatically extensible at a basic level. The mechanisms that deployers +use to extend it will be necessarily coarse. Typically views, routes, and +resources will be capable of being overridden. But for most minor (and even +some major) customizations, these are often the only override plug points +necessary. If the application doesn't do exactly what the deployment requires, +it's often possible for a deployer to override a view, route, or resource, and +quickly make it do what they want it to do in ways *not necessarily anticipated +by the original developer*. Here are some example scenarios demonstrating the +benefits of such a feature. + +- If a deployment needs a different styling, the deployer may override the main + template and the CSS in a separate Python package which defines overrides. + +- If a deployment needs an application page to do something differently, or to + expose more or different information, the deployer may override the view that + renders the page within a separate Python package. - If a deployment needs an additional feature, the deployer may add a view to the override package. -As long as the fundamental design of the upstream package doesn't change, -these types of modifications often survive across many releases of the -upstream package without needing to be revisited. +As long as the fundamental design of the upstream package doesn't change, these +types of modifications often survive across many releases of the upstream +package without needing to be revisited. Extending an application externally is not a panacea, and carries a set of -risks similar to branching and merging: sometimes major changes upstream will -cause you to need to revisit and update some of your modifications. But you -won't regularly need to deal wth meaningless textual merge conflicts that -trivial changes to upstream packages often entail when it comes time to -update the upstream package, because if you extend an application externally, -there just is no textual merge done. Your modifications will also, for -whatever it's worth, be contained in one, canonical, well-defined place. +risks similar to branching and merging. Sometimes major changes upstream will +cause you to revisit and update some of your modifications. But you won't +regularly need to deal with meaningless textual merge conflicts that trivial +changes to upstream packages often entail when it comes time to update the +upstream package, because if you extend an application externally, there just +is no textual merge done. Your modifications will also, for whatever it's +worth, be contained in one, canonical, well-defined place. Branching an application and continually merging in order to get new features -and bugfixes is clearly useful. You can do that with a :app:`Pyramid` -application just as usefully as you can do it with any application. But +and bug fixes is clearly useful. You can do that with a :app:`Pyramid` +application just as usefully as you can do it with any application. But deployment of an application written in :app:`Pyramid` makes it possible to -avoid the need for this even if the application doesn't define any plugpoints -ahead of time. It's possible that promoters of competing web frameworks -dismiss this feature in favor of branching and merging because applications -written in their framework of choice aren't extensible out of the box in a -comparably fundamental way. +avoid the need for this even if the application doesn't define any plug points +ahead of time. It's possible that promoters of competing web frameworks dismiss +this feature in favor of branching and merging because applications written in +their framework of choice aren't extensible out of the box in a comparably +fundamental way. While :app:`Pyramid` applications are fundamentally extensible even if you don't write them with specific extensibility in mind, if you're moderately -adventurous, you can also take it a step further. If you learn more about -the :term:`Zope Component Architecture`, you can optionally use it to expose -other more domain-specific configuration plugpoints while developing an -application. The plugpoints you expose needn't be as coarse as the ones -provided automatically by :app:`Pyramid` itself. For example, you might -compose your own directive that configures a set of views for a prebaked -purpose (e.g. ``restview`` or somesuch) , allowing other people to refer to -that directive when they make declarations in the ``includeme`` of their -customization package. There is a cost for this: the developer of an -application that defines custom plugpoints for its deployers will need to -understand the ZCA or he will need to develop his own similar extensibility -system. - -Ultimately, any argument about whether the extensibility features lent to -applications by :app:`Pyramid` are good or bad is mostly pointless. You -needn't take advantage of the extensibility features provided by a particular +adventurous, you can also take it a step further. If you learn more about the +:term:`Zope Component Architecture`, you can optionally use it to expose other +more domain-specific configuration plug points while developing an application. +The plug points you expose needn't be as coarse as the ones provided +automatically by :app:`Pyramid` itself. For example, you might compose your own +directive that configures a set of views for a pre-baked purpose (e.g., +``restview`` or somesuch), allowing other people to refer to that directive +when they make declarations in the ``includeme`` of their customization +package. There is a cost for this: the developer of an application that defines +custom plug points for its deployers will need to understand the ZCA or they +will need to develop their own similar extensibility system. + +Ultimately any argument about whether the extensibility features lent to +applications by :app:`Pyramid` are good or bad is mostly pointless. You needn't +take advantage of the extensibility features provided by a particular :app:`Pyramid` application in order to affect a modification for a particular -set of its deployments. You can ignore the application's extensibility -plugpoints entirely, and use version control branching and merging to -manage application deployment modifications instead, as if you were deploying -an application written using any other web framework. +set of its deployments. You can ignore the application's extensibility plug +points entirely, and use version control branching and merging to manage +application deployment modifications instead, as if you were deploying an +application written using any other web framework. + Zope 3 Enforces "TTW" Authorization Checks By Default; Pyramid Does Not ----------------------------------------------------------------------- -- cgit v1.2.3 From 99d06f3646d8f60f8d58aba1b85b1fe1eb0b8f67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Raczy=C5=84ski?= Date: Tue, 26 Jan 2016 23:03:34 +0100 Subject: Fixed --browser behaviour when --server-name provided Fixed PR #1533. Calling: pserve --server-name abc --browser app.ini caused error : "LookupError: No section 'main' (prefixed by 'server') found in config app.ini" --- pyramid/scripts/pserve.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyramid/scripts/pserve.py b/pyramid/scripts/pserve.py index 155b82bdc..6737966ba 100644 --- a/pyramid/scripts/pserve.py +++ b/pyramid/scripts/pserve.py @@ -9,7 +9,7 @@ # lib/site.py import atexit -import ctypes +import ctypes. import errno import logging import optparse @@ -391,7 +391,7 @@ a real process manager for your processes like Systemd, Circus, or Supervisor. if self.options.browser: def open_browser(): - context = loadcontext(SERVER, app_spec, name=app_name, relative_to=base, + context = loadcontext(SERVER, app_spec, name=server_name, relative_to=base, global_conf=vars) url = 'http://127.0.0.1:{port}/'.format(**context.config()) time.sleep(1) -- cgit v1.2.3 From a7760fa5a2b0d37337d5e76093cf09a627407222 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Raczy=C5=84ski?= Date: Tue, 26 Jan 2016 23:07:45 +0100 Subject: Typo --- pyramid/scripts/pserve.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/scripts/pserve.py b/pyramid/scripts/pserve.py index 6737966ba..3ea614eb5 100644 --- a/pyramid/scripts/pserve.py +++ b/pyramid/scripts/pserve.py @@ -9,7 +9,7 @@ # lib/site.py import atexit -import ctypes. +import ctypes import errno import logging import optparse -- cgit v1.2.3 From 26adba13bb999b73398727925fe0ace7c947f59d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Raczy=C5=84ski?= Date: Tue, 26 Jan 2016 23:24:19 +0100 Subject: Added myself to contributor list --- CONTRIBUTORS.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 1f3597e84..7c895ac15 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -258,3 +258,5 @@ Contributors - Rami Chousein, 2015/10/28 - Sri Sanketh Uppalapati, 2015/12/12 + +- Marcin Raczyński, 2016/01/26 -- cgit v1.2.3 From 802c3f69b29a4c3ba2af68f0265372dce08e5bcc Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 26 Jan 2016 17:00:55 -0600 Subject: update changelog for #2292 --- CHANGES.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index d31e471f5..ffa5f51e0 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -4,6 +4,12 @@ unreleased - Dropped Python 3.2 support. See https://github.com/Pylons/pyramid/pull/2256 +- Fix ``pserve --browser`` to use the ``--server-name`` instead of the + app name when selecting a section to use. This was only working for people + who had server and app sections with the same name, for example + ``[app:main]`` and ``[server:main]``. + See https://github.com/Pylons/pyramid/pull/2292 + 1.6 (2015-04-14) ================ -- cgit v1.2.3 From ef01de3970ecaabc7fbabbbd461adcada2997254 Mon Sep 17 00:00:00 2001 From: Zsolt Ero Date: Wed, 27 Jan 2016 02:24:19 +0100 Subject: check_csrf vs. csrf_token in view_config docs --- pyramid/view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/view.py b/pyramid/view.py index 2867e3d6f..7e8996ca4 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -165,7 +165,7 @@ class view_config(object): ``request_type``, ``route_name``, ``request_method``, ``request_param``, ``containment``, ``xhr``, ``accept``, ``header``, ``path_info``, ``custom_predicates``, ``decorator``, ``mapper``, ``http_cache``, - ``match_param``, ``csrf_token``, ``physical_path``, and ``predicates``. + ``match_param``, ``check_csrf``, ``physical_path``, and ``predicates``. The meanings of these arguments are the same as the arguments passed to :meth:`pyramid.config.Configurator.add_view`. If any argument is left -- cgit v1.2.3 From 4df9a09807a844192e7769489d452a071b59c80c Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Wed, 27 Jan 2016 10:54:43 -0800 Subject: minor grammar fixes, rewrap to 79 columns, in section "Zope 3 Enforces 'TTW'..." --- docs/designdefense.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/designdefense.rst b/docs/designdefense.rst index f757a8e70..b7aca07ea 100644 --- a/docs/designdefense.rst +++ b/docs/designdefense.rst @@ -840,17 +840,16 @@ application deployment modifications instead, as if you were deploying an application written using any other web framework. -Zope 3 Enforces "TTW" Authorization Checks By Default; Pyramid Does Not +Zope 3 Enforces "TTW" Authorization Checks by Default; Pyramid Does Not ----------------------------------------------------------------------- Challenge +++++++++ :app:`Pyramid` performs automatic authorization checks only at :term:`view` -execution time. Zope 3 wraps context objects with a `security proxy -`_, which causes Zope 3 to -do also security checks during attribute access. I like this, because it -means: +execution time. Zope 3 wraps context objects with a `security proxy +`_, which causes Zope 3 also +to do security checks during attribute access. I like this, because it means: #) When I use the security proxy machinery, I can have a view that conditionally displays certain HTML elements (like form fields) or @@ -882,7 +881,7 @@ web framework. And since we tend to use the same toolkit for all web applications, it's just never been a concern to be able to use the same set of restricted-execution -code under two web different frameworks. +code under two different web frameworks. Justifications for disabling security proxies by default notwithstanding, given that Zope 3 security proxies are viral by nature, the only requirement @@ -895,6 +894,7 @@ Zope3-security-proxy-wrapped objects for each traversed object (including the :term:`context` and the :term:`root`). This would have the effect of creating a more Zope3-like environment without much effort. + .. _http_exception_hierarchy: Pyramid uses its own HTTP exception class hierarchy rather than :mod:`webob.exc` -- cgit v1.2.3 From 9270e08bf8839e2bf8afa11033834a0f3b68d3dd Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Wed, 27 Jan 2016 22:20:10 -0600 Subject: add test to reproduce #2294 --- pyramid/tests/test_session.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pyramid/tests/test_session.py b/pyramid/tests/test_session.py index 82e4fb001..914d28a83 100644 --- a/pyramid/tests/test_session.py +++ b/pyramid/tests/test_session.py @@ -695,6 +695,13 @@ class Test_check_csrf_token(unittest.TestCase): result = self._callFUT(request, 'csrf_token', raises=False) self.assertEqual(result, False) + def test_token_differing_types(self): + from pyramid.compat import text_ + request = testing.DummyRequest() + request.session['_csrft_'] = text_('foo') + request.params['csrf_token'] = b'foo' + self.assertEqual(self._callFUT(request, token='csrf_token'), True) + class DummySerializer(object): def dumps(self, value): return base64.b64encode(json.dumps(value).encode('utf-8')) -- cgit v1.2.3 From 183804c747ec465383fac7f57c5b3f61a81fde51 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Wed, 27 Jan 2016 22:20:19 -0600 Subject: set DummySession to use unicode csrf token by default like SignedCookieSessionFactory --- pyramid/testing.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyramid/testing.py b/pyramid/testing.py index 58dcb0b59..14432b01f 100644 --- a/pyramid/testing.py +++ b/pyramid/testing.py @@ -16,6 +16,7 @@ from pyramid.compat import ( PY3, PYPY, class_types, + text_, ) from pyramid.config import Configurator @@ -274,7 +275,7 @@ class DummySession(dict): return storage def new_csrf_token(self): - token = '0123456789012345678901234567890123456789' + token = text_('0123456789012345678901234567890123456789') self['_csrft_'] = token return token -- cgit v1.2.3 From f16a1bc04b8b42324ccb6c6d01e887633e5448dd Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Wed, 27 Jan 2016 22:20:59 -0600 Subject: convert csrf tokens to bytes prior to string compare --- pyramid/session.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyramid/session.py b/pyramid/session.py index b3be68705..a4cdf910d 100644 --- a/pyramid/session.py +++ b/pyramid/session.py @@ -126,7 +126,8 @@ def check_csrf_token(request, .. versionadded:: 1.4a2 """ supplied_token = request.params.get(token, request.headers.get(header, "")) - if strings_differ(request.session.get_csrf_token(), supplied_token): + expected_token = request.session.get_csrf_token() + if strings_differ(bytes_(expected_token), bytes_(supplied_token)): if raises: raise BadCSRFToken('check_csrf_token(): Invalid token') return False -- cgit v1.2.3 From e636eeb1ad08dc0f5f1457ee086636045c4ce1e8 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Fri, 29 Jan 2016 00:32:31 -0800 Subject: minor grammar fixes, rewrap to 79 columns, in section "Pyramid uses its own HTTP exception class hierarchy" --- docs/designdefense.rst | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/designdefense.rst b/docs/designdefense.rst index b7aca07ea..2da10108a 100644 --- a/docs/designdefense.rst +++ b/docs/designdefense.rst @@ -907,28 +907,29 @@ much like the ones defined in :mod:`webob.exc`, (e.g., :class:`~pyramid.httpexceptions.HTTPNotFound` or :class:`~pyramid.httpexceptions.HTTPForbidden`). They have the same names and largely the same behavior, and all have a very similar implementation, but not -the same identity. Here's why they have a separate identity: +the same identity. Here's why they have a separate identity. - Making them separate allows the HTTP exception classes to subclass :class:`pyramid.response.Response`. This speeds up response generation - slightly due to the way the Pyramid router works. The same speedup could be + slightly due to the way the Pyramid router works. The same speed up could be gained by monkeypatching :class:`webob.response.Response`, but it's usually the case that monkeypatching turns out to be evil and wrong. -- Making them separate allows them to provide alternate ``__call__`` logic +- Making them separate allows them to provide alternate ``__call__`` logic, which also speeds up response generation. - Making them separate allows the exception classes to provide for the proper value of ``RequestClass`` (:class:`pyramid.request.Request`). -- Making them separate allows us freedom from having to think about backwards - compatibility code present in :mod:`webob.exc` having to do with Python 2.4, - which we no longer support in Pyramid 1.1+. +- Making them separate gives us freedom from thinking about backwards + compatibility code present in :mod:`webob.exc` related to Python 2.4, which + we no longer support in Pyramid 1.1+. - We change the behavior of two classes (:class:`~pyramid.httpexceptions.HTTPNotFound` and :class:`~pyramid.httpexceptions.HTTPForbidden`) in the module so that they - can be used by Pyramid internally for notfound and forbidden exceptions. + can be used by Pyramid internally for ``notfound`` and ``forbidden`` + exceptions. - Making them separate allows us to influence the docstrings of the exception classes to provide Pyramid-specific documentation. @@ -937,6 +938,7 @@ the same identity. Here's why they have a separate identity: Python 2.6 when the response objects are used as exceptions (related to ``self.message``). + .. _simpler_traversal_model: Pyramid has Simpler Traversal Machinery than Does Zope -- cgit v1.2.3 From a9f92bebb31eeeb43fc79eef132567c9abdb3719 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Fri, 29 Jan 2016 22:39:19 -0800 Subject: add update docs/conf.py for RELEASING.txt --- RELEASING.txt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/RELEASING.txt b/RELEASING.txt index 61420ce8b..bc8f2c721 100644 --- a/RELEASING.txt +++ b/RELEASING.txt @@ -28,12 +28,16 @@ Releasing Pyramid include a link under "Bug Fix Releases" to the minor feature changes in CHANGES.txt . -- update README.rst to use correct versions of badges and URLs according to +- Update README.rst to use correct versions of badges and URLs according to each branch and context, i.e., RTD "latest" == GitHub/Travis "1.x-branch". - Update whatsnew-X.X.rst in docs to point at change log entries for individual releases if applicable. +- For major version releases, in docs/conf.py, update values under + html_theme_options for in_progress and outdated across master, releasing + branch, and previously released branch. + - Change setup.py version to the new version number. - Change CHANGES.txt heading to reflect the new version number. -- cgit v1.2.3 From bd88dcd361a7a25450ee4abb7416b83cddd93ee2 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Fri, 29 Jan 2016 22:43:22 -0800 Subject: update instructions for major release in conf.py html_theme_options --- docs/conf.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index 073811eca..4cac1e913 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -129,7 +129,10 @@ html_theme = 'pyramid' html_theme_path = pylons_sphinx_themes.get_html_themes_path() html_theme_options = dict( github_url='https://github.com/Pylons/pyramid', + # on master branch true, else false in_progress='true', + # on previous branches/major releases true, else false + outdated='false', ) # The name for this set of Sphinx documents. If None, it defaults to -- cgit v1.2.3 From 8d2a2b967ff912aa0b8e74128cf2b5473d6db44a Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Fri, 29 Jan 2016 23:20:25 -0800 Subject: fix heading under/overlines for rst syntax (cherry picked from commit 60b74ee) --- docs/tutorials/wiki/design.rst | 4 ++-- docs/tutorials/wiki2/design.rst | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/tutorials/wiki/design.rst b/docs/tutorials/wiki/design.rst index 49c30d29a..46c2a2f30 100644 --- a/docs/tutorials/wiki/design.rst +++ b/docs/tutorials/wiki/design.rst @@ -1,6 +1,6 @@ -========== +====== Design -========== +====== Following is a quick overview of the design of our wiki application, to help us understand the changes that we will be making as we work through the diff --git a/docs/tutorials/wiki2/design.rst b/docs/tutorials/wiki2/design.rst index e9f361e7d..52f2ce7a5 100644 --- a/docs/tutorials/wiki2/design.rst +++ b/docs/tutorials/wiki2/design.rst @@ -1,6 +1,6 @@ -========== +====== Design -========== +====== Following is a quick overview of the design of our wiki application, to help us understand the changes that we will be making as we work through the -- cgit v1.2.3 From 1cb1104f983ca92695ca24cb79ad6bfbb432aad3 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sat, 30 Jan 2016 00:31:01 -0800 Subject: clean up principal and userid glossary entries for grammar, rst syntax --- docs/glossary.rst | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/glossary.rst b/docs/glossary.rst index 60f03f000..2683ff369 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -234,7 +234,7 @@ Glossary object *location-aware*. permission - A string or unicode object that represents an action being taken against + A string or Unicode object that represents an action being taken against a :term:`context` resource. A permission is associated with a view name and a resource type by the developer. Resources are decorated with security declarations (e.g. an :term:`ACL`), which reference these @@ -291,22 +291,22 @@ Glossary :term:`authorization policy`. principal - A *principal* is a string or unicode object representing an - entity, typically a user or group. Principals are provided by an - :term:`authentication policy`. For example, if a user had the - :term:`userid` `"bob"`, and was part of two groups named `"group foo"` - and "group bar", the request might have information attached to - it that would indicate that Bob was represented by three - principals: `"bob"`, `"group foo"` and `"group bar"`. + A *principal* is a string or Unicode object representing an entity, + typically a user or group. Principals are provided by an + :term:`authentication policy`. For example, if a user has the + :term:`userid` `bob`, and is a member of two groups named `group foo` and + `group bar`, then the request might have information attached to it + indicating that Bob was represented by three principals: `bob`, `group + foo` and `group bar`. userid - A *userid* is a string or unicode object used to identify and - authenticate a real-world user (or client). A userid is - supplied to an :term:`authentication policy` in order to discover - the user's :term:`principals `. The default behavior - of the authentication policies :app:`Pyramid` provides is to - return the user's userid as a principal, but this is not strictly - necessary in custom policies that define their principals differently. + A *userid* is a string or Unicode object used to identify and authenticate + a real-world user or client. A userid is supplied to an + :term:`authentication policy` in order to discover the user's + :term:`principals `. In the authentication policies which + :app:`Pyramid` provides, the default behavior returns the user's userid as + a principal, but this is not strictly necessary in custom policies that + define their principals differently. authorization policy An authorization policy in :app:`Pyramid` terms is a bit of -- cgit v1.2.3 From 48738dc116f9916356ac1d41029c3682b978a4ed Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sat, 30 Jan 2016 18:04:49 -0800 Subject: add instructions for enabling pylons_sphinx_latesturl on previously released branch when making a new major release --- RELEASING.txt | 3 ++- docs/conf.py | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/RELEASING.txt b/RELEASING.txt index bc8f2c721..142005ed7 100644 --- a/RELEASING.txt +++ b/RELEASING.txt @@ -36,7 +36,8 @@ Releasing Pyramid - For major version releases, in docs/conf.py, update values under html_theme_options for in_progress and outdated across master, releasing - branch, and previously released branch. + branch, and previously released branch. Also in the previously released + branch only, uncomment the sections to enable pylons_sphinx_latesturl. - Change setup.py version to the new version number. diff --git a/docs/conf.py b/docs/conf.py index 4cac1e913..1004e1cf9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -55,6 +55,8 @@ extensions = [ 'sphinx.ext.viewcode', 'sphinx.ext.intersphinx', 'sphinxcontrib.programoutput', + # enable pylons_sphinx_latesturl when this branch is no longer "latest" + # 'pylons_sphinx_latesturl', ] # Looks for objects in external projects @@ -124,6 +126,21 @@ if book: # Options for HTML output # ----------------------- +# enable pylons_sphinx_latesturl when this branch is no longer "latest" +# pylons_sphinx_latesturl_base = ( +# 'http://docs.pylonsproject.org/projects/pyramid/en/latest/') +# pylons_sphinx_latesturl_pagename_overrides = { +# # map old pagename -> new pagename +# 'whatsnew-1.0': 'index', +# 'whatsnew-1.1': 'index', +# 'whatsnew-1.2': 'index', +# 'whatsnew-1.3': 'index', +# 'whatsnew-1.4': 'index', +# 'whatsnew-1.5': 'index', +# 'tutorials/gae/index': 'index', +# 'api/chameleon_text': 'api', +# 'api/chameleon_zpt': 'api', +# } html_theme = 'pyramid' html_theme_path = pylons_sphinx_themes.get_html_themes_path() -- cgit v1.2.3 From 13846e641d9c6f7be65ac535c0a31fcf1f538267 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 31 Jan 2016 01:41:10 -0600 Subject: minor tweaks --- docs/tutorials/wiki2/installation.rst | 18 +++++++++--------- .../alchemy/+package+/scripts/initializedb.py | 7 +++---- pyramid/scaffolds/alchemy/+package+/tests.py_tmpl | 4 ++-- .../scaffolds/alchemy/+package+/views/default.py_tmpl | 4 ++-- pyramid/scaffolds/alchemy/MANIFEST.in_tmpl | 2 +- pyramid/scaffolds/alchemy/production.ini_tmpl | 2 -- 6 files changed, 17 insertions(+), 20 deletions(-) diff --git a/docs/tutorials/wiki2/installation.rst b/docs/tutorials/wiki2/installation.rst index 1385ab8c7..960eec861 100644 --- a/docs/tutorials/wiki2/installation.rst +++ b/docs/tutorials/wiki2/installation.rst @@ -94,11 +94,11 @@ Install SQLite3 and its development packages If you used a package manager to install your Python or if you compiled your Python from source, then you must install SQLite3 and its development packages. If you downloaded your Python as an installer -from python.org, then you already have it installed and can proceed to -the next section :ref:`sql_making_a_project`.. +from https://www.python.org, then you already have it installed and can +proceed to the next section :ref:`sql_making_a_project`. If you need to install the SQLite3 packages, then, for example, using -the Debian system and apt-get, the command would be the following: +the Debian system and ``apt-get``, the command would be the following: .. code-block:: text @@ -133,8 +133,8 @@ the :term:`scaffold` named ``alchemy`` which generates an application that uses :term:`SQLAlchemy` and :term:`URL dispatch`. :app:`Pyramid` supplies a variety of scaffolds to generate sample -projects. We will use `pcreate`—a script that comes with Pyramid to -quickly and easily generate scaffolds, usually with a single command—to +projects. We will use `pcreate` — a script that comes with Pyramid to +quickly and easily generate scaffolds, usually with a single command — to create the scaffold for our project. By passing `alchemy` into the `pcreate` command, the script creates @@ -225,7 +225,7 @@ For a successful test run, you should see output that ends like this:: . ---------------------------------------------------------------------- Ran 1 test in 0.094s - + OK Expose test coverage information @@ -383,8 +383,8 @@ This means the server is ready to accept requests. Visit the application in a browser ================================== -In a browser, visit `http://localhost:6543/ `_. You -will see the generated application's default page. +In a browser, visit http://localhost:6543/. You will see the generated +application's default page. One thing you'll notice is the "debug toolbar" icon on right hand side of the page. You can read more about the purpose of the icon at @@ -401,7 +401,7 @@ assumptions: - you are willing to use :term:`URL dispatch` to map URLs to code -- you want to use ``ZopeTransactionExtension`` and ``pyramid_tm`` to scope +- you want to use ``zope.sqlalchemy`` and ``pyramid_tm`` to scope sessions to requests .. note:: diff --git a/pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py b/pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py index f0d09729e..4f9711d93 100644 --- a/pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py +++ b/pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py @@ -15,7 +15,7 @@ from ..models.meta import ( get_engine, get_dbmaker, ) -from ..models.mymodel import MyModel +from ..models import MyModel def usage(argv): @@ -34,12 +34,11 @@ def main(argv=sys.argv): settings = get_appsettings(config_uri, options=options) engine = get_engine(settings) - dbmaker = get_dbmaker(engine) + Base.metadata.create_all(engine) + dbmaker = get_dbmaker(engine) dbsession = get_session(transaction.manager, dbmaker) - Base.metadata.create_all(engine) - with transaction.manager: model = MyModel(name='one', value=1) dbsession.add(model) diff --git a/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl b/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl index 074c7a773..963377b78 100644 --- a/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl +++ b/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl @@ -36,7 +36,7 @@ class BaseTest(unittest.TestCase): testing.tearDown() transaction.abort() - Base.metadata.create_all(self.engine) + Base.metadata.drop_all(self.engine) class TestMyViewSuccessCondition(BaseTest): @@ -45,7 +45,7 @@ class TestMyViewSuccessCondition(BaseTest): super(TestMyViewSuccessCondition, self).setUp() self.init_database() - from .models.mymodel import MyModel + from .models import MyModel model = MyModel(name='one', value=55) self.session.add(model) diff --git a/pyramid/scaffolds/alchemy/+package+/views/default.py_tmpl b/pyramid/scaffolds/alchemy/+package+/views/default.py_tmpl index 43fb33e05..7bf0026e5 100644 --- a/pyramid/scaffolds/alchemy/+package+/views/default.py_tmpl +++ b/pyramid/scaffolds/alchemy/+package+/views/default.py_tmpl @@ -3,7 +3,7 @@ from pyramid.view import view_config from sqlalchemy.exc import DBAPIError -from ..models.mymodel import MyModel +from ..models import MyModel @view_config(route_name='home', renderer='../templates/mytemplate.jinja2') @@ -12,7 +12,7 @@ def my_view(request): query = request.dbsession.query(MyModel) one = query.filter(MyModel.name == 'one').first() except DBAPIError: - return Response(db_err_msg, content_type='text/plain', status_int=500) + return Response(db_err_msg, content_type='text/plain', status=500) return {'one': one, 'project': '{{project}}'} diff --git a/pyramid/scaffolds/alchemy/MANIFEST.in_tmpl b/pyramid/scaffolds/alchemy/MANIFEST.in_tmpl index 0ff6eb7a0..f93f45544 100644 --- a/pyramid/scaffolds/alchemy/MANIFEST.in_tmpl +++ b/pyramid/scaffolds/alchemy/MANIFEST.in_tmpl @@ -1,2 +1,2 @@ include *.txt *.ini *.cfg *.rst -recursive-include {{package}} *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml +recursive-include {{package}} *.ico *.png *.css *.gif *.jpg *.jinja2 *.pt *.txt *.mak *.mako *.js *.html *.xml diff --git a/pyramid/scaffolds/alchemy/production.ini_tmpl b/pyramid/scaffolds/alchemy/production.ini_tmpl index 022bc0b7b..4d9f835d4 100644 --- a/pyramid/scaffolds/alchemy/production.ini_tmpl +++ b/pyramid/scaffolds/alchemy/production.ini_tmpl @@ -11,8 +11,6 @@ pyramid.debug_authorization = false pyramid.debug_notfound = false pyramid.debug_routematch = false pyramid.default_locale_name = en -pyramid.includes = - pyramid_tm sqlalchemy.url = sqlite:///%(here)s/{{project}}.sqlite -- cgit v1.2.3 From ab495cf5e745688640b0d1313ecf2de1e5ed2be8 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 31 Jan 2016 01:44:48 -0600 Subject: add a 404 error view --- pyramid/scaffolds/alchemy/+package+/templates/404.jinja2_tmpl | 8 ++++++++ pyramid/scaffolds/alchemy/+package+/views/errors.py_tmpl | 7 +++++++ 2 files changed, 15 insertions(+) create mode 100644 pyramid/scaffolds/alchemy/+package+/templates/404.jinja2_tmpl create mode 100644 pyramid/scaffolds/alchemy/+package+/views/errors.py_tmpl diff --git a/pyramid/scaffolds/alchemy/+package+/templates/404.jinja2_tmpl b/pyramid/scaffolds/alchemy/+package+/templates/404.jinja2_tmpl new file mode 100644 index 000000000..1917f83c7 --- /dev/null +++ b/pyramid/scaffolds/alchemy/+package+/templates/404.jinja2_tmpl @@ -0,0 +1,8 @@ +{% extends "layout.jinja2" %} + +{% block content %} +
+

Pyramid Alchemy scaffold

+

404 Page Not Found

+
+{% endblock content %} diff --git a/pyramid/scaffolds/alchemy/+package+/views/errors.py_tmpl b/pyramid/scaffolds/alchemy/+package+/views/errors.py_tmpl new file mode 100644 index 000000000..3a20f8487 --- /dev/null +++ b/pyramid/scaffolds/alchemy/+package+/views/errors.py_tmpl @@ -0,0 +1,7 @@ +from pyramid.view import ( + notfound_view_config, +) + +@notfound_view_config(renderer='../templates/404.jinja2') +def notfound_view(request): + return {} -- cgit v1.2.3 From 67f733e161f19bb2b7322edd120b9bf489154536 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 31 Jan 2016 01:46:34 -0600 Subject: stop using the meta package from the outside --- pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py | 2 +- pyramid/scaffolds/alchemy/+package+/tests.py_tmpl | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py b/pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py index 4f9711d93..0b2a42c59 100644 --- a/pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py +++ b/pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py @@ -9,7 +9,7 @@ from pyramid.paster import ( from pyramid.scripts.common import parse_vars -from ..models.meta import ( +from ..models import ( Base, get_session, get_engine, diff --git a/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl b/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl index 963377b78..01f2cb4cc 100644 --- a/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl +++ b/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl @@ -13,10 +13,10 @@ class BaseTest(unittest.TestCase): self.config = testing.setUp(settings={ 'sqlalchemy.url': 'sqlite:///:memory:' }) - self.config.include('.models.meta') + self.config.include('.models') settings = self.config.get_settings() - from .models.meta import ( + from .models import ( get_session, get_engine, get_dbmaker, @@ -28,7 +28,7 @@ class BaseTest(unittest.TestCase): self.session = get_session(transaction.manager, dbmaker) def init_database(self): - from .models.meta import Base + from .models import Base Base.metadata.create_all(self.engine) def tearDown(self): -- cgit v1.2.3 From 7045a9a29c06bba453381463d46752ffb261ee73 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Mon, 1 Feb 2016 01:18:10 -0800 Subject: minor grammar, "simpler traversal machinery" --- docs/designdefense.rst | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/docs/designdefense.rst b/docs/designdefense.rst index 2da10108a..19d4fc49f 100644 --- a/docs/designdefense.rst +++ b/docs/designdefense.rst @@ -941,7 +941,7 @@ the same identity. Here's why they have a separate identity. .. _simpler_traversal_model: -Pyramid has Simpler Traversal Machinery than Does Zope +Pyramid has simpler traversal machinery than does Zope ------------------------------------------------------ Zope's default traverser: @@ -951,26 +951,26 @@ Zope's default traverser: - Attempts to use an adaptation to obtain the next element in the path from the currently traversed object, falling back to ``__bobo_traverse__``, - ``__getitem__`` and eventually ``__getattr__``. + ``__getitem__``, and eventually ``__getattr__``. Zope's default traverser allows developers to mutate the traversal name stack -during traversal by mutating ``REQUEST['TraversalNameStack']``. Pyramid's -default traverser (``pyramid.traversal.ResourceTreeTraverser``) does not -offer a way to do this; it does not maintain a stack as a request attribute -and, even if it did, it does not pass the request to resource objects while -it's traversing. While it was handy at times, this feature was abused in -frameworks built atop Zope (like CMF and Plone), often making it difficult to -tell exactly what was happening when a traversal didn't match a view. I felt -it was better to make folks that wanted the feature replace the traverser -rather than build that particular honey pot in to the default traverser. +during traversal by mutating ``REQUEST['TraversalNameStack']``. Pyramid's +default traverser (``pyramid.traversal.ResourceTreeTraverser``) does not offer +a way to do this. It does not maintain a stack as a request attribute and, even +if it did, it does not pass the request to resource objects while it's +traversing. While it was handy at times, this feature was abused in frameworks +built atop Zope (like CMF and Plone), often making it difficult to tell exactly +what was happening when a traversal didn't match a view. I felt it was better +for folks that wanted the feature to make them replace the traverser rather +than build that particular honey pot in to the default traverser. Zope uses multiple mechanisms to attempt to obtain the next element in the resource tree based on a name. It first tries an adaptation of the current -resource to ``ITraversable``, and if that fails, it falls back to attempting +resource to ``ITraversable``, and if that fails, it falls back to attempting a number of magic methods on the resource (``__bobo_traverse__``, -``__getitem__``, and ``__getattr__``). My experience while both using Zope -and attempting to reimplement its publisher in ``repoze.zope2`` led me to -believe the following: +``__getitem__``, and ``__getattr__``). My experience while both using Zope and +attempting to reimplement its publisher in ``repoze.zope2`` led me to believe +the following: - The *default* traverser should be as simple as possible. Zope's publisher is somewhat difficult to follow and replicate due to the fallbacks it tried @@ -991,7 +991,7 @@ believe the following: default implementation of the larger component, no one understands when (or whether) they should ever override the larger component entrirely. This results, over time, in a rusting together of the larger "replaceable" - component and the framework itself, because people come to depend on the + component and the framework itself because people come to depend on the availability of the default component in order just to turn its knobs. The default component effectively becomes part of the framework, which entirely subverts the goal of making it replaceable. In Pyramid, typically if a @@ -1000,6 +1000,7 @@ believe the following: you will replace the component instead of turning knobs attached to the component. + .. _microframeworks_smaller_hello_world: Microframeworks Have Smaller Hello World Programs -- cgit v1.2.3 From 5ec93dc8c06b7838a9ed8a9f40cfa333b3ba1da2 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Tue, 2 Feb 2016 12:22:56 -0700 Subject: Deprecate --log-file As we remove the daemonisation code, we want to also deprecate features that only make sense when you are running as a deamon. Logging to a file currently does not allow log rotation for example, and really logging should be done external to pserve --- pyramid/scripts/pserve.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyramid/scripts/pserve.py b/pyramid/scripts/pserve.py index 3ea614eb5..431afe6f4 100644 --- a/pyramid/scripts/pserve.py +++ b/pyramid/scripts/pserve.py @@ -114,7 +114,7 @@ class PServeCommand(object): '--log-file', dest='log_file', metavar='LOG_FILE', - help="Save output to the given log file (redirects stdout)") + help="Save output to the given log file (redirects stdout) [DEPRECATED]") parser.add_option( '--reload', dest='reload', @@ -287,7 +287,7 @@ class PServeCommand(object): base = os.getcwd() # warn before setting a default - if self.options.pid_file: + if self.options.pid_file or self.options.log_file: self._warn_daemon_deprecated() if getattr(self.options, 'daemon', False): @@ -675,7 +675,7 @@ in a future release per Pyramid's deprecation policy. Please consider using a real process manager for your processes like Systemd, Circus, or Supervisor. The following commands are deprecated: - [start,stop,restart,status] --daemon, --stop-server, --status, --pid-file + [start,stop,restart,status] --daemon, --stop-server, --status, --pid-file, --log-file ''') class LazyWriter(object): -- cgit v1.2.3 From daa0e4deaca9d087e0feef46083e42974d6e1115 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Wed, 3 Feb 2016 02:15:09 -0800 Subject: each step is for every release, unless explicitly stated as "major release only" --- RELEASING.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/RELEASING.txt b/RELEASING.txt index 142005ed7..75a4fcea2 100644 --- a/RELEASING.txt +++ b/RELEASING.txt @@ -56,14 +56,14 @@ Releasing Pyramid $ python setup.py sdist bdist_wheel $ twine upload dist/pyramid-X.X-* -- Edit Pylons/pylonshq/templates/home/home.mako for minor and major updates. +- Edit Pylons/pylonshq/templates/home/home.mako. -- Edit Pylons/pylonshq/templates/home/inside.rst for major updates only. +- Edit Pylons/pylonshq/templates/home/inside.rst for major releases only. -- Edit Pylons/trypyramid.com/src/templates/resources.html for major updates +- Edit Pylons/trypyramid.com/src/templates/resources.html for major releases only. -- Edit Pylons/pylonsrtd/pylonsrtd/docs/pyramid.rst for all updates. +- Edit Pylons/pylonsrtd/pylonsrtd/docs/pyramid.rst for major releases only. - Edit `http://wiki.python.org/moin/WebFrameworks `_. -- cgit v1.2.3 From a6d08e1ad8c8bbcc7e14eabf06e26507e636d538 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Wed, 3 Feb 2016 23:46:40 -0600 Subject: improve the models api/usage and add a lot of comments --- pyramid/scaffolds/alchemy/+package+/__init__.py | 2 +- .../scaffolds/alchemy/+package+/models/__init__.py | 71 +++++++++++++++++++++- pyramid/scaffolds/alchemy/+package+/models/meta.py | 33 ---------- .../scaffolds/alchemy/+package+/models/mymodel.py | 3 +- .../alchemy/+package+/scripts/initializedb.py | 9 +-- pyramid/scaffolds/alchemy/+package+/tests.py_tmpl | 8 +-- 6 files changed, 80 insertions(+), 46 deletions(-) diff --git a/pyramid/scaffolds/alchemy/+package+/__init__.py b/pyramid/scaffolds/alchemy/+package+/__init__.py index 7994bbfa8..17763812a 100644 --- a/pyramid/scaffolds/alchemy/+package+/__init__.py +++ b/pyramid/scaffolds/alchemy/+package+/__init__.py @@ -6,7 +6,7 @@ def main(global_config, **settings): """ config = Configurator(settings=settings) config.include('pyramid_jinja2') - config.include('.models.meta') + config.include('.models') config.add_static_view('static', 'static', cache_max_age=3600) config.add_route('home', '/') config.scan() diff --git a/pyramid/scaffolds/alchemy/+package+/models/__init__.py b/pyramid/scaffolds/alchemy/+package+/models/__init__.py index 6ffc10a78..e55689f2c 100644 --- a/pyramid/scaffolds/alchemy/+package+/models/__init__.py +++ b/pyramid/scaffolds/alchemy/+package+/models/__init__.py @@ -1,7 +1,72 @@ +from sqlalchemy import engine_from_config +from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import configure_mappers -# import all models classes here for sqlalchemy mappers -# to pick up +import zope.sqlalchemy + +# import or define all models here to ensure they are attached to the +# Base.metadata prior to any initialization routines from .mymodel import MyModel # flake8: noqa -# run configure mappers to ensure we avoid any race conditions +# run configure_mappers after defining all of the models to ensure +# all relationships can be setup configure_mappers() + + +def get_engine(settings, prefix='sqlalchemy.'): + return engine_from_config(settings, prefix) + + +def get_sessionmaker(engine): + dbmaker = sessionmaker() + dbmaker.configure(bind=engine) + return dbmaker + + +def get_tm_session(dbmaker, transaction_manager): + """ + Get a ``sqlalchemy.orm.Session`` instance backed by a transaction. + + This function will hook the session to the transaction manager which + will take care of committing any changes. + + - When using pyramid_tm it will automatically be committed or aborted + depending on whether an exception is raised. + + - When using scripts you should wrap the session in a manager yourself. + For example:: + + import transaction + + engine = get_engine(settings) + dbmaker = get_sessionmaker(engine) + with transaction.manager: + dbsession = get_tm_session(dbmaker, transaction.manager) + + """ + dbsession = dbmaker() + zope.sqlalchemy.register( + dbsession, transaction_manager=transaction_manager) + return dbsession + + +def includeme(config): + """ + Initialize the model for a Pyramid app. + + Activate this setup using ``config.include('{{package}}.models')``. + + """ + settings = config.get_settings() + + # use pyramid_tm to hook the transaction lifecycle to the request + config.include('pyramid_tm') + + dbmaker = get_sessionmaker(get_engine(settings)) + + # make request.dbsession available for use in Pyramid + config.add_request_method( + # r.tm is the transaction manager used by pyramid_tm + lambda r: get_tm_session(dbmaker, r.tm), + 'dbsession', + reify=True + ) diff --git a/pyramid/scaffolds/alchemy/+package+/models/meta.py b/pyramid/scaffolds/alchemy/+package+/models/meta.py index 80ececd8c..fc3e8f1dd 100644 --- a/pyramid/scaffolds/alchemy/+package+/models/meta.py +++ b/pyramid/scaffolds/alchemy/+package+/models/meta.py @@ -1,8 +1,5 @@ -from sqlalchemy import engine_from_config from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import sessionmaker from sqlalchemy.schema import MetaData -import zope.sqlalchemy # Recommended naming convention used by Alembic, as various different database # providers will autogenerate vastly different names making migrations more @@ -17,33 +14,3 @@ NAMING_CONVENTION = { metadata = MetaData(naming_convention=NAMING_CONVENTION) Base = declarative_base(metadata=metadata) - - -def includeme(config): - settings = config.get_settings() - dbmaker = get_dbmaker(get_engine(settings)) - - config.add_request_method( - lambda r: get_session(r.tm, dbmaker), - 'dbsession', - reify=True - ) - - config.include('pyramid_tm') - - -def get_session(transaction_manager, dbmaker): - dbsession = dbmaker() - zope.sqlalchemy.register(dbsession, - transaction_manager=transaction_manager) - return dbsession - - -def get_engine(settings, prefix='sqlalchemy.'): - return engine_from_config(settings, prefix) - - -def get_dbmaker(engine): - dbmaker = sessionmaker() - dbmaker.configure(bind=engine) - return dbmaker diff --git a/pyramid/scaffolds/alchemy/+package+/models/mymodel.py b/pyramid/scaffolds/alchemy/+package+/models/mymodel.py index 5a2b5890c..d65a01a42 100644 --- a/pyramid/scaffolds/alchemy/+package+/models/mymodel.py +++ b/pyramid/scaffolds/alchemy/+package+/models/mymodel.py @@ -1,4 +1,3 @@ -from .meta import Base from sqlalchemy import ( Column, Index, @@ -6,6 +5,8 @@ from sqlalchemy import ( Text, ) +from .meta import Base + class MyModel(Base): __tablename__ = 'models' diff --git a/pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py b/pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py index 0b2a42c59..13d4e543e 100644 --- a/pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py +++ b/pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py @@ -11,9 +11,9 @@ from pyramid.scripts.common import parse_vars from ..models import ( Base, - get_session, get_engine, - get_dbmaker, + get_sessionmaker, + get_tm_session, ) from ..models import MyModel @@ -36,9 +36,10 @@ def main(argv=sys.argv): engine = get_engine(settings) Base.metadata.create_all(engine) - dbmaker = get_dbmaker(engine) - dbsession = get_session(transaction.manager, dbmaker) + dbmaker = get_sessionmaker(engine) with transaction.manager: + dbsession = get_tm_session(dbmaker, transaction.manager) + model = MyModel(name='one', value=1) dbsession.add(model) diff --git a/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl b/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl index 01f2cb4cc..4eecaf33c 100644 --- a/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl +++ b/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl @@ -17,15 +17,15 @@ class BaseTest(unittest.TestCase): settings = self.config.get_settings() from .models import ( - get_session, get_engine, - get_dbmaker, + get_sessionmaker, + get_tm_session, ) self.engine = get_engine(settings) - dbmaker = get_dbmaker(self.engine) + dbmaker = get_sessionmaker(self.engine) - self.session = get_session(transaction.manager, dbmaker) + self.session = get_tm_session(dbmaker, transaction.manager) def init_database(self): from .models import Base -- cgit v1.2.3 From f4e1ca8bb88f7d899f2b8c39049089a9c0e723f2 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 4 Feb 2016 21:15:49 -0600 Subject: rename dbmaker to session_factory --- .../scaffolds/alchemy/+package+/models/__init__.py | 20 ++++++++++---------- .../alchemy/+package+/scripts/initializedb.py | 6 +++--- pyramid/scaffolds/alchemy/+package+/tests.py_tmpl | 6 +++--- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/pyramid/scaffolds/alchemy/+package+/models/__init__.py b/pyramid/scaffolds/alchemy/+package+/models/__init__.py index e55689f2c..c1ceacbf4 100644 --- a/pyramid/scaffolds/alchemy/+package+/models/__init__.py +++ b/pyramid/scaffolds/alchemy/+package+/models/__init__.py @@ -16,13 +16,13 @@ def get_engine(settings, prefix='sqlalchemy.'): return engine_from_config(settings, prefix) -def get_sessionmaker(engine): - dbmaker = sessionmaker() - dbmaker.configure(bind=engine) - return dbmaker +def get_session_factory(engine): + factory = sessionmaker() + factory.configure(bind=engine) + return factory -def get_tm_session(dbmaker, transaction_manager): +def get_tm_session(session_factory, transaction_manager): """ Get a ``sqlalchemy.orm.Session`` instance backed by a transaction. @@ -38,12 +38,12 @@ def get_tm_session(dbmaker, transaction_manager): import transaction engine = get_engine(settings) - dbmaker = get_sessionmaker(engine) + session_factory = get_session_factory(engine) with transaction.manager: - dbsession = get_tm_session(dbmaker, transaction.manager) + dbsession = get_tm_session(session_factory, transaction.manager) """ - dbsession = dbmaker() + dbsession = session_factory() zope.sqlalchemy.register( dbsession, transaction_manager=transaction_manager) return dbsession @@ -61,12 +61,12 @@ def includeme(config): # use pyramid_tm to hook the transaction lifecycle to the request config.include('pyramid_tm') - dbmaker = get_sessionmaker(get_engine(settings)) + session_factory = get_session_factory(get_engine(settings)) # make request.dbsession available for use in Pyramid config.add_request_method( # r.tm is the transaction manager used by pyramid_tm - lambda r: get_tm_session(dbmaker, r.tm), + lambda r: get_tm_session(session_factory, r.tm), 'dbsession', reify=True ) diff --git a/pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py b/pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py index 13d4e543e..da63c180a 100644 --- a/pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py +++ b/pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py @@ -12,7 +12,7 @@ from pyramid.scripts.common import parse_vars from ..models import ( Base, get_engine, - get_sessionmaker, + get_session_factory, get_tm_session, ) from ..models import MyModel @@ -36,10 +36,10 @@ def main(argv=sys.argv): engine = get_engine(settings) Base.metadata.create_all(engine) - dbmaker = get_sessionmaker(engine) + session_factory = get_session_factory(engine) with transaction.manager: - dbsession = get_tm_session(dbmaker, transaction.manager) + dbsession = get_tm_session(session_factory, transaction.manager) model = MyModel(name='one', value=1) dbsession.add(model) diff --git a/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl b/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl index 4eecaf33c..be42cb5d9 100644 --- a/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl +++ b/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl @@ -18,14 +18,14 @@ class BaseTest(unittest.TestCase): from .models import ( get_engine, - get_sessionmaker, + get_session_factory, get_tm_session, ) self.engine = get_engine(settings) - dbmaker = get_sessionmaker(self.engine) + session_factory = get_session_factory(self.engine) - self.session = get_tm_session(dbmaker, transaction.manager) + self.session = get_tm_session(session_factory, transaction.manager) def init_database(self): from .models import Base -- cgit v1.2.3 From 0e21c2abcf1174e73b139e7e78f599643d7f84e4 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 4 Feb 2016 21:54:09 -0600 Subject: make models/__init__.py a template --- .../scaffolds/alchemy/+package+/models/__init__.py | 72 ---------------------- .../alchemy/+package+/models/__init__.py_tmpl | 72 ++++++++++++++++++++++ .../alchemy/+package+/views/errors.py_tmpl | 4 +- 3 files changed, 73 insertions(+), 75 deletions(-) delete mode 100644 pyramid/scaffolds/alchemy/+package+/models/__init__.py create mode 100644 pyramid/scaffolds/alchemy/+package+/models/__init__.py_tmpl diff --git a/pyramid/scaffolds/alchemy/+package+/models/__init__.py b/pyramid/scaffolds/alchemy/+package+/models/__init__.py deleted file mode 100644 index c1ceacbf4..000000000 --- a/pyramid/scaffolds/alchemy/+package+/models/__init__.py +++ /dev/null @@ -1,72 +0,0 @@ -from sqlalchemy import engine_from_config -from sqlalchemy.orm import sessionmaker -from sqlalchemy.orm import configure_mappers -import zope.sqlalchemy - -# import or define all models here to ensure they are attached to the -# Base.metadata prior to any initialization routines -from .mymodel import MyModel # flake8: noqa - -# run configure_mappers after defining all of the models to ensure -# all relationships can be setup -configure_mappers() - - -def get_engine(settings, prefix='sqlalchemy.'): - return engine_from_config(settings, prefix) - - -def get_session_factory(engine): - factory = sessionmaker() - factory.configure(bind=engine) - return factory - - -def get_tm_session(session_factory, transaction_manager): - """ - Get a ``sqlalchemy.orm.Session`` instance backed by a transaction. - - This function will hook the session to the transaction manager which - will take care of committing any changes. - - - When using pyramid_tm it will automatically be committed or aborted - depending on whether an exception is raised. - - - When using scripts you should wrap the session in a manager yourself. - For example:: - - import transaction - - engine = get_engine(settings) - session_factory = get_session_factory(engine) - with transaction.manager: - dbsession = get_tm_session(session_factory, transaction.manager) - - """ - dbsession = session_factory() - zope.sqlalchemy.register( - dbsession, transaction_manager=transaction_manager) - return dbsession - - -def includeme(config): - """ - Initialize the model for a Pyramid app. - - Activate this setup using ``config.include('{{package}}.models')``. - - """ - settings = config.get_settings() - - # use pyramid_tm to hook the transaction lifecycle to the request - config.include('pyramid_tm') - - session_factory = get_session_factory(get_engine(settings)) - - # make request.dbsession available for use in Pyramid - config.add_request_method( - # r.tm is the transaction manager used by pyramid_tm - lambda r: get_tm_session(session_factory, r.tm), - 'dbsession', - reify=True - ) diff --git a/pyramid/scaffolds/alchemy/+package+/models/__init__.py_tmpl b/pyramid/scaffolds/alchemy/+package+/models/__init__.py_tmpl new file mode 100644 index 000000000..7d0c94a14 --- /dev/null +++ b/pyramid/scaffolds/alchemy/+package+/models/__init__.py_tmpl @@ -0,0 +1,72 @@ +from sqlalchemy import engine_from_config +from sqlalchemy.orm import sessionmaker +from sqlalchemy.orm import configure_mappers +import zope.sqlalchemy + +# import or define all models here to ensure they are attached to the +# Base.metadata prior to any initialization routines +from .mymodel import MyModel # flake8: noqa + +# run configure_mappers after defining all of the models to ensure +# all relationships can be setup +configure_mappers() + + +def get_engine(settings, prefix='sqlalchemy.'): + return engine_from_config(settings, prefix) + + +def get_session_factory(engine): + factory = sessionmaker() + factory.configure(bind=engine) + return factory + + +def get_tm_session(session_factory, transaction_manager): + """ + Get a ``sqlalchemy.orm.Session`` instance backed by a transaction. + + This function will hook the session to the transaction manager which + will take care of committing any changes. + + - When using pyramid_tm it will automatically be committed or aborted + depending on whether an exception is raised. + + - When using scripts you should wrap the session in a manager yourself. + For example:: + + import transaction + + engine = get_engine(settings) + session_factory = get_session_factory(engine) + with transaction.manager: + dbsession = get_tm_session(session_factory, transaction.manager) + + """ + dbsession = session_factory() + zope.sqlalchemy.register( + dbsession, transaction_manager=transaction_manager) + return dbsession + + +def includeme(config): + """ + Initialize the model for a Pyramid app. + + Activate this setup using ``config.include('{{project}}.models')``. + + """ + settings = config.get_settings() + + # use pyramid_tm to hook the transaction lifecycle to the request + config.include('pyramid_tm') + + session_factory = get_session_factory(get_engine(settings)) + + # make request.dbsession available for use in Pyramid + config.add_request_method( + # r.tm is the transaction manager used by pyramid_tm + lambda r: get_tm_session(session_factory, r.tm), + 'dbsession', + reify=True + ) diff --git a/pyramid/scaffolds/alchemy/+package+/views/errors.py_tmpl b/pyramid/scaffolds/alchemy/+package+/views/errors.py_tmpl index 3a20f8487..a4b8201f1 100644 --- a/pyramid/scaffolds/alchemy/+package+/views/errors.py_tmpl +++ b/pyramid/scaffolds/alchemy/+package+/views/errors.py_tmpl @@ -1,6 +1,4 @@ -from pyramid.view import ( - notfound_view_config, -) +from pyramid.view import notfound_view_config @notfound_view_config(renderer='../templates/404.jinja2') def notfound_view(request): -- cgit v1.2.3 From 35dc507d9394a7dc0834838a5a596f4e47ab95fb Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 4 Feb 2016 23:38:43 -0600 Subject: update source for basiclayout --- docs/tutorials/wiki2/src/basiclayout/MANIFEST.in | 2 +- .../tutorials/wiki2/src/basiclayout/production.ini | 2 - .../wiki2/src/basiclayout/tutorial/__init__.py | 2 +- .../src/basiclayout/tutorial/models/__init__.py | 71 +++++++++++++++++++++- .../wiki2/src/basiclayout/tutorial/models/meta.py | 33 ---------- .../src/basiclayout/tutorial/models/mymodel.py | 3 +- .../basiclayout/tutorial/scripts/initializedb.py | 16 ++--- .../src/basiclayout/tutorial/templates/404.jinja2 | 8 +++ .../wiki2/src/basiclayout/tutorial/tests.py | 18 +++--- .../src/basiclayout/tutorial/views/default.py | 4 +- .../wiki2/src/basiclayout/tutorial/views/errors.py | 5 ++ 11 files changed, 104 insertions(+), 60 deletions(-) create mode 100644 docs/tutorials/wiki2/src/basiclayout/tutorial/templates/404.jinja2 create mode 100644 docs/tutorials/wiki2/src/basiclayout/tutorial/views/errors.py diff --git a/docs/tutorials/wiki2/src/basiclayout/MANIFEST.in b/docs/tutorials/wiki2/src/basiclayout/MANIFEST.in index 81beba1b1..42cd299b5 100644 --- a/docs/tutorials/wiki2/src/basiclayout/MANIFEST.in +++ b/docs/tutorials/wiki2/src/basiclayout/MANIFEST.in @@ -1,2 +1,2 @@ include *.txt *.ini *.cfg *.rst -recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml +recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.jinja2 *.pt *.txt *.mak *.mako *.js *.html *.xml diff --git a/docs/tutorials/wiki2/src/basiclayout/production.ini b/docs/tutorials/wiki2/src/basiclayout/production.ini index 97acfbd7d..cb1db3211 100644 --- a/docs/tutorials/wiki2/src/basiclayout/production.ini +++ b/docs/tutorials/wiki2/src/basiclayout/production.ini @@ -11,8 +11,6 @@ pyramid.debug_authorization = false pyramid.debug_notfound = false pyramid.debug_routematch = false pyramid.default_locale_name = en -pyramid.includes = - pyramid_tm sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py index 7994bbfa8..17763812a 100644 --- a/docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py @@ -6,7 +6,7 @@ def main(global_config, **settings): """ config = Configurator(settings=settings) config.include('pyramid_jinja2') - config.include('.models.meta') + config.include('.models') config.add_static_view('static', 'static', cache_max_age=3600) config.add_route('home', '/') config.scan() diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/__init__.py index 6ffc10a78..a4026fcd6 100644 --- a/docs/tutorials/wiki2/src/basiclayout/tutorial/models/__init__.py +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/__init__.py @@ -1,7 +1,72 @@ +from sqlalchemy import engine_from_config +from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import configure_mappers -# import all models classes here for sqlalchemy mappers -# to pick up +import zope.sqlalchemy + +# import or define all models here to ensure they are attached to the +# Base.metadata prior to any initialization routines from .mymodel import MyModel # flake8: noqa -# run configure mappers to ensure we avoid any race conditions +# run configure_mappers after defining all of the models to ensure +# all relationships can be setup configure_mappers() + + +def get_engine(settings, prefix='sqlalchemy.'): + return engine_from_config(settings, prefix) + + +def get_session_factory(engine): + factory = sessionmaker() + factory.configure(bind=engine) + return factory + + +def get_tm_session(session_factory, transaction_manager): + """ + Get a ``sqlalchemy.orm.Session`` instance backed by a transaction. + + This function will hook the session to the transaction manager which + will take care of committing any changes. + + - When using pyramid_tm it will automatically be committed or aborted + depending on whether an exception is raised. + + - When using scripts you should wrap the session in a manager yourself. + For example:: + + import transaction + + engine = get_engine(settings) + session_factory = get_session_factory(engine) + with transaction.manager: + dbsession = get_tm_session(session_factory, transaction.manager) + + """ + dbsession = session_factory() + zope.sqlalchemy.register( + dbsession, transaction_manager=transaction_manager) + return dbsession + + +def includeme(config): + """ + Initialize the model for a Pyramid app. + + Activate this setup using ``config.include('tutorial.models')``. + + """ + settings = config.get_settings() + + # use pyramid_tm to hook the transaction lifecycle to the request + config.include('pyramid_tm') + + session_factory = get_session_factory(get_engine(settings)) + + # make request.dbsession available for use in Pyramid + config.add_request_method( + # r.tm is the transaction manager used by pyramid_tm + lambda r: get_tm_session(session_factory, r.tm), + 'dbsession', + reify=True + ) diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/models/meta.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/meta.py index 80ececd8c..fc3e8f1dd 100644 --- a/docs/tutorials/wiki2/src/basiclayout/tutorial/models/meta.py +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/meta.py @@ -1,8 +1,5 @@ -from sqlalchemy import engine_from_config from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import sessionmaker from sqlalchemy.schema import MetaData -import zope.sqlalchemy # Recommended naming convention used by Alembic, as various different database # providers will autogenerate vastly different names making migrations more @@ -17,33 +14,3 @@ NAMING_CONVENTION = { metadata = MetaData(naming_convention=NAMING_CONVENTION) Base = declarative_base(metadata=metadata) - - -def includeme(config): - settings = config.get_settings() - dbmaker = get_dbmaker(get_engine(settings)) - - config.add_request_method( - lambda r: get_session(r.tm, dbmaker), - 'dbsession', - reify=True - ) - - config.include('pyramid_tm') - - -def get_session(transaction_manager, dbmaker): - dbsession = dbmaker() - zope.sqlalchemy.register(dbsession, - transaction_manager=transaction_manager) - return dbsession - - -def get_engine(settings, prefix='sqlalchemy.'): - return engine_from_config(settings, prefix) - - -def get_dbmaker(engine): - dbmaker = sessionmaker() - dbmaker.configure(bind=engine) - return dbmaker diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/models/mymodel.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/mymodel.py index 5a2b5890c..d65a01a42 100644 --- a/docs/tutorials/wiki2/src/basiclayout/tutorial/models/mymodel.py +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/mymodel.py @@ -1,4 +1,3 @@ -from .meta import Base from sqlalchemy import ( Column, Index, @@ -6,6 +5,8 @@ from sqlalchemy import ( Text, ) +from .meta import Base + class MyModel(Base): __tablename__ = 'models' diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/initializedb.py index f0d09729e..da63c180a 100644 --- a/docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/initializedb.py +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/initializedb.py @@ -9,13 +9,13 @@ from pyramid.paster import ( from pyramid.scripts.common import parse_vars -from ..models.meta import ( +from ..models import ( Base, - get_session, get_engine, - get_dbmaker, + get_session_factory, + get_tm_session, ) -from ..models.mymodel import MyModel +from ..models import MyModel def usage(argv): @@ -34,12 +34,12 @@ def main(argv=sys.argv): settings = get_appsettings(config_uri, options=options) engine = get_engine(settings) - dbmaker = get_dbmaker(engine) - - dbsession = get_session(transaction.manager, dbmaker) - Base.metadata.create_all(engine) + session_factory = get_session_factory(engine) + with transaction.manager: + dbsession = get_tm_session(session_factory, transaction.manager) + model = MyModel(name='one', value=1) dbsession.add(model) diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/404.jinja2 b/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/404.jinja2 new file mode 100644 index 000000000..1917f83c7 --- /dev/null +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/404.jinja2 @@ -0,0 +1,8 @@ +{% extends "layout.jinja2" %} + +{% block content %} +
+

Pyramid Alchemy scaffold

+

404 Page Not Found

+
+{% endblock content %} diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/tests.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/tests.py index b947e3bb1..c54945c28 100644 --- a/docs/tutorials/wiki2/src/basiclayout/tutorial/tests.py +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/tests.py @@ -13,22 +13,22 @@ class BaseTest(unittest.TestCase): self.config = testing.setUp(settings={ 'sqlalchemy.url': 'sqlite:///:memory:' }) - self.config.include('.models.meta') + self.config.include('.models') settings = self.config.get_settings() - from .models.meta import ( - get_session, + from .models import ( get_engine, - get_dbmaker, + get_session_factory, + get_tm_session, ) self.engine = get_engine(settings) - dbmaker = get_dbmaker(self.engine) + session_factory = get_session_factory(self.engine) - self.session = get_session(transaction.manager, dbmaker) + self.session = get_tm_session(session_factory, transaction.manager) def init_database(self): - from .models.meta import Base + from .models import Base Base.metadata.create_all(self.engine) def tearDown(self): @@ -36,7 +36,7 @@ class BaseTest(unittest.TestCase): testing.tearDown() transaction.abort() - Base.metadata.create_all(self.engine) + Base.metadata.drop_all(self.engine) class TestMyViewSuccessCondition(BaseTest): @@ -45,7 +45,7 @@ class TestMyViewSuccessCondition(BaseTest): super(TestMyViewSuccessCondition, self).setUp() self.init_database() - from .models.mymodel import MyModel + from .models import MyModel model = MyModel(name='one', value=55) self.session.add(model) diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/views/default.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/views/default.py index 13ad8793c..ad0c728d7 100644 --- a/docs/tutorials/wiki2/src/basiclayout/tutorial/views/default.py +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/views/default.py @@ -3,7 +3,7 @@ from pyramid.view import view_config from sqlalchemy.exc import DBAPIError -from ..models.mymodel import MyModel +from ..models import MyModel @view_config(route_name='home', renderer='../templates/mytemplate.jinja2') @@ -12,7 +12,7 @@ def my_view(request): query = request.dbsession.query(MyModel) one = query.filter(MyModel.name == 'one').first() except DBAPIError: - return Response(db_err_msg, content_type='text/plain', status_int=500) + return Response(db_err_msg, content_type='text/plain', status=500) return {'one': one, 'project': 'tutorial'} diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/views/errors.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/views/errors.py new file mode 100644 index 000000000..a4b8201f1 --- /dev/null +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/views/errors.py @@ -0,0 +1,5 @@ +from pyramid.view import notfound_view_config + +@notfound_view_config(renderer='../templates/404.jinja2') +def notfound_view(request): + return {} -- cgit v1.2.3 From 9b83981f85c1d822ecf5a9aa2a4fde1851c0d0cd Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 4 Feb 2016 23:50:00 -0600 Subject: fix the Base import --- docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/initializedb.py | 2 +- pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/initializedb.py index da63c180a..7307ecc5c 100644 --- a/docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/initializedb.py +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/initializedb.py @@ -9,8 +9,8 @@ from pyramid.paster import ( from pyramid.scripts.common import parse_vars +from ..models.meta import Base from ..models import ( - Base, get_engine, get_session_factory, get_tm_session, diff --git a/pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py b/pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py index da63c180a..7307ecc5c 100644 --- a/pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py +++ b/pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py @@ -9,8 +9,8 @@ from pyramid.paster import ( from pyramid.scripts.common import parse_vars +from ..models.meta import Base from ..models import ( - Base, get_engine, get_session_factory, get_tm_session, -- cgit v1.2.3 From 8ec0b01ef0de3d7859e081652ef752d9af612fc5 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 5 Feb 2016 00:04:23 -0600 Subject: unindent literalincludes --- docs/tutorials/wiki2/basiclayout.rst | 166 +++++++++++++++++------------------ 1 file changed, 83 insertions(+), 83 deletions(-) diff --git a/docs/tutorials/wiki2/basiclayout.rst b/docs/tutorials/wiki2/basiclayout.rst index e3d0a0a3c..d55ce807f 100644 --- a/docs/tutorials/wiki2/basiclayout.rst +++ b/docs/tutorials/wiki2/basiclayout.rst @@ -19,25 +19,25 @@ code. Open ``tutorial/tutorial/__init__.py``. It should already contain the following: - .. literalinclude:: src/basiclayout/tutorial/__init__.py - :linenos: - :language: py +.. literalinclude:: src/basiclayout/tutorial/__init__.py + :linenos: + :language: py Let's go over this piece-by-piece. First, we need some imports to support later code: - .. literalinclude:: src/basiclayout/tutorial/__init__.py - :end-before: main - :linenos: - :language: py +.. literalinclude:: src/basiclayout/tutorial/__init__.py + :end-before: main + :linenos: + :language: py ``__init__.py`` defines a function named ``main``. Here is the entirety of the ``main`` function we've defined in our ``__init__.py``: - .. literalinclude:: src/basiclayout/tutorial/__init__.py - :pyobject: main - :lineno-start: 4 - :linenos: +.. literalinclude:: src/basiclayout/tutorial/__init__.py + :pyobject: main + :lineno-start: 4 + :linenos: :language: py When you invoke the ``pserve development.ini`` command, the ``main`` function @@ -46,10 +46,10 @@ application. (See :ref:`startup_chapter` for more about ``pserve``.) Next in ``main``, construct a :term:`Configurator` object: - .. literalinclude:: src/basiclayout/tutorial/__init__.py - :lines: 7 - :lineno-start: 7 - :language: py +.. literalinclude:: src/basiclayout/tutorial/__init__.py + :lines: 7 + :lineno-start: 7 + :language: py ``settings`` is passed to the Configurator as a keyword argument with the dictionary values passed as the ``**settings`` argument. This will be a @@ -60,26 +60,26 @@ deployment-related values such as ``pyramid.reload_templates``, Next include :term:`Jinja2` templating bindings so that we can use renderers with the ``.jinja2`` extension within our project. - .. literalinclude:: src/basiclayout/tutorial/__init__.py - :lines: 8 - :lineno-start: 8 - :language: py +.. literalinclude:: src/basiclayout/tutorial/__init__.py + :lines: 8 + :lineno-start: 8 + :language: py Next include the module ``meta`` from the package ``models`` using a dotted Python path. - .. literalinclude:: src/basiclayout/tutorial/__init__.py - :lines: 9 - :lineno-start: 9 - :language: py +.. literalinclude:: src/basiclayout/tutorial/__init__.py + :lines: 9 + :lineno-start: 9 + :language: py ``main`` now calls :meth:`pyramid.config.Configurator.add_static_view` with two arguments: ``static`` (the name), and ``static`` (the path): - .. literalinclude:: src/basiclayout/tutorial/__init__.py - :lines: 10 - :lineno-start: 10 - :language: py +.. literalinclude:: src/basiclayout/tutorial/__init__.py + :lines: 10 + :lineno-start: 10 + :language: py This registers a static resource view which will match any URL that starts with the prefix ``/static`` (by virtue of the first argument to @@ -95,10 +95,10 @@ Using the configurator ``main`` also registers a :term:`route configuration` via the :meth:`pyramid.config.Configurator.add_route` method that will be used when the URL is ``/``: - .. literalinclude:: src/basiclayout/tutorial/__init__.py - :lines: 11 - :lineno-start: 11 - :language: py +.. literalinclude:: src/basiclayout/tutorial/__init__.py + :lines: 11 + :lineno-start: 11 + :language: py Since this route has a ``pattern`` equaling ``/``, it is the route that will be matched when the URL ``/`` is visited, e.g., ``http://localhost:6543/``. @@ -110,19 +110,19 @@ other special) decorators. When it finds a ``@view_config`` decorator, a view configuration will be registered, which will allow one of our application URLs to be mapped to some code. - .. literalinclude:: src/basiclayout/tutorial/__init__.py - :lines: 12 - :lineno-start: 12 - :language: py +.. literalinclude:: src/basiclayout/tutorial/__init__.py + :lines: 12 + :lineno-start: 12 + :language: py Finally ``main`` is finished configuring things, so it uses the :meth:`pyramid.config.Configurator.make_wsgi_app` method to return a :term:`WSGI` application: - .. literalinclude:: src/basiclayout/tutorial/__init__.py - :lines: 13 - :lineno-start: 13 - :language: py +.. literalinclude:: src/basiclayout/tutorial/__init__.py + :lines: 13 + :lineno-start: 13 + :language: py View declarations via the ``views`` package @@ -136,9 +136,9 @@ corresponding :term:`route`. Our application uses the Open ``tutorial/tutorial/views/default.py`` in the ``views`` package. It should already contain the following: - .. literalinclude:: src/basiclayout/tutorial/views/default.py - :linenos: - :language: py +.. literalinclude:: src/basiclayout/tutorial/views/default.py + :linenos: + :language: py The important part here is that the ``@view_config`` decorator associates the function it decorates (``my_view``) with a :term:`view configuration`, @@ -181,9 +181,9 @@ scaffold put the classes that implement our models. First, open ``tutorial/tutorial/models/__init__.py``, which should already contain the following: - .. literalinclude:: src/basiclayout/tutorial/models/__init__.py - :linenos: - :language: py +.. literalinclude:: src/basiclayout/tutorial/models/__init__.py + :linenos: + :language: py Our ``__init__.py`` will perform some imports to support later code, then calls the function :func:`sqlalchemy.orm.configure_mappers`. @@ -191,17 +191,17 @@ the function :func:`sqlalchemy.orm.configure_mappers`. Next open ``tutorial/tutorial/models/meta.py``, which should already contain the following: - .. literalinclude:: src/basiclayout/tutorial/models/meta.py - :linenos: - :language: py +.. literalinclude:: src/basiclayout/tutorial/models/meta.py + :linenos: + :language: py ``meta.py`` contains imports that are used to support later code. We create a dictionary ``NAMING_CONVENTION`` as well. - .. literalinclude:: src/basiclayout/tutorial/models/meta.py - :end-before: metadata - :linenos: - :language: py +.. literalinclude:: src/basiclayout/tutorial/models/meta.py + :end-before: metadata + :linenos: + :language: py Next we create a ``metadata`` object from the class :class:`sqlalchemy.schema.MetaData`, using ``NAMING_CONVENTION`` as the value @@ -210,60 +210,60 @@ for the ``naming_convention`` argument. We also need to create a declarative will inherit from the ``Base`` class so they can be associated with our particular database connection. - .. literalinclude:: src/basiclayout/tutorial/models/meta.py - :lines: 18-19 - :lineno-start: 18 - :linenos: - :language: py +.. literalinclude:: src/basiclayout/tutorial/models/meta.py + :lines: 18-19 + :lineno-start: 18 + :linenos: + :language: py Next we define several functions, the first of which is ``includeme``, which configures various database settings by calling subsequently defined functions. - .. literalinclude:: src/basiclayout/tutorial/models/meta.py - :pyobject: includeme - :lineno-start: 22 - :linenos: - :language: py +.. literalinclude:: src/basiclayout/tutorial/models/meta.py + :pyobject: includeme + :lineno-start: 22 + :linenos: + :language: py The function ``get_session`` registers a database session with a transaction manager, and returns a ``dbsession`` object. With the transaction manager, our application will automatically issue a transaction commit after every request unless an exception is raised, in which case the transaction will be aborted. - .. literalinclude:: src/basiclayout/tutorial/models/meta.py - :pyobject: get_session - :lineno-start: 35 - :linenos: - :language: py +.. literalinclude:: src/basiclayout/tutorial/models/meta.py + :pyobject: get_session + :lineno-start: 35 + :linenos: + :language: py The ``get_engine`` function creates an :term:`SQLAlchemy` database engine using :func:`sqlalchemy.engine_from_config` from the ``sqlalchemy.``-prefixed settings in the ``development.ini`` file's ``[app:main]`` section, which is a URI, something like ``sqlite://``. - .. literalinclude:: src/basiclayout/tutorial/models/meta.py - :pyobject: get_engine - :lineno-start: 42 - :linenos: - :language: py +.. literalinclude:: src/basiclayout/tutorial/models/meta.py + :pyobject: get_engine + :lineno-start: 42 + :linenos: + :language: py The function ``get_dbmaker`` accepts an :term:`SQLAlchemy` database engine, and creates a database session object ``dbmaker`` from the :term:`SQLAlchemy` class :class:`sqlalchemy.orm.session.sessionmaker`, which is then used for creating a session with the database engine. - .. literalinclude:: src/basiclayout/tutorial/models/meta.py - :pyobject: get_dbmaker - :lineno-start: 46 - :linenos: - :language: py +.. literalinclude:: src/basiclayout/tutorial/models/meta.py + :pyobject: get_dbmaker + :lineno-start: 46 + :linenos: + :language: py To give a simple example of a model class, we define one named ``MyModel``: - .. literalinclude:: src/basiclayout/tutorial/models/mymodel.py - :pyobject: MyModel - :linenos: - :language: py +.. literalinclude:: src/basiclayout/tutorial/models/mymodel.py + :pyobject: MyModel + :linenos: + :language: py Our example model does not require an ``__init__`` method because SQLAlchemy supplies for us a default constructor if one is not already present, which @@ -282,5 +282,5 @@ class. That's about all there is to it regarding models, views, and initialization code in our stock application. -The Index import and the Index object creation is not required for this -tutorial, and will be removed in the next step. +The ``Index`` import and the ``Index`` object creation is not required for +this tutorial, and will be removed in the next step. -- cgit v1.2.3 From 7b89a7e435b9edb4da6976e9185ae425717d4085 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 5 Feb 2016 01:00:49 -0600 Subject: update basiclayout prose --- docs/tutorials/wiki2/basiclayout.rst | 168 ++++++++++++++++++++--------------- 1 file changed, 98 insertions(+), 70 deletions(-) diff --git a/docs/tutorials/wiki2/basiclayout.rst b/docs/tutorials/wiki2/basiclayout.rst index d55ce807f..976f12e90 100644 --- a/docs/tutorials/wiki2/basiclayout.rst +++ b/docs/tutorials/wiki2/basiclayout.rst @@ -29,6 +29,7 @@ later code: .. literalinclude:: src/basiclayout/tutorial/__init__.py :end-before: main :linenos: + :lineno-match: :language: py ``__init__.py`` defines a function named ``main``. Here is the entirety of @@ -36,9 +37,9 @@ the ``main`` function we've defined in our ``__init__.py``: .. literalinclude:: src/basiclayout/tutorial/__init__.py :pyobject: main - :lineno-start: 4 :linenos: - :language: py + :lineno-match: + :language: py When you invoke the ``pserve development.ini`` command, the ``main`` function above is executed. It accepts some settings and returns a :term:`WSGI` @@ -48,7 +49,7 @@ Next in ``main``, construct a :term:`Configurator` object: .. literalinclude:: src/basiclayout/tutorial/__init__.py :lines: 7 - :lineno-start: 7 + :lineno-match: :language: py ``settings`` is passed to the Configurator as a keyword argument with the @@ -62,15 +63,15 @@ with the ``.jinja2`` extension within our project. .. literalinclude:: src/basiclayout/tutorial/__init__.py :lines: 8 - :lineno-start: 8 + :lineno-match: :language: py -Next include the module ``meta`` from the package ``models`` using a dotted -Python path. +Next include the the package ``models`` using a dotted Python path. The +exact setup of the models will be covered later. .. literalinclude:: src/basiclayout/tutorial/__init__.py :lines: 9 - :lineno-start: 9 + :lineno-match: :language: py ``main`` now calls :meth:`pyramid.config.Configurator.add_static_view` with @@ -78,7 +79,7 @@ two arguments: ``static`` (the name), and ``static`` (the path): .. literalinclude:: src/basiclayout/tutorial/__init__.py :lines: 10 - :lineno-start: 10 + :lineno-match: :language: py This registers a static resource view which will match any URL that starts @@ -97,7 +98,7 @@ used when the URL is ``/``: .. literalinclude:: src/basiclayout/tutorial/__init__.py :lines: 11 - :lineno-start: 11 + :lineno-match: :language: py Since this route has a ``pattern`` equaling ``/``, it is the route that will @@ -112,7 +113,7 @@ application URLs to be mapped to some code. .. literalinclude:: src/basiclayout/tutorial/__init__.py :lines: 12 - :lineno-start: 12 + :lineno-match: :language: py Finally ``main`` is finished configuring things, so it uses the @@ -178,25 +179,16 @@ In a SQLAlchemy-based application, a *model* object is an object composed by querying the SQL database. The ``models`` package is where the ``alchemy`` scaffold put the classes that implement our models. -First, open ``tutorial/tutorial/models/__init__.py``, which should already -contain the following: - -.. literalinclude:: src/basiclayout/tutorial/models/__init__.py - :linenos: - :language: py - -Our ``__init__.py`` will perform some imports to support later code, then calls -the function :func:`sqlalchemy.orm.configure_mappers`. - -Next open ``tutorial/tutorial/models/meta.py``, which should already contain +First, open ``tutorial/tutorial/models/meta.py``, which should already contain the following: .. literalinclude:: src/basiclayout/tutorial/models/meta.py :linenos: :language: py -``meta.py`` contains imports that are used to support later code. We create a -dictionary ``NAMING_CONVENTION`` as well. +``meta.py`` contains imports and support code for defining the models. We +create a dictionary ``NAMING_CONVENTION`` as well for consistent naming of +support objects like indices and constraints. .. literalinclude:: src/basiclayout/tutorial/models/meta.py :end-before: metadata @@ -205,82 +197,118 @@ dictionary ``NAMING_CONVENTION`` as well. Next we create a ``metadata`` object from the class :class:`sqlalchemy.schema.MetaData`, using ``NAMING_CONVENTION`` as the value -for the ``naming_convention`` argument. We also need to create a declarative -``Base`` object to use as a base class for our model. Then our model classes -will inherit from the ``Base`` class so they can be associated with our -particular database connection. +for the ``naming_convention`` argument. + +A ``MetaData`` object represents the table and other schema definitions for +a single database. We also need to create a declarative ``Base`` object to use +as a base class for our models. Our models will inherit from this ``Base`` +which will attach the tables to the ``metadata`` we created and define our +application's database schema. .. literalinclude:: src/basiclayout/tutorial/models/meta.py - :lines: 18-19 - :lineno-start: 18 + :lines: 15-16 + :lineno-match: :linenos: :language: py -Next we define several functions, the first of which is ``includeme``, which -configures various database settings by calling subsequently defined functions. +We've defined the ``models`` as a packge to make it straightforward to +define models separately in different modules. To give a simple example of a +model class, we define one named ``MyModel`` in a ``mymodel.py``: -.. literalinclude:: src/basiclayout/tutorial/models/meta.py - :pyobject: includeme - :lineno-start: 22 +.. literalinclude:: src/basiclayout/tutorial/models/mymodel.py + :pyobject: MyModel :linenos: :language: py -The function ``get_session`` registers a database session with a transaction -manager, and returns a ``dbsession`` object. With the transaction manager, our -application will automatically issue a transaction commit after every request -unless an exception is raised, in which case the transaction will be aborted. +Our example model does not require an ``__init__`` method because SQLAlchemy +supplies for us a default constructor if one is not already present, which +accepts keyword arguments of the same name as that of the mapped attributes. -.. literalinclude:: src/basiclayout/tutorial/models/meta.py - :pyobject: get_session - :lineno-start: 35 +.. note:: Example usage of MyModel: + + .. code-block:: python + + johnny = MyModel(name="John Doe", value=10) + +The ``MyModel`` class has a ``__tablename__`` attribute. This informs +SQLAlchemy which table to use to store the data representing instances of this +class. + +Finally, open ``tutorial/tutorial/models/__init__.py``, which should already +contain the following: + +.. literalinclude:: src/basiclayout/tutorial/models/__init__.py :linenos: :language: py -The ``get_engine`` function creates an :term:`SQLAlchemy` database engine using -:func:`sqlalchemy.engine_from_config` from the ``sqlalchemy.``-prefixed -settings in the ``development.ini`` file's ``[app:main]`` section, which is a -URI, something like ``sqlite://``. +Our ``models/__init__.py`` module defines the primary API we will use for +configuring the database connections within our application and it contains +several functions we will cover below. + +As we mentioned above, the purpose of the ``models.meta.metadata`` object is +to describe the schema of the database and this is done by defining models +that inherit from the ``Base`` attached to that ``metadata`` object. In +Python, code is only executed if it is imported and so to attach the +``models`` table, defined in ``mymodel.py`` to the ``metadata`` we must +import it. If we skip this step then later when we run ``metadata.create_all`` +the table will not be created because the ``metadata`` does not know about it! +Another important reason to import all of the models is that when +defining relationships between models they must all exist in order for +SQLAlchemy to find and build those internal mappings. This is why after +importing all the models we explicitly execute the function +:func:`sqlalchemy.orm.configure_mappers`, once we are sure all the models have +been defined and before we start creating connections. + +Next we define several functions for connecting to our database. The first +and lowest level is the ``get_engine`` function which creates an +:term:`SQLAlchemy` database engine using :func:`sqlalchemy.engine_from_config` +from the ``sqlalchemy.``-prefixed settings in the ``development.ini`` +file's ``[app:main]`` section, which is a URI (something like ``sqlite://``). -.. literalinclude:: src/basiclayout/tutorial/models/meta.py +.. literalinclude:: src/basiclayout/tutorial/models/__init__.py :pyobject: get_engine - :lineno-start: 42 + :lineno-match: :linenos: :language: py -The function ``get_dbmaker`` accepts an :term:`SQLAlchemy` database engine, -and creates a database session object ``dbmaker`` from the :term:`SQLAlchemy` +The function ``get_session_factory`` accepts an :term:`SQLAlchemy` database +engine, and creates a ``session_factory`` from the :term:`SQLAlchemy` class :class:`sqlalchemy.orm.session.sessionmaker`, which is then used for -creating a session with the database engine. +creating sessions bound to the database engine. -.. literalinclude:: src/basiclayout/tutorial/models/meta.py - :pyobject: get_dbmaker - :lineno-start: 46 +.. literalinclude:: src/basiclayout/tutorial/models/__init__.py + :pyobject: get_session_factory + :lineno-match: :linenos: :language: py -To give a simple example of a model class, we define one named ``MyModel``: +The function ``get_tm_session`` registers a database session with a transaction +manager, and returns a ``dbsession`` object. With the transaction manager, our +application will automatically issue a transaction commit after every request +unless an exception is raised, in which case the transaction will be aborted. -.. literalinclude:: src/basiclayout/tutorial/models/mymodel.py - :pyobject: MyModel +.. literalinclude:: src/basiclayout/tutorial/models/__init__.py + :pyobject: get_tm_session + :lineno-match: :linenos: :language: py -Our example model does not require an ``__init__`` method because SQLAlchemy -supplies for us a default constructor if one is not already present, which -accepts keyword arguments of the same name as that of the mapped attributes. - -.. note:: Example usage of MyModel: - - .. code-block:: python - - johnny = MyModel(name="John Doe", value=10) +Finally, we define an ``includeme`` function, which is a hook for use with +:meth:`pyramid.config.Configurator.include` to activate code in a Pyramid +application addon. It is the code that is executed above when we ran +``config.include('.models')`` in our application's ``main`` function. This +function will take the settings from the application, create an engine +and define a ``request.dbsession`` property which we can use to do work +on behalf of an incoming request to our application. -The ``MyModel`` class has a ``__tablename__`` attribute. This informs -SQLAlchemy which table to use to store the data representing instances of this -class. +.. literalinclude:: src/basiclayout/tutorial/models/__init__.py + :pyobject: includeme + :lineno-match: + :linenos: + :language: py That's about all there is to it regarding models, views, and initialization code in our stock application. -The ``Index`` import and the ``Index`` object creation is not required for -this tutorial, and will be removed in the next step. +The ``Index`` import and the ``Index`` object creation in ``mymodel.py`` is +not required for this tutorial, and will be removed in the next step. -- cgit v1.2.3 From 21d69fd97b66401264746bb7dad1e8d4bd2491b9 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 5 Feb 2016 01:09:11 -0600 Subject: link to create_all sqla method --- docs/tutorials/wiki2/basiclayout.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/tutorials/wiki2/basiclayout.rst b/docs/tutorials/wiki2/basiclayout.rst index 976f12e90..6d1ff73a0 100644 --- a/docs/tutorials/wiki2/basiclayout.rst +++ b/docs/tutorials/wiki2/basiclayout.rst @@ -250,8 +250,10 @@ to describe the schema of the database and this is done by defining models that inherit from the ``Base`` attached to that ``metadata`` object. In Python, code is only executed if it is imported and so to attach the ``models`` table, defined in ``mymodel.py`` to the ``metadata`` we must -import it. If we skip this step then later when we run ``metadata.create_all`` -the table will not be created because the ``metadata`` does not know about it! +import it. If we skip this step then later when we run +:meth:`sqlalchemy.schema.MetaData.create_all` the table will not be created +because the ``metadata`` does not know about it! + Another important reason to import all of the models is that when defining relationships between models they must all exist in order for SQLAlchemy to find and build those internal mappings. This is why after -- cgit v1.2.3 From ab68f1f89a0a1602078bf1a99741d0635ce06dd0 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Fri, 5 Feb 2016 01:28:18 -0800 Subject: minor grammar and punctuation tweaks, break up run-on sentences. --- docs/tutorials/wiki2/basiclayout.rst | 50 ++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/docs/tutorials/wiki2/basiclayout.rst b/docs/tutorials/wiki2/basiclayout.rst index 6d1ff73a0..3533bb455 100644 --- a/docs/tutorials/wiki2/basiclayout.rst +++ b/docs/tutorials/wiki2/basiclayout.rst @@ -201,8 +201,8 @@ for the ``naming_convention`` argument. A ``MetaData`` object represents the table and other schema definitions for a single database. We also need to create a declarative ``Base`` object to use -as a base class for our models. Our models will inherit from this ``Base`` -which will attach the tables to the ``metadata`` we created and define our +as a base class for our models. Our models will inherit from this ``Base``, +which will attach the tables to the ``metadata`` we created, and define our application's database schema. .. literalinclude:: src/basiclayout/tutorial/models/meta.py @@ -242,30 +242,30 @@ contain the following: :language: py Our ``models/__init__.py`` module defines the primary API we will use for -configuring the database connections within our application and it contains +configuring the database connections within our application, and it contains several functions we will cover below. As we mentioned above, the purpose of the ``models.meta.metadata`` object is -to describe the schema of the database and this is done by defining models -that inherit from the ``Base`` attached to that ``metadata`` object. In -Python, code is only executed if it is imported and so to attach the -``models`` table, defined in ``mymodel.py`` to the ``metadata`` we must -import it. If we skip this step then later when we run -:meth:`sqlalchemy.schema.MetaData.create_all` the table will not be created +to describe the schema of the database. This is done by defining models that +inherit from the ``Base`` attached to that ``metadata`` object. In Python, code +is only executed if it is imported, and so to attach the ``models`` table +defined in ``mymodel.py`` to the ``metadata``, we must import it. If we skip +this step, then later, when we run +:meth:`sqlalchemy.schema.MetaData.create_all`, the table will not be created because the ``metadata`` does not know about it! -Another important reason to import all of the models is that when -defining relationships between models they must all exist in order for -SQLAlchemy to find and build those internal mappings. This is why after -importing all the models we explicitly execute the function +Another important reason to import all of the models is that, when defining +relationships between models, they must all exist in order for SQLAlchemy to +find and build those internal mappings. This is why, after importing all the +models, we explicitly execute the function :func:`sqlalchemy.orm.configure_mappers`, once we are sure all the models have been defined and before we start creating connections. -Next we define several functions for connecting to our database. The first -and lowest level is the ``get_engine`` function which creates an -:term:`SQLAlchemy` database engine using :func:`sqlalchemy.engine_from_config` -from the ``sqlalchemy.``-prefixed settings in the ``development.ini`` -file's ``[app:main]`` section, which is a URI (something like ``sqlite://``). +Next we define several functions for connecting to our database. The first and +lowest level is the ``get_engine`` function. This creates an :term:`SQLAlchemy` +database engine using :func:`sqlalchemy.engine_from_config` from the +``sqlalchemy.``-prefixed settings in the ``development.ini`` file's +``[app:main]`` section. This setting is a URI (something like ``sqlite://``). .. literalinclude:: src/basiclayout/tutorial/models/__init__.py :pyobject: get_engine @@ -274,9 +274,9 @@ file's ``[app:main]`` section, which is a URI (something like ``sqlite://``). :language: py The function ``get_session_factory`` accepts an :term:`SQLAlchemy` database -engine, and creates a ``session_factory`` from the :term:`SQLAlchemy` -class :class:`sqlalchemy.orm.session.sessionmaker`, which is then used for -creating sessions bound to the database engine. +engine, and creates a ``session_factory`` from the :term:`SQLAlchemy` class +:class:`sqlalchemy.orm.session.sessionmaker`. This ``session_factory`` is then +used for creating sessions bound to the database engine. .. literalinclude:: src/basiclayout/tutorial/models/__init__.py :pyobject: get_session_factory @@ -286,7 +286,7 @@ creating sessions bound to the database engine. The function ``get_tm_session`` registers a database session with a transaction manager, and returns a ``dbsession`` object. With the transaction manager, our -application will automatically issue a transaction commit after every request +application will automatically issue a transaction commit after every request, unless an exception is raised, in which case the transaction will be aborted. .. literalinclude:: src/basiclayout/tutorial/models/__init__.py @@ -297,10 +297,10 @@ unless an exception is raised, in which case the transaction will be aborted. Finally, we define an ``includeme`` function, which is a hook for use with :meth:`pyramid.config.Configurator.include` to activate code in a Pyramid -application addon. It is the code that is executed above when we ran +application add-on. It is the code that is executed above when we ran ``config.include('.models')`` in our application's ``main`` function. This -function will take the settings from the application, create an engine -and define a ``request.dbsession`` property which we can use to do work +function will take the settings from the application, create an engine, +and define a ``request.dbsession`` property, which we can use to do work on behalf of an incoming request to our application. .. literalinclude:: src/basiclayout/tutorial/models/__init__.py -- cgit v1.2.3 From ed5209f2bdbb7bc8c54488cea97a0274af44be58 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sat, 6 Feb 2016 01:30:28 -0800 Subject: minor grammar and punctuation --- docs/designdefense.rst | 43 ++++++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/docs/designdefense.rst b/docs/designdefense.rst index 19d4fc49f..bc4e6fcfd 100644 --- a/docs/designdefense.rst +++ b/docs/designdefense.rst @@ -1003,31 +1003,32 @@ the following: .. _microframeworks_smaller_hello_world: -Microframeworks Have Smaller Hello World Programs +Microframeworks have smaller Hello World programs ------------------------------------------------- -Self-described "microframeworks" exist: `Bottle `_ and -`Flask `_ are two that are becoming popular. `Bobo -`_ doesn't describe itself as a microframework, -but its intended userbase is much the same. Many others exist. We've -actually even (only as a teaching tool, not as any sort of official project) -`created one using Pyramid `_ (the -videos use BFG, a precursor to Pyramid, but the resulting code is `available -for Pyramid too `_). Microframeworks are -small frameworks with one common feature: each allows its users to create a -fully functional application that lives in a single Python file. +Self-described "microframeworks" exist. `Bottle `_ and +`Flask `_ are two that are becoming popular. `Bobo +`_ doesn't describe itself as a microframework, but +its intended user base is much the same. Many others exist. We've even (only as +a teaching tool, not as any sort of official project) `created one using +Pyramid `_. The videos use BFG, a +precursor to Pyramid, but the resulting code is `available for Pyramid too +`_). Microframeworks are small frameworks +with one common feature: each allows its users to create a fully functional +application that lives in a single Python file. Some developers and microframework authors point out that Pyramid's "hello -world" single-file program is longer (by about five lines) than the -equivalent program in their favorite microframework. Guilty as charged. - -This loss isn't for lack of trying. Pyramid is useful in the same -circumstance in which microframeworks claim dominance: single-file -applications. But Pyramid doesn't sacrifice its ability to credibly support -larger applications in order to achieve hello-world LoC parity with the -current crop of microframeworks. Pyramid's design instead tries to avoid -some common pitfalls associated with naive declarative configuration schemes. -The subsections which follow explain the rationale. +world" single-file program is longer (by about five lines) than the equivalent +program in their favorite microframework. Guilty as charged. + +This loss isn't for lack of trying. Pyramid is useful in the same circumstance +in which microframeworks claim dominance: single-file applications. But Pyramid +doesn't sacrifice its ability to credibly support larger applications in order +to achieve "hello world" lines of code parity with the current crop of +microframeworks. Pyramid's design instead tries to avoid some common pitfalls +associated with naive declarative configuration schemes. The subsections which +follow explain the rationale. + .. _you_dont_own_modulescope: -- cgit v1.2.3 From 46f74df66cd940b8ed8b2cd0607f3eb4d8b65948 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sun, 7 Feb 2016 01:04:38 -0800 Subject: minor grammar and punctuation through "import-time side-effects are evil" --- docs/designdefense.rst | 146 ++++++++++++++++++++++++------------------------- 1 file changed, 72 insertions(+), 74 deletions(-) diff --git a/docs/designdefense.rst b/docs/designdefense.rst index bc4e6fcfd..d33ae2fd8 100644 --- a/docs/designdefense.rst +++ b/docs/designdefense.rst @@ -1032,10 +1032,10 @@ follow explain the rationale. .. _you_dont_own_modulescope: -Application Programmers Don't Control The Module-Scope Codepath (Import-Time Side-Effects Are Evil) +Application programmers don't control the module-scope codepath (import-time side-effects are evil) +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -Please imagine a directory structure with a set of Python files in it: +Imagine a directory structure with a set of Python files in it: .. code-block:: text @@ -1083,13 +1083,13 @@ The contents of ``config.py``: L.append(func) return func -If we cd to the directory that holds these files and we run ``python app.py`` -given the directory structure and code above, what happens? Presumably, our -``decorator`` decorator will be used twice, once by the decorated function -``foo`` in ``app.py`` and once by the decorated function ``bar`` in -``app2.py``. Since each time the decorator is used, the list ``L`` in -``config.py`` is appended to, we'd expect a list with two elements to be -printed, right? Sadly, no: +If we ``cd`` to the directory that holds these files, and we run +``python app.py``, given the directory structure and code above, what happens? +Presumably, our ``decorator`` decorator will be used twice, once by the +decorated function ``foo`` in ``app.py``, and once by the decorated function +``bar`` in ``app2.py``. Since each time the decorator is used, the list ``L`` +in ``config.py`` is appended to, we'd expect a list with two elements to be +printed, right? Sadly, no: .. code-block:: text @@ -1099,21 +1099,21 @@ printed, right? Sadly, no: ] By visual inspection, that outcome (three different functions in the list) -seems impossible. We only defined two functions and we decorated each of -those functions only once, so we believe that the ``decorator`` decorator -will only run twice. However, what we believe is wrong because the code at -module scope in our ``app.py`` module was *executed twice*. The code is +seems impossible. We defined only two functions, and we decorated each of those +functions only once, so we believe that the ``decorator`` decorator will run +only twice. However, what we believe is in fact wrong, because the code at +module scope in our ``app.py`` module was *executed twice*. The code is executed once when the script is run as ``__main__`` (via ``python app.py``), and then it is executed again when ``app2.py`` imports the same file as ``app``. -What does this have to do with our comparison to microframeworks? Many -microframeworks in the current crop (e.g. Bottle, Flask) encourage you to -attach configuration decorators to objects defined at module scope. These -decorators execute arbitrarily complex registration code which populates a -singleton registry that is a global defined in external Python module. This -is analogous to the above example: the "global registry" in the above example -is the list ``L``. +What does this have to do with our comparison to microframeworks? Many +microframeworks in the current crop (e.g., Bottle and Flask) encourage you to +attach configuration decorators to objects defined at module scope. These +decorators execute arbitrarily complex registration code, which populates a +singleton registry that is a global which is in turn defined in external Python +module. This is analogous to the above example: the "global registry" in the +above example is the list ``L``. Let's see what happens when we use the same pattern with the `Groundhog `_ microframework. Replace the contents @@ -1166,41 +1166,39 @@ will be. The encouragement to use decorators which perform population of an external registry has an unintended consequence: the application developer now must -assert ownership of every codepath that executes Python module scope -code. Module-scope code is presumed by the current crop of decorator-based -microframeworks to execute once and only once; if it executes more than once, -weird things will start to happen. It is up to the application developer to -maintain this invariant. Unfortunately, however, in reality, this is an -impossible task, because, Python programmers *do not own the module scope -codepath, and never will*. Anyone who tries to sell you on the idea that -they do is simply mistaken. Test runners that you may want to use to run -your code's tests often perform imports of arbitrary code in strange orders -that manifest bugs like the one demonstrated above. API documentation -generation tools do the same. Some people even think it's safe to use the -Python ``reload`` command or delete objects from ``sys.modules``, each of -which has hilarious effects when used against code that has import-time side -effects. - -Global-registry-mutating microframework programmers therefore will at some -point need to start reading the tea leaves about what *might* happen if -module scope code gets executed more than once like we do in the previous -paragraph. When Python programmers assume they can use the module-scope -codepath to run arbitrary code (especially code which populates an external -registry), and this assumption is challenged by reality, the application -developer is often required to undergo a painful, meticulous debugging -process to find the root cause of an inevitably obscure symptom. The -solution is often to rearrange application import ordering or move an import -statement from module-scope into a function body. The rationale for doing so -can never be expressed adequately in the checkin message which accompanies -the fix and can't be documented succinctly enough for the benefit of the rest -of the development team so that the problem never happens again. It will -happen again, especially if you are working on a project with other people -who haven't yet internalized the lessons you learned while you stepped -through module-scope code using ``pdb``. This is a really pretty poor -situation to find yourself in as an application developer: you probably -didn't even know your or your team signed up for the job, because the -documentation offered by decorator-based microframeworks don't warn you about -it. +assert ownership of every code path that executes Python module scope code. +Module-scope code is presumed by the current crop of decorator-based +microframeworks to execute once and only once. If it executes more than once, +weird things will start to happen. It is up to the application developer to +maintain this invariant. Unfortunately, in reality this is an impossible task, +because Python programmers *do not own the module scope code path, and never +will*. Anyone who tries to sell you on the idea that they do so is simply +mistaken. Test runners that you may want to use to run your code's tests often +perform imports of arbitrary code in strange orders that manifest bugs like the +one demonstrated above. API documentation generation tools do the same. Some +people even think it's safe to use the Python ``reload`` command, or delete +objects from ``sys.modules``, each of which has hilarious effects when used +against code that has import-time side effects. + +Global registry-mutating microframework programmers therefore will at some +point need to start reading the tea leaves about what *might* happen if module +scope code gets executed more than once, like we do in the previous paragraph. +When Python programmers assume they can use the module-scope code path to run +arbitrary code (especially code which populates an external registry), and this +assumption is challenged by reality, the application developer is often +required to undergo a painful, meticulous debugging process to find the root +cause of an inevitably obscure symptom. The solution is often to rearrange +application import ordering, or move an import statement from module-scope into +a function body. The rationale for doing so can never be expressed adequately +in the commit message which accompanies the fix, and can't be documented +succinctly enough for the benefit of the rest of the development team so that +the problem never happens again. It will happen again, especially if you are +working on a project with other people who haven't yet internalized the lessons +you learned while you stepped through module-scope code using ``pdb``. This is +a very poor situation in which to find yourself as an application developer: +you probably didn't even know you or your team signed up for the job, because +the documentation offered by decorator-based microframeworks don't warn you +about it. Folks who have a large investment in eager decorator-based configuration that populates an external data structure (such as microframework authors) may @@ -1216,7 +1214,7 @@ time, and application complexity. If microframework authors do admit that the circumstance isn't contrived, they might then argue that real damage will never happen as the result of the -double-execution (or triple-execution, etc) of module scope code. You would +double-execution (or triple-execution, etc.) of module scope code. You would be wise to disbelieve this assertion. The potential outcomes of multiple execution are too numerous to predict because they involve delicate relationships between application and framework code as well as chronology of @@ -1224,14 +1222,14 @@ code execution. It's literally impossible for a framework author to know what will happen in all circumstances. But even if given the gift of omniscience for some limited set of circumstances, the framework author almost certainly does not have the double-execution anomaly in mind when -coding new features. He's thinking of adding a feature, not protecting +coding new features. They're thinking of adding a feature, not protecting against problems that might be caused by the 1% multiple execution case. However, any 1% case may cause 50% of your pain on a project, so it'd be nice -if it never occured. +if it never occurred. -Responsible microframeworks actually offer a back-door way around the -problem. They allow you to disuse decorator based configuration entirely. -Instead of requiring you to do the following: +Responsible microframeworks actually offer a back-door way around the problem. +They allow you to disuse decorator-based configuration entirely. Instead of +requiring you to do the following: .. code-block:: python :linenos: @@ -1245,7 +1243,7 @@ Instead of requiring you to do the following: if __name__ == '__main__': gh.run() -They allow you to disuse the decorator syntax and go almost-all-imperative: +They allow you to disuse the decorator syntax and go almost all-imperative: .. code-block:: python :linenos: @@ -1269,18 +1267,18 @@ predictability. .. note:: - Astute readers may notice that Pyramid has configuration decorators too. - Aha! Don't these decorators have the same problems? No. These decorators - do not populate an external Python module when they are executed. They - only mutate the functions (and classes and methods) they're attached to. - These mutations must later be found during a scan process that has a - predictable and structured import phase. Module-localized mutation is - actually the best-case circumstance for double-imports; if a module only - mutates itself and its contents at import time, if it is imported twice, - that's OK, because each decorator invocation will always be mutating an - independent copy of the object it's attached to, not a shared resource like - a registry in another module. This has the effect that - double-registrations will never be performed. + Astute readers may notice that Pyramid has configuration decorators too. Aha! + Don't these decorators have the same problems? No. These decorators do not + populate an external Python module when they are executed. They only mutate + the functions (and classes and methods) to which they're attached. These + mutations must later be found during a scan process that has a predictable + and structured import phase. Module-localized mutation is actually the + best-case circumstance for double-imports. If a module only mutates itself + and its contents at import time, if it is imported twice, that's OK, because + each decorator invocation will always be mutating an independent copy of the + object to which it's attached, not a shared resource like a registry in + another module. This has the effect that double-registrations will never be + performed. .. _routes_need_ordering: -- cgit v1.2.3 From d1cb34643e086ac74965455b486ce0058764324f Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 7 Feb 2016 13:57:51 -0600 Subject: assume the user is in the tutorial folder this is already assumed inside of installation where commands are run relative to setup.py --- docs/tutorials/wiki2/basiclayout.rst | 8 ++++---- docs/tutorials/wiki2/definingmodels.rst | 13 +++++++------ docs/tutorials/wiki2/definingviews.rst | 6 +++--- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/docs/tutorials/wiki2/basiclayout.rst b/docs/tutorials/wiki2/basiclayout.rst index 6d1ff73a0..eb315d2cb 100644 --- a/docs/tutorials/wiki2/basiclayout.rst +++ b/docs/tutorials/wiki2/basiclayout.rst @@ -16,7 +16,7 @@ package. We use ``__init__.py`` both as a marker, indicating the directory in which it's contained is a package, and to contain application configuration code. -Open ``tutorial/tutorial/__init__.py``. It should already contain the +Open ``tutorial/__init__.py``. It should already contain the following: .. literalinclude:: src/basiclayout/tutorial/__init__.py @@ -134,7 +134,7 @@ The main function of a web framework is mapping each URL pattern to code (a corresponding :term:`route`. Our application uses the :meth:`pyramid.view.view_config` decorator to perform this mapping. -Open ``tutorial/tutorial/views/default.py`` in the ``views`` package. It +Open ``tutorial/views/default.py`` in the ``views`` package. It should already contain the following: .. literalinclude:: src/basiclayout/tutorial/views/default.py @@ -179,7 +179,7 @@ In a SQLAlchemy-based application, a *model* object is an object composed by querying the SQL database. The ``models`` package is where the ``alchemy`` scaffold put the classes that implement our models. -First, open ``tutorial/tutorial/models/meta.py``, which should already contain +First, open ``tutorial/models/meta.py``, which should already contain the following: .. literalinclude:: src/basiclayout/tutorial/models/meta.py @@ -234,7 +234,7 @@ The ``MyModel`` class has a ``__tablename__`` attribute. This informs SQLAlchemy which table to use to store the data representing instances of this class. -Finally, open ``tutorial/tutorial/models/__init__.py``, which should already +Finally, open ``tutorial/models/__init__.py``, which should already contain the following: .. literalinclude:: src/basiclayout/tutorial/models/__init__.py diff --git a/docs/tutorials/wiki2/definingmodels.rst b/docs/tutorials/wiki2/definingmodels.rst index b38177d04..9b517994f 100644 --- a/docs/tutorials/wiki2/definingmodels.rst +++ b/docs/tutorials/wiki2/definingmodels.rst @@ -14,11 +14,12 @@ Edit ``mymodel.py`` There is nothing special about the filename ``mymodel.py``. A project may have many models throughout its codebase in arbitrarily named - files. Files implementing models often have ``model`` in their filenames - or they may live in a Python subpackage of your application package named - ``models`` (as we've done in this tutorial), but this is only by convention. + modules. Modules implementing models often have ``model`` in their + names or they may live in a Python subpackage of your application package + named ``models`` (as we've done in this tutorial), but this is only a + convention and not a requirement. -Open the ``tutorial/tutorial/models/mymodel.py`` file and edit it to look like +Open the ``tutorial/models/mymodel.py`` file and edit it to look like the following: .. literalinclude:: src/models/tutorial/models/mymodel.py @@ -59,7 +60,7 @@ Edit ``models/__init__.py`` Since we are using a package for our models, we also need to update our ``__init__.py`` file. -Open the ``tutorial/tutorial/models/__init__.py`` file and edit it to look like +Open the ``tutorial/models/__init__.py`` file and edit it to look like the following: .. literalinclude:: src/models/tutorial/models/__init__.py @@ -84,7 +85,7 @@ Since we've changed our model, we need to make changes to our to create a ``Page`` rather than a ``MyModel`` and add it to our ``DBSession``. -Open ``tutorial/tutorial/scripts/initializedb.py`` and edit it to look like +Open ``tutorial/scripts/initializedb.py`` and edit it to look like the following: .. literalinclude:: src/models/tutorial/scripts/initializedb.py diff --git a/docs/tutorials/wiki2/definingviews.rst b/docs/tutorials/wiki2/definingviews.rst index 08fa8f16b..8660c2772 100644 --- a/docs/tutorials/wiki2/definingviews.rst +++ b/docs/tutorials/wiki2/definingviews.rst @@ -67,7 +67,7 @@ like this:: Adding view functions in ``views/default.py`` ============================================= -It's time for a major change. Open ``tutorial/tutorial/views/default.py`` and +It's time for a major change. Open ``tutorial/views/default.py`` and edit it to look like the following: .. literalinclude:: src/views/tutorial/views/default.py @@ -243,7 +243,7 @@ as such. The ``view.jinja2`` template ---------------------------- -Create ``tutorial/tutorial/templates/view.jinja2`` and add the following +Create ``tutorial/templates/view.jinja2`` and add the following content: .. literalinclude:: src/views/tutorial/templates/view.jinja2 @@ -263,7 +263,7 @@ wiki page. It includes: The ``edit.jinja2`` template ---------------------------- -Create ``tutorial/tutorial/templates/edit.jinja2`` and add the following +Create ``tutorial/templates/edit.jinja2`` and add the following content: .. literalinclude:: src/views/tutorial/templates/edit.jinja2 -- cgit v1.2.3 From ed04017d5a8e82db2e46412f841c55d83ef062b0 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 7 Feb 2016 14:00:20 -0600 Subject: fix pyramid_tm url --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 1004e1cf9..a895bc6c3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -69,7 +69,7 @@ intersphinx_mapping = { 'python': ('http://docs.python.org', None), 'python3': ('http://docs.python.org/3', None), 'sqla': ('http://docs.sqlalchemy.org/en/latest', None), - 'tm': ('http://docs.pylonsproject.org/projects/pyramid_tm/en/latest/', None), + 'tm': ('http://docs.pylonsproject.org/projects/pyramid-tm/en/latest/', None), 'toolbar': ('http://docs.pylonsproject.org/projects/pyramid-debugtoolbar/en/latest', None), 'tstring': ('http://docs.pylonsproject.org/projects/translationstring/en/latest', None), 'tutorials': ('http://docs.pylonsproject.org/projects/pyramid-tutorials/en/latest/', None), -- cgit v1.2.3 From 8d457153240be8158eb22c6204fc37196e52b654 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 7 Feb 2016 13:58:21 -0600 Subject: reference addon links for pyramid_jinja2, pyramid_tm, zope.sqlalchemy and transaction --- docs/tutorials/wiki2/installation.rst | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/docs/tutorials/wiki2/installation.rst b/docs/tutorials/wiki2/installation.rst index 047c66c06..70d0444b7 100644 --- a/docs/tutorials/wiki2/installation.rst +++ b/docs/tutorials/wiki2/installation.rst @@ -397,12 +397,17 @@ Decisions the ``alchemy`` scaffold has made for you Creating a project using the ``alchemy`` scaffold makes the following assumptions: -- you are willing to use :term:`SQLAlchemy` as a database access tool +- You are willing to use :term:`SQLAlchemy` as a database access tool. -- you are willing to use :term:`URL dispatch` to map URLs to code +- You are willing to use :term:`URL dispatch` to map URLs to code. -- you want to use ``zope.sqlalchemy`` and ``pyramid_tm`` to scope - sessions to requests +- You want to use zope.sqlalchemy_, pyramid_tm_ and the transaction_ package + to scope sessions to requests. + +- You want to use pyramid_jinja2_ to render your templates. + Different templating engines can be used but we had to choose one to + make the tutorial. See :ref:`available_template_system_bindings` for some + options. .. note:: @@ -411,3 +416,15 @@ assumptions: mechanism to map URLs to code (:term:`traversal`). However, for the purposes of this tutorial, we'll only be using URL dispatch and SQLAlchemy. + +.. _pyramid_jinja2: + http://docs.pylonsproject.org/projects/pyramid-jinja2/en/latest/ + +.. _pyramid_tm: + http://docs.pylonsproject.org/projects/pyramid-tm/en/latest/ + +.. _zope.sqlalchemy: + https://pypi.python.org/pypi/zope.sqlalchemy + +.. _transaction: + http://zodb.readthedocs.org/en/latest/transactions.html -- cgit v1.2.3 From d6243ac1e7724cce26a738de5b86f187ef444e77 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 7 Feb 2016 15:29:17 -0600 Subject: update definingmodels chapter of wiki2 tutorial --- docs/tutorials/wiki2/definingmodels.rst | 32 +++++----- docs/tutorials/wiki2/installation.rst | 7 +++ docs/tutorials/wiki2/src/models/MANIFEST.in | 2 +- docs/tutorials/wiki2/src/models/production.ini | 2 - .../wiki2/src/models/tutorial/__init__.py | 2 +- .../wiki2/src/models/tutorial/models/__init__.py | 71 +++++++++++++++++++++- .../wiki2/src/models/tutorial/models/meta.py | 33 ---------- .../wiki2/src/models/tutorial/models/mymodel.py | 3 +- .../src/models/tutorial/scripts/initializedb.py | 27 ++++---- .../wiki2/src/models/tutorial/templates/404.jinja2 | 8 +++ docs/tutorials/wiki2/src/models/tutorial/tests.py | 18 +++--- .../wiki2/src/models/tutorial/views/default.py | 4 +- .../wiki2/src/models/tutorial/views/errors.py | 5 ++ 13 files changed, 133 insertions(+), 81 deletions(-) create mode 100644 docs/tutorials/wiki2/src/models/tutorial/templates/404.jinja2 create mode 100644 docs/tutorials/wiki2/src/models/tutorial/views/errors.py diff --git a/docs/tutorials/wiki2/definingmodels.rst b/docs/tutorials/wiki2/definingmodels.rst index 9b517994f..b90bf77e6 100644 --- a/docs/tutorials/wiki2/definingmodels.rst +++ b/docs/tutorials/wiki2/definingmodels.rst @@ -3,7 +3,7 @@ Defining the Domain Model ========================= The first change we'll make to our stock ``pcreate``-generated application will -be to define a :term:`domain model` constructor representing a wiki page. +be to define a wiki page :term:`domain model`. We'll do this inside our ``mymodel.py`` file. @@ -12,12 +12,12 @@ Edit ``mymodel.py`` .. note:: - There is nothing special about the filename ``mymodel.py``. A - project may have many models throughout its codebase in arbitrarily named - modules. Modules implementing models often have ``model`` in their - names or they may live in a Python subpackage of your application package - named ``models`` (as we've done in this tutorial), but this is only a - convention and not a requirement. + There is nothing special about the filename ``mymodel.py`` except that it + is a Python module. A project may have many models throughout its codebase + in arbitrarily named modules. Modules implementing models often have + ``model`` in their names or they may live in a Python subpackage of your + application package named ``models`` (as we've done in this tutorial), but + this is only a convention and not a requirement. Open the ``tutorial/models/mymodel.py`` file and edit it to look like the following: @@ -25,7 +25,7 @@ the following: .. literalinclude:: src/models/tutorial/models/mymodel.py :linenos: :language: py - :emphasize-lines: 9-11,13,14 + :emphasize-lines: 10-12,14-15 The highlighted lines are the ones that need to be changed, as well as removing lines that reference ``Index``. @@ -34,7 +34,7 @@ The first thing we've done is remove the stock ``MyModel`` class from the generated ``models.py`` file. The ``MyModel`` class is only a sample and we're not going to use it. -Then we added a ``Page`` class. Because this is an SQLAlchemy application, +Then we added a ``Page`` class. Because this is a SQLAlchemy application, this class inherits from an instance of :func:`sqlalchemy.ext.declarative.declarative_base`. @@ -43,7 +43,7 @@ this class inherits from an instance of :linenos: :language: python -As you can see, our ``Page`` class has a class level attribute +As you can see, our ``Page`` class has a class-level attribute ``__tablename__`` which equals the string ``'pages'``. This means that SQLAlchemy will store our wiki data in a SQL table named ``pages``. Our ``Page`` class will also have class-level attributes named ``id``, ``name``, @@ -58,7 +58,7 @@ Edit ``models/__init__.py`` --------------------------- Since we are using a package for our models, we also need to update our -``__init__.py`` file. +``__init__.py`` file to ensure that the model is attached to the metadata. Open the ``tutorial/models/__init__.py`` file and edit it to look like the following: @@ -66,7 +66,7 @@ the following: .. literalinclude:: src/models/tutorial/models/__init__.py :linenos: :language: py - :emphasize-lines: 4 + :emphasize-lines: 8 Here we need to align our import with the name of the model ``Page``. @@ -83,7 +83,7 @@ Since we've changed our model, we need to make changes to our ``initializedb.py`` script. In particular, we'll replace our import of ``MyModel`` with one of ``Page`` and we'll change the very end of the script to create a ``Page`` rather than a ``MyModel`` and add it to our -``DBSession``. +``dbsession``. Open ``tutorial/scripts/initializedb.py`` and edit it to look like the following: @@ -91,11 +91,9 @@ the following: .. literalinclude:: src/models/tutorial/scripts/initializedb.py :linenos: :language: python - :emphasize-lines: 16,31,41 + :emphasize-lines: 18,44-45 -Only the highlighted lines need to be changed, as well as removing the lines -referencing ``pyramid.scripts.common`` and ``options`` under the ``main`` -function. +Only the highlighted lines need to be changed. Installing the project and re-initializing the database diff --git a/docs/tutorials/wiki2/installation.rst b/docs/tutorials/wiki2/installation.rst index 70d0444b7..5d6d8e56b 100644 --- a/docs/tutorials/wiki2/installation.rst +++ b/docs/tutorials/wiki2/installation.rst @@ -298,6 +298,13 @@ Initializing the database We need to use the ``initialize_tutorial_db`` :term:`console script` to initialize our database. +.. note:: + + The ``initialize_tutorial_db`` command is not performing a migration but + rather simply creating missing tables and adding some dummy data. If you + already have a database, you should delete it before running + ``initialize_tutorial_db`` again. + Type the following command, making sure you are still in the ``tutorial`` directory (the directory with a ``development.ini`` in it): diff --git a/docs/tutorials/wiki2/src/models/MANIFEST.in b/docs/tutorials/wiki2/src/models/MANIFEST.in index 81beba1b1..42cd299b5 100644 --- a/docs/tutorials/wiki2/src/models/MANIFEST.in +++ b/docs/tutorials/wiki2/src/models/MANIFEST.in @@ -1,2 +1,2 @@ include *.txt *.ini *.cfg *.rst -recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml +recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.jinja2 *.pt *.txt *.mak *.mako *.js *.html *.xml diff --git a/docs/tutorials/wiki2/src/models/production.ini b/docs/tutorials/wiki2/src/models/production.ini index 97acfbd7d..cb1db3211 100644 --- a/docs/tutorials/wiki2/src/models/production.ini +++ b/docs/tutorials/wiki2/src/models/production.ini @@ -11,8 +11,6 @@ pyramid.debug_authorization = false pyramid.debug_notfound = false pyramid.debug_routematch = false pyramid.default_locale_name = en -pyramid.includes = - pyramid_tm sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite diff --git a/docs/tutorials/wiki2/src/models/tutorial/__init__.py b/docs/tutorials/wiki2/src/models/tutorial/__init__.py index 7994bbfa8..17763812a 100644 --- a/docs/tutorials/wiki2/src/models/tutorial/__init__.py +++ b/docs/tutorials/wiki2/src/models/tutorial/__init__.py @@ -6,7 +6,7 @@ def main(global_config, **settings): """ config = Configurator(settings=settings) config.include('pyramid_jinja2') - config.include('.models.meta') + config.include('.models') config.add_static_view('static', 'static', cache_max_age=3600) config.add_route('home', '/') config.scan() diff --git a/docs/tutorials/wiki2/src/models/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/models/tutorial/models/__init__.py index 7b1c62867..4810c357a 100644 --- a/docs/tutorials/wiki2/src/models/tutorial/models/__init__.py +++ b/docs/tutorials/wiki2/src/models/tutorial/models/__init__.py @@ -1,7 +1,72 @@ +from sqlalchemy import engine_from_config +from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import configure_mappers -# import all models classes here for sqlalchemy mappers -# to pick up +import zope.sqlalchemy + +# import or define all models here to ensure they are attached to the +# Base.metadata prior to any initialization routines from .mymodel import Page # flake8: noqa -# run configure mappers to ensure we avoid any race conditions +# run configure_mappers after defining all of the models to ensure +# all relationships can be setup configure_mappers() + + +def get_engine(settings, prefix='sqlalchemy.'): + return engine_from_config(settings, prefix) + + +def get_session_factory(engine): + factory = sessionmaker() + factory.configure(bind=engine) + return factory + + +def get_tm_session(session_factory, transaction_manager): + """ + Get a ``sqlalchemy.orm.Session`` instance backed by a transaction. + + This function will hook the session to the transaction manager which + will take care of committing any changes. + + - When using pyramid_tm it will automatically be committed or aborted + depending on whether an exception is raised. + + - When using scripts you should wrap the session in a manager yourself. + For example:: + + import transaction + + engine = get_engine(settings) + session_factory = get_session_factory(engine) + with transaction.manager: + dbsession = get_tm_session(session_factory, transaction.manager) + + """ + dbsession = session_factory() + zope.sqlalchemy.register( + dbsession, transaction_manager=transaction_manager) + return dbsession + + +def includeme(config): + """ + Initialize the model for a Pyramid app. + + Activate this setup using ``config.include('tutorial.models')``. + + """ + settings = config.get_settings() + + # use pyramid_tm to hook the transaction lifecycle to the request + config.include('pyramid_tm') + + session_factory = get_session_factory(get_engine(settings)) + + # make request.dbsession available for use in Pyramid + config.add_request_method( + # r.tm is the transaction manager used by pyramid_tm + lambda r: get_tm_session(session_factory, r.tm), + 'dbsession', + reify=True + ) diff --git a/docs/tutorials/wiki2/src/models/tutorial/models/meta.py b/docs/tutorials/wiki2/src/models/tutorial/models/meta.py index 80ececd8c..fc3e8f1dd 100644 --- a/docs/tutorials/wiki2/src/models/tutorial/models/meta.py +++ b/docs/tutorials/wiki2/src/models/tutorial/models/meta.py @@ -1,8 +1,5 @@ -from sqlalchemy import engine_from_config from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import sessionmaker from sqlalchemy.schema import MetaData -import zope.sqlalchemy # Recommended naming convention used by Alembic, as various different database # providers will autogenerate vastly different names making migrations more @@ -17,33 +14,3 @@ NAMING_CONVENTION = { metadata = MetaData(naming_convention=NAMING_CONVENTION) Base = declarative_base(metadata=metadata) - - -def includeme(config): - settings = config.get_settings() - dbmaker = get_dbmaker(get_engine(settings)) - - config.add_request_method( - lambda r: get_session(r.tm, dbmaker), - 'dbsession', - reify=True - ) - - config.include('pyramid_tm') - - -def get_session(transaction_manager, dbmaker): - dbsession = dbmaker() - zope.sqlalchemy.register(dbsession, - transaction_manager=transaction_manager) - return dbsession - - -def get_engine(settings, prefix='sqlalchemy.'): - return engine_from_config(settings, prefix) - - -def get_dbmaker(engine): - dbmaker = sessionmaker() - dbmaker.configure(bind=engine) - return dbmaker diff --git a/docs/tutorials/wiki2/src/models/tutorial/models/mymodel.py b/docs/tutorials/wiki2/src/models/tutorial/models/mymodel.py index 45571d78e..b23d0c0d2 100644 --- a/docs/tutorials/wiki2/src/models/tutorial/models/mymodel.py +++ b/docs/tutorials/wiki2/src/models/tutorial/models/mymodel.py @@ -1,10 +1,11 @@ -from .meta import Base from sqlalchemy import ( Column, Integer, Text, ) +from .meta import Base + class Page(Base): """ The SQLAlchemy declarative model class for a Page object. """ diff --git a/docs/tutorials/wiki2/src/models/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/models/tutorial/scripts/initializedb.py index 4aac4a848..601a6e73f 100644 --- a/docs/tutorials/wiki2/src/models/tutorial/scripts/initializedb.py +++ b/docs/tutorials/wiki2/src/models/tutorial/scripts/initializedb.py @@ -7,13 +7,15 @@ from pyramid.paster import ( setup_logging, ) -from ..models.meta import ( - Base, - get_session, +from pyramid.scripts.common import parse_vars + +from ..models.meta import Base +from ..models import ( get_engine, - get_dbmaker, + get_session_factory, + get_tm_session, ) -from ..models.mymodel import Page +from ..models import Page def usage(argv): @@ -27,16 +29,17 @@ def main(argv=sys.argv): if len(argv) < 2: usage(argv) config_uri = argv[1] + options = parse_vars(argv[2:]) setup_logging(config_uri) - settings = get_appsettings(config_uri) + settings = get_appsettings(config_uri, options=options) engine = get_engine(settings) - dbmaker = get_dbmaker(engine) - - dbsession = get_session(transaction.manager, dbmaker) - Base.metadata.create_all(engine) + session_factory = get_session_factory(engine) + with transaction.manager: - model = Page(name='FrontPage', data='This is the front page') - dbsession.add(model) + dbsession = get_tm_session(session_factory, transaction.manager) + + page = Page(name='FrontPage', data='This is the front page') + dbsession.add(page) diff --git a/docs/tutorials/wiki2/src/models/tutorial/templates/404.jinja2 b/docs/tutorials/wiki2/src/models/tutorial/templates/404.jinja2 new file mode 100644 index 000000000..1917f83c7 --- /dev/null +++ b/docs/tutorials/wiki2/src/models/tutorial/templates/404.jinja2 @@ -0,0 +1,8 @@ +{% extends "layout.jinja2" %} + +{% block content %} +
+

Pyramid Alchemy scaffold

+

404 Page Not Found

+
+{% endblock content %} diff --git a/docs/tutorials/wiki2/src/models/tutorial/tests.py b/docs/tutorials/wiki2/src/models/tutorial/tests.py index b947e3bb1..c54945c28 100644 --- a/docs/tutorials/wiki2/src/models/tutorial/tests.py +++ b/docs/tutorials/wiki2/src/models/tutorial/tests.py @@ -13,22 +13,22 @@ class BaseTest(unittest.TestCase): self.config = testing.setUp(settings={ 'sqlalchemy.url': 'sqlite:///:memory:' }) - self.config.include('.models.meta') + self.config.include('.models') settings = self.config.get_settings() - from .models.meta import ( - get_session, + from .models import ( get_engine, - get_dbmaker, + get_session_factory, + get_tm_session, ) self.engine = get_engine(settings) - dbmaker = get_dbmaker(self.engine) + session_factory = get_session_factory(self.engine) - self.session = get_session(transaction.manager, dbmaker) + self.session = get_tm_session(session_factory, transaction.manager) def init_database(self): - from .models.meta import Base + from .models import Base Base.metadata.create_all(self.engine) def tearDown(self): @@ -36,7 +36,7 @@ class BaseTest(unittest.TestCase): testing.tearDown() transaction.abort() - Base.metadata.create_all(self.engine) + Base.metadata.drop_all(self.engine) class TestMyViewSuccessCondition(BaseTest): @@ -45,7 +45,7 @@ class TestMyViewSuccessCondition(BaseTest): super(TestMyViewSuccessCondition, self).setUp() self.init_database() - from .models.mymodel import MyModel + from .models import MyModel model = MyModel(name='one', value=55) self.session.add(model) diff --git a/docs/tutorials/wiki2/src/models/tutorial/views/default.py b/docs/tutorials/wiki2/src/models/tutorial/views/default.py index 13ad8793c..ad0c728d7 100644 --- a/docs/tutorials/wiki2/src/models/tutorial/views/default.py +++ b/docs/tutorials/wiki2/src/models/tutorial/views/default.py @@ -3,7 +3,7 @@ from pyramid.view import view_config from sqlalchemy.exc import DBAPIError -from ..models.mymodel import MyModel +from ..models import MyModel @view_config(route_name='home', renderer='../templates/mytemplate.jinja2') @@ -12,7 +12,7 @@ def my_view(request): query = request.dbsession.query(MyModel) one = query.filter(MyModel.name == 'one').first() except DBAPIError: - return Response(db_err_msg, content_type='text/plain', status_int=500) + return Response(db_err_msg, content_type='text/plain', status=500) return {'one': one, 'project': 'tutorial'} diff --git a/docs/tutorials/wiki2/src/models/tutorial/views/errors.py b/docs/tutorials/wiki2/src/models/tutorial/views/errors.py new file mode 100644 index 000000000..a4b8201f1 --- /dev/null +++ b/docs/tutorials/wiki2/src/models/tutorial/views/errors.py @@ -0,0 +1,5 @@ +from pyramid.view import notfound_view_config + +@notfound_view_config(renderer='../templates/404.jinja2') +def notfound_view(request): + return {} -- cgit v1.2.3 From 4b23c9f1344a359214455668741b52c3db8cf6ea Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 7 Feb 2016 22:08:52 -0600 Subject: update definingviews chapter of wiki2 tutorial --- docs/tutorials/wiki2/definingviews.rst | 29 +++++---- docs/tutorials/wiki2/src/views/MANIFEST.in | 2 +- docs/tutorials/wiki2/src/views/production.ini | 2 - .../tutorials/wiki2/src/views/tutorial/__init__.py | 2 +- .../wiki2/src/views/tutorial/models/__init__.py | 71 +++++++++++++++++++++- .../wiki2/src/views/tutorial/models/meta.py | 33 ---------- .../wiki2/src/views/tutorial/models/mymodel.py | 3 +- .../src/views/tutorial/scripts/initializedb.py | 27 ++++---- .../wiki2/src/views/tutorial/templates/404.jinja2 | 8 +++ .../wiki2/src/views/tutorial/templates/edit.jinja2 | 2 +- .../wiki2/src/views/tutorial/templates/view.jinja2 | 2 +- docs/tutorials/wiki2/src/views/tutorial/tests.py | 18 +++--- .../wiki2/src/views/tutorial/views/default.py | 23 ++++--- .../wiki2/src/views/tutorial/views/errors.py | 5 ++ 14 files changed, 138 insertions(+), 89 deletions(-) create mode 100644 docs/tutorials/wiki2/src/views/tutorial/templates/404.jinja2 create mode 100644 docs/tutorials/wiki2/src/views/tutorial/views/errors.py diff --git a/docs/tutorials/wiki2/definingviews.rst b/docs/tutorials/wiki2/definingviews.rst index 8660c2772..4bc7f461b 100644 --- a/docs/tutorials/wiki2/definingviews.rst +++ b/docs/tutorials/wiki2/definingviews.rst @@ -96,11 +96,12 @@ We'll describe each one briefly in the following sections. .. note:: - There is nothing special about the filename ``default.py``. A project may - have many view callables throughout its codebase in arbitrarily named files. - Files implementing view callables often have ``view`` in their filenames (or + There is nothing special about the filename ``default.py`` exept that + it is a Python module. A project may have many view callables throughout + its codebase in arbitrarily named modules. + Modules implementing view callables often have ``view`` in their name (or may live in a Python subpackage of your application package named ``views``, - as in our case), but this is only by convention. + as in our case), but this is only by convention, not a requirement. The ``view_wiki`` view function ------------------------------- @@ -109,7 +110,7 @@ Following is the code for the ``view_wiki`` view function and its decorator: .. literalinclude:: src/views/tutorial/views/default.py :lines: 17-20 - :lineno-start: 17 + :lineno-match: :linenos: :language: python @@ -119,8 +120,8 @@ represents the path to our "FrontPage". The ``view_wiki`` view callable always redirects to the URL of a Page resource named "FrontPage". To do so, it returns an instance of the -:class:`pyramid.httpexceptions.HTTPFound` class (instances of which implement -the :class:`pyramid.interfaces.IResponse` interface, like +:class:`pyramid.httpexceptions.HTTPFound` class (instances of which +implement the :class:`pyramid.interfaces.IResponse` interface, like :class:`pyramid.response.Response` does). It uses the :meth:`pyramid.request.Request.route_url` API to construct an URL to the ``FrontPage`` page (i.e., ``http://localhost:6543/FrontPage``), and uses it as @@ -133,7 +134,7 @@ Here is the code for the ``view_page`` view function and its decorator: .. literalinclude:: src/views/tutorial/views/default.py :lines: 22-42 - :lineno-start: 22 + :lineno-match: :linenos: :language: python @@ -159,7 +160,7 @@ template, and we return a dictionary with a number of arguments. The fact that ``view_page()`` returns a dictionary (as opposed to a :term:`response` object) is a cue to :app:`Pyramid` that it should try to use a :term:`renderer` associated with the view configuration to render a response. In our case, the -renderer used will be the ``templates/view.jinja2`` template, as indicated in +renderer used will be the ``view.jinja2`` template, as indicated in the ``@view_config`` decorator that is applied to ``view_page()``. The ``add_page`` view function @@ -169,7 +170,7 @@ Here is the code for the ``add_page`` view function and its decorator: .. literalinclude:: src/views/tutorial/views/default.py :lines: 44-55 - :lineno-start: 44 + :lineno-match: :linenos: :language: python @@ -209,8 +210,8 @@ The ``edit_page`` view function Here is the code for the ``edit_page`` view function and its decorator: .. literalinclude:: src/views/tutorial/views/default.py - :lines: 57-69 - :lineno-start: 57 + :lines: 57-68 + :lineno-match: :linenos: :language: python @@ -281,7 +282,9 @@ editing a wiki page. It displays a page containing a form that includes: The form POSTs back to the ``save_url`` argument supplied by the view (line 42). The view will use the ``body`` and ``form.submitted`` values. -.. note:: Our templates use a ``request`` object that none of our tutorial +.. note:: + + Our templates use a ``request`` object that none of our tutorial views return in their dictionary. ``request`` is one of several names that are available "by default" in a template when a template renderer is used. See :ref:`renderer_system_values` for information about other names that diff --git a/docs/tutorials/wiki2/src/views/MANIFEST.in b/docs/tutorials/wiki2/src/views/MANIFEST.in index 81beba1b1..42cd299b5 100644 --- a/docs/tutorials/wiki2/src/views/MANIFEST.in +++ b/docs/tutorials/wiki2/src/views/MANIFEST.in @@ -1,2 +1,2 @@ include *.txt *.ini *.cfg *.rst -recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml +recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.jinja2 *.pt *.txt *.mak *.mako *.js *.html *.xml diff --git a/docs/tutorials/wiki2/src/views/production.ini b/docs/tutorials/wiki2/src/views/production.ini index 97acfbd7d..cb1db3211 100644 --- a/docs/tutorials/wiki2/src/views/production.ini +++ b/docs/tutorials/wiki2/src/views/production.ini @@ -11,8 +11,6 @@ pyramid.debug_authorization = false pyramid.debug_notfound = false pyramid.debug_routematch = false pyramid.default_locale_name = en -pyramid.includes = - pyramid_tm sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite diff --git a/docs/tutorials/wiki2/src/views/tutorial/__init__.py b/docs/tutorials/wiki2/src/views/tutorial/__init__.py index d28f09ca4..5d8c7fba2 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/__init__.py +++ b/docs/tutorials/wiki2/src/views/tutorial/__init__.py @@ -6,7 +6,7 @@ def main(global_config, **settings): """ config = Configurator(settings=settings) config.include('pyramid_jinja2') - config.include('.models.meta') + config.include('.models') config.add_static_view('static', 'static', cache_max_age=3600) config.add_route('view_wiki', '/') config.add_route('view_page', '/{pagename}') diff --git a/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py index 7b1c62867..4810c357a 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py +++ b/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py @@ -1,7 +1,72 @@ +from sqlalchemy import engine_from_config +from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import configure_mappers -# import all models classes here for sqlalchemy mappers -# to pick up +import zope.sqlalchemy + +# import or define all models here to ensure they are attached to the +# Base.metadata prior to any initialization routines from .mymodel import Page # flake8: noqa -# run configure mappers to ensure we avoid any race conditions +# run configure_mappers after defining all of the models to ensure +# all relationships can be setup configure_mappers() + + +def get_engine(settings, prefix='sqlalchemy.'): + return engine_from_config(settings, prefix) + + +def get_session_factory(engine): + factory = sessionmaker() + factory.configure(bind=engine) + return factory + + +def get_tm_session(session_factory, transaction_manager): + """ + Get a ``sqlalchemy.orm.Session`` instance backed by a transaction. + + This function will hook the session to the transaction manager which + will take care of committing any changes. + + - When using pyramid_tm it will automatically be committed or aborted + depending on whether an exception is raised. + + - When using scripts you should wrap the session in a manager yourself. + For example:: + + import transaction + + engine = get_engine(settings) + session_factory = get_session_factory(engine) + with transaction.manager: + dbsession = get_tm_session(session_factory, transaction.manager) + + """ + dbsession = session_factory() + zope.sqlalchemy.register( + dbsession, transaction_manager=transaction_manager) + return dbsession + + +def includeme(config): + """ + Initialize the model for a Pyramid app. + + Activate this setup using ``config.include('tutorial.models')``. + + """ + settings = config.get_settings() + + # use pyramid_tm to hook the transaction lifecycle to the request + config.include('pyramid_tm') + + session_factory = get_session_factory(get_engine(settings)) + + # make request.dbsession available for use in Pyramid + config.add_request_method( + # r.tm is the transaction manager used by pyramid_tm + lambda r: get_tm_session(session_factory, r.tm), + 'dbsession', + reify=True + ) diff --git a/docs/tutorials/wiki2/src/views/tutorial/models/meta.py b/docs/tutorials/wiki2/src/views/tutorial/models/meta.py index 80ececd8c..fc3e8f1dd 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/models/meta.py +++ b/docs/tutorials/wiki2/src/views/tutorial/models/meta.py @@ -1,8 +1,5 @@ -from sqlalchemy import engine_from_config from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import sessionmaker from sqlalchemy.schema import MetaData -import zope.sqlalchemy # Recommended naming convention used by Alembic, as various different database # providers will autogenerate vastly different names making migrations more @@ -17,33 +14,3 @@ NAMING_CONVENTION = { metadata = MetaData(naming_convention=NAMING_CONVENTION) Base = declarative_base(metadata=metadata) - - -def includeme(config): - settings = config.get_settings() - dbmaker = get_dbmaker(get_engine(settings)) - - config.add_request_method( - lambda r: get_session(r.tm, dbmaker), - 'dbsession', - reify=True - ) - - config.include('pyramid_tm') - - -def get_session(transaction_manager, dbmaker): - dbsession = dbmaker() - zope.sqlalchemy.register(dbsession, - transaction_manager=transaction_manager) - return dbsession - - -def get_engine(settings, prefix='sqlalchemy.'): - return engine_from_config(settings, prefix) - - -def get_dbmaker(engine): - dbmaker = sessionmaker() - dbmaker.configure(bind=engine) - return dbmaker diff --git a/docs/tutorials/wiki2/src/views/tutorial/models/mymodel.py b/docs/tutorials/wiki2/src/views/tutorial/models/mymodel.py index 45571d78e..b23d0c0d2 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/models/mymodel.py +++ b/docs/tutorials/wiki2/src/views/tutorial/models/mymodel.py @@ -1,10 +1,11 @@ -from .meta import Base from sqlalchemy import ( Column, Integer, Text, ) +from .meta import Base + class Page(Base): """ The SQLAlchemy declarative model class for a Page object. """ diff --git a/docs/tutorials/wiki2/src/views/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/views/tutorial/scripts/initializedb.py index 4aac4a848..601a6e73f 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/scripts/initializedb.py +++ b/docs/tutorials/wiki2/src/views/tutorial/scripts/initializedb.py @@ -7,13 +7,15 @@ from pyramid.paster import ( setup_logging, ) -from ..models.meta import ( - Base, - get_session, +from pyramid.scripts.common import parse_vars + +from ..models.meta import Base +from ..models import ( get_engine, - get_dbmaker, + get_session_factory, + get_tm_session, ) -from ..models.mymodel import Page +from ..models import Page def usage(argv): @@ -27,16 +29,17 @@ def main(argv=sys.argv): if len(argv) < 2: usage(argv) config_uri = argv[1] + options = parse_vars(argv[2:]) setup_logging(config_uri) - settings = get_appsettings(config_uri) + settings = get_appsettings(config_uri, options=options) engine = get_engine(settings) - dbmaker = get_dbmaker(engine) - - dbsession = get_session(transaction.manager, dbmaker) - Base.metadata.create_all(engine) + session_factory = get_session_factory(engine) + with transaction.manager: - model = Page(name='FrontPage', data='This is the front page') - dbsession.add(model) + dbsession = get_tm_session(session_factory, transaction.manager) + + page = Page(name='FrontPage', data='This is the front page') + dbsession.add(page) diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/404.jinja2 b/docs/tutorials/wiki2/src/views/tutorial/templates/404.jinja2 new file mode 100644 index 000000000..1917f83c7 --- /dev/null +++ b/docs/tutorials/wiki2/src/views/tutorial/templates/404.jinja2 @@ -0,0 +1,8 @@ +{% extends "layout.jinja2" %} + +{% block content %} +
+

Pyramid Alchemy scaffold

+

404 Page Not Found

+
+{% endblock content %} diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/edit.jinja2 b/docs/tutorials/wiki2/src/views/tutorial/templates/edit.jinja2 index b3aadfc2e..a41d232e5 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/templates/edit.jinja2 +++ b/docs/tutorials/wiki2/src/views/tutorial/templates/edit.jinja2 @@ -37,7 +37,7 @@ Editing {% if page.name %}{{page.name}}{% else %}Page Name Goes Here{% endif %}

You can return to the - FrontPage. + FrontPage.

diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/view.jinja2 b/docs/tutorials/wiki2/src/views/tutorial/templates/view.jinja2 index 36bb96870..fa09baf70 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/templates/view.jinja2 +++ b/docs/tutorials/wiki2/src/views/tutorial/templates/view.jinja2 @@ -43,7 +43,7 @@ Viewing {% if page.name %}{{page.name}}{% else %}Page Name Goes Here{% endif %}

You can return to the - FrontPage. + FrontPage.

diff --git a/docs/tutorials/wiki2/src/views/tutorial/tests.py b/docs/tutorials/wiki2/src/views/tutorial/tests.py index b947e3bb1..c54945c28 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/tests.py +++ b/docs/tutorials/wiki2/src/views/tutorial/tests.py @@ -13,22 +13,22 @@ class BaseTest(unittest.TestCase): self.config = testing.setUp(settings={ 'sqlalchemy.url': 'sqlite:///:memory:' }) - self.config.include('.models.meta') + self.config.include('.models') settings = self.config.get_settings() - from .models.meta import ( - get_session, + from .models import ( get_engine, - get_dbmaker, + get_session_factory, + get_tm_session, ) self.engine = get_engine(settings) - dbmaker = get_dbmaker(self.engine) + session_factory = get_session_factory(self.engine) - self.session = get_session(transaction.manager, dbmaker) + self.session = get_tm_session(session_factory, transaction.manager) def init_database(self): - from .models.meta import Base + from .models import Base Base.metadata.create_all(self.engine) def tearDown(self): @@ -36,7 +36,7 @@ class BaseTest(unittest.TestCase): testing.tearDown() transaction.abort() - Base.metadata.create_all(self.engine) + Base.metadata.drop_all(self.engine) class TestMyViewSuccessCondition(BaseTest): @@ -45,7 +45,7 @@ class TestMyViewSuccessCondition(BaseTest): super(TestMyViewSuccessCondition, self).setUp() self.init_database() - from .models.mymodel import MyModel + from .models import MyModel model = MyModel(name='one', value=55) self.session.add(model) diff --git a/docs/tutorials/wiki2/src/views/tutorial/views/default.py b/docs/tutorials/wiki2/src/views/tutorial/views/default.py index 3e5c61a72..96df85a97 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/views/default.py +++ b/docs/tutorials/wiki2/src/views/tutorial/views/default.py @@ -9,17 +9,17 @@ from pyramid.httpexceptions import ( from pyramid.view import view_config -from ..models.mymodel import Page +from ..models import Page # regular expression used to find WikiWords wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)") @view_config(route_name='view_wiki') def view_wiki(request): - return HTTPFound(location=request.route_url('view_page', - pagename='FrontPage')) + next_url = request.route_url('view_page', pagename='FrontPage') + return HTTPFound(location=next_url) -@view_config(route_name='view_page', renderer='templates/view.jinja2') +@view_config(route_name='view_page', renderer='../templates/view.jinja2') def view_page(request): pagename = request.matchdict['pagename'] page = request.dbsession.query(Page).filter_by(name=pagename).first() @@ -41,29 +41,28 @@ def view_page(request): edit_url = request.route_url('edit_page', pagename=pagename) return dict(page=page, content=content, edit_url=edit_url) -@view_config(route_name='add_page', renderer='templates/edit.jinja2') +@view_config(route_name='add_page', renderer='../templates/edit.jinja2') def add_page(request): pagename = request.matchdict['pagename'] if 'form.submitted' in request.params: body = request.params['body'] page = Page(name=pagename, data=body) request.dbsession.add(page) - return HTTPFound(location = request.route_url('view_page', - pagename=pagename)) + next_url = request.route_url('view_page', pagename=pagename) + return HTTPFound(location=next_url) save_url = request.route_url('add_page', pagename=pagename) page = Page(name='', data='') return dict(page=page, save_url=save_url) -@view_config(route_name='edit_page', renderer='templates/edit.jinja2') +@view_config(route_name='edit_page', renderer='../templates/edit.jinja2') def edit_page(request): pagename = request.matchdict['pagename'] page = request.dbsession.query(Page).filter_by(name=pagename).one() if 'form.submitted' in request.params: page.data = request.params['body'] - request.dbsession.add(page) - return HTTPFound(location = request.route_url('view_page', - pagename=pagename)) + next_url = request.route_url('view_page', pagename=pagename) + return HTTPFound(location=next_url) return dict( page=page, - save_url = request.route_url('edit_page', pagename=pagename), + save_url=request.route_url('edit_page', pagename=pagename), ) diff --git a/docs/tutorials/wiki2/src/views/tutorial/views/errors.py b/docs/tutorials/wiki2/src/views/tutorial/views/errors.py new file mode 100644 index 000000000..a4b8201f1 --- /dev/null +++ b/docs/tutorials/wiki2/src/views/tutorial/views/errors.py @@ -0,0 +1,5 @@ +from pyramid.view import notfound_view_config + +@notfound_view_config(renderer='../templates/404.jinja2') +def notfound_view(request): + return {} -- cgit v1.2.3 From 14cff75aca9c2858d0575d8e6beba9758eb012d6 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 7 Feb 2016 23:39:33 -0600 Subject: update authorization chapter of wiki2 tutorial --- docs/tutorials/wiki2/authorization.rst | 115 +++++++-------------- docs/tutorials/wiki2/src/authorization/MANIFEST.in | 2 +- .../wiki2/src/authorization/production.ini | 2 - .../wiki2/src/authorization/tutorial/__init__.py | 11 +- .../src/authorization/tutorial/models/__init__.py | 71 ++++++++++++- .../src/authorization/tutorial/models/meta.py | 33 ------ .../src/authorization/tutorial/models/mymodel.py | 19 ++-- .../authorization/tutorial/scripts/initializedb.py | 27 ++--- .../authorization/tutorial/security/__init__.py | 1 - .../src/authorization/tutorial/security/default.py | 11 +- .../authorization/tutorial/templates/404.jinja2 | 8 ++ .../authorization/tutorial/templates/edit.jinja2 | 6 +- .../authorization/tutorial/templates/view.jinja2 | 6 +- .../wiki2/src/authorization/tutorial/tests.py | 18 ++-- .../src/authorization/tutorial/views/default.py | 54 +++++----- .../src/authorization/tutorial/views/errors.py | 5 + 16 files changed, 200 insertions(+), 189 deletions(-) create mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/templates/404.jinja2 create mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/views/errors.py diff --git a/docs/tutorials/wiki2/authorization.rst b/docs/tutorials/wiki2/authorization.rst index e40433497..1ee5cc714 100644 --- a/docs/tutorials/wiki2/authorization.rst +++ b/docs/tutorials/wiki2/authorization.rst @@ -42,7 +42,7 @@ Access control Add users and groups ~~~~~~~~~~~~~~~~~~~~ -Create a new ``tutorial/tutorial/security/default.py`` subpackage with the +Create a new ``tutorial/security/default.py`` subpackage with the following content: .. literalinclude:: src/authorization/tutorial/security/default.py @@ -68,21 +68,17 @@ database, but here we use "dummy" data to represent user and groups sources. Add an ACL ~~~~~~~~~~ -Open ``tutorial/tutorial/models/mymodel.py`` and add the following import -statement just after the ``Base`` import at the top: +Open ``tutorial/models/mymodel.py`` and add the following import +statement at the top: .. literalinclude:: src/authorization/tutorial/models/mymodel.py - :lines: 3-6 - :linenos: - :lineno-start: 3 + :lines: 1-4 :language: python Add the following class definition at the end: .. literalinclude:: src/authorization/tutorial/models/mymodel.py - :lines: 22-26 - :linenos: - :lineno-start: 22 + :lines: 22-29 :language: python We import :data:`~pyramid.security.Allow`, an action that means that @@ -100,13 +96,13 @@ need to associate it to our :app:`Pyramid` application, so the ACL is provided to each view in the :term:`context` of the request as the ``context`` attribute. -Open ``tutorial/tutorial/__init__.py`` and add a ``root_factory`` parameter to -our :term:`Configurator` constructor, that points to the class we created -above: +Open ``tutorial/__init__.py`` and define a new root factory using +:meth:`pyramid.config.Configurator.set_root_factory` using the class that we +created above: .. literalinclude:: src/authorization/tutorial/__init__.py - :lines: 13-14 - :emphasize-lines: 2 + :lines: 14-17 + :emphasize-lines: 17 :language: python Only the highlighted line needs to be added. @@ -122,22 +118,19 @@ for more information about what an :term:`ACL` represents. Add authentication and authorization policies ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Open ``tutorial/tutorial/__init__.py`` and add the highlighted import +Open ``tutorial/__init__.py`` and add the highlighted import statements: .. literalinclude:: src/authorization/tutorial/__init__.py :lines: 1-5 - :linenos: :emphasize-lines: 2-5 :language: python Now add those policies to the configuration: .. literalinclude:: src/authorization/tutorial/__init__.py - :lines: 7-16 - :linenos: - :lineno-start: 7 - :emphasize-lines: 4-6,9-10 + :lines: 11-19 + :emphasize-lines: 1-3,8-9 :language: python Only the highlighted lines need to be added. @@ -157,17 +150,17 @@ machinery represented by this policy; it is required. The ``callback`` is the Add permission declarations ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Open ``tutorial/tutorial/views/default.py`` and add a ``permission='view'`` +Open ``tutorial/views/default.py`` and add a ``permission='view'`` parameter to the ``@view_config`` decorator for ``view_wiki()`` and ``view_page()`` as follows: .. literalinclude:: src/authorization/tutorial/views/default.py - :lines: 27-29 - :emphasize-lines: 1-2 + :lines: 24-25 + :emphasize-lines: 1 :language: python .. literalinclude:: src/authorization/tutorial/views/default.py - :lines: 33-35 + :lines: 29-31 :emphasize-lines: 1-2 :language: python @@ -180,12 +173,12 @@ Add a ``permission='edit'`` parameter to the ``@view_config`` decorators for ``add_page()`` and ``edit_page()``: .. literalinclude:: src/authorization/tutorial/views/default.py - :lines: 57-59 + :lines: 52-54 :emphasize-lines: 1-2 :language: python .. literalinclude:: src/authorization/tutorial/views/default.py - :lines: 72-74 + :lines: 66-68 :emphasize-lines: 1-2 :language: python @@ -203,11 +196,11 @@ Login, logout Add routes for /login and /logout ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Go back to ``tutorial/tutorial/__init__.py`` and add these two routes as +Go back to ``tutorial/__init__.py`` and add these two routes as highlighted: .. literalinclude:: src/authorization/tutorial/__init__.py - :lines: 20-23 + :lines: 21-24 :emphasize-lines: 2-3 :language: python @@ -215,7 +208,7 @@ highlighted: ``view_page`` route definition: .. literalinclude:: src/authorization/tutorial/__init__.py - :lines: 23 + :lines: 24 :language: python This is because ``view_page``'s route definition uses a catch-all @@ -235,12 +228,12 @@ We'll also add a ``logout`` view callable to our application and provide a link to it. This view will clear the credentials of the logged in user and redirect back to the front page. -Add the following import statements to ``tutorial/tutorial/views/default.py`` +Add the following import statements to ``tutorial/views/default.py`` after the import from ``pyramid.httpexceptions``: .. literalinclude:: src/authorization/tutorial/views/default.py - :lines: 10-20 - :emphasize-lines: 1-11 + :lines: 9-19 + :emphasize-lines: 1-8,11 :language: python All the highlighted lines need to be added or edited. @@ -253,7 +246,7 @@ cookie. Now add the ``login`` and ``logout`` views at the end of the file: .. literalinclude:: src/authorization/tutorial/views/default.py - :lines: 88-121 + :lines: 81-112 :language: python ``login()`` has two decorators: @@ -274,7 +267,7 @@ it with the ``logout`` route. It will be invoked when we visit ``/logout``. Add the ``login.jinja2`` template ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Create ``tutorial/tutorial/templates/login.jinja2`` with the following content: +Create ``tutorial/templates/login.jinja2`` with the following content: .. literalinclude:: src/authorization/tutorial/templates/login.jinja2 :language: html @@ -282,38 +275,11 @@ Create ``tutorial/tutorial/templates/login.jinja2`` with the following content: The above template is referenced in the login view that we just added in ``views/default.py``. -Return a ``logged_in`` flag to the renderer -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Open ``tutorial/tutorial/views/default.py`` again. Add a ``logged_in`` -parameter to the return value of ``view_page()``, ``add_page()``, and -``edit_page()`` as follows: - -.. literalinclude:: src/authorization/tutorial/views/default.py - :lines: 54-55 - :emphasize-lines: 1-2 - :language: python - -.. literalinclude:: src/authorization/tutorial/views/default.py - :lines: 69-70 - :emphasize-lines: 1-2 - :language: python - -.. literalinclude:: src/authorization/tutorial/views/default.py - :lines: 82-86 - :emphasize-lines: 3-4 - :language: python - -Only the highlighted lines need to be added or edited. - -The :meth:`pyramid.request.Request.authenticated_userid` will be ``None`` if -the user is not authenticated, or a userid if the user is authenticated. - Add a "Logout" link when logged in ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Open ``tutorial/tutorial/templates/edit.jinja2`` and -``tutorial/tutorial/templates/view.jinja2`` and add the following code as +Open ``tutorial/templates/edit.jinja2`` and +``tutorial/templates/view.jinja2`` and add the following code as indicated by the highlighted lines. .. literalinclude:: src/authorization/tutorial/templates/edit.jinja2 @@ -321,42 +287,41 @@ indicated by the highlighted lines. :emphasize-lines: 3-7 :language: html -The attribute ``logged_in`` will make the element be included when -``logged_in`` is any user id. The link will invoke the logout view. The above -element will not be included if ``logged_in`` is ``None``, such as when a user -is not authenticated. +The :meth:`pyramid.request.Request.authenticated_userid` will be ``None`` if +the user is not authenticated, or a userid if the user is authenticated. This +check will make the logout link active only when the user is logged in. Reviewing our changes --------------------- -Our ``tutorial/tutorial/__init__.py`` will look like this when we're done: +Our ``tutorial/__init__.py`` will look like this when we're done: .. literalinclude:: src/authorization/tutorial/__init__.py :linenos: - :emphasize-lines: 2-3,5,10-12,14-16,21-22 + :emphasize-lines: 2-3,5,11-13,17-19,22-23 :language: python Only the highlighted lines need to be added or edited. -Our ``tutorial/tutorial/models/mymodel.py`` will look like this when we're done: +Our ``tutorial/models/mymodel.py`` will look like this when we're done: .. literalinclude:: src/authorization/tutorial/models/mymodel.py :linenos: - :emphasize-lines: 3-6,22-26 + :emphasize-lines: 1-4,22-29 :language: python Only the highlighted lines need to be added or edited. -Our ``tutorial/tutorial/views/default.py`` will look like this when we're done: +Our ``tutorial/views/default.py`` will look like this when we're done: .. literalinclude:: src/authorization/tutorial/views/default.py :linenos: - :emphasize-lines: 10-20,27-28,33-34,54-55,57-58,69-70,72-73,84-85,88-121 + :emphasize-lines: 9-16,19,24,29-30,52-53,66-67,81-112 :language: python Only the highlighted lines need to be added or edited. -Our ``tutorial/tutorial/templates/edit.jinja2`` template will look like this when +Our ``tutorial/templates/edit.jinja2`` template will look like this when we're done: .. literalinclude:: src/authorization/tutorial/templates/edit.jinja2 @@ -366,7 +331,7 @@ we're done: Only the highlighted lines need to be added or edited. -Our ``tutorial/tutorial/templates/view.jinja2`` template will look like this when +Our ``tutorial/templates/view.jinja2`` template will look like this when we're done: .. literalinclude:: src/authorization/tutorial/templates/view.jinja2 diff --git a/docs/tutorials/wiki2/src/authorization/MANIFEST.in b/docs/tutorials/wiki2/src/authorization/MANIFEST.in index 81beba1b1..42cd299b5 100644 --- a/docs/tutorials/wiki2/src/authorization/MANIFEST.in +++ b/docs/tutorials/wiki2/src/authorization/MANIFEST.in @@ -1,2 +1,2 @@ include *.txt *.ini *.cfg *.rst -recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml +recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.jinja2 *.pt *.txt *.mak *.mako *.js *.html *.xml diff --git a/docs/tutorials/wiki2/src/authorization/production.ini b/docs/tutorials/wiki2/src/authorization/production.ini index 97acfbd7d..cb1db3211 100644 --- a/docs/tutorials/wiki2/src/authorization/production.ini +++ b/docs/tutorials/wiki2/src/authorization/production.ini @@ -11,8 +11,6 @@ pyramid.debug_authorization = false pyramid.debug_notfound = false pyramid.debug_routematch = false pyramid.default_locale_name = en -pyramid.includes = - pyramid_tm sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py b/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py index 084fee19f..a62c42378 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py @@ -2,7 +2,8 @@ from pyramid.config import Configurator from pyramid.authentication import AuthTktAuthenticationPolicy from pyramid.authorization import ACLAuthorizationPolicy -from security.default import groupfinder +from .security.default import groupfinder + def main(global_config, **settings): """ This function returns a Pyramid WSGI application. @@ -10,12 +11,12 @@ def main(global_config, **settings): authn_policy = AuthTktAuthenticationPolicy( 'sosecret', callback=groupfinder, hashalg='sha512') authz_policy = ACLAuthorizationPolicy() - config = Configurator(settings=settings, - root_factory='tutorial.models.mymodel.RootFactory') + config = Configurator(settings=settings) + config.include('pyramid_jinja2') + config.include('.models') + config.set_root_factory('.models.mymodel.RootFactory') config.set_authentication_policy(authn_policy) config.set_authorization_policy(authz_policy) - config.include('pyramid_jinja2') - config.include('.models.meta') config.add_static_view('static', 'static', cache_max_age=3600) config.add_route('view_wiki', '/') config.add_route('login', '/login') diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py index 7b1c62867..4810c357a 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py @@ -1,7 +1,72 @@ +from sqlalchemy import engine_from_config +from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import configure_mappers -# import all models classes here for sqlalchemy mappers -# to pick up +import zope.sqlalchemy + +# import or define all models here to ensure they are attached to the +# Base.metadata prior to any initialization routines from .mymodel import Page # flake8: noqa -# run configure mappers to ensure we avoid any race conditions +# run configure_mappers after defining all of the models to ensure +# all relationships can be setup configure_mappers() + + +def get_engine(settings, prefix='sqlalchemy.'): + return engine_from_config(settings, prefix) + + +def get_session_factory(engine): + factory = sessionmaker() + factory.configure(bind=engine) + return factory + + +def get_tm_session(session_factory, transaction_manager): + """ + Get a ``sqlalchemy.orm.Session`` instance backed by a transaction. + + This function will hook the session to the transaction manager which + will take care of committing any changes. + + - When using pyramid_tm it will automatically be committed or aborted + depending on whether an exception is raised. + + - When using scripts you should wrap the session in a manager yourself. + For example:: + + import transaction + + engine = get_engine(settings) + session_factory = get_session_factory(engine) + with transaction.manager: + dbsession = get_tm_session(session_factory, transaction.manager) + + """ + dbsession = session_factory() + zope.sqlalchemy.register( + dbsession, transaction_manager=transaction_manager) + return dbsession + + +def includeme(config): + """ + Initialize the model for a Pyramid app. + + Activate this setup using ``config.include('tutorial.models')``. + + """ + settings = config.get_settings() + + # use pyramid_tm to hook the transaction lifecycle to the request + config.include('pyramid_tm') + + session_factory = get_session_factory(get_engine(settings)) + + # make request.dbsession available for use in Pyramid + config.add_request_method( + # r.tm is the transaction manager used by pyramid_tm + lambda r: get_tm_session(session_factory, r.tm), + 'dbsession', + reify=True + ) diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models/meta.py b/docs/tutorials/wiki2/src/authorization/tutorial/models/meta.py index 80ececd8c..fc3e8f1dd 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/models/meta.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/models/meta.py @@ -1,8 +1,5 @@ -from sqlalchemy import engine_from_config from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import sessionmaker from sqlalchemy.schema import MetaData -import zope.sqlalchemy # Recommended naming convention used by Alembic, as various different database # providers will autogenerate vastly different names making migrations more @@ -17,33 +14,3 @@ NAMING_CONVENTION = { metadata = MetaData(naming_convention=NAMING_CONVENTION) Base = declarative_base(metadata=metadata) - - -def includeme(config): - settings = config.get_settings() - dbmaker = get_dbmaker(get_engine(settings)) - - config.add_request_method( - lambda r: get_session(r.tm, dbmaker), - 'dbsession', - reify=True - ) - - config.include('pyramid_tm') - - -def get_session(transaction_manager, dbmaker): - dbsession = dbmaker() - zope.sqlalchemy.register(dbsession, - transaction_manager=transaction_manager) - return dbsession - - -def get_engine(settings, prefix='sqlalchemy.'): - return engine_from_config(settings, prefix) - - -def get_dbmaker(engine): - dbmaker = sessionmaker() - dbmaker.configure(bind=engine) - return dbmaker diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models/mymodel.py b/docs/tutorials/wiki2/src/authorization/tutorial/models/mymodel.py index 03e2f90ca..25209c745 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/models/mymodel.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/models/mymodel.py @@ -1,15 +1,14 @@ -from .meta import Base - from pyramid.security import ( Allow, Everyone, - ) - +) from sqlalchemy import ( Column, Integer, Text, - ) +) + +from .meta import Base class Page(Base): @@ -19,8 +18,12 @@ class Page(Base): name = Column(Text, unique=True) data = Column(Integer) + class RootFactory(object): - __acl__ = [ (Allow, Everyone, 'view'), - (Allow, 'group:editors', 'edit') ] + __acl__ = [ + (Allow, Everyone, 'view'), + (Allow, 'group:editors', 'edit'), + ] + def __init__(self, request): - pass \ No newline at end of file + pass diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/authorization/tutorial/scripts/initializedb.py index 4aac4a848..601a6e73f 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/scripts/initializedb.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/scripts/initializedb.py @@ -7,13 +7,15 @@ from pyramid.paster import ( setup_logging, ) -from ..models.meta import ( - Base, - get_session, +from pyramid.scripts.common import parse_vars + +from ..models.meta import Base +from ..models import ( get_engine, - get_dbmaker, + get_session_factory, + get_tm_session, ) -from ..models.mymodel import Page +from ..models import Page def usage(argv): @@ -27,16 +29,17 @@ def main(argv=sys.argv): if len(argv) < 2: usage(argv) config_uri = argv[1] + options = parse_vars(argv[2:]) setup_logging(config_uri) - settings = get_appsettings(config_uri) + settings = get_appsettings(config_uri, options=options) engine = get_engine(settings) - dbmaker = get_dbmaker(engine) - - dbsession = get_session(transaction.manager, dbmaker) - Base.metadata.create_all(engine) + session_factory = get_session_factory(engine) + with transaction.manager: - model = Page(name='FrontPage', data='This is the front page') - dbsession.add(model) + dbsession = get_tm_session(session_factory, transaction.manager) + + page = Page(name='FrontPage', data='This is the front page') + dbsession.add(page) diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/security/__init__.py b/docs/tutorials/wiki2/src/authorization/tutorial/security/__init__.py index 5bb534f79..e69de29bb 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/security/__init__.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/security/__init__.py @@ -1 +0,0 @@ -# package diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/security/default.py b/docs/tutorials/wiki2/src/authorization/tutorial/security/default.py index d88c9c71f..7fc1ea7c8 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/security/default.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/security/default.py @@ -1,6 +1,11 @@ -USERS = {'editor':'editor', - 'viewer':'viewer'} -GROUPS = {'editor':['group:editors']} +USERS = { + 'editor': 'editor', + 'viewer': 'viewer', +} + +GROUPS = { + 'editor': ['group:editors'], +} def groupfinder(userid, request): if userid in USERS: diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/404.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/404.jinja2 new file mode 100644 index 000000000..1917f83c7 --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/404.jinja2 @@ -0,0 +1,8 @@ +{% extends "layout.jinja2" %} + +{% block content %} +
+

Pyramid Alchemy scaffold

+

404 Page Not Found

+
+{% endblock content %} diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja2 index c4f3a2c93..70ce49b73 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja2 +++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja2 @@ -33,16 +33,16 @@
- {% if logged_in %} + {% if request.authenticated_userid is not None %}

- Logout + Logout

{% endif %}

Editing {% if page.name %}{{page.name}}{% else %}Page Name Goes Here{% endif %}

You can return to the - FrontPage. + FrontPage.

diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.jinja2 index a7afc66fc..b12ca5b0c 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.jinja2 +++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.jinja2 @@ -33,9 +33,9 @@
- {% if logged_in %} + {% if request.authenticated_userid is not None %}

- Logout + Logout

{% endif %}

{{ content|safe }}

@@ -48,7 +48,7 @@ Viewing {% if page.name %}{{page.name}}{% else %}Page Name Goes Here{% endif %}

You can return to the - FrontPage. + FrontPage.

diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/tests.py b/docs/tutorials/wiki2/src/authorization/tutorial/tests.py index b947e3bb1..c54945c28 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/tests.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/tests.py @@ -13,22 +13,22 @@ class BaseTest(unittest.TestCase): self.config = testing.setUp(settings={ 'sqlalchemy.url': 'sqlite:///:memory:' }) - self.config.include('.models.meta') + self.config.include('.models') settings = self.config.get_settings() - from .models.meta import ( - get_session, + from .models import ( get_engine, - get_dbmaker, + get_session_factory, + get_tm_session, ) self.engine = get_engine(settings) - dbmaker = get_dbmaker(self.engine) + session_factory = get_session_factory(self.engine) - self.session = get_session(transaction.manager, dbmaker) + self.session = get_tm_session(session_factory, transaction.manager) def init_database(self): - from .models.meta import Base + from .models import Base Base.metadata.create_all(self.engine) def tearDown(self): @@ -36,7 +36,7 @@ class BaseTest(unittest.TestCase): testing.tearDown() transaction.abort() - Base.metadata.create_all(self.engine) + Base.metadata.drop_all(self.engine) class TestMyViewSuccessCondition(BaseTest): @@ -45,7 +45,7 @@ class TestMyViewSuccessCondition(BaseTest): super(TestMyViewSuccessCondition, self).setUp() self.init_database() - from .models.mymodel import MyModel + from .models import MyModel model = MyModel(name='one', value=55) self.session.add(model) diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py b/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py index f35f041a4..aa77facd7 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py @@ -6,31 +6,27 @@ from pyramid.httpexceptions import ( HTTPFound, HTTPNotFound, ) - from pyramid.view import ( view_config, forbidden_view_config, ) - from pyramid.security import ( remember, forget, ) +from ..models import Page from ..security.default import USERS -from ..models.mymodel import Page - # regular expression used to find WikiWords wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)") -@view_config(route_name='view_wiki', - permission='view') +@view_config(route_name='view_wiki', permission='view') def view_wiki(request): - return HTTPFound(location=request.route_url('view_page', - pagename='FrontPage')) + next_url = request.route_url('view_page', pagename='FrontPage') + return HTTPFound(location=next_url) -@view_config(route_name='view_page', renderer='templates/view.jinja2', +@view_config(route_name='view_page', renderer='../templates/view.jinja2', permission='view') def view_page(request): pagename = request.matchdict['pagename'] @@ -51,10 +47,9 @@ def view_page(request): content = publish_parts(page.data, writer_name='html')['html_body'] content = wikiwords.sub(check, content) edit_url = request.route_url('edit_page', pagename=pagename) - return dict(page=page, content=content, edit_url=edit_url, - logged_in=request.authenticated_userid) + return dict(page=page, content=content, edit_url=edit_url) -@view_config(route_name='add_page', renderer='templates/edit.jinja2', +@view_config(route_name='add_page', renderer='../templates/edit.jinja2', permission='edit') def add_page(request): pagename = request.matchdict['pagename'] @@ -62,29 +57,27 @@ def add_page(request): body = request.params['body'] page = Page(name=pagename, data=body) request.dbsession.add(page) - return HTTPFound(location = request.route_url('view_page', - pagename=pagename)) + next_url = request.route_url('view_page', pagename=pagename) + return HTTPFound(location=next_url) save_url = request.route_url('add_page', pagename=pagename) page = Page(name='', data='') - return dict(page=page, save_url=save_url, - logged_in=request.authenticated_userid) + return dict(page=page, save_url=save_url) -@view_config(route_name='edit_page', renderer='templates/edit.jinja2', +@view_config(route_name='edit_page', renderer='../templates/edit.jinja2', permission='edit') def edit_page(request): pagename = request.matchdict['pagename'] page = request.dbsession.query(Page).filter_by(name=pagename).one() if 'form.submitted' in request.params: page.data = request.params['body'] - request.dbsession.add(page) - return HTTPFound(location = request.route_url('view_page', - pagename=pagename)) + next_url = request.route_url('view_page', pagename=pagename) + return HTTPFound(location=next_url) return dict( page=page, - save_url = request.route_url('edit_page', pagename=pagename), - logged_in=request.authenticated_userid + save_url=request.route_url('edit_page', pagename=pagename), ) + @view_config(route_name='login', renderer='templates/login.jinja2') @forbidden_view_config(renderer='templates/login.jinja2') def login(request): @@ -101,20 +94,19 @@ def login(request): password = request.params['password'] if USERS.get(login) == password: headers = remember(request, login) - return HTTPFound(location = came_from, - headers = headers) + return HTTPFound(location=came_from, headers=headers) message = 'Failed login' return dict( - message = message, - url = request.application_url + '/login', - came_from = came_from, - login = login, - password = password, + message=message, + url=request.route_url('login'), + came_from=came_from, + login=login, + password=password, ) @view_config(route_name='logout') def logout(request): headers = forget(request) - return HTTPFound(location = request.route_url('view_wiki'), - headers = headers) + next_url = request.route_url('view_wiki') + return HTTPFound(location=next_url, headers=headers) diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views/errors.py b/docs/tutorials/wiki2/src/authorization/tutorial/views/errors.py new file mode 100644 index 000000000..a4b8201f1 --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/views/errors.py @@ -0,0 +1,5 @@ +from pyramid.view import notfound_view_config + +@notfound_view_config(renderer='../templates/404.jinja2') +def notfound_view(request): + return {} -- cgit v1.2.3 From 4337a25b30f53ad8f64babe5835a4d3d35f29a41 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 8 Feb 2016 00:00:23 -0600 Subject: minor fixes to wiki2 distributing chapter --- docs/tutorials/wiki2/distributing.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/tutorials/wiki2/distributing.rst b/docs/tutorials/wiki2/distributing.rst index fee50a1cf..ec90859a9 100644 --- a/docs/tutorials/wiki2/distributing.rst +++ b/docs/tutorials/wiki2/distributing.rst @@ -4,9 +4,8 @@ Distributing Your Application Once your application works properly, you can create a "tarball" from it by using the ``setup.py sdist`` command. The following commands assume your -current working directory is the ``tutorial`` package we've created and that -the parent directory of the ``tutorial`` package is a virtualenv representing -a :app:`Pyramid` environment. +current working directory contains the ``tutorial`` package and the +``setup.py`` file. On UNIX: -- cgit v1.2.3 From 0b02e46ff9dafcdf9d4c03bac2958c8b20c596f6 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 8 Feb 2016 00:19:31 -0600 Subject: expose the session factory on the registry --- docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py | 1 + docs/tutorials/wiki2/src/basiclayout/tutorial/models/__init__.py | 1 + docs/tutorials/wiki2/src/models/tutorial/models/__init__.py | 1 + pyramid/scaffolds/alchemy/+package+/models/__init__.py_tmpl | 1 + 4 files changed, 4 insertions(+) diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py index 4810c357a..3d3efe06f 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py @@ -62,6 +62,7 @@ def includeme(config): config.include('pyramid_tm') session_factory = get_session_factory(get_engine(settings)) + config.registry['dbsession_factory'] = session_factory # make request.dbsession available for use in Pyramid config.add_request_method( diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/__init__.py index a4026fcd6..48a957ecb 100644 --- a/docs/tutorials/wiki2/src/basiclayout/tutorial/models/__init__.py +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/models/__init__.py @@ -62,6 +62,7 @@ def includeme(config): config.include('pyramid_tm') session_factory = get_session_factory(get_engine(settings)) + config.registry['dbsession_factory'] = session_factory # make request.dbsession available for use in Pyramid config.add_request_method( diff --git a/docs/tutorials/wiki2/src/models/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/models/tutorial/models/__init__.py index 4810c357a..3d3efe06f 100644 --- a/docs/tutorials/wiki2/src/models/tutorial/models/__init__.py +++ b/docs/tutorials/wiki2/src/models/tutorial/models/__init__.py @@ -62,6 +62,7 @@ def includeme(config): config.include('pyramid_tm') session_factory = get_session_factory(get_engine(settings)) + config.registry['dbsession_factory'] = session_factory # make request.dbsession available for use in Pyramid config.add_request_method( diff --git a/pyramid/scaffolds/alchemy/+package+/models/__init__.py_tmpl b/pyramid/scaffolds/alchemy/+package+/models/__init__.py_tmpl index 7d0c94a14..26b50aaf6 100644 --- a/pyramid/scaffolds/alchemy/+package+/models/__init__.py_tmpl +++ b/pyramid/scaffolds/alchemy/+package+/models/__init__.py_tmpl @@ -62,6 +62,7 @@ def includeme(config): config.include('pyramid_tm') session_factory = get_session_factory(get_engine(settings)) + config.registry['dbsession_factory'] = session_factory # make request.dbsession available for use in Pyramid config.add_request_method( -- cgit v1.2.3 From 1c108019dae884e810d6436e10f8648c77bdd181 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 8 Feb 2016 00:43:47 -0600 Subject: [wip] update tests in wiki2 tutorial --- docs/tutorials/wiki2/src/tests/MANIFEST.in | 2 +- docs/tutorials/wiki2/src/tests/production.ini | 2 - docs/tutorials/wiki2/src/tests/setup.py | 1 - .../tutorials/wiki2/src/tests/tutorial/__init__.py | 11 ++-- .../wiki2/src/tests/tutorial/models/__init__.py | 72 +++++++++++++++++++++- .../wiki2/src/tests/tutorial/models/meta.py | 33 ---------- .../wiki2/src/tests/tutorial/models/mymodel.py | 19 +++--- .../src/tests/tutorial/scripts/initializedb.py | 27 ++++---- .../wiki2/src/tests/tutorial/security/__init__.py | 1 - .../wiki2/src/tests/tutorial/security/default.py | 11 +++- .../wiki2/src/tests/tutorial/templates/404.jinja2 | 8 +++ .../wiki2/src/tests/tutorial/templates/edit.jinja2 | 6 +- .../wiki2/src/tests/tutorial/templates/view.jinja2 | 6 +- .../wiki2/src/tests/tutorial/tests/__init__.py | 1 - .../src/tests/tutorial/tests/test_functional.py | 29 ++------- .../wiki2/src/tests/tutorial/tests/test_views.py | 46 +++----------- .../wiki2/src/tests/tutorial/views/default.py | 54 +++++++--------- .../wiki2/src/tests/tutorial/views/errors.py | 5 ++ .../wiki2/src/views/tutorial/models/__init__.py | 1 + docs/tutorials/wiki2/tests.rst | 8 +++ 20 files changed, 176 insertions(+), 167 deletions(-) create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/templates/404.jinja2 create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/views/errors.py diff --git a/docs/tutorials/wiki2/src/tests/MANIFEST.in b/docs/tutorials/wiki2/src/tests/MANIFEST.in index 81beba1b1..42cd299b5 100644 --- a/docs/tutorials/wiki2/src/tests/MANIFEST.in +++ b/docs/tutorials/wiki2/src/tests/MANIFEST.in @@ -1,2 +1,2 @@ include *.txt *.ini *.cfg *.rst -recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml +recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.jinja2 *.pt *.txt *.mak *.mako *.js *.html *.xml diff --git a/docs/tutorials/wiki2/src/tests/production.ini b/docs/tutorials/wiki2/src/tests/production.ini index 97acfbd7d..cb1db3211 100644 --- a/docs/tutorials/wiki2/src/tests/production.ini +++ b/docs/tutorials/wiki2/src/tests/production.ini @@ -11,8 +11,6 @@ pyramid.debug_authorization = false pyramid.debug_notfound = false pyramid.debug_routematch = false pyramid.default_locale_name = en -pyramid.includes = - pyramid_tm sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite diff --git a/docs/tutorials/wiki2/src/tests/setup.py b/docs/tutorials/wiki2/src/tests/setup.py index f640b4399..d4e5a4072 100644 --- a/docs/tutorials/wiki2/src/tests/setup.py +++ b/docs/tutorials/wiki2/src/tests/setup.py @@ -18,7 +18,6 @@ requires = [ 'zope.sqlalchemy', 'waitress', 'docutils', - 'WebTest', ] setup(name='tutorial', diff --git a/docs/tutorials/wiki2/src/tests/tutorial/__init__.py b/docs/tutorials/wiki2/src/tests/tutorial/__init__.py index 084fee19f..a62c42378 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/__init__.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/__init__.py @@ -2,7 +2,8 @@ from pyramid.config import Configurator from pyramid.authentication import AuthTktAuthenticationPolicy from pyramid.authorization import ACLAuthorizationPolicy -from security.default import groupfinder +from .security.default import groupfinder + def main(global_config, **settings): """ This function returns a Pyramid WSGI application. @@ -10,12 +11,12 @@ def main(global_config, **settings): authn_policy = AuthTktAuthenticationPolicy( 'sosecret', callback=groupfinder, hashalg='sha512') authz_policy = ACLAuthorizationPolicy() - config = Configurator(settings=settings, - root_factory='tutorial.models.mymodel.RootFactory') + config = Configurator(settings=settings) + config.include('pyramid_jinja2') + config.include('.models') + config.set_root_factory('.models.mymodel.RootFactory') config.set_authentication_policy(authn_policy) config.set_authorization_policy(authz_policy) - config.include('pyramid_jinja2') - config.include('.models.meta') config.add_static_view('static', 'static', cache_max_age=3600) config.add_route('view_wiki', '/') config.add_route('login', '/login') diff --git a/docs/tutorials/wiki2/src/tests/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/tests/tutorial/models/__init__.py index 7b1c62867..3d3efe06f 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/models/__init__.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/models/__init__.py @@ -1,7 +1,73 @@ +from sqlalchemy import engine_from_config +from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import configure_mappers -# import all models classes here for sqlalchemy mappers -# to pick up +import zope.sqlalchemy + +# import or define all models here to ensure they are attached to the +# Base.metadata prior to any initialization routines from .mymodel import Page # flake8: noqa -# run configure mappers to ensure we avoid any race conditions +# run configure_mappers after defining all of the models to ensure +# all relationships can be setup configure_mappers() + + +def get_engine(settings, prefix='sqlalchemy.'): + return engine_from_config(settings, prefix) + + +def get_session_factory(engine): + factory = sessionmaker() + factory.configure(bind=engine) + return factory + + +def get_tm_session(session_factory, transaction_manager): + """ + Get a ``sqlalchemy.orm.Session`` instance backed by a transaction. + + This function will hook the session to the transaction manager which + will take care of committing any changes. + + - When using pyramid_tm it will automatically be committed or aborted + depending on whether an exception is raised. + + - When using scripts you should wrap the session in a manager yourself. + For example:: + + import transaction + + engine = get_engine(settings) + session_factory = get_session_factory(engine) + with transaction.manager: + dbsession = get_tm_session(session_factory, transaction.manager) + + """ + dbsession = session_factory() + zope.sqlalchemy.register( + dbsession, transaction_manager=transaction_manager) + return dbsession + + +def includeme(config): + """ + Initialize the model for a Pyramid app. + + Activate this setup using ``config.include('tutorial.models')``. + + """ + settings = config.get_settings() + + # use pyramid_tm to hook the transaction lifecycle to the request + config.include('pyramid_tm') + + session_factory = get_session_factory(get_engine(settings)) + config.registry['dbsession_factory'] = session_factory + + # make request.dbsession available for use in Pyramid + config.add_request_method( + # r.tm is the transaction manager used by pyramid_tm + lambda r: get_tm_session(session_factory, r.tm), + 'dbsession', + reify=True + ) diff --git a/docs/tutorials/wiki2/src/tests/tutorial/models/meta.py b/docs/tutorials/wiki2/src/tests/tutorial/models/meta.py index 80ececd8c..fc3e8f1dd 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/models/meta.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/models/meta.py @@ -1,8 +1,5 @@ -from sqlalchemy import engine_from_config from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import sessionmaker from sqlalchemy.schema import MetaData -import zope.sqlalchemy # Recommended naming convention used by Alembic, as various different database # providers will autogenerate vastly different names making migrations more @@ -17,33 +14,3 @@ NAMING_CONVENTION = { metadata = MetaData(naming_convention=NAMING_CONVENTION) Base = declarative_base(metadata=metadata) - - -def includeme(config): - settings = config.get_settings() - dbmaker = get_dbmaker(get_engine(settings)) - - config.add_request_method( - lambda r: get_session(r.tm, dbmaker), - 'dbsession', - reify=True - ) - - config.include('pyramid_tm') - - -def get_session(transaction_manager, dbmaker): - dbsession = dbmaker() - zope.sqlalchemy.register(dbsession, - transaction_manager=transaction_manager) - return dbsession - - -def get_engine(settings, prefix='sqlalchemy.'): - return engine_from_config(settings, prefix) - - -def get_dbmaker(engine): - dbmaker = sessionmaker() - dbmaker.configure(bind=engine) - return dbmaker diff --git a/docs/tutorials/wiki2/src/tests/tutorial/models/mymodel.py b/docs/tutorials/wiki2/src/tests/tutorial/models/mymodel.py index 03e2f90ca..25209c745 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/models/mymodel.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/models/mymodel.py @@ -1,15 +1,14 @@ -from .meta import Base - from pyramid.security import ( Allow, Everyone, - ) - +) from sqlalchemy import ( Column, Integer, Text, - ) +) + +from .meta import Base class Page(Base): @@ -19,8 +18,12 @@ class Page(Base): name = Column(Text, unique=True) data = Column(Integer) + class RootFactory(object): - __acl__ = [ (Allow, Everyone, 'view'), - (Allow, 'group:editors', 'edit') ] + __acl__ = [ + (Allow, Everyone, 'view'), + (Allow, 'group:editors', 'edit'), + ] + def __init__(self, request): - pass \ No newline at end of file + pass diff --git a/docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py index 4aac4a848..601a6e73f 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py @@ -7,13 +7,15 @@ from pyramid.paster import ( setup_logging, ) -from ..models.meta import ( - Base, - get_session, +from pyramid.scripts.common import parse_vars + +from ..models.meta import Base +from ..models import ( get_engine, - get_dbmaker, + get_session_factory, + get_tm_session, ) -from ..models.mymodel import Page +from ..models import Page def usage(argv): @@ -27,16 +29,17 @@ def main(argv=sys.argv): if len(argv) < 2: usage(argv) config_uri = argv[1] + options = parse_vars(argv[2:]) setup_logging(config_uri) - settings = get_appsettings(config_uri) + settings = get_appsettings(config_uri, options=options) engine = get_engine(settings) - dbmaker = get_dbmaker(engine) - - dbsession = get_session(transaction.manager, dbmaker) - Base.metadata.create_all(engine) + session_factory = get_session_factory(engine) + with transaction.manager: - model = Page(name='FrontPage', data='This is the front page') - dbsession.add(model) + dbsession = get_tm_session(session_factory, transaction.manager) + + page = Page(name='FrontPage', data='This is the front page') + dbsession.add(page) diff --git a/docs/tutorials/wiki2/src/tests/tutorial/security/__init__.py b/docs/tutorials/wiki2/src/tests/tutorial/security/__init__.py index 5bb534f79..e69de29bb 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/security/__init__.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/security/__init__.py @@ -1 +0,0 @@ -# package diff --git a/docs/tutorials/wiki2/src/tests/tutorial/security/default.py b/docs/tutorials/wiki2/src/tests/tutorial/security/default.py index d88c9c71f..7fc1ea7c8 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/security/default.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/security/default.py @@ -1,6 +1,11 @@ -USERS = {'editor':'editor', - 'viewer':'viewer'} -GROUPS = {'editor':['group:editors']} +USERS = { + 'editor': 'editor', + 'viewer': 'viewer', +} + +GROUPS = { + 'editor': ['group:editors'], +} def groupfinder(userid, request): if userid in USERS: diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/404.jinja2 b/docs/tutorials/wiki2/src/tests/tutorial/templates/404.jinja2 new file mode 100644 index 000000000..1917f83c7 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/templates/404.jinja2 @@ -0,0 +1,8 @@ +{% extends "layout.jinja2" %} + +{% block content %} +
+

Pyramid Alchemy scaffold

+

404 Page Not Found

+
+{% endblock content %} diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/edit.jinja2 b/docs/tutorials/wiki2/src/tests/tutorial/templates/edit.jinja2 index c4f3a2c93..70ce49b73 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/templates/edit.jinja2 +++ b/docs/tutorials/wiki2/src/tests/tutorial/templates/edit.jinja2 @@ -33,16 +33,16 @@
- {% if logged_in %} + {% if request.authenticated_userid is not None %}

- Logout + Logout

{% endif %}

Editing {% if page.name %}{{page.name}}{% else %}Page Name Goes Here{% endif %}

You can return to the - FrontPage. + FrontPage.

diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/view.jinja2 b/docs/tutorials/wiki2/src/tests/tutorial/templates/view.jinja2 index a7afc66fc..b12ca5b0c 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/templates/view.jinja2 +++ b/docs/tutorials/wiki2/src/tests/tutorial/templates/view.jinja2 @@ -33,9 +33,9 @@
- {% if logged_in %} + {% if request.authenticated_userid is not None %}

- Logout + Logout

{% endif %}

{{ content|safe }}

@@ -48,7 +48,7 @@ Viewing {% if page.name %}{{page.name}}{% else %}Page Name Goes Here{% endif %}

You can return to the - FrontPage. + FrontPage.

diff --git a/docs/tutorials/wiki2/src/tests/tutorial/tests/__init__.py b/docs/tutorials/wiki2/src/tests/tutorial/tests/__init__.py index 8b1378917..e69de29bb 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/tests/__init__.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/tests/__init__.py @@ -1 +0,0 @@ - diff --git a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py index 339c60bc2..eda47c064 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py @@ -1,17 +1,5 @@ import unittest -from pyramid import testing - - -def dummy_request(dbsession): - return testing.DummyRequest(dbsession=dbsession) - - -def _register_routes(config): - config.add_route('view_page', '{pagename}') - config.add_route('edit_page', '{pagename}/edit_page') - config.add_route('add_page', 'add_page/{pagename}') - class FunctionalTests(unittest.TestCase): @@ -27,11 +15,8 @@ class FunctionalTests(unittest.TestCase): @staticmethod def setup_database(): import transaction - from tutorial.models.mymodel import Page - from tutorial.models.meta import ( - Base, - ) - import tutorial.models.meta + from tutorial.models import Page + from tutorial.models.meta import Base def initialize_db(dbsession, engine): @@ -40,11 +25,9 @@ class FunctionalTests(unittest.TestCase): model = Page(name='FrontPage', data='This is the front page') dbsession.add(model) - def wrap_get_session(transaction_manager, dbmaker): - dbsession = get_session(transaction_manager, dbmaker) + def wrap_get_tm_session(session_factory, transaction_manager): + dbsession = get_tm_session(session_factory, transaction_manager) initialize_db(dbsession, engine) - tutorial.models.meta.get_session = get_session - tutorial.models.meta.get_engine = get_engine return dbsession def wrap_get_engine(settings): @@ -53,10 +36,10 @@ class FunctionalTests(unittest.TestCase): return engine get_session = tutorial.models.meta.get_session - tutorial.models.meta.get_session = wrap_get_session + tutorial.models.get_tm_session = wrap_get_tm_session get_engine = tutorial.models.meta.get_engine - tutorial.models.meta.get_engine = wrap_get_engine + tutorial.models.get_engine = wrap_get_engine @classmethod def setUpClass(cls): diff --git a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_views.py b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_views.py index d70311e38..81d84fa30 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_views.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_views.py @@ -10,35 +10,29 @@ def dummy_request(dbsession): def _register_routes(config): config.add_route('view_page', '{pagename}') - config.add_route('edit_page', '{pagename}/edit_page') config.add_route('add_page', 'add_page/{pagename}') + config.add_route('edit_page', '{pagename}/edit_page') class BaseTest(unittest.TestCase): def setUp(self): + from ..models import get_tm_session self.config = testing.setUp(settings={ 'sqlalchemy.url': 'sqlite:///:memory:' }) - self.config.include('..models.meta') - _register_routes(self.config) - settings = self.config.get_settings() + self.config.include('..models') + self.config.include(_register_routes) - from ..models.meta import ( - get_session, - get_engine, - get_dbmaker, - ) - - self.engine = get_engine(settings) - dbmaker = get_dbmaker(self.engine) - - self.session = get_session(transaction.manager, dbmaker) + session_factory = self.config.registry['dbsession_factory'] + self.session = get_tm_session(session_factory, transaction.manager) self.init_database() def init_database(self): from ..models.meta import Base - Base.metadata.create_all(self.engine) + session_factory = self.config.registry['dbsession_factory'] + engine = session_factory.get_bind() + Base.metadata.create_all(engine) def tearDown(self): testing.tearDown() @@ -46,7 +40,6 @@ class BaseTest(unittest.TestCase): class ViewWikiTests(unittest.TestCase): - def setUp(self): self.config = testing.setUp() _register_routes(self.config) @@ -65,13 +58,6 @@ class ViewWikiTests(unittest.TestCase): class ViewPageTests(BaseTest): - def setUp(self): - super(ViewPageTests, self).setUp() - - def tearDown(self): - transaction.abort() - testing.tearDown() - def _callFUT(self, request): from tutorial.views.default import view_page return view_page(request) @@ -102,13 +88,6 @@ class ViewPageTests(BaseTest): class AddPageTests(BaseTest): - def setUp(self): - super(AddPageTests, self).setUp() - - def tearDown(self): - transaction.abort() - testing.tearDown() - def _callFUT(self, request): from tutorial.views.default import add_page return add_page(request) @@ -133,13 +112,6 @@ class AddPageTests(BaseTest): class EditPageTests(BaseTest): - def setUp(self): - super(EditPageTests, self).setUp() - - def tearDown(self): - transaction.abort() - testing.tearDown() - def _callFUT(self, request): from tutorial.views.default import edit_page return edit_page(request) diff --git a/docs/tutorials/wiki2/src/tests/tutorial/views/default.py b/docs/tutorials/wiki2/src/tests/tutorial/views/default.py index f35f041a4..aa77facd7 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/views/default.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/views/default.py @@ -6,31 +6,27 @@ from pyramid.httpexceptions import ( HTTPFound, HTTPNotFound, ) - from pyramid.view import ( view_config, forbidden_view_config, ) - from pyramid.security import ( remember, forget, ) +from ..models import Page from ..security.default import USERS -from ..models.mymodel import Page - # regular expression used to find WikiWords wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)") -@view_config(route_name='view_wiki', - permission='view') +@view_config(route_name='view_wiki', permission='view') def view_wiki(request): - return HTTPFound(location=request.route_url('view_page', - pagename='FrontPage')) + next_url = request.route_url('view_page', pagename='FrontPage') + return HTTPFound(location=next_url) -@view_config(route_name='view_page', renderer='templates/view.jinja2', +@view_config(route_name='view_page', renderer='../templates/view.jinja2', permission='view') def view_page(request): pagename = request.matchdict['pagename'] @@ -51,10 +47,9 @@ def view_page(request): content = publish_parts(page.data, writer_name='html')['html_body'] content = wikiwords.sub(check, content) edit_url = request.route_url('edit_page', pagename=pagename) - return dict(page=page, content=content, edit_url=edit_url, - logged_in=request.authenticated_userid) + return dict(page=page, content=content, edit_url=edit_url) -@view_config(route_name='add_page', renderer='templates/edit.jinja2', +@view_config(route_name='add_page', renderer='../templates/edit.jinja2', permission='edit') def add_page(request): pagename = request.matchdict['pagename'] @@ -62,29 +57,27 @@ def add_page(request): body = request.params['body'] page = Page(name=pagename, data=body) request.dbsession.add(page) - return HTTPFound(location = request.route_url('view_page', - pagename=pagename)) + next_url = request.route_url('view_page', pagename=pagename) + return HTTPFound(location=next_url) save_url = request.route_url('add_page', pagename=pagename) page = Page(name='', data='') - return dict(page=page, save_url=save_url, - logged_in=request.authenticated_userid) + return dict(page=page, save_url=save_url) -@view_config(route_name='edit_page', renderer='templates/edit.jinja2', +@view_config(route_name='edit_page', renderer='../templates/edit.jinja2', permission='edit') def edit_page(request): pagename = request.matchdict['pagename'] page = request.dbsession.query(Page).filter_by(name=pagename).one() if 'form.submitted' in request.params: page.data = request.params['body'] - request.dbsession.add(page) - return HTTPFound(location = request.route_url('view_page', - pagename=pagename)) + next_url = request.route_url('view_page', pagename=pagename) + return HTTPFound(location=next_url) return dict( page=page, - save_url = request.route_url('edit_page', pagename=pagename), - logged_in=request.authenticated_userid + save_url=request.route_url('edit_page', pagename=pagename), ) + @view_config(route_name='login', renderer='templates/login.jinja2') @forbidden_view_config(renderer='templates/login.jinja2') def login(request): @@ -101,20 +94,19 @@ def login(request): password = request.params['password'] if USERS.get(login) == password: headers = remember(request, login) - return HTTPFound(location = came_from, - headers = headers) + return HTTPFound(location=came_from, headers=headers) message = 'Failed login' return dict( - message = message, - url = request.application_url + '/login', - came_from = came_from, - login = login, - password = password, + message=message, + url=request.route_url('login'), + came_from=came_from, + login=login, + password=password, ) @view_config(route_name='logout') def logout(request): headers = forget(request) - return HTTPFound(location = request.route_url('view_wiki'), - headers = headers) + next_url = request.route_url('view_wiki') + return HTTPFound(location=next_url, headers=headers) diff --git a/docs/tutorials/wiki2/src/tests/tutorial/views/errors.py b/docs/tutorials/wiki2/src/tests/tutorial/views/errors.py new file mode 100644 index 000000000..a4b8201f1 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/views/errors.py @@ -0,0 +1,5 @@ +from pyramid.view import notfound_view_config + +@notfound_view_config(renderer='../templates/404.jinja2') +def notfound_view(request): + return {} diff --git a/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py index 4810c357a..3d3efe06f 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py +++ b/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py @@ -62,6 +62,7 @@ def includeme(config): config.include('pyramid_tm') session_factory = get_session_factory(get_engine(settings)) + config.registry['dbsession_factory'] = session_factory # make request.dbsession available for use in Pyramid config.add_request_method( diff --git a/docs/tutorials/wiki2/tests.rst b/docs/tutorials/wiki2/tests.rst index fe3fdaf2c..a99cd68cc 100644 --- a/docs/tutorials/wiki2/tests.rst +++ b/docs/tutorials/wiki2/tests.rst @@ -18,6 +18,14 @@ subpackage, and add several new tests. Start by creating a new directory and a new empty file ``tests/__init__.py``. +.. warning:: + + It is very important when refactoring a Python module into a package to + be sure to delete the cache files (``.pyc`` files or ``__pycache__`` + folders) sitting around! Python will prioritize the cache files before + traversing into folders and so it will use the old code and you will wonder + why none of your changes are working! + Test the views ============== -- cgit v1.2.3 From d6a758e58ef1c4782ecd3fe53c8563284f2496ca Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 8 Feb 2016 22:37:22 -0600 Subject: fix tests to get the bind from dbsession_factory properly --- docs/tutorials/wiki2/src/tests/tutorial/tests/test_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_views.py b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_views.py index 81d84fa30..b2830d070 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_views.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_views.py @@ -31,7 +31,7 @@ class BaseTest(unittest.TestCase): def init_database(self): from ..models.meta import Base session_factory = self.config.registry['dbsession_factory'] - engine = session_factory.get_bind() + engine = session_factory.kw['bind'] Base.metadata.create_all(engine) def tearDown(self): -- cgit v1.2.3 From 91ffccabafd2f074ac7620b5b64e52a8eb3cb31a Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 8 Feb 2016 23:00:48 -0600 Subject: fix jinja2 none test --- docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja2 | 2 +- docs/tutorials/wiki2/src/authorization/tutorial/templates/view.jinja2 | 2 +- docs/tutorials/wiki2/src/tests/tutorial/templates/edit.jinja2 | 2 +- docs/tutorials/wiki2/src/tests/tutorial/templates/view.jinja2 | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja2 index 70ce49b73..4d767cfbe 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja2 +++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja2 @@ -33,7 +33,7 @@
- {% if request.authenticated_userid is not None %} + {% if request.authenticated_userid is not none %}

Logout

diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.jinja2 index b12ca5b0c..942b8479b 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.jinja2 +++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.jinja2 @@ -33,7 +33,7 @@
- {% if request.authenticated_userid is not None %} + {% if request.authenticated_userid is not none %}

Logout

diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/edit.jinja2 b/docs/tutorials/wiki2/src/tests/tutorial/templates/edit.jinja2 index 70ce49b73..4d767cfbe 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/templates/edit.jinja2 +++ b/docs/tutorials/wiki2/src/tests/tutorial/templates/edit.jinja2 @@ -33,7 +33,7 @@
- {% if request.authenticated_userid is not None %} + {% if request.authenticated_userid is not none %}

Logout

diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/view.jinja2 b/docs/tutorials/wiki2/src/tests/tutorial/templates/view.jinja2 index b12ca5b0c..942b8479b 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/templates/view.jinja2 +++ b/docs/tutorials/wiki2/src/tests/tutorial/templates/view.jinja2 @@ -33,7 +33,7 @@
- {% if request.authenticated_userid is not None %} + {% if request.authenticated_userid is not none %}

Logout

-- cgit v1.2.3 From 62f0411a12a7f86bd7e3060b14f223cfb96322ad Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 8 Feb 2016 23:00:58 -0600 Subject: fix functional tests --- docs/tutorials/wiki2/src/tests/setup.py | 5 ++ .../src/tests/tutorial/tests/test_functional.py | 57 +++++++--------------- 2 files changed, 23 insertions(+), 39 deletions(-) diff --git a/docs/tutorials/wiki2/src/tests/setup.py b/docs/tutorials/wiki2/src/tests/setup.py index d4e5a4072..93195a68c 100644 --- a/docs/tutorials/wiki2/src/tests/setup.py +++ b/docs/tutorials/wiki2/src/tests/setup.py @@ -20,6 +20,10 @@ requires = [ 'docutils', ] +tests_require = [ + 'WebTest', +] + setup(name='tutorial', version='0.0', description='tutorial', @@ -39,6 +43,7 @@ setup(name='tutorial', zip_safe=False, test_suite='tutorial', install_requires=requires, + tests_require=tests_require, entry_points="""\ [paste.app_factory] main = tutorial:main diff --git a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py index eda47c064..2c08f9a64 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py @@ -1,4 +1,6 @@ +import transaction import unittest +from webtest import TestApp class FunctionalTests(unittest.TestCase): @@ -10,55 +12,32 @@ class FunctionalTests(unittest.TestCase): editor_login = '/login?login=editor&password=editor' \ '&came_from=FrontPage&form.submitted=Login' - engine = None - - @staticmethod - def setup_database(): - import transaction - from tutorial.models import Page - from tutorial.models.meta import Base - - - def initialize_db(dbsession, engine): - Base.metadata.create_all(engine) - with transaction.manager: - model = Page(name='FrontPage', data='This is the front page') - dbsession.add(model) - - def wrap_get_tm_session(session_factory, transaction_manager): - dbsession = get_tm_session(session_factory, transaction_manager) - initialize_db(dbsession, engine) - return dbsession - - def wrap_get_engine(settings): - global engine - engine = get_engine(settings) - return engine - - get_session = tutorial.models.meta.get_session - tutorial.models.get_tm_session = wrap_get_tm_session - - get_engine = tutorial.models.meta.get_engine - tutorial.models.get_engine = wrap_get_engine - @classmethod def setUpClass(cls): - cls.setup_database() - - from webtest import TestApp + from tutorial.models.meta import Base + from tutorial.models import ( + Page, + get_tm_session, + ) from tutorial import main + settings = {'sqlalchemy.url': 'sqlite://'} app = main({}, **settings) cls.testapp = TestApp(app) + session_factory = app.registry['dbsession_factory'] + cls.engine = session_factory.kw['bind'] + Base.metadata.create_all(bind=cls.engine) + + with transaction.manager: + dbsession = get_tm_session(session_factory, transaction.manager) + model = Page(name='FrontPage', data='This is the front page') + dbsession.add(model) + @classmethod def tearDownClass(cls): from tutorial.models.meta import Base - Base.metadata.drop_all(engine) - - def tearDown(self): - import transaction - transaction.abort() + Base.metadata.drop_all(bind=cls.engine) def test_root(self): res = self.testapp.get('/', status=302) -- cgit v1.2.3 From 53142852745ddd5668d11801a179b03e343554c4 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 9 Feb 2016 20:25:53 -0500 Subject: check in sketch code so raydeo can look at it --- pyramid/view.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/pyramid/view.py b/pyramid/view.py index 7e8996ca4..cedcfaef1 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -10,6 +10,7 @@ from pyramid.interfaces import ( IView, IViewClassifier, IRequest, + IExceptionViewClassifier, ) from pyramid.compat import decode_path_info @@ -547,3 +548,32 @@ def _call_view( raise pme return response + +class ViewMethodsMixin(object): + """ Request methods mixin for BaseRequest having to do with executing + views """ + def invoke_exception_view( + self, + exc, + request=None, + secure=True + ): + if request is None: + request = self + registry = getattr(request, 'registry', None) + if registry is None: + registry = get_current_registry() + context_iface = providedBy(exc) + view_name = getattr(request, 'view_name', '') + response = _call_view( + registry, + request, + exc, + context_iface, + view_name, + view_types=None, + view_classifier=IExceptionViewClassifier, + secure=secure, + request_iface=None, + ) + return response -- cgit v1.2.3 From e40ef23c9c364249318e030e5f1393a9fff17cdd Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 9 Feb 2016 20:40:58 -0500 Subject: use exc_info instead of exc, add better docstring, mix the mixin in --- pyramid/request.py | 2 ++ pyramid/view.py | 38 +++++++++++++++++++++++++++++++++++--- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/pyramid/request.py b/pyramid/request.py index 45d936cef..c1c1da514 100644 --- a/pyramid/request.py +++ b/pyramid/request.py @@ -32,6 +32,7 @@ from pyramid.util import ( InstancePropertyHelper, InstancePropertyMixin, ) +from pyramid.view import ViewMethodsMixin class TemplateContext(object): pass @@ -154,6 +155,7 @@ class Request( LocalizerRequestMixin, AuthenticationAPIMixin, AuthorizationAPIMixin, + ViewMethodsMixin, ): """ A subclass of the :term:`WebOb` Request class. An instance of diff --git a/pyramid/view.py b/pyramid/view.py index cedcfaef1..b09c08ccc 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -1,4 +1,5 @@ import itertools +import traceback import venusian from zope.interface import providedBy @@ -554,21 +555,52 @@ class ViewMethodsMixin(object): views """ def invoke_exception_view( self, - exc, + exc_info=None, request=None, secure=True ): + """ Executes an exception view related to the request it's called upon. + The arguments it takes are these: + + ``exc_info`` + + If provided, should be a 3-tuple in the form provided by + ``traceback.exc_info()``. If not provided, + ``traceback.exc_info()`` will be called to obtain the current + interpreter exception information. Default: ``None``. + + ``request`` + + If the request to be used is not the same one as the instance that + this method is called upon, it may be passed here. Default: + ``None``. + + ``secure`` + + If the exception view should not be rendered if the current user + does not have the appropriate permission, this should be ``True``. + Default: ``True``. + + If called with no arguments, it uses the global exception information + returned by ``traceback.exc_info()`` as ``exc_info``, the request + object that the method is a member of as the ``request``, and + ``secure`` is ``True``. + + This method returns a :term:`response` object.""" + if request is None: request = self registry = getattr(request, 'registry', None) if registry is None: registry = get_current_registry() - context_iface = providedBy(exc) + if exc_info is None: + exc_info = traceback.exc_info() + context_iface = providedBy(exc_info[0]) view_name = getattr(request, 'view_name', '') response = _call_view( registry, request, - exc, + exc_info[0], context_iface, view_name, view_types=None, -- cgit v1.2.3 From ff3dd9e9431035b479136abcc761a8f20341e3e2 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 9 Feb 2016 20:42:43 -0500 Subject: add request.exception and request.exc_info --- pyramid/view.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pyramid/view.py b/pyramid/view.py index b09c08ccc..0f1a5e41c 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -583,8 +583,8 @@ class ViewMethodsMixin(object): If called with no arguments, it uses the global exception information returned by ``traceback.exc_info()`` as ``exc_info``, the request - object that the method is a member of as the ``request``, and - ``secure`` is ``True``. + object that this method is attached to as the ``request``, and + ``True`` for ``secure``. This method returns a :term:`response` object.""" @@ -597,6 +597,8 @@ class ViewMethodsMixin(object): exc_info = traceback.exc_info() context_iface = providedBy(exc_info[0]) view_name = getattr(request, 'view_name', '') + request.exception = exc_info[0] + request.exc_info = exc_info response = _call_view( registry, request, -- cgit v1.2.3 From 7d8175fe8f893668f6061e2a26f9c673c2f00ccc Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 9 Feb 2016 20:47:39 -0500 Subject: docs about what happens when no excview can be found --- pyramid/view.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyramid/view.py b/pyramid/view.py index 0f1a5e41c..edf3d8585 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -586,7 +586,8 @@ class ViewMethodsMixin(object): object that this method is attached to as the ``request``, and ``True`` for ``secure``. - This method returns a :term:`response` object.""" + This method returns a :term:`response` object or ``None`` if no + matching exception view can be found..""" if request is None: request = self -- cgit v1.2.3 From ca529f0d7d3870d851181375f6a15805cef208b5 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 9 Feb 2016 20:51:35 -0500 Subject: bring into line with excview --- pyramid/view.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/pyramid/view.py b/pyramid/view.py index edf3d8585..2555cbe30 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -596,10 +596,17 @@ class ViewMethodsMixin(object): registry = get_current_registry() if exc_info is None: exc_info = traceback.exc_info() + attrs = request.__dict__ context_iface = providedBy(exc_info[0]) - view_name = getattr(request, 'view_name', '') - request.exception = exc_info[0] - request.exc_info = exc_info + view_name = attrs.get('view_name', '') + # probably need something like "with temporarily_munged_request(req)" + # here, which adds exception and exc_info as request attrs, and + # removes response object temporarily (as per the excview tween) + attrs['exception'] = exc_info[0] + attrs['exc_info'] = request.exc_info = exc_info + # we use .get instead of .__getitem__ below due to + # https://github.com/Pylons/pyramid/issues/700 + request_iface = attrs.get('request_iface', IRequest) response = _call_view( registry, request, @@ -609,6 +616,6 @@ class ViewMethodsMixin(object): view_types=None, view_classifier=IExceptionViewClassifier, secure=secure, - request_iface=None, + request_iface=request_iface.combined, ) return response -- cgit v1.2.3 From 22bf919e5eaa21bf7d826bb4143958d7d20e0469 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 9 Feb 2016 20:52:43 -0500 Subject: dont set it twice --- pyramid/view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/view.py b/pyramid/view.py index 2555cbe30..242a1b7de 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -603,7 +603,7 @@ class ViewMethodsMixin(object): # here, which adds exception and exc_info as request attrs, and # removes response object temporarily (as per the excview tween) attrs['exception'] = exc_info[0] - attrs['exc_info'] = request.exc_info = exc_info + attrs['exc_info'] = exc_info # we use .get instead of .__getitem__ below due to # https://github.com/Pylons/pyramid/issues/700 request_iface = attrs.get('request_iface', IRequest) -- cgit v1.2.3 From 252fa52ee7628253dfec7636f4e55b8124efea2a Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 9 Feb 2016 20:53:41 -0500 Subject: its sys, not traceback --- pyramid/view.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pyramid/view.py b/pyramid/view.py index 242a1b7de..1f20622dc 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -1,5 +1,6 @@ import itertools -import traceback +import sys + import venusian from zope.interface import providedBy @@ -565,8 +566,8 @@ class ViewMethodsMixin(object): ``exc_info`` If provided, should be a 3-tuple in the form provided by - ``traceback.exc_info()``. If not provided, - ``traceback.exc_info()`` will be called to obtain the current + ``sys.exc_info()``. If not provided, + ``sys.exc_info()`` will be called to obtain the current interpreter exception information. Default: ``None``. ``request`` @@ -582,7 +583,7 @@ class ViewMethodsMixin(object): Default: ``True``. If called with no arguments, it uses the global exception information - returned by ``traceback.exc_info()`` as ``exc_info``, the request + returned by ``sys.exc_info()`` as ``exc_info``, the request object that this method is attached to as the ``request``, and ``True`` for ``secure``. @@ -595,7 +596,7 @@ class ViewMethodsMixin(object): if registry is None: registry = get_current_registry() if exc_info is None: - exc_info = traceback.exc_info() + exc_info = sys.exc_info() attrs = request.__dict__ context_iface = providedBy(exc_info[0]) view_name = attrs.get('view_name', '') -- cgit v1.2.3 From ff88c1535c65a717a030a480e39723724d53d985 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Wed, 10 Feb 2016 22:28:47 -0600 Subject: split login from forbidden --- .../wiki2/src/tests/tutorial/tests/test_functional.py | 8 ++++---- docs/tutorials/wiki2/src/tests/tutorial/views/default.py | 7 +------ docs/tutorials/wiki2/src/tests/tutorial/views/errors.py | 11 ++++++++++- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py index 2c08f9a64..b25d9a332 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py @@ -70,21 +70,21 @@ class FunctionalTests(unittest.TestCase): self.assertTrue(b'Logout' not in res.body) def test_anonymous_user_cannot_edit(self): - res = self.testapp.get('/FrontPage/edit_page', status=200) + res = self.testapp.get('/FrontPage/edit_page', status=302).follow() self.assertTrue(b'Login' in res.body) def test_anonymous_user_cannot_add(self): - res = self.testapp.get('/add_page/NewPage', status=200) + res = self.testapp.get('/add_page/NewPage', status=302).follow() self.assertTrue(b'Login' in res.body) def test_viewer_user_cannot_edit(self): self.testapp.get(self.viewer_login, status=302) - res = self.testapp.get('/FrontPage/edit_page', status=200) + res = self.testapp.get('/FrontPage/edit_page', status=302).follow() self.assertTrue(b'Login' in res.body) def test_viewer_user_cannot_add(self): self.testapp.get(self.viewer_login, status=302) - res = self.testapp.get('/add_page/NewPage', status=200) + res = self.testapp.get('/add_page/NewPage', status=302).follow() self.assertTrue(b'Login' in res.body) def test_editors_member_user_can_edit(self): diff --git a/docs/tutorials/wiki2/src/tests/tutorial/views/default.py b/docs/tutorials/wiki2/src/tests/tutorial/views/default.py index aa77facd7..bc8a59fe8 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/views/default.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/views/default.py @@ -6,14 +6,11 @@ from pyramid.httpexceptions import ( HTTPFound, HTTPNotFound, ) -from pyramid.view import ( - view_config, - forbidden_view_config, - ) from pyramid.security import ( remember, forget, ) +from pyramid.view import view_config from ..models import Page from ..security.default import USERS @@ -77,9 +74,7 @@ def edit_page(request): save_url=request.route_url('edit_page', pagename=pagename), ) - @view_config(route_name='login', renderer='templates/login.jinja2') -@forbidden_view_config(renderer='templates/login.jinja2') def login(request): login_url = request.route_url('login') referrer = request.url diff --git a/docs/tutorials/wiki2/src/tests/tutorial/views/errors.py b/docs/tutorials/wiki2/src/tests/tutorial/views/errors.py index a4b8201f1..f8dbe4e05 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/views/errors.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/views/errors.py @@ -1,5 +1,14 @@ -from pyramid.view import notfound_view_config +from pyramid.httpexceptions import HTTPFound +from pyramid.view import ( + forbidden_view_config, + notfound_view_config, +) @notfound_view_config(renderer='../templates/404.jinja2') def notfound_view(request): return {} + +@forbidden_view_config() +def forbidden_view(request): + next_url = request.route_url('login', _query={'came_from': request.url}) + return HTTPFound(location=next_url) -- cgit v1.2.3 From 3f6547b8f21912deda79fd4e4cfd0112a8971754 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Wed, 10 Feb 2016 20:46:08 -0800 Subject: minor grammar and punctuation through "routes need relative ordering" --- docs/designdefense.rst | 88 +++++++++++++++++++++++++------------------------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/docs/designdefense.rst b/docs/designdefense.rst index d33ae2fd8..530a9c17c 100644 --- a/docs/designdefense.rst +++ b/docs/designdefense.rst @@ -1283,7 +1283,7 @@ predictability. .. _routes_need_ordering: -Routes Need Relative Ordering +Routes need relative ordering +++++++++++++++++++++++++++++ Consider the following simple `Groundhog @@ -1311,8 +1311,8 @@ Consider the following simple `Groundhog app.run() If you run this application and visit the URL ``/admin``, you will see the -"admin" page. This is the intended result. However, what if you rearrange -the order of the function definitions in the file? +"admin" page. This is the intended result. However, what if you rearrange the +order of the function definitions in the file? .. code-block:: python :linenos: @@ -1335,11 +1335,11 @@ the order of the function definitions in the file? if __name__ == '__main__': app.run() -If you run this application and visit the URL ``/admin``, you will now be -returned a 404 error. This is probably not what you intended. The reason -you see a 404 error when you rearrange function definition ordering is that -routing declarations expressed via our microframework's routing decorators -have an *ordering*, and that ordering matters. +If you run this application and visit the URL ``/admin``, your app will now +return a 404 error. This is probably not what you intended. The reason you see +a 404 error when you rearrange function definition ordering is that routing +declarations expressed via our microframework's routing decorators have an +*ordering*, and that ordering matters. In the first case, where we achieved the expected result, we first added a route with the pattern ``/admin``, then we added a route with the pattern @@ -1347,63 +1347,63 @@ route with the pattern ``/admin``, then we added a route with the pattern scope. When a request with a ``PATH_INFO`` of ``/admin`` enters our application, the web framework loops over each of our application's route patterns in the order in which they were defined in our module. As a result, -the view associated with the ``/admin`` routing pattern will be invoked: it -matches first. All is right with the world. +the view associated with the ``/admin`` routing pattern will be invoked because +it matches first. All is right with the world. In the second case, where we did not achieve the expected result, we first added a route with the pattern ``/:action``, then we added a route with the pattern ``/admin``. When a request with a ``PATH_INFO`` of ``/admin`` enters our application, the web framework loops over each of our application's route patterns in the order in which they were defined in our module. As a result, -the view associated with the ``/:action`` routing pattern will be invoked: it -matches first. A 404 error is raised. This is not what we wanted; it just -happened due to the order in which we defined our view functions. - -This is because Groundhog routes are added to the routing map in import -order, and matched in the same order when a request comes in. Bottle, like -Groundhog, as of this writing, matches routes in the order in which they're -defined at Python execution time. Flask, on the other hand, does not order -route matching based on import order; it reorders the routes you add to your -application based on their "complexity". Other microframeworks have varying +the view associated with the ``/:action`` routing pattern will be invoked +because it matches first. A 404 error is raised. This is not what we wanted; it +just happened due to the order in which we defined our view functions. + +This is because Groundhog routes are added to the routing map in import order, +and matched in the same order when a request comes in. Bottle, like Groundhog, +as of this writing, matches routes in the order in which they're defined at +Python execution time. Flask, on the other hand, does not order route matching +based on import order. Instead it reorders the routes you add to your +application based on their "complexity". Other microframeworks have varying strategies to do route ordering. Your application may be small enough where route ordering will never cause an -issue. If your application becomes large enough, however, being able to -specify or predict that ordering as your application grows larger will be -difficult. At some point, you will likely need to more explicitly start -controlling route ordering, especially in applications that require -extensibility. +issue. If your application becomes large enough, however, being able to specify +or predict that ordering as your application grows larger will be difficult. +At some point, you will likely need to start controlling route ordering more +explicitly, especially in applications that require extensibility. If your microframework orders route matching based on complexity, you'll need to understand what is meant by "complexity", and you'll need to attempt to -inject a "less complex" route to have it get matched before any "more -complex" one to ensure that it's tried first. +inject a "less complex" route to have it get matched before any "more complex" +one to ensure that it's tried first. If your microframework orders its route matching based on relative import/execution of function decorator definitions, you will need to ensure -you execute all of these statements in the "right" order, and you'll need to -be cognizant of this import/execution ordering as you grow your application -or try to extend it. This is a difficult invariant to maintain for all but -the smallest applications. - -In either case, your application must import the non-``__main__`` modules -which contain configuration decorations somehow for their configuration to be -executed. Does that make you a little uncomfortable? It should, because +that you execute all of these statements in the "right" order, and you'll need +to be cognizant of this import/execution ordering as you grow your application +or try to extend it. This is a difficult invariant to maintain for all but the +smallest applications. + +In either case, your application must import the non-``__main__`` modules which +contain configuration decorations somehow for their configuration to be +executed. Does that make you a little uncomfortable? It should, because :ref:`you_dont_own_modulescope`. Pyramid uses neither decorator import time ordering nor does it attempt to -divine the relative complexity of one route to another in order to define a -route match ordering. In Pyramid, you have to maintain relative route -ordering imperatively via the chronology of multiple executions of the -:meth:`pyramid.config.Configurator.add_route` method. The order in which you +divine the relative complexity of one route to another as a means to define a +route match ordering. In Pyramid, you have to maintain relative route ordering +imperatively via the chronology of multiple executions of the +:meth:`pyramid.config.Configurator.add_route` method. The order in which you repeatedly call ``add_route`` becomes the order of route matching. If needing to maintain this imperative ordering truly bugs you, you can use -:term:`traversal` instead of route matching, which is a completely -declarative (and completely predictable) mechanism to map code to URLs. -While URL dispatch is easier to understand for small non-extensible -applications, traversal is a great fit for very large applications and -applications that need to be arbitrarily extensible. +:term:`traversal` instead of route matching, which is a completely declarative +(and completely predictable) mechanism to map code to URLs. While URL dispatch +is easier to understand for small non-extensible applications, traversal is a +great fit for very large applications and applications that need to be +arbitrarily extensible. + "Stacked Object Proxies" Are Too Clever / Thread Locals Are A Nuisance ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -- cgit v1.2.3 From 07d38f5d4c9ebaf267d4ecaf8c0bd4c508f1848f Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Wed, 10 Feb 2016 22:38:38 -0600 Subject: several simple refactorings - move auth from default.py to auth.py - rename errors to notfound - drop basic templates (mytemplate.jinja2, layout.jinja2) --- .../authorization/tutorial/templates/layout.jinja2 | 66 ---------------- .../tutorial/templates/mytemplate.jinja2 | 8 -- .../wiki2/src/authorization/tutorial/views/auth.py | 49 ++++++++++++ .../src/authorization/tutorial/views/default.py | 44 +---------- .../src/authorization/tutorial/views/errors.py | 5 -- .../src/authorization/tutorial/views/notfound.py | 7 ++ .../wiki2/src/basiclayout/tutorial/views/errors.py | 5 -- .../src/basiclayout/tutorial/views/notfound.py | 7 ++ .../wiki2/src/models/tutorial/views/errors.py | 5 -- .../wiki2/src/models/tutorial/views/notfound.py | 7 ++ .../src/tests/tutorial/templates/layout.jinja2 | 66 ---------------- .../src/tests/tutorial/templates/mytemplate.jinja2 | 8 -- .../src/tests/tutorial/tests/test_functional.py | 15 ++-- .../wiki2/src/tests/tutorial/views/auth.py | 49 ++++++++++++ .../wiki2/src/tests/tutorial/views/default.py | 37 --------- .../wiki2/src/tests/tutorial/views/errors.py | 14 ---- .../wiki2/src/tests/tutorial/views/notfound.py | 7 ++ .../wiki2/src/views/tutorial/templates/404.jinja2 | 2 +- .../wiki2/src/views/tutorial/templates/edit.jinja2 | 88 +++++----------------- .../src/views/tutorial/templates/layout.jinja2 | 19 +---- .../src/views/tutorial/templates/mytemplate.jinja2 | 8 -- .../wiki2/src/views/tutorial/templates/view.jinja2 | 82 ++++---------------- .../wiki2/src/views/tutorial/views/errors.py | 5 -- .../wiki2/src/views/tutorial/views/notfound.py | 7 ++ .../alchemy/+package+/views/errors.py_tmpl | 5 -- .../alchemy/+package+/views/notfound.py_tmpl | 7 ++ 26 files changed, 191 insertions(+), 431 deletions(-) delete mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/templates/layout.jinja2 delete mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/templates/mytemplate.jinja2 create mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/views/auth.py delete mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/views/errors.py create mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/views/notfound.py delete mode 100644 docs/tutorials/wiki2/src/basiclayout/tutorial/views/errors.py create mode 100644 docs/tutorials/wiki2/src/basiclayout/tutorial/views/notfound.py delete mode 100644 docs/tutorials/wiki2/src/models/tutorial/views/errors.py create mode 100644 docs/tutorials/wiki2/src/models/tutorial/views/notfound.py delete mode 100644 docs/tutorials/wiki2/src/tests/tutorial/templates/layout.jinja2 delete mode 100644 docs/tutorials/wiki2/src/tests/tutorial/templates/mytemplate.jinja2 create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/views/auth.py delete mode 100644 docs/tutorials/wiki2/src/tests/tutorial/views/errors.py create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/views/notfound.py delete mode 100644 docs/tutorials/wiki2/src/views/tutorial/templates/mytemplate.jinja2 delete mode 100644 docs/tutorials/wiki2/src/views/tutorial/views/errors.py create mode 100644 docs/tutorials/wiki2/src/views/tutorial/views/notfound.py delete mode 100644 pyramid/scaffolds/alchemy/+package+/views/errors.py_tmpl create mode 100644 pyramid/scaffolds/alchemy/+package+/views/notfound.py_tmpl diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/layout.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/layout.jinja2 deleted file mode 100644 index ff624c65b..000000000 --- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/layout.jinja2 +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - - - - - Alchemy Scaffold for The Pyramid Web Framework - - - - - - - - - - - - - -
-
-
-
- -
-
- {% block content %} -

No content

- {% endblock content %} -
-
-
- -
-
- -
-
-
- - - - - - - - diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/mytemplate.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/mytemplate.jinja2 deleted file mode 100644 index bb622bf5a..000000000 --- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/mytemplate.jinja2 +++ /dev/null @@ -1,8 +0,0 @@ -{% extends "layout.jinja2" %} - -{% block content %} -
-

Pyramid Alchemy scaffold

-

Welcome to {{project}}, an application generated by
the Pyramid Web Framework 1.7.dev0.

-
-{% endblock content %} diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views/auth.py b/docs/tutorials/wiki2/src/authorization/tutorial/views/auth.py new file mode 100644 index 000000000..08aa2bfad --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/views/auth.py @@ -0,0 +1,49 @@ +from pyramid.httpexceptions import HTTPFound +from pyramid.security import ( + remember, + forget, + ) +from pyramid.view import ( + forbidden_view_config, + view_config, +) + +from ..security.default import USERS + + +@view_config(route_name='login', renderer='templates/login.jinja2') +def login(request): + login_url = request.route_url('login') + referrer = request.url + if referrer == login_url: + referrer = '/' # never use the login form itself as came_from + came_from = request.params.get('came_from', referrer) + message = '' + login = '' + password = '' + if 'form.submitted' in request.params: + login = request.params['login'] + password = request.params['password'] + if USERS.get(login) == password: + headers = remember(request, login) + return HTTPFound(location=came_from, headers=headers) + message = 'Failed login' + + return dict( + message=message, + url=request.route_url('login'), + came_from=came_from, + login=login, + password=password, + ) + +@view_config(route_name='logout') +def logout(request): + headers = forget(request) + next_url = request.route_url('view_wiki') + return HTTPFound(location=next_url, headers=headers) + +@forbidden_view_config() +def forbidden_view(request): + next_url = request.route_url('login', _query={'came_from': request.url}) + return HTTPFound(location=next_url) diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py b/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py index aa77facd7..6fb3c8744 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py @@ -6,17 +6,9 @@ from pyramid.httpexceptions import ( HTTPFound, HTTPNotFound, ) -from pyramid.view import ( - view_config, - forbidden_view_config, - ) -from pyramid.security import ( - remember, - forget, - ) +from pyramid.view import view_config from ..models import Page -from ..security.default import USERS # regular expression used to find WikiWords wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)") @@ -76,37 +68,3 @@ def edit_page(request): page=page, save_url=request.route_url('edit_page', pagename=pagename), ) - - -@view_config(route_name='login', renderer='templates/login.jinja2') -@forbidden_view_config(renderer='templates/login.jinja2') -def login(request): - login_url = request.route_url('login') - referrer = request.url - if referrer == login_url: - referrer = '/' # never use the login form itself as came_from - came_from = request.params.get('came_from', referrer) - message = '' - login = '' - password = '' - if 'form.submitted' in request.params: - login = request.params['login'] - password = request.params['password'] - if USERS.get(login) == password: - headers = remember(request, login) - return HTTPFound(location=came_from, headers=headers) - message = 'Failed login' - - return dict( - message=message, - url=request.route_url('login'), - came_from=came_from, - login=login, - password=password, - ) - -@view_config(route_name='logout') -def logout(request): - headers = forget(request) - next_url = request.route_url('view_wiki') - return HTTPFound(location=next_url, headers=headers) diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views/errors.py b/docs/tutorials/wiki2/src/authorization/tutorial/views/errors.py deleted file mode 100644 index a4b8201f1..000000000 --- a/docs/tutorials/wiki2/src/authorization/tutorial/views/errors.py +++ /dev/null @@ -1,5 +0,0 @@ -from pyramid.view import notfound_view_config - -@notfound_view_config(renderer='../templates/404.jinja2') -def notfound_view(request): - return {} diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views/notfound.py b/docs/tutorials/wiki2/src/authorization/tutorial/views/notfound.py new file mode 100644 index 000000000..69d6e2804 --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/views/notfound.py @@ -0,0 +1,7 @@ +from pyramid.view import notfound_view_config + + +@notfound_view_config(renderer='../templates/404.jinja2') +def notfound_view(request): + request.response.status = 404 + return {} diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/views/errors.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/views/errors.py deleted file mode 100644 index a4b8201f1..000000000 --- a/docs/tutorials/wiki2/src/basiclayout/tutorial/views/errors.py +++ /dev/null @@ -1,5 +0,0 @@ -from pyramid.view import notfound_view_config - -@notfound_view_config(renderer='../templates/404.jinja2') -def notfound_view(request): - return {} diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/views/notfound.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/views/notfound.py new file mode 100644 index 000000000..69d6e2804 --- /dev/null +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/views/notfound.py @@ -0,0 +1,7 @@ +from pyramid.view import notfound_view_config + + +@notfound_view_config(renderer='../templates/404.jinja2') +def notfound_view(request): + request.response.status = 404 + return {} diff --git a/docs/tutorials/wiki2/src/models/tutorial/views/errors.py b/docs/tutorials/wiki2/src/models/tutorial/views/errors.py deleted file mode 100644 index a4b8201f1..000000000 --- a/docs/tutorials/wiki2/src/models/tutorial/views/errors.py +++ /dev/null @@ -1,5 +0,0 @@ -from pyramid.view import notfound_view_config - -@notfound_view_config(renderer='../templates/404.jinja2') -def notfound_view(request): - return {} diff --git a/docs/tutorials/wiki2/src/models/tutorial/views/notfound.py b/docs/tutorials/wiki2/src/models/tutorial/views/notfound.py new file mode 100644 index 000000000..69d6e2804 --- /dev/null +++ b/docs/tutorials/wiki2/src/models/tutorial/views/notfound.py @@ -0,0 +1,7 @@ +from pyramid.view import notfound_view_config + + +@notfound_view_config(renderer='../templates/404.jinja2') +def notfound_view(request): + request.response.status = 404 + return {} diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/layout.jinja2 b/docs/tutorials/wiki2/src/tests/tutorial/templates/layout.jinja2 deleted file mode 100644 index ff624c65b..000000000 --- a/docs/tutorials/wiki2/src/tests/tutorial/templates/layout.jinja2 +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - - - - - Alchemy Scaffold for The Pyramid Web Framework - - - - - - - - - - - - - -
-
-
-
- -
-
- {% block content %} -

No content

- {% endblock content %} -
-
-
- -
-
- -
-
-
- - - - - - - - diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/mytemplate.jinja2 b/docs/tutorials/wiki2/src/tests/tutorial/templates/mytemplate.jinja2 deleted file mode 100644 index bb622bf5a..000000000 --- a/docs/tutorials/wiki2/src/tests/tutorial/templates/mytemplate.jinja2 +++ /dev/null @@ -1,8 +0,0 @@ -{% extends "layout.jinja2" %} - -{% block content %} -
-

Pyramid Alchemy scaffold

-

Welcome to {{project}}, an application generated by
the Pyramid Web Framework 1.7.dev0.

-
-{% endblock content %} diff --git a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py index b25d9a332..c716537ae 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py @@ -5,12 +5,15 @@ from webtest import TestApp class FunctionalTests(unittest.TestCase): - viewer_login = '/login?login=viewer&password=viewer' \ - '&came_from=FrontPage&form.submitted=Login' - viewer_wrong_login = '/login?login=viewer&password=incorrect' \ - '&came_from=FrontPage&form.submitted=Login' - editor_login = '/login?login=editor&password=editor' \ - '&came_from=FrontPage&form.submitted=Login' + viewer_login = ( + '/login?login=viewer&password=viewer' + '&came_from=FrontPage&form.submitted=Login') + viewer_wrong_login = ( + '/login?login=viewer&password=incorrect' + '&came_from=FrontPage&form.submitted=Login') + editor_login = ( + '/login?login=editor&password=editor' + '&came_from=FrontPage&form.submitted=Login') @classmethod def setUpClass(cls): diff --git a/docs/tutorials/wiki2/src/tests/tutorial/views/auth.py b/docs/tutorials/wiki2/src/tests/tutorial/views/auth.py new file mode 100644 index 000000000..08aa2bfad --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/views/auth.py @@ -0,0 +1,49 @@ +from pyramid.httpexceptions import HTTPFound +from pyramid.security import ( + remember, + forget, + ) +from pyramid.view import ( + forbidden_view_config, + view_config, +) + +from ..security.default import USERS + + +@view_config(route_name='login', renderer='templates/login.jinja2') +def login(request): + login_url = request.route_url('login') + referrer = request.url + if referrer == login_url: + referrer = '/' # never use the login form itself as came_from + came_from = request.params.get('came_from', referrer) + message = '' + login = '' + password = '' + if 'form.submitted' in request.params: + login = request.params['login'] + password = request.params['password'] + if USERS.get(login) == password: + headers = remember(request, login) + return HTTPFound(location=came_from, headers=headers) + message = 'Failed login' + + return dict( + message=message, + url=request.route_url('login'), + came_from=came_from, + login=login, + password=password, + ) + +@view_config(route_name='logout') +def logout(request): + headers = forget(request) + next_url = request.route_url('view_wiki') + return HTTPFound(location=next_url, headers=headers) + +@forbidden_view_config() +def forbidden_view(request): + next_url = request.route_url('login', _query={'came_from': request.url}) + return HTTPFound(location=next_url) diff --git a/docs/tutorials/wiki2/src/tests/tutorial/views/default.py b/docs/tutorials/wiki2/src/tests/tutorial/views/default.py index bc8a59fe8..6fb3c8744 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/views/default.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/views/default.py @@ -6,14 +6,9 @@ from pyramid.httpexceptions import ( HTTPFound, HTTPNotFound, ) -from pyramid.security import ( - remember, - forget, - ) from pyramid.view import view_config from ..models import Page -from ..security.default import USERS # regular expression used to find WikiWords wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)") @@ -73,35 +68,3 @@ def edit_page(request): page=page, save_url=request.route_url('edit_page', pagename=pagename), ) - -@view_config(route_name='login', renderer='templates/login.jinja2') -def login(request): - login_url = request.route_url('login') - referrer = request.url - if referrer == login_url: - referrer = '/' # never use the login form itself as came_from - came_from = request.params.get('came_from', referrer) - message = '' - login = '' - password = '' - if 'form.submitted' in request.params: - login = request.params['login'] - password = request.params['password'] - if USERS.get(login) == password: - headers = remember(request, login) - return HTTPFound(location=came_from, headers=headers) - message = 'Failed login' - - return dict( - message=message, - url=request.route_url('login'), - came_from=came_from, - login=login, - password=password, - ) - -@view_config(route_name='logout') -def logout(request): - headers = forget(request) - next_url = request.route_url('view_wiki') - return HTTPFound(location=next_url, headers=headers) diff --git a/docs/tutorials/wiki2/src/tests/tutorial/views/errors.py b/docs/tutorials/wiki2/src/tests/tutorial/views/errors.py deleted file mode 100644 index f8dbe4e05..000000000 --- a/docs/tutorials/wiki2/src/tests/tutorial/views/errors.py +++ /dev/null @@ -1,14 +0,0 @@ -from pyramid.httpexceptions import HTTPFound -from pyramid.view import ( - forbidden_view_config, - notfound_view_config, -) - -@notfound_view_config(renderer='../templates/404.jinja2') -def notfound_view(request): - return {} - -@forbidden_view_config() -def forbidden_view(request): - next_url = request.route_url('login', _query={'came_from': request.url}) - return HTTPFound(location=next_url) diff --git a/docs/tutorials/wiki2/src/tests/tutorial/views/notfound.py b/docs/tutorials/wiki2/src/tests/tutorial/views/notfound.py new file mode 100644 index 000000000..69d6e2804 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/views/notfound.py @@ -0,0 +1,7 @@ +from pyramid.view import notfound_view_config + + +@notfound_view_config(renderer='../templates/404.jinja2') +def notfound_view(request): + request.response.status = 404 + return {} diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/404.jinja2 b/docs/tutorials/wiki2/src/views/tutorial/templates/404.jinja2 index 1917f83c7..37b0a16b6 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/templates/404.jinja2 +++ b/docs/tutorials/wiki2/src/views/tutorial/templates/404.jinja2 @@ -2,7 +2,7 @@ {% block content %}
-

Pyramid Alchemy scaffold

+

Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki)

404 Page Not Found

{% endblock content %} diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/edit.jinja2 b/docs/tutorials/wiki2/src/views/tutorial/templates/edit.jinja2 index a41d232e5..e47b3aabf 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/templates/edit.jinja2 +++ b/docs/tutorials/wiki2/src/views/tutorial/templates/edit.jinja2 @@ -1,68 +1,20 @@ - - - - - - - - - - - Edit{% if page.name %} {{page.name}}{% endif %} - Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki) - - - - - - - - - - - - - -
-
-
-
- -
-
-
-

- Editing {% if page.name %}{{page.name}}{% else %}Page Name Goes Here{% endif %} -

-

You can return to the - FrontPage. -

- -
- -
-
- -
- -
-
-
-
- -
-
-
- - - - - - - - +{% extends 'layout.jinja2' %} + +{% block title %}Edit {{page.name}} - {% endblock title %} + +{% block content %} +

+Editing {% if page.name %}{{page.name}}{% else %}Page Name Goes Here{% endif %} +

+

You can return to the +FrontPage. +

+
+
+ +
+
+ +
+
+{% endblock content %} diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/layout.jinja2 b/docs/tutorials/wiki2/src/views/tutorial/templates/layout.jinja2 index ff624c65b..68743e0df 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/templates/layout.jinja2 +++ b/docs/tutorials/wiki2/src/views/tutorial/templates/layout.jinja2 @@ -8,7 +8,7 @@ - Alchemy Scaffold for The Pyramid Web Framework + {% block title %}{% if page.name %} {{page.name}} - {% endif %}{% endblock title %}Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki) @@ -32,20 +32,9 @@
- {% block content %} -

No content

- {% endblock content %} -
-
-
-
diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/mytemplate.jinja2 b/docs/tutorials/wiki2/src/views/tutorial/templates/mytemplate.jinja2 deleted file mode 100644 index bb622bf5a..000000000 --- a/docs/tutorials/wiki2/src/views/tutorial/templates/mytemplate.jinja2 +++ /dev/null @@ -1,8 +0,0 @@ -{% extends "layout.jinja2" %} - -{% block content %} -
-

Pyramid Alchemy scaffold

-

Welcome to {{project}}, an application generated by
the Pyramid Web Framework 1.7.dev0.

-
-{% endblock content %} diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/view.jinja2 b/docs/tutorials/wiki2/src/views/tutorial/templates/view.jinja2 index fa09baf70..c582ce1f9 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/templates/view.jinja2 +++ b/docs/tutorials/wiki2/src/views/tutorial/templates/view.jinja2 @@ -1,66 +1,16 @@ - - - - - - - - - - - {{page.name}} - Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki) - - - - - - - - - - - - - -
-
-
-
- -
-
-
-

{{ content|safe }}

-

- - Edit this page - -

-

- Viewing {% if page.name %}{{page.name}}{% else %}Page Name Goes Here{% endif %} -

-

You can return to the - FrontPage. -

-
-
-
-
- -
-
-
- - - - - - - - +{% extends 'layout.jinja2' %} + +{% block content %} +

{{ content|safe }}

+

+ + Edit this page + +

+

+ Viewing {% if page.name %}{{page.name}}{% else %}Page Name Goes Here{% endif %} +

+

You can return to the +FrontPage. +

+{% endblock content %} diff --git a/docs/tutorials/wiki2/src/views/tutorial/views/errors.py b/docs/tutorials/wiki2/src/views/tutorial/views/errors.py deleted file mode 100644 index a4b8201f1..000000000 --- a/docs/tutorials/wiki2/src/views/tutorial/views/errors.py +++ /dev/null @@ -1,5 +0,0 @@ -from pyramid.view import notfound_view_config - -@notfound_view_config(renderer='../templates/404.jinja2') -def notfound_view(request): - return {} diff --git a/docs/tutorials/wiki2/src/views/tutorial/views/notfound.py b/docs/tutorials/wiki2/src/views/tutorial/views/notfound.py new file mode 100644 index 000000000..69d6e2804 --- /dev/null +++ b/docs/tutorials/wiki2/src/views/tutorial/views/notfound.py @@ -0,0 +1,7 @@ +from pyramid.view import notfound_view_config + + +@notfound_view_config(renderer='../templates/404.jinja2') +def notfound_view(request): + request.response.status = 404 + return {} diff --git a/pyramid/scaffolds/alchemy/+package+/views/errors.py_tmpl b/pyramid/scaffolds/alchemy/+package+/views/errors.py_tmpl deleted file mode 100644 index a4b8201f1..000000000 --- a/pyramid/scaffolds/alchemy/+package+/views/errors.py_tmpl +++ /dev/null @@ -1,5 +0,0 @@ -from pyramid.view import notfound_view_config - -@notfound_view_config(renderer='../templates/404.jinja2') -def notfound_view(request): - return {} diff --git a/pyramid/scaffolds/alchemy/+package+/views/notfound.py_tmpl b/pyramid/scaffolds/alchemy/+package+/views/notfound.py_tmpl new file mode 100644 index 000000000..69d6e2804 --- /dev/null +++ b/pyramid/scaffolds/alchemy/+package+/views/notfound.py_tmpl @@ -0,0 +1,7 @@ +from pyramid.view import notfound_view_config + + +@notfound_view_config(renderer='../templates/404.jinja2') +def notfound_view(request): + request.response.status = 404 + return {} -- cgit v1.2.3 From e01b270c451ce6f23b53181ad79b430666ebe003 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Wed, 10 Feb 2016 23:45:32 -0600 Subject: explain the base layout.jinja2 template and notfound view --- docs/tutorials/wiki2/definingviews.rst | 81 +++++++++++++++++++--- .../wiki2/src/views/tutorial/views/default.py | 2 +- 2 files changed, 74 insertions(+), 9 deletions(-) diff --git a/docs/tutorials/wiki2/definingviews.rst b/docs/tutorials/wiki2/definingviews.rst index 4bc7f461b..6629839f8 100644 --- a/docs/tutorials/wiki2/definingviews.rst +++ b/docs/tutorials/wiki2/definingviews.rst @@ -73,7 +73,7 @@ edit it to look like the following: .. literalinclude:: src/views/tutorial/views/default.py :linenos: :language: python - :emphasize-lines: 1-9,12-70 + :emphasize-lines: 1-9,12-68 The highlighted lines need to be added or edited. @@ -241,6 +241,26 @@ These templates will live in the ``templates`` directory of our tutorial package. Jinja2 templates must have a ``.jinja2`` extension to be recognized as such. +The ``layout.jinja2`` template +------------------------------ + +Replace ``tutorial/templates/layout.jinja2`` with the following content: + +.. literalinclude:: src/views/tutorial/templates/layout.jinja2 + :linenos: + :emphasize-lines: 11,36 + :language: html + +Since we're using a templating engine we can factor common boilerplate out of +our page templates into reusable components. One method for doing this +is template inheritance via blocks. + +- We have defined 2 placeholders in the layout template where a child template + can override the content. These blocks are named ``title`` (line 11) and + ``content`` (line 36). +- Please refer to the Jinja2_ documentation for more information about + template inheritance. + The ``view.jinja2`` template ---------------------------- @@ -249,17 +269,21 @@ content: .. literalinclude:: src/views/tutorial/templates/view.jinja2 :linenos: - :emphasize-lines: 36,38-40 + :emphasize-lines: 1,4,6-8 :language: html This template is used by ``view_page()`` for displaying a single wiki page. It includes: +- We begin by extending the ``layout.jinja2`` template defined above + which provides the skeleton of the page (line 1). +- We override the ``content`` block from the base layout to insert our markup + into the body (line 3). - A variable that is replaced with the ``content`` value provided by the view - (line 36). ``content`` contains HTML, so the ``|safe`` filter is used to + (line 4). ``content`` contains HTML, so the ``|safe`` filter is used to prevent escaping it (e.g., changing ">" to ">"). - A link that points at the "edit" URL which invokes the ``edit_page`` view for - the page being viewed (lines 38-40). + the page being viewed (lines 6-8). The ``edit.jinja2`` template ---------------------------- @@ -269,18 +293,57 @@ content: .. literalinclude:: src/views/tutorial/templates/edit.jinja2 :linenos: - :emphasize-lines: 42,44,47 + :emphasize-lines: 3,12,14,17 :language: html This template is used by ``add_page()`` and ``edit_page()`` for adding and editing a wiki page. It displays a page containing a form that includes: +- Again we are extending the ``layout.jinja2`` template which provides + the skeleton of the page. +- Override the ``title`` block to affect the ```` tag in the + ``head`` of the page (line 3). - A 10-row by 60-column ``textarea`` field named ``body`` that is filled with - any existing page data when it is rendered (line 44). -- A submit button that has the name ``form.submitted`` (line 47). + any existing page data when it is rendered (line 14). +- A submit button that has the name ``form.submitted`` (line 17). The form POSTs back to the ``save_url`` argument supplied by the view (line -42). The view will use the ``body`` and ``form.submitted`` values. +12). The view will use the ``body`` and ``form.submitted`` values. + +The ``404.jinja2`` template +--------------------------- + +Replace ``tutorial/templates/404.jinja2`` with the following content: + +.. literalinclude:: src/views/tutorial/templates/404.jinja2 + :linenos: + :language: html + +This template is linked from the ``notfound_view`` defined in +``tutorial/views/notfound.py`` as shown here: + +.. literalinclude:: src/views/tutorial/views/notfound.py + :linenos: + :language: python + +There are several important things to note about this configuration: + +- The ``notfound_view`` in the above snippet is called an + :term:`exception view`. For more information see + :ref:`special_exceptions_in_callables`. +- The ``notfound_view`` sets the response status to 404. It's possible to + affect the response object used by the renderer via + :ref:`request_response_attr`. +- The ``notfound_view`` is registered as an exception view and will be invoked + **only** if ``pyramid.httpexceptions.HTTPNotFound`` is raised as an + exception. This means it will not be invoked for any responses returned + from a view normally. For example, on line 27 of + ``tutorial/views/default.py`` the exception is raised which will trigger + the view. + +Finally, you may delete the ``tutorial/templates/mytemplate.jinja2`` +template that was provided by the ``alchemy`` scaffold as we have created +our own templates for the wiki. .. note:: @@ -373,3 +436,5 @@ each of the following URLs, checking that the result is as expected: will generate a ``NoResultFound: No row was found for one()`` error. You'll see an interactive traceback facility provided by :term:`pyramid_debugtoolbar`. + +.. _jinja2: http://jinja.pocoo.org/ diff --git a/docs/tutorials/wiki2/src/views/tutorial/views/default.py b/docs/tutorials/wiki2/src/views/tutorial/views/default.py index 96df85a97..ca37f39f5 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/views/default.py +++ b/docs/tutorials/wiki2/src/views/tutorial/views/default.py @@ -24,7 +24,7 @@ def view_page(request): pagename = request.matchdict['pagename'] page = request.dbsession.query(Page).filter_by(name=pagename).first() if page is None: - return HTTPNotFound('No such page') + raise HTTPNotFound('No such page') def check(match): word = match.group(1) -- cgit v1.2.3 From 9a7cfe3b4e248451750f5694255450bf1983e848 Mon Sep 17 00:00:00 2001 From: Michael Merickel <michael@merickel.org> Date: Wed, 10 Feb 2016 23:54:51 -0600 Subject: update 404 templates --- docs/tutorials/wiki2/src/authorization/tutorial/templates/404.jinja2 | 2 +- docs/tutorials/wiki2/src/tests/tutorial/templates/404.jinja2 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/404.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/404.jinja2 index 1917f83c7..37b0a16b6 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/404.jinja2 +++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/404.jinja2 @@ -2,7 +2,7 @@ {% block content %} <div class="content"> - <h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Alchemy scaffold</span></h1> + <h1><span class="font-semi-bold">Pyramid tutorial wiki</span> <span class="smaller">(based on TurboGears 20-Minute Wiki)</span></h1> <p class="lead"><span class="font-semi-bold">404</span> Page Not Found</p> </div> {% endblock content %} diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/404.jinja2 b/docs/tutorials/wiki2/src/tests/tutorial/templates/404.jinja2 index 1917f83c7..37b0a16b6 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/templates/404.jinja2 +++ b/docs/tutorials/wiki2/src/tests/tutorial/templates/404.jinja2 @@ -2,7 +2,7 @@ {% block content %} <div class="content"> - <h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Alchemy scaffold</span></h1> + <h1><span class="font-semi-bold">Pyramid tutorial wiki</span> <span class="smaller">(based on TurboGears 20-Minute Wiki)</span></h1> <p class="lead"><span class="font-semi-bold">404</span> Page Not Found</p> </div> {% endblock content %} -- cgit v1.2.3 From f2e9c68e8168cfe51f7dc5ed86fea0471968f508 Mon Sep 17 00:00:00 2001 From: Michael Merickel <michael@merickel.org> Date: Wed, 10 Feb 2016 23:55:03 -0600 Subject: move security into one place --- .../wiki2/src/authorization/tutorial/__init__.py | 11 +---- .../src/authorization/tutorial/models/mymodel.py | 14 ------ .../wiki2/src/authorization/tutorial/security.py | 51 ++++++++++++++++++++++ .../authorization/tutorial/security/__init__.py | 0 .../src/authorization/tutorial/security/default.py | 12 ----- 5 files changed, 52 insertions(+), 36 deletions(-) create mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/security.py delete mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/security/__init__.py delete mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/security/default.py diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py b/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py index a62c42378..8eacdee5a 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py @@ -1,22 +1,13 @@ from pyramid.config import Configurator -from pyramid.authentication import AuthTktAuthenticationPolicy -from pyramid.authorization import ACLAuthorizationPolicy - -from .security.default import groupfinder def main(global_config, **settings): """ This function returns a Pyramid WSGI application. """ - authn_policy = AuthTktAuthenticationPolicy( - 'sosecret', callback=groupfinder, hashalg='sha512') - authz_policy = ACLAuthorizationPolicy() config = Configurator(settings=settings) config.include('pyramid_jinja2') config.include('.models') - config.set_root_factory('.models.mymodel.RootFactory') - config.set_authentication_policy(authn_policy) - config.set_authorization_policy(authz_policy) + config.include('.security') config.add_static_view('static', 'static', cache_max_age=3600) config.add_route('view_wiki', '/') config.add_route('login', '/login') diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models/mymodel.py b/docs/tutorials/wiki2/src/authorization/tutorial/models/mymodel.py index 25209c745..b23d0c0d2 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/models/mymodel.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/models/mymodel.py @@ -1,7 +1,3 @@ -from pyramid.security import ( - Allow, - Everyone, -) from sqlalchemy import ( Column, Integer, @@ -17,13 +13,3 @@ class Page(Base): id = Column(Integer, primary_key=True) name = Column(Text, unique=True) data = Column(Integer) - - -class RootFactory(object): - __acl__ = [ - (Allow, Everyone, 'view'), - (Allow, 'group:editors', 'edit'), - ] - - def __init__(self, request): - pass diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/security.py b/docs/tutorials/wiki2/src/authorization/tutorial/security.py new file mode 100644 index 000000000..7bceabf3f --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/security.py @@ -0,0 +1,51 @@ +from pyramid.authentication import AuthTktAuthenticationPolicy +from pyramid.authorization import ACLAuthorizationPolicy + +from pyramid.security import ( + Allow, + Authenticated, + Everyone, +) + + +USERS = { + 'editor': 'editor', + 'viewer': 'viewer', +} + +GROUPS = { + 'editor': ['group:editors'], +} + +class MyAuthenticationPolicy(AuthTktAuthenticationPolicy): + def authenticated_userid(self, request): + userid = self.unauthenticated_userid(request) + if userid in USERS: + return userid + + def effective_principals(self, request): + principals = [Everyone] + userid = self.authenticated_userid(request) + if userid is not None: + principals.append(Authenticated) + principals.append(userid) + + groups = GROUPS.get(userid, []) + principals.extend(groups) + return principals + +class RootFactory(object): + __acl__ = [ + (Allow, Everyone, 'view'), + (Allow, 'group:editors', 'edit'), + ] + + def __init__(self, request): + pass + +def includeme(config): + authn_policy = MyAuthenticationPolicy('sosecret', hashalg='sha512') + authz_policy = ACLAuthorizationPolicy() + config.set_root_factory(RootFactory) + config.set_authentication_policy(authn_policy) + config.set_authorization_policy(authz_policy) diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/security/__init__.py b/docs/tutorials/wiki2/src/authorization/tutorial/security/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/security/default.py b/docs/tutorials/wiki2/src/authorization/tutorial/security/default.py deleted file mode 100644 index 7fc1ea7c8..000000000 --- a/docs/tutorials/wiki2/src/authorization/tutorial/security/default.py +++ /dev/null @@ -1,12 +0,0 @@ -USERS = { - 'editor': 'editor', - 'viewer': 'viewer', -} - -GROUPS = { - 'editor': ['group:editors'], -} - -def groupfinder(userid, request): - if userid in USERS: - return GROUPS.get(userid, []) -- cgit v1.2.3 From cb5a84802171ed22b67958c7733cc0eddc680d34 Mon Sep 17 00:00:00 2001 From: Michael Merickel <michael@merickel.org> Date: Thu, 11 Feb 2016 23:01:38 -0600 Subject: copy layout and templates from views to authorization --- .../authorization/tutorial/templates/edit.jinja2 | 93 +++++-------------- .../authorization/tutorial/templates/layout.jinja2 | 60 +++++++++++++ .../authorization/tutorial/templates/login.jinja2 | 100 ++++++--------------- .../authorization/tutorial/templates/view.jinja2 | 87 ++++-------------- .../src/authorization/tutorial/views/default.py | 2 +- .../src/views/tutorial/templates/layout.jinja2 | 5 ++ 6 files changed, 128 insertions(+), 219 deletions(-) create mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/templates/layout.jinja2 diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja2 index 4d767cfbe..e47b3aabf 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja2 +++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja2 @@ -1,73 +1,20 @@ -<!DOCTYPE html> -<html lang="{{request.locale_name}}"> - <head> - <meta charset="utf-8"> - <meta http-equiv="X-UA-Compatible" content="IE=edge"> - <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <meta name="description" content="pyramid web application"> - <meta name="author" content="Pylons Project"> - <link rel="shortcut icon" href="{{request.static_url('tutorial:static/pyramid-16x16.png')}}"> - - <title>Edit{% if page.name %} {{page.name}}{% endif %} - Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki) - - - - - - - - - - - - - -
-
-
-
- -
-
-
- {% if request.authenticated_userid is not none %} -

- Logout -

- {% endif %} -

- Editing {% if page.name %}{{page.name}}{% else %}Page Name Goes Here{% endif %} -

-

You can return to the - FrontPage. -

-
-
- -
-
- -
-
-
-
-
-
- -
-
-
- - - - - - - - +{% extends 'layout.jinja2' %} + +{% block title %}Edit {{page.name}} - {% endblock title %} + +{% block content %} +

+Editing {% if page.name %}{{page.name}}{% else %}Page Name Goes Here{% endif %} +

+

You can return to the +FrontPage. +

+
+
+ +
+
+ +
+
+{% endblock content %} diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/layout.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/layout.jinja2 new file mode 100644 index 000000000..82a144abf --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/layout.jinja2 @@ -0,0 +1,60 @@ + + + + + + + + + + + {% block title %}{% if page.name %} {{page.name}} - {% endif %}{% endblock title %}Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki) + + + + + + + + + + + + + +
+
+
+
+ +
+
+
+ {% if request.authenticated_userid is not none %} +

+ Logout +

+ {% endif %} + {% block content %}{% endblock %} +
+
+
+
+ +
+
+
+ + + + + + + + diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.jinja2 index a80a2a165..99d369173 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.jinja2 +++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.jinja2 @@ -1,74 +1,26 @@ - - - - - - - - - - - Login - Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki) - - - - - - - - - - - - - -
-
-
-
- -
-
-
-

- - Login -
- {{ message }} -

-
- -
- - -
-
- - -
-
- -
-
-
-
-
-
- -
-
-
- - - - - - - - +{% extends 'layout.jinja2' %} + +{% block title %}Login - {% endblock title %} + +{% block content %} +

+ + Login +
+{{ message }} +

+
+ +
+ + +
+
+ + +
+
+ +
+
+{% endblock content %} diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.jinja2 index 942b8479b..c582ce1f9 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.jinja2 +++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.jinja2 @@ -1,71 +1,16 @@ - - - - - - - - - - - {{page.name}} - Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki) - - - - - - - - - - - - - -
-
-
-
- -
-
-
- {% if request.authenticated_userid is not none %} -

- Logout -

- {% endif %} -

{{ content|safe }}

-

- - Edit this page - -

-

- Viewing {% if page.name %}{{page.name}}{% else %}Page Name Goes Here{% endif %} -

-

You can return to the - FrontPage. -

-
-
-
-
- -
-
-
- - - - - - - - +{% extends 'layout.jinja2' %} + +{% block content %} +

{{ content|safe }}

+

+ + Edit this page + +

+

+ Viewing {% if page.name %}{{page.name}}{% else %}Page Name Goes Here{% endif %} +

+

You can return to the +FrontPage. +

+{% endblock content %} diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py b/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py index 6fb3c8744..e152e73e0 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py @@ -24,7 +24,7 @@ def view_page(request): pagename = request.matchdict['pagename'] page = request.dbsession.query(Page).filter_by(name=pagename).first() if page is None: - return HTTPNotFound('No such page') + raise HTTPNotFound('No such page') def check(match): word = match.group(1) diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/layout.jinja2 b/docs/tutorials/wiki2/src/views/tutorial/templates/layout.jinja2 index 68743e0df..82a144abf 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/templates/layout.jinja2 +++ b/docs/tutorials/wiki2/src/views/tutorial/templates/layout.jinja2 @@ -33,6 +33,11 @@
+ {% if request.authenticated_userid is not none %} +

+ Logout +

+ {% endif %} {% block content %}{% endblock %}
-- cgit v1.2.3 From 81e5989ed5b2bd7ea1a2b843dea9726b253b38ce Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 12 Feb 2016 00:18:40 -0600 Subject: create an actual user model to prepare for security --- .../src/authorization/tutorial/views/default.py | 3 ++- docs/tutorials/wiki2/src/models/setup.py | 1 + .../wiki2/src/models/tutorial/models/__init__.py | 3 ++- .../wiki2/src/models/tutorial/models/mymodel.py | 15 ------------ .../wiki2/src/models/tutorial/models/page.py | 20 ++++++++++++++++ .../wiki2/src/models/tutorial/models/user.py | 28 ++++++++++++++++++++++ .../src/models/tutorial/scripts/initializedb.py | 16 +++++++++++-- 7 files changed, 67 insertions(+), 19 deletions(-) delete mode 100644 docs/tutorials/wiki2/src/models/tutorial/models/mymodel.py create mode 100644 docs/tutorials/wiki2/src/models/tutorial/models/page.py create mode 100644 docs/tutorials/wiki2/src/models/tutorial/models/user.py diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py b/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py index e152e73e0..f74059be0 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py @@ -10,6 +10,7 @@ from pyramid.view import view_config from ..models import Page + # regular expression used to find WikiWords wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)") @@ -42,7 +43,7 @@ def view_page(request): return dict(page=page, content=content, edit_url=edit_url) @view_config(route_name='add_page', renderer='../templates/edit.jinja2', - permission='edit') + permission='create') def add_page(request): pagename = request.matchdict['pagename'] if 'form.submitted' in request.params: diff --git a/docs/tutorials/wiki2/src/models/setup.py b/docs/tutorials/wiki2/src/models/setup.py index eb771010f..df9fec4d4 100644 --- a/docs/tutorials/wiki2/src/models/setup.py +++ b/docs/tutorials/wiki2/src/models/setup.py @@ -9,6 +9,7 @@ with open(os.path.join(here, 'CHANGES.txt')) as f: CHANGES = f.read() requires = [ + 'bcrypt', 'pyramid', 'pyramid_jinja2', 'pyramid_debugtoolbar', diff --git a/docs/tutorials/wiki2/src/models/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/models/tutorial/models/__init__.py index 3d3efe06f..a8871f6f5 100644 --- a/docs/tutorials/wiki2/src/models/tutorial/models/__init__.py +++ b/docs/tutorials/wiki2/src/models/tutorial/models/__init__.py @@ -5,7 +5,8 @@ import zope.sqlalchemy # import or define all models here to ensure they are attached to the # Base.metadata prior to any initialization routines -from .mymodel import Page # flake8: noqa +from .page import Page # flake8: noqa +from .user import User # flake8: noqa # run configure_mappers after defining all of the models to ensure # all relationships can be setup diff --git a/docs/tutorials/wiki2/src/models/tutorial/models/mymodel.py b/docs/tutorials/wiki2/src/models/tutorial/models/mymodel.py deleted file mode 100644 index b23d0c0d2..000000000 --- a/docs/tutorials/wiki2/src/models/tutorial/models/mymodel.py +++ /dev/null @@ -1,15 +0,0 @@ -from sqlalchemy import ( - Column, - Integer, - Text, -) - -from .meta import Base - - -class Page(Base): - """ The SQLAlchemy declarative model class for a Page object. """ - __tablename__ = 'pages' - id = Column(Integer, primary_key=True) - name = Column(Text, unique=True) - data = Column(Integer) diff --git a/docs/tutorials/wiki2/src/models/tutorial/models/page.py b/docs/tutorials/wiki2/src/models/tutorial/models/page.py new file mode 100644 index 000000000..4dd5b5721 --- /dev/null +++ b/docs/tutorials/wiki2/src/models/tutorial/models/page.py @@ -0,0 +1,20 @@ +from sqlalchemy import ( + Column, + ForeignKey, + Integer, + Text, +) +from sqlalchemy.orm import relationship + +from .meta import Base + + +class Page(Base): + """ The SQLAlchemy declarative model class for a Page object. """ + __tablename__ = 'pages' + id = Column(Integer, primary_key=True) + name = Column(Text, nullable=False, unique=True) + data = Column(Integer, nullable=False) + + creator_id = Column(ForeignKey('users.id'), nullable=False) + creator = relationship('User', backref='created_pages') diff --git a/docs/tutorials/wiki2/src/models/tutorial/models/user.py b/docs/tutorials/wiki2/src/models/tutorial/models/user.py new file mode 100644 index 000000000..6123a3aad --- /dev/null +++ b/docs/tutorials/wiki2/src/models/tutorial/models/user.py @@ -0,0 +1,28 @@ +import bcrypt +from sqlalchemy import ( + Column, + Integer, + Text, +) + +from .meta import Base + + +class User(Base): + """ The SQLAlchemy declarative model class for a User object. """ + __tablename__ = 'users' + id = Column(Integer, primary_key=True) + name = Column(Text, nullable=False, unique=True) + role = Column(Text, nullable=False) + + password_hash = Column(Text) + + def set_password(self, pw, pre_hashed=False): + if pre_hashed: + pwhash = pw + else: + pwhash = bcrypt.hashpw(pw, bcrypt.gensalt()) + self.password_hash = pwhash + + def check_password(self, pw): + return bcrypt.hashpw(pw, self.password_hash) == self.password_hash diff --git a/docs/tutorials/wiki2/src/models/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/models/tutorial/scripts/initializedb.py index 601a6e73f..175b7190f 100644 --- a/docs/tutorials/wiki2/src/models/tutorial/scripts/initializedb.py +++ b/docs/tutorials/wiki2/src/models/tutorial/scripts/initializedb.py @@ -15,7 +15,7 @@ from ..models import ( get_session_factory, get_tm_session, ) -from ..models import Page +from ..models import Page, User def usage(argv): @@ -41,5 +41,17 @@ def main(argv=sys.argv): with transaction.manager: dbsession = get_tm_session(session_factory, transaction.manager) - page = Page(name='FrontPage', data='This is the front page') + editor = User(name='editor', role='editor') + editor.set_password('editor') + dbsession.add(editor) + + viewer = User(name='viewer', role='viewer') + viewer.set_password('viewer') + dbsession.add(viewer) + + page = Page( + name='FrontPage', + creator=editor, + data='This is the front page', + ) dbsession.add(page) -- cgit v1.2.3 From e6e4f655f2abe8d1d5ff63ecd70255094af6de73 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 12 Feb 2016 01:09:01 -0600 Subject: let's go ahead and bite off more than we can chew by adding object-security we'll allow anyone to create pages, not just editors finally we'll allow page creators of pages to edit their pages even if they are not editors --- docs/tutorials/wiki2/definingmodels.rst | 65 +++++++++++------ docs/tutorials/wiki2/design.rst | 82 +++++++++++++--------- .../src/models/tutorial/scripts/initializedb.py | 6 +- 3 files changed, 94 insertions(+), 59 deletions(-) diff --git a/docs/tutorials/wiki2/definingmodels.rst b/docs/tutorials/wiki2/definingmodels.rst index b90bf77e6..5af8110da 100644 --- a/docs/tutorials/wiki2/definingmodels.rst +++ b/docs/tutorials/wiki2/definingmodels.rst @@ -106,28 +106,49 @@ made to both the models.py file and to the initializedb.py file. See Success will look something like this:: - 2015-05-24 15:34:14,542 INFO [sqlalchemy.engine.base.Engine:1192][MainThread] SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1 - 2015-05-24 15:34:14,542 INFO [sqlalchemy.engine.base.Engine:1193][MainThread] () - 2015-05-24 15:34:14,543 INFO [sqlalchemy.engine.base.Engine:1192][MainThread] SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1 - 2015-05-24 15:34:14,543 INFO [sqlalchemy.engine.base.Engine:1193][MainThread] () - 2015-05-24 15:34:14,543 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] PRAGMA table_info("pages") - 2015-05-24 15:34:14,544 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] () - 2015-05-24 15:34:14,544 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] - CREATE TABLE pages ( - id INTEGER NOT NULL, - name TEXT, - data TEXT, - PRIMARY KEY (id), - UNIQUE (name) - ) - - - 2015-05-24 15:34:14,545 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] () - 2015-05-24 15:34:14,546 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT - 2015-05-24 15:34:14,548 INFO [sqlalchemy.engine.base.Engine:646][MainThread] BEGIN (implicit) - 2015-05-24 15:34:14,549 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] INSERT INTO pages (name, data) VALUES (?, ?) - 2015-05-24 15:34:14,549 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ('FrontPage', 'This is the front page') - 2015-05-24 15:34:14,550 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT + 2016-02-12 01:06:35,855 INFO [sqlalchemy.engine.base.Engine:1192][MainThread] SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1 + 2016-02-12 01:06:35,855 INFO [sqlalchemy.engine.base.Engine:1193][MainThread] () + 2016-02-12 01:06:35,855 INFO [sqlalchemy.engine.base.Engine:1192][MainThread] SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1 + 2016-02-12 01:06:35,855 INFO [sqlalchemy.engine.base.Engine:1193][MainThread] () + 2016-02-12 01:06:35,856 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] PRAGMA table_info("pages") + 2016-02-12 01:06:35,856 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] () + 2016-02-12 01:06:35,856 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] PRAGMA table_info("users") + 2016-02-12 01:06:35,856 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] () + 2016-02-12 01:06:35,857 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] + CREATE TABLE users ( + id INTEGER NOT NULL, + name TEXT NOT NULL, + role TEXT NOT NULL, + password_hash TEXT, + CONSTRAINT pk_users PRIMARY KEY (id), + CONSTRAINT uq_users_name UNIQUE (name) + ) + + + 2016-02-12 01:06:35,857 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] () + 2016-02-12 01:06:35,858 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT + 2016-02-12 01:06:35,858 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] + CREATE TABLE pages ( + id INTEGER NOT NULL, + name TEXT NOT NULL, + data INTEGER NOT NULL, + creator_id INTEGER NOT NULL, + CONSTRAINT pk_pages PRIMARY KEY (id), + CONSTRAINT uq_pages_name UNIQUE (name), + CONSTRAINT fk_pages_creator_id_users FOREIGN KEY(creator_id) REFERENCES users (id) + ) + + + 2016-02-12 01:06:35,859 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] () + 2016-02-12 01:06:35,859 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT + 2016-02-12 01:06:36,383 INFO [sqlalchemy.engine.base.Engine:646][MainThread] BEGIN (implicit) + 2016-02-12 01:06:36,384 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] INSERT INTO users (name, role, password_hash) VALUES (?, ?, ?) + 2016-02-12 01:06:36,384 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ('editor', 'editor', '$2b$12$bSr5QR3wFs1LAnld7R94e.TXPj7DVoTxu2hA1kY6rm.Q3cAhD.AQO') + 2016-02-12 01:06:36,384 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] INSERT INTO users (name, role, password_hash) VALUES (?, ?, ?) + 2016-02-12 01:06:36,384 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ('basic', 'basic', '$2b$12$.v0BQK2xWEQOnywbX2BFs.qzXo5Qf9oZohGWux/MOSj6Z.pVaY2Z6') + 2016-02-12 01:06:36,385 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] INSERT INTO pages (name, data, creator_id) VALUES (?, ?, ?) + 2016-02-12 01:06:36,385 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ('FrontPage', 'This is the front page', 1) + 2016-02-12 01:06:36,385 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT View the application in a browser --------------------------------- diff --git a/docs/tutorials/wiki2/design.rst b/docs/tutorials/wiki2/design.rst index 8e3bb4c13..42f06f7bf 100644 --- a/docs/tutorials/wiki2/design.rst +++ b/docs/tutorials/wiki2/design.rst @@ -19,11 +19,17 @@ Models We'll be using an SQLite database to hold our wiki data, and we'll be using :term:`SQLAlchemy` to access the data in this database. -Within the database, we define a single table named `pages`, whose elements -will store the wiki pages. There are two columns: `name` and `data`. +Within the database, we will define two tables: -URLs like ``/PageName`` will try to find an element in the table that has a -corresponding name. +- The `users` table which will store the `name`, `password_hash` and `role`. +- The `pages` table, whose elements will store the wiki pages. + There are three columns: `name`, `data` and `creator_id`. + +There is a one-to-many relationship between `users` and `pages` tracking +the user who created each wiki page. + +URLs like ``/PageName`` will try to find an element in the `pages` table that +has a corresponding name. To add a page to the wiki, a new row is created and the text is stored in `data`. @@ -32,8 +38,8 @@ A page named ``FrontPage`` containing the text *This is the front page*, will be created when the storage is initialized, and will be used as the wiki home page. -Views ------ +Wiki Views +---------- There will be three views to handle the normal operations of adding, editing, and viewing wiki pages, plus one view for the wiki front page. Two templates @@ -47,33 +53,41 @@ templates. Security -------- -We'll eventually be adding security to our application. The components we'll -use to do this are below. - -- USERS, a dictionary mapping :term:`userids ` to their corresponding - passwords. - -- GROUPS, a dictionary mapping :term:`userids ` to a list of groups to - which they belong. - -- ``groupfinder``, an *authorization callback* that looks up USERS and GROUPS. - It will be provided in a new ``security/default.py`` subpackage and file. - -- An :term:`ACL` is attached to the root :term:`resource`. Each row below - details an :term:`ACE`: - - +----------+----------------+----------------+ - | Action | Principal | Permission | - +==========+================+================+ - | Allow | Everyone | View | - +----------+----------------+----------------+ - | Allow | group:editors | Edit | - +----------+----------------+----------------+ - -- Permission declarations are added to the views to assert the security - policies as each request is handled. - -Two additional views and one template will handle the login and logout tasks. +We'll eventually be adding security to our application. To do this, we'll +be using a very simple role-based security model. We'll assign a single +role category to each user in our system. + +`basic` + An authenticated user who can view content and create new pages. A `basic` + user may also edit the pages they have created but not pages created by + other users. + +`editor` + An authenticated user who can create and edit any content in the system. + +In order to accomplish this we'll need to define an authentication policy +which can identify users by their :term:`userid` and role. Then we'll +need to define a page :term:`resource` which contains the appropriate +:term:`ACL`: + ++----------+--------------------+----------------+ +| Action | Principal | Permission | ++==========+====================+================+ +| Allow | Everyone | view | ++----------+--------------------+----------------+ +| Allow | group:basic | create | ++----------+--------------------+----------------+ +| Allow | group:editors | edit | ++----------+--------------------+----------------+ +| Allow | | edit | ++----------+--------------------+----------------+ + +Permission declarations will be added to the views to assert the security +policies as each request is handled. + +On the security side of the application there are two additional views for +handling login and logout as well as two exception views for handling +invalid access attempts and unhandled URLs. Summary ------- @@ -102,7 +116,7 @@ in the following table: | | submitted, redirect | | | | | | to /PageName | | | | +----------------------+-----------------------+-------------+----------------+------------+ -| /add_page/PageName | Create the page | add_page | edit.jinja2 | edit | +| /add_page/PageName | Create the page | add_page | edit.jinja2 | create | | | *PageName* in | | | | | | storage, display | | | | | | the edit form | | | | diff --git a/docs/tutorials/wiki2/src/models/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/models/tutorial/scripts/initializedb.py index 175b7190f..f3c0a6fef 100644 --- a/docs/tutorials/wiki2/src/models/tutorial/scripts/initializedb.py +++ b/docs/tutorials/wiki2/src/models/tutorial/scripts/initializedb.py @@ -45,9 +45,9 @@ def main(argv=sys.argv): editor.set_password('editor') dbsession.add(editor) - viewer = User(name='viewer', role='viewer') - viewer.set_password('viewer') - dbsession.add(viewer) + basic = User(name='basic', role='basic') + basic.set_password('basic') + dbsession.add(basic) page = Page( name='FrontPage', -- cgit v1.2.3 From 574ba1aa6d81498220d123d149192eeba81afee7 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 12 Feb 2016 02:22:48 -0600 Subject: update the models chapter with the new user model --- docs/tutorials/wiki2/definingmodels.rst | 119 +++++++++++++-------- .../wiki2/src/models/tutorial/models/user.py | 11 +- 2 files changed, 82 insertions(+), 48 deletions(-) diff --git a/docs/tutorials/wiki2/definingmodels.rst b/docs/tutorials/wiki2/definingmodels.rst index 5af8110da..beb6cee5a 100644 --- a/docs/tutorials/wiki2/definingmodels.rst +++ b/docs/tutorials/wiki2/definingmodels.rst @@ -4,61 +4,84 @@ Defining the Domain Model The first change we'll make to our stock ``pcreate``-generated application will be to define a wiki page :term:`domain model`. -We'll do this inside our ``mymodel.py`` file. +.. note:: -Edit ``mymodel.py`` -------------------- + There is nothing special about the filename ``user.py`` or ``page.py`` except + that they are Python modules. A project may have many models throughout its + codebase in arbitrarily named modules. Modules implementing models often + have ``model`` in their names or they may live in a Python subpackage of + your application package named ``models`` (as we've done in this tutorial), + but this is only a convention and not a requirement. -.. note:: - There is nothing special about the filename ``mymodel.py`` except that it - is a Python module. A project may have many models throughout its codebase - in arbitrarily named modules. Modules implementing models often have - ``model`` in their names or they may live in a Python subpackage of your - application package named ``models`` (as we've done in this tutorial), but - this is only a convention and not a requirement. +Remove ``mymodel.py`` +--------------------- + +The first thing we'll do is delete the file ``tutorial/models/mymodel.py``. +The ``MyModel`` class is only a sample and we're not going to use it. -Open the ``tutorial/models/mymodel.py`` file and edit it to look like -the following: -.. literalinclude:: src/models/tutorial/models/mymodel.py +Add ``user.py`` +--------------- + +Create a new file ``tutorial/models/user.py`` with the following contents: + +.. literalinclude:: src/models/tutorial/models/user.py :linenos: :language: py - :emphasize-lines: 10-12,14-15 -The highlighted lines are the ones that need to be changed, as well as -removing lines that reference ``Index``. +This is a very basic model for a user who can authenticate with our wiki. + +We discussed briefly in the previous chapter that our models will inherit +from a SQLAlchemy :func:`sqlalchemy.ext.declarative.declarative_base`. This +will attach the model to our schema. + +As you can see, our ``User`` class has a class-level attribute +``__tablename__`` which equals the string ``users``. Our ``User`` class +will also have class-level attributes named ``id``, ``name``, +``password_hash`` and ``role`` (all instances of +:class:`sqlalchemy.schema.Column`). These will map to columns in the ``users`` +table. The ``id`` attribute will be the primary key in the table. The ``name`` +attribute will be a text column, each value of which needs to be unique within +the column. The ``password_hash`` is a nullable text attribute that will +contain a securely hashed password [1]_. Finally, the ``role`` text attribute +will hold the role of the user. -The first thing we've done is remove the stock ``MyModel`` class -from the generated ``models.py`` file. The ``MyModel`` class is only a -sample and we're not going to use it. +There are two helper methods that will help us later when using the +user objects. The first is ``set_password`` which will take a raw password +and transform it using bcrypt_ into an irreversible representation. The +``check_password`` method will allow us to compare input passwords to +see if they resolve to the same hash signifying a match. -Then we added a ``Page`` class. Because this is a SQLAlchemy application, -this class inherits from an instance of -:func:`sqlalchemy.ext.declarative.declarative_base`. -.. literalinclude:: src/models/tutorial/models/mymodel.py - :pyobject: Page +Add ``page.py`` +--------------- + +Create a new file ``tutorial/models/page.py`` with the following contents: + +.. literalinclude:: src/models/tutorial/models/page.py :linenos: - :language: python + :language: py -As you can see, our ``Page`` class has a class-level attribute -``__tablename__`` which equals the string ``'pages'``. This means that -SQLAlchemy will store our wiki data in a SQL table named ``pages``. Our -``Page`` class will also have class-level attributes named ``id``, ``name``, -and ``data`` (all instances of :class:`sqlalchemy.schema.Column`). These will -map to columns in the ``pages`` table. The ``id`` attribute will be the -primary key in the table. The ``name`` attribute will be a text attribute, -each value of which needs to be unique within the column. The ``data`` -attribute is a text attribute that will hold the body of each page. +As you can see, our ``Page`` class is very similar to the ``User`` defined +above except with attributes focused on storing information about a wiki +page including ``id``, ``name``, and ``data``. The only new construct +introduced here is the ``creator_id`` column which is a foreign key +referencing the ``users`` table. Foreign keys are very useful at the +schema-level but since we want to relate ``User`` objects with ``Page`` +objects we also define a the ``creator`` attribute which is an ORM-level +mapping between the two tables. SQLAlchemy will automatically populate this +value using the foreign key referencing the user. Since the foreign key +has ``nullable=False`` we are guaranteed that an instance of ``page`` will +have a corresponding ``page.creator`` which will be a ``User`` instance. Edit ``models/__init__.py`` --------------------------- Since we are using a package for our models, we also need to update our -``__init__.py`` file to ensure that the model is attached to the metadata. +``__init__.py`` file to ensure that the models are attached to the metadata. Open the ``tutorial/models/__init__.py`` file and edit it to look like the following: @@ -66,9 +89,10 @@ the following: .. literalinclude:: src/models/tutorial/models/__init__.py :linenos: :language: py - :emphasize-lines: 8 + :emphasize-lines: 8,9 -Here we need to align our import with the name of the model ``Page``. +Here we need to align our imports with the names of the models ``User``, +and ``Page``. Edit ``scripts/initializedb.py`` @@ -77,13 +101,13 @@ Edit ``scripts/initializedb.py`` We haven't looked at the details of this file yet, but within the ``scripts`` directory of your ``tutorial`` package is a file named ``initializedb.py``. Code in this file is executed whenever we run the ``initialize_tutorial_db`` -command, as we did in the installation step of this tutorial. +command, as we did in the installation step of this tutorial [2]_. Since we've changed our model, we need to make changes to our ``initializedb.py`` script. In particular, we'll replace our import of -``MyModel`` with one of ``Page`` and we'll change the very end of the script -to create a ``Page`` rather than a ``MyModel`` and add it to our -``dbsession``. +``MyModel`` with those of ``User`` and ``Page`` and we'll change the very end +of the script to create two ``User`` objects (``basic`` and ``editor``) and a +``Page`` rather than a ``MyModel`` and add them to our ``dbsession``. Open ``tutorial/scripts/initializedb.py`` and edit it to look like the following: @@ -91,7 +115,7 @@ the following: .. literalinclude:: src/models/tutorial/scripts/initializedb.py :linenos: :language: python - :emphasize-lines: 18,44-45 + :emphasize-lines: 18,44-57 Only the highlighted lines need to be changed. @@ -164,3 +188,14 @@ up with a Python traceback on your console that ends with this exception: ImportError: cannot import name MyModel This will also happen if you attempt to run the tests. + +.. _bcrypt: https://pypi.python.org/pypi/bcrypt + +.. [1] We are using the bcrypt_ package from PyPI to hash our passwords + securely. There are other one-way hash algorithms for passwords if + bcrypt is an issue on your system. Just make sure that it's an + algorithm approved for storing passwords versus a generic one-way hash. + +.. [2] The command is named ``initialize_tutorial_db`` because of the mapping + defined in the ``[console_scripts]`` entry point of our project's + ``setup.py`` file. diff --git a/docs/tutorials/wiki2/src/models/tutorial/models/user.py b/docs/tutorials/wiki2/src/models/tutorial/models/user.py index 6123a3aad..25b0a8187 100644 --- a/docs/tutorials/wiki2/src/models/tutorial/models/user.py +++ b/docs/tutorials/wiki2/src/models/tutorial/models/user.py @@ -17,12 +17,11 @@ class User(Base): password_hash = Column(Text) - def set_password(self, pw, pre_hashed=False): - if pre_hashed: - pwhash = pw - else: - pwhash = bcrypt.hashpw(pw, bcrypt.gensalt()) + def set_password(self, pw): + pwhash = bcrypt.hashpw(pw, bcrypt.gensalt()) self.password_hash = pwhash def check_password(self, pw): - return bcrypt.hashpw(pw, self.password_hash) == self.password_hash + if self.password_hash is not None: + return bcrypt.hashpw(pw, self.password_hash) == self.password_hash + return False -- cgit v1.2.3 From a115c6d30fe8e497f67604370db4ffc8f2b124a9 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 12 Feb 2016 02:42:04 -0600 Subject: add the bcrypt dependency --- docs/tutorials/wiki2/definingmodels.rst | 22 ++++++++++++++++++++++ docs/tutorials/wiki2/design.rst | 8 +++++--- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/docs/tutorials/wiki2/definingmodels.rst b/docs/tutorials/wiki2/definingmodels.rst index beb6cee5a..33e7beb4f 100644 --- a/docs/tutorials/wiki2/definingmodels.rst +++ b/docs/tutorials/wiki2/definingmodels.rst @@ -15,6 +15,28 @@ be to define a wiki page :term:`domain model`. but this is only a convention and not a requirement. +Declaring dependencies in our ``setup.py`` file +=============================================== + +The models code in our application will depend on a package which is not a +dependency of the original "tutorial" application. The original "tutorial" +application was generated by the ``pcreate`` command; it doesn't know +about our custom application requirements. + +We need to add a dependency on the ``bcrypt`` package to our ``tutorial`` +package's ``setup.py`` file by assigning this dependency to the ``requires`` +parameter in the ``setup()`` function. + +Open ``tutorial/setup.py`` and edit it to look like the following: + +.. literalinclude:: src/models/setup.py + :linenos: + :emphasize-lines: 12 + :language: python + +Only the highlighted line needs to be added. + + Remove ``mymodel.py`` --------------------- diff --git a/docs/tutorials/wiki2/design.rst b/docs/tutorials/wiki2/design.rst index 42f06f7bf..de43447d3 100644 --- a/docs/tutorials/wiki2/design.rst +++ b/docs/tutorials/wiki2/design.rst @@ -21,12 +21,14 @@ We'll be using an SQLite database to hold our wiki data, and we'll be using Within the database, we will define two tables: -- The `users` table which will store the `name`, `password_hash` and `role`. +- The `users` table which will store the `id`, `name`, `password_hash` and + `role` of each wiki user. - The `pages` table, whose elements will store the wiki pages. - There are three columns: `name`, `data` and `creator_id`. + There are four columns: `id`, `name`, `data` and `creator_id`. There is a one-to-many relationship between `users` and `pages` tracking -the user who created each wiki page. +the user who created each wiki page defined by the `creator_id` column on the +`pages` table. URLs like ``/PageName`` will try to find an element in the `pages` table that has a corresponding name. -- cgit v1.2.3 From 4872a1e713f894b383990f62cf82c2b21f810c16 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 12 Feb 2016 02:48:09 -0600 Subject: forward port changes to models / scripts to later chapters --- docs/tutorials/wiki2/src/authorization/setup.py | 3 ++- .../src/authorization/tutorial/models/__init__.py | 3 ++- .../src/authorization/tutorial/models/mymodel.py | 15 ----------- .../src/authorization/tutorial/models/page.py | 20 +++++++++++++++ .../src/authorization/tutorial/models/user.py | 27 ++++++++++++++++++++ .../authorization/tutorial/scripts/initializedb.py | 16 ++++++++++-- docs/tutorials/wiki2/src/tests/setup.py | 3 ++- .../wiki2/src/tests/tutorial/models/__init__.py | 3 ++- .../wiki2/src/tests/tutorial/models/mymodel.py | 29 ---------------------- .../wiki2/src/tests/tutorial/models/page.py | 20 +++++++++++++++ .../wiki2/src/tests/tutorial/models/user.py | 27 ++++++++++++++++++++ .../src/tests/tutorial/scripts/initializedb.py | 16 ++++++++++-- docs/tutorials/wiki2/src/views/setup.py | 3 ++- .../wiki2/src/views/tutorial/models/__init__.py | 3 ++- .../wiki2/src/views/tutorial/models/mymodel.py | 15 ----------- .../wiki2/src/views/tutorial/models/page.py | 20 +++++++++++++++ .../wiki2/src/views/tutorial/models/user.py | 27 ++++++++++++++++++++ .../src/views/tutorial/scripts/initializedb.py | 16 ++++++++++-- 18 files changed, 195 insertions(+), 71 deletions(-) delete mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/models/mymodel.py create mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/models/page.py create mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/models/user.py delete mode 100644 docs/tutorials/wiki2/src/tests/tutorial/models/mymodel.py create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/models/page.py create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/models/user.py delete mode 100644 docs/tutorials/wiki2/src/views/tutorial/models/mymodel.py create mode 100644 docs/tutorials/wiki2/src/views/tutorial/models/page.py create mode 100644 docs/tutorials/wiki2/src/views/tutorial/models/user.py diff --git a/docs/tutorials/wiki2/src/authorization/setup.py b/docs/tutorials/wiki2/src/authorization/setup.py index d4e5a4072..c342c1aba 100644 --- a/docs/tutorials/wiki2/src/authorization/setup.py +++ b/docs/tutorials/wiki2/src/authorization/setup.py @@ -9,6 +9,8 @@ with open(os.path.join(here, 'CHANGES.txt')) as f: CHANGES = f.read() requires = [ + 'bcrypt', + 'docutils', 'pyramid', 'pyramid_jinja2', 'pyramid_debugtoolbar', @@ -17,7 +19,6 @@ requires = [ 'transaction', 'zope.sqlalchemy', 'waitress', - 'docutils', ] setup(name='tutorial', diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py index 3d3efe06f..a8871f6f5 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py @@ -5,7 +5,8 @@ import zope.sqlalchemy # import or define all models here to ensure they are attached to the # Base.metadata prior to any initialization routines -from .mymodel import Page # flake8: noqa +from .page import Page # flake8: noqa +from .user import User # flake8: noqa # run configure_mappers after defining all of the models to ensure # all relationships can be setup diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models/mymodel.py b/docs/tutorials/wiki2/src/authorization/tutorial/models/mymodel.py deleted file mode 100644 index b23d0c0d2..000000000 --- a/docs/tutorials/wiki2/src/authorization/tutorial/models/mymodel.py +++ /dev/null @@ -1,15 +0,0 @@ -from sqlalchemy import ( - Column, - Integer, - Text, -) - -from .meta import Base - - -class Page(Base): - """ The SQLAlchemy declarative model class for a Page object. """ - __tablename__ = 'pages' - id = Column(Integer, primary_key=True) - name = Column(Text, unique=True) - data = Column(Integer) diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models/page.py b/docs/tutorials/wiki2/src/authorization/tutorial/models/page.py new file mode 100644 index 000000000..4dd5b5721 --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/models/page.py @@ -0,0 +1,20 @@ +from sqlalchemy import ( + Column, + ForeignKey, + Integer, + Text, +) +from sqlalchemy.orm import relationship + +from .meta import Base + + +class Page(Base): + """ The SQLAlchemy declarative model class for a Page object. """ + __tablename__ = 'pages' + id = Column(Integer, primary_key=True) + name = Column(Text, nullable=False, unique=True) + data = Column(Integer, nullable=False) + + creator_id = Column(ForeignKey('users.id'), nullable=False) + creator = relationship('User', backref='created_pages') diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models/user.py b/docs/tutorials/wiki2/src/authorization/tutorial/models/user.py new file mode 100644 index 000000000..25b0a8187 --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/models/user.py @@ -0,0 +1,27 @@ +import bcrypt +from sqlalchemy import ( + Column, + Integer, + Text, +) + +from .meta import Base + + +class User(Base): + """ The SQLAlchemy declarative model class for a User object. """ + __tablename__ = 'users' + id = Column(Integer, primary_key=True) + name = Column(Text, nullable=False, unique=True) + role = Column(Text, nullable=False) + + password_hash = Column(Text) + + def set_password(self, pw): + pwhash = bcrypt.hashpw(pw, bcrypt.gensalt()) + self.password_hash = pwhash + + def check_password(self, pw): + if self.password_hash is not None: + return bcrypt.hashpw(pw, self.password_hash) == self.password_hash + return False diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/authorization/tutorial/scripts/initializedb.py index 601a6e73f..f3c0a6fef 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/scripts/initializedb.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/scripts/initializedb.py @@ -15,7 +15,7 @@ from ..models import ( get_session_factory, get_tm_session, ) -from ..models import Page +from ..models import Page, User def usage(argv): @@ -41,5 +41,17 @@ def main(argv=sys.argv): with transaction.manager: dbsession = get_tm_session(session_factory, transaction.manager) - page = Page(name='FrontPage', data='This is the front page') + editor = User(name='editor', role='editor') + editor.set_password('editor') + dbsession.add(editor) + + basic = User(name='basic', role='basic') + basic.set_password('basic') + dbsession.add(basic) + + page = Page( + name='FrontPage', + creator=editor, + data='This is the front page', + ) dbsession.add(page) diff --git a/docs/tutorials/wiki2/src/tests/setup.py b/docs/tutorials/wiki2/src/tests/setup.py index 93195a68c..e06aa06e4 100644 --- a/docs/tutorials/wiki2/src/tests/setup.py +++ b/docs/tutorials/wiki2/src/tests/setup.py @@ -9,6 +9,8 @@ with open(os.path.join(here, 'CHANGES.txt')) as f: CHANGES = f.read() requires = [ + 'bcrypt', + 'docutils', 'pyramid', 'pyramid_jinja2', 'pyramid_debugtoolbar', @@ -17,7 +19,6 @@ requires = [ 'transaction', 'zope.sqlalchemy', 'waitress', - 'docutils', ] tests_require = [ diff --git a/docs/tutorials/wiki2/src/tests/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/tests/tutorial/models/__init__.py index 3d3efe06f..a8871f6f5 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/models/__init__.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/models/__init__.py @@ -5,7 +5,8 @@ import zope.sqlalchemy # import or define all models here to ensure they are attached to the # Base.metadata prior to any initialization routines -from .mymodel import Page # flake8: noqa +from .page import Page # flake8: noqa +from .user import User # flake8: noqa # run configure_mappers after defining all of the models to ensure # all relationships can be setup diff --git a/docs/tutorials/wiki2/src/tests/tutorial/models/mymodel.py b/docs/tutorials/wiki2/src/tests/tutorial/models/mymodel.py deleted file mode 100644 index 25209c745..000000000 --- a/docs/tutorials/wiki2/src/tests/tutorial/models/mymodel.py +++ /dev/null @@ -1,29 +0,0 @@ -from pyramid.security import ( - Allow, - Everyone, -) -from sqlalchemy import ( - Column, - Integer, - Text, -) - -from .meta import Base - - -class Page(Base): - """ The SQLAlchemy declarative model class for a Page object. """ - __tablename__ = 'pages' - id = Column(Integer, primary_key=True) - name = Column(Text, unique=True) - data = Column(Integer) - - -class RootFactory(object): - __acl__ = [ - (Allow, Everyone, 'view'), - (Allow, 'group:editors', 'edit'), - ] - - def __init__(self, request): - pass diff --git a/docs/tutorials/wiki2/src/tests/tutorial/models/page.py b/docs/tutorials/wiki2/src/tests/tutorial/models/page.py new file mode 100644 index 000000000..4dd5b5721 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/models/page.py @@ -0,0 +1,20 @@ +from sqlalchemy import ( + Column, + ForeignKey, + Integer, + Text, +) +from sqlalchemy.orm import relationship + +from .meta import Base + + +class Page(Base): + """ The SQLAlchemy declarative model class for a Page object. """ + __tablename__ = 'pages' + id = Column(Integer, primary_key=True) + name = Column(Text, nullable=False, unique=True) + data = Column(Integer, nullable=False) + + creator_id = Column(ForeignKey('users.id'), nullable=False) + creator = relationship('User', backref='created_pages') diff --git a/docs/tutorials/wiki2/src/tests/tutorial/models/user.py b/docs/tutorials/wiki2/src/tests/tutorial/models/user.py new file mode 100644 index 000000000..25b0a8187 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/models/user.py @@ -0,0 +1,27 @@ +import bcrypt +from sqlalchemy import ( + Column, + Integer, + Text, +) + +from .meta import Base + + +class User(Base): + """ The SQLAlchemy declarative model class for a User object. """ + __tablename__ = 'users' + id = Column(Integer, primary_key=True) + name = Column(Text, nullable=False, unique=True) + role = Column(Text, nullable=False) + + password_hash = Column(Text) + + def set_password(self, pw): + pwhash = bcrypt.hashpw(pw, bcrypt.gensalt()) + self.password_hash = pwhash + + def check_password(self, pw): + if self.password_hash is not None: + return bcrypt.hashpw(pw, self.password_hash) == self.password_hash + return False diff --git a/docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py index 601a6e73f..f3c0a6fef 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py @@ -15,7 +15,7 @@ from ..models import ( get_session_factory, get_tm_session, ) -from ..models import Page +from ..models import Page, User def usage(argv): @@ -41,5 +41,17 @@ def main(argv=sys.argv): with transaction.manager: dbsession = get_tm_session(session_factory, transaction.manager) - page = Page(name='FrontPage', data='This is the front page') + editor = User(name='editor', role='editor') + editor.set_password('editor') + dbsession.add(editor) + + basic = User(name='basic', role='basic') + basic.set_password('basic') + dbsession.add(basic) + + page = Page( + name='FrontPage', + creator=editor, + data='This is the front page', + ) dbsession.add(page) diff --git a/docs/tutorials/wiki2/src/views/setup.py b/docs/tutorials/wiki2/src/views/setup.py index d4e5a4072..c342c1aba 100644 --- a/docs/tutorials/wiki2/src/views/setup.py +++ b/docs/tutorials/wiki2/src/views/setup.py @@ -9,6 +9,8 @@ with open(os.path.join(here, 'CHANGES.txt')) as f: CHANGES = f.read() requires = [ + 'bcrypt', + 'docutils', 'pyramid', 'pyramid_jinja2', 'pyramid_debugtoolbar', @@ -17,7 +19,6 @@ requires = [ 'transaction', 'zope.sqlalchemy', 'waitress', - 'docutils', ] setup(name='tutorial', diff --git a/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py index 3d3efe06f..a8871f6f5 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py +++ b/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py @@ -5,7 +5,8 @@ import zope.sqlalchemy # import or define all models here to ensure they are attached to the # Base.metadata prior to any initialization routines -from .mymodel import Page # flake8: noqa +from .page import Page # flake8: noqa +from .user import User # flake8: noqa # run configure_mappers after defining all of the models to ensure # all relationships can be setup diff --git a/docs/tutorials/wiki2/src/views/tutorial/models/mymodel.py b/docs/tutorials/wiki2/src/views/tutorial/models/mymodel.py deleted file mode 100644 index b23d0c0d2..000000000 --- a/docs/tutorials/wiki2/src/views/tutorial/models/mymodel.py +++ /dev/null @@ -1,15 +0,0 @@ -from sqlalchemy import ( - Column, - Integer, - Text, -) - -from .meta import Base - - -class Page(Base): - """ The SQLAlchemy declarative model class for a Page object. """ - __tablename__ = 'pages' - id = Column(Integer, primary_key=True) - name = Column(Text, unique=True) - data = Column(Integer) diff --git a/docs/tutorials/wiki2/src/views/tutorial/models/page.py b/docs/tutorials/wiki2/src/views/tutorial/models/page.py new file mode 100644 index 000000000..4dd5b5721 --- /dev/null +++ b/docs/tutorials/wiki2/src/views/tutorial/models/page.py @@ -0,0 +1,20 @@ +from sqlalchemy import ( + Column, + ForeignKey, + Integer, + Text, +) +from sqlalchemy.orm import relationship + +from .meta import Base + + +class Page(Base): + """ The SQLAlchemy declarative model class for a Page object. """ + __tablename__ = 'pages' + id = Column(Integer, primary_key=True) + name = Column(Text, nullable=False, unique=True) + data = Column(Integer, nullable=False) + + creator_id = Column(ForeignKey('users.id'), nullable=False) + creator = relationship('User', backref='created_pages') diff --git a/docs/tutorials/wiki2/src/views/tutorial/models/user.py b/docs/tutorials/wiki2/src/views/tutorial/models/user.py new file mode 100644 index 000000000..25b0a8187 --- /dev/null +++ b/docs/tutorials/wiki2/src/views/tutorial/models/user.py @@ -0,0 +1,27 @@ +import bcrypt +from sqlalchemy import ( + Column, + Integer, + Text, +) + +from .meta import Base + + +class User(Base): + """ The SQLAlchemy declarative model class for a User object. """ + __tablename__ = 'users' + id = Column(Integer, primary_key=True) + name = Column(Text, nullable=False, unique=True) + role = Column(Text, nullable=False) + + password_hash = Column(Text) + + def set_password(self, pw): + pwhash = bcrypt.hashpw(pw, bcrypt.gensalt()) + self.password_hash = pwhash + + def check_password(self, pw): + if self.password_hash is not None: + return bcrypt.hashpw(pw, self.password_hash) == self.password_hash + return False diff --git a/docs/tutorials/wiki2/src/views/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/views/tutorial/scripts/initializedb.py index 601a6e73f..f3c0a6fef 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/scripts/initializedb.py +++ b/docs/tutorials/wiki2/src/views/tutorial/scripts/initializedb.py @@ -15,7 +15,7 @@ from ..models import ( get_session_factory, get_tm_session, ) -from ..models import Page +from ..models import Page, User def usage(argv): @@ -41,5 +41,17 @@ def main(argv=sys.argv): with transaction.manager: dbsession = get_tm_session(session_factory, transaction.manager) - page = Page(name='FrontPage', data='This is the front page') + editor = User(name='editor', role='editor') + editor.set_password('editor') + dbsession.add(editor) + + basic = User(name='basic', role='basic') + basic.set_password('basic') + dbsession.add(basic) + + page = Page( + name='FrontPage', + creator=editor, + data='This is the front page', + ) dbsession.add(page) -- cgit v1.2.3 From 23c2d7b337a5873dba0ca6c146e1174136ac2187 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 12 Feb 2016 02:54:37 -0600 Subject: update the views/models with setup.py develop --- docs/tutorials/wiki2/definingmodels.rst | 30 +++++++++++++++++++++++ docs/tutorials/wiki2/definingviews.rst | 43 ++++++--------------------------- docs/tutorials/wiki2/design.rst | 10 ++++---- 3 files changed, 43 insertions(+), 40 deletions(-) diff --git a/docs/tutorials/wiki2/definingmodels.rst b/docs/tutorials/wiki2/definingmodels.rst index 33e7beb4f..41f36fa26 100644 --- a/docs/tutorials/wiki2/definingmodels.rst +++ b/docs/tutorials/wiki2/definingmodels.rst @@ -37,6 +37,36 @@ Open ``tutorial/setup.py`` and edit it to look like the following: Only the highlighted line needs to be added. +Running ``setup.py develop`` +============================ + +Since a new software dependency was added, you will need to run ``python +setup.py develop`` again inside the root of the ``tutorial`` package to obtain +and register the newly added dependency distribution. + +Make sure your current working directory is the root of the project (the +directory in which ``setup.py`` lives) and execute the following command. + +On UNIX: + +.. code-block:: bash + + $ cd tutorial + $ $VENV/bin/python setup.py develop + +On Windows: + +.. code-block:: text + + c:\pyramidtut> cd tutorial + c:\pyramidtut\tutorial> %VENV%\Scripts\python setup.py develop + +Success executing this command will end with a line to the console something +like this:: + + Finished processing dependencies for tutorial==0.0 + + Remove ``mymodel.py`` --------------------- diff --git a/docs/tutorials/wiki2/definingviews.rst b/docs/tutorials/wiki2/definingviews.rst index 6629839f8..8bccc3fc0 100644 --- a/docs/tutorials/wiki2/definingviews.rst +++ b/docs/tutorials/wiki2/definingviews.rst @@ -14,13 +14,12 @@ and a user visits ``http://example.com/foo/bar``, our pattern would be matched against ``/foo/bar`` and the ``matchdict`` would look like ``{'one':'foo', 'two':'bar'}``. -Declaring dependencies in our ``setup.py`` file -=============================================== +Adding the ``docutils`` dependency +================================== -The view code in our application will depend on a package which is not a -dependency of the original "tutorial" application. The original "tutorial" -application was generated by the ``pcreate`` command; it doesn't know -about our custom application requirements. +Remember in the previous chapter we added a new dependency on the ``bcrypt`` +package. Again, the view code in our application will depend on a package which +is not a dependency of the original "tutorial" application. We need to add a dependency on the ``docutils`` package to our ``tutorial`` package's ``setup.py`` file by assigning this dependency to the ``requires`` @@ -30,39 +29,13 @@ Open ``tutorial/setup.py`` and edit it to look like the following: .. literalinclude:: src/views/setup.py :linenos: - :emphasize-lines: 20 + :emphasize-lines: 13 :language: python Only the highlighted line needs to be added. -Running ``setup.py develop`` -============================ - -Since a new software dependency was added, you will need to run ``python -setup.py develop`` again inside the root of the ``tutorial`` package to obtain -and register the newly added dependency distribution. - -Make sure your current working directory is the root of the project (the -directory in which ``setup.py`` lives) and execute the following command. - -On UNIX: - -.. code-block:: bash - - $ cd tutorial - $ $VENV/bin/python setup.py develop - -On Windows: - -.. code-block:: text - - c:\pyramidtut> cd tutorial - c:\pyramidtut\tutorial> %VENV%\Scripts\python setup.py develop - -Success executing this command will end with a line to the console something -like this:: - - Finished processing dependencies for tutorial==0.0 +Again, as we did in the previous chapter, the dependency now needs to be +installed so re-run the ``python setup.py develop`` command. Adding view functions in ``views/default.py`` ============================================= diff --git a/docs/tutorials/wiki2/design.rst b/docs/tutorials/wiki2/design.rst index de43447d3..45e2fddd0 100644 --- a/docs/tutorials/wiki2/design.rst +++ b/docs/tutorials/wiki2/design.rst @@ -6,7 +6,7 @@ Following is a quick overview of the design of our wiki application to help us understand the changes that we will be making as we work through the tutorial. Overall -------- +======= We choose to use :term:`reStructuredText` markup in the wiki text. Translation from reStructuredText to HTML is provided by the widely used ``docutils`` @@ -14,7 +14,7 @@ Python module. We will add this module in the dependency list on the project ``setup.py`` file. Models ------- +====== We'll be using an SQLite database to hold our wiki data, and we'll be using :term:`SQLAlchemy` to access the data in this database. @@ -41,7 +41,7 @@ be created when the storage is initialized, and will be used as the wiki home page. Wiki Views ----------- +========== There will be three views to handle the normal operations of adding, editing, and viewing wiki pages, plus one view for the wiki front page. Two templates @@ -53,7 +53,7 @@ designer-friendly templating language for Python, modeled after Django's templates. Security --------- +======== We'll eventually be adding security to our application. To do this, we'll be using a very simple role-based security model. We'll assign a single @@ -92,7 +92,7 @@ handling login and logout as well as two exception views for handling invalid access attempts and unhandled URLs. Summary -------- +======= The URL, actions, template, and permission associated to each view are listed in the following table: -- cgit v1.2.3 From 288b462aa21ac0d9086fc2a616033468dc3846a6 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Fri, 12 Feb 2016 20:06:27 -0800 Subject: minor grammar and punctuation through "thread locals are a nuisance" --- docs/designdefense.rst | 45 ++++++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/docs/designdefense.rst b/docs/designdefense.rst index 530a9c17c..98005f3f9 100644 --- a/docs/designdefense.rst +++ b/docs/designdefense.rst @@ -1405,7 +1405,9 @@ great fit for very large applications and applications that need to be arbitrarily extensible. -"Stacked Object Proxies" Are Too Clever / Thread Locals Are A Nuisance +.. _thread_local_nuisance: + +"Stacked object proxies" are too clever / thread locals are a nuisance ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Some microframeworks use the ``import`` statement to get a handle to an @@ -1448,32 +1450,33 @@ code below: for i in range(10): print(i) -By its nature, the *request* object created as the result of a WSGI server's -call into a long-lived web framework cannot be global, because the lifetime -of a single request will be much shorter than the lifetime of the process -running the framework. A request object created by a web framework actually -has more similarity to the ``i`` loop counter in our example above than it -has to any comparable importable object defined in the Python standard +By its nature, the *request* object that is created as the result of a WSGI +server's call into a long-lived web framework cannot be global, because the +lifetime of a single request will be much shorter than the lifetime of the +process running the framework. A request object created by a web framework +actually has more similarity to the ``i`` loop counter in our example above +than it has to any comparable importable object defined in the Python standard library or in normal library code. However, systems which use stacked object proxies promote locally scoped -objects such as ``request`` out to module scope, for the purpose of being +objects, such as ``request``, out to module scope, for the purpose of being able to offer users a nice spelling involving ``import``. They, for what I -consider dubious reasons, would rather present to their users the canonical -way of getting at a ``request`` as ``from framework import request`` instead -of a saner ``from myframework.threadlocals import get_request; request = -get_request()`` even though the latter is more explicit. +consider dubious reasons, would rather present to their users the canonical way +of getting at a ``request`` as ``from framework import request`` instead of a +saner ``from myframework.threadlocals import get_request; request = +get_request()``, even though the latter is more explicit. It would be *most* explicit if the microframeworks did not use thread local -variables at all. Pyramid view functions are passed a request object; many -of Pyramid's APIs require that an explicit request object be passed to them. -It is *possible* to retrieve the current Pyramid request as a threadlocal -variable but it is a "in case of emergency, break glass" type of activity. -This explicitness makes Pyramid view functions more easily unit testable, as -you don't need to rely on the framework to manufacture suitable "dummy" -request (and other similarly-scoped) objects during test setup. It also -makes them more likely to work on arbitrary systems, such as async servers -that do no monkeypatching. +variables at all. Pyramid view functions are passed a request object. Many of +Pyramid's APIs require that an explicit request object be passed to them. It is +*possible* to retrieve the current Pyramid request as a threadlocal variable, +but it is an "in case of emergency, break glass" type of activity. This +explicitness makes Pyramid view functions more easily unit testable, as you +don't need to rely on the framework to manufacture suitable "dummy" request +(and other similarly-scoped) objects during test setup. It also makes them +more likely to work on arbitrary systems, such as async servers, that do no +monkeypatching. + Explicitly WSGI +++++++++++++++ -- cgit v1.2.3 From 60891b844c883d2c9ce864522f2202d9514d8d83 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sat, 13 Feb 2016 13:26:05 -0600 Subject: improve the views section by removing quirks and explaining transactions --- docs/tutorials/wiki2/definingviews.rst | 274 ++++++++++++--------- .../wiki2/src/views/tutorial/templates/edit.jinja2 | 6 +- .../src/views/tutorial/templates/layout.jinja2 | 7 +- .../wiki2/src/views/tutorial/templates/view.jinja2 | 4 +- .../wiki2/src/views/tutorial/views/default.py | 36 +-- 5 files changed, 185 insertions(+), 142 deletions(-) diff --git a/docs/tutorials/wiki2/definingviews.rst b/docs/tutorials/wiki2/definingviews.rst index 8bccc3fc0..8f0f7b51d 100644 --- a/docs/tutorials/wiki2/definingviews.rst +++ b/docs/tutorials/wiki2/definingviews.rst @@ -37,6 +37,80 @@ Only the highlighted line needs to be added. Again, as we did in the previous chapter, the dependency now needs to be installed so re-run the ``python setup.py develop`` command. +Static assets +------------- + +Our templates name static assets, including CSS and images. We don't need +to create these files within our package's ``static`` directory because they +were provided at the time we created the project. + +As an example, the CSS file will be accessed via +``http://localhost:6543/static/theme.css`` by virtue of the call to the +``add_static_view`` directive we've made in the ``__init__.py`` file. Any +number and type of static assets can be placed in this directory (or +subdirectories) and are just referred to by URL or by using the convenience +method ``static_url``, e.g., +``request.static_url(':static/foo.css')`` within templates. + +Adding routes to ``__init__.py`` +================================ + +This is the URL Dispatch tutorial and so let's start by adding some +URL patterns to our app. Later we'll attach views to handle the URLs. + +The ``__init__.py`` file contains +:meth:`pyramid.config.Configurator.add_route` calls which serve to add routes +to our application. First, we’ll get rid of the existing route created by +the template using the name ``'home'``. It’s only an example and isn’t +relevant to our application. + +We then need to add four calls to ``add_route``. Note that the *ordering* of +these declarations is very important. ``route`` declarations are matched in +the order they're found in the ``__init__.py`` file. + +#. Add a declaration which maps the pattern ``/`` (signifying the root URL) + to the route named ``view_wiki``. It maps to our ``view_wiki`` view + callable by virtue of the ``@view_config`` attached to the ``view_wiki`` + view function indicating ``route_name='view_wiki'``. + +#. Add a declaration which maps the pattern ``/{pagename}`` to the route named + ``view_page``. This is the regular view for a page. It maps + to our ``view_page`` view callable by virtue of the ``@view_config`` + attached to the ``view_page`` view function indicating + ``route_name='view_page'``. + +#. Add a declaration which maps the pattern ``/add_page/{pagename}`` to the + route named ``add_page``. This is the add view for a new page. It maps + to our ``add_page`` view callable by virtue of the ``@view_config`` + attached to the ``add_page`` view function indicating + ``route_name='add_page'``. + +#. Add a declaration which maps the pattern ``/{pagename}/edit_page`` to the + route named ``edit_page``. This is the edit view for a page. It maps + to our ``edit_page`` view callable by virtue of the ``@view_config`` + attached to the ``edit_page`` view function indicating + ``route_name='edit_page'``. + +As a result of our edits, the ``__init__.py`` file should look +something like: + +.. literalinclude:: src/views/tutorial/__init__.py + :linenos: + :emphasize-lines: 11-14 + :language: python + +The highlighted lines are the ones that need to be added or edited. + +.. warn:: + + The order of the routes is important! If you placed + ``/{pagename}/edit_page`` **before** ``/add_page/{pagename}`` then we would + never be able to add pages because the first route would always match + a request to ``/add_page/edit_page`` whereas we want ``/add_page/..`` to + have priority. This isn't a huge problem in this particular app because + wiki pages are always camel case but it's important to be aware of this + behavior in your own apps. + Adding view functions in ``views/default.py`` ============================================= @@ -46,7 +120,7 @@ edit it to look like the following: .. literalinclude:: src/views/tutorial/views/default.py :linenos: :language: python - :emphasize-lines: 1-9,12-68 + :emphasize-lines: 1-9,12-70 The highlighted lines need to be added or edited. @@ -54,7 +128,7 @@ We added some imports, and created a regular expression to find "WikiWords". We got rid of the ``my_view`` view function and its decorator that was added when we originally rendered the ``alchemy`` scaffold. It was only an example -and isn't relevant to our application. We also delated the ``db_err_msg`` +and isn't relevant to our application. We also deleted the ``db_err_msg`` string. Then we added four :term:`view callable` functions to our ``views/default.py`` @@ -88,7 +162,7 @@ Following is the code for the ``view_wiki`` view function and its decorator: :language: python ``view_wiki()`` is the :term:`default view` that gets called when a request is -made to the root URL of our wiki. It always redirects to an URL which +made to the root URL of our wiki. It always redirects to a URL which represents the path to our "FrontPage". The ``view_wiki`` view callable always redirects to the URL of a Page resource @@ -96,7 +170,7 @@ named "FrontPage". To do so, it returns an instance of the :class:`pyramid.httpexceptions.HTTPFound` class (instances of which implement the :class:`pyramid.interfaces.IResponse` interface, like :class:`pyramid.response.Response` does). It uses the -:meth:`pyramid.request.Request.route_url` API to construct an URL to the +:meth:`pyramid.request.Request.route_url` API to construct a URL to the ``FrontPage`` page (i.e., ``http://localhost:6543/FrontPage``), and uses it as the "location" of the ``HTTPFound`` response, forming an HTTP redirect. @@ -116,12 +190,12 @@ Here is the code for the ``view_page`` view function and its decorator: ``Page`` model object) as HTML. Then it substitutes an HTML anchor for each *WikiWord* reference in the rendered HTML using a compiled regular expression. -The curried function named ``check`` is used as the first argument to +The curried function named ``add_link`` is used as the first argument to ``wikiwords.sub``, indicating that it should be called to provide a value for each WikiWord match found in the content. If the wiki already contains a -page with the matched WikiWord name, ``check()`` generates a view +page with the matched WikiWord name, ``add_link()`` generates a view link to be used as the substitution value and returns it. If the wiki does -not already contain a page with the matched WikiWord name, ``check()`` +not already contain a page with the matched WikiWord name, ``add_link()`` generates an "add" link as the substitution value and returns it. As a result, the ``content`` variable is now a fully formed bit of HTML @@ -136,19 +210,73 @@ associated with the view configuration to render a response. In our case, the renderer used will be the ``view.jinja2`` template, as indicated in the ``@view_config`` decorator that is applied to ``view_page()``. +If the page does not exist then we need to handle that by raising +:class:`pyramid.httpexceptions.HTTPNotFound`` to trigger our 404 handling +defined in ``tutorial/views/notfound.py``. + +.. note:: + + Using ``raise`` versus ``return`` with the http exceptions is an important + distinction that can commonly mess people up. In + ``tutorial/views/notfound.py`` there is an :term:`exception view` + registered for handling the ``HTTPNotFound`` exception. Exception views + are only triggered for raised exceptions. If the ``HTTPNotFound`` is + returned then it has an internal "stock" template that it will use + to render itself as a response. If you aren't seeing your exception + view being executed this is probably the problem! See + :ref:`special_exceptions_in_callables` for more information about + exception views. + +The ``edit_page`` view function +------------------------------- + +Here is the code for the ``edit_page`` view function and its decorator: + +.. literalinclude:: src/views/tutorial/views/default.py + :lines: 44-56 + :lineno-match: + :linenos: + :language: python + +``edit_page()`` is invoked when a user clicks the "Edit this +Page" button on the view form. It renders an edit form, but it also acts as +the handler for the form it renders. The ``matchdict`` attribute of the +request passed to the ``edit_page`` view will have a ``'pagename'`` key +matching the name of the page the user wants to edit. + +If the view execution *is* a result of a form submission (i.e., the expression +``'form.submitted' in request.params`` is ``True``), the view grabs the +``body`` element of the request parameters and sets it as the ``data`` +attribute of the page object. It then redirects to the ``view_page`` view +of the wiki page. + +If the view execution is *not* a result of a form submission (i.e., the +expression ``'form.submitted' in request.params`` is ``False``), the view +simply renders the edit form, passing the page object and a ``save_url`` +which will be used as the action of the generated form. + +.. note:: + + Since our ``request.dbsession`` defined in the previous chapter is + registered with the ``pyramid_tm`` transaction manager any changes we make + to objects managed by the that session will be committed automatically. + In the event that there was an error (even later, in our template code) the + changes would be aborted. This means the view itself does not need to + concern itself with commit/rollback logic. + The ``add_page`` view function ------------------------------ Here is the code for the ``add_page`` view function and its decorator: .. literalinclude:: src/views/tutorial/views/default.py - :lines: 44-55 + :lines: 58-70 :lineno-match: :linenos: :language: python ``add_page()`` is invoked when a user clicks on a *WikiWord* which -isn't yet represented as a page in the system. The ``check`` function +isn't yet represented as a page in the system. The ``add_link`` function within the ``view_page`` view generates URLs to this view. ``add_page()`` also acts as a handler for the form that is generated when we want to add a page object. The ``matchdict`` attribute of the @@ -164,8 +292,12 @@ If the view execution *is* a result of a form submission (i.e., the expression ``'form.submitted' in request.params`` is ``True``), we grab the page body from the form data, create a Page object with this page body and the name taken from ``matchdict['pagename']``, and save it into the database using -``request.dbession.add``. We then redirect back to the ``view_page`` view for -the newly created page. +``request.dbession.add``. Since we have not yet covered authentication we +don't have a logged-in user to add as the page's ``creator``. Until we +get to that point in the tutorial we'll just assume that all pages are created +by the ``editor`` user so we query that object and set it on ``page.creator``. +Finally, we redirect the client back to the ``view_page`` view for the newly +created page. If the view execution is *not* a result of a form submission (i.e., the expression ``'form.submitted' in request.params`` is ``False``), the view @@ -177,34 +309,6 @@ in order to satisfy the edit form's desire to have *some* page object exposed as ``page``. :app:`Pyramid` will render the template associated with this view to a response. -The ``edit_page`` view function -------------------------------- - -Here is the code for the ``edit_page`` view function and its decorator: - -.. literalinclude:: src/views/tutorial/views/default.py - :lines: 57-68 - :lineno-match: - :linenos: - :language: python - -``edit_page()`` is invoked when a user clicks the "Edit this -Page" button on the view form. It renders an edit form, but it also acts as -the handler for the form it renders. The ``matchdict`` attribute of the -request passed to the ``edit_page`` view will have a ``'pagename'`` key -matching the name of the page the user wants to edit. - -If the view execution *is* a result of a form submission (i.e., the expression -``'form.submitted' in request.params`` is ``True``), the view grabs the -``body`` element of the request parameters and sets it as the ``data`` -attribute of the page object. It then redirects to the ``view_page`` view -of the wiki page. - -If the view execution is *not* a result of a form submission (i.e., the -expression ``'form.submitted' in request.params`` is ``False``), the view -simply renders the edit form, passing the page object and a ``save_url`` -which will be used as the action of the generated form. - Adding templates ================ @@ -229,7 +333,7 @@ our page templates into reusable components. One method for doing this is template inheritance via blocks. - We have defined 2 placeholders in the layout template where a child template - can override the content. These blocks are named ``title`` (line 11) and + can override the content. These blocks are named ``subtitle`` (line 11) and ``content`` (line 36). - Please refer to the Jinja2_ documentation for more information about template inheritance. @@ -237,44 +341,45 @@ is template inheritance via blocks. The ``view.jinja2`` template ---------------------------- -Create ``tutorial/templates/view.jinja2`` and add the following -content: +Create ``tutorial/templates/view.jinja2`` and add the following content: .. literalinclude:: src/views/tutorial/templates/view.jinja2 :linenos: :emphasize-lines: 1,4,6-8 :language: html -This template is used by ``view_page()`` for displaying a single -wiki page. It includes: +This template is used by ``view_page()`` for displaying a single wiki page. +It includes: - We begin by extending the ``layout.jinja2`` template defined above which provides the skeleton of the page (line 1). +- We override the ``subtitle`` block from the base layout to insert the + page name of the page into the page's title (line 3). - We override the ``content`` block from the base layout to insert our markup - into the body (line 3). + into the body (line 5-18). - A variable that is replaced with the ``content`` value provided by the view - (line 4). ``content`` contains HTML, so the ``|safe`` filter is used to + (line 6). ``content`` contains HTML, so the ``|safe`` filter is used to prevent escaping it (e.g., changing ">" to ">"). - A link that points at the "edit" URL which invokes the ``edit_page`` view for - the page being viewed (lines 6-8). + the page being viewed (line 9). The ``edit.jinja2`` template ---------------------------- -Create ``tutorial/templates/edit.jinja2`` and add the following -content: +Create ``tutorial/templates/edit.jinja2`` and add the following content: .. literalinclude:: src/views/tutorial/templates/edit.jinja2 :linenos: :emphasize-lines: 3,12,14,17 :language: html -This template is used by ``add_page()`` and ``edit_page()`` for adding and -editing a wiki page. It displays a page containing a form that includes: +This template serves two use-cases. It is used by ``add_page()`` and +``edit_page()`` for adding and editing a wiki page. It displays a page +containing a form that includes: -- Again we are extending the ``layout.jinja2`` template which provides - the skeleton of the page. -- Override the ``title`` block to affect the ```` tag in the +- Again, we are extending the ``layout.jinja2`` template which provides + the skeleton of the page (line 1). +- Override the ``subtitle`` block to affect the ``<title>`` tag in the ``head`` of the page (line 3). - A 10-row by 60-column ``textarea`` field named ``body`` that is filled with any existing page data when it is rendered (line 14). @@ -326,67 +431,6 @@ our own templates for the wiki. See :ref:`renderer_system_values` for information about other names that are available by default when a template is used as a renderer. -Static assets -------------- - -Our templates name static assets, including CSS and images. We don't need -to create these files within our package's ``static`` directory because they -were provided at the time we created the project. - -As an example, the CSS file will be accessed via -``http://localhost:6543/static/theme.css`` by virtue of the call to the -``add_static_view`` directive we've made in the ``__init__.py`` file. Any -number and type of static assets can be placed in this directory (or -subdirectories) and are just referred to by URL or by using the convenience -method ``static_url``, e.g., -``request.static_url('<package>:static/foo.css')`` within templates. - -Adding routes to ``__init__.py`` -================================ - -The ``__init__.py`` file contains -:meth:`pyramid.config.Configurator.add_route` calls which serve to add routes -to our application. First, we’ll get rid of the existing route created by -the template using the name ``'home'``. It’s only an example and isn’t -relevant to our application. - -We then need to add four calls to ``add_route``. Note that the *ordering* of -these declarations is very important. ``route`` declarations are matched in -the order they're found in the ``__init__.py`` file. - -#. Add a declaration which maps the pattern ``/`` (signifying the root URL) - to the route named ``view_wiki``. It maps to our ``view_wiki`` view - callable by virtue of the ``@view_config`` attached to the ``view_wiki`` - view function indicating ``route_name='view_wiki'``. - -#. Add a declaration which maps the pattern ``/{pagename}`` to the route named - ``view_page``. This is the regular view for a page. It maps - to our ``view_page`` view callable by virtue of the ``@view_config`` - attached to the ``view_page`` view function indicating - ``route_name='view_page'``. - -#. Add a declaration which maps the pattern ``/add_page/{pagename}`` to the - route named ``add_page``. This is the add view for a new page. It maps - to our ``add_page`` view callable by virtue of the ``@view_config`` - attached to the ``add_page`` view function indicating - ``route_name='add_page'``. - -#. Add a declaration which maps the pattern ``/{pagename}/edit_page`` to the - route named ``edit_page``. This is the edit view for a page. It maps - to our ``edit_page`` view callable by virtue of the ``@view_config`` - attached to the ``edit_page`` view function indicating - ``route_name='edit_page'``. - -As a result of our edits, the ``__init__.py`` file should look -something like: - -.. literalinclude:: src/views/tutorial/__init__.py - :linenos: - :emphasize-lines: 11-14 - :language: python - -The highlighted lines are the ones that need to be added or edited. - Viewing the application in a browser ==================================== diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/edit.jinja2 b/docs/tutorials/wiki2/src/views/tutorial/templates/edit.jinja2 index e47b3aabf..7db25c674 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/templates/edit.jinja2 +++ b/docs/tutorials/wiki2/src/views/tutorial/templates/edit.jinja2 @@ -1,17 +1,17 @@ {% extends 'layout.jinja2' %} -{% block title %}Edit {{page.name}} - {% endblock title %} +{% block subtitle %}Edit {{pagename}} - {% endblock subtitle %} {% block content %} <p> -Editing <strong>{% if page.name %}{{page.name}}{% else %}Page Name Goes Here{% endif %}</strong> +Editing <strong>{{pagename}}</strong> </p> <p>You can return to the <a href="{{request.route_url('view_page', pagename='FrontPage')}}">FrontPage</a>. </p> <form action="{{ save_url }}" method="post"> <div class="form-group"> - <textarea class="form-control" name="body" rows="10" cols="60">{{ page.data }}</textarea> + <textarea class="form-control" name="body" rows="10" cols="60">{{ pagedata }}</textarea> </div> <div class="form-group"> <button type="submit" name="form.submitted" value="Save" class="btn btn-default">Save</button> diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/layout.jinja2 b/docs/tutorials/wiki2/src/views/tutorial/templates/layout.jinja2 index 82a144abf..71785157f 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/templates/layout.jinja2 +++ b/docs/tutorials/wiki2/src/views/tutorial/templates/layout.jinja2 @@ -8,7 +8,7 @@ <meta name="author" content="Pylons Project"> <link rel="shortcut icon" href="{{request.static_url('tutorial:static/pyramid-16x16.png')}}"> - <title>{% block title %}{% if page.name %} {{page.name}} - {% endif %}{% endblock title %}Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki) + {% block subtitle %}{% endblock %}Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki) @@ -33,11 +33,6 @@
- {% if request.authenticated_userid is not none %} -

- Logout -

- {% endif %} {% block content %}{% endblock %}
diff --git a/docs/tutorials/wiki2/src/views/tutorial/templates/view.jinja2 b/docs/tutorials/wiki2/src/views/tutorial/templates/view.jinja2 index c582ce1f9..94419e228 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/templates/view.jinja2 +++ b/docs/tutorials/wiki2/src/views/tutorial/templates/view.jinja2 @@ -1,5 +1,7 @@ {% extends 'layout.jinja2' %} +{% block subtitle %}{{page.name}} - {% endblock subtitle %} + {% block content %}

{{ content|safe }}

@@ -8,7 +10,7 @@

- Viewing {% if page.name %}{{page.name}}{% else %}Page Name Goes Here{% endif %} + Viewing {{page.name}}, created by {{page.creator.name}}.

You can return to the FrontPage. diff --git a/docs/tutorials/wiki2/src/views/tutorial/views/default.py b/docs/tutorials/wiki2/src/views/tutorial/views/default.py index ca37f39f5..7a4073b3f 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/views/default.py +++ b/docs/tutorials/wiki2/src/views/tutorial/views/default.py @@ -9,7 +9,7 @@ from pyramid.httpexceptions import ( from pyramid.view import view_config -from ..models import Page +from ..models import Page, User # regular expression used to find WikiWords wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)") @@ -26,7 +26,7 @@ def view_page(request): if page is None: raise HTTPNotFound('No such page') - def check(match): + def add_link(match): word = match.group(1) exists = request.dbsession.query(Page).filter_by(name=word).all() if exists: @@ -37,23 +37,10 @@ def view_page(request): return '%s' % (add_url, cgi.escape(word)) content = publish_parts(page.data, writer_name='html')['html_body'] - content = wikiwords.sub(check, content) + content = wikiwords.sub(add_link, content) edit_url = request.route_url('edit_page', pagename=pagename) return dict(page=page, content=content, edit_url=edit_url) -@view_config(route_name='add_page', renderer='../templates/edit.jinja2') -def add_page(request): - pagename = request.matchdict['pagename'] - if 'form.submitted' in request.params: - body = request.params['body'] - page = Page(name=pagename, data=body) - request.dbsession.add(page) - next_url = request.route_url('view_page', pagename=pagename) - return HTTPFound(location=next_url) - save_url = request.route_url('add_page', pagename=pagename) - page = Page(name='', data='') - return dict(page=page, save_url=save_url) - @view_config(route_name='edit_page', renderer='../templates/edit.jinja2') def edit_page(request): pagename = request.matchdict['pagename'] @@ -63,6 +50,21 @@ def edit_page(request): next_url = request.route_url('view_page', pagename=pagename) return HTTPFound(location=next_url) return dict( - page=page, + pagename=page.name, + pagedata=page.data, save_url=request.route_url('edit_page', pagename=pagename), ) + +@view_config(route_name='add_page', renderer='../templates/edit.jinja2') +def add_page(request): + pagename = request.matchdict['pagename'] + if 'form.submitted' in request.params: + body = request.params['body'] + page = Page(name=pagename, data=body) + page.creator = ( + request.dbsession.query(User).filter_by(name='editor').one()) + request.dbsession.add(page) + next_url = request.route_url('view_page', pagename=pagename) + return HTTPFound(location=next_url) + save_url = request.route_url('add_page', pagename=pagename) + return dict(pagename=pagename, pagedata='', save_url=save_url) -- cgit v1.2.3 From 4c391c55057acbb36df28215f562c42d2b616872 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sat, 13 Feb 2016 21:01:09 -0600 Subject: fix syntax --- docs/tutorials/wiki2/definingviews.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorials/wiki2/definingviews.rst b/docs/tutorials/wiki2/definingviews.rst index 8f0f7b51d..acaefe6e8 100644 --- a/docs/tutorials/wiki2/definingviews.rst +++ b/docs/tutorials/wiki2/definingviews.rst @@ -101,7 +101,7 @@ something like: The highlighted lines are the ones that need to be added or edited. -.. warn:: +.. warning:: The order of the routes is important! If you placed ``/{pagename}/edit_page`` **before** ``/add_page/{pagename}`` then we would -- cgit v1.2.3 From bca6c996d9e879c21d8b207bb36bc10ebe1db256 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sat, 13 Feb 2016 21:08:58 -0600 Subject: highlight more appropriate lines in views --- docs/tutorials/wiki2/definingviews.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/tutorials/wiki2/definingviews.rst b/docs/tutorials/wiki2/definingviews.rst index acaefe6e8..fea682628 100644 --- a/docs/tutorials/wiki2/definingviews.rst +++ b/docs/tutorials/wiki2/definingviews.rst @@ -345,7 +345,7 @@ Create ``tutorial/templates/view.jinja2`` and add the following content: .. literalinclude:: src/views/tutorial/templates/view.jinja2 :linenos: - :emphasize-lines: 1,4,6-8 + :emphasize-lines: 1,3,6 :language: html This template is used by ``view_page()`` for displaying a single wiki page. @@ -402,6 +402,7 @@ This template is linked from the ``notfound_view`` defined in .. literalinclude:: src/views/tutorial/views/notfound.py :linenos: + :emphasize-lines: 6 :language: python There are several important things to note about this configuration: @@ -409,8 +410,8 @@ There are several important things to note about this configuration: - The ``notfound_view`` in the above snippet is called an :term:`exception view`. For more information see :ref:`special_exceptions_in_callables`. -- The ``notfound_view`` sets the response status to 404. It's possible to - affect the response object used by the renderer via +- The ``notfound_view`` sets the response status to 404. It's possible + to affect the response object used by the renderer via :ref:`request_response_attr`. - The ``notfound_view`` is registered as an exception view and will be invoked **only** if ``pyramid.httpexceptions.HTTPNotFound`` is raised as an -- cgit v1.2.3 From b0fcf1a457df3501c734ca625f372d3589d2aa0f Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sun, 14 Feb 2016 00:55:10 -0800 Subject: minor grammar and punctuation through "Explicitly WSGI" --- docs/designdefense.rst | 42 ++++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/docs/designdefense.rst b/docs/designdefense.rst index 98005f3f9..bd38e5194 100644 --- a/docs/designdefense.rst +++ b/docs/designdefense.rst @@ -1478,6 +1478,8 @@ more likely to work on arbitrary systems, such as async servers, that do no monkeypatching. +.. _explicitly_wsgi: + Explicitly WSGI +++++++++++++++ @@ -1490,29 +1492,29 @@ import a WSGI server and use it to serve up their Pyramid application as per the documentation of that WSGI server. The extra lines saved by abstracting away the serving step behind ``run()`` -seem to have driven dubious second-order decisions related to API in some -microframeworks. For example, Bottle contains a ``ServerAdapter`` subclass -for each type of WSGI server it supports via its ``app.run()`` mechanism. -This means that there exists code in ``bottle.py`` that depends on the -following modules: ``wsgiref``, ``flup``, ``paste``, ``cherrypy``, ``fapws``, +seems to have driven dubious second-order decisions related to its API in some +microframeworks. For example, Bottle contains a ``ServerAdapter`` subclass for +each type of WSGI server it supports via its ``app.run()`` mechanism. This +means that there exists code in ``bottle.py`` that depends on the following +modules: ``wsgiref``, ``flup``, ``paste``, ``cherrypy``, ``fapws``, ``tornado``, ``google.appengine``, ``twisted.web``, ``diesel``, ``gevent``, -``gunicorn``, ``eventlet``, and ``rocket``. You choose the kind of server -you want to run by passing its name into the ``run`` method. In theory, this -sounds great: I can try Bottle out on ``gunicorn`` just by passing in a name! -However, to fully test Bottle, all of these third-party systems must be -installed and functional; the Bottle developers must monitor changes to each -of these packages and make sure their code still interfaces properly with -them. This expands the packages required for testing greatly; this is a -*lot* of requirements. It is likely difficult to fully automate these tests -due to requirements conflicts and build issues. +``gunicorn``, ``eventlet``, and ``rocket``. You choose the kind of server you +want to run by passing its name into the ``run`` method. In theory, this sounds +great: I can try out Bottle on ``gunicorn`` just by passing in a name! However, +to fully test Bottle, all of these third-party systems must be installed and +functional. The Bottle developers must monitor changes to each of these +packages and make sure their code still interfaces properly with them. This +increases the number of packages required for testing greatly; this is a *lot* +of requirements. It is likely difficult to fully automate these tests due to +requirements conflicts and build issues. As a result, for single-file apps, we currently don't bother to offer a -``run()`` shortcut; we tell folks to import their WSGI server of choice and -run it by hand. For the people who want a server abstraction layer, we -suggest that they use PasteDeploy. In PasteDeploy-based systems, the onus -for making sure that the server can interface with a WSGI application is -placed on the server developer, not the web framework developer, making it -more likely to be timely and correct. +``run()`` shortcut. We tell folks to import their WSGI server of choice and run +it by hand. For the people who want a server abstraction layer, we suggest that +they use PasteDeploy. In PasteDeploy-based systems, the onus for making sure +that the server can interface with a WSGI application is placed on the server +developer, not the web framework developer, making it more likely to be timely +and correct. Wrapping Up +++++++++++ -- cgit v1.2.3 From eee002a2b0b010834a2ebede550329dbea3b3732 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Sun, 14 Feb 2016 06:56:17 -0500 Subject: Use lexer name compatible w/ Pygments 1.5. Attempt to fix Jenkins build failures such as: http://jenkins.pylonsproject.org/job/pyramid/1992/console. --- docs/quick_tutorial/requirements.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quick_tutorial/requirements.rst b/docs/quick_tutorial/requirements.rst index f855dcb55..b00cb141e 100644 --- a/docs/quick_tutorial/requirements.rst +++ b/docs/quick_tutorial/requirements.rst @@ -109,7 +109,7 @@ For Linux, the commands to do so are as follows: For Windows: -.. code-block:: ps1con +.. code-block:: powershell # Windows c:\> cd \ -- cgit v1.2.3 From 3ef43f3560ed1b28def03bb8973c0f287134ba6b Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Sun, 14 Feb 2016 07:07:18 -0500 Subject: Revert "Use lexer name compatible w/ Pygments 1.5." This reverts commit eee002a2b0b010834a2ebede550329dbea3b3732. --- docs/quick_tutorial/requirements.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quick_tutorial/requirements.rst b/docs/quick_tutorial/requirements.rst index b00cb141e..f855dcb55 100644 --- a/docs/quick_tutorial/requirements.rst +++ b/docs/quick_tutorial/requirements.rst @@ -109,7 +109,7 @@ For Linux, the commands to do so are as follows: For Windows: -.. code-block:: powershell +.. code-block:: ps1con # Windows c:\> cd \ -- cgit v1.2.3 From 42c93166fbe676341b1d94ec3659ae772dd073d8 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 14 Feb 2016 17:35:01 -0600 Subject: fix unicode issues with check_password --- docs/tutorials/wiki2/src/models/tutorial/models/user.py | 6 ++++-- docs/tutorials/wiki2/src/views/tutorial/models/user.py | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/tutorials/wiki2/src/models/tutorial/models/user.py b/docs/tutorials/wiki2/src/models/tutorial/models/user.py index 25b0a8187..6fb32a1b2 100644 --- a/docs/tutorials/wiki2/src/models/tutorial/models/user.py +++ b/docs/tutorials/wiki2/src/models/tutorial/models/user.py @@ -18,10 +18,12 @@ class User(Base): password_hash = Column(Text) def set_password(self, pw): - pwhash = bcrypt.hashpw(pw, bcrypt.gensalt()) + pwhash = bcrypt.hashpw(pw.encode('utf8'), bcrypt.gensalt()) self.password_hash = pwhash def check_password(self, pw): if self.password_hash is not None: - return bcrypt.hashpw(pw, self.password_hash) == self.password_hash + expected_hash = self.password_hash.encode('utf8') + actual_hash = bcrypt.hashpw(pw.encode('utf8'), expected_hash) + return expected_hash == actual_hash return False diff --git a/docs/tutorials/wiki2/src/views/tutorial/models/user.py b/docs/tutorials/wiki2/src/views/tutorial/models/user.py index 25b0a8187..6fb32a1b2 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/models/user.py +++ b/docs/tutorials/wiki2/src/views/tutorial/models/user.py @@ -18,10 +18,12 @@ class User(Base): password_hash = Column(Text) def set_password(self, pw): - pwhash = bcrypt.hashpw(pw, bcrypt.gensalt()) + pwhash = bcrypt.hashpw(pw.encode('utf8'), bcrypt.gensalt()) self.password_hash = pwhash def check_password(self, pw): if self.password_hash is not None: - return bcrypt.hashpw(pw, self.password_hash) == self.password_hash + expected_hash = self.password_hash.encode('utf8') + actual_hash = bcrypt.hashpw(pw.encode('utf8'), expected_hash) + return expected_hash == actual_hash return False -- cgit v1.2.3 From da5ebc28c38ea32ad99389b5bc23e2f847af8047 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 14 Feb 2016 17:40:03 -0600 Subject: split routes into a separate module --- docs/tutorials/wiki2/basiclayout.rst | 64 +++++++++++++--------- docs/tutorials/wiki2/definingviews.rst | 17 +++--- .../wiki2/src/basiclayout/tutorial/__init__.py | 3 +- .../wiki2/src/basiclayout/tutorial/routes.py | 3 + .../wiki2/src/models/tutorial/__init__.py | 3 +- docs/tutorials/wiki2/src/models/tutorial/routes.py | 3 + .../tutorials/wiki2/src/views/tutorial/__init__.py | 6 +- docs/tutorials/wiki2/src/views/tutorial/routes.py | 6 ++ pyramid/scaffolds/alchemy/+package+/__init__.py | 3 +- pyramid/scaffolds/alchemy/+package+/routes.py | 3 + 10 files changed, 66 insertions(+), 45 deletions(-) create mode 100644 docs/tutorials/wiki2/src/basiclayout/tutorial/routes.py create mode 100644 docs/tutorials/wiki2/src/models/tutorial/routes.py create mode 100644 docs/tutorials/wiki2/src/views/tutorial/routes.py create mode 100644 pyramid/scaffolds/alchemy/+package+/routes.py diff --git a/docs/tutorials/wiki2/basiclayout.rst b/docs/tutorials/wiki2/basiclayout.rst index 485d38047..1ae51eb93 100644 --- a/docs/tutorials/wiki2/basiclayout.rst +++ b/docs/tutorials/wiki2/basiclayout.rst @@ -74,35 +74,19 @@ exact setup of the models will be covered later. :lineno-match: :language: py -``main`` now calls :meth:`pyramid.config.Configurator.add_static_view` with -two arguments: ``static`` (the name), and ``static`` (the path): +Next include the ``routes`` module using a dotted Python path. This module +will be explained in the next section. .. literalinclude:: src/basiclayout/tutorial/__init__.py :lines: 10 :lineno-match: :language: py -This registers a static resource view which will match any URL that starts -with the prefix ``/static`` (by virtue of the first argument to -``add_static_view``). This will serve up static resources for us from within -the ``static`` directory of our ``tutorial`` package, in this case, via -``http://localhost:6543/static/`` and below (by virtue of the second argument -to ``add_static_view``). With this declaration, we're saying that any URL that -starts with ``/static`` should go to the static view; any remainder of its -path (e.g. the ``/foo`` in ``/static/foo``) will be used to compose a path to -a static file resource, such as a CSS file. - -Using the configurator ``main`` also registers a :term:`route configuration` -via the :meth:`pyramid.config.Configurator.add_route` method that will be -used when the URL is ``/``: - -.. literalinclude:: src/basiclayout/tutorial/__init__.py - :lines: 11 - :lineno-match: - :language: py +.. note:: -Since this route has a ``pattern`` equaling ``/``, it is the route that will -be matched when the URL ``/`` is visited, e.g., ``http://localhost:6543/``. + Pyramid's :meth:`pyramid.config.Configurator.include` method is the + primary mechanism for extending the configurator and breaking your code + into feature-focused modules. ``main`` next calls the ``scan`` method of the configurator (:meth:`pyramid.config.Configurator.scan`), which will recursively scan our @@ -112,7 +96,7 @@ view configuration will be registered, which will allow one of our application URLs to be mapped to some code. .. literalinclude:: src/basiclayout/tutorial/__init__.py - :lines: 12 + :lines: 11 :lineno-match: :language: py @@ -121,11 +105,41 @@ Finally ``main`` is finished configuring things, so it uses the :term:`WSGI` application: .. literalinclude:: src/basiclayout/tutorial/__init__.py - :lines: 13 - :lineno-start: 13 + :lines: 12 + :lineno-match: :language: py +Route declarations +------------------ + +Open the ``tutorials/routes.py`` file. It should already contain the +following: + +.. literalinclude:: src/basiclayout/tutorial/routes.py + :linenos: + :language: py + +First, on line 2, call :meth:`pyramid.config.Configurator.add_static_view` +with two arguments: ``static`` (the name), and ``static`` (the path). + +This registers a static resource view which will match any URL that starts +with the prefix ``/static`` (by virtue of the first argument to +``add_static_view``). This will serve up static resources for us from within +the ``static`` directory of our ``tutorial`` package, in this case, via +``http://localhost:6543/static/`` and below (by virtue of the second argument +to ``add_static_view``). With this declaration, we're saying that any URL that +starts with ``/static`` should go to the static view; any remainder of its +path (e.g. the ``/foo`` in ``/static/foo``) will be used to compose a path to +a static file resource, such as a CSS file. + +Second, on line 3, the module registers a :term:`route configuration` +via the :meth:`pyramid.config.Configurator.add_route` method that will be +used when the URL is ``/``. Since this route has a ``pattern`` equaling +``/``, it is the route that will be matched when the URL ``/`` is visited, +e.g., ``http://localhost:6543/``. + + View declarations via the ``views`` package ------------------------------------------- diff --git a/docs/tutorials/wiki2/definingviews.rst b/docs/tutorials/wiki2/definingviews.rst index fea682628..184f9e1fa 100644 --- a/docs/tutorials/wiki2/definingviews.rst +++ b/docs/tutorials/wiki2/definingviews.rst @@ -46,19 +46,19 @@ were provided at the time we created the project. As an example, the CSS file will be accessed via ``http://localhost:6543/static/theme.css`` by virtue of the call to the -``add_static_view`` directive we've made in the ``__init__.py`` file. Any +``add_static_view`` directive we've made in the ``routes.py`` file. Any number and type of static assets can be placed in this directory (or subdirectories) and are just referred to by URL or by using the convenience method ``static_url``, e.g., ``request.static_url(':static/foo.css')`` within templates. -Adding routes to ``__init__.py`` -================================ +Adding routes to ``routes.py`` +============================== This is the URL Dispatch tutorial and so let's start by adding some URL patterns to our app. Later we'll attach views to handle the URLs. -The ``__init__.py`` file contains +The ``routes.py`` file contains :meth:`pyramid.config.Configurator.add_route` calls which serve to add routes to our application. First, we’ll get rid of the existing route created by the template using the name ``'home'``. It’s only an example and isn’t @@ -66,7 +66,7 @@ relevant to our application. We then need to add four calls to ``add_route``. Note that the *ordering* of these declarations is very important. ``route`` declarations are matched in -the order they're found in the ``__init__.py`` file. +the order they're registered. #. Add a declaration which maps the pattern ``/`` (signifying the root URL) to the route named ``view_wiki``. It maps to our ``view_wiki`` view @@ -91,12 +91,11 @@ the order they're found in the ``__init__.py`` file. attached to the ``edit_page`` view function indicating ``route_name='edit_page'``. -As a result of our edits, the ``__init__.py`` file should look -something like: +As a result of our edits, the ``routes.py`` file should look something like: -.. literalinclude:: src/views/tutorial/__init__.py +.. literalinclude:: src/views/tutorial/routes.py :linenos: - :emphasize-lines: 11-14 + :emphasize-lines: 3-6 :language: python The highlighted lines are the ones that need to be added or edited. diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py index 17763812a..4dab44823 100644 --- a/docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/__init__.py @@ -7,7 +7,6 @@ def main(global_config, **settings): config = Configurator(settings=settings) config.include('pyramid_jinja2') config.include('.models') - config.add_static_view('static', 'static', cache_max_age=3600) - config.add_route('home', '/') + config.include('.routes') config.scan() return config.make_wsgi_app() diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/routes.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/routes.py new file mode 100644 index 000000000..25504ad4d --- /dev/null +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/routes.py @@ -0,0 +1,3 @@ +def includeme(config): + config.add_static_view('static', 'static', cache_max_age=3600) + config.add_route('home', '/') diff --git a/docs/tutorials/wiki2/src/models/tutorial/__init__.py b/docs/tutorials/wiki2/src/models/tutorial/__init__.py index 17763812a..4dab44823 100644 --- a/docs/tutorials/wiki2/src/models/tutorial/__init__.py +++ b/docs/tutorials/wiki2/src/models/tutorial/__init__.py @@ -7,7 +7,6 @@ def main(global_config, **settings): config = Configurator(settings=settings) config.include('pyramid_jinja2') config.include('.models') - config.add_static_view('static', 'static', cache_max_age=3600) - config.add_route('home', '/') + config.include('.routes') config.scan() return config.make_wsgi_app() diff --git a/docs/tutorials/wiki2/src/models/tutorial/routes.py b/docs/tutorials/wiki2/src/models/tutorial/routes.py new file mode 100644 index 000000000..25504ad4d --- /dev/null +++ b/docs/tutorials/wiki2/src/models/tutorial/routes.py @@ -0,0 +1,3 @@ +def includeme(config): + config.add_static_view('static', 'static', cache_max_age=3600) + config.add_route('home', '/') diff --git a/docs/tutorials/wiki2/src/views/tutorial/__init__.py b/docs/tutorials/wiki2/src/views/tutorial/__init__.py index 5d8c7fba2..4dab44823 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/__init__.py +++ b/docs/tutorials/wiki2/src/views/tutorial/__init__.py @@ -7,10 +7,6 @@ def main(global_config, **settings): config = Configurator(settings=settings) config.include('pyramid_jinja2') config.include('.models') - config.add_static_view('static', 'static', cache_max_age=3600) - config.add_route('view_wiki', '/') - config.add_route('view_page', '/{pagename}') - config.add_route('add_page', '/add_page/{pagename}') - config.add_route('edit_page', '/{pagename}/edit_page') + config.include('.routes') config.scan() return config.make_wsgi_app() diff --git a/docs/tutorials/wiki2/src/views/tutorial/routes.py b/docs/tutorials/wiki2/src/views/tutorial/routes.py new file mode 100644 index 000000000..72df58efe --- /dev/null +++ b/docs/tutorials/wiki2/src/views/tutorial/routes.py @@ -0,0 +1,6 @@ +def includeme(config): + config.add_static_view('static', 'static', cache_max_age=3600) + config.add_route('view_wiki', '/') + config.add_route('view_page', '/{pagename}') + config.add_route('add_page', '/add_page/{pagename}') + config.add_route('edit_page', '/{pagename}/edit_page') diff --git a/pyramid/scaffolds/alchemy/+package+/__init__.py b/pyramid/scaffolds/alchemy/+package+/__init__.py index 17763812a..4dab44823 100644 --- a/pyramid/scaffolds/alchemy/+package+/__init__.py +++ b/pyramid/scaffolds/alchemy/+package+/__init__.py @@ -7,7 +7,6 @@ def main(global_config, **settings): config = Configurator(settings=settings) config.include('pyramid_jinja2') config.include('.models') - config.add_static_view('static', 'static', cache_max_age=3600) - config.add_route('home', '/') + config.include('.routes') config.scan() return config.make_wsgi_app() diff --git a/pyramid/scaffolds/alchemy/+package+/routes.py b/pyramid/scaffolds/alchemy/+package+/routes.py new file mode 100644 index 000000000..25504ad4d --- /dev/null +++ b/pyramid/scaffolds/alchemy/+package+/routes.py @@ -0,0 +1,3 @@ +def includeme(config): + config.add_static_view('static', 'static', cache_max_age=3600) + config.add_route('home', '/') -- cgit v1.2.3 From 00b2c691f88fcf42dfc81222aed939833f7f1f05 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 14 Feb 2016 17:47:48 -0600 Subject: implement the authentication example code --- .../tutorials/wiki2/src/authentication/CHANGES.txt | 4 + .../tutorials/wiki2/src/authentication/MANIFEST.in | 2 + docs/tutorials/wiki2/src/authentication/README.txt | 14 ++ .../wiki2/src/authentication/development.ini | 73 ++++++++++ .../wiki2/src/authentication/production.ini | 62 +++++++++ docs/tutorials/wiki2/src/authentication/setup.py | 49 +++++++ .../wiki2/src/authentication/tutorial/__init__.py | 13 ++ .../src/authentication/tutorial/models/__init__.py | 74 ++++++++++ .../src/authentication/tutorial/models/meta.py | 16 +++ .../src/authentication/tutorial/models/page.py | 20 +++ .../src/authentication/tutorial/models/user.py | 29 ++++ .../wiki2/src/authentication/tutorial/routes.py | 8 ++ .../authentication/tutorial/scripts/__init__.py | 1 + .../tutorial/scripts/initializedb.py | 57 ++++++++ .../wiki2/src/authentication/tutorial/security.py | 28 ++++ .../tutorial/static/pyramid-16x16.png | Bin 0 -> 1319 bytes .../src/authentication/tutorial/static/pyramid.png | Bin 0 -> 12901 bytes .../src/authentication/tutorial/static/theme.css | 154 +++++++++++++++++++++ .../authentication/tutorial/static/theme.min.css | 1 + .../authentication/tutorial/templates/404.jinja2 | 8 ++ .../authentication/tutorial/templates/edit.jinja2 | 20 +++ .../tutorial/templates/layout.jinja2 | 64 +++++++++ .../authentication/tutorial/templates/login.jinja2 | 26 ++++ .../authentication/tutorial/templates/view.jinja2 | 18 +++ .../wiki2/src/authentication/tutorial/tests.py | 65 +++++++++ .../src/authentication/tutorial/views/__init__.py | 0 .../src/authentication/tutorial/views/auth.py | 44 ++++++ .../src/authentication/tutorial/views/default.py | 76 ++++++++++ .../src/authentication/tutorial/views/notfound.py | 7 + 29 files changed, 933 insertions(+) create mode 100644 docs/tutorials/wiki2/src/authentication/CHANGES.txt create mode 100644 docs/tutorials/wiki2/src/authentication/MANIFEST.in create mode 100644 docs/tutorials/wiki2/src/authentication/README.txt create mode 100644 docs/tutorials/wiki2/src/authentication/development.ini create mode 100644 docs/tutorials/wiki2/src/authentication/production.ini create mode 100644 docs/tutorials/wiki2/src/authentication/setup.py create mode 100644 docs/tutorials/wiki2/src/authentication/tutorial/__init__.py create mode 100644 docs/tutorials/wiki2/src/authentication/tutorial/models/__init__.py create mode 100644 docs/tutorials/wiki2/src/authentication/tutorial/models/meta.py create mode 100644 docs/tutorials/wiki2/src/authentication/tutorial/models/page.py create mode 100644 docs/tutorials/wiki2/src/authentication/tutorial/models/user.py create mode 100644 docs/tutorials/wiki2/src/authentication/tutorial/routes.py create mode 100644 docs/tutorials/wiki2/src/authentication/tutorial/scripts/__init__.py create mode 100644 docs/tutorials/wiki2/src/authentication/tutorial/scripts/initializedb.py create mode 100644 docs/tutorials/wiki2/src/authentication/tutorial/security.py create mode 100644 docs/tutorials/wiki2/src/authentication/tutorial/static/pyramid-16x16.png create mode 100644 docs/tutorials/wiki2/src/authentication/tutorial/static/pyramid.png create mode 100644 docs/tutorials/wiki2/src/authentication/tutorial/static/theme.css create mode 100644 docs/tutorials/wiki2/src/authentication/tutorial/static/theme.min.css create mode 100644 docs/tutorials/wiki2/src/authentication/tutorial/templates/404.jinja2 create mode 100644 docs/tutorials/wiki2/src/authentication/tutorial/templates/edit.jinja2 create mode 100644 docs/tutorials/wiki2/src/authentication/tutorial/templates/layout.jinja2 create mode 100644 docs/tutorials/wiki2/src/authentication/tutorial/templates/login.jinja2 create mode 100644 docs/tutorials/wiki2/src/authentication/tutorial/templates/view.jinja2 create mode 100644 docs/tutorials/wiki2/src/authentication/tutorial/tests.py create mode 100644 docs/tutorials/wiki2/src/authentication/tutorial/views/__init__.py create mode 100644 docs/tutorials/wiki2/src/authentication/tutorial/views/auth.py create mode 100644 docs/tutorials/wiki2/src/authentication/tutorial/views/default.py create mode 100644 docs/tutorials/wiki2/src/authentication/tutorial/views/notfound.py diff --git a/docs/tutorials/wiki2/src/authentication/CHANGES.txt b/docs/tutorials/wiki2/src/authentication/CHANGES.txt new file mode 100644 index 000000000..35a34f332 --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/CHANGES.txt @@ -0,0 +1,4 @@ +0.0 +--- + +- Initial version diff --git a/docs/tutorials/wiki2/src/authentication/MANIFEST.in b/docs/tutorials/wiki2/src/authentication/MANIFEST.in new file mode 100644 index 000000000..42cd299b5 --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/MANIFEST.in @@ -0,0 +1,2 @@ +include *.txt *.ini *.cfg *.rst +recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.jinja2 *.pt *.txt *.mak *.mako *.js *.html *.xml diff --git a/docs/tutorials/wiki2/src/authentication/README.txt b/docs/tutorials/wiki2/src/authentication/README.txt new file mode 100644 index 000000000..68f430110 --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/README.txt @@ -0,0 +1,14 @@ +tutorial README +================== + +Getting Started +--------------- + +- cd + +- $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/authentication/development.ini b/docs/tutorials/wiki2/src/authentication/development.ini new file mode 100644 index 000000000..f3079727e --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/development.ini @@ -0,0 +1,73 @@ +### +# 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 +pyramid.debug_routematch = false +pyramid.default_locale_name = en +pyramid.includes = + pyramid_debugtoolbar + pyramid_tm + +sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite + +auth.secret = seekrit + +# 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 = 127.0.0.1 +port = 6543 + +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +### + +[loggers] +keys = root, tutorial, sqlalchemy + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + +[logger_sqlalchemy] +level = INFO +handlers = +qualname = sqlalchemy.engine +# "level = INFO" logs SQL queries. +# "level = DEBUG" logs SQL queries and results. +# "level = WARN" logs neither. (Recommended for production systems.) + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/tutorials/wiki2/src/authentication/production.ini b/docs/tutorials/wiki2/src/authentication/production.ini new file mode 100644 index 000000000..686dba48a --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/production.ini @@ -0,0 +1,62 @@ +### +# 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 +pyramid.debug_routematch = false +pyramid.default_locale_name = en + +sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite + +auth.secret = real-seekrit + +[server:main] +use = egg:waitress#main +host = 0.0.0.0 +port = 6543 + +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +### + +[loggers] +keys = root, tutorial, sqlalchemy + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console + +[logger_tutorial] +level = WARN +handlers = +qualname = tutorial + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine +# "level = INFO" logs SQL queries. +# "level = DEBUG" logs SQL queries and results. +# "level = WARN" logs neither. (Recommended for production systems.) + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/tutorials/wiki2/src/authentication/setup.py b/docs/tutorials/wiki2/src/authentication/setup.py new file mode 100644 index 000000000..c342c1aba --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/setup.py @@ -0,0 +1,49 @@ +import os + +from setuptools import setup, find_packages + +here = os.path.abspath(os.path.dirname(__file__)) +with open(os.path.join(here, 'README.txt')) as f: + README = f.read() +with open(os.path.join(here, 'CHANGES.txt')) as f: + CHANGES = f.read() + +requires = [ + 'bcrypt', + 'docutils', + 'pyramid', + 'pyramid_jinja2', + 'pyramid_debugtoolbar', + 'pyramid_tm', + 'SQLAlchemy', + 'transaction', + 'zope.sqlalchemy', + 'waitress', + ] + +setup(name='tutorial', + version='0.0', + description='tutorial', + long_description=README + '\n\n' + CHANGES, + classifiers=[ + "Programming Language :: Python", + "Framework :: Pyramid", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", + ], + author='', + author_email='', + url='', + keywords='web wsgi bfg pylons pyramid', + packages=find_packages(), + include_package_data=True, + zip_safe=False, + test_suite='tutorial', + 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/authentication/tutorial/__init__.py b/docs/tutorials/wiki2/src/authentication/tutorial/__init__.py new file mode 100644 index 000000000..f5c033b8b --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/__init__.py @@ -0,0 +1,13 @@ +from pyramid.config import Configurator + + +def main(global_config, **settings): + """ This function returns a Pyramid WSGI application. + """ + config = Configurator(settings=settings) + config.include('pyramid_jinja2') + config.include('.models') + config.include('.routes') + config.include('.security') + config.scan() + return config.make_wsgi_app() diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/authentication/tutorial/models/__init__.py new file mode 100644 index 000000000..a8871f6f5 --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/models/__init__.py @@ -0,0 +1,74 @@ +from sqlalchemy import engine_from_config +from sqlalchemy.orm import sessionmaker +from sqlalchemy.orm import configure_mappers +import zope.sqlalchemy + +# import or define all models here to ensure they are attached to the +# Base.metadata prior to any initialization routines +from .page import Page # flake8: noqa +from .user import User # flake8: noqa + +# run configure_mappers after defining all of the models to ensure +# all relationships can be setup +configure_mappers() + + +def get_engine(settings, prefix='sqlalchemy.'): + return engine_from_config(settings, prefix) + + +def get_session_factory(engine): + factory = sessionmaker() + factory.configure(bind=engine) + return factory + + +def get_tm_session(session_factory, transaction_manager): + """ + Get a ``sqlalchemy.orm.Session`` instance backed by a transaction. + + This function will hook the session to the transaction manager which + will take care of committing any changes. + + - When using pyramid_tm it will automatically be committed or aborted + depending on whether an exception is raised. + + - When using scripts you should wrap the session in a manager yourself. + For example:: + + import transaction + + engine = get_engine(settings) + session_factory = get_session_factory(engine) + with transaction.manager: + dbsession = get_tm_session(session_factory, transaction.manager) + + """ + dbsession = session_factory() + zope.sqlalchemy.register( + dbsession, transaction_manager=transaction_manager) + return dbsession + + +def includeme(config): + """ + Initialize the model for a Pyramid app. + + Activate this setup using ``config.include('tutorial.models')``. + + """ + settings = config.get_settings() + + # use pyramid_tm to hook the transaction lifecycle to the request + config.include('pyramid_tm') + + session_factory = get_session_factory(get_engine(settings)) + config.registry['dbsession_factory'] = session_factory + + # make request.dbsession available for use in Pyramid + config.add_request_method( + # r.tm is the transaction manager used by pyramid_tm + lambda r: get_tm_session(session_factory, r.tm), + 'dbsession', + reify=True + ) diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/models/meta.py b/docs/tutorials/wiki2/src/authentication/tutorial/models/meta.py new file mode 100644 index 000000000..fc3e8f1dd --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/models/meta.py @@ -0,0 +1,16 @@ +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.schema import MetaData + +# Recommended naming convention used by Alembic, as various different database +# providers will autogenerate vastly different names making migrations more +# difficult. See: http://alembic.readthedocs.org/en/latest/naming.html +NAMING_CONVENTION = { + "ix": 'ix_%(column_0_label)s', + "uq": "uq_%(table_name)s_%(column_0_name)s", + "ck": "ck_%(table_name)s_%(constraint_name)s", + "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", + "pk": "pk_%(table_name)s" +} + +metadata = MetaData(naming_convention=NAMING_CONVENTION) +Base = declarative_base(metadata=metadata) diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/models/page.py b/docs/tutorials/wiki2/src/authentication/tutorial/models/page.py new file mode 100644 index 000000000..4dd5b5721 --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/models/page.py @@ -0,0 +1,20 @@ +from sqlalchemy import ( + Column, + ForeignKey, + Integer, + Text, +) +from sqlalchemy.orm import relationship + +from .meta import Base + + +class Page(Base): + """ The SQLAlchemy declarative model class for a Page object. """ + __tablename__ = 'pages' + id = Column(Integer, primary_key=True) + name = Column(Text, nullable=False, unique=True) + data = Column(Integer, nullable=False) + + creator_id = Column(ForeignKey('users.id'), nullable=False) + creator = relationship('User', backref='created_pages') diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/models/user.py b/docs/tutorials/wiki2/src/authentication/tutorial/models/user.py new file mode 100644 index 000000000..6fb32a1b2 --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/models/user.py @@ -0,0 +1,29 @@ +import bcrypt +from sqlalchemy import ( + Column, + Integer, + Text, +) + +from .meta import Base + + +class User(Base): + """ The SQLAlchemy declarative model class for a User object. """ + __tablename__ = 'users' + id = Column(Integer, primary_key=True) + name = Column(Text, nullable=False, unique=True) + role = Column(Text, nullable=False) + + password_hash = Column(Text) + + def set_password(self, pw): + pwhash = bcrypt.hashpw(pw.encode('utf8'), bcrypt.gensalt()) + self.password_hash = pwhash + + def check_password(self, pw): + if self.password_hash is not None: + expected_hash = self.password_hash.encode('utf8') + actual_hash = bcrypt.hashpw(pw.encode('utf8'), expected_hash) + return expected_hash == actual_hash + return False diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/routes.py b/docs/tutorials/wiki2/src/authentication/tutorial/routes.py new file mode 100644 index 000000000..cb747244f --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/routes.py @@ -0,0 +1,8 @@ +def includeme(config): + config.add_static_view('static', 'static', cache_max_age=3600) + config.add_route('view_wiki', '/') + config.add_route('login', '/login') + config.add_route('logout', '/logout') + config.add_route('view_page', '/{pagename}') + config.add_route('add_page', '/add_page/{pagename}') + config.add_route('edit_page', '/{pagename}/edit_page') diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/scripts/__init__.py b/docs/tutorials/wiki2/src/authentication/tutorial/scripts/__init__.py new file mode 100644 index 000000000..5bb534f79 --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/scripts/__init__.py @@ -0,0 +1 @@ +# package diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/authentication/tutorial/scripts/initializedb.py new file mode 100644 index 000000000..f3c0a6fef --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/scripts/initializedb.py @@ -0,0 +1,57 @@ +import os +import sys +import transaction + +from pyramid.paster import ( + get_appsettings, + setup_logging, + ) + +from pyramid.scripts.common import parse_vars + +from ..models.meta import Base +from ..models import ( + get_engine, + get_session_factory, + get_tm_session, + ) +from ..models import Page, User + + +def usage(argv): + cmd = os.path.basename(argv[0]) + print('usage: %s [var=value]\n' + '(example: "%s development.ini")' % (cmd, cmd)) + sys.exit(1) + + +def main(argv=sys.argv): + if len(argv) < 2: + usage(argv) + config_uri = argv[1] + options = parse_vars(argv[2:]) + setup_logging(config_uri) + settings = get_appsettings(config_uri, options=options) + + engine = get_engine(settings) + Base.metadata.create_all(engine) + + session_factory = get_session_factory(engine) + + with transaction.manager: + dbsession = get_tm_session(session_factory, transaction.manager) + + editor = User(name='editor', role='editor') + editor.set_password('editor') + dbsession.add(editor) + + basic = User(name='basic', role='basic') + basic.set_password('basic') + dbsession.add(basic) + + page = Page( + name='FrontPage', + creator=editor, + data='This is the front page', + ) + dbsession.add(page) diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/security.py b/docs/tutorials/wiki2/src/authentication/tutorial/security.py new file mode 100644 index 000000000..24035c8b9 --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/security.py @@ -0,0 +1,28 @@ +from pyramid.authentication import AuthTktAuthenticationPolicy +from pyramid.authorization import ACLAuthorizationPolicy + +from .models import User + + +class MyAuthenticationPolicy(AuthTktAuthenticationPolicy): + def authenticated_userid(self, request): + user = request.user + if user is not None: + return user.id + +def get_user(request): + user_id = request.unauthenticated_userid + if user_id is not None: + user = request.dbsession.query(User).get(user_id) + return user + +def includeme(config): + settings = config.get_settings() + authn_policy = MyAuthenticationPolicy( + settings['auth.secret'], + hashalg='sha512', + ) + + config.set_authentication_policy(authn_policy) + config.set_authorization_policy(ACLAuthorizationPolicy()) + config.add_request_method(get_user, 'user', reify=True) diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/static/pyramid-16x16.png b/docs/tutorials/wiki2/src/authentication/tutorial/static/pyramid-16x16.png new file mode 100644 index 000000000..979203112 Binary files /dev/null and b/docs/tutorials/wiki2/src/authentication/tutorial/static/pyramid-16x16.png differ diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/static/pyramid.png b/docs/tutorials/wiki2/src/authentication/tutorial/static/pyramid.png new file mode 100644 index 000000000..4ab837be9 Binary files /dev/null and b/docs/tutorials/wiki2/src/authentication/tutorial/static/pyramid.png differ diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/static/theme.css b/docs/tutorials/wiki2/src/authentication/tutorial/static/theme.css new file mode 100644 index 000000000..0f4b1a4d4 --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/static/theme.css @@ -0,0 +1,154 @@ +@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700); +body { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; + color: #ffffff; + background: #bc2131; +} +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; +} +p { + font-weight: 300; +} +.font-normal { + font-weight: 400; +} +.font-semi-bold { + font-weight: 600; +} +.font-bold { + font-weight: 700; +} +.starter-template { + margin-top: 250px; +} +.starter-template .content { + margin-left: 10px; +} +.starter-template .content h1 { + margin-top: 10px; + font-size: 60px; +} +.starter-template .content h1 .smaller { + font-size: 40px; + color: #f2b7bd; +} +.starter-template .content .lead { + font-size: 25px; + color: #f2b7bd; +} +.starter-template .content .lead .font-normal { + color: #ffffff; +} +.starter-template .links { + float: right; + right: 0; + margin-top: 125px; +} +.starter-template .links ul { + display: block; + padding: 0; + margin: 0; +} +.starter-template .links ul li { + list-style: none; + display: inline; + margin: 0 10px; +} +.starter-template .links ul li:first-child { + margin-left: 0; +} +.starter-template .links ul li:last-child { + margin-right: 0; +} +.starter-template .links ul li.current-version { + color: #f2b7bd; + font-weight: 400; +} +.starter-template .links ul li a, a { + color: #f2b7bd; + text-decoration: underline; +} +.starter-template .links ul li a:hover, a:hover { + color: #ffffff; + text-decoration: underline; +} +.starter-template .links ul li .icon-muted { + color: #eb8b95; + margin-right: 5px; +} +.starter-template .links ul li:hover .icon-muted { + color: #ffffff; +} +.starter-template .copyright { + margin-top: 10px; + font-size: 0.9em; + color: #f2b7bd; + text-transform: lowercase; + float: right; + right: 0; +} +@media (max-width: 1199px) { + .starter-template .content h1 { + font-size: 45px; + } + .starter-template .content h1 .smaller { + font-size: 30px; + } + .starter-template .content .lead { + font-size: 20px; + } +} +@media (max-width: 991px) { + .starter-template { + margin-top: 0; + } + .starter-template .logo { + margin: 40px auto; + } + .starter-template .content { + margin-left: 0; + text-align: center; + } + .starter-template .content h1 { + margin-bottom: 20px; + } + .starter-template .links { + float: none; + text-align: center; + margin-top: 60px; + } + .starter-template .copyright { + float: none; + text-align: center; + } +} +@media (max-width: 767px) { + .starter-template .content h1 .smaller { + font-size: 25px; + display: block; + } + .starter-template .content .lead { + font-size: 16px; + } + .starter-template .links { + margin-top: 40px; + } + .starter-template .links ul li { + display: block; + margin: 0; + } + .starter-template .links ul li .icon-muted { + display: none; + } + .starter-template .copyright { + margin-top: 20px; + } +} diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/static/theme.min.css b/docs/tutorials/wiki2/src/authentication/tutorial/static/theme.min.css new file mode 100644 index 000000000..0d25de5b6 --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/static/theme.min.css @@ -0,0 +1 @@ +@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);body{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300;color:#fff;background:#bc2131}h1,h2,h3,h4,h5,h6{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300}p{font-weight:300}.font-normal{font-weight:400}.font-semi-bold{font-weight:600}.font-bold{font-weight:700}.starter-template{margin-top:250px}.starter-template .content{margin-left:10px}.starter-template .content h1{margin-top:10px;font-size:60px}.starter-template .content h1 .smaller{font-size:40px;color:#f2b7bd}.starter-template .content .lead{font-size:25px;color:#f2b7bd}.starter-template .content .lead .font-normal{color:#fff}.starter-template .links{float:right;right:0;margin-top:125px}.starter-template .links ul{display:block;padding:0;margin:0}.starter-template .links ul li{list-style:none;display:inline;margin:0 10px}.starter-template .links ul li:first-child{margin-left:0}.starter-template .links ul li:last-child{margin-right:0}.starter-template .links ul li.current-version{color:#f2b7bd;font-weight:400}.starter-template .links ul li a,a{color:#f2b7bd;text-decoration:underline}.starter-template .links ul li a:hover,a:hover{color:#fff;text-decoration:underline}.starter-template .links ul li .icon-muted{color:#eb8b95;margin-right:5px}.starter-template .links ul li:hover .icon-muted{color:#fff}.starter-template .copyright{margin-top:10px;font-size:.9em;color:#f2b7bd;text-transform:lowercase;float:right;right:0}@media (max-width:1199px){.starter-template .content h1{font-size:45px}.starter-template .content h1 .smaller{font-size:30px}.starter-template .content .lead{font-size:20px}}@media (max-width:991px){.starter-template{margin-top:0}.starter-template .logo{margin:40px auto}.starter-template .content{margin-left:0;text-align:center}.starter-template .content h1{margin-bottom:20px}.starter-template .links{float:none;text-align:center;margin-top:60px}.starter-template .copyright{float:none;text-align:center}}@media (max-width:767px){.starter-template .content h1 .smaller{font-size:25px;display:block}.starter-template .content .lead{font-size:16px}.starter-template .links{margin-top:40px}.starter-template .links ul li{display:block;margin:0}.starter-template .links ul li .icon-muted{display:none}.starter-template .copyright{margin-top:20px}} diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/templates/404.jinja2 b/docs/tutorials/wiki2/src/authentication/tutorial/templates/404.jinja2 new file mode 100644 index 000000000..37b0a16b6 --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/templates/404.jinja2 @@ -0,0 +1,8 @@ +{% extends "layout.jinja2" %} + +{% block content %} +

+

Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki)

+

404 Page Not Found

+
+{% endblock content %} diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/templates/edit.jinja2 b/docs/tutorials/wiki2/src/authentication/tutorial/templates/edit.jinja2 new file mode 100644 index 000000000..7db25c674 --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/templates/edit.jinja2 @@ -0,0 +1,20 @@ +{% extends 'layout.jinja2' %} + +{% block subtitle %}Edit {{pagename}} - {% endblock subtitle %} + +{% block content %} +

+Editing {{pagename}} +

+

You can return to the +FrontPage. +

+
+
+ +
+
+ +
+
+{% endblock content %} diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/templates/layout.jinja2 b/docs/tutorials/wiki2/src/authentication/tutorial/templates/layout.jinja2 new file mode 100644 index 000000000..44d14304e --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/templates/layout.jinja2 @@ -0,0 +1,64 @@ + + + + + + + + + + + {% block subtitle %}{% endblock %}Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki) + + + + + + + + + + + + + +
+
+
+
+ +
+
+
+ {% if request.user is none %} +

+ Login +

+ {% else %} +

+ {{request.user.name}} Logout +

+ {% endif %} + {% block content %}{% endblock %} +
+
+
+
+ +
+
+
+ + + + + + + + diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/templates/login.jinja2 b/docs/tutorials/wiki2/src/authentication/tutorial/templates/login.jinja2 new file mode 100644 index 000000000..1806de0ff --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/templates/login.jinja2 @@ -0,0 +1,26 @@ +{% extends 'layout.jinja2' %} + +{% block title %}Login - {% endblock title %} + +{% block content %} +

+ + Login +
+{{ message }} +

+
+ +
+ + +
+
+ + +
+
+ +
+
+{% endblock content %} diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/templates/view.jinja2 b/docs/tutorials/wiki2/src/authentication/tutorial/templates/view.jinja2 new file mode 100644 index 000000000..94419e228 --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/templates/view.jinja2 @@ -0,0 +1,18 @@ +{% extends 'layout.jinja2' %} + +{% block subtitle %}{{page.name}} - {% endblock subtitle %} + +{% block content %} +

{{ content|safe }}

+

+ + Edit this page + +

+

+ Viewing {{page.name}}, created by {{page.creator.name}}. +

+

You can return to the +FrontPage. +

+{% endblock content %} diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/tests.py b/docs/tutorials/wiki2/src/authentication/tutorial/tests.py new file mode 100644 index 000000000..c54945c28 --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/tests.py @@ -0,0 +1,65 @@ +import unittest +import transaction + +from pyramid import testing + + +def dummy_request(dbsession): + return testing.DummyRequest(dbsession=dbsession) + + +class BaseTest(unittest.TestCase): + def setUp(self): + self.config = testing.setUp(settings={ + 'sqlalchemy.url': 'sqlite:///:memory:' + }) + self.config.include('.models') + settings = self.config.get_settings() + + from .models import ( + get_engine, + get_session_factory, + get_tm_session, + ) + + self.engine = get_engine(settings) + session_factory = get_session_factory(self.engine) + + self.session = get_tm_session(session_factory, transaction.manager) + + def init_database(self): + from .models import Base + Base.metadata.create_all(self.engine) + + def tearDown(self): + from .models.meta import Base + + testing.tearDown() + transaction.abort() + Base.metadata.drop_all(self.engine) + + +class TestMyViewSuccessCondition(BaseTest): + + def setUp(self): + super(TestMyViewSuccessCondition, self).setUp() + self.init_database() + + from .models import MyModel + + model = MyModel(name='one', value=55) + self.session.add(model) + + def test_passing_view(self): + from .views.default import my_view + info = my_view(dummy_request(self.session)) + self.assertEqual(info['one'].name, 'one') + self.assertEqual(info['project'], 'tutorial') + + +class TestMyViewFailureCondition(BaseTest): + + def test_failing_view(self): + from .views.default import my_view + info = my_view(dummy_request(self.session)) + self.assertEqual(info.status_int, 500) diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/views/__init__.py b/docs/tutorials/wiki2/src/authentication/tutorial/views/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/views/auth.py b/docs/tutorials/wiki2/src/authentication/tutorial/views/auth.py new file mode 100644 index 000000000..d3db34132 --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/views/auth.py @@ -0,0 +1,44 @@ +from pyramid.httpexceptions import HTTPFound +from pyramid.security import ( + remember, + forget, + ) +from pyramid.view import ( + forbidden_view_config, + view_config, +) + +from ..models import User + + +@view_config(route_name='login', renderer='../templates/login.jinja2') +def login(request): + next_url = request.params.get('next', request.referrer) + message = '' + login = '' + if 'form.submitted' in request.params: + login = request.params['login'] + password = request.params['password'] + user = request.dbsession.query(User).filter_by(name=login).first() + if user is not None and user.check_password(password): + headers = remember(request, user.id) + return HTTPFound(location=next_url, headers=headers) + message = 'Failed login' + + return dict( + message=message, + url=request.route_url('login'), + next_url=next_url, + login=login, + ) + +@view_config(route_name='logout') +def logout(request): + headers = forget(request) + next_url = request.route_url('view_wiki') + return HTTPFound(location=next_url, headers=headers) + +@forbidden_view_config() +def forbidden_view(request): + next_url = request.route_url('login', _query={'next': request.url}) + return HTTPFound(location=next_url) diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py b/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py new file mode 100644 index 000000000..55aa74d04 --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py @@ -0,0 +1,76 @@ +import cgi +import re +from docutils.core import publish_parts + +from pyramid.httpexceptions import ( + HTTPForbidden, + HTTPFound, + HTTPNotFound, + ) + +from pyramid.view import view_config + +from ..models import Page + +# regular expression used to find WikiWords +wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)") + +@view_config(route_name='view_wiki') +def view_wiki(request): + next_url = request.route_url('view_page', pagename='FrontPage') + return HTTPFound(location=next_url) + +@view_config(route_name='view_page', renderer='../templates/view.jinja2') +def view_page(request): + pagename = request.matchdict['pagename'] + page = request.dbsession.query(Page).filter_by(name=pagename).first() + if page is None: + raise HTTPNotFound('No such page') + + def add_link(match): + word = match.group(1) + exists = request.dbsession.query(Page).filter_by(name=word).all() + if exists: + view_url = request.route_url('view_page', pagename=word) + return '%s' % (view_url, cgi.escape(word)) + else: + add_url = request.route_url('add_page', pagename=word) + return '%s' % (add_url, cgi.escape(word)) + + content = publish_parts(page.data, writer_name='html')['html_body'] + content = wikiwords.sub(add_link, content) + edit_url = request.route_url('edit_page', pagename=pagename) + return dict(page=page, content=content, edit_url=edit_url) + +@view_config(route_name='edit_page', renderer='../templates/edit.jinja2') +def edit_page(request): + pagename = request.matchdict['pagename'] + page = request.dbsession.query(Page).filter_by(name=pagename).one() + user = request.user + if user is None or (user.role != 'editor' and page.creator != user): + raise HTTPForbidden + if 'form.submitted' in request.params: + page.data = request.params['body'] + next_url = request.route_url('view_page', pagename=pagename) + return HTTPFound(location=next_url) + return dict( + pagename=page.name, + pagedata=page.data, + save_url=request.route_url('edit_page', pagename=pagename), + ) + +@view_config(route_name='add_page', renderer='../templates/edit.jinja2') +def add_page(request): + user = request.user + if user is None or user.role not in ('editor', 'basic'): + raise HTTPForbidden + pagename = request.matchdict['pagename'] + if 'form.submitted' in request.params: + body = request.params['body'] + page = Page(name=pagename, data=body) + page.creator = request.user + request.dbsession.add(page) + next_url = request.route_url('view_page', pagename=pagename) + return HTTPFound(location=next_url) + save_url = request.route_url('add_page', pagename=pagename) + return dict(pagename=pagename, pagedata='', save_url=save_url) diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/views/notfound.py b/docs/tutorials/wiki2/src/authentication/tutorial/views/notfound.py new file mode 100644 index 000000000..69d6e2804 --- /dev/null +++ b/docs/tutorials/wiki2/src/authentication/tutorial/views/notfound.py @@ -0,0 +1,7 @@ +from pyramid.view import notfound_view_config + + +@notfound_view_config(renderer='../templates/404.jinja2') +def notfound_view(request): + request.response.status = 404 + return {} -- cgit v1.2.3 From 659a254157c25f9f161f24403a22a2b349d37c67 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 16 Feb 2016 00:18:24 -0600 Subject: add a new authentication chapter --- docs/tutorials/wiki2/authentication.rst | 296 ++++++++++++++++++++++++++++++++ docs/tutorials/wiki2/index.rst | 1 + 2 files changed, 297 insertions(+) create mode 100644 docs/tutorials/wiki2/authentication.rst diff --git a/docs/tutorials/wiki2/authentication.rst b/docs/tutorials/wiki2/authentication.rst new file mode 100644 index 000000000..c33ed5138 --- /dev/null +++ b/docs/tutorials/wiki2/authentication.rst @@ -0,0 +1,296 @@ +.. _wiki2_adding_authentication: + +===================== +Adding authentication +===================== + +:app:`Pyramid` provides facilities for :term:`authentication` and +:term:`authorization`. In this section we'll focus solely on the +authentication APIs to add login/logout functionality to our wiki. + +We will implement authentication with the following steps: + +* Add an :term:`authentication policy` and a ``request.user`` computed + property (``security.py``). +* Add routes for /login and /logout (``routes.py``). +* Add login and logout views (``views/auth.py``). +* Add a login template (``login.jinja2``). +* Add "Login" and "Logout" links to every page based on the user's + authenticated state (``layout.jinja2``). +* Make the existing views verify user state (``views/default.py``). +* Redirect to /login when a user is denied access to any of the views + that require permission, instead of a default "403 Forbidden" page + (``views/auth.py``). + +Authenticating requests +----------------------- + +The core of :app:`Pyramid` authentication is a :term:`authentication policy` +which is used to identify authentication information from a ``request``, +as well as handling the low-level login/logout operations required to +track users across requests (via cookies or headers or whatever else you can +imagine). + +Add the authentication policy +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Create a new file ``tutorial/security.py``: + +.. literalinclude:: src/authentication/tutorial/security.py + :linenos: + :emphasize-lines: 9,14,28 + :language: python + +Here we've defined: + +* A new authentication policy named ``MyAuthenticationPolicy`` which is + subclassed from pyramid's + :class:`pyramid.authentication.AuthTktAuthenticationPolicy` which tracks + the :term:`userid` using a signed cookie. +* A ``get_user`` function which can convert the ``unauthenticated_userid`` + from the policy into a ``User`` object from our database. +* Finally, the ``get_user`` is registered on the request as ``request.user`` + to be used throughout our application as the authenticated ``User`` object + for the logged-in user. + +The logic in this file is a little bit interesting and so we'll go into +detail about what's happening here: + +First, the default authentication policies all provide a method named +``unauthenticated_userid`` which is responsible for the low-level parsing +of the information in the request (cookies, headers, etc). If a ``userid`` +is found then it is returned from this method. This is named +``unauthenticated_userid`` because at the lowest level it knows the value of +the userid in the cookie but it doesn't know if it's actually a user in our +system (remember, anything the user sends to our app is untrusted). + +Second, our application should only care about ``authenticated_userid`` and +``request.user`` which have gone through our application-specific process of +validating that the user is logged-in. + +In order to provide an ``authenticated_userid`` we need a verification step. +That can happen anywhere, so we've elected to do it inside of the cached +``request.user`` computed property. This is a convenience that makes +``request.user`` the source of truth in our system. It is either ``None`` or +a ``User`` object from our database. This is why the ``get_user`` function +uses the ``unauthenticated_userid`` to check the database + +Configure the app +~~~~~~~~~~~~~~~~~ + +Since we've added a new ``tutorial/security.py`` module we need to include it. + +Open the file ``tutorial/__init__.py`` and edit the following lines: + +.. literalinclude:: src/authentication/tutorial/__init__.py + :linenos: + :emphasize-lines: 11 + :language: python + +Our authentication policy is expecting a new setting, ``auth.secret``. Open +the file ``development.ini`` and add the highlighted line below: + +.. literalinclude:: src/authentication/development.ini + :lines: 18-20 + :emphasize-lines: 3 + :lineno-match: + :language: ini + +Finally best-practices tell us to use a different secret for production so +open ``production.ini`` and add a different secret: + +.. literalinclude:: src/authentication/production.ini + :lines: 15-17 + :emphasize-lines: 3 + :lineno-match: + :language: ini + +Add permission checks +~~~~~~~~~~~~~~~~~~~~~ + +:app:`Pyramid` has full support for declarative authorization which we'll +cover in the next chapter. However many people looking to get their feet +wet are just interested in authentication with some basic form of +home-grown authorization. We'll show below how to accomplish the simple +security goals of our wiki now that we can track the logged-in state of users. + +Remember our goals: + +* Allow only ``editor`` and ``basic`` logged-in users to create new pages. +* Only allow ``editor`` users and the page creator (possibly a ``basic`` user) + to edit pages. + +Open the file ``tutorial/views/default.py`` and fix the following imports: + +.. literalinclude:: src/authentication/tutorial/views/default.py + :lines: 5-13 + :lineno-match: + :emphasize-lines: 2,9 + :language: python + +Only the highlighted lines need to be changed. + +Now edit the ``add_page`` view function: + +.. literalinclude:: src/authentication/tutorial/views/default.py + :lines: 62-76 + :lineno-match: + :emphasize-lines: 3-5,10 + :language: python + +Only the highlighted lines need to be changed. + +If the user is not logged in or is not in the ``basic`` or ``editor`` roles +then we raise ``HTTPForbidden`` which will return a "403 Forbidden" response +to the user. However we hook this later to redirect to the login page. Also, +now that we have ``request.user`` we no longer have to hard-code the creator +as the ``editor`` user so we can finally drop that hack. + +Now edit the ``edit_page`` view function: + +.. literalinclude:: src/authentication/tutorial/views/default.py + :lines: 45-60 + :lineno-match: + :emphasize-lines: 5-7 + :language: python + +Only the highlighted lines need to be changed. + +If the user is not logged in or the user is not the page's creator **and** +not an ``editor`` then we raise ``HTTPForbidden``. + +These simple checks should protect our views. + +Login, logout +------------- + +Now that we've got the ability to detect logged-in users, we need to +add the /login and /logout views so that they can actually login! + +Add routes for /login and /logout +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Go back to ``tutorial/routes.py`` and add these two routes as highlighted: + +.. literalinclude:: src/authentication/tutorial/routes.py + :lines: 3-6 + :lineno-match: + :emphasize-lines: 2-3 + :language: python + +.. note:: The preceding lines must be added *before* the following + ``view_page`` route definition: + + .. literalinclude:: src/authentication/tutorial/routes.py + :lines: 6 + :language: python + + This is because ``view_page``'s route definition uses a catch-all + "replacement marker" ``/{pagename}`` (see :ref:`route_pattern_syntax`) + which will catch any route that was not already caught by any route + registered before it. Hence, for ``login`` and ``logout`` views to + have the opportunity of being matched (or "caught"), they must be above + ``/{pagename}``. + +Add login, logout and forbidden views +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Create a new file ``tutorial/views/auth.py`` where we will add the following +code: + +.. literalinclude:: src/authentication/tutorial/views/auth.py + :linenos: + :language: python + +This code adds 3 new views to application: + +- The ``login`` view renders a login form and processes the post from the + login form, checking credentials against our ``users`` table in the database. + + The check is done by first finding a ``User`` record in the database and + then using our ``user.check_password`` method to compare the passwords. + + If the credentials are valid then we use our authentication policy to + store the user's id in the response using :meth:`pyramid.security.remember`. + + Finally, the user is redirected back to the page they were trying to access + (``next``) or the front page as a fallback. This parameter is used by + our forbidden view as explained below to finish the login workflow. + +- The ``logout`` view handles requests to /logout by clearing the credentials + using :meth:`pyramid.security.forget` and then redirecting them to the front + page. + +- The ``forbidden_view`` is registered using the + :class:`pyramid.view.forbidden_view_config` decorator. This is a special + :term:`exception view` which is invoked when a + :class:`pyramid.httpexceptions.HTTPForbidden` exception is raised. + + This view will handle a forbidden error by redirecting the user to /login. + As a convenience it also sets the ``next=`` query string to the current url + (the one that is forbidding access). This way if the user successfully logs + in they will be sent back to the page they had been trying to access. + +Add the ``login.jinja2`` template +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Create ``tutorial/templates/login.jinja2`` with the following content: + +.. literalinclude:: src/authentication/tutorial/templates/login.jinja2 + :language: html + +The above template is referenced in the login view that we just added in +``tutorial/views/auth.py``. + +Add a "Login" and "Logout" links +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Open ``tutorial/templates/layout.jinja2`` and add the following code as +indicated by the highlighted lines. + +.. literalinclude:: src/authentication/tutorial/templates/layout.jinja2 + :lines: 35-46 + :lineno-match: + :emphasize-lines: 2-10 + :language: html + +The ``request.user`` will be ``None`` if the user is not authenticated, or a +``tutorial.models.User`` object if the user is authenticated. This +check will make the logout link active only when the user is logged in and +vice versa the login link is only active when the user is logged out. + +Viewing the application in a browser +------------------------------------ + +We can finally examine our application in a browser (See +:ref:`wiki2-start-the-application`). Launch a browser and visit each of the +following URLs, checking that the result is as expected: + +- http://localhost:6543/ invokes the ``view_wiki`` view. This always + redirects to the ``view_page`` view of the ``FrontPage`` page object. It + is executable by any user. + +- http://localhost:6543/FrontPage invokes the ``view_page`` view of the + ``FrontPage`` page object. There is a "Login" link in the upper right corner. + +- http://localhost:6543/FrontPage/edit_page invokes the edit view for the + FrontPage object. It is executable by only the ``editor`` user. If a + different user (or the anonymous user) invokes it, a login form will be + displayed. Supplying the credentials with the username ``editor``, password + ``editor`` will display the edit page form. + +- http://localhost:6543/add_page/SomePageName invokes the add view for a page. + It is executable by the ``editor`` or ``basic`` user. If a different user + (or the anonymous user) invokes it, a login form will be displayed. Supplying + the credentials with the username ``basic``, password ``basic`` will display + the edit page form. + +- http://localhost:6543/SomePageName/edit_page is editable by the ``basic`` + if the page was created by that user in the previous step. If, instead, the + page was created by ``editor`` then the login page should be shown for the + ``basic`` user. + +- After logging in (as a result of hitting an edit or add page and submitting + the login form with the ``editor`` credentials), we'll see a Logout link in + the upper right hand corner. When we click it, we're logged out, and + redirected back to the front page. diff --git a/docs/tutorials/wiki2/index.rst b/docs/tutorials/wiki2/index.rst index 0a3873dcd..74fb5bfd5 100644 --- a/docs/tutorials/wiki2/index.rst +++ b/docs/tutorials/wiki2/index.rst @@ -22,6 +22,7 @@ which corresponds to the same location if you have Pyramid sources. basiclayout definingmodels definingviews + authentication authorization tests distributing -- cgit v1.2.3 From 583caef0ca5a803f8784b502d2661efd6d74021b Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Tue, 16 Feb 2016 03:07:01 -0800 Subject: minor grammar and punctuation through "wrapping up" --- docs/designdefense.rst | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/designdefense.rst b/docs/designdefense.rst index bd38e5194..28da84368 100644 --- a/docs/designdefense.rst +++ b/docs/designdefense.rst @@ -1516,11 +1516,11 @@ that the server can interface with a WSGI application is placed on the server developer, not the web framework developer, making it more likely to be timely and correct. -Wrapping Up +Wrapping up +++++++++++ -Here's a diagrammed version of the simplest pyramid application, where -comments take into account what we've discussed in the +Here's a diagrammed version of the simplest pyramid application, where the +inlined comments take into account what we've discussed in the :ref:`microframeworks_smaller_hello_world` section. .. code-block:: python @@ -1531,16 +1531,17 @@ comments take into account what we've discussed in the def hello_world(request): # accepts a request; no request thread local reqd # explicit response object means no response threadlocal - return Response('Hello world!') + return Response('Hello world!') if __name__ == '__main__': from pyramid.config import Configurator - config = Configurator() # no global application object. + config = Configurator() # no global application object config.add_view(hello_world) # explicit non-decorator registration app = config.make_wsgi_app() # explicitly WSGI server = make_server('0.0.0.0', 8080, app) server.serve_forever() # explicitly WSGI + Pyramid Doesn't Offer Pluggable Apps ------------------------------------ -- cgit v1.2.3 From 38b40761f1ba31773aca64ae600428516c98534c Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 16 Feb 2016 23:23:08 -0600 Subject: use page.name to prepare for context --- docs/tutorials/wiki2/src/authentication/tutorial/views/default.py | 6 +++--- docs/tutorials/wiki2/src/views/tutorial/views/default.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py b/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py index 55aa74d04..ffe967f6e 100644 --- a/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py +++ b/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py @@ -39,7 +39,7 @@ def view_page(request): content = publish_parts(page.data, writer_name='html')['html_body'] content = wikiwords.sub(add_link, content) - edit_url = request.route_url('edit_page', pagename=pagename) + edit_url = request.route_url('edit_page', pagename=page.name) return dict(page=page, content=content, edit_url=edit_url) @view_config(route_name='edit_page', renderer='../templates/edit.jinja2') @@ -51,12 +51,12 @@ def edit_page(request): raise HTTPForbidden if 'form.submitted' in request.params: page.data = request.params['body'] - next_url = request.route_url('view_page', pagename=pagename) + next_url = request.route_url('view_page', pagename=page.name) return HTTPFound(location=next_url) return dict( pagename=page.name, pagedata=page.data, - save_url=request.route_url('edit_page', pagename=pagename), + save_url=request.route_url('edit_page', pagename=page.name), ) @view_config(route_name='add_page', renderer='../templates/edit.jinja2') diff --git a/docs/tutorials/wiki2/src/views/tutorial/views/default.py b/docs/tutorials/wiki2/src/views/tutorial/views/default.py index 7a4073b3f..c1d402f6a 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/views/default.py +++ b/docs/tutorials/wiki2/src/views/tutorial/views/default.py @@ -38,7 +38,7 @@ def view_page(request): content = publish_parts(page.data, writer_name='html')['html_body'] content = wikiwords.sub(add_link, content) - edit_url = request.route_url('edit_page', pagename=pagename) + edit_url = request.route_url('edit_page', pagename=page.name) return dict(page=page, content=content, edit_url=edit_url) @view_config(route_name='edit_page', renderer='../templates/edit.jinja2') @@ -47,12 +47,12 @@ def edit_page(request): page = request.dbsession.query(Page).filter_by(name=pagename).one() if 'form.submitted' in request.params: page.data = request.params['body'] - next_url = request.route_url('view_page', pagename=pagename) + next_url = request.route_url('view_page', pagename=page.name) return HTTPFound(location=next_url) return dict( pagename=page.name, pagedata=page.data, - save_url=request.route_url('edit_page', pagename=pagename), + save_url=request.route_url('edit_page', pagename=page.name), ) @view_config(route_name='add_page', renderer='../templates/edit.jinja2') -- cgit v1.2.3 From f2c43689b50152d55ddc98e8f56754ee61f9a8c7 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 16 Feb 2016 23:23:23 -0600 Subject: remove whitespace --- docs/tutorials/wiki2/authentication.rst | 2 +- docs/tutorials/wiki2/src/authentication/tutorial/security.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/tutorials/wiki2/authentication.rst b/docs/tutorials/wiki2/authentication.rst index c33ed5138..0b5e71099 100644 --- a/docs/tutorials/wiki2/authentication.rst +++ b/docs/tutorials/wiki2/authentication.rst @@ -38,7 +38,7 @@ Create a new file ``tutorial/security.py``: .. literalinclude:: src/authentication/tutorial/security.py :linenos: - :emphasize-lines: 9,14,28 + :emphasize-lines: 9,14,21-27 :language: python Here we've defined: diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/security.py b/docs/tutorials/wiki2/src/authentication/tutorial/security.py index 24035c8b9..8ea3858d2 100644 --- a/docs/tutorials/wiki2/src/authentication/tutorial/security.py +++ b/docs/tutorials/wiki2/src/authentication/tutorial/security.py @@ -22,7 +22,6 @@ def includeme(config): settings['auth.secret'], hashalg='sha512', ) - config.set_authentication_policy(authn_policy) config.set_authorization_policy(ACLAuthorizationPolicy()) config.add_request_method(get_user, 'user', reify=True) -- cgit v1.2.3 From 2fa90465bfdd213b6ce51ca8de6eaf9b614c283e Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 16 Feb 2016 23:42:04 -0600 Subject: add first cut at source for authorization chapter --- .../wiki2/src/authorization/development.ini | 2 + .../wiki2/src/authorization/production.ini | 2 + .../wiki2/src/authorization/tutorial/__init__.py | 8 +--- .../src/authorization/tutorial/models/user.py | 6 ++- .../wiki2/src/authorization/tutorial/routes.py | 50 ++++++++++++++++++++ .../wiki2/src/authorization/tutorial/security.py | 51 ++++++++------------- .../authorization/tutorial/templates/edit.jinja2 | 6 +-- .../authorization/tutorial/templates/layout.jinja2 | 10 ++-- .../authorization/tutorial/templates/login.jinja2 | 4 +- .../authorization/tutorial/templates/view.jinja2 | 4 +- .../wiki2/src/authorization/tutorial/views/auth.py | 23 ++++------ .../src/authorization/tutorial/views/default.py | 53 ++++++++++------------ 12 files changed, 126 insertions(+), 93 deletions(-) create mode 100644 docs/tutorials/wiki2/src/authorization/tutorial/routes.py diff --git a/docs/tutorials/wiki2/src/authorization/development.ini b/docs/tutorials/wiki2/src/authorization/development.ini index 99c4ff0fe..f3079727e 100644 --- a/docs/tutorials/wiki2/src/authorization/development.ini +++ b/docs/tutorials/wiki2/src/authorization/development.ini @@ -17,6 +17,8 @@ pyramid.includes = sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite +auth.secret = seekrit + # By default, the toolbar only appears for clients from IP addresses # '127.0.0.1' and '::1'. # debugtoolbar.hosts = 127.0.0.1 ::1 diff --git a/docs/tutorials/wiki2/src/authorization/production.ini b/docs/tutorials/wiki2/src/authorization/production.ini index cb1db3211..686dba48a 100644 --- a/docs/tutorials/wiki2/src/authorization/production.ini +++ b/docs/tutorials/wiki2/src/authorization/production.ini @@ -14,6 +14,8 @@ pyramid.default_locale_name = en sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite +auth.secret = real-seekrit + [server:main] use = egg:waitress#main host = 0.0.0.0 diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py b/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py index 8eacdee5a..f5c033b8b 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py @@ -7,13 +7,7 @@ def main(global_config, **settings): config = Configurator(settings=settings) config.include('pyramid_jinja2') config.include('.models') + config.include('.routes') config.include('.security') - config.add_static_view('static', 'static', cache_max_age=3600) - config.add_route('view_wiki', '/') - config.add_route('login', '/login') - config.add_route('logout', '/logout') - config.add_route('view_page', '/{pagename}') - config.add_route('add_page', '/add_page/{pagename}') - config.add_route('edit_page', '/{pagename}/edit_page') config.scan() return config.make_wsgi_app() diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models/user.py b/docs/tutorials/wiki2/src/authorization/tutorial/models/user.py index 25b0a8187..6fb32a1b2 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/models/user.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/models/user.py @@ -18,10 +18,12 @@ class User(Base): password_hash = Column(Text) def set_password(self, pw): - pwhash = bcrypt.hashpw(pw, bcrypt.gensalt()) + pwhash = bcrypt.hashpw(pw.encode('utf8'), bcrypt.gensalt()) self.password_hash = pwhash def check_password(self, pw): if self.password_hash is not None: - return bcrypt.hashpw(pw, self.password_hash) == self.password_hash + expected_hash = self.password_hash.encode('utf8') + actual_hash = bcrypt.hashpw(pw.encode('utf8'), expected_hash) + return expected_hash == actual_hash return False diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/routes.py b/docs/tutorials/wiki2/src/authorization/tutorial/routes.py new file mode 100644 index 000000000..c7c3a2120 --- /dev/null +++ b/docs/tutorials/wiki2/src/authorization/tutorial/routes.py @@ -0,0 +1,50 @@ +from pyramid.httpexceptions import HTTPNotFound +from pyramid.security import ( + Allow, + Everyone, +) + +from .models import Page + +def includeme(config): + config.add_static_view('static', 'static', cache_max_age=3600) + config.add_route('view_wiki', '/') + config.add_route('login', '/login') + config.add_route('logout', '/logout') + config.add_route('view_page', '/{pagename}', factory=page_factory) + config.add_route('add_page', '/add_page/{pagename}', + factory=new_page_factory) + config.add_route('edit_page', '/{pagename}/edit_page', + factory=page_factory) + +def new_page_factory(request): + pagename = request.matchdict['pagename'] + return NewPage(pagename) + +class NewPage(object): + def __init__(self, pagename): + self.pagename = pagename + + def __acl__(self): + return [ + (Allow, 'role:editor', 'create'), + (Allow, 'role:basic', 'create'), + ] + +def page_factory(request): + pagename = request.matchdict['pagename'] + page = request.dbsession.query(Page).filter_by(name=pagename).first() + if page is None: + raise HTTPNotFound + return PageResource(page) + +class PageResource(object): + def __init__(self, page): + self.page = page + + def __acl__(self): + return [ + (Allow, Everyone, 'view'), + (Allow, 'role:editor', 'edit'), + (Allow, str(self.page.creator_id), 'edit'), + ] diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/security.py b/docs/tutorials/wiki2/src/authorization/tutorial/security.py index 7bceabf3f..25cff7b05 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/security.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/security.py @@ -1,51 +1,40 @@ from pyramid.authentication import AuthTktAuthenticationPolicy from pyramid.authorization import ACLAuthorizationPolicy - from pyramid.security import ( - Allow, Authenticated, Everyone, ) +from .models import User -USERS = { - 'editor': 'editor', - 'viewer': 'viewer', -} - -GROUPS = { - 'editor': ['group:editors'], -} class MyAuthenticationPolicy(AuthTktAuthenticationPolicy): def authenticated_userid(self, request): - userid = self.unauthenticated_userid(request) - if userid in USERS: - return userid + user = request.user + if user is not None: + return user.id def effective_principals(self, request): principals = [Everyone] - userid = self.authenticated_userid(request) - if userid is not None: + user = request.user + if user is not None: principals.append(Authenticated) - principals.append(userid) - - groups = GROUPS.get(userid, []) - principals.extend(groups) + principals.append(str(user.id)) + principals.append('role:' + user.role) return principals -class RootFactory(object): - __acl__ = [ - (Allow, Everyone, 'view'), - (Allow, 'group:editors', 'edit'), - ] - - def __init__(self, request): - pass +def get_user(request): + user_id = request.unauthenticated_userid + if user_id is not None: + user = request.dbsession.query(User).get(user_id) + return user def includeme(config): - authn_policy = MyAuthenticationPolicy('sosecret', hashalg='sha512') - authz_policy = ACLAuthorizationPolicy() - config.set_root_factory(RootFactory) + settings = config.get_settings() + authn_policy = MyAuthenticationPolicy( + settings['auth.secret'], + hashalg='sha512', + ) config.set_authentication_policy(authn_policy) - config.set_authorization_policy(authz_policy) + config.set_authorization_policy(ACLAuthorizationPolicy()) + config.add_request_method(get_user, 'user', reify=True) diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja2 index e47b3aabf..7db25c674 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja2 +++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/edit.jinja2 @@ -1,17 +1,17 @@ {% extends 'layout.jinja2' %} -{% block title %}Edit {{page.name}} - {% endblock title %} +{% block subtitle %}Edit {{pagename}} - {% endblock subtitle %} {% block content %}

-Editing {% if page.name %}{{page.name}}{% else %}Page Name Goes Here{% endif %} +Editing {{pagename}}

You can return to the FrontPage.

- +
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/layout.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/layout.jinja2 index 82a144abf..44d14304e 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/layout.jinja2 +++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/layout.jinja2 @@ -8,7 +8,7 @@ - {% block title %}{% if page.name %} {{page.name}} - {% endif %}{% endblock title %}Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki) + {% block subtitle %}{% endblock %}Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki) @@ -33,9 +33,13 @@
- {% if request.authenticated_userid is not none %} + {% if request.user is none %}

- Logout + Login +

+ {% else %} +

+ {{request.user.name}} Logout

{% endif %} {% block content %}{% endblock %} diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.jinja2 index 99d369173..1806de0ff 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.jinja2 +++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/login.jinja2 @@ -10,14 +10,14 @@ {{ message }}

- +
- +
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.jinja2 index c582ce1f9..94419e228 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.jinja2 +++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/view.jinja2 @@ -1,5 +1,7 @@ {% extends 'layout.jinja2' %} +{% block subtitle %}{{page.name}} - {% endblock subtitle %} + {% block content %}

{{ content|safe }}

@@ -8,7 +10,7 @@

- Viewing {% if page.name %}{{page.name}}{% else %}Page Name Goes Here{% endif %} + Viewing {{page.name}}, created by {{page.creator.name}}.

You can return to the FrontPage. diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views/auth.py b/docs/tutorials/wiki2/src/authorization/tutorial/views/auth.py index 08aa2bfad..d3db34132 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/views/auth.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/views/auth.py @@ -8,33 +8,28 @@ from pyramid.view import ( view_config, ) -from ..security.default import USERS +from ..models import User -@view_config(route_name='login', renderer='templates/login.jinja2') +@view_config(route_name='login', renderer='../templates/login.jinja2') def login(request): - login_url = request.route_url('login') - referrer = request.url - if referrer == login_url: - referrer = '/' # never use the login form itself as came_from - came_from = request.params.get('came_from', referrer) + next_url = request.params.get('next', request.referrer) message = '' login = '' - password = '' if 'form.submitted' in request.params: login = request.params['login'] password = request.params['password'] - if USERS.get(login) == password: - headers = remember(request, login) - return HTTPFound(location=came_from, headers=headers) + user = request.dbsession.query(User).filter_by(name=login).first() + if user is not None and user.check_password(password): + headers = remember(request, user.id) + return HTTPFound(location=next_url, headers=headers) message = 'Failed login' return dict( message=message, url=request.route_url('login'), - came_from=came_from, + next_url=next_url, login=login, - password=password, ) @view_config(route_name='logout') @@ -45,5 +40,5 @@ def logout(request): @forbidden_view_config() def forbidden_view(request): - next_url = request.route_url('login', _query={'came_from': request.url}) + next_url = request.route_url('login', _query={'next': request.url}) return HTTPFound(location=next_url) diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py b/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py index f74059be0..9358993ea 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py @@ -2,19 +2,15 @@ import cgi import re from docutils.core import publish_parts -from pyramid.httpexceptions import ( - HTTPFound, - HTTPNotFound, - ) +from pyramid.httpexceptions import HTTPFound from pyramid.view import view_config from ..models import Page - # regular expression used to find WikiWords wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)") -@view_config(route_name='view_wiki', permission='view') +@view_config(route_name='view_wiki') def view_wiki(request): next_url = request.route_url('view_page', pagename='FrontPage') return HTTPFound(location=next_url) @@ -22,12 +18,9 @@ def view_wiki(request): @view_config(route_name='view_page', renderer='../templates/view.jinja2', permission='view') def view_page(request): - pagename = request.matchdict['pagename'] - page = request.dbsession.query(Page).filter_by(name=pagename).first() - if page is None: - raise HTTPNotFound('No such page') + page = request.context.page - def check(match): + def add_link(match): word = match.group(1) exists = request.dbsession.query(Page).filter_by(name=word).all() if exists: @@ -38,34 +31,34 @@ def view_page(request): return '%s' % (add_url, cgi.escape(word)) content = publish_parts(page.data, writer_name='html')['html_body'] - content = wikiwords.sub(check, content) - edit_url = request.route_url('edit_page', pagename=pagename) + content = wikiwords.sub(add_link, content) + edit_url = request.route_url('edit_page', pagename=page.name) return dict(page=page, content=content, edit_url=edit_url) +@view_config(route_name='edit_page', renderer='../templates/edit.jinja2', + permission='edit') +def edit_page(request): + page = request.context.page + if 'form.submitted' in request.params: + page.data = request.params['body'] + next_url = request.route_url('view_page', pagename=page.name) + return HTTPFound(location=next_url) + return dict( + pagename=page.name, + pagedata=page.data, + save_url=request.route_url('edit_page', pagename=page.name), + ) + @view_config(route_name='add_page', renderer='../templates/edit.jinja2', permission='create') def add_page(request): - pagename = request.matchdict['pagename'] + pagename = request.context.pagename if 'form.submitted' in request.params: body = request.params['body'] page = Page(name=pagename, data=body) + page.creator = request.user request.dbsession.add(page) next_url = request.route_url('view_page', pagename=pagename) return HTTPFound(location=next_url) save_url = request.route_url('add_page', pagename=pagename) - page = Page(name='', data='') - return dict(page=page, save_url=save_url) - -@view_config(route_name='edit_page', renderer='../templates/edit.jinja2', - permission='edit') -def edit_page(request): - pagename = request.matchdict['pagename'] - page = request.dbsession.query(Page).filter_by(name=pagename).one() - if 'form.submitted' in request.params: - page.data = request.params['body'] - next_url = request.route_url('view_page', pagename=pagename) - return HTTPFound(location=next_url) - return dict( - page=page, - save_url=request.route_url('edit_page', pagename=pagename), - ) + return dict(pagename=pagename, pagedata='', save_url=save_url) -- cgit v1.2.3 From 9e85d2bf9489fff46ec7ea47b79bebcdc19d9a8e Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 18 Feb 2016 01:18:20 -0600 Subject: update the authorization chapter --- docs/tutorials/wiki2/authentication.rst | 2 +- docs/tutorials/wiki2/authorization.rst | 418 +++++++++++--------------------- 2 files changed, 147 insertions(+), 273 deletions(-) diff --git a/docs/tutorials/wiki2/authentication.rst b/docs/tutorials/wiki2/authentication.rst index 0b5e71099..1b18e5c55 100644 --- a/docs/tutorials/wiki2/authentication.rst +++ b/docs/tutorials/wiki2/authentication.rst @@ -49,7 +49,7 @@ Here we've defined: the :term:`userid` using a signed cookie. * A ``get_user`` function which can convert the ``unauthenticated_userid`` from the policy into a ``User`` object from our database. -* Finally, the ``get_user`` is registered on the request as ``request.user`` +* The ``get_user`` is registered on the request as ``request.user`` to be used throughout our application as the authenticated ``User`` object for the logged-in user. diff --git a/docs/tutorials/wiki2/authorization.rst b/docs/tutorials/wiki2/authorization.rst index 1ee5cc714..eb9269dff 100644 --- a/docs/tutorials/wiki2/authorization.rst +++ b/docs/tutorials/wiki2/authorization.rst @@ -4,342 +4,216 @@ Adding authorization ==================== -:app:`Pyramid` provides facilities for :term:`authentication` and -:term:`authorization`. We'll make use of both features to provide security -to our application. Our application currently allows anyone with access to -the server to view, edit, and add pages to our wiki. We'll change that to -allow only people who are members of a *group* named ``group:editors`` to add -and edit wiki pages but we'll continue allowing anyone with access to the -server to view pages. - -We will also add a login page and a logout link on all the pages. The login -page will be shown when a user is denied access to any of the views that -require permission, instead of a default "403 Forbidden" page. - -We will implement the access control with the following steps: - -* Add users and groups (``security/default.py``, a new subpackage). -* Add an :term:`ACL` (``models/mymodel.py`` and ``__init__.py``). -* Add an :term:`authentication policy` and an :term:`authorization policy` - (``__init__.py``). -* Add :term:`permission` declarations to the ``edit_page`` and ``add_page`` - views (``views/default.py``). - -Then we will add the login and logout feature: - -* Add routes for /login and /logout (``__init__.py``). -* Add ``login`` and ``logout`` views (``views/default.py``). -* Add a login template (``login.jinja2``). -* Make the existing views return a ``logged_in`` flag to the renderer +In the last chapter we built :term:`authentication` into our wiki2. We also +went one step further and used the ``request.user`` object to perform some explicit :term:`authorization` checks. This is fine for a lot of +applications but :app:`Pyramid` provides some facilities for cleaning this +up and decoupling the constraints from the view function itself. + +We will implement access control with the following steps: + +* Update the :term:`authentication policy` to break down the + :term:`userid` into a list of :term:`principals ` + (``security.py``). +* Define an :term:`authorization policy` for mapping users, resources and + permissions (``security.py``). +* Add new :term:`resource` definitions that will be used as the + :term:`context` for the wiki pages (``routes.py``). +* Add an :term:`ACL` to each resource (``routes.py``). +* Replace the inline checks on the views with :term:`permission` declarations (``views/default.py``). -* Add a "Logout" link to be shown when logged in and viewing or editing a page - (``view.jinja2``, ``edit.jinja2``). +Add user principals +------------------- -Access control --------------- +A :term:`principal` is a level of abstraction on top of the raw +:term:`userid` that describes the user in terms of capabilities, roles or +other identifiers that are easier to generalize. The permissions are then +written against the principals without focusing on the exact user involved. -Add users and groups -~~~~~~~~~~~~~~~~~~~~ +:app:`Pyramid` defines two builtin principals used in every application: +:attr:`pyramid.security.Everyone` and :attr:`pyramid.security.Authenticated`. +On top of these we have already mentioned the required principals for this +application in the original design. The user has two possible roles: +``editor`` and ``basic``. These will be prefixed by the ``role:`` +string to avoid clasing with any other types of principals. -Create a new ``tutorial/security/default.py`` subpackage with the -following content: +Open the file ``tutorial/security.py`` and edit the following lines: -.. literalinclude:: src/authorization/tutorial/security/default.py +.. literalinclude:: src/authorization/tutorial/security.py :linenos: + :emphasize-lines: 3-6,17-24 :language: python -The ``groupfinder`` function accepts a userid and a request and -returns one of these values: - -- If the userid exists in the system, it will return a sequence of group - identifiers (or an empty sequence if the user isn't a member of any groups). -- If the userid *does not* exist in the system, it will return ``None``. - -For example, ``groupfinder('editor', request )`` returns ``['group:editor']``, -``groupfinder('viewer', request)`` returns ``[]``, and ``groupfinder('admin', -request)`` returns ``None``. We will use ``groupfinder()`` as an -:term:`authentication policy` "callback" that will provide the -:term:`principal` or principals for a user. - -In a production system, user and group data will most often come from a -database, but here we use "dummy" data to represent user and groups sources. +Only the highlighted lines need to be added. -Add an ACL -~~~~~~~~~~ +Note that the role comes from the ``User`` object and finally we also +add the ``user.id`` as a principal for when we want to allow that exact +user to edit page's they've created. -Open ``tutorial/models/mymodel.py`` and add the following import -statement at the top: +Add the authorization policy +---------------------------- -.. literalinclude:: src/authorization/tutorial/models/mymodel.py - :lines: 1-4 - :language: python +We already added the :term:`authorization policy` in the previous chapter +because :app:`Pyramid` requires one when adding an +:term:`authentication policy`. However, it was not used anywhere and so we'll +mention it now. -Add the following class definition at the end: +Open the file ``tutorial/security.py`` and notice the following lines: -.. literalinclude:: src/authorization/tutorial/models/mymodel.py - :lines: 22-29 +.. literalinclude:: src/authorization/tutorial/security.py + :lines: 38-40 + :lineno-match: + :emphasize-lines: 2 :language: python -We import :data:`~pyramid.security.Allow`, an action that means that -permission is allowed, and :data:`~pyramid.security.Everyone`, a special -:term:`principal` that is associated to all requests. Both are used in the -:term:`ACE` entries that make up the ACL. - -The ACL is a list that needs to be named `__acl__` and be an attribute of a -class. We define an :term:`ACL` with two :term:`ACE` entries. The first entry -allows any user (``Everyone``) the `view` permission. The second entry allows -the ``group:editors`` principal the `edit` permission. +We're using the :class:`pyramid.authorization.ACLAuthorizationPolicy` which +will suffice for most applications. It uses the :term:`context` to define +the mapping between a :term:`principal` and :term:`permission` for the +current request via the ``__acl__``. -The ``RootFactory`` class that contains the ACL is a :term:`root factory`. We -need to associate it to our :app:`Pyramid` application, so the ACL is provided -to each view in the :term:`context` of the request as the ``context`` -attribute. +Add resources and ACLs +---------------------- -Open ``tutorial/__init__.py`` and define a new root factory using -:meth:`pyramid.config.Configurator.set_root_factory` using the class that we -created above: +Resources are the hidden gem of :app:`Pyramid`. You've made it! -.. literalinclude:: src/authorization/tutorial/__init__.py - :lines: 14-17 - :emphasize-lines: 17 - :language: python +Every URL in a web application is representing a :term:`resource` +(the **R** in Uniform Resource Locator). Often the resource is something +in your data model but it could also be an abstraction over many models. -Only the highlighted line needs to be added. +Our wiki has two resources: -We are now providing the ACL to the application. See :ref:`assigning_acls` -for more information about what an :term:`ACL` represents. +#. A ``PageResource``. Represents a ``Page`` that is to be viewed or edited. + Only ``editor`` users as well as the original creator of the ``Page`` + may edit the ``PageResource`` but anyone may view it. -.. note:: Although we don't use the functionality here, the ``factory`` used - to create route contexts may differ per-route as opposed to globally. See - the ``factory`` argument to :meth:`pyramid.config.Configurator.add_route` - for more info. +#. A ``NewPage``. Represents a potential ``Page`` that does not exist. + Any logged-in user (roles ``basic`` or ``editor``) can create pages. -Add authentication and authorization policies -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. note:: -Open ``tutorial/__init__.py`` and add the highlighted import -statements: + The wiki data model is simple enough that the ``PageResource`` is + actually mostly redundant with our ``models.Page`` SQLAlchemy class. It is + completely valid to combine these into one class. However, for this + tutorial they are explicitly separated to make it clear the + parts that :app:`Pyramid` cares about versus application-defined objects. -.. literalinclude:: src/authorization/tutorial/__init__.py - :lines: 1-5 - :emphasize-lines: 2-5 - :language: python +There are many ways to define these resources, and they can even be grouped +into collections with a hierarchy. However, we're keeping it simple here! -Now add those policies to the configuration: +Open the file ``tutorial/routes.py`` and edit the following lines: -.. literalinclude:: src/authorization/tutorial/__init__.py - :lines: 11-19 - :emphasize-lines: 1-3,8-9 +.. literalinclude:: src/authorization/tutorial/routes.py + :linenos: + :emphasize-lines: 1-7,14-50 :language: python -Only the highlighted lines need to be added. - -We are enabling an ``AuthTktAuthenticationPolicy``, which is based in an auth -ticket that may be included in the request. We are also enabling an -``ACLAuthorizationPolicy``, which uses an ACL to determine the *allow* or -*deny* outcome for a view. - -Note that the :class:`pyramid.authentication.AuthTktAuthenticationPolicy` -constructor accepts two arguments: ``secret`` and ``callback``. ``secret`` is -a string representing an encryption key used by the "authentication ticket" -machinery represented by this policy; it is required. The ``callback`` is the -``groupfinder()`` function that we created before. +The highlighted lines need to be edited or added. +The ``NewPage`` has an ``__acl__`` on it that returns a list of +mappings from :term:`principal` to :term:`permission`. This defines **who** +can do **what** with that :term:`resource`. In our case we want to only +allow users with the principals ``role:editor`` and ``role:basic`` to +have the ``create`` permission: -Add permission declarations -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Open ``tutorial/views/default.py`` and add a ``permission='view'`` -parameter to the ``@view_config`` decorator for ``view_wiki()`` and -``view_page()`` as follows: - -.. literalinclude:: src/authorization/tutorial/views/default.py - :lines: 24-25 - :emphasize-lines: 1 +.. literalinclude:: src/authorization/tutorial/routes.py + :lines: 20-32 + :lineno-match: + :emphasize-lines: 11,12 :language: python -.. literalinclude:: src/authorization/tutorial/views/default.py - :lines: 29-31 - :emphasize-lines: 1-2 - :language: python - -Only the highlighted lines, along with their preceding commas, need to be -edited and added. - -This allows anyone to invoke these two views. +The ``NewPage`` is loaded as the :term:`context` of the ``add_page`` +route by declaring a ``factory`` on the route: -Add a ``permission='edit'`` parameter to the ``@view_config`` decorators for -``add_page()`` and ``edit_page()``: - -.. literalinclude:: src/authorization/tutorial/views/default.py - :lines: 52-54 - :emphasize-lines: 1-2 +.. literalinclude:: src/authorization/tutorial/routes.py + :lines: 15-16 + :lineno-match: + :emphasize-lines: 2 :language: python -.. literalinclude:: src/authorization/tutorial/views/default.py - :lines: 66-68 - :emphasize-lines: 1-2 - :language: python +The ``PageResource`` defines the :term:`ACL` for a ``Page``. It uses an +actual ``Page`` object to determine **who** can do **what** to the page. -Only the highlighted lines, along with their preceding commas, need to be -edited and added. - -The result is that only users who possess the ``edit`` permission at the time -of the request may invoke those two views. - -We are done with the changes needed to control access. The changes that -follow will add the login and logout feature. - -Login, logout -------------- - -Add routes for /login and /logout -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Go back to ``tutorial/__init__.py`` and add these two routes as -highlighted: - -.. literalinclude:: src/authorization/tutorial/__init__.py - :lines: 21-24 - :emphasize-lines: 2-3 +.. literalinclude:: src/authorization/tutorial/routes.py + :lines: 34-50 + :lineno-match: + :emphasize-lines: 14-16 :language: python -.. note:: The preceding lines must be added *before* the following - ``view_page`` route definition: - - .. literalinclude:: src/authorization/tutorial/__init__.py - :lines: 24 - :language: python +The ``PageResource`` is loaded as the :term:`context` of the ``view_page`` +and ``edit_page`` route by declaring a ``factory`` on the routes: - This is because ``view_page``'s route definition uses a catch-all - "replacement marker" ``/{pagename}`` (see :ref:`route_pattern_syntax`) - which will catch any route that was not already caught by any route listed - above it in ``__init__.py``. Hence, for ``login`` and ``logout`` views to - have the opportunity of being matched (or "caught"), they must be above - ``/{pagename}``. +.. literalinclude:: src/authorization/tutorial/routes.py + :lines: 14-18 + :lineno-match: + :emphasize-lines: 1,4-5 + :language: python -Add login and logout views -~~~~~~~~~~~~~~~~~~~~~~~~~~ +Add view permissions +-------------------- -We'll add a ``login`` view which renders a login form and processes the post -from the login form, checking credentials. +At this point we've modified our application to load the ``PageResource``, +including the actual ``Page`` model in the ``page_factory``. The +``PageResource`` is now the :term:`context` for all ``view_page`` and +``edit_page`` views. Similarly the ``NewPage`` will be the context for +the ``add_page`` view. -We'll also add a ``logout`` view callable to our application and provide a -link to it. This view will clear the credentials of the logged in user and -redirect back to the front page. +Open the file ``views/default.py``. -Add the following import statements to ``tutorial/views/default.py`` -after the import from ``pyramid.httpexceptions``: +First, you can drop a few imports that are no longer necessary: .. literalinclude:: src/authorization/tutorial/views/default.py - :lines: 9-19 - :emphasize-lines: 1-8,11 + :lines: 5-7 + :lineno-match: + :emphasize-lines: 1 :language: python -All the highlighted lines need to be added or edited. - -:meth:`~pyramid.view.forbidden_view_config` will be used to customize the -default 403 Forbidden page. :meth:`~pyramid.security.remember` and -:meth:`~pyramid.security.forget` help to create and expire an auth ticket -cookie. - -Now add the ``login`` and ``logout`` views at the end of the file: +Edit the ``view_page`` view to declare the ``view`` permission and remove +the explicit checks within the view: .. literalinclude:: src/authorization/tutorial/views/default.py - :lines: 81-112 - :language: python - -``login()`` has two decorators: - -- a ``@view_config`` decorator which associates it with the ``login`` route - and makes it visible when we visit ``/login``, and -- a ``@forbidden_view_config`` decorator which turns it into a - :term:`forbidden view`. ``login()`` will be invoked when a user tries to - execute a view callable for which they lack authorization. For example, if - a user has not logged in and tries to add or edit a wiki page, they will be - shown the login form before being allowed to continue. - -The order of these two :term:`view configuration` decorators is unimportant. - -``logout()`` is decorated with a ``@view_config`` decorator which associates -it with the ``logout`` route. It will be invoked when we visit ``/logout``. - -Add the ``login.jinja2`` template -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Create ``tutorial/templates/login.jinja2`` with the following content: - -.. literalinclude:: src/authorization/tutorial/templates/login.jinja2 - :language: html - -The above template is referenced in the login view that we just added in -``views/default.py``. - -Add a "Logout" link when logged in -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Open ``tutorial/templates/edit.jinja2`` and -``tutorial/templates/view.jinja2`` and add the following code as -indicated by the highlighted lines. - -.. literalinclude:: src/authorization/tutorial/templates/edit.jinja2 - :lines: 34-40 - :emphasize-lines: 3-7 - :language: html - -The :meth:`pyramid.request.Request.authenticated_userid` will be ``None`` if -the user is not authenticated, or a userid if the user is authenticated. This -check will make the logout link active only when the user is logged in. - -Reviewing our changes ---------------------- - -Our ``tutorial/__init__.py`` will look like this when we're done: - -.. literalinclude:: src/authorization/tutorial/__init__.py - :linenos: - :emphasize-lines: 2-3,5,11-13,17-19,22-23 + :lines: 18-23 + :lineno-match: + :emphasize-lines: 2,4 :language: python -Only the highlighted lines need to be added or edited. +The work of loading the page has already been done in the factory so we +can just pull the ``page`` object out of the ``PageResource`` loaded as +``request.context``. Our factory also guarantees we will have a ``Page`` as it +raises ``HTTPNotFound`` otherwise - again simplifying the view logic. -Our ``tutorial/models/mymodel.py`` will look like this when we're done: +Edit the ``edit_page`` view to declare the ``edit`` permission: -.. literalinclude:: src/authorization/tutorial/models/mymodel.py - :linenos: - :emphasize-lines: 1-4,22-29 +.. literalinclude:: src/authorization/tutorial/views/default.py + :lines: 38-42 + :lineno-match: + :emphasize-lines: 2,4 :language: python -Only the highlighted lines need to be added or edited. - -Our ``tutorial/views/default.py`` will look like this when we're done: +Edit the ``add_page`` view to declare the ``create`` permission: .. literalinclude:: src/authorization/tutorial/views/default.py - :linenos: - :emphasize-lines: 9-16,19,24,29-30,52-53,66-67,81-112 + :lines: 52-56 + :lineno-match: + :emphasize-lines: 2,4 :language: python -Only the highlighted lines need to be added or edited. - -Our ``tutorial/templates/edit.jinja2`` template will look like this when -we're done: - -.. literalinclude:: src/authorization/tutorial/templates/edit.jinja2 - :linenos: - :emphasize-lines: 36-40 - :language: html +Note the ``pagename`` here is pulled off of the context instead of +``request.matchdict``. The factory has done a lot of work for us to hide the +actual route pattern. -Only the highlighted lines need to be added or edited. +The ACLs defined on each :term:`resource` are used by the +:term:`authorization policy` to determine if any +:term:`principal` is allowed to have some :term:`permission`. If this check +fails (for example, the user is not logged in) then a ``HTTPForbidden`` +exception will be raised automatically, thus we're able to drop those +exceptions and checks from the views themselves. Rather we've defined them in +terms of operations on a resource. -Our ``tutorial/templates/view.jinja2`` template will look like this when -we're done: +The final ``tutorial/views/default.py`` should look like the following: -.. literalinclude:: src/authorization/tutorial/templates/view.jinja2 +.. literalinclude:: src/authorization/tutorial/views/default.py :linenos: - :emphasize-lines: 36-40 - :language: html - -Only the highlighted lines need to be added or edited. + :language: python Viewing the application in a browser ------------------------------------ -- cgit v1.2.3 From 91f7ed469664bf71f98b6e55ea096f5bdddae953 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 18 Feb 2016 01:53:49 -0600 Subject: add webtest and tests_require to setup.py --- docs/tutorials/wiki2/src/authentication/setup.py | 5 +++++ docs/tutorials/wiki2/src/authorization/setup.py | 5 +++++ docs/tutorials/wiki2/src/basiclayout/setup.py | 5 +++++ docs/tutorials/wiki2/src/models/setup.py | 5 +++++ docs/tutorials/wiki2/src/views/setup.py | 5 +++++ pyramid/scaffolds/alchemy/setup.py_tmpl | 5 +++++ 6 files changed, 30 insertions(+) diff --git a/docs/tutorials/wiki2/src/authentication/setup.py b/docs/tutorials/wiki2/src/authentication/setup.py index c342c1aba..57538f2d0 100644 --- a/docs/tutorials/wiki2/src/authentication/setup.py +++ b/docs/tutorials/wiki2/src/authentication/setup.py @@ -21,6 +21,10 @@ requires = [ 'waitress', ] +tests_require = [ + 'WebTest', +] + setup(name='tutorial', version='0.0', description='tutorial', @@ -39,6 +43,7 @@ setup(name='tutorial', include_package_data=True, zip_safe=False, test_suite='tutorial', + tests_require=tests_require, install_requires=requires, entry_points="""\ [paste.app_factory] diff --git a/docs/tutorials/wiki2/src/authorization/setup.py b/docs/tutorials/wiki2/src/authorization/setup.py index c342c1aba..57538f2d0 100644 --- a/docs/tutorials/wiki2/src/authorization/setup.py +++ b/docs/tutorials/wiki2/src/authorization/setup.py @@ -21,6 +21,10 @@ requires = [ 'waitress', ] +tests_require = [ + 'WebTest', +] + setup(name='tutorial', version='0.0', description='tutorial', @@ -39,6 +43,7 @@ setup(name='tutorial', include_package_data=True, zip_safe=False, test_suite='tutorial', + tests_require=tests_require, install_requires=requires, entry_points="""\ [paste.app_factory] diff --git a/docs/tutorials/wiki2/src/basiclayout/setup.py b/docs/tutorials/wiki2/src/basiclayout/setup.py index eb771010f..7bc697730 100644 --- a/docs/tutorials/wiki2/src/basiclayout/setup.py +++ b/docs/tutorials/wiki2/src/basiclayout/setup.py @@ -19,6 +19,10 @@ requires = [ 'waitress', ] +tests_require = [ + 'WebTest', +] + setup(name='tutorial', version='0.0', description='tutorial', @@ -37,6 +41,7 @@ setup(name='tutorial', include_package_data=True, zip_safe=False, test_suite='tutorial', + tests_require=tests_require, install_requires=requires, entry_points="""\ [paste.app_factory] diff --git a/docs/tutorials/wiki2/src/models/setup.py b/docs/tutorials/wiki2/src/models/setup.py index df9fec4d4..bdc9ceed7 100644 --- a/docs/tutorials/wiki2/src/models/setup.py +++ b/docs/tutorials/wiki2/src/models/setup.py @@ -20,6 +20,10 @@ requires = [ 'waitress', ] +tests_require = [ + 'WebTest', +] + setup(name='tutorial', version='0.0', description='tutorial', @@ -38,6 +42,7 @@ setup(name='tutorial', include_package_data=True, zip_safe=False, test_suite='tutorial', + tests_require=tests_require, install_requires=requires, entry_points="""\ [paste.app_factory] diff --git a/docs/tutorials/wiki2/src/views/setup.py b/docs/tutorials/wiki2/src/views/setup.py index c342c1aba..57538f2d0 100644 --- a/docs/tutorials/wiki2/src/views/setup.py +++ b/docs/tutorials/wiki2/src/views/setup.py @@ -21,6 +21,10 @@ requires = [ 'waitress', ] +tests_require = [ + 'WebTest', +] + setup(name='tutorial', version='0.0', description='tutorial', @@ -39,6 +43,7 @@ setup(name='tutorial', include_package_data=True, zip_safe=False, test_suite='tutorial', + tests_require=tests_require, install_requires=requires, entry_points="""\ [paste.app_factory] diff --git a/pyramid/scaffolds/alchemy/setup.py_tmpl b/pyramid/scaffolds/alchemy/setup.py_tmpl index af193a73d..d837ea0a9 100644 --- a/pyramid/scaffolds/alchemy/setup.py_tmpl +++ b/pyramid/scaffolds/alchemy/setup.py_tmpl @@ -19,6 +19,10 @@ requires = [ 'waitress', ] +tests_require = [ + 'WebTest', +] + setup(name='{{project}}', version='0.0', description='{{project}}', @@ -37,6 +41,7 @@ setup(name='{{project}}', include_package_data=True, zip_safe=False, test_suite='{{package}}', + tests_require=tests_require, install_requires=requires, entry_points="""\ [paste.app_factory] -- cgit v1.2.3 From 50e08a743d097616ef7f76c9689833eab215cb94 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 18 Feb 2016 02:22:26 -0600 Subject: add fallback for next_url --- docs/tutorials/wiki2/src/authentication/tutorial/views/auth.py | 2 ++ docs/tutorials/wiki2/src/authorization/tutorial/views/auth.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/views/auth.py b/docs/tutorials/wiki2/src/authentication/tutorial/views/auth.py index d3db34132..2b993b430 100644 --- a/docs/tutorials/wiki2/src/authentication/tutorial/views/auth.py +++ b/docs/tutorials/wiki2/src/authentication/tutorial/views/auth.py @@ -14,6 +14,8 @@ from ..models import User @view_config(route_name='login', renderer='../templates/login.jinja2') def login(request): next_url = request.params.get('next', request.referrer) + if not next_url: + next_url = request.route_url('view_wiki') message = '' login = '' if 'form.submitted' in request.params: diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views/auth.py b/docs/tutorials/wiki2/src/authorization/tutorial/views/auth.py index d3db34132..2b993b430 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/views/auth.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/views/auth.py @@ -14,6 +14,8 @@ from ..models import User @view_config(route_name='login', renderer='../templates/login.jinja2') def login(request): next_url = request.params.get('next', request.referrer) + if not next_url: + next_url = request.route_url('view_wiki') message = '' login = '' if 'form.submitted' in request.params: -- cgit v1.2.3 From 66fabb4ac707b5b4289db0094756f1a1af7269cc Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 18 Feb 2016 02:32:08 -0600 Subject: update tests chapter --- docs/tutorials/wiki2/src/tests/development.ini | 2 + docs/tutorials/wiki2/src/tests/production.ini | 2 + docs/tutorials/wiki2/src/tests/setup.py | 2 +- .../tutorials/wiki2/src/tests/tutorial/__init__.py | 19 +--- .../wiki2/src/tests/tutorial/models/user.py | 6 +- docs/tutorials/wiki2/src/tests/tutorial/routes.py | 50 +++++++++++ .../tutorials/wiki2/src/tests/tutorial/security.py | 40 +++++++++ .../wiki2/src/tests/tutorial/security/__init__.py | 0 .../wiki2/src/tests/tutorial/security/default.py | 12 --- .../wiki2/src/tests/tutorial/templates/edit.jinja2 | 93 +++++-------------- .../src/tests/tutorial/templates/layout.jinja2 | 64 +++++++++++++ .../src/tests/tutorial/templates/login.jinja2 | 100 ++++++--------------- .../wiki2/src/tests/tutorial/templates/view.jinja2 | 89 ++++-------------- .../src/tests/tutorial/tests/test_functional.py | 56 +++++++----- .../wiki2/src/tests/tutorial/tests/test_views.py | 66 ++++++++------ .../wiki2/src/tests/tutorial/views/auth.py | 25 +++--- .../wiki2/src/tests/tutorial/views/default.py | 54 +++++------ docs/tutorials/wiki2/tests.rst | 43 +++------ 18 files changed, 354 insertions(+), 369 deletions(-) create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/routes.py create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/security.py delete mode 100644 docs/tutorials/wiki2/src/tests/tutorial/security/__init__.py delete mode 100644 docs/tutorials/wiki2/src/tests/tutorial/security/default.py create mode 100644 docs/tutorials/wiki2/src/tests/tutorial/templates/layout.jinja2 diff --git a/docs/tutorials/wiki2/src/tests/development.ini b/docs/tutorials/wiki2/src/tests/development.ini index 99c4ff0fe..f3079727e 100644 --- a/docs/tutorials/wiki2/src/tests/development.ini +++ b/docs/tutorials/wiki2/src/tests/development.ini @@ -17,6 +17,8 @@ pyramid.includes = sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite +auth.secret = seekrit + # By default, the toolbar only appears for clients from IP addresses # '127.0.0.1' and '::1'. # debugtoolbar.hosts = 127.0.0.1 ::1 diff --git a/docs/tutorials/wiki2/src/tests/production.ini b/docs/tutorials/wiki2/src/tests/production.ini index cb1db3211..686dba48a 100644 --- a/docs/tutorials/wiki2/src/tests/production.ini +++ b/docs/tutorials/wiki2/src/tests/production.ini @@ -14,6 +14,8 @@ pyramid.default_locale_name = en sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite +auth.secret = real-seekrit + [server:main] use = egg:waitress#main host = 0.0.0.0 diff --git a/docs/tutorials/wiki2/src/tests/setup.py b/docs/tutorials/wiki2/src/tests/setup.py index e06aa06e4..57538f2d0 100644 --- a/docs/tutorials/wiki2/src/tests/setup.py +++ b/docs/tutorials/wiki2/src/tests/setup.py @@ -43,8 +43,8 @@ setup(name='tutorial', include_package_data=True, zip_safe=False, test_suite='tutorial', - install_requires=requires, tests_require=tests_require, + install_requires=requires, entry_points="""\ [paste.app_factory] main = tutorial:main diff --git a/docs/tutorials/wiki2/src/tests/tutorial/__init__.py b/docs/tutorials/wiki2/src/tests/tutorial/__init__.py index a62c42378..f5c033b8b 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/__init__.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/__init__.py @@ -1,28 +1,13 @@ from pyramid.config import Configurator -from pyramid.authentication import AuthTktAuthenticationPolicy -from pyramid.authorization import ACLAuthorizationPolicy - -from .security.default import groupfinder def main(global_config, **settings): """ This function returns a Pyramid WSGI application. """ - authn_policy = AuthTktAuthenticationPolicy( - 'sosecret', callback=groupfinder, hashalg='sha512') - authz_policy = ACLAuthorizationPolicy() config = Configurator(settings=settings) config.include('pyramid_jinja2') config.include('.models') - config.set_root_factory('.models.mymodel.RootFactory') - config.set_authentication_policy(authn_policy) - config.set_authorization_policy(authz_policy) - config.add_static_view('static', 'static', cache_max_age=3600) - config.add_route('view_wiki', '/') - config.add_route('login', '/login') - config.add_route('logout', '/logout') - config.add_route('view_page', '/{pagename}') - config.add_route('add_page', '/add_page/{pagename}') - config.add_route('edit_page', '/{pagename}/edit_page') + config.include('.routes') + config.include('.security') config.scan() return config.make_wsgi_app() diff --git a/docs/tutorials/wiki2/src/tests/tutorial/models/user.py b/docs/tutorials/wiki2/src/tests/tutorial/models/user.py index 25b0a8187..6fb32a1b2 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/models/user.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/models/user.py @@ -18,10 +18,12 @@ class User(Base): password_hash = Column(Text) def set_password(self, pw): - pwhash = bcrypt.hashpw(pw, bcrypt.gensalt()) + pwhash = bcrypt.hashpw(pw.encode('utf8'), bcrypt.gensalt()) self.password_hash = pwhash def check_password(self, pw): if self.password_hash is not None: - return bcrypt.hashpw(pw, self.password_hash) == self.password_hash + expected_hash = self.password_hash.encode('utf8') + actual_hash = bcrypt.hashpw(pw.encode('utf8'), expected_hash) + return expected_hash == actual_hash return False diff --git a/docs/tutorials/wiki2/src/tests/tutorial/routes.py b/docs/tutorials/wiki2/src/tests/tutorial/routes.py new file mode 100644 index 000000000..c7c3a2120 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/routes.py @@ -0,0 +1,50 @@ +from pyramid.httpexceptions import HTTPNotFound +from pyramid.security import ( + Allow, + Everyone, +) + +from .models import Page + +def includeme(config): + config.add_static_view('static', 'static', cache_max_age=3600) + config.add_route('view_wiki', '/') + config.add_route('login', '/login') + config.add_route('logout', '/logout') + config.add_route('view_page', '/{pagename}', factory=page_factory) + config.add_route('add_page', '/add_page/{pagename}', + factory=new_page_factory) + config.add_route('edit_page', '/{pagename}/edit_page', + factory=page_factory) + +def new_page_factory(request): + pagename = request.matchdict['pagename'] + return NewPage(pagename) + +class NewPage(object): + def __init__(self, pagename): + self.pagename = pagename + + def __acl__(self): + return [ + (Allow, 'role:editor', 'create'), + (Allow, 'role:basic', 'create'), + ] + +def page_factory(request): + pagename = request.matchdict['pagename'] + page = request.dbsession.query(Page).filter_by(name=pagename).first() + if page is None: + raise HTTPNotFound + return PageResource(page) + +class PageResource(object): + def __init__(self, page): + self.page = page + + def __acl__(self): + return [ + (Allow, Everyone, 'view'), + (Allow, 'role:editor', 'edit'), + (Allow, str(self.page.creator_id), 'edit'), + ] diff --git a/docs/tutorials/wiki2/src/tests/tutorial/security.py b/docs/tutorials/wiki2/src/tests/tutorial/security.py new file mode 100644 index 000000000..25cff7b05 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/security.py @@ -0,0 +1,40 @@ +from pyramid.authentication import AuthTktAuthenticationPolicy +from pyramid.authorization import ACLAuthorizationPolicy +from pyramid.security import ( + Authenticated, + Everyone, +) + +from .models import User + + +class MyAuthenticationPolicy(AuthTktAuthenticationPolicy): + def authenticated_userid(self, request): + user = request.user + if user is not None: + return user.id + + def effective_principals(self, request): + principals = [Everyone] + user = request.user + if user is not None: + principals.append(Authenticated) + principals.append(str(user.id)) + principals.append('role:' + user.role) + return principals + +def get_user(request): + user_id = request.unauthenticated_userid + if user_id is not None: + user = request.dbsession.query(User).get(user_id) + return user + +def includeme(config): + settings = config.get_settings() + authn_policy = MyAuthenticationPolicy( + settings['auth.secret'], + hashalg='sha512', + ) + config.set_authentication_policy(authn_policy) + config.set_authorization_policy(ACLAuthorizationPolicy()) + config.add_request_method(get_user, 'user', reify=True) diff --git a/docs/tutorials/wiki2/src/tests/tutorial/security/__init__.py b/docs/tutorials/wiki2/src/tests/tutorial/security/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/tutorials/wiki2/src/tests/tutorial/security/default.py b/docs/tutorials/wiki2/src/tests/tutorial/security/default.py deleted file mode 100644 index 7fc1ea7c8..000000000 --- a/docs/tutorials/wiki2/src/tests/tutorial/security/default.py +++ /dev/null @@ -1,12 +0,0 @@ -USERS = { - 'editor': 'editor', - 'viewer': 'viewer', -} - -GROUPS = { - 'editor': ['group:editors'], -} - -def groupfinder(userid, request): - if userid in USERS: - return GROUPS.get(userid, []) diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/edit.jinja2 b/docs/tutorials/wiki2/src/tests/tutorial/templates/edit.jinja2 index 4d767cfbe..7db25c674 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/templates/edit.jinja2 +++ b/docs/tutorials/wiki2/src/tests/tutorial/templates/edit.jinja2 @@ -1,73 +1,20 @@ - - - - - - - - - - - Edit{% if page.name %} {{page.name}}{% endif %} - Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki) - - - - - - - - - - - - - -

-
-
-
- -
-
-
- {% if request.authenticated_userid is not none %} -

- Logout -

- {% endif %} -

- Editing {% if page.name %}{{page.name}}{% else %}Page Name Goes Here{% endif %} -

-

You can return to the - FrontPage. -

- -
- -
-
- -
- -
-
-
-
- -
-
-
- - - - - - - - +{% extends 'layout.jinja2' %} + +{% block subtitle %}Edit {{pagename}} - {% endblock subtitle %} + +{% block content %} +

+Editing {{pagename}} +

+

You can return to the +FrontPage. +

+
+
+ +
+
+ +
+
+{% endblock content %} diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/layout.jinja2 b/docs/tutorials/wiki2/src/tests/tutorial/templates/layout.jinja2 new file mode 100644 index 000000000..44d14304e --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/templates/layout.jinja2 @@ -0,0 +1,64 @@ + + + + + + + + + + + {% block subtitle %}{% endblock %}Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki) + + + + + + + + + + + + + +
+
+
+
+ +
+
+
+ {% if request.user is none %} +

+ Login +

+ {% else %} +

+ {{request.user.name}} Logout +

+ {% endif %} + {% block content %}{% endblock %} +
+
+
+
+ +
+
+
+ + + + + + + + diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/login.jinja2 b/docs/tutorials/wiki2/src/tests/tutorial/templates/login.jinja2 index a80a2a165..1806de0ff 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/templates/login.jinja2 +++ b/docs/tutorials/wiki2/src/tests/tutorial/templates/login.jinja2 @@ -1,74 +1,26 @@ - - - - - - - - - - - Login - Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki) - - - - - - - - - - - - - -
-
-
-
- -
-
-
-

- - Login -
- {{ message }} -

-
- -
- - -
-
- - -
-
- -
-
-
-
-
-
- -
-
-
- - - - - - - - +{% extends 'layout.jinja2' %} + +{% block title %}Login - {% endblock title %} + +{% block content %} +

+ + Login +
+{{ message }} +

+
+ +
+ + +
+
+ + +
+
+ +
+
+{% endblock content %} diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/view.jinja2 b/docs/tutorials/wiki2/src/tests/tutorial/templates/view.jinja2 index 942b8479b..94419e228 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/templates/view.jinja2 +++ b/docs/tutorials/wiki2/src/tests/tutorial/templates/view.jinja2 @@ -1,71 +1,18 @@ - - - - - - - - - - - {{page.name}} - Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki) - - - - - - - - - - - - - -
-
-
-
- -
-
-
- {% if request.authenticated_userid is not none %} -

- Logout -

- {% endif %} -

{{ content|safe }}

-

- - Edit this page - -

-

- Viewing {% if page.name %}{{page.name}}{% else %}Page Name Goes Here{% endif %} -

-

You can return to the - FrontPage. -

-
-
-
-
- -
-
-
- - - - - - - - +{% extends 'layout.jinja2' %} + +{% block subtitle %}{{page.name}} - {% endblock subtitle %} + +{% block content %} +

{{ content|safe }}

+

+ + Edit this page + +

+

+ Viewing {{page.name}}, created by {{page.creator.name}}. +

+

You can return to the +FrontPage. +

+{% endblock content %} diff --git a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py index c716537ae..b2c6e0975 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py @@ -5,26 +5,30 @@ from webtest import TestApp class FunctionalTests(unittest.TestCase): - viewer_login = ( - '/login?login=viewer&password=viewer' - '&came_from=FrontPage&form.submitted=Login') - viewer_wrong_login = ( - '/login?login=viewer&password=incorrect' - '&came_from=FrontPage&form.submitted=Login') + basic_login = ( + '/login?login=basic&password=basic' + '&next=FrontPage&form.submitted=Login') + basic_wrong_login = ( + '/login?login=basic&password=incorrect' + '&next=FrontPage&form.submitted=Login') editor_login = ( '/login?login=editor&password=editor' - '&came_from=FrontPage&form.submitted=Login') + '&next=FrontPage&form.submitted=Login') @classmethod def setUpClass(cls): from tutorial.models.meta import Base from tutorial.models import ( + User, Page, get_tm_session, ) from tutorial import main - settings = {'sqlalchemy.url': 'sqlite://'} + settings = { + 'sqlalchemy.url': 'sqlite://', + 'auth.secret': 'seekrit', + } app = main({}, **settings) cls.testapp = TestApp(app) @@ -34,8 +38,15 @@ class FunctionalTests(unittest.TestCase): with transaction.manager: dbsession = get_tm_session(session_factory, transaction.manager) - model = Page(name='FrontPage', data='This is the front page') - dbsession.add(model) + editor = User(name='editor', role='editor') + editor.set_password('editor') + basic = User(name='basic', role='basic') + basic.set_password('basic') + page1 = Page(name='FrontPage', data='This is the front page') + page1.creator = editor + page2 = Page(name='BackPage', data='This is the back page') + page2.creator = basic + dbsession.add_all([basic, editor, page1, page2]) @classmethod def tearDownClass(cls): @@ -54,20 +65,20 @@ class FunctionalTests(unittest.TestCase): self.testapp.get('/SomePage', status=404) def test_successful_log_in(self): - res = self.testapp.get(self.viewer_login, status=302) + res = self.testapp.get(self.basic_login, status=302) self.assertEqual(res.location, 'http://localhost/FrontPage') def test_failed_log_in(self): - res = self.testapp.get(self.viewer_wrong_login, status=200) + res = self.testapp.get(self.basic_wrong_login, status=200) self.assertTrue(b'login' in res.body) def test_logout_link_present_when_logged_in(self): - self.testapp.get(self.viewer_login, status=302) + self.testapp.get(self.basic_login, status=302) res = self.testapp.get('/FrontPage', status=200) self.assertTrue(b'Logout' in res.body) def test_logout_link_not_present_after_logged_out(self): - self.testapp.get(self.viewer_login, status=302) + self.testapp.get(self.basic_login, status=302) self.testapp.get('/FrontPage', status=200) res = self.testapp.get('/logout', status=302) self.assertTrue(b'Logout' not in res.body) @@ -80,15 +91,20 @@ class FunctionalTests(unittest.TestCase): res = self.testapp.get('/add_page/NewPage', status=302).follow() self.assertTrue(b'Login' in res.body) - def test_viewer_user_cannot_edit(self): - self.testapp.get(self.viewer_login, status=302) + def test_basic_user_cannot_edit_front(self): + self.testapp.get(self.basic_login, status=302) res = self.testapp.get('/FrontPage/edit_page', status=302).follow() self.assertTrue(b'Login' in res.body) - def test_viewer_user_cannot_add(self): - self.testapp.get(self.viewer_login, status=302) - res = self.testapp.get('/add_page/NewPage', status=302).follow() - self.assertTrue(b'Login' in res.body) + def test_basic_user_can_edit_back(self): + self.testapp.get(self.basic_login, status=302) + res = self.testapp.get('/BackPage/edit_page', status=200) + self.assertTrue(b'Editing' in res.body) + + def test_basic_user_can_add(self): + self.testapp.get(self.basic_login, status=302) + res = self.testapp.get('/add_page/NewPage', status=200) + self.assertTrue(b'Editing' in res.body) def test_editors_member_user_can_edit(self): self.testapp.get(self.editor_login, status=302) diff --git a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_views.py b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_views.py index b2830d070..5253183df 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_views.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_views.py @@ -8,12 +8,6 @@ def dummy_request(dbsession): return testing.DummyRequest(dbsession=dbsession) -def _register_routes(config): - config.add_route('view_page', '{pagename}') - config.add_route('add_page', 'add_page/{pagename}') - config.add_route('edit_page', '{pagename}/edit_page') - - class BaseTest(unittest.TestCase): def setUp(self): from ..models import get_tm_session @@ -21,7 +15,7 @@ class BaseTest(unittest.TestCase): 'sqlalchemy.url': 'sqlite:///:memory:' }) self.config.include('..models') - self.config.include(_register_routes) + self.config.include('..routes') session_factory = self.config.registry['dbsession_factory'] self.session = get_tm_session(session_factory, transaction.manager) @@ -38,11 +32,21 @@ class BaseTest(unittest.TestCase): testing.tearDown() transaction.abort() + def makeUser(self, name, role, password='dummy'): + from ..models import User + user = User(name=name, role=role) + user.set_password(password) + return user + + def makePage(self, name, data, creator): + from ..models import Page + return Page(name=name, data=data, creator=creator) + class ViewWikiTests(unittest.TestCase): def setUp(self): self.config = testing.setUp() - _register_routes(self.config) + self.config.include('..routes') def tearDown(self): testing.tearDown() @@ -63,14 +67,16 @@ class ViewPageTests(BaseTest): return view_page(request) def test_it(self): + from ..routes import PageResource + # add a page to the db - from ..models.mymodel import Page - page = Page(name='IDoExist', data='Hello CruelWorld IDoExist') - self.session.add(page) + user = self.makeUser('foo', 'editor') + page = self.makePage('IDoExist', 'Hello CruelWorld IDoExist', user) + self.session.add_all([page, user]) # create a request asking for the page we've created request = dummy_request(self.session) - request.matchdict['pagename'] = 'IDoExist' + request.context = PageResource(page) # call the view we're testing and check its behavior info = self._callFUT(request) @@ -93,19 +99,23 @@ class AddPageTests(BaseTest): return add_page(request) def test_it_notsubmitted(self): + from ..routes import NewPage request = dummy_request(self.session) - request.matchdict = {'pagename': 'AnotherPage'} + request.user = self.makeUser('foo', 'editor') + request.context = NewPage('AnotherPage') info = self._callFUT(request) - self.assertEqual(info['page'].data, '') + self.assertEqual(info['pagedata'], '') self.assertEqual(info['save_url'], 'http://example.com/add_page/AnotherPage') def test_it_submitted(self): - from ..models.mymodel import Page + from ..models import Page + from ..routes import NewPage request = testing.DummyRequest({'form.submitted': True, 'body': 'Hello yo!'}, dbsession=self.session) - request.matchdict = {'pagename': 'AnotherPage'} + request.user = self.makeUser('foo', 'editor') + request.context = NewPage('AnotherPage') self._callFUT(request) page = self.session.query(Page).filter_by(name='AnotherPage').one() self.assertEqual(page.data, 'Hello yo!') @@ -116,25 +126,31 @@ class EditPageTests(BaseTest): from tutorial.views.default import edit_page return edit_page(request) + def makeContext(self, page): + from ..routes import PageResource + return PageResource(page) + def test_it_notsubmitted(self): - from ..models.mymodel import Page + user = self.makeUser('foo', 'editor') + page = self.makePage('abc', 'hello', user) + self.session.add_all([page, user]) + request = dummy_request(self.session) - request.matchdict = {'pagename': 'abc'} - page = Page(name='abc', data='hello') - self.session.add(page) + request.context = self.makeContext(page) info = self._callFUT(request) - self.assertEqual(info['page'], page) + self.assertEqual(info['pagename'], 'abc') self.assertEqual(info['save_url'], 'http://example.com/abc/edit_page') def test_it_submitted(self): - from ..models.mymodel import Page + user = self.makeUser('foo', 'editor') + page = self.makePage('abc', 'hello', user) + self.session.add_all([page, user]) + request = testing.DummyRequest({'form.submitted': True, 'body': 'Hello yo!'}, dbsession=self.session) - request.matchdict = {'pagename': 'abc'} - page = Page(name='abc', data='hello') - self.session.add(page) + request.context = self.makeContext(page) response = self._callFUT(request) self.assertEqual(response.location, 'http://example.com/abc') self.assertEqual(page.data, 'Hello yo!') diff --git a/docs/tutorials/wiki2/src/tests/tutorial/views/auth.py b/docs/tutorials/wiki2/src/tests/tutorial/views/auth.py index 08aa2bfad..2b993b430 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/views/auth.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/views/auth.py @@ -8,33 +8,30 @@ from pyramid.view import ( view_config, ) -from ..security.default import USERS +from ..models import User -@view_config(route_name='login', renderer='templates/login.jinja2') +@view_config(route_name='login', renderer='../templates/login.jinja2') def login(request): - login_url = request.route_url('login') - referrer = request.url - if referrer == login_url: - referrer = '/' # never use the login form itself as came_from - came_from = request.params.get('came_from', referrer) + next_url = request.params.get('next', request.referrer) + if not next_url: + next_url = request.route_url('view_wiki') message = '' login = '' - password = '' if 'form.submitted' in request.params: login = request.params['login'] password = request.params['password'] - if USERS.get(login) == password: - headers = remember(request, login) - return HTTPFound(location=came_from, headers=headers) + user = request.dbsession.query(User).filter_by(name=login).first() + if user is not None and user.check_password(password): + headers = remember(request, user.id) + return HTTPFound(location=next_url, headers=headers) message = 'Failed login' return dict( message=message, url=request.route_url('login'), - came_from=came_from, + next_url=next_url, login=login, - password=password, ) @view_config(route_name='logout') @@ -45,5 +42,5 @@ def logout(request): @forbidden_view_config() def forbidden_view(request): - next_url = request.route_url('login', _query={'came_from': request.url}) + next_url = request.route_url('login', _query={'next': request.url}) return HTTPFound(location=next_url) diff --git a/docs/tutorials/wiki2/src/tests/tutorial/views/default.py b/docs/tutorials/wiki2/src/tests/tutorial/views/default.py index 6fb3c8744..9358993ea 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/views/default.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/views/default.py @@ -2,10 +2,7 @@ import cgi import re from docutils.core import publish_parts -from pyramid.httpexceptions import ( - HTTPFound, - HTTPNotFound, - ) +from pyramid.httpexceptions import HTTPFound from pyramid.view import view_config from ..models import Page @@ -13,7 +10,7 @@ from ..models import Page # regular expression used to find WikiWords wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)") -@view_config(route_name='view_wiki', permission='view') +@view_config(route_name='view_wiki') def view_wiki(request): next_url = request.route_url('view_page', pagename='FrontPage') return HTTPFound(location=next_url) @@ -21,12 +18,9 @@ def view_wiki(request): @view_config(route_name='view_page', renderer='../templates/view.jinja2', permission='view') def view_page(request): - pagename = request.matchdict['pagename'] - page = request.dbsession.query(Page).filter_by(name=pagename).first() - if page is None: - return HTTPNotFound('No such page') + page = request.context.page - def check(match): + def add_link(match): word = match.group(1) exists = request.dbsession.query(Page).filter_by(name=word).all() if exists: @@ -37,34 +31,34 @@ def view_page(request): return '%s' % (add_url, cgi.escape(word)) content = publish_parts(page.data, writer_name='html')['html_body'] - content = wikiwords.sub(check, content) - edit_url = request.route_url('edit_page', pagename=pagename) + content = wikiwords.sub(add_link, content) + edit_url = request.route_url('edit_page', pagename=page.name) return dict(page=page, content=content, edit_url=edit_url) -@view_config(route_name='add_page', renderer='../templates/edit.jinja2', +@view_config(route_name='edit_page', renderer='../templates/edit.jinja2', permission='edit') +def edit_page(request): + page = request.context.page + if 'form.submitted' in request.params: + page.data = request.params['body'] + next_url = request.route_url('view_page', pagename=page.name) + return HTTPFound(location=next_url) + return dict( + pagename=page.name, + pagedata=page.data, + save_url=request.route_url('edit_page', pagename=page.name), + ) + +@view_config(route_name='add_page', renderer='../templates/edit.jinja2', + permission='create') def add_page(request): - pagename = request.matchdict['pagename'] + pagename = request.context.pagename if 'form.submitted' in request.params: body = request.params['body'] page = Page(name=pagename, data=body) + page.creator = request.user request.dbsession.add(page) next_url = request.route_url('view_page', pagename=pagename) return HTTPFound(location=next_url) save_url = request.route_url('add_page', pagename=pagename) - page = Page(name='', data='') - return dict(page=page, save_url=save_url) - -@view_config(route_name='edit_page', renderer='../templates/edit.jinja2', - permission='edit') -def edit_page(request): - pagename = request.matchdict['pagename'] - page = request.dbsession.query(Page).filter_by(name=pagename).one() - if 'form.submitted' in request.params: - page.data = request.params['body'] - next_url = request.route_url('view_page', pagename=pagename) - return HTTPFound(location=next_url) - return dict( - page=page, - save_url=request.route_url('edit_page', pagename=pagename), - ) + return dict(pagename=pagename, pagedata='', save_url=save_url) diff --git a/docs/tutorials/wiki2/tests.rst b/docs/tutorials/wiki2/tests.rst index a99cd68cc..667550467 100644 --- a/docs/tutorials/wiki2/tests.rst +++ b/docs/tutorials/wiki2/tests.rst @@ -43,7 +43,7 @@ Functional tests We'll test the whole application, covering security aspects that are not tested in the unit tests, like logging in, logging out, checking that -the ``viewer`` user cannot add or edit pages, but the ``editor`` user +the ``basic`` user cannot edit pages it didn't create, but the ``editor`` user can, and so on. @@ -65,39 +65,20 @@ follows: :language: python -Running the tests -================= - -We can run these tests by using ``setup.py test`` in the same way we did in -:ref:`running_tests`. However, first we must edit our ``setup.py`` to include -a dependency on `WebTest -`_, which we've used -in our ``tests.py``. Change the ``requires`` list in ``setup.py`` to include -``WebTest``. - -.. literalinclude:: src/tests/setup.py - :linenos: - :language: python - :lines: 11-22 - :emphasize-lines: 11 +.. note:: -After we've added a dependency on WebTest in ``setup.py``, we need to run -``setup.py develop`` to get WebTest installed into our virtualenv. Assuming -our shell's current working directory is the "tutorial" distribution directory: + We're utilizing the excellent WebTest_ package to do functional testing + of the application. This is defined in the ``tests_require`` section of + our ``setup.py``. Any other dependencies needed only for testing purposes + can be added there and will be installed automatically when running + ``setup.py test``. -On UNIX: - -.. code-block:: bash - $ $VENV/bin/python setup.py develop - -On Windows: - -.. code-block:: text - - c:\pyramidtut\tutorial> %VENV%\Scripts\python setup.py develop +Running the tests +================= -Once that command has completed successfully, we can run the tests themselves: +We can run these tests by using ``setup.py test`` in the same way we did in +:ref:`running_tests`: On UNIX: @@ -122,3 +103,5 @@ The expected result should look like the following: OK Process finished with exit code 0 + +.. _webtest: http://docs.pylonsproject.org/projects/webtest/en/latest/ -- cgit v1.2.3 From 8d212aac349c381fa20c2c8927acdaf4873e394e Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sat, 20 Feb 2016 19:09:34 -0800 Subject: fix links for babel and chameleon --- docs/glossary.rst | 23 +++++++++++------------ docs/narr/i18n.rst | 8 ++++---- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/docs/glossary.rst b/docs/glossary.rst index 2683ff369..bbc86db41 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -367,13 +367,13 @@ Glossary file. It was developed by Ian Bicking. Chameleon - `chameleon `_ is an attribute language - template compiler which supports the :term:`ZPT` templating - specification. It is written and maintained by Malthe Borch. It has - several extensions, such as the ability to use bracketed (Mako-style) - ``${name}`` syntax. It is also much faster than the reference - implementation of ZPT. :app:`Pyramid` offers Chameleon templating out - of the box in ZPT and text flavors. + `chameleon `_ is an + attribute language template compiler which supports the :term:`ZPT` + templating specification. It is written and maintained by Malthe Borch. It + has several extensions, such as the ability to use bracketed (Mako-style) + ``${name}`` syntax. It is also much faster than the reference + implementation of ZPT. :app:`Pyramid` offers Chameleon templating out of + the box in ZPT and text flavors. ZPT The `Zope Page Template `_ @@ -815,11 +815,10 @@ Glossary library, used by the :app:`Pyramid` translation machinery. Babel - A `collection of tools `_ for - internationalizing Python applications. :app:`Pyramid` does - not depend on Babel to operate, but if Babel is installed, - additional locale functionality becomes available to your - application. + A `collection of tools `_ for + internationalizing Python applications. :app:`Pyramid` does not depend on + Babel to operate, but if Babel is installed, additional locale + functionality becomes available to your application. Lingua A package by Wichert Akkerman which provides the ``pot-create`` diff --git a/docs/narr/i18n.rst b/docs/narr/i18n.rst index ecc48aa2b..839a48df4 100644 --- a/docs/narr/i18n.rst +++ b/docs/narr/i18n.rst @@ -585,10 +585,10 @@ Performing Date Formatting and Currency Formatting :app:`Pyramid` does not itself perform date and currency formatting for different locales. However, :term:`Babel` can help you do this via the :class:`babel.core.Locale` class. The `Babel documentation for this class -`_ -provides minimal information about how to perform date and currency related -locale operations. See :ref:`installing_babel` for information about how to -install Babel. +`_ provides +minimal information about how to perform date and currency related locale +operations. See :ref:`installing_babel` for information about how to install +Babel. The :class:`babel.core.Locale` class requires a :term:`locale name` as an argument to its constructor. You can use :app:`Pyramid` APIs to obtain the -- cgit v1.2.3 From 6a2bdac1ec3b1fb6ed37bc8b92b807df6c0fa07d Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sun, 21 Feb 2016 03:21:10 -0800 Subject: wiki2 docs update WIP - minor grammar, .rst syntax - add pip to glossary - add pip instructions, commented until 1.7 is released --- docs/glossary.rst | 4 ++++ docs/tutorials/wiki2/background.rst | 4 ++-- docs/tutorials/wiki2/design.rst | 2 +- docs/tutorials/wiki2/index.rst | 12 ++++++------ 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/docs/glossary.rst b/docs/glossary.rst index 2683ff369..cbf58c226 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -1093,3 +1093,7 @@ Glossary A technique used when serving a cacheable static asset in order to force a client to query the new version of the asset. See :ref:`cache_busting` for more information. + + pip + The `Python Packaging Authority `_ recommended tool + for installing Python packages. diff --git a/docs/tutorials/wiki2/background.rst b/docs/tutorials/wiki2/background.rst index b8afb8305..2dac847d8 100644 --- a/docs/tutorials/wiki2/background.rst +++ b/docs/tutorials/wiki2/background.rst @@ -5,13 +5,13 @@ Background This version of the :app:`Pyramid` wiki tutorial presents a :app:`Pyramid` application that uses technologies which will be familiar to someone with SQL database experience. It uses -:term:`SQLAlchemy` as a persistence mechanism and :term:`url dispatch` to map +:term:`SQLAlchemy` as a persistence mechanism and :term:`URL dispatch` to map URLs to code. It can also be followed by people without any prior Python web framework experience. To code along with this tutorial, the developer will need a UNIX machine with development tools (Mac OS X with XCode, any Linux or BSD -variant, etc) *or* a Windows system of any kind. +variant, etc.) *or* a Windows system of any kind. .. note:: diff --git a/docs/tutorials/wiki2/design.rst b/docs/tutorials/wiki2/design.rst index 45e2fddd0..929bc7806 100644 --- a/docs/tutorials/wiki2/design.rst +++ b/docs/tutorials/wiki2/design.rst @@ -10,7 +10,7 @@ Overall We choose to use :term:`reStructuredText` markup in the wiki text. Translation from reStructuredText to HTML is provided by the widely used ``docutils`` -Python module. We will add this module in the dependency list on the project +Python module. We will add this module to the dependency list in the project's ``setup.py`` file. Models diff --git a/docs/tutorials/wiki2/index.rst b/docs/tutorials/wiki2/index.rst index 74fb5bfd5..18e9f552e 100644 --- a/docs/tutorials/wiki2/index.rst +++ b/docs/tutorials/wiki2/index.rst @@ -1,15 +1,15 @@ .. _bfg_sql_wiki_tutorial: -SQLAlchemy + URL Dispatch Wiki Tutorial +SQLAlchemy + URL dispatch wiki tutorial ======================================= -This tutorial introduces a :term:`SQLAlchemy` and :term:`url dispatch`-based +This tutorial introduces an :term:`SQLAlchemy` and :term:`URL dispatch`-based :app:`Pyramid` application to a developer familiar with Python. When the -tutorial is finished, the developer will have created a basic Wiki -application with authentication. +tutorial is finished, the developer will have created a basic wiki +application with authentication and authorization. -For cut and paste purposes, the source code for all stages of this -tutorial can be browsed on GitHub at `docs/tutorials/wiki2/src +For cut and paste purposes, the source code for all stages of this tutorial can +be browsed on GitHub at `docs/tutorials/wiki2/src `_, which corresponds to the same location if you have Pyramid sources. -- cgit v1.2.3 From 25fed631357e23aaa4d54c2923e8c463cd41930e Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sun, 21 Feb 2016 03:22:13 -0800 Subject: oopsie, include installation in commit --- docs/tutorials/wiki2/installation.rst | 127 ++++++++++++++++++++-------------- 1 file changed, 74 insertions(+), 53 deletions(-) diff --git a/docs/tutorials/wiki2/installation.rst b/docs/tutorials/wiki2/installation.rst index 5d6d8e56b..1dd71cb76 100644 --- a/docs/tutorials/wiki2/installation.rst +++ b/docs/tutorials/wiki2/installation.rst @@ -9,9 +9,9 @@ This tutorial assumes that you have already followed the steps in :ref:`installing_chapter`, except **do not create a virtualenv or install Pyramid**. Thereby you will satisfy the following requirements. -* Python interpreter is installed on your operating system -* :term:`setuptools` or :term:`distribute` is installed -* :term:`virtualenv` is installed +* A Python interpreter is installed on your operating system. +* :term:`virtualenv` is installed. +* :term:`pip` will be installed when we create a virtual environment. Create directory to contain the project --------------------------------------- @@ -21,38 +21,36 @@ We need a workspace for our project files. On UNIX ^^^^^^^ -.. code-block:: text +.. code-block:: bash $ mkdir ~/pyramidtut On Windows ^^^^^^^^^^ -.. code-block:: text +.. code-block:: ps1con c:\> mkdir pyramidtut Create and use a virtual Python environment ------------------------------------------- -Next let's create a `virtualenv` workspace for our project. We will -use the `VENV` environment variable instead of the absolute path of the -virtual environment. +Next let's create a ``virtualenv`` workspace for our project. We will use the +``VENV`` environment variable instead of the absolute path of the virtual +environment. On UNIX ^^^^^^^ -.. code-block:: text +.. code-block:: bash $ export VENV=~/pyramidtut $ virtualenv $VENV - New python executable in /home/foo/env/bin/python - Installing setuptools.............done. On Windows ^^^^^^^^^^ -.. code-block:: text +.. code-block:: ps1con c:\> set VENV=c:\pyramidtut @@ -61,15 +59,24 @@ path to the command for your Python version. Python 2.7: -.. code-block:: text +.. code-block:: ps1con c:\> c:\Python27\Scripts\virtualenv %VENV% -Python 3.3: +Python 3.5: -.. code-block:: text +.. code-block:: ps1con + + c:\> c:\Python35\Scripts\virtualenv %VENV% + + +.. Upgrade pip in the virtual environment + -------------------------------------- + +.. .. code-block:: bash + +.. $ $VENV/bin/pip install --upgrade pip - c:\> c:\Python33\Scripts\virtualenv %VENV% Install Pyramid into the virtual Python environment --------------------------------------------------- @@ -77,52 +84,58 @@ Install Pyramid into the virtual Python environment On UNIX ^^^^^^^ -.. code-block:: text +.. code-block:: bash $ $VENV/bin/easy_install pyramid +.. $ $VENV/bin/pip install pyramid + On Windows ^^^^^^^^^^ -.. code-block:: text +.. code-block:: ps1con c:\> %VENV%\Scripts\easy_install pyramid +.. c:\> %VENV%\Scripts\pip install pyramid + Install SQLite3 and its development packages -------------------------------------------- If you used a package manager to install your Python or if you compiled your Python from source, then you must install SQLite3 and its development packages. If you downloaded your Python as an installer -from https://www.python.org, then you already have it installed and can -proceed to the next section :ref:`sql_making_a_project`. +from https://www.python.org, then you already have it installed and can skip +this step. If you need to install the SQLite3 packages, then, for example, using the Debian system and ``apt-get``, the command would be the following: -.. code-block:: text +.. code-block:: bash $ sudo apt-get install libsqlite3-dev Change directory to your virtual Python environment --------------------------------------------------- -Change directory to the ``pyramidtut`` directory. +Change directory to the ``pyramidtut`` directory, which is both your workspace +and your virtual environment. On UNIX ^^^^^^^ -.. code-block:: text +.. code-block:: bash $ cd pyramidtut On Windows ^^^^^^^^^^ -.. code-block:: text +.. code-block:: ps1con c:\> cd pyramidtut + .. _sql_making_a_project: Making a project @@ -132,30 +145,29 @@ Your next step is to create a project. For this tutorial we will use the :term:`scaffold` named ``alchemy`` which generates an application that uses :term:`SQLAlchemy` and :term:`URL dispatch`. -:app:`Pyramid` supplies a variety of scaffolds to generate sample -projects. We will use `pcreate` — a script that comes with Pyramid to -quickly and easily generate scaffolds, usually with a single command — to -create the scaffold for our project. +:app:`Pyramid` supplies a variety of scaffolds to generate sample projects. We +will use ``pcreate``, a script that comes with Pyramid, to create our project +using a scaffold. -By passing `alchemy` into the `pcreate` command, the script creates -the files needed to use SQLAlchemy. By passing in our application name -`tutorial`, the script inserts that application name into all the -required files. For example, `pcreate` creates the -``initialize_tutorial_db`` in the ``pyramidtut/bin`` directory. +By passing ``alchemy`` into the ``pcreate`` command, the script creates the +files needed to use SQLAlchemy. By passing in our application name +``tutorial``, the script inserts that application name into all the required +files. For example, ``pcreate`` creates the ``initialize_tutorial_db`` in the +``pyramidtut/bin`` directory. The below instructions assume your current working directory is "pyramidtut". On UNIX ------- -.. code-block:: text +.. code-block:: bash $ $VENV/bin/pcreate -s alchemy tutorial On Windows ---------- -.. code-block:: text +.. code-block:: ps1con c:\pyramidtut> %VENV%\Scripts\pcreate -s alchemy tutorial @@ -165,34 +177,39 @@ On Windows startup problems, try putting both the virtualenv and the project into directories that do not contain spaces in their paths. + .. _installing_project_in_dev_mode: Installing the project in development mode ========================================== -In order to do development on the project easily, you must "register" -the project as a development egg in your workspace using the -``setup.py develop`` command. In order to do so, cd to the `tutorial` -directory you created in :ref:`sql_making_a_project`, and run the -``setup.py develop`` command using the virtualenv Python interpreter. +In order to do development on the project easily, you must "register" the +project as a development egg in your workspace using the ``setup.py develop`` +command. In order to do so, change directory to the ``tutorial`` directory that +you created in :ref:`sql_making_a_project`, and run the ``setup.py develop`` +command using the virtualenv Python interpreter. On UNIX ------- -.. code-block:: text +.. code-block:: bash $ cd tutorial $ $VENV/bin/python setup.py develop +.. $ $VENV/bin/pip install -e . + On Windows ---------- -.. code-block:: text +.. code-block:: ps1con c:\pyramidtut> cd tutorial c:\pyramidtut\tutorial> %VENV%\Scripts\python setup.py develop -The console will show `setup.py` checking for packages and installing +.. c:\pyramidtut\tutorial> %VENV%\Scripts\pip install -e . + +The console will show ``setup.py`` checking for packages and installing missing packages. Success executing this command will show a line like the following:: @@ -209,17 +226,21 @@ the tests for the project. On UNIX ------- -.. code-block:: text +.. code-block:: bash $ $VENV/bin/python setup.py test -q +.. py.test? See https://github.com/Pylons/pyramid/issues/2104#issuecomment-155852046 + On Windows ---------- -.. code-block:: text +.. code-block:: ps1con c:\pyramidtut\tutorial> %VENV%\Scripts\python setup.py test -q +.. py.test? See https://github.com/Pylons/pyramid/issues/2104#issuecomment-155852046 + For a successful test run, you should see output that ends like this:: . @@ -243,14 +264,14 @@ To get this functionality working, we'll need to install the ``nose`` and On UNIX ------- -.. code-block:: text +.. code-block:: bash $ $VENV/bin/easy_install nose coverage On Windows ---------- -.. code-block:: text +.. code-block:: ps1con c:\pyramidtut\tutorial> %VENV%\Scripts\easy_install nose coverage @@ -260,14 +281,14 @@ coverage tests. On UNIX ------- -.. code-block:: text +.. code-block:: bash $ $VENV/bin/nosetests --cover-package=tutorial --cover-erase --with-coverage On Windows ---------- -.. code-block:: text +.. code-block:: ps1con c:\pyramidtut\tutorial> %VENV%\Scripts\nosetests --cover-package=tutorial \ --cover-erase --with-coverage @@ -311,14 +332,14 @@ directory (the directory with a ``development.ini`` in it): On UNIX ------- -.. code-block:: text +.. code-block:: bash $ $VENV/bin/initialize_tutorial_db development.ini On Windows ---------- -.. code-block:: text +.. code-block:: ps1con c:\pyramidtut\tutorial> %VENV%\Scripts\initialize_tutorial_db development.ini @@ -363,14 +384,14 @@ Start the application. On UNIX ------- -.. code-block:: text +.. code-block:: bash $ $VENV/bin/pserve development.ini --reload On Windows ---------- -.. code-block:: text +.. code-block:: ps1con c:\pyramidtut\tutorial> %VENV%\Scripts\pserve development.ini --reload -- cgit v1.2.3 From 798e8549dfc8dc779b2ce5bd5552f5603fef60ea Mon Sep 17 00:00:00 2001 From: Marcin Lulek Date: Sun, 21 Feb 2016 22:19:45 +0100 Subject: Fix the import from meta --- pyramid/scaffolds/alchemy/+package+/tests.py_tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl b/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl index be42cb5d9..072eab5b2 100644 --- a/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl +++ b/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl @@ -28,7 +28,7 @@ class BaseTest(unittest.TestCase): self.session = get_tm_session(session_factory, transaction.manager) def init_database(self): - from .models import Base + from .models.meta import Base Base.metadata.create_all(self.engine) def tearDown(self): -- cgit v1.2.3 From 93fa23ae1463e78230cc3af8ecb89a6813b5556b Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Sun, 21 Feb 2016 15:51:55 -0700 Subject: Remove references to pip --- docs/glossary.rst | 4 ---- docs/tutorials/wiki2/design.rst | 2 +- docs/tutorials/wiki2/index.rst | 12 ++++++------ docs/tutorials/wiki2/installation.rst | 26 +++----------------------- 4 files changed, 10 insertions(+), 34 deletions(-) diff --git a/docs/glossary.rst b/docs/glossary.rst index cbf58c226..2683ff369 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -1093,7 +1093,3 @@ Glossary A technique used when serving a cacheable static asset in order to force a client to query the new version of the asset. See :ref:`cache_busting` for more information. - - pip - The `Python Packaging Authority `_ recommended tool - for installing Python packages. diff --git a/docs/tutorials/wiki2/design.rst b/docs/tutorials/wiki2/design.rst index 929bc7806..45e2fddd0 100644 --- a/docs/tutorials/wiki2/design.rst +++ b/docs/tutorials/wiki2/design.rst @@ -10,7 +10,7 @@ Overall We choose to use :term:`reStructuredText` markup in the wiki text. Translation from reStructuredText to HTML is provided by the widely used ``docutils`` -Python module. We will add this module to the dependency list in the project's +Python module. We will add this module in the dependency list on the project ``setup.py`` file. Models diff --git a/docs/tutorials/wiki2/index.rst b/docs/tutorials/wiki2/index.rst index 18e9f552e..74fb5bfd5 100644 --- a/docs/tutorials/wiki2/index.rst +++ b/docs/tutorials/wiki2/index.rst @@ -1,15 +1,15 @@ .. _bfg_sql_wiki_tutorial: -SQLAlchemy + URL dispatch wiki tutorial +SQLAlchemy + URL Dispatch Wiki Tutorial ======================================= -This tutorial introduces an :term:`SQLAlchemy` and :term:`URL dispatch`-based +This tutorial introduces a :term:`SQLAlchemy` and :term:`url dispatch`-based :app:`Pyramid` application to a developer familiar with Python. When the -tutorial is finished, the developer will have created a basic wiki -application with authentication and authorization. +tutorial is finished, the developer will have created a basic Wiki +application with authentication. -For cut and paste purposes, the source code for all stages of this tutorial can -be browsed on GitHub at `docs/tutorials/wiki2/src +For cut and paste purposes, the source code for all stages of this +tutorial can be browsed on GitHub at `docs/tutorials/wiki2/src `_, which corresponds to the same location if you have Pyramid sources. diff --git a/docs/tutorials/wiki2/installation.rst b/docs/tutorials/wiki2/installation.rst index 1dd71cb76..eb6cf50e0 100644 --- a/docs/tutorials/wiki2/installation.rst +++ b/docs/tutorials/wiki2/installation.rst @@ -9,9 +9,9 @@ This tutorial assumes that you have already followed the steps in :ref:`installing_chapter`, except **do not create a virtualenv or install Pyramid**. Thereby you will satisfy the following requirements. -* A Python interpreter is installed on your operating system. -* :term:`virtualenv` is installed. -* :term:`pip` will be installed when we create a virtual environment. +* Python interpreter is installed on your operating system +* :term:`setuptools` or :term:`distribute` is installed +* :term:`virtualenv` is installed Create directory to contain the project --------------------------------------- @@ -70,14 +70,6 @@ Python 3.5: c:\> c:\Python35\Scripts\virtualenv %VENV% -.. Upgrade pip in the virtual environment - -------------------------------------- - -.. .. code-block:: bash - -.. $ $VENV/bin/pip install --upgrade pip - - Install Pyramid into the virtual Python environment --------------------------------------------------- @@ -88,8 +80,6 @@ On UNIX $ $VENV/bin/easy_install pyramid -.. $ $VENV/bin/pip install pyramid - On Windows ^^^^^^^^^^ @@ -97,8 +87,6 @@ On Windows c:\> %VENV%\Scripts\easy_install pyramid -.. c:\> %VENV%\Scripts\pip install pyramid - Install SQLite3 and its development packages -------------------------------------------- @@ -197,8 +185,6 @@ On UNIX $ cd tutorial $ $VENV/bin/python setup.py develop -.. $ $VENV/bin/pip install -e . - On Windows ---------- @@ -207,8 +193,6 @@ On Windows c:\pyramidtut> cd tutorial c:\pyramidtut\tutorial> %VENV%\Scripts\python setup.py develop -.. c:\pyramidtut\tutorial> %VENV%\Scripts\pip install -e . - The console will show ``setup.py`` checking for packages and installing missing packages. Success executing this command will show a line like the following:: @@ -230,8 +214,6 @@ On UNIX $ $VENV/bin/python setup.py test -q -.. py.test? See https://github.com/Pylons/pyramid/issues/2104#issuecomment-155852046 - On Windows ---------- @@ -239,8 +221,6 @@ On Windows c:\pyramidtut\tutorial> %VENV%\Scripts\python setup.py test -q -.. py.test? See https://github.com/Pylons/pyramid/issues/2104#issuecomment-155852046 - For a successful test run, you should see output that ends like this:: . -- cgit v1.2.3 From 1c0a3d7e326b06dc4ea0e015ab25c4dedb754342 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sun, 21 Feb 2016 23:12:19 -0800 Subject: revert a couple of the reversions --- docs/tutorials/wiki2/design.rst | 2 +- docs/tutorials/wiki2/index.rst | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/tutorials/wiki2/design.rst b/docs/tutorials/wiki2/design.rst index 45e2fddd0..929bc7806 100644 --- a/docs/tutorials/wiki2/design.rst +++ b/docs/tutorials/wiki2/design.rst @@ -10,7 +10,7 @@ Overall We choose to use :term:`reStructuredText` markup in the wiki text. Translation from reStructuredText to HTML is provided by the widely used ``docutils`` -Python module. We will add this module in the dependency list on the project +Python module. We will add this module to the dependency list in the project's ``setup.py`` file. Models diff --git a/docs/tutorials/wiki2/index.rst b/docs/tutorials/wiki2/index.rst index 74fb5bfd5..18e9f552e 100644 --- a/docs/tutorials/wiki2/index.rst +++ b/docs/tutorials/wiki2/index.rst @@ -1,15 +1,15 @@ .. _bfg_sql_wiki_tutorial: -SQLAlchemy + URL Dispatch Wiki Tutorial +SQLAlchemy + URL dispatch wiki tutorial ======================================= -This tutorial introduces a :term:`SQLAlchemy` and :term:`url dispatch`-based +This tutorial introduces an :term:`SQLAlchemy` and :term:`URL dispatch`-based :app:`Pyramid` application to a developer familiar with Python. When the -tutorial is finished, the developer will have created a basic Wiki -application with authentication. +tutorial is finished, the developer will have created a basic wiki +application with authentication and authorization. -For cut and paste purposes, the source code for all stages of this -tutorial can be browsed on GitHub at `docs/tutorials/wiki2/src +For cut and paste purposes, the source code for all stages of this tutorial can +be browsed on GitHub at `docs/tutorials/wiki2/src `_, which corresponds to the same location if you have Pyramid sources. -- cgit v1.2.3 From 8571f2bf08afed56f75d793f4f1676d1d86a9dac Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Mon, 22 Feb 2016 00:08:34 -0800 Subject: update installation - update command line output to reflect changes to scaffold - fix inconsistent heading levels - add spacing to separate sections - minor grammar and syntax --- docs/tutorials/wiki2/installation.rst | 222 +++++++++++++++++----------------- 1 file changed, 113 insertions(+), 109 deletions(-) diff --git a/docs/tutorials/wiki2/installation.rst b/docs/tutorials/wiki2/installation.rst index eb6cf50e0..891305bf5 100644 --- a/docs/tutorials/wiki2/installation.rst +++ b/docs/tutorials/wiki2/installation.rst @@ -3,7 +3,7 @@ Installation ============ Before you begin -================ +---------------- This tutorial assumes that you have already followed the steps in :ref:`installing_chapter`, except **do not create a virtualenv or install @@ -13,6 +13,7 @@ Pyramid**. Thereby you will satisfy the following requirements. * :term:`setuptools` or :term:`distribute` is installed * :term:`virtualenv` is installed + Create directory to contain the project --------------------------------------- @@ -32,6 +33,7 @@ On Windows c:\> mkdir pyramidtut + Create and use a virtual Python environment ------------------------------------------- @@ -54,7 +56,7 @@ On Windows c:\> set VENV=c:\pyramidtut -Versions of Python use different paths, so you will need to adjust the +Each version of Python uses different paths, so you will need to adjust the path to the command for your Python version. Python 2.7: @@ -87,22 +89,23 @@ On Windows c:\> %VENV%\Scripts\easy_install pyramid + Install SQLite3 and its development packages -------------------------------------------- -If you used a package manager to install your Python or if you compiled -your Python from source, then you must install SQLite3 and its -development packages. If you downloaded your Python as an installer -from https://www.python.org, then you already have it installed and can skip -this step. +If you used a package manager to install your Python or if you compiled your +Python from source, then you must install SQLite3 and its development packages. +If you downloaded your Python as an installer from https://www.python.org, then +you already have it installed and can skip this step. -If you need to install the SQLite3 packages, then, for example, using -the Debian system and ``apt-get``, the command would be the following: +If you need to install the SQLite3 packages, then, for example, using the +Debian system and ``apt-get``, the command would be the following: .. code-block:: bash $ sudo apt-get install libsqlite3-dev + Change directory to your virtual Python environment --------------------------------------------------- @@ -127,7 +130,7 @@ On Windows .. _sql_making_a_project: Making a project -================ +---------------- Your next step is to create a project. For this tutorial we will use the :term:`scaffold` named ``alchemy`` which generates an application @@ -146,30 +149,29 @@ files. For example, ``pcreate`` creates the ``initialize_tutorial_db`` in the The below instructions assume your current working directory is "pyramidtut". On UNIX -------- +^^^^^^^ .. code-block:: bash $ $VENV/bin/pcreate -s alchemy tutorial On Windows ----------- +^^^^^^^^^^ .. code-block:: ps1con c:\pyramidtut> %VENV%\Scripts\pcreate -s alchemy tutorial -.. note:: If you are using Windows, the ``alchemy`` - scaffold may not deal gracefully with installation into a - location that contains spaces in the path. If you experience - startup problems, try putting both the virtualenv and the project - into directories that do not contain spaces in their paths. +.. note:: If you are using Windows, the ``alchemy`` scaffold may not deal + gracefully with installation into a location that contains spaces in the + path. If you experience startup problems, try putting both the virtualenv + and the project into directories that do not contain spaces in their paths. .. _installing_project_in_dev_mode: Installing the project in development mode -========================================== +------------------------------------------ In order to do development on the project easily, you must "register" the project as a development egg in your workspace using the ``setup.py develop`` @@ -178,7 +180,7 @@ you created in :ref:`sql_making_a_project`, and run the ``setup.py develop`` command using the virtualenv Python interpreter. On UNIX -------- +^^^^^^^ .. code-block:: bash @@ -186,36 +188,35 @@ On UNIX $ $VENV/bin/python setup.py develop On Windows ----------- +^^^^^^^^^^ .. code-block:: ps1con c:\pyramidtut> cd tutorial c:\pyramidtut\tutorial> %VENV%\Scripts\python setup.py develop -The console will show ``setup.py`` checking for packages and installing -missing packages. Success executing this command will show a line like -the following:: +The console will show ``setup.py`` checking for packages and installing missing +packages. Success executing this command will show a line like the following:: Finished processing dependencies for tutorial==0.0 .. _sql_running_tests: Run the tests -============= +------------- -After you've installed the project in development mode, you may run -the tests for the project. +After you've installed the project in development mode, you may run the tests +for the project. On UNIX -------- +^^^^^^^ .. code-block:: bash $ $VENV/bin/python setup.py test -q On Windows ----------- +^^^^^^^^^^ .. code-block:: ps1con @@ -223,50 +224,49 @@ On Windows For a successful test run, you should see output that ends like this:: - . - ---------------------------------------------------------------------- - Ran 1 test in 0.094s + .. + ---------------------------------------------------------------------- + Ran 2 tests in 0.053s - OK + OK Expose test coverage information -================================ +-------------------------------- -You can run the ``nosetests`` command to see test coverage -information. This runs the tests in the same way that ``setup.py -test`` does but provides additional "coverage" information, exposing -which lines of your project are "covered" (or not covered) by the -tests. +You can run the ``nosetests`` command to see test coverage information. This +runs the tests in the same way that ``setup.py test`` does, but provides +additional "coverage" information, exposing which lines of your project are +covered by the tests. To get this functionality working, we'll need to install the ``nose`` and ``coverage`` packages into our ``virtualenv``: On UNIX -------- +^^^^^^^ .. code-block:: bash $ $VENV/bin/easy_install nose coverage On Windows ----------- +^^^^^^^^^^ .. code-block:: ps1con c:\pyramidtut\tutorial> %VENV%\Scripts\easy_install nose coverage -Once ``nose`` and ``coverage`` are installed, we can actually run the -coverage tests. +Once ``nose`` and ``coverage`` are installed, we can run the tests with +coverage. On UNIX -------- +^^^^^^^ .. code-block:: bash $ $VENV/bin/nosetests --cover-package=tutorial --cover-erase --with-coverage On Windows ----------- +^^^^^^^^^^ .. code-block:: ps1con @@ -275,34 +275,38 @@ On Windows If successful, you will see output something like this:: - . - Name Stmts Miss Cover Missing - --------------------------------------------------- - tutorial.py 13 9 31% 13-21 - tutorial/models.py 12 0 100% - tutorial/scripts.py 0 0 100% - tutorial/views.py 11 0 100% - --------------------------------------------------- - TOTAL 36 9 75% - ---------------------------------------------------------------------- - Ran 2 tests in 0.643s + .. + Name Stmts Miss Cover Missing + ---------------------------------------------------------- + tutorial.py 8 6 25% 7-12 + tutorial/models.py 22 0 100% + tutorial/models/meta.py 5 0 100% + tutorial/models/mymodel.py 8 0 100% + tutorial/scripts.py 0 0 100% + tutorial/views.py 0 0 100% + tutorial/views/default.py 12 0 100% + ---------------------------------------------------------- + TOTAL 55 6 89% + ---------------------------------------------------------------------- + Ran 2 tests in 0.579s - OK + OK + +Our package doesn't quite have 100% test coverage. -Looks like our package doesn't quite have 100% test coverage. .. _initialize_db_wiki2: Initializing the database -========================= +------------------------- -We need to use the ``initialize_tutorial_db`` :term:`console -script` to initialize our database. +We need to use the ``initialize_tutorial_db`` :term:`console script` to +initialize our database. .. note:: - The ``initialize_tutorial_db`` command is not performing a migration but - rather simply creating missing tables and adding some dummy data. If you + The ``initialize_tutorial_db`` command does not perform a migration, but + rather it simply creates missing tables and adds some dummy data. If you already have a database, you should delete it before running ``initialize_tutorial_db`` again. @@ -310,14 +314,14 @@ Type the following command, making sure you are still in the ``tutorial`` directory (the directory with a ``development.ini`` in it): On UNIX -------- +^^^^^^^ .. code-block:: bash $ $VENV/bin/initialize_tutorial_db development.ini On Windows ----------- +^^^^^^^^^^ .. code-block:: ps1con @@ -325,51 +329,51 @@ On Windows The output to your console should be something like this:: - 2015-05-23 16:49:49,609 INFO [sqlalchemy.engine.base.Engine:1192][MainThread] SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1 - 2015-05-23 16:49:49,609 INFO [sqlalchemy.engine.base.Engine:1193][MainThread] () - 2015-05-23 16:49:49,610 INFO [sqlalchemy.engine.base.Engine:1192][MainThread] SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1 - 2015-05-23 16:49:49,610 INFO [sqlalchemy.engine.base.Engine:1193][MainThread] () - 2015-05-23 16:49:49,610 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] PRAGMA table_info("models") - 2015-05-23 16:49:49,610 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] () - 2015-05-23 16:49:49,612 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] - CREATE TABLE models ( - id INTEGER NOT NULL, - name TEXT, - value INTEGER, - PRIMARY KEY (id) - ) - - - 2015-05-23 16:49:49,612 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] () - 2015-05-23 16:49:49,613 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT - 2015-05-23 16:49:49,613 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] CREATE UNIQUE INDEX my_index ON models (name) - 2015-05-23 16:49:49,613 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] () - 2015-05-23 16:49:49,614 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT - 2015-05-23 16:49:49,616 INFO [sqlalchemy.engine.base.Engine:646][MainThread] BEGIN (implicit) - 2015-05-23 16:49:49,617 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] INSERT INTO models (name, value) VALUES (?, ?) - 2015-05-23 16:49:49,617 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ('one', 1) - 2015-05-23 16:49:49,618 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT - -Success! You should now have a ``tutorial.sqlite`` file in your current working -directory. This will be a SQLite database with a single table defined in it + 2016-02-21 23:57:41,793 INFO [sqlalchemy.engine.base.Engine:1192][MainThread] SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1 + 2016-02-21 23:57:41,793 INFO [sqlalchemy.engine.base.Engine:1193][MainThread] () + 2016-02-21 23:57:41,794 INFO [sqlalchemy.engine.base.Engine:1192][MainThread] SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1 + 2016-02-21 23:57:41,794 INFO [sqlalchemy.engine.base.Engine:1193][MainThread] () + 2016-02-21 23:57:41,796 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] PRAGMA table_info("models") + 2016-02-21 23:57:41,796 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] () + 2016-02-21 23:57:41,798 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] + CREATE TABLE models ( + id INTEGER NOT NULL, + name TEXT, + value INTEGER, + CONSTRAINT pk_models PRIMARY KEY (id) + ) + + + 2016-02-21 23:57:41,798 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] () + 2016-02-21 23:57:41,798 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT + 2016-02-21 23:57:41,799 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] CREATE UNIQUE INDEX my_index ON models (name) + 2016-02-21 23:57:41,799 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] () + 2016-02-21 23:57:41,799 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT + 2016-02-21 23:57:41,801 INFO [sqlalchemy.engine.base.Engine:646][MainThread] BEGIN (implicit) + 2016-02-21 23:57:41,802 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] INSERT INTO models (name, value) VALUES (?, ?) + 2016-02-21 23:57:41,802 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ('one', 1) + 2016-02-21 23:57:41,821 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT + +Success! You should now have a ``tutorial.sqlite`` file in your current +working directory. This is an SQLite database with a single table defined in it (``models``). .. _wiki2-start-the-application: Start the application -===================== +--------------------- Start the application. On UNIX -------- +^^^^^^^ .. code-block:: bash $ $VENV/bin/pserve development.ini --reload On Windows ----------- +^^^^^^^^^^ .. code-block:: ps1con @@ -382,14 +386,15 @@ On Windows If successful, you will see something like this on your console:: - Starting subprocess with file monitor - Starting server in PID 8966. - Starting HTTP server on http://0.0.0.0:6543 + Starting subprocess with file monitor + Starting server in PID 82349. + serving on http://127.0.0.1:6543 This means the server is ready to accept requests. + Visit the application in a browser -================================== +---------------------------------- In a browser, visit http://localhost:6543/. You will see the generated application's default page. @@ -399,8 +404,9 @@ page. You can read more about the purpose of the icon at :ref:`debug_toolbar`. It allows you to get information about your application while you develop. + Decisions the ``alchemy`` scaffold has made for you -=================================================== +--------------------------------------------------- Creating a project using the ``alchemy`` scaffold makes the following assumptions: @@ -409,21 +415,19 @@ assumptions: - You are willing to use :term:`URL dispatch` to map URLs to code. -- You want to use zope.sqlalchemy_, pyramid_tm_ and the transaction_ package - to scope sessions to requests. +- You want to use zope.sqlalchemy_, pyramid_tm_ and the transaction_ package to + scope sessions to requests. -- You want to use pyramid_jinja2_ to render your templates. - Different templating engines can be used but we had to choose one to - make the tutorial. See :ref:`available_template_system_bindings` for some - options. +- You want to use pyramid_jinja2_ to render your templates. Different + templating engines can be used, but we had to choose one to make this + tutorial. See :ref:`available_template_system_bindings` for some options. .. note:: :app:`Pyramid` supports any persistent storage mechanism (e.g., object - database or filesystem files). It also supports an additional - mechanism to map URLs to code (:term:`traversal`). However, for the - purposes of this tutorial, we'll only be using URL dispatch and - SQLAlchemy. + database or filesystem files). It also supports an additional mechanism to + map URLs to code (:term:`traversal`). However, for the purposes of this + tutorial, we'll only be using URL dispatch and SQLAlchemy. .. _pyramid_jinja2: http://docs.pylonsproject.org/projects/pyramid-jinja2/en/latest/ -- cgit v1.2.3 From 4f87e545ed9596b9be5ca5958e80be55dba28a87 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Mon, 22 Feb 2016 00:58:03 -0800 Subject: update basiclayout - minor grammar and syntax - insert complete mymodel.py code --- docs/tutorials/wiki2/basiclayout.rst | 126 ++++++++++++++++++----------------- 1 file changed, 66 insertions(+), 60 deletions(-) diff --git a/docs/tutorials/wiki2/basiclayout.rst b/docs/tutorials/wiki2/basiclayout.rst index 1ae51eb93..1310e0969 100644 --- a/docs/tutorials/wiki2/basiclayout.rst +++ b/docs/tutorials/wiki2/basiclayout.rst @@ -16,15 +16,14 @@ package. We use ``__init__.py`` both as a marker, indicating the directory in which it's contained is a package, and to contain application configuration code. -Open ``tutorial/__init__.py``. It should already contain the -following: +Open ``tutorial/__init__.py``. It should already contain the following: .. literalinclude:: src/basiclayout/tutorial/__init__.py :linenos: :language: py -Let's go over this piece-by-piece. First, we need some imports to support -later code: +Let's go over this piece-by-piece. First we need some imports to support later +code: .. literalinclude:: src/basiclayout/tutorial/__init__.py :end-before: main @@ -52,10 +51,10 @@ Next in ``main``, construct a :term:`Configurator` object: :lineno-match: :language: py -``settings`` is passed to the Configurator as a keyword argument with the -dictionary values passed as the ``**settings`` argument. This will be a +``settings`` is passed to the ``Configurator`` as a keyword argument with the +dictionary values passed as the ``**settings`` argument. This will be a dictionary of settings parsed from the ``.ini`` file, which contains -deployment-related values such as ``pyramid.reload_templates``, +deployment-related values, such as ``pyramid.reload_templates``, ``sqlalchemy.url``, and so on. Next include :term:`Jinja2` templating bindings so that we can use renderers @@ -66,16 +65,16 @@ with the ``.jinja2`` extension within our project. :lineno-match: :language: py -Next include the the package ``models`` using a dotted Python path. The -exact setup of the models will be covered later. +Next include the the package ``models`` using a dotted Python path. The exact +setup of the models will be covered later. .. literalinclude:: src/basiclayout/tutorial/__init__.py :lines: 9 :lineno-match: :language: py -Next include the ``routes`` module using a dotted Python path. This module -will be explained in the next section. +Next include the ``routes`` module using a dotted Python path. This module will +be explained in the next section. .. literalinclude:: src/basiclayout/tutorial/__init__.py :lines: 10 @@ -84,16 +83,16 @@ will be explained in the next section. .. note:: - Pyramid's :meth:`pyramid.config.Configurator.include` method is the - primary mechanism for extending the configurator and breaking your code - into feature-focused modules. + Pyramid's :meth:`pyramid.config.Configurator.include` method is the primary + mechanism for extending the configurator and breaking your code into + feature-focused modules. ``main`` next calls the ``scan`` method of the configurator (:meth:`pyramid.config.Configurator.scan`), which will recursively scan our -``tutorial`` package, looking for ``@view_config`` (and -other special) decorators. When it finds a ``@view_config`` decorator, a -view configuration will be registered, which will allow one of our -application URLs to be mapped to some code. +``tutorial`` package, looking for ``@view_config`` and other special +decorators. When it finds a ``@view_config`` decorator, a view configuration +will be registered, allowing one of our application URLs to be mapped to some +code. .. literalinclude:: src/basiclayout/tutorial/__init__.py :lines: 11 @@ -113,31 +112,31 @@ Finally ``main`` is finished configuring things, so it uses the Route declarations ------------------ -Open the ``tutorials/routes.py`` file. It should already contain the -following: +Open the ``tutorials/routes.py`` file. It should already contain the following: .. literalinclude:: src/basiclayout/tutorial/routes.py :linenos: :language: py -First, on line 2, call :meth:`pyramid.config.Configurator.add_static_view` -with two arguments: ``static`` (the name), and ``static`` (the path). +On line 2, we call :meth:`pyramid.config.Configurator.add_static_view` with +three arguments: ``static`` (the name), ``static`` (the path), and +``cache_max_age`` (a keyword argument). This registers a static resource view which will match any URL that starts with the prefix ``/static`` (by virtue of the first argument to -``add_static_view``). This will serve up static resources for us from within -the ``static`` directory of our ``tutorial`` package, in this case, via +``add_static_view``). This will serve up static resources for us from within +the ``static`` directory of our ``tutorial`` package, in this case via ``http://localhost:6543/static/`` and below (by virtue of the second argument to ``add_static_view``). With this declaration, we're saying that any URL that starts with ``/static`` should go to the static view; any remainder of its -path (e.g. the ``/foo`` in ``/static/foo``) will be used to compose a path to +path (e.g., the ``/foo`` in ``/static/foo``) will be used to compose a path to a static file resource, such as a CSS file. -Second, on line 3, the module registers a :term:`route configuration` -via the :meth:`pyramid.config.Configurator.add_route` method that will be -used when the URL is ``/``. Since this route has a ``pattern`` equaling -``/``, it is the route that will be matched when the URL ``/`` is visited, -e.g., ``http://localhost:6543/``. +On line 3, the module registers a :term:`route configuration` via the +:meth:`pyramid.config.Configurator.add_route` method that will be used when the +URL is ``/``. Since this route has a ``pattern`` equaling ``/``, it is the +route that will be matched when the URL ``/`` is visited, e.g., +``http://localhost:6543/``. View declarations via the ``views`` package @@ -148,15 +147,15 @@ The main function of a web framework is mapping each URL pattern to code (a corresponding :term:`route`. Our application uses the :meth:`pyramid.view.view_config` decorator to perform this mapping. -Open ``tutorial/views/default.py`` in the ``views`` package. It -should already contain the following: +Open ``tutorial/views/default.py`` in the ``views`` package. It should already +contain the following: .. literalinclude:: src/basiclayout/tutorial/views/default.py :linenos: :language: py The important part here is that the ``@view_config`` decorator associates the -function it decorates (``my_view``) with a :term:`view configuration`, +function it decorates (``my_view``) with a :term:`view configuration`, consisting of: * a ``route_name`` (``home``) @@ -172,12 +171,11 @@ Note that ``my_view()`` accepts a single argument named ``request``. This is the standard call signature for a Pyramid :term:`view callable`. Remember in our ``__init__.py`` when we executed the -:meth:`pyramid.config.Configurator.scan` method ``config.scan()``? The -purpose of calling the scan method was to find and process this -``@view_config`` decorator in order to create a view configuration within our -application. Without being processed by ``scan``, the decorator effectively -does nothing. ``@view_config`` is inert without being detected via a -:term:`scan`. +:meth:`pyramid.config.Configurator.scan` method ``config.scan()``? The purpose +of calling the scan method was to find and process this ``@view_config`` +decorator in order to create a view configuration within our 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 @@ -189,12 +187,12 @@ to inform the user about possible actions to take to solve the problem. Content models with the ``models`` package ------------------------------------------ -In a SQLAlchemy-based application, a *model* object is an object composed by +In an SQLAlchemy-based application, a *model* object is an object composed by querying the SQL database. The ``models`` package is where the ``alchemy`` scaffold put the classes that implement our models. -First, open ``tutorial/models/meta.py``, which should already contain -the following: +First, open ``tutorial/models/meta.py``, which should already contain the +following: .. literalinclude:: src/basiclayout/tutorial/models/meta.py :linenos: @@ -213,10 +211,10 @@ Next we create a ``metadata`` object from the class :class:`sqlalchemy.schema.MetaData`, using ``NAMING_CONVENTION`` as the value for the ``naming_convention`` argument. -A ``MetaData`` object represents the table and other schema definitions for -a single database. We also need to create a declarative ``Base`` object to use -as a base class for our models. Our models will inherit from this ``Base``, -which will attach the tables to the ``metadata`` we created, and define our +A ``MetaData`` object represents the table and other schema definitions for a +single database. We also need to create a declarative ``Base`` object to use as +a base class for our models. Our models will inherit from this ``Base``, which +will attach the tables to the ``metadata`` we created, and define our application's database schema. .. literalinclude:: src/basiclayout/tutorial/models/meta.py @@ -225,17 +223,25 @@ application's database schema. :linenos: :language: py -We've defined the ``models`` as a packge to make it straightforward to -define models separately in different modules. To give a simple example of a -model class, we define one named ``MyModel`` in a ``mymodel.py``: +Next open ``tutorial/models/mymodel.py``, which should already contain the +following: + +.. literalinclude:: src/basiclayout/tutorial/models/mymodel.py + :linenos: + :language: py + +Notice we've defined the ``models`` as a package to make it straightforward for +defining models in separate modules. To give a simple example of a model class, +we have defined one named ``MyModel`` in ``mymodel.py``: .. literalinclude:: src/basiclayout/tutorial/models/mymodel.py :pyobject: MyModel + :lineno-match: :linenos: :language: py Our example model does not require an ``__init__`` method because SQLAlchemy -supplies for us a default constructor if one is not already present, which +supplies for us a default constructor, if one is not already present, which accepts keyword arguments of the same name as that of the mapped attributes. .. note:: Example usage of MyModel: @@ -259,14 +265,14 @@ Our ``models/__init__.py`` module defines the primary API we will use for configuring the database connections within our application, and it contains several functions we will cover below. -As we mentioned above, the purpose of the ``models.meta.metadata`` object is -to describe the schema of the database. This is done by defining models that -inherit from the ``Base`` attached to that ``metadata`` object. In Python, code -is only executed if it is imported, and so to attach the ``models`` table -defined in ``mymodel.py`` to the ``metadata``, we must import it. If we skip -this step, then later, when we run +As we mentioned above, the purpose of the ``models.meta.metadata`` object is to +describe the schema of the database. This is done by defining models that +inherit from the ``Base`` object attached to that ``metadata`` object. In +Python, code is only executed if it is imported, and so to attach the +``models`` table defined in ``mymodel.py`` to the ``metadata``, we must import +it. If we skip this step, then later, when we run :meth:`sqlalchemy.schema.MetaData.create_all`, the table will not be created -because the ``metadata`` does not know about it! +because the ``metadata`` object does not know about it! Another important reason to import all of the models is that, when defining relationships between models, they must all exist in order for SQLAlchemy to @@ -313,9 +319,9 @@ Finally, we define an ``includeme`` function, which is a hook for use with :meth:`pyramid.config.Configurator.include` to activate code in a Pyramid application add-on. It is the code that is executed above when we ran ``config.include('.models')`` in our application's ``main`` function. This -function will take the settings from the application, create an engine, -and define a ``request.dbsession`` property, which we can use to do work -on behalf of an incoming request to our application. +function will take the settings from the application, create an engine, and +define a ``request.dbsession`` property, which we can use to do work on behalf +of an incoming request to our application. .. literalinclude:: src/basiclayout/tutorial/models/__init__.py :pyobject: includeme -- cgit v1.2.3 From 19016b0e5ab6c66a4b5eea01ab7a53e34145fbe6 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 22 Feb 2016 23:48:55 -0600 Subject: move p.response.temporary_response context manager to p.util.hide_attrs --- pyramid/renderers.py | 23 +++-------------- pyramid/tests/test_renderers.py | 42 ------------------------------ pyramid/tests/test_util.py | 57 +++++++++++++++++++++++++++++++++++++++++ pyramid/util.py | 20 +++++++++++++++ pyramid/view.py | 50 +++++++++++++++++++++--------------- 5 files changed, 110 insertions(+), 82 deletions(-) diff --git a/pyramid/renderers.py b/pyramid/renderers.py index 456b16c82..bcbcbb0aa 100644 --- a/pyramid/renderers.py +++ b/pyramid/renderers.py @@ -1,4 +1,3 @@ -import contextlib import json import os import re @@ -30,6 +29,7 @@ from pyramid.path import caller_package from pyramid.response import _get_response_factory from pyramid.threadlocal import get_current_registry +from pyramid.util import hide_attrs # API @@ -77,7 +77,7 @@ def render(renderer_name, value, request=None, package=None): helper = RendererHelper(name=renderer_name, package=package, registry=registry) - with temporary_response(request): + with hide_attrs(request, 'response'): result = helper.render(value, None, request=request) return result @@ -138,30 +138,13 @@ def render_to_response(renderer_name, helper = RendererHelper(name=renderer_name, package=package, registry=registry) - with temporary_response(request): + with hide_attrs(request, 'response'): if response is not None: request.response = response result = helper.render_to_response(value, None, request=request) return result -_marker = object() - -@contextlib.contextmanager -def temporary_response(request): - """ - Temporarily delete request.response and restore it afterward. - """ - attrs = request.__dict__ if request is not None else {} - saved_response = attrs.pop('response', _marker) - try: - yield - finally: - if saved_response is not _marker: - attrs['response'] = saved_response - elif 'response' in attrs: - del attrs['response'] - def get_renderer(renderer_name, package=None): """ Return the renderer object for the renderer ``renderer_name``. diff --git a/pyramid/tests/test_renderers.py b/pyramid/tests/test_renderers.py index 2458ea830..65bfa5582 100644 --- a/pyramid/tests/test_renderers.py +++ b/pyramid/tests/test_renderers.py @@ -592,48 +592,6 @@ class Test_render_to_response(unittest.TestCase): self.assertEqual(result.body, b'{"a": 1}') self.assertFalse('response' in request.__dict__) -class Test_temporary_response(unittest.TestCase): - def _callFUT(self, request): - from pyramid.renderers import temporary_response - return temporary_response(request) - - def test_restores_response(self): - request = testing.DummyRequest() - orig_response = request.response - with self._callFUT(request): - request.response = object() - self.assertEqual(request.response, orig_response) - - def test_restores_response_on_exception(self): - request = testing.DummyRequest() - orig_response = request.response - try: - with self._callFUT(request): - request.response = object() - raise RuntimeError() - except RuntimeError: - self.assertEqual(request.response, orig_response) - else: # pragma: no cover - self.fail("RuntimeError not raised") - - def test_restores_response_to_none(self): - request = testing.DummyRequest(response=None) - with self._callFUT(request): - request.response = object() - self.assertEqual(request.response, None) - - def test_deletes_response(self): - request = testing.DummyRequest() - with self._callFUT(request): - request.response = object() - self.assertTrue('response' not in request.__dict__) - - def test_does_not_delete_response_if_no_response_to_delete(self): - request = testing.DummyRequest() - with self._callFUT(request): - pass - self.assertTrue('response' not in request.__dict__) - class Test_get_renderer(unittest.TestCase): def setUp(self): self.config = testing.setUp() diff --git a/pyramid/tests/test_util.py b/pyramid/tests/test_util.py index 0be99e949..c606a4b6b 100644 --- a/pyramid/tests/test_util.py +++ b/pyramid/tests/test_util.py @@ -794,6 +794,63 @@ class TestCallableName(unittest.TestCase): self.assertRaises(ConfigurationError, get_bad_name) +class Test_hide_attrs(unittest.TestCase): + def _callFUT(self, obj, *attrs): + from pyramid.util import hide_attrs + return hide_attrs(obj, *attrs) + + def _makeDummy(self): + from pyramid.decorator import reify + class Dummy(object): + x = 1 + + @reify + def foo(self): + return self.x + return Dummy() + + def test_restores_attrs(self): + obj = self._makeDummy() + obj.bar = 'asdf' + orig_foo = obj.foo + with self._callFUT(obj, 'foo', 'bar'): + obj.foo = object() + obj.bar = 'nope' + self.assertEqual(obj.foo, orig_foo) + self.assertEqual(obj.bar, 'asdf') + + def test_restores_attrs_on_exception(self): + obj = self._makeDummy() + orig_foo = obj.foo + try: + with self._callFUT(obj, 'foo'): + obj.foo = object() + raise RuntimeError() + except RuntimeError: + self.assertEqual(obj.foo, orig_foo) + else: # pragma: no cover + self.fail("RuntimeError not raised") + + def test_restores_attrs_to_none(self): + obj = self._makeDummy() + obj.foo = None + with self._callFUT(obj, 'foo'): + obj.foo = object() + self.assertEqual(obj.foo, None) + + def test_deletes_attrs(self): + obj = self._makeDummy() + with self._callFUT(obj, 'foo'): + obj.foo = object() + self.assertTrue('foo' not in obj.__dict__) + + def test_does_not_delete_attr_if_no_attr_to_delete(self): + obj = self._makeDummy() + with self._callFUT(obj, 'foo'): + pass + self.assertTrue('foo' not in obj.__dict__) + + def dummyfunc(): pass diff --git a/pyramid/util.py b/pyramid/util.py index 0a73cedaf..e1113e0ec 100644 --- a/pyramid/util.py +++ b/pyramid/util.py @@ -1,3 +1,4 @@ +import contextlib import functools try: # py2.7.7+ and py3.3+ have native comparison support @@ -591,3 +592,22 @@ def get_callable_name(name): 'used on __name__ of the method' ) raise ConfigurationError(msg % name) + +@contextlib.contextmanager +def hide_attrs(obj, *attrs): + """ + Temporarily delete object attrs and restore afterward. + """ + obj_vals = obj.__dict__ if obj is not None else {} + saved_vals = {} + for name in attrs: + saved_vals[name] = obj_vals.pop(name, _marker) + try: + yield + finally: + for name in attrs: + saved_val = saved_vals[name] + if saved_val is not _marker: + obj_vals[name] = saved_val + elif name in obj_vals: + del obj_vals[name] diff --git a/pyramid/view.py b/pyramid/view.py index 1f20622dc..2966b0a50 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -25,6 +25,7 @@ from pyramid.httpexceptions import ( ) from pyramid.threadlocal import get_current_registry +from pyramid.util import hide_attrs _marker = object() @@ -600,23 +601,32 @@ class ViewMethodsMixin(object): attrs = request.__dict__ context_iface = providedBy(exc_info[0]) view_name = attrs.get('view_name', '') - # probably need something like "with temporarily_munged_request(req)" - # here, which adds exception and exc_info as request attrs, and - # removes response object temporarily (as per the excview tween) - attrs['exception'] = exc_info[0] - attrs['exc_info'] = exc_info - # we use .get instead of .__getitem__ below due to - # https://github.com/Pylons/pyramid/issues/700 - request_iface = attrs.get('request_iface', IRequest) - response = _call_view( - registry, - request, - exc_info[0], - context_iface, - view_name, - view_types=None, - view_classifier=IExceptionViewClassifier, - secure=secure, - request_iface=request_iface.combined, - ) - return response + + # WARNING: do not assign the result of sys.exc_info() to a local + # var here, doing so will cause a leak. We used to actually + # explicitly delete both "exception" and "exc_info" from ``attrs`` + # in a ``finally:`` clause below, but now we do not because these + # attributes are useful to upstream tweens. This actually still + # apparently causes a reference cycle, but it is broken + # successfully by the garbage collector (see + # https://github.com/Pylons/pyramid/issues/1223). + + # clear old generated request.response, if any; it may + # have been mutated by the view, and its state is not + # sane (e.g. caching headers) + with hide_attrs(request, 'exception', 'exc_info', 'response'): + # we use .get instead of .__getitem__ below due to + # https://github.com/Pylons/pyramid/issues/700 + request_iface = attrs.get('request_iface', IRequest) + response = _call_view( + registry, + request, + exc_info[0], + context_iface, + view_name, + view_types=None, + view_classifier=IExceptionViewClassifier, + secure=secure, + request_iface=request_iface.combined, + ) + return response -- cgit v1.2.3 From 7f1174bc5aafff4050ba0863629401c7641588b2 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 23 Feb 2016 00:07:04 -0600 Subject: add methods to DummyRequest --- pyramid/testing.py | 2 ++ pyramid/view.py | 9 --------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/pyramid/testing.py b/pyramid/testing.py index 14432b01f..3cb5d17b9 100644 --- a/pyramid/testing.py +++ b/pyramid/testing.py @@ -41,6 +41,7 @@ from pyramid.i18n import LocalizerRequestMixin from pyramid.request import CallbackMethodsMixin from pyramid.url import URLMethodsMixin from pyramid.util import InstancePropertyMixin +from pyramid.view import ViewMethodsMixin _marker = object() @@ -293,6 +294,7 @@ class DummyRequest( LocalizerRequestMixin, AuthenticationAPIMixin, AuthorizationAPIMixin, + ViewMethodsMixin, ): """ A DummyRequest object (incompletely) imitates a :term:`request` object. diff --git a/pyramid/view.py b/pyramid/view.py index 2966b0a50..822fb29d6 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -602,15 +602,6 @@ class ViewMethodsMixin(object): context_iface = providedBy(exc_info[0]) view_name = attrs.get('view_name', '') - # WARNING: do not assign the result of sys.exc_info() to a local - # var here, doing so will cause a leak. We used to actually - # explicitly delete both "exception" and "exc_info" from ``attrs`` - # in a ``finally:`` clause below, but now we do not because these - # attributes are useful to upstream tweens. This actually still - # apparently causes a reference cycle, but it is broken - # successfully by the garbage collector (see - # https://github.com/Pylons/pyramid/issues/1223). - # clear old generated request.response, if any; it may # have been mutated by the view, and its state is not # sane (e.g. caching headers) -- cgit v1.2.3 From 15fb09b473e55f1edf60466f9fa499a5c8fbb93b Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Tue, 23 Feb 2016 02:10:50 -0800 Subject: update definingmodels (WIP) - minor grammar and syntax - define hashing and its purpose --- docs/tutorials/wiki2/definingmodels.rst | 54 ++++++++++++++++++--------------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/docs/tutorials/wiki2/definingmodels.rst b/docs/tutorials/wiki2/definingmodels.rst index 41f36fa26..b0781dfe4 100644 --- a/docs/tutorials/wiki2/definingmodels.rst +++ b/docs/tutorials/wiki2/definingmodels.rst @@ -20,10 +20,10 @@ Declaring dependencies in our ``setup.py`` file The models code in our application will depend on a package which is not a dependency of the original "tutorial" application. The original "tutorial" -application was generated by the ``pcreate`` command; it doesn't know -about our custom application requirements. +application was generated by the ``pcreate`` command; it doesn't know about our +custom application requirements. -We need to add a dependency on the ``bcrypt`` package to our ``tutorial`` +We need to add a dependency, the ``bcrypt`` package, to our ``tutorial`` package's ``setup.py`` file by assigning this dependency to the ``requires`` parameter in the ``setup()`` function. @@ -56,7 +56,7 @@ On UNIX: On Windows: -.. code-block:: text +.. code-block:: ps1con c:\pyramidtut> cd tutorial c:\pyramidtut\tutorial> %VENV%\Scripts\python setup.py develop @@ -70,8 +70,8 @@ like this:: Remove ``mymodel.py`` --------------------- -The first thing we'll do is delete the file ``tutorial/models/mymodel.py``. -The ``MyModel`` class is only a sample and we're not going to use it. +Let's delete the file ``tutorial/models/mymodel.py``. The ``MyModel`` class is +only a sample and we're not going to use it. Add ``user.py`` @@ -85,26 +85,32 @@ Create a new file ``tutorial/models/user.py`` with the following contents: This is a very basic model for a user who can authenticate with our wiki. -We discussed briefly in the previous chapter that our models will inherit -from a SQLAlchemy :func:`sqlalchemy.ext.declarative.declarative_base`. This -will attach the model to our schema. +We discussed briefly in the previous chapter that our models will inherit from +an SQLAlchemy :func:`sqlalchemy.ext.declarative.declarative_base`. This will +attach the model to our schema. As you can see, our ``User`` class has a class-level attribute -``__tablename__`` which equals the string ``users``. Our ``User`` class -will also have class-level attributes named ``id``, ``name``, -``password_hash`` and ``role`` (all instances of -:class:`sqlalchemy.schema.Column`). These will map to columns in the ``users`` -table. The ``id`` attribute will be the primary key in the table. The ``name`` -attribute will be a text column, each value of which needs to be unique within -the column. The ``password_hash`` is a nullable text attribute that will -contain a securely hashed password [1]_. Finally, the ``role`` text attribute -will hold the role of the user. - -There are two helper methods that will help us later when using the -user objects. The first is ``set_password`` which will take a raw password -and transform it using bcrypt_ into an irreversible representation. The -``check_password`` method will allow us to compare input passwords to -see if they resolve to the same hash signifying a match. +``__tablename__`` which equals the string ``users``. Our ``User`` class will +also have class-level attributes named ``id``, ``name``, ``password_hash``, +and ``role`` (all instances of :class:`sqlalchemy.schema.Column`). These will +map to columns in the ``users`` table. The ``id`` attribute will be the primary +key in the table. The ``name`` attribute will be a text column, each value of +which needs to be unique within the column. The ``password_hash`` is a nullable +text attribute that will contain a securely hashed password [1]_. Finally, the +``role`` text attribute will hold the role of the user. + +There are two helper methods that will help us later when using the user +objects. The first is ``set_password`` which will take a raw password and +transform it using bcrypt_ into an irreversible representation, a process known +as "hashing". The second method, ``check_password``, will allow us to compare +the hashed value of the submitted password against the hashed value of the +password stored in the user's record in the database. If the two hashed values +match, then the submitted password is valid, and we can authenticate the user. + +We hash passwords so that it is impossible to decrypt them and use them to +authenticate in the application. If we stored passwords foolishly in clear +text, then anyone with access to the database could retrieve any password to +authenticate as any user. Add ``page.py`` -- cgit v1.2.3 From 48129670005377c1d0b8e265c786525cc316c5e2 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Tue, 23 Feb 2016 13:47:58 -0500 Subject: Use correct attribute name in the ZODB Wiki tutorial view template. --- docs/tutorials/wiki/src/views/tutorial/templates/view.pt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorials/wiki/src/views/tutorial/templates/view.pt b/docs/tutorials/wiki/src/views/tutorial/templates/view.pt index e7b0dc23e..93580658b 100644 --- a/docs/tutorials/wiki/src/views/tutorial/templates/view.pt +++ b/docs/tutorials/wiki/src/views/tutorial/templates/view.pt @@ -8,7 +8,7 @@ - ${page.name} - Pyramid tutorial wiki (based on + <title>${page.__name__} - Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki) -- cgit v1.2.3 From ec82bb52601b9f923207a9d42c65e05819cea335 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Tue, 23 Feb 2016 15:04:33 -0500 Subject: Fix view name order, windows for 'Return logged_in to renderer. --- docs/tutorials/wiki/authorization.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorials/wiki/authorization.rst b/docs/tutorials/wiki/authorization.rst index b0a8c155d..c6f551b42 100644 --- a/docs/tutorials/wiki/authorization.rst +++ b/docs/tutorials/wiki/authorization.rst @@ -248,7 +248,7 @@ Return a ``logged_in`` flag to the renderer ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Open ``tutorial/tutorial/views.py`` again. Add a ``logged_in`` parameter to -the return value of ``view_page()``, ``edit_page()``, and ``add_page()`` as +the return value of ``view_page()``, ``add_page()``, and ``edit_page()`` as follows: .. literalinclude:: src/authorization/tutorial/views.py @@ -262,7 +262,7 @@ follows: :language: python .. literalinclude:: src/authorization/tutorial/views.py - :lines: 75-77 + :lines: 78-80 :emphasize-lines: 2-3 :language: python -- cgit v1.2.3 From 2dc061b569fcfc8d370efb358576a71d233f514f Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Wed, 24 Feb 2016 01:10:22 -0800 Subject: update definingmodels (done) - minor grammar and syntax --- docs/tutorials/wiki2/definingmodels.rst | 45 ++++++++++++++++----------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/docs/tutorials/wiki2/definingmodels.rst b/docs/tutorials/wiki2/definingmodels.rst index b0781dfe4..14099582c 100644 --- a/docs/tutorials/wiki2/definingmodels.rst +++ b/docs/tutorials/wiki2/definingmodels.rst @@ -123,16 +123,16 @@ Create a new file ``tutorial/models/page.py`` with the following contents: :language: py As you can see, our ``Page`` class is very similar to the ``User`` defined -above except with attributes focused on storing information about a wiki -page including ``id``, ``name``, and ``data``. The only new construct -introduced here is the ``creator_id`` column which is a foreign key -referencing the ``users`` table. Foreign keys are very useful at the -schema-level but since we want to relate ``User`` objects with ``Page`` -objects we also define a the ``creator`` attribute which is an ORM-level -mapping between the two tables. SQLAlchemy will automatically populate this -value using the foreign key referencing the user. Since the foreign key -has ``nullable=False`` we are guaranteed that an instance of ``page`` will -have a corresponding ``page.creator`` which will be a ``User`` instance. +above, except with attributes focused on storing information about a wiki page, +including ``id``, ``name``, and ``data``. The only new construct introduced +here is the ``creator_id`` column, which is a foreign key referencing the +``users`` table. Foreign keys are very useful at the schema-level, but since we +want to relate ``User`` objects with ``Page`` objects, we also define a +``creator`` attribute as an ORM-level mapping between the two tables. +SQLAlchemy will automatically populate this value using the foreign key +referencing the user. Since the foreign key has ``nullable=False``, we are +guaranteed that an instance of ``page`` will have a corresponding +``page.creator``, which will be a ``User`` instance. Edit ``models/__init__.py`` @@ -149,8 +149,7 @@ the following: :language: py :emphasize-lines: 8,9 -Here we need to align our imports with the names of the models ``User``, -and ``Page``. +Here we align our imports with the names of the models, ``User`` and ``Page``. Edit ``scripts/initializedb.py`` @@ -163,12 +162,12 @@ command, as we did in the installation step of this tutorial [2]_. Since we've changed our model, we need to make changes to our ``initializedb.py`` script. In particular, we'll replace our import of -``MyModel`` with those of ``User`` and ``Page`` and we'll change the very end -of the script to create two ``User`` objects (``basic`` and ``editor``) and a -``Page`` rather than a ``MyModel`` and add them to our ``dbsession``. +``MyModel`` with those of ``User`` and ``Page``. We'll also change the very end +of the script to create two ``User`` objects (``basic`` and ``editor``) as well +as a ``Page``, rather than a ``MyModel``, and add them to our ``dbsession``. -Open ``tutorial/scripts/initializedb.py`` and edit it to look like -the following: +Open ``tutorial/scripts/initializedb.py`` and edit it to look like the +following: .. literalinclude:: src/models/tutorial/scripts/initializedb.py :linenos: @@ -181,9 +180,9 @@ Only the highlighted lines need to be changed. Installing the project and re-initializing the database ------------------------------------------------------- -Because our model has changed, in order to reinitialize the database, we need -to rerun the ``initialize_tutorial_db`` command to pick up the changes you've -made to both the models.py file and to the initializedb.py file. See +Because our model has changed, and in order to reinitialize the database, we +need to rerun the ``initialize_tutorial_db`` command to pick up the changes +we've made to both the models.py file and to the initializedb.py file. See :ref:`initialize_db_wiki2` for instructions. Success will look something like this:: @@ -237,9 +236,9 @@ View the application in a browser We can't. At this point, our system is in a "non-runnable" state; we'll need to change view-related files in the next chapter to be able to start the -application successfully. If you try to start the application (See -:ref:`wiki2-start-the-application`), you'll wind -up with a Python traceback on your console that ends with this exception: +application successfully. If you try to start the application (see +:ref:`wiki2-start-the-application`), you'll wind up with a Python traceback on +your console that ends with this exception: .. code-block:: text -- cgit v1.2.3 From c956c9bf07607dbd66f092a11b8cbd1d359829df Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Fri, 26 Feb 2016 02:02:59 -0800 Subject: update definingviews (done) - minor grammar and syntax --- docs/tutorials/wiki2/definingviews.rst | 249 +++++++++++++++++---------------- 1 file changed, 130 insertions(+), 119 deletions(-) diff --git a/docs/tutorials/wiki2/definingviews.rst b/docs/tutorials/wiki2/definingviews.rst index 184f9e1fa..e48980dc8 100644 --- a/docs/tutorials/wiki2/definingviews.rst +++ b/docs/tutorials/wiki2/definingviews.rst @@ -3,10 +3,10 @@ Defining Views ============== A :term:`view callable` in a :app:`Pyramid` application is typically a simple -Python function that accepts a single parameter named :term:`request`. A -view callable is assumed to return a :term:`response` object. +Python function that accepts a single parameter named :term:`request`. A view +callable is assumed to return a :term:`response` object. -The request object has a dictionary as an attribute named ``matchdict``. A +The request object has a dictionary as an attribute named ``matchdict``. A ``matchdict`` maps the placeholders in the matching URL ``pattern`` to the substrings of the path in the :term:`request` URL. For instance, if a call to :meth:`pyramid.config.Configurator.add_route` has the pattern ``/{one}/{two}``, @@ -14,10 +14,11 @@ and a user visits ``http://example.com/foo/bar``, our pattern would be matched against ``/foo/bar`` and the ``matchdict`` would look like ``{'one':'foo', 'two':'bar'}``. + Adding the ``docutils`` dependency ================================== -Remember in the previous chapter we added a new dependency on the ``bcrypt`` +Remember in the previous chapter we added a new dependency of the ``bcrypt`` package. Again, the view code in our application will depend on a package which is not a dependency of the original "tutorial" application. @@ -35,7 +36,8 @@ Open ``tutorial/setup.py`` and edit it to look like the following: Only the highlighted line needs to be added. Again, as we did in the previous chapter, the dependency now needs to be -installed so re-run the ``python setup.py develop`` command. +installed, so re-run the ``python setup.py develop`` command. + Static assets ------------- @@ -46,52 +48,54 @@ were provided at the time we created the project. As an example, the CSS file will be accessed via ``http://localhost:6543/static/theme.css`` by virtue of the call to the -``add_static_view`` directive we've made in the ``routes.py`` file. Any -number and type of static assets can be placed in this directory (or -subdirectories) and are just referred to by URL or by using the convenience -method ``static_url``, e.g., -``request.static_url(':static/foo.css')`` within templates. +``add_static_view`` directive we've made in the ``routes.py`` file. Any number +and type of static assets can be placed in this directory (or subdirectories) +and are just referred to by URL or by using the convenience method +``static_url``, e.g., ``request.static_url(':static/foo.css')`` within +templates. + Adding routes to ``routes.py`` ============================== -This is the URL Dispatch tutorial and so let's start by adding some -URL patterns to our app. Later we'll attach views to handle the URLs. +This is the `URL Dispatch` tutorial, so let's start by adding some URL patterns +to our app. Later we'll attach views to handle the URLs. -The ``routes.py`` file contains -:meth:`pyramid.config.Configurator.add_route` calls which serve to add routes -to our application. First, we’ll get rid of the existing route created by -the template using the name ``'home'``. It’s only an example and isn’t -relevant to our application. +The ``routes.py`` file contains :meth:`pyramid.config.Configurator.add_route` +calls which serve to add routes to our application. First we'll get rid of the +existing route created by the template using the name ``'home'``. It's only an +example and isn't relevant to our application. -We then need to add four calls to ``add_route``. Note that the *ordering* of -these declarations is very important. ``route`` declarations are matched in -the order they're registered. +We then need to add four calls to ``add_route``. Note that the *ordering* of +these declarations is very important. Route declarations are matched in the +order they're registered. -#. Add a declaration which maps the pattern ``/`` (signifying the root URL) - to the route named ``view_wiki``. It maps to our ``view_wiki`` view - callable by virtue of the ``@view_config`` attached to the ``view_wiki`` - view function indicating ``route_name='view_wiki'``. +#. Add a declaration which maps the pattern ``/`` (signifying the root URL) to + the route named ``view_wiki``. In the next step, we will map it to our + ``view_wiki`` view callable by virtue of the ``@view_config`` decorator + attached to the ``view_wiki`` view function, which in turn will be indicated + by ``route_name='view_wiki'``. #. Add a declaration which maps the pattern ``/{pagename}`` to the route named - ``view_page``. This is the regular view for a page. It maps - to our ``view_page`` view callable by virtue of the ``@view_config`` - attached to the ``view_page`` view function indicating - ``route_name='view_page'``. + ``view_page``. This is the regular view for a page. Again, in the next step, + we will map it to our ``view_page`` view callable by virtue of the + ``@view_config`` decorator attached to the ``view_page`` view function, + whin in turn will be indicated by ``route_name='view_page'``. #. Add a declaration which maps the pattern ``/add_page/{pagename}`` to the - route named ``add_page``. This is the add view for a new page. It maps - to our ``add_page`` view callable by virtue of the ``@view_config`` - attached to the ``add_page`` view function indicating - ``route_name='add_page'``. + route named ``add_page``. This is the add view for a new page. We will map + it to our ``add_page`` view callable by virtue of the ``@view_config`` + decorator attached to the ``add_page`` view function, which in turn will be + indicated by ``route_name='add_page'``. #. Add a declaration which maps the pattern ``/{pagename}/edit_page`` to the - route named ``edit_page``. This is the edit view for a page. It maps + route named ``edit_page``. This is the edit view for a page. We will map it to our ``edit_page`` view callable by virtue of the ``@view_config`` - attached to the ``edit_page`` view function indicating - ``route_name='edit_page'``. + decorator attached to the ``edit_page`` view function, which in turn will be + indicated by ``route_name='edit_page'``. -As a result of our edits, the ``routes.py`` file should look something like: +As a result of our edits, the ``routes.py`` file should look like the +following: .. literalinclude:: src/views/tutorial/routes.py :linenos: @@ -103,13 +107,14 @@ The highlighted lines are the ones that need to be added or edited. .. warning:: The order of the routes is important! If you placed - ``/{pagename}/edit_page`` **before** ``/add_page/{pagename}`` then we would - never be able to add pages because the first route would always match - a request to ``/add_page/edit_page`` whereas we want ``/add_page/..`` to - have priority. This isn't a huge problem in this particular app because - wiki pages are always camel case but it's important to be aware of this + ``/{pagename}/edit_page`` *before* ``/add_page/{pagename}``, then we would + never be able to add pages. This is because the first route would always + match a request to ``/add_page/edit_page`` whereas we want ``/add_page/..`` + to have priority. This isn't a huge problem in this particular app because + wiki pages are always camel case, but it's important to be aware of this behavior in your own apps. + Adding view functions in ``views/default.py`` ============================================= @@ -131,23 +136,24 @@ and isn't relevant to our application. We also deleted the ``db_err_msg`` string. Then we added four :term:`view callable` functions to our ``views/default.py`` -module: +module, as mentioned in the previous step: * ``view_wiki()`` - Displays the wiki itself. It will answer on the root URL. * ``view_page()`` - Displays an individual page. -* ``add_page()`` - Allows the user to add a page. * ``edit_page()`` - Allows the user to edit a page. +* ``add_page()`` - Allows the user to add a page. We'll describe each one briefly in the following sections. .. note:: - There is nothing special about the filename ``default.py`` exept that - it is a Python module. A project may have many view callables throughout - its codebase in arbitrarily named modules. - Modules implementing view callables often have ``view`` in their name (or - may live in a Python subpackage of your application package named ``views``, - as in our case), but this is only by convention, not a requirement. + There is nothing special about the filename ``default.py`` exept that it is a + Python module. A project may have many view callables throughout its codebase + in arbitrarily named modules. Modules implementing view callables often have + ``view`` in their name (or may live in a Python subpackage of your + application package named ``views``, as in our case), but this is only by + convention, not a requirement. + The ``view_wiki`` view function ------------------------------- @@ -166,13 +172,14 @@ represents the path to our "FrontPage". The ``view_wiki`` view callable always redirects to the URL of a Page resource named "FrontPage". To do so, it returns an instance of the -:class:`pyramid.httpexceptions.HTTPFound` class (instances of which -implement the :class:`pyramid.interfaces.IResponse` interface, like -:class:`pyramid.response.Response` does). It uses the +:class:`pyramid.httpexceptions.HTTPFound` class (instances of which implement +the :class:`pyramid.interfaces.IResponse` interface, like +:class:`pyramid.response.Response`). It uses the :meth:`pyramid.request.Request.route_url` API to construct a URL to the ``FrontPage`` page (i.e., ``http://localhost:6543/FrontPage``), and uses it as the "location" of the ``HTTPFound`` response, forming an HTTP redirect. + The ``view_page`` view function ------------------------------- @@ -201,7 +208,7 @@ As a result, the ``content`` variable is now a fully formed bit of HTML containing various view and add links for WikiWords based on the content of our current page object. -We then generate an edit URL because it's easier to do here than in the +We then generate an edit URL, because it's easier to do here than in the template, and we return a dictionary with a number of arguments. The fact that ``view_page()`` returns a dictionary (as opposed to a :term:`response` object) is a cue to :app:`Pyramid` that it should try to use a :term:`renderer` @@ -209,22 +216,22 @@ associated with the view configuration to render a response. In our case, the renderer used will be the ``view.jinja2`` template, as indicated in the ``@view_config`` decorator that is applied to ``view_page()``. -If the page does not exist then we need to handle that by raising -:class:`pyramid.httpexceptions.HTTPNotFound`` to trigger our 404 handling +If the page does not exist, then we need to handle that by raising a +:class:`pyramid.httpexceptions.HTTPNotFound` to trigger our 404 handling, defined in ``tutorial/views/notfound.py``. .. note:: - Using ``raise`` versus ``return`` with the http exceptions is an important + Using ``raise`` versus ``return`` with the HTTP exceptions is an important distinction that can commonly mess people up. In ``tutorial/views/notfound.py`` there is an :term:`exception view` - registered for handling the ``HTTPNotFound`` exception. Exception views - are only triggered for raised exceptions. If the ``HTTPNotFound`` is - returned then it has an internal "stock" template that it will use - to render itself as a response. If you aren't seeing your exception - view being executed this is probably the problem! See - :ref:`special_exceptions_in_callables` for more information about - exception views. + registered for handling the ``HTTPNotFound`` exception. Exception views are + only triggered for raised exceptions. If the ``HTTPNotFound`` is returned, + then it has an internal "stock" template that it will use to render itself + as a response. If you aren't seeing your exception view being executed, this + is most likely the problem! See :ref:`special_exceptions_in_callables` for + more information about exception views. + The ``edit_page`` view function ------------------------------- @@ -237,11 +244,11 @@ Here is the code for the ``edit_page`` view function and its decorator: :linenos: :language: python -``edit_page()`` is invoked when a user clicks the "Edit this -Page" button on the view form. It renders an edit form, but it also acts as -the handler for the form it renders. The ``matchdict`` attribute of the -request passed to the ``edit_page`` view will have a ``'pagename'`` key -matching the name of the page the user wants to edit. +``edit_page()`` is invoked when a user clicks the "Edit this Page" button on +the view form. It renders an edit form, but it also acts as the handler for the +form which it renders. The ``matchdict`` attribute of the request passed to the +``edit_page`` view will have a ``'pagename'`` key matching the name of the page +that the user wants to edit. If the view execution *is* a result of a form submission (i.e., the expression ``'form.submitted' in request.params`` is ``True``), the view grabs the @@ -257,12 +264,13 @@ which will be used as the action of the generated form. .. note:: Since our ``request.dbsession`` defined in the previous chapter is - registered with the ``pyramid_tm`` transaction manager any changes we make - to objects managed by the that session will be committed automatically. - In the event that there was an error (even later, in our template code) the + registered with the ``pyramid_tm`` transaction manager, any changes we make + to objects managed by the that session will be committed automatically. In + the event that there was an error (even later, in our template code), the changes would be aborted. This means the view itself does not need to concern itself with commit/rollback logic. + The ``add_page`` view function ------------------------------ @@ -274,27 +282,26 @@ Here is the code for the ``add_page`` view function and its decorator: :linenos: :language: python -``add_page()`` is invoked when a user clicks on a *WikiWord* which -isn't yet represented as a page in the system. The ``add_link`` function -within the ``view_page`` view generates URLs to this view. -``add_page()`` also acts as a handler for the form that is generated -when we want to add a page object. The ``matchdict`` attribute of the -request passed to the ``add_page()`` view will have the values we need -to construct URLs and find model objects. +``add_page()`` is invoked when a user clicks on a *WikiWord* which isn't yet +represented as a page in the system. The ``add_link`` function within the +``view_page`` view generates URLs to this view. ``add_page()`` also acts as a +handler for the form that is generated when we want to add a page object. The +``matchdict`` attribute of the request passed to the ``add_page()`` view will +have the values we need to construct URLs and find model objects. -The ``matchdict`` will have a ``'pagename'`` key that matches the name of -the page we'd like to add. If our add view is invoked via, -e.g., ``http://localhost:6543/add_page/SomeName``, the value for -``'pagename'`` in the ``matchdict`` will be ``'SomeName'``. +The ``matchdict`` will have a ``'pagename'`` key that matches the name of the +page we'd like to add. If our add view is invoked via, for example, +``http://localhost:6543/add_page/SomeName``, the value for ``'pagename'`` in +the ``matchdict`` will be ``'SomeName'``. If the view execution *is* a result of a form submission (i.e., the expression -``'form.submitted' in request.params`` is ``True``), we grab the page body -from the form data, create a Page object with this page body and the name -taken from ``matchdict['pagename']``, and save it into the database using -``request.dbession.add``. Since we have not yet covered authentication we -don't have a logged-in user to add as the page's ``creator``. Until we -get to that point in the tutorial we'll just assume that all pages are created -by the ``editor`` user so we query that object and set it on ``page.creator``. +``'form.submitted' in request.params`` is ``True``), we grab the page body from +the form data, create a Page object with this page body and the name taken from +``matchdict['pagename']``, and save it into the database using +``request.dbession.add``. Since we have not yet covered authentication, we +don't have a logged-in user to add as the page's ``creator``. Until we get to +that point in the tutorial, we'll just assume that all pages are created by the +``editor`` user. Thus we query for that object, and set it on ``page.creator``. Finally, we redirect the client back to the ``view_page`` view for the newly created page. @@ -308,6 +315,7 @@ in order to satisfy the edit form's desire to have *some* page object exposed as ``page``. :app:`Pyramid` will render the template associated with this view to a response. + Adding templates ================ @@ -317,25 +325,28 @@ These templates will live in the ``templates`` directory of our tutorial package. Jinja2 templates must have a ``.jinja2`` extension to be recognized as such. + The ``layout.jinja2`` template ------------------------------ -Replace ``tutorial/templates/layout.jinja2`` with the following content: +Update ``tutorial/templates/layout.jinja2`` with the following content, as +indicated by the emphasized lines: .. literalinclude:: src/views/tutorial/templates/layout.jinja2 :linenos: :emphasize-lines: 11,36 :language: html -Since we're using a templating engine we can factor common boilerplate out of -our page templates into reusable components. One method for doing this -is template inheritance via blocks. +Since we're using a templating engine, we can factor common boilerplate out of +our page templates into reusable components. One method for doing this is +template inheritance via blocks. + +- We have defined two placeholders in the layout template where a child + template can override the content. These blocks are named ``subtitle`` (line + 11) and ``content`` (line 36). +- Please refer to the Jinja2_ documentation for more information about template + inheritance. -- We have defined 2 placeholders in the layout template where a child template - can override the content. These blocks are named ``subtitle`` (line 11) and - ``content`` (line 36). -- Please refer to the Jinja2_ documentation for more information about - template inheritance. The ``view.jinja2`` template ---------------------------- @@ -344,23 +355,22 @@ Create ``tutorial/templates/view.jinja2`` and add the following content: .. literalinclude:: src/views/tutorial/templates/view.jinja2 :linenos: - :emphasize-lines: 1,3,6 :language: html This template is used by ``view_page()`` for displaying a single wiki page. -It includes: -- We begin by extending the ``layout.jinja2`` template defined above - which provides the skeleton of the page (line 1). -- We override the ``subtitle`` block from the base layout to insert the - page name of the page into the page's title (line 3). +- We begin by extending the ``layout.jinja2`` template defined above, which + provides the skeleton of the page (line 1). +- We override the ``subtitle`` block from the base layout, inserting the page + name into the page's title (line 3). - We override the ``content`` block from the base layout to insert our markup - into the body (line 5-18). -- A variable that is replaced with the ``content`` value provided by the view - (line 6). ``content`` contains HTML, so the ``|safe`` filter is used to + into the body (lines 5-18). +- We use a variable that is replaced with the ``content`` value provided by the + view (line 6). ``content`` contains HTML, so the ``|safe`` filter is used to prevent escaping it (e.g., changing ">" to ">"). -- A link that points at the "edit" URL which invokes the ``edit_page`` view for - the page being viewed (line 9). +- We create a link that points at the "edit" URL, which when clicked invokes + the ``edit_page`` view for the requested page (line 9). + The ``edit.jinja2`` template ---------------------------- @@ -369,23 +379,23 @@ Create ``tutorial/templates/edit.jinja2`` and add the following content: .. literalinclude:: src/views/tutorial/templates/edit.jinja2 :linenos: - :emphasize-lines: 3,12,14,17 + :emphasize-lines: 1,3,12,14,17 :language: html -This template serves two use-cases. It is used by ``add_page()`` and +This template serves two use cases. It is used by ``add_page()`` and ``edit_page()`` for adding and editing a wiki page. It displays a page -containing a form that includes: +containing a form and which provides the following: -- Again, we are extending the ``layout.jinja2`` template which provides - the skeleton of the page (line 1). +- Again, we extend the ``layout.jinja2`` template, which provides the skeleton + of the page (line 1). - Override the ``subtitle`` block to affect the ```` tag in the ``head`` of the page (line 3). - A 10-row by 60-column ``textarea`` field named ``body`` that is filled with any existing page data when it is rendered (line 14). - A submit button that has the name ``form.submitted`` (line 17). +- The form POSTs back to the ``save_url`` argument supplied by the view (line + 12). The view will use the ``body`` and ``form.submitted`` values. -The form POSTs back to the ``save_url`` argument supplied by the view (line -12). The view will use the ``body`` and ``form.submitted`` values. The ``404.jinja2`` template --------------------------- @@ -419,9 +429,9 @@ There are several important things to note about this configuration: ``tutorial/views/default.py`` the exception is raised which will trigger the view. -Finally, you may delete the ``tutorial/templates/mytemplate.jinja2`` -template that was provided by the ``alchemy`` scaffold as we have created -our own templates for the wiki. +Finally, we may delete the ``tutorial/templates/mytemplate.jinja2`` template +that was provided by the ``alchemy`` scaffold, as we have created our own +templates for the wiki. .. note:: @@ -431,6 +441,7 @@ our own templates for the wiki. See :ref:`renderer_system_values` for information about other names that are available by default when a template is used as a renderer. + Viewing the application in a browser ==================================== -- cgit v1.2.3 From 492b800ab252521380c465c259a78cc77c934309 Mon Sep 17 00:00:00 2001 From: Steve Piercy <web@stevepiercy.com> Date: Fri, 26 Feb 2016 22:55:48 -0800 Subject: minor grammar --- docs/tutorials/wiki2/design.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorials/wiki2/design.rst b/docs/tutorials/wiki2/design.rst index 929bc7806..a57b318ca 100644 --- a/docs/tutorials/wiki2/design.rst +++ b/docs/tutorials/wiki2/design.rst @@ -139,8 +139,8 @@ in the following table: | | - If authentication | | | | | | succeeds, | | | | | | redirect to the | | | | -| | page that we | | | | -| | came from. | | | | +| | page that from | | | | +| | which we came. | | | | | | | | | | | | - If authentication | | | | | | fails, display | | | | -- cgit v1.2.3 From 79054f01a8e2763cb51c1885c60f427802102ac5 Mon Sep 17 00:00:00 2001 From: Steve Piercy <web@stevepiercy.com> Date: Fri, 26 Feb 2016 22:58:19 -0800 Subject: update authentication (done) - remove highlighting from some code blocks because it didn't make sense and added visual noise and dissonance - minor grammar and syntax --- docs/tutorials/wiki2/authentication.rst | 190 +++++++++++++++++--------------- 1 file changed, 101 insertions(+), 89 deletions(-) diff --git a/docs/tutorials/wiki2/authentication.rst b/docs/tutorials/wiki2/authentication.rst index 1b18e5c55..115f7f689 100644 --- a/docs/tutorials/wiki2/authentication.rst +++ b/docs/tutorials/wiki2/authentication.rst @@ -5,81 +5,82 @@ Adding authentication ===================== :app:`Pyramid` provides facilities for :term:`authentication` and -:term:`authorization`. In this section we'll focus solely on the -authentication APIs to add login/logout functionality to our wiki. +:term:`authorization`. In this section we'll focus solely on the authentication +APIs to add login and logout functionality to our wiki. We will implement authentication with the following steps: -* Add an :term:`authentication policy` and a ``request.user`` computed - property (``security.py``). -* Add routes for /login and /logout (``routes.py``). +* Add an :term:`authentication policy` and a ``request.user`` computed property + (``security.py``). +* Add routes for ``/login`` and ``/logout`` (``routes.py``). * Add login and logout views (``views/auth.py``). * Add a login template (``login.jinja2``). * Add "Login" and "Logout" links to every page based on the user's authenticated state (``layout.jinja2``). * Make the existing views verify user state (``views/default.py``). -* Redirect to /login when a user is denied access to any of the views - that require permission, instead of a default "403 Forbidden" page +* Redirect to ``/login`` when a user is denied access to any of the views that + require permission, instead of a default "403 Forbidden" page (``views/auth.py``). + Authenticating requests ----------------------- -The core of :app:`Pyramid` authentication is a :term:`authentication policy` +The core of :app:`Pyramid` authentication is an :term:`authentication policy` which is used to identify authentication information from a ``request``, -as well as handling the low-level login/logout operations required to -track users across requests (via cookies or headers or whatever else you can +as well as handling the low-level login and logout operations required to +track users across requests (via cookies, headers, or whatever else you can imagine). + Add the authentication policy ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Create a new file ``tutorial/security.py``: +Create a new file ``tutorial/security.py`` with the following content: .. literalinclude:: src/authentication/tutorial/security.py :linenos: - :emphasize-lines: 9,14,21-27 :language: python Here we've defined: -* A new authentication policy named ``MyAuthenticationPolicy`` which is - subclassed from pyramid's - :class:`pyramid.authentication.AuthTktAuthenticationPolicy` which tracks - the :term:`userid` using a signed cookie. -* A ``get_user`` function which can convert the ``unauthenticated_userid`` - from the policy into a ``User`` object from our database. -* The ``get_user`` is registered on the request as ``request.user`` - to be used throughout our application as the authenticated ``User`` object - for the logged-in user. +* A new authentication policy named ``MyAuthenticationPolicy``, which is + subclassed from Pyramid's + :class:`pyramid.authentication.AuthTktAuthenticationPolicy`, which tracks the + :term:`userid` using a signed cookie (lines 7-11). +* A ``get_user`` function, which can convert the ``unauthenticated_userid`` + from the policy into a ``User`` object from our database (lines 13-17). +* The ``get_user`` is registered on the request as ``request.user`` to be used + throughout our application as the authenticated ``User`` object for the + logged-in user (line 27). -The logic in this file is a little bit interesting and so we'll go into -detail about what's happening here: +The logic in this file is a little bit interesting, so we'll go into detail +about what's happening here: First, the default authentication policies all provide a method named ``unauthenticated_userid`` which is responsible for the low-level parsing -of the information in the request (cookies, headers, etc). If a ``userid`` -is found then it is returned from this method. This is named -``unauthenticated_userid`` because at the lowest level it knows the value of -the userid in the cookie but it doesn't know if it's actually a user in our +of the information in the request (cookies, headers, etc.). If a ``userid`` +is found, then it is returned from this method. This is named +``unauthenticated_userid`` because, at the lowest level, it knows the value of +the userid in the cookie, but it doesn't know if it's actually a user in our system (remember, anything the user sends to our app is untrusted). Second, our application should only care about ``authenticated_userid`` and -``request.user`` which have gone through our application-specific process of -validating that the user is logged-in. +``request.user``, which have gone through our application-specific process of +validating that the user is logged in. In order to provide an ``authenticated_userid`` we need a verification step. That can happen anywhere, so we've elected to do it inside of the cached ``request.user`` computed property. This is a convenience that makes ``request.user`` the source of truth in our system. It is either ``None`` or a ``User`` object from our database. This is why the ``get_user`` function -uses the ``unauthenticated_userid`` to check the database +uses the ``unauthenticated_userid`` to check the database. + Configure the app ~~~~~~~~~~~~~~~~~ -Since we've added a new ``tutorial/security.py`` module we need to include it. - +Since we've added a new ``tutorial/security.py`` module, we need to include it. Open the file ``tutorial/__init__.py`` and edit the following lines: .. literalinclude:: src/authentication/tutorial/__init__.py @@ -96,7 +97,7 @@ the file ``development.ini`` and add the highlighted line below: :lineno-match: :language: ini -Finally best-practices tell us to use a different secret for production so +Finally, best practices tell us to use a different secret for production, so open ``production.ini`` and add a different secret: .. literalinclude:: src/authentication/production.ini @@ -105,14 +106,15 @@ open ``production.ini`` and add a different secret: :lineno-match: :language: ini + Add permission checks ~~~~~~~~~~~~~~~~~~~~~ -:app:`Pyramid` has full support for declarative authorization which we'll -cover in the next chapter. However many people looking to get their feet -wet are just interested in authentication with some basic form of -home-grown authorization. We'll show below how to accomplish the simple -security goals of our wiki now that we can track the logged-in state of users. +:app:`Pyramid` has full support for declarative authorization, which we'll +cover in the next chapter. However, many people looking to get their feet wet +are just interested in authentication with some basic form of home-grown +authorization. We'll show below how to accomplish the simple security goals of +our wiki, now that we can track the logged-in state of users. Remember our goals: @@ -128,9 +130,9 @@ Open the file ``tutorial/views/default.py`` and fix the following imports: :emphasize-lines: 2,9 :language: python -Only the highlighted lines need to be changed. +Change the two highlighted lines. -Now edit the ``add_page`` view function: +In the same file, now edit the ``add_page`` view function: .. literalinclude:: src/authentication/tutorial/views/default.py :lines: 62-76 @@ -140,11 +142,11 @@ Now edit the ``add_page`` view function: Only the highlighted lines need to be changed. -If the user is not logged in or is not in the ``basic`` or ``editor`` roles -then we raise ``HTTPForbidden`` which will return a "403 Forbidden" response -to the user. However we hook this later to redirect to the login page. Also, -now that we have ``request.user`` we no longer have to hard-code the creator -as the ``editor`` user so we can finally drop that hack. +If the user either is not logged in or is not in the ``basic`` or ``editor`` +roles, then we raise ``HTTPForbidden``, which will return a "403 Forbidden" +response to the user. However, we will hook this later to redirect to the login +page. Also, now that we have ``request.user``, we no longer have to hard-code +the creator as the ``editor`` user, so we can finally drop that hack. Now edit the ``edit_page`` view function: @@ -156,19 +158,21 @@ Now edit the ``edit_page`` view function: Only the highlighted lines need to be changed. -If the user is not logged in or the user is not the page's creator **and** -not an ``editor`` then we raise ``HTTPForbidden``. +If the user either is not logged in or the user is not the page's creator +*and* not an ``editor``, then we raise ``HTTPForbidden``. These simple checks should protect our views. + Login, logout ------------- -Now that we've got the ability to detect logged-in users, we need to -add the /login and /logout views so that they can actually login! +Now that we've got the ability to detect logged-in users, we need to add the +``/login`` and ``/logout`` views so that they can actually login and logout! -Add routes for /login and /logout -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Add routes for ``/login`` and ``/logout`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Go back to ``tutorial/routes.py`` and add these two routes as highlighted: @@ -183,53 +187,57 @@ Go back to ``tutorial/routes.py`` and add these two routes as highlighted: .. literalinclude:: src/authentication/tutorial/routes.py :lines: 6 + :lineno-match: :language: python This is because ``view_page``'s route definition uses a catch-all - "replacement marker" ``/{pagename}`` (see :ref:`route_pattern_syntax`) + "replacement marker" ``/{pagename}`` (see :ref:`route_pattern_syntax`), which will catch any route that was not already caught by any route registered before it. Hence, for ``login`` and ``logout`` views to have the opportunity of being matched (or "caught"), they must be above ``/{pagename}``. -Add login, logout and forbidden views -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Create a new file ``tutorial/views/auth.py`` where we will add the following -code: +Add login, logout, and forbidden views +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Create a new file ``tutorial/views/auth.py``, and add the following code to it: .. literalinclude:: src/authentication/tutorial/views/auth.py :linenos: :language: python -This code adds 3 new views to application: +This code adds three new views to application: - The ``login`` view renders a login form and processes the post from the login form, checking credentials against our ``users`` table in the database. - The check is done by first finding a ``User`` record in the database and - then using our ``user.check_password`` method to compare the passwords. + The check is done by first finding a ``User`` record in the database, then + using our ``user.check_password`` method to compare the hashed passwords. - If the credentials are valid then we use our authentication policy to - store the user's id in the response using :meth:`pyramid.security.remember`. + If the credentials are valid, then we use our authentication policy to store + the user's id in the response using :meth:`pyramid.security.remember`. - Finally, the user is redirected back to the page they were trying to access - (``next``) or the front page as a fallback. This parameter is used by - our forbidden view as explained below to finish the login workflow. + Finally, the user is redirected back to either the page which they were + trying to access (``next``) or the front page as a fallback. This parameter + is used by our forbidden view, as explained below, to finish the login + workflow. -- The ``logout`` view handles requests to /logout by clearing the credentials - using :meth:`pyramid.security.forget` and then redirecting them to the front - page. +- The ``logout`` view handles requests to ``/logout`` by clearing the + credentials using :meth:`pyramid.security.forget`, then redirecting them to + the front page. - The ``forbidden_view`` is registered using the :class:`pyramid.view.forbidden_view_config` decorator. This is a special - :term:`exception view` which is invoked when a + :term:`exception view`, which is invoked when a :class:`pyramid.httpexceptions.HTTPForbidden` exception is raised. - This view will handle a forbidden error by redirecting the user to /login. - As a convenience it also sets the ``next=`` query string to the current url - (the one that is forbidding access). This way if the user successfully logs - in they will be sent back to the page they had been trying to access. + This view will handle a forbidden error by redirecting the user to + ``/login``. As a convenience, it also sets the ``next=`` query string to the + current URL (the one that is forbidding access). This way, if the user + successfully logs in, they will be sent back to the page which they had been + trying to access. + Add the ``login.jinja2`` template ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -242,8 +250,9 @@ Create ``tutorial/templates/login.jinja2`` with the following content: The above template is referenced in the login view that we just added in ``tutorial/views/auth.py``. -Add a "Login" and "Logout" links -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Add "Login" and "Logout" links +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Open ``tutorial/templates/layout.jinja2`` and add the following code as indicated by the highlighted lines. @@ -255,9 +264,10 @@ indicated by the highlighted lines. :language: html The ``request.user`` will be ``None`` if the user is not authenticated, or a -``tutorial.models.User`` object if the user is authenticated. This -check will make the logout link active only when the user is logged in and -vice versa the login link is only active when the user is logged out. +``tutorial.models.User`` object if the user is authenticated. This check will +make the logout link shown only when the user is logged in, and conversely the +login link is only shown when the user is logged out. + Viewing the application in a browser ------------------------------------ @@ -271,26 +281,28 @@ following URLs, checking that the result is as expected: is executable by any user. - http://localhost:6543/FrontPage invokes the ``view_page`` view of the - ``FrontPage`` page object. There is a "Login" link in the upper right corner. + ``FrontPage`` page object. There is a "Login" link in the upper right corner + while the user is not authenticated, else it is a "Logout" link when the user + is authenticated. - http://localhost:6543/FrontPage/edit_page invokes the edit view for the - FrontPage object. It is executable by only the ``editor`` user. If a - different user (or the anonymous user) invokes it, a login form will be - displayed. Supplying the credentials with the username ``editor``, password + ``FrontPage`` object. It is executable by only the ``editor`` user. If a + different user (or the anonymous user) invokes it, then a login form will be + displayed. Supplying the credentials with the username ``editor``, password ``editor`` will display the edit page form. - http://localhost:6543/add_page/SomePageName invokes the add view for a page. - It is executable by the ``editor`` or ``basic`` user. If a different user - (or the anonymous user) invokes it, a login form will be displayed. Supplying - the credentials with the username ``basic``, password ``basic`` will display - the edit page form. + It is executable by the ``editor`` or ``basic`` user. If a different user (or + the anonymous user) invokes it, then a login form will be displayed. + Supplying the credentials with the username ``basic``, password ``basic``, + will display the edit page form. - http://localhost:6543/SomePageName/edit_page is editable by the ``basic`` - if the page was created by that user in the previous step. If, instead, the - page was created by ``editor`` then the login page should be shown for the - ``basic`` user. + user if the page was created by that user in the previous step. If, instead, + the page was created by the ``editor`` user, then the login page should be + shown for the ``basic`` user. - After logging in (as a result of hitting an edit or add page and submitting - the login form with the ``editor`` credentials), we'll see a Logout link in + the login form with the ``editor`` credentials), we'll see a "Logout" link in the upper right hand corner. When we click it, we're logged out, and redirected back to the front page. -- cgit v1.2.3 From 082d3b2cb9127f8acfd4d081e69c427a37bae91d Mon Sep 17 00:00:00 2001 From: Steve Piercy <web@stevepiercy.com> Date: Sat, 27 Feb 2016 00:59:17 -0800 Subject: wiki2 authentication bug fix and improvement against timing attack - Bytes type does not have encode method. The expected_hash retrieved from the database is a bytes object. - Use hmac.compare_digest instead of == to avoid timing attacks as a recommended security best practice. See https://www.python.org/dev/peps/pep-0466/ https://bugs.python.org/issue21306 and https://codahale.com/a-lesson-in-timing-attacks/ for details. Note, however, this was not backported to py2.6. For a tutorial, I am OK with stating this will not work on Python 2.6 with a clear warning note at the start of the tutorial and on the authentication step. --- docs/tutorials/wiki2/src/authentication/tutorial/models/user.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/models/user.py b/docs/tutorials/wiki2/src/authentication/tutorial/models/user.py index 6fb32a1b2..6499491b2 100644 --- a/docs/tutorials/wiki2/src/authentication/tutorial/models/user.py +++ b/docs/tutorials/wiki2/src/authentication/tutorial/models/user.py @@ -1,4 +1,5 @@ import bcrypt +import hmac from sqlalchemy import ( Column, Integer, @@ -23,7 +24,7 @@ class User(Base): def check_password(self, pw): if self.password_hash is not None: - expected_hash = self.password_hash.encode('utf8') + expected_hash = self.password_hash actual_hash = bcrypt.hashpw(pw.encode('utf8'), expected_hash) - return expected_hash == actual_hash + return hmac.compare_digest(expected_hash, actual_hash) return False -- cgit v1.2.3 From f63feb6c3f71f00389ce04f1100ae5675c748557 Mon Sep 17 00:00:00 2001 From: Steve Piercy <web@stevepiercy.com> Date: Sat, 27 Feb 2016 03:47:53 -0800 Subject: grammar fix --- docs/tutorials/wiki2/design.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorials/wiki2/design.rst b/docs/tutorials/wiki2/design.rst index a57b318ca..ba25b0697 100644 --- a/docs/tutorials/wiki2/design.rst +++ b/docs/tutorials/wiki2/design.rst @@ -139,8 +139,8 @@ in the following table: | | - If authentication | | | | | | succeeds, | | | | | | redirect to the | | | | -| | page that from | | | | -| | which we came. | | | | +| | page from which | | | | +| | we came. | | | | | | | | | | | | - If authentication | | | | | | fails, display | | | | -- cgit v1.2.3 From f99052e0790b8706161e432da0172b45a805d308 Mon Sep 17 00:00:00 2001 From: Steve Piercy <web@stevepiercy.com> Date: Sun, 28 Feb 2016 01:18:23 -0800 Subject: wiki2 authorization (done) - minor grammar and syntax - align order of bullet points for NewPage and PageResource with code - synch up "viewing app in browser" sections between authentication and authzn --- docs/tutorials/wiki2/authentication.rst | 13 +-- docs/tutorials/wiki2/authorization.rst | 169 +++++++++++++++++--------------- 2 files changed, 98 insertions(+), 84 deletions(-) diff --git a/docs/tutorials/wiki2/authentication.rst b/docs/tutorials/wiki2/authentication.rst index 115f7f689..8d9855460 100644 --- a/docs/tutorials/wiki2/authentication.rst +++ b/docs/tutorials/wiki2/authentication.rst @@ -288,14 +288,15 @@ following URLs, checking that the result is as expected: - http://localhost:6543/FrontPage/edit_page invokes the edit view for the ``FrontPage`` object. It is executable by only the ``editor`` user. If a different user (or the anonymous user) invokes it, then a login form will be - displayed. Supplying the credentials with the username ``editor``, password - ``editor`` will display the edit page form. + displayed. Supplying the credentials with the username ``editor`` and + password ``editor`` will display the edit page form. - http://localhost:6543/add_page/SomePageName invokes the add view for a page. - It is executable by the ``editor`` or ``basic`` user. If a different user (or - the anonymous user) invokes it, then a login form will be displayed. - Supplying the credentials with the username ``basic``, password ``basic``, - will display the edit page form. + It is executable by either the ``editor`` or ``basic`` user. If a different + user (or the anonymous user) invokes it, then a login form will be displayed. + Supplying the credentials with either the username ``editor`` and password + ``editor``, or username ``basic`` and password ``basic``, will display the + edit page form. - http://localhost:6543/SomePageName/edit_page is editable by the ``basic`` user if the page was created by that user in the previous step. If, instead, diff --git a/docs/tutorials/wiki2/authorization.rst b/docs/tutorials/wiki2/authorization.rst index eb9269dff..a2865d8cd 100644 --- a/docs/tutorials/wiki2/authorization.rst +++ b/docs/tutorials/wiki2/authorization.rst @@ -5,39 +5,40 @@ Adding authorization ==================== In the last chapter we built :term:`authentication` into our wiki2. We also -went one step further and used the ``request.user`` object to perform some explicit :term:`authorization` checks. This is fine for a lot of -applications but :app:`Pyramid` provides some facilities for cleaning this -up and decoupling the constraints from the view function itself. +went one step further and used the ``request.user`` object to perform some +explicit :term:`authorization` checks. This is fine for a lot of applications, +but :app:`Pyramid` provides some facilities for cleaning this up and decoupling +the constraints from the view function itself. We will implement access control with the following steps: -* Update the :term:`authentication policy` to break down the - :term:`userid` into a list of :term:`principals <principal>` - (``security.py``). +* Update the :term:`authentication policy` to break down the :term:`userid` + into a list of :term:`principals <principal>` (``security.py``). * Define an :term:`authorization policy` for mapping users, resources and permissions (``security.py``). -* Add new :term:`resource` definitions that will be used as the - :term:`context` for the wiki pages (``routes.py``). +* Add new :term:`resource` definitions that will be used as the :term:`context` + for the wiki pages (``routes.py``). * Add an :term:`ACL` to each resource (``routes.py``). * Replace the inline checks on the views with :term:`permission` declarations (``views/default.py``). + Add user principals ------------------- -A :term:`principal` is a level of abstraction on top of the raw -:term:`userid` that describes the user in terms of capabilities, roles or -other identifiers that are easier to generalize. The permissions are then -written against the principals without focusing on the exact user involved. +A :term:`principal` is a level of abstraction on top of the raw :term:`userid` +that describes the user in terms of its capabilities, roles, or other +identifiers that are easier to generalize. The permissions are then written +against the principals without focusing on the exact user involved. :app:`Pyramid` defines two builtin principals used in every application: :attr:`pyramid.security.Everyone` and :attr:`pyramid.security.Authenticated`. On top of these we have already mentioned the required principals for this -application in the original design. The user has two possible roles: -``editor`` and ``basic``. These will be prefixed by the ``role:`` -string to avoid clasing with any other types of principals. +application in the original design. The user has two possible roles: ``editor`` +or ``basic``. These will be prefixed by the string ``role:`` to avoid clashing +with any other types of principals. -Open the file ``tutorial/security.py`` and edit the following lines: +Open the file ``tutorial/security.py`` and edit it as follows: .. literalinclude:: src/authorization/tutorial/security.py :linenos: @@ -46,19 +47,20 @@ Open the file ``tutorial/security.py`` and edit the following lines: Only the highlighted lines need to be added. -Note that the role comes from the ``User`` object and finally we also -add the ``user.id`` as a principal for when we want to allow that exact -user to edit page's they've created. +Note that the role comes from the ``User`` object. We also add the ``user.id`` +as a principal for when we want to allow that exact user to edit pages which +they have created. + Add the authorization policy ---------------------------- We already added the :term:`authorization policy` in the previous chapter because :app:`Pyramid` requires one when adding an -:term:`authentication policy`. However, it was not used anywhere and so we'll +:term:`authentication policy`. However, it was not used anywhere, so we'll mention it now. -Open the file ``tutorial/security.py`` and notice the following lines: +In the file ``tutorial/security.py``, notice the following lines: .. literalinclude:: src/authorization/tutorial/security.py :lines: 38-40 @@ -66,36 +68,38 @@ Open the file ``tutorial/security.py`` and notice the following lines: :emphasize-lines: 2 :language: python -We're using the :class:`pyramid.authorization.ACLAuthorizationPolicy` which -will suffice for most applications. It uses the :term:`context` to define -the mapping between a :term:`principal` and :term:`permission` for the -current request via the ``__acl__``. +We're using the :class:`pyramid.authorization.ACLAuthorizationPolicy`, which +will suffice for most applications. It uses the :term:`context` to define the +mapping between a :term:`principal` and :term:`permission` for the current +request via the ``__acl__``. + Add resources and ACLs ---------------------- Resources are the hidden gem of :app:`Pyramid`. You've made it! -Every URL in a web application is representing a :term:`resource` -(the **R** in Uniform Resource Locator). Often the resource is something -in your data model but it could also be an abstraction over many models. +Every URL in a web application represents a :term:`resource` (the "R" in +Uniform Resource Locator). Often the resource is something in your data model, +but it could also be an abstraction over many models. Our wiki has two resources: -#. A ``PageResource``. Represents a ``Page`` that is to be viewed or edited. - Only ``editor`` users as well as the original creator of the ``Page`` - may edit the ``PageResource`` but anyone may view it. +#. A ``NewPage``. Represents a potential ``Page`` that does not exist. Any + logged-in user, having either role of ``basic`` or ``editor``, can create + pages. -#. A ``NewPage``. Represents a potential ``Page`` that does not exist. - Any logged-in user (roles ``basic`` or ``editor``) can create pages. +#. A ``PageResource``. Represents a ``Page`` that is to be viewed or edited. + ``editor`` users, as well as the original creator of the ``Page``, may edit + the ``PageResource``. Anyone may view it. .. note:: - The wiki data model is simple enough that the ``PageResource`` is - actually mostly redundant with our ``models.Page`` SQLAlchemy class. It is - completely valid to combine these into one class. However, for this - tutorial they are explicitly separated to make it clear the - parts that :app:`Pyramid` cares about versus application-defined objects. + The wiki data model is simple enough that the ``PageResource`` is mostly + redundant with our ``models.Page`` SQLAlchemy class. It is completely valid + to combine these into one class. However, for this tutorial, they are + explicitly separated to make clear the distinction between the parts about + which :app:`Pyramid` cares versus application-defined objects. There are many ways to define these resources, and they can even be grouped into collections with a hierarchy. However, we're keeping it simple here! @@ -109,11 +113,11 @@ Open the file ``tutorial/routes.py`` and edit the following lines: The highlighted lines need to be edited or added. -The ``NewPage`` has an ``__acl__`` on it that returns a list of -mappings from :term:`principal` to :term:`permission`. This defines **who** -can do **what** with that :term:`resource`. In our case we want to only -allow users with the principals ``role:editor`` and ``role:basic`` to -have the ``create`` permission: +The ``NewPage`` class has an ``__acl__`` on it that returns a list of mappings +from :term:`principal` to :term:`permission`. This defines *who* can do *what* +with that :term:`resource`. In our case we want to allow only those users with +the principals of either ``role:editor`` or ``role:basic`` to have the +``create`` permission: .. literalinclude:: src/authorization/tutorial/routes.py :lines: 20-32 @@ -121,8 +125,8 @@ have the ``create`` permission: :emphasize-lines: 11,12 :language: python -The ``NewPage`` is loaded as the :term:`context` of the ``add_page`` -route by declaring a ``factory`` on the route: +The ``NewPage`` is loaded as the :term:`context` of the ``add_page`` route by +declaring a ``factory`` on the route: .. literalinclude:: src/authorization/tutorial/routes.py :lines: 15-16 @@ -130,8 +134,8 @@ route by declaring a ``factory`` on the route: :emphasize-lines: 2 :language: python -The ``PageResource`` defines the :term:`ACL` for a ``Page``. It uses an -actual ``Page`` object to determine **who** can do **what** to the page. +The ``PageResource`` class defines the :term:`ACL` for a ``Page``. It uses an +actual ``Page`` object to determine *who* can do *what* to the page. .. literalinclude:: src/authorization/tutorial/routes.py :lines: 34-50 @@ -139,8 +143,8 @@ actual ``Page`` object to determine **who** can do **what** to the page. :emphasize-lines: 14-16 :language: python -The ``PageResource`` is loaded as the :term:`context` of the ``view_page`` -and ``edit_page`` route by declaring a ``factory`` on the routes: +The ``PageResource`` is loaded as the :term:`context` of the ``view_page`` and +``edit_page`` routes by declaring a ``factory`` on the routes: .. literalinclude:: src/authorization/tutorial/routes.py :lines: 14-18 @@ -148,14 +152,15 @@ and ``edit_page`` route by declaring a ``factory`` on the routes: :emphasize-lines: 1,4-5 :language: python + Add view permissions -------------------- At this point we've modified our application to load the ``PageResource``, including the actual ``Page`` model in the ``page_factory``. The ``PageResource`` is now the :term:`context` for all ``view_page`` and -``edit_page`` views. Similarly the ``NewPage`` will be the context for -the ``add_page`` view. +``edit_page`` views. Similarly the ``NewPage`` will be the context for the +``add_page`` view. Open the file ``views/default.py``. @@ -167,26 +172,27 @@ First, you can drop a few imports that are no longer necessary: :emphasize-lines: 1 :language: python -Edit the ``view_page`` view to declare the ``view`` permission and remove -the explicit checks within the view: +Edit the ``view_page`` view to declare the ``view`` permission, and remove the +explicit checks within the view: .. literalinclude:: src/authorization/tutorial/views/default.py :lines: 18-23 :lineno-match: - :emphasize-lines: 2,4 + :emphasize-lines: 1-2,4 :language: python -The work of loading the page has already been done in the factory so we -can just pull the ``page`` object out of the ``PageResource`` loaded as -``request.context``. Our factory also guarantees we will have a ``Page`` as it -raises ``HTTPNotFound`` otherwise - again simplifying the view logic. +The work of loading the page has already been done in the factory, so we can +just pull the ``page`` object out of the ``PageResource``, loaded as +``request.context``. Our factory also guarantees we will have a ``Page``, as it +raises the ``HTTPNotFound`` exception if no ``Page`` exists, again simplifying +the view logic. Edit the ``edit_page`` view to declare the ``edit`` permission: .. literalinclude:: src/authorization/tutorial/views/default.py :lines: 38-42 :lineno-match: - :emphasize-lines: 2,4 + :emphasize-lines: 1-2,4 :language: python Edit the ``add_page`` view to declare the ``create`` permission: @@ -194,22 +200,21 @@ Edit the ``add_page`` view to declare the ``create`` permission: .. literalinclude:: src/authorization/tutorial/views/default.py :lines: 52-56 :lineno-match: - :emphasize-lines: 2,4 + :emphasize-lines: 1-2,4 :language: python Note the ``pagename`` here is pulled off of the context instead of ``request.matchdict``. The factory has done a lot of work for us to hide the actual route pattern. -The ACLs defined on each :term:`resource` are used by the -:term:`authorization policy` to determine if any -:term:`principal` is allowed to have some :term:`permission`. If this check -fails (for example, the user is not logged in) then a ``HTTPForbidden`` -exception will be raised automatically, thus we're able to drop those -exceptions and checks from the views themselves. Rather we've defined them in -terms of operations on a resource. +The ACLs defined on each :term:`resource` are used by the :term:`authorization +policy` to determine if any :term:`principal` is allowed to have some +:term:`permission`. If this check fails (for example, the user is not logged +in) then an ``HTTPForbidden`` exception will be raised automatically. Thus +we're able to drop those exceptions and checks from the views themselves. +Rather we've defined them in terms of operations on a resource. -The final ``tutorial/views/default.py`` should look like the following: +The final ``views/default.py`` should look like the following: .. literalinclude:: src/authorization/tutorial/views/default.py :linenos: @@ -227,21 +232,29 @@ following URLs, checking that the result is as expected: is executable by any user. - http://localhost:6543/FrontPage invokes the ``view_page`` view of the - ``FrontPage`` page object. + ``FrontPage`` page object. There is a "Login" link in the upper right corner + while the user is not authenticated, else it is a "Logout" link when the user + is authenticated. - http://localhost:6543/FrontPage/edit_page invokes the edit view for the - FrontPage object. It is executable by only the ``editor`` user. If a - different user (or the anonymous user) invokes it, a login form will be - displayed. Supplying the credentials with the username ``editor``, password - ``editor`` will display the edit page form. + ``FrontPage`` object. It is executable by only the ``editor`` user. If a + different user (or the anonymous user) invokes it, then a login form will be + displayed. Supplying the credentials with the username ``editor`` and + password ``editor`` will display the edit page form. - http://localhost:6543/add_page/SomePageName invokes the add view for a page. - It is executable by only the ``editor`` user. If a different user (or the - anonymous user) invokes it, a login form will be displayed. Supplying the - credentials with the username ``editor``, password ``editor`` will display - the edit page form. + It is executable by either the ``editor`` or ``basic`` user. If a different + user (or the anonymous user) invokes it, then a login form will be displayed. + Supplying the credentials with either the username ``editor`` and password + ``editor``, or username ``basic`` and password ``basic``, will display the + edit page form. + +- http://localhost:6543/SomePageName/edit_page is editable by the ``basic`` + user if the page was created by that user in the previous step. If, instead, + the page was created by the ``editor`` user, then the login page should be + shown for the ``basic`` user. - After logging in (as a result of hitting an edit or add page and submitting - the login form with the ``editor`` credentials), we'll see a Logout link in + the login form with the ``editor`` credentials), we'll see a "Logout" link in the upper right hand corner. When we click it, we're logged out, and redirected back to the front page. -- cgit v1.2.3 From ee4050ac0a4a60343eb736a90b47113ebfbb60a3 Mon Sep 17 00:00:00 2001 From: Steve Piercy <web@stevepiercy.com> Date: Sun, 28 Feb 2016 01:22:20 -0800 Subject: wiki2 revert unnecessary hmac stuff --- docs/tutorials/wiki2/src/authentication/tutorial/models/user.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/models/user.py b/docs/tutorials/wiki2/src/authentication/tutorial/models/user.py index 6499491b2..6bd3315d6 100644 --- a/docs/tutorials/wiki2/src/authentication/tutorial/models/user.py +++ b/docs/tutorials/wiki2/src/authentication/tutorial/models/user.py @@ -1,5 +1,4 @@ import bcrypt -import hmac from sqlalchemy import ( Column, Integer, @@ -26,5 +25,5 @@ class User(Base): if self.password_hash is not None: expected_hash = self.password_hash actual_hash = bcrypt.hashpw(pw.encode('utf8'), expected_hash) - return hmac.compare_digest(expected_hash, actual_hash) + return expected_hash == actual_hash return False -- cgit v1.2.3 From a26a08b92f390aeaa3eb0c39241bb5e76f5a72ea Mon Sep 17 00:00:00 2001 From: Steve Piercy <web@stevepiercy.com> Date: Sun, 28 Feb 2016 01:29:55 -0800 Subject: apply change to all src/*/user.py --- docs/tutorials/wiki2/src/authorization/tutorial/models/user.py | 2 +- docs/tutorials/wiki2/src/models/tutorial/models/user.py | 2 +- docs/tutorials/wiki2/src/tests/tutorial/models/user.py | 2 +- docs/tutorials/wiki2/src/views/tutorial/models/user.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models/user.py b/docs/tutorials/wiki2/src/authorization/tutorial/models/user.py index 6fb32a1b2..6bd3315d6 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/models/user.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/models/user.py @@ -23,7 +23,7 @@ class User(Base): def check_password(self, pw): if self.password_hash is not None: - expected_hash = self.password_hash.encode('utf8') + expected_hash = self.password_hash actual_hash = bcrypt.hashpw(pw.encode('utf8'), expected_hash) return expected_hash == actual_hash return False diff --git a/docs/tutorials/wiki2/src/models/tutorial/models/user.py b/docs/tutorials/wiki2/src/models/tutorial/models/user.py index 6fb32a1b2..6bd3315d6 100644 --- a/docs/tutorials/wiki2/src/models/tutorial/models/user.py +++ b/docs/tutorials/wiki2/src/models/tutorial/models/user.py @@ -23,7 +23,7 @@ class User(Base): def check_password(self, pw): if self.password_hash is not None: - expected_hash = self.password_hash.encode('utf8') + expected_hash = self.password_hash actual_hash = bcrypt.hashpw(pw.encode('utf8'), expected_hash) return expected_hash == actual_hash return False diff --git a/docs/tutorials/wiki2/src/tests/tutorial/models/user.py b/docs/tutorials/wiki2/src/tests/tutorial/models/user.py index 6fb32a1b2..6bd3315d6 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/models/user.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/models/user.py @@ -23,7 +23,7 @@ class User(Base): def check_password(self, pw): if self.password_hash is not None: - expected_hash = self.password_hash.encode('utf8') + expected_hash = self.password_hash actual_hash = bcrypt.hashpw(pw.encode('utf8'), expected_hash) return expected_hash == actual_hash return False diff --git a/docs/tutorials/wiki2/src/views/tutorial/models/user.py b/docs/tutorials/wiki2/src/views/tutorial/models/user.py index 6fb32a1b2..6bd3315d6 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/models/user.py +++ b/docs/tutorials/wiki2/src/views/tutorial/models/user.py @@ -23,7 +23,7 @@ class User(Base): def check_password(self, pw): if self.password_hash is not None: - expected_hash = self.password_hash.encode('utf8') + expected_hash = self.password_hash actual_hash = bcrypt.hashpw(pw.encode('utf8'), expected_hash) return expected_hash == actual_hash return False -- cgit v1.2.3 From 606f753fc2ca310debc501874db16b60180773d2 Mon Sep 17 00:00:00 2001 From: Steve Piercy <web@stevepiercy.com> Date: Sun, 28 Feb 2016 02:34:24 -0800 Subject: update tests.rst (done) - minor grammar - mention BaseTest class - clean up test output --- docs/tutorials/wiki2/tests.rst | 52 +++++++++++++++++++----------------------- 1 file changed, 24 insertions(+), 28 deletions(-) diff --git a/docs/tutorials/wiki2/tests.rst b/docs/tutorials/wiki2/tests.rst index 667550467..fa9cbe2fc 100644 --- a/docs/tutorials/wiki2/tests.rst +++ b/docs/tutorials/wiki2/tests.rst @@ -13,38 +13,36 @@ should contain tests for its corresponding module in our application. Each corresponding pair of modules should have the same names, except the test module should have the prefix ``test_``. -We will move parts of ``tests.py`` into appropriate new files in the ``tests`` -subpackage, and add several new tests. - -Start by creating a new directory and a new empty file ``tests/__init__.py``. +Start by deleting ``tests.py``, then create a new directory to contain our new +tests as well as a new empty file ``tests/__init__.py``. .. warning:: - It is very important when refactoring a Python module into a package to - be sure to delete the cache files (``.pyc`` files or ``__pycache__`` - folders) sitting around! Python will prioritize the cache files before - traversing into folders and so it will use the old code and you will wonder - why none of your changes are working! + It is very important when refactoring a Python module into a package to be + sure to delete the cache files (``.pyc`` files or ``__pycache__`` folders) + sitting around! Python will prioritize the cache files before traversing + into folders, using the old code, and you will wonder why none of your + changes are working! Test the views ============== -We'll create a new ``tests/test_views.py`` file, adding tests for each view -function we previously added to our application. As a result, we'll *delete* -the ``ViewTests`` class that the ``alchemy`` scaffold provided, and add four -other test classes: ``ViewWikiTests``, ``ViewPageTests``, ``AddPageTests``, and -``EditPageTests``. These test the ``view_wiki``, ``view_page``, ``add_page``, -and ``edit_page`` views. +We'll create a new ``tests/test_views.py`` file, adding a ``BaseTest`` class +used as the base for other test classes. Next we'll add tests for each view +function we previously added to our application. We'll add four test classes: +``ViewWikiTests``, ``ViewPageTests``, ``AddPageTests``, and ``EditPageTests``. +These test the ``view_wiki``, ``view_page``, ``add_page``, and ``edit_page`` +views. Functional tests ================ -We'll test the whole application, covering security aspects that are not -tested in the unit tests, like logging in, logging out, checking that -the ``basic`` user cannot edit pages it didn't create, but the ``editor`` user -can, and so on. +We'll test the whole application, covering security aspects that are not tested +in the unit tests, like logging in, logging out, checking that the ``basic`` +user cannot edit pages that it didn't create but the ``editor`` user can, and +so on. View the results of all our edits to ``tests`` subpackage @@ -67,10 +65,10 @@ follows: .. note:: - We're utilizing the excellent WebTest_ package to do functional testing - of the application. This is defined in the ``tests_require`` section of - our ``setup.py``. Any other dependencies needed only for testing purposes - can be added there and will be installed automatically when running + We're utilizing the excellent WebTest_ package to do functional testing of + the application. This is defined in the ``tests_require`` section of our + ``setup.py``. Any other dependencies needed only for testing purposes can be + added there and will be installed automatically when running ``setup.py test``. @@ -88,7 +86,7 @@ On UNIX: On Windows: -.. code-block:: text +.. code-block:: ps1con c:\pyramidtut\tutorial> %VENV%\Scripts\python setup.py test -q @@ -96,12 +94,10 @@ The expected result should look like the following: .. code-block:: text - .................... + ..................... ---------------------------------------------------------------------- - Ran 20 tests in 0.524s + Ran 21 tests in 5.117s OK - Process finished with exit code 0 - .. _webtest: http://docs.pylonsproject.org/projects/webtest/en/latest/ -- cgit v1.2.3 From 3a89ff8ac584f4dcbeca49e1bfbb516006f45446 Mon Sep 17 00:00:00 2001 From: Steve Piercy <web@stevepiercy.com> Date: Sun, 28 Feb 2016 02:38:58 -0800 Subject: update distributing.rst (done) - minor grammar - clean up sdist output --- docs/tutorials/wiki2/distributing.rst | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/tutorials/wiki2/distributing.rst b/docs/tutorials/wiki2/distributing.rst index ec90859a9..84e0e6d84 100644 --- a/docs/tutorials/wiki2/distributing.rst +++ b/docs/tutorials/wiki2/distributing.rst @@ -9,13 +9,13 @@ current working directory contains the ``tutorial`` package and the On UNIX: -.. code-block:: text +.. code-block:: bash $ $VENV/bin/python setup.py sdist On Windows: -.. code-block:: text +.. code-block:: ps1con c:\pyramidtut> %VENV%\Scripts\python setup.py sdist @@ -26,8 +26,7 @@ The output of such a command will be something like: running sdist # .. more output .. creating dist - tar -cf dist/tutorial-0.0.tar tutorial-0.0 - gzip -f9 dist/tutorial-0.0.tar + Creating tar archive removing 'tutorial-0.0' (and everything under it) Note that this command creates a tarball in the "dist" subdirectory named -- cgit v1.2.3 From 44c087008f39dba616ea62eb413f2fa96a5b6fb3 Mon Sep 17 00:00:00 2001 From: Michael Merickel <michael@merickel.org> Date: Sun, 28 Feb 2016 13:04:30 -0600 Subject: fix some nits --- docs/tutorials/wiki2/authentication.rst | 2 +- docs/tutorials/wiki2/authorization.rst | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/tutorials/wiki2/authentication.rst b/docs/tutorials/wiki2/authentication.rst index 8d9855460..72c11f311 100644 --- a/docs/tutorials/wiki2/authentication.rst +++ b/docs/tutorials/wiki2/authentication.rst @@ -207,7 +207,7 @@ Create a new file ``tutorial/views/auth.py``, and add the following code to it: :linenos: :language: python -This code adds three new views to application: +This code adds three new views to the application: - The ``login`` view renders a login form and processes the post from the login form, checking credentials against our ``users`` table in the database. diff --git a/docs/tutorials/wiki2/authorization.rst b/docs/tutorials/wiki2/authorization.rst index a2865d8cd..1be961f61 100644 --- a/docs/tutorials/wiki2/authorization.rst +++ b/docs/tutorials/wiki2/authorization.rst @@ -162,7 +162,7 @@ including the actual ``Page`` model in the ``page_factory``. The ``edit_page`` views. Similarly the ``NewPage`` will be the context for the ``add_page`` view. -Open the file ``views/default.py``. +Open the file ``tutorial/views/default.py``. First, you can drop a few imports that are no longer necessary: @@ -214,7 +214,7 @@ in) then an ``HTTPForbidden`` exception will be raised automatically. Thus we're able to drop those exceptions and checks from the views themselves. Rather we've defined them in terms of operations on a resource. -The final ``views/default.py`` should look like the following: +The final ``tutorial/views/default.py`` should look like the following: .. literalinclude:: src/authorization/tutorial/views/default.py :linenos: -- cgit v1.2.3 From de3062576ce5aa8b2e854626a48e3f5c46b29cb7 Mon Sep 17 00:00:00 2001 From: Steve Piercy <web@stevepiercy.com> Date: Sun, 28 Feb 2016 11:57:14 -0800 Subject: add note about deprecation warnings on py3 --- docs/tutorials/wiki2/tests.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/tutorials/wiki2/tests.rst b/docs/tutorials/wiki2/tests.rst index fa9cbe2fc..dabb73956 100644 --- a/docs/tutorials/wiki2/tests.rst +++ b/docs/tutorials/wiki2/tests.rst @@ -100,4 +100,11 @@ The expected result should look like the following: OK +.. note:: If you use Python 3 during this tutorial, you will see deprecation + warnings in the output, which we will choose to ignore. In making this + tutorial run on both Python 2 and 3, the authors prioritized simplicity and + focus for the learner over accommodating warnings. In your own app or as + extra credit, you may choose to either drop Python 2 support or hack your + code to work without warnings on both Python 2 and 3. + .. _webtest: http://docs.pylonsproject.org/projects/webtest/en/latest/ -- cgit v1.2.3 From 53c7f7c0f93883dcd37facebd550b38ec79034e6 Mon Sep 17 00:00:00 2001 From: Michael Merickel <michael@merickel.org> Date: Sun, 28 Feb 2016 14:52:09 -0600 Subject: add a failing test case with #1370 --- pyramid/tests/test_config/test_predicates.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyramid/tests/test_config/test_predicates.py b/pyramid/tests/test_config/test_predicates.py index ee24de093..9cd8f2734 100644 --- a/pyramid/tests/test_config/test_predicates.py +++ b/pyramid/tests/test_config/test_predicates.py @@ -120,9 +120,9 @@ class TestRequestParamPredicate(unittest.TestCase): self.assertTrue(result) def test___call___true_multi(self): - inst = self._makeOne(('abc', 'def =2 ')) + inst = self._makeOne(('abc', '=def =2= ')) request = Dummy() - request.params = {'abc':'1', 'def': '2'} + request.params = {'abc':'1', '=def': '2='} result = inst(None, request) self.assertTrue(result) -- cgit v1.2.3 From 4c1f5ac9aa29616421e75a42157d4c619c9269d6 Mon Sep 17 00:00:00 2001 From: Michael Merickel <michael@merickel.org> Date: Sun, 28 Feb 2016 14:59:23 -0600 Subject: fix #1370 to use split instead of rsplit to keep consistent --- pyramid/config/predicates.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pyramid/config/predicates.py b/pyramid/config/predicates.py index d87fd1aae..0b76bbd70 100644 --- a/pyramid/config/predicates.py +++ b/pyramid/config/predicates.py @@ -70,11 +70,13 @@ class RequestParamPredicate(object): for p in val: k = p v = None - if '=' in p: - if p.startswith('='): - k, v = p.rsplit('=', 1) - else: - k, v = p.split('=', 1) + if p.startswith('='): + if '=' in p[1:]: + k, v = p[1:].split('=', 1) + k = '=' + k + k, v = k.strip(), v.strip() + elif '=' in p: + k, v = p.split('=', 1) k, v = k.strip(), v.strip() reqs.append((k, v)) self.val = val -- cgit v1.2.3 From cb98a90c3dcc40dc42813143a601ef631249f5f4 Mon Sep 17 00:00:00 2001 From: Michael Merickel <michael@merickel.org> Date: Sun, 28 Feb 2016 15:06:30 -0600 Subject: add changelog for #1370 --- CHANGES.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index ffa5f51e0..84a62837c 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -10,6 +10,11 @@ unreleased ``[app:main]`` and ``[server:main]``. See https://github.com/Pylons/pyramid/pull/2292 +- Allow a leading ``=`` on the key of the request param predicate. + For example, '=abc=1' is equivalent down to + ``request.params['=abc'] == '1'``. + See https://github.com/Pylons/pyramid/pull/1370 + 1.6 (2015-04-14) ================ -- cgit v1.2.3 From 3e30040da7c2d5c38b330727b48d9f6b852956d9 Mon Sep 17 00:00:00 2001 From: Steve Piercy <web@stevepiercy.com> Date: Sun, 28 Feb 2016 22:30:22 -0800 Subject: redirect to edit page when user attempts to add page that already exists - update src/*/views/default.py - update src/*/routes.py - write new test - revise docs, double-checking line counts and highlighting --- docs/tutorials/wiki2/authentication.rst | 61 ++++++++++++---------- docs/tutorials/wiki2/authorization.rst | 51 +++++++++--------- docs/tutorials/wiki2/definingviews.rst | 23 +++++--- .../src/authentication/tutorial/views/default.py | 3 ++ .../wiki2/src/authorization/tutorial/routes.py | 8 ++- docs/tutorials/wiki2/src/tests/tutorial/routes.py | 8 ++- .../wiki2/src/tests/tutorial/tests/test_views.py | 12 +++++ .../wiki2/src/views/tutorial/views/default.py | 3 ++ docs/tutorials/wiki2/tests.rst | 2 +- 9 files changed, 108 insertions(+), 63 deletions(-) diff --git a/docs/tutorials/wiki2/authentication.rst b/docs/tutorials/wiki2/authentication.rst index 8d9855460..9ee223a50 100644 --- a/docs/tutorials/wiki2/authentication.rst +++ b/docs/tutorials/wiki2/authentication.rst @@ -132,34 +132,34 @@ Open the file ``tutorial/views/default.py`` and fix the following imports: Change the two highlighted lines. -In the same file, now edit the ``add_page`` view function: +In the same file, now edit the ``edit_page`` view function: .. literalinclude:: src/authentication/tutorial/views/default.py - :lines: 62-76 + :lines: 45-60 :lineno-match: - :emphasize-lines: 3-5,10 + :emphasize-lines: 5-7 :language: python Only the highlighted lines need to be changed. -If the user either is not logged in or is not in the ``basic`` or ``editor`` -roles, then we raise ``HTTPForbidden``, which will return a "403 Forbidden" -response to the user. However, we will hook this later to redirect to the login -page. Also, now that we have ``request.user``, we no longer have to hard-code -the creator as the ``editor`` user, so we can finally drop that hack. +If the user either is not logged in or the user is not the page's creator +*and* not an ``editor``, then we raise ``HTTPForbidden``. -Now edit the ``edit_page`` view function: +In the same file, now edit the ``add_page`` view function: .. literalinclude:: src/authentication/tutorial/views/default.py - :lines: 45-60 + :lines: 62-76 :lineno-match: - :emphasize-lines: 5-7 + :emphasize-lines: 3-5,13 :language: python Only the highlighted lines need to be changed. -If the user either is not logged in or the user is not the page's creator -*and* not an ``editor``, then we raise ``HTTPForbidden``. +If the user either is not logged in or is not in the ``basic`` or ``editor`` +roles, then we raise ``HTTPForbidden``, which will return a "403 Forbidden" +response to the user. However, we will hook this later to redirect to the login +page. Also, now that we have ``request.user``, we no longer have to hard-code +the creator as the ``editor`` user, so we can finally drop that hack. These simple checks should protect our views. @@ -285,25 +285,28 @@ following URLs, checking that the result is as expected: while the user is not authenticated, else it is a "Logout" link when the user is authenticated. -- http://localhost:6543/FrontPage/edit_page invokes the edit view for the - ``FrontPage`` object. It is executable by only the ``editor`` user. If a - different user (or the anonymous user) invokes it, then a login form will be - displayed. Supplying the credentials with the username ``editor`` and +- http://localhost:6543/FrontPage/edit_page invokes the ``edit_page`` view for + the ``FrontPage`` page object. It is executable by only the ``editor`` user. + If a different user (or the anonymous user) invokes it, then a login form + will be displayed. Supplying the credentials with the username ``editor`` and password ``editor`` will display the edit page form. -- http://localhost:6543/add_page/SomePageName invokes the add view for a page. - It is executable by either the ``editor`` or ``basic`` user. If a different - user (or the anonymous user) invokes it, then a login form will be displayed. - Supplying the credentials with either the username ``editor`` and password - ``editor``, or username ``basic`` and password ``basic``, will display the - edit page form. +- http://localhost:6543/add_page/SomePageName invokes the ``add_page`` view for + a page. If the page already exists, then it redirects the user to the + ``edit_page`` view for the page object. It is executable by either the + ``editor`` or ``basic`` user. If a different user (or the anonymous user) + invokes it, then a login form will be displayed. Supplying the credentials + with either the username ``editor`` and password ``editor``, or username + ``basic`` and password ``basic``, will display the edit page form. -- http://localhost:6543/SomePageName/edit_page is editable by the ``basic`` - user if the page was created by that user in the previous step. If, instead, - the page was created by the ``editor`` user, then the login page should be - shown for the ``basic`` user. +- http://localhost:6543/SomePageName/edit_page invokes the ``edit_page`` view + for an existing page, or generates an error if the page does not exist. It is + editable by the ``basic`` user if the page was created by that user in the + previous step. If, instead, the page was created by the ``editor`` user, then + the login page should be shown for the ``basic`` user. - After logging in (as a result of hitting an edit or add page and submitting the login form with the ``editor`` credentials), we'll see a "Logout" link in - the upper right hand corner. When we click it, we're logged out, and - redirected back to the front page. + the upper right hand corner. When we click it, we're logged out, redirected + back to the front page, and a "Login" link is shown in the upper right hand + corner. diff --git a/docs/tutorials/wiki2/authorization.rst b/docs/tutorials/wiki2/authorization.rst index a2865d8cd..3dec21a23 100644 --- a/docs/tutorials/wiki2/authorization.rst +++ b/docs/tutorials/wiki2/authorization.rst @@ -108,7 +108,7 @@ Open the file ``tutorial/routes.py`` and edit the following lines: .. literalinclude:: src/authorization/tutorial/routes.py :linenos: - :emphasize-lines: 1-7,14-50 + :emphasize-lines: 1-11,17- :language: python The highlighted lines need to be edited or added. @@ -120,34 +120,34 @@ the principals of either ``role:editor`` or ``role:basic`` to have the ``create`` permission: .. literalinclude:: src/authorization/tutorial/routes.py - :lines: 20-32 + :lines: 30-38 :lineno-match: - :emphasize-lines: 11,12 + :emphasize-lines: 5-9 :language: python The ``NewPage`` is loaded as the :term:`context` of the ``add_page`` route by declaring a ``factory`` on the route: .. literalinclude:: src/authorization/tutorial/routes.py - :lines: 15-16 + :lines: 18-19 :lineno-match: - :emphasize-lines: 2 + :emphasize-lines: 1-2 :language: python The ``PageResource`` class defines the :term:`ACL` for a ``Page``. It uses an actual ``Page`` object to determine *who* can do *what* to the page. .. literalinclude:: src/authorization/tutorial/routes.py - :lines: 34-50 + :lines: 47- :lineno-match: - :emphasize-lines: 14-16 + :emphasize-lines: 5-10 :language: python The ``PageResource`` is loaded as the :term:`context` of the ``view_page`` and ``edit_page`` routes by declaring a ``factory`` on the routes: .. literalinclude:: src/authorization/tutorial/routes.py - :lines: 14-18 + :lines: 17-21 :lineno-match: :emphasize-lines: 1,4-5 :language: python @@ -236,25 +236,28 @@ following URLs, checking that the result is as expected: while the user is not authenticated, else it is a "Logout" link when the user is authenticated. -- http://localhost:6543/FrontPage/edit_page invokes the edit view for the - ``FrontPage`` object. It is executable by only the ``editor`` user. If a - different user (or the anonymous user) invokes it, then a login form will be - displayed. Supplying the credentials with the username ``editor`` and +- http://localhost:6543/FrontPage/edit_page invokes the ``edit_page`` view for + the ``FrontPage`` page object. It is executable by only the ``editor`` user. + If a different user (or the anonymous user) invokes it, then a login form + will be displayed. Supplying the credentials with the username ``editor`` and password ``editor`` will display the edit page form. -- http://localhost:6543/add_page/SomePageName invokes the add view for a page. - It is executable by either the ``editor`` or ``basic`` user. If a different - user (or the anonymous user) invokes it, then a login form will be displayed. - Supplying the credentials with either the username ``editor`` and password - ``editor``, or username ``basic`` and password ``basic``, will display the - edit page form. +- http://localhost:6543/add_page/SomePageName invokes the ``add_page`` view for + a page. If the page already exists, then it redirects the user to the + ``edit_page`` view for the page object. It is executable by either the + ``editor`` or ``basic`` user. If a different user (or the anonymous user) + invokes it, then a login form will be displayed. Supplying the credentials + with either the username ``editor`` and password ``editor``, or username + ``basic`` and password ``basic``, will display the edit page form. -- http://localhost:6543/SomePageName/edit_page is editable by the ``basic`` - user if the page was created by that user in the previous step. If, instead, - the page was created by the ``editor`` user, then the login page should be - shown for the ``basic`` user. +- http://localhost:6543/SomePageName/edit_page invokes the ``edit_page`` view + for an existing page, or generates an error if the page does not exist. It is + editable by the ``basic`` user if the page was created by that user in the + previous step. If, instead, the page was created by the ``editor`` user, then + the login page should be shown for the ``basic`` user. - After logging in (as a result of hitting an edit or add page and submitting the login form with the ``editor`` credentials), we'll see a "Logout" link in - the upper right hand corner. When we click it, we're logged out, and - redirected back to the front page. + the upper right hand corner. When we click it, we're logged out, redirected + back to the front page, and a "Login" link is shown in the upper right hand + corner. diff --git a/docs/tutorials/wiki2/definingviews.rst b/docs/tutorials/wiki2/definingviews.rst index e48980dc8..b0cbe7dc4 100644 --- a/docs/tutorials/wiki2/definingviews.rst +++ b/docs/tutorials/wiki2/definingviews.rst @@ -124,7 +124,7 @@ edit it to look like the following: .. literalinclude:: src/views/tutorial/views/default.py :linenos: :language: python - :emphasize-lines: 1-9,12-70 + :emphasize-lines: 1-9,12- The highlighted lines need to be added or edited. @@ -277,7 +277,7 @@ The ``add_page`` view function Here is the code for the ``add_page`` view function and its decorator: .. literalinclude:: src/views/tutorial/views/default.py - :lines: 58-70 + :lines: 58- :lineno-match: :linenos: :language: python @@ -294,6 +294,10 @@ page we'd like to add. If our add view is invoked via, for example, ``http://localhost:6543/add_page/SomeName``, the value for ``'pagename'`` in the ``matchdict`` will be ``'SomeName'``. +Next a check is performed to determine whether the ``Page`` already exists in +the database. If it already exists, then the client is redirected to the +``edit_page`` view, else we continue to the next check. + If the view execution *is* a result of a form submission (i.e., the expression ``'form.submitted' in request.params`` is ``True``), we grab the page body from the form data, create a Page object with this page body and the name taken from @@ -452,13 +456,18 @@ each of the following URLs, checking that the result is as expected: - http://localhost:6543/ invokes the ``view_wiki`` view. This always redirects to the ``view_page`` view of the ``FrontPage`` page object. -- http://localhost:6543/FrontPage invokes the ``view_page`` view of the front - page object. +- http://localhost:6543/FrontPage invokes the ``view_page`` view of the + ``FrontPage`` page object. + +- http://localhost:6543/FrontPage/edit_page invokes the ``edit_page`` view for + the ``FrontPage`` page object. -- http://localhost:6543/FrontPage/edit_page invokes the edit view for the - front page object. +- http://localhost:6543/add_page/SomePageName invokes the ``add_page`` view for + a page. If the page already exists, then it redirects the user to the + ``edit_page`` view for the page object. -- http://localhost:6543/add_page/SomePageName invokes the add view for a page. +- http://localhost:6543/SomePageName/edit_page invokes the ``edit_page`` view + for an existing page, or generates an error if the page does not exist. - To generate an error, visit http://localhost:6543/foobars/edit_page which will generate a ``NoResultFound: No row was found for one()`` error. You'll diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py b/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py index ffe967f6e..1b071434c 100644 --- a/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py +++ b/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py @@ -65,6 +65,9 @@ def add_page(request): if user is None or user.role not in ('editor', 'basic'): raise HTTPForbidden pagename = request.matchdict['pagename'] + if request.dbsession.query(Page).filter_by(name=pagename).count() > 0: + next_url = request.route_url('edit_page', pagename=pagename) + return HTTPFound(location=next_url) if 'form.submitted' in request.params: body = request.params['body'] page = Page(name=pagename, data=body) diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/routes.py b/docs/tutorials/wiki2/src/authorization/tutorial/routes.py index c7c3a2120..f0a8b7f96 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/routes.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/routes.py @@ -1,4 +1,7 @@ -from pyramid.httpexceptions import HTTPNotFound +from pyramid.httpexceptions import ( + HTTPNotFound, + HTTPFound, +) from pyramid.security import ( Allow, Everyone, @@ -19,6 +22,9 @@ def includeme(config): def new_page_factory(request): pagename = request.matchdict['pagename'] + if request.dbsession.query(Page).filter_by(name=pagename).count() > 0: + next_url = request.route_url('edit_page', pagename=pagename) + raise HTTPFound(location=next_url) return NewPage(pagename) class NewPage(object): diff --git a/docs/tutorials/wiki2/src/tests/tutorial/routes.py b/docs/tutorials/wiki2/src/tests/tutorial/routes.py index c7c3a2120..f0a8b7f96 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/routes.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/routes.py @@ -1,4 +1,7 @@ -from pyramid.httpexceptions import HTTPNotFound +from pyramid.httpexceptions import ( + HTTPNotFound, + HTTPFound, +) from pyramid.security import ( Allow, Everyone, @@ -19,6 +22,9 @@ def includeme(config): def new_page_factory(request): pagename = request.matchdict['pagename'] + if request.dbsession.query(Page).filter_by(name=pagename).count() > 0: + next_url = request.route_url('edit_page', pagename=pagename) + raise HTTPFound(location=next_url) return NewPage(pagename) class NewPage(object): diff --git a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_views.py b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_views.py index 5253183df..2c945ab33 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_views.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_views.py @@ -98,6 +98,18 @@ class AddPageTests(BaseTest): from tutorial.views.default import add_page return add_page(request) + def test_it_pageexists(self): + from ..models import Page + from ..routes import NewPage + request = testing.DummyRequest({'form.submitted': True, + 'body': 'Hello yo!'}, + dbsession=self.session) + request.user = self.makeUser('foo', 'editor') + request.context = NewPage('AnotherPage') + self._callFUT(request) + pagecount = self.session.query(Page).filter_by(name='AnotherPage').count() + self.assertGreater(pagecount, 0) + def test_it_notsubmitted(self): from ..routes import NewPage request = dummy_request(self.session) diff --git a/docs/tutorials/wiki2/src/views/tutorial/views/default.py b/docs/tutorials/wiki2/src/views/tutorial/views/default.py index c1d402f6a..bb6300b75 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/views/default.py +++ b/docs/tutorials/wiki2/src/views/tutorial/views/default.py @@ -58,6 +58,9 @@ def edit_page(request): @view_config(route_name='add_page', renderer='../templates/edit.jinja2') def add_page(request): pagename = request.matchdict['pagename'] + if request.dbsession.query(Page).filter_by(name=pagename).count() > 0: + next_url = request.route_url('edit_page', pagename=pagename) + return HTTPFound(location=next_url) if 'form.submitted' in request.params: body = request.params['body'] page = Page(name=pagename, data=body) diff --git a/docs/tutorials/wiki2/tests.rst b/docs/tutorials/wiki2/tests.rst index dabb73956..54aea28c6 100644 --- a/docs/tutorials/wiki2/tests.rst +++ b/docs/tutorials/wiki2/tests.rst @@ -96,7 +96,7 @@ The expected result should look like the following: ..................... ---------------------------------------------------------------------- - Ran 21 tests in 5.117s + Ran 22 tests in 5.320s OK -- cgit v1.2.3 From d5c361d8bccb57e3b6969a91209511cc4a45134a Mon Sep 17 00:00:00 2001 From: Michael Merickel <michael@merickel.org> Date: Mon, 29 Feb 2016 13:05:04 -0600 Subject: update changelog for #2024 --- CHANGES.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 84a62837c..3a1305c95 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,6 +1,12 @@ unreleased ========== +- A complete overhaul of the ``alchemy`` scaffold as well as the + Wiki2 SQLAlchemy + URLDispatch tutorial to introduce more modern features + into the usage of SQLAlchemy with Pyramid and provide a better starting + point for new projects. + See https://github.com/Pylons/pyramid/pull/2024 + - Dropped Python 3.2 support. See https://github.com/Pylons/pyramid/pull/2256 -- cgit v1.2.3 From 8be4c4501b1fd684d5e1ce24bd05be121c4813ee Mon Sep 17 00:00:00 2001 From: Steve Piercy <web@stevepiercy.com> Date: Mon, 29 Feb 2016 15:01:28 -0800 Subject: update major release descriptions --- contributing.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/contributing.md b/contributing.md index c2d2ecefd..af19ed093 100644 --- a/contributing.md +++ b/contributing.md @@ -27,11 +27,8 @@ listed below. * [master](https://github.com/Pylons/pyramid/) - The branch on which further development takes place. The default branch on GitHub. * [1.6-branch](https://github.com/Pylons/pyramid/tree/1.6-branch) - The branch -to which further development on master should be backported. This is also a -development branch. -* [1.5-branch](https://github.com/Pylons/pyramid/tree/1.5-branch) - The branch -classified as "stable" or "latest". Actively maintained. -* [1.4-branch](https://github.com/Pylons/pyramid/tree/1.4-branch) - The oldest +classified as "stable" or "latest". Actively maintained. +* [1.5-branch](https://github.com/Pylons/pyramid/tree/1.5-branch) - The oldest actively maintained and stable branch. Older branches are not actively maintained. In general, two stable branches and -- cgit v1.2.3 From 19af3f4f5c3ad69ea6b71d63e18aa86fe8f69aae Mon Sep 17 00:00:00 2001 From: Steve Piercy <web@stevepiercy.com> Date: Mon, 29 Feb 2016 15:04:23 -0800 Subject: add step to update contributing.md --- RELEASING.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/RELEASING.txt b/RELEASING.txt index 75a4fcea2..b69971710 100644 --- a/RELEASING.txt +++ b/RELEASING.txt @@ -34,6 +34,8 @@ Releasing Pyramid - Update whatsnew-X.X.rst in docs to point at change log entries for individual releases if applicable. +- For major version releases, in contributing.md, update branch descriptions. + - For major version releases, in docs/conf.py, update values under html_theme_options for in_progress and outdated across master, releasing branch, and previously released branch. Also in the previously released -- cgit v1.2.3 From 68b303afa462db0cdfb903e02e65813a08f2e0cf Mon Sep 17 00:00:00 2001 From: Michael Merickel <michael@merickel.org> Date: Mon, 29 Feb 2016 21:20:35 -0600 Subject: add exception and exc_info to request --- pyramid/view.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyramid/view.py b/pyramid/view.py index 822fb29d6..589b13578 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -606,6 +606,8 @@ class ViewMethodsMixin(object): # have been mutated by the view, and its state is not # sane (e.g. caching headers) with hide_attrs(request, 'exception', 'exc_info', 'response'): + attrs['exception'] = exc_info[0] + attrs['exc_info'] = exc_info # we use .get instead of .__getitem__ below due to # https://github.com/Pylons/pyramid/issues/700 request_iface = attrs.get('request_iface', IRequest) -- cgit v1.2.3 From 4ff751448a205d14ad03eeab205e51a17a0c05f9 Mon Sep 17 00:00:00 2001 From: Michael Merickel <michael@merickel.org> Date: Mon, 29 Feb 2016 21:21:46 -0600 Subject: exception views should never have a name= --- pyramid/view.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyramid/view.py b/pyramid/view.py index 589b13578..16b60b77a 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -600,7 +600,6 @@ class ViewMethodsMixin(object): exc_info = sys.exc_info() attrs = request.__dict__ context_iface = providedBy(exc_info[0]) - view_name = attrs.get('view_name', '') # clear old generated request.response, if any; it may # have been mutated by the view, and its state is not @@ -616,7 +615,7 @@ class ViewMethodsMixin(object): request, exc_info[0], context_iface, - view_name, + '', view_types=None, view_classifier=IExceptionViewClassifier, secure=secure, -- cgit v1.2.3 From 51f056043f9286734e93cb61f44bb0163c5b1fa3 Mon Sep 17 00:00:00 2001 From: Steve Piercy <web@stevepiercy.com> Date: Wed, 2 Mar 2016 03:47:56 -0800 Subject: give sentence a colostomy, break into two --- docs/quick_tutorial/view_classes.rst | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/quick_tutorial/view_classes.rst b/docs/quick_tutorial/view_classes.rst index 50a7ee0af..6198eed63 100644 --- a/docs/quick_tutorial/view_classes.rst +++ b/docs/quick_tutorial/view_classes.rst @@ -10,11 +10,10 @@ then move some declarations to the class level. Background ========== -So far our views have been simple, free-standing functions. Many times -your views are related: different ways to look at or work on the same -data or a REST API that handles multiple operations. Grouping these -together as a -:ref:`view class <class_as_view>` makes sense: +So far our views have been simple, free-standing functions. Many times your +views are related to one another. They may be different ways to look at or work +on the same data, or be a REST API that handles multiple operations. Grouping +these views together as a :ref:`view class <class_as_view>` makes sense: - Group views -- cgit v1.2.3 From d5225795b3b98b8ef746ba5f9a807eb701d099bb Mon Sep 17 00:00:00 2001 From: Michael Merickel <michael@merickel.org> Date: Wed, 2 Mar 2016 23:46:37 -0600 Subject: fix and add tests for invoke_exception_view --- pyramid/tests/test_view.py | 132 +++++++++++++++++++++++++++++++++++++++++++++ pyramid/view.py | 7 +-- 2 files changed, 136 insertions(+), 3 deletions(-) diff --git a/pyramid/tests/test_view.py b/pyramid/tests/test_view.py index e6b9f9e7e..2be47e318 100644 --- a/pyramid/tests/test_view.py +++ b/pyramid/tests/test_view.py @@ -673,6 +673,138 @@ class Test_view_defaults(unittest.TestCase): class Bar(Foo): pass self.assertEqual(Bar.__view_defaults__, {}) +class TestViewMethodsMixin(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def _makeOne(self, environ=None): + from pyramid.decorator import reify + from pyramid.view import ViewMethodsMixin + if environ is None: + environ = {} + class Request(ViewMethodsMixin): + def __init__(self, environ): + self.environ = environ + + @reify + def response(self): + return DummyResponse() + request = Request(environ) + request.registry = self.config.registry + return request + + def test_it(self): + def exc_view(exc, request): + self.assertTrue(exc is dummy_exc) + self.assertTrue(request.exception is dummy_exc) + return DummyResponse(b'foo') + self.config.add_view(exc_view, context=RuntimeError) + request = self._makeOne() + dummy_exc = RuntimeError() + try: + raise dummy_exc + except RuntimeError: + response = request.invoke_exception_view() + self.assertEqual(response.app_iter, [b'foo']) + else: # pragma: no cover + self.fail() + + def test_it_hides_attrs(self): + def exc_view(exc, request): + self.assertTrue(exc is not orig_exc) + self.assertTrue(request.exception is not orig_exc) + self.assertTrue(request.exc_info is not orig_exc_info) + self.assertTrue(request.response is not orig_response) + request.response.app_iter = [b'bar'] + return request.response + self.config.add_view(exc_view, context=RuntimeError) + request = self._makeOne() + orig_exc = request.exception = DummyContext() + orig_exc_info = request.exc_info = DummyContext() + orig_response = request.response = DummyResponse(b'foo') + try: + raise RuntimeError + except RuntimeError: + response = request.invoke_exception_view() + self.assertEqual(response.app_iter, [b'bar']) + self.assertTrue(request.exception is orig_exc) + self.assertTrue(request.exc_info is orig_exc_info) + self.assertTrue(request.response is orig_response) + else: # pragma: no cover + self.fail() + + def test_it_supports_alternate_requests(self): + def exc_view(exc, request): + self.assertTrue(request is other_req) + return DummyResponse(b'foo') + self.config.add_view(exc_view, context=RuntimeError) + request = self._makeOne() + other_req = self._makeOne() + try: + raise RuntimeError + except RuntimeError: + response = request.invoke_exception_view(request=other_req) + self.assertEqual(response.app_iter, [b'foo']) + else: # pragma: no cover + self.fail() + + def test_it_supports_threadlocal_registry(self): + def exc_view(exc, request): + return DummyResponse(b'foo') + self.config.add_view(exc_view, context=RuntimeError) + request = self._makeOne() + del request.registry + try: + raise RuntimeError + except RuntimeError: + response = request.invoke_exception_view() + self.assertEqual(response.app_iter, [b'foo']) + else: # pragma: no cover + self.fail() + + def test_it_supports_alternate_exc_info(self): + def exc_view(exc, request): + self.assertTrue(request.exc_info is exc_info) + return DummyResponse(b'foo') + self.config.add_view(exc_view, context=RuntimeError) + request = self._makeOne() + try: + raise RuntimeError + except RuntimeError: + exc_info = sys.exc_info() + response = request.invoke_exception_view(exc_info=exc_info) + self.assertEqual(response.app_iter, [b'foo']) + + def test_it_rejects_secured_view(self): + from pyramid.exceptions import Forbidden + def exc_view(exc, request): pass + self.config.testing_securitypolicy(permissive=False) + self.config.add_view(exc_view, context=RuntimeError, permission='view') + request = self._makeOne() + try: + raise RuntimeError + except RuntimeError: + self.assertRaises(Forbidden, request.invoke_exception_view) + else: # pragma: no cover + self.fail() + + def test_it_allows_secured_view(self): + def exc_view(exc, request): + return DummyResponse(b'foo') + self.config.testing_securitypolicy(permissive=False) + self.config.add_view(exc_view, context=RuntimeError, permission='view') + request = self._makeOne() + try: + raise RuntimeError + except RuntimeError: + response = request.invoke_exception_view(secure=False) + self.assertEqual(response.app_iter, [b'foo']) + else: # pragma: no cover + self.fail() + class ExceptionResponse(Exception): status = '404 Not Found' app_iter = ['Not Found'] diff --git a/pyramid/view.py b/pyramid/view.py index 16b60b77a..9108f120e 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -598,14 +598,15 @@ class ViewMethodsMixin(object): registry = get_current_registry() if exc_info is None: exc_info = sys.exc_info() + exc = exc_info[1] attrs = request.__dict__ - context_iface = providedBy(exc_info[0]) + context_iface = providedBy(exc) # clear old generated request.response, if any; it may # have been mutated by the view, and its state is not # sane (e.g. caching headers) with hide_attrs(request, 'exception', 'exc_info', 'response'): - attrs['exception'] = exc_info[0] + attrs['exception'] = exc attrs['exc_info'] = exc_info # we use .get instead of .__getitem__ below due to # https://github.com/Pylons/pyramid/issues/700 @@ -613,7 +614,7 @@ class ViewMethodsMixin(object): response = _call_view( registry, request, - exc_info[0], + exc, context_iface, '', view_types=None, -- cgit v1.2.3 From 6c36d783bf6d3a289afe559d6595d96de3d99d89 Mon Sep 17 00:00:00 2001 From: Steve Piercy <web@stevepiercy.com> Date: Wed, 2 Mar 2016 22:57:48 -0800 Subject: update link to videos --- docs/designdefense.rst | 8 ++++---- docs/quick_tutorial/routing.rst | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/designdefense.rst b/docs/designdefense.rst index 28da84368..5f3295305 100644 --- a/docs/designdefense.rst +++ b/docs/designdefense.rst @@ -1011,8 +1011,8 @@ Self-described "microframeworks" exist. `Bottle <http://bottle.paws.de>`_ and <http://bobo.digicool.com/>`_ doesn't describe itself as a microframework, but its intended user base is much the same. Many others exist. We've even (only as a teaching tool, not as any sort of official project) `created one using -Pyramid <http://bfg.repoze.org/videos#groundhog1>`_. The videos use BFG, a -precursor to Pyramid, but the resulting code is `available for Pyramid too +Pyramid <http://static.repoze.org/casts/videotags.html>`_. The videos use BFG, +a precursor to Pyramid, but the resulting code is `available for Pyramid too <https://github.com/Pylons/groundhog>`_). Microframeworks are small frameworks with one common feature: each allows its users to create a fully functional application that lives in a single Python file. @@ -1542,7 +1542,7 @@ inlined comments take into account what we've discussed in the server.serve_forever() # explicitly WSGI -Pyramid Doesn't Offer Pluggable Apps +Pyramid doesn't offer pluggable apps ------------------------------------ It is "Pyramidic" to compose multiple external sources into the same @@ -1550,7 +1550,7 @@ configuration using :meth:`~pyramid.config.Configurator.include`. Any number of includes can be done to compose an application; includes can even be done from within other includes. Any directive can be used within an include that can be used outside of one (such as -:meth:`~pyramid.config.Configurator.add_view`, etc). +:meth:`~pyramid.config.Configurator.add_view`). Pyramid has a conflict detection system that will throw an error if two included externals try to add the same configuration in a conflicting way diff --git a/docs/quick_tutorial/routing.rst b/docs/quick_tutorial/routing.rst index 1b79a5889..416a346fa 100644 --- a/docs/quick_tutorial/routing.rst +++ b/docs/quick_tutorial/routing.rst @@ -23,14 +23,14 @@ Previously we saw the basics of routing URLs to views in Pyramid. .. note:: - Why do this twice? Other Python web frameworks let you create a - route and associate it with a view in one step. As - illustrated in :ref:`routes_need_ordering`, multiple routes might match the - same URL pattern. Rather than provide ways to help guess, Pyramid lets you - be explicit in ordering. Pyramid also gives facilities to avoid the - problem. It's relatively easy to build a system that uses implicit route - ordering with Pyramid too. See `The Groundhog series of screencasts - <http://bfg.repoze.org/videos#groundhog1>`_ if you're interested in + Why do this twice? Other Python web frameworks let you create a route and + associate it with a view in one step. As illustrated in + :ref:`routes_need_ordering`, multiple routes might match the same URL + pattern. Rather than provide ways to help guess, Pyramid lets you be + explicit in ordering. Pyramid also gives facilities to avoid the problem. + It's relatively easy to build a system that uses implicit route ordering + with Pyramid too. See `The Groundhog series of screencasts + <http://static.repoze.org/casts/videotags.html>`_ if you're interested in doing so. Objectives -- cgit v1.2.3 From bc092500e047d14a8ca1a97f1abc00a5678748fd Mon Sep 17 00:00:00 2001 From: Michael Merickel <michael@merickel.org> Date: Thu, 3 Mar 2016 20:14:09 -0600 Subject: link invoke_exception_view to api docs --- docs/api/request.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/api/request.rst b/docs/api/request.rst index 105ffb5a7..52bf50078 100644 --- a/docs/api/request.rst +++ b/docs/api/request.rst @@ -13,7 +13,8 @@ current_route_path, static_url, static_path, model_url, resource_url, resource_path, set_property, effective_principals, authenticated_userid, - unauthenticated_userid, has_permission + unauthenticated_userid, has_permission, + invoke_exception_view .. attribute:: context @@ -259,6 +260,8 @@ See also :ref:`subrequest_chapter`. + .. automethod:: invoke_exception_view + .. automethod:: has_permission .. automethod:: add_response_callback -- cgit v1.2.3 From 0076004e2e2fd5de1e8193d8c66107672267d16c Mon Sep 17 00:00:00 2001 From: Michael Merickel <michael@merickel.org> Date: Fri, 4 Mar 2016 01:47:29 -0600 Subject: define deriver api as (view, info) using IViewDeriverInfo - drop the concept of default values from view derivers (we'll later bring this back with add_view_option(name, default=..) - define a new IViewDeriverInfo (mirroring IRendererInfo) which has attributes on it like the predicate list, registry and dictionary of options - define new deriver api as (view, info) --- pyramid/config/derivations.py | 79 +++++++------ pyramid/config/views.py | 161 +++++++++++++++----------- pyramid/interfaces.py | 16 ++- pyramid/tests/test_config/test_derivations.py | 105 ++++------------- 4 files changed, 169 insertions(+), 192 deletions(-) diff --git a/pyramid/config/derivations.py b/pyramid/config/derivations.py index c23e5c479..59efeb0f4 100644 --- a/pyramid/config/derivations.py +++ b/pyramid/config/derivations.py @@ -154,8 +154,8 @@ class DefaultViewMapper(object): def wraps_view(wrapper): - def inner(view, value, **kw): - wrapper_view = wrapper(view, value, **kw) + def inner(view, info): + wrapper_view = wrapper(view, info) return preserve_view_attrs(view, wrapper_view) return inner @@ -193,21 +193,21 @@ def preserve_view_attrs(view, wrapper): return wrapper -def mapped_view(view, value, **kw): - mapper = kw.get('mapper') +def mapped_view(view, info): + mapper = info.options.get('mapper') if mapper is None: mapper = getattr(view, '__view_mapper__', None) if mapper is None: - mapper = kw['registry'].queryUtility(IViewMapperFactory) + mapper = info.registry.queryUtility(IViewMapperFactory) if mapper is None: mapper = DefaultViewMapper - mapped_view = mapper(**kw)(view) + mapped_view = mapper(**info.options)(view) return mapped_view -def owrapped_view(view, value, **kw): - wrapper_viewname = kw.get('wrapper_viewname') - viewname = kw.get('viewname') +def owrapped_view(view, info): + wrapper_viewname = info.options.get('wrapper_viewname') + viewname = info.options.get('viewname') if not wrapper_viewname: return view def _owrapped_view(context, request): @@ -224,11 +224,11 @@ def owrapped_view(view, value, **kw): return wrapped_response return _owrapped_view -def http_cached_view(view, value, **kw): - if kw['registry'].settings.get('prevent_http_cache', False): +def http_cached_view(view, info): + if info.settings.get('prevent_http_cache', False): return view - seconds = kw.get('http_cache') + seconds = info.options.get('http_cache') if seconds is None: return view @@ -253,8 +253,8 @@ def http_cached_view(view, value, **kw): return wrapper -def secured_view(view, value, **kw): - permission = kw.get('permission') +def secured_view(view, info): + permission = info.options.get('permission') if permission == NO_PERMISSION_REQUIRED: # allow views registered within configurations that have a # default permission to explicitly override the default @@ -262,8 +262,8 @@ def secured_view(view, value, **kw): permission = None wrapped_view = view - authn_policy = kw['registry'].queryUtility(IAuthenticationPolicy) - authz_policy = kw['registry'].queryUtility(IAuthorizationPolicy) + authn_policy = info.registry.queryUtility(IAuthenticationPolicy) + authz_policy = info.registry.queryUtility(IAuthorizationPolicy) if authn_policy and authz_policy and (permission is not None): def _permitted(context, request): @@ -285,13 +285,13 @@ def secured_view(view, value, **kw): return wrapped_view -def authdebug_view(view, value, **kw): +def authdebug_view(view, info): wrapped_view = view - settings = kw['registry'].settings - permission = kw.get('permission') - authn_policy = kw['registry'].queryUtility(IAuthenticationPolicy) - authz_policy = kw['registry'].queryUtility(IAuthorizationPolicy) - logger = kw['registry'].queryUtility(IDebugLogger) + settings = info.settings + permission = info.options.get('permission') + authn_policy = info.registry.queryUtility(IAuthenticationPolicy) + authz_policy = info.registry.queryUtility(IAuthorizationPolicy) + logger = info.registry.queryUtility(IDebugLogger) if settings and settings.get('debug_authorization', False): def _authdebug_view(context, request): view_name = getattr(request, 'view_name', None) @@ -322,8 +322,8 @@ def authdebug_view(view, value, **kw): return wrapped_view -def predicated_view(view, value, **kw): - preds = kw.get('predicates', ()) +def predicated_view(view, info): + preds = info.predicates if not preds: return view def predicate_wrapper(context, request): @@ -341,11 +341,11 @@ def predicated_view(view, value, **kw): predicate_wrapper.__predicates__ = preds return predicate_wrapper -def attr_wrapped_view(view, value, **kw): - kw = kw - accept, order, phash = (kw.get('accept', None), - kw.get('order', MAX_ORDER), - kw.get('phash', DEFAULT_PHASH)) +def attr_wrapped_view(view, info): + opts = info.options + accept, order, phash = (opts.get('accept', None), + opts.get('order', MAX_ORDER), + opts.get('phash', DEFAULT_PHASH)) # this is a little silly but we don't want to decorate the original # function with attributes that indicate accept, order, and phash, # so we use a wrapper @@ -360,15 +360,14 @@ def attr_wrapped_view(view, value, **kw): attr_view.__accept__ = accept attr_view.__order__ = order attr_view.__phash__ = phash - attr_view.__view_attr__ = kw.get('attr') - attr_view.__permission__ = kw.get('permission') + attr_view.__view_attr__ = opts.get('attr') + attr_view.__permission__ = opts.get('permission') return attr_view -def rendered_view(view, value, **kw): +def rendered_view(view, info): # one way or another this wrapper must produce a Response (unless # the renderer is a NullRendererHelper) - renderer = kw.get('renderer') - registry = kw['registry'] + renderer = info.options.get('renderer') if renderer is None: # register a default renderer if you want super-dynamic # rendering. registering a default renderer will also allow @@ -379,7 +378,7 @@ def rendered_view(view, value, **kw): if result.__class__ is Response: # common case response = result else: - response = registry.queryAdapterOrSelf(result, IResponse) + response = info.registry.queryAdapterOrSelf(result, IResponse) if response is None: if result is None: append = (' You may have forgotten to return a value ' @@ -410,7 +409,7 @@ def rendered_view(view, value, **kw): else: # this must adapt, it can't do a simple interface check # (avoid trying to render webob responses) - response = registry.queryAdapterOrSelf(result, IResponse) + response = info.registry.queryAdapterOrSelf(result, IResponse) if response is None: attrs = getattr(request, '__dict__', {}) if 'override_renderer' in attrs: @@ -418,8 +417,8 @@ def rendered_view(view, value, **kw): renderer_name = attrs.pop('override_renderer') view_renderer = renderers.RendererHelper( name=renderer_name, - package=kw.get('package'), - registry=registry) + package=info.package, + registry=info.registry) else: view_renderer = renderer.clone() if '__view__' in attrs: @@ -432,8 +431,8 @@ def rendered_view(view, value, **kw): return rendered_view -def decorated_view(view, value, **kw): - decorator = kw.get('decorator') +def decorated_view(view, info): + decorator = info.options.get('decorator') if decorator is None: return view return decorator(view) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 3e27f0bfb..890fd2113 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -27,6 +27,7 @@ from pyramid.interfaces import ( IView, IViewClassifier, IViewDerivers, + IViewDeriverInfo, IViewMapperFactory, PHASE1_CONFIG, ) @@ -42,6 +43,8 @@ from pyramid.compat import ( is_nonstr_iter, ) +from pyramid.decorator import reify + from pyramid.exceptions import ( ConfigurationError, PredicateMismatch, @@ -653,12 +656,17 @@ class ViewsConfiguratorMixin(object): Pass a key/value pair here to use a third-party predicate or set a value for a view derivative option registered via :meth:`pyramid.config.Configurator.add_view_predicate` or - :meth:`pyramid.config.Configurator.add_view_derivation`. More than + :meth:`pyramid.config.Configurator.add_view_option`. More than one key/value pair can be used at the same time. See :ref:`view_and_route_predicates` for more information about third-party predicates. - .. versionadded: 1.7 + .. versionadded: 1.4a1 + + .. versionchanged: 1.7 + + Support setting arbitrary view options. Previously, only + predicate values could be supplied. """ if custom_predicates: @@ -726,21 +734,19 @@ class ViewsConfiguratorMixin(object): introspectables = [] ovals = view_options.copy() - ovals.update( - dict( - xhr=xhr, - request_method=request_method, - path_info=path_info, - request_param=request_param, - header=header, - accept=accept, - containment=containment, - request_type=request_type, - match_param=match_param, - check_csrf=check_csrf, - custom=predvalseq(custom_predicates), - ) - ) + ovals.update(dict( + xhr=xhr, + request_method=request_method, + path_info=path_info, + request_param=request_param, + header=header, + accept=accept, + containment=containment, + request_type=request_type, + match_param=match_param, + check_csrf=check_csrf, + custom=predvalseq(custom_predicates), + )) def discrim_func(): # We need to defer the discriminator until we know what the phash @@ -749,13 +755,10 @@ class ViewsConfiguratorMixin(object): # called. valid_predicates = predlist.names() pvals = {} - options = {} for (k, v) in ovals.items(): if k in valid_predicates: pvals[k] = v - else: - options[k] = v order, preds, phash = predlist.make(self, **pvals) @@ -763,7 +766,6 @@ class ViewsConfiguratorMixin(object): 'phash': phash, 'order': order, 'predicates': preds, - 'options': options }) return ('view', context, name, route_name, phash) @@ -781,26 +783,25 @@ class ViewsConfiguratorMixin(object): discriminator, view_desc, 'view') - view_intr.update( - dict(name=name, - context=context, - containment=containment, - request_param=request_param, - request_methods=request_method, - route_name=route_name, - attr=attr, - xhr=xhr, - accept=accept, - header=header, - path_info=path_info, - match_param=match_param, - check_csrf=check_csrf, - callable=view, - mapper=mapper, - decorator=decorator, - ) - ) - view_intr.update(**view_options) + view_intr.update(dict( + name=name, + context=context, + containment=containment, + request_param=request_param, + request_methods=request_method, + route_name=route_name, + attr=attr, + xhr=xhr, + accept=accept, + header=header, + path_info=path_info, + match_param=match_param, + check_csrf=check_csrf, + callable=view, + mapper=mapper, + decorator=decorator, + )) + view_intr.update(view_options) introspectables.append(view_intr) predlist = self.get_predlist('view') @@ -832,14 +833,12 @@ class ViewsConfiguratorMixin(object): # added by discrim_func above during conflict resolving preds = view_intr['predicates'] - opts = view_intr['options'] order = view_intr['order'] phash = view_intr['phash'] # __no_permission_required__ handled by _secure_view - derived_view = self._apply_view_derivations( + derived_view = self._derive_view( view, - registry=self.registry, permission=permission, predicates=preds, attr=attr, @@ -849,12 +848,11 @@ class ViewsConfiguratorMixin(object): accept=accept, order=order, phash=phash, - package=self.package, - mapper=mapper, decorator=decorator, + mapper=mapper, http_cache=http_cache, - options=opts, - ) + extra_options=view_options, + ) derived_view.__discriminator__ = lambda *arg: discriminator # __discriminator__ is used by superdynamic systems # that require it for introspection after manual view lookup; @@ -1013,19 +1011,20 @@ class ViewsConfiguratorMixin(object): introspectables.append(perm_intr) self.action(discriminator, register, introspectables=introspectables) - def _apply_view_derivations(self, view, **kw): + def _apply_view_derivations(self, info): d = pyramid.config.derivations # These inner derivations have fixed order - inner_derivers = [('mapped_view', (d.mapped_view, None))] + inner_derivers = [('mapped_view', d.mapped_view)] - outer_derivers = [('predicated_view', (d.predicated_view, None)), - ('attr_wrapped_view', (d.attr_wrapped_view, None)),] + outer_derivers = [('predicated_view', d.predicated_view), + ('attr_wrapped_view', d.attr_wrapped_view)] + view = info.orig_view derivers = self.registry.queryUtility(IViewDerivers, default=[]) - for name, val in inner_derivers + derivers.sorted() + outer_derivers: - derivation, default = val - value = kw['options'].get(name, default) - view = wraps_view(derivation)(view, value, **kw) + for name, derivation in ( + inner_derivers + derivers.sorted() + outer_derivers + ): + view = wraps_view(derivation)(view, info) return view @action_method @@ -1076,7 +1075,9 @@ class ViewsConfiguratorMixin(object): self.add_view_predicate(name, factory) @action_method - def add_view_derivation(self, name, factory, default, + def add_view_derivation(self, + name, + factory, under=None, over=None): if under is None and over is None: @@ -1098,9 +1099,7 @@ class ViewsConfiguratorMixin(object): if derivers is None: derivers = TopologicalSorter() self.registry.registerUtility(derivers, IViewDerivers) - derivers.add(name, (factory, default), - after=under, - before=over) + derivers.add(name, factory, after=under, before=over) self.action(discriminator, register, introspectables=(intr,), order=PHASE1_CONFIG) # must be registered early @@ -1115,10 +1114,15 @@ class ViewsConfiguratorMixin(object): ] after = pyramid.util.FIRST for name, deriver in derivers: - self.add_view_derivation(name, deriver, default=None, under=after) + self.add_view_derivation(name, deriver, under=after) after = name - self.add_view_derivation('rendered_view', d.rendered_view, default=None, under=pyramid.util.FIRST, over='decorated_view') + self.add_view_derivation( + 'rendered_view', + d.rendered_view, + under=pyramid.util.FIRST, + over='decorated_view', + ) def derive_view(self, view, attr=None, renderer=None): """ @@ -1199,11 +1203,11 @@ class ViewsConfiguratorMixin(object): return self._derive_view(view, attr=attr, renderer=renderer) # b/w compat - def _derive_view(self, view, permission=None, predicates=(), options=dict(), + def _derive_view(self, view, permission=None, predicates=(), attr=None, renderer=None, wrapper_viewname=None, viewname=None, accept=None, order=MAX_ORDER, phash=DEFAULT_PHASH, decorator=None, - mapper=None, http_cache=None): + mapper=None, http_cache=None, extra_options=None): view = self.maybe_dotted(view) mapper = self.maybe_dotted(mapper) if isinstance(renderer, string_types): @@ -1218,11 +1222,8 @@ class ViewsConfiguratorMixin(object): package=self.package, registry=self.registry) - return self._apply_view_derivations( - view, - registry=self.registry, + options = dict( permission=permission, - predicates=predicates, attr=attr, renderer=renderer, wrapper_viewname=wrapper_viewname, @@ -1230,13 +1231,23 @@ class ViewsConfiguratorMixin(object): accept=accept, order=order, phash=phash, - package=self.package, mapper=mapper, decorator=decorator, http_cache=http_cache, + ) + if extra_options: + options.update(extra_options) + + info = ViewDeriverInfo( + view=view, + registry=self.registry, + package=self.package, + predicates=predicates, options=options, ) + return self._apply_view_derivations(info) + @viewdefaults @action_method def add_forbidden_view( @@ -1624,6 +1635,18 @@ def isexception(o): (inspect.isclass(o) and (issubclass(o, Exception))) ) +@implementer(IViewDeriverInfo) +class ViewDeriverInfo(object): + def __init__(self, view, registry, package, predicates, options): + self.orig_view = view + self.registry = registry + self.package = package + self.predicates = predicates or [] + self.options = options or {} + + @reify + def settings(self): + return self.registry.settings @implementer(IStaticURLInfo) class StaticURLInfo(object): diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index 4c3e87f39..5ae7cfbf8 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -1189,7 +1189,21 @@ class IPredicateList(Interface): class IViewDerivers(Interface): """ Interface for view derivers list """ - + +class IViewDeriverInfo(Interface): + """ An object implementing this interface is passed to every + :term:`view deriver` during configuration.""" + registry = Attribute('The "current" application registry when the ' + 'view was created') + package = Attribute('The "current package" when the view ' + 'configuration statement was found') + settings = Attribute('The deployment settings dictionary related ' + 'to the current application') + options = Attribute('The view options passed to the view, including any ' + 'default values that were not overriden') + predicates = Attribute('The list of predicates active on the view') + orig_view = Attribute('The original view object being wrapped') + class ICacheBuster(Interface): """ A cache buster modifies the URL generation machinery for diff --git a/pyramid/tests/test_config/test_derivations.py b/pyramid/tests/test_config/test_derivations.py index 7095d25bf..1c7a0be07 100644 --- a/pyramid/tests/test_config/test_derivations.py +++ b/pyramid/tests/test_config/test_derivations.py @@ -1097,9 +1097,9 @@ class TestDerivationOrder(unittest.TestCase): def test_right_order_user_sorted(self): from pyramid.interfaces import IViewDerivers - self.config.add_view_derivation('deriv1', None, default=None) - self.config.add_view_derivation('deriv2', None, default=None, over='deriv1') - self.config.add_view_derivation('deriv3', None, default=None, under='deriv2') + self.config.add_view_derivation('deriv1', None) + self.config.add_view_derivation('deriv2', None, over='deriv1') + self.config.add_view_derivation('deriv3', None, under='deriv2') derivers = self.config.registry.queryUtility(IViewDerivers, default=[]) derivers_sorted = derivers.sorted() @@ -1119,9 +1119,9 @@ class TestDerivationOrder(unittest.TestCase): def test_right_order_implicit(self): from pyramid.interfaces import IViewDerivers - self.config.add_view_derivation('deriv1', None, default=None) - self.config.add_view_derivation('deriv2', None, default=None) - self.config.add_view_derivation('deriv3', None, default=None) + self.config.add_view_derivation('deriv1', None) + self.config.add_view_derivation('deriv2', None) + self.config.add_view_derivation('deriv3', None) derivers = self.config.registry.queryUtility(IViewDerivers, default=[]) derivers_sorted = derivers.sorted() @@ -1141,7 +1141,7 @@ class TestDerivationOrder(unittest.TestCase): def test_right_order_over_rendered_view(self): from pyramid.interfaces import IViewDerivers - self.config.add_view_derivation('deriv1', None, default=None, over='rendered_view') + self.config.add_view_derivation('deriv1', None, over='rendered_view') derivers = self.config.registry.queryUtility(IViewDerivers, default=[]) derivers_sorted = derivers.sorted() @@ -1159,9 +1159,9 @@ class TestDerivationOrder(unittest.TestCase): def test_right_order_over_rendered_view_others(self): from pyramid.interfaces import IViewDerivers - self.config.add_view_derivation('deriv1', None, default=None, over='rendered_view') - self.config.add_view_derivation('deriv2', None, default=None) - self.config.add_view_derivation('deriv3', None, default=None) + self.config.add_view_derivation('deriv1', None, over='rendered_view') + self.config.add_view_derivation('deriv2', None) + self.config.add_view_derivation('deriv3', None) derivers = self.config.registry.queryUtility(IViewDerivers, default=[]) derivers_sorted = derivers.sorted() @@ -1192,33 +1192,18 @@ class TestAddDerivation(unittest.TestCase): response.deriv = False view = lambda *arg: response - def deriv(view, value, **kw): + def deriv(view, info): self.assertFalse(response.deriv) - self.assertEqual(value, None) response.deriv = True return view result = self.config._derive_view(view) self.assertFalse(response.deriv) - self.config.add_view_derivation('test_deriv', deriv, default=None) + self.config.add_view_derivation('test_deriv', deriv) result = self.config._derive_view(view) self.assertTrue(response.deriv) - def test_derivation_default(self): - response = DummyResponse() - response.deriv_value = None - test_default = object() - view = lambda *arg: response - - def deriv(view, value, **kw): - response.deriv_value = value - return view - - self.config.add_view_derivation('test_default_deriv', deriv, default=test_default) - result = self.config._derive_view(view) - self.assertEqual(response.deriv_value, test_default) - def test_override_derivation(self): flags = {} @@ -1235,36 +1220,18 @@ class TestAddDerivation(unittest.TestCase): return view view1 = AView() - self.config.add_view_derivation('test_deriv', deriv1, default=None) + self.config.add_view_derivation('test_deriv', deriv1) result = self.config._derive_view(view1) self.assertTrue(flags.get('deriv1')) self.assertFalse(flags.get('deriv2')) flags.clear() view2 = AView() - self.config.add_view_derivation('test_deriv', deriv2, default=None) + self.config.add_view_derivation('test_deriv', deriv2) result = self.config._derive_view(view2) self.assertFalse(flags.get('deriv1')) self.assertTrue(flags.get('deriv2')) - def test_override_derivation_default(self): - response = DummyResponse() - response.deriv_value = None - test_default1 = 'first default' - test_default2 = 'second default' - view = lambda *arg: response - - def deriv(view, value, **kw): - response.deriv_value = value - return view - - self.config.add_view_derivation('test_default_deriv', deriv, default=test_default1) - result = self.config._derive_view(view) - self.assertEqual(response.deriv_value, test_default1) - self.config.add_view_derivation('test_default_deriv', deriv, default=test_default2) - result = self.config._derive_view(view) - self.assertEqual(response.deriv_value, test_default2) - def test_add_multi_derivations_ordered(self): response = DummyResponse() view = lambda *arg: response @@ -1282,9 +1249,9 @@ class TestAddDerivation(unittest.TestCase): response.deriv.append('deriv3') return view - self.config.add_view_derivation('deriv1', deriv1, default=None) - self.config.add_view_derivation('deriv2', deriv2, default=None, over='deriv1') - self.config.add_view_derivation('deriv3', deriv3, default=None, under='deriv2') + self.config.add_view_derivation('deriv1', deriv1) + self.config.add_view_derivation('deriv2', deriv2, over='deriv1') + self.config.add_view_derivation('deriv3', deriv3, under='deriv2') result = self.config._derive_view(view) self.assertEqual(response.deriv, ['deriv2', 'deriv3', 'deriv1']) @@ -1323,16 +1290,16 @@ class TestDerivationIntegration(unittest.TestCase): view = lambda *arg: response response.deriv = [] - def deriv1(view, value, **kw): - response.deriv.append(kw['options']['deriv1']) + def deriv1(view, info): + response.deriv.append(info.options['deriv1']) return view - def deriv2(view, value, **kw): - response.deriv.append(kw['options']['deriv2']) + def deriv2(view, info): + response.deriv.append(info.options['deriv2']) return view - self.config.add_view_derivation('deriv1', deriv1, default=None) - self.config.add_view_derivation('deriv2', deriv2, default=None) + self.config.add_view_derivation('deriv1', deriv1) + self.config.add_view_derivation('deriv2', deriv2) self.config.add_view(view, deriv1='test1', deriv2='test2') self.config.commit() @@ -1342,32 +1309,6 @@ class TestDerivationIntegration(unittest.TestCase): self.assertEqual(wrapper(None, request), response) self.assertEqual(['test1', 'test2'], response.deriv) - def test_view_options_default_or_not(self): - response = DummyResponse() - view = lambda *arg: response - response.deriv = [] - - def deriv1(view, value, **kw): - response.deriv.append(value) - response.deriv.append(kw['options'].get('deriv1', None)) - return view - - def deriv2(view, value, **kw): - response.deriv.append(value) - response.deriv.append(kw['options'].get('deriv2', None)) - return view - - self.config.add_view_derivation('deriv1', deriv1, default=None) - self.config.add_view_derivation('deriv2', deriv2, default='test2') - self.config.add_view(view, deriv1='test1') - self.config.commit() - - wrapper = self._getViewCallable(self.config) - request = self._makeRequest(self.config) - request.method = 'GET' - self.assertEqual(wrapper(None, request), response) - self.assertEqual(['test1', 'test1', 'test2', None], response.deriv) - from zope.interface import implementer from pyramid.interfaces import ( -- cgit v1.2.3 From 3e0b1d5fbbd6f323943b73bf3cb77f9d6073d35c Mon Sep 17 00:00:00 2001 From: Ian Joseph Wilson <ianjosephwilson@gmail.com> Date: Mon, 7 Mar 2016 16:17:50 -0800 Subject: Extra colon in docs breaks code block highlighting. --- pyramid/static.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/static.py b/pyramid/static.py index 4054d5be0..8d1f55406 100644 --- a/pyramid/static.py +++ b/pyramid/static.py @@ -229,7 +229,7 @@ class ManifestCacheBuster(object): By default, it is a JSON-serialized dictionary where the keys are the source asset paths used in calls to - :meth:`~pyramid.request.Request.static_url`. For example:: + :meth:`~pyramid.request.Request.static_url`. For example: .. code-block:: python -- cgit v1.2.3 From a996acc1221fb49319ec916378ca388c899968a7 Mon Sep 17 00:00:00 2001 From: Steve Piercy <web@stevepiercy.com> Date: Mon, 7 Mar 2016 22:59:46 -0800 Subject: Use Python console lexer --- pyramid/static.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/static.py b/pyramid/static.py index 8d1f55406..0965be95c 100644 --- a/pyramid/static.py +++ b/pyramid/static.py @@ -231,7 +231,7 @@ class ManifestCacheBuster(object): source asset paths used in calls to :meth:`~pyramid.request.Request.static_url`. For example: - .. code-block:: python + .. code-block:: pycon >>> request.static_url('myapp:static/css/main.css') "http://www.example.com/static/css/main-678b7c80.css" -- cgit v1.2.3 From 94bad269d735dddeb9eade39341b6993c7127ce6 Mon Sep 17 00:00:00 2001 From: Steve Piercy <web@stevepiercy.com> Date: Tue, 8 Mar 2016 00:22:05 -0800 Subject: rewrite renderer glossary entry to read more easily --- docs/glossary.rst | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/glossary.rst b/docs/glossary.rst index bbc86db41..9657d9219 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -558,12 +558,11 @@ Glossary A popular `Javascript library <http://jquery.org>`_. renderer - A serializer that can be referred to via :term:`view - configuration` which converts a non-:term:`Response` return - values from a :term:`view` into a string (and ultimately a - response). Using a renderer can make writing views that require - templating or other serialization less tedious. See - :ref:`views_which_use_a_renderer` for more information. + A serializer which converts non-:term:`Response` return values from a + :term:`view` into a string, and ultimately into a response, usually + through :term:`view configuration`. Using a renderer can make writing + views that require templating or other serialization, like JSON, less + tedious. See :ref:`views_which_use_a_renderer` for more information. renderer factory A factory which creates a :term:`renderer`. See -- cgit v1.2.3 From e292cca8a2f07090697435b7fcdfb2d827e88f44 Mon Sep 17 00:00:00 2001 From: Michael Merickel <michael@merickel.org> Date: Tue, 8 Mar 2016 23:45:12 -0600 Subject: rename wrapper_viewname and viewname to wrapper and name in deriver options --- pyramid/config/derivations.py | 4 ++-- pyramid/config/views.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyramid/config/derivations.py b/pyramid/config/derivations.py index 59efeb0f4..e6179d35f 100644 --- a/pyramid/config/derivations.py +++ b/pyramid/config/derivations.py @@ -206,8 +206,8 @@ def mapped_view(view, info): return mapped_view def owrapped_view(view, info): - wrapper_viewname = info.options.get('wrapper_viewname') - viewname = info.options.get('viewname') + wrapper_viewname = info.options.get('wrapper') + viewname = info.options.get('name') if not wrapper_viewname: return view def _owrapped_view(context, request): diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 890fd2113..0ffcc81b0 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1226,8 +1226,8 @@ class ViewsConfiguratorMixin(object): permission=permission, attr=attr, renderer=renderer, - wrapper_viewname=wrapper_viewname, - viewname=viewname, + wrapper=wrapper_viewname, + name=viewname, accept=accept, order=order, phash=phash, -- cgit v1.2.3 From dad9505d95824ce65e557d9e29044291ccc3a904 Mon Sep 17 00:00:00 2001 From: Michael Merickel <michael@merickel.org> Date: Tue, 8 Mar 2016 23:46:04 -0600 Subject: remove order and phash from deriver options the order and phash are not technically options and are only passed to make the predicated deriver work which is only a deriver as an implementation detail --- pyramid/config/derivations.py | 11 +++++------ pyramid/config/views.py | 7 +++++-- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/pyramid/config/derivations.py b/pyramid/config/derivations.py index e6179d35f..0e109f425 100644 --- a/pyramid/config/derivations.py +++ b/pyramid/config/derivations.py @@ -342,10 +342,9 @@ def predicated_view(view, info): return predicate_wrapper def attr_wrapped_view(view, info): - opts = info.options - accept, order, phash = (opts.get('accept', None), - opts.get('order', MAX_ORDER), - opts.get('phash', DEFAULT_PHASH)) + accept, order, phash = (info.options.get('accept', None), + getattr(info, 'order', MAX_ORDER), + getattr(info, 'phash', DEFAULT_PHASH)) # this is a little silly but we don't want to decorate the original # function with attributes that indicate accept, order, and phash, # so we use a wrapper @@ -360,8 +359,8 @@ def attr_wrapped_view(view, info): attr_view.__accept__ = accept attr_view.__order__ = order attr_view.__phash__ = phash - attr_view.__view_attr__ = opts.get('attr') - attr_view.__permission__ = opts.get('permission') + attr_view.__view_attr__ = info.options.get('attr') + attr_view.__permission__ = info.options.get('permission') return attr_view def rendered_view(view, info): diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 0ffcc81b0..fbf91f3ae 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1229,8 +1229,6 @@ class ViewsConfiguratorMixin(object): wrapper=wrapper_viewname, name=viewname, accept=accept, - order=order, - phash=phash, mapper=mapper, decorator=decorator, http_cache=http_cache, @@ -1246,6 +1244,11 @@ class ViewsConfiguratorMixin(object): options=options, ) + # order and phash are only necessary for the predicated view and + # are not really view derivation options + info.order = order + info.phash = phash + return self._apply_view_derivations(info) @viewdefaults -- cgit v1.2.3 From a59312683873a27cf7f8cf9961002765f66cde1f Mon Sep 17 00:00:00 2001 From: Michael Merickel <michael@merickel.org> Date: Tue, 8 Mar 2016 23:47:09 -0600 Subject: add new deriver option ``context`` --- pyramid/config/views.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index fbf91f3ae..4a5408ef7 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -842,6 +842,7 @@ class ViewsConfiguratorMixin(object): permission=permission, predicates=preds, attr=attr, + context=context, renderer=renderer, wrapper_viewname=wrapper, viewname=name, @@ -1207,7 +1208,8 @@ class ViewsConfiguratorMixin(object): attr=None, renderer=None, wrapper_viewname=None, viewname=None, accept=None, order=MAX_ORDER, phash=DEFAULT_PHASH, decorator=None, - mapper=None, http_cache=None, extra_options=None): + mapper=None, http_cache=None, context=None, + extra_options=None): view = self.maybe_dotted(view) mapper = self.maybe_dotted(mapper) if isinstance(renderer, string_types): @@ -1223,6 +1225,7 @@ class ViewsConfiguratorMixin(object): registry=self.registry) options = dict( + context=context, permission=permission, attr=attr, renderer=renderer, -- cgit v1.2.3 From a610d0e52d98fcbeb0bacf6eebc61dbaba2d1cf9 Mon Sep 17 00:00:00 2001 From: Michael Merickel <michael@merickel.org> Date: Tue, 8 Mar 2016 23:58:22 -0600 Subject: rename info.orig_view to info.original_view --- pyramid/config/views.py | 5 +++-- pyramid/interfaces.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 4a5408ef7..ac95eaa59 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1020,7 +1020,7 @@ class ViewsConfiguratorMixin(object): outer_derivers = [('predicated_view', d.predicated_view), ('attr_wrapped_view', d.attr_wrapped_view)] - view = info.orig_view + view = info.original_view derivers = self.registry.queryUtility(IViewDerivers, default=[]) for name, derivation in ( inner_derivers + derivers.sorted() + outer_derivers @@ -1225,6 +1225,7 @@ class ViewsConfiguratorMixin(object): registry=self.registry) options = dict( + view=view, context=context, permission=permission, attr=attr, @@ -1644,7 +1645,7 @@ def isexception(o): @implementer(IViewDeriverInfo) class ViewDeriverInfo(object): def __init__(self, view, registry, package, predicates, options): - self.orig_view = view + self.original_view = view self.registry = registry self.package = package self.predicates = predicates or [] diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index 5ae7cfbf8..6791befce 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -1202,7 +1202,7 @@ class IViewDeriverInfo(Interface): options = Attribute('The view options passed to the view, including any ' 'default values that were not overriden') predicates = Attribute('The list of predicates active on the view') - orig_view = Attribute('The original view object being wrapped') + original_view = Attribute('The original view object being wrapped') class ICacheBuster(Interface): """ -- cgit v1.2.3 From ceb1f29815d15537a66bdab67b0deca4206fa4b8 Mon Sep 17 00:00:00 2001 From: Michael Merickel <michael@merickel.org> Date: Wed, 9 Mar 2016 00:03:05 -0600 Subject: rename view derivations to view derivers --- pyramid/config/__init__.py | 2 +- pyramid/config/views.py | 34 +++++++++----------- pyramid/testing.py | 2 +- pyramid/tests/test_config/test_derivations.py | 46 +++++++++++++-------------- 4 files changed, 40 insertions(+), 44 deletions(-) diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index 621796552..553f32c9b 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -378,7 +378,7 @@ class Configurator( self.add_default_response_adapters() self.add_default_renderers() self.add_default_view_predicates() - self.add_default_view_derivations() + self.add_default_view_derivers() self.add_default_route_predicates() if exceptionresponse_view is not None: diff --git a/pyramid/config/views.py b/pyramid/config/views.py index ac95eaa59..918437f03 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1012,7 +1012,7 @@ class ViewsConfiguratorMixin(object): introspectables.append(perm_intr) self.action(discriminator, register, introspectables=introspectables) - def _apply_view_derivations(self, info): + def _apply_view_derivers(self, info): d = pyramid.config.derivations # These inner derivations have fixed order inner_derivers = [('mapped_view', d.mapped_view)] @@ -1076,23 +1076,19 @@ class ViewsConfiguratorMixin(object): self.add_view_predicate(name, factory) @action_method - def add_view_derivation(self, - name, - factory, - under=None, - over=None): + def add_view_deriver(self, name, deriver, under=None, over=None): if under is None and over is None: over = 'decorated_view' - factory = self.maybe_dotted(factory) - discriminator = ('view option', name) + deriver = self.maybe_dotted(deriver) + discriminator = ('view deriver', name) intr = self.introspectable( - '%s derivation' % type, - discriminator, - '%s derivation named %s' % (type, name), - '%s derivation' % type) + 'view derivers', + name, + name, + 'view deriver') intr['name'] = name - intr['factory'] = factory + intr['deriver'] = deriver intr['under'] = under intr['over'] = over def register(): @@ -1100,11 +1096,11 @@ class ViewsConfiguratorMixin(object): if derivers is None: derivers = TopologicalSorter() self.registry.registerUtility(derivers, IViewDerivers) - derivers.add(name, factory, after=under, before=over) + derivers.add(name, deriver, after=under, before=over) self.action(discriminator, register, introspectables=(intr,), order=PHASE1_CONFIG) # must be registered early - def add_default_view_derivations(self): + def add_default_view_derivers(self): d = pyramid.config.derivations derivers = [ ('decorated_view', d.decorated_view), @@ -1115,10 +1111,10 @@ class ViewsConfiguratorMixin(object): ] after = pyramid.util.FIRST for name, deriver in derivers: - self.add_view_derivation(name, deriver, under=after) + self.add_view_deriver(name, deriver, under=after) after = name - self.add_view_derivation( + self.add_view_deriver( 'rendered_view', d.rendered_view, under=pyramid.util.FIRST, @@ -1249,11 +1245,11 @@ class ViewsConfiguratorMixin(object): ) # order and phash are only necessary for the predicated view and - # are not really view derivation options + # are not really view deriver options info.order = order info.phash = phash - return self._apply_view_derivations(info) + return self._apply_view_derivers(info) @viewdefaults @action_method diff --git a/pyramid/testing.py b/pyramid/testing.py index f1be04eec..c25105c6c 100644 --- a/pyramid/testing.py +++ b/pyramid/testing.py @@ -474,8 +474,8 @@ def setUp(registry=None, request=None, hook_zca=True, autocommit=True, # method. config.add_default_renderers() config.add_default_view_predicates() + config.add_default_view_derivers() config.add_default_route_predicates() - config.add_default_view_derivations() config.commit() global have_zca try: diff --git a/pyramid/tests/test_config/test_derivations.py b/pyramid/tests/test_config/test_derivations.py index 1c7a0be07..9eb8598e4 100644 --- a/pyramid/tests/test_config/test_derivations.py +++ b/pyramid/tests/test_config/test_derivations.py @@ -1097,9 +1097,9 @@ class TestDerivationOrder(unittest.TestCase): def test_right_order_user_sorted(self): from pyramid.interfaces import IViewDerivers - self.config.add_view_derivation('deriv1', None) - self.config.add_view_derivation('deriv2', None, over='deriv1') - self.config.add_view_derivation('deriv3', None, under='deriv2') + self.config.add_view_deriver('deriv1', None) + self.config.add_view_deriver('deriv2', None, over='deriv1') + self.config.add_view_deriver('deriv3', None, under='deriv2') derivers = self.config.registry.queryUtility(IViewDerivers, default=[]) derivers_sorted = derivers.sorted() @@ -1119,9 +1119,9 @@ class TestDerivationOrder(unittest.TestCase): def test_right_order_implicit(self): from pyramid.interfaces import IViewDerivers - self.config.add_view_derivation('deriv1', None) - self.config.add_view_derivation('deriv2', None) - self.config.add_view_derivation('deriv3', None) + self.config.add_view_deriver('deriv1', None) + self.config.add_view_deriver('deriv2', None) + self.config.add_view_deriver('deriv3', None) derivers = self.config.registry.queryUtility(IViewDerivers, default=[]) derivers_sorted = derivers.sorted() @@ -1141,7 +1141,7 @@ class TestDerivationOrder(unittest.TestCase): def test_right_order_over_rendered_view(self): from pyramid.interfaces import IViewDerivers - self.config.add_view_derivation('deriv1', None, over='rendered_view') + self.config.add_view_deriver('deriv1', None, over='rendered_view') derivers = self.config.registry.queryUtility(IViewDerivers, default=[]) derivers_sorted = derivers.sorted() @@ -1159,9 +1159,9 @@ class TestDerivationOrder(unittest.TestCase): def test_right_order_over_rendered_view_others(self): from pyramid.interfaces import IViewDerivers - self.config.add_view_derivation('deriv1', None, over='rendered_view') - self.config.add_view_derivation('deriv2', None) - self.config.add_view_derivation('deriv3', None) + self.config.add_view_deriver('deriv1', None, over='rendered_view') + self.config.add_view_deriver('deriv2', None) + self.config.add_view_deriver('deriv3', None) derivers = self.config.registry.queryUtility(IViewDerivers, default=[]) derivers_sorted = derivers.sorted() @@ -1178,7 +1178,7 @@ class TestDerivationOrder(unittest.TestCase): ], dlist) -class TestAddDerivation(unittest.TestCase): +class TestAddDeriver(unittest.TestCase): def setUp(self): self.config = testing.setUp() @@ -1187,7 +1187,7 @@ class TestAddDerivation(unittest.TestCase): self.config = None testing.tearDown() - def test_add_single_derivation(self): + def test_add_single_deriver(self): response = DummyResponse() response.deriv = False view = lambda *arg: response @@ -1199,12 +1199,12 @@ class TestAddDerivation(unittest.TestCase): result = self.config._derive_view(view) self.assertFalse(response.deriv) - self.config.add_view_derivation('test_deriv', deriv) + self.config.add_view_deriver('test_deriv', deriv) result = self.config._derive_view(view) self.assertTrue(response.deriv) - def test_override_derivation(self): + def test_override_deriver(self): flags = {} class AView: @@ -1220,19 +1220,19 @@ class TestAddDerivation(unittest.TestCase): return view view1 = AView() - self.config.add_view_derivation('test_deriv', deriv1) + self.config.add_view_deriver('test_deriv', deriv1) result = self.config._derive_view(view1) self.assertTrue(flags.get('deriv1')) self.assertFalse(flags.get('deriv2')) flags.clear() view2 = AView() - self.config.add_view_derivation('test_deriv', deriv2) + self.config.add_view_deriver('test_deriv', deriv2) result = self.config._derive_view(view2) self.assertFalse(flags.get('deriv1')) self.assertTrue(flags.get('deriv2')) - def test_add_multi_derivations_ordered(self): + def test_add_multi_derivers_ordered(self): response = DummyResponse() view = lambda *arg: response response.deriv = [] @@ -1249,14 +1249,14 @@ class TestAddDerivation(unittest.TestCase): response.deriv.append('deriv3') return view - self.config.add_view_derivation('deriv1', deriv1) - self.config.add_view_derivation('deriv2', deriv2, over='deriv1') - self.config.add_view_derivation('deriv3', deriv3, under='deriv2') + self.config.add_view_deriver('deriv1', deriv1) + self.config.add_view_deriver('deriv2', deriv2, over='deriv1') + self.config.add_view_deriver('deriv3', deriv3, under='deriv2') result = self.config._derive_view(view) self.assertEqual(response.deriv, ['deriv2', 'deriv3', 'deriv1']) -class TestDerivationIntegration(unittest.TestCase): +class TestDeriverIntegration(unittest.TestCase): def setUp(self): self.config = testing.setUp() @@ -1298,8 +1298,8 @@ class TestDerivationIntegration(unittest.TestCase): response.deriv.append(info.options['deriv2']) return view - self.config.add_view_derivation('deriv1', deriv1) - self.config.add_view_derivation('deriv2', deriv2) + self.config.add_view_deriver('deriv1', deriv1) + self.config.add_view_deriver('deriv2', deriv2) self.config.add_view(view, deriv1='test1', deriv2='test2') self.config.commit() -- cgit v1.2.3 From ff8c19258b05ed25824ae4a6ae90e62762df8c74 Mon Sep 17 00:00:00 2001 From: Michael Merickel <michael@merickel.org> Date: Wed, 9 Mar 2016 00:09:31 -0600 Subject: pass the predicate options to the derivers --- pyramid/config/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 918437f03..8b4a89388 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -852,7 +852,7 @@ class ViewsConfiguratorMixin(object): decorator=decorator, mapper=mapper, http_cache=http_cache, - extra_options=view_options, + extra_options=ovals, ) derived_view.__discriminator__ = lambda *arg: discriminator # __discriminator__ is used by superdynamic systems -- cgit v1.2.3 From 46fd86919578221b339a18de90e926ef12e764d3 Mon Sep 17 00:00:00 2001 From: Michael Merickel <michael@merickel.org> Date: Wed, 9 Mar 2016 00:17:01 -0600 Subject: expect derivers it was a bug to try ``[].sorted()`` if no derivers, so just expect some for now as the fallback was unused --- pyramid/config/views.py | 8 ++++---- pyramid/tests/test_config/test_derivations.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 8b4a89388..6511ecd1e 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -751,7 +751,7 @@ class ViewsConfiguratorMixin(object): def discrim_func(): # We need to defer the discriminator until we know what the phash # is. It can't be computed any sooner because thirdparty - # predicates/view derivations may not yet exist when add_view is + # predicates/view derivers may not yet exist when add_view is # called. valid_predicates = predlist.names() pvals = {} @@ -1021,11 +1021,11 @@ class ViewsConfiguratorMixin(object): ('attr_wrapped_view', d.attr_wrapped_view)] view = info.original_view - derivers = self.registry.queryUtility(IViewDerivers, default=[]) - for name, derivation in ( + derivers = self.registry.getUtility(IViewDerivers) + for name, deriver in ( inner_derivers + derivers.sorted() + outer_derivers ): - view = wraps_view(derivation)(view, info) + view = wraps_view(deriver)(view, info) return view @action_method diff --git a/pyramid/tests/test_config/test_derivations.py b/pyramid/tests/test_config/test_derivations.py index 9eb8598e4..ec7ef1434 100644 --- a/pyramid/tests/test_config/test_derivations.py +++ b/pyramid/tests/test_config/test_derivations.py @@ -1101,7 +1101,7 @@ class TestDerivationOrder(unittest.TestCase): self.config.add_view_deriver('deriv2', None, over='deriv1') self.config.add_view_deriver('deriv3', None, under='deriv2') - derivers = self.config.registry.queryUtility(IViewDerivers, default=[]) + derivers = self.config.registry.getUtility(IViewDerivers) derivers_sorted = derivers.sorted() dlist = [d for (d, _) in derivers_sorted] self.assertEqual([ @@ -1123,7 +1123,7 @@ class TestDerivationOrder(unittest.TestCase): self.config.add_view_deriver('deriv2', None) self.config.add_view_deriver('deriv3', None) - derivers = self.config.registry.queryUtility(IViewDerivers, default=[]) + derivers = self.config.registry.getUtility(IViewDerivers) derivers_sorted = derivers.sorted() dlist = [d for (d, _) in derivers_sorted] self.assertEqual([ @@ -1143,7 +1143,7 @@ class TestDerivationOrder(unittest.TestCase): self.config.add_view_deriver('deriv1', None, over='rendered_view') - derivers = self.config.registry.queryUtility(IViewDerivers, default=[]) + derivers = self.config.registry.getUtility(IViewDerivers) derivers_sorted = derivers.sorted() dlist = [d for (d, _) in derivers_sorted] self.assertEqual(['deriv1', @@ -1163,7 +1163,7 @@ class TestDerivationOrder(unittest.TestCase): self.config.add_view_deriver('deriv2', None) self.config.add_view_deriver('deriv3', None) - derivers = self.config.registry.queryUtility(IViewDerivers, default=[]) + derivers = self.config.registry.getUtility(IViewDerivers) derivers_sorted = derivers.sorted() dlist = [d for (d, _) in derivers_sorted] self.assertEqual(['deriv1', -- cgit v1.2.3 From e4b931a67455f14d84d415be49e6596d06cc0a42 Mon Sep 17 00:00:00 2001 From: Michael Merickel <michael@merickel.org> Date: Wed, 9 Mar 2016 00:46:59 -0600 Subject: add options support to view derivers exposed a new IViewDeriver api with an optional ``options`` list to expose support for new kwargs that may be passed to config.add_view --- docs/api/interfaces.rst | 6 ++++++ pyramid/config/derivations.py | 16 ++++++++++++++++ pyramid/config/views.py | 15 +++++++++++++++ pyramid/interfaces.py | 20 ++++++++++++++++++-- pyramid/tests/test_config/test_derivations.py | 11 ++++++++++- pyramid/util.py | 3 +++ 6 files changed, 68 insertions(+), 3 deletions(-) diff --git a/docs/api/interfaces.rst b/docs/api/interfaces.rst index de2a664a4..635d3c5b6 100644 --- a/docs/api/interfaces.rst +++ b/docs/api/interfaces.rst @@ -91,3 +91,9 @@ Other Interfaces .. autointerface:: ICacheBuster :members: + + .. autointerface:: IViewDeriver + :members: + + .. autointerface:: IViewDeriverInfo + :members: diff --git a/pyramid/config/derivations.py b/pyramid/config/derivations.py index 0e109f425..99baf46f9 100644 --- a/pyramid/config/derivations.py +++ b/pyramid/config/derivations.py @@ -205,6 +205,8 @@ def mapped_view(view, info): mapped_view = mapper(**info.options)(view) return mapped_view +mapped_view.options = ('mapper', 'attr') + def owrapped_view(view, info): wrapper_viewname = info.options.get('wrapper') viewname = info.options.get('name') @@ -224,6 +226,8 @@ def owrapped_view(view, info): return wrapped_response return _owrapped_view +owrapped_view.options = ('name', 'wrapper') + def http_cached_view(view, info): if info.settings.get('prevent_http_cache', False): return view @@ -253,6 +257,8 @@ def http_cached_view(view, info): return wrapper +http_cached_view.options = ('http_cache',) + def secured_view(view, info): permission = info.options.get('permission') if permission == NO_PERMISSION_REQUIRED: @@ -285,6 +291,8 @@ def secured_view(view, info): return wrapped_view +secured_view.options = ('permission',) + def authdebug_view(view, info): wrapped_view = view settings = info.settings @@ -322,6 +330,8 @@ def authdebug_view(view, info): return wrapped_view +authdebug_view.options = ('permission',) + def predicated_view(view, info): preds = info.predicates if not preds: @@ -363,6 +373,8 @@ def attr_wrapped_view(view, info): attr_view.__permission__ = info.options.get('permission') return attr_view +attr_wrapped_view.options = ('accept', 'attr', 'permission') + def rendered_view(view, info): # one way or another this wrapper must produce a Response (unless # the renderer is a NullRendererHelper) @@ -430,8 +442,12 @@ def rendered_view(view, info): return rendered_view +rendered_view.options = ('renderer',) + def decorated_view(view, info): decorator = info.options.get('decorator') if decorator is None: return view return decorator(view) + +decorated_view.options = ('decorator',) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 6511ecd1e..c3e5d360e 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -755,10 +755,15 @@ class ViewsConfiguratorMixin(object): # called. valid_predicates = predlist.names() pvals = {} + dvals = {} for (k, v) in ovals.items(): if k in valid_predicates: pvals[k] = v + else: + dvals[k] = v + + self._check_view_options(**dvals) order, preds, phash = predlist.make(self, **pvals) @@ -1012,6 +1017,16 @@ class ViewsConfiguratorMixin(object): introspectables.append(perm_intr) self.action(discriminator, register, introspectables=introspectables) + def _check_view_options(self, **kw): + # we only need to validate deriver options because the predicates + # were checked by the predlist + derivers = self.registry.getUtility(IViewDerivers) + for deriver in derivers.values(): + for opt in getattr(deriver, 'options', []): + kw.pop(opt, None) + if kw: + raise ConfigurationError('Unknown view options: %s' % (kw,)) + def _apply_view_derivers(self, info): d = pyramid.config.derivations # These inner derivations have fixed order diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index 6791befce..692bf3d6d 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -1187,8 +1187,21 @@ class IJSONAdapter(Interface): class IPredicateList(Interface): """ Interface representing a predicate list """ -class IViewDerivers(Interface): - """ Interface for view derivers list """ +class IViewDeriver(Interface): + options = Attribute('An list of supported options to be passed to ' + ':meth:`pyramid.config.Configurator.add_view`. ' + 'This attribute is optional.') + + def __call__(view, info): + """ + Derive a new view from the supplied view. + + View options, package information and registry are available on + ``info``, an instance of :class:`pyramid.interfaces.IViewDeriverInfo`. + + The ``view`` is a callable accepting ``(context, request)``. + + """ class IViewDeriverInfo(Interface): """ An object implementing this interface is passed to every @@ -1204,6 +1217,9 @@ class IViewDeriverInfo(Interface): predicates = Attribute('The list of predicates active on the view') original_view = Attribute('The original view object being wrapped') +class IViewDerivers(Interface): + """ Interface for view derivers list """ + class ICacheBuster(Interface): """ A cache buster modifies the URL generation machinery for diff --git a/pyramid/tests/test_config/test_derivations.py b/pyramid/tests/test_config/test_derivations.py index ec7ef1434..12fcc300a 100644 --- a/pyramid/tests/test_config/test_derivations.py +++ b/pyramid/tests/test_config/test_derivations.py @@ -1293,15 +1293,16 @@ class TestDeriverIntegration(unittest.TestCase): def deriv1(view, info): response.deriv.append(info.options['deriv1']) return view + deriv1.options = ('deriv1',) def deriv2(view, info): response.deriv.append(info.options['deriv2']) return view + deriv2.options = ('deriv2',) self.config.add_view_deriver('deriv1', deriv1) self.config.add_view_deriver('deriv2', deriv2) self.config.add_view(view, deriv1='test1', deriv2='test2') - self.config.commit() wrapper = self._getViewCallable(self.config) request = self._makeRequest(self.config) @@ -1309,6 +1310,14 @@ class TestDeriverIntegration(unittest.TestCase): self.assertEqual(wrapper(None, request), response) self.assertEqual(['test1', 'test2'], response.deriv) + def test_unexpected_view_options(self): + from pyramid.exceptions import ConfigurationError + def deriv1(view, info): pass + self.config.add_view_deriver('deriv1', deriv1) + self.assertRaises( + ConfigurationError, + lambda: self.config.add_view(lambda r: {}, deriv1='test1')) + from zope.interface import implementer from pyramid.interfaces import ( diff --git a/pyramid/util.py b/pyramid/util.py index 0a73cedaf..27140936c 100644 --- a/pyramid/util.py +++ b/pyramid/util.py @@ -380,6 +380,9 @@ class TopologicalSorter(object): self.first = first self.last = last + def values(self): + return self.name2val.values() + def remove(self, name): """ Remove a node from the sort input """ self.names.remove(name) -- cgit v1.2.3 From b4147ba4b34b13eaf32f064e9827da733ff6ab7c Mon Sep 17 00:00:00 2001 From: Michael Merickel <michael@merickel.org> Date: Wed, 9 Mar 2016 01:01:58 -0600 Subject: add "view deriver" to the glossary --- docs/glossary.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/glossary.rst b/docs/glossary.rst index bbc86db41..9e068a18c 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -1092,3 +1092,8 @@ Glossary A technique used when serving a cacheable static asset in order to force a client to query the new version of the asset. See :ref:`cache_busting` for more information. + + view deriver + A view deriver is a composable component of the view pipeline which is + used to create a :term:`view callable`. A view deriver is a callable + implementing the :class:`pyramid.interfaces.IViewDeriver` interface. -- cgit v1.2.3 From 05b2b50706cd303c28233a321ef3dcc2e45486be Mon Sep 17 00:00:00 2001 From: Steve Piercy <web@stevepiercy.com> Date: Sun, 13 Mar 2016 12:32:03 -0700 Subject: minor update to request processing diagram --- docs/_static/pyramid_request_processing.graffle | 60 ++++++++++++------------ docs/_static/pyramid_request_processing.png | Bin 122854 -> 123953 bytes docs/_static/pyramid_request_processing.svg | 2 +- 3 files changed, 31 insertions(+), 31 deletions(-) diff --git a/docs/_static/pyramid_request_processing.graffle b/docs/_static/pyramid_request_processing.graffle index 71319610b..16b360543 100644 --- a/docs/_static/pyramid_request_processing.graffle +++ b/docs/_static/pyramid_request_processing.graffle @@ -53,7 +53,7 @@ <key>Creator</key> <string>Steve Piercy</string> <key>DisplayScale</key> - <string>1 0/72 in = 1 0/72 in</string> + <string>1 0/72 in = 1.0000 in</string> <key>GraphDocumentVersion</key> <integer>8</integer> <key>GraphicsList</key> @@ -84,8 +84,8 @@ <integer>0</integer> <key>Points</key> <array> - <string>{344.41667175292969, 402.88506673894034}</string> - <string>{375.5, 402.27232108797347}</string> + <string>{344.41668319702148, 402.88506673894034}</string> + <string>{375.5, 402.77232108797347}</string> </array> <key>Style</key> <dict> @@ -113,7 +113,7 @@ <key>Tail</key> <dict> <key>ID</key> - <integer>169428</integer> + <integer>169509</integer> </dict> </dict> <dict> @@ -243,11 +243,11 @@ <array> <dict> <key>Bounds</key> - <string>{{238.8333613077798, 284.99999999999994}, {105.66668701171875, 18.656048080136394}}</string> + <string>{{238.74999618530273, 284.99999999999994}, {105.75002924601222, 18.656048080136394}}</string> <key>Class</key> <string>ShapedGraphic</string> <key>ID</key> - <integer>169425</integer> + <integer>169506</integer> <key>Magnets</key> <array> <string>{0, 1}</string> @@ -296,11 +296,11 @@ </dict> <dict> <key>Bounds</key> - <string>{{238.75000762939453, 412.15071036499205}, {105.66666412353516, 18.656048080136394}}</string> + <string>{{238.74999618530273, 412.15071036499205}, {105.66668701171875, 18.656048080136394}}</string> <key>Class</key> <string>ShapedGraphic</string> <key>ID</key> - <integer>169426</integer> + <integer>169507</integer> <key>Magnets</key> <array> <string>{0, 1}</string> @@ -349,11 +349,11 @@ </dict> <dict> <key>Bounds</key> - <string>{{238.75000762939453, 303.65604172230951}, {105.66666412353516, 18.656048080136394}}</string> + <string>{{238.74999618530273, 303.65604172230951}, {105.66668701171875, 18.656048080136394}}</string> <key>Class</key> <string>ShapedGraphic</string> <key>ID</key> - <integer>169427</integer> + <integer>169508</integer> <key>Magnets</key> <array> <string>{0, 1}</string> @@ -402,11 +402,11 @@ </dict> <dict> <key>Bounds</key> - <string>{{238.75000762939453, 393.55704269887212}, {105.66666412353516, 18.656048080136394}}</string> + <string>{{238.74999618530273, 393.55704269887212}, {105.66668701171875, 18.656048080136394}}</string> <key>Class</key> <string>ShapedGraphic</string> <key>ID</key> - <integer>169428</integer> + <integer>169509</integer> <key>Magnets</key> <array> <string>{0, 1}</string> @@ -453,11 +453,11 @@ </dict> <dict> <key>Bounds</key> - <string>{{238.75000762939453, 374.90099016834085}, {105.66666412353516, 18.656048080136394}}</string> + <string>{{238.74999618530273, 374.90099016834085}, {105.66668701171875, 18.656048080136394}}</string> <key>Class</key> <string>ShapedGraphic</string> <key>ID</key> - <integer>169429</integer> + <integer>169510</integer> <key>Magnets</key> <array> <string>{0, 1}</string> @@ -504,11 +504,11 @@ </dict> <dict> <key>Bounds</key> - <string>{{238.75000762939453, 341.36561209044055}, {105.66666412353516, 33.089282989501953}}</string> + <string>{{238.74999618530273, 341.36561209044055}, {105.66668701171875, 33.089282989501953}}</string> <key>Class</key> <string>ShapedGraphic</string> <key>ID</key> - <integer>169430</integer> + <integer>169511</integer> <key>Magnets</key> <array> <string>{0, 1}</string> @@ -555,11 +555,11 @@ </dict> <dict> <key>Bounds</key> - <string>{{238.75000762939453, 322.26348241170439}, {105.66666412353516, 18.656048080136394}}</string> + <string>{{238.74999618530273, 322.26348241170439}, {105.66668701171875, 18.656048080136394}}</string> <key>Class</key> <string>ShapedGraphic</string> <key>ID</key> - <integer>169431</integer> + <integer>169512</integer> <key>Magnets</key> <array> <string>{0, 1}</string> @@ -606,7 +606,7 @@ </dict> </array> <key>ID</key> - <integer>169424</integer> + <integer>169505</integer> <key>Layer</key> <integer>0</integer> </dict> @@ -1094,7 +1094,7 @@ <integer>0</integer> <key>Points</key> <array> - <string>{238.75000762939462, 430.80675844512831}</string> + <string>{238.74999618530282, 430.80675844512831}</string> <string>{207.66666666666765, 385.656005859375}</string> </array> <key>Style</key> @@ -1123,7 +1123,7 @@ <key>Tail</key> <dict> <key>ID</key> - <integer>169426</integer> + <integer>169507</integer> <key>Info</key> <integer>6</integer> </dict> @@ -1144,7 +1144,7 @@ <integer>0</integer> <key>Points</key> <array> - <string>{239.33336141608385, 285.57837549845181}</string> + <string>{239.25039065750093, 285.57837549845181}</string> <string>{207.66666666666777, 353.07514659563753}</string> </array> <key>Style</key> @@ -1173,7 +1173,7 @@ <key>Tail</key> <dict> <key>ID</key> - <integer>169425</integer> + <integer>169506</integer> <key>Info</key> <integer>6</integer> </dict> @@ -1515,7 +1515,7 @@ {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc -\f0\fs20 \cf0 view}</string> +\f0\fs20 \cf0 view deriver}</string> <key>VerticalPad</key> <integer>0</integer> </dict> @@ -1742,7 +1742,7 @@ </dict> <dict> <key>Bounds</key> - <string>{{375.5, 391}, {105.66666412353516, 22.544642175946908}}</string> + <string>{{375.5, 391.5}, {105.66666412353516, 22.544642175946908}}</string> <key>Class</key> <string>ShapedGraphic</string> <key>ID</key> @@ -9637,7 +9637,7 @@ <key>MasterSheets</key> <array/> <key>ModificationDate</key> - <string>2014-11-23 07:19:11 +0000</string> + <string>2016-03-13 08:04:48 +0000</string> <key>Modifier</key> <string>Steve Piercy</string> <key>NotesVisible</key> @@ -9732,15 +9732,15 @@ <key>SidebarWidth</key> <integer>163</integer> <key>VisibleRegion</key> - <string>{{-231, -226}, {1037, 1186}}</string> + <string>{{152.25, 226.5}, {255.75, 292.75}}</string> <key>Zoom</key> - <real>1</real> + <real>4</real> <key>ZoomValues</key> <array> <array> <string>Request Processing</string> - <real>1</real> - <real>2</real> + <real>4</real> + <real>8</real> </array> </array> </dict> diff --git a/docs/_static/pyramid_request_processing.png b/docs/_static/pyramid_request_processing.png index 2fbb1e164..c684255fa 100644 Binary files a/docs/_static/pyramid_request_processing.png and b/docs/_static/pyramid_request_processing.png differ diff --git a/docs/_static/pyramid_request_processing.svg b/docs/_static/pyramid_request_processing.svg index 21bbcb532..d32d5c5bc 100644 --- a/docs/_static/pyramid_request_processing.svg +++ b/docs/_static/pyramid_request_processing.svg @@ -1,3 +1,3 @@ <?xml version="1.0"?> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> -<svg xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="91 11 424 533" width="424pt" height="533pt"><metadata xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:date>2014-11-23 07:19Z</dc:date><!-- Produced by OmniGraffle Professional 5.4.4 --></metadata><defs><marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="SharpArrow_Marker" viewBox="-4 -4 10 8" markerWidth="10" markerHeight="8" color="#191919"><g><path d="M 5 0 L -3 -3 L 0 0 L 0 0 L -3 3 Z" fill="currentColor" stroke="currentColor" stroke-width="1"/></g></marker><font-face font-family="Helvetica" font-size="10" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.316406" slope="0" x-height="522.94922" cap-height="717.28516" ascent="770.01953" descent="-229.98047" font-weight="500"><font-face-src><font-face-name name="Helvetica"/></font-face-src></font-face><font-face font-family="Helvetica" font-size="12" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.316406" slope="0" x-height="532.22656" cap-height="719.72656" ascent="770.01953" descent="-229.98047" font-weight="bold"><font-face-src><font-face-name name="Helvetica-Bold"/></font-face-src></font-face><font-face font-family="Helvetica" font-size="10" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.316406" slope="0" x-height="532.22656" cap-height="719.72656" ascent="770.01953" descent="-229.98047" font-weight="bold"><font-face-src><font-face-name name="Helvetica-Bold"/></font-face-src></font-face></defs><g stroke="none" stroke-opacity="1" stroke-dasharray="none" fill="none" fill-opacity="1"><title>Request Processingno exceptionsmiddleware ingress tween ingresstraversalContextFoundtween egressresponse callbacksfinished callbacksmiddleware egressBeforeRenderRequest ProcessingLegendeventcallbackviewexternal process (middleware, tween)internal processview pipelinepredicatesview lookuproute predicatesURL dispatchNewRequestNewResponseview mapper ingressviewview mapper egressresponse adapterdecorators ingressdecorators egressauthorization +2016-03-13 08:04ZRequest Processingno exceptionsmiddleware ingress tween ingresstraversalContextFoundtween egressresponse callbacksfinished callbacksmiddleware egressBeforeRenderRequest ProcessingLegendeventcallbackview deriverexternal process (middleware, tween)internal processview pipelinepredicatesview lookuproute predicatesURL dispatchNewRequestNewResponseview mapper ingressviewview mapper egressresponse adapterdecorators ingressdecorators egressauthorization -- cgit v1.2.3 From 7fc181d89e9553b152618066490d1fb659d45e13 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 13 Mar 2016 16:24:53 -0500 Subject: add examples of view derivers to the glossary --- docs/glossary.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/glossary.rst b/docs/glossary.rst index 9e068a18c..8928254f7 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -1097,3 +1097,5 @@ Glossary A view deriver is a composable component of the view pipeline which is used to create a :term:`view callable`. A view deriver is a callable implementing the :class:`pyramid.interfaces.IViewDeriver` interface. + Examples of builtin derivers including view mapper, the permission + checker, and applying a renderer to a dictionary returned from the view. -- cgit v1.2.3 From 4d4688b7053ddfcfd91b36bf9504c1db76a92763 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 14 Mar 2016 13:27:14 -0500 Subject: add changelog for #2393 --- CHANGES.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 3a1305c95..a17f4aab5 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -21,6 +21,12 @@ unreleased ``request.params['=abc'] == '1'``. See https://github.com/Pylons/pyramid/pull/1370 +- A new ``request.invoke_exception_view(...)`` method which can be used to + invoke an exception view and get back a response. This is useful for + rendering an exception view outside of the context of the excview tween + where you may need more control over the request. + See https://github.com/Pylons/pyramid/pull/2393 + 1.6 (2015-04-14) ================ -- cgit v1.2.3 From 542f86ba5cd3e2eec18224b2e4eff88fe58b6f43 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 14 Mar 2016 14:20:26 -0500 Subject: add a new docs section on invoking exception views --- docs/narr/subrequest.rst | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/docs/narr/subrequest.rst b/docs/narr/subrequest.rst index daa3cc43f..ae1dd1b91 100644 --- a/docs/narr/subrequest.rst +++ b/docs/narr/subrequest.rst @@ -279,3 +279,49 @@ within a tween, because tweens already, by definition, have access to a function that will cause a subrequest (they are passed a ``handle`` function). It's fine to invoke :meth:`~pyramid.request.Request.invoke_subrequest` from within an event handler, however. + +Invoking an Exception View +-------------------------- + +.. versionadded:: 1.7 + +:app:`Pyramid` apps may define a :term:`exception views ` which +can handle any raised exceptions that escape from your code while processing +a request. By default, an unhandled exception will be caught by the `EXCVIEW` +:term:`tween` which will then lookup an exception view that can handle the +exception type, generating an appropriate error response. + +In :app:`Pyramid` 1.7 the :meth:`pyramid.request.Request.invoke_exception_view` +was introduced which allows a user to invoke an exception view while manually +handling an exception. This can be useful in a few different circumstances: + +- Manually handling an exception losing the current call stack / flow. + +- Handling exceptions outside of the context of the `EXCVIEW` tween. The + tween only covers certain parts of the request processing pipeline + (See :ref:`router chapter`) and there are some corner cases where an + exception can be raised which will still bubble up to middleware and possibly + to the web server where a generic ``500 Internal Server Error`` will be + returned to the client. + +Below is an example usage of +:meth:`pyramid.request.Request.invoke_exception_view`: + +.. code-block:: python + :linenos: + + def foo(request): + try: + some_func_that_errors() + return response + except Exception: + response = request.invoke_exception_view() + # there is no exception view for this exception, simply + # re-raise and let someone else handle it + if response is not None: + return response + else: + raise + +Please note that in most cases you do not need to write code like this an may +rely on the `EXCVIEW` tween to handle this for you. -- cgit v1.2.3 From 78cf75aef700f2db65d3dac379811c7f17eab1f7 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 14 Mar 2016 14:28:18 -0500 Subject: fix broken ref --- docs/narr/subrequest.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/narr/subrequest.rst b/docs/narr/subrequest.rst index ae1dd1b91..60972b156 100644 --- a/docs/narr/subrequest.rst +++ b/docs/narr/subrequest.rst @@ -299,7 +299,7 @@ handling an exception. This can be useful in a few different circumstances: - Handling exceptions outside of the context of the `EXCVIEW` tween. The tween only covers certain parts of the request processing pipeline - (See :ref:`router chapter`) and there are some corner cases where an + (See :ref:`router_chapter`) and there are some corner cases where an exception can be raised which will still bubble up to middleware and possibly to the web server where a generic ``500 Internal Server Error`` will be returned to the client. -- cgit v1.2.3 From 7175a51bec9491ff58a8ef3a94cc7804b476541d Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 14 Mar 2016 14:32:14 -0500 Subject: move comment closer to relevant logic --- docs/narr/subrequest.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/narr/subrequest.rst b/docs/narr/subrequest.rst index 60972b156..a943dca5d 100644 --- a/docs/narr/subrequest.rst +++ b/docs/narr/subrequest.rst @@ -316,11 +316,11 @@ Below is an example usage of return response except Exception: response = request.invoke_exception_view() - # there is no exception view for this exception, simply - # re-raise and let someone else handle it if response is not None: return response else: + # there is no exception view for this exception, simply + # re-raise and let someone else handle it raise Please note that in most cases you do not need to write code like this an may -- cgit v1.2.3 From 3b2c22da7117086cc2fd2978d1964622a24f57e2 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Mon, 14 Mar 2016 13:39:06 -0700 Subject: remove extra period --- pyramid/view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/view.py b/pyramid/view.py index 9108f120e..0129526ce 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -589,7 +589,7 @@ class ViewMethodsMixin(object): ``True`` for ``secure``. This method returns a :term:`response` object or ``None`` if no - matching exception view can be found..""" + matching exception view can be found.""" if request is None: request = self -- cgit v1.2.3 From dc3f79537f6901cf81a25f557c8d7bad1fe4f111 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Mon, 14 Mar 2016 13:56:08 -0700 Subject: polish Invoking an Exception View docs - add index entry - minor grammar, syntax --- docs/narr/subrequest.rst | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/docs/narr/subrequest.rst b/docs/narr/subrequest.rst index a943dca5d..7c847de50 100644 --- a/docs/narr/subrequest.rst +++ b/docs/narr/subrequest.rst @@ -280,28 +280,32 @@ function that will cause a subrequest (they are passed a ``handle`` function). It's fine to invoke :meth:`~pyramid.request.Request.invoke_subrequest` from within an event handler, however. + +.. index:: + pair: subrequest; exception view + Invoking an Exception View -------------------------- .. versionadded:: 1.7 -:app:`Pyramid` apps may define a :term:`exception views ` which +:app:`Pyramid` apps may define :term:`exception views ` which can handle any raised exceptions that escape from your code while processing -a request. By default, an unhandled exception will be caught by the `EXCVIEW` -:term:`tween` which will then lookup an exception view that can handle the +a request. By default an unhandled exception will be caught by the ``EXCVIEW`` +:term:`tween`, which will then lookup an exception view that can handle the exception type, generating an appropriate error response. In :app:`Pyramid` 1.7 the :meth:`pyramid.request.Request.invoke_exception_view` -was introduced which allows a user to invoke an exception view while manually +was introduced, allowing a user to invoke an exception view while manually handling an exception. This can be useful in a few different circumstances: -- Manually handling an exception losing the current call stack / flow. +- Manually handling an exception losing the current call stack or flow. -- Handling exceptions outside of the context of the `EXCVIEW` tween. The - tween only covers certain parts of the request processing pipeline - (See :ref:`router_chapter`) and there are some corner cases where an - exception can be raised which will still bubble up to middleware and possibly - to the web server where a generic ``500 Internal Server Error`` will be +- Handling exceptions outside of the context of the ``EXCVIEW`` tween. The + tween only covers certain parts of the request processing pipeline (See + :ref:`router_chapter`). There are also some corner cases where an exception + can be raised that will still bubble up to middleware, and possibly to the + web server in which case a generic ``500 Internal Server Error`` will be returned to the client. Below is an example usage of @@ -323,5 +327,5 @@ Below is an example usage of # re-raise and let someone else handle it raise -Please note that in most cases you do not need to write code like this an may -rely on the `EXCVIEW` tween to handle this for you. +Please note that in most cases you do not need to write code like this, and you +may rely on the ``EXCVIEW`` tween to handle this for you. -- cgit v1.2.3 From fdd1f8352fc341bc60e0b7d32dadd2b4109a2b41 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 14 Mar 2016 22:08:04 -0500 Subject: first cut at documenting view derivers --- docs/narr/extconfig.rst | 1 + docs/narr/hooks.rst | 140 ++++++++++++++++++++++++++ pyramid/config/views.py | 14 +-- pyramid/tests/test_config/test_derivations.py | 13 +-- 4 files changed, 153 insertions(+), 15 deletions(-) diff --git a/docs/narr/extconfig.rst b/docs/narr/extconfig.rst index fee8d0d3a..af7d0a349 100644 --- a/docs/narr/extconfig.rst +++ b/docs/narr/extconfig.rst @@ -259,6 +259,7 @@ Pre-defined Phases - :meth:`pyramid.config.Configurator.add_route_predicate` - :meth:`pyramid.config.Configurator.add_subscriber_predicate` - :meth:`pyramid.config.Configurator.add_view_predicate` +- :meth:`pyramid.config.Configurator.add_view_deriver` - :meth:`pyramid.config.Configurator.set_authorization_policy` - :meth:`pyramid.config.Configurator.set_default_permission` - :meth:`pyramid.config.Configurator.set_view_mapper` diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index 7ff119b53..13daed1fc 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -1547,3 +1547,143 @@ in every subscriber registration. It is not the responsibility of the predicate author to make every predicate make sense for every event type; it is the responsibility of the predicate consumer to use predicates that make sense for a particular event type registration. + +.. index:: + single: view derivers + +.. _view_derivers: + +View Derivers +------------- + +Every URL processed by :app:`Pyramid` is matched against a custom view +pipeline. See :ref:`router_chapter` for how this works. The view pipeline +itself is built from the user-supplied :term:`view callable` which is then +composed with :term:`view derivers `. A view deriver is a +composable element of the view pipeline which is used to wrap a view with +added functionality. View derivers are very similar to the ``decorator`` +argument to :meth:`pyramid.config.Configurator.add_view` except that they have +the option to execute for every view in the application. + +Built-in View Derivers +~~~~~~~~~~~~~~~~~~~~~~ + +There are several builtin view derivers that :app:`Pyramid` will automatically +apply to any view. They are defined in order from closest to furthest from +the user-defined :term:`view callable`: + +``mapped_view`` + + Applies the :term:`view mapper` defined by the ``mapper`` option or the + application's default view mapper to the :term:`view callable`. This + is always the closest deriver to the user-defined view and standardizes the + view pipeline interface to accept ``(context, request)`` from all previous + view derivers. + +``decorated_view`` + + Wraps the view with the decorators from the ``decorator`` option. + +``http_cached_view`` + + Applies cache control headers to the response defined by the ``http_cache`` + option. This element is a noop if the ``pyramid.prevent_http_cache`` setting + is enabled or the ``http_cache`` option is ``None``. + +``owrapped_view`` + + Invokes the wrapped view defined by the ``wrapper`` option. + +``secured_view`` + + Enforce the ``permission`` defined on the view. This element is a noop if + no permission is defined. Note there will always be a permission defined + if a default permission was assigned via + :meth:`pyramid.config.Configurator.set_default_permission`. + +``authdebug_view`` + + Used to output useful debugging information when + ``pyramid.debug_authorization`` is enabled. This element is a noop otherwise. + +Custom View Derivers +~~~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 1.7 + +It is possible to define custom view derivers which will affect all views in +an application. There are many uses for this but most will likely be centered +around monitoring and security. In order to register a custom +:term:`view deriver` you should create a callable that conforms to the +:class:`pyramid.interfaces.IViewDeriver` interface. For example, below +is a callable that can provide timing information for the view pipeline: + +.. code-block:: python + :linenos: + + import time + + def timing_view(view, info): + def wrapper_view(context, request): + start = time.time() + response = view(context, request) + end = time.time() + response.headers['X-View-Performance'] = '%.3f' % (end - start,) + return wrapper_view + + config.add_view_deriver('timing_view', timing_view) + +View derivers are unique in that they have access to most of the options +passed to :meth:`pyramid.config.Configurator.add_view` in order to decide what +to do and they have a chance to affect every view in the application. + +Let's look at one more example which will protect views by requiring a CSRF +token unless ``disable_csrf=True`` is passed to the view: + +.. code-block:: python + :linenos: + + from pyramid.session import check_csrf_token + + def require_csrf_view(view, info): + wrapper_view = view + if not info.options.get('disable_csrf', False): + def wrapper_view(context, request): + if request.method == 'POST': + check_csrf_token(request) + return view(context, request) + return wrapper_view + + require_csrf_view.options = ('disable_csrf',) + + config.add_view_deriver('require_csrf_view', require_csrf_view) + + def myview(request): + return 'protected' + + def my_unprotected_view(request): + return 'unprotected' + + config.add_view(myview, name='safe', renderer='string') + config.add_view(my_unprotected_, name='unsafe', disable_csrf=True, renderer='string') + +Navigating to ``/safe`` with a POST request will then fail when the call to +:func:`pyramid.session.check_csrf_token` raises a +:class:`pyramid.exceptions.BadCSRFToken` exception. However, ``/unsafe`` will +not error. + +Ordering View Derivers +~~~~~~~~~~~~~~~~~~~~~~ + +By default, every new view deriver is added between the ``decorated_view`` +and ``mapped_view`` built-in derivers. It is possible to customize this +ordering using the ``over`` and ``under`` options. Each option can use the +names of other view derivers in order to specify an ordering. There should +rarely be a reason to worry about the ordering of the derivers. + +It is not possible to add a deriver OVER the ``mapped_view`` as the +:term:`view mapper` is intimately tied to the signature of the user-defined +:term:`view callable`. If you simply need to know what the original view +callable was, it can be found as ``info.original_view`` on the provided +:class:`pyramid.interfaces.IViewDeriverInfo` object passed to every view +deriver. diff --git a/pyramid/config/views.py b/pyramid/config/views.py index c3e5d360e..b762a711c 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -654,19 +654,19 @@ class ViewsConfiguratorMixin(object): view_options: Pass a key/value pair here to use a third-party predicate or set a - value for a view derivative option registered via - :meth:`pyramid.config.Configurator.add_view_predicate` or - :meth:`pyramid.config.Configurator.add_view_option`. More than - one key/value pair can be used at the same time. See + value for a view deriver. See + :meth:`pyramid.config.Configurator.add_view_predicate` and + :meth:`pyramid.config.Configurator.add_view_deriver`. See :ref:`view_and_route_predicates` for more information about - third-party predicates. + third-party predicates and :ref:`view_derivers` for information + about view derivers. .. versionadded: 1.4a1 .. versionchanged: 1.7 - Support setting arbitrary view options. Previously, only - predicate values could be supplied. + Support setting view deriver options. Previously, only custom + view predicate values could be supplied. """ if custom_predicates: diff --git a/pyramid/tests/test_config/test_derivations.py b/pyramid/tests/test_config/test_derivations.py index 12fcc300a..7202c5bb6 100644 --- a/pyramid/tests/test_config/test_derivations.py +++ b/pyramid/tests/test_config/test_derivations.py @@ -1,7 +1,12 @@ import unittest +from zope.interface import implementer from pyramid import testing from pyramid.exceptions import ConfigurationError +from pyramid.interfaces import ( + IResponse, + IRequest, + ) class TestDeriveView(unittest.TestCase): @@ -1318,13 +1323,6 @@ class TestDeriverIntegration(unittest.TestCase): ConfigurationError, lambda: self.config.add_view(lambda r: {}, deriv1='test1')) - -from zope.interface import implementer -from pyramid.interfaces import ( - IResponse, - IRequest, - ) - @implementer(IResponse) class DummyResponse(object): content_type = None @@ -1374,4 +1372,3 @@ def assert_similar_datetime(one, two): two_attr = getattr(two, attr) if not one_attr == two_attr: # pragma: no cover raise AssertionError('%r != %r in %s' % (one_attr, two_attr, attr)) - -- cgit v1.2.3 From cbf686706bcc8e471977185a6b27678b924f4dc2 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 14 Mar 2016 22:33:33 -0500 Subject: swap the order of view deriver arguments so that the name is implicit --- pyramid/config/views.py | 14 +++++--- pyramid/tests/test_config/test_derivations.py | 47 ++++++++++++++++----------- 2 files changed, 37 insertions(+), 24 deletions(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index b762a711c..5455c29df 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1091,11 +1091,15 @@ class ViewsConfiguratorMixin(object): self.add_view_predicate(name, factory) @action_method - def add_view_deriver(self, name, deriver, under=None, over=None): + def add_view_deriver(self, deriver, name=None, under=None, over=None): + deriver = self.maybe_dotted(deriver) + if under is None and over is None: over = 'decorated_view' - deriver = self.maybe_dotted(deriver) + if name is None: + name = deriver.__name__ + discriminator = ('view deriver', name) intr = self.introspectable( 'view derivers', @@ -1113,7 +1117,7 @@ class ViewsConfiguratorMixin(object): self.registry.registerUtility(derivers, IViewDerivers) derivers.add(name, deriver, after=under, before=over) self.action(discriminator, register, introspectables=(intr,), - order=PHASE1_CONFIG) # must be registered early + order=PHASE1_CONFIG) # must be registered before add_view def add_default_view_derivers(self): d = pyramid.config.derivations @@ -1126,12 +1130,12 @@ class ViewsConfiguratorMixin(object): ] after = pyramid.util.FIRST for name, deriver in derivers: - self.add_view_deriver(name, deriver, under=after) + self.add_view_deriver(deriver, name=name, under=after) after = name self.add_view_deriver( - 'rendered_view', d.rendered_view, + name='rendered_view', under=pyramid.util.FIRST, over='decorated_view', ) diff --git a/pyramid/tests/test_config/test_derivations.py b/pyramid/tests/test_config/test_derivations.py index 7202c5bb6..e1b6c8bc9 100644 --- a/pyramid/tests/test_config/test_derivations.py +++ b/pyramid/tests/test_config/test_derivations.py @@ -1102,9 +1102,9 @@ class TestDerivationOrder(unittest.TestCase): def test_right_order_user_sorted(self): from pyramid.interfaces import IViewDerivers - self.config.add_view_deriver('deriv1', None) - self.config.add_view_deriver('deriv2', None, over='deriv1') - self.config.add_view_deriver('deriv3', None, under='deriv2') + self.config.add_view_deriver(None, 'deriv1') + self.config.add_view_deriver(None, 'deriv2', over='deriv1') + self.config.add_view_deriver(None, 'deriv3', under='deriv2') derivers = self.config.registry.getUtility(IViewDerivers) derivers_sorted = derivers.sorted() @@ -1124,9 +1124,9 @@ class TestDerivationOrder(unittest.TestCase): def test_right_order_implicit(self): from pyramid.interfaces import IViewDerivers - self.config.add_view_deriver('deriv1', None) - self.config.add_view_deriver('deriv2', None) - self.config.add_view_deriver('deriv3', None) + self.config.add_view_deriver(None, 'deriv1') + self.config.add_view_deriver(None, 'deriv2') + self.config.add_view_deriver(None, 'deriv3') derivers = self.config.registry.getUtility(IViewDerivers) derivers_sorted = derivers.sorted() @@ -1146,7 +1146,7 @@ class TestDerivationOrder(unittest.TestCase): def test_right_order_over_rendered_view(self): from pyramid.interfaces import IViewDerivers - self.config.add_view_deriver('deriv1', None, over='rendered_view') + self.config.add_view_deriver(None, 'deriv1', over='rendered_view') derivers = self.config.registry.getUtility(IViewDerivers) derivers_sorted = derivers.sorted() @@ -1164,9 +1164,9 @@ class TestDerivationOrder(unittest.TestCase): def test_right_order_over_rendered_view_others(self): from pyramid.interfaces import IViewDerivers - self.config.add_view_deriver('deriv1', None, over='rendered_view') - self.config.add_view_deriver('deriv2', None) - self.config.add_view_deriver('deriv3', None) + self.config.add_view_deriver(None, 'deriv1', over='rendered_view') + self.config.add_view_deriver(None, 'deriv2') + self.config.add_view_deriver(None, 'deriv3') derivers = self.config.registry.getUtility(IViewDerivers) derivers_sorted = derivers.sorted() @@ -1204,7 +1204,7 @@ class TestAddDeriver(unittest.TestCase): result = self.config._derive_view(view) self.assertFalse(response.deriv) - self.config.add_view_deriver('test_deriv', deriv) + self.config.add_view_deriver(deriv, 'test_deriv') result = self.config._derive_view(view) self.assertTrue(response.deriv) @@ -1225,14 +1225,14 @@ class TestAddDeriver(unittest.TestCase): return view view1 = AView() - self.config.add_view_deriver('test_deriv', deriv1) + self.config.add_view_deriver(deriv1, 'test_deriv') result = self.config._derive_view(view1) self.assertTrue(flags.get('deriv1')) self.assertFalse(flags.get('deriv2')) flags.clear() view2 = AView() - self.config.add_view_deriver('test_deriv', deriv2) + self.config.add_view_deriver(deriv2, 'test_deriv') result = self.config._derive_view(view2) self.assertFalse(flags.get('deriv1')) self.assertTrue(flags.get('deriv2')) @@ -1254,12 +1254,21 @@ class TestAddDeriver(unittest.TestCase): response.deriv.append('deriv3') return view - self.config.add_view_deriver('deriv1', deriv1) - self.config.add_view_deriver('deriv2', deriv2, over='deriv1') - self.config.add_view_deriver('deriv3', deriv3, under='deriv2') + self.config.add_view_deriver(deriv1, 'deriv1') + self.config.add_view_deriver(deriv2, 'deriv2', over='deriv1') + self.config.add_view_deriver(deriv3, 'deriv3', under='deriv2') result = self.config._derive_view(view) self.assertEqual(response.deriv, ['deriv2', 'deriv3', 'deriv1']) + def test_add_deriver_without_name(self): + from pyramid.interfaces import IViewDerivers + + derivers = self.config.registry.getUtility(IViewDerivers) + self.assertFalse('deriv' in derivers.names) + def deriv(view, info): pass + self.config.add_view_deriver(deriv) + self.assertTrue('deriv' in derivers.names) + class TestDeriverIntegration(unittest.TestCase): def setUp(self): @@ -1305,8 +1314,8 @@ class TestDeriverIntegration(unittest.TestCase): return view deriv2.options = ('deriv2',) - self.config.add_view_deriver('deriv1', deriv1) - self.config.add_view_deriver('deriv2', deriv2) + self.config.add_view_deriver(deriv1, 'deriv1') + self.config.add_view_deriver(deriv2, 'deriv2') self.config.add_view(view, deriv1='test1', deriv2='test2') wrapper = self._getViewCallable(self.config) @@ -1318,7 +1327,7 @@ class TestDeriverIntegration(unittest.TestCase): def test_unexpected_view_options(self): from pyramid.exceptions import ConfigurationError def deriv1(view, info): pass - self.config.add_view_deriver('deriv1', deriv1) + self.config.add_view_deriver(deriv1, 'deriv1') self.assertRaises( ConfigurationError, lambda: self.config.add_view(lambda r: {}, deriv1='test1')) -- cgit v1.2.3 From 64bf7eec9b868fbc113341c7f5675c063aea002b Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 14 Mar 2016 22:35:19 -0500 Subject: use the implicit name in the doc examples --- docs/narr/hooks.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index 13daed1fc..3f66f4988 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -1631,7 +1631,7 @@ is a callable that can provide timing information for the view pipeline: response.headers['X-View-Performance'] = '%.3f' % (end - start,) return wrapper_view - config.add_view_deriver('timing_view', timing_view) + config.add_view_deriver(timing_view) View derivers are unique in that they have access to most of the options passed to :meth:`pyramid.config.Configurator.add_view` in order to decide what @@ -1656,7 +1656,7 @@ token unless ``disable_csrf=True`` is passed to the view: require_csrf_view.options = ('disable_csrf',) - config.add_view_deriver('require_csrf_view', require_csrf_view) + config.add_view_deriver(require_csrf_view) def myview(request): return 'protected' -- cgit v1.2.3 From a0945399b24fb38607107a55b12b7997723de2a0 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 14 Mar 2016 23:25:10 -0500 Subject: do not guess at the name of the view deriver without further discussion --- docs/narr/hooks.rst | 4 ++-- pyramid/config/views.py | 3 --- pyramid/tests/test_config/test_derivations.py | 9 --------- 3 files changed, 2 insertions(+), 14 deletions(-) diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index 3f66f4988..e3843cfbd 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -1631,7 +1631,7 @@ is a callable that can provide timing information for the view pipeline: response.headers['X-View-Performance'] = '%.3f' % (end - start,) return wrapper_view - config.add_view_deriver(timing_view) + config.add_view_deriver(timing_view, 'timing view') View derivers are unique in that they have access to most of the options passed to :meth:`pyramid.config.Configurator.add_view` in order to decide what @@ -1656,7 +1656,7 @@ token unless ``disable_csrf=True`` is passed to the view: require_csrf_view.options = ('disable_csrf',) - config.add_view_deriver(require_csrf_view) + config.add_view_deriver(require_csrf_view, 'require_csrf_view') def myview(request): return 'protected' diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 5455c29df..6f8b24aa2 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1097,9 +1097,6 @@ class ViewsConfiguratorMixin(object): if under is None and over is None: over = 'decorated_view' - if name is None: - name = deriver.__name__ - discriminator = ('view deriver', name) intr = self.introspectable( 'view derivers', diff --git a/pyramid/tests/test_config/test_derivations.py b/pyramid/tests/test_config/test_derivations.py index e1b6c8bc9..69d8797f4 100644 --- a/pyramid/tests/test_config/test_derivations.py +++ b/pyramid/tests/test_config/test_derivations.py @@ -1260,15 +1260,6 @@ class TestAddDeriver(unittest.TestCase): result = self.config._derive_view(view) self.assertEqual(response.deriv, ['deriv2', 'deriv3', 'deriv1']) - def test_add_deriver_without_name(self): - from pyramid.interfaces import IViewDerivers - - derivers = self.config.registry.getUtility(IViewDerivers) - self.assertFalse('deriv' in derivers.names) - def deriv(view, info): pass - self.config.add_view_deriver(deriv) - self.assertTrue('deriv' in derivers.names) - class TestDeriverIntegration(unittest.TestCase): def setUp(self): -- cgit v1.2.3 From 35e632635b1b4e0a767024689d69d9469ae98c0f Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 14 Mar 2016 23:28:15 -0500 Subject: add a docstring for add_view_deriver and expose the method to the api docs --- docs/api/config.rst | 1 + docs/narr/hooks.rst | 12 +++++++++--- pyramid/config/views.py | 31 ++++++++++++++++++++++++++++++- 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/docs/api/config.rst b/docs/api/config.rst index ae913d32c..e083dbc68 100644 --- a/docs/api/config.rst +++ b/docs/api/config.rst @@ -66,6 +66,7 @@ .. automethod:: add_tween .. automethod:: add_route_predicate .. automethod:: add_view_predicate + .. automethod:: add_view_deriver .. automethod:: set_request_factory .. automethod:: set_root_factory .. automethod:: set_session_factory diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index e3843cfbd..a5a03ef95 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -1580,6 +1580,10 @@ the user-defined :term:`view callable`: view pipeline interface to accept ``(context, request)`` from all previous view derivers. +``rendered_view`` + + Adapts the result of :term:`view callable` into a :term:`response` object. + ``decorated_view`` Wraps the view with the decorators from the ``decorator`` option. @@ -1615,8 +1619,10 @@ It is possible to define custom view derivers which will affect all views in an application. There are many uses for this but most will likely be centered around monitoring and security. In order to register a custom :term:`view deriver` you should create a callable that conforms to the -:class:`pyramid.interfaces.IViewDeriver` interface. For example, below -is a callable that can provide timing information for the view pipeline: +:class:`pyramid.interfaces.IViewDeriver` interface and then register it with +your application using :meth:`pyramid.config.Configurator.add_view_deriver`. +For example, below is a callable that can provide timing information for the +view pipeline: .. code-block:: python :linenos: @@ -1676,7 +1682,7 @@ Ordering View Derivers ~~~~~~~~~~~~~~~~~~~~~~ By default, every new view deriver is added between the ``decorated_view`` -and ``mapped_view`` built-in derivers. It is possible to customize this +and ``rendered_view`` built-in derivers. It is possible to customize this ordering using the ``over`` and ``under`` options. Each option can use the names of other view derivers in order to specify an ordering. There should rarely be a reason to worry about the ordering of the derivers. diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 6f8b24aa2..e90b95420 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1091,7 +1091,36 @@ class ViewsConfiguratorMixin(object): self.add_view_predicate(name, factory) @action_method - def add_view_deriver(self, deriver, name=None, under=None, over=None): + def add_view_deriver(self, deriver, name, under=None, over=None): + """ + .. versionadded:: 1.7 + + Add a :term:`view deriver` to the view pipeline. View derivers are + a feature used by extension authors to wrap views in custom code + controllable by view-specific options. + + ``deriver`` should be a callable conforming to the + :class:`pyramid.interfaces.IViewDeriver` interface. + + ``name`` should be the name of the view deriver. There are no + restrictions on the name of a view deriver. If left unspecified, the + name will be constructed from the name of the ``deriver``. + + The ``under`` and ``over`` options may be used to control the ordering + of view derivers by providing hints about where in the view pipeline + the deriver is used. + + ``under`` means further away from user-defined :term:`view callable`, + and ``over`` means closer to the original :term:`view callable`. + + Specifying neither ``under`` nor ``over`` is equivalent to specifying + ``over='decorated_view'`` and ``under='rendered_view'``, placing the + deriver somewhere between the ``decorated_view`` and ``rendered_view`` + derivers. + + See :ref:`view_derivers` for more information. + + """ deriver = self.maybe_dotted(deriver) if under is None and over is None: -- cgit v1.2.3 From cf9dcb10a02d32cee09c5540c9a3e9590ea4f490 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 17 Mar 2016 00:45:56 -0500 Subject: "over" is closer to ingress and "under" is closer to mapped_view --- pyramid/config/tweens.py | 7 +-- pyramid/config/util.py | 7 +++ pyramid/config/views.py | 46 +++++++++--------- pyramid/tests/test_config/test_derivations.py | 70 ++++++++++++++------------- 4 files changed, 68 insertions(+), 62 deletions(-) diff --git a/pyramid/config/tweens.py b/pyramid/config/tweens.py index cd14c9ff6..8e1800f33 100644 --- a/pyramid/config/tweens.py +++ b/pyramid/config/tweens.py @@ -18,6 +18,7 @@ from pyramid.tweens import ( from pyramid.config.util import ( action_method, + is_string_or_iterable, TopologicalSorter, ) @@ -122,12 +123,6 @@ class TweensConfiguratorMixin(object): tween_factory = self.maybe_dotted(tween_factory) - def is_string_or_iterable(v): - if isinstance(v, string_types): - return True - if hasattr(v, '__iter__'): - return True - for t, p in [('over', over), ('under', under)]: if p is not None: if not is_string_or_iterable(p): diff --git a/pyramid/config/util.py b/pyramid/config/util.py index 85ce826a4..626e8d5fe 100644 --- a/pyramid/config/util.py +++ b/pyramid/config/util.py @@ -5,6 +5,7 @@ from pyramid.compat import ( bytes_, getargspec, is_nonstr_iter, + string_types, ) from pyramid.compat import im_func @@ -23,6 +24,12 @@ ActionInfo = ActionInfo # support bw compat imports MAX_ORDER = 1 << 30 DEFAULT_PHASH = md5().hexdigest() +def is_string_or_iterable(v): + if isinstance(v, string_types): + return True + if hasattr(v, '__iter__'): + return True + def as_sorted_tuple(val): if not is_nonstr_iter(val): val = (val,) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index e90b95420..db56fa761 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -89,6 +89,7 @@ from pyramid.config.derivations import ( from pyramid.config.util import ( DEFAULT_PHASH, MAX_ORDER, + is_string_or_iterable, ) urljoin = urlparse.urljoin @@ -690,7 +691,7 @@ class ViewsConfiguratorMixin(object): def combine(*decorators): def decorated(view_callable): - # reversed() is allows a more natural ordering in the api + # reversed() allows a more natural ordering in the api for decorator in reversed(decorators): view_callable = decorator(view_callable) return view_callable @@ -1029,16 +1030,15 @@ class ViewsConfiguratorMixin(object): def _apply_view_derivers(self, info): d = pyramid.config.derivations - # These inner derivations have fixed order + # These derivations have fixed order + outer_derivers = [('attr_wrapped_view', d.attr_wrapped_view), + ('predicated_view', d.predicated_view)] inner_derivers = [('mapped_view', d.mapped_view)] - outer_derivers = [('predicated_view', d.predicated_view), - ('attr_wrapped_view', d.attr_wrapped_view)] - view = info.original_view derivers = self.registry.getUtility(IViewDerivers) - for name, deriver in ( - inner_derivers + derivers.sorted() + outer_derivers + for name, deriver in reversed( + outer_derivers + derivers.sorted() + inner_derivers ): view = wraps_view(deriver)(view, info) return view @@ -1110,11 +1110,11 @@ class ViewsConfiguratorMixin(object): of view derivers by providing hints about where in the view pipeline the deriver is used. - ``under`` means further away from user-defined :term:`view callable`, - and ``over`` means closer to the original :term:`view callable`. + ``under`` means closer to the user-defined :term:`view callable`, + and ``over`` means closer to view pipeline ingress. Specifying neither ``under`` nor ``over`` is equivalent to specifying - ``over='decorated_view'`` and ``under='rendered_view'``, placing the + ``over='rendered_view'`` and ``under='decorated_view'``, placing the deriver somewhere between the ``decorated_view`` and ``rendered_view`` derivers. @@ -1124,7 +1124,8 @@ class ViewsConfiguratorMixin(object): deriver = self.maybe_dotted(deriver) if under is None and over is None: - over = 'decorated_view' + under = 'decorated_view' + over = 'rendered_view' discriminator = ('view deriver', name) intr = self.introspectable( @@ -1141,29 +1142,30 @@ class ViewsConfiguratorMixin(object): if derivers is None: derivers = TopologicalSorter() self.registry.registerUtility(derivers, IViewDerivers) - derivers.add(name, deriver, after=under, before=over) + derivers.add(name, deriver, before=over, after=under) self.action(discriminator, register, introspectables=(intr,), order=PHASE1_CONFIG) # must be registered before add_view def add_default_view_derivers(self): d = pyramid.config.derivations derivers = [ - ('decorated_view', d.decorated_view), - ('http_cached_view', d.http_cached_view), - ('owrapped_view', d.owrapped_view), - ('secured_view', d.secured_view), ('authdebug_view', d.authdebug_view), + ('secured_view', d.secured_view), + ('owrapped_view', d.owrapped_view), + ('http_cached_view', d.http_cached_view), + ('decorated_view', d.decorated_view), ] - after = pyramid.util.FIRST + last = pyramid.util.FIRST for name, deriver in derivers: - self.add_view_deriver(deriver, name=name, under=after) - after = name + self.add_view_deriver(deriver, name=name, under=last) + last = name + # ensure rendered_view is over LAST self.add_view_deriver( d.rendered_view, - name='rendered_view', - under=pyramid.util.FIRST, - over='decorated_view', + 'rendered_view', + under=last, + over=pyramid.util.LAST, ) def derive_view(self, view, attr=None, renderer=None): diff --git a/pyramid/tests/test_config/test_derivations.py b/pyramid/tests/test_config/test_derivations.py index 69d8797f4..d93b37f38 100644 --- a/pyramid/tests/test_config/test_derivations.py +++ b/pyramid/tests/test_config/test_derivations.py @@ -1110,15 +1110,15 @@ class TestDerivationOrder(unittest.TestCase): derivers_sorted = derivers.sorted() dlist = [d for (d, _) in derivers_sorted] self.assertEqual([ - 'rendered_view', + 'authdebug_view', + 'secured_view', + 'owrapped_view', + 'http_cached_view', + 'decorated_view', 'deriv2', 'deriv3', 'deriv1', - 'decorated_view', - 'http_cached_view', - 'owrapped_view', - 'secured_view', - 'authdebug_view', + 'rendered_view', ], dlist) def test_right_order_implicit(self): @@ -1132,54 +1132,56 @@ class TestDerivationOrder(unittest.TestCase): derivers_sorted = derivers.sorted() dlist = [d for (d, _) in derivers_sorted] self.assertEqual([ - 'rendered_view', - 'deriv1', - 'deriv2', - 'deriv3', - 'decorated_view', - 'http_cached_view', - 'owrapped_view', - 'secured_view', 'authdebug_view', + 'secured_view', + 'owrapped_view', + 'http_cached_view', + 'decorated_view', + 'deriv3', + 'deriv2', + 'deriv1', + 'rendered_view', ], dlist) - def test_right_order_over_rendered_view(self): + def test_right_order_under_rendered_view(self): from pyramid.interfaces import IViewDerivers - self.config.add_view_deriver(None, 'deriv1', over='rendered_view') + self.config.add_view_deriver(None, 'deriv1', under='rendered_view') derivers = self.config.registry.getUtility(IViewDerivers) derivers_sorted = derivers.sorted() dlist = [d for (d, _) in derivers_sorted] - self.assertEqual(['deriv1', - 'rendered_view', - 'decorated_view', - 'http_cached_view', - 'owrapped_view', - 'secured_view', + self.assertEqual([ 'authdebug_view', + 'secured_view', + 'owrapped_view', + 'http_cached_view', + 'decorated_view', + 'rendered_view', + 'deriv1', ], dlist) - def test_right_order_over_rendered_view_others(self): + def test_right_order_under_rendered_view_others(self): from pyramid.interfaces import IViewDerivers - self.config.add_view_deriver(None, 'deriv1', over='rendered_view') + self.config.add_view_deriver(None, 'deriv1', under='rendered_view') self.config.add_view_deriver(None, 'deriv2') self.config.add_view_deriver(None, 'deriv3') derivers = self.config.registry.getUtility(IViewDerivers) derivers_sorted = derivers.sorted() dlist = [d for (d, _) in derivers_sorted] - self.assertEqual(['deriv1', - 'rendered_view', - 'deriv2', - 'deriv3', - 'decorated_view', - 'http_cached_view', - 'owrapped_view', - 'secured_view', + self.assertEqual([ 'authdebug_view', + 'secured_view', + 'owrapped_view', + 'http_cached_view', + 'decorated_view', + 'deriv3', + 'deriv2', + 'rendered_view', + 'deriv1', ], dlist) @@ -1255,8 +1257,8 @@ class TestAddDeriver(unittest.TestCase): return view self.config.add_view_deriver(deriv1, 'deriv1') - self.config.add_view_deriver(deriv2, 'deriv2', over='deriv1') - self.config.add_view_deriver(deriv3, 'deriv3', under='deriv2') + self.config.add_view_deriver(deriv2, 'deriv2', under='deriv1') + self.config.add_view_deriver(deriv3, 'deriv3', over='deriv2') result = self.config._derive_view(view) self.assertEqual(response.deriv, ['deriv2', 'deriv3', 'deriv1']) -- cgit v1.2.3 From 8ff071aff40df4dccaf8619b55c4ac318c6e0246 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 17 Mar 2016 00:54:41 -0500 Subject: remove unused import --- pyramid/config/views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index db56fa761..1516743ad 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -89,7 +89,6 @@ from pyramid.config.derivations import ( from pyramid.config.util import ( DEFAULT_PHASH, MAX_ORDER, - is_string_or_iterable, ) urljoin = urlparse.urljoin -- cgit v1.2.3 From a116948ffe14449ac6ef29145b62eb2976130d83 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 17 Mar 2016 01:24:21 -0500 Subject: fix deriver docs to explain ordering issues --- docs/narr/hooks.rst | 81 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 50 insertions(+), 31 deletions(-) diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index a5a03ef95..4a1233244 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -1556,6 +1556,8 @@ for a particular event type registration. View Derivers ------------- +.. versionadded:: 1.7 + Every URL processed by :app:`Pyramid` is matched against a custom view pipeline. See :ref:`router_chapter` for how this works. The view pipeline itself is built from the user-supplied :term:`view callable` which is then @@ -1565,28 +1567,33 @@ added functionality. View derivers are very similar to the ``decorator`` argument to :meth:`pyramid.config.Configurator.add_view` except that they have the option to execute for every view in the application. +It is helpful to think of a :term:`view deriver` as middleware for views. +Unlike tweens or WSGI middleware which are scoped to the application itself, +a view deriver is invoked once per view in the application and can use +configuration options from the view to customize its behavior. + Built-in View Derivers ~~~~~~~~~~~~~~~~~~~~~~ There are several builtin view derivers that :app:`Pyramid` will automatically -apply to any view. They are defined in order from closest to furthest from +apply to any view. Below they are defined in order from furthest to closest to the user-defined :term:`view callable`: -``mapped_view`` +``authdebug_view`` - Applies the :term:`view mapper` defined by the ``mapper`` option or the - application's default view mapper to the :term:`view callable`. This - is always the closest deriver to the user-defined view and standardizes the - view pipeline interface to accept ``(context, request)`` from all previous - view derivers. + Used to output useful debugging information when + ``pyramid.debug_authorization`` is enabled. This element is a noop otherwise. -``rendered_view`` +``secured_view`` - Adapts the result of :term:`view callable` into a :term:`response` object. + Enforce the ``permission`` defined on the view. This element is a noop if + no permission is defined. Note there will always be a permission defined + if a default permission was assigned via + :meth:`pyramid.config.Configurator.set_default_permission`. -``decorated_view`` +``owrapped_view`` - Wraps the view with the decorators from the ``decorator`` option. + Invokes the wrapped view defined by the ``wrapper`` option. ``http_cached_view`` @@ -1594,27 +1601,26 @@ the user-defined :term:`view callable`: option. This element is a noop if the ``pyramid.prevent_http_cache`` setting is enabled or the ``http_cache`` option is ``None``. -``owrapped_view`` +``decorated_view`` - Invokes the wrapped view defined by the ``wrapper`` option. + Wraps the view with the decorators from the ``decorator`` option. -``secured_view`` +``rendered_view`` - Enforce the ``permission`` defined on the view. This element is a noop if - no permission is defined. Note there will always be a permission defined - if a default permission was assigned via - :meth:`pyramid.config.Configurator.set_default_permission`. + Adapts the result of the :term:`view callable` into a :term:`response` + object. Below this point the result may be any Python object. -``authdebug_view`` +``mapped_view`` - Used to output useful debugging information when - ``pyramid.debug_authorization`` is enabled. This element is a noop otherwise. + Applies the :term:`view mapper` defined by the ``mapper`` option or the + application's default view mapper to the :term:`view callable`. This + is always the closest deriver to the user-defined view and standardizes the + view pipeline interface to accept ``(context, request)`` from all previous + view derivers. Custom View Derivers ~~~~~~~~~~~~~~~~~~~~ -.. versionadded:: 1.7 - It is possible to define custom view derivers which will affect all views in an application. There are many uses for this but most will likely be centered around monitoring and security. In order to register a custom @@ -1649,6 +1655,7 @@ token unless ``disable_csrf=True`` is passed to the view: .. code-block:: python :linenos: + from pyramid.response import Response from pyramid.session import check_csrf_token def require_csrf_view(view, info): @@ -1664,14 +1671,14 @@ token unless ``disable_csrf=True`` is passed to the view: config.add_view_deriver(require_csrf_view, 'require_csrf_view') - def myview(request): - return 'protected' + def protected_view(request): + return Response('protected') - def my_unprotected_view(request): - return 'unprotected' + def unprotected_view(request): + return Response('unprotected') - config.add_view(myview, name='safe', renderer='string') - config.add_view(my_unprotected_, name='unsafe', disable_csrf=True, renderer='string') + config.add_view(protected_view, name='safe') + config.add_view(unprotected_view, name='unsafe', disable_csrf=True) Navigating to ``/safe`` with a POST request will then fail when the call to :func:`pyramid.session.check_csrf_token` raises a @@ -1685,11 +1692,23 @@ By default, every new view deriver is added between the ``decorated_view`` and ``rendered_view`` built-in derivers. It is possible to customize this ordering using the ``over`` and ``under`` options. Each option can use the names of other view derivers in order to specify an ordering. There should -rarely be a reason to worry about the ordering of the derivers. +rarely be a reason to worry about the ordering of the derivers. Both ``over`` +and ``under`` may also be iterables of constraints. For either option, if one +or more constraints was defined, at least one must be satisfied or a +:class:`pyramid.exceptions.ConfigurationError` will be raised. This may be +used to define fallback constraints if another deriver is missing. -It is not possible to add a deriver OVER the ``mapped_view`` as the +It is not possible to add a view deriver under the ``mapped_view`` as the :term:`view mapper` is intimately tied to the signature of the user-defined :term:`view callable`. If you simply need to know what the original view callable was, it can be found as ``info.original_view`` on the provided :class:`pyramid.interfaces.IViewDeriverInfo` object passed to every view deriver. + +.. warning:: + + Any view derivers defined ``under`` the ``rendered_view`` are not + guaranteed to receive a valid response object. Rather they will receive the + result from the :term:`view mapper` which is likely the original response + returned from the view. This is possibly a dictionary for a renderer but it + may be any Python object that may be adapted into a response. -- cgit v1.2.3 From 6fb44f451abb657716ae0066ed70af66060ef3b8 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Mon, 21 Mar 2016 04:32:21 -0700 Subject: polish view derivers docs, minor grammar --- docs/narr/hooks.rst | 51 +++++++++++++++++++++++++++------------------------ 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index 4a1233244..a32e94d1a 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -1548,6 +1548,7 @@ predicate author to make every predicate make sense for every event type; it is the responsibility of the predicate consumer to use predicates that make sense for a particular event type registration. + .. index:: single: view derivers @@ -1560,35 +1561,36 @@ View Derivers Every URL processed by :app:`Pyramid` is matched against a custom view pipeline. See :ref:`router_chapter` for how this works. The view pipeline -itself is built from the user-supplied :term:`view callable` which is then +itself is built from the user-supplied :term:`view callable`, which is then composed with :term:`view derivers `. A view deriver is a composable element of the view pipeline which is used to wrap a view with added functionality. View derivers are very similar to the ``decorator`` -argument to :meth:`pyramid.config.Configurator.add_view` except that they have +argument to :meth:`pyramid.config.Configurator.add_view`, except that they have the option to execute for every view in the application. It is helpful to think of a :term:`view deriver` as middleware for views. Unlike tweens or WSGI middleware which are scoped to the application itself, -a view deriver is invoked once per view in the application and can use +a view deriver is invoked once per view in the application, and can use configuration options from the view to customize its behavior. Built-in View Derivers ~~~~~~~~~~~~~~~~~~~~~~ -There are several builtin view derivers that :app:`Pyramid` will automatically +There are several built-in view derivers that :app:`Pyramid` will automatically apply to any view. Below they are defined in order from furthest to closest to the user-defined :term:`view callable`: ``authdebug_view`` Used to output useful debugging information when - ``pyramid.debug_authorization`` is enabled. This element is a noop otherwise. + ``pyramid.debug_authorization`` is enabled. This element is a no-op + otherwise. ``secured_view`` - Enforce the ``permission`` defined on the view. This element is a noop if - no permission is defined. Note there will always be a permission defined - if a default permission was assigned via + Enforce the ``permission`` defined on the view. This element is a no-op if no + permission is defined. Note there will always be a permission defined if a + default permission was assigned via :meth:`pyramid.config.Configurator.set_default_permission`. ``owrapped_view`` @@ -1598,7 +1600,7 @@ the user-defined :term:`view callable`: ``http_cached_view`` Applies cache control headers to the response defined by the ``http_cache`` - option. This element is a noop if the ``pyramid.prevent_http_cache`` setting + option. This element is a no-op if the ``pyramid.prevent_http_cache`` setting is enabled or the ``http_cache`` option is ``None``. ``decorated_view`` @@ -1621,11 +1623,11 @@ the user-defined :term:`view callable`: Custom View Derivers ~~~~~~~~~~~~~~~~~~~~ -It is possible to define custom view derivers which will affect all views in -an application. There are many uses for this but most will likely be centered -around monitoring and security. In order to register a custom -:term:`view deriver` you should create a callable that conforms to the -:class:`pyramid.interfaces.IViewDeriver` interface and then register it with +It is possible to define custom view derivers which will affect all views in an +application. There are many uses for this, but most will likely be centered +around monitoring and security. In order to register a custom :term:`view +deriver`, you should create a callable that conforms to the +:class:`pyramid.interfaces.IViewDeriver` interface, and then register it with your application using :meth:`pyramid.config.Configurator.add_view_deriver`. For example, below is a callable that can provide timing information for the view pipeline: @@ -1647,7 +1649,7 @@ view pipeline: View derivers are unique in that they have access to most of the options passed to :meth:`pyramid.config.Configurator.add_view` in order to decide what -to do and they have a chance to affect every view in the application. +to do, and they have a chance to affect every view in the application. Let's look at one more example which will protect views by requiring a CSRF token unless ``disable_csrf=True`` is passed to the view: @@ -1688,15 +1690,16 @@ not error. Ordering View Derivers ~~~~~~~~~~~~~~~~~~~~~~ -By default, every new view deriver is added between the ``decorated_view`` -and ``rendered_view`` built-in derivers. It is possible to customize this -ordering using the ``over`` and ``under`` options. Each option can use the -names of other view derivers in order to specify an ordering. There should -rarely be a reason to worry about the ordering of the derivers. Both ``over`` -and ``under`` may also be iterables of constraints. For either option, if one -or more constraints was defined, at least one must be satisfied or a -:class:`pyramid.exceptions.ConfigurationError` will be raised. This may be -used to define fallback constraints if another deriver is missing. +By default, every new view deriver is added between the ``decorated_view`` and +``rendered_view`` built-in derivers. It is possible to customize this ordering +using the ``over`` and ``under`` options. Each option can use the names of +other view derivers in order to specify an ordering. There should rarely be a +reason to worry about the ordering of the derivers. + +Both ``over`` and ``under`` may also be iterables of constraints. For either +option, if one or more constraints was defined, at least one must be satisfied, +else a :class:`pyramid.exceptions.ConfigurationError` will be raised. This may +be used to define fallback constraints if another deriver is missing. It is not possible to add a view deriver under the ``mapped_view`` as the :term:`view mapper` is intimately tied to the signature of the user-defined -- cgit v1.2.3 From 890ea8f7d57d680bd07e04bcab9a778d0f12f737 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Mon, 21 Mar 2016 04:34:43 -0700 Subject: polish view derivers docs, minor grammar --- docs/glossary.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/glossary.rst b/docs/glossary.rst index 8928254f7..5e6aa145c 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -1097,5 +1097,5 @@ Glossary A view deriver is a composable component of the view pipeline which is used to create a :term:`view callable`. A view deriver is a callable implementing the :class:`pyramid.interfaces.IViewDeriver` interface. - Examples of builtin derivers including view mapper, the permission + Examples of built-in derivers including view mapper, the permission checker, and applying a renderer to a dictionary returned from the view. -- cgit v1.2.3 From 0b0f7e1a6d411bcc2af615da8e9dec7ea7519152 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Mon, 21 Mar 2016 04:37:15 -0700 Subject: polish view derivers docs, minor grammar --- pyramid/interfaces.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index 692bf3d6d..64bb4b50c 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -1188,7 +1188,7 @@ class IPredicateList(Interface): """ Interface representing a predicate list """ class IViewDeriver(Interface): - options = Attribute('An list of supported options to be passed to ' + options = Attribute('A list of supported options to be passed to ' ':meth:`pyramid.config.Configurator.add_view`. ' 'This attribute is optional.') -- cgit v1.2.3 From 0936051e27b1d9ff7054311d3e1a4c9008f61428 Mon Sep 17 00:00:00 2001 From: Pavlo Kapyshin Date: Sat, 26 Mar 2016 19:20:08 +0200 Subject: Fix typo --- docs/whatsnew-1.6.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/whatsnew-1.6.rst b/docs/whatsnew-1.6.rst index f5c307b5d..77d89b017 100644 --- a/docs/whatsnew-1.6.rst +++ b/docs/whatsnew-1.6.rst @@ -98,7 +98,7 @@ Feature Additions relative to the top-level package. See https://github.com/Pylons/pyramid/pull/1337 -- Overall improvments for the ``proutes`` command. Added ``--format`` and +- Overall improvements for the ``proutes`` command. Added ``--format`` and ``--glob`` arguments to the command, introduced the ``method`` column for displaying available request methods, and improved the ``view`` output by showing the module instead of just ``__repr__``. See -- cgit v1.2.3 From a7dd0531b427d8633fc222830f24b737048e9c8a Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Wed, 30 Mar 2016 05:53:35 -0700 Subject: update installation and glossary --- docs/glossary.rst | 4 ++ docs/tutorials/wiki2/installation.rst | 76 +++++++++++++++++++++++++++++------ 2 files changed, 67 insertions(+), 13 deletions(-) diff --git a/docs/glossary.rst b/docs/glossary.rst index 9657d9219..ef90a0f41 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -1091,3 +1091,7 @@ Glossary A technique used when serving a cacheable static asset in order to force a client to query the new version of the asset. See :ref:`cache_busting` for more information. + + pip + The `Python Packaging Authority `_ recommended tool + for installing Python packages. diff --git a/docs/tutorials/wiki2/installation.rst b/docs/tutorials/wiki2/installation.rst index 891305bf5..b263ccbd9 100644 --- a/docs/tutorials/wiki2/installation.rst +++ b/docs/tutorials/wiki2/installation.rst @@ -9,9 +9,9 @@ This tutorial assumes that you have already followed the steps in :ref:`installing_chapter`, except **do not create a virtualenv or install Pyramid**. Thereby you will satisfy the following requirements. -* Python interpreter is installed on your operating system -* :term:`setuptools` or :term:`distribute` is installed -* :term:`virtualenv` is installed +* A Python interpreter is installed on your operating system. +* :term:`virtualenv` is installed. +* :term:`pip` will be installed when we create a virtual environment. Create directory to contain the project @@ -72,6 +72,24 @@ Python 3.5: c:\> c:\Python35\Scripts\virtualenv %VENV% +Upgrade pip in the virtual environment +-------------------------------------- + +On UNIX +^^^^^^^ + +.. code-block:: bash + + $ $VENV/bin/pip install --upgrade pip + +On Windows +^^^^^^^^^^ + +.. code-block:: ps1con + + c:\> %VENV%\Scripts\pip install --upgrade pip + + Install Pyramid into the virtual Python environment --------------------------------------------------- @@ -80,26 +98,27 @@ On UNIX .. code-block:: bash - $ $VENV/bin/easy_install pyramid + $ $VENV/bin/pip install pyramid On Windows ^^^^^^^^^^ .. code-block:: ps1con - c:\> %VENV%\Scripts\easy_install pyramid + c:\> %VENV%\Scripts\pip install pyramid Install SQLite3 and its development packages -------------------------------------------- -If you used a package manager to install your Python or if you compiled your -Python from source, then you must install SQLite3 and its development packages. -If you downloaded your Python as an installer from https://www.python.org, then -you already have it installed and can skip this step. +If you used a package manager to install your Python or if you compiled +your Python from source, then you must install SQLite3 and its +development packages. If you downloaded your Python as an installer +from https://www.python.org, then you already have it installed and can skip +this step. -If you need to install the SQLite3 packages, then, for example, using the -Debian system and ``apt-get``, the command would be the following: +If you need to install the SQLite3 packages, then, for example, using +the Debian system and ``apt-get``, the command would be the following: .. code-block:: bash @@ -168,6 +187,7 @@ On Windows and the project into directories that do not contain spaces in their paths. + .. _installing_project_in_dev_mode: Installing the project in development mode @@ -185,7 +205,7 @@ On UNIX .. code-block:: bash $ cd tutorial - $ $VENV/bin/python setup.py develop + $ $VENV/bin/pip install -e . On Windows ^^^^^^^^^^ @@ -193,7 +213,7 @@ On Windows .. code-block:: ps1con c:\pyramidtut> cd tutorial - c:\pyramidtut\tutorial> %VENV%\Scripts\python setup.py develop + c:\pyramidtut\tutorial> %VENV%\Scripts\pip install -e . The console will show ``setup.py`` checking for packages and installing missing packages. Success executing this command will show a line like the following:: @@ -215,6 +235,8 @@ On UNIX $ $VENV/bin/python setup.py test -q +.. py.test? See https://github.com/Pylons/pyramid/issues/2104#issuecomment-155852046 + On Windows ^^^^^^^^^^ @@ -222,6 +244,8 @@ On Windows c:\pyramidtut\tutorial> %VENV%\Scripts\python setup.py test -q +.. py.test? See https://github.com/Pylons/pyramid/issues/2104#issuecomment-155852046 + For a successful test run, you should see output that ends like this:: .. @@ -310,6 +334,13 @@ initialize our database. already have a database, you should delete it before running ``initialize_tutorial_db`` again. +.. note:: + + The ``initialize_tutorial_db`` command is not performing a migration but + rather simply creating missing tables and adding some dummy data. If you + already have a database, you should delete it before running + ``initialize_tutorial_db`` again. + Type the following command, making sure you are still in the ``tutorial`` directory (the directory with a ``development.ini`` in it): @@ -415,6 +446,13 @@ assumptions: - You are willing to use :term:`URL dispatch` to map URLs to code. +- You want to use zope.sqlalchemy_, pyramid_tm_ and the transaction_ package + to scope sessions to requests. + +- You want to use pyramid_jinja2_ to render your templates. + Different templating engines can be used but we had to choose one to + make the tutorial. See :ref:`available_template_system_bindings` for some + options. - You want to use zope.sqlalchemy_, pyramid_tm_ and the transaction_ package to scope sessions to requests. @@ -440,3 +478,15 @@ assumptions: .. _transaction: http://zodb.readthedocs.org/en/latest/transactions.html + +.. _pyramid_jinja2: + http://docs.pylonsproject.org/projects/pyramid-jinja2/en/latest/ + +.. _pyramid_tm: + http://docs.pylonsproject.org/projects/pyramid-tm/en/latest/ + +.. _zope.sqlalchemy: + https://pypi.python.org/pypi/zope.sqlalchemy + +.. _transaction: + http://zodb.readthedocs.org/en/latest/transactions.html -- cgit v1.2.3 From 5562586cd45b994325fce4893dc0f74580eccdea Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Thu, 31 Mar 2016 02:24:43 -0700 Subject: - update links to PyPA site as practical - update various easy_install/setup.py commands to use pip - update to use py35 - other small improvements --- docs/quick_tour.rst | 53 +++++++++++++++++++++++++---------------------------- 1 file changed, 25 insertions(+), 28 deletions(-) diff --git a/docs/quick_tour.rst b/docs/quick_tour.rst index 65310bf4d..3554fc724 100644 --- a/docs/quick_tour.rst +++ b/docs/quick_tour.rst @@ -15,28 +15,29 @@ Installation Once you have a standard Python environment setup, getting started with Pyramid is a breeze. Unfortunately "standard" is not so simple in Python. For this -Quick Tour, it means `Python `_, a `virtual -environment `_ (or `virtualenv -for Python 2.7 `_), and `setuptools -`_. +Quick Tour, it means `Python `_, `venv +`_ (or `virtualenv for +Python 2.7 `_), +`pip `_, and `setuptools +`_. -As an example, for Python 3.3+ on Linux: +As an example, for Python 3.5+ on Linux: .. parsed-literal:: - $ pyvenv env33 - $ wget https://bitbucket.org/pypa/setuptools/raw/bootstrap/ez_setup.py -O - | env33/bin/python - $ env33/bin/easy_install "pyramid==\ |release|\ " + $ pyvenv env35 + $ env35/bin/pip install pyramid + # or for a specific released version + $ env35/bin/pip install "pyramid==\ |release|\ " For Windows: .. parsed-literal:: - # Use your browser to download: - # https://bitbucket.org/pypa/setuptools/raw/bootstrap/ez_setup.py - c:\\> c:\\Python33\\python -m venv env33 - c:\\> env33\\Scripts\\python ez_setup.py - c:\\> env33\\Scripts\\easy_install "pyramid==\ |release|\ " + c:\\> c:\\Python35\\python -m venv env35 + c:\\> env35\\Scripts\\pip install pyramid + # or for a specific released version + c:\\> env35\\Scripts\\pip install "pyramid==\ |release|\ " Of course Pyramid runs fine on Python 2.6+, as do the examples in this *Quick Tour*. We're just showing Python 3 a little love (Pyramid had production @@ -44,14 +45,8 @@ support for Python 3 in October 2011). .. note:: - Why ``easy_install`` and not ``pip``? Some distributions upon which - Pyramid depends have optional C extensions for performance. ``pip`` cannot - install some binary Python distributions. With ``easy_install``, Windows - users are able to obtain binary Python distributions, so they get the - benefit of the C extensions without needing a C compiler. Also there can - be issues when ``pip`` and ``easy_install`` are used side-by-side in the - same environment, so we chose to recommend ``easy_install`` for the sake of - reducing the complexity of these instructions. + If you use Python 2.6 or 2.7, then you might need to install + ``setuptools``. See references below for more information. .. seealso:: See also: :ref:`Quick Tutorial section on Requirements `, @@ -249,7 +244,7 @@ Chameleon as a :term:`renderer` in our Pyramid application: .. code-block:: bash - $ easy_install pyramid_chameleon + $ env35/bin/pip install pyramid_chameleon With the package installed, we can include the template bindings into our configuration in ``app.py``: @@ -293,7 +288,7 @@ Jinja2 as a :term:`renderer` in our Pyramid applications: .. code-block:: bash - $ easy_install pyramid_jinja2 + $ env35/bin/pip install pyramid_jinja2 With the package installed, we can include the template bindings into our configuration: @@ -502,7 +497,7 @@ We next use the normal Python command to set up our package for development: .. code-block:: bash $ cd hello_world - $ python ./setup.py develop + $ $VENV/bin/pip install -e . We are moving in the direction of a full-featured Pyramid project, with a proper setup for Python standards (packaging) and Pyramid configuration. This @@ -617,7 +612,7 @@ It was installed when you previously ran: .. code-block:: bash - $ python ./setup.py develop + $ $VENV/bin/pip install -e . The ``pyramid_debugtoolbar`` package is a Pyramid add-on, which means we need to include its configuration into our web application. The ``pyramid_jinja2`` @@ -670,7 +665,7 @@ following: }, We changed ``setup.py`` which means we need to rerun -``python ./setup.py develop``. We can now run all our tests: +``$VENV/bin/pip install -e .``. We can now run all our tests: .. code-block:: bash @@ -746,7 +741,9 @@ These emphasized sections in the configuration file: Our application, a package named ``hello_world``, is set up as a logger and configured to log messages at a ``DEBUG`` or higher level. When you visit -http://localhost:6543, your console will now show:: +http://localhost:6543, your console will now show: + +.. code-block:: text 2016-01-18 13:55:55,040 DEBUG [hello_world.views:10][waitress] Some Message @@ -827,7 +824,7 @@ Pyramid and SQLAlchemy are great friends. That friendship includes a scaffold! $ pcreate --scaffold alchemy sqla_demo $ cd sqla_demo - $ python setup.py develop + $ $VENV/bin/pip install -e . We now have a working sample SQLAlchemy application with all dependencies installed. The sample project provides a console script to initialize a SQLite -- cgit v1.2.3 From e4995d86ce6570dcfea440cafc92aecc40c421ad Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Thu, 31 Mar 2016 02:28:14 -0700 Subject: - change env35 to just env. --- docs/quick_tour.rst | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/quick_tour.rst b/docs/quick_tour.rst index 3554fc724..067395218 100644 --- a/docs/quick_tour.rst +++ b/docs/quick_tour.rst @@ -25,19 +25,19 @@ As an example, for Python 3.5+ on Linux: .. parsed-literal:: - $ pyvenv env35 - $ env35/bin/pip install pyramid + $ pyvenv env + $ env/bin/pip install pyramid # or for a specific released version - $ env35/bin/pip install "pyramid==\ |release|\ " + $ env/bin/pip install "pyramid==\ |release|\ " For Windows: .. parsed-literal:: - c:\\> c:\\Python35\\python -m venv env35 - c:\\> env35\\Scripts\\pip install pyramid + c:\\> c:\\Python35\\python -m venv env + c:\\> env\\Scripts\\pip install pyramid # or for a specific released version - c:\\> env35\\Scripts\\pip install "pyramid==\ |release|\ " + c:\\> env\\Scripts\\pip install "pyramid==\ |release|\ " Of course Pyramid runs fine on Python 2.6+, as do the examples in this *Quick Tour*. We're just showing Python 3 a little love (Pyramid had production @@ -244,7 +244,7 @@ Chameleon as a :term:`renderer` in our Pyramid application: .. code-block:: bash - $ env35/bin/pip install pyramid_chameleon + $ env/bin/pip install pyramid_chameleon With the package installed, we can include the template bindings into our configuration in ``app.py``: @@ -288,7 +288,7 @@ Jinja2 as a :term:`renderer` in our Pyramid applications: .. code-block:: bash - $ env35/bin/pip install pyramid_jinja2 + $ env/bin/pip install pyramid_jinja2 With the package installed, we can include the template bindings into our configuration: -- cgit v1.2.3 From 7725b8ca96ae63f0767145aa192e3d5a67da4a1b Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Thu, 31 Mar 2016 02:29:40 -0700 Subject: - change $VENV to just env. --- docs/quick_tour.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/quick_tour.rst b/docs/quick_tour.rst index 067395218..32f049d6a 100644 --- a/docs/quick_tour.rst +++ b/docs/quick_tour.rst @@ -497,7 +497,7 @@ We next use the normal Python command to set up our package for development: .. code-block:: bash $ cd hello_world - $ $VENV/bin/pip install -e . + $ env/bin/pip install -e . We are moving in the direction of a full-featured Pyramid project, with a proper setup for Python standards (packaging) and Pyramid configuration. This @@ -612,7 +612,7 @@ It was installed when you previously ran: .. code-block:: bash - $ $VENV/bin/pip install -e . + $ env/bin/pip install -e . The ``pyramid_debugtoolbar`` package is a Pyramid add-on, which means we need to include its configuration into our web application. The ``pyramid_jinja2`` @@ -665,7 +665,7 @@ following: }, We changed ``setup.py`` which means we need to rerun -``$VENV/bin/pip install -e .``. We can now run all our tests: +``env/bin/pip install -e .``. We can now run all our tests: .. code-block:: bash @@ -824,7 +824,7 @@ Pyramid and SQLAlchemy are great friends. That friendship includes a scaffold! $ pcreate --scaffold alchemy sqla_demo $ cd sqla_demo - $ $VENV/bin/pip install -e . + $ env/bin/pip install -e . We now have a working sample SQLAlchemy application with all dependencies installed. The sample project provides a console script to initialize a SQLite -- cgit v1.2.3 From afd04cf867e9916deef2bb7d4302c15ca5a21c63 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sat, 2 Apr 2016 00:45:09 -0700 Subject: update url --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e878b9932..e26ae80d8 100644 --- a/setup.py +++ b/setup.py @@ -95,7 +95,7 @@ setup(name='pyramid', keywords='web wsgi pylons pyramid', author="Chris McDonough, Agendaless Consulting", author_email="pylons-discuss@googlegroups.com", - url="http://docs.pylonsproject.org/en/latest/docs/pyramid.html", + url="https://trypyramid.com", license="BSD-derived (http://www.repoze.org/LICENSE.txt)", packages=find_packages(), include_package_data=True, -- cgit v1.2.3 From f1eb47f14aea0e9a1dde39ed08a9bf6728614cbf Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sat, 2 Apr 2016 02:05:22 -0700 Subject: remove note to install setuptools --- docs/quick_tour.rst | 5 ----- 1 file changed, 5 deletions(-) diff --git a/docs/quick_tour.rst b/docs/quick_tour.rst index 32f049d6a..f1cec97e9 100644 --- a/docs/quick_tour.rst +++ b/docs/quick_tour.rst @@ -43,11 +43,6 @@ Of course Pyramid runs fine on Python 2.6+, as do the examples in this *Quick Tour*. We're just showing Python 3 a little love (Pyramid had production support for Python 3 in October 2011). -.. note:: - - If you use Python 2.6 or 2.7, then you might need to install - ``setuptools``. See references below for more information. - .. seealso:: See also: :ref:`Quick Tutorial section on Requirements `, :ref:`installing_unix`, :ref:`Before You Install `, and -- cgit v1.2.3 From 44bbbc32b607b043e708a625c4b1756db8919bdd Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sun, 3 Apr 2016 02:17:59 -0700 Subject: - replace easy_install with pip - bump Python version to 3.5 or generalize to Python 3 - rewrite seealso's - use ps1con lexer for windows powershell console - add hyperlink targets --- docs/narr/install.rst | 6 ++ docs/quick_tutorial/requirements.rst | 118 +++++++++++------------------------ 2 files changed, 42 insertions(+), 82 deletions(-) diff --git a/docs/narr/install.rst b/docs/narr/install.rst index 767b16fc0..4a2f228a3 100644 --- a/docs/narr/install.rst +++ b/docs/narr/install.rst @@ -29,6 +29,9 @@ Some :app:`Pyramid` dependencies may attempt to build C extensions for performance speedups. If a compiler or Python headers are unavailable the dependency will fall back to using pure Python instead. + +.. _for-mac-os-x-users: + For Mac OS X Users ~~~~~~~~~~~~~~~~~~ @@ -52,6 +55,9 @@ Alternatively, you can use the `homebrew `_ package manager. If you use an installer for your Python, then you can skip to the section :ref:`installing_unix`. + +.. _if-you-don-t-yet-have-a-python-interpreter-unix: + If You Don't Yet Have a Python Interpreter (UNIX) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/quick_tutorial/requirements.rst b/docs/quick_tutorial/requirements.rst index f855dcb55..3373ba2bc 100644 --- a/docs/quick_tutorial/requirements.rst +++ b/docs/quick_tutorial/requirements.rst @@ -4,9 +4,9 @@ Requirements ============ -Let's get our tutorial environment setup. Most of the setup work is in -standard Python development practices (install Python, -make an isolated environment, and setup packaging tools.) +Let's get our tutorial environment set up. Most of the set up work is in +standard Python development practices (install Python and make an isolated +environment.) .. note:: @@ -19,16 +19,14 @@ make an isolated environment, and setup packaging tools.) This *Quick Tutorial* is based on: -* **Python 3.3**. Pyramid fully supports Python 3.3+ and Python 2.6+. - This tutorial uses **Python 3.3** but runs fine under Python 2.7. +* **Python 3.5**. Pyramid fully supports Python 3.3+ and Python 2.6+. This + tutorial uses **Python 3.5** but runs fine under Python 2.7. -* **pyvenv**. We believe in virtual environments. For this tutorial, - we use Python 3.3's built-in solution, the ``pyvenv`` command. - For Python 2.7, you can install ``virtualenv``. +* **pyvenv**. We believe in virtual environments. For this tutorial, we use + Python 3.5's built-in solution, the ``pyvenv`` command. For Python 2.7, you + can install ``virtualenv``. -* **setuptools and easy_install**. We use - `setuptools `_ - and its ``easy_install`` for package management. +* **pip**. We use ``pip`` for package management. * **Workspaces, projects, and packages.** Our home directory will contain a *tutorial workspace* with our Python virtual @@ -46,34 +44,39 @@ This *Quick Tutorial* is based on: Steps ===== -#. :ref:`install-python-3.3-or-greater` +#. :ref:`install-python-3` #. :ref:`create-a-project-directory-structure` #. :ref:`set-an-environment-variable` #. :ref:`create-a-virtual-environment` -#. :ref:`install-setuptools-(python-packaging-tools)` #. :ref:`install-pyramid` -.. _install-python-3.3-or-greater: -Install Python 3.3 or greater ------------------------------ +.. _install-python-3: -Download the latest standard Python 3.3+ release (not development release) -from `python.org `_. +Install Python 3 +---------------- Windows and Mac OS X users can download and run an installer. +Download the latest standard Python 3 release (not development release) from +`python.org `_. + Windows users should also install the `Python for Windows extensions `_. Carefully read the ``README.txt`` file at the end of the list of builds, and follow its directions. Make sure you get the proper 32- or 64-bit build and Python version. -Linux users can either use their package manager to install Python 3.3 -or may `build Python 3.3 from source +Linux users can either use their package manager to install Python 3 +or may `build Python 3 from source `_. +.. seealso:: See also :ref:`For Mac OS X Users `, + :ref:`If You Don't Yet Have a Python Interpreter (UNIX) + `, and :ref:`Installing + Pyramid on a Windows System `. + .. _create-a-project-directory-structure: @@ -142,6 +145,8 @@ environment. We set an environment variable to save typing later. # Mac and Linux $ export VENV=~/projects/quick_tutorial/env +.. code-block:: ps1con + # Windows # TODO: This command does not work c:\> set VENV=c:\projects\quick_tutorial\env @@ -158,7 +163,7 @@ Create a Virtual Environment and `PEP 453 `_ for a proposed resolution. -``pyvenv`` is a tool to create isolated Python 3.3 environments, each +``pyvenv`` is a tool to create isolated Python 3 environments, each with its own Python binary and independent set of installed Python packages in its site directories. Let's create one, using the location we just specified in the environment variable. @@ -168,46 +173,13 @@ we just specified in the environment variable. # Mac and Linux $ pyvenv $VENV - # Windows - c:\> c:\Python33\python -m venv %VENV% - -.. seealso:: See also Python 3's :mod:`venv module `, - Python 2's `virtualenv `_ - package, - :ref:`Installing Pyramid on a Windows System ` - - -.. _install-setuptools-(python-packaging-tools): - -Install ``setuptools`` (Python packaging tools) ------------------------------------------------ - -The following command will download a script to install ``setuptools``, then -pipe it to your environment's version of Python. - -.. code-block:: bash - - # Mac and Linux - $ wget https://bitbucket.org/pypa/setuptools/raw/bootstrap/ez_setup.py -O - | $VENV/bin/python +.. code-block:: ps1con # Windows - # - # Use your web browser to download this file: - # https://bitbucket.org/pypa/setuptools/raw/bootstrap/ez_setup.py - # - # ...and save it to: - # c:\projects\quick_tutorial\ez_setup.py - # - # Then run the following command: - - c:\> %VENV%\Scripts\python ez_setup.py - -If ``wget`` complains with a certificate error, then run this command instead: + c:\> c:\Python35\python -m venv %VENV% -.. code-block:: bash - - # Mac and Linux - $ wget --no-check-certificate https://bitbucket.org/pypa/setuptools/raw/bootstrap/ez_setup.py -O - | $VENV/bin/python +.. seealso:: See also Python 3's :mod:`venv module ` and Python + 2's `virtualenv `_ package. .. _install-pyramid: @@ -221,10 +193,10 @@ part is pretty easy: .. parsed-literal:: # Mac and Linux - $ $VENV/bin/easy_install "pyramid==\ |release|\ " + $ $VENV/bin/pip install "pyramid==\ |release|\ " # Windows - c:\\> %VENV%\\Scripts\\easy_install "pyramid==\ |release|\ " + c:\\> %VENV%\\Scripts\\pip install "pyramid==\ |release|\ " Our Python virtual environment now has the Pyramid software available. @@ -234,30 +206,12 @@ during this tutorial: .. code-block:: bash # Mac and Linux - $ $VENV/bin/easy_install nose webtest deform sqlalchemy \ + $ $VENV/bin/pip install nose webtest deform sqlalchemy \ pyramid_chameleon pyramid_debugtoolbar waitress \ pyramid_tm zope.sqlalchemy - # Windows - c:\> %VENV%\Scripts\easy_install nose webtest deform sqlalchemy pyramid_chameleon pyramid_debugtoolbar waitress pyramid_tm zope.sqlalchemy - - -.. note:: +.. code-block:: ps1con - Why ``easy_install`` and not ``pip``? Pyramid encourages use of namespace - packages, for which ``pip``'s support is less-than-optimal. Also, Pyramid's - dependencies use some optional C extensions for performance: with - ``easy_install``, Windows users can get these extensions without needing - a C compiler (``pip`` does not support installing binary Windows - distributions, except for ``wheels``, which are not yet available for - all dependencies). - -.. seealso:: See also :ref:`installing_unix`. For instructions to set up your - Python environment for development using Windows or Python 2, see Pyramid's - :ref:`Before You Install `. - - See also Python 3's :mod:`venv module `, the `setuptools - installation instructions - `_, - and `easy_install help `_. + # Windows + c:\> %VENV%\Scripts\pip install nose webtest deform sqlalchemy pyramid_chameleon pyramid_debugtoolbar waitress pyramid_tm zope.sqlalchemy -- cgit v1.2.3 From 74c56e0e8d767ac0942cb17cde535113e97d8db1 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Mon, 4 Apr 2016 00:43:27 -0700 Subject: - replace setup.py with pip --- docs/quick_tutorial/hello_world.rst | 2 +- docs/quick_tutorial/scaffolds.rst | 11 ++++++----- docs/quick_tutorial/tutorial_approach.rst | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/quick_tutorial/hello_world.rst b/docs/quick_tutorial/hello_world.rst index 4ae80ca87..fb661e9c5 100644 --- a/docs/quick_tutorial/hello_world.rst +++ b/docs/quick_tutorial/hello_world.rst @@ -5,7 +5,7 @@ ================================ What's the simplest way to get started in Pyramid? A single-file module. -No Python packages, no ``setup.py``, no other machinery. +No Python packages, no ``pip install -e .``, no other machinery. Background ========== diff --git a/docs/quick_tutorial/scaffolds.rst b/docs/quick_tutorial/scaffolds.rst index 4f2694100..319eb9d90 100644 --- a/docs/quick_tutorial/scaffolds.rst +++ b/docs/quick_tutorial/scaffolds.rst @@ -12,7 +12,7 @@ Background ========== We're going to cover a lot in this tutorial, focusing on one topic at a -time and writing everything from scratch. As a warmup, though, +time and writing everything from scratch. As a warm up, though, it sure would be nice to see some pixels on a screen. Like other web development frameworks, Pyramid provides a number of @@ -47,21 +47,22 @@ Steps $ $VENV/bin/pcreate --scaffold starter scaffolds -#. Use normal Python development to setup our project for development: +#. Install our project in editable mode for development in the current + directory: .. code-block:: bash $ cd scaffolds - $ $VENV/bin/python setup.py develop + $ $VENV/bin/pip install -e . -#. Startup the application by pointing Pyramid's ``pserve`` command at +#. Start up the application by pointing Pyramid's ``pserve`` command at the project's (generated) configuration file: .. code-block:: bash $ $VENV/bin/pserve development.ini --reload - On startup, ``pserve`` logs some output: + On start up, ``pserve`` logs some output: .. code-block:: bash diff --git a/docs/quick_tutorial/tutorial_approach.rst b/docs/quick_tutorial/tutorial_approach.rst index 204d388b0..8298a4710 100644 --- a/docs/quick_tutorial/tutorial_approach.rst +++ b/docs/quick_tutorial/tutorial_approach.rst @@ -17,7 +17,7 @@ repo, where each step/topic/directory is a Python package. To successfully run each step:: $ cd request_response - $ $VENV/bin/python setup.py develop + $ $VENV/bin/pip install -e . ...and repeat for each step you would like to work on. In most cases we will start with the results of an earlier step. -- cgit v1.2.3 From 7c29ea97c6617d1f6b2f621bf88aa6a0ab0209fa Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Tue, 5 Apr 2016 03:34:34 -0700 Subject: - replace easy_install with pip - add python3 for intersphinx. See #2429 - minor grammar --- docs/quick_tutorial/conf.py | 3 +++ docs/quick_tutorial/package.rst | 40 ++++++++++++++++++---------------------- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/docs/quick_tutorial/conf.py b/docs/quick_tutorial/conf.py index 47b8fae41..feebdf02a 100644 --- a/docs/quick_tutorial/conf.py +++ b/docs/quick_tutorial/conf.py @@ -257,6 +257,9 @@ intersphinx_mapping = { 'python': ( 'http://docs.python.org/2', None), + 'python3': ( + 'https://docs.python.org/3/', + None), 'sqla': ( 'http://docs.sqlalchemy.org/en/latest', None), diff --git a/docs/quick_tutorial/package.rst b/docs/quick_tutorial/package.rst index 54a6a0bd9..9e175bdaa 100644 --- a/docs/quick_tutorial/package.rst +++ b/docs/quick_tutorial/package.rst @@ -10,43 +10,38 @@ Background ========== Python developers can organize a collection of modules and files into a -namespaced unit called a :ref:`package `. If a +namespaced unit called a :ref:`package `. If a directory is on ``sys.path`` and has a special file named ``__init__.py``, it is treated as a Python package. -Packages can be bundled up, made available for installation, -and installed through a (muddled, but improving) toolchain oriented -around a ``setup.py`` file for a -`setuptools project `_. -Explaining it all in this -tutorial will induce madness. For this tutorial, this is all you need to -know: +Packages can be bundled up, made available for installation, and installed +through a toolchain oriented around a ``setup.py`` file. For this tutorial, +this is all you need to know: -- We will have a directory for each tutorial step as a setuptools *project* +- We will have a directory for each tutorial step as a *project*. -- This project will contain a ``setup.py`` which injects the features - of the setuptool's project machinery into the directory +- This project will contain a ``setup.py`` which injects the features of the + project machinery into the directory. - In this project we will make a ``tutorial`` subdirectory into a Python - *package* using an ``__init__.py`` Python module file + *package* using an ``__init__.py`` Python module file. -- We will run ``python setup.py develop`` to install our project in - development mode +- We will run ``pip install -e .`` to install our project in development mode. In summary: -- You'll do your development in a Python *package* +- You'll do your development in a Python *package*. -- That package will be part of a setuptools *project* +- That package will be part of a *project*. Objectives ========== -- Make a Python "package" directory with an ``__init__.py`` +- Make a Python "package" directory with an ``__init__.py``. -- Get a minimum Python "project" in place by making a ``setup.py`` +- Get a minimum Python "project" in place by making a ``setup.py``. -- Install our ``tutorial`` project in development mode +- Install our ``tutorial`` project in development mode. Steps ===== @@ -66,7 +61,7 @@ Steps .. code-block:: bash - $ $VENV/bin/python setup.py develop + $ $VENV/bin/pip install -e . $ mkdir tutorial #. Enter the following into ``package/tutorial/__init__.py``: @@ -107,5 +102,6 @@ of an odd duck. We would never do this unless we were writing a tutorial that tries to capture how this stuff works a step at a time. It's generally a bad idea to run a Python module inside a package directly as a script. -.. seealso:: :ref:`Python Packages `, - `setuptools Entry Points `_ +.. seealso:: :ref:`Python Packages ` and `Working in + "Development Mode" + `_. -- cgit v1.2.3 From 186b72e56600c79888795fa4eed286a5ebf71974 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Wed, 6 Apr 2016 04:24:28 -0700 Subject: - remove conf.py straggler - update intersphinx link to python3 docs - Closes #2429 --- docs/conf.py | 3 +- docs/quick_tutorial/conf.py | 284 ----------------------------------- docs/quick_tutorial/package.rst | 4 +- docs/quick_tutorial/requirements.rst | 2 +- 4 files changed, 4 insertions(+), 289 deletions(-) delete mode 100644 docs/quick_tutorial/conf.py diff --git a/docs/conf.py b/docs/conf.py index a895bc6c3..1600e1f5c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -66,8 +66,7 @@ intersphinx_mapping = { 'deform': ('http://docs.pylonsproject.org/projects/deform/en/latest', None), 'jinja2': ('http://docs.pylonsproject.org/projects/pyramid-jinja2/en/latest/', None), 'pylonswebframework': ('http://docs.pylonsproject.org/projects/pylons-webframework/en/latest/', None), - 'python': ('http://docs.python.org', None), - 'python3': ('http://docs.python.org/3', None), + 'python': ('https://docs.python.org/3', None), 'sqla': ('http://docs.sqlalchemy.org/en/latest', None), 'tm': ('http://docs.pylonsproject.org/projects/pyramid-tm/en/latest/', None), 'toolbar': ('http://docs.pylonsproject.org/projects/pyramid-debugtoolbar/en/latest', None), diff --git a/docs/quick_tutorial/conf.py b/docs/quick_tutorial/conf.py deleted file mode 100644 index feebdf02a..000000000 --- a/docs/quick_tutorial/conf.py +++ /dev/null @@ -1,284 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Getting Started with Pyramid and REST documentation build configuration file, created by -# sphinx-quickstart on Mon Aug 26 14:44:57 2013. -# -# This file is execfile()d with the current directory set to its containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -import sys, os - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) - -# -- General configuration ----------------------------------------------------- - -# If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be extensions -# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.intersphinx'] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix of source filenames. -source_suffix = '.rst' - -# The encoding of source files. -#source_encoding = 'utf-8-sig' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = u'Getting Started with Pyramid and REST' -copyright = u'2013, Agendaless Consulting' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = '1.0' -# The full version, including alpha/beta/rc tags. -release = '1.0' - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -#language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -#today = '' -# Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -exclude_patterns = ['_build'] - -# The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -#add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -#show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] - -# If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False - - -# -- Options for HTML output --------------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -html_theme = 'default' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -#html_theme_options = {} - -# Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] - -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -#html_title = None - -# A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -#html_logo = None - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -#html_favicon = None - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -#html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -#html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -#html_additional_pages = {} - -# If false, no module index is generated. -#html_domain_indices = True - -# If false, no index is generated. -#html_use_index = True - -# If true, the index is split into individual pages for each letter. -#html_split_index = False - -# If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -#html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None - -# Output file base name for HTML help builder. -htmlhelp_basename = 'GettingStartedwithPyramidandRESTdoc' - - -# -- Options for LaTeX output -------------------------------------------------- - -latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - #'papersize': 'letterpaper', - - # The font size ('10pt', '11pt' or '12pt'). - #'pointsize': '10pt', - - # Additional stuff for the LaTeX preamble. - #'preamble': '', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, documentclass [howto/manual]). -latex_documents = [ - ('index', 'GettingStartedwithPyramidandREST.tex', - u'Getting Started with Pyramid and REST Documentation', - u'Agendaless Consulting', 'manual'), -] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -#latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -#latex_use_parts = False - -# If true, show page references after internal links. -#latex_show_pagerefs = False - -# If true, show URL addresses after external links. -#latex_show_urls = False - -# Documents to append as an appendix to all manuals. -#latex_appendices = [] - -# If false, no module index is generated. -#latex_domain_indices = True - - -# -- Options for manual page output -------------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - ('index', 'gettingstartedwithpyramidandrest', - u'Getting Started with Pyramid and REST Documentation', - [u'Agendaless Consulting'], 1) -] - -# If true, show URL addresses after external links. -#man_show_urls = False - - -# -- Options for Texinfo output ------------------------------------------------ - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - ('index', 'GettingStartedwithPyramidandREST', - u'Getting Started with Pyramid and REST Documentation', - u'Agendaless Consulting', 'GettingStartedwithPyramidandREST', - 'One line description of project.', - 'Miscellaneous'), -] - -# Documents to append as an appendix to all manuals. -#texinfo_appendices = [] - -# If false, no module index is generated. -#texinfo_domain_indices = True - -# How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' - -# If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False - - -# Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = { - 'python': ( - 'http://docs.python.org/2', - None), - 'python3': ( - 'https://docs.python.org/3/', - None), - 'sqla': ( - 'http://docs.sqlalchemy.org/en/latest', - None), - 'pyramid': ( - 'http://docs.pylonsproject.org/projects/pyramid/en/latest/', - None), - 'jinja2': ( - 'http://docs.pylonsproject.org/projects/pyramid_jinja2/en/latest/', - None), - 'toolbar': ( - 'http://docs.pylonsproject.org/projects/pyramid_debugtoolbar/en/latest', - None), - 'deform': ( - 'http://docs.pylonsproject.org/projects/deform/en/latest', - None), - 'colander': ( - 'http://docs.pylonsproject.org/projects/colander/en/latest', - None), - 'tutorials': ( - 'http://docs.pylonsproject.org/projects/pyramid_tutorials/en/latest/', - None), -} diff --git a/docs/quick_tutorial/package.rst b/docs/quick_tutorial/package.rst index 9e175bdaa..6a379032e 100644 --- a/docs/quick_tutorial/package.rst +++ b/docs/quick_tutorial/package.rst @@ -10,7 +10,7 @@ Background ========== Python developers can organize a collection of modules and files into a -namespaced unit called a :ref:`package `. If a +namespaced unit called a :ref:`package `. If a directory is on ``sys.path`` and has a special file named ``__init__.py``, it is treated as a Python package. @@ -102,6 +102,6 @@ of an odd duck. We would never do this unless we were writing a tutorial that tries to capture how this stuff works a step at a time. It's generally a bad idea to run a Python module inside a package directly as a script. -.. seealso:: :ref:`Python Packages ` and `Working in +.. seealso:: :ref:`Python Packages ` and `Working in "Development Mode" `_. diff --git a/docs/quick_tutorial/requirements.rst b/docs/quick_tutorial/requirements.rst index 3373ba2bc..299ad2ac0 100644 --- a/docs/quick_tutorial/requirements.rst +++ b/docs/quick_tutorial/requirements.rst @@ -178,7 +178,7 @@ we just specified in the environment variable. # Windows c:\> c:\Python35\python -m venv %VENV% -.. seealso:: See also Python 3's :mod:`venv module ` and Python +.. seealso:: See also Python 3's :mod:`venv module ` and Python 2's `virtualenv `_ package. -- cgit v1.2.3 From 1514ea003dfe39fa79a0ec07bbbc14f239cb4eb2 Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Mon, 7 Mar 2016 08:20:39 -0800 Subject: Pass vars to logging.config.fileConfig This allows one to set up a logging configuration that is parameterized based on variables specified on the command-line. e.g.: the application .ini file could have: ```ini [logger_root] level = %(LOGGING_LOGGER_ROOT_LEVEL)s handlers = console [handler_console] class = StreamHandler args = (sys.stderr,) level = %(LOGGING_HANDLER_CONSOLE_LEVEL)s formatter = generic ``` This app could be launched with: ``` pserve development.ini LOGGING_LOGGER_ROOT_LEVEL=DEBUG LOGGING_HANDLER_CONSOLE_LEVEL=DEBUG ``` --- CHANGES.txt | 6 ++++++ docs/api/paster.rst | 2 +- pyramid/paster.py | 17 ++++++++++++----- pyramid/scripts/pserve.py | 2 +- pyramid/tests/test_paster.py | 26 +++++++++++++++++++++++--- 5 files changed, 43 insertions(+), 10 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index a17f4aab5..4a61dbffa 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -27,6 +27,12 @@ unreleased where you may need more control over the request. See https://github.com/Pylons/pyramid/pull/2393 +- Allow using variable substitutions like ``%(LOGGING_LOGGER_ROOT_LEVEL)s`` + for logging sections of the .ini file and populate these variables from + the ``pserve`` command line -- e.g.: + ``pserve development.ini LOGGING_LOGGER_ROOT_LEVEL=DEBUG`` + See https://github.com/Pylons/pyramid/pull/2399 + 1.6 (2015-04-14) ================ diff --git a/docs/api/paster.rst b/docs/api/paster.rst index edc3738fc..27bc81a1f 100644 --- a/docs/api/paster.rst +++ b/docs/api/paster.rst @@ -11,4 +11,4 @@ .. autofunction:: get_appsettings(config_uri, name=None, options=None) - .. autofunction:: setup_logging(config_uri) + .. autofunction:: setup_logging(config_uri, global_conf=None) diff --git a/pyramid/paster.py b/pyramid/paster.py index 967543849..5e2200ea7 100644 --- a/pyramid/paster.py +++ b/pyramid/paster.py @@ -52,7 +52,8 @@ def get_appsettings(config_uri, name=None, options=None, appconfig=appconfig): relative_to=here_dir, global_conf=options) -def setup_logging(config_uri, fileConfig=fileConfig, +def setup_logging(config_uri, global_conf=None, + fileConfig=fileConfig, configparser=configparser): """ Set up logging via the logging module's fileConfig function with the @@ -61,16 +62,22 @@ def setup_logging(config_uri, fileConfig=fileConfig, ConfigParser defaults are specified for the special ``__file__`` and ``here`` variables, similar to PasteDeploy config loading. + Extra defaults can optionally be specified as a dict in ``global_conf``. """ path, _ = _getpathsec(config_uri, None) parser = configparser.ConfigParser() parser.read([path]) if parser.has_section('loggers'): config_file = os.path.abspath(path) - return fileConfig( - config_file, - dict(__file__=config_file, here=os.path.dirname(config_file)) - ) + if global_conf: + # Copy to avoid side effects + global_conf = dict(global_conf) + else: + global_conf = {} + global_conf.update( + __file__=config_file, + here=os.path.dirname(config_file)) + return fileConfig(config_file, global_conf) def _getpathsec(config_uri, name): if '#' in config_uri: diff --git a/pyramid/scripts/pserve.py b/pyramid/scripts/pserve.py index 431afe6f4..74bda1dce 100644 --- a/pyramid/scripts/pserve.py +++ b/pyramid/scripts/pserve.py @@ -362,7 +362,7 @@ a real process manager for your processes like Systemd, Circus, or Supervisor. log_fn = None if log_fn: log_fn = os.path.join(base, log_fn) - setup_logging(log_fn) + setup_logging(log_fn, global_conf=vars) server = self.loadserver(server_spec, name=server_name, relative_to=base, global_conf=vars) diff --git a/pyramid/tests/test_paster.py b/pyramid/tests/test_paster.py index 5e341172c..22a5cde3d 100644 --- a/pyramid/tests/test_paster.py +++ b/pyramid/tests/test_paster.py @@ -105,18 +105,38 @@ class Test_get_appsettings(unittest.TestCase): self.assertEqual(result['foo'], 'baz') class Test_setup_logging(unittest.TestCase): - def _callFUT(self, config_file): + def _callFUT(self, config_file, global_conf=None): from pyramid.paster import setup_logging dummy_cp = DummyConfigParserModule - return setup_logging(config_file, self.fileConfig, dummy_cp) + return setup_logging( + config_uri=config_file, + global_conf=global_conf, + fileConfig=self.fileConfig, + configparser=dummy_cp, + ) - def test_it(self): + def test_it_no_global_conf(self): config_file, dict = self._callFUT('/abc') # os.path.abspath is a sop to Windows self.assertEqual(config_file, os.path.abspath('/abc')) self.assertEqual(dict['__file__'], os.path.abspath('/abc')) self.assertEqual(dict['here'], os.path.abspath('/')) + def test_it_global_conf_empty(self): + config_file, dict = self._callFUT('/abc', global_conf={}) + # os.path.abspath is a sop to Windows + self.assertEqual(config_file, os.path.abspath('/abc')) + self.assertEqual(dict['__file__'], os.path.abspath('/abc')) + self.assertEqual(dict['here'], os.path.abspath('/')) + + def test_it_global_conf_not_empty(self): + config_file, dict = self._callFUT('/abc', global_conf={'key': 'val'}) + # os.path.abspath is a sop to Windows + self.assertEqual(config_file, os.path.abspath('/abc')) + self.assertEqual(dict['__file__'], os.path.abspath('/abc')) + self.assertEqual(dict['here'], os.path.abspath('/')) + self.assertEqual(dict['key'], 'val') + def fileConfig(self, config_file, dict): return config_file, dict -- cgit v1.2.3 From f19d8b325392043631be7254d226b71a300c6e40 Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Wed, 6 Apr 2016 11:05:55 -0700 Subject: setup_logging: Intersphinx link to fileConfig So Sphinx docs have a clickable link to `logging.config.fileConfig`. --- pyramid/paster.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyramid/paster.py b/pyramid/paster.py index 967543849..460056658 100644 --- a/pyramid/paster.py +++ b/pyramid/paster.py @@ -55,8 +55,8 @@ def get_appsettings(config_uri, name=None, options=None, appconfig=appconfig): def setup_logging(config_uri, fileConfig=fileConfig, configparser=configparser): """ - Set up logging via the logging module's fileConfig function with the - filename specified via ``config_uri`` (a string in the form + Set up logging via :func:`logging.config.fileConfig` with the filename + specified via ``config_uri`` (a string in the form ``filename#sectionname``). ConfigParser defaults are specified for the special ``__file__`` -- cgit v1.2.3 From 44809378b8e1d53cc2d5482b08d92a208e9fbfdb Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Wed, 6 Apr 2016 13:48:45 -0700 Subject: Simplify setup_logging global_conf handling A somewhat simpler and nicer implementation of handling `global_conf` than what I had in https://github.com/Pylons/pyramid/pull/2399 --- pyramid/paster.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/pyramid/paster.py b/pyramid/paster.py index b371eea6f..3916be8f0 100644 --- a/pyramid/paster.py +++ b/pyramid/paster.py @@ -69,15 +69,12 @@ def setup_logging(config_uri, global_conf=None, parser.read([path]) if parser.has_section('loggers'): config_file = os.path.abspath(path) - if global_conf: - # Copy to avoid side effects - global_conf = dict(global_conf) - else: - global_conf = {} - global_conf.update( + full_global_conf = dict( __file__=config_file, here=os.path.dirname(config_file)) - return fileConfig(config_file, global_conf) + if global_conf: + full_global_conf.update(global_conf) + return fileConfig(config_file, full_global_conf) def _getpathsec(config_uri, name): if '#' in config_uri: -- cgit v1.2.3 From ecc9d82ef6bd50a08dcbee7286ba4581d3caa902 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Wed, 6 Apr 2016 21:22:36 -0500 Subject: rename pyramid.config.derivations to pyramid.viewderivers --- pyramid/config/derivations.py | 453 -------- pyramid/config/views.py | 8 +- pyramid/tests/test_config/test_derivations.py | 1376 ------------------------- pyramid/tests/test_viewderivers.py | 1376 +++++++++++++++++++++++++ pyramid/viewderivers.py | 453 ++++++++ 5 files changed, 1833 insertions(+), 1833 deletions(-) delete mode 100644 pyramid/config/derivations.py delete mode 100644 pyramid/tests/test_config/test_derivations.py create mode 100644 pyramid/tests/test_viewderivers.py create mode 100644 pyramid/viewderivers.py diff --git a/pyramid/config/derivations.py b/pyramid/config/derivations.py deleted file mode 100644 index 99baf46f9..000000000 --- a/pyramid/config/derivations.py +++ /dev/null @@ -1,453 +0,0 @@ -import inspect - -from zope.interface import ( - implementer, - provider, - ) - -from pyramid.security import NO_PERMISSION_REQUIRED -from pyramid.response import Response - -from pyramid.interfaces import ( - IAuthenticationPolicy, - IAuthorizationPolicy, - IDebugLogger, - IResponse, - IViewMapper, - IViewMapperFactory, - ) - -from pyramid.compat import ( - is_bound_method, - is_unbound_method, - ) - -from pyramid.config.util import ( - DEFAULT_PHASH, - MAX_ORDER, - takes_one_arg, - ) - -from pyramid.exceptions import ( - ConfigurationError, - PredicateMismatch, - ) -from pyramid.httpexceptions import HTTPForbidden -from pyramid.util import object_description -from pyramid.view import render_view_to_response -from pyramid import renderers - - -def view_description(view): - try: - return view.__text__ - except AttributeError: - # custom view mappers might not add __text__ - return object_description(view) - -def requestonly(view, attr=None): - return takes_one_arg(view, attr=attr, argname='request') - -@implementer(IViewMapper) -@provider(IViewMapperFactory) -class DefaultViewMapper(object): - def __init__(self, **kw): - self.attr = kw.get('attr') - - def __call__(self, view): - if is_unbound_method(view) and self.attr is None: - raise ConfigurationError(( - 'Unbound method calls are not supported, please set the class ' - 'as your `view` and the method as your `attr`' - )) - - if inspect.isclass(view): - view = self.map_class(view) - else: - view = self.map_nonclass(view) - return view - - def map_class(self, view): - ronly = requestonly(view, self.attr) - if ronly: - mapped_view = self.map_class_requestonly(view) - else: - mapped_view = self.map_class_native(view) - mapped_view.__text__ = 'method %s of %s' % ( - self.attr or '__call__', object_description(view)) - return mapped_view - - def map_nonclass(self, view): - # We do more work here than appears necessary to avoid wrapping the - # view unless it actually requires wrapping (to avoid function call - # overhead). - mapped_view = view - ronly = requestonly(view, self.attr) - if ronly: - mapped_view = self.map_nonclass_requestonly(view) - elif self.attr: - mapped_view = self.map_nonclass_attr(view) - if inspect.isroutine(mapped_view): - # This branch will be true if the view is a function or a method. - # We potentially mutate an unwrapped object here if it's a - # function. We do this to avoid function call overhead of - # injecting another wrapper. However, we must wrap if the - # function is a bound method because we can't set attributes on a - # bound method. - if is_bound_method(view): - _mapped_view = mapped_view - def mapped_view(context, request): - return _mapped_view(context, request) - if self.attr is not None: - mapped_view.__text__ = 'attr %s of %s' % ( - self.attr, object_description(view)) - else: - mapped_view.__text__ = object_description(view) - return mapped_view - - def map_class_requestonly(self, view): - # its a class that has an __init__ which only accepts request - attr = self.attr - def _class_requestonly_view(context, request): - inst = view(request) - request.__view__ = inst - if attr is None: - response = inst() - else: - response = getattr(inst, attr)() - return response - return _class_requestonly_view - - def map_class_native(self, view): - # its a class that has an __init__ which accepts both context and - # request - attr = self.attr - def _class_view(context, request): - inst = view(context, request) - request.__view__ = inst - if attr is None: - response = inst() - else: - response = getattr(inst, attr)() - return response - return _class_view - - def map_nonclass_requestonly(self, view): - # its a function that has a __call__ which accepts only a single - # request argument - attr = self.attr - def _requestonly_view(context, request): - if attr is None: - response = view(request) - else: - response = getattr(view, attr)(request) - return response - return _requestonly_view - - def map_nonclass_attr(self, view): - # its a function that has a __call__ which accepts both context and - # request, but still has an attr - def _attr_view(context, request): - response = getattr(view, self.attr)(context, request) - return response - return _attr_view - - -def wraps_view(wrapper): - def inner(view, info): - wrapper_view = wrapper(view, info) - return preserve_view_attrs(view, wrapper_view) - return inner - -def preserve_view_attrs(view, wrapper): - if view is None: - return wrapper - - if wrapper is view: - return view - - original_view = getattr(view, '__original_view__', None) - - if original_view is None: - original_view = view - - wrapper.__wraps__ = view - wrapper.__original_view__ = original_view - wrapper.__module__ = view.__module__ - wrapper.__doc__ = view.__doc__ - - try: - wrapper.__name__ = view.__name__ - except AttributeError: - wrapper.__name__ = repr(view) - - # attrs that may not exist on "view", but, if so, must be attached to - # "wrapped view" - for attr in ('__permitted__', '__call_permissive__', '__permission__', - '__predicated__', '__predicates__', '__accept__', - '__order__', '__text__'): - try: - setattr(wrapper, attr, getattr(view, attr)) - except AttributeError: - pass - - return wrapper - -def mapped_view(view, info): - mapper = info.options.get('mapper') - if mapper is None: - mapper = getattr(view, '__view_mapper__', None) - if mapper is None: - mapper = info.registry.queryUtility(IViewMapperFactory) - if mapper is None: - mapper = DefaultViewMapper - - mapped_view = mapper(**info.options)(view) - return mapped_view - -mapped_view.options = ('mapper', 'attr') - -def owrapped_view(view, info): - wrapper_viewname = info.options.get('wrapper') - viewname = info.options.get('name') - if not wrapper_viewname: - return view - def _owrapped_view(context, request): - response = view(context, request) - request.wrapped_response = response - request.wrapped_body = response.body - request.wrapped_view = view - wrapped_response = render_view_to_response(context, request, - wrapper_viewname) - if wrapped_response is None: - raise ValueError( - 'No wrapper view named %r found when executing view ' - 'named %r' % (wrapper_viewname, viewname)) - return wrapped_response - return _owrapped_view - -owrapped_view.options = ('name', 'wrapper') - -def http_cached_view(view, info): - if info.settings.get('prevent_http_cache', False): - return view - - seconds = info.options.get('http_cache') - - if seconds is None: - return view - - options = {} - - if isinstance(seconds, (tuple, list)): - try: - seconds, options = seconds - except ValueError: - raise ConfigurationError( - 'If http_cache parameter is a tuple or list, it must be ' - 'in the form (seconds, options); not %s' % (seconds,)) - - def wrapper(context, request): - response = view(context, request) - prevent_caching = getattr(response.cache_control, 'prevent_auto', - False) - if not prevent_caching: - response.cache_expires(seconds, **options) - return response - - return wrapper - -http_cached_view.options = ('http_cache',) - -def secured_view(view, info): - permission = info.options.get('permission') - if permission == NO_PERMISSION_REQUIRED: - # allow views registered within configurations that have a - # default permission to explicitly override the default - # permission, replacing it with no permission at all - permission = None - - wrapped_view = view - authn_policy = info.registry.queryUtility(IAuthenticationPolicy) - authz_policy = info.registry.queryUtility(IAuthorizationPolicy) - - if authn_policy and authz_policy and (permission is not None): - def _permitted(context, request): - principals = authn_policy.effective_principals(request) - return authz_policy.permits(context, principals, permission) - def _secured_view(context, request): - result = _permitted(context, request) - if result: - return view(context, request) - view_name = getattr(view, '__name__', view) - msg = getattr( - request, 'authdebug_message', - 'Unauthorized: %s failed permission check' % view_name) - raise HTTPForbidden(msg, result=result) - _secured_view.__call_permissive__ = view - _secured_view.__permitted__ = _permitted - _secured_view.__permission__ = permission - wrapped_view = _secured_view - - return wrapped_view - -secured_view.options = ('permission',) - -def authdebug_view(view, info): - wrapped_view = view - settings = info.settings - permission = info.options.get('permission') - authn_policy = info.registry.queryUtility(IAuthenticationPolicy) - authz_policy = info.registry.queryUtility(IAuthorizationPolicy) - logger = info.registry.queryUtility(IDebugLogger) - if settings and settings.get('debug_authorization', False): - def _authdebug_view(context, request): - view_name = getattr(request, 'view_name', None) - - if authn_policy and authz_policy: - if permission is NO_PERMISSION_REQUIRED: - msg = 'Allowed (NO_PERMISSION_REQUIRED)' - elif permission is None: - msg = 'Allowed (no permission registered)' - else: - principals = authn_policy.effective_principals(request) - msg = str(authz_policy.permits( - context, principals, permission)) - else: - msg = 'Allowed (no authorization policy in use)' - - view_name = getattr(request, 'view_name', None) - url = getattr(request, 'url', None) - msg = ('debug_authorization of url %s (view name %r against ' - 'context %r): %s' % (url, view_name, context, msg)) - if logger: - logger.debug(msg) - if request is not None: - request.authdebug_message = msg - return view(context, request) - - wrapped_view = _authdebug_view - - return wrapped_view - -authdebug_view.options = ('permission',) - -def predicated_view(view, info): - preds = info.predicates - if not preds: - return view - def predicate_wrapper(context, request): - for predicate in preds: - if not predicate(context, request): - view_name = getattr(view, '__name__', view) - raise PredicateMismatch( - 'predicate mismatch for view %s (%s)' % ( - view_name, predicate.text())) - return view(context, request) - def checker(context, request): - return all((predicate(context, request) for predicate in - preds)) - predicate_wrapper.__predicated__ = checker - predicate_wrapper.__predicates__ = preds - return predicate_wrapper - -def attr_wrapped_view(view, info): - accept, order, phash = (info.options.get('accept', None), - getattr(info, 'order', MAX_ORDER), - getattr(info, 'phash', DEFAULT_PHASH)) - # this is a little silly but we don't want to decorate the original - # function with attributes that indicate accept, order, and phash, - # so we use a wrapper - if ( - (accept is None) and - (order == MAX_ORDER) and - (phash == DEFAULT_PHASH) - ): - return view # defaults - def attr_view(context, request): - return view(context, request) - attr_view.__accept__ = accept - attr_view.__order__ = order - attr_view.__phash__ = phash - attr_view.__view_attr__ = info.options.get('attr') - attr_view.__permission__ = info.options.get('permission') - return attr_view - -attr_wrapped_view.options = ('accept', 'attr', 'permission') - -def rendered_view(view, info): - # one way or another this wrapper must produce a Response (unless - # the renderer is a NullRendererHelper) - renderer = info.options.get('renderer') - if renderer is None: - # register a default renderer if you want super-dynamic - # rendering. registering a default renderer will also allow - # override_renderer to work if a renderer is left unspecified for - # a view registration. - def viewresult_to_response(context, request): - result = view(context, request) - if result.__class__ is Response: # common case - response = result - else: - response = info.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 - - if renderer is renderers.null_renderer: - return view - - def rendered_view(context, request): - result = view(context, request) - if result.__class__ is Response: # potential common case - response = result - else: - # this must adapt, it can't do a simple interface check - # (avoid trying to render webob responses) - response = info.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') - view_renderer = renderers.RendererHelper( - name=renderer_name, - package=info.package, - registry=info.registry) - else: - view_renderer = renderer.clone() - if '__view__' in attrs: - view_inst = attrs.pop('__view__') - else: - view_inst = getattr(view, '__original_view__', view) - response = view_renderer.render_view( - request, result, view_inst, context) - return response - - return rendered_view - -rendered_view.options = ('renderer',) - -def decorated_view(view, info): - decorator = info.options.get('decorator') - if decorator is None: - return view - return decorator(view) - -decorated_view.options = ('decorator',) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 1516743ad..2a019726f 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -76,9 +76,9 @@ from pyramid.util import ( ) import pyramid.config.predicates -import pyramid.config.derivations +import pyramid.viewderivers -from pyramid.config.derivations import ( +from pyramid.viewderivers import ( preserve_view_attrs, view_description, requestonly, @@ -1028,7 +1028,7 @@ class ViewsConfiguratorMixin(object): raise ConfigurationError('Unknown view options: %s' % (kw,)) def _apply_view_derivers(self, info): - d = pyramid.config.derivations + d = pyramid.viewderivers # These derivations have fixed order outer_derivers = [('attr_wrapped_view', d.attr_wrapped_view), ('predicated_view', d.predicated_view)] @@ -1146,7 +1146,7 @@ class ViewsConfiguratorMixin(object): order=PHASE1_CONFIG) # must be registered before add_view def add_default_view_derivers(self): - d = pyramid.config.derivations + d = pyramid.viewderivers derivers = [ ('authdebug_view', d.authdebug_view), ('secured_view', d.secured_view), diff --git a/pyramid/tests/test_config/test_derivations.py b/pyramid/tests/test_config/test_derivations.py deleted file mode 100644 index d93b37f38..000000000 --- a/pyramid/tests/test_config/test_derivations.py +++ /dev/null @@ -1,1376 +0,0 @@ -import unittest -from zope.interface import implementer - -from pyramid import testing -from pyramid.exceptions import ConfigurationError -from pyramid.interfaces import ( - IResponse, - IRequest, - ) - -class TestDeriveView(unittest.TestCase): - - def setUp(self): - self.config = testing.setUp() - - def tearDown(self): - self.config = None - testing.tearDown() - - def _makeRequest(self): - request = DummyRequest() - request.registry = self.config.registry - return request - - def _registerLogger(self): - from pyramid.interfaces import IDebugLogger - logger = DummyLogger() - self.config.registry.registerUtility(logger, IDebugLogger) - return logger - - def _registerSecurityPolicy(self, permissive): - from pyramid.interfaces import IAuthenticationPolicy - from pyramid.interfaces import IAuthorizationPolicy - policy = DummySecurityPolicy(permissive) - self.config.registry.registerUtility(policy, IAuthenticationPolicy) - self.config.registry.registerUtility(policy, IAuthorizationPolicy) - - def test_function_returns_non_adaptable(self): - def view(request): - return None - result = self.config.derive_view(view) - self.assertFalse(result is view) - try: - result(None, None) - except ValueError as e: - self.assertEqual( - e.args[0], - 'Could not convert return value of the view callable function ' - 'pyramid.tests.test_config.test_derivations.view into a response ' - 'object. The value returned was None. You may have forgotten ' - 'to return a value from the view callable.' - ) - else: # pragma: no cover - raise AssertionError - - def test_function_returns_non_adaptable_dict(self): - def view(request): - return {'a':1} - result = self.config.derive_view(view) - self.assertFalse(result is view) - try: - result(None, None) - except ValueError as e: - self.assertEqual( - e.args[0], - "Could not convert return value of the view callable function " - "pyramid.tests.test_config.test_derivations.view into a response " - "object. The value returned was {'a': 1}. You may have " - "forgotten to define a renderer in the view configuration." - ) - else: # pragma: no cover - raise AssertionError - - def test_instance_returns_non_adaptable(self): - class AView(object): - def __call__(self, request): - return None - view = AView() - result = self.config.derive_view(view) - self.assertFalse(result is view) - try: - result(None, None) - except ValueError as e: - msg = e.args[0] - self.assertTrue(msg.startswith( - 'Could not convert return value of the view callable object ' - ' into a response object. The value returned was None. You ' - 'may have forgotten to return a value from the view callable.')) - 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 - result = self.config.derive_view(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() - result = self.config.derive_view(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): - def __init__(self, request): - pass - def __call__(self): - return None - result = self.config.derive_view(AView) - self.assertFalse(result is AView) - try: - result(None, request) - except ValueError as e: - self.assertEqual( - e.args[0], - 'Could not convert return value of the view callable ' - 'method __call__ of ' - 'class pyramid.tests.test_config.test_derivations.AView into a ' - 'response object. The value returned was None. You may have ' - 'forgotten to return a value from the view callable.' - ) - else: # pragma: no cover - raise AssertionError - - def test_requestonly_nondefault_method_returns_non_adaptable(self): - request = DummyRequest() - class AView(object): - def __init__(self, request): - pass - def theviewmethod(self): - return None - result = self.config.derive_view(AView, attr='theviewmethod') - self.assertFalse(result is AView) - try: - result(None, request) - except ValueError as e: - self.assertEqual( - e.args[0], - 'Could not convert return value of the view callable ' - 'method theviewmethod of ' - 'class pyramid.tests.test_config.test_derivations.AView into a ' - 'response object. The value returned was None. You may have ' - 'forgotten to return a value from the view callable.' - ) - else: # pragma: no cover - raise AssertionError - - def test_requestonly_function(self): - response = DummyResponse() - def view(request): - return response - result = self.config.derive_view(view) - self.assertFalse(result is view) - self.assertEqual(result(None, None), response) - - def test_requestonly_function_with_renderer(self): - response = DummyResponse() - class moo(object): - def render_view(inself, req, resp, view_inst, ctx): - self.assertEqual(req, request) - self.assertEqual(resp, 'OK') - self.assertEqual(view_inst, view) - self.assertEqual(ctx, context) - return response - def clone(self): - return self - def view(request): - return 'OK' - result = self.config.derive_view(view, renderer=moo()) - self.assertFalse(result.__wraps__ is view) - request = self._makeRequest() - context = testing.DummyResource() - self.assertEqual(result(context, request), response) - - def test_requestonly_function_with_renderer_request_override(self): - def moo(info): - def inner(value, system): - self.assertEqual(value, 'OK') - self.assertEqual(system['request'], request) - self.assertEqual(system['context'], context) - return b'moo' - return inner - def view(request): - return 'OK' - self.config.add_renderer('moo', moo) - result = self.config.derive_view(view, renderer='string') - self.assertFalse(result is view) - request = self._makeRequest() - request.override_renderer = 'moo' - context = testing.DummyResource() - self.assertEqual(result(context, request).body, b'moo') - - def test_requestonly_function_with_renderer_request_has_view(self): - response = DummyResponse() - class moo(object): - def render_view(inself, req, resp, view_inst, ctx): - self.assertEqual(req, request) - self.assertEqual(resp, 'OK') - self.assertEqual(view_inst, 'view') - self.assertEqual(ctx, context) - return response - def clone(self): - return self - def view(request): - return 'OK' - result = self.config.derive_view(view, renderer=moo()) - self.assertFalse(result.__wraps__ is view) - request = self._makeRequest() - request.__view__ = 'view' - context = testing.DummyResource() - r = result(context, request) - self.assertEqual(r, response) - self.assertFalse(hasattr(request, '__view__')) - - def test_class_without_attr(self): - response = DummyResponse() - class View(object): - def __init__(self, request): - pass - def __call__(self): - return response - result = self.config.derive_view(View) - request = self._makeRequest() - self.assertEqual(result(None, request), response) - self.assertEqual(request.__view__.__class__, View) - - def test_class_with_attr(self): - response = DummyResponse() - class View(object): - def __init__(self, request): - pass - def another(self): - return response - result = self.config.derive_view(View, attr='another') - request = self._makeRequest() - self.assertEqual(result(None, request), response) - self.assertEqual(request.__view__.__class__, View) - - def test_as_function_context_and_request(self): - def view(context, request): - return 'OK' - result = self.config.derive_view(view) - self.assertTrue(result.__wraps__ is view) - self.assertFalse(hasattr(result, '__call_permissive__')) - self.assertEqual(view(None, None), 'OK') - - def test_as_function_requestonly(self): - response = DummyResponse() - def view(request): - return response - result = self.config.derive_view(view) - self.assertFalse(result is view) - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.assertFalse(hasattr(result, '__call_permissive__')) - self.assertEqual(result(None, None), response) - - def test_as_newstyle_class_context_and_request(self): - response = DummyResponse() - class view(object): - def __init__(self, context, request): - pass - def __call__(self): - return response - result = self.config.derive_view(view) - self.assertFalse(result is view) - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.assertFalse(hasattr(result, '__call_permissive__')) - request = self._makeRequest() - self.assertEqual(result(None, request), response) - self.assertEqual(request.__view__.__class__, view) - - def test_as_newstyle_class_requestonly(self): - response = DummyResponse() - class view(object): - def __init__(self, context, request): - pass - def __call__(self): - return response - result = self.config.derive_view(view) - self.assertFalse(result is view) - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.assertFalse(hasattr(result, '__call_permissive__')) - request = self._makeRequest() - self.assertEqual(result(None, request), response) - self.assertEqual(request.__view__.__class__, view) - - def test_as_oldstyle_class_context_and_request(self): - response = DummyResponse() - class view: - def __init__(self, context, request): - pass - def __call__(self): - return response - result = self.config.derive_view(view) - self.assertFalse(result is view) - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.assertFalse(hasattr(result, '__call_permissive__')) - request = self._makeRequest() - self.assertEqual(result(None, request), response) - self.assertEqual(request.__view__.__class__, view) - - def test_as_oldstyle_class_requestonly(self): - response = DummyResponse() - class view: - def __init__(self, context, request): - pass - def __call__(self): - return response - result = self.config.derive_view(view) - self.assertFalse(result is view) - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.assertFalse(hasattr(result, '__call_permissive__')) - request = self._makeRequest() - self.assertEqual(result(None, request), response) - self.assertEqual(request.__view__.__class__, view) - - def test_as_instance_context_and_request(self): - response = DummyResponse() - class View: - def __call__(self, context, request): - return response - view = View() - result = self.config.derive_view(view) - self.assertTrue(result.__wraps__ is view) - self.assertFalse(hasattr(result, '__call_permissive__')) - self.assertEqual(result(None, None), response) - - def test_as_instance_requestonly(self): - response = DummyResponse() - class View: - def __call__(self, request): - return response - view = View() - result = self.config.derive_view(view) - self.assertFalse(result is view) - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertTrue('test_derivations' in result.__name__) - self.assertFalse(hasattr(result, '__call_permissive__')) - self.assertEqual(result(None, None), response) - - def test_with_debug_authorization_no_authpol(self): - response = DummyResponse() - view = lambda *arg: response - self.config.registry.settings = dict( - debug_authorization=True, reload_templates=True) - logger = self._registerLogger() - result = self.config._derive_view(view, permission='view') - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.assertFalse(hasattr(result, '__call_permissive__')) - request = self._makeRequest() - request.view_name = 'view_name' - request.url = 'url' - self.assertEqual(result(None, request), response) - self.assertEqual(len(logger.messages), 1) - self.assertEqual(logger.messages[0], - "debug_authorization of url url (view name " - "'view_name' against context None): Allowed " - "(no authorization policy in use)") - - def test_with_debug_authorization_authn_policy_no_authz_policy(self): - response = DummyResponse() - view = lambda *arg: response - self.config.registry.settings = dict(debug_authorization=True) - from pyramid.interfaces import IAuthenticationPolicy - policy = DummySecurityPolicy(False) - self.config.registry.registerUtility(policy, IAuthenticationPolicy) - logger = self._registerLogger() - result = self.config._derive_view(view, permission='view') - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.assertFalse(hasattr(result, '__call_permissive__')) - request = self._makeRequest() - request.view_name = 'view_name' - request.url = 'url' - self.assertEqual(result(None, request), response) - self.assertEqual(len(logger.messages), 1) - self.assertEqual(logger.messages[0], - "debug_authorization of url url (view name " - "'view_name' against context None): Allowed " - "(no authorization policy in use)") - - def test_with_debug_authorization_authz_policy_no_authn_policy(self): - response = DummyResponse() - view = lambda *arg: response - self.config.registry.settings = dict(debug_authorization=True) - from pyramid.interfaces import IAuthorizationPolicy - policy = DummySecurityPolicy(False) - self.config.registry.registerUtility(policy, IAuthorizationPolicy) - logger = self._registerLogger() - result = self.config._derive_view(view, permission='view') - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.assertFalse(hasattr(result, '__call_permissive__')) - request = self._makeRequest() - request.view_name = 'view_name' - request.url = 'url' - self.assertEqual(result(None, request), response) - self.assertEqual(len(logger.messages), 1) - self.assertEqual(logger.messages[0], - "debug_authorization of url url (view name " - "'view_name' against context None): Allowed " - "(no authorization policy in use)") - - def test_with_debug_authorization_no_permission(self): - response = DummyResponse() - view = lambda *arg: response - self.config.registry.settings = dict( - debug_authorization=True, reload_templates=True) - self._registerSecurityPolicy(True) - logger = self._registerLogger() - result = self.config._derive_view(view) - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.assertFalse(hasattr(result, '__call_permissive__')) - request = self._makeRequest() - request.view_name = 'view_name' - request.url = 'url' - self.assertEqual(result(None, request), response) - self.assertEqual(len(logger.messages), 1) - self.assertEqual(logger.messages[0], - "debug_authorization of url url (view name " - "'view_name' against context None): Allowed (" - "no permission registered)") - - def test_debug_auth_permission_authpol_permitted(self): - response = DummyResponse() - view = lambda *arg: response - self.config.registry.settings = dict( - debug_authorization=True, reload_templates=True) - logger = self._registerLogger() - self._registerSecurityPolicy(True) - result = self.config._derive_view(view, permission='view') - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.assertEqual(result.__call_permissive__.__wraps__, view) - request = self._makeRequest() - request.view_name = 'view_name' - request.url = 'url' - self.assertEqual(result(None, request), response) - self.assertEqual(len(logger.messages), 1) - self.assertEqual(logger.messages[0], - "debug_authorization of url url (view name " - "'view_name' against context None): True") - - def test_debug_auth_permission_authpol_permitted_no_request(self): - response = DummyResponse() - view = lambda *arg: response - self.config.registry.settings = dict( - debug_authorization=True, reload_templates=True) - logger = self._registerLogger() - self._registerSecurityPolicy(True) - result = self.config._derive_view(view, permission='view') - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.assertEqual(result.__call_permissive__.__wraps__, view) - self.assertEqual(result(None, None), response) - self.assertEqual(len(logger.messages), 1) - self.assertEqual(logger.messages[0], - "debug_authorization of url None (view name " - "None against context None): True") - - def test_debug_auth_permission_authpol_denied(self): - from pyramid.httpexceptions import HTTPForbidden - response = DummyResponse() - view = lambda *arg: response - self.config.registry.settings = dict( - debug_authorization=True, reload_templates=True) - logger = self._registerLogger() - self._registerSecurityPolicy(False) - result = self.config._derive_view(view, permission='view') - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.assertEqual(result.__call_permissive__.__wraps__, view) - request = self._makeRequest() - request.view_name = 'view_name' - request.url = 'url' - self.assertRaises(HTTPForbidden, result, None, request) - self.assertEqual(len(logger.messages), 1) - self.assertEqual(logger.messages[0], - "debug_authorization of url url (view name " - "'view_name' against context None): False") - - def test_debug_auth_permission_authpol_denied2(self): - view = lambda *arg: 'OK' - self.config.registry.settings = dict( - debug_authorization=True, reload_templates=True) - self._registerLogger() - self._registerSecurityPolicy(False) - result = self.config._derive_view(view, permission='view') - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - request = self._makeRequest() - request.view_name = 'view_name' - request.url = 'url' - permitted = result.__permitted__(None, None) - self.assertEqual(permitted, False) - - def test_debug_auth_permission_authpol_overridden(self): - from pyramid.security import NO_PERMISSION_REQUIRED - response = DummyResponse() - view = lambda *arg: response - self.config.registry.settings = dict( - debug_authorization=True, reload_templates=True) - logger = self._registerLogger() - self._registerSecurityPolicy(False) - result = self.config._derive_view(view, permission=NO_PERMISSION_REQUIRED) - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.assertFalse(hasattr(result, '__call_permissive__')) - request = self._makeRequest() - request.view_name = 'view_name' - request.url = 'url' - self.assertEqual(result(None, request), response) - self.assertEqual(len(logger.messages), 1) - self.assertEqual(logger.messages[0], - "debug_authorization of url url (view name " - "'view_name' against context None): " - "Allowed (NO_PERMISSION_REQUIRED)") - - def test_secured_view_authn_policy_no_authz_policy(self): - response = DummyResponse() - view = lambda *arg: response - self.config.registry.settings = {} - from pyramid.interfaces import IAuthenticationPolicy - policy = DummySecurityPolicy(False) - self.config.registry.registerUtility(policy, IAuthenticationPolicy) - result = self.config._derive_view(view, permission='view') - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.assertFalse(hasattr(result, '__call_permissive__')) - request = self._makeRequest() - request.view_name = 'view_name' - request.url = 'url' - self.assertEqual(result(None, request), response) - - def test_secured_view_authz_policy_no_authn_policy(self): - response = DummyResponse() - view = lambda *arg: response - self.config.registry.settings = {} - from pyramid.interfaces import IAuthorizationPolicy - policy = DummySecurityPolicy(False) - self.config.registry.registerUtility(policy, IAuthorizationPolicy) - result = self.config._derive_view(view, permission='view') - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.assertFalse(hasattr(result, '__call_permissive__')) - request = self._makeRequest() - request.view_name = 'view_name' - request.url = 'url' - self.assertEqual(result(None, request), response) - - def test_secured_view_raises_forbidden_no_name(self): - from pyramid.interfaces import IAuthenticationPolicy - from pyramid.interfaces import IAuthorizationPolicy - from pyramid.httpexceptions import HTTPForbidden - response = DummyResponse() - view = lambda *arg: response - self.config.registry.settings = {} - policy = DummySecurityPolicy(False) - self.config.registry.registerUtility(policy, IAuthenticationPolicy) - self.config.registry.registerUtility(policy, IAuthorizationPolicy) - result = self.config._derive_view(view, permission='view') - request = self._makeRequest() - request.view_name = 'view_name' - request.url = 'url' - try: - result(None, request) - except HTTPForbidden as e: - self.assertEqual(e.message, - 'Unauthorized: failed permission check') - else: # pragma: no cover - raise AssertionError - - def test_secured_view_raises_forbidden_with_name(self): - from pyramid.interfaces import IAuthenticationPolicy - from pyramid.interfaces import IAuthorizationPolicy - from pyramid.httpexceptions import HTTPForbidden - def myview(request): pass - self.config.registry.settings = {} - policy = DummySecurityPolicy(False) - self.config.registry.registerUtility(policy, IAuthenticationPolicy) - self.config.registry.registerUtility(policy, IAuthorizationPolicy) - result = self.config._derive_view(myview, permission='view') - request = self._makeRequest() - request.view_name = 'view_name' - request.url = 'url' - try: - result(None, request) - except HTTPForbidden as e: - self.assertEqual(e.message, - 'Unauthorized: myview failed permission check') - else: # pragma: no cover - raise AssertionError - - def test_predicate_mismatch_view_has_no_name(self): - from pyramid.exceptions import PredicateMismatch - response = DummyResponse() - view = lambda *arg: response - def predicate1(context, request): - return False - predicate1.text = lambda *arg: 'text' - result = self.config._derive_view(view, predicates=[predicate1]) - request = self._makeRequest() - request.method = 'POST' - try: - result(None, None) - except PredicateMismatch as e: - self.assertEqual(e.detail, - 'predicate mismatch for view (text)') - else: # pragma: no cover - raise AssertionError - - def test_predicate_mismatch_view_has_name(self): - from pyramid.exceptions import PredicateMismatch - def myview(request): pass - def predicate1(context, request): - return False - predicate1.text = lambda *arg: 'text' - result = self.config._derive_view(myview, predicates=[predicate1]) - request = self._makeRequest() - request.method = 'POST' - try: - result(None, None) - except PredicateMismatch as e: - self.assertEqual(e.detail, - 'predicate mismatch for view myview (text)') - else: # pragma: no cover - raise AssertionError - - def test_predicate_mismatch_exception_has_text_in_detail(self): - from pyramid.exceptions import PredicateMismatch - def myview(request): pass - def predicate1(context, request): - return True - predicate1.text = lambda *arg: 'pred1' - def predicate2(context, request): - return False - predicate2.text = lambda *arg: 'pred2' - result = self.config._derive_view(myview, - predicates=[predicate1, predicate2]) - request = self._makeRequest() - request.method = 'POST' - try: - result(None, None) - except PredicateMismatch as e: - self.assertEqual(e.detail, - 'predicate mismatch for view myview (pred2)') - else: # pragma: no cover - raise AssertionError - - def test_with_predicates_all(self): - response = DummyResponse() - view = lambda *arg: response - predicates = [] - def predicate1(context, request): - predicates.append(True) - return True - def predicate2(context, request): - predicates.append(True) - return True - result = self.config._derive_view(view, - predicates=[predicate1, predicate2]) - request = self._makeRequest() - request.method = 'POST' - next = result(None, None) - self.assertEqual(next, response) - self.assertEqual(predicates, [True, True]) - - def test_with_predicates_checker(self): - view = lambda *arg: 'OK' - predicates = [] - def predicate1(context, request): - predicates.append(True) - return True - def predicate2(context, request): - predicates.append(True) - return True - result = self.config._derive_view(view, - predicates=[predicate1, predicate2]) - request = self._makeRequest() - request.method = 'POST' - next = result.__predicated__(None, None) - self.assertEqual(next, True) - self.assertEqual(predicates, [True, True]) - - def test_with_predicates_notall(self): - from pyramid.httpexceptions import HTTPNotFound - view = lambda *arg: 'OK' - predicates = [] - def predicate1(context, request): - predicates.append(True) - return True - predicate1.text = lambda *arg: 'text' - def predicate2(context, request): - predicates.append(True) - return False - predicate2.text = lambda *arg: 'text' - result = self.config._derive_view(view, - predicates=[predicate1, predicate2]) - request = self._makeRequest() - request.method = 'POST' - self.assertRaises(HTTPNotFound, result, None, None) - self.assertEqual(predicates, [True, True]) - - def test_with_wrapper_viewname(self): - from pyramid.response import Response - from pyramid.interfaces import IView - from pyramid.interfaces import IViewClassifier - inner_response = Response('OK') - def inner_view(context, request): - return inner_response - def outer_view(context, request): - self.assertEqual(request.wrapped_response, inner_response) - self.assertEqual(request.wrapped_body, inner_response.body) - self.assertEqual(request.wrapped_view.__original_view__, - inner_view) - return Response(b'outer ' + request.wrapped_body) - self.config.registry.registerAdapter( - outer_view, (IViewClassifier, None, None), IView, 'owrap') - result = self.config._derive_view(inner_view, viewname='inner', - wrapper_viewname='owrap') - self.assertFalse(result is inner_view) - self.assertEqual(inner_view.__module__, result.__module__) - self.assertEqual(inner_view.__doc__, result.__doc__) - request = self._makeRequest() - response = result(None, request) - self.assertEqual(response.body, b'outer OK') - - def test_with_wrapper_viewname_notfound(self): - from pyramid.response import Response - inner_response = Response('OK') - def inner_view(context, request): - return inner_response - wrapped = self.config._derive_view(inner_view, viewname='inner', - wrapper_viewname='owrap') - request = self._makeRequest() - self.assertRaises(ValueError, wrapped, None, request) - - def test_as_newstyle_class_context_and_request_attr_and_renderer(self): - response = DummyResponse() - class renderer(object): - def render_view(inself, req, resp, view_inst, ctx): - self.assertEqual(req, request) - self.assertEqual(resp, {'a':'1'}) - self.assertEqual(view_inst.__class__, View) - self.assertEqual(ctx, context) - return response - def clone(self): - return self - class View(object): - def __init__(self, context, request): - pass - def index(self): - return {'a':'1'} - result = self.config._derive_view(View, - renderer=renderer(), attr='index') - self.assertFalse(result is View) - self.assertEqual(result.__module__, View.__module__) - self.assertEqual(result.__doc__, View.__doc__) - self.assertEqual(result.__name__, View.__name__) - request = self._makeRequest() - context = testing.DummyResource() - self.assertEqual(result(context, request), response) - - def test_as_newstyle_class_requestonly_attr_and_renderer(self): - response = DummyResponse() - class renderer(object): - def render_view(inself, req, resp, view_inst, ctx): - self.assertEqual(req, request) - self.assertEqual(resp, {'a':'1'}) - self.assertEqual(view_inst.__class__, View) - self.assertEqual(ctx, context) - return response - def clone(self): - return self - class View(object): - def __init__(self, request): - pass - def index(self): - return {'a':'1'} - result = self.config.derive_view(View, - renderer=renderer(), attr='index') - self.assertFalse(result is View) - self.assertEqual(result.__module__, View.__module__) - self.assertEqual(result.__doc__, View.__doc__) - self.assertEqual(result.__name__, View.__name__) - request = self._makeRequest() - context = testing.DummyResource() - self.assertEqual(result(context, request), response) - - def test_as_oldstyle_cls_context_request_attr_and_renderer(self): - response = DummyResponse() - class renderer(object): - def render_view(inself, req, resp, view_inst, ctx): - self.assertEqual(req, request) - self.assertEqual(resp, {'a':'1'}) - self.assertEqual(view_inst.__class__, View) - self.assertEqual(ctx, context) - return response - def clone(self): - return self - class View: - def __init__(self, context, request): - pass - def index(self): - return {'a':'1'} - result = self.config.derive_view(View, - renderer=renderer(), attr='index') - self.assertFalse(result is View) - self.assertEqual(result.__module__, View.__module__) - self.assertEqual(result.__doc__, View.__doc__) - self.assertEqual(result.__name__, View.__name__) - request = self._makeRequest() - context = testing.DummyResource() - self.assertEqual(result(context, request), response) - - def test_as_oldstyle_cls_requestonly_attr_and_renderer(self): - response = DummyResponse() - class renderer(object): - def render_view(inself, req, resp, view_inst, ctx): - self.assertEqual(req, request) - self.assertEqual(resp, {'a':'1'}) - self.assertEqual(view_inst.__class__, View) - self.assertEqual(ctx, context) - return response - def clone(self): - return self - class View: - def __init__(self, request): - pass - def index(self): - return {'a':'1'} - result = self.config.derive_view(View, - renderer=renderer(), attr='index') - self.assertFalse(result is View) - self.assertEqual(result.__module__, View.__module__) - self.assertEqual(result.__doc__, View.__doc__) - self.assertEqual(result.__name__, View.__name__) - request = self._makeRequest() - context = testing.DummyResource() - self.assertEqual(result(context, request), response) - - def test_as_instance_context_and_request_attr_and_renderer(self): - response = DummyResponse() - class renderer(object): - def render_view(inself, req, resp, view_inst, ctx): - self.assertEqual(req, request) - self.assertEqual(resp, {'a':'1'}) - self.assertEqual(view_inst, view) - self.assertEqual(ctx, context) - return response - def clone(self): - return self - class View: - def index(self, context, request): - return {'a':'1'} - view = View() - result = self.config.derive_view(view, - renderer=renderer(), attr='index') - self.assertFalse(result is view) - self.assertEqual(result.__module__, view.__module__) - self.assertEqual(result.__doc__, view.__doc__) - request = self._makeRequest() - context = testing.DummyResource() - self.assertEqual(result(context, request), response) - - def test_as_instance_requestonly_attr_and_renderer(self): - response = DummyResponse() - class renderer(object): - def render_view(inself, req, resp, view_inst, ctx): - self.assertEqual(req, request) - self.assertEqual(resp, {'a':'1'}) - self.assertEqual(view_inst, view) - self.assertEqual(ctx, context) - return response - def clone(self): - return self - class View: - def index(self, request): - return {'a':'1'} - view = View() - result = self.config.derive_view(view, - renderer=renderer(), attr='index') - self.assertFalse(result is view) - self.assertEqual(result.__module__, view.__module__) - self.assertEqual(result.__doc__, view.__doc__) - request = self._makeRequest() - context = testing.DummyResource() - self.assertEqual(result(context, request), response) - - def test_with_view_mapper_config_specified(self): - response = DummyResponse() - class mapper(object): - def __init__(self, **kw): - self.kw = kw - def __call__(self, view): - def wrapped(context, request): - return response - return wrapped - def view(context, request): return 'NOTOK' - result = self.config._derive_view(view, mapper=mapper) - self.assertFalse(result.__wraps__ is view) - self.assertEqual(result(None, None), response) - - def test_with_view_mapper_view_specified(self): - from pyramid.response import Response - response = Response() - def mapper(**kw): - def inner(view): - def superinner(context, request): - self.assertEqual(request, None) - return response - return superinner - return inner - def view(context, request): return 'NOTOK' - view.__view_mapper__ = mapper - result = self.config.derive_view(view) - self.assertFalse(result.__wraps__ is view) - self.assertEqual(result(None, None), response) - - def test_with_view_mapper_default_mapper_specified(self): - from pyramid.response import Response - response = Response() - def mapper(**kw): - def inner(view): - def superinner(context, request): - self.assertEqual(request, None) - return response - return superinner - return inner - self.config.set_view_mapper(mapper) - def view(context, request): return 'NOTOK' - result = self.config.derive_view(view) - self.assertFalse(result.__wraps__ is view) - self.assertEqual(result(None, None), response) - - def test_attr_wrapped_view_branching_default_phash(self): - from pyramid.config.util import DEFAULT_PHASH - def view(context, request): pass - result = self.config._derive_view(view, phash=DEFAULT_PHASH) - self.assertEqual(result.__wraps__, view) - - def test_attr_wrapped_view_branching_nondefault_phash(self): - def view(context, request): pass - result = self.config._derive_view(view, phash='nondefault') - self.assertNotEqual(result, view) - - def test_http_cached_view_integer(self): - import datetime - from pyramid.response import Response - response = Response('OK') - def inner_view(context, request): - return response - result = self.config._derive_view(inner_view, http_cache=3600) - self.assertFalse(result is inner_view) - self.assertEqual(inner_view.__module__, result.__module__) - self.assertEqual(inner_view.__doc__, result.__doc__) - request = self._makeRequest() - when = datetime.datetime.utcnow() + datetime.timedelta(hours=1) - result = result(None, request) - self.assertEqual(result, response) - headers = dict(result.headerlist) - expires = parse_httpdate(headers['Expires']) - assert_similar_datetime(expires, when) - self.assertEqual(headers['Cache-Control'], 'max-age=3600') - - def test_http_cached_view_timedelta(self): - import datetime - from pyramid.response import Response - response = Response('OK') - def inner_view(context, request): - return response - result = self.config._derive_view(inner_view, - http_cache=datetime.timedelta(hours=1)) - self.assertFalse(result is inner_view) - self.assertEqual(inner_view.__module__, result.__module__) - self.assertEqual(inner_view.__doc__, result.__doc__) - request = self._makeRequest() - when = datetime.datetime.utcnow() + datetime.timedelta(hours=1) - result = result(None, request) - self.assertEqual(result, response) - headers = dict(result.headerlist) - expires = parse_httpdate(headers['Expires']) - assert_similar_datetime(expires, when) - self.assertEqual(headers['Cache-Control'], 'max-age=3600') - - def test_http_cached_view_tuple(self): - import datetime - from pyramid.response import Response - response = Response('OK') - def inner_view(context, request): - return response - result = self.config._derive_view(inner_view, - http_cache=(3600, {'public':True})) - self.assertFalse(result is inner_view) - self.assertEqual(inner_view.__module__, result.__module__) - self.assertEqual(inner_view.__doc__, result.__doc__) - request = self._makeRequest() - when = datetime.datetime.utcnow() + datetime.timedelta(hours=1) - result = result(None, request) - self.assertEqual(result, response) - headers = dict(result.headerlist) - expires = parse_httpdate(headers['Expires']) - assert_similar_datetime(expires, when) - self.assertEqual(headers['Cache-Control'], 'max-age=3600, public') - - def test_http_cached_view_tuple_seconds_None(self): - from pyramid.response import Response - response = Response('OK') - def inner_view(context, request): - return response - result = self.config._derive_view(inner_view, - http_cache=(None, {'public':True})) - self.assertFalse(result is inner_view) - self.assertEqual(inner_view.__module__, result.__module__) - self.assertEqual(inner_view.__doc__, result.__doc__) - request = self._makeRequest() - result = result(None, request) - self.assertEqual(result, response) - headers = dict(result.headerlist) - self.assertFalse('Expires' in headers) - self.assertEqual(headers['Cache-Control'], 'public') - - def test_http_cached_view_prevent_auto_set(self): - from pyramid.response import Response - response = Response() - response.cache_control.prevent_auto = True - def inner_view(context, request): - return response - result = self.config._derive_view(inner_view, http_cache=3600) - request = self._makeRequest() - result = result(None, request) - self.assertEqual(result, response) # doesn't blow up - headers = dict(result.headerlist) - self.assertFalse('Expires' in headers) - self.assertFalse('Cache-Control' in headers) - - def test_http_cached_prevent_http_cache_in_settings(self): - self.config.registry.settings['prevent_http_cache'] = True - from pyramid.response import Response - response = Response() - def inner_view(context, request): - return response - result = self.config._derive_view(inner_view, http_cache=3600) - request = self._makeRequest() - result = result(None, request) - self.assertEqual(result, response) - headers = dict(result.headerlist) - self.assertFalse('Expires' in headers) - self.assertFalse('Cache-Control' in headers) - - def test_http_cached_view_bad_tuple(self): - def view(request): pass - self.assertRaises(ConfigurationError, self.config._derive_view, - view, http_cache=(None,)) - - -class TestDerivationOrder(unittest.TestCase): - def setUp(self): - self.config = testing.setUp() - - def tearDown(self): - self.config = None - testing.tearDown() - - def test_right_order_user_sorted(self): - from pyramid.interfaces import IViewDerivers - - self.config.add_view_deriver(None, 'deriv1') - self.config.add_view_deriver(None, 'deriv2', over='deriv1') - self.config.add_view_deriver(None, 'deriv3', under='deriv2') - - derivers = self.config.registry.getUtility(IViewDerivers) - derivers_sorted = derivers.sorted() - dlist = [d for (d, _) in derivers_sorted] - self.assertEqual([ - 'authdebug_view', - 'secured_view', - 'owrapped_view', - 'http_cached_view', - 'decorated_view', - 'deriv2', - 'deriv3', - 'deriv1', - 'rendered_view', - ], dlist) - - def test_right_order_implicit(self): - from pyramid.interfaces import IViewDerivers - - self.config.add_view_deriver(None, 'deriv1') - self.config.add_view_deriver(None, 'deriv2') - self.config.add_view_deriver(None, 'deriv3') - - derivers = self.config.registry.getUtility(IViewDerivers) - derivers_sorted = derivers.sorted() - dlist = [d for (d, _) in derivers_sorted] - self.assertEqual([ - 'authdebug_view', - 'secured_view', - 'owrapped_view', - 'http_cached_view', - 'decorated_view', - 'deriv3', - 'deriv2', - 'deriv1', - 'rendered_view', - ], dlist) - - def test_right_order_under_rendered_view(self): - from pyramid.interfaces import IViewDerivers - - self.config.add_view_deriver(None, 'deriv1', under='rendered_view') - - derivers = self.config.registry.getUtility(IViewDerivers) - derivers_sorted = derivers.sorted() - dlist = [d for (d, _) in derivers_sorted] - self.assertEqual([ - 'authdebug_view', - 'secured_view', - 'owrapped_view', - 'http_cached_view', - 'decorated_view', - 'rendered_view', - 'deriv1', - ], dlist) - - - def test_right_order_under_rendered_view_others(self): - from pyramid.interfaces import IViewDerivers - - self.config.add_view_deriver(None, 'deriv1', under='rendered_view') - self.config.add_view_deriver(None, 'deriv2') - self.config.add_view_deriver(None, 'deriv3') - - derivers = self.config.registry.getUtility(IViewDerivers) - derivers_sorted = derivers.sorted() - dlist = [d for (d, _) in derivers_sorted] - self.assertEqual([ - 'authdebug_view', - 'secured_view', - 'owrapped_view', - 'http_cached_view', - 'decorated_view', - 'deriv3', - 'deriv2', - 'rendered_view', - 'deriv1', - ], dlist) - - -class TestAddDeriver(unittest.TestCase): - - def setUp(self): - self.config = testing.setUp() - - def tearDown(self): - self.config = None - testing.tearDown() - - def test_add_single_deriver(self): - response = DummyResponse() - response.deriv = False - view = lambda *arg: response - - def deriv(view, info): - self.assertFalse(response.deriv) - response.deriv = True - return view - - result = self.config._derive_view(view) - self.assertFalse(response.deriv) - self.config.add_view_deriver(deriv, 'test_deriv') - - result = self.config._derive_view(view) - self.assertTrue(response.deriv) - - def test_override_deriver(self): - flags = {} - - class AView: - def __init__(self): - self.response = DummyResponse() - - def deriv1(view, value, **kw): - flags['deriv1'] = True - return view - - def deriv2(view, value, **kw): - flags['deriv2'] = True - return view - - view1 = AView() - self.config.add_view_deriver(deriv1, 'test_deriv') - result = self.config._derive_view(view1) - self.assertTrue(flags.get('deriv1')) - self.assertFalse(flags.get('deriv2')) - - flags.clear() - view2 = AView() - self.config.add_view_deriver(deriv2, 'test_deriv') - result = self.config._derive_view(view2) - self.assertFalse(flags.get('deriv1')) - self.assertTrue(flags.get('deriv2')) - - def test_add_multi_derivers_ordered(self): - response = DummyResponse() - view = lambda *arg: response - response.deriv = [] - - def deriv1(view, value, **kw): - response.deriv.append('deriv1') - return view - - def deriv2(view, value, **kw): - response.deriv.append('deriv2') - return view - - def deriv3(view, value, **kw): - response.deriv.append('deriv3') - return view - - self.config.add_view_deriver(deriv1, 'deriv1') - self.config.add_view_deriver(deriv2, 'deriv2', under='deriv1') - self.config.add_view_deriver(deriv3, 'deriv3', over='deriv2') - result = self.config._derive_view(view) - self.assertEqual(response.deriv, ['deriv2', 'deriv3', 'deriv1']) - - -class TestDeriverIntegration(unittest.TestCase): - def setUp(self): - self.config = testing.setUp() - - def tearDown(self): - self.config = None - testing.tearDown() - - def _getViewCallable(self, config, ctx_iface=None, request_iface=None, - name=''): - from zope.interface import Interface - from pyramid.interfaces import IRequest - from pyramid.interfaces import IView - from pyramid.interfaces import IViewClassifier - from pyramid.interfaces import IExceptionViewClassifier - classifier = IViewClassifier - if ctx_iface is None: - ctx_iface = Interface - if request_iface is None: - request_iface = IRequest - return config.registry.adapters.lookup( - (classifier, request_iface, ctx_iface), IView, name=name, - default=None) - - def _makeRequest(self, config): - request = DummyRequest() - request.registry = config.registry - return request - - def test_view_options(self): - response = DummyResponse() - view = lambda *arg: response - response.deriv = [] - - def deriv1(view, info): - response.deriv.append(info.options['deriv1']) - return view - deriv1.options = ('deriv1',) - - def deriv2(view, info): - response.deriv.append(info.options['deriv2']) - return view - deriv2.options = ('deriv2',) - - self.config.add_view_deriver(deriv1, 'deriv1') - self.config.add_view_deriver(deriv2, 'deriv2') - self.config.add_view(view, deriv1='test1', deriv2='test2') - - wrapper = self._getViewCallable(self.config) - request = self._makeRequest(self.config) - request.method = 'GET' - self.assertEqual(wrapper(None, request), response) - self.assertEqual(['test1', 'test2'], response.deriv) - - def test_unexpected_view_options(self): - from pyramid.exceptions import ConfigurationError - def deriv1(view, info): pass - self.config.add_view_deriver(deriv1, 'deriv1') - self.assertRaises( - ConfigurationError, - lambda: self.config.add_view(lambda r: {}, deriv1='test1')) - -@implementer(IResponse) -class DummyResponse(object): - content_type = None - default_content_type = None - body = None - -class DummyRequest: - subpath = () - matchdict = None - request_iface = IRequest - - def __init__(self, environ=None): - if environ is None: - environ = {} - self.environ = environ - self.params = {} - self.cookies = {} - self.response = DummyResponse() - -class DummyLogger: - def __init__(self): - self.messages = [] - def info(self, msg): - self.messages.append(msg) - warn = info - debug = info - -class DummySecurityPolicy: - def __init__(self, permitted=True): - self.permitted = permitted - - def effective_principals(self, request): - return [] - - def permits(self, context, principals, permission): - return self.permitted - -def parse_httpdate(s): - import datetime - # cannot use %Z, must use literal GMT; Jython honors timezone - # but CPython does not - return datetime.datetime.strptime(s, "%a, %d %b %Y %H:%M:%S GMT") - -def assert_similar_datetime(one, two): - for attr in ('year', 'month', 'day', 'hour', 'minute'): - one_attr = getattr(one, attr) - two_attr = getattr(two, attr) - if not one_attr == two_attr: # pragma: no cover - raise AssertionError('%r != %r in %s' % (one_attr, two_attr, attr)) diff --git a/pyramid/tests/test_viewderivers.py b/pyramid/tests/test_viewderivers.py new file mode 100644 index 000000000..dfac827dc --- /dev/null +++ b/pyramid/tests/test_viewderivers.py @@ -0,0 +1,1376 @@ +import unittest +from zope.interface import implementer + +from pyramid import testing +from pyramid.exceptions import ConfigurationError +from pyramid.interfaces import ( + IResponse, + IRequest, + ) + +class TestDeriveView(unittest.TestCase): + + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + self.config = None + testing.tearDown() + + def _makeRequest(self): + request = DummyRequest() + request.registry = self.config.registry + return request + + def _registerLogger(self): + from pyramid.interfaces import IDebugLogger + logger = DummyLogger() + self.config.registry.registerUtility(logger, IDebugLogger) + return logger + + def _registerSecurityPolicy(self, permissive): + from pyramid.interfaces import IAuthenticationPolicy + from pyramid.interfaces import IAuthorizationPolicy + policy = DummySecurityPolicy(permissive) + self.config.registry.registerUtility(policy, IAuthenticationPolicy) + self.config.registry.registerUtility(policy, IAuthorizationPolicy) + + def test_function_returns_non_adaptable(self): + def view(request): + return None + result = self.config.derive_view(view) + self.assertFalse(result is view) + try: + result(None, None) + except ValueError as e: + self.assertEqual( + e.args[0], + 'Could not convert return value of the view callable function ' + 'pyramid.tests.test_viewderivers.view into a response ' + 'object. The value returned was None. You may have forgotten ' + 'to return a value from the view callable.' + ) + else: # pragma: no cover + raise AssertionError + + def test_function_returns_non_adaptable_dict(self): + def view(request): + return {'a':1} + result = self.config.derive_view(view) + self.assertFalse(result is view) + try: + result(None, None) + except ValueError as e: + self.assertEqual( + e.args[0], + "Could not convert return value of the view callable function " + "pyramid.tests.test_viewderivers.view into a response " + "object. The value returned was {'a': 1}. You may have " + "forgotten to define a renderer in the view configuration." + ) + else: # pragma: no cover + raise AssertionError + + def test_instance_returns_non_adaptable(self): + class AView(object): + def __call__(self, request): + return None + view = AView() + result = self.config.derive_view(view) + self.assertFalse(result is view) + try: + result(None, None) + except ValueError as e: + msg = e.args[0] + self.assertTrue(msg.startswith( + 'Could not convert return value of the view callable object ' + ' into a response object. The value returned was None. You ' + 'may have forgotten to return a value from the view callable.')) + 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 + result = self.config.derive_view(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() + result = self.config.derive_view(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): + def __init__(self, request): + pass + def __call__(self): + return None + result = self.config.derive_view(AView) + self.assertFalse(result is AView) + try: + result(None, request) + except ValueError as e: + self.assertEqual( + e.args[0], + 'Could not convert return value of the view callable ' + 'method __call__ of ' + 'class pyramid.tests.test_viewderivers.AView into a ' + 'response object. The value returned was None. You may have ' + 'forgotten to return a value from the view callable.' + ) + else: # pragma: no cover + raise AssertionError + + def test_requestonly_nondefault_method_returns_non_adaptable(self): + request = DummyRequest() + class AView(object): + def __init__(self, request): + pass + def theviewmethod(self): + return None + result = self.config.derive_view(AView, attr='theviewmethod') + self.assertFalse(result is AView) + try: + result(None, request) + except ValueError as e: + self.assertEqual( + e.args[0], + 'Could not convert return value of the view callable ' + 'method theviewmethod of ' + 'class pyramid.tests.test_viewderivers.AView into a ' + 'response object. The value returned was None. You may have ' + 'forgotten to return a value from the view callable.' + ) + else: # pragma: no cover + raise AssertionError + + def test_requestonly_function(self): + response = DummyResponse() + def view(request): + return response + result = self.config.derive_view(view) + self.assertFalse(result is view) + self.assertEqual(result(None, None), response) + + def test_requestonly_function_with_renderer(self): + response = DummyResponse() + class moo(object): + def render_view(inself, req, resp, view_inst, ctx): + self.assertEqual(req, request) + self.assertEqual(resp, 'OK') + self.assertEqual(view_inst, view) + self.assertEqual(ctx, context) + return response + def clone(self): + return self + def view(request): + return 'OK' + result = self.config.derive_view(view, renderer=moo()) + self.assertFalse(result.__wraps__ is view) + request = self._makeRequest() + context = testing.DummyResource() + self.assertEqual(result(context, request), response) + + def test_requestonly_function_with_renderer_request_override(self): + def moo(info): + def inner(value, system): + self.assertEqual(value, 'OK') + self.assertEqual(system['request'], request) + self.assertEqual(system['context'], context) + return b'moo' + return inner + def view(request): + return 'OK' + self.config.add_renderer('moo', moo) + result = self.config.derive_view(view, renderer='string') + self.assertFalse(result is view) + request = self._makeRequest() + request.override_renderer = 'moo' + context = testing.DummyResource() + self.assertEqual(result(context, request).body, b'moo') + + def test_requestonly_function_with_renderer_request_has_view(self): + response = DummyResponse() + class moo(object): + def render_view(inself, req, resp, view_inst, ctx): + self.assertEqual(req, request) + self.assertEqual(resp, 'OK') + self.assertEqual(view_inst, 'view') + self.assertEqual(ctx, context) + return response + def clone(self): + return self + def view(request): + return 'OK' + result = self.config.derive_view(view, renderer=moo()) + self.assertFalse(result.__wraps__ is view) + request = self._makeRequest() + request.__view__ = 'view' + context = testing.DummyResource() + r = result(context, request) + self.assertEqual(r, response) + self.assertFalse(hasattr(request, '__view__')) + + def test_class_without_attr(self): + response = DummyResponse() + class View(object): + def __init__(self, request): + pass + def __call__(self): + return response + result = self.config.derive_view(View) + request = self._makeRequest() + self.assertEqual(result(None, request), response) + self.assertEqual(request.__view__.__class__, View) + + def test_class_with_attr(self): + response = DummyResponse() + class View(object): + def __init__(self, request): + pass + def another(self): + return response + result = self.config.derive_view(View, attr='another') + request = self._makeRequest() + self.assertEqual(result(None, request), response) + self.assertEqual(request.__view__.__class__, View) + + def test_as_function_context_and_request(self): + def view(context, request): + return 'OK' + result = self.config.derive_view(view) + self.assertTrue(result.__wraps__ is view) + self.assertFalse(hasattr(result, '__call_permissive__')) + self.assertEqual(view(None, None), 'OK') + + def test_as_function_requestonly(self): + response = DummyResponse() + def view(request): + return response + result = self.config.derive_view(view) + self.assertFalse(result is view) + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertFalse(hasattr(result, '__call_permissive__')) + self.assertEqual(result(None, None), response) + + def test_as_newstyle_class_context_and_request(self): + response = DummyResponse() + class view(object): + def __init__(self, context, request): + pass + def __call__(self): + return response + result = self.config.derive_view(view) + self.assertFalse(result is view) + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertFalse(hasattr(result, '__call_permissive__')) + request = self._makeRequest() + self.assertEqual(result(None, request), response) + self.assertEqual(request.__view__.__class__, view) + + def test_as_newstyle_class_requestonly(self): + response = DummyResponse() + class view(object): + def __init__(self, context, request): + pass + def __call__(self): + return response + result = self.config.derive_view(view) + self.assertFalse(result is view) + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertFalse(hasattr(result, '__call_permissive__')) + request = self._makeRequest() + self.assertEqual(result(None, request), response) + self.assertEqual(request.__view__.__class__, view) + + def test_as_oldstyle_class_context_and_request(self): + response = DummyResponse() + class view: + def __init__(self, context, request): + pass + def __call__(self): + return response + result = self.config.derive_view(view) + self.assertFalse(result is view) + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertFalse(hasattr(result, '__call_permissive__')) + request = self._makeRequest() + self.assertEqual(result(None, request), response) + self.assertEqual(request.__view__.__class__, view) + + def test_as_oldstyle_class_requestonly(self): + response = DummyResponse() + class view: + def __init__(self, context, request): + pass + def __call__(self): + return response + result = self.config.derive_view(view) + self.assertFalse(result is view) + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertFalse(hasattr(result, '__call_permissive__')) + request = self._makeRequest() + self.assertEqual(result(None, request), response) + self.assertEqual(request.__view__.__class__, view) + + def test_as_instance_context_and_request(self): + response = DummyResponse() + class View: + def __call__(self, context, request): + return response + view = View() + result = self.config.derive_view(view) + self.assertTrue(result.__wraps__ is view) + self.assertFalse(hasattr(result, '__call_permissive__')) + self.assertEqual(result(None, None), response) + + def test_as_instance_requestonly(self): + response = DummyResponse() + class View: + def __call__(self, request): + return response + view = View() + result = self.config.derive_view(view) + self.assertFalse(result is view) + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertTrue('test_viewderivers' in result.__name__) + self.assertFalse(hasattr(result, '__call_permissive__')) + self.assertEqual(result(None, None), response) + + def test_with_debug_authorization_no_authpol(self): + response = DummyResponse() + view = lambda *arg: response + self.config.registry.settings = dict( + debug_authorization=True, reload_templates=True) + logger = self._registerLogger() + result = self.config._derive_view(view, permission='view') + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertFalse(hasattr(result, '__call_permissive__')) + request = self._makeRequest() + request.view_name = 'view_name' + request.url = 'url' + self.assertEqual(result(None, request), response) + self.assertEqual(len(logger.messages), 1) + self.assertEqual(logger.messages[0], + "debug_authorization of url url (view name " + "'view_name' against context None): Allowed " + "(no authorization policy in use)") + + def test_with_debug_authorization_authn_policy_no_authz_policy(self): + response = DummyResponse() + view = lambda *arg: response + self.config.registry.settings = dict(debug_authorization=True) + from pyramid.interfaces import IAuthenticationPolicy + policy = DummySecurityPolicy(False) + self.config.registry.registerUtility(policy, IAuthenticationPolicy) + logger = self._registerLogger() + result = self.config._derive_view(view, permission='view') + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertFalse(hasattr(result, '__call_permissive__')) + request = self._makeRequest() + request.view_name = 'view_name' + request.url = 'url' + self.assertEqual(result(None, request), response) + self.assertEqual(len(logger.messages), 1) + self.assertEqual(logger.messages[0], + "debug_authorization of url url (view name " + "'view_name' against context None): Allowed " + "(no authorization policy in use)") + + def test_with_debug_authorization_authz_policy_no_authn_policy(self): + response = DummyResponse() + view = lambda *arg: response + self.config.registry.settings = dict(debug_authorization=True) + from pyramid.interfaces import IAuthorizationPolicy + policy = DummySecurityPolicy(False) + self.config.registry.registerUtility(policy, IAuthorizationPolicy) + logger = self._registerLogger() + result = self.config._derive_view(view, permission='view') + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertFalse(hasattr(result, '__call_permissive__')) + request = self._makeRequest() + request.view_name = 'view_name' + request.url = 'url' + self.assertEqual(result(None, request), response) + self.assertEqual(len(logger.messages), 1) + self.assertEqual(logger.messages[0], + "debug_authorization of url url (view name " + "'view_name' against context None): Allowed " + "(no authorization policy in use)") + + def test_with_debug_authorization_no_permission(self): + response = DummyResponse() + view = lambda *arg: response + self.config.registry.settings = dict( + debug_authorization=True, reload_templates=True) + self._registerSecurityPolicy(True) + logger = self._registerLogger() + result = self.config._derive_view(view) + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertFalse(hasattr(result, '__call_permissive__')) + request = self._makeRequest() + request.view_name = 'view_name' + request.url = 'url' + self.assertEqual(result(None, request), response) + self.assertEqual(len(logger.messages), 1) + self.assertEqual(logger.messages[0], + "debug_authorization of url url (view name " + "'view_name' against context None): Allowed (" + "no permission registered)") + + def test_debug_auth_permission_authpol_permitted(self): + response = DummyResponse() + view = lambda *arg: response + self.config.registry.settings = dict( + debug_authorization=True, reload_templates=True) + logger = self._registerLogger() + self._registerSecurityPolicy(True) + result = self.config._derive_view(view, permission='view') + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertEqual(result.__call_permissive__.__wraps__, view) + request = self._makeRequest() + request.view_name = 'view_name' + request.url = 'url' + self.assertEqual(result(None, request), response) + self.assertEqual(len(logger.messages), 1) + self.assertEqual(logger.messages[0], + "debug_authorization of url url (view name " + "'view_name' against context None): True") + + def test_debug_auth_permission_authpol_permitted_no_request(self): + response = DummyResponse() + view = lambda *arg: response + self.config.registry.settings = dict( + debug_authorization=True, reload_templates=True) + logger = self._registerLogger() + self._registerSecurityPolicy(True) + result = self.config._derive_view(view, permission='view') + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertEqual(result.__call_permissive__.__wraps__, view) + self.assertEqual(result(None, None), response) + self.assertEqual(len(logger.messages), 1) + self.assertEqual(logger.messages[0], + "debug_authorization of url None (view name " + "None against context None): True") + + def test_debug_auth_permission_authpol_denied(self): + from pyramid.httpexceptions import HTTPForbidden + response = DummyResponse() + view = lambda *arg: response + self.config.registry.settings = dict( + debug_authorization=True, reload_templates=True) + logger = self._registerLogger() + self._registerSecurityPolicy(False) + result = self.config._derive_view(view, permission='view') + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertEqual(result.__call_permissive__.__wraps__, view) + request = self._makeRequest() + request.view_name = 'view_name' + request.url = 'url' + self.assertRaises(HTTPForbidden, result, None, request) + self.assertEqual(len(logger.messages), 1) + self.assertEqual(logger.messages[0], + "debug_authorization of url url (view name " + "'view_name' against context None): False") + + def test_debug_auth_permission_authpol_denied2(self): + view = lambda *arg: 'OK' + self.config.registry.settings = dict( + debug_authorization=True, reload_templates=True) + self._registerLogger() + self._registerSecurityPolicy(False) + result = self.config._derive_view(view, permission='view') + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + request = self._makeRequest() + request.view_name = 'view_name' + request.url = 'url' + permitted = result.__permitted__(None, None) + self.assertEqual(permitted, False) + + def test_debug_auth_permission_authpol_overridden(self): + from pyramid.security import NO_PERMISSION_REQUIRED + response = DummyResponse() + view = lambda *arg: response + self.config.registry.settings = dict( + debug_authorization=True, reload_templates=True) + logger = self._registerLogger() + self._registerSecurityPolicy(False) + result = self.config._derive_view(view, permission=NO_PERMISSION_REQUIRED) + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertFalse(hasattr(result, '__call_permissive__')) + request = self._makeRequest() + request.view_name = 'view_name' + request.url = 'url' + self.assertEqual(result(None, request), response) + self.assertEqual(len(logger.messages), 1) + self.assertEqual(logger.messages[0], + "debug_authorization of url url (view name " + "'view_name' against context None): " + "Allowed (NO_PERMISSION_REQUIRED)") + + def test_secured_view_authn_policy_no_authz_policy(self): + response = DummyResponse() + view = lambda *arg: response + self.config.registry.settings = {} + from pyramid.interfaces import IAuthenticationPolicy + policy = DummySecurityPolicy(False) + self.config.registry.registerUtility(policy, IAuthenticationPolicy) + result = self.config._derive_view(view, permission='view') + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertFalse(hasattr(result, '__call_permissive__')) + request = self._makeRequest() + request.view_name = 'view_name' + request.url = 'url' + self.assertEqual(result(None, request), response) + + def test_secured_view_authz_policy_no_authn_policy(self): + response = DummyResponse() + view = lambda *arg: response + self.config.registry.settings = {} + from pyramid.interfaces import IAuthorizationPolicy + policy = DummySecurityPolicy(False) + self.config.registry.registerUtility(policy, IAuthorizationPolicy) + result = self.config._derive_view(view, permission='view') + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertFalse(hasattr(result, '__call_permissive__')) + request = self._makeRequest() + request.view_name = 'view_name' + request.url = 'url' + self.assertEqual(result(None, request), response) + + def test_secured_view_raises_forbidden_no_name(self): + from pyramid.interfaces import IAuthenticationPolicy + from pyramid.interfaces import IAuthorizationPolicy + from pyramid.httpexceptions import HTTPForbidden + response = DummyResponse() + view = lambda *arg: response + self.config.registry.settings = {} + policy = DummySecurityPolicy(False) + self.config.registry.registerUtility(policy, IAuthenticationPolicy) + self.config.registry.registerUtility(policy, IAuthorizationPolicy) + result = self.config._derive_view(view, permission='view') + request = self._makeRequest() + request.view_name = 'view_name' + request.url = 'url' + try: + result(None, request) + except HTTPForbidden as e: + self.assertEqual(e.message, + 'Unauthorized: failed permission check') + else: # pragma: no cover + raise AssertionError + + def test_secured_view_raises_forbidden_with_name(self): + from pyramid.interfaces import IAuthenticationPolicy + from pyramid.interfaces import IAuthorizationPolicy + from pyramid.httpexceptions import HTTPForbidden + def myview(request): pass + self.config.registry.settings = {} + policy = DummySecurityPolicy(False) + self.config.registry.registerUtility(policy, IAuthenticationPolicy) + self.config.registry.registerUtility(policy, IAuthorizationPolicy) + result = self.config._derive_view(myview, permission='view') + request = self._makeRequest() + request.view_name = 'view_name' + request.url = 'url' + try: + result(None, request) + except HTTPForbidden as e: + self.assertEqual(e.message, + 'Unauthorized: myview failed permission check') + else: # pragma: no cover + raise AssertionError + + def test_predicate_mismatch_view_has_no_name(self): + from pyramid.exceptions import PredicateMismatch + response = DummyResponse() + view = lambda *arg: response + def predicate1(context, request): + return False + predicate1.text = lambda *arg: 'text' + result = self.config._derive_view(view, predicates=[predicate1]) + request = self._makeRequest() + request.method = 'POST' + try: + result(None, None) + except PredicateMismatch as e: + self.assertEqual(e.detail, + 'predicate mismatch for view (text)') + else: # pragma: no cover + raise AssertionError + + def test_predicate_mismatch_view_has_name(self): + from pyramid.exceptions import PredicateMismatch + def myview(request): pass + def predicate1(context, request): + return False + predicate1.text = lambda *arg: 'text' + result = self.config._derive_view(myview, predicates=[predicate1]) + request = self._makeRequest() + request.method = 'POST' + try: + result(None, None) + except PredicateMismatch as e: + self.assertEqual(e.detail, + 'predicate mismatch for view myview (text)') + else: # pragma: no cover + raise AssertionError + + def test_predicate_mismatch_exception_has_text_in_detail(self): + from pyramid.exceptions import PredicateMismatch + def myview(request): pass + def predicate1(context, request): + return True + predicate1.text = lambda *arg: 'pred1' + def predicate2(context, request): + return False + predicate2.text = lambda *arg: 'pred2' + result = self.config._derive_view(myview, + predicates=[predicate1, predicate2]) + request = self._makeRequest() + request.method = 'POST' + try: + result(None, None) + except PredicateMismatch as e: + self.assertEqual(e.detail, + 'predicate mismatch for view myview (pred2)') + else: # pragma: no cover + raise AssertionError + + def test_with_predicates_all(self): + response = DummyResponse() + view = lambda *arg: response + predicates = [] + def predicate1(context, request): + predicates.append(True) + return True + def predicate2(context, request): + predicates.append(True) + return True + result = self.config._derive_view(view, + predicates=[predicate1, predicate2]) + request = self._makeRequest() + request.method = 'POST' + next = result(None, None) + self.assertEqual(next, response) + self.assertEqual(predicates, [True, True]) + + def test_with_predicates_checker(self): + view = lambda *arg: 'OK' + predicates = [] + def predicate1(context, request): + predicates.append(True) + return True + def predicate2(context, request): + predicates.append(True) + return True + result = self.config._derive_view(view, + predicates=[predicate1, predicate2]) + request = self._makeRequest() + request.method = 'POST' + next = result.__predicated__(None, None) + self.assertEqual(next, True) + self.assertEqual(predicates, [True, True]) + + def test_with_predicates_notall(self): + from pyramid.httpexceptions import HTTPNotFound + view = lambda *arg: 'OK' + predicates = [] + def predicate1(context, request): + predicates.append(True) + return True + predicate1.text = lambda *arg: 'text' + def predicate2(context, request): + predicates.append(True) + return False + predicate2.text = lambda *arg: 'text' + result = self.config._derive_view(view, + predicates=[predicate1, predicate2]) + request = self._makeRequest() + request.method = 'POST' + self.assertRaises(HTTPNotFound, result, None, None) + self.assertEqual(predicates, [True, True]) + + def test_with_wrapper_viewname(self): + from pyramid.response import Response + from pyramid.interfaces import IView + from pyramid.interfaces import IViewClassifier + inner_response = Response('OK') + def inner_view(context, request): + return inner_response + def outer_view(context, request): + self.assertEqual(request.wrapped_response, inner_response) + self.assertEqual(request.wrapped_body, inner_response.body) + self.assertEqual(request.wrapped_view.__original_view__, + inner_view) + return Response(b'outer ' + request.wrapped_body) + self.config.registry.registerAdapter( + outer_view, (IViewClassifier, None, None), IView, 'owrap') + result = self.config._derive_view(inner_view, viewname='inner', + wrapper_viewname='owrap') + self.assertFalse(result is inner_view) + self.assertEqual(inner_view.__module__, result.__module__) + self.assertEqual(inner_view.__doc__, result.__doc__) + request = self._makeRequest() + response = result(None, request) + self.assertEqual(response.body, b'outer OK') + + def test_with_wrapper_viewname_notfound(self): + from pyramid.response import Response + inner_response = Response('OK') + def inner_view(context, request): + return inner_response + wrapped = self.config._derive_view(inner_view, viewname='inner', + wrapper_viewname='owrap') + request = self._makeRequest() + self.assertRaises(ValueError, wrapped, None, request) + + def test_as_newstyle_class_context_and_request_attr_and_renderer(self): + response = DummyResponse() + class renderer(object): + def render_view(inself, req, resp, view_inst, ctx): + self.assertEqual(req, request) + self.assertEqual(resp, {'a':'1'}) + self.assertEqual(view_inst.__class__, View) + self.assertEqual(ctx, context) + return response + def clone(self): + return self + class View(object): + def __init__(self, context, request): + pass + def index(self): + return {'a':'1'} + result = self.config._derive_view(View, + renderer=renderer(), attr='index') + self.assertFalse(result is View) + self.assertEqual(result.__module__, View.__module__) + self.assertEqual(result.__doc__, View.__doc__) + self.assertEqual(result.__name__, View.__name__) + request = self._makeRequest() + context = testing.DummyResource() + self.assertEqual(result(context, request), response) + + def test_as_newstyle_class_requestonly_attr_and_renderer(self): + response = DummyResponse() + class renderer(object): + def render_view(inself, req, resp, view_inst, ctx): + self.assertEqual(req, request) + self.assertEqual(resp, {'a':'1'}) + self.assertEqual(view_inst.__class__, View) + self.assertEqual(ctx, context) + return response + def clone(self): + return self + class View(object): + def __init__(self, request): + pass + def index(self): + return {'a':'1'} + result = self.config.derive_view(View, + renderer=renderer(), attr='index') + self.assertFalse(result is View) + self.assertEqual(result.__module__, View.__module__) + self.assertEqual(result.__doc__, View.__doc__) + self.assertEqual(result.__name__, View.__name__) + request = self._makeRequest() + context = testing.DummyResource() + self.assertEqual(result(context, request), response) + + def test_as_oldstyle_cls_context_request_attr_and_renderer(self): + response = DummyResponse() + class renderer(object): + def render_view(inself, req, resp, view_inst, ctx): + self.assertEqual(req, request) + self.assertEqual(resp, {'a':'1'}) + self.assertEqual(view_inst.__class__, View) + self.assertEqual(ctx, context) + return response + def clone(self): + return self + class View: + def __init__(self, context, request): + pass + def index(self): + return {'a':'1'} + result = self.config.derive_view(View, + renderer=renderer(), attr='index') + self.assertFalse(result is View) + self.assertEqual(result.__module__, View.__module__) + self.assertEqual(result.__doc__, View.__doc__) + self.assertEqual(result.__name__, View.__name__) + request = self._makeRequest() + context = testing.DummyResource() + self.assertEqual(result(context, request), response) + + def test_as_oldstyle_cls_requestonly_attr_and_renderer(self): + response = DummyResponse() + class renderer(object): + def render_view(inself, req, resp, view_inst, ctx): + self.assertEqual(req, request) + self.assertEqual(resp, {'a':'1'}) + self.assertEqual(view_inst.__class__, View) + self.assertEqual(ctx, context) + return response + def clone(self): + return self + class View: + def __init__(self, request): + pass + def index(self): + return {'a':'1'} + result = self.config.derive_view(View, + renderer=renderer(), attr='index') + self.assertFalse(result is View) + self.assertEqual(result.__module__, View.__module__) + self.assertEqual(result.__doc__, View.__doc__) + self.assertEqual(result.__name__, View.__name__) + request = self._makeRequest() + context = testing.DummyResource() + self.assertEqual(result(context, request), response) + + def test_as_instance_context_and_request_attr_and_renderer(self): + response = DummyResponse() + class renderer(object): + def render_view(inself, req, resp, view_inst, ctx): + self.assertEqual(req, request) + self.assertEqual(resp, {'a':'1'}) + self.assertEqual(view_inst, view) + self.assertEqual(ctx, context) + return response + def clone(self): + return self + class View: + def index(self, context, request): + return {'a':'1'} + view = View() + result = self.config.derive_view(view, + renderer=renderer(), attr='index') + self.assertFalse(result is view) + self.assertEqual(result.__module__, view.__module__) + self.assertEqual(result.__doc__, view.__doc__) + request = self._makeRequest() + context = testing.DummyResource() + self.assertEqual(result(context, request), response) + + def test_as_instance_requestonly_attr_and_renderer(self): + response = DummyResponse() + class renderer(object): + def render_view(inself, req, resp, view_inst, ctx): + self.assertEqual(req, request) + self.assertEqual(resp, {'a':'1'}) + self.assertEqual(view_inst, view) + self.assertEqual(ctx, context) + return response + def clone(self): + return self + class View: + def index(self, request): + return {'a':'1'} + view = View() + result = self.config.derive_view(view, + renderer=renderer(), attr='index') + self.assertFalse(result is view) + self.assertEqual(result.__module__, view.__module__) + self.assertEqual(result.__doc__, view.__doc__) + request = self._makeRequest() + context = testing.DummyResource() + self.assertEqual(result(context, request), response) + + def test_with_view_mapper_config_specified(self): + response = DummyResponse() + class mapper(object): + def __init__(self, **kw): + self.kw = kw + def __call__(self, view): + def wrapped(context, request): + return response + return wrapped + def view(context, request): return 'NOTOK' + result = self.config._derive_view(view, mapper=mapper) + self.assertFalse(result.__wraps__ is view) + self.assertEqual(result(None, None), response) + + def test_with_view_mapper_view_specified(self): + from pyramid.response import Response + response = Response() + def mapper(**kw): + def inner(view): + def superinner(context, request): + self.assertEqual(request, None) + return response + return superinner + return inner + def view(context, request): return 'NOTOK' + view.__view_mapper__ = mapper + result = self.config.derive_view(view) + self.assertFalse(result.__wraps__ is view) + self.assertEqual(result(None, None), response) + + def test_with_view_mapper_default_mapper_specified(self): + from pyramid.response import Response + response = Response() + def mapper(**kw): + def inner(view): + def superinner(context, request): + self.assertEqual(request, None) + return response + return superinner + return inner + self.config.set_view_mapper(mapper) + def view(context, request): return 'NOTOK' + result = self.config.derive_view(view) + self.assertFalse(result.__wraps__ is view) + self.assertEqual(result(None, None), response) + + def test_attr_wrapped_view_branching_default_phash(self): + from pyramid.config.util import DEFAULT_PHASH + def view(context, request): pass + result = self.config._derive_view(view, phash=DEFAULT_PHASH) + self.assertEqual(result.__wraps__, view) + + def test_attr_wrapped_view_branching_nondefault_phash(self): + def view(context, request): pass + result = self.config._derive_view(view, phash='nondefault') + self.assertNotEqual(result, view) + + def test_http_cached_view_integer(self): + import datetime + from pyramid.response import Response + response = Response('OK') + def inner_view(context, request): + return response + result = self.config._derive_view(inner_view, http_cache=3600) + self.assertFalse(result is inner_view) + self.assertEqual(inner_view.__module__, result.__module__) + self.assertEqual(inner_view.__doc__, result.__doc__) + request = self._makeRequest() + when = datetime.datetime.utcnow() + datetime.timedelta(hours=1) + result = result(None, request) + self.assertEqual(result, response) + headers = dict(result.headerlist) + expires = parse_httpdate(headers['Expires']) + assert_similar_datetime(expires, when) + self.assertEqual(headers['Cache-Control'], 'max-age=3600') + + def test_http_cached_view_timedelta(self): + import datetime + from pyramid.response import Response + response = Response('OK') + def inner_view(context, request): + return response + result = self.config._derive_view(inner_view, + http_cache=datetime.timedelta(hours=1)) + self.assertFalse(result is inner_view) + self.assertEqual(inner_view.__module__, result.__module__) + self.assertEqual(inner_view.__doc__, result.__doc__) + request = self._makeRequest() + when = datetime.datetime.utcnow() + datetime.timedelta(hours=1) + result = result(None, request) + self.assertEqual(result, response) + headers = dict(result.headerlist) + expires = parse_httpdate(headers['Expires']) + assert_similar_datetime(expires, when) + self.assertEqual(headers['Cache-Control'], 'max-age=3600') + + def test_http_cached_view_tuple(self): + import datetime + from pyramid.response import Response + response = Response('OK') + def inner_view(context, request): + return response + result = self.config._derive_view(inner_view, + http_cache=(3600, {'public':True})) + self.assertFalse(result is inner_view) + self.assertEqual(inner_view.__module__, result.__module__) + self.assertEqual(inner_view.__doc__, result.__doc__) + request = self._makeRequest() + when = datetime.datetime.utcnow() + datetime.timedelta(hours=1) + result = result(None, request) + self.assertEqual(result, response) + headers = dict(result.headerlist) + expires = parse_httpdate(headers['Expires']) + assert_similar_datetime(expires, when) + self.assertEqual(headers['Cache-Control'], 'max-age=3600, public') + + def test_http_cached_view_tuple_seconds_None(self): + from pyramid.response import Response + response = Response('OK') + def inner_view(context, request): + return response + result = self.config._derive_view(inner_view, + http_cache=(None, {'public':True})) + self.assertFalse(result is inner_view) + self.assertEqual(inner_view.__module__, result.__module__) + self.assertEqual(inner_view.__doc__, result.__doc__) + request = self._makeRequest() + result = result(None, request) + self.assertEqual(result, response) + headers = dict(result.headerlist) + self.assertFalse('Expires' in headers) + self.assertEqual(headers['Cache-Control'], 'public') + + def test_http_cached_view_prevent_auto_set(self): + from pyramid.response import Response + response = Response() + response.cache_control.prevent_auto = True + def inner_view(context, request): + return response + result = self.config._derive_view(inner_view, http_cache=3600) + request = self._makeRequest() + result = result(None, request) + self.assertEqual(result, response) # doesn't blow up + headers = dict(result.headerlist) + self.assertFalse('Expires' in headers) + self.assertFalse('Cache-Control' in headers) + + def test_http_cached_prevent_http_cache_in_settings(self): + self.config.registry.settings['prevent_http_cache'] = True + from pyramid.response import Response + response = Response() + def inner_view(context, request): + return response + result = self.config._derive_view(inner_view, http_cache=3600) + request = self._makeRequest() + result = result(None, request) + self.assertEqual(result, response) + headers = dict(result.headerlist) + self.assertFalse('Expires' in headers) + self.assertFalse('Cache-Control' in headers) + + def test_http_cached_view_bad_tuple(self): + def view(request): pass + self.assertRaises(ConfigurationError, self.config._derive_view, + view, http_cache=(None,)) + + +class TestDerivationOrder(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + self.config = None + testing.tearDown() + + def test_right_order_user_sorted(self): + from pyramid.interfaces import IViewDerivers + + self.config.add_view_deriver(None, 'deriv1') + self.config.add_view_deriver(None, 'deriv2', over='deriv1') + self.config.add_view_deriver(None, 'deriv3', under='deriv2') + + derivers = self.config.registry.getUtility(IViewDerivers) + derivers_sorted = derivers.sorted() + dlist = [d for (d, _) in derivers_sorted] + self.assertEqual([ + 'authdebug_view', + 'secured_view', + 'owrapped_view', + 'http_cached_view', + 'decorated_view', + 'deriv2', + 'deriv3', + 'deriv1', + 'rendered_view', + ], dlist) + + def test_right_order_implicit(self): + from pyramid.interfaces import IViewDerivers + + self.config.add_view_deriver(None, 'deriv1') + self.config.add_view_deriver(None, 'deriv2') + self.config.add_view_deriver(None, 'deriv3') + + derivers = self.config.registry.getUtility(IViewDerivers) + derivers_sorted = derivers.sorted() + dlist = [d for (d, _) in derivers_sorted] + self.assertEqual([ + 'authdebug_view', + 'secured_view', + 'owrapped_view', + 'http_cached_view', + 'decorated_view', + 'deriv3', + 'deriv2', + 'deriv1', + 'rendered_view', + ], dlist) + + def test_right_order_under_rendered_view(self): + from pyramid.interfaces import IViewDerivers + + self.config.add_view_deriver(None, 'deriv1', under='rendered_view') + + derivers = self.config.registry.getUtility(IViewDerivers) + derivers_sorted = derivers.sorted() + dlist = [d for (d, _) in derivers_sorted] + self.assertEqual([ + 'authdebug_view', + 'secured_view', + 'owrapped_view', + 'http_cached_view', + 'decorated_view', + 'rendered_view', + 'deriv1', + ], dlist) + + + def test_right_order_under_rendered_view_others(self): + from pyramid.interfaces import IViewDerivers + + self.config.add_view_deriver(None, 'deriv1', under='rendered_view') + self.config.add_view_deriver(None, 'deriv2') + self.config.add_view_deriver(None, 'deriv3') + + derivers = self.config.registry.getUtility(IViewDerivers) + derivers_sorted = derivers.sorted() + dlist = [d for (d, _) in derivers_sorted] + self.assertEqual([ + 'authdebug_view', + 'secured_view', + 'owrapped_view', + 'http_cached_view', + 'decorated_view', + 'deriv3', + 'deriv2', + 'rendered_view', + 'deriv1', + ], dlist) + + +class TestAddDeriver(unittest.TestCase): + + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + self.config = None + testing.tearDown() + + def test_add_single_deriver(self): + response = DummyResponse() + response.deriv = False + view = lambda *arg: response + + def deriv(view, info): + self.assertFalse(response.deriv) + response.deriv = True + return view + + result = self.config._derive_view(view) + self.assertFalse(response.deriv) + self.config.add_view_deriver(deriv, 'test_deriv') + + result = self.config._derive_view(view) + self.assertTrue(response.deriv) + + def test_override_deriver(self): + flags = {} + + class AView: + def __init__(self): + self.response = DummyResponse() + + def deriv1(view, value, **kw): + flags['deriv1'] = True + return view + + def deriv2(view, value, **kw): + flags['deriv2'] = True + return view + + view1 = AView() + self.config.add_view_deriver(deriv1, 'test_deriv') + result = self.config._derive_view(view1) + self.assertTrue(flags.get('deriv1')) + self.assertFalse(flags.get('deriv2')) + + flags.clear() + view2 = AView() + self.config.add_view_deriver(deriv2, 'test_deriv') + result = self.config._derive_view(view2) + self.assertFalse(flags.get('deriv1')) + self.assertTrue(flags.get('deriv2')) + + def test_add_multi_derivers_ordered(self): + response = DummyResponse() + view = lambda *arg: response + response.deriv = [] + + def deriv1(view, value, **kw): + response.deriv.append('deriv1') + return view + + def deriv2(view, value, **kw): + response.deriv.append('deriv2') + return view + + def deriv3(view, value, **kw): + response.deriv.append('deriv3') + return view + + self.config.add_view_deriver(deriv1, 'deriv1') + self.config.add_view_deriver(deriv2, 'deriv2', under='deriv1') + self.config.add_view_deriver(deriv3, 'deriv3', over='deriv2') + result = self.config._derive_view(view) + self.assertEqual(response.deriv, ['deriv2', 'deriv3', 'deriv1']) + + +class TestDeriverIntegration(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + self.config = None + testing.tearDown() + + def _getViewCallable(self, config, ctx_iface=None, request_iface=None, + name=''): + from zope.interface import Interface + from pyramid.interfaces import IRequest + from pyramid.interfaces import IView + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IExceptionViewClassifier + classifier = IViewClassifier + if ctx_iface is None: + ctx_iface = Interface + if request_iface is None: + request_iface = IRequest + return config.registry.adapters.lookup( + (classifier, request_iface, ctx_iface), IView, name=name, + default=None) + + def _makeRequest(self, config): + request = DummyRequest() + request.registry = config.registry + return request + + def test_view_options(self): + response = DummyResponse() + view = lambda *arg: response + response.deriv = [] + + def deriv1(view, info): + response.deriv.append(info.options['deriv1']) + return view + deriv1.options = ('deriv1',) + + def deriv2(view, info): + response.deriv.append(info.options['deriv2']) + return view + deriv2.options = ('deriv2',) + + self.config.add_view_deriver(deriv1, 'deriv1') + self.config.add_view_deriver(deriv2, 'deriv2') + self.config.add_view(view, deriv1='test1', deriv2='test2') + + wrapper = self._getViewCallable(self.config) + request = self._makeRequest(self.config) + request.method = 'GET' + self.assertEqual(wrapper(None, request), response) + self.assertEqual(['test1', 'test2'], response.deriv) + + def test_unexpected_view_options(self): + from pyramid.exceptions import ConfigurationError + def deriv1(view, info): pass + self.config.add_view_deriver(deriv1, 'deriv1') + self.assertRaises( + ConfigurationError, + lambda: self.config.add_view(lambda r: {}, deriv1='test1')) + +@implementer(IResponse) +class DummyResponse(object): + content_type = None + default_content_type = None + body = None + +class DummyRequest: + subpath = () + matchdict = None + request_iface = IRequest + + def __init__(self, environ=None): + if environ is None: + environ = {} + self.environ = environ + self.params = {} + self.cookies = {} + self.response = DummyResponse() + +class DummyLogger: + def __init__(self): + self.messages = [] + def info(self, msg): + self.messages.append(msg) + warn = info + debug = info + +class DummySecurityPolicy: + def __init__(self, permitted=True): + self.permitted = permitted + + def effective_principals(self, request): + return [] + + def permits(self, context, principals, permission): + return self.permitted + +def parse_httpdate(s): + import datetime + # cannot use %Z, must use literal GMT; Jython honors timezone + # but CPython does not + return datetime.datetime.strptime(s, "%a, %d %b %Y %H:%M:%S GMT") + +def assert_similar_datetime(one, two): + for attr in ('year', 'month', 'day', 'hour', 'minute'): + one_attr = getattr(one, attr) + two_attr = getattr(two, attr) + if not one_attr == two_attr: # pragma: no cover + raise AssertionError('%r != %r in %s' % (one_attr, two_attr, attr)) diff --git a/pyramid/viewderivers.py b/pyramid/viewderivers.py new file mode 100644 index 000000000..99baf46f9 --- /dev/null +++ b/pyramid/viewderivers.py @@ -0,0 +1,453 @@ +import inspect + +from zope.interface import ( + implementer, + provider, + ) + +from pyramid.security import NO_PERMISSION_REQUIRED +from pyramid.response import Response + +from pyramid.interfaces import ( + IAuthenticationPolicy, + IAuthorizationPolicy, + IDebugLogger, + IResponse, + IViewMapper, + IViewMapperFactory, + ) + +from pyramid.compat import ( + is_bound_method, + is_unbound_method, + ) + +from pyramid.config.util import ( + DEFAULT_PHASH, + MAX_ORDER, + takes_one_arg, + ) + +from pyramid.exceptions import ( + ConfigurationError, + PredicateMismatch, + ) +from pyramid.httpexceptions import HTTPForbidden +from pyramid.util import object_description +from pyramid.view import render_view_to_response +from pyramid import renderers + + +def view_description(view): + try: + return view.__text__ + except AttributeError: + # custom view mappers might not add __text__ + return object_description(view) + +def requestonly(view, attr=None): + return takes_one_arg(view, attr=attr, argname='request') + +@implementer(IViewMapper) +@provider(IViewMapperFactory) +class DefaultViewMapper(object): + def __init__(self, **kw): + self.attr = kw.get('attr') + + def __call__(self, view): + if is_unbound_method(view) and self.attr is None: + raise ConfigurationError(( + 'Unbound method calls are not supported, please set the class ' + 'as your `view` and the method as your `attr`' + )) + + if inspect.isclass(view): + view = self.map_class(view) + else: + view = self.map_nonclass(view) + return view + + def map_class(self, view): + ronly = requestonly(view, self.attr) + if ronly: + mapped_view = self.map_class_requestonly(view) + else: + mapped_view = self.map_class_native(view) + mapped_view.__text__ = 'method %s of %s' % ( + self.attr or '__call__', object_description(view)) + return mapped_view + + def map_nonclass(self, view): + # We do more work here than appears necessary to avoid wrapping the + # view unless it actually requires wrapping (to avoid function call + # overhead). + mapped_view = view + ronly = requestonly(view, self.attr) + if ronly: + mapped_view = self.map_nonclass_requestonly(view) + elif self.attr: + mapped_view = self.map_nonclass_attr(view) + if inspect.isroutine(mapped_view): + # This branch will be true if the view is a function or a method. + # We potentially mutate an unwrapped object here if it's a + # function. We do this to avoid function call overhead of + # injecting another wrapper. However, we must wrap if the + # function is a bound method because we can't set attributes on a + # bound method. + if is_bound_method(view): + _mapped_view = mapped_view + def mapped_view(context, request): + return _mapped_view(context, request) + if self.attr is not None: + mapped_view.__text__ = 'attr %s of %s' % ( + self.attr, object_description(view)) + else: + mapped_view.__text__ = object_description(view) + return mapped_view + + def map_class_requestonly(self, view): + # its a class that has an __init__ which only accepts request + attr = self.attr + def _class_requestonly_view(context, request): + inst = view(request) + request.__view__ = inst + if attr is None: + response = inst() + else: + response = getattr(inst, attr)() + return response + return _class_requestonly_view + + def map_class_native(self, view): + # its a class that has an __init__ which accepts both context and + # request + attr = self.attr + def _class_view(context, request): + inst = view(context, request) + request.__view__ = inst + if attr is None: + response = inst() + else: + response = getattr(inst, attr)() + return response + return _class_view + + def map_nonclass_requestonly(self, view): + # its a function that has a __call__ which accepts only a single + # request argument + attr = self.attr + def _requestonly_view(context, request): + if attr is None: + response = view(request) + else: + response = getattr(view, attr)(request) + return response + return _requestonly_view + + def map_nonclass_attr(self, view): + # its a function that has a __call__ which accepts both context and + # request, but still has an attr + def _attr_view(context, request): + response = getattr(view, self.attr)(context, request) + return response + return _attr_view + + +def wraps_view(wrapper): + def inner(view, info): + wrapper_view = wrapper(view, info) + return preserve_view_attrs(view, wrapper_view) + return inner + +def preserve_view_attrs(view, wrapper): + if view is None: + return wrapper + + if wrapper is view: + return view + + original_view = getattr(view, '__original_view__', None) + + if original_view is None: + original_view = view + + wrapper.__wraps__ = view + wrapper.__original_view__ = original_view + wrapper.__module__ = view.__module__ + wrapper.__doc__ = view.__doc__ + + try: + wrapper.__name__ = view.__name__ + except AttributeError: + wrapper.__name__ = repr(view) + + # attrs that may not exist on "view", but, if so, must be attached to + # "wrapped view" + for attr in ('__permitted__', '__call_permissive__', '__permission__', + '__predicated__', '__predicates__', '__accept__', + '__order__', '__text__'): + try: + setattr(wrapper, attr, getattr(view, attr)) + except AttributeError: + pass + + return wrapper + +def mapped_view(view, info): + mapper = info.options.get('mapper') + if mapper is None: + mapper = getattr(view, '__view_mapper__', None) + if mapper is None: + mapper = info.registry.queryUtility(IViewMapperFactory) + if mapper is None: + mapper = DefaultViewMapper + + mapped_view = mapper(**info.options)(view) + return mapped_view + +mapped_view.options = ('mapper', 'attr') + +def owrapped_view(view, info): + wrapper_viewname = info.options.get('wrapper') + viewname = info.options.get('name') + if not wrapper_viewname: + return view + def _owrapped_view(context, request): + response = view(context, request) + request.wrapped_response = response + request.wrapped_body = response.body + request.wrapped_view = view + wrapped_response = render_view_to_response(context, request, + wrapper_viewname) + if wrapped_response is None: + raise ValueError( + 'No wrapper view named %r found when executing view ' + 'named %r' % (wrapper_viewname, viewname)) + return wrapped_response + return _owrapped_view + +owrapped_view.options = ('name', 'wrapper') + +def http_cached_view(view, info): + if info.settings.get('prevent_http_cache', False): + return view + + seconds = info.options.get('http_cache') + + if seconds is None: + return view + + options = {} + + if isinstance(seconds, (tuple, list)): + try: + seconds, options = seconds + except ValueError: + raise ConfigurationError( + 'If http_cache parameter is a tuple or list, it must be ' + 'in the form (seconds, options); not %s' % (seconds,)) + + def wrapper(context, request): + response = view(context, request) + prevent_caching = getattr(response.cache_control, 'prevent_auto', + False) + if not prevent_caching: + response.cache_expires(seconds, **options) + return response + + return wrapper + +http_cached_view.options = ('http_cache',) + +def secured_view(view, info): + permission = info.options.get('permission') + if permission == NO_PERMISSION_REQUIRED: + # allow views registered within configurations that have a + # default permission to explicitly override the default + # permission, replacing it with no permission at all + permission = None + + wrapped_view = view + authn_policy = info.registry.queryUtility(IAuthenticationPolicy) + authz_policy = info.registry.queryUtility(IAuthorizationPolicy) + + if authn_policy and authz_policy and (permission is not None): + def _permitted(context, request): + principals = authn_policy.effective_principals(request) + return authz_policy.permits(context, principals, permission) + def _secured_view(context, request): + result = _permitted(context, request) + if result: + return view(context, request) + view_name = getattr(view, '__name__', view) + msg = getattr( + request, 'authdebug_message', + 'Unauthorized: %s failed permission check' % view_name) + raise HTTPForbidden(msg, result=result) + _secured_view.__call_permissive__ = view + _secured_view.__permitted__ = _permitted + _secured_view.__permission__ = permission + wrapped_view = _secured_view + + return wrapped_view + +secured_view.options = ('permission',) + +def authdebug_view(view, info): + wrapped_view = view + settings = info.settings + permission = info.options.get('permission') + authn_policy = info.registry.queryUtility(IAuthenticationPolicy) + authz_policy = info.registry.queryUtility(IAuthorizationPolicy) + logger = info.registry.queryUtility(IDebugLogger) + if settings and settings.get('debug_authorization', False): + def _authdebug_view(context, request): + view_name = getattr(request, 'view_name', None) + + if authn_policy and authz_policy: + if permission is NO_PERMISSION_REQUIRED: + msg = 'Allowed (NO_PERMISSION_REQUIRED)' + elif permission is None: + msg = 'Allowed (no permission registered)' + else: + principals = authn_policy.effective_principals(request) + msg = str(authz_policy.permits( + context, principals, permission)) + else: + msg = 'Allowed (no authorization policy in use)' + + view_name = getattr(request, 'view_name', None) + url = getattr(request, 'url', None) + msg = ('debug_authorization of url %s (view name %r against ' + 'context %r): %s' % (url, view_name, context, msg)) + if logger: + logger.debug(msg) + if request is not None: + request.authdebug_message = msg + return view(context, request) + + wrapped_view = _authdebug_view + + return wrapped_view + +authdebug_view.options = ('permission',) + +def predicated_view(view, info): + preds = info.predicates + if not preds: + return view + def predicate_wrapper(context, request): + for predicate in preds: + if not predicate(context, request): + view_name = getattr(view, '__name__', view) + raise PredicateMismatch( + 'predicate mismatch for view %s (%s)' % ( + view_name, predicate.text())) + return view(context, request) + def checker(context, request): + return all((predicate(context, request) for predicate in + preds)) + predicate_wrapper.__predicated__ = checker + predicate_wrapper.__predicates__ = preds + return predicate_wrapper + +def attr_wrapped_view(view, info): + accept, order, phash = (info.options.get('accept', None), + getattr(info, 'order', MAX_ORDER), + getattr(info, 'phash', DEFAULT_PHASH)) + # this is a little silly but we don't want to decorate the original + # function with attributes that indicate accept, order, and phash, + # so we use a wrapper + if ( + (accept is None) and + (order == MAX_ORDER) and + (phash == DEFAULT_PHASH) + ): + return view # defaults + def attr_view(context, request): + return view(context, request) + attr_view.__accept__ = accept + attr_view.__order__ = order + attr_view.__phash__ = phash + attr_view.__view_attr__ = info.options.get('attr') + attr_view.__permission__ = info.options.get('permission') + return attr_view + +attr_wrapped_view.options = ('accept', 'attr', 'permission') + +def rendered_view(view, info): + # one way or another this wrapper must produce a Response (unless + # the renderer is a NullRendererHelper) + renderer = info.options.get('renderer') + if renderer is None: + # register a default renderer if you want super-dynamic + # rendering. registering a default renderer will also allow + # override_renderer to work if a renderer is left unspecified for + # a view registration. + def viewresult_to_response(context, request): + result = view(context, request) + if result.__class__ is Response: # common case + response = result + else: + response = info.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 + + if renderer is renderers.null_renderer: + return view + + def rendered_view(context, request): + result = view(context, request) + if result.__class__ is Response: # potential common case + response = result + else: + # this must adapt, it can't do a simple interface check + # (avoid trying to render webob responses) + response = info.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') + view_renderer = renderers.RendererHelper( + name=renderer_name, + package=info.package, + registry=info.registry) + else: + view_renderer = renderer.clone() + if '__view__' in attrs: + view_inst = attrs.pop('__view__') + else: + view_inst = getattr(view, '__original_view__', view) + response = view_renderer.render_view( + request, result, view_inst, context) + return response + + return rendered_view + +rendered_view.options = ('renderer',) + +def decorated_view(view, info): + decorator = info.options.get('decorator') + if decorator is None: + return view + return decorator(view) + +decorated_view.options = ('decorator',) -- cgit v1.2.3 From a3db3cab713fecc0c83c742cfe3f0736b1d94a92 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 7 Apr 2016 01:16:45 -0500 Subject: separate the viewderiver module and allow overriding the mapper --- docs/api/viewderivers.rst | 16 +++++++ docs/narr/hooks.rst | 9 ++-- pyramid/config/views.py | 72 ++++++++++++++++++++++-------- pyramid/tests/test_viewderivers.py | 89 +++++++++++++++++++++++++++++++------- pyramid/viewderivers.py | 16 ++++--- 5 files changed, 156 insertions(+), 46 deletions(-) create mode 100644 docs/api/viewderivers.rst diff --git a/docs/api/viewderivers.rst b/docs/api/viewderivers.rst new file mode 100644 index 000000000..a4ec107b6 --- /dev/null +++ b/docs/api/viewderivers.rst @@ -0,0 +1,16 @@ +.. _viewderivers_module: + +:mod:`pyramid.viewderivers` +--------------------------- + +.. automodule:: pyramid.viewderivers + + .. attribute:: INGRESS + + Constant representing the request ingress, for use in ``under`` + arguments to :meth:`pyramid.config.Configurator.add_view_deriver`. + + .. attribute:: MAPPED_VIEW + + Constant representing the closest view deriver, for use in ``over`` + arguments to :meth:`pyramid.config.Configurator.add_view_deriver`. diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index a32e94d1a..3a1ad8363 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -1580,12 +1580,6 @@ There are several built-in view derivers that :app:`Pyramid` will automatically apply to any view. Below they are defined in order from furthest to closest to the user-defined :term:`view callable`: -``authdebug_view`` - - Used to output useful debugging information when - ``pyramid.debug_authorization`` is enabled. This element is a no-op - otherwise. - ``secured_view`` Enforce the ``permission`` defined on the view. This element is a no-op if no @@ -1593,6 +1587,9 @@ the user-defined :term:`view callable`: default permission was assigned via :meth:`pyramid.config.Configurator.set_default_permission`. + This element will also output useful debugging information when + ``pyramid.debug_authorization`` is enabled. + ``owrapped_view`` Invokes the wrapped view defined by the ``wrapper`` option. diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 2a019726f..1e161177b 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -79,6 +79,8 @@ import pyramid.config.predicates import pyramid.viewderivers from pyramid.viewderivers import ( + INGRESS, + MAPPED_VIEW, preserve_view_attrs, view_description, requestonly, @@ -89,6 +91,7 @@ from pyramid.viewderivers import ( from pyramid.config.util import ( DEFAULT_PHASH, MAX_ORDER, + as_sorted_tuple, ) urljoin = urlparse.urljoin @@ -1029,16 +1032,14 @@ class ViewsConfiguratorMixin(object): def _apply_view_derivers(self, info): d = pyramid.viewderivers - # These derivations have fixed order + + # These derivers are not really derivers and so have fixed order outer_derivers = [('attr_wrapped_view', d.attr_wrapped_view), ('predicated_view', d.predicated_view)] - inner_derivers = [('mapped_view', d.mapped_view)] view = info.original_view derivers = self.registry.getUtility(IViewDerivers) - for name, deriver in reversed( - outer_derivers + derivers.sorted() + inner_derivers - ): + for name, deriver in reversed(outer_derivers + derivers.sorted()): view = wraps_view(deriver)(view, info) return view @@ -1090,7 +1091,7 @@ class ViewsConfiguratorMixin(object): self.add_view_predicate(name, factory) @action_method - def add_view_deriver(self, deriver, name, under=None, over=None): + def add_view_deriver(self, deriver, name=None, under=None, over=None): """ .. versionadded:: 1.7 @@ -1105,9 +1106,11 @@ class ViewsConfiguratorMixin(object): restrictions on the name of a view deriver. If left unspecified, the name will be constructed from the name of the ``deriver``. - The ``under`` and ``over`` options may be used to control the ordering + The ``under`` and ``over`` options can be used to control the ordering of view derivers by providing hints about where in the view pipeline - the deriver is used. + the deriver is used. Each option may be a string or a list of strings. + At least one view deriver in each, the over and under directions, must + exist to fully satisfy the constraints. ``under`` means closer to the user-defined :term:`view callable`, and ``over`` means closer to view pipeline ingress. @@ -1122,10 +1125,37 @@ class ViewsConfiguratorMixin(object): """ deriver = self.maybe_dotted(deriver) + if name is None: + name = deriver.__name__ + + if name in (INGRESS,): + raise ConfigurationError('%s is a reserved view deriver name' + % name) + if under is None and over is None: under = 'decorated_view' over = 'rendered_view' + if over is None and name != MAPPED_VIEW: + raise ConfigurationError('must specify an "over" constraint for ' + 'the %s view deriver' % name) + elif over is not None: + over = as_sorted_tuple(over) + + if under is None: + raise ConfigurationError('must specify an "under" constraint for ' + 'the %s view deriver' % name) + else: + under = as_sorted_tuple(under) + + if over is not None and INGRESS in over: + raise ConfigurationError('%s cannot be over view deriver INGRESS' + % name) + + if MAPPED_VIEW in under: + raise ConfigurationError('%s cannot be under view deriver ' + 'MAPPED_VIEW' % name) + discriminator = ('view deriver', name) intr = self.introspectable( 'view derivers', @@ -1139,7 +1169,12 @@ class ViewsConfiguratorMixin(object): def register(): derivers = self.registry.queryUtility(IViewDerivers) if derivers is None: - derivers = TopologicalSorter() + derivers = TopologicalSorter( + default_before=None, + default_after=INGRESS, + first=INGRESS, + last=MAPPED_VIEW, + ) self.registry.registerUtility(derivers, IViewDerivers) derivers.add(name, deriver, before=over, after=under) self.action(discriminator, register, introspectables=(intr,), @@ -1148,24 +1183,23 @@ class ViewsConfiguratorMixin(object): def add_default_view_derivers(self): d = pyramid.viewderivers derivers = [ - ('authdebug_view', d.authdebug_view), ('secured_view', d.secured_view), ('owrapped_view', d.owrapped_view), ('http_cached_view', d.http_cached_view), ('decorated_view', d.decorated_view), + ('rendered_view', d.rendered_view), ] - last = pyramid.util.FIRST + last = INGRESS for name, deriver in derivers: - self.add_view_deriver(deriver, name=name, under=last) + self.add_view_deriver( + deriver, + name=name, + under=last, + over=MAPPED_VIEW, + ) last = name - # ensure rendered_view is over LAST - self.add_view_deriver( - d.rendered_view, - 'rendered_view', - under=last, - over=pyramid.util.LAST, - ) + self.add_view_deriver(d.mapped_view, name=MAPPED_VIEW, under=last) def derive_view(self, view, attr=None, renderer=None): """ diff --git a/pyramid/tests/test_viewderivers.py b/pyramid/tests/test_viewderivers.py index dfac827dc..dd142769b 100644 --- a/pyramid/tests/test_viewderivers.py +++ b/pyramid/tests/test_viewderivers.py @@ -1103,14 +1103,13 @@ class TestDerivationOrder(unittest.TestCase): from pyramid.interfaces import IViewDerivers self.config.add_view_deriver(None, 'deriv1') - self.config.add_view_deriver(None, 'deriv2', over='deriv1') - self.config.add_view_deriver(None, 'deriv3', under='deriv2') + self.config.add_view_deriver(None, 'deriv2', 'decorated_view', 'deriv1') + self.config.add_view_deriver(None, 'deriv3', 'deriv2', 'deriv1') derivers = self.config.registry.getUtility(IViewDerivers) derivers_sorted = derivers.sorted() dlist = [d for (d, _) in derivers_sorted] self.assertEqual([ - 'authdebug_view', 'secured_view', 'owrapped_view', 'http_cached_view', @@ -1119,6 +1118,7 @@ class TestDerivationOrder(unittest.TestCase): 'deriv3', 'deriv1', 'rendered_view', + 'mapped_view', ], dlist) def test_right_order_implicit(self): @@ -1132,7 +1132,6 @@ class TestDerivationOrder(unittest.TestCase): derivers_sorted = derivers.sorted() dlist = [d for (d, _) in derivers_sorted] self.assertEqual([ - 'authdebug_view', 'secured_view', 'owrapped_view', 'http_cached_view', @@ -1141,31 +1140,32 @@ class TestDerivationOrder(unittest.TestCase): 'deriv2', 'deriv1', 'rendered_view', + 'mapped_view', ], dlist) def test_right_order_under_rendered_view(self): from pyramid.interfaces import IViewDerivers - self.config.add_view_deriver(None, 'deriv1', under='rendered_view') + self.config.add_view_deriver(None, 'deriv1', 'rendered_view', 'mapped_view') derivers = self.config.registry.getUtility(IViewDerivers) derivers_sorted = derivers.sorted() dlist = [d for (d, _) in derivers_sorted] self.assertEqual([ - 'authdebug_view', 'secured_view', 'owrapped_view', 'http_cached_view', 'decorated_view', 'rendered_view', 'deriv1', + 'mapped_view', ], dlist) def test_right_order_under_rendered_view_others(self): from pyramid.interfaces import IViewDerivers - self.config.add_view_deriver(None, 'deriv1', under='rendered_view') + self.config.add_view_deriver(None, 'deriv1', 'rendered_view', 'mapped_view') self.config.add_view_deriver(None, 'deriv2') self.config.add_view_deriver(None, 'deriv3') @@ -1173,7 +1173,6 @@ class TestDerivationOrder(unittest.TestCase): derivers_sorted = derivers.sorted() dlist = [d for (d, _) in derivers_sorted] self.assertEqual([ - 'authdebug_view', 'secured_view', 'owrapped_view', 'http_cached_view', @@ -1182,6 +1181,7 @@ class TestDerivationOrder(unittest.TestCase): 'deriv2', 'rendered_view', 'deriv1', + 'mapped_view', ], dlist) @@ -1218,11 +1218,11 @@ class TestAddDeriver(unittest.TestCase): def __init__(self): self.response = DummyResponse() - def deriv1(view, value, **kw): + def deriv1(view, info): flags['deriv1'] = True return view - def deriv2(view, value, **kw): + def deriv2(view, info): flags['deriv2'] = True return view @@ -1240,27 +1240,84 @@ class TestAddDeriver(unittest.TestCase): self.assertTrue(flags.get('deriv2')) def test_add_multi_derivers_ordered(self): + from pyramid.viewderivers import INGRESS response = DummyResponse() view = lambda *arg: response response.deriv = [] - def deriv1(view, value, **kw): + def deriv1(view, info): response.deriv.append('deriv1') return view - def deriv2(view, value, **kw): + def deriv2(view, info): response.deriv.append('deriv2') return view - def deriv3(view, value, **kw): + def deriv3(view, info): response.deriv.append('deriv3') return view self.config.add_view_deriver(deriv1, 'deriv1') - self.config.add_view_deriver(deriv2, 'deriv2', under='deriv1') - self.config.add_view_deriver(deriv3, 'deriv3', over='deriv2') + self.config.add_view_deriver(deriv2, 'deriv2', INGRESS, 'deriv1') + self.config.add_view_deriver(deriv3, 'deriv3', 'deriv2', 'deriv1') result = self.config._derive_view(view) - self.assertEqual(response.deriv, ['deriv2', 'deriv3', 'deriv1']) + self.assertEqual(response.deriv, ['deriv1', 'deriv3', 'deriv2']) + + def test_add_deriver_without_name(self): + from pyramid.interfaces import IViewDerivers + def deriv1(view, info): pass + self.config.add_view_deriver(deriv1) + derivers = self.config.registry.getUtility(IViewDerivers) + self.assertTrue('deriv1' in derivers.names) + + def test_add_deriver_reserves_ingress(self): + from pyramid.exceptions import ConfigurationError + from pyramid.viewderivers import INGRESS + def deriv1(view, info): pass + self.assertRaises( + ConfigurationError, self.config.add_view_deriver, deriv1, INGRESS) + + def test_add_deriver_enforces_over_is_defined(self): + from pyramid.exceptions import ConfigurationError + def deriv1(view, info): pass + try: + self.config.add_view_deriver(deriv1, under='rendered_view') + except ConfigurationError as ex: + self.assertTrue('must specify an "over" constraint' in ex.args[0]) + else: # pragma: no cover + raise AssertionError + + def test_add_deriver_enforces_under_is_defined(self): + from pyramid.exceptions import ConfigurationError + def deriv1(view, info): pass + try: + self.config.add_view_deriver(deriv1, over='rendered_view') + except ConfigurationError as ex: + self.assertTrue('must specify an "under" constraint' in ex.args[0]) + else: # pragma: no cover + raise AssertionError + + def test_add_deriver_enforces_ingress_is_first(self): + from pyramid.exceptions import ConfigurationError + from pyramid.viewderivers import INGRESS + def deriv1(view, info): pass + try: + self.config.add_view_deriver(deriv1, under='rendered_view', over=INGRESS) + except ConfigurationError as ex: + self.assertTrue('cannot be over view deriver INGRESS' in ex.args[0]) + else: # pragma: no cover + raise AssertionError + + def test_add_deriver_enforces_mapped_view_is_last(self): + from pyramid.exceptions import ConfigurationError + def deriv1(view, info): pass + try: + self.config.add_view_deriver( + deriv1, 'deriv1', 'mapped_view', 'rendered_view') + except ConfigurationError as ex: + self.assertTrue('cannot be under view deriver MAPPED_VIEW' in ex.args[0]) + else: # pragma: no cover + raise AssertionError class TestDeriverIntegration(unittest.TestCase): diff --git a/pyramid/viewderivers.py b/pyramid/viewderivers.py index 99baf46f9..f97099cc8 100644 --- a/pyramid/viewderivers.py +++ b/pyramid/viewderivers.py @@ -260,6 +260,13 @@ def http_cached_view(view, info): http_cached_view.options = ('http_cache',) def secured_view(view, info): + for wrapper in (_secured_view, _authdebug_view): + view = wraps_view(wrapper)(view, info) + return view + +secured_view.options = ('permission',) + +def _secured_view(view, info): permission = info.options.get('permission') if permission == NO_PERMISSION_REQUIRED: # allow views registered within configurations that have a @@ -291,9 +298,7 @@ def secured_view(view, info): return wrapped_view -secured_view.options = ('permission',) - -def authdebug_view(view, info): +def _authdebug_view(view, info): wrapped_view = view settings = info.settings permission = info.options.get('permission') @@ -330,8 +335,6 @@ def authdebug_view(view, info): return wrapped_view -authdebug_view.options = ('permission',) - def predicated_view(view, info): preds = info.predicates if not preds: @@ -451,3 +454,6 @@ def decorated_view(view, info): return decorator(view) decorated_view.options = ('decorator',) + +MAPPED_VIEW = 'mapped_view' +INGRESS = 'INGRESS' -- cgit v1.2.3 From 21fd514e069f9d172ac0f96febd721ed93917ae3 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Thu, 7 Apr 2016 03:12:27 -0700 Subject: - update ini.rst --- docs/quick_tutorial/ini.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quick_tutorial/ini.rst b/docs/quick_tutorial/ini.rst index 36942c767..0aed304df 100644 --- a/docs/quick_tutorial/ini.rst +++ b/docs/quick_tutorial/ini.rst @@ -50,7 +50,7 @@ Steps .. code-block:: bash - $ $VENV/bin/python setup.py develop + $ $VENV/bin/pip install -e . #. Let's make a file ``ini/development.ini`` for our configuration: -- cgit v1.2.3 From ba78808ec3749aeb6bf5512c06a7999abeacd08f Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Thu, 7 Apr 2016 03:14:04 -0700 Subject: - update debugtoolbar --- docs/quick_tutorial/debugtoolbar.rst | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/quick_tutorial/debugtoolbar.rst b/docs/quick_tutorial/debugtoolbar.rst index f11abc493..1f89cd319 100644 --- a/docs/quick_tutorial/debugtoolbar.rst +++ b/docs/quick_tutorial/debugtoolbar.rst @@ -4,8 +4,7 @@ 04: Easier Development with ``debugtoolbar`` ============================================ -Error-handling and introspection using the ``pyramid_debugtoolbar`` -add-on. +Error handling and introspection using the ``pyramid_debugtoolbar`` add-on. Background ========== @@ -36,8 +35,8 @@ Steps .. code-block:: bash $ cd ..; cp -r ini debugtoolbar; cd debugtoolbar - $ $VENV/bin/python setup.py develop - $ $VENV/bin/easy_install pyramid_debugtoolbar + $ $VENV/bin/pip install -e . + $ $VENV/bin/pip install pyramid_debugtoolbar #. Our ``debugtoolbar/development.ini`` gets a configuration entry for ``pyramid.includes``: -- cgit v1.2.3 From d68cbc6f69446317bc8b01062609c4779624cfc5 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Thu, 7 Apr 2016 03:15:45 -0700 Subject: - update unit_testing.rst --- docs/quick_tutorial/unit_testing.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/quick_tutorial/unit_testing.rst b/docs/quick_tutorial/unit_testing.rst index 4cb7ef714..58512d1cc 100644 --- a/docs/quick_tutorial/unit_testing.rst +++ b/docs/quick_tutorial/unit_testing.rst @@ -48,8 +48,8 @@ Steps .. code-block:: bash $ cd ..; cp -r debugtoolbar unit_testing; cd unit_testing - $ $VENV/bin/python setup.py develop - $ $VENV/bin/easy_install nose + $ $VENV/bin/pip install -e . + $ $VENV/bin/pip install nose #. Now we write a simple unit test in ``unit_testing/tutorial/tests.py``: -- cgit v1.2.3 From 9459d8c11a1bd8d62c84c7ff1f761c6aead61510 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Thu, 7 Apr 2016 03:17:03 -0700 Subject: - update functional_testing.rst --- docs/quick_tutorial/functional_testing.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/quick_tutorial/functional_testing.rst b/docs/quick_tutorial/functional_testing.rst index 6f1544e79..b8aa7e87d 100644 --- a/docs/quick_tutorial/functional_testing.rst +++ b/docs/quick_tutorial/functional_testing.rst @@ -34,8 +34,8 @@ Steps .. code-block:: bash $ cd ..; cp -r unit_testing functional_testing; cd functional_testing - $ $VENV/bin/python setup.py develop - $ $VENV/bin/easy_install webtest + $ $VENV/bin/pip install -e . + $ $VENV/bin/pip install webtest #. Let's extend ``functional_testing/tutorial/tests.py`` to include a functional test: -- cgit v1.2.3 From 010b7cf0e08f1d9815e66fc915bf7412df699f1f Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Thu, 7 Apr 2016 03:18:24 -0700 Subject: - update views.rst --- docs/quick_tutorial/views.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quick_tutorial/views.rst b/docs/quick_tutorial/views.rst index 6728925fd..5b6e2960b 100644 --- a/docs/quick_tutorial/views.rst +++ b/docs/quick_tutorial/views.rst @@ -43,7 +43,7 @@ Steps .. code-block:: bash $ cd ..; cp -r functional_testing views; cd views - $ $VENV/bin/python setup.py develop + $ $VENV/bin/pip install -e . #. Our ``views/tutorial/__init__.py`` gets a lot shorter: -- cgit v1.2.3 From 96ca13ad07593fe15c8981e8a0372d749f6bd411 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Thu, 7 Apr 2016 03:19:59 -0700 Subject: - update templating.rst --- docs/quick_tutorial/jinja2.rst | 2 +- docs/quick_tutorial/templating.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/quick_tutorial/jinja2.rst b/docs/quick_tutorial/jinja2.rst index 2121803f9..8622e968a 100644 --- a/docs/quick_tutorial/jinja2.rst +++ b/docs/quick_tutorial/jinja2.rst @@ -27,7 +27,7 @@ Steps $ cd ..; cp -r view_classes jinja2; cd jinja2 $ $VENV/bin/python setup.py develop - $ $VENV/bin/easy_install pyramid_jinja2 + $ $VENV/bin/pip install pyramid_jinja2 #. We need to include ``pyramid_jinja2`` in ``jinja2/tutorial/__init__.py``: diff --git a/docs/quick_tutorial/templating.rst b/docs/quick_tutorial/templating.rst index cf56d2a96..a975d9ec2 100644 --- a/docs/quick_tutorial/templating.rst +++ b/docs/quick_tutorial/templating.rst @@ -56,7 +56,7 @@ Steps .. code-block:: bash - $ $VENV/bin/python setup.py develop + $ $VENV/bin/pip install -e . #. We need to connect ``pyramid_chameleon`` as a renderer by making a call in the setup of ``templating/tutorial/__init__.py``: -- cgit v1.2.3 From fd965fe8bfb62a467521e3883237b0506296b7b4 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Thu, 7 Apr 2016 03:21:14 -0700 Subject: - update view_classes.rst --- docs/quick_tutorial/view_classes.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quick_tutorial/view_classes.rst b/docs/quick_tutorial/view_classes.rst index 6198eed63..cc5337493 100644 --- a/docs/quick_tutorial/view_classes.rst +++ b/docs/quick_tutorial/view_classes.rst @@ -41,7 +41,7 @@ Steps .. code-block:: bash $ cd ..; cp -r templating view_classes; cd view_classes - $ $VENV/bin/python setup.py develop + $ $VENV/bin/pip install -e . #. Our ``view_classes/tutorial/views.py`` now has a view class with our two views: -- cgit v1.2.3 From 5e088cc17818cf3921c3634ca4e35011525e4076 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Thu, 7 Apr 2016 03:22:20 -0700 Subject: - update request_response.rst --- docs/quick_tutorial/request_response.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quick_tutorial/request_response.rst b/docs/quick_tutorial/request_response.rst index 4f8de0221..f42423de8 100644 --- a/docs/quick_tutorial/request_response.rst +++ b/docs/quick_tutorial/request_response.rst @@ -41,7 +41,7 @@ Steps .. code-block:: bash $ cd ..; cp -r view_classes request_response; cd request_response - $ $VENV/bin/python setup.py develop + $ $VENV/bin/pip install -e . #. Simplify the routes in ``request_response/tutorial/__init__.py``: -- cgit v1.2.3 From e612f1ef16a25febbb61ed1b12634c814b251d83 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Thu, 7 Apr 2016 03:23:47 -0700 Subject: - update routing.rst --- docs/quick_tutorial/routing.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quick_tutorial/routing.rst b/docs/quick_tutorial/routing.rst index 416a346fa..7b6d0904d 100644 --- a/docs/quick_tutorial/routing.rst +++ b/docs/quick_tutorial/routing.rst @@ -48,7 +48,7 @@ Steps .. code-block:: bash $ cd ..; cp -r view_classes routing; cd routing - $ $VENV/bin/python setup.py develop + $ $VENV/bin/pip install -e . #. Our ``routing/tutorial/__init__.py`` needs a route with a replacement pattern: -- cgit v1.2.3 From 45fabb70bf376b4ee1ae6594be71577f4e3ebb08 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Thu, 7 Apr 2016 03:24:59 -0700 Subject: - update jinja2.rst --- docs/quick_tutorial/jinja2.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quick_tutorial/jinja2.rst b/docs/quick_tutorial/jinja2.rst index 8622e968a..6b9d5feba 100644 --- a/docs/quick_tutorial/jinja2.rst +++ b/docs/quick_tutorial/jinja2.rst @@ -26,7 +26,7 @@ Steps .. code-block:: bash $ cd ..; cp -r view_classes jinja2; cd jinja2 - $ $VENV/bin/python setup.py develop + $ $VENV/bin/pip install -e . $ $VENV/bin/pip install pyramid_jinja2 #. We need to include ``pyramid_jinja2`` in -- cgit v1.2.3 From 2d2ced28cd261b0080bcec9f101432b4fe40c13b Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Thu, 7 Apr 2016 03:26:06 -0700 Subject: - update static_assets.rst --- docs/quick_tutorial/static_assets.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quick_tutorial/static_assets.rst b/docs/quick_tutorial/static_assets.rst index 3a7496ec7..61c5fbd50 100644 --- a/docs/quick_tutorial/static_assets.rst +++ b/docs/quick_tutorial/static_assets.rst @@ -23,7 +23,7 @@ Steps .. code-block:: bash $ cd ..; cp -r view_classes static_assets; cd static_assets - $ $VENV/bin/python setup.py develop + $ $VENV/bin/pip install -e . #. We add a call ``config.add_static_view`` in ``static_assets/tutorial/__init__.py``: -- cgit v1.2.3 From b37b6f4b5393191047edf44173d3d8c2581861a7 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Thu, 7 Apr 2016 03:26:52 -0700 Subject: - update json.rst --- docs/quick_tutorial/json.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quick_tutorial/json.rst b/docs/quick_tutorial/json.rst index aa789d833..49421829b 100644 --- a/docs/quick_tutorial/json.rst +++ b/docs/quick_tutorial/json.rst @@ -31,7 +31,7 @@ Steps .. code-block:: bash $ cd ..; cp -r view_classes json; cd json - $ $VENV/bin/python setup.py develop + $ $VENV/bin/pip install -e . #. We add a new route for ``hello_json`` in ``json/tutorial/__init__.py``: -- cgit v1.2.3 From 9f66915a3878ad9764b8fd5039465db54f7a47c1 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Thu, 7 Apr 2016 03:27:57 -0700 Subject: - update more_view_classes.rst --- docs/quick_tutorial/more_view_classes.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quick_tutorial/more_view_classes.rst b/docs/quick_tutorial/more_view_classes.rst index afbb7cc3a..fb97cceb2 100644 --- a/docs/quick_tutorial/more_view_classes.rst +++ b/docs/quick_tutorial/more_view_classes.rst @@ -57,7 +57,7 @@ Steps .. code-block:: bash $ cd ..; cp -r templating more_view_classes; cd more_view_classes - $ $VENV/bin/python setup.py develop + $ $VENV/bin/pip install -e . #. Our route in ``more_view_classes/tutorial/__init__.py`` needs some replacement patterns: -- cgit v1.2.3 From 14fd6dc1d9b74b2ea0034d9c485e50ad9bcbbecf Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Thu, 7 Apr 2016 03:28:38 -0700 Subject: - update logging.rst --- docs/quick_tutorial/logging.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quick_tutorial/logging.rst b/docs/quick_tutorial/logging.rst index 5d29cd196..556a09bf0 100644 --- a/docs/quick_tutorial/logging.rst +++ b/docs/quick_tutorial/logging.rst @@ -35,7 +35,7 @@ Steps .. code-block:: bash $ cd ..; cp -r view_classes logging; cd logging - $ $VENV/bin/python setup.py develop + $ $VENV/bin/pip install -e . #. Extend ``logging/tutorial/views.py`` to log a message: -- cgit v1.2.3 From 6e45624d143631aafc808d97cff66263e6152ed5 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Thu, 7 Apr 2016 03:29:16 -0700 Subject: - update sessions.rst --- docs/quick_tutorial/sessions.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quick_tutorial/sessions.rst b/docs/quick_tutorial/sessions.rst index f97405500..06176f2b6 100644 --- a/docs/quick_tutorial/sessions.rst +++ b/docs/quick_tutorial/sessions.rst @@ -34,7 +34,7 @@ Steps .. code-block:: bash $ cd ..; cp -r view_classes sessions; cd sessions - $ $VENV/bin/python setup.py develop + $ $VENV/bin/pip install -e . #. Our ``sessions/tutorial/__init__.py`` needs a choice of session factory to get registered with the :term:`configurator`: -- cgit v1.2.3 From 9845f718ccd609b6ecf514b165a4fd2f9dfb8d8c Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Thu, 7 Apr 2016 03:30:26 -0700 Subject: - update forms.rst --- docs/quick_tutorial/forms.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quick_tutorial/forms.rst b/docs/quick_tutorial/forms.rst index f81b88fc2..023e7127f 100644 --- a/docs/quick_tutorial/forms.rst +++ b/docs/quick_tutorial/forms.rst @@ -50,7 +50,7 @@ Steps .. code-block:: bash - $ $VENV/bin/python setup.py develop + $ $VENV/bin/pip install -e . #. Register a static view in ``forms/tutorial/__init__.py`` for Deform's CSS/JS etc. as well as our demo wikipage scenario's -- cgit v1.2.3 From a42b09be68e52d8204bc0f3c18697bb2020f8fc4 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Thu, 7 Apr 2016 03:31:34 -0700 Subject: - update databases.rst --- docs/quick_tutorial/databases.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/quick_tutorial/databases.rst b/docs/quick_tutorial/databases.rst index 19dfd066d..311ff6ec5 100644 --- a/docs/quick_tutorial/databases.rst +++ b/docs/quick_tutorial/databases.rst @@ -53,7 +53,7 @@ Steps .. note:: - We aren't yet doing ``$VENV/bin/python setup.py develop`` as we + We aren't yet doing ``$VENV/bin/pip install -e .`` as we will change it later. #. Our configuration file at ``databases/development.ini`` wires @@ -78,7 +78,7 @@ Steps .. code-block:: bash - $ $VENV/bin/python setup.py develop + $ $VENV/bin/pip install -e . #. The script references some models in ``databases/tutorial/models.py``: -- cgit v1.2.3 From 2c8511fffc2914480646ab5afb47d12c44ffc6a3 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Thu, 7 Apr 2016 03:32:19 -0700 Subject: - update authentication.rst --- docs/quick_tutorial/authentication.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quick_tutorial/authentication.rst b/docs/quick_tutorial/authentication.rst index 7fd8173d4..5b4a6224d 100644 --- a/docs/quick_tutorial/authentication.rst +++ b/docs/quick_tutorial/authentication.rst @@ -33,7 +33,7 @@ Steps .. code-block:: bash $ cd ..; cp -r view_classes authentication; cd authentication - $ $VENV/bin/python setup.py develop + $ $VENV/bin/pip install -e . #. Put the security hash in the ``authentication/development.ini`` configuration file as ``tutorial.secret`` instead of putting it in -- cgit v1.2.3 From d9c4cbb73b974db2973985369493efe0aec63737 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Thu, 7 Apr 2016 03:34:05 -0700 Subject: - update authorization.rst - add intersphinx target links --- docs/quick_tutorial/authentication.rst | 2 ++ docs/quick_tutorial/authorization.rst | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/quick_tutorial/authentication.rst b/docs/quick_tutorial/authentication.rst index 5b4a6224d..cb3839b08 100644 --- a/docs/quick_tutorial/authentication.rst +++ b/docs/quick_tutorial/authentication.rst @@ -1,3 +1,5 @@ +.. _qtut_authentication: + ============================== 20: Logins With Authentication ============================== diff --git a/docs/quick_tutorial/authorization.rst b/docs/quick_tutorial/authorization.rst index 855043f7f..a4a12774b 100644 --- a/docs/quick_tutorial/authorization.rst +++ b/docs/quick_tutorial/authorization.rst @@ -1,3 +1,5 @@ +.. _qtut_authorization: + =========================================== 21: Protecting Resources With Authorization =========================================== @@ -38,7 +40,7 @@ Steps .. code-block:: bash $ cd ..; cp -r authentication authorization; cd authorization - $ $VENV/bin/python setup.py develop + $ $VENV/bin/pip install -e . #. Start by changing ``authorization/tutorial/__init__.py`` to specify a root factory to the :term:`configurator`: -- cgit v1.2.3 From 3c6d7e587142a884710cc4a41c12c7b26547fc44 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Thu, 7 Apr 2016 15:58:14 -0700 Subject: replace easy_install/setup.py with pip --- docs/tutorials/modwsgi/index.rst | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/tutorials/modwsgi/index.rst b/docs/tutorials/modwsgi/index.rst index ddd968927..1a149b44a 100644 --- a/docs/tutorials/modwsgi/index.rst +++ b/docs/tutorials/modwsgi/index.rst @@ -1,7 +1,7 @@ .. _modwsgi_tutorial: Running a :app:`Pyramid` Application under ``mod_wsgi`` -========================================================== +======================================================= :term:`mod_wsgi` is an Apache module developed by Graham Dumpleton. It allows :term:`WSGI` programs to be served using the Apache web @@ -30,11 +30,11 @@ specific path information for commands and files. for your platform into your system's Apache installation. #. Install :term:`virtualenv` into the Python which mod_wsgi will - run using the ``easy_install`` program. + run using pip. .. code-block:: text - $ sudo /usr/bin/easy_install-2.6 virtualenv + $ sudo /usr/bin/pip install virtualenv This command may need to be performed as the root user. @@ -53,7 +53,7 @@ specific path information for commands and files. .. code-block:: text $ cd ~/modwsgi/env - $ $VENV/bin/easy_install pyramid + $ $VENV/bin/pip install pyramid #. Create and install your :app:`Pyramid` application. For the purposes of this tutorial, we'll just be using the ``pyramid_starter`` application as @@ -65,7 +65,7 @@ specific path information for commands and files. $ cd ~/modwsgi/env $ $VENV/bin/pcreate -s starter myapp $ cd myapp - $ $VENV/bin/python setup.py install + $ $VENV/bin/pip install -e . #. Within the virtualenv directory (``~/modwsgi/env``), create a script named ``pyramid.wsgi``. Give it these contents: @@ -131,4 +131,3 @@ serve up a :app:`Pyramid` application. See the `mod_wsgi configuration documentation `_ for more in-depth configuration information. - -- cgit v1.2.3 From c231d8174e811eec5a3faeafa5aee60757c6d31f Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 8 Apr 2016 00:47:01 -0500 Subject: update constraints for derivers as well as docs --- docs/api/viewderivers.rst | 7 +++--- docs/narr/hooks.rst | 34 +++++++++++++++++++------ pyramid/config/views.py | 51 +++++++++++++++++++------------------- pyramid/tests/test_viewderivers.py | 49 +++++++++++++++++++++--------------- pyramid/viewderivers.py | 2 +- 5 files changed, 85 insertions(+), 58 deletions(-) diff --git a/docs/api/viewderivers.rst b/docs/api/viewderivers.rst index a4ec107b6..2a141501e 100644 --- a/docs/api/viewderivers.rst +++ b/docs/api/viewderivers.rst @@ -10,7 +10,8 @@ Constant representing the request ingress, for use in ``under`` arguments to :meth:`pyramid.config.Configurator.add_view_deriver`. - .. attribute:: MAPPED_VIEW + .. attribute:: VIEW - Constant representing the closest view deriver, for use in ``over`` - arguments to :meth:`pyramid.config.Configurator.add_view_deriver`. + Constant representing the :term:`view callable` at the end of the view + pipeline, for use in ``over`` arguments to + :meth:`pyramid.config.Configurator.add_view_deriver`. diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index 3a1ad8363..2c3782387 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -1617,6 +1617,14 @@ the user-defined :term:`view callable`: view pipeline interface to accept ``(context, request)`` from all previous view derivers. +.. warning:: + + Any view derivers defined ``under`` the ``rendered_view`` are not + guaranteed to receive a valid response object. Rather they will receive the + result from the :term:`view mapper` which is likely the original response + returned from the view. This is possibly a dictionary for a renderer but it + may be any Python object that may be adapted into a response. + Custom View Derivers ~~~~~~~~~~~~~~~~~~~~ @@ -1642,7 +1650,7 @@ view pipeline: response.headers['X-View-Performance'] = '%.3f' % (end - start,) return wrapper_view - config.add_view_deriver(timing_view, 'timing view') + config.add_view_deriver(timing_view) View derivers are unique in that they have access to most of the options passed to :meth:`pyramid.config.Configurator.add_view` in order to decide what @@ -1668,7 +1676,7 @@ token unless ``disable_csrf=True`` is passed to the view: require_csrf_view.options = ('disable_csrf',) - config.add_view_deriver(require_csrf_view, 'require_csrf_view') + config.add_view_deriver(require_csrf_view) def protected_view(request): return Response('protected') @@ -1691,13 +1699,19 @@ By default, every new view deriver is added between the ``decorated_view`` and ``rendered_view`` built-in derivers. It is possible to customize this ordering using the ``over`` and ``under`` options. Each option can use the names of other view derivers in order to specify an ordering. There should rarely be a -reason to worry about the ordering of the derivers. +reason to worry about the ordering of the derivers except when the deriver +depends on other operations in the view pipeline. Both ``over`` and ``under`` may also be iterables of constraints. For either option, if one or more constraints was defined, at least one must be satisfied, else a :class:`pyramid.exceptions.ConfigurationError` will be raised. This may be used to define fallback constraints if another deriver is missing. +Two sentinel values exist, :attr:`pyramid.viewderivers.INGRESS` and +:attr:`pyramid.viewderivers.VIEW`, which may be used when specifying +constraints at the edges of the view pipeline. For example, to add a deriver +at the start of the pipeline you may use ``under=INGRESS``. + It is not possible to add a view deriver under the ``mapped_view`` as the :term:`view mapper` is intimately tied to the signature of the user-defined :term:`view callable`. If you simply need to know what the original view @@ -1707,8 +1721,12 @@ deriver. .. warning:: - Any view derivers defined ``under`` the ``rendered_view`` are not - guaranteed to receive a valid response object. Rather they will receive the - result from the :term:`view mapper` which is likely the original response - returned from the view. This is possibly a dictionary for a renderer but it - may be any Python object that may be adapted into a response. + The default constraints for any view deriver are ``over='rendered_view'`` + and ``under='decorated_view'``. When escaping these constraints you must + take care to avoid cyclic dependencies between derivers. For example, if + you want to add a new view deriver before ``secured_view`` then + simply specifying ``over='secured_view'`` is not enough, because the + default is also under ``decorated view`` there will be an unsatisfiable + cycle. You must specify a valid ``under`` constraint as well, such as + ``under=INGRESS`` to fall between INGRESS and ``secured_view`` at the + beginning of the view pipeline. diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 1e161177b..3f6a9080d 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -80,7 +80,7 @@ import pyramid.viewderivers from pyramid.viewderivers import ( INGRESS, - MAPPED_VIEW, + VIEW, preserve_view_attrs, view_description, requestonly, @@ -1115,9 +1115,12 @@ class ViewsConfiguratorMixin(object): ``under`` means closer to the user-defined :term:`view callable`, and ``over`` means closer to view pipeline ingress. - Specifying neither ``under`` nor ``over`` is equivalent to specifying - ``over='rendered_view'`` and ``under='decorated_view'``, placing the - deriver somewhere between the ``decorated_view`` and ``rendered_view`` + The default value for ``over`` is ``rendered_view`` and ``under`` is + ``decorated_view``. This places the deriver somewhere between the two + in the view pipeline. If the deriver should be placed elsewhere in the + pipeline, such as above ``decorated_view``, then you MUST also specify + ``under`` to something earlier in the order, or a + ``CyclicDependencyError`` will be raised when trying to sort the derivers. See :ref:`view_derivers` for more information. @@ -1128,33 +1131,30 @@ class ViewsConfiguratorMixin(object): if name is None: name = deriver.__name__ - if name in (INGRESS,): + if name in (INGRESS, VIEW): raise ConfigurationError('%s is a reserved view deriver name' % name) - if under is None and over is None: + if under is None: under = 'decorated_view' + + if over is None: over = 'rendered_view' - if over is None and name != MAPPED_VIEW: - raise ConfigurationError('must specify an "over" constraint for ' - 'the %s view deriver' % name) - elif over is not None: - over = as_sorted_tuple(over) + over = as_sorted_tuple(over) + under = as_sorted_tuple(under) - if under is None: - raise ConfigurationError('must specify an "under" constraint for ' - 'the %s view deriver' % name) - else: - under = as_sorted_tuple(under) + if INGRESS in over: + raise ConfigurationError('%s cannot be over INGRESS' % name) - if over is not None and INGRESS in over: - raise ConfigurationError('%s cannot be over view deriver INGRESS' - % name) + # ensure everything is always over mapped_view + if VIEW in over and name != 'mapped_view': + over = as_sorted_tuple(over + ('mapped_view',)) - if MAPPED_VIEW in under: - raise ConfigurationError('%s cannot be under view deriver ' - 'MAPPED_VIEW' % name) + if VIEW in under: + raise ConfigurationError('%s cannot be under VIEW' % name) + if 'mapped_view' in under: + raise ConfigurationError('%s cannot be under "mapped_view"' % name) discriminator = ('view deriver', name) intr = self.introspectable( @@ -1173,7 +1173,7 @@ class ViewsConfiguratorMixin(object): default_before=None, default_after=INGRESS, first=INGRESS, - last=MAPPED_VIEW, + last=VIEW, ) self.registry.registerUtility(derivers, IViewDerivers) derivers.add(name, deriver, before=over, after=under) @@ -1188,6 +1188,7 @@ class ViewsConfiguratorMixin(object): ('http_cached_view', d.http_cached_view), ('decorated_view', d.decorated_view), ('rendered_view', d.rendered_view), + ('mapped_view', d.mapped_view), ] last = INGRESS for name, deriver in derivers: @@ -1195,12 +1196,10 @@ class ViewsConfiguratorMixin(object): deriver, name=name, under=last, - over=MAPPED_VIEW, + over=VIEW, ) last = name - self.add_view_deriver(d.mapped_view, name=MAPPED_VIEW, under=last) - def derive_view(self, view, attr=None, renderer=None): """ Create a :term:`view callable` using the function, instance, diff --git a/pyramid/tests/test_viewderivers.py b/pyramid/tests/test_viewderivers.py index dd142769b..1823beb4d 100644 --- a/pyramid/tests/test_viewderivers.py +++ b/pyramid/tests/test_viewderivers.py @@ -1239,6 +1239,25 @@ class TestAddDeriver(unittest.TestCase): self.assertFalse(flags.get('deriv1')) self.assertTrue(flags.get('deriv2')) + def test_override_mapped_view(self): + from pyramid.viewderivers import VIEW + response = DummyResponse() + view = lambda *arg: response + flags = {} + + def deriv1(view, info): + flags['deriv1'] = True + return view + + result = self.config._derive_view(view) + self.assertFalse(flags.get('deriv1')) + + flags.clear() + self.config.add_view_deriver( + deriv1, name='mapped_view', under='rendered_view', over=VIEW) + result = self.config._derive_view(view) + self.assertTrue(flags.get('deriv1')) + def test_add_multi_derivers_ordered(self): from pyramid.viewderivers import INGRESS response = DummyResponse() @@ -1277,34 +1296,25 @@ class TestAddDeriver(unittest.TestCase): self.assertRaises( ConfigurationError, self.config.add_view_deriver, deriv1, INGRESS) - def test_add_deriver_enforces_over_is_defined(self): - from pyramid.exceptions import ConfigurationError - def deriv1(view, info): pass - try: - self.config.add_view_deriver(deriv1, under='rendered_view') - except ConfigurationError as ex: - self.assertTrue('must specify an "over" constraint' in ex.args[0]) - else: # pragma: no cover - raise AssertionError - - def test_add_deriver_enforces_under_is_defined(self): + def test_add_deriver_enforces_ingress_is_first(self): from pyramid.exceptions import ConfigurationError + from pyramid.viewderivers import INGRESS def deriv1(view, info): pass try: - self.config.add_view_deriver(deriv1, over='rendered_view') + self.config.add_view_deriver(deriv1, over=INGRESS) except ConfigurationError as ex: - self.assertTrue('must specify an "under" constraint' in ex.args[0]) + self.assertTrue('cannot be over INGRESS' in ex.args[0]) else: # pragma: no cover raise AssertionError - def test_add_deriver_enforces_ingress_is_first(self): + def test_add_deriver_enforces_view_is_last(self): from pyramid.exceptions import ConfigurationError - from pyramid.viewderivers import INGRESS + from pyramid.viewderivers import VIEW def deriv1(view, info): pass try: - self.config.add_view_deriver(deriv1, under='rendered_view', over=INGRESS) + self.config.add_view_deriver(deriv1, under=VIEW) except ConfigurationError as ex: - self.assertTrue('cannot be over view deriver INGRESS' in ex.args[0]) + self.assertTrue('cannot be under VIEW' in ex.args[0]) else: # pragma: no cover raise AssertionError @@ -1312,10 +1322,9 @@ class TestAddDeriver(unittest.TestCase): from pyramid.exceptions import ConfigurationError def deriv1(view, info): pass try: - self.config.add_view_deriver( - deriv1, 'deriv1', 'mapped_view', 'rendered_view') + self.config.add_view_deriver(deriv1, 'deriv1', under='mapped_view') except ConfigurationError as ex: - self.assertTrue('cannot be under view deriver MAPPED_VIEW' in ex.args[0]) + self.assertTrue('cannot be under "mapped_view"' in ex.args[0]) else: # pragma: no cover raise AssertionError diff --git a/pyramid/viewderivers.py b/pyramid/viewderivers.py index f97099cc8..8061e5d4a 100644 --- a/pyramid/viewderivers.py +++ b/pyramid/viewderivers.py @@ -455,5 +455,5 @@ def decorated_view(view, info): decorated_view.options = ('decorator',) -MAPPED_VIEW = 'mapped_view' +VIEW = 'VIEW' INGRESS = 'INGRESS' -- cgit v1.2.3 From 88c92b8f01ace15b57550cec2ab5c9127667f300 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Fri, 8 Apr 2016 02:18:51 -0700 Subject: Add pyramid_jinja2 example to i18n docs. Fixes #2437. --- docs/narr/i18n.rst | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/narr/i18n.rst b/docs/narr/i18n.rst index 839a48df4..b385eaf96 100644 --- a/docs/narr/i18n.rst +++ b/docs/narr/i18n.rst @@ -670,6 +670,21 @@ There exists a recipe within the :term:`Pyramid Community Cookbook` named :ref:`Mako Internationalization ` which explains how to add idiomatic i18n support to :term:`Mako` templates. + +.. index:: + single: Jinja2 i18n + +Jinja2 Pyramid i18n Support +--------------------------- + +The add-on `pyramid_jinja2 `_ +provides a scaffold with an example of how to use internationalization with +Jinja2 in Pyramid. See the documentation sections `Internalization (i18n) +`_ +and `Paster Template I18N +`_. + + .. index:: single: localization deployment settings single: default_locale_name -- cgit v1.2.3 From e551a4f026c1522ba4045cfb1799d050bdb072bc Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Fri, 8 Apr 2016 04:43:08 -0700 Subject: rough draft for wiki2, replace setup.py with pip. See #2104. --- docs/tutorials/wiki2/installation.rst | 206 ++++++++++++++++++++++++---------- 1 file changed, 148 insertions(+), 58 deletions(-) diff --git a/docs/tutorials/wiki2/installation.rst b/docs/tutorials/wiki2/installation.rst index b263ccbd9..ca7b8ae8a 100644 --- a/docs/tutorials/wiki2/installation.rst +++ b/docs/tutorials/wiki2/installation.rst @@ -72,8 +72,8 @@ Python 3.5: c:\> c:\Python35\Scripts\virtualenv %VENV% -Upgrade pip in the virtual environment --------------------------------------- +Upgrade ``pip`` in the virtual environment +------------------------------------------ On UNIX ^^^^^^^ @@ -187,16 +187,15 @@ On Windows and the project into directories that do not contain spaces in their paths. - .. _installing_project_in_dev_mode: Installing the project in development mode ------------------------------------------ In order to do development on the project easily, you must "register" the -project as a development egg in your workspace using the ``setup.py develop`` +project as a development egg in your workspace using the ``pip install -e .`` command. In order to do so, change directory to the ``tutorial`` directory that -you created in :ref:`sql_making_a_project`, and run the ``setup.py develop`` +you created in :ref:`sql_making_a_project`, and run the ``pip install -e .`` command using the virtualenv Python interpreter. On UNIX @@ -215,25 +214,130 @@ On Windows c:\pyramidtut> cd tutorial c:\pyramidtut\tutorial> %VENV%\Scripts\pip install -e . -The console will show ``setup.py`` checking for packages and installing missing -packages. Success executing this command will show a line like the following:: +The console will show ``pip`` checking for packages and installing missing +packages. Success executing this command will show a line like the following: + +.. code-block:: bash + + Successfully installed Chameleon-2.24 Mako-1.0.4 MarkupSafe-0.23 \ + Pygments-2.1.3 SQLAlchemy-1.0.12 pyramid-chameleon-0.3 \ + pyramid-debugtoolbar-2.4.2 pyramid-mako-1.0.2 pyramid-tm-0.12.1 \ + transaction-1.4.4 tutorial waitress-0.8.10 zope.sqlalchemy-0.7.6 + + +.. _install-testing-requirements: + +Install testing requirements +---------------------------- + +In order to run tests, we need to install the testing requirements, which means +we need to edit our ``setup.py``: + +.. .. literalinclude:: src/installation/setup.py + :language: py + :linenos: + :emphasize-lines: 23-30,50-52 + +.. code-block:: python + :linenos: + :emphasize-lines: 23-29,49-51 + + import os + + from setuptools import setup, find_packages + + here = os.path.abspath(os.path.dirname(__file__)) + with open(os.path.join(here, 'README.txt')) as f: + README = f.read() + with open(os.path.join(here, 'CHANGES.txt')) as f: + CHANGES = f.read() + + requires = [ + 'pyramid', + 'pyramid_jinja2', + 'pyramid_debugtoolbar', + 'pyramid_tm', + 'SQLAlchemy', + 'transaction', + 'zope.sqlalchemy', + 'waitress', + ] + + tests_require = [ + 'WebTest >= 1.3.1', # py3 compat + ] + + testing_extras = tests_require + [ + 'pytest', # includes virtualenv + 'pytest-cov', + ] + + setup(name='tutorial', + version='0.0', + description='tutorial', + long_description=README + '\n\n' + CHANGES, + classifiers=[ + "Programming Language :: Python", + "Framework :: Pyramid", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", + ], + author='', + author_email='', + url='', + keywords='web wsgi bfg pylons pyramid', + packages=find_packages(), + include_package_data=True, + zip_safe=False, + test_suite='tutorial', + extras_require={ + 'testing': testing_extras, + }, + tests_require=tests_require, + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + [console_scripts] + initialize_tutorial_db = tutorial.scripts.initializedb:main + """, + ) + +Only the emphasized lines need to be edited. + +Next install the testing requirements. + +On UNIX +^^^^^^^ + +.. code-block:: bash + + $ $VENV/bin/pip install -e ".[testing]" + +On Windows +^^^^^^^^^^ + +.. code-block:: ps1con + + c:\pyramidtut\tutorial> %VENV%\Scripts\pip install -e ".[testing]" - Finished processing dependencies for tutorial==0.0 .. _sql_running_tests: Run the tests ------------- -After you've installed the project in development mode, you may run the tests -for the project. +After you've installed the project in development mode as well as the testing +requirements, you may run the tests for the project. On UNIX ^^^^^^^ .. code-block:: bash - $ $VENV/bin/python setup.py test -q + $ $VENV/bin/py.test tutorial/tests.py -q + +.. TODO remove comments .. py.test? See https://github.com/Pylons/pyramid/issues/2104#issuecomment-155852046 @@ -242,79 +346,65 @@ On Windows .. code-block:: ps1con - c:\pyramidtut\tutorial> %VENV%\Scripts\python setup.py test -q + c:\pyramidtut\tutorial> %VENV%\Scripts\py.test tutorial\tests.py -q .. py.test? See https://github.com/Pylons/pyramid/issues/2104#issuecomment-155852046 -For a successful test run, you should see output that ends like this:: +For a successful test run, you should see output that ends like this: + +.. code-block:: bash .. - ---------------------------------------------------------------------- - Ran 2 tests in 0.053s + 2 passed in 0.44 seconds - OK Expose test coverage information -------------------------------- -You can run the ``nosetests`` command to see test coverage information. This -runs the tests in the same way that ``setup.py test`` does, but provides -additional "coverage" information, exposing which lines of your project are -covered by the tests. +You can run the ``py.test`` command to see test coverage information. This +runs the tests in the same way that ``py.test`` does, but provides additional +"coverage" information, exposing which lines of your project are covered by the +tests. -To get this functionality working, we'll need to install the ``nose`` and -``coverage`` packages into our ``virtualenv``: +We've already installed the ``pytest-cov`` package into our ``virtualenv``, so +we can run the tests with coverage. On UNIX ^^^^^^^ .. code-block:: bash - $ $VENV/bin/easy_install nose coverage + $ $VENV/bin/py.test --cov=tutorial tutorial/tests.py On Windows ^^^^^^^^^^ .. code-block:: ps1con - c:\pyramidtut\tutorial> %VENV%\Scripts\easy_install nose coverage - -Once ``nose`` and ``coverage`` are installed, we can run the tests with -coverage. - -On UNIX -^^^^^^^ - -.. code-block:: bash - - $ $VENV/bin/nosetests --cover-package=tutorial --cover-erase --with-coverage - -On Windows -^^^^^^^^^^ - -.. code-block:: ps1con - - c:\pyramidtut\tutorial> %VENV%\Scripts\nosetests --cover-package=tutorial \ - --cover-erase --with-coverage + c:\pyramidtut\tutorial> %VENV%\Scripts\py.test --cov=tutorial tutorial\tests.py If successful, you will see output something like this:: - .. - Name Stmts Miss Cover Missing - ---------------------------------------------------------- - tutorial.py 8 6 25% 7-12 - tutorial/models.py 22 0 100% - tutorial/models/meta.py 5 0 100% - tutorial/models/mymodel.py 8 0 100% - tutorial/scripts.py 0 0 100% - tutorial/views.py 0 0 100% - tutorial/views/default.py 12 0 100% - ---------------------------------------------------------- - TOTAL 55 6 89% - ---------------------------------------------------------------------- - Ran 2 tests in 0.579s - - OK +======================== test session starts ======================== +platform Python 3.5.1, pytest-2.9.1, py-1.4.31, pluggy-0.3.1 +rootdir: /Users/stevepiercy/projects/pyramidtut/tutorial, inifile: +plugins: cov-2.2.1 +collected 2 items + +tutorial/tests.py .. +------------------ coverage: platform Python 3.5.1 ------------------ +Name Stmts Miss Cover +------------------------------------------------------ +tutorial/__init__.py 13 9 31% +tutorial/models.py 12 0 100% +tutorial/scripts/__init__.py 0 0 100% +tutorial/scripts/initializedb.py 24 24 0% +tutorial/tests.py 39 0 100% +tutorial/views.py 11 0 100% +------------------------------------------------------ +TOTAL 99 33 67% + +===================== 2 passed in 0.57 seconds ====================== Our package doesn't quite have 100% test coverage. -- cgit v1.2.3 From 25c809968d16fbaa07f667713fd35d64d34de877 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Fri, 8 Apr 2016 04:46:45 -0700 Subject: fix syntax highlighting --- docs/tutorials/wiki2/installation.rst | 46 ++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/docs/tutorials/wiki2/installation.rst b/docs/tutorials/wiki2/installation.rst index ca7b8ae8a..d6745e758 100644 --- a/docs/tutorials/wiki2/installation.rst +++ b/docs/tutorials/wiki2/installation.rst @@ -383,28 +383,30 @@ On Windows c:\pyramidtut\tutorial> %VENV%\Scripts\py.test --cov=tutorial tutorial\tests.py -If successful, you will see output something like this:: - -======================== test session starts ======================== -platform Python 3.5.1, pytest-2.9.1, py-1.4.31, pluggy-0.3.1 -rootdir: /Users/stevepiercy/projects/pyramidtut/tutorial, inifile: -plugins: cov-2.2.1 -collected 2 items - -tutorial/tests.py .. ------------------- coverage: platform Python 3.5.1 ------------------ -Name Stmts Miss Cover ------------------------------------------------------- -tutorial/__init__.py 13 9 31% -tutorial/models.py 12 0 100% -tutorial/scripts/__init__.py 0 0 100% -tutorial/scripts/initializedb.py 24 24 0% -tutorial/tests.py 39 0 100% -tutorial/views.py 11 0 100% ------------------------------------------------------- -TOTAL 99 33 67% - -===================== 2 passed in 0.57 seconds ====================== +If successful, you will see output something like this: + +.. code-block:: bash + + ======================== test session starts ======================== + platform Python 3.5.1, pytest-2.9.1, py-1.4.31, pluggy-0.3.1 + rootdir: /Users/stevepiercy/projects/pyramidtut/tutorial, inifile: + plugins: cov-2.2.1 + collected 2 items + + tutorial/tests.py .. + ------------------ coverage: platform Python 3.5.1 ------------------ + Name Stmts Miss Cover + ------------------------------------------------------ + tutorial/__init__.py 13 9 31% + tutorial/models.py 12 0 100% + tutorial/scripts/__init__.py 0 0 100% + tutorial/scripts/initializedb.py 24 24 0% + tutorial/tests.py 39 0 100% + tutorial/views.py 11 0 100% + ------------------------------------------------------ + TOTAL 99 33 67% + + ===================== 2 passed in 0.57 seconds ====================== Our package doesn't quite have 100% test coverage. -- cgit v1.2.3 From aa51dcc629028fda64ee83340d9b3981be7fbb29 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Fri, 8 Apr 2016 05:14:31 -0700 Subject: use `--cov-report=term-missing` --- docs/tutorials/wiki2/installation.rst | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/tutorials/wiki2/installation.rst b/docs/tutorials/wiki2/installation.rst index d6745e758..70c77f57f 100644 --- a/docs/tutorials/wiki2/installation.rst +++ b/docs/tutorials/wiki2/installation.rst @@ -374,14 +374,15 @@ On UNIX .. code-block:: bash - $ $VENV/bin/py.test --cov=tutorial tutorial/tests.py + $ $VENV/bin/py.test --cov=tutorial --cov-report=term-missing tutorial/tests.py On Windows ^^^^^^^^^^ .. code-block:: ps1con - c:\pyramidtut\tutorial> %VENV%\Scripts\py.test --cov=tutorial tutorial\tests.py + c:\pyramidtut\tutorial> %VENV%\Scripts\py.test --cov=tutorial \ + --cov-report=term-missing tutorial\tests.py If successful, you will see output something like this: @@ -395,15 +396,15 @@ If successful, you will see output something like this: tutorial/tests.py .. ------------------ coverage: platform Python 3.5.1 ------------------ - Name Stmts Miss Cover - ------------------------------------------------------ - tutorial/__init__.py 13 9 31% + Name Stmts Miss Cover Missing + ---------------------------------------------------------------- + tutorial/__init__.py 13 9 31% 13-21 tutorial/models.py 12 0 100% tutorial/scripts/__init__.py 0 0 100% - tutorial/scripts/initializedb.py 24 24 0% + tutorial/scripts/initializedb.py 24 24 0% 1-40 tutorial/tests.py 39 0 100% tutorial/views.py 11 0 100% - ------------------------------------------------------ + ---------------------------------------------------------------- TOTAL 99 33 67% ===================== 2 passed in 0.57 seconds ====================== -- cgit v1.2.3 From c239c4a68b41dab9d3f4477ea7de6431ec7548dd Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Fri, 8 Apr 2016 12:50:10 -0700 Subject: simplify test dependencies per https://github.com/Pylons/pyramid/pull/2442/files/aa51dcc629028fda64ee83340d9b3981be7fbb29#r59052781 --- docs/tutorials/wiki2/installation.rst | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/docs/tutorials/wiki2/installation.rst b/docs/tutorials/wiki2/installation.rst index 70c77f57f..221b62ece 100644 --- a/docs/tutorials/wiki2/installation.rst +++ b/docs/tutorials/wiki2/installation.rst @@ -263,11 +263,8 @@ we need to edit our ``setup.py``: 'waitress', ] - tests_require = [ + testing_extras = [ 'WebTest >= 1.3.1', # py3 compat - ] - - testing_extras = tests_require + [ 'pytest', # includes virtualenv 'pytest-cov', ] @@ -293,7 +290,6 @@ we need to edit our ``setup.py``: extras_require={ 'testing': testing_extras, }, - tests_require=tests_require, install_requires=requires, entry_points="""\ [paste.app_factory] -- cgit v1.2.3 From 3c0d2ead8e7b54ac2a3b31e3589b676b2cc97a53 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Fri, 8 Apr 2016 12:52:07 -0700 Subject: adjust line numbers for emphasis --- docs/tutorials/wiki2/installation.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorials/wiki2/installation.rst b/docs/tutorials/wiki2/installation.rst index 221b62ece..2ea0c8339 100644 --- a/docs/tutorials/wiki2/installation.rst +++ b/docs/tutorials/wiki2/installation.rst @@ -240,7 +240,7 @@ we need to edit our ``setup.py``: .. code-block:: python :linenos: - :emphasize-lines: 23-29,49-51 + :emphasize-lines: 22-26,46-48 import os -- cgit v1.2.3 From 9b61be1feebe26c4943232997ccbdecfb2272cc7 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Fri, 8 Apr 2016 13:36:01 -0700 Subject: removed test_suite line. ping @mmerickel @bertjwregeer --- docs/tutorials/wiki2/installation.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/tutorials/wiki2/installation.rst b/docs/tutorials/wiki2/installation.rst index 2ea0c8339..bd52b8f7a 100644 --- a/docs/tutorials/wiki2/installation.rst +++ b/docs/tutorials/wiki2/installation.rst @@ -286,7 +286,6 @@ we need to edit our ``setup.py``: packages=find_packages(), include_package_data=True, zip_safe=False, - test_suite='tutorial', extras_require={ 'testing': testing_extras, }, -- cgit v1.2.3 From 9b0730733d29fd88b2358ad8ac08eb4761fc5e93 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sat, 9 Apr 2016 00:50:42 -0700 Subject: update alchemy scaffold's setup.py to allow use of pip. See #2104. --- pyramid/scaffolds/alchemy/setup.py_tmpl | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/pyramid/scaffolds/alchemy/setup.py_tmpl b/pyramid/scaffolds/alchemy/setup.py_tmpl index d837ea0a9..9deed9e8a 100644 --- a/pyramid/scaffolds/alchemy/setup.py_tmpl +++ b/pyramid/scaffolds/alchemy/setup.py_tmpl @@ -19,20 +19,22 @@ requires = [ 'waitress', ] -tests_require = [ - 'WebTest', -] +testing_extras = [ + 'WebTest >= 1.3.1', # py3 compat + 'pytest', # includes virtualenv + 'pytest-cov', + ] setup(name='{{project}}', version='0.0', description='{{project}}', long_description=README + '\n\n' + CHANGES, classifiers=[ - "Programming Language :: Python", - "Framework :: Pyramid", - "Topic :: Internet :: WWW/HTTP", - "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", - ], + "Programming Language :: Python", + "Framework :: Pyramid", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", + ], author='', author_email='', url='', @@ -40,8 +42,9 @@ setup(name='{{project}}', packages=find_packages(), include_package_data=True, zip_safe=False, - test_suite='{{package}}', - tests_require=tests_require, + extras_require={ + 'testing': testing_extras, + }, install_requires=requires, entry_points="""\ [paste.app_factory] -- cgit v1.2.3 From b50a19224bbd2b1600e956f83e60bdb5ea908dd4 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sat, 9 Apr 2016 00:52:23 -0700 Subject: add result of installation step in wiki2 tutorial, but using the recently updated scaffold from master and normalize its version to 1.7. See #2104. --- docs/tutorials/wiki2/src/installation/CHANGES.txt | 4 + .../wiki2/src/installation/tutorial/__init__.py | 12 ++ .../src/installation/tutorial/models/__init__.py | 73 ++++++++++ .../wiki2/src/installation/tutorial/models/meta.py | 16 +++ .../src/installation/tutorial/models/mymodel.py | 18 +++ .../wiki2/src/installation/tutorial/routes.py | 3 + .../src/installation/tutorial/scripts/__init__.py | 1 + .../installation/tutorial/scripts/initializedb.py | 45 ++++++ .../installation/tutorial/static/pyramid-16x16.png | Bin 0 -> 1319 bytes .../src/installation/tutorial/static/pyramid.png | Bin 0 -> 12901 bytes .../src/installation/tutorial/static/theme.css | 154 +++++++++++++++++++++ .../src/installation/tutorial/static/theme.min.css | 1 + .../src/installation/tutorial/templates/404.jinja2 | 8 ++ .../installation/tutorial/templates/layout.jinja2 | 66 +++++++++ .../tutorial/templates/mytemplate.jinja2 | 8 ++ .../wiki2/src/installation/tutorial/tests.py | 65 +++++++++ .../src/installation/tutorial/views/__init__.py | 0 .../src/installation/tutorial/views/default.py | 33 +++++ .../src/installation/tutorial/views/notfound.py | 7 + 19 files changed, 514 insertions(+) create mode 100644 docs/tutorials/wiki2/src/installation/CHANGES.txt create mode 100644 docs/tutorials/wiki2/src/installation/tutorial/__init__.py create mode 100644 docs/tutorials/wiki2/src/installation/tutorial/models/__init__.py create mode 100644 docs/tutorials/wiki2/src/installation/tutorial/models/meta.py create mode 100644 docs/tutorials/wiki2/src/installation/tutorial/models/mymodel.py create mode 100644 docs/tutorials/wiki2/src/installation/tutorial/routes.py create mode 100644 docs/tutorials/wiki2/src/installation/tutorial/scripts/__init__.py create mode 100644 docs/tutorials/wiki2/src/installation/tutorial/scripts/initializedb.py create mode 100644 docs/tutorials/wiki2/src/installation/tutorial/static/pyramid-16x16.png create mode 100644 docs/tutorials/wiki2/src/installation/tutorial/static/pyramid.png create mode 100644 docs/tutorials/wiki2/src/installation/tutorial/static/theme.css create mode 100644 docs/tutorials/wiki2/src/installation/tutorial/static/theme.min.css create mode 100644 docs/tutorials/wiki2/src/installation/tutorial/templates/404.jinja2 create mode 100644 docs/tutorials/wiki2/src/installation/tutorial/templates/layout.jinja2 create mode 100644 docs/tutorials/wiki2/src/installation/tutorial/templates/mytemplate.jinja2 create mode 100644 docs/tutorials/wiki2/src/installation/tutorial/tests.py create mode 100644 docs/tutorials/wiki2/src/installation/tutorial/views/__init__.py create mode 100644 docs/tutorials/wiki2/src/installation/tutorial/views/default.py create mode 100644 docs/tutorials/wiki2/src/installation/tutorial/views/notfound.py diff --git a/docs/tutorials/wiki2/src/installation/CHANGES.txt b/docs/tutorials/wiki2/src/installation/CHANGES.txt new file mode 100644 index 000000000..35a34f332 --- /dev/null +++ b/docs/tutorials/wiki2/src/installation/CHANGES.txt @@ -0,0 +1,4 @@ +0.0 +--- + +- Initial version diff --git a/docs/tutorials/wiki2/src/installation/tutorial/__init__.py b/docs/tutorials/wiki2/src/installation/tutorial/__init__.py new file mode 100644 index 000000000..4dab44823 --- /dev/null +++ b/docs/tutorials/wiki2/src/installation/tutorial/__init__.py @@ -0,0 +1,12 @@ +from pyramid.config import Configurator + + +def main(global_config, **settings): + """ This function returns a Pyramid WSGI application. + """ + config = Configurator(settings=settings) + config.include('pyramid_jinja2') + config.include('.models') + config.include('.routes') + config.scan() + return config.make_wsgi_app() diff --git a/docs/tutorials/wiki2/src/installation/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/installation/tutorial/models/__init__.py new file mode 100644 index 000000000..48a957ecb --- /dev/null +++ b/docs/tutorials/wiki2/src/installation/tutorial/models/__init__.py @@ -0,0 +1,73 @@ +from sqlalchemy import engine_from_config +from sqlalchemy.orm import sessionmaker +from sqlalchemy.orm import configure_mappers +import zope.sqlalchemy + +# import or define all models here to ensure they are attached to the +# Base.metadata prior to any initialization routines +from .mymodel import MyModel # flake8: noqa + +# run configure_mappers after defining all of the models to ensure +# all relationships can be setup +configure_mappers() + + +def get_engine(settings, prefix='sqlalchemy.'): + return engine_from_config(settings, prefix) + + +def get_session_factory(engine): + factory = sessionmaker() + factory.configure(bind=engine) + return factory + + +def get_tm_session(session_factory, transaction_manager): + """ + Get a ``sqlalchemy.orm.Session`` instance backed by a transaction. + + This function will hook the session to the transaction manager which + will take care of committing any changes. + + - When using pyramid_tm it will automatically be committed or aborted + depending on whether an exception is raised. + + - When using scripts you should wrap the session in a manager yourself. + For example:: + + import transaction + + engine = get_engine(settings) + session_factory = get_session_factory(engine) + with transaction.manager: + dbsession = get_tm_session(session_factory, transaction.manager) + + """ + dbsession = session_factory() + zope.sqlalchemy.register( + dbsession, transaction_manager=transaction_manager) + return dbsession + + +def includeme(config): + """ + Initialize the model for a Pyramid app. + + Activate this setup using ``config.include('tutorial.models')``. + + """ + settings = config.get_settings() + + # use pyramid_tm to hook the transaction lifecycle to the request + config.include('pyramid_tm') + + session_factory = get_session_factory(get_engine(settings)) + config.registry['dbsession_factory'] = session_factory + + # make request.dbsession available for use in Pyramid + config.add_request_method( + # r.tm is the transaction manager used by pyramid_tm + lambda r: get_tm_session(session_factory, r.tm), + 'dbsession', + reify=True + ) diff --git a/docs/tutorials/wiki2/src/installation/tutorial/models/meta.py b/docs/tutorials/wiki2/src/installation/tutorial/models/meta.py new file mode 100644 index 000000000..fc3e8f1dd --- /dev/null +++ b/docs/tutorials/wiki2/src/installation/tutorial/models/meta.py @@ -0,0 +1,16 @@ +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.schema import MetaData + +# Recommended naming convention used by Alembic, as various different database +# providers will autogenerate vastly different names making migrations more +# difficult. See: http://alembic.readthedocs.org/en/latest/naming.html +NAMING_CONVENTION = { + "ix": 'ix_%(column_0_label)s', + "uq": "uq_%(table_name)s_%(column_0_name)s", + "ck": "ck_%(table_name)s_%(constraint_name)s", + "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", + "pk": "pk_%(table_name)s" +} + +metadata = MetaData(naming_convention=NAMING_CONVENTION) +Base = declarative_base(metadata=metadata) diff --git a/docs/tutorials/wiki2/src/installation/tutorial/models/mymodel.py b/docs/tutorials/wiki2/src/installation/tutorial/models/mymodel.py new file mode 100644 index 000000000..d65a01a42 --- /dev/null +++ b/docs/tutorials/wiki2/src/installation/tutorial/models/mymodel.py @@ -0,0 +1,18 @@ +from sqlalchemy import ( + Column, + Index, + Integer, + Text, +) + +from .meta import Base + + +class MyModel(Base): + __tablename__ = 'models' + id = Column(Integer, primary_key=True) + name = Column(Text) + value = Column(Integer) + + +Index('my_index', MyModel.name, unique=True, mysql_length=255) diff --git a/docs/tutorials/wiki2/src/installation/tutorial/routes.py b/docs/tutorials/wiki2/src/installation/tutorial/routes.py new file mode 100644 index 000000000..25504ad4d --- /dev/null +++ b/docs/tutorials/wiki2/src/installation/tutorial/routes.py @@ -0,0 +1,3 @@ +def includeme(config): + config.add_static_view('static', 'static', cache_max_age=3600) + config.add_route('home', '/') diff --git a/docs/tutorials/wiki2/src/installation/tutorial/scripts/__init__.py b/docs/tutorials/wiki2/src/installation/tutorial/scripts/__init__.py new file mode 100644 index 000000000..5bb534f79 --- /dev/null +++ b/docs/tutorials/wiki2/src/installation/tutorial/scripts/__init__.py @@ -0,0 +1 @@ +# package diff --git a/docs/tutorials/wiki2/src/installation/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/installation/tutorial/scripts/initializedb.py new file mode 100644 index 000000000..7307ecc5c --- /dev/null +++ b/docs/tutorials/wiki2/src/installation/tutorial/scripts/initializedb.py @@ -0,0 +1,45 @@ +import os +import sys +import transaction + +from pyramid.paster import ( + get_appsettings, + setup_logging, + ) + +from pyramid.scripts.common import parse_vars + +from ..models.meta import Base +from ..models import ( + get_engine, + get_session_factory, + get_tm_session, + ) +from ..models import MyModel + + +def usage(argv): + cmd = os.path.basename(argv[0]) + print('usage: %s [var=value]\n' + '(example: "%s development.ini")' % (cmd, cmd)) + sys.exit(1) + + +def main(argv=sys.argv): + if len(argv) < 2: + usage(argv) + config_uri = argv[1] + options = parse_vars(argv[2:]) + setup_logging(config_uri) + settings = get_appsettings(config_uri, options=options) + + engine = get_engine(settings) + Base.metadata.create_all(engine) + + session_factory = get_session_factory(engine) + + with transaction.manager: + dbsession = get_tm_session(session_factory, transaction.manager) + + model = MyModel(name='one', value=1) + dbsession.add(model) diff --git a/docs/tutorials/wiki2/src/installation/tutorial/static/pyramid-16x16.png b/docs/tutorials/wiki2/src/installation/tutorial/static/pyramid-16x16.png new file mode 100644 index 000000000..979203112 Binary files /dev/null and b/docs/tutorials/wiki2/src/installation/tutorial/static/pyramid-16x16.png differ diff --git a/docs/tutorials/wiki2/src/installation/tutorial/static/pyramid.png b/docs/tutorials/wiki2/src/installation/tutorial/static/pyramid.png new file mode 100644 index 000000000..4ab837be9 Binary files /dev/null and b/docs/tutorials/wiki2/src/installation/tutorial/static/pyramid.png differ diff --git a/docs/tutorials/wiki2/src/installation/tutorial/static/theme.css b/docs/tutorials/wiki2/src/installation/tutorial/static/theme.css new file mode 100644 index 000000000..0f4b1a4d4 --- /dev/null +++ b/docs/tutorials/wiki2/src/installation/tutorial/static/theme.css @@ -0,0 +1,154 @@ +@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700); +body { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; + color: #ffffff; + background: #bc2131; +} +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; +} +p { + font-weight: 300; +} +.font-normal { + font-weight: 400; +} +.font-semi-bold { + font-weight: 600; +} +.font-bold { + font-weight: 700; +} +.starter-template { + margin-top: 250px; +} +.starter-template .content { + margin-left: 10px; +} +.starter-template .content h1 { + margin-top: 10px; + font-size: 60px; +} +.starter-template .content h1 .smaller { + font-size: 40px; + color: #f2b7bd; +} +.starter-template .content .lead { + font-size: 25px; + color: #f2b7bd; +} +.starter-template .content .lead .font-normal { + color: #ffffff; +} +.starter-template .links { + float: right; + right: 0; + margin-top: 125px; +} +.starter-template .links ul { + display: block; + padding: 0; + margin: 0; +} +.starter-template .links ul li { + list-style: none; + display: inline; + margin: 0 10px; +} +.starter-template .links ul li:first-child { + margin-left: 0; +} +.starter-template .links ul li:last-child { + margin-right: 0; +} +.starter-template .links ul li.current-version { + color: #f2b7bd; + font-weight: 400; +} +.starter-template .links ul li a, a { + color: #f2b7bd; + text-decoration: underline; +} +.starter-template .links ul li a:hover, a:hover { + color: #ffffff; + text-decoration: underline; +} +.starter-template .links ul li .icon-muted { + color: #eb8b95; + margin-right: 5px; +} +.starter-template .links ul li:hover .icon-muted { + color: #ffffff; +} +.starter-template .copyright { + margin-top: 10px; + font-size: 0.9em; + color: #f2b7bd; + text-transform: lowercase; + float: right; + right: 0; +} +@media (max-width: 1199px) { + .starter-template .content h1 { + font-size: 45px; + } + .starter-template .content h1 .smaller { + font-size: 30px; + } + .starter-template .content .lead { + font-size: 20px; + } +} +@media (max-width: 991px) { + .starter-template { + margin-top: 0; + } + .starter-template .logo { + margin: 40px auto; + } + .starter-template .content { + margin-left: 0; + text-align: center; + } + .starter-template .content h1 { + margin-bottom: 20px; + } + .starter-template .links { + float: none; + text-align: center; + margin-top: 60px; + } + .starter-template .copyright { + float: none; + text-align: center; + } +} +@media (max-width: 767px) { + .starter-template .content h1 .smaller { + font-size: 25px; + display: block; + } + .starter-template .content .lead { + font-size: 16px; + } + .starter-template .links { + margin-top: 40px; + } + .starter-template .links ul li { + display: block; + margin: 0; + } + .starter-template .links ul li .icon-muted { + display: none; + } + .starter-template .copyright { + margin-top: 20px; + } +} diff --git a/docs/tutorials/wiki2/src/installation/tutorial/static/theme.min.css b/docs/tutorials/wiki2/src/installation/tutorial/static/theme.min.css new file mode 100644 index 000000000..0d25de5b6 --- /dev/null +++ b/docs/tutorials/wiki2/src/installation/tutorial/static/theme.min.css @@ -0,0 +1 @@ +@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);body{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300;color:#fff;background:#bc2131}h1,h2,h3,h4,h5,h6{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300}p{font-weight:300}.font-normal{font-weight:400}.font-semi-bold{font-weight:600}.font-bold{font-weight:700}.starter-template{margin-top:250px}.starter-template .content{margin-left:10px}.starter-template .content h1{margin-top:10px;font-size:60px}.starter-template .content h1 .smaller{font-size:40px;color:#f2b7bd}.starter-template .content .lead{font-size:25px;color:#f2b7bd}.starter-template .content .lead .font-normal{color:#fff}.starter-template .links{float:right;right:0;margin-top:125px}.starter-template .links ul{display:block;padding:0;margin:0}.starter-template .links ul li{list-style:none;display:inline;margin:0 10px}.starter-template .links ul li:first-child{margin-left:0}.starter-template .links ul li:last-child{margin-right:0}.starter-template .links ul li.current-version{color:#f2b7bd;font-weight:400}.starter-template .links ul li a,a{color:#f2b7bd;text-decoration:underline}.starter-template .links ul li a:hover,a:hover{color:#fff;text-decoration:underline}.starter-template .links ul li .icon-muted{color:#eb8b95;margin-right:5px}.starter-template .links ul li:hover .icon-muted{color:#fff}.starter-template .copyright{margin-top:10px;font-size:.9em;color:#f2b7bd;text-transform:lowercase;float:right;right:0}@media (max-width:1199px){.starter-template .content h1{font-size:45px}.starter-template .content h1 .smaller{font-size:30px}.starter-template .content .lead{font-size:20px}}@media (max-width:991px){.starter-template{margin-top:0}.starter-template .logo{margin:40px auto}.starter-template .content{margin-left:0;text-align:center}.starter-template .content h1{margin-bottom:20px}.starter-template .links{float:none;text-align:center;margin-top:60px}.starter-template .copyright{float:none;text-align:center}}@media (max-width:767px){.starter-template .content h1 .smaller{font-size:25px;display:block}.starter-template .content .lead{font-size:16px}.starter-template .links{margin-top:40px}.starter-template .links ul li{display:block;margin:0}.starter-template .links ul li .icon-muted{display:none}.starter-template .copyright{margin-top:20px}} diff --git a/docs/tutorials/wiki2/src/installation/tutorial/templates/404.jinja2 b/docs/tutorials/wiki2/src/installation/tutorial/templates/404.jinja2 new file mode 100644 index 000000000..1917f83c7 --- /dev/null +++ b/docs/tutorials/wiki2/src/installation/tutorial/templates/404.jinja2 @@ -0,0 +1,8 @@ +{% extends "layout.jinja2" %} + +{% block content %} +
+

Pyramid Alchemy scaffold

+

404 Page Not Found

+
+{% endblock content %} diff --git a/docs/tutorials/wiki2/src/installation/tutorial/templates/layout.jinja2 b/docs/tutorials/wiki2/src/installation/tutorial/templates/layout.jinja2 new file mode 100644 index 000000000..ab8c5ea3d --- /dev/null +++ b/docs/tutorials/wiki2/src/installation/tutorial/templates/layout.jinja2 @@ -0,0 +1,66 @@ + + + + + + + + + + + Alchemy Scaffold for The Pyramid Web Framework + + + + + + + + + + + + + +
+
+
+
+ +
+
+ {% block content %} +

No content

+ {% endblock content %} +
+
+
+ +
+
+ +
+
+
+ + + + + + + + diff --git a/docs/tutorials/wiki2/src/installation/tutorial/templates/mytemplate.jinja2 b/docs/tutorials/wiki2/src/installation/tutorial/templates/mytemplate.jinja2 new file mode 100644 index 000000000..6b49869c4 --- /dev/null +++ b/docs/tutorials/wiki2/src/installation/tutorial/templates/mytemplate.jinja2 @@ -0,0 +1,8 @@ +{% extends "layout.jinja2" %} + +{% block content %} +
+

Pyramid Alchemy scaffold

+

Welcome to {{project}}, an application generated by
the Pyramid Web Framework 1.7.

+
+{% endblock content %} diff --git a/docs/tutorials/wiki2/src/installation/tutorial/tests.py b/docs/tutorials/wiki2/src/installation/tutorial/tests.py new file mode 100644 index 000000000..99e95efd3 --- /dev/null +++ b/docs/tutorials/wiki2/src/installation/tutorial/tests.py @@ -0,0 +1,65 @@ +import unittest +import transaction + +from pyramid import testing + + +def dummy_request(dbsession): + return testing.DummyRequest(dbsession=dbsession) + + +class BaseTest(unittest.TestCase): + def setUp(self): + self.config = testing.setUp(settings={ + 'sqlalchemy.url': 'sqlite:///:memory:' + }) + self.config.include('.models') + settings = self.config.get_settings() + + from .models import ( + get_engine, + get_session_factory, + get_tm_session, + ) + + self.engine = get_engine(settings) + session_factory = get_session_factory(self.engine) + + self.session = get_tm_session(session_factory, transaction.manager) + + def init_database(self): + from .models.meta import Base + Base.metadata.create_all(self.engine) + + def tearDown(self): + from .models.meta import Base + + testing.tearDown() + transaction.abort() + Base.metadata.drop_all(self.engine) + + +class TestMyViewSuccessCondition(BaseTest): + + def setUp(self): + super(TestMyViewSuccessCondition, self).setUp() + self.init_database() + + from .models import MyModel + + model = MyModel(name='one', value=55) + self.session.add(model) + + def test_passing_view(self): + from .views.default import my_view + info = my_view(dummy_request(self.session)) + self.assertEqual(info['one'].name, 'one') + self.assertEqual(info['project'], 'tutorial') + + +class TestMyViewFailureCondition(BaseTest): + + def test_failing_view(self): + from .views.default import my_view + info = my_view(dummy_request(self.session)) + self.assertEqual(info.status_int, 500) diff --git a/docs/tutorials/wiki2/src/installation/tutorial/views/__init__.py b/docs/tutorials/wiki2/src/installation/tutorial/views/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs/tutorials/wiki2/src/installation/tutorial/views/default.py b/docs/tutorials/wiki2/src/installation/tutorial/views/default.py new file mode 100644 index 000000000..ad0c728d7 --- /dev/null +++ b/docs/tutorials/wiki2/src/installation/tutorial/views/default.py @@ -0,0 +1,33 @@ +from pyramid.response import Response +from pyramid.view import view_config + +from sqlalchemy.exc import DBAPIError + +from ..models import MyModel + + +@view_config(route_name='home', renderer='../templates/mytemplate.jinja2') +def my_view(request): + try: + query = request.dbsession.query(MyModel) + one = query.filter(MyModel.name == 'one').first() + except DBAPIError: + return Response(db_err_msg, content_type='text/plain', status=500) + return {'one': one, 'project': 'tutorial'} + + +db_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/installation/tutorial/views/notfound.py b/docs/tutorials/wiki2/src/installation/tutorial/views/notfound.py new file mode 100644 index 000000000..69d6e2804 --- /dev/null +++ b/docs/tutorials/wiki2/src/installation/tutorial/views/notfound.py @@ -0,0 +1,7 @@ +from pyramid.view import notfound_view_config + + +@notfound_view_config(renderer='../templates/404.jinja2') +def notfound_view(request): + request.response.status = 404 + return {} -- cgit v1.2.3 From d8be5a579541ebf48fdb0ee613b87e06222fa9ca Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sat, 9 Apr 2016 01:00:41 -0700 Subject: - use literalinclude for setup.py source - strip comments - update py.test output using recently updated scaffold from master branch - remove duplicate bullet points --- docs/tutorials/wiki2/installation.rst | 145 ++++++++++------------------------ 1 file changed, 42 insertions(+), 103 deletions(-) diff --git a/docs/tutorials/wiki2/installation.rst b/docs/tutorials/wiki2/installation.rst index bd52b8f7a..a592029cd 100644 --- a/docs/tutorials/wiki2/installation.rst +++ b/docs/tutorials/wiki2/installation.rst @@ -230,77 +230,22 @@ packages. Success executing this command will show a line like the following: Install testing requirements ---------------------------- -In order to run tests, we need to install the testing requirements, which means -we need to edit our ``setup.py``: +In order to run tests, we need to install the testing requirements. This is +done through our project's ``setup.py`` file, in the ``testing_extras`` and +``extras_requires`` stanzas, and by issuing the command below for your +operating system. -.. .. literalinclude:: src/installation/setup.py - :language: py +.. literalinclude:: src/installation/setup.py + :language: python :linenos: - :emphasize-lines: 23-30,50-52 + :lineno-start: 22 + :lines: 22-26 -.. code-block:: python +.. literalinclude:: src/installation/setup.py + :language: python :linenos: - :emphasize-lines: 22-26,46-48 - - import os - - from setuptools import setup, find_packages - - here = os.path.abspath(os.path.dirname(__file__)) - with open(os.path.join(here, 'README.txt')) as f: - README = f.read() - with open(os.path.join(here, 'CHANGES.txt')) as f: - CHANGES = f.read() - - requires = [ - 'pyramid', - 'pyramid_jinja2', - 'pyramid_debugtoolbar', - 'pyramid_tm', - 'SQLAlchemy', - 'transaction', - 'zope.sqlalchemy', - 'waitress', - ] - - testing_extras = [ - 'WebTest >= 1.3.1', # py3 compat - 'pytest', # includes virtualenv - 'pytest-cov', - ] - - setup(name='tutorial', - version='0.0', - description='tutorial', - long_description=README + '\n\n' + CHANGES, - classifiers=[ - "Programming Language :: Python", - "Framework :: Pyramid", - "Topic :: Internet :: WWW/HTTP", - "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", - ], - author='', - author_email='', - url='', - keywords='web wsgi bfg pylons pyramid', - packages=find_packages(), - include_package_data=True, - zip_safe=False, - extras_require={ - 'testing': testing_extras, - }, - install_requires=requires, - entry_points="""\ - [paste.app_factory] - main = tutorial:main - [console_scripts] - initialize_tutorial_db = tutorial.scripts.initializedb:main - """, - ) - -Only the emphasized lines need to be edited. - -Next install the testing requirements. + :lineno-start: 45 + :lines: 45-47 On UNIX ^^^^^^^ @@ -332,10 +277,6 @@ On UNIX $ $VENV/bin/py.test tutorial/tests.py -q -.. TODO remove comments - -.. py.test? See https://github.com/Pylons/pyramid/issues/2104#issuecomment-155852046 - On Windows ^^^^^^^^^^ @@ -343,8 +284,6 @@ On Windows c:\pyramidtut\tutorial> %VENV%\Scripts\py.test tutorial\tests.py -q -.. py.test? See https://github.com/Pylons/pyramid/issues/2104#issuecomment-155852046 - For a successful test run, you should see output that ends like this: .. code-block:: bash @@ -393,14 +332,19 @@ If successful, you will see output something like this: ------------------ coverage: platform Python 3.5.1 ------------------ Name Stmts Miss Cover Missing ---------------------------------------------------------------- - tutorial/__init__.py 13 9 31% 13-21 - tutorial/models.py 12 0 100% + tutorial/__init__.py 8 6 25% 7-12 + tutorial/models/__init__.py 22 0 100% + tutorial/models/meta.py 5 0 100% + tutorial/models/mymodel.py 8 0 100% + tutorial/routes.py 3 3 0% 1-3 tutorial/scripts/__init__.py 0 0 100% - tutorial/scripts/initializedb.py 24 24 0% 1-40 + tutorial/scripts/initializedb.py 26 26 0% 1-45 tutorial/tests.py 39 0 100% - tutorial/views.py 11 0 100% + tutorial/views/__init__.py 0 0 100% + tutorial/views/default.py 12 0 100% + tutorial/views/notfound.py 4 4 0% 1-7 ---------------------------------------------------------------- - TOTAL 99 33 67% + TOTAL 127 39 69% ===================== 2 passed in 0.57 seconds ====================== @@ -446,15 +390,17 @@ On Windows c:\pyramidtut\tutorial> %VENV%\Scripts\initialize_tutorial_db development.ini -The output to your console should be something like this:: +The output to your console should be something like this: - 2016-02-21 23:57:41,793 INFO [sqlalchemy.engine.base.Engine:1192][MainThread] SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1 - 2016-02-21 23:57:41,793 INFO [sqlalchemy.engine.base.Engine:1193][MainThread] () - 2016-02-21 23:57:41,794 INFO [sqlalchemy.engine.base.Engine:1192][MainThread] SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1 - 2016-02-21 23:57:41,794 INFO [sqlalchemy.engine.base.Engine:1193][MainThread] () - 2016-02-21 23:57:41,796 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] PRAGMA table_info("models") - 2016-02-21 23:57:41,796 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] () - 2016-02-21 23:57:41,798 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] +.. code-block:: bash + + 2016-04-09 00:53:37,801 INFO [sqlalchemy.engine.base.Engine:1192][MainThread] SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1 + 2016-04-09 00:53:37,801 INFO [sqlalchemy.engine.base.Engine:1193][MainThread] () + 2016-04-09 00:53:37,802 INFO [sqlalchemy.engine.base.Engine:1192][MainThread] SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1 + 2016-04-09 00:53:37,802 INFO [sqlalchemy.engine.base.Engine:1193][MainThread] () + 2016-04-09 00:53:37,802 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] PRAGMA table_info("models") + 2016-04-09 00:53:37,803 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] () + 2016-04-09 00:53:37,803 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] CREATE TABLE models ( id INTEGER NOT NULL, name TEXT, @@ -463,15 +409,15 @@ The output to your console should be something like this:: ) - 2016-02-21 23:57:41,798 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] () - 2016-02-21 23:57:41,798 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT - 2016-02-21 23:57:41,799 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] CREATE UNIQUE INDEX my_index ON models (name) - 2016-02-21 23:57:41,799 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] () - 2016-02-21 23:57:41,799 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT - 2016-02-21 23:57:41,801 INFO [sqlalchemy.engine.base.Engine:646][MainThread] BEGIN (implicit) - 2016-02-21 23:57:41,802 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] INSERT INTO models (name, value) VALUES (?, ?) - 2016-02-21 23:57:41,802 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ('one', 1) - 2016-02-21 23:57:41,821 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT + 2016-04-09 00:53:37,803 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] () + 2016-04-09 00:53:37,804 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT + 2016-04-09 00:53:37,805 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] CREATE UNIQUE INDEX my_index ON models (name) + 2016-04-09 00:53:37,805 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] () + 2016-04-09 00:53:37,806 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT + 2016-04-09 00:53:37,807 INFO [sqlalchemy.engine.base.Engine:646][MainThread] BEGIN (implicit) + 2016-04-09 00:53:37,808 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] INSERT INTO models (name, value) VALUES (?, ?) + 2016-04-09 00:53:37,808 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ('one', 1) + 2016-04-09 00:53:37,809 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT Success! You should now have a ``tutorial.sqlite`` file in your current working directory. This is an SQLite database with a single table defined in it @@ -534,16 +480,9 @@ assumptions: - You are willing to use :term:`URL dispatch` to map URLs to code. -- You want to use zope.sqlalchemy_, pyramid_tm_ and the transaction_ package +- You want to use zope.sqlalchemy_, pyramid_tm_, and the transaction_ packages to scope sessions to requests. -- You want to use pyramid_jinja2_ to render your templates. - Different templating engines can be used but we had to choose one to - make the tutorial. See :ref:`available_template_system_bindings` for some - options. -- You want to use zope.sqlalchemy_, pyramid_tm_ and the transaction_ package to - scope sessions to requests. - - You want to use pyramid_jinja2_ to render your templates. Different templating engines can be used, but we had to choose one to make this tutorial. See :ref:`available_template_system_bindings` for some options. -- cgit v1.2.3 From 458d513ba5ffd054323be1da9306ad0d37d0c10f Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sat, 9 Apr 2016 01:11:43 -0700 Subject: - add missing files --- docs/tutorials/wiki2/src/installation/MANIFEST.in | 2 + docs/tutorials/wiki2/src/installation/README.txt | 14 +++++ .../wiki2/src/installation/development.ini | 71 ++++++++++++++++++++++ .../wiki2/src/installation/production.ini | 60 ++++++++++++++++++ docs/tutorials/wiki2/src/installation/setup.py | 55 +++++++++++++++++ 5 files changed, 202 insertions(+) create mode 100644 docs/tutorials/wiki2/src/installation/MANIFEST.in create mode 100644 docs/tutorials/wiki2/src/installation/README.txt create mode 100644 docs/tutorials/wiki2/src/installation/development.ini create mode 100644 docs/tutorials/wiki2/src/installation/production.ini create mode 100644 docs/tutorials/wiki2/src/installation/setup.py diff --git a/docs/tutorials/wiki2/src/installation/MANIFEST.in b/docs/tutorials/wiki2/src/installation/MANIFEST.in new file mode 100644 index 000000000..42cd299b5 --- /dev/null +++ b/docs/tutorials/wiki2/src/installation/MANIFEST.in @@ -0,0 +1,2 @@ +include *.txt *.ini *.cfg *.rst +recursive-include tutorial *.ico *.png *.css *.gif *.jpg *.jinja2 *.pt *.txt *.mak *.mako *.js *.html *.xml diff --git a/docs/tutorials/wiki2/src/installation/README.txt b/docs/tutorials/wiki2/src/installation/README.txt new file mode 100644 index 000000000..68f430110 --- /dev/null +++ b/docs/tutorials/wiki2/src/installation/README.txt @@ -0,0 +1,14 @@ +tutorial README +================== + +Getting Started +--------------- + +- cd + +- $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/installation/development.ini b/docs/tutorials/wiki2/src/installation/development.ini new file mode 100644 index 000000000..22b733e10 --- /dev/null +++ b/docs/tutorials/wiki2/src/installation/development.ini @@ -0,0 +1,71 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html +### + +[app:main] +use = egg:tutorial + +pyramid.reload_templates = true +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en +pyramid.includes = + pyramid_debugtoolbar + pyramid_tm + +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 = 127.0.0.1 +port = 6543 + +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html +### + +[loggers] +keys = root, tutorial, sqlalchemy + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + +[logger_sqlalchemy] +level = INFO +handlers = +qualname = sqlalchemy.engine +# "level = INFO" logs SQL queries. +# "level = DEBUG" logs SQL queries and results. +# "level = WARN" logs neither. (Recommended for production systems.) + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/tutorials/wiki2/src/installation/production.ini b/docs/tutorials/wiki2/src/installation/production.ini new file mode 100644 index 000000000..d2ecfe22a --- /dev/null +++ b/docs/tutorials/wiki2/src/installation/production.ini @@ -0,0 +1,60 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html +### + +[app:main] +use = egg:tutorial + +pyramid.reload_templates = false +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en + +sqlalchemy.url = sqlite:///%(here)s/tutorial.sqlite + +[server:main] +use = egg:waitress#main +host = 0.0.0.0 +port = 6543 + +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html +### + +[loggers] +keys = root, tutorial, sqlalchemy + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console + +[logger_tutorial] +level = WARN +handlers = +qualname = tutorial + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine +# "level = INFO" logs SQL queries. +# "level = DEBUG" logs SQL queries and results. +# "level = WARN" logs neither. (Recommended for production systems.) + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/tutorials/wiki2/src/installation/setup.py b/docs/tutorials/wiki2/src/installation/setup.py new file mode 100644 index 000000000..c82c880c9 --- /dev/null +++ b/docs/tutorials/wiki2/src/installation/setup.py @@ -0,0 +1,55 @@ +import os + +from setuptools import setup, find_packages + +here = os.path.abspath(os.path.dirname(__file__)) +with open(os.path.join(here, 'README.txt')) as f: + README = f.read() +with open(os.path.join(here, 'CHANGES.txt')) as f: + CHANGES = f.read() + +requires = [ + 'pyramid', + 'pyramid_jinja2', + 'pyramid_debugtoolbar', + 'pyramid_tm', + 'SQLAlchemy', + 'transaction', + 'zope.sqlalchemy', + 'waitress', + ] + +testing_extras = [ + 'WebTest >= 1.3.1', # py3 compat + 'pytest', # includes virtualenv + 'pytest-cov', + ] + +setup(name='tutorial', + version='0.0', + description='tutorial', + long_description=README + '\n\n' + CHANGES, + classifiers=[ + "Programming Language :: Python", + "Framework :: Pyramid", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", + ], + author='', + author_email='', + url='', + keywords='web wsgi bfg pylons pyramid', + packages=find_packages(), + include_package_data=True, + zip_safe=False, + extras_require={ + 'testing': testing_extras, + }, + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + [console_scripts] + initialize_tutorial_db = tutorial.scripts.initializedb:main + """, + ) -- cgit v1.2.3 From b93b011f1a4904963f453c2583f4c0d3aa50ffe5 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sat, 9 Apr 2016 02:10:54 -0700 Subject: - update wiki2/src/basiclayout files --- .../wiki2/src/basiclayout/development.ini | 4 ++-- .../tutorials/wiki2/src/basiclayout/production.ini | 4 ++-- docs/tutorials/wiki2/src/basiclayout/setup.py | 23 ++++++++++++---------- .../basiclayout/tutorial/templates/layout.jinja2 | 2 +- .../tutorial/templates/mytemplate.jinja2 | 2 +- .../wiki2/src/basiclayout/tutorial/tests.py | 2 +- 6 files changed, 20 insertions(+), 17 deletions(-) diff --git a/docs/tutorials/wiki2/src/basiclayout/development.ini b/docs/tutorials/wiki2/src/basiclayout/development.ini index 99c4ff0fe..22b733e10 100644 --- a/docs/tutorials/wiki2/src/basiclayout/development.ini +++ b/docs/tutorials/wiki2/src/basiclayout/development.ini @@ -1,6 +1,6 @@ ### # app configuration -# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html ### [app:main] @@ -32,7 +32,7 @@ port = 6543 ### # logging configuration -# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html ### [loggers] diff --git a/docs/tutorials/wiki2/src/basiclayout/production.ini b/docs/tutorials/wiki2/src/basiclayout/production.ini index cb1db3211..d2ecfe22a 100644 --- a/docs/tutorials/wiki2/src/basiclayout/production.ini +++ b/docs/tutorials/wiki2/src/basiclayout/production.ini @@ -1,6 +1,6 @@ ### # app configuration -# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html ### [app:main] @@ -21,7 +21,7 @@ port = 6543 ### # logging configuration -# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +# http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html ### [loggers] diff --git a/docs/tutorials/wiki2/src/basiclayout/setup.py b/docs/tutorials/wiki2/src/basiclayout/setup.py index 7bc697730..c82c880c9 100644 --- a/docs/tutorials/wiki2/src/basiclayout/setup.py +++ b/docs/tutorials/wiki2/src/basiclayout/setup.py @@ -19,20 +19,22 @@ requires = [ 'waitress', ] -tests_require = [ - 'WebTest', -] +testing_extras = [ + 'WebTest >= 1.3.1', # py3 compat + 'pytest', # includes virtualenv + 'pytest-cov', + ] setup(name='tutorial', version='0.0', description='tutorial', long_description=README + '\n\n' + CHANGES, classifiers=[ - "Programming Language :: Python", - "Framework :: Pyramid", - "Topic :: Internet :: WWW/HTTP", - "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", - ], + "Programming Language :: Python", + "Framework :: Pyramid", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", + ], author='', author_email='', url='', @@ -40,8 +42,9 @@ setup(name='tutorial', packages=find_packages(), include_package_data=True, zip_safe=False, - test_suite='tutorial', - tests_require=tests_require, + extras_require={ + 'testing': testing_extras, + }, install_requires=requires, entry_points="""\ [paste.app_factory] diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/layout.jinja2 b/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/layout.jinja2 index ff624c65b..ab8c5ea3d 100644 --- a/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/layout.jinja2 +++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/layout.jinja2 @@ -40,7 +40,7 @@