A Whirlwind Tour of Advanced Pyramid Configuration Tactics

Concepts: Configuration, Directives, and Statements

This article attempts to demonstrate some of Pyramid's more advanced startup-time configuration features. The stuff below talks about "configuration", which is a shorthand word I'll use to mean the state that is changed when a developer adds views, routes, subscribers, and other bits. A developer adds configuration by calling configuration directives. For example, config.add_route() is a configuration directive. A particular use of config.add_route() is a configuration statement. In the below code block, the execution of the add_route() directive is a configuration statement. Configuration statements change pending configuration state:

config = pyramid.config.Configurator()
config.add_route('home', '/')

Here are a few core concepts related to Pyramid startup configuration:

  1. Due to the way the configuration statements work, statement ordering is usually irrelevant. For example, calling add_view, then add_route has the same outcome as calling add_route, then add_view. There are some important exceptions to this, but in general, unless the documentation for a given configuration directive states otherwise, you don't need to care in what order your code adds configuration statements.

  2. When a configuration statement is executed, it usually doesn't do much configuration immediately. Instead, it generates a discriminator and produces a callback. The discriminator is a hashable value that represents the configuration statement uniquely amongst all other configuration statements. The callback, when eventually called, actually performs the work related to the configuration statement. Pyramid adds the discriminator and the callback into a list of pending actions that may later be committed.

  3. Pending configuration actions can be committed at any time. At commit time, Pyramid compares each of the discriminators generated by a configuration statement to every other discriminator generated by other configuration statements in the pending actions list. If two or more configuration statements have generated the same discriminator, this is a conflict. Pyramid will attempt to resolve the conflict automatically; if it cannot, startup will exit with an error. If all conflicts are resolved, each callback associated with a configuration statement is executed. Per-action sanity-checking is also performed as the result of a commit.

  4. Pending actions can be committed more than once during startup in order to avoid a configuration state that contains conflicts. This is useful if you need to perform configuration overrides in a brute-force, deployment-specific way.

  5. An application can be created via configuration statements (for example, calls to add_route or add_view) composed from logic defined in multiple locations. The configuration statements usually live within Python functions. Those functions can live anywhere, as long as they can be imported. If the config.include() API is used to stitch these configuration functions together, some configuration conflicts can be automatically resolved.

  6. Developers can add directives which participate in Pyramid's phased configuration process. These directives can be made to work exactly like "built-in" directives like add_route and add_view.

  7. Application configuration is never added as the result of someone or something just happening to import a Python module. Adding configuration is always more explicit than that.

Let's see some of those concepts in action. Here's one of the simplest possible Pyramid applications:

 1# app.py
 2
 3from wsgiref.simple_server import make_server
 4from pyramid.config import Configurator
 5from pyramid.response import Response
 6
 7def hello_world(request):
 8    return Response('Hello world!')
 9
10if __name__ == '__main__':
11    config = Configurator()
12    config.add_route('home', '/')
13    config.add_view(hello_world, route_name='home')
14    app = config.make_wsgi_app()
15    server = make_server('0.0.0.0', 8080, app)
16    server.serve_forever()

If we run this application via python app.py, we'll get a Hello world! printed when we visit http://localhost:8080/ in a browser. Not very exciting.

What happens when we reorder our configuration statements? We'll change the relative ordering of add_view() and add_route() configuration statements. Instead of adding a route, then a view, we'll add a view then a route:

 1# app.py
 2
 3from wsgiref.simple_server import make_server
 4from pyramid.config import Configurator
 5from pyramid.response import Response
 6
 7def hello_world(request):
 8    return Response('Hello world!')
 9
10if __name__ == '__main__':
11    config = Configurator()
12    config.add_view(hello_world, route_name='home') # moved this up
13    config.add_route('home', '/')
14    app = config.make_wsgi_app()
15    server = make_server('0.0.0.0', 8080, app)
16    server.serve_forever()

