diff options
| -rw-r--r-- | CHANGES.txt | 4 | ||||
| -rw-r--r-- | TODO.txt | 3 | ||||
| -rw-r--r-- | docs/index.rst | 1 | ||||
| -rw-r--r-- | docs/latexindex.rst | 1 | ||||
| -rw-r--r-- | docs/narr/advconfig.rst | 373 |
5 files changed, 381 insertions, 1 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index 6932a0c25..e28ac9792 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -59,6 +59,10 @@ Documentation - Changed the "ZODB + Traversal Wiki Tutorial" based on changes to ``pyramid_zodb`` Paster template. +- Added "Advanced Configuration" narrative chapter which documents how to + deal with configuration conflicts, two-phase configuration, ``include`` and + ``commit``. + - Fix API documentation rendering for ``pyramid.view.static`` 1.0a7 (2010-12-20) @@ -4,7 +4,8 @@ Pyramid TODOs Must-Have (before 1.0) ---------------------- -- Narrative docs for ``Configurator.include`` and ``Configurator.commit``. +- Reconcile "extending an existing application" chapter with existence of + "advanced configuration" chapter. - Consider deprecations for ``model`` and ``resource`` APIs. diff --git a/docs/index.rst b/docs/index.rst index 343fb28ba..3830a83f9 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -58,6 +58,7 @@ Narrative documentation in chapter form explaining how to use narr/environment narr/testing narr/hooks + narr/advconfig narr/declarative narr/extending narr/assets diff --git a/docs/latexindex.rst b/docs/latexindex.rst index 713c9841f..2d4644416 100644 --- a/docs/latexindex.rst +++ b/docs/latexindex.rst @@ -51,6 +51,7 @@ Narrative Documentation narr/environment narr/testing narr/hooks + narr/advconfig narr/declarative narr/extending narr/assets diff --git a/docs/narr/advconfig.rst b/docs/narr/advconfig.rst new file mode 100644 index 000000000..a4692e4c6 --- /dev/null +++ b/docs/narr/advconfig.rst @@ -0,0 +1,373 @@ +.. index:: + pair: advanced; configuration + +.. _advconfig_narr: + +Advanced Configuration +====================== + +To support application extensibility, the :app:`Pyramid` +:term:`Configurator`, by default, detects configuration conflicts and allows +you to include configuration imperatively from other packages or modules. It +also, by default, performs configuration in two separate phases. This allows +you to ignore relative configuration statement ordering in some +circumstances. + +.. index:: + single: imperative configuration + +.. _conflict_detection: + +Conflict Detection +------------------ + +Here's a familiar example of one of the simplest :app:`Pyramid` applications, +configured imperatively: + +.. code-block:: python + :linenos: + + from paste.httpserver import serve + from pyramid.config import Configurator + from pyramid.response import Response + + def hello_world(request): + return Response('Hello world!') + + if __name__ == '__main__': + config = Configurator() + config.add_view(hello_world) + app = config.make_wsgi_app() + serve(app, host='0.0.0.0') + +When you start this application, all will be OK. However, what happens if we +try to add another view to the configuration with the same set of +:term:`predicate` arguments as one we've already added? + +.. code-block:: python + :linenos: + + from paste.httpserver import serve + from pyramid.config import Configurator + from pyramid.response import Response + + def hello_world(request): + return Response('Hello world!') + + def goodbye_world(request): + return Response('Goodbye world!') + + if __name__ == '__main__': + config = Configurator() + + config.add_view(hello_world, name='hello') + + # conflicting view configuration + config.add_view(goodbye_world, name='hello') + + app = config.make_wsgi_app() + serve(app, host='0.0.0.0') + +The application now has two conflicting view configuration statements. When +we try to start it again, it won't start. Instead, we'll receive a traceback +that ends something like this: + +.. code-block:: guess + :linenos: + + Traceback (most recent call last): + File "app.py", line 12, in <module> + app = config.make_wsgi_app() + File "pyramid/config.py", line 839, in make_wsgi_app + self.commit() + File "pyramid/pyramid/config.py", line 473, in commit + self._ctx.execute_actions() + File "zope/configuration/config.py", line 600, in execute_actions + for action in resolveConflicts(self.actions): + File "zope/configuration/config.py", line 1507, in resolveConflicts + raise ConfigurationConflictError(conflicts) + zope.configuration.config.ConfigurationConflictError: + Conflicting configuration actions + For: ('view', None, '', None, <InterfaceClass pyramid.interfaces.IView>, + None, None, None, None, None, False, None, None, None) + ('app.py', 14, '<module>', 'config.add_view(hello_world)') + ('app.py', 17, '<module>', 'config.add_view(hello_world)') + +This traceback is trying to tell us: + +- We've got conflicting information for a set of view configuration + statements (The ``For:`` line). + +- There are two statements which conflict, shown beneath the ``For:`` line: + ``config.add_view(hello_world. 'hello')`` on line 14 of ``app.py``, and + ``config.add_view(goodbye_world, 'hello')`` on line 17 of ``app.py``. + +These two configuration statements are in conflict because we've tried to +tell the system that the set of :term:`predicate` values for both view +configurations are exactly the same. Both the ``hello_world`` and +``goodbye_world`` views are configured to respond under the same set of +circumstances. This circumstance: the :term:`view name` (represented by the +``name=`` predicate) is ``hello``. + +This presents an ambiguity that :app:`Pyramid` cannot resolve. Rather than +allowing the circumstance to go unreported, by default Pyramid raises a +:exc:`ConfigurationConflictError` error and prevents the application from +running. + +Conflict detection happens for any kind of configuration: imperative +configuration, :term:`ZCML` configuration, or configuration that results from +the execution of a :term:`scan`. + +Manually Resolving Conflicts +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +There are a number of ways to manually resolve conflicts: the "right" way, by +strategically using :meth:`pyramid.config.Configurator.commit`, or by using +an "autocommitting" configurator. + +The Right Thing ++++++++++++++++ + +The most correct way to resolve conflicts is to "do the needful": change your +configuration code to not have conflicting configuration statements. The +details of how this is done depends entirely on the configuration statements +made by your application. Use the detail provided in the +:exc:`ConfigurationConflictError` to track down the offending conflicts and +modify your configuration code accordingly. + +If you're getting a conflict while trying to extend an existing application, +and that application has a function which performs configuration like this +one: + +.. code-block:: python + :linenos: + + def add_routes(config): + config.add_route(...) + +Don't call this function directly with ``config`` as an argument. Instead, +use :meth:`pyramid.config.Configuration.include`: + +.. code-block:: python + :linenos: + + config.include(add_routes) + +Using :meth:`~pyramid.config.Configuration.include` instead of calling the +function directly provides a modicum of automated conflict resolution, with +the configuration statements you define in the calling code overriding those +of the included function. See also :ref:`automatic_conflict_resolution` and +:ref:`including_configuration`. + +Using ``config.commit()`` ++++++++++++++++++++++++++ + +You can manually commit a configuration by using the +:meth:`pyramid.config.Configurator.commit` method between configuration +calls. For example, we can make the application we examined previously which +generated a conflict exception + +.. code-block:: python + :linenos: + + from paste.httpserver import serve + from pyramid.config import Configurator + from pyramid.response import Response + + def hello_world(request): + return Response('Hello world!') + + def goodbye_world(request): + return Response('Goodbye world!') + + if __name__ == '__main__': + config = Configurator() + + config.add_view(hello_world, name='hello') + + # conflicting view configuration + config.add_view(goodbye_world, name='hello') + + app = config.make_wsgi_app() + serve(app, host='0.0.0.0') + +We can prevent the two ``add_view`` calls from conflicting by issuing a call +to :meth:`~pyramid.config.Configurator.commit` between them: + +.. code-block:: python + :linenos: + + from paste.httpserver import serve + from pyramid.config import Configurator + from pyramid.response import Response + + def hello_world(request): + return Response('Hello world!') + + def goodbye_world(request): + return Response('Goodbye world!') + + if __name__ == '__main__': + config = Configurator() + + config.add_view(hello_world, name='hello') + + config.commit() # commit any pending configuration actions + + # no-longer-conflicting view configuration + config.add_view(goodbye_world, name='hello') + + app = config.make_wsgi_app() + serve(app, host='0.0.0.0') + +In the above example we've issued a call to +:meth:`~pyramid.config.Configurator.commit` between the two ``add_view`` +calls. :meth:`~pyramid.config.Configurator.commit` will cause any pending +configuration statements. + +Calling :meth:`~pyramid.config.Configurator.commit` is safe at any time. It +executes all pending configuration actions and leaves the configuration +action list "clean". + +.. _autocommitting_configurator: + +Using An Autocommitting Configurator +++++++++++++++++++++++++++++++++++++ + +You can also use a heavy hammer to circumvent conflict detection by using a +configurator constructor parameter: ``autocommit=True``. For example: + +.. code-block:: python + :linenos: + + from pyramid.config import Configurator + + if __name__ == '__main__': + config = Configurator(autocommit=True) + +When the ``autocommit`` parameter passed to the Configurator is ``True``, +conflict detection (and :ref:`twophase_config`) is disabled. +:meth:`pyramid.config.Configurator.commit` has no effect when ``autocommit`` +is ``True`` either. + +If you use a Configurator in code that performs unit testing, it's usually a +good idea to use an autocommitting Configurator, because you are usually +unconcerned about conflict detection or two-phase configuration in test code. + +.. _automatic_conflict_resolution: + +Automatic Conflict Resolution +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If your code uses the :meth:`pyramid.config.Configurator.include` method to +include external configuration, some conflicts are automatically resolved. +Configuration statements that are made as the result of an "include" will be +overridden by configuration statements that happen within the caller of +the "include" method. See also + +Automatic conflict resolution supports this goal: if a user wants to reuse a +Pyramid application, and they want to customize the configuration of this +application without hacking its code "from outside", they can "include" the +package and override only some of its configuration statements within the +code that does the include. No conflicts will be generated by configuration +statements within the code which does the including, even if configuration +statements in the included code would conflict if it was moved "up" to the +calling code. + +Methods Which Provide Conflict Detection +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +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.set_request_factory`, +:meth:`~pyramid.config.Configurator.set_renderer_globals_factory` +:meth:`~pyramid.config.Configurator.set_locale_negotiator` and +:meth:`~pyramid.config.Configurator.set_default_permission`. + +Some other methods of the configurator also indirectly provide conflict +detection, because they're implemented in terms of conflict-aware methods: + +- :meth:`~pyramid.config.Configurator.add_handler`, a frontend for + ``add_route`` and ``add_view``. + +- :meth:`~pyramid.config.Configurator.add_route` does a second type of + conflict detection when a ``view`` parameter is passed (it calls + ``add_view``). + +- :meth:`~pyramid.config.Configurator.static_view`, a frontend for + ``add_route`` and ``add_view``. + +.. _including_configuration: + +Including Configuration from External Sources +--------------------------------------------- + +Some application programmers will factor their configuration code in such a +way that it is easy to reuse and override configuration statements. For +example, such a developer might factor out a function used to add routes to +his application: + +.. code-block:: python + :linenos: + + def add_routes(config): + config.add_route(...) + +Rather than calling this function directly with ``config`` as an argument. +Instead, use :meth:`pyramid.config.Configuration.include`: + +.. code-block:: python + :linenos: + + config.include(add_routes) + +Using ``include`` rather than calling the function directly will allow +:ref:`automatic_conflict_resolution` to work. + +.. _twophase_config: + +Two-Phase Configuration +----------------------- + +When a non-autocommitting :term:`Configurator` is used to do configuration +(the default), configuration execution happens in two phases. In the first +phase, "eager" configuration actions (actions that must happen before all +others, such as registering a renderer) are executed, and *discriminators* +are computed for each of the actions that depend on the result of the eager +actions. In the second phase, the discriminators of all actions are compared +to do conflict detection. + +Due to this, for configuration methods that have no internal ordering +constraints, execution order of configuration method calls is not important. +For example, the relative ordering of +:meth:`pyramid.config.Configurator.add_view` and +:meth:`pyramid.config.Configurator.add_renderer` is unimportant when a +non-autocommitting configurator is used. This code snippet: + +.. code-block:: python + :linenos: + + config.add_view('some.view', renderer='path_to_custom/renderer.rn') + config.add_renderer('.rn', SomeCustomRendererFactory) + +Has the same result as: + +.. code-block:: python + :linenos: + + config.add_renderer('.rn', SomeCustomRendererFactory) + config.add_view('some.view', renderer='path_to_custom/renderer.rn') + +Even though the view statement depends on the registration of a custom +renderer, due to two-phase configuration, the order in which the +configuration statements are issued is not important. + +The same is untrue when you use an *autocommitting* configurator (see +:ref:`autocommitting_configurator`). When an autocommitting configurator is +used, two-phase configuration is disabled, and configuration statements must +be ordered in dependency order. + + |
