Whoops, posted wrong version of Dict + slicing module

Kenneth Manheimer (klm@NIST.GOV)
Fri, 29 Jul 1994 14:49:59 -0400 (EDT)

I mistakenly posted an old, buggy version of my Dict class wrapper
yesterday. Here is the working version, in case anyone is interested.
It implements slice-style references and manipulations of python
dictionaries - see the bottom for some basic examples.

I think the extrapolation to dictionaries of the slicing concept holds
water, and would be happy to see it incorporate in native python
dictionaries. Thus this class, as a proof-of-concept. Not to mention
an exercise in class coding.

Ken again.

# An enhanced class wrapper for dict type object, which implements
# slice-style dict operations, including addition, deletion, and
# assignement of aggregate components. See bottom for some examples.

# $Id: Dict.py,v 1.2 1994/07/19 04:19:31 klm Exp klm $

import sys

class Dict:
data = None
faux = None # For coercion fake-out vals.

def __init__(self, initVal=None):
# initVal: initial dictionary value

if initVal == None: # For defaulting bug, where default
initVal = {} # val only evaluated at method def.

if type(initVal) == type({}):
self.data = initVal
return # ===>
try:
self.data = initVal.dict() # try for a dict val
return # ===>
except:
try:
initVal.index # do we have a list-like obj?
self.faux = initVal # generate a faux object, for
# coercion's sake.
except TypeError:
raise TypeError, ('Invalid initial %s val %s' %
(self.__class__.__name__, `initVal`[:10]))
def products(self, obj):
# Encapsulate obj as an instance of self's class.
#
# This is an explicit method so it can be overriden in derived classes
# that need to produce an object of the underlying type for results of
# operations like addition, subtraction, and selection.
#
# Eg, Dbm-instance addition should not be have any affiliation with dbm
# files - the results should have Dict, not Dbm, behavior. Thus Dbm
# objects should define products to produce an instance of the Dict
# class.
return self.__class__(obj)

def keys(self): return self.data.keys()
def has_key(self, key): return self.data.has_key(key)
def items(self): return self.data.items()
def values(self): return self.data.values()
def dict(self): return self.data

def __len__(self): return len(self.data)
def __getitem__(self, keyOrWhat):
# Like regular getitem, except composite keys produce subsets that
# match the components.
#
# keyOrWhat == self: return self.
# keyOrWhat == list: return self's items that have keys on keyOrWhat
# keyOrWhat == mapping: return interesection with keyOrWhat's dict

try:
# First try a regular dict reference, returning that if successful:
return self.data[keyOrWhat] # ===>
except TypeError: pass

if id(keyOrWhat) == id(self): # Degenerate entire self ref:
return self # ===>

# Try seqence-style reference:
result = {}
try:
for key in keyOrWhat:
try:
result[key] = self.data[key]
except KeyError: pass # Skip missing keys:
return self.products(result) # ===>
except TypeError: # Must be dict-style ref
for key, val in keyOrWhat.items():
try:
if self.data[key] == val:
result[key] = val
except KeyError: pass
return self.products(result) # ===>

def __delitem__(self, keyOrWhat):
# Like regular delitem, except aggregate key affects multiple elements:
#
# key == self: clear self.
# key == seq: delete items having keys in sequence.
# key is dictful: delete items in intersection.

try:
del self.data[keyOrWhat]
return # ===>
except TypeError: pass # Deal below with exotic keys.

if id(keyOrWhat) == id(self): # Delete everything in self:
for key in self.data.keys():
del self[key]
return # ===>

try: # Do as seq, skipping invalid keys:
for key in keyOrWhat:
try:
del self.data[key]
except (KeyError, TypeError): pass
except TypeError: # Do as mapping, skipping invalid keys:
for key, val in keyOrWhat.items():
try:
if self.data[key] == val:
del self.data[key]
except (KeyError, TypeError): pass

def __setitem__(self, keyOrWhat, valOrWhat):
# Regular setitem plus provisions for aggregate key references.
#
# Aggregate key's items are deleted from self and then aggregate
# vals are added. (Vals are assigned to individual item with normal
# keys.)
#
# key == self: del all items then merge items in val
# key == list: del items having keys on list and then merge val items
# key is dictful: del items in intersection and then merge val items
#
# Aggregate of val is copied before deletions are applied, so additions
# will not be changed by deletions (eg, if self is part of additions
# ref).

# Try the standard route:
try:
self.data[keyOrWhat] = valOrWhat
return # ===>
except TypeError: pass # Handle exotic keys below.

# Standard dict assignment no go. Try aggregate-style refs (whoopee).
# Parameters are sanity checked before any changes are applied.

# Get the deletions (or barf if invalid):
if id(keyOrWhat) == id(self):
deletions = self
else:
try: # ... as sequence:
deletions = keyOrWhat[:]
except TypeError:
deletions = {}
for key, val in keyOrWhat.items():
deletions[key] = val

# Get the additions (must be a dict-type obj):
additions = {}
for key, val in valOrWhat.items():
additions[key] = val

# At this point, deletions and additions are the aggregates we need.

del self[deletions] # Do deletions using __delitem__.
for key, val in additions.items():
self.data[key] = val # Do the additions.