If you start this application, you'll note that, like before, visiting / serves up Hello world!. In other words, it works exactly like it did before we switched the ordering around. You might not expect this configuration to work, because we're referencing the name of a route (home) within our add_view statement (config.add_view(hello_world, route_name='home') that hasn't been added yet. When we execute add_view, add_route('home', '/') has not yet been executed. This out-of-order execution works because Pyramid defers configuration execution until a commit is performed as the result of config.make_wsgi_app() being called. Relative ordering between config.add_route() and config.add_view() calls is not important. Pyramid implicitly commits the configuration state when make_wsgi_app() gets called; only when it's committed is the configuration state sanity-checked. In particular, in this case, we're relying on the fact that Pyramid makes sure that all route configuration happens before any view configuration at commit time. If a view references a nonexistent route, an error will be raised at commit time rather than at configuration statement execution time.

Sanity Checks

We can see this sanity-checking feature in action in a failure case. Let's change our application, commenting out our call to config.add_route() temporarily within app.py:

 1# app.py
 2
 3from wsgiref.simple_server import make_server
 4from pyramid.config import Configurator
 5from pyramid.response import Response
 6
 7def hello_world(request):
 8    return Response('Hello world!')
 9
10if __name__ == '__main__':
11    config = Configurator()
12    config.add_view(hello_world, route_name='home') # moved this up
13    # config.add_route('home', '/') # we temporarily commented this line
14    app = config.make_wsgi_app()
15    server = make_server('0.0.0.0', 8080, app)
16    server.serve_forever()

When we attempt to run this Pyramid application, we get a traceback:

 1Traceback (most recent call last):
 2  File "app.py", line 12, in <module>
 3    app = config.make_wsgi_app()
 4  File "/home/chrism/projects/pyramid/pyramid/config/__init__.py", line 955, in make_wsgi_app
 5    self.commit()
 6  File "/home/chrism/projects/pyramid/pyramid/config/__init__.py", line 629, in commit
 7    self.action_state.execute_actions(introspector=self.introspector)
 8  File "/home/chrism/projects/pyramid/pyramid/config/__init__.py", line 1083, in execute_actions
 9    tb)
10  File "/home/chrism/projects/pyramid/pyramid/config/__init__.py", line 1075, in execute_actions
11    callable(*args, **kw)
12  File "/home/chrism/projects/pyramid/pyramid/config/views.py", line 1124, in register
13    route_name)
14pyramid.exceptions.ConfigurationExecutionError: <class 'pyramid.exceptions.ConfigurationError'>: No route named home found for view registration
15  in:
16  Line 10 of file app.py:
17    config.add_view(hello_world, route_name='home')

It's telling us that we attempted to add a view which references a nonexistent route. Configuration directives sometimes introduce sanity-checking to startup, as demonstrated here.

Configuration Conflicts

Let's change our application once again. We'll undo our last change, and add a configuration statement that attempts to add another view:

 1# app.py
 2
 3from wsgiref.simple_server import make_server
 4from pyramid.config import Configurator
 5from pyramid.response import Response
 6
 7def hello_world(request):
 8    return Response('Hello world!')
 9
10def hi_world(request): # added
11    return Response('Hi world!')
12
13if __name__ == '__main__':
14    config = Configurator()
15    config.add_route('home', '/')
16    config.add_view(hello_world, route_name='home')
17    config.add_view(hi_world, route_name='home') # added
18    app = config.make_wsgi_app()
19    server = make_server('0.0.0.0', 8080, app)
20    server.serve_forever()

If you notice above, we're now calling add_view twice with two different view callables. Each call to add_view names the same route name. What happens when we try to run this program now?:

 1Traceback (most recent call last):
 2  File "app.py", line 17, in <module>
 3    app = config.make_wsgi_app()
 4  File "/home/chrism/projects/pyramid/pyramid/config/__init__.py", line 955, in make_wsgi_app
 5    self.commit()
 6  File "/home/chrism/projects/pyramid/pyramid/config/__init__.py", line 629, in commit
 7    self.action_state.execute_actions(introspector=self.introspector)
 8  File "/home/chrism/projects/pyramid/pyramid/config/__init__.py", line 1064, in execute_actions
 9    for action in resolveConflicts(self.actions):
