From 9ce3a358ab9638adf1cdab30aff01436c6fb64aa 2008-06-10 01:18:40 From: Stefan van der Walt <bzr@mentat.za.net> Date: 2008-06-10 01:18:40 Subject: [PATCH] Merge sconfig branch. --- diff --git a/IPython/config/api.py b/IPython/config/api.py index 1fca60e..a24a42c 100644 --- a/IPython/config/api.py +++ b/IPython/config/api.py @@ -19,6 +19,11 @@ import os from IPython.config.cutils import get_home_dir, get_ipython_dir from IPython.external.configobj import ConfigObj +# Traitlets config imports +from IPython.config import traitlets +from IPython.config.config import * +from traitlets import * + class ConfigObjManager(object): def __init__(self, configObj, filename): diff --git a/IPython/config/config.py b/IPython/config/config.py new file mode 100644 index 0000000..e88fb8a --- /dev/null +++ b/IPython/config/config.py @@ -0,0 +1,179 @@ +"""Load and store configuration objects. + +Test this module using + +nosetests -v --with-doctest --doctest-tests IPython.config + +""" + +from __future__ import with_statement +from contextlib import contextmanager + +import inspect +import types +from IPython.config import traitlets +from traitlets import Traitlet +from IPython.external.configobj import ConfigObj + +def debug(s): + import sys + sys.stderr.write(str(s) + '\n') + +@contextmanager +def raw(config): + """Context manager for accessing traitlets directly. + + """ + config.__getattribute__('',raw_access=True) + yield config + config.__getattribute__('',raw_access=False) + +class Config(object): + """ + Implementation Notes + ==================== + All instances of the same Config class share properties. Therefore, + + >>> class Sample(Config): + ... my_float = traitlets.Float(3) + + >>> s0 = Sample() + >>> s1 = Sample() + >>> s0.my_float = 5 + >>> s0.my_float == s1.my_float + True + + """ + def __init__(self): + # Instantiate subconfigs + with raw(self): + subconfigs = [(n,v) for n,v in + inspect.getmembers(self, inspect.isclass) + if not n.startswith('__')] + + for n,v in subconfigs: + setattr(self, n, v()) + + def __getattribute__(self,attr,raw_access=None, + _ns={'raw_access':False}): + if raw_access is not None: + _ns['raw_access'] = raw_access + return + + obj = object.__getattribute__(self,attr) + if isinstance(obj,Traitlet) and not _ns['raw_access']: + return obj.__call__() + else: + return obj + + def __setattr__(self,attr,value): + obj = object.__getattribute__(self,attr) + if isinstance(obj,Traitlet): + obj(value) + else: + self.__dict__[attr] = value + + def __str__(self,level=1,only_modified=True): + ci = ConfigInspector(self) + out = '' + spacer = ' '*(level-1) + + # Add traitlet representations + for p,v in ci.properties: + if (v.modified and only_modified) or not only_modified: + out += spacer + '%s = %s\n' % (p,v) + + # Add subconfig representations + for (n,v) in ci.subconfigs: + sub_str = v.__str__(level=level+1,only_modified=only_modified) + if sub_str: + out += '\n' + spacer + '[' * level + ('%s' % n) \ + + ']'*level + '\n' + out += sub_str + + return out + + def __iadd__(self,source): + """Load configuration from filename, and update self. + + """ + if not isinstance(source,dict): + source = ConfigObj(source, unrepr=True) + update_from_dict(self,source) + return self + + +class ConfigInspector(object): + """Allow the inspection of Config objects. + + """ + def __init__(self,config): + self._config = config + + @property + def properties(self): + "Return all traitlet names." + with raw(self._config): + return inspect.getmembers(self._config, + lambda obj: isinstance(obj, Traitlet)) + + @property + def subconfigs(self): + "Return all subconfig names and values." + with raw(self._config): + return [(n,v) for n,v in + inspect.getmembers(self._config, + lambda obj: isinstance(obj,Config)) + if not n.startswith('__')] + + def reset(self): + for (p,v) in self.properties: + v.reset() + + for (s,v) in self.subconfigs: + ConfigInspector(v).reset() + +def update_from_dict(config,d): + """Propagate the values of the dictionary to the given configuration. + + Useful to load configobj instances. + + """ + for k,v in d.items(): + try: + prop_or_subconfig = getattr(config, k) + except AttributeError: + print "Invalid section/property in config file: %s" % k + else: + if isinstance(v,dict): + update_from_dict(prop_or_subconfig,v) + else: + setattr(config, k, v) + +def dict_from_config(config,only_modified=True): + """Create a dictionary from a Config object.""" + ci = ConfigInspector(config) + out = {} + + for p,v in ci.properties: + if (v.modified and only_modified) or not only_modified: + out[p] = v + + for s,v in ci.subconfigs: + d = dict_from_config(v,only_modified) + if d != {}: + out[s] = d + + return out + +def write(config, target): + """Write a configuration to file. + + """ + if isinstance(target, str): + target = open(target, 'w+') + target.flush() + target.seek(0) + + confobj = ConfigObj(dict_from_config(config), unrepr=True) + confobj.write(target) diff --git a/IPython/config/sconfig.py b/IPython/config/sconfig.py deleted file mode 100644 index 886d201..0000000 --- a/IPython/config/sconfig.py +++ /dev/null @@ -1,622 +0,0 @@ -# encoding: utf-8 - -"""Mix of ConfigObj and Struct-like access. - -Provides: - -- Coupling a Struct 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. - - -Notes: - -The file creation policy is: - -1. Creating a SConfigManager(FooConfig,'missingfile.conf') will work -fine, and 'missingfile.conf' will be created empty. - -2. Creating SConfigManager(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. -""" - -__docformat__ = "restructuredtext en" -__license__ = 'BSD' - -#------------------------------------------------------------------------------- -# Copyright (C) 2008 The IPython Development Team -# -# Distributed under the terms of the BSD License. The full license is in -# the file COPYING, distributed as part of this software. -#------------------------------------------------------------------------------- - -#------------------------------------------------------------------------------- -# Imports -#------------------------------------------------------------------------------- - -############################################################################ -# Stdlib imports -############################################################################ -from cStringIO import StringIO -from inspect import isclass - -import os -import textwrap - -############################################################################ -# External imports -############################################################################ - -from IPython.external 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 get_config_filename(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 sconf2File(sconf,fname,force=False): - """Write a SConfig 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(sconf) - - 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('_sconf_'): - # Skip altogether private _sconf_ attributes, so we actually issue - # a 'continue' call to avoid the append(t) below - i += 1 - continue - scalars.append(t) - i += 1 - - return scalars - - -def get_scalars(obj): - """Return scalars for a Sconf class object""" - - skip = set(['trait_added','trait_modified']) - sc = [k for k in obj.__dict__ if not k.startswith('_')] - sc.sort() - return filter_scalars(sc) - - -def get_sections(obj,sectionClass): - """Return sections for a Sconf 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 Sconf instance""" - sections = [(k,v) for k,v in inst.__dict__.iteritems() - if isinstance(v,SConfig) and not k=='_sconf_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 Sconf instance. - """ - scnames = [] - sections = [] - for k,v in obj.__dict__.iteritems(): - if isinstance(v,SConfig): - if not k=='_sconf_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 mk_ConfigObj(filename,mk_missing_file=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 mk_missing_file: - 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 = mk_ConfigObj(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,mk_missing_file=True): - conf = mk_ConfigObj(filename,mk_missing_file) - - # 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 SConfig - # 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(mk_ConfigObj(filename,mk_missing_file)) - - 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,mk_missing_file=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 SConfig class and supporting exceptions -############################################################################ - -class SConfigError(Exception): pass - -class SConfigInvalidKeyError(SConfigError): pass - -class SConfig(object): - """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 _sconf_ 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 - _sconf_parent = None - - def __init__(self,config=None,parent=None,monitor=None): - """Makes an SConfig object out of a ConfigObj instance - """ - - if config is None: - config = mk_ConfigObj(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 = get_config_filename(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 SConfigInvalidKeyError(m) - - # ... and sections - section_items = get_sections(self.__class__,SConfig) - 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 = get_config_filename(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 SConfigInvalidKeyError(m) - - self._sconf_parent = parent - - # Now set the traits based on the config - for k in my_scalars: - setattr(self,k,config[k]) - - # 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) - - 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 SConfig and -# ConfigObj instances. -############################################################################## - -def path_to_root(obj): - """Find the path to the root of a nested SConfig instance.""" - ob = obj - path = [] - while ob._sconf_parent is not None: - path.append(ob.__class__.__name__) - ob = ob._sconf_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 SConfig 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 SConfigManager(object): - """A simple object to manage and sync a SConfig and a ConfigObj pair. - """ - - def __init__(self,configClass,configFilename,filePriority=True): - """Make a new SConfigManager. - - :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.fconf_combined, 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.fconf_combined = rconf.conf - - # Create a monitor to track and apply trait changes to the sconf - # instance over into the fconf one - monitor = fmonitor(self.fconf) - - if filePriority: - self.sconf = configClass(self.fconf_combined,monitor=monitor) - else: - # Push defaults onto file object - self.sconf = configClass(mk_ConfigObj(None),monitor=monitor) - self.fconfUpdate(self.fconf,self.sconf) - - def fconfUpdate(self,fconf,sconf): - """Update the fconf object with the data from sconf""" - - scalars, sections = partition_instance(sconf) - - 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: - file_obj = open(filename,'w') - out = self.fconf.write(file_obj) - file_obj.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 .fconf_combined - object with the *entire* .sconf 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: - file_obj = open(filename,'w') - self.fconfUpdate(self.fconf_combined,self.sconf) - out = self.fconf_combined.write(file_obj) - file_obj.close() - return out - else: - self.fconfUpdate(self.fconf_combined,self.sconf) - return self.fconf_combined.write() - - def sconf_str(self): - return str(self.sconf) - - def fconf_str(self): - return configobj2str(self.fconf) - - __repr__ = __str__ = fconf_str diff --git a/IPython/config/tests/sample_config.py b/IPython/config/tests/sample_config.py new file mode 100644 index 0000000..0efddfe --- /dev/null +++ b/IPython/config/tests/sample_config.py @@ -0,0 +1,19 @@ +from IPython.config.api import * + +class SubSubSample(Config): + my_int = Int(3) + + +class Sample(Config): + my_float = Float(3) + my_choice = Enum('a','b','c') + + class MiddleSection(Config): + left_alone = Enum('1','2','c') + unknown_mod = Module('asd') + + class SubSample(Config): + subsample_uri = URI('http://localhost:8080') + + # Example of how to include external config + SubSubSample = SubSubSample() diff --git a/IPython/config/tests/sctst.py b/IPython/config/tests/sctst.py deleted file mode 100644 index 7769d08..0000000 --- a/IPython/config/tests/sctst.py +++ /dev/null @@ -1,37 +0,0 @@ -"""Little utilities for testing tconfig. - -This module is meant to be used via - -import sctst; reload(sctst) -from sctst import * - -at the top of the actual test scripts, so that they all get the entire set of -common test tools with minimal fuss. -""" - -# Standard library imports -import os -import sys -from pprint import pprint - -# Our own imports. - -from IPython.config import sconfig -reload(sconfig) - -from sconfig import mkConfigObj, RecursiveConfigObj, SConfigManager, \ - sconf2file - -# Simple utilities/classes for testing - -def cat(fname): - print '### FILENAME:',fname - print open(fname).read() - - -class App(object): - """A trivial 'application' class to be initialized. - """ - def __init__(self,config_class,config_filename): - self.rcman = SConfigManager(config_class,config_filename) - self.rc = self.rcman.sconf diff --git a/IPython/config/tests/simple.conf b/IPython/config/tests/simple.conf deleted file mode 100644 index ba0ca67..0000000 --- a/IPython/config/tests/simple.conf +++ /dev/null @@ -1,14 +0,0 @@ -# Toy example of a TConfig-based configuration description - -# This is the class declaration for the configuration: - -# SimpleConfig -# Configuration for my application - -solver = "Iterative2" - -[Protocol] - # Specify the Protocol - - ptype = "http2" - max_users = 4 diff --git a/IPython/config/tests/simple.spec.conf b/IPython/config/tests/simple.spec.conf deleted file mode 100644 index 1fcb9e1..0000000 --- a/IPython/config/tests/simple.spec.conf +++ /dev/null @@ -1,14 +0,0 @@ -# Toy example of a TConfig-based configuration description - -# This is the class declaration for the configuration: - -# SimpleConfig -# Configuration for my application - -datafile = string(default='data.txt') -solver = option('Direct','Iterative') - -[Protocol] - # Specify the Protocol - ptype = option('http','ftp','ssh') - max_users = integer(1,10) diff --git a/IPython/config/tests/simpleconf.py b/IPython/config/tests/simpleconf.py deleted file mode 100644 index 2af9794..0000000 --- a/IPython/config/tests/simpleconf.py +++ /dev/null @@ -1,59 +0,0 @@ -"""Toy example of reading an SConf object.""" - -from IPython.external.configobj import ConfigObj -from IPython.external import configobj, validate - - -from IPython.config import sconfig -reload(sconfig) - -configspecfilename = 'simple.spec.conf' -filename = 'simple.conf' - -print '*'*80 -configspec = ConfigObj(configspecfilename, encoding='UTF8', - list_values=False) -print sconfig.configobj2str(configspec) - -print '*'*80 -config = ConfigObj(filename, configspec=configspec, - interpolation='Template', - unrepr=True) -print sconfig.configobj2str(config) -vdt = validate.Validator() -test = config.validate(vdt,preserve_errors=True) - -#### -vdt = validate.Validator() -class Bunch: pass -vf = Bunch() -vf.__dict__.update(vdt.functions) -vf.pass_ = vdt.functions['pass'] -vf.__dict__.pop('',None) -vf.__dict__.pop('pass',None) -### - - -if test==True: - print 'All OK' -else: - err = configobj.flatten_errors(config,test) - print 'Flat errors:' - for secs,key,result in err: - if secs == []: - print 'DEFAULT:','key:',key,'err:',result - else: - print 'Secs:',secs,'key:',key,'err:',result - - -## -print '*'*80 - -sc = sconfig.SConfig(configspecfilename) - - - -#### - - - diff --git a/IPython/config/tests/test_config.py b/IPython/config/tests/test_config.py new file mode 100644 index 0000000..27ab192 --- /dev/null +++ b/IPython/config/tests/test_config.py @@ -0,0 +1,135 @@ +""" +# Test utilities + +>>> import os + +>>> def dict_as_sorted_list(d): +... for k in d: +... if isinstance(d[k],dict): +... d[k] = dict_as_sorted_list(d[k]) +... return sorted(d.items()) + +>>> def pprint(d,level=0): +... if isinstance(d,dict): +... d = dict_as_sorted_list(d) +... for item,value in d: +... if isinstance(value,list): +... print "%s%s" % (' '*level, item) +... pprint(value,level+2) +... else: +... print "%s%s: %s" % (' '*level, item, value) + + +# Tests + +>>> from IPython.config.api import * +>>> from sample_config import * + +>>> s = Sample() +>>> print s.my_float +3.0 +>>> s.my_float = 4 +>>> print s.my_float +4.0 +>>> print type(s.my_float) +<type 'float'> +>>> s.SubSample.SubSubSample.my_int = 5.0 +>>> print s.SubSample.SubSubSample.my_int +5 + +>>> i = ConfigInspector(s) +>>> print i.properties +[('my_choice', 'a'), ('my_float', 4.0)] +>>> print tuple(s for s,v in i.subconfigs) +('MiddleSection', 'SubSample') + +>>> print s +my_float = 4.0 +<BLANKLINE> +[SubSample] +<BLANKLINE> + [[SubSubSample]] + my_int = 5 +<BLANKLINE> + +>>> import tempfile +>>> fn = tempfile.mktemp() +>>> f = open(fn,'w') +>>> f.write(str(s)) +>>> f.close() + +>>> s += fn + +>>> from IPython.external.configobj import ConfigObj +>>> c = ConfigObj(fn) +>>> c['SubSample']['subsample_uri'] = 'http://ipython.scipy.org' + +>>> s += c +>>> print s +my_float = 4.0 +<BLANKLINE> +[SubSample] + subsample_uri = 'http://ipython.scipy.org' +<BLANKLINE> + [[SubSubSample]] + my_int = 5 +<BLANKLINE> + +>>> pprint(dict_from_config(s,only_modified=False)) +MiddleSection + left_alone: '1' + unknown_mod: 'asd' +SubSample + SubSubSample + my_int: 5 + subsample_uri: 'http://ipython.scipy.org' +my_choice: 'a' +my_float: 4.0 + +>>> pprint(dict_from_config(s)) +SubSample + SubSubSample + my_int: 5 + subsample_uri: 'http://ipython.scipy.org' +my_float: 4.0 + +Test roundtripping: + +>>> fn = tempfile.mktemp() +>>> f = open(fn, 'w') +>>> f.write(''' +... [MiddleSection] +... # some comment here +... left_alone = 'c' +... ''') +>>> f.close() + +>>> s += fn + +>>> pprint(dict_from_config(s)) +MiddleSection + left_alone: 'c' +SubSample + SubSubSample + my_int: 5 + subsample_uri: 'http://ipython.scipy.org' +my_float: 4.0 + +>>> write(s, fn) +>>> f = file(fn,'r') +>>> ConfigInspector(s).reset() +>>> pprint(dict_from_config(s)) + +>>> s += fn +>>> os.unlink(fn) +>>> pprint(dict_from_config(s)) +MiddleSection + left_alone: 'c' +SubSample + SubSubSample + my_int: 5 + subsample_uri: 'http://ipython.scipy.org' +my_float: 4.0 + + +""" diff --git a/IPython/config/traitlets.py b/IPython/config/traitlets.py new file mode 100644 index 0000000..f1720c5 --- /dev/null +++ b/IPython/config/traitlets.py @@ -0,0 +1,322 @@ +"""Traitlets -- a light-weight meta-class free stand-in for Traits. + +Traitlet behaviour +================== +- Automatic casting, equivalent to traits.C* classes, e.g. CFloat, CBool etc. + +- By default, validation is done by attempting to cast a given value + to the underlying type, e.g. bool for Bool, float for Float etc. + +- To set or get a Traitlet value, use the ()-operator. E.g. + + >>> b = Bool(False) + >>> b(True) + >>> print b # returns a string representation of the Traitlet + True + >>> print b() # returns the underlying bool object + True + + This makes it possible to change values "in-place", unlike an assigment + of the form + + >>> c = Bool(False) + >>> c = True + + which results in + + >>> print type(b), type(c) + <class 'IPython.config.traitlets.Bool'> <type 'bool'> + +- Each Traitlet keeps track of its modification state, e.g. + + >>> c = Bool(False) + >>> print c.modified + False + >>> c(False) + >>> print c.modified + False + >>> c(True) + >>> print c.modified + True + +How to customize Traitlets +========================== + +The easiest way to create a new Traitlet is by wrapping an underlying +Python type. This is done by setting the "_type" class attribute. For +example, creating an int-like Traitlet is done as follows: + +>>> class MyInt(Traitlet): +... _type = int + +>>> i = MyInt(3) +>>> i(4) +>>> print i +4 + +>>> try: +... i('a') +... except ValidationError: +... pass # this is expected +... else: +... "This should not be reached." + +Furthermore, the following methods are provided for finer grained +control of validation and assignment: + + - validate(self,value) + Ensure that "value" is valid. If not, raise an exception of any kind + with a suitable error message, which is reported to the user. + + - prepare_value(self) + When using the ()-operator to query the underlying Traitlet value, + that value is first passed through prepare_value. For example: + + >>> class Eval(Traitlet): + ... _type = str + ... + ... def prepare_value(self): + ... return eval(self._value) + + >>> x = Eval('1+1') + >>> print x + '1+1' + >>> print x() + 2 + + - __repr__(self) + By default, repr(self._value) is returned. This can be customised + to, for example, first call prepare_value and return the repr of + the resulting object. + +""" + +import re +import types + +class ValidationError(Exception): + pass + +class Traitlet(object): + """Traitlet which knows its modification state. + + """ + def __init__(self, value): + "Validate and store the default value. State is 'unmodified'." + self._type = getattr(self,'_type',None) + value = self._parse_validation(value) + self._default_value = value + self.reset() + + def reset(self): + self._value = self._default_value + self._changed = False + + def validate(self, value): + "Validate the given value." + if self._type is not None: + self._type(value) + + def _parse_validation(self, value): + """Run validation and return a descriptive error if needed. + + """ + try: + self.validate(value) + except Exception, e: + err_message = 'Cannot convert "%s" to %s' % \ + (value, self.__class__.__name__.lower()) + if e.message: + err_message += ': %s' % e.message + raise ValidationError(err_message) + else: + # Cast to appropriate type before storing + if self._type is not None: + value = self._type(value) + return value + + def prepare_value(self): + """Run on value before it is ever returned to the user. + + """ + return self._value + + def __call__(self,value=None): + """Query or set value depending on whether `value` is specified. + + """ + if value is None: + return self.prepare_value() + + self._value = self._parse_validation(value) + self._changed = (self._value != self._default_value) + + @property + def modified(self): + "Whether value has changed from original definition." + return self._changed + + def __repr__(self): + """This class is represented by the underlying repr. Used when + dumping value to file. + + """ + return repr(self._value) + +class Float(Traitlet): + """ + >>> f = Float(0) + >>> print f.modified + False + + >>> f(3) + >>> print f() + 3.0 + >>> print f.modified + True + + >>> f(0) + >>> print f() + 0.0 + >>> print f.modified + False + + >>> try: + ... f('a') + ... except ValidationError: + ... pass + + """ + _type = float + +class Enum(Traitlet): + """ + >>> c = Enum('a','b','c') + >>> print c() + a + + >>> try: + ... c('unknown') + ... except ValidationError: + ... pass + + >>> print c.modified + False + + >>> c('b') + >>> print c() + b + + """ + def __init__(self, *options): + self._options = options + super(Enum,self).__init__(options[0]) + + def validate(self, value): + if not value in self._options: + raise ValueError('must be one of %s' % str(self._options)) + +class Module(Traitlet): + """ + >>> m = Module('some.unknown.module') + >>> print m + 'some.unknown.module' + + >>> m = Module('re') + >>> assert type(m()) is types.ModuleType + + """ + _type = str + + def prepare_value(self): + try: + module = eval(self._value) + except: + module = None + + if type(module) is not types.ModuleType: + raise ValueError("Invalid module name: %s" % self._value) + else: + return module + + +class URI(Traitlet): + """ + >>> u = URI('http://') + + >>> try: + ... u = URI('something.else') + ... except ValidationError: + ... pass + + >>> u = URI('http://ipython.scipy.org/') + >>> print u + 'http://ipython.scipy.org/' + + """ + _regexp = re.compile(r'^[a-zA-Z]+:\/\/') + _type = str + + def validate(self, uri): + if not self._regexp.match(uri): + raise ValueError() + +class Int(Traitlet): + """ + >>> i = Int(3.5) + >>> print i + 3 + >>> print i() + 3 + + >>> i = Int('4') + >>> print i + 4 + + >>> try: + ... i = Int('a') + ... except ValidationError: + ... pass + ... else: + ... raise "Should fail" + + """ + _type = int + +class Bool(Traitlet): + """ + >>> b = Bool(2) + >>> print b + True + >>> print b() + True + + >>> b = Bool('True') + >>> print b + True + >>> b(True) + >>> print b.modified + False + + >>> print Bool(0) + False + + """ + _type = bool + +class Unicode(Traitlet): + """ + >>> u = Unicode(123) + >>> print u + u'123' + + >>> u = Unicode('123') + >>> print u.modified + False + + >>> u('hello world') + >>> print u + u'hello world' + + """ + _type = unicode