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:
Due to the way the configuration statements work, statement ordering is usually irrelevant. For example, calling
add_view
, thenadd_route
has the same outcome as callingadd_route
, thenadd_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.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.
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.
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.
An application can be created via configuration statements (for example, calls to
add_route
oradd_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 theconfig.include()
API is used to stitch these configuration functions together, some configuration conflicts can be automatically resolved.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
andadd_view
.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.