def __repr__(self):
teaser = `self.dict()`
if len(teaser) > 75:
return teaser[:70] + ' ...}'
else:
return teaser

def __cmp__(self, obj):
# Dict objects are compared with other objects exactly
# according to their dictionaries
if id(obj) == id(self):
return 0 # ===>
elif (type(self) == type(obj)) and (self.__class__ == obj.__class__):
return(cmp(self.data, obj.data))
elif type(self.data) == type(obj):
return(cmp(self.data, obj))
elif type(obj) == type({}):
return(cmp(self.dict(), obj))
else:
try: # try to compare as dicts:
objDict = obj.dict()
return(cmp(self.dict(), objDict))
except: # let the data try to do the compare:
return(cmp(self.data), obj)

def __add__(self, obj):
# Return a dictionary containing self's dictionary merged with obj's
# dictionary.
# Establish that obj has keys before duplicating self:
try:
objitems = obj.items()
except AttributeError:
objitems = obj # Ah, try assuming obj is dictful

result = {}
for key, val in self.items() + objitems:
result[key] = val
return self.products(result) # ===>

def __coerce__(self, other):
# It's unfortunate that coercion is mandatory for all numeric ops
# except '+' and '*', at least as of 1.0.2. This imposes what is often
# unnecessary conversions, but oh well.
return (self, self.products(other)) # ===>

def __sub__(self, obj):
# Return a Dict containing self's dictionary sans the intersection with
# obj (which may have been dict-type or seq type; coercion will have
# forced it to be a Dict, but perhaps a faux one.
result = {}
if obj.data:
for key, val in self.items():
if (key not in obj.data.keys()) or (obj.data[key] != val):
result[key] = val
return self.products(result) # ===>
elif obj.faux:
for key in self.data.keys():
if key not in obj.faux:
result[key] = self.data[key]
return self.products(result) # ===>
else:
raise TypeError, 'invalid subtrand %s' % `obj`[:20]

# These following function are only used by tests()
def squawk(expr, optmsg=None):
# Evaluate an expression that should be true, and squawk, informatively,
# if evaluation (in the calling context) returns false or if it raises an
# exception.
x, locals, globals = exterior()
if type(globals) != type({}): globals = globls.__dict__
res = prob = None
try:
res = eval(expr, globals, locals)
if not res: prob = 'test failed'
except:
prob = 'exception: %s/%s' % (sys.exc_type, sys.exc_value)
if prob:
if optmsg: prob = '(%s) %s' % (optmsg, prob)
raise squawkError, '%s\n\t %s' % (prob, expr)
else: return res

# Note that there is a bug in the python traceback handling, as of
# 1.0.3, that is worked-around by having a call to 'vars()' before the
# calls to the function *that calls* this function. Sigh.
def exterior():
# Return a triple of the code object name, local dict (or None if identical
# to global dict), and global dict for the environment in which the
# calling function was invoked. Ie, the environment of the caller's
# caller.
import __main__
bogus = 'bogus'
try: raise bogus
except bogus:
at = sys.exc_traceback.tb_frame
at = at.f_back
if at.f_back: at = at.f_back
where = at.f_code
globals = at.f_globals
locals = at.f_locals
if locals == globals:
locals = None
if globals == vars(__main__):
where = None
globals = __main__
if where:
where = where.co_name
return (where, locals, globals)

def tests():
# Exercise some basic features of this dict class.

import sys

it = Dict() # first an empty-initted dict

vars()
squawk("not it", 'Should be empty 1')

vars() # These totally spurious vars() calls
# are necessary to get around a
# traceback bug as of python 1.0.3
squawk("len(it) == 0", 'Should be empty 2')

# Now with an item:
akey, aval = 'a', 'val of a'
it[akey] = aval
vars(); squawk("it")
vars(); squawk("len(it) == 1")
vars(); squawk("it.has_key(akey)")
vars(); squawk("it['a'] == aval")
vars(); squawk("not it.has_key('xyz')")
# Ensure that KeyError is raised for ref to non-existent key:
try:
zzz = it['xyz']
except KeyError:
pass
except:
squawk("it['xyz']", "Should raise KeyError")

vars(); squawk("it.keys() == [akey]")
vars(); squawk("it.items() == [(akey, aval)]")

otherkey, otherval = 'another', 'another val'
it[{}] = {otherkey: otherval}
vars(); squawk("len(it) == 2")

vars(); squawk("it.keys().sort() == [akey, otherkey].sort()")
vars()
squawk("it.items().sort() == [(akey, aval), (otherkey, otherval)].sort()")

vars(); squawk("it[{otherkey: otherval}][otherkey] == otherval")

return 'ok'

# Uses:
#
# d[{}] = {1: 2} merges {1: 2} into dict, in place (no new dict minted)
# d + {1: 2} new dict that is the conjunction of d and {1: 2}
# (keys in latter obj take precedence)
# y = d + {} dup d
# d[range(10)] returns a Dict containing all items with keys between 1 and 10
# d[{1: 2}] = {} makes d the intersection with {1: 2}, in place
# d[d] = {} clears Dict, and more generally:
# d[d] = nother replaces d's current dict with nother's