From c48a2472d48369bbee75c1942f8cd52e88eb9ea8 2009-07-23 19:42:43 From: Brian Granger Date: 2009-07-23 19:42:43 Subject: [PATCH] Full refactor of ipstruct.Struct. This class is now based on a subclass of dict and has a full doctest test suite and rest documentation. Woohooo! --- diff --git a/IPython/core/ipmaker.py b/IPython/core/ipmaker.py index ee879e6..b468616 100644 --- a/IPython/core/ipmaker.py +++ b/IPython/core/ipmaker.py @@ -483,10 +483,10 @@ object? -> Details about 'object'. ?object also works, ?? prints more. IP_rc.update(opts_def) if rcfiledata: - # now we can update IP_rc.update(rcfiledata) IP_rc.update(opts) - IP_rc.update(rc_override) + if rc_override is not None: + IP_rc.update(rc_override) # Store the original cmd line for reference: IP_rc.opts = opts diff --git a/IPython/utils/ipstruct.py b/IPython/utils/ipstruct.py index d35aa93..1f620cf 100644 --- a/IPython/utils/ipstruct.py +++ b/IPython/utils/ipstruct.py @@ -1,219 +1,227 @@ -# -*- coding: utf-8 -*- -"""Mimic C structs with lots of extra functionality. +#!/usr/bin/env python +# encoding: utf-8 +"""A dict subclass that supports attribute style access. + +Authors: + +* Fernando Perez (original) +* Brian Granger (refactoring to a dict subclass) """ -#***************************************************************************** -# Copyright (C) 2001-2004 Fernando Perez +#----------------------------------------------------------------------------- +# Copyright (C) 2008-2009 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. -#***************************************************************************** +#----------------------------------------------------------------------------- -__all__ = ['Struct'] +#----------------------------------------------------------------------------- +# Imports +#----------------------------------------------------------------------------- -import inspect -import types import pprint from IPython.utils.genutils import list2dict2 -class Struct(object): - """Class to mimic C structs but also provide convenient dictionary-like - functionality. - - Instances can be initialized with a dictionary, a list of key=value pairs - or both. If both are present, the dictionary must come first. - - Because Python classes provide direct assignment to their members, it's - easy to overwrite normal methods (S.copy = 1 would destroy access to - S.copy()). For this reason, all builtin method names are protected and - can't be assigned to. An attempt to do s.copy=1 or s['copy']=1 will raise - a KeyError exception. If you really want to, you can bypass this - protection by directly assigning to __dict__: s.__dict__['copy']=1 will - still work. Doing this will break functionality, though. As in most of - Python, namespace protection is weakly enforced, so feel free to shoot - yourself if you really want to. - - Note that this class uses more memory and is *much* slower than a regular - dictionary, so be careful in situations where memory or performance are - critical. But for day to day use it should behave fine. It is particularly - convenient for storing configuration data in programs. - - +,+=,- and -= are implemented. +/+= do merges (non-destructive updates), - -/-= remove keys from the original. See the method descripitions. - - This class allows a quick access syntax: both s.key and s['key'] are - valid. This syntax has a limitation: each 'key' has to be explicitly - accessed by its original name. The normal s.key syntax doesn't provide - access to the keys via variables whose values evaluate to the desired - keys. An example should clarify this: - - Define a dictionary and initialize both with dict and k=v pairs: - >>> d={'a':1,'b':2} - >>> s=Struct(d,hi=10,ho=20) - - The return of __repr__ can be used to create a new instance: - >>> s - Struct({'__allownew': True, 'a': 1, 'b': 2, 'hi': 10, 'ho': 20}) - - Note: the special '__allownew' key is used for internal purposes. - - __str__ (called by print) shows it's not quite a regular dictionary: - >>> print s - Struct({'__allownew': True, 'a': 1, 'b': 2, 'hi': 10, 'ho': 20}) - - Access by explicitly named key with dot notation: - >>> s.a - 1 - - Or like a dictionary: - >>> s['a'] - 1 - - If you want a variable to hold the key value, only dictionary access works: - >>> key='hi' - >>> s.key - Traceback (most recent call last): - File "", line 1, in ? - AttributeError: Struct instance has no attribute 'key' - - >>> s[key] - 10 - - Another limitation of the s.key syntax (and Struct(key=val) - initialization): keys can't be numbers. But numeric keys can be used and - accessed using the dictionary syntax. Again, an example: - - This doesn't work (prompt changed to avoid confusing the test system): - ->> s=Struct(4='hi') - Traceback (most recent call last): - ... - SyntaxError: keyword can't be an expression - - But this does: - >>> s=Struct() - >>> s[4]='hi' - >>> s - Struct({4: 'hi', '__allownew': True}) - >>> s[4] - 'hi' - """ +__all__ = ['Struct'] + +#----------------------------------------------------------------------------- +# Code +#----------------------------------------------------------------------------- + + +class Struct(dict): + """A dict subclass with attribute style access. + + This dict subclass has a a few extra features: - # Attributes to which __setitem__ and __setattr__ will block access. - # Note: much of this will be moot in Python 2.2 and will be done in a much - # cleaner way. - __protected = ('copy dict dictcopy get has_attr has_key items keys ' - 'merge popitem setdefault update values ' - '__make_dict __dict_invert ').split() + * Attribute style access. + * Protection of class members (like keys, items) when using attribute + style access. + * The ability to restrict assignment to only existing keys. + * Intelligent merging. + * Overloaded operators. + """ - def __init__(self,data=None,**kw): + def __init__(self, *args, **kw): """Initialize with a dictionary, another Struct, or data. Parameters ---------- - data : dict, Struct - Initialize with this data. + args : dict, Struct + Initialize with one dict or Struct kw : dict Initialize with key, value pairs. Examples -------- - + + >>> s = Struct(a=10,b=30) + >>> s.a + 10 + >>> s.b + 30 + >>> s2 = Struct(s,c=30) + >>> s2.keys() + ['a', 'c', 'b'] """ object.__setattr__(self, '_allownew', True) - object.__setattr__(self, '_data',{}) - if data is None: - data = {} - if isinstance(data, Struct): - data = data.dict() - elif data and not isinstance(data, dict): - raise TypeError('initialize with a dict, Struct or key=val pairs') - data.update(kw) - # do the updating by hand to guarantee that we go through the - # safety-checked __setitem__ - for k, v in data.items(): - self[k] = v + dict.__init__(self, *args, **kw) def __setitem__(self, key, value): - """Used when struct[key] = val calls are made.""" + """Set an item with check for allownew. + + Examples + -------- + + >>> s = Struct() + >>> s['a'] = 10 + >>> s.allow_new_attr(False) + >>> s['a'] = 10 + >>> s['a'] + 10 + >>> try: + ... s['b'] = 20 + ... except KeyError: + ... print 'this is not allowed' + ... + this is not allowed + """ + if not self._allownew and not self.has_key(key): + raise KeyError( + "can't create new attribute %s when allow_new_attr(False)" % key) + dict.__setitem__(self, key, value) + + def __setattr__(self, key, value): + """Set an attr with protection of class members. + + This calls :meth:`self.__setitem__` but convert :exc:`KeyError` to + :exc:`AttributeError`. + + Examples + -------- + + >>> s = Struct() + >>> s.a = 10 + >>> s.a + 10 + >>> try: + ... s.get = 10 + ... except AttributeError: + ... print "you can't set a class member" + ... + you can't set a class member + """ + # If key is an str it might be a class member or instance var if isinstance(key, str): # I can't simply call hasattr here because it calls getattr, which # calls self.__getattr__, which returns True for keys in # self._data. But I only want keys in the class and in # self.__dict__ if key in self.__dict__ or hasattr(Struct, key): - raise KeyError( - 'key %s is a protected key of class Struct.' % key + raise AttributeError( + 'attr %s is a protected member of class Struct.' % key ) - if not self._allownew and key not in self._data: - raise KeyError( - "can't create unknown attribute %s. Check for typos, or use allow_new_attr" % key) - self._data[key] = value - - def __setattr__(self, key, value): - self.__setitem__(key, value) + try: + self.__setitem__(key, value) + except KeyError, e: + raise AttributeError(e) def __getattr__(self, key): + """Get an attr by calling :meth:`dict.__getitem__`. + + Like :meth:`__setattr__`, this method converts :exc:`KeyError` to + :exc:`AttributeError`. + + Examples + -------- + + >>> s = Struct(a=10) + >>> s.a + 10 + >>> type(s.get) + + >>> try: + ... s.b + ... except AttributeError: + ... print "I don't have that key" + ... + I don't have that key + """ try: - result = self._data[key] + result = self[key] except KeyError: raise AttributeError(key) else: return result - def __getitem__(self, key): - return self._data[key] - - def __str__(self): - return 'Struct('+ pprint.pformat(self._data)+')' - - def __repr__(self): - return self.__str__() - - def __contains__(self, key): - return key in self._data - def __iadd__(self, other): - """S += S2 is a shorthand for S.merge(S2).""" + """s += s2 is a shorthand for s.merge(s2). + + Examples + -------- + + >>> s = Struct(a=10,b=30) + >>> s2 = Struct(a=20,c=40) + >>> s += s2 + >>> s + {'a': 10, 'c': 40, 'b': 30} + """ self.merge(other) return self def __add__(self,other): - """S + S2 -> New Struct made from S.merge(S2)""" - Sout = self.copy() - Sout.merge(other) - return Sout + """s + s2 -> New Struct made from s.merge(s2). + + Examples + -------- + + >>> s1 = Struct(a=10,b=30) + >>> s2 = Struct(a=20,c=40) + >>> s = s1 + s2 + >>> s + {'a': 10, 'c': 40, 'b': 30} + """ + sout = self.copy() + sout.merge(other) + return sout def __sub__(self,other): - """Out of place remove keys from self that are in other.""" - Sout = self.copy() - Sout -= other - return Sout + """s1 - s2 -> remove keys in s2 from s1. + + Examples + -------- + + >>> s1 = Struct(a=10,b=30) + >>> s2 = Struct(a=40) + >>> s = s1 - s2 + >>> s + {'b': 30} + """ + sout = self.copy() + sout -= other + return sout def __isub__(self,other): - """Inplace remove keys from self that are in other.""" + """Inplace remove keys from self that are in other. + + Examples + -------- + + >>> s1 = Struct(a=10,b=30) + >>> s2 = Struct(a=40) + >>> s1 -= s2 + >>> s1 + {'b': 30} + """ for k in other.keys(): if self.has_key(k): - del self._data[k] + del self[k] + return self - def __make_dict(self,__loc_data__,**kw): - """Helper function for update and merge. Return a dict from data. - """ - if __loc_data__ == None: - data = {} - elif isinstance(__loc_data__, dict): - data = __loc_data__ - elif isinstance(__loc_data__, Struct): - data = __loc_data__._data - else: - raise TypeError('update with a dict, Struct or key=val pairs') - if kw: - data.update(kw) - return data - def __dict_invert(self, data): """Helper function for merge. - + Takes a dictionary whose values are lists and returns a dict with the elements of each list as keys and the original keys as values. """ @@ -224,106 +232,140 @@ class Struct(object): for entry in lst: outdict[entry] = k return outdict - - def clear(self): - """Clear all attributes.""" - self._data.clear() - - def copy(self): - """Return a (shallow) copy of a Struct.""" - return Struct(self._data.copy()) - + def dict(self): - """Return the Struct's dictionary.""" - return self._data - - def dictcopy(self): - """Return a (shallow) copy of the Struct's dictionary.""" - return self._data.copy() - - def popitem(self): - """Return (key, value) tuple and remove from Struct. + return self + + def copy(self): + """Return a copy as a Struct. - If key is not present raise KeyError. - """ - return self._data.popitem() - - def update(self,__loc_data__=None,**kw): - """Update (merge) with data from another Struct or dict. + Examples + -------- - Parameters - ---------- - __loc_data : dict, Struct - The new data to add to self. - kw : dict - Key, value pairs to add to self. + >>> s = Struct(a=10,b=30) + >>> s2 = s.copy() + >>> s2 + {'a': 10, 'b': 30} + >>> type(s2).__name__ + 'Struct' + """ + return Struct(dict.copy(self)) + + def hasattr(self, key): + """hasattr function available as a method. + + Implemented like has_key. + + Examples + -------- + + >>> s = Struct(a=10) + >>> s.hasattr('a') + True + >>> s.hasattr('b') + False + >>> s.hasattr('get') + False """ - # The funny name __loc_data__ is to prevent a common variable name - # which could be a fieled of a Struct to collide with this - # parameter. The problem would arise if the function is called with a - # keyword with this same name that a user means to add as a Struct - # field. - newdict = self.__make_dict(__loc_data__, **kw) - for k, v in newdict.iteritems(): - self[k] = v - + return self.has_key(key) + + def allow_new_attr(self, allow = True): + """Set whether new attributes can be created in this Struct. + + This can be used to catch typos by verifying that the attribute user + tries to change already exists in this Struct. + """ + object.__setattr__(self, '_allownew', allow) + def merge(self, __loc_data__=None, __conflict_solve=None, **kw): - """S.merge(data,conflict,k=v1,k=v2,...) -> merge data and k=v into S. - - This is similar to update(), but much more flexible. First, a dict is - made from data+key=value pairs. When merging this dict with the Struct - S, the optional dictionary 'conflict' is used to decide what to do. - + """Merge two Structs with customizable conflict resolution. + + This is similar to :meth:`update`, but much more flexible. First, a + dict is made from data+key=value pairs. When merging this dict with + the Struct S, the optional dictionary 'conflict' is used to decide + what to do. + If conflict is not given, the default behavior is to preserve any keys - with their current value (the opposite of the update method's + with their current value (the opposite of the :meth:`update` method's behavior). - - conflict is a dictionary of binary functions which will be used to - solve key conflicts. It must have the following structure: - - conflict == { fn1 : [Skey1,Skey2,...], fn2 : [Skey3], etc } - - Values must be lists or whitespace separated strings which are - automatically converted to lists of strings by calling string.split(). - - Each key of conflict is a function which defines a policy for - resolving conflicts when merging with the input data. Each fn must be - a binary function which returns the desired outcome for a key - conflict. These functions will be called as fn(old,new). - - An example is probably in order. Suppose you are merging the struct S - with a dict D and the following conflict policy dict: - - S.merge(D,{fn1:['a','b',4], fn2:'key_c key_d'}) - - If the key 'a' is found in both S and D, the merge method will call: - - S['a'] = fn1(S['a'],D['a']) - + + Parameters + ---------- + __loc_data : dict, Struct + The data to merge into self + __conflict_solve : dict + The conflict policy dict. The keys are binary functions used to + resolve the conflict and the values are lists of strings naming + the keys the conflict resolution function applies to. Instead of + a list of strings a space separated string can be used, like + 'a b c'. + kw : dict + Additional key, value pairs to merge in + + Notes + ----- + + The `__conflict_solve` dict is a dictionary of binary functions which will be used to + solve key conflicts. Here is an example:: + + __conflict_solve = dict( + func1=['a','b','c'], + func2=['d','e'] + ) + + In this case, the function :func:`func1` will be used to resolve + keys 'a', 'b' and 'c' and the function :func:`func2` will be used for + keys 'd' and 'e'. This could also be written as:: + + __conflict_solve = dict(func1='a b c',func2='d e') + + These functions will be called for each key they apply to with the + form:: + + func1(self['a'], other['a']) + + The return value is used as the final merged value. + As a convenience, merge() provides five (the most commonly needed) pre-defined policies: preserve, update, add, add_flip and add_s. The - easiest explanation is their implementation: - - preserve = lambda old,new: old - update = lambda old,new: new - add = lambda old,new: old + new - add_flip = lambda old,new: new + old # note change of order! - add_s = lambda old,new: old + ' ' + new # only works for strings! - - You can use those four words (as strings) as keys in conflict instead + easiest explanation is their implementation:: + + preserve = lambda old,new: old + update = lambda old,new: new + add = lambda old,new: old + new + add_flip = lambda old,new: new + old # note change of order! + add_s = lambda old,new: old + ' ' + new # only for str! + + You can use those four words (as strings) as keys instead of defining them as functions, and the merge method will substitute - the appropriate functions for you. That is, the call - - S.merge(D,{'preserve':'a b c','add':[4,5,'d'],my_function:[6]}) - - will automatically substitute the functions preserve and add for the - names 'preserve' and 'add' before making any function calls. - + the appropriate functions for you. + For more complicated conflict resolution policies, you still need to - construct your own functions. """ - - data_dict = self.__make_dict(__loc_data__,**kw) - + construct your own functions. + + Examples + -------- + + This show the default policy: + + >>> s = Struct(a=10,b=30) + >>> s2 = Struct(a=20,c=40) + >>> s.merge(s2) + >>> s + {'a': 10, 'c': 40, 'b': 30} + + Now, show how to specify a conflict dict: + + >>> s = Struct(a=10,b=30) + >>> s2 = Struct(a=20,b=40) + >>> conflict = {'update':'a','add':'b'} + >>> s.merge(s2,conflict) + >>> s + {'a': 20, 'b': 70} + """ + + data_dict = dict(__loc_data__,**kw) + # policies for conflict resolution: two argument functions which return # the value that will go in the new struct preserve = lambda old,new: old @@ -331,10 +373,10 @@ class Struct(object): add = lambda old,new: old + new add_flip = lambda old,new: new + old # note change of order! add_s = lambda old,new: old + ' ' + new - + # default policy is to keep current keys when there's a conflict conflict_solve = list2dict2(self.keys(), default = preserve) - + # the conflict_solve dictionary is given by the user 'inverted': we # need a name-function mapping, it comes as a function -> names # dict. Make a local copy (b/c we'll make changes), replace user @@ -355,63 +397,4 @@ class Struct(object): self[key] = data_dict[key] else: self[key] = conflict_solve[key](self[key],data_dict[key]) - - def has_key(self,key): - """Like has_key() dictionary method.""" - return self._data.has_key(key) - - def hasattr(self,key): - """hasattr function available as a method. - - Implemented like has_key, to make sure that all available keys in the - internal dictionary of the Struct appear also as attributes (even - numeric keys).""" - return self._data.has_key(key) - - def items(self): - """Return the items in the Struct's dictionary as (key, value)'s.""" - return self._data.items() - - def keys(self): - """Return the keys in the Struct's dictionary..""" - return self._data.keys() - - def values(self, keys=None): - """Return the values in the Struct's dictionary. - - Can be called with an optional argument keys, which must be a list or - tuple of keys. In this case it returns only the values corresponding - to those keys (allowing a form of 'slicing' for Structs). - """ - if not keys: - return self._data.values() - else: - result=[] - for k in keys: - result.append(self[k]) - return result - - def get(self, attr, val=None): - """S.get(k[,d]) -> S[k] if k in S, else d. d defaults to None.""" - try: - return self[attr] - except KeyError: - return val - - def setdefault(self, attr, val=None): - """S.setdefault(k[,d]) -> S.get(k,d), also set S[k]=d if k not in S""" - if not self._data.has_key(attr): - self[attr] = val - return self.get(attr, val) - - def allow_new_attr(self, allow = True): - """Set whether new attributes can be created in this Struct. - - This can be used to catch typos by verifying that the attribute user - tries to change already exists in this Struct. - """ - object.__setattr__(self, '_allownew', allow) - - -# end class Struct