Menu

[r7609]: / branches / mathtex / lib / matplotlib / config / tconfig.py  Maximize  Restore  History

Download this file

636 lines (487 with data), 21.1 kB

  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
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
"""Mix of Traits and ConfigObj.
Provides:
- Coupling a Traits object to a ConfigObj one, so that changes to the Traited
instance propagate back into the ConfigObj.
- A declarative interface for describing configurations that automatically maps
to valid ConfigObj representations.
- From these descriptions, valid .conf files can be auto-generated, with class
docstrings and traits information used for initial auto-documentation.
- Hierarchical inclusion of files, so that a base config can be overridden only
in specific spots.
- Automatic GUI editing of configuration objects.
Notes:
The file creation policy is:
1. Creating a TConfigManager(FooConfig,'missingfile.conf') will work
fine, and 'missingfile.conf' will be created empty.
2. Creating TConfigManager(FooConfig,'OKfile.conf') where OKfile.conf has
include = 'missingfile.conf'
conks out with IOError.
My rationale is that creating top-level empty files is a common and
reasonable need, but that having invalid include statements should
raise an error right away, so people know immediately that their files
have gone stale.
TODO:
- Turn the currently interactive tests into proper doc/unit tests. Complete
docstrings.
- Write the real ipython1 config system using this. That one is more
complicated than either the MPL one or the fake 'ipythontest' that I wrote
here, and it requires solving the issue of declaring references to other
objects inside the config files.
- [Low priority] Write a custom TraitsUI view so that hierarchical
configurations provide nicer interactive editing. The automatic system is
remarkably good, but for very complex configurations having a nicely
organized view would be nice.
"""
__license__ = 'BSD'
############################################################################
# Stdlib imports
############################################################################
from cStringIO import StringIO
from inspect import isclass
import os
import textwrap
############################################################################
# External imports
############################################################################
from enthought.traits import api as T
# For now we ship this internally so users don't have to download it, since
# it's just a single-file dependency.
import configobj
############################################################################
# Utility functions
############################################################################
def get_split_ind(seq, N):
"""seq is a list of words. Return the index into seq such that
len(' '.join(seq[:ind])<=N
"""
sLen = 0
# todo: use Alex's xrange pattern from the cbook for efficiency
for (word, ind) in zip(seq, range(len(seq))):
sLen += len(word) + 1 # +1 to account for the len(' ')
if sLen>=N: return ind
return len(seq)
def wrap(prefix, text, cols, max_lines=6):
'wrap text with prefix at length cols'
pad = ' '*len(prefix.expandtabs())
available = cols - len(pad)
seq = text.split(' ')
Nseq = len(seq)
ind = 0
lines = []
while ind<Nseq:
lastInd = ind
ind += get_split_ind(seq[ind:], available)
lines.append(seq[lastInd:ind])
num_lines = len(lines)
abbr_end = max_lines // 2
abbr_start = max_lines - abbr_end
lines_skipped = False
for i in range(num_lines):
if i == 0:
# add the prefix to the first line, pad with spaces otherwise
ret = prefix + ' '.join(lines[i]) + '\n'
elif i < abbr_start or i > num_lines-abbr_end-1:
ret += pad + ' '.join(lines[i]) + '\n'
else:
if not lines_skipped:
lines_skipped = True
ret += ' <...snipped %d lines...> \n' % (num_lines-max_lines)
# for line in lines[1:]:
# ret += pad + ' '.join(line) + '\n'
return ret[:-1]
def dedent(txt):
"""A modified version of textwrap.dedent, specialized for docstrings.
This version doesn't get confused by the first line of text having
inconsistent indentation from the rest, which happens a lot in docstrings.
:Examples:
>>> s = '''
... First line.
... More...
... End'''
>>> print dedent(s)
First line.
More...
End
>>> s = '''First line
... More...
... End'''
>>> print dedent(s)
First line
More...
End
"""
out = [textwrap.dedent(t) for t in txt.split('\n',1)
if t and not t.isspace()]
return '\n'.join(out)
def comment(strng,indent=''):
"""return an input string, commented out"""
template = indent + '# %s'
lines = [template % s for s in strng.splitlines(True)]
return ''.join(lines)
def configObj2Str(cobj):
"""Dump a Configobj instance to a string."""
outstr = StringIO()
cobj.write(outstr)
return outstr.getvalue()
def getConfigFilename(conf):
"""Find the filename attribute of a ConfigObj given a sub-section object.
"""
depth = conf.depth
for d in range(depth):
conf = conf.parent
return conf.filename
def tconf2File(tconf,fname,force=False):
"""Write a TConfig instance to a given filename.
:Keywords:
force : bool (False)
If true, force writing even if the file exists.
"""
if os.path.isfile(fname) and not force:
raise IOError("File %s already exists, use force=True to overwrite" %
fname)
txt = repr(tconf)
fobj = open(fname,'w')
fobj.write(txt)
fobj.close()
def filter_scalars(sc):
""" input sc MUST be sorted!!!"""
scalars = []
maxi = len(sc)-1
i = 0
while i<len(sc):
t = sc[i]
if t.startswith('_tconf_'):
# Skip altogether private _tconf_ attributes, so we actually issue
# a 'continue' call to avoid the append(t) below
i += 1
continue
if i<maxi and t+'_' == sc[i+1]:
# skip one ahead in the loop, to skip over the names of shadow
# traits, which we don't want to expose in the config files.
i += 1
scalars.append(t)
i += 1
return scalars
def get_scalars(obj):
"""Return scalars for a TConf class object"""
skip = set(['trait_added','trait_modified'])
sc = [k for k in obj.trait_names() if k not in skip]
sc.sort()
return filter_scalars(sc)
def get_sections(obj,sectionClass):
"""Return sections for a TConf class object"""
return [(n,v) for (n,v) in obj.__dict__.iteritems()
if isclass(v) and issubclass(v,sectionClass)]
def get_instance_sections(inst):
"""Return sections for a TConf instance"""
sections = [(k,v) for k,v in inst.__dict__.iteritems()
if isinstance(v,TConfig) and not k=='_tconf_parent']
# Sort the sections by name
sections.sort(key=lambda x:x[0])
return sections
def partition_instance(obj):
"""Return scalars,sections for a given TConf instance.
"""
scnames = []
sections = []
for k,v in obj.__dict__.iteritems():
if isinstance(v,TConfig):
if not k=='_tconf_parent':
sections.append((k,v))
else:
scnames.append(k)
# Sort the sections by name
sections.sort(key=lambda x:x[0])
# Sort the scalar names, filter them and then extract the actual objects
scnames.sort()
scnames = filter_scalars(scnames)
scalars = [(s,obj.__dict__[s]) for s in scnames]
return scalars, sections
def mkConfigObj(filename,makeMissingFile=True):
"""Return a ConfigObj instance with our hardcoded conventions.
Use a simple factory that wraps our option choices for using ConfigObj.
I'm hard-wiring certain choices here, so we'll always use instances with
THESE choices.
:Parameters:
filename : string
File to read from.
:Keywords:
makeMissingFile : bool (True)
If true, the file named by `filename` may not yet exist and it will be
automatically created (empty). Else, if `filename` doesn't exist, an
IOError will be raised.
"""
if makeMissingFile:
create_empty = True
file_error = False
else:
create_empty = False
file_error = True
return configobj.ConfigObj(filename,
create_empty=create_empty,
file_error=file_error,
indent_type=' ',
interpolation='Template',
unrepr=True)
nullConf = mkConfigObj(None)
class RecursiveConfigObj(object):
"""Object-oriented interface for recursive ConfigObj constructions."""
def __init__(self,filename):
"""Return a ConfigObj instance with our hardcoded conventions.
Use a simple factory that wraps our option choices for using ConfigObj.
I'm hard-wiring certain choices here, so we'll always use instances with
THESE choices.
:Parameters:
filename : string
File to read from.
"""
self.comp = []
self.conf = self._load(filename)
def _load(self,filename,makeMissingFile=True):
conf = mkConfigObj(filename,makeMissingFile)
# Do recursive loading. We only allow (or at least honor) the include
# tag at the top-level. For now, we drop the inclusion information so
# that there are no restrictions on which levels of the TConfig
# hierarchy can use include statements. But this means that
# if bookkeeping of each separate component of the recursive
# construction was requested, make a separate object for storage
# there, since we don't want that to be modified by the inclusion
# process.
self.comp.append(mkConfigObj(filename,makeMissingFile))
incfname = conf.pop('include',None)
if incfname is not None:
# Do recursive load. We don't want user includes that point to
# missing files to fail silently, so in the recursion we disable
# auto-creation of missing files.
confinc = self._load(incfname,makeMissingFile=False)
# Update with self to get proper ordering (included files provide
# base data, current one overwrites)
confinc.update(conf)
# And do swap to return the updated structure
conf = confinc
# Set the filename to be the original file instead of the included
# one
conf.filename = filename
return conf
############################################################################
# Main TConfig class and supporting exceptions
############################################################################
class TConfigError(Exception): pass
class TConfigInvalidKeyError(TConfigError): pass
class TConfig(T.HasStrictTraits):
"""A class representing configuration objects.
Note: this class should NOT have any traits itself, since the actual traits
will be declared by subclasses. This class is meant to ONLY declare the
necessary initialization/validation methods. """
# Any traits declared here are prefixed with _tconf_ so that our special
# formatting/analysis utilities can distinguish them from user traits and
# can avoid them.
# Once created, the tree's hierarchy can NOT be modified
_tconf_parent = T.ReadOnly
def __init__(self,config=None,parent=None,monitor=None):
"""Makes a Traited config object out of a ConfigObj instance
"""
if config is None:
config = mkConfigObj(None)
# Validate the set of scalars ...
my_scalars = set(get_scalars(self))
cf_scalars = set(config.scalars)
invalid_scalars = cf_scalars - my_scalars
if invalid_scalars:
config_fname = getConfigFilename(config)
m=("In config defined in file: %r\n"
"Error processing section: %s\n"
"These keys are invalid : %s\n"
"Valid key names : %s\n"
% (config_fname,self.__class__.__name__,
list(invalid_scalars),list(my_scalars)))
raise TConfigInvalidKeyError(m)
# ... and sections
section_items = get_sections(self.__class__,TConfig)
my_sections = set([n for n,v in section_items])
cf_sections = set(config.sections)
invalid_sections = cf_sections - my_sections
if invalid_sections:
config_fname = getConfigFilename(config)
m=("In config defined in file: %r\n"
"Error processing section: %s\n"
"These subsections are invalid : %s\n"
"Valid subsection names : %s\n"
% (config_fname,self.__class__.__name__,
list(invalid_sections),list(my_sections)))
raise TConfigInvalidKeyError(m)
self._tconf_parent = parent
# Now set the traits based on the config
try:
for k in my_scalars:
try:
setattr(self,k,config[k])
except KeyError:
# This seems silly, but it forces some of Trait's magic to
# fire and actually set the value on the instance in such a
# way that it will later be properly read by introspection
# tools.
getattr(self,k)
scal = getattr(self,k)
except T.TraitError,e:
t = self.__class_traits__[k]
msg = "Bad key,value pair given: %s -> %s\n" % (k,config[k])
msg += "Expected type: %s" % t.handler.info()
raise TConfigError(msg)
# And build subsections
for s,v in section_items:
sec_config = config.setdefault(s,{})
section = v(sec_config,self,monitor=monitor)
# We must use add_trait instead of setattr because we inherit from
# HasStrictTraits, but we need to then do a 'dummy' getattr call on
# self so the class trait propagates to the instance.
self.add_trait(s,section)
getattr(self,s)
if monitor:
#print 'Adding monitor to:',self.__class__.__name__ # dbg
self.on_trait_change(monitor)
def __repr__(self,depth=0):
"""Dump a section to a string."""
indent = ' '*(depth)
top_name = self.__class__.__name__
if depth == 0:
label = '# %s - plaintext (in .conf format)\n' % top_name
else:
# Section titles are indented one level less than their contents in
# the ConfigObj write methods.
sec_indent = ' '*(depth-1)
label = '\n'+sec_indent+('[' * depth) + top_name + (']'*depth)
out = [label]
doc = self.__class__.__doc__
if doc is not None:
out.append(comment(dedent(doc),indent))
scalars, sections = partition_instance(self)
for s,v in scalars:
try:
info = self.__base_traits__[s].handler.info()
# Get a short version of info with lines of max. 78 chars, so
# that after commenting them out (with '# ') they are at most
# 80-chars long.
out.append(comment(wrap('',info.replace('\n', ' '),78-len(indent)),indent))
except (KeyError,AttributeError):
pass
out.append(indent+('%s = %r' % (s,v)))
for sname,sec in sections:
out.append(sec.__repr__(depth+1))
return '\n'.join(out)
def __str__(self):
return self.__class__.__name__
##############################################################################
# High-level class(es) and utilities for handling a coupled pair of TConfig and
# ConfigObj instances.
##############################################################################
def path_to_root(obj):
"""Find the path to the root of a nested TConfig instance."""
ob = obj
path = []
while ob._tconf_parent is not None:
path.append(ob.__class__.__name__)
ob = ob._tconf_parent
path.reverse()
return path
def set_value(fconf,path,key,value):
"""Set a value on a ConfigObj instance, arbitrarily deep."""
section = fconf
for sname in path:
section = section.setdefault(sname,{})
section[key] = value
def fmonitor(fconf):
"""Make a monitor for coupling TConfig instances to ConfigObj ones.
We must use a closure because Traits makes assumptions about the functions
used with on_trait_change() that prevent the use of a callable instance.
"""
def mon(obj,name,new):
#print 'OBJ:',obj # dbg
#print 'NAM:',name # dbg
#print 'NEW:',new # dbg
set_value(fconf,path_to_root(obj),name,new)
return mon
class TConfigManager(object):
"""A simple object to manage and sync a TConfig and a ConfigObj pair.
"""
def __init__(self,configClass,configFilename,filePriority=True):
"""Make a new TConfigManager.
:Parameters:
configClass : class
configFilename : string
If the filename points to a non-existent file, it will be created
empty. This is useful when creating a file form from an existing
configClass with the class defaults.
:Keywords:
filePriority : bool (True)
If true, at construction time the file object takes priority and
overwrites the contents of the config object. Else, the data flow
is reversed and the file object will be overwritten with the
configClass defaults at write() time.
"""
rconf = RecursiveConfigObj(configFilename)
# In a hierarchical object, the two following fconfs are *very*
# different. In self.fconf, we'll keep the outer-most fconf associated
# directly to the original filename. self.fconfCombined, instead,
# contains an object which has the combined effect of having merged all
# the called files in the recursive chain.
self.fconf = rconf.comp[0]
self.fconfCombined = rconf.conf
# Create a monitor to track and apply trait changes to the tconf
# instance over into the fconf one
monitor = fmonitor(self.fconf)
if filePriority:
self.tconf = configClass(self.fconfCombined,monitor=monitor)
else:
# Push defaults onto file object
self.tconf = configClass(mkConfigObj(None),monitor=monitor)
self.fconfUpdate(self.fconf,self.tconf)
def fconfUpdate(self,fconf,tconf):
"""Update the fconf object with the data from tconf"""
scalars, sections = partition_instance(tconf)
for s,v in scalars:
fconf[s] = v
for secname,sec in sections:
self.fconfUpdate(fconf.setdefault(secname,{}),sec)
def write(self,filename=None):
"""Write out to disk.
This method writes out only to the top file in a hierarchical
configuration, which means that the class defaults and other values not
explicitly set in the top level file are NOT written out.
:Keywords:
filename : string (None)
If given, the output is written to this file, otherwise the
.filename attribute of the top-level configuration object is used.
"""
if filename is not None:
fileObj = open(filename,'w')
out = self.fconf.write(fileObj)
fileObj.close()
return out
else:
return self.fconf.write()
def writeAll(self,filename=None):
"""Write out the entire configuration to disk.
This method, in contrast with write(), updates the .fconfCombined
object with the *entire* .tconf instance, and then writes it out to
disk. This method is thus useful for generating files that have a
self-contained, non-hierarchical file.
:Keywords:
filename : string (None)
If given, the output is written to this file, otherwise the
.filename attribute of the top-level configuration object is used.
"""
if filename is not None:
fileObj = open(filename,'w')
self.fconfUpdate(self.fconfCombined,self.tconf)
out = self.fconfCombined.write(fileObj)
fileObj.close()
return out
else:
self.fconfUpdate(self.fconfCombined,self.tconf)
return self.fconfCombined.write()
def tconfStr(self):
return str(self.tconf)
def fconfStr(self):
return configObj2Str(self.fconf)
__repr__ = __str__ = fconfStr