10  File "/home/chrism/projects/pyramid/pyramid/config/__init__.py", line 1192, in resolveConflicts
11    raise ConfigurationConflictError(conflicts)
12pyramid.exceptions.ConfigurationConflictError: Conflicting configuration actions
13  For: ('view', None, '', 'home', 'd41d8cd98f00b204e9800998ecf8427e')
14    Line 14 of file app.py:
15        config.add_view(hello_world, route_name='home')
16    Line 15 of file app.py:
17        config.add_view(hi_world, route_name='home')

This traceback is telling us that there was a configuration conflict between two configuration statements: the add_view statement on line 14 of app.py and the add_view statement on line 15 of app.py. This happens because the discriminator generated by add_view statement on line 14 turned out to be the same as the discriminator generated by the add_view statement on line 15. The discriminator is printed above the line conflict output: For: ('view', None, '', 'home', 'd41d8cd98f00b204e9800998ecf8427e') .

注釈

The discriminator itself has to be opaque in order to service all of the use cases required by add_view. It's not really meant to be parsed by a human, and is kinda really printed only for consumption by core Pyramid developers. We may consider changing things in future Pyramid versions so that it doesn't get printed when a conflict exception happens.

Why is this exception raised? Pyramid couldn't work out what you wanted to do. You told it to serve up more than one view for exactly the same set of request-time circumstances ("when the route name matches home, serve this view"). This is an impossibility: Pyramid needs to serve one view or the other in this circumstance; it can't serve both. So rather than trying to guess what you meant, Pyramid raises a configuration conflict error and refuses to start.

Resolving Conflicts

Obviously it's necessary to be able to resolve configuration conflicts. Sometimes these conflicts are done by mistake, so they're easy to resolve. You just change the code so that the conflict is no longer present. We can do that pretty easily:

 1# app.py
 2
 3from wsgiref.simple_server import make_server
 4from pyramid.config import Configurator
 5from pyramid.response import Response
 6
 7def hello_world(request):
 8    return Response('Hello world!')
 9
10def hi_world(request):
11    return Response('Hi world!')
12
13if __name__ == '__main__':
14    config = Configurator()
15    config.add_route('home', '/')
16    config.add_view(hello_world, route_name='home')
17    config.add_view(hi_world, route_name='home', request_param='use_hi')
18    app = config.make_wsgi_app()
19    server = make_server('0.0.0.0', 8080, app)
20    server.serve_forever()

In the above code, we've gotten rid of the conflict. Now the hello_world view will be called by default when / is visited without a query string, but if / is visted when the URL contains a use_hi query string, the hi_world view will be executed instead. In other words, visiting / in the browser produces Hello world!, but visiting /?use_hi=1 produces Hi world!.

There's an alternative way to resolve conflicts that doesn't change the semantics of the code as much. You can issue a config.commit() statement to flush pending configuration actions before issuing more. To see this in action, let's change our application back to the way it was before we added the request_param predicate to our second add_view statement:

 1# app.py
 2
 3from wsgiref.simple_server import make_server
 4from pyramid.config import Configurator
 5from pyramid.response import Response
 6
 7def hello_world(request):
 8    return Response('Hello world!')
 9
10def hi_world(request): # added
11    return Response('Hi world!')
12
13if __name__ == '__main__':
14    config = Configurator()
15    config.add_route('home', '/')
16    config.add_view(hello_world, route_name='home')
17    config.add_view(hi_world, route_name='home') # added
18    app = config.make_wsgi_app()
19    server = make_server('0.0.0.0', 8080, app)
20    server.serve_forever()

If we try to run this application as-is, we'll wind up with a configuration conflict error. We can actually sort of brute-force our way around that by adding a manual call to commit between the two add_view statements which conflict:

 1# app.py
 2
 3from wsgiref.simple_server import make_server
 4from pyramid.config import Configurator
 5from pyramid.response import Response
 6
 7def hello_world(request):
 8    return Response('Hello world!')
 9
10def hi_world(request): # added
11    return Response('Hi world!')
12
13if __name__ == '__main__':
14    config = Configurator()
15    config.add_route('home', '/')
16    config.add_view(hello_world, route_name='home')
17    config.commit() # added
18    config.add_view(hi_world, route_name='home') # added
19    app = config.make_wsgi_app()
20    server = make_server('0.0.0.0', 8080, app)
21    server.serve_forever()

