/
configuration.py
609 lines (464 loc) · 22.3 KB
/
configuration.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
"""Configuration Helpers for TurboGears 2"""
import atexit
import os
import stat
import logging
from copy import copy
from UserDict import DictMixin
from pylons.i18n import ugettext
from genshi.filters import Translator
from pylons import config as pylons_config
from beaker.middleware import SessionMiddleware, CacheMiddleware
from paste.cascade import Cascade
from paste.registry import RegistryManager
from paste.urlparser import StaticURLParser
from paste.deploy.converters import asbool
from pylons.middleware import report_libs, StatusCodeRedirect
import tg
from tg import TGApp
from tg.error import ErrorHandler
from tg.util import Bunch, get_partial_dict
from routes import Mapper
from routes.middleware import RoutesMiddleware
from webob import Request
import mimetypes
from tw.api import make_middleware as tw_middleware
log = logging.getLogger(__name__)
class PylonsConfigWrapper(DictMixin):
"""Wrapper for the Pylons configuration.
Simple wrapper for the Pylons config object that provides attribute
style access to the Pylons config dictionary.
When used in TG, items with keys like "pylons.response_options" will
be available via config.pylons.response_options as well as
config['pylons.response_options'].
This class works by proxying all attribute and dictionary access to
the underlying Pylons config object, which is an application local
proxy that allows for multiple Pylons/TG2 applicatoins to live
in the same process simultaneously, but to always get the right
config data for the application that's requesting them.
Sites, with seeking to maximize needs may prefer to use the Pylons
config stacked object proxy directly, using just dictionary style
access, particularly whenever config is checked on a per-request basis.
"""
def __init__(self, dict_to_wrap):
"""Initialize the object by passing in pylons config to be wrapped"""
self.__dict__['config_proxy'] = dict_to_wrap
def __getitem__(self, key):
return self.config_proxy.current_conf()[key]
def __setitem__(self, key, value):
self.config_proxy.current_conf()[key] = value
def __getattr__(self, key):
"""Our custom attribute getter.
Tries to get the attribute off the wrapped object first,
if that does not work, tries dictionary lookup, and finally
tries to grab all keys that start with the attribute and
return sub-dictionaries that can be looked up.
"""
try:
return self.config_proxy.__getattribute__(key)
except AttributeError:
try:
return self.config_proxy.current_conf()[key]
except KeyError:
return get_partial_dict(key, self.config_proxy.current_conf())
def __setattr__(self, key, value):
self.config_proxy.current_conf()[key] = value
def __delattr__(self, name):
try:
del self.config_proxy.current_conf()[name]
except KeyError:
raise AttributeError(name)
def keys(self):
return self.config_proxy.keys()
#Create a config object that has attribute style lookup built in.
config = PylonsConfigWrapper(pylons_config)
class AppConfig(Bunch):
"""Class to store application configuration.
This class should have configuration/setup information
that is *necessary* for proper application function.
Deployment specific configuration information should go in
the config files (e.g. development.ini or deployment.ini).
AppConfig instances have a number of methods that are meant to be
overridden by users who wish to have finer grained control over
the setup of the WSGI envirnment in which their application is run.
This is the place to configure custom routes, transaction handling,
error handling, etc.
"""
def __init__(self):
"""Creates some configuration defaults"""
# Create a few bunches we know we'll use
self.paths = Bunch()
self.render_functions = Bunch()
# And also very often...
self.sa_auth = Bunch()
self.sa_auth.translations = Bunch()
#Set individual defaults
self.auto_reload_templates = True
self.auth_backend = None
self.default_renderer = 'genshi'
self.serve_static = True
self.stand_alone = True
# this is to activate the legacy renderers
# legacy renderers are buffet interface plugins
self.use_legacy_renderer = False
# if this is set to True the @expose decorator will be able to
# specify template names using a dotted name that will be searched
# in the python path. This option is used in tg.render.render_genshi
# TODO: we should set this to False once we implement simple names
# support in the @expose decorator as explained in #1942
# for the moment only the dotted names notation is supported with the
# new generation renderer functions
self.use_dotted_templatenames = True
self.use_toscawidgets = True
self.use_transaction_manager = True
#Registy for functions to be called on startup/teardown
self.call_on_startup = []
self.call_on_shutdown = []
# The codes TG should display an error page for. All other HTTP errors are
# sent to the client or left for some middleware above us to handle
self.handle_status_codes = [403, 404]
def setup_startup_and_shutdown(self):
for cmd in self.call_on_startup:
if callable(cmd):
try:
cmd()
except Exception, error:
log.debug("Error registering %s at startup: %s" % (cmd, error ))
else:
log.debug("Unable to register %s for startup" % cmd )
for cmd in self.call_on_shutdown:
if callable(cmd):
atexit.register(cmd)
else:
log.debug("Unable to register %s for shutdown" % cmd )
def setup_paths(self):
root = os.path.dirname(os.path.abspath(self.package.__file__))
# The default paths:
paths = Bunch(root=root,
controllers=os.path.join(root, 'controllers'),
static_files=os.path.join(root, 'public'),
templates=[os.path.join(root, 'templates')])
# If the user defined custom paths, then use them instead of the
# default ones:
paths.update(self.paths)
self.paths = paths
def init_config(self, global_conf, app_conf):
"""Initialize the config object.
tg.config is a proxy for pylons.config that allows attribute style
access, so it's automatically setup when we create the pylons config.
Besides basic initialization, this method copies all the values
in base_config into the ``pylons.config`` and ``tg.config`` objects.
"""
pylons_config.init_app(global_conf, app_conf,
package=self.package.__name__,
paths=self.paths)
config.update(self)
def setup_routes(self):
"""Setup the default TG2 routes
Overide this and setup your own routes maps if you want to use
custom routes.
"""
map = Mapper(directory=config['pylons.paths']['controllers'],
always_scan=config['debug'])
# Setup a default route for the root of object dispatch
map.connect('*url', controller='root', action='routes_placeholder')
config['routes.map'] = map
def setup_helpers_and_globals(self):
"""Add helpers and globals objects to the config.
Override this method to customize the way that ``app_globals``
and ``helpers`` are setup.
"""
config['pylons.app_globals'] = self.package.lib.app_globals.Globals()
config['pylons.helpers'] = self.package.lib.helpers
config['pylons.h'] = self.package.lib.helpers
def setup_sa_auth_backend(self):
"""This method adds sa_auth information to the config."""
defaults = {
'form_plugin': None
}
# The developer must have defined a 'sa_auth' section, because
# values such as the User, Group or Permission classes must be
# explicitly defined.
config['sa_auth'] = defaults
config['sa_auth'].update(self.sa_auth)
def setup_mako_renderer(self):
"""Setup a renderer and loader for mako templates.
Override this to customize the way that the mako template
renderer is setup. In particular if you want to setup
a different set of search paths, different encodings, or
additonal imports, all you need to do is update the
``TemplateLookup`` constructor.
You can also use your own render_mako function instead of the one
provided by tg.render.
"""
from tg.dottednamesupport import DottedTemplateLookup
from mako.lookup import TemplateLookup
from tg.render import render_mako
if config.get('use_dotted_templatenames', False):
# Support dotted names by injecting a slightly different template
# lookup system that will return templates from dotted template
# notation.
config['pylons.app_globals'].mako_lookup = DottedTemplateLookup(
input_encoding='utf-8', output_encoding='utf-8',
imports=['from webhelpers.html import escape'],
default_filters=['escape'])
else:
# If no dotted names support was required we will just setup
# a file system based template lookup mechanism.
config['pylons.app_globals'].mako_lookup = TemplateLookup(
directories=self.paths['templates'],
module_directory=self.paths['templates'],
input_encoding='utf-8', output_encoding='utf-8',
imports=['from webhelpers.html import escape'],
default_filters=['escape'],
filesystem_checks=self.auto_reload_templates)
self.render_functions.mako = render_mako
def setup_chameleongenshi_renderer(self):
"""Setup a renderer and loader for the chameleon.genshi engine."""
from chameleon.genshi.loader import TemplateLoader as ChameleonLoader
from tg.render import render_chameleon_genshi
loader = ChameleonLoader(search_path=self.paths.templates,
auto_reload=self.auto_reload_templates)
config['pylons.app_globals'].chameleon_genshi_loader = loader
self.render_functions.chameleon_genshi = render_chameleon_genshi
def setup_genshi_renderer(self):
"""Setup a renderer and loader for Genshi templates.
Override this to customize the way that the internationalization
filter, template loader
"""
from genshi.template import TemplateLoader
from tg.render import render_genshi
def template_loaded(template):
"""Plug-in our i18n function to Genshi, once the template is loaded.
This function will be called by genshi TemplateLoader after
loading the template.
"""
template.filters.insert(0, Translator(ugettext))
loader = TemplateLoader(search_path=self.paths.templates,
auto_reload=self.auto_reload_templates,
callback=template_loaded)
config['pylons.app_globals'].genshi_loader = loader
self.render_functions.genshi = render_genshi
def setup_jinja_renderer(self):
"""Setup a renderer and loader for Jinja templates."""
from jinja import ChoiceLoader, Environment, FileSystemLoader
from tg.render import render_jinja
config['pylons.app_globals'].jinja2_env = Environment(loader=ChoiceLoader(
[FileSystemLoader(path, auto_reload=self.auto_reload_templates)
for path in self.paths['templates']]))
# Jinja's unable to request c's attributes without strict_c
config['pylons.strict_c'] = True
self.render_functions.jinja = render_jinja
def setup_default_renderer(self):
"""Setup template defaults in the buffed plugin.
This is only used when use_legacy_renderer is set to True
and it will not get deprecated in the next major TurboGears release.
"""
#T his is specific to buffet, will not be needed later
config['buffet.template_engines'].pop()
template_location = '%s.templates' %self.package.__name__
template_location = '%s.templates' % self.package.__name__
def template_loaded(template):
template.filters.insert(0, Translator(ugettext))
# Set some default options for genshi
options = {
'genshi.loader_callback': template_loaded,
'genshi.default_format': 'xhtml',
}
# Override those options from config
config['buffet.template_options'].update(options)
config.add_template_engine(self.default_renderer,
template_location, {})
def setup_mimetypes(self):
lookup = {'.json':'application/json'}
lookup.update(config.get('mimetype_lookup', {}))
for key, value in lookup.iteritems():
mimetypes.add_type(value, key)
def setup_sqlalchemy(self):
"""Setup SQLAlchemy database engine."""
from sqlalchemy import engine_from_config
engine = engine_from_config(pylons_config, 'sqlalchemy.')
config['pylons.app_globals'].sa_engine = engine
# Pass the engine to initmodel, to be able to introspect tables
self.package.model.init_model(engine)
def make_load_environment(self):
"""Return a load_environment function.
The returned load_environment function can be called to configure
the TurboGears runtime environment for this particular application.
You can do this dynamically with multiple nested TG applications
if necessary.
"""
def load_environment(global_conf, app_conf):
"""Configure the Pylons environment via ``pylons.config``."""
global_conf=Bunch(global_conf)
app_conf=Bunch(app_conf)
#Regesters functions to be called at startup and shutdown
#from self.call_on_startup and shutdown respectively.
self.setup_startup_and_shutdown()
self.setup_paths()
self.init_config(global_conf, app_conf)
self.setup_routes()
self.setup_helpers_and_globals()
self.setup_mimetypes()
if self.auth_backend == "sqlalchemy":
self.setup_sa_auth_backend()
if 'genshi' in self.renderers:
self.setup_genshi_renderer()
if 'chameleon_genshi' in self.renderers:
self.setup_chameleongenshi_renderer()
if 'mako' in self.renderers:
self.setup_mako_renderer()
if 'jinja' in self.renderers:
self.setup_jinja_renderer()
if self.use_legacy_renderer:
self.setup_default_renderer()
if self.use_sqlalchemy:
self.setup_sqlalchemy()
return load_environment
def add_error_middleware(self, global_conf, app):
"""Add middleware which handles errors and exceptions."""
app = ErrorHandler(app, global_conf, **config['pylons.errorware'])
# Display error documents for self.handle_status_codes status codes (and
# 500 when debug is disabled)
if asbool(config['debug']):
app = StatusCodeRedirect(app, self.handle_status_codes)
else:
app = StatusCodeRedirect(app, self.handle_status_codes + [500])
return app
def add_auth_middleware(self, app):
"""Configure authentication and authorization."""
from repoze.what.plugins.quickstart import setup_sql_auth
from repoze.what.plugins.pylonshq import booleanize_predicates
# Predicates booleanized:
booleanize_predicates()
# Configuring auth logging:
if 'log_stream' not in self.sa_auth:
self.sa_auth['log_stream'] = logging.getLogger('auth')
# Removing keywords not used by repoze.who:
auth_args = copy(self.sa_auth)
if 'password_encryption_method' in auth_args:
del auth_args['password_encryption_method']
app = setup_sql_auth(app, **auth_args)
return app
def add_core_middleware(self, app):
"""Add support for routes dispatch, sessions, and caching."""
app = RoutesMiddleware(app, config['routes.map'])
app = SessionMiddleware(app, config)
app = CacheMiddleware(app, config)
return app
def add_tosca_middleware(self, app):
"""Configure the ToscaWidgets middleware."""
app = tw_middleware(app, {
'toscawidgets.framework.default_view':
self.default_renderer,
'toscawidgets.middleware.inject_resources': True,
})
return app
def add_static_file_middleware(self, app):
static_app = StaticURLParser(config['pylons.paths']['static_files'])
app = Cascade([static_app, app])
return app
def commit_veto(self, environ, status, headers):
"""Veto a commit.
This hook is called by repoze.tm in case we want to veto a commit
for some reason. Return True to force a rollback.
By default we veto if the response's status code is an error code.
Override this method, or monkey patch the instancemethod, to fine
tune this behaviour.
"""
return not 200 <= int(status.split(None, 1)[0]) < 400
def add_tm_middleware(self, app):
"""Set up the transaction managment middleware.
To abort a transaction inside a TG2 app::
import transaction
transaction.doom()
By default http error responses also roll back transactions, but this
behavior can be overridden by overriding base_config.commit_veto.
"""
from repoze.tm import make_tm
return make_tm(app, self.commit_veto)
def add_dbsession_remover_middleware(self, app):
"""Set up middleware that cleans up the sqlalchemy session.
The default behavior of TG 2 is to clean up the session on every
request. Only override this method if you know what you are doing!
"""
def remover(environ, start_response):
try:
return app(environ, start_response)
finally:
log.debug("Removing DBSession from current thread")
self.DBSession.remove()
return remover
def setup_tg_wsgi_app(self, load_environment):
"""Create a base TG app, with all the standard middleware.
``load_environment``
A required callable, which sets up the basic evironment
needed for the application.
``setup_vars``
A dictionary with all special values necessary for setting up
the base wsgi app.
"""
def make_base_app(global_conf, wrap_app=None, full_stack=True, **app_conf):
"""Create a tg WSGI application and return it.
``wrap_app``
a WSGI middleware component which takes the core turbogears
application and wraps it -- inside all the WSGI-components
provided by TG and Pylons. This allows you to work with the
full environment that your TG application would get before
anything happens in the application itself.
``global_conf``
The inherited configuration for this application. Normally
from the [DEFAULT] section of the Paste ini file.
``full_stack``
Whether or not this application provides a full WSGI stack (by
default, meaning it handles its own exceptions and errors).
Disable full_stack when this application is "managed" by
another WSGI middleware.
``app_conf``
The application's local configuration. Normally specified in
the [app:<name>] section of the Paste ini file (where <name>
defaults to main).
"""
# Configure the Pylons environment
load_environment(global_conf, app_conf)
app = TGApp()
if wrap_app:
app = wrap_app(app)
app = self.add_core_middleware(app)
if self.use_toscawidgets:
app = self.add_tosca_middleware(app)
if self.auth_backend == "sqlalchemy":
app = self.add_auth_middleware(app)
if self.use_transaction_manager:
app = self.add_tm_middleware(app)
if self.use_sqlalchemy:
if not hasattr(self, 'DBSession'):
# If the user hasn't specified a scoped_session, assume
# he/she uses the default DBSession in model
self.DBSession = self.model.DBSession
app = self.add_dbsession_remover_middleware(app)
app = maybe_make_body_seekable(app)
if asbool(full_stack):
if (self.auth_backend is None
and 401 not in self.handle_status_codes):
# If there's no auth backend configured which traps 401
# responses we redirect those responses to a nicely
# formatted error page
self.handle_status_codes.append(401)
# This should nevery be true for internal nested apps
app = self.add_error_middleware(global_conf, app)
# Establish the registry for this application
app = RegistryManager(app)
# Static files (if running in production, and Apache or another
# web server is serving static files)
if self.serve_static:
app = self.add_static_file_middleware(app)
return app
return make_base_app
def maybe_make_body_seekable(app):
def wrapper(environ, start_response):
if pylons_config.get('make_body_seekable'):
log.debug("Making request body seekable")
Request(environ).make_body_seekable()
return app(environ, start_response)
return wrapper