##// END OF EJS Templates
This is a manual merge of certain things in the ipython1-dev branch, revision 46, into the main ...
Brian E Granger -
r1234:52b55407
parent child
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -0,0 +1,14
1 # encoding: utf-8
2
3 __docformat__ = "restructuredtext en"
4
5 #-------------------------------------------------------------------------------
6 # Copyright (C) 2008 The IPython Development Team
7 #
8 # Distributed under the terms of the BSD License. The full license is in
9 # the file COPYING, distributed as part of this software.
10 #-------------------------------------------------------------------------------
11
12 #-------------------------------------------------------------------------------
13 # Imports
14 #------------------------------------------------------------------------------- No newline at end of file
@@ -0,0 +1,99
1 # encoding: utf-8
2
3 """This is the official entry point to IPython's configuration system. """
4
5 __docformat__ = "restructuredtext en"
6
7 #-------------------------------------------------------------------------------
8 # Copyright (C) 2008 The IPython Development Team
9 #
10 # Distributed under the terms of the BSD License. The full license is in
11 # the file COPYING, distributed as part of this software.
12 #-------------------------------------------------------------------------------
13
14 #-------------------------------------------------------------------------------
15 # Imports
16 #-------------------------------------------------------------------------------
17
18 import os
19 from IPython.config.cutils import get_home_dir, get_ipython_dir
20 from IPython.external.configobj import ConfigObj
21
22 class ConfigObjManager(object):
23
24 def __init__(self, configObj, filename):
25 self.current = configObj
26 self.current.indent_type = ' '
27 self.filename = filename
28 # self.write_default_config_file()
29
30 def get_config_obj(self):
31 return self.current
32
33 def update_config_obj(self, newConfig):
34 self.current.merge(newConfig)
35
36 def update_config_obj_from_file(self, filename):
37 newConfig = ConfigObj(filename, file_error=False)
38 self.current.merge(newConfig)
39
40 def update_config_obj_from_default_file(self, ipythondir=None):
41 fname = self.resolve_file_path(self.filename, ipythondir)
42 self.update_config_obj_from_file(fname)
43
44 def write_config_obj_to_file(self, filename):
45 f = open(filename, 'w')
46 self.current.write(f)
47 f.close()
48
49 def write_default_config_file(self):
50 ipdir = get_ipython_dir()
51 fname = ipdir + '/' + self.filename
52 if not os.path.isfile(fname):
53 print "Writing the configuration file to: " + fname
54 self.write_config_obj_to_file(fname)
55
56 def _import(self, key):
57 package = '.'.join(key.split('.')[0:-1])
58 obj = key.split('.')[-1]
59 execString = 'from %s import %s' % (package, obj)
60 exec execString
61 exec 'temp = %s' % obj
62 return temp
63
64 def resolve_file_path(self, filename, ipythondir = None):
65 """Resolve filenames into absolute paths.
66
67 This function looks in the following directories in order:
68
69 1. In the current working directory or by absolute path with ~ expanded
70 2. In ipythondir if that is set
71 3. In the IPYTHONDIR environment variable if it exists
72 4. In the ~/.ipython directory
73
74 Note: The IPYTHONDIR is also used by the trunk version of IPython so
75 changing it will also affect it was well.
76 """
77
78 # In cwd or by absolute path with ~ expanded
79 trythis = os.path.expanduser(filename)
80 if os.path.isfile(trythis):
81 return trythis
82
83 # In ipythondir if it is set
84 if ipythondir is not None:
85 trythis = ipythondir + '/' + filename
86 if os.path.isfile(trythis):
87 return trythis
88
89 trythis = get_ipython_dir() + '/' + filename
90 if os.path.isfile(trythis):
91 return trythis
92
93 return None
94
95
96
97
98
99
@@ -0,0 +1,99
1 # encoding: utf-8
2
3 """Configuration-related utilities for all IPython."""
4
5 __docformat__ = "restructuredtext en"
6
7 #-------------------------------------------------------------------------------
8 # Copyright (C) 2008 The IPython Development Team
9 #
10 # Distributed under the terms of the BSD License. The full license is in
11 # the file COPYING, distributed as part of this software.
12 #-------------------------------------------------------------------------------
13
14 #-------------------------------------------------------------------------------
15 # Imports
16 #-------------------------------------------------------------------------------
17
18 import os
19 import sys
20
21 #---------------------------------------------------------------------------
22 # Normal code begins
23 #---------------------------------------------------------------------------
24
25 class HomeDirError(Exception):
26 pass
27
28 def get_home_dir():
29 """Return the closest possible equivalent to a 'home' directory.
30
31 We first try $HOME. Absent that, on NT it's $HOMEDRIVE\$HOMEPATH.
32
33 Currently only Posix and NT are implemented, a HomeDirError exception is
34 raised for all other OSes. """
35
36 isdir = os.path.isdir
37 env = os.environ
38 try:
39 homedir = env['HOME']
40 if not isdir(homedir):
41 # in case a user stuck some string which does NOT resolve to a
42 # valid path, it's as good as if we hadn't foud it
43 raise KeyError
44 return homedir
45 except KeyError:
46 if os.name == 'posix':
47 raise HomeDirError,'undefined $HOME, IPython can not proceed.'
48 elif os.name == 'nt':
49 # For some strange reason, win9x returns 'nt' for os.name.
50 try:
51 homedir = os.path.join(env['HOMEDRIVE'],env['HOMEPATH'])
52 if not isdir(homedir):
53 homedir = os.path.join(env['USERPROFILE'])
54 if not isdir(homedir):
55 raise HomeDirError
56 return homedir
57 except:
58 try:
59 # Use the registry to get the 'My Documents' folder.
60 import _winreg as wreg
61 key = wreg.OpenKey(wreg.HKEY_CURRENT_USER,
62 "Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders")
63 homedir = wreg.QueryValueEx(key,'Personal')[0]
64 key.Close()
65 if not isdir(homedir):
66 e = ('Invalid "Personal" folder registry key '
67 'typically "My Documents".\n'
68 'Value: %s\n'
69 'This is not a valid directory on your system.' %
70 homedir)
71 raise HomeDirError(e)
72 return homedir
73 except HomeDirError:
74 raise
75 except:
76 return 'C:\\'
77 elif os.name == 'dos':
78 # Desperate, may do absurd things in classic MacOS. May work under DOS.
79 return 'C:\\'
80 else:
81 raise HomeDirError,'support for your operating system not implemented.'
82
83 def get_ipython_dir():
84 ipdir_def = '.ipython'
85 home_dir = get_home_dir()
86 ipdir = os.path.abspath(os.environ.get('IPYTHONDIR',
87 os.path.join(home_dir,ipdir_def)))
88 return ipdir
89
90 def import_item(key):
91 """
92 Import and return bar given the string foo.bar.
93 """
94 package = '.'.join(key.split('.')[0:-1])
95 obj = key.split('.')[-1]
96 execString = 'from %s import %s' % (package, obj)
97 exec execString
98 exec 'temp = %s' % obj
99 return temp
This diff has been collapsed as it changes many lines, (622 lines changed) Show them Hide them
@@ -0,0 +1,622
1 # encoding: utf-8
2
3 """Mix of ConfigObj and Struct-like access.
4
5 Provides:
6
7 - Coupling a Struct object to a ConfigObj one, so that changes to the Traited
8 instance propagate back into the ConfigObj.
9
10 - A declarative interface for describing configurations that automatically maps
11 to valid ConfigObj representations.
12
13 - From these descriptions, valid .conf files can be auto-generated, with class
14 docstrings and traits information used for initial auto-documentation.
15
16 - Hierarchical inclusion of files, so that a base config can be overridden only
17 in specific spots.
18
19
20 Notes:
21
22 The file creation policy is:
23
24 1. Creating a SConfigManager(FooConfig,'missingfile.conf') will work
25 fine, and 'missingfile.conf' will be created empty.
26
27 2. Creating SConfigManager(FooConfig,'OKfile.conf') where OKfile.conf has
28
29 include = 'missingfile.conf'
30
31 conks out with IOError.
32
33 My rationale is that creating top-level empty files is a common and
34 reasonable need, but that having invalid include statements should
35 raise an error right away, so people know immediately that their files
36 have gone stale.
37
38
39 TODO:
40
41 - Turn the currently interactive tests into proper doc/unit tests. Complete
42 docstrings.
43
44 - Write the real ipython1 config system using this. That one is more
45 complicated than either the MPL one or the fake 'ipythontest' that I wrote
46 here, and it requires solving the issue of declaring references to other
47 objects inside the config files.
48
49 - [Low priority] Write a custom TraitsUI view so that hierarchical
50 configurations provide nicer interactive editing. The automatic system is
51 remarkably good, but for very complex configurations having a nicely
52 organized view would be nice.
53 """
54
55 __docformat__ = "restructuredtext en"
56 __license__ = 'BSD'
57
58 #-------------------------------------------------------------------------------
59 # Copyright (C) 2008 The IPython Development Team
60 #
61 # Distributed under the terms of the BSD License. The full license is in
62 # the file COPYING, distributed as part of this software.
63 #-------------------------------------------------------------------------------
64
65 #-------------------------------------------------------------------------------
66 # Imports
67 #-------------------------------------------------------------------------------
68
69 ############################################################################
70 # Stdlib imports
71 ############################################################################
72 from cStringIO import StringIO
73 from inspect import isclass
74
75 import os
76 import textwrap
77
78 ############################################################################
79 # External imports
80 ############################################################################
81
82 from IPython.external import configobj
83
84 ############################################################################
85 # Utility functions
86 ############################################################################
87
88 def get_split_ind(seq, N):
89 """seq is a list of words. Return the index into seq such that
90 len(' '.join(seq[:ind])<=N
91 """
92
93 sLen = 0
94 # todo: use Alex's xrange pattern from the cbook for efficiency
95 for (word, ind) in zip(seq, range(len(seq))):
96 sLen += len(word) + 1 # +1 to account for the len(' ')
97 if sLen>=N: return ind
98 return len(seq)
99
100 def wrap(prefix, text, cols, max_lines=6):
101 """'wrap text with prefix at length cols"""
102 pad = ' '*len(prefix.expandtabs())
103 available = cols - len(pad)
104
105 seq = text.split(' ')
106 Nseq = len(seq)
107 ind = 0
108 lines = []
109 while ind<Nseq:
110 lastInd = ind
111 ind += get_split_ind(seq[ind:], available)
112 lines.append(seq[lastInd:ind])
113
114 num_lines = len(lines)
115 abbr_end = max_lines // 2
116 abbr_start = max_lines - abbr_end
117 lines_skipped = False
118 for i in range(num_lines):
119 if i == 0:
120 # add the prefix to the first line, pad with spaces otherwise
121 ret = prefix + ' '.join(lines[i]) + '\n'
122 elif i < abbr_start or i > num_lines-abbr_end-1:
123 ret += pad + ' '.join(lines[i]) + '\n'
124 else:
125 if not lines_skipped:
126 lines_skipped = True
127 ret += ' <...snipped %d lines...> \n' % (num_lines-max_lines)
128 # for line in lines[1:]:
129 # ret += pad + ' '.join(line) + '\n'
130 return ret[:-1]
131
132 def dedent(txt):
133 """A modified version of textwrap.dedent, specialized for docstrings.
134
135 This version doesn't get confused by the first line of text having
136 inconsistent indentation from the rest, which happens a lot in docstrings.
137
138 :Examples:
139
140 >>> s = '''
141 ... First line.
142 ... More...
143 ... End'''
144
145 >>> print dedent(s)
146 First line.
147 More...
148 End
149
150 >>> s = '''First line
151 ... More...
152 ... End'''
153
154 >>> print dedent(s)
155 First line
156 More...
157 End
158 """
159 out = [textwrap.dedent(t) for t in txt.split('\n',1)
160 if t and not t.isspace()]
161 return '\n'.join(out)
162
163
164 def comment(strng,indent=''):
165 """return an input string, commented out"""
166 template = indent + '# %s'
167 lines = [template % s for s in strng.splitlines(True)]
168 return ''.join(lines)
169
170
171 def configobj2str(cobj):
172 """Dump a Configobj instance to a string."""
173 outstr = StringIO()
174 cobj.write(outstr)
175 return outstr.getvalue()
176
177 def get_config_filename(conf):
178 """Find the filename attribute of a ConfigObj given a sub-section object.
179 """
180 depth = conf.depth
181 for d in range(depth):
182 conf = conf.parent
183 return conf.filename
184
185 def sconf2File(sconf,fname,force=False):
186 """Write a SConfig instance to a given filename.
187
188 :Keywords:
189
190 force : bool (False)
191 If true, force writing even if the file exists.
192 """
193
194 if os.path.isfile(fname) and not force:
195 raise IOError("File %s already exists, use force=True to overwrite" %
196 fname)
197
198 txt = repr(sconf)
199
200 fobj = open(fname,'w')
201 fobj.write(txt)
202 fobj.close()
203
204 def filter_scalars(sc):
205 """ input sc MUST be sorted!!!"""
206 scalars = []
207 maxi = len(sc)-1
208 i = 0
209 while i<len(sc):
210 t = sc[i]
211 if t.startswith('_sconf_'):
212 # Skip altogether private _sconf_ attributes, so we actually issue
213 # a 'continue' call to avoid the append(t) below
214 i += 1
215 continue
216 scalars.append(t)
217 i += 1
218
219 return scalars
220
221
222 def get_scalars(obj):
223 """Return scalars for a Sconf class object"""
224
225 skip = set(['trait_added','trait_modified'])
226 sc = [k for k in obj.__dict__ if not k.startswith('_')]
227 sc.sort()
228 return filter_scalars(sc)
229
230
231 def get_sections(obj,sectionClass):
232 """Return sections for a Sconf class object"""
233 return [(n,v) for (n,v) in obj.__dict__.iteritems()
234 if isclass(v) and issubclass(v,sectionClass)]
235
236
237 def get_instance_sections(inst):
238 """Return sections for a Sconf instance"""
239 sections = [(k,v) for k,v in inst.__dict__.iteritems()
240 if isinstance(v,SConfig) and not k=='_sconf_parent']
241 # Sort the sections by name
242 sections.sort(key=lambda x:x[0])
243 return sections
244
245
246 def partition_instance(obj):
247 """Return scalars,sections for a given Sconf instance.
248 """
249 scnames = []
250 sections = []
251 for k,v in obj.__dict__.iteritems():
252 if isinstance(v,SConfig):
253 if not k=='_sconf_parent':
254 sections.append((k,v))
255 else:
256 scnames.append(k)
257
258 # Sort the sections by name
259 sections.sort(key=lambda x:x[0])
260
261 # Sort the scalar names, filter them and then extract the actual objects
262 scnames.sort()
263 scnames = filter_scalars(scnames)
264 scalars = [(s,obj.__dict__[s]) for s in scnames]
265
266 return scalars, sections
267
268
269 def mk_ConfigObj(filename,mk_missing_file=True):
270 """Return a ConfigObj instance with our hardcoded conventions.
271
272 Use a simple factory that wraps our option choices for using ConfigObj.
273 I'm hard-wiring certain choices here, so we'll always use instances with
274 THESE choices.
275
276 :Parameters:
277
278 filename : string
279 File to read from.
280
281 :Keywords:
282 makeMissingFile : bool (True)
283 If true, the file named by `filename` may not yet exist and it will be
284 automatically created (empty). Else, if `filename` doesn't exist, an
285 IOError will be raised.
286 """
287
288 if mk_missing_file:
289 create_empty = True
290 file_error = False
291 else:
292 create_empty = False
293 file_error = True
294
295 return configobj.ConfigObj(filename,
296 create_empty=create_empty,
297 file_error=file_error,
298 indent_type=' ',
299 interpolation='Template',
300 unrepr=True)
301
302 nullConf = mk_ConfigObj(None)
303
304
305 class RecursiveConfigObj(object):
306 """Object-oriented interface for recursive ConfigObj constructions."""
307
308 def __init__(self,filename):
309 """Return a ConfigObj instance with our hardcoded conventions.
310
311 Use a simple factory that wraps our option choices for using ConfigObj.
312 I'm hard-wiring certain choices here, so we'll always use instances with
313 THESE choices.
314
315 :Parameters:
316
317 filename : string
318 File to read from.
319 """
320
321 self.comp = []
322 self.conf = self._load(filename)
323
324 def _load(self,filename,mk_missing_file=True):
325 conf = mk_ConfigObj(filename,mk_missing_file)
326
327 # Do recursive loading. We only allow (or at least honor) the include
328 # tag at the top-level. For now, we drop the inclusion information so
329 # that there are no restrictions on which levels of the SConfig
330 # hierarchy can use include statements. But this means that
331
332 # if bookkeeping of each separate component of the recursive
333 # construction was requested, make a separate object for storage
334 # there, since we don't want that to be modified by the inclusion
335 # process.
336 self.comp.append(mk_ConfigObj(filename,mk_missing_file))
337
338 incfname = conf.pop('include',None)
339 if incfname is not None:
340 # Do recursive load. We don't want user includes that point to
341 # missing files to fail silently, so in the recursion we disable
342 # auto-creation of missing files.
343 confinc = self._load(incfname,mk_missing_file=False)
344
345 # Update with self to get proper ordering (included files provide
346 # base data, current one overwrites)
347 confinc.update(conf)
348 # And do swap to return the updated structure
349 conf = confinc
350 # Set the filename to be the original file instead of the included
351 # one
352 conf.filename = filename
353 return conf
354
355 ############################################################################
356 # Main SConfig class and supporting exceptions
357 ############################################################################
358
359 class SConfigError(Exception): pass
360
361 class SConfigInvalidKeyError(SConfigError): pass
362
363 class SConfig(object):
364 """A class representing configuration objects.
365
366 Note: this class should NOT have any traits itself, since the actual traits
367 will be declared by subclasses. This class is meant to ONLY declare the
368 necessary initialization/validation methods. """
369
370 # Any traits declared here are prefixed with _sconf_ so that our special
371 # formatting/analysis utilities can distinguish them from user traits and
372 # can avoid them.
373
374 # Once created, the tree's hierarchy can NOT be modified
375 _sconf_parent = None
376
377 def __init__(self,config=None,parent=None,monitor=None):
378 """Makes an SConfig object out of a ConfigObj instance
379 """
380
381 if config is None:
382 config = mk_ConfigObj(None)
383
384 # Validate the set of scalars ...
385 my_scalars = set(get_scalars(self))
386 cf_scalars = set(config.scalars)
387 invalid_scalars = cf_scalars - my_scalars
388 if invalid_scalars:
389 config_fname = get_config_filename(config)
390 m=("In config defined in file: %r\n"
391 "Error processing section: %s\n"
392 "These keys are invalid : %s\n"
393 "Valid key names : %s\n"
394 % (config_fname,self.__class__.__name__,
395 list(invalid_scalars),list(my_scalars)))
396 raise SConfigInvalidKeyError(m)
397
398 # ... and sections
399 section_items = get_sections(self.__class__,SConfig)
400 my_sections = set([n for n,v in section_items])
401 cf_sections = set(config.sections)
402 invalid_sections = cf_sections - my_sections
403 if invalid_sections:
404 config_fname = get_config_filename(config)
405 m = ("In config defined in file: %r\n"
406 "Error processing section: %s\n"
407 "These subsections are invalid : %s\n"
408 "Valid subsection names : %s\n"
409 % (config_fname,self.__class__.__name__,
410 list(invalid_sections),list(my_sections)))
411 raise SConfigInvalidKeyError(m)
412
413 self._sconf_parent = parent
414
415 # Now set the traits based on the config
416 for k in my_scalars:
417 setattr(self,k,config[k])
418
419 # And build subsections
420 for s,v in section_items:
421 sec_config = config.setdefault(s,{})
422 section = v(sec_config,self,monitor=monitor)
423
424 # We must use add_trait instead of setattr because we inherit from
425 # HasStrictTraits, but we need to then do a 'dummy' getattr call on
426 # self so the class trait propagates to the instance.
427 self.add_trait(s,section)
428 getattr(self,s)
429
430 def __repr__(self,depth=0):
431 """Dump a section to a string."""
432
433 indent = ' '*(depth)
434
435 top_name = self.__class__.__name__
436
437 if depth == 0:
438 label = '# %s - plaintext (in .conf format)\n' % top_name
439 else:
440 # Section titles are indented one level less than their contents in
441 # the ConfigObj write methods.
442 sec_indent = ' '*(depth-1)
443 label = '\n'+sec_indent+('[' * depth) + top_name + (']'*depth)
444
445 out = [label]
446
447 doc = self.__class__.__doc__
448 if doc is not None:
449 out.append(comment(dedent(doc),indent))
450
451 scalars, sections = partition_instance(self)
452
453 for s,v in scalars:
454 try:
455 info = self.__base_traits__[s].handler.info()
456 # Get a short version of info with lines of max. 78 chars, so
457 # that after commenting them out (with '# ') they are at most
458 # 80-chars long.
459 out.append(comment(wrap('',info.replace('\n', ' '),78-len(indent)),indent))
460 except (KeyError,AttributeError):
461 pass
462 out.append(indent+('%s = %r' % (s,v)))
463
464 for sname,sec in sections:
465 out.append(sec.__repr__(depth+1))
466
467 return '\n'.join(out)
468
469 def __str__(self):
470 return self.__class__.__name__
471
472
473 ##############################################################################
474 # High-level class(es) and utilities for handling a coupled pair of SConfig and
475 # ConfigObj instances.
476 ##############################################################################
477
478 def path_to_root(obj):
479 """Find the path to the root of a nested SConfig instance."""
480 ob = obj
481 path = []
482 while ob._sconf_parent is not None:
483 path.append(ob.__class__.__name__)
484 ob = ob._sconf_parent
485 path.reverse()
486 return path
487
488
489 def set_value(fconf,path,key,value):
490 """Set a value on a ConfigObj instance, arbitrarily deep."""
491 section = fconf
492 for sname in path:
493 section = section.setdefault(sname,{})
494 section[key] = value
495
496
497 def fmonitor(fconf):
498 """Make a monitor for coupling SConfig instances to ConfigObj ones.
499
500 We must use a closure because Traits makes assumptions about the functions
501 used with on_trait_change() that prevent the use of a callable instance.
502 """
503
504 def mon(obj,name,new):
505 #print 'OBJ:',obj # dbg
506 #print 'NAM:',name # dbg
507 #print 'NEW:',new # dbg
508 set_value(fconf,path_to_root(obj),name,new)
509
510 return mon
511
512
513 class SConfigManager(object):
514 """A simple object to manage and sync a SConfig and a ConfigObj pair.
515 """
516
517 def __init__(self,configClass,configFilename,filePriority=True):
518 """Make a new SConfigManager.
519
520 :Parameters:
521
522 configClass : class
523
524 configFilename : string
525 If the filename points to a non-existent file, it will be created
526 empty. This is useful when creating a file form from an existing
527 configClass with the class defaults.
528
529
530 :Keywords:
531
532 filePriority : bool (True)
533
534 If true, at construction time the file object takes priority and
535 overwrites the contents of the config object. Else, the data flow
536 is reversed and the file object will be overwritten with the
537 configClass defaults at write() time.
538 """
539
540 rconf = RecursiveConfigObj(configFilename)
541 # In a hierarchical object, the two following fconfs are *very*
542 # different. In self.fconf, we'll keep the outer-most fconf associated
543 # directly to the original filename. self.fconf_combined, instead,
544 # contains an object which has the combined effect of having merged all
545 # the called files in the recursive chain.
546 self.fconf = rconf.comp[0]
547 self.fconf_combined = rconf.conf
548
549 # Create a monitor to track and apply trait changes to the sconf
550 # instance over into the fconf one
551 monitor = fmonitor(self.fconf)
552