If we run this application, it will start up. And if we visit / in our browser, we'll see Hi world!. Why doesn't this application throw a configuration conflict error at the time it starts up? Because we flushed the pending configuration action impled by the first call to add_view by calling config.commit() explicitly. When we called the add_view the second time, the discriminator of the first call to add_view was no longer in the pending actions list to conflict with. The conflict was resolved because the pending actions list got flushed. Why do we see Hi world! in our browser instead of Hello world!? Because the call to config.make_wsgi_app() implies a second commit. The second commit caused the second add_view configuration callback to be called, and this callback overwrote the view configuration added by the first commit.

Calling config.commit() is a brute-force way to resolve configuration conflicts.

Including Configuration from Other Modules

Now that we have played around a bit with configuration that exists all in the same module, let's add some code to app.py that causes configuration that lives in another module to be included. We do that by adding a call to config.include() within app.py:

 1# app.py
 2
 3from wsgiref.simple_server import make_server
 4from pyramid.config import Configurator
 5from pyramid.response import Response
 6
 7def hello_world(request):
 8    return Response('Hello world!')
 9
10if __name__ == '__main__':
11    config = Configurator()
12    config.add_route('home', '/')
13    config.add_view(hello_world, route_name='home')
14    config.include('another.moreconfiguration')  # added
15    app = config.make_wsgi_app()
16    server = make_server('0.0.0.0', 8080, app)
17    server.serve_forever()

We added the line config.include('another.moreconfiguration') above. If we try to run the application now, we'll receive a traceback:

 1Traceback (most recent call last):
 2  File "app.py", line 12, in <module>
 3    config.include('another')
 4  File "/home/chrism/projects/pyramid/pyramid/config/__init__.py", line 744, in include
 5    c = self.maybe_dotted(callable)
 6  File "/home/chrism/projects/pyramid/pyramid/config/__init__.py", line 844, in maybe_dotted
 7    return self.name_resolver.maybe_resolve(dotted)
 8  File "/home/chrism/projects/pyramid/pyramid/path.py", line 318, in maybe_resolve
 9    return self._resolve(dotted, package)
10  File "/home/chrism/projects/pyramid/pyramid/path.py", line 325, in _resolve
11    return self._zope_dottedname_style(dotted, package)
12  File "/home/chrism/projects/pyramid/pyramid/path.py", line 368, in _zope_dottedname_style
13    found = __import__(used)
14ImportError: No module named another

That's exactly as we expected, because we attempted to include a module that doesn't yet exist. Let's add a module named another.py right next to our app.py module:

 1# another.py
 2
 3from pyramid.response import Response
 4
 5def goodbye(request):
 6    return Response('Goodbye world!')
 7
 8def moreconfiguration(config):
 9    config.add_route('goodbye', '/goodbye')
10    config.add_view(goodbye, route_name='goodbye')

Now what happens when we run the application via python app.py? It starts. And, like before, if we visit / in a browser, it still show Hello world!. But, unlike before, now if we visit /goodbye in a browser, it will show us Goodbye world!.

When we called include('another.moreconfiguration') within app.py, Pyramid interpreted this call as "please find the function named moreconfiguration in a module or package named another and call it with a configurator as the only argument". And that's indeed what happened: the moreconfiguration function within another.py was called; it accepted a configurator as its first argument and added a route and a view, which is why we can now visit /goodbye in the browser and get a response. It's the same effective outcome as if we had issued the add_route and add_view statements for the "goodbye" view from within app.py. An application can be created via configuration statements composed from multiple locations.

You might be asking yourself at this point "So what?! That's just a function call hidden under an API that resolves a module name to a function. I could just import the moreconfiguration function from another and call it directly with the configurator!" You're mostly right. However, config.include() does more than that. Please stick with me, we'll get to it.

The includeme() Convention

Now, let's change our app.py slightly. We'll change the config.include() line in app.py to include a slightly different name:

 1# app.py
 2
 3from wsgiref.simple_server import make_server
 4from pyramid.config import Configurator
 5from pyramid.response import Response
 6
 7def hello_world(request):
 8    return Response('Hello world!')
 9
10if __name__ == '__main__':
11    config = Configurator()
12    config.add_route('home', '/')
13    config.add_view(hello_world, route_name='home')
14    config.include('another')  # <-- changed
15    app = config.make_wsgi_app()
16    server = make_server('0.0.0.0', 8080, app)
17    server.serve_forever()

