diff options
Diffstat (limited to 'docs')
35 files changed, 661 insertions, 185 deletions
diff --git a/docs/api.rst b/docs/api.rst index d510c0d27..9e540b49b 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -10,10 +10,9 @@ documentation is organized alphabetically by module name. api/authorization api/authentication - api/chameleon_text - api/chameleon_zpt api/compat api/config + api/decorator api/events api/exceptions api/httpexceptions diff --git a/docs/api/chameleon_text.rst b/docs/api/chameleon_text.rst deleted file mode 100644 index 494f5b464..000000000 --- a/docs/api/chameleon_text.rst +++ /dev/null @@ -1,31 +0,0 @@ -.. _chameleon_text_module: - -:mod:`pyramid.chameleon_text` ----------------------------------- - -.. automodule:: pyramid.chameleon_text - - .. autofunction:: get_template - - .. autofunction:: render_template - - .. autofunction:: render_template_to_response - -These APIs will will work against template files which contain simple -``${Genshi}`` - style replacement markers. - -The API of :mod:`pyramid.chameleon_text` is identical to that of -:mod:`pyramid.chameleon_zpt`; only its import location is -different. If you need to import an API functions from this module as -well as the :mod:`pyramid.chameleon_zpt` module within the same -view file, use the ``as`` feature of the Python import statement, -e.g.: - -.. code-block:: python - :linenos: - - from pyramid.chameleon_zpt import render_template as zpt_render - from pyramid.chameleon_text import render_template as text_render - - - diff --git a/docs/api/chameleon_zpt.rst b/docs/api/chameleon_zpt.rst deleted file mode 100644 index df9a36a56..000000000 --- a/docs/api/chameleon_zpt.rst +++ /dev/null @@ -1,28 +0,0 @@ -.. _chameleon_zpt_module: - -:mod:`pyramid.chameleon_zpt` -------------------------------- - -.. automodule:: pyramid.chameleon_zpt - - .. autofunction:: get_template - - .. autofunction:: render_template - - .. autofunction:: render_template_to_response - -These APIs will work against files which supply template text which -matches the :term:`ZPT` specification. - -The API of :mod:`pyramid.chameleon_zpt` is identical to that of -:mod:`pyramid.chameleon_text`; only its import location is -different. If you need to import an API functions from this module as -well as the :mod:`pyramid.chameleon_text` module within the same -view file, use the ``as`` feature of the Python import statement, -e.g.: - -.. code-block:: python - :linenos: - - from pyramid.chameleon_zpt import render_template as zpt_render - from pyramid.chameleon_text import render_template as text_render diff --git a/docs/api/config.rst b/docs/api/config.rst index cd58e74d3..5d2bce23e 100644 --- a/docs/api/config.rst +++ b/docs/api/config.rst @@ -36,9 +36,11 @@ .. automethod:: set_authentication_policy .. automethod:: set_authorization_policy .. automethod:: set_default_permission + .. automethod:: add_permission - :methodcategory:`Setting Request Properties` + :methodcategory:`Extending the Request Object` + .. automethod:: add_request_method .. automethod:: set_request_property :methodcategory:`Using I18N` @@ -66,6 +68,8 @@ .. automethod:: add_response_adapter .. automethod:: add_traverser .. automethod:: add_tween + .. automethod:: add_route_predicate + .. automethod:: add_view_predicate .. automethod:: set_request_factory .. automethod:: set_root_factory .. automethod:: set_session_factory diff --git a/docs/api/decorator.rst b/docs/api/decorator.rst new file mode 100644 index 000000000..35d9131df --- /dev/null +++ b/docs/api/decorator.rst @@ -0,0 +1,9 @@ +.. _decorator_module: + +:mod:`pyramid.decorator` +-------------------------- + +.. automodule:: pyramid.decorator + +.. autofunction:: reify + diff --git a/docs/api/registry.rst b/docs/api/registry.rst index e62e2ba6f..1d5d52248 100644 --- a/docs/api/registry.rst +++ b/docs/api/registry.rst @@ -38,3 +38,17 @@ This class is new as of :app:`Pyramid` 1.3. +.. autoclass:: Deferred + + This class is new as of :app:`Pyramid` 1.4. + +.. autofunction:: undefer + + This function is new as of :app:`Pyramid` 1.4. + +.. autoclass:: predvalseq + + This class is new as of :app:`Pyramid` 1.4. + + + diff --git a/docs/api/settings.rst b/docs/api/settings.rst index 6b12c038c..cd802e138 100644 --- a/docs/api/settings.rst +++ b/docs/api/settings.rst @@ -5,8 +5,6 @@ .. automodule:: pyramid.settings - .. autofunction:: get_settings - .. autofunction:: asbool .. autofunction:: aslist diff --git a/docs/changes.rst b/docs/changes.rst index 6294123ed..fdeaf1e99 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -1,3 +1,5 @@ +.. _changelog: + :app:`Pyramid` Change History ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/glossary.rst b/docs/glossary.rst index 45a79326f..34cf1b078 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -994,3 +994,10 @@ Glossary Aka ``gunicorn``, a fast :term:`WSGI` server that runs on UNIX under Python 2.5+ (although at the time of this writing does not support Python 3). See http://gunicorn.org/ for detailed information. + + predicate factory + A callable which is used by a third party during the registration of a + route, view, or subscriber predicates to extend the configuration + system. See :ref:`registering_thirdparty_predicates` for more + information. + diff --git a/docs/index.rst b/docs/index.rst index 31c2fde6b..321fe1fed 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -13,15 +13,9 @@ Here is one of the simplest :app:`Pyramid` applications you can make: .. literalinclude:: narr/helloworld.py -When saved to ``helloworld.py``, the above application can be run via: - -.. code-block:: text - - $ easy_install pyramid - $ python helloworld.py - -When you visit ``http://localhost:8080/hello/world`` in a browser, you will -see the text ``Hello, world!``. +After you install :app:`Pyramid` and run this application, when you visit +``http://localhost:8080/hello/world`` in a browser, you will see the text +``Hello, world!`` See :ref:`firstapp_chapter` for a full explanation of how this application works. Read the :ref:`html_narrative_documentation` to understand how @@ -94,6 +88,7 @@ Narrative documentation in chapter form explaining how to use narr/advconfig narr/extconfig narr/scaffolding + narr/upgrading narr/threadlocals narr/zca diff --git a/docs/latexindex.rst b/docs/latexindex.rst index 99ec2f54d..604e6e7c6 100644 --- a/docs/latexindex.rst +++ b/docs/latexindex.rst @@ -85,8 +85,6 @@ API Reference api/authorization api/authentication - api/chameleon_text - api/chameleon_zpt api/config api/events api/exceptions diff --git a/docs/narr/advconfig.rst b/docs/narr/advconfig.rst index 2949dc808..ad22a82c9 100644 --- a/docs/narr/advconfig.rst +++ b/docs/narr/advconfig.rst @@ -294,6 +294,7 @@ These are the methods of the configurator which provide conflict detection: :meth:`~pyramid.config.Configurator.add_view`, :meth:`~pyramid.config.Configurator.add_route`, :meth:`~pyramid.config.Configurator.add_renderer`, +:meth:`~pyramid.config.Configurator.add_request_method`, :meth:`~pyramid.config.Configurator.set_request_factory`, :meth:`~pyramid.config.Configurator.set_session_factory`, :meth:`~pyramid.config.Configurator.set_request_property`, @@ -413,3 +414,10 @@ constraints: the routes they imply require relative ordering. Such ordering constraints are not absolved by two-phase configuration. Routes are still added in configuration execution order. +More Information +---------------- + +For more information, see the article entitled `"A Whirlwind Rour of Advanced +Configuration Tactics" +<http://docs.pylonsproject.org/projects/pyramid_cookbook/en/latest/configuration/whirlwind_tour.html>`_ +in the Pyramid Cookbook. diff --git a/docs/narr/commandline.rst b/docs/narr/commandline.rst index af53c1f78..3bdf8c5cd 100644 --- a/docs/narr/commandline.rst +++ b/docs/narr/commandline.rst @@ -349,7 +349,7 @@ setting) orderings using the ``ptweens`` command. Tween factories will show up represented by their standard Python dotted name in the ``ptweens`` output. -For example, here's the ``pwteens`` command run against a system +For example, here's the ``ptweens`` command run against a system configured without any explicit tweens: .. code-block:: text @@ -367,7 +367,7 @@ configured without any explicit tweens: 1 pyramid.tweens.excview_tween_factory excview - - MAIN -Here's the ``pwteens`` command run against a system configured *with* +Here's the ``ptweens`` command run against a system configured *with* explicit tweens defined in its ``development.ini`` file: .. code-block:: text diff --git a/docs/narr/firstapp.rst b/docs/narr/firstapp.rst index 1ca188d7e..ccaa6e9e2 100644 --- a/docs/narr/firstapp.rst +++ b/docs/narr/firstapp.rst @@ -8,7 +8,8 @@ Creating Your First :app:`Pyramid` Application In this chapter, we will walk through the creation of a tiny :app:`Pyramid` application. After we're finished creating the application, we'll explain in -more detail how it works. +more detail how it works. It assumes you already have :app:`Pyramid` installed. +If you do not, head over to the :ref:`installing_chapter` section. .. _helloworld_imperative: @@ -126,7 +127,7 @@ defined imports and function definitions, placed within the confines of an .. literalinclude:: helloworld.py :linenos: - :lines: 8-13 + :lines: 9-15 Let's break this down piece-by-piece. @@ -135,7 +136,7 @@ Configurator Construction .. literalinclude:: helloworld.py :linenos: - :lines: 8-9 + :lines: 9-10 The ``if __name__ == '__main__':`` line in the code sample above represents a Python idiom: the code inside this if clause is not invoked unless the script @@ -168,7 +169,7 @@ Adding Configuration .. ignore-next-block .. literalinclude:: helloworld.py :linenos: - :lines: 10-11 + :lines: 11-12 First line above calls the :meth:`pyramid.config.Configurator.add_route` method, which registers a :term:`route` to match any URL path that begins @@ -188,7 +189,7 @@ WSGI Application Creation .. ignore-next-block .. literalinclude:: helloworld.py :linenos: - :lines: 12 + :lines: 13 After configuring views and ending configuration, the script creates a WSGI *application* via the :meth:`pyramid.config.Configurator.make_wsgi_app` @@ -217,7 +218,7 @@ WSGI Application Serving .. ignore-next-block .. literalinclude:: helloworld.py :linenos: - :lines: 13 + :lines: 14-15 Finally, we actually serve the application to requestors by starting up a WSGI server. We happen to use the :mod:`wsgiref` ``make_server`` server diff --git a/docs/narr/helloworld.py b/docs/narr/helloworld.py index 7c26c8cdc..c01329af9 100644 --- a/docs/narr/helloworld.py +++ b/docs/narr/helloworld.py @@ -2,14 +2,15 @@ from wsgiref.simple_server import make_server from pyramid.config import Configurator from pyramid.response import Response + def hello_world(request): - return Response('Hello %(name)s!' % request.matchdict) + return Response('Hello %(name)s!' % request.matchdict) if __name__ == '__main__': - config = Configurator() - config.add_route('hello', '/hello/{name}') - config.add_view(hello_world, route_name='hello') - app = config.make_wsgi_app() - server = make_server('0.0.0.0', 8080, app) - server.serve_forever() + config = Configurator() + config.add_route('hello', '/hello/{name}') + config.add_view(hello_world, route_name='hello') + app = config.make_wsgi_app() + server = make_server('0.0.0.0', 8080, app) + server.serve_forever() diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index a2143b3c5..96fa77a07 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -289,6 +289,36 @@ keys added to the renderer globals dictionary by all :class:`pyramid.events.BeforeRender` subscribers and renderer globals factories must be unique. +The dictionary returned from the view is accessible through the +:attr:`rendering_val` attribute of a :class:`~pyramid.events.BeforeRender` +event. + +Suppose you return ``{'mykey': 'somevalue', 'mykey2': 'somevalue2'}`` from +your view callable, like so: + +.. code-block:: python + :linenos: + + from pyramid.view import view_config + + @view_config(renderer='some_renderer') + def myview(request): + return {'mykey': 'somevalue', 'mykey2': 'somevalue2'} + +:attr:`rendering_val` can be used to access these values from the +:class:`~pyramid.events.BeforeRender` object: + +.. code-block:: python + :linenos: + + from pyramid.events import subscriber + from pyramid.events import BeforeRender + + @subscriber(BeforeRender) + def read_return(event): + # {'mykey': 'somevalue'} is returned from the view + print(event.rendering_val['mykey']) + See the API documentation for the :class:`~pyramid.events.BeforeRender` event interface at :class:`pyramid.interfaces.IBeforeRender`. @@ -1202,3 +1232,203 @@ Displaying Tween Ordering The ``ptweens`` command-line utility can be used to report the current implict and explicit tween chains used by an application. See :ref:`displaying_tweens`. + +.. _registering_thirdparty_predicates: + +Adding A Third Party View, Route, or Subscriber Predicate +--------------------------------------------------------- + +.. note:: + + Third-party view, route, and subscriber predicates are a feature new as of + Pyramid 1.4. + +.. _view_and_route_predicates: + +View and Route Predicates +~~~~~~~~~~~~~~~~~~~~~~~~~ + +View and route predicates used during configuration allow you to narrow the +set of circumstances under which a view or route will match. For example, +the ``request_method`` view predicate can be used to ensure a view callable +is only invoked when the request's method is ``POST``: + +.. code-block:: python + + @view_config(request_method='POST') + def someview(request): + ... + +Likewise, a similar predicate can be used as a *route* predicate: + +.. code-block:: python + + config.add_route('name', '/foo', request_method='POST') + +Many other built-in predicates exists (``request_param``, and others). You +can add third-party predicates to the list of available predicates by using +one of :meth:`pyramid.config.Configurator.add_view_predicate` or +:meth:`pyramid.config.Configurator.add_route_predicate`. The former adds a +view predicate, the latter a route predicate. + +When using one of those APIs, you pass a *name* and a *factory* to add a +predicate during Pyramid's configuration stage. For example: + +.. code-block:: python + + config.add_view_predicate('content_type', ContentTypePredicate) + +The above example adds a new predicate named ``content_type`` to the list of +available predicates for views. This will allow the following view +configuration statement to work: + +.. code-block:: python + :linenos: + + @view_config(content_type='File') + def aview(request): ... + +The first argument to :meth:`pyramid.config.Configurator.add_view_predicate`, +the name, is a string representing the name that is expected to be passed to +``view_config`` (or its imperative analogue ``add_view``). + +The second argument is a view or route predicate factory. A view or route +predicate factory is most often a class with a constructor (``__init__``), a +``text`` method, a ``phash`` method and a ``__call__`` method. For example: + +.. code-block:: python + :linenos: + + class ContentTypePredicate(object): + def __init__(self, val, config): + self.val = val + + def text(self): + return 'content_type = %s' % (self.val,) + + phash = text + + def __call__(self, context, request): + return getattr(context, 'content_type', None) == self.val + +The constructor of a predicate factory takes two arguments: ``val`` and +``config``. The ``val`` argument will be the argument passed to +``view_config`` (or ``add_view``). In the example above, it will be the +string ``File``. The second arg, ``config`` will be the Configurator +instance at the time of configuration. + +The ``text`` method must return a string. It should be useful to describe +the behavior of the predicate in error messages. + +The ``phash`` method must return a string or a sequence of strings. It's +most often the same as ``text``, as long as ``text`` uniquely describes the +predicate's name and the value passed to the constructor. If ``text`` is +more general, or doesn't describe things that way, ``phash`` should return a +string with the name and the value serialized. The result of ``phash`` is +not seen in output anywhere, it just informs the uniqueness constraints for +view configuration. + +The ``__call__`` method of a predicate factory must accept a resource +(``context``) and a request, and must return ``True`` or ``False``. It is +the "meat" of the predicate. + +You can use the same predicate factory as both a view predicate and as a +route predicate, but you'll need to call ``add_view_predicate`` and +``add_route_predicate`` separately with the same factory. + +.. _subscriber_predicates: + +Subscriber Predicates +~~~~~~~~~~~~~~~~~~~~~ + +Subscriber predicates work almost exactly like view and route predicates. +They narrow the set of circumstances in which a subscriber will be called. +There are several minor differences between a subscriber predicate and a +view/route predicate: + +- There are no default subscriber predicates. You must register one to use + one. + +- The ``__call__`` method of a subscriber predicate accepts a single + ``event`` object instead of a ``context`` and a ``request``. + +- Not every subscriber predicate can be used with every event type. Some + subscriber predicates will assume a certain event type. + +Here's an example of a subscriber predicate that can be used in conjunction +with a subscriber that subscribes to the :class:`pyramid.events.NewReqest` +event type. + +.. code-block:: python + :linenos: + + class RequestPathStartsWith(object): + def __init__(self, val, config): + self.val = val + + def text(self): + return 'path_startswith = %s' % (self.val,) + + phash = text + + def __call__(self, event): + return event.request.path.startswith(self.val) + +Once you've created a subscriber predicate, it may registered via +:meth:`pyramid.config.Configurator.add_subscriber_predicate`. For example: + +.. code-block:: python + + config.add_subscriber_predicate( + 'request_path_startswith', RequestPathStartsWith) + +Once a subscriber predicate is registered, you can use it in a call to +:meth:`pyramid.config.Configurator.add_subscriber` or to +:class:`pyramid.events.subscriber`. Here's an example of using the +previously registered ``request_path_startswith`` predicate in a call to +:meth:`~pyramid.config.Configurator.add_subscriber`: + +.. code-block:: python + :linenos: + + # define a subscriber in your code + + def yosubscriber(event): + event.request.yo = 'YO!' + + # and at configuration time + + config.add_subscriber(yosubscriber, NewRequest, + request_path_startswith='/add_yo') + +Here's the same subscriber/predicate/event-type combination used via +:class:`~pyramid.events.subscriber`. + +.. code-block:: python + :linenos: + + from pyramid.events import subscriber + + @subscriber(NewRequest, request_path_startswith='/add_yo') + def yosubscriber(event): + event.request.yo = 'YO!' + +In either of the above configurations, the ``yosubscriber`` callable will +only be called if the request path starts with ``/add_yo``. Otherwise the +event subscriber will not be called. + +Note that the ``request_path_startswith`` subscriber you defined can be used +with events that have a ``request`` attribute, but not ones that do not. So, +for example, the predicate can be used with subscribers registered for +:class:`pyramid.events.NewRequest` and :class:`pyramid.events.ContextFound` +events, but it cannot be used with subscribers registered for +:class:`pyramid.events.ApplicationCreated` because the latter type of event +has no ``request`` attribute. The point being: unlike route and view +predicates, not every type of subscriber predicate will necessarily be +applicable for use in every subscriber registration. It is not the +responsibility of the predicate author to make every predicate make sense for +every event type; it is the responsibility of the predicate consumer to use +predicates that make sense for a particular event type registration. + + + diff --git a/docs/narr/introduction.rst b/docs/narr/introduction.rst index b5fa6a9f7..7c0f9223f 100644 --- a/docs/narr/introduction.rst +++ b/docs/narr/introduction.rst @@ -803,7 +803,7 @@ within a function called when another user uses the See also :ref:`add_directive`. Programmatic Introspection --------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~~~ If you're building a large system that other users may plug code into, it's useful to be able to get an enumeration of what code they plugged in *at @@ -831,7 +831,7 @@ callable: See also :ref:`using_introspection`. Python 3 Compatibility ----------------------- +~~~~~~~~~~~~~~~~~~~~~~ Pyramid and most of its add-ons are Python 3 compatible. If you develop a Pyramid application today, you won't need to worry that five years from now diff --git a/docs/narr/renderers.rst b/docs/narr/renderers.rst index 57b5bc65b..63287e2cd 100644 --- a/docs/narr/renderers.rst +++ b/docs/narr/renderers.rst @@ -306,7 +306,9 @@ See :class:`pyramid.renderers.JSON` and JSONP Renderer ~~~~~~~~~~~~~~ -.. note:: This feature is new in Pyramid 1.1. +.. note:: + + This feature is new in Pyramid 1.1. :class:`pyramid.renderers.JSONP` is a `JSONP <http://en.wikipedia.org/wiki/JSONP>`_ renderer factory helper which diff --git a/docs/narr/sessions.rst b/docs/narr/sessions.rst index 6ff9e3dea..1aa1b6341 100644 --- a/docs/narr/sessions.rst +++ b/docs/narr/sessions.rst @@ -151,13 +151,12 @@ Using Alternate Session Factories --------------------------------- At the time of this writing, exactly one alternate session factory -implementation exists, named ``pyramid_beaker``. This is a session -factory that uses the `Beaker <http://beaker.groovie.org/>`_ library -as a backend. Beaker has support for file-based sessions, database -based sessions, and encrypted cookie-based sessions. See -`http://github.com/Pylons/pyramid_beaker -<http://github.com/Pylons/pyramid_beaker>`_ for more information about -``pyramid_beaker``. +implementation exists, named ``pyramid_beaker``. This is a session factory +that uses the `Beaker <http://beaker.groovie.org/>`_ library as a backend. +Beaker has support for file-based sessions, database based sessions, and +encrypted cookie-based sessions. See `the pyramid_beaker documentation +<http://docs.pylonsproject.org/projects/pyramid_beaker/en/latest/>`_ for more +information about ``pyramid_beaker``. .. index:: single: session factory (custom) diff --git a/docs/narr/templates.rst b/docs/narr/templates.rst index 9db0b1c4d..ba72ebfbf 100644 --- a/docs/narr/templates.rst +++ b/docs/narr/templates.rst @@ -46,20 +46,6 @@ within the body of a view callable like so: {'foo':1, 'bar':2}, request=request) -.. warning:: - - Earlier iterations of this documentation - (pre-version-1.3) encouraged the application developer to use - ZPT-specific APIs such as - :func:`pyramid.chameleon_zpt.render_template_to_response` and - :func:`pyramid.chameleon_zpt.render_template` to render templates - directly. This style of rendering still works, but at least for - purposes of this documentation, those functions are deprecated. - Application developers are encouraged instead to use the functions - available in the :mod:`pyramid.renderers` module to perform - rendering tasks. This set of functions works to render templates - for all renderer extensions registered with :app:`Pyramid`. - The ``sample_view`` :term:`view callable` function above returns a :term:`response` object which contains the body of the ``templates/foo.pt`` template. In this case, the ``templates`` @@ -79,12 +65,12 @@ prefix on Windows. .. warning:: Only :term:`Chameleon` templates support defining a renderer for a - template relative to the location of the module where the view - callable is defined. Mako templates, and other templating system - bindings work differently. In particular, Mako templates use a - "lookup path" as defined by the ``mako.directories`` configuration - file instead of treating relative paths as relative to the current - view module. See :ref:`mako_templates`. + template relative to the location of the module where the view callable is + defined. Mako templates, and other templating system bindings work + differently. In particular, Mako templates use a "lookup path" as defined + by the ``mako.directories`` configuration file instead of treating + relative paths as relative to the current view module. See + :ref:`mako_templates`. The path can alternately be a :term:`asset specification` in the form ``some.dotted.package_name:relative/path``. This makes it possible to @@ -534,6 +520,33 @@ And ``templates/mytemplate.pt`` might look like so: </span> </html> + +Using A Chameleon Macro Name Within a Renderer Name +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Sommetime you'd like to render a macro inside of a Chameleon ZPT template +instead of the full Chameleon ZPT template. To render the content of a +``define-macro`` field inside a Chameleon ZPT template, given a Chameleon +template file named ``foo.pt`` and a macro named ``bar`` defined within it +(e.g. ``<div metal:define-macro="bar">...</div>``), you can configure the +template as a :term:`renderer` like so: + +.. code-block:: python + :linenos: + + from pyramid.view import view_config + + @view_config(renderer='foo#bar.pt') + def my_view(request): + return {'project':'my project'} + +The above will render only the ``bar`` macro defined within the ``foo.pt`` +template instead of the entire template. + +.. note:: + + This feature is new in Pyramid 1.4. + .. index:: single: Chameleon text templates @@ -573,10 +586,6 @@ When the template is rendered, it will show: Hello, world! -If you'd rather use templates directly within a view callable (without -the indirection of using a renderer), see :ref:`chameleon_text_module` -for the API description. - See also :ref:`built_in_renderers` for more general information about renderers, including Chameleon text renderers. @@ -714,6 +723,30 @@ This template doesn't use any advanced features of Mako, only the :term:`renderer globals`. See the `the Mako documentation <http://www.makotemplates.org/>`_ to use more advanced features. +Using A Mako def name Within a Renderer Name +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Sommetime you'd like to render a ``def`` inside of a Mako template instead of +the full Mako template. To render a def inside a Mako template, given a +:term:`Mako` template file named ``foo.mak`` and a def named ``bar``, you can +configure the template as a :term:`renderer` like so: + +.. code-block:: python + :linenos: + + from pyramid.view import view_config + + @view_config(renderer='foo#bar.mak') + def my_view(request): + return {'project':'my project'} + +The above will render the ``bar`` def from within the ``foo.mak`` template +instead of the entire template. + +.. note:: + + This feature is new in Pyramid 1.4. + .. index:: single: automatic reloading of templates single: template automatic reload diff --git a/docs/narr/testing.rst b/docs/narr/testing.rst index 89bc1a089..20017064b 100644 --- a/docs/narr/testing.rst +++ b/docs/narr/testing.rst @@ -52,7 +52,7 @@ The suggested mechanism for unit and integration testing of a :app:`Pyramid` application is the Python :mod:`unittest` module. Although this module is named :mod:`unittest`, it is actually capable of driving both unit and integration tests. A good :mod:`unittest` tutorial is available within `Dive -Into Python <http://diveintopython.nfshost.com/unit_testing/index.html>`_ by Mark +Into Python <http://www.diveintopython.net/unit_testing/index.html>`_ by Mark Pilgrim. :app:`Pyramid` provides a number of facilities that make unit, integration, diff --git a/docs/narr/upgrading.rst b/docs/narr/upgrading.rst new file mode 100644 index 000000000..92f125c0a --- /dev/null +++ b/docs/narr/upgrading.rst @@ -0,0 +1,232 @@ +Upgrading Pyramid +================= + +When a new version of Pyramid is released, it will sometimes deprecate a +feature or remove a feature that was deprecated in an older release. When +features are removed from Pyramid, applications that depend on those features +will begin to break. This chapter explains how to ensure your Pyramid +applications keep working when you upgrade the Pyramid version you're using. + +.. sidebar:: About Release Numbering + + Conventionally, application version numbering in Python is described as + ``major.minor.micro``. If your Pyramid version is "1.2.3", it means + you're running a version of Pyramid with the major version "1", the minor + version "2" and the micro version "3". A "major" release is one that + increments the first-dot number; 2.X.X might follow 1.X.X. A "minor" + release is one that increments the second-dot number; 1.3.X might follow + 1.2.X. A "micro" release is one that increments the third-dot number; + 1.2.3 might follow 1.2.2. In general, micro releases are "bugfix-only", + and contain no new features, minor releases contain new features but are + largely backwards compatible with older versions, and a major release + indicates a large set of backwards incompatibilities. + +The Pyramid core team is conservative when it comes to removing features. We +don't remove features unnecessarily, but we're human, and we make mistakes +which cause some features to be evolutionary dead ends. Though we are +willing to support dead-end features for some amount of time, some eventually +have to be removed when the cost of supporting them outweighs the benefit of +keeping them around, because each feature in Pyramid represents a certain +documentation and maintenance burden. + +Deprecation and Removal Policy +------------------------------ + +When a feature is scheduled for removal from Pyramid or any of its official +add-ons, the core development team takes these steps: + +- Using the feature will begin to generate a `DeprecationWarning`, indicating + the version in which the feature became deprecated. + +- A note is added to the documentation indicating that the feature is + deprecated. + +- A note is added to the :ref:`changelog` about the deprecation. + +When a deprecated feature is eventually removed: + +- The feature is removed. + +- A note is added to the :ref:`changelog` about the removal. + +Features are never removed in *micro* releases. They are only removed in +minor and major releases. Deprecated features are kept around for at least +*three* minor releases from the time the feature became deprecated. +Therefore, if a feature is added in Pyramid 1.0, but it's deprecated in +Pyramid 1.1, it will be kept around through all 1.1.X releases, all 1.2.X +releases and all 1.3.X releases. It will finally be removed in the first +1.4.X release. + +Sometimes features are "docs-deprecated" instead of formally deprecated. +This means that the feature will be kept around indefinitely, but it will be +removed from the documentation or a note will be added to the documentation +telling folks to use some other newer feature. This happens when the cost of +keeping an old feature around is very minimal and the support and +documentation burden is very low. For example, we might rename a function +that is an API without changing the arguments it accepts. In this case, +we'll often rename the function, and change the docs to point at the new +function name, but leave around a backwards compatibility alias to the old +function name so older code doesn't break. + +"Docs deprecated" features tend to work "forever", meaning that they won't be +removed, and they'll never generate a deprecation warning. However, such +changes are noted in the :ref:`changelog`, so it's possible to know that you +should change older spellings to newer ones to ensure that people reading +your code can find the APIs you're using in the Pyramid docs. + +Consulting the Change History +----------------------------- + +Your first line of defense against application failures caused by upgrading +to a newer Pyramid release is always to read the :ref:`changelog`. to find +the deprecations and removals for each release between the release you're +currently running and the one you wish to upgrade to. The change history +notes every deprecation within a ``Deprecation`` section and every removal +within a ``Backwards Incompatibilies`` section for each release. + +The change history often contains instructions for changing your code to +avoid deprecation warnings and how to change docs-deprecated spellings to +newer ones. You can follow along with each deprecation explanation in the +change history, simply doing a grep or other code search to your application, +using the change log examples to remediate each potential problem. + +.. _testing_under_new_release: + +Testing Your Application Under a New Pyramid Release +---------------------------------------------------- + +Once you've upgraded your application to a new Pyramid release and you've +remediated as much as possible by using the change history notes, you'll want +to run your application's tests (see :ref:`running_tests`) in such a way that +you can see DeprecationWarnings printed to the console when the tests run. + +.. code-block:: bash + + $ python -Wd setup.py test -q + +The ``-Wd`` argument is an argument that tells Python to print deprecation +warnings to the console. Note that the ``-Wd`` flag is only required for +Python 2.7 and better: Python versions 2.6 and older print deprecation +warnings to the console by default. See `the Python -W flag documentation +<http://docs.python.org/using/cmdline.html#cmdoption-W>`_ for more +information. + +As your tests run, deprecation warnings will be printed to the console +explaining the deprecation and providing instructions about how to prevent +the deprecation warning from being issued. For example: + +.. code-block:: text + + $ python -Wd setup.py test -q + # .. elided ... + running build_ext + /home/chrism/projects/pyramid/env27/myproj/myproj/views.py:3: + DeprecationWarning: static: The "pyramid.view.static" class is deprecated + as of Pyramid 1.1; use the "pyramid.static.static_view" class instead with + the "use_subpath" argument set to True. + from pyramid.view import static + . + ---------------------------------------------------------------------- + Ran 1 test in 0.014s + + OK + +In the above case, it's line #3 in the ``myproj.views`` module (``from +pyramid.view import static``) that is causing the problem: + +.. code-block:: python + :linenos: + + from pyramid.view import view_config + + from pyramid.view import static + myview = static('static', 'static') + +The deprecation warning tells me how to fix it, so I can change the code to +do things the newer way: + +.. code-block:: python + :linenos: + + from pyramid.view. import view_config + + from pyramid.static import static_view + myview = static_view('static', 'static', use_subpath=True) + +When I run the tests again, the deprecation warning is no longer printed to +my console: + +.. code-block:: text + + $ python -Wd setup.py test -q + # .. elided ... + running build_ext + . + ---------------------------------------------------------------------- + Ran 1 test in 0.014s + + OK + + +My Application Doesn't Have Any Tests or Has Few Tests +------------------------------------------------------ + +If your application has no tests, or has only moderate test coverage, running +tests won't tell you very much, because the Pyramid codepaths that generate +deprecation warnings won't be executed. + +In this circumstance, you can start your application interactively under a +server run with the ``PYTHONWARNINGS`` environment variable set to +``default``. On UNIX, you can do that via: + +.. code-block:: bash + + $ PYTHONWARNINGS=default bin/pserve development.ini + +On Windows, you need to issue two commands: + +.. code-block:: bash + + C:\> set PYTHONWARNINGS=default + C:\> Scripts/pserve.exe development.ini + +At this point, it's ensured that deprecation warnings will be printed to the +console whenever a codepath is hit that generates one. You can then click +around in your application interactively to try to generate them, and +remediate as explained in :ref:`testing_under_new_release`. + +See `the PYTHONWARNINGS environment variable documentation +<http://docs.python.org/using/cmdline.html#envvar-PYTHONWARNINGS>`_ or `the +Python -W flag documentation +<http://docs.python.org/using/cmdline.html#cmdoption-W>`_ for more +information. + +Upgrading to the Very Latest Pyramid Release +-------------------------------------------- + +When you upgrade your application to the very most recent Pyramid release, +it's advisable to upgrade step-wise through each most recent minor release, +beginning with the one that you know your application currently runs under, +and ending on the most recent release. For example, if your application is +running in production on Pyramid 1.2.1, and the most recent Pyramid 1.3 +release is Pyramid 1.3.3, and the most recent Pyramid release is 1.4.4, it's +advisable to do this: + +- Upgrade your environment to the most recent 1.2 release. For example, the + most recent 1.2 release might be 1.2.3, so upgrade to it. Then run your + application's tests under 1.2.3 as described in + :ref:`testing_under_new_release`. Note any deprecation warnings and + remediate. + +- Upgrade to the most recent 1.3 release, 1.3.3. Run your application's + tests, note any deprecation warnings and remediate. + +- Upgrade to 1.4.4. Run your application's tests, note any deprecation + warnings and remediate. + +If you skip testing your application under each minor release (for example if +you upgrade directly from 1.2.1 to 1.4.4), you might miss a deprecation +warning and waste more time trying to figure out an error caused by a feature +removal than it would take to upgrade stepwise through each minor release. + + diff --git a/docs/narr/views.rst b/docs/narr/views.rst index f6ee9a8d5..9e41464a6 100644 --- a/docs/narr/views.rst +++ b/docs/narr/views.rst @@ -177,7 +177,7 @@ HTTP Exceptions ~~~~~~~~~~~~~~~ All classes documented in the :mod:`pyramid.httpexceptions` module documented -as inheriting from the :class:`pryamid.httpexceptions.HTTPException` are +as inheriting from the :class:`pyramid.httpexceptions.HTTPException` are :term:`http exception` objects. Instances of an HTTP exception object may either be *returned* or *raised* from within view code. In either case (return or raise) the instance will be used as as the view's response. diff --git a/docs/tutorials/modwsgi/index.rst b/docs/tutorials/modwsgi/index.rst index e070f8eda..a22f12610 100644 --- a/docs/tutorials/modwsgi/index.rst +++ b/docs/tutorials/modwsgi/index.rst @@ -7,12 +7,11 @@ Running a :app:`Pyramid` Application under ``mod_wsgi`` It allows :term:`WSGI` programs to be served using the Apache web server. -This guide will outline broad steps that can be used to get a -:app:`Pyramid` application running under Apache via ``mod_wsgi``. -This particular tutorial was developed under Apple's Mac OS X platform -(Snow Leopard, on a 32-bit Mac), but the instructions should be -largely the same for all systems, delta specific path information for -commands and files. +This guide will outline broad steps that can be used to get a :app:`Pyramid` +application running under Apache via ``mod_wsgi``. This particular tutorial +was developed under Apple's Mac OS X platform (Snow Leopard, on a 32-bit +Mac), but the instructions should be largely the same for all systems, delta +specific path information for commands and files. .. note:: Unfortunately these instructions almost certainly won't work for deploying a :app:`Pyramid` application on a Windows system using @@ -73,9 +72,10 @@ commands and files. .. code-block:: python - from pyramid.paster import get_app - application = get_app( - '/Users/chrism/modwsgi/env/myapp/production.ini', 'main') + from pyramid.paster import get_app, setup_logging + ini_path = '/Users/chrism/modwsgi/env/myapp/production.ini' + setup_logging(ini_path) + application = get_app(ini_path, 'main') The first argument to ``get_app`` is the project configuration file name. It's best to use the ``production.ini`` file provided by your @@ -85,12 +85,15 @@ commands and files. ``application`` is important: mod_wsgi requires finding such an assignment when it opens the file. -#. Make the ``pyramid.wsgi`` script executable. + The call to ``setup_logging`` initializes the standard library's + `logging` module to allow logging within your application. + See :ref:`logging_config`. - .. code-block:: text - - $ cd ~/modwsgi/env - $ chmod 755 pyramid.wsgi + There is no need to make the ``pyramid.wsgi`` script executable. + However, you'll need to make sure that *two* users have access to change + into the ``~/modwsgi/env`` directory: your current user (mine is + ``chrism`` and the user that Apache will run as often named ``apache`` or + ``httpd``). Make sure both of these users can "cd" into that directory. #. Edit your Apache configuration and add some stuff. I happened to create a file named ``/etc/apache2/other/modwsgi.conf`` on my own diff --git a/docs/tutorials/wiki/definingviews.rst b/docs/tutorials/wiki/definingviews.rst index 28cecb787..529603546 100644 --- a/docs/tutorials/wiki/definingviews.rst +++ b/docs/tutorials/wiki/definingviews.rst @@ -251,7 +251,7 @@ wiki page. It includes: - A ``div`` element that is replaced with the ``content`` value provided by the view (rows 45-47). ``content`` contains HTML, so the ``structure`` keyword is used - to prevent escaping it (i.e. changing ">" to >, etc.) + to prevent escaping it (i.e. changing ">" to ">", etc.) - A link that points at the "edit" URL which invokes the ``edit_page`` view for the page being viewed (rows 49-51). diff --git a/docs/tutorials/wiki/src/authorization/tutorial/views.py b/docs/tutorials/wiki/src/authorization/tutorial/views.py index 21f12b31d..50485d279 100644 --- a/docs/tutorials/wiki/src/authorization/tutorial/views.py +++ b/docs/tutorials/wiki/src/authorization/tutorial/views.py @@ -51,17 +51,17 @@ def view_page(context, request): renderer='templates/edit.pt', permission='edit') def add_page(context, request): - name = request.subpath[0] + pagename = request.subpath[0] if 'form.submitted' in request.params: body = request.params['body'] page = Page(body) - page.__name__ = name + page.__name__ = pagename page.__parent__ = context - context[name] = page + context[pagename] = page return HTTPFound(location = request.resource_url(page)) - save_url = request.resource_url(context, 'add_page', name) + save_url = request.resource_url(context, 'add_page', pagename) page = Page('') - page.__name__ = name + page.__name__ = pagename page.__parent__ = context return dict(page = page, save_url = save_url, diff --git a/docs/tutorials/wiki/src/tests/tutorial/views.py b/docs/tutorials/wiki/src/tests/tutorial/views.py index 21f12b31d..50485d279 100644 --- a/docs/tutorials/wiki/src/tests/tutorial/views.py +++ b/docs/tutorials/wiki/src/tests/tutorial/views.py @@ -51,17 +51,17 @@ def view_page(context, request): renderer='templates/edit.pt', permission='edit') def add_page(context, request): - name = request.subpath[0] + pagename = request.subpath[0] if 'form.submitted' in request.params: body = request.params['body'] page = Page(body) - page.__name__ = name + page.__name__ = pagename page.__parent__ = context - context[name] = page + context[pagename] = page return HTTPFound(location = request.resource_url(page)) - save_url = request.resource_url(context, 'add_page', name) + save_url = request.resource_url(context, 'add_page', pagename) page = Page('') - page.__name__ = name + page.__name__ = pagename page.__parent__ = context return dict(page = page, save_url = save_url, diff --git a/docs/tutorials/wiki/src/views/tutorial/views.py b/docs/tutorials/wiki/src/views/tutorial/views.py index 016f5b6bb..b0c15297f 100644 --- a/docs/tutorials/wiki/src/views/tutorial/views.py +++ b/docs/tutorials/wiki/src/views/tutorial/views.py @@ -35,17 +35,17 @@ def view_page(context, request): @view_config(name='add_page', context='.models.Wiki', renderer='templates/edit.pt') def add_page(context, request): - name = request.subpath[0] + pagename = request.subpath[0] if 'form.submitted' in request.params: body = request.params['body'] page = Page(body) - page.__name__ = name + page.__name__ = pagename page.__parent__ = context - context[name] = page + context[pagename] = page return HTTPFound(location = request.resource_url(page)) - save_url = request.resource_url(context, 'add_page', name) + save_url = request.resource_url(context, 'add_page', pagename) page = Page('') - page.__name__ = name + page.__name__ = pagename page.__parent__ = context return dict(page = page, save_url = save_url) diff --git a/docs/tutorials/wiki2/authorization.rst b/docs/tutorials/wiki2/authorization.rst index 2ef55d15b..d7bd24a53 100644 --- a/docs/tutorials/wiki2/authorization.rst +++ b/docs/tutorials/wiki2/authorization.rst @@ -353,7 +353,7 @@ when we're done: .. literalinclude:: src/authorization/tutorial/views.py :linenos: - :emphasize-lines: 11,14-18,31,37,58,61,73,76,88,91-117,119-123 + :emphasize-lines: 11,14-18,25,31,37,58,61,73,76,88,91-117,119-123 :language: python (Only the highlighted lines need to be added.) diff --git a/docs/tutorials/wiki2/basiclayout.rst b/docs/tutorials/wiki2/basiclayout.rst index 5f4ea671c..b3184c4fc 100644 --- a/docs/tutorials/wiki2/basiclayout.rst +++ b/docs/tutorials/wiki2/basiclayout.rst @@ -100,7 +100,7 @@ used when the URL is ``/``: :language: py Since this route has a ``pattern`` equalling ``/`` it is the route that will -be matched when the URL ``/`` is visted, e.g. ``http://localhost:6543/``. +be matched when the URL ``/`` is visited, e.g. ``http://localhost:6543/``. ``main`` next calls the ``scan`` method of the configurator, which will recursively scan our ``tutorial`` package, looking for ``@view_config`` (and @@ -190,7 +190,7 @@ Next we set up a SQLAlchemy "DBSession" object: ``scoped_session`` allows us to access our database connection globally. ``sessionmaker`` creates a database session object. We pass to ``sessionmaker`` the ``extension=ZopeTransactionExtension()`` extension -option in order to allow the system to automatically manage datbase +option in order to allow the system to automatically manage database transactions. With ``ZopeTransactionExtension`` activated, our application will automatically issue a transaction commit after every request unless an exception is raised, in which case the transaction will be aborted. diff --git a/docs/tutorials/wiki2/definingviews.rst b/docs/tutorials/wiki2/definingviews.rst index efb72230e..24ac4338d 100644 --- a/docs/tutorials/wiki2/definingviews.rst +++ b/docs/tutorials/wiki2/definingviews.rst @@ -248,7 +248,7 @@ wiki page. It includes: - A ``div`` element that is replaced with the ``content`` value provided by the view (rows 45-47). ``content`` contains HTML, so the ``structure`` keyword is used - to prevent escaping it (i.e. changing ">" to >, etc.) + to prevent escaping it (i.e. changing ">" to ">", etc.) - A link that points at the "edit" URL which invokes the ``edit_page`` view for the page being viewed (rows 49-51). diff --git a/docs/tutorials/wiki2/design.rst b/docs/tutorials/wiki2/design.rst index 2e6fc0e77..deaf32ef6 100644 --- a/docs/tutorials/wiki2/design.rst +++ b/docs/tutorials/wiki2/design.rst @@ -20,7 +20,7 @@ Models We'll be using a SQLite database to hold our wiki data, and we'll be using :term:`SQLAlchemy` to access the data in this database. -Within the database, we define a single table named `tables`, whose elements +Within the database, we define a single table named `pages`, whose elements will store the wiki pages. There are two columns: `name` and `data`. URLs like ``/PageName`` will try to find an element in diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views.py b/docs/tutorials/wiki2/src/authorization/tutorial/views.py index c7670b049..0d085b0e2 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/views.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/views.py @@ -60,14 +60,14 @@ def view_page(request): @view_config(route_name='add_page', renderer='templates/edit.pt', permission='edit') def add_page(request): - name = request.matchdict['pagename'] + pagename = request.matchdict['pagename'] if 'form.submitted' in request.params: body = request.params['body'] - page = Page(name, body) + page = Page(pagename, body) DBSession.add(page) return HTTPFound(location = request.route_url('view_page', - pagename=name)) - save_url = request.route_url('add_page', pagename=name) + pagename=pagename)) + save_url = request.route_url('add_page', pagename=pagename) page = Page('', '') return dict(page=page, save_url=save_url, logged_in=authenticated_userid(request)) @@ -75,16 +75,16 @@ def add_page(request): @view_config(route_name='edit_page', renderer='templates/edit.pt', permission='edit') def edit_page(request): - name = request.matchdict['pagename'] - page = DBSession.query(Page).filter_by(name=name).one() + pagename = request.matchdict['pagename'] + page = DBSession.query(Page).filter_by(name=pagename).one() if 'form.submitted' in request.params: page.data = request.params['body'] DBSession.add(page) return HTTPFound(location = request.route_url('view_page', - pagename=name)) + pagename=pagename)) return dict( page=page, - save_url = request.route_url('edit_page', pagename=name), + save_url = request.route_url('edit_page', pagename=pagename), logged_in=authenticated_userid(request), ) diff --git a/docs/tutorials/wiki2/src/tests/tutorial/views.py b/docs/tutorials/wiki2/src/tests/tutorial/views.py index f2a33af1e..42ac0eb7f 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/views.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/views.py @@ -61,15 +61,15 @@ def view_page(request): @view_config(route_name='add_page', renderer='templates/edit.pt', permission='edit') def add_page(request): - name = request.matchdict['pagename'] + pagename = request.matchdict['pagename'] if 'form.submitted' in request.params: session = DBSession() body = request.params['body'] - page = Page(name, body) + page = Page(pagename, body) session.add(page) return HTTPFound(location = request.route_url('view_page', - pagename=name)) - save_url = request.route_url('add_page', pagename=name) + pagename=pagename)) + save_url = request.route_url('add_page', pagename=pagename) page = Page('', '') return dict(page=page, save_url=save_url, logged_in=authenticated_userid(request)) @@ -77,17 +77,17 @@ def add_page(request): @view_config(route_name='edit_page', renderer='templates/edit.pt', permission='edit') def edit_page(request): - name = request.matchdict['pagename'] + pagename = request.matchdict['pagename'] session = DBSession() - page = session.query(Page).filter_by(name=name).one() + page = session.query(Page).filter_by(name=pagename).one() if 'form.submitted' in request.params: page.data = request.params['body'] session.add(page) return HTTPFound(location = request.route_url('view_page', - pagename=name)) + pagename=pagename)) return dict( page=page, - save_url = request.route_url('edit_page', pagename=name), + save_url = request.route_url('edit_page', pagename=pagename), logged_in=authenticated_userid(request), ) diff --git a/docs/tutorials/wiki2/src/views/tutorial/views.py b/docs/tutorials/wiki2/src/views/tutorial/views.py index c2a94a96b..5a9c75a61 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/views.py +++ b/docs/tutorials/wiki2/src/views/tutorial/views.py @@ -44,27 +44,27 @@ def view_page(request): @view_config(route_name='add_page', renderer='templates/edit.pt') def add_page(request): - name = request.matchdict['pagename'] + pagename = request.matchdict['pagename'] if 'form.submitted' in request.params: body = request.params['body'] - page = Page(name, body) + page = Page(pagename, body) DBSession.add(page) return HTTPFound(location = request.route_url('view_page', - pagename=name)) - save_url = request.route_url('add_page', pagename=name) + pagename=pagename)) + save_url = request.route_url('add_page', pagename=pagename) page = Page('', '') return dict(page=page, save_url=save_url) @view_config(route_name='edit_page', renderer='templates/edit.pt') def edit_page(request): - name = request.matchdict['pagename'] - page = DBSession.query(Page).filter_by(name=name).one() + pagename = request.matchdict['pagename'] + page = DBSession.query(Page).filter_by(name=pagename).one() if 'form.submitted' in request.params: page.data = request.params['body'] DBSession.add(page) return HTTPFound(location = request.route_url('view_page', - pagename=name)) + pagename=pagename)) return dict( page=page, - save_url = request.route_url('edit_page', pagename=name), + save_url = request.route_url('edit_page', pagename=pagename), ) |
