diff --git a/IPython/config/configurable.py b/IPython/config/configurable.py new file mode 100644 index 0000000..2d3e3f4 --- /dev/null +++ b/IPython/config/configurable.py @@ -0,0 +1,356 @@ +# encoding: utf-8 +""" +A base class for objects that are configurable. + +Inheritance diagram: + +.. inheritance-diagram:: IPython.config.configurable + :parts: 3 + +Authors: + +* Brian Granger +* Fernando Perez +* Min RK +""" + +#----------------------------------------------------------------------------- +# Copyright (C) 2008-2011 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 +#----------------------------------------------------------------------------- + +import datetime +from copy import deepcopy + +from loader import Config +from IPython.utils.traitlets import HasTraits, Instance +from IPython.utils.text import indent, wrap_paragraphs + + +#----------------------------------------------------------------------------- +# Helper classes for Configurables +#----------------------------------------------------------------------------- + + +class ConfigurableError(Exception): + pass + + +class MultipleInstanceError(ConfigurableError): + pass + +#----------------------------------------------------------------------------- +# Configurable implementation +#----------------------------------------------------------------------------- + +class Configurable(HasTraits): + + config = Instance(Config,(),{}) + created = None + + def __init__(self, **kwargs): + """Create a configurable given a config config. + + Parameters + ---------- + config : Config + If this is empty, default values are used. If config is a + :class:`Config` instance, it will be used to configure the + instance. + + Notes + ----- + Subclasses of Configurable must call the :meth:`__init__` method of + :class:`Configurable` *before* doing anything else and using + :func:`super`:: + + class MyConfigurable(Configurable): + def __init__(self, config=None): + super(MyConfigurable, self).__init__(config) + # Then any other code you need to finish initialization. + + This ensures that instances will be configured properly. + """ + config = kwargs.pop('config', None) + if config is not None: + # We used to deepcopy, but for now we are trying to just save + # by reference. This *could* have side effects as all components + # will share config. In fact, I did find such a side effect in + # _config_changed below. If a config attribute value was a mutable type + # all instances of a component were getting the same copy, effectively + # making that a class attribute. + # self.config = deepcopy(config) + self.config = config + # This should go second so individual keyword arguments override + # the values in config. + super(Configurable, self).__init__(**kwargs) + self.created = datetime.datetime.now() + + #------------------------------------------------------------------------- + # Static trait notifiations + #------------------------------------------------------------------------- + + def _config_changed(self, name, old, new): + """Update all the class traits having ``config=True`` as metadata. + + For any class trait with a ``config`` metadata attribute that is + ``True``, we update the trait with the value of the corresponding + config entry. + """ + # Get all traits with a config metadata entry that is True + traits = self.traits(config=True) + + # We auto-load config section for this class as well as any parent + # classes that are Configurable subclasses. This starts with Configurable + # and works down the mro loading the config for each section. + section_names = [cls.__name__ for cls in \ + reversed(self.__class__.__mro__) if + issubclass(cls, Configurable) and issubclass(self.__class__, cls)] + + for sname in section_names: + # Don't do a blind getattr as that would cause the config to + # dynamically create the section with name self.__class__.__name__. + if new._has_section(sname): + my_config = new[sname] + for k, v in traits.iteritems(): + # Don't allow traitlets with config=True to start with + # uppercase. Otherwise, they are confused with Config + # subsections. But, developers shouldn't have uppercase + # attributes anyways! (PEP 6) + if k[0].upper()==k[0] and not k.startswith('_'): + raise ConfigurableError('Configurable traitlets with ' + 'config=True must start with a lowercase so they are ' + 'not confused with Config subsections: %s.%s' % \ + (self.__class__.__name__, k)) + try: + # Here we grab the value from the config + # If k has the naming convention of a config + # section, it will be auto created. + config_value = my_config[k] + except KeyError: + pass + else: + # print "Setting %s.%s from %s.%s=%r" % \ + # (self.__class__.__name__,k,sname,k,config_value) + # We have to do a deepcopy here if we don't deepcopy the entire + # config object. If we don't, a mutable config_value will be + # shared by all instances, effectively making it a class attribute. + setattr(self, k, deepcopy(config_value)) + + def update_config(self, config): + """Fire the traits events when the config is updated.""" + # Save a copy of the current config. + newconfig = deepcopy(self.config) + # Merge the new config into the current one. + newconfig._merge(config) + # Save the combined config as self.config, which triggers the traits + # events. + self.config = newconfig + + @classmethod + def class_get_help(cls, inst=None): + """Get the help string for this class in ReST format. + + If `inst` is given, it's current trait values will be used in place of + class defaults. + """ + assert inst is None or isinstance(inst, cls) + cls_traits = cls.class_traits(config=True) + final_help = [] + final_help.append(u'%s options' % cls.__name__) + final_help.append(len(final_help[0])*u'-') + for k,v in sorted(cls.class_traits(config=True).iteritems()): + help = cls.class_get_trait_help(v, inst) + final_help.append(help) + return '\n'.join(final_help) + + @classmethod + def class_get_trait_help(cls, trait, inst=None): + """Get the help string for a single trait. + + If `inst` is given, it's current trait values will be used in place of + the class default. + """ + assert inst is None or isinstance(inst, cls) + lines = [] + header = "--%s.%s=<%s>" % (cls.__name__, trait.name, trait.__class__.__name__) + lines.append(header) + if inst is not None: + lines.append(indent('Current: %r' % getattr(inst, trait.name), 4)) + else: + try: + dvr = repr(trait.get_default_value()) + except Exception: + dvr = None # ignore defaults we can't construct + if dvr is not None: + if len(dvr) > 64: + dvr = dvr[:61]+'...' + lines.append(indent('Default: %s' % dvr, 4)) + if 'Enum' in trait.__class__.__name__: + # include Enum choices + lines.append(indent('Choices: %r' % (trait.values,))) + + help = trait.get_metadata('help') + if help is not None: + help = '\n'.join(wrap_paragraphs(help, 76)) + lines.append(indent(help, 4)) + return '\n'.join(lines) + + @classmethod + def class_print_help(cls, inst=None): + """Get the help string for a single trait and print it.""" + print cls.class_get_help(inst) + + @classmethod + def class_config_section(cls): + """Get the config class config section""" + def c(s): + """return a commented, wrapped block.""" + s = '\n\n'.join(wrap_paragraphs(s, 78)) + + return '# ' + s.replace('\n', '\n# ') + + # section header + breaker = '#' + '-'*78 + s = "# %s configuration"%cls.__name__ + lines = [breaker, s, breaker, ''] + # get the description trait + desc = cls.class_traits().get('description') + if desc: + desc = desc.default_value + else: + # no description trait, use __doc__ + desc = getattr(cls, '__doc__', '') + if desc: + lines.append(c(desc)) + lines.append('') + + parents = [] + for parent in cls.mro(): + # only include parents that are not base classes + # and are not the class itself + # and have some configurable traits to inherit + if parent is not cls and issubclass(parent, Configurable) and \ + parent.class_traits(config=True): + parents.append(parent) + + if parents: + pstr = ', '.join([ p.__name__ for p in parents ]) + lines.append(c('%s will inherit config from: %s'%(cls.__name__, pstr))) + lines.append('') + + for name,trait in cls.class_traits(config=True).iteritems(): + help = trait.get_metadata('help') or '' + lines.append(c(help)) + lines.append('# c.%s.%s = %r'%(cls.__name__, name, trait.get_default_value())) + lines.append('') + return '\n'.join(lines) + + + +class SingletonConfigurable(Configurable): + """A configurable that only allows one instance. + + This class is for classes that should only have one instance of itself + or *any* subclass. To create and retrieve such a class use the + :meth:`SingletonConfigurable.instance` method. + """ + + _instance = None + + @classmethod + def _walk_mro(cls): + """Walk the cls.mro() for parent classes that are also singletons + + For use in instance() + """ + + for subclass in cls.mro(): + if issubclass(cls, subclass) and \ + issubclass(subclass, SingletonConfigurable) and \ + subclass != SingletonConfigurable: + yield subclass + + @classmethod + def clear_instance(cls): + """unset _instance for this class and singleton parents. + """ + if not cls.initialized(): + return + for subclass in cls._walk_mro(): + if isinstance(subclass._instance, cls): + # only clear instances that are instances + # of the calling class + subclass._instance = None + + @classmethod + def instance(cls, *args, **kwargs): + """Returns a global instance of this class. + + This method create a new instance if none have previously been created + and returns a previously created instance is one already exists. + + The arguments and keyword arguments passed to this method are passed + on to the :meth:`__init__` method of the class upon instantiation. + + Examples + -------- + + Create a singleton class using instance, and retrieve it:: + + >>> from IPython.config.configurable import SingletonConfigurable + >>> class Foo(SingletonConfigurable): pass + >>> foo = Foo.instance() + >>> foo == Foo.instance() + True + + Create a subclass that is retrived using the base class instance:: + + >>> class Bar(SingletonConfigurable): pass + >>> class Bam(Bar): pass + >>> bam = Bam.instance() + >>> bam == Bar.instance() + True + """ + # Create and save the instance + if cls._instance is None: + inst = cls(*args, **kwargs) + # Now make sure that the instance will also be returned by + # parent classes' _instance attribute. + for subclass in cls._walk_mro(): + subclass._instance = inst + + if isinstance(cls._instance, cls): + return cls._instance + else: + raise MultipleInstanceError( + 'Multiple incompatible subclass instances of ' + '%s are being created.' % cls.__name__ + ) + + @classmethod + def initialized(cls): + """Has an instance been created?""" + return hasattr(cls, "_instance") and cls._instance is not None + + +class LoggingConfigurable(Configurable): + """A parent class for Configurables that log. + + Subclasses have a log trait, and the default behavior + is to get the logger from the currently running Application + via Application.instance().log. + """ + + log = Instance('logging.Logger') + def _log_default(self): + from IPython.config.application import Application + return Application.instance().log + + diff --git a/IPython/utils/text.py b/IPython/utils/text.py new file mode 100644 index 0000000..86d52a1 --- /dev/null +++ b/IPython/utils/text.py @@ -0,0 +1,850 @@ +# encoding: utf-8 +""" +Utilities for working with strings and text. + +Inheritance diagram: + +.. inheritance-diagram:: IPython.utils.text + :parts: 3 +""" + +#----------------------------------------------------------------------------- +# Copyright (C) 2008-2011 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 +#----------------------------------------------------------------------------- + +import __main__ + +import os +import re +import shutil +import sys +import textwrap +from string import Formatter + +from IPython.external.path import path +from IPython.testing.skipdoctest import skip_doctest_py3, skip_doctest +from IPython.utils import py3compat +from IPython.utils.io import nlprint +from IPython.utils.data import flatten + +#----------------------------------------------------------------------------- +# Code +#----------------------------------------------------------------------------- + +def unquote_ends(istr): + """Remove a single pair of quotes from the endpoints of a string.""" + + if not istr: + return istr + if (istr[0]=="'" and istr[-1]=="'") or \ + (istr[0]=='"' and istr[-1]=='"'): + return istr[1:-1] + else: + return istr + + +class LSString(str): + """String derivative with a special access attributes. + + These are normal strings, but with the special attributes: + + .l (or .list) : value as list (split on newlines). + .n (or .nlstr): original value (the string itself). + .s (or .spstr): value as whitespace-separated string. + .p (or .paths): list of path objects + + Any values which require transformations are computed only once and + cached. + + Such strings are very useful to efficiently interact with the shell, which + typically only understands whitespace-separated options for commands.""" + + def get_list(self): + try: + return self.__list + except AttributeError: + self.__list = self.split('\n') + return self.__list + + l = list = property(get_list) + + def get_spstr(self): + try: + return self.__spstr + except AttributeError: + self.__spstr = self.replace('\n',' ') + return self.__spstr + + s = spstr = property(get_spstr) + + def get_nlstr(self): + return self + + n = nlstr = property(get_nlstr) + + def get_paths(self): + try: + return self.__paths + except AttributeError: + self.__paths = [path(p) for p in self.split('\n') if os.path.exists(p)] + return self.__paths + + p = paths = property(get_paths) + +# FIXME: We need to reimplement type specific displayhook and then add this +# back as a custom printer. This should also be moved outside utils into the +# core. + +# def print_lsstring(arg): +# """ Prettier (non-repr-like) and more informative printer for LSString """ +# print "LSString (.p, .n, .l, .s available). Value:" +# print arg +# +# +# print_lsstring = result_display.when_type(LSString)(print_lsstring) + + +class SList(list): + """List derivative with a special access attributes. + + These are normal lists, but with the special attributes: + + .l (or .list) : value as list (the list itself). + .n (or .nlstr): value as a string, joined on newlines. + .s (or .spstr): value as a string, joined on spaces. + .p (or .paths): list of path objects + + Any values which require transformations are computed only once and + cached.""" + + def get_list(self): + return self + + l = list = property(get_list) + + def get_spstr(self): + try: + return self.__spstr + except AttributeError: + self.__spstr = ' '.join(self) + return self.__spstr + + s = spstr = property(get_spstr) + + def get_nlstr(self): + try: + return self.__nlstr + except AttributeError: + self.__nlstr = '\n'.join(self) + return self.__nlstr + + n = nlstr = property(get_nlstr) + + def get_paths(self): + try: + return self.__paths + except AttributeError: + self.__paths = [path(p) for p in self if os.path.exists(p)] + return self.__paths + + p = paths = property(get_paths) + + def grep(self, pattern, prune = False, field = None): + """ Return all strings matching 'pattern' (a regex or callable) + + This is case-insensitive. If prune is true, return all items + NOT matching the pattern. + + If field is specified, the match must occur in the specified + whitespace-separated field. + + Examples:: + + a.grep( lambda x: x.startswith('C') ) + a.grep('Cha.*log', prune=1) + a.grep('chm', field=-1) + """ + + def match_target(s): + if field is None: + return s + parts = s.split() + try: + tgt = parts[field] + return tgt + except IndexError: + return "" + + if isinstance(pattern, basestring): + pred = lambda x : re.search(pattern, x, re.IGNORECASE) + else: + pred = pattern + if not prune: + return SList([el for el in self if pred(match_target(el))]) + else: + return SList([el for el in self if not pred(match_target(el))]) + + def fields(self, *fields): + """ Collect whitespace-separated fields from string list + + Allows quick awk-like usage of string lists. + + Example data (in var a, created by 'a = !ls -l'):: + -rwxrwxrwx 1 ville None 18 Dec 14 2006 ChangeLog + drwxrwxrwx+ 6 ville None 0 Oct 24 18:05 IPython + + a.fields(0) is ['-rwxrwxrwx', 'drwxrwxrwx+'] + a.fields(1,0) is ['1 -rwxrwxrwx', '6 drwxrwxrwx+'] + (note the joining by space). + a.fields(-1) is ['ChangeLog', 'IPython'] + + IndexErrors are ignored. + + Without args, fields() just split()'s the strings. + """ + if len(fields) == 0: + return [el.split() for el in self] + + res = SList() + for el in [f.split() for f in self]: + lineparts = [] + + for fd in fields: + try: + lineparts.append(el[fd]) + except IndexError: + pass + if lineparts: + res.append(" ".join(lineparts)) + + return res + + def sort(self,field= None, nums = False): + """ sort by specified fields (see fields()) + + Example:: + a.sort(1, nums = True) + + Sorts a by second field, in numerical order (so that 21 > 3) + + """ + + #decorate, sort, undecorate + if field is not None: + dsu = [[SList([line]).fields(field), line] for line in self] + else: + dsu = [[line, line] for line in self] + if nums: + for i in range(len(dsu)): + numstr = "".join([ch for ch in dsu[i][0] if ch.isdigit()]) + try: + n = int(numstr) + except ValueError: + n = 0; + dsu[i][0] = n + + + dsu.sort() + return SList([t[1] for t in dsu]) + + +# FIXME: We need to reimplement type specific displayhook and then add this +# back as a custom printer. This should also be moved outside utils into the +# core. + +# def print_slist(arg): +# """ Prettier (non-repr-like) and more informative printer for SList """ +# print "SList (.p, .n, .l, .s, .grep(), .fields(), sort() available):" +# if hasattr(arg, 'hideonce') and arg.hideonce: +# arg.hideonce = False +# return +# +# nlprint(arg) +# +# print_slist = result_display.when_type(SList)(print_slist) + + +def esc_quotes(strng): + """Return the input string with single and double quotes escaped out""" + + return strng.replace('"','\\"').replace("'","\\'") + + +def qw(words,flat=0,sep=None,maxsplit=-1): + """Similar to Perl's qw() operator, but with some more options. + + qw(words,flat=0,sep=' ',maxsplit=-1) -> words.split(sep,maxsplit) + + words can also be a list itself, and with flat=1, the output will be + recursively flattened. + + Examples: + + >>> qw('1 2') + ['1', '2'] + + >>> qw(['a b','1 2',['m n','p q']]) + [['a', 'b'], ['1', '2'], [['m', 'n'], ['p', 'q']]] + + >>> qw(['a b','1 2',['m n','p q']],flat=1) + ['a', 'b', '1', '2', 'm', 'n', 'p', 'q'] + """ + + if isinstance(words, basestring): + return [word.strip() for word in words.split(sep,maxsplit) + if word and not word.isspace() ] + if flat: + return flatten(map(qw,words,[1]*len(words))) + return map(qw,words) + + +def qwflat(words,sep=None,maxsplit=-1): + """Calls qw(words) in flat mode. It's just a convenient shorthand.""" + return qw(words,1,sep,maxsplit) + + +def qw_lol(indata): + """qw_lol('a b') -> [['a','b']], + otherwise it's just a call to qw(). + + We need this to make sure the modules_some keys *always* end up as a + list of lists.""" + + if isinstance(indata, basestring): + return [qw(indata)] + else: + return qw(indata) + + +def grep(pat,list,case=1): + """Simple minded grep-like function. + grep(pat,list) returns occurrences of pat in list, None on failure. + + It only does simple string matching, with no support for regexps. Use the + option case=0 for case-insensitive matching.""" + + # This is pretty crude. At least it should implement copying only references + # to the original data in case it's big. Now it copies the data for output. + out=[] + if case: + for term in list: + if term.find(pat)>-1: out.append(term) + else: + lpat=pat.lower() + for term in list: + if term.lower().find(lpat)>-1: out.append(term) + + if len(out): return out + else: return None + + +def dgrep(pat,*opts): + """Return grep() on dir()+dir(__builtins__). + + A very common use of grep() when working interactively.""" + + return grep(pat,dir(__main__)+dir(__main__.__builtins__),*opts) + + +def idgrep(pat): + """Case-insensitive dgrep()""" + + return dgrep(pat,0) + + +def igrep(pat,list): + """Synonym for case-insensitive grep.""" + + return grep(pat,list,case=0) + + +def indent(instr,nspaces=4, ntabs=0, flatten=False): + """Indent a string a given number of spaces or tabstops. + + indent(str,nspaces=4,ntabs=0) -> indent str by ntabs+nspaces. + + Parameters + ---------- + + instr : basestring + The string to be indented. + nspaces : int (default: 4) + The number of spaces to be indented. + ntabs : int (default: 0) + The number of tabs to be indented. + flatten : bool (default: False) + Whether to scrub existing indentation. If True, all lines will be + aligned to the same indentation. If False, existing indentation will + be strictly increased. + + Returns + ------- + + str|unicode : string indented by ntabs and nspaces. + + """ + if instr is None: + return + ind = '\t'*ntabs+' '*nspaces + if flatten: + pat = re.compile(r'^\s*', re.MULTILINE) + else: + pat = re.compile(r'^', re.MULTILINE) + outstr = re.sub(pat, ind, instr) + if outstr.endswith(os.linesep+ind): + return outstr[:-len(ind)] + else: + return outstr + +def native_line_ends(filename,backup=1): + """Convert (in-place) a file to line-ends native to the current OS. + + If the optional backup argument is given as false, no backup of the + original file is left. """ + + backup_suffixes = {'posix':'~','dos':'.bak','nt':'.bak','mac':'.bak'} + + bak_filename = filename + backup_suffixes[os.name] + + original = open(filename).read() + shutil.copy2(filename,bak_filename) + try: + new = open(filename,'wb') + new.write(os.linesep.join(original.splitlines())) + new.write(os.linesep) # ALWAYS put an eol at the end of the file + new.close() + except: + os.rename(bak_filename,filename) + if not backup: + try: + os.remove(bak_filename) + except: + pass + + +def list_strings(arg): + """Always return a list of strings, given a string or list of strings + as input. + + :Examples: + + In [7]: list_strings('A single string') + Out[7]: ['A single string'] + + In [8]: list_strings(['A single string in a list']) + Out[8]: ['A single string in a list'] + + In [9]: list_strings(['A','list','of','strings']) + Out[9]: ['A', 'list', 'of', 'strings'] + """ + + if isinstance(arg,basestring): return [arg] + else: return arg + + +def marquee(txt='',width=78,mark='*'): + """Return the input string centered in a 'marquee'. + + :Examples: + + In [16]: marquee('A test',40) + Out[16]: '**************** A test ****************' + + In [17]: marquee('A test',40,'-') + Out[17]: '---------------- A test ----------------' + + In [18]: marquee('A test',40,' ') + Out[18]: ' A test ' + + """ + if not txt: + return (mark*width)[:width] + nmark = (width-len(txt)-2)//len(mark)//2 + if nmark < 0: nmark =0 + marks = mark*nmark + return '%s %s %s' % (marks,txt,marks) + + +ini_spaces_re = re.compile(r'^(\s+)') + +def num_ini_spaces(strng): + """Return the number of initial spaces in a string""" + + ini_spaces = ini_spaces_re.match(strng) + if ini_spaces: + return ini_spaces.end() + else: + return 0 + + +def format_screen(strng): + """Format a string for screen printing. + + This removes some latex-type format codes.""" + # Paragraph continue + par_re = re.compile(r'\\$',re.MULTILINE) + strng = par_re.sub('',strng) + return strng + + +def dedent(text): + """Equivalent of textwrap.dedent that ignores unindented first line. + + This means it will still dedent strings like: + '''foo + is a bar + ''' + + For use in wrap_paragraphs. + """ + + if text.startswith('\n'): + # text starts with blank line, don't ignore the first line + return textwrap.dedent(text) + + # split first line + splits = text.split('\n',1) + if len(splits) == 1: + # only one line + return textwrap.dedent(text) + + first, rest = splits + # dedent everything but the first line + rest = textwrap.dedent(rest) + return '\n'.join([first, rest]) + + +def wrap_paragraphs(text, ncols=80): + """Wrap multiple paragraphs to fit a specified width. + + This is equivalent to textwrap.wrap, but with support for multiple + paragraphs, as separated by empty lines. + + Returns + ------- + + list of complete paragraphs, wrapped to fill `ncols` columns. + """ + paragraph_re = re.compile(r'\n(\s*\n)+', re.MULTILINE) + text = dedent(text).strip() + paragraphs = paragraph_re.split(text)[::2] # every other entry is space + out_ps = [] + indent_re = re.compile(r'\n\s+', re.MULTILINE) + for p in paragraphs: + # presume indentation that survives dedent is meaningful formatting, + # so don't fill unless text is flush. + if indent_re.search(p) is None: + # wrap paragraph + p = textwrap.fill(p, ncols) + out_ps.append(p) + return out_ps + + +def long_substr(data): + """Return the longest common substring in a list of strings. + + Credit: http://stackoverflow.com/questions/2892931/longest-common-substring-from-more-than-two-strings-python + """ + substr = '' + if len(data) > 1 and len(data[0]) > 0: + for i in range(len(data[0])): + for j in range(len(data[0])-i+1): + if j > len(substr) and all(data[0][i:i+j] in x for x in data): + substr = data[0][i:i+j] + elif len(data) == 1: + substr = data[0] + return substr + + +def strip_email_quotes(text): + """Strip leading email quotation characters ('>'). + + Removes any combination of leading '>' interspersed with whitespace that + appears *identically* in all lines of the input text. + + Parameters + ---------- + text : str + + Examples + -------- + + Simple uses:: + + In [2]: strip_email_quotes('> > text') + Out[2]: 'text' + + In [3]: strip_email_quotes('> > text\\n> > more') + Out[3]: 'text\\nmore' + + Note how only the common prefix that appears in all lines is stripped:: + + In [4]: strip_email_quotes('> > text\\n> > more\\n> more...') + Out[4]: '> text\\n> more\\nmore...' + + So if any line has no quote marks ('>') , then none are stripped from any + of them :: + + In [5]: strip_email_quotes('> > text\\n> > more\\nlast different') + Out[5]: '> > text\\n> > more\\nlast different' + """ + lines = text.splitlines() + matches = set() + for line in lines: + prefix = re.match(r'^(\s*>[ >]*)', line) + if prefix: + matches.add(prefix.group(1)) + else: + break + else: + prefix = long_substr(list(matches)) + if prefix: + strip = len(prefix) + text = '\n'.join([ ln[strip:] for ln in lines]) + return text + + +class EvalFormatter(Formatter): + """A String Formatter that allows evaluation of simple expressions. + + Note that this version interprets a : as specifying a format string (as per + standard string formatting), so if slicing is required, you must explicitly + create a slice. + + This is to be used in templating cases, such as the parallel batch + script templates, where simple arithmetic on arguments is useful. + + Examples + -------- + + In [1]: f = EvalFormatter() + In [2]: f.format('{n//4}', n=8) + Out [2]: '2' + + In [3]: f.format("{greeting[slice(2,4)]}", greeting="Hello") + Out [3]: 'll' + """ + def get_field(self, name, args, kwargs): + v = eval(name, kwargs) + return v, name + + +@skip_doctest_py3 +class FullEvalFormatter(Formatter): + """A String Formatter that allows evaluation of simple expressions. + + Any time a format key is not found in the kwargs, + it will be tried as an expression in the kwargs namespace. + + Note that this version allows slicing using [1:2], so you cannot specify + a format string. Use :class:`EvalFormatter` to permit format strings. + + Examples + -------- + + In [1]: f = FullEvalFormatter() + In [2]: f.format('{n//4}', n=8) + Out[2]: u'2' + + In [3]: f.format('{list(range(5))[2:4]}') + Out[3]: u'[2, 3]' + + In [4]: f.format('{3*2}') + Out[4]: u'6' + """ + # copied from Formatter._vformat with minor changes to allow eval + # and replace the format_spec code with slicing + def _vformat(self, format_string, args, kwargs, used_args, recursion_depth): + if recursion_depth < 0: + raise ValueError('Max string recursion exceeded') + result = [] + for literal_text, field_name, format_spec, conversion in \ + self.parse(format_string): + + # output the literal text + if literal_text: + result.append(literal_text) + + # if there's a field, output it + if field_name is not None: + # this is some markup, find the object and do + # the formatting + + if format_spec: + # override format spec, to allow slicing: + field_name = ':'.join([field_name, format_spec]) + + # eval the contents of the field for the object + # to be formatted + obj = eval(field_name, kwargs) + + # do any conversion on the resulting object + obj = self.convert_field(obj, conversion) + + # format the object and append to the result + result.append(self.format_field(obj, '')) + + return u''.join(py3compat.cast_unicode(s) for s in result) + + +@skip_doctest_py3 +class DollarFormatter(FullEvalFormatter): + """Formatter allowing Itpl style $foo replacement, for names and attribute + access only. Standard {foo} replacement also works, and allows full + evaluation of its arguments. + + Examples + -------- + In [1]: f = DollarFormatter() + In [2]: f.format('{n//4}', n=8) + Out[2]: u'2' + + In [3]: f.format('23 * 76 is $result', result=23*76) + Out[3]: u'23 * 76 is 1748' + + In [4]: f.format('$a or {b}', a=1, b=2) + Out[4]: u'1 or 2' + """ + _dollar_pattern = re.compile("(.*?)\$(\$?[\w\.]+)") + def parse(self, fmt_string): + for literal_txt, field_name, format_spec, conversion \ + in Formatter.parse(self, fmt_string): + + # Find $foo patterns in the literal text. + continue_from = 0 + txt = "" + for m in self._dollar_pattern.finditer(literal_txt): + new_txt, new_field = m.group(1,2) + # $$foo --> $foo + if new_field.startswith("$"): + txt += new_txt + new_field + else: + yield (txt + new_txt, new_field, "", None) + txt = "" + continue_from = m.end() + + # Re-yield the {foo} style pattern + yield (txt + literal_txt[continue_from:], field_name, format_spec, conversion) + +#----------------------------------------------------------------------------- +# Utils to columnize a list of string +#----------------------------------------------------------------------------- + +def _chunks(l, n): + """Yield successive n-sized chunks from l.""" + for i in xrange(0, len(l), n): + yield l[i:i+n] + + +def _find_optimal(rlist , separator_size=2 , displaywidth=80): + """Calculate optimal info to columnize a list of string""" + for nrow in range(1, len(rlist)+1) : + chk = map(max,_chunks(rlist, nrow)) + sumlength = sum(chk) + ncols = len(chk) + if sumlength+separator_size*(ncols-1) <= displaywidth : + break; + return {'columns_numbers' : ncols, + 'optimal_separator_width':(displaywidth - sumlength)/(ncols-1) if (ncols -1) else 0, + 'rows_numbers' : nrow, + 'columns_width' : chk + } + + +def _get_or_default(mylist, i, default=None): + """return list item number, or default if don't exist""" + if i >= len(mylist): + return default + else : + return mylist[i] + + +@skip_doctest +def compute_item_matrix(items, empty=None, *args, **kwargs) : + """Returns a nested list, and info to columnize items + + Parameters : + ------------ + + items : + list of strings to columize + empty : (default None) + default value to fill list if needed + separator_size : int (default=2) + How much caracters will be used as a separation between each columns. + displaywidth : int (default=80) + The width of the area onto wich the columns should enter + + Returns : + --------- + + Returns a tuple of (strings_matrix, dict_info) + + strings_matrix : + + nested list of string, the outer most list contains as many list as + rows, the innermost lists have each as many element as colums. If the + total number of elements in `items` does not equal the product of + rows*columns, the last element of some lists are filled with `None`. + + dict_info : + some info to make columnize easier: + + columns_numbers : number of columns + rows_numbers : number of rows + columns_width : list of with of each columns + optimal_separator_width : best separator width between columns + + Exemple : + --------- + + In [1]: l = ['aaa','b','cc','d','eeeee','f','g','h','i','j','k','l'] + ...: compute_item_matrix(l,displaywidth=12) + Out[1]: + ([['aaa', 'f', 'k'], + ['b', 'g', 'l'], + ['cc', 'h', None], + ['d', 'i', None], + ['eeeee', 'j', None]], + {'columns_numbers': 3, + 'columns_width': [5, 1, 1], + 'optimal_separator_width': 2, + 'rows_numbers': 5}) + + """ + info = _find_optimal(map(len, items), *args, **kwargs) + nrow, ncol = info['rows_numbers'], info['columns_numbers'] + return ([[ _get_or_default(items, c*nrow+i, default=empty) for c in range(ncol) ] for i in range(nrow) ], info) + + +def columnize(items, separator=' ', displaywidth=80): + """ Transform a list of strings into a single string with columns. + + Parameters + ---------- + items : sequence of strings + The strings to process. + + separator : str, optional [default is two spaces] + The string that separates columns. + + displaywidth : int, optional [default is 80] + Width of the display in number of characters. + + Returns + ------- + The formatted string. + """ + if not items : + return '\n' + matrix, info = compute_item_matrix(items, separator_size=len(separator), displaywidth=displaywidth) + fmatrix = [filter(None, x) for x in matrix] + sjoin = lambda x : separator.join([ y.ljust(w, ' ') for y, w in zip(x, info['columns_width'])]) + return '\n'.join(map(sjoin, fmatrix))+'\n' diff --git a/IPython/utils/traitlets.py b/IPython/utils/traitlets.py new file mode 100644 index 0000000..21764e7 --- /dev/null +++ b/IPython/utils/traitlets.py @@ -0,0 +1,1439 @@ +# encoding: utf-8 +""" +A lightweight Traits like module. + +This is designed to provide a lightweight, simple, pure Python version of +many of the capabilities of enthought.traits. This includes: + +* Validation +* Type specification with defaults +* Static and dynamic notification +* Basic predefined types +* An API that is similar to enthought.traits + +We don't support: + +* Delegation +* Automatic GUI generation +* A full set of trait types. Most importantly, we don't provide container + traits (list, dict, tuple) that can trigger notifications if their + contents change. +* API compatibility with enthought.traits + +There are also some important difference in our design: + +* enthought.traits does not validate default values. We do. + +We choose to create this module because we need these capabilities, but +we need them to be pure Python so they work in all Python implementations, +including Jython and IronPython. + +Inheritance diagram: + +.. inheritance-diagram:: IPython.utils.traitlets + :parts: 3 + +Authors: + +* Brian Granger +* Enthought, Inc. Some of the code in this file comes from enthought.traits + and is licensed under the BSD license. Also, many of the ideas also come + from enthought.traits even though our implementation is very different. +""" + +#----------------------------------------------------------------------------- +# Copyright (C) 2008-2011 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 +#----------------------------------------------------------------------------- + + +import inspect +import re +import sys +import types +from types import FunctionType +try: + from types import ClassType, InstanceType + ClassTypes = (ClassType, type) +except: + ClassTypes = (type,) + +from .importstring import import_item +from IPython.utils import py3compat + +SequenceTypes = (list, tuple, set, frozenset) + +#----------------------------------------------------------------------------- +# Basic classes +#----------------------------------------------------------------------------- + + +class NoDefaultSpecified ( object ): pass +NoDefaultSpecified = NoDefaultSpecified() + + +class Undefined ( object ): pass +Undefined = Undefined() + +class TraitError(Exception): + pass + +#----------------------------------------------------------------------------- +# Utilities +#----------------------------------------------------------------------------- + + +def class_of ( object ): + """ Returns a string containing the class name of an object with the + correct indefinite article ('a' or 'an') preceding it (e.g., 'an Image', + 'a PlotValue'). + """ + if isinstance( object, basestring ): + return add_article( object ) + + return add_article( object.__class__.__name__ ) + + +def add_article ( name ): + """ Returns a string containing the correct indefinite article ('a' or 'an') + prefixed to the specified string. + """ + if name[:1].lower() in 'aeiou': + return 'an ' + name + + return 'a ' + name + + +def repr_type(obj): + """ Return a string representation of a value and its type for readable + error messages. + """ + the_type = type(obj) + if (not py3compat.PY3) and the_type is InstanceType: + # Old-style class. + the_type = obj.__class__ + msg = '%r %r' % (obj, the_type) + return msg + + +def is_trait(t): + """ Returns whether the given value is an instance or subclass of TraitType. + """ + return (isinstance(t, TraitType) or + (isinstance(t, type) and issubclass(t, TraitType))) + + +def parse_notifier_name(name): + """Convert the name argument to a list of names. + + Examples + -------- + + >>> parse_notifier_name('a') + ['a'] + >>> parse_notifier_name(['a','b']) + ['a', 'b'] + >>> parse_notifier_name(None) + ['anytrait'] + """ + if isinstance(name, str): + return [name] + elif name is None: + return ['anytrait'] + elif isinstance(name, (list, tuple)): + for n in name: + assert isinstance(n, str), "names must be strings" + return name + + +class _SimpleTest: + def __init__ ( self, value ): self.value = value + def __call__ ( self, test ): + return test == self.value + def __repr__(self): + return " 0: + if len(self.metadata) > 0: + self._metadata = self.metadata.copy() + self._metadata.update(metadata) + else: + self._metadata = metadata + else: + self._metadata = self.metadata + + self.init() + + def init(self): + pass + + def get_default_value(self): + """Create a new instance of the default value.""" + return self.default_value + + def instance_init(self, obj): + """This is called by :meth:`HasTraits.__new__` to finish init'ing. + + Some stages of initialization must be delayed until the parent + :class:`HasTraits` instance has been created. This method is + called in :meth:`HasTraits.__new__` after the instance has been + created. + + This method trigger the creation and validation of default values + and also things like the resolution of str given class names in + :class:`Type` and :class`Instance`. + + Parameters + ---------- + obj : :class:`HasTraits` instance + The parent :class:`HasTraits` instance that has just been + created. + """ + self.set_default_value(obj) + + def set_default_value(self, obj): + """Set the default value on a per instance basis. + + This method is called by :meth:`instance_init` to create and + validate the default value. The creation and validation of + default values must be delayed until the parent :class:`HasTraits` + class has been instantiated. + """ + # Check for a deferred initializer defined in the same class as the + # trait declaration or above. + mro = type(obj).mro() + meth_name = '_%s_default' % self.name + for cls in mro[:mro.index(self.this_class)+1]: + if meth_name in cls.__dict__: + break + else: + # We didn't find one. Do static initialization. + dv = self.get_default_value() + newdv = self._validate(obj, dv) + obj._trait_values[self.name] = newdv + return + # Complete the dynamic initialization. + obj._trait_dyn_inits[self.name] = cls.__dict__[meth_name] + + def __get__(self, obj, cls=None): + """Get the value of the trait by self.name for the instance. + + Default values are instantiated when :meth:`HasTraits.__new__` + is called. Thus by the time this method gets called either the + default value or a user defined value (they called :meth:`__set__`) + is in the :class:`HasTraits` instance. + """ + if obj is None: + return self + else: + try: + value = obj._trait_values[self.name] + except KeyError: + # Check for a dynamic initializer. + if self.name in obj._trait_dyn_inits: + value = obj._trait_dyn_inits[self.name](obj) + # FIXME: Do we really validate here? + value = self._validate(obj, value) + obj._trait_values[self.name] = value + return value + else: + raise TraitError('Unexpected error in TraitType: ' + 'both default value and dynamic initializer are ' + 'absent.') + except Exception: + # HasTraits should call set_default_value to populate + # this. So this should never be reached. + raise TraitError('Unexpected error in TraitType: ' + 'default value not set properly') + else: + return value + + def __set__(self, obj, value): + new_value = self._validate(obj, value) + old_value = self.__get__(obj) + obj._trait_values[self.name] = new_value + if old_value != new_value: + obj._notify_trait(self.name, old_value, new_value) + + def _validate(self, obj, value): + if hasattr(self, 'validate'): + return self.validate(obj, value) + elif hasattr(self, 'is_valid_for'): + valid = self.is_valid_for(value) + if valid: + return value + else: + raise TraitError('invalid value for type: %r' % value) + elif hasattr(self, 'value_for'): + return self.value_for(value) + else: + return value + + def info(self): + return self.info_text + + def error(self, obj, value): + if obj is not None: + e = "The '%s' trait of %s instance must be %s, but a value of %s was specified." \ + % (self.name, class_of(obj), + self.info(), repr_type(value)) + else: + e = "The '%s' trait must be %s, but a value of %r was specified." \ + % (self.name, self.info(), repr_type(value)) + raise TraitError(e) + + def get_metadata(self, key): + return getattr(self, '_metadata', {}).get(key, None) + + def set_metadata(self, key, value): + getattr(self, '_metadata', {})[key] = value + + +#----------------------------------------------------------------------------- +# The HasTraits implementation +#----------------------------------------------------------------------------- + + +class MetaHasTraits(type): + """A metaclass for HasTraits. + + This metaclass makes sure that any TraitType class attributes are + instantiated and sets their name attribute. + """ + + def __new__(mcls, name, bases, classdict): + """Create the HasTraits class. + + This instantiates all TraitTypes in the class dict and sets their + :attr:`name` attribute. + """ + # print "MetaHasTraitlets (mcls, name): ", mcls, name + # print "MetaHasTraitlets (bases): ", bases + # print "MetaHasTraitlets (classdict): ", classdict + for k,v in classdict.iteritems(): + if isinstance(v, TraitType): + v.name = k + elif inspect.isclass(v): + if issubclass(v, TraitType): + vinst = v() + vinst.name = k + classdict[k] = vinst + return super(MetaHasTraits, mcls).__new__(mcls, name, bases, classdict) + + def __init__(cls, name, bases, classdict): + """Finish initializing the HasTraits class. + + This sets the :attr:`this_class` attribute of each TraitType in the + class dict to the newly created class ``cls``. + """ + for k, v in classdict.iteritems(): + if isinstance(v, TraitType): + v.this_class = cls + super(MetaHasTraits, cls).__init__(name, bases, classdict) + +class HasTraits(object): + + __metaclass__ = MetaHasTraits + + def __new__(cls, **kw): + # This is needed because in Python 2.6 object.__new__ only accepts + # the cls argument. + new_meth = super(HasTraits, cls).__new__ + if new_meth is object.__new__: + inst = new_meth(cls) + else: + inst = new_meth(cls, **kw) + inst._trait_values = {} + inst._trait_notifiers = {} + inst._trait_dyn_inits = {} + # Here we tell all the TraitType instances to set their default + # values on the instance. + for key in dir(cls): + # Some descriptors raise AttributeError like zope.interface's + # __provides__ attributes even though they exist. This causes + # AttributeErrors even though they are listed in dir(cls). + try: + value = getattr(cls, key) + except AttributeError: + pass + else: + if isinstance(value, TraitType): + value.instance_init(inst) + + return inst + + def __init__(self, **kw): + # Allow trait values to be set using keyword arguments. + # We need to use setattr for this to trigger validation and + # notifications. + for key, value in kw.iteritems(): + setattr(self, key, value) + + def _notify_trait(self, name, old_value, new_value): + + # First dynamic ones + callables = self._trait_notifiers.get(name,[]) + more_callables = self._trait_notifiers.get('anytrait',[]) + callables.extend(more_callables) + + # Now static ones + try: + cb = getattr(self, '_%s_changed' % name) + except: + pass + else: + callables.append(cb) + + # Call them all now + for c in callables: + # Traits catches and logs errors here. I allow them to raise + if callable(c): + argspec = inspect.getargspec(c) + nargs = len(argspec[0]) + # Bound methods have an additional 'self' argument + # I don't know how to treat unbound methods, but they + # can't really be used for callbacks. + if isinstance(c, types.MethodType): + offset = -1 + else: + offset = 0 + if nargs + offset == 0: + c() + elif nargs + offset == 1: + c(name) + elif nargs + offset == 2: + c(name, new_value) + elif nargs + offset == 3: + c(name, old_value, new_value) + else: + raise TraitError('a trait changed callback ' + 'must have 0-3 arguments.') + else: + raise TraitError('a trait changed callback ' + 'must be callable.') + + + def _add_notifiers(self, handler, name): + if name not in self._trait_notifiers: + nlist = [] + self._trait_notifiers[name] = nlist + else: + nlist = self._trait_notifiers[name] + if handler not in nlist: + nlist.append(handler) + + def _remove_notifiers(self, handler, name): + if name in self._trait_notifiers: + nlist = self._trait_notifiers[name] + try: + index = nlist.index(handler) + except ValueError: + pass + else: + del nlist[index] + + def on_trait_change(self, handler, name=None, remove=False): + """Setup a handler to be called when a trait changes. + + This is used to setup dynamic notifications of trait changes. + + Static handlers can be created by creating methods on a HasTraits + subclass with the naming convention '_[traitname]_changed'. Thus, + to create static handler for the trait 'a', create the method + _a_changed(self, name, old, new) (fewer arguments can be used, see + below). + + Parameters + ---------- + handler : callable + A callable that is called when a trait changes. Its + signature can be handler(), handler(name), handler(name, new) + or handler(name, old, new). + name : list, str, None + If None, the handler will apply to all traits. If a list + of str, handler will apply to all names in the list. If a + str, the handler will apply just to that name. + remove : bool + If False (the default), then install the handler. If True + then unintall it. + """ + if remove: + names = parse_notifier_name(name) + for n in names: + self._remove_notifiers(handler, n) + else: + names = parse_notifier_name(name) + for n in names: + self._add_notifiers(handler, n) + + @classmethod + def class_trait_names(cls, **metadata): + """Get a list of all the names of this classes traits. + + This method is just like the :meth:`trait_names` method, but is unbound. + """ + return cls.class_traits(**metadata).keys() + + @classmethod + def class_traits(cls, **metadata): + """Get a list of all the traits of this class. + + This method is just like the :meth:`traits` method, but is unbound. + + The TraitTypes returned don't know anything about the values + that the various HasTrait's instances are holding. + + This follows the same algorithm as traits does and does not allow + for any simple way of specifying merely that a metadata name + exists, but has any value. This is because get_metadata returns + None if a metadata key doesn't exist. + """ + traits = dict([memb for memb in getmembers(cls) if \ + isinstance(memb[1], TraitType)]) + + if len(metadata) == 0: + return traits + + for meta_name, meta_eval in metadata.items(): + if type(meta_eval) is not FunctionType: + metadata[meta_name] = _SimpleTest(meta_eval) + + result = {} + for name, trait in traits.items(): + for meta_name, meta_eval in metadata.items(): + if not meta_eval(trait.get_metadata(meta_name)): + break + else: + result[name] = trait + + return result + + def trait_names(self, **metadata): + """Get a list of all the names of this classes traits.""" + return self.traits(**metadata).keys() + + def traits(self, **metadata): + """Get a list of all the traits of this class. + + The TraitTypes returned don't know anything about the values + that the various HasTrait's instances are holding. + + This follows the same algorithm as traits does and does not allow + for any simple way of specifying merely that a metadata name + exists, but has any value. This is because get_metadata returns + None if a metadata key doesn't exist. + """ + traits = dict([memb for memb in getmembers(self.__class__) if \ + isinstance(memb[1], TraitType)]) + + if len(metadata) == 0: + return traits + + for meta_name, meta_eval in metadata.items(): + if type(meta_eval) is not FunctionType: + metadata[meta_name] = _SimpleTest(meta_eval) + + result = {} + for name, trait in traits.items(): + for meta_name, meta_eval in metadata.items(): + if not meta_eval(trait.get_metadata(meta_name)): + break + else: + result[name] = trait + + return result + + def trait_metadata(self, traitname, key): + """Get metadata values for trait by key.""" + try: + trait = getattr(self.__class__, traitname) + except AttributeError: + raise TraitError("Class %s does not have a trait named %s" % + (self.__class__.__name__, traitname)) + else: + return trait.get_metadata(key) + +#----------------------------------------------------------------------------- +# Actual TraitTypes implementations/subclasses +#----------------------------------------------------------------------------- + +#----------------------------------------------------------------------------- +# TraitTypes subclasses for handling classes and instances of classes +#----------------------------------------------------------------------------- + + +class ClassBasedTraitType(TraitType): + """A trait with error reporting for Type, Instance and This.""" + + def error(self, obj, value): + kind = type(value) + if (not py3compat.PY3) and kind is InstanceType: + msg = 'class %s' % value.__class__.__name__ + else: + msg = '%s (i.e. %s)' % ( str( kind )[1:-1], repr( value ) ) + + if obj is not None: + e = "The '%s' trait of %s instance must be %s, but a value of %s was specified." \ + % (self.name, class_of(obj), + self.info(), msg) + else: + e = "The '%s' trait must be %s, but a value of %r was specified." \ + % (self.name, self.info(), msg) + + raise TraitError(e) + + +class Type(ClassBasedTraitType): + """A trait whose value must be a subclass of a specified class.""" + + def __init__ (self, default_value=None, klass=None, allow_none=True, **metadata ): + """Construct a Type trait + + A Type trait specifies that its values must be subclasses of + a particular class. + + If only ``default_value`` is given, it is used for the ``klass`` as + well. + + Parameters + ---------- + default_value : class, str or None + The default value must be a subclass of klass. If an str, + the str must be a fully specified class name, like 'foo.bar.Bah'. + The string is resolved into real class, when the parent + :class:`HasTraits` class is instantiated. + klass : class, str, None + Values of this trait must be a subclass of klass. The klass + may be specified in a string like: 'foo.bar.MyClass'. + The string is resolved into real class, when the parent + :class:`HasTraits` class is instantiated. + allow_none : boolean + Indicates whether None is allowed as an assignable value. Even if + ``False``, the default value may be ``None``. + """ + if default_value is None: + if klass is None: + klass = object + elif klass is None: + klass = default_value + + if not (inspect.isclass(klass) or isinstance(klass, basestring)): + raise TraitError("A Type trait must specify a class.") + + self.klass = klass + self._allow_none = allow_none + + super(Type, self).__init__(default_value, **metadata) + + def validate(self, obj, value): + """Validates that the value is a valid object instance.""" + try: + if issubclass(value, self.klass): + return value + except: + if (value is None) and (self._allow_none): + return value + + self.error(obj, value) + + def info(self): + """ Returns a description of the trait.""" + if isinstance(self.klass, basestring): + klass = self.klass + else: + klass = self.klass.__name__ + result = 'a subclass of ' + klass + if self._allow_none: + return result + ' or None' + return result + + def instance_init(self, obj): + self._resolve_classes() + super(Type, self).instance_init(obj) + + def _resolve_classes(self): + if isinstance(self.klass, basestring): + self.klass = import_item(self.klass) + if isinstance(self.default_value, basestring): + self.default_value = import_item(self.default_value) + + def get_default_value(self): + return self.default_value + + +class DefaultValueGenerator(object): + """A class for generating new default value instances.""" + + def __init__(self, *args, **kw): + self.args = args + self.kw = kw + + def generate(self, klass): + return klass(*self.args, **self.kw) + + +class Instance(ClassBasedTraitType): + """A trait whose value must be an instance of a specified class. + + The value can also be an instance of a subclass of the specified class. + """ + + def __init__(self, klass=None, args=None, kw=None, + allow_none=True, **metadata ): + """Construct an Instance trait. + + This trait allows values that are instances of a particular + class or its sublclasses. Our implementation is quite different + from that of enthough.traits as we don't allow instances to be used + for klass and we handle the ``args`` and ``kw`` arguments differently. + + Parameters + ---------- + klass : class, str + The class that forms the basis for the trait. Class names + can also be specified as strings, like 'foo.bar.Bar'. + args : tuple + Positional arguments for generating the default value. + kw : dict + Keyword arguments for generating the default value. + allow_none : bool + Indicates whether None is allowed as a value. + + Default Value + ------------- + If both ``args`` and ``kw`` are None, then the default value is None. + If ``args`` is a tuple and ``kw`` is a dict, then the default is + created as ``klass(*args, **kw)``. If either ``args`` or ``kw`` is + not (but not both), None is replace by ``()`` or ``{}``. + """ + + self._allow_none = allow_none + + if (klass is None) or (not (inspect.isclass(klass) or isinstance(klass, basestring))): + raise TraitError('The klass argument must be a class' + ' you gave: %r' % klass) + self.klass = klass + + # self.klass is a class, so handle default_value + if args is None and kw is None: + default_value = None + else: + if args is None: + # kw is not None + args = () + elif kw is None: + # args is not None + kw = {} + + if not isinstance(kw, dict): + raise TraitError("The 'kw' argument must be a dict or None.") + if not isinstance(args, tuple): + raise TraitError("The 'args' argument must be a tuple or None.") + + default_value = DefaultValueGenerator(*args, **kw) + + super(Instance, self).__init__(default_value, **metadata) + + def validate(self, obj, value): + if value is None: + if self._allow_none: + return value + self.error(obj, value) + + if isinstance(value, self.klass): + return value + else: + self.error(obj, value) + + def info(self): + if isinstance(self.klass, basestring): + klass = self.klass + else: + klass = self.klass.__name__ + result = class_of(klass) + if self._allow_none: + return result + ' or None' + + return result + + def instance_init(self, obj): + self._resolve_classes() + super(Instance, self).instance_init(obj) + + def _resolve_classes(self): + if isinstance(self.klass, basestring): + self.klass = import_item(self.klass) + + def get_default_value(self): + """Instantiate a default value instance. + + This is called when the containing HasTraits classes' + :meth:`__new__` method is called to ensure that a unique instance + is created for each HasTraits instance. + """ + dv = self.default_value + if isinstance(dv, DefaultValueGenerator): + return dv.generate(self.klass) + else: + return dv + + +class This(ClassBasedTraitType): + """A trait for instances of the class containing this trait. + + Because how how and when class bodies are executed, the ``This`` + trait can only have a default value of None. This, and because we + always validate default values, ``allow_none`` is *always* true. + """ + + info_text = 'an instance of the same type as the receiver or None' + + def __init__(self, **metadata): + super(This, self).__init__(None, **metadata) + + def validate(self, obj, value): + # What if value is a superclass of obj.__class__? This is + # complicated if it was the superclass that defined the This + # trait. + if isinstance(value, self.this_class) or (value is None): + return value + else: + self.error(obj, value) + + +#----------------------------------------------------------------------------- +# Basic TraitTypes implementations/subclasses +#----------------------------------------------------------------------------- + + +class Any(TraitType): + default_value = None + info_text = 'any value' + + +class Int(TraitType): + """An int trait.""" + + default_value = 0 + info_text = 'an int' + + def validate(self, obj, value): + if isinstance(value, int): + return value + self.error(obj, value) + +class CInt(Int): + """A casting version of the int trait.""" + + def validate(self, obj, value): + try: + return int(value) + except: + self.error(obj, value) + +if py3compat.PY3: + Long, CLong = Int, CInt + Integer = Int +else: + class Long(TraitType): + """A long integer trait.""" + + default_value = 0L + info_text = 'a long' + + def validate(self, obj, value): + if isinstance(value, long): + return value + if isinstance(value, int): + return long(value) + self.error(obj, value) + + + class CLong(Long): + """A casting version of the long integer trait.""" + + def validate(self, obj, value): + try: + return long(value) + except: + self.error(obj, value) + + class Integer(TraitType): + """An integer trait. + + Longs that are unnecessary (<= sys.maxint) are cast to ints.""" + + default_value = 0 + info_text = 'an integer' + + def validate(self, obj, value): + if isinstance(value, int): + return value + if isinstance(value, long): + # downcast longs that fit in int: + # note that int(n > sys.maxint) returns a long, so + # we don't need a condition on this cast + return int(value) + if sys.platform == "cli": + from System import Int64 + if isinstance(value, Int64): + return int(value) + self.error(obj, value) + + +class Float(TraitType): + """A float trait.""" + + default_value = 0.0 + info_text = 'a float' + + def validate(self, obj, value): + if isinstance(value, float): + return value + if isinstance(value, int): + return float(value) + self.error(obj, value) + + +class CFloat(Float): + """A casting version of the float trait.""" + + def validate(self, obj, value): + try: + return float(value) + except: + self.error(obj, value) + +class Complex(TraitType): + """A trait for complex numbers.""" + + default_value = 0.0 + 0.0j + info_text = 'a complex number' + + def validate(self, obj, value): + if isinstance(value, complex): + return value + if isinstance(value, (float, int)): + return complex(value) + self.error(obj, value) + + +class CComplex(Complex): + """A casting version of the complex number trait.""" + + def validate (self, obj, value): + try: + return complex(value) + except: + self.error(obj, value) + +# We should always be explicit about whether we're using bytes or unicode, both +# for Python 3 conversion and for reliable unicode behaviour on Python 2. So +# we don't have a Str type. +class Bytes(TraitType): + """A trait for byte strings.""" + + default_value = b'' + info_text = 'a string' + + def validate(self, obj, value): + if isinstance(value, bytes): + return value + self.error(obj, value) + + +class CBytes(Bytes): + """A casting version of the byte string trait.""" + + def validate(self, obj, value): + try: + return bytes(value) + except: + self.error(obj, value) + + +class Unicode(TraitType): + """A trait for unicode strings.""" + + default_value = u'' + info_text = 'a unicode string' + + def validate(self, obj, value): + if isinstance(value, unicode): + return value + if isinstance(value, bytes): + return unicode(value) + self.error(obj, value) + + +class CUnicode(Unicode): + """A casting version of the unicode trait.""" + + def validate(self, obj, value): + try: + return unicode(value) + except: + self.error(obj, value) + + +class ObjectName(TraitType): + """A string holding a valid object name in this version of Python. + + This does not check that the name exists in any scope.""" + info_text = "a valid object identifier in Python" + + if py3compat.PY3: + # Python 3: + coerce_str = staticmethod(lambda _,s: s) + + else: + # Python 2: + def coerce_str(self, obj, value): + "In Python 2, coerce ascii-only unicode to str" + if isinstance(value, unicode): + try: + return str(value) + except UnicodeEncodeError: + self.error(obj, value) + return value + + def validate(self, obj, value): + value = self.coerce_str(obj, value) + + if isinstance(value, str) and py3compat.isidentifier(value): + return value + self.error(obj, value) + +class DottedObjectName(ObjectName): + """A string holding a valid dotted object name in Python, such as A.b3._c""" + def validate(self, obj, value): + value = self.coerce_str(obj, value) + + if isinstance(value, str) and py3compat.isidentifier(value, dotted=True): + return value + self.error(obj, value) + + +class Bool(TraitType): + """A boolean (True, False) trait.""" + + default_value = False + info_text = 'a boolean' + + def validate(self, obj, value): + if isinstance(value, bool): + return value + self.error(obj, value) + + +class CBool(Bool): + """A casting version of the boolean trait.""" + + def validate(self, obj, value): + try: + return bool(value) + except: + self.error(obj, value) + + +class Enum(TraitType): + """An enum that whose value must be in a given sequence.""" + + def __init__(self, values, default_value=None, allow_none=True, **metadata): + self.values = values + self._allow_none = allow_none + super(Enum, self).__init__(default_value, **metadata) + + def validate(self, obj, value): + if value is None: + if self._allow_none: + return value + + if value in self.values: + return value + self.error(obj, value) + + def info(self): + """ Returns a description of the trait.""" + result = 'any of ' + repr(self.values) + if self._allow_none: + return result + ' or None' + return result + +class CaselessStrEnum(Enum): + """An enum of strings that are caseless in validate.""" + + def validate(self, obj, value): + if value is None: + if self._allow_none: + return value + + if not isinstance(value, basestring): + self.error(obj, value) + + for v in self.values: + if v.lower() == value.lower(): + return v + self.error(obj, value) + +class Container(Instance): + """An instance of a container (list, set, etc.) + + To be subclassed by overriding klass. + """ + klass = None + _valid_defaults = SequenceTypes + _trait = None + + def __init__(self, trait=None, default_value=None, allow_none=True, + **metadata): + """Create a container trait type from a list, set, or tuple. + + The default value is created by doing ``List(default_value)``, + which creates a copy of the ``default_value``. + + ``trait`` can be specified, which restricts the type of elements + in the container to that TraitType. + + If only one arg is given and it is not a Trait, it is taken as + ``default_value``: + + ``c = List([1,2,3])`` + + Parameters + ---------- + + trait : TraitType [ optional ] + the type for restricting the contents of the Container. If unspecified, + types are not checked. + + default_value : SequenceType [ optional ] + The default value for the Trait. Must be list/tuple/set, and + will be cast to the container type. + + allow_none : Bool [ default True ] + Whether to allow the value to be None + + **metadata : any + further keys for extensions to the Trait (e.g. config) + + """ + # allow List([values]): + if default_value is None and not is_trait(trait): + default_value = trait + trait = None + + if default_value is None: + args = () + elif isinstance(default_value, self._valid_defaults): + args = (default_value,) + else: + raise TypeError('default value of %s was %s' %(self.__class__.__name__, default_value)) + + if is_trait(trait): + self._trait = trait() if isinstance(trait, type) else trait + self._trait.name = 'element' + elif trait is not None: + raise TypeError("`trait` must be a Trait or None, got %s"%repr_type(trait)) + + super(Container,self).__init__(klass=self.klass, args=args, + allow_none=allow_none, **metadata) + + def element_error(self, obj, element, validator): + e = "Element of the '%s' trait of %s instance must be %s, but a value of %s was specified." \ + % (self.name, class_of(obj), validator.info(), repr_type(element)) + raise TraitError(e) + + def validate(self, obj, value): + value = super(Container, self).validate(obj, value) + if value is None: + return value + + value = self.validate_elements(obj, value) + + return value + + def validate_elements(self, obj, value): + validated = [] + if self._trait is None or isinstance(self._trait, Any): + return value + for v in value: + try: + v = self._trait.validate(obj, v) + except TraitError: + self.element_error(obj, v, self._trait) + else: + validated.append(v) + return self.klass(validated) + + +class List(Container): + """An instance of a Python list.""" + klass = list + + def __init__(self, trait=None, default_value=None, minlen=0, maxlen=sys.maxsize, + allow_none=True, **metadata): + """Create a List trait type from a list, set, or tuple. + + The default value is created by doing ``List(default_value)``, + which creates a copy of the ``default_value``. + + ``trait`` can be specified, which restricts the type of elements + in the container to that TraitType. + + If only one arg is given and it is not a Trait, it is taken as + ``default_value``: + + ``c = List([1,2,3])`` + + Parameters + ---------- + + trait : TraitType [ optional ] + the type for restricting the contents of the Container. If unspecified, + types are not checked. + + default_value : SequenceType [ optional ] + The default value for the Trait. Must be list/tuple/set, and + will be cast to the container type. + + minlen : Int [ default 0 ] + The minimum length of the input list + + maxlen : Int [ default sys.maxsize ] + The maximum length of the input list + + allow_none : Bool [ default True ] + Whether to allow the value to be None + + **metadata : any + further keys for extensions to the Trait (e.g. config) + + """ + self._minlen = minlen + self._maxlen = maxlen + super(List, self).__init__(trait=trait, default_value=default_value, + allow_none=allow_none, **metadata) + + def length_error(self, obj, value): + e = "The '%s' trait of %s instance must be of length %i <= L <= %i, but a value of %s was specified." \ + % (self.name, class_of(obj), self._minlen, self._maxlen, value) + raise TraitError(e) + + def validate_elements(self, obj, value): + length = len(value) + if length < self._minlen or length > self._maxlen: + self.length_error(obj, value) + + return super(List, self).validate_elements(obj, value) + + +class Set(Container): + """An instance of a Python set.""" + klass = set + +class Tuple(Container): + """An instance of a Python tuple.""" + klass = tuple + + def __init__(self, *traits, **metadata): + """Tuple(*traits, default_value=None, allow_none=True, **medatata) + + Create a tuple from a list, set, or tuple. + + Create a fixed-type tuple with Traits: + + ``t = Tuple(Int, Str, CStr)`` + + would be length 3, with Int,Str,CStr for each element. + + If only one arg is given and it is not a Trait, it is taken as + default_value: + + ``t = Tuple((1,2,3))`` + + Otherwise, ``default_value`` *must* be specified by keyword. + + Parameters + ---------- + + *traits : TraitTypes [ optional ] + the tsype for restricting the contents of the Tuple. If unspecified, + types are not checked. If specified, then each positional argument + corresponds to an element of the tuple. Tuples defined with traits + are of fixed length. + + default_value : SequenceType [ optional ] + The default value for the Tuple. Must be list/tuple/set, and + will be cast to a tuple. If `traits` are specified, the + `default_value` must conform to the shape and type they specify. + + allow_none : Bool [ default True ] + Whether to allow the value to be None + + **metadata : any + further keys for extensions to the Trait (e.g. config) + + """ + default_value = metadata.pop('default_value', None) + allow_none = metadata.pop('allow_none', True) + + # allow Tuple((values,)): + if len(traits) == 1 and default_value is None and not is_trait(traits[0]): + default_value = traits[0] + traits = () + + if default_value is None: + args = () + elif isinstance(default_value, self._valid_defaults): + args = (default_value,) + else: + raise TypeError('default value of %s was %s' %(self.__class__.__name__, default_value)) + + self._traits = [] + for trait in traits: + t = trait() if isinstance(trait, type) else trait + t.name = 'element' + self._traits.append(t) + + if self._traits and default_value is None: + # don't allow default to be an empty container if length is specified + args = None + super(Container,self).__init__(klass=self.klass, args=args, + allow_none=allow_none, **metadata) + + def validate_elements(self, obj, value): + if not self._traits: + # nothing to validate + return value + if len(value) != len(self._traits): + e = "The '%s' trait of %s instance requires %i elements, but a value of %s was specified." \ + % (self.name, class_of(obj), len(self._traits), repr_type(value)) + raise TraitError(e) + + validated = [] + for t,v in zip(self._traits, value): + try: + v = t.validate(obj, v) + except TraitError: + self.element_error(obj, v, t) + else: + validated.append(v) + return tuple(validated) + + +class Dict(Instance): + """An instance of a Python dict.""" + + def __init__(self, default_value=None, allow_none=True, **metadata): + """Create a dict trait type from a dict. + + The default value is created by doing ``dict(default_value)``, + which creates a copy of the ``default_value``. + """ + if default_value is None: + args = ((),) + elif isinstance(default_value, dict): + args = (default_value,) + elif isinstance(default_value, SequenceTypes): + args = (default_value,) + else: + raise TypeError('default value of Dict was %s' % default_value) + + super(Dict,self).__init__(klass=dict, args=args, + allow_none=allow_none, **metadata) + +class TCPAddress(TraitType): + """A trait for an (ip, port) tuple. + + This allows for both IPv4 IP addresses as well as hostnames. + """ + + default_value = ('127.0.0.1', 0) + info_text = 'an (ip, port) tuple' + + def validate(self, obj, value): + if isinstance(value, tuple): + if len(value) == 2: + if isinstance(value[0], basestring) and isinstance(value[1], int): + port = value[1] + if port >= 0 and port <= 65535: + return value + self.error(obj, value) + +class CRegExp(TraitType): + """A casting compiled regular expression trait. + + Accepts both strings and compiled regular expressions. The resulting + attribute will be a compiled regular expression.""" + + info_text = 'a regular expression' + + def validate(self, obj, value): + try: + return re.compile(value) + except: + self.error(obj, value)