And we'll edit another.py, changing the name of the moreconfiguration function to includeme:

 1# another.py
 2
 3from pyramid.response import Response
 4
 5def goodbye(request):
 6    return Response('Goodbye world!')
 7
 8def includeme(config): # <-- previously named moreconfiguration
 9    config.add_route('goodbye', '/goodbye')
10    config.add_view(goodbye, route_name='goodbye')

When we run the application, it works exactly like our last iteration. You can visit / and /goodbye and get the exact same results. Why is this so? We didn't tell Pyramid the name of our new includeme function like we did before for moreconfiguration by saying config.include('another.includeme'), we just pointed it at the module in which includeme lived by saying config.include('another'). This is a Pyramid convenience shorthand: if you tell Pyramid to include a Python module or package, it will assume that you're telling it to include the includeme function from within that module/package. Effectively, config.include('amodule') always means config.include('amodule.includeme').

Nested Includes

Something which is included can also do including. Let's add a file named yetanother.py next to app.py:

 1# yetanother.py
 2
 3from pyramid.response import Response
 4
 5def whoa(request):
 6    return Response('Whoa')
 7
 8def includeme(config):
 9    config.add_route('whoa', '/whoa')
10    config.add_view(whoa, route_name='whoa')

And let's change our another.py file to include it:

 1# another.py
 2
 3from pyramid.response import Response
 4
 5def goodbye(request):
 6    return Response('Goodbye world!')
 7
 8def includeme(config): # <-- previously named moreconfiguration
 9    config.add_route('goodbye', '/goodbye')
10    config.add_view(goodbye, route_name='goodbye')
11    config.include('yetanother')

When we start up this application, we can visit /, /goodbye and /whoa and see responses on each. app.py includes another.py which includes yetanother.py. You can nest configuration includes within configuration includes ad infinitum. It's turtles all the way down.

Automatic Resolution via Includes

As we saw previously, it's relatively easy to manually resolve configuration conflicts that are produced by mistake. But sometimes configuration conflicts are not injected by mistake. Sometimes they're introduced on purpose in the desire to override one configuration statement with another. Pyramid anticipates this need in two ways: by offering automatic conflict resolution via config.include(), and the ability to manually commit configuration before a conflict occurs.

Let's change our another.py to contain a hi_world view function, and we'll change its includeme to add that view that should answer when / is visited:

 1# another.py
 2
 3from pyramid.response import Response
 4
 5def goodbye(request):
 6    return Response('Goodbye world!')
 7
 8def hi_world(request): # added
 9    return Response('Hi world!')
10
11def includeme(config):
12    config.add_route('goodbye', '/goodbye')
13    config.add_view(goodbye, route_name='goodbye')
14    config.add_view(hi_world, route_name='home') # added

When we attempt to start the application, it will start without a conflict error. This is strange, because we have what appears to be the same configuration that caused a conflict error before when all of the same configuration statements were made in app.py. In particular, hi_world and hello_world are both being registered as the view that should be called when the home route is executed. When the application runs, when you visit / in your browser, you will see Hello world! (not Hi world!). The registration for the hello_world view in app.py "won" over the registration for the hi_world view in another.py.

Here's what's going on: Pyramid was able to automatically resolve a conflict for us. Configuration statements which generate the same discriminator will conflict. But if one of those configuration statements was performed as the result of being included "below" the other one, Pyramid will make an assumption: it's assuming that the thing doing the including (app.py) wants to override configuration statements done in the thing being included (another.py). In the above code configuration, even though the discriminator generated by config.add_view(hello_world, route_name='home') in app.py conflicts with the discriminator generated by config.add_view(hi_world, route_name='home') in another.py, Pyramid assumes that the former should override the latter, because app.py includes another.py.

Note that the same conflict resolution behavior does not occur if you simply import another.includeme from within app.py and call it, passing it a config object. This is why using config.include is different than just factoring your configuration into functions and arranging to call those functions at startup time directly. Using config.include() makes automatic conflict resolution work properly.

Custom Configuration Directives

