高级Pyramid配置策略的旋风之旅

概念:配置、指令和语句

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 指令. 例如, config.add_route() is a configuration directive. 特定的 use 属于 config.add_route() 陈述. In the below code block, the execution of the add_route()

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

以下是与Pyramid启动配置相关的一些核心概念:

  1. Due to the way the configuration statements work, statement ordering is usually irrelevant. For example, calling add_view 然后 add_route add_route 然后 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 产生一个 回调. 坚信的.

  3. 冲突. 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. 为了避免包含冲突的配置状态,可以在启动期间多次提交挂起操作。如果您需要以一种蛮力的、特定于部署的方式执行配置覆盖,那么这非常有用。

  5. An application can be created via configuration statements (for example, calls to add_routeadd_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. 如果 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_routeadd_view .

  7. 应用程序配置永远不会因为刚刚导入一个python模块而被添加。添加配置总是比这更明确。

让我们看看这些概念的一些实际应用。以下是最简单的Pyramid应用程序之一:

 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 我们会得到一个 Hello world! http://localhost:8080/ 在浏览器中。Not very exciting.

What happens when we reorder our configuration statements? We'll change the relative ordering of add_view()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 / 服役 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 config.add_view(hello_world, route_name='home') that hasn't been added yet. 当我们执行时 add_viewadd_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() 被召唤。Relative ordering between config.add_route()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.

清醒检查

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()

当我们尝试运行这个Pyramid应用程序时,会得到一个回溯:

 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')

它告诉我们,我们试图添加一个引用不存在路径的视图。配置指令有时会向启动引入健全性检查,如本文所示。

配置冲突

让我们再次更改我们的应用程序。我们将撤消上一次更改,并添加一个配置语句,尝试添加另一个视图::

 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()

如果你注意到上面,我们现在调用来 add_view 两次使用两个不同的视图可调用文件。每次调用 add_view

 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 生成的 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

显然,必须能够解决配置冲突。有时这些冲突是错误的,所以很容易解决。您只需更改代码,使冲突不再存在。我们可以很容易地做到:

 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()

在上面的代码中,我们已经摆脱了冲突。现在 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! 但参观 /?use_hi=1 生产 Hi world! .

There's an alternative way to resolve conflicts that doesn't change the semantics of the code as much. 你可以发布一个 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 声明:

 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 两者之间 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()

如果我们运行这个应用程序,它将启动。如果我们访问 / 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 通过呼叫 config.commit() 明确地。当我们调用给 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. 为什么我们看到 Hi world! in our browser instead of Hello world! ?因为调用给 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.

调用 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 包括. We do that by adding a call to config.include() 在内部 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') 上面。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 模块:

 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 ?开始了。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! .

当我们呼唤 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 函数在 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_routeadd_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. 然而, config.include() does more than that. Please stick with me, we'll get to it.

这个 includeme() 公约

Now, let's change our app.py 略微。We'll change the config.include() 行在 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()

我们将编辑 another.py , changing the name of the moreconfiguration 函数到 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. 你可以参观 //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 moreconfigurationconfig.include('another.includeme') , we just pointed it at the module in which includeme 活着说 config.include('another') . This is a Pyramid convenience shorthand: if you tell Pyramid to include a Python module 或 包, it will assume that you're telling it to include the includeme function from within that module/package. 有效地, config.include('amodule') 总是意味着 config.include('amodule.includeme') .

嵌套包含

包含的内容也可以包括在内。让我们添加一个名为 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')

让我们改变一下 another.py 要包含的文件:

 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/whoa 并查看每个问题的答案。 app.py 包括 another.py 其中包括 yetanother.py . You can nest configuration includes within configuration includes ad infinitum. It's turtles all the way down.

自动分辨率VIA包括

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 包含一个 hi_world view function, and we'll change its includeme to add that view that should answer when / 访问::

 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 . 特别地, hi_worldhello_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 / 在浏览器中,您将看到 Hello world! (不是) Hi world! )The registration for the hello_world 视图 app.py "won" over the registration for the hi_world 视图 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 )想要 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') 在里面 app.py conflicts with the discriminator generated by config.add_view(hi_world, route_name='home') 在里面 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 对象。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. 使用 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_routeadd_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()

改变我们 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 以上,我们称之为 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 在里面 another.includeme . We added a function that uses the config.action method to register a discriminator and a callback for a custom 指令。让我们改变 app.py 再次向添加第二个调用 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()

当我们试图启动应用程序时,我们将得到这个回溯:

 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')

我们添加了一个使用 Pyramid 配置冲突检测的自定义指令。当我们尝试设置两次站点名称时, Pyramid 检测到一个冲突并告诉我们。与内置指令一样, Pyramid 自定义指令也将参与自动冲突解决。让我们把第一个电话转到 set_site_name into another included function. As a result, our 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
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 打印到控制台。No conflict will be raised, even though we have two calls to set_site_name 正在执行。This is because our custom directive is making use of automatic conflict resolution: Pyramid determines that the call to set_site_name('bar') 应该“赢”,因为它比设置为“bar”的其他调用“更接近应用程序的顶部”。

Why This Is Great

现在,我们来大致描述一下,是什么让这一切变得伟大。

您将注意到,在我们的micro应用程序中,仅仅导入一个模块不会导致添加任何类型的配置状态,我们现有的任何模块也不会依赖于在导入之前发生的某些配置。应用程序配置永远不会因为某个人或某物刚刚导入模块而被添加。这似乎是一个显而易见的设计选择,但并非所有Web框架都是如此。一些Web框架依赖于特定的导入顺序:在通过导入初始化其他模块之前,您可能无法成功导入应用程序代码。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.

大多数其他Web框架没有冲突检测系统,当它们被送入两个逻辑上冲突的配置语句时,它们将静默地选择其中一个,这让您有时会想知道为什么看不到预期的输出。同样,在大多数其他Web框架中,配置语句的执行顺序也非常重要; Pyramid 并没有让您非常关心它。

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 可包括的. He doesn't necessarily have to anticipate what 他的一些应用程序可能会被重写,只是 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. 这个 config.include() 系统甚至允许希望更改应用程序的第三方开发人员根本不考虑覆盖的机制;他只是在包含原始开发人员的配置语句之前或之后添加语句,并依靠自动冲突解决为他解决问题。

配置逻辑可以从任何地方包含,并跨多个包和文件系统位置拆分。没有一组特殊的pyramid-y“application”目录,其中包含的配置必须全部存在于一个位置。其他Web框架引入“比其他框架更特殊”的包或目录,以提供类似的功能。要扩展使用其他Web框架编写的应用程序,有时必须通过更改中心目录结构来添加到它们的集合中。

该系统是 meta-configurable. 您可以使用 config.add_directive() . 这意味着您可以有效地扩展 Pyramid 本身,而无需从头重写或重新编制解决方案:您只需告诉人们该指令存在,并告诉他们它与其他 Pyramid 指令一样工作。你也会得到冲突检测和解决的所有好处。

本文中的所有示例都使用“命令式” Pyramid 配置API,其中用户调用配置器对象上的方法来执行配置。为了开发人员的方便, Pyramid 还公开了声明性配置机制,通常通过提供通过 scan. 这样的修饰符只是将回调附加到他们正在修饰的对象上,在扫描过程中,这些回调被调用:回调只是代表用户在配置器上调用方法,就好像他自己键入了方法一样。这些修饰符参与 Pyramid 的配置方案,就像命令式方法调用一样。

有关的详细信息 config.include() and creating extensible applications, see Advanced ConfigurationExtending an Existing Pyramid Application Extending Pyramid Configuration .