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 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>> 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) + +>>> 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 + +[SubSample] + + [[SubSubSample]] + my_int = 5 + + +>>> 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 + +[SubSample] + subsample_uri = 'http://ipython.scipy.org' + + [[SubSubSample]] + my_int = 5 + + +>>> 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) + + +- 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