A developer needn't satisfy himself with only the directives provided by Pyramid like add_route and add_view. He can add directives to the Configurator. This makes it easy for him to allow other developers to add application-specific configuration. For example, let's pretend you're creating an extensible application, and you'd like to allow developers to change the "site name" of your application (the site name is used in some web UI somewhere). Let's further pretend you'd like to do this by allowing people to call a set_site_name directive on the Configurator. This is a bit of a contrived example, because it would probably be a bit easier in this particular case just to use a deployment setting, but humor me for the purpose of this example. Let's change our app.py to look like this:

 1# app.py
 2
 3from wsgiref.simple_server import make_server
 4from pyramid.config import Configurator
 5from pyramid.response import Response
 6
 7def hello_world(request):
 8    return Response('Hello world!')
 9
10if __name__ == '__main__':
11    config = Configurator()
12    config.add_route('home', '/')
13    config.add_view(hello_world, route_name='home')
14    config.include('another')
15    config.set_site_name('foo')
16    app = config.make_wsgi_app()
17    print app.registry.site_name
18    server = make_server('0.0.0.0', 8080, app)
19    server.serve_forever()

And change our another.py to look like this:

 1# another.py
 2
 3from pyramid.response import Response
 4
 5def goodbye(request):
 6    return Response('Goodbye world!')
 7
 8def hi_world(request):
 9    return Response('Hi world!')
10
11def set_site_name(config, site_name):
12    def callback():
13        config.registry.site_name = site_name
14    discriminator = ('set_site_name',)
15    config.action(discriminator, callable=callback)
16
17def includeme(config):
18    config.add_route('goodbye', '/goodbye')
19    config.add_view(goodbye, route_name='goodbye')
20    config.add_view(hi_world, route_name='home')
21    config.add_directive('set_site_name', set_site_name)

When this application runs, you'll see printed to the console foo. You'll notice in the app.py above, we call config.set_site_name. This is not a Pyramid built-in directive. It was added as the result of the call to config.add_directive in another.includeme. We added a function that uses the config.action method to register a discriminator and a callback for a custom directive. Let's change app.py again, adding a second call to set_site_name:

 1# app.py
 2
 3from wsgiref.simple_server import make_server
 4from pyramid.config import Configurator
 5from pyramid.response import Response
 6
 7def hello_world(request):
 8    return Response('Hello world!')
 9
10if __name__ == '__main__':
11    config = Configurator()
12    config.add_route('home', '/')
13    config.add_view(hello_world, route_name='home')
14    config.include('another')
15    config.set_site_name('foo')
16    config.set_site_name('bar') # added this
17    app = config.make_wsgi_app()
18    print app.registry.site_name
19    server = make_server('0.0.0.0', 8080, app)
20    server.serve_forever()

When we try to start the application, we'll get this traceback:

 1Traceback (most recent call last):
 2  File "app.py", line 15, in <module>
 3    app = config.make_wsgi_app()
 4  File "/home/chrism/projects/pyramid/pyramid/config/__init__.py", line 955, in make_wsgi_app
 5    self.commit()
 6  File "/home/chrism/projects/pyramid/pyramid/config/__init__.py", line 629, in commit
 7    self.action_state.execute_actions(introspector=self.introspector)
 8  File "/home/chrism/projects/pyramid/pyramid/config/__init__.py", line 1064, in execute_actions
 9    for action in resolveConflicts(self.actions):
10  File "/home/chrism/projects/pyramid/pyramid/config/__init__.py", line 1192, in resolveConflicts
11    raise ConfigurationConflictError(conflicts)
12pyramid.exceptions.ConfigurationConflictError: Conflicting configuration actions
13  For: ('site-name',)
14    Line 13 of file app.py:
15        config.set_site_name('foo')
16    Line 14 of file app.py:
17        config.set_site_name('bar')

We added a custom directive that made use of Pyramid's configuration conflict detection. When we tried to set the site name twice, Pyramid detected a conflict and told us. Just like built-in directives, Pyramid custom directives will also participate in automatic conflict resolution. Let's see that in action by moving our first call to set_site_name into another included function. As a result, our app.py will look like this:

 1# app.py
 2
 3from wsgiref.simple_server import make_server
 4from pyramid.config import Configurator
 5from pyramid.response import Response
 6
 7def hello_world(request):
 8    return Response('Hello world!')
 9
10def moarconfig(config):
11    config.set_site_name('foo')
12
13if __name__ == '__main__':
14    config = Configurator()
15    config.add_route('home', '/')
16    config.add_view(hello_world, route_name='home')
17    config.include('another')
18    config.include('.moarconfig')
19    config.set_site_name('bar')
20    app = config.make_wsgi_app()
21    print app.registry.site_name
22    server = make_server('0.0.0.0', 8080, app)
23    server.serve_forever()

If we start this application up, we'll see bar printed to the console. No conflict will be raised, even though we have two calls to set_site_name being executed. This is because our custom directive is making use of automatic conflict resolution: Pyramid determines that the call to set_site_name('bar') should "win" because it's "closer to the top of the application" than the other call which sets it to "bar".

Why This Is Great

Now for some general descriptions of what makes the way all of this works great.

You'll note that a mere import of a module in our tiny application doesn't cause any sort of configuration state to be added, nor do any of our existing modules rely on some configuration having occurred before they can be imported. Application configuration is never added as the result of someone or something just happening to import a module. This seems like an obvious design choice, but it's not true of all web frameworks. Some web frameworks rely on a particular import ordering: you might not be able to successfully import your application code until some other module has been initialized via an import. Some web frameworks depend on configuration happening as a side effect of decorator execution: as a result, you might be required to import all of your application's modules for it to be configured in its entirety. Our application relies on neither: importing our code requires no prior import to have happened, and no configuration is done as the side effect of importing any of our code. This explicitness helps you build larger systems because you're never left guessing about the configuration state: you are entirely in charge at all times.

Most other web frameworks don't have a conflict detection system, and when they're fed two configuration statements that are logically conflicting, they'll choose one or the other silently, leaving you sometimes to wonder why you're not seeing the output you expect. Likewise, the execution ordering of configuration statements in most other web frameworks matters deeply; Pyramid doesn't make you care much about it.

A third party developer can override parts of an existing application's configuration as long as that application's original developer anticipates it minimally by factoring his configuration statements into a function that is includable. He doesn't necessarily have to anticipate what bits of his application might be overridden, just that something might be overridden. This is unlike other web frameworks, which, if they allow for application extensibility at all, indeed tend to force the original application developer to think hard about what might be overridden. Under other frameworks, an application developer that wants to provide application extensibility is usually required to write ad-hoc code that allows a user to override various parts of his application such as views, routes, subscribers, and templates. In Pyramid, he is not required to do this: everything is overridable, and he just refers anyone who wants to change the way it works to the Pyramid docs. The config.include() system even allows a third-party developer who wants to change an application to not think about the mechanics of overriding at all; he just adds statements before or after including the original developer's configuration statements, and he relies on automatic conflict resolution to work things out for him.

Configuration logic can be included from anywhere, and split across multiple packages and filesystem locations. There is no special set of Pyramid-y "application" directories containing configuration that must exist all in one place. Other web frameworks introduce packages or directories that are "more special than others" to offer similar features. To extend an application written using other web frameworks, you sometimes have to add to the set of them by changing a central directory structure.

The system is meta-configurable. You can extend the set of configuration directives offered to users by using config.add_directive(). This means that you can effectively extend Pyramid itself without needing to rewrite or redocument a solution from scratch: you just tell people the directive exists and tell them it works like every other Pyramid directive. You'll get all the goodness of conflict detection and resolution too.

All of the examples in this article use the "imperative" Pyramid configuration API, where a user calls methods on a Configurator object to perform configuration. For developer convenience, Pyramid also exposes a declarative configuration mechanism, usually by offering a function, class, and method decorator that is activated via a scan. Such decorators simply attach a callback to the object they're decorating, and during the scan process these callbacks are called: the callbacks just call methods on a configurator on the behalf of the user as if he had typed them himself. These decorators participate in Pyramid's configuration scheme exactly like imperative method calls.

For more information about config.include() and creating extensible applications, see Advanced Configuration and Extending an Existing Pyramid Application in the Pyramid narrative documenation. For more information about creating directives, see Extending Pyramid Configuration.