__docformat__ = 'restructuredtext' from pattern import Pattern class Category: """ A Category is a "set" of states. The category specifies a name, certain arguments that must be provided, and constraints for the values of those arguments. Any possible state which meets these restrictions is in the category. """ def __init__(self, name, **args): """ Categories are constructed with a `name` and a series of keyword arguments. `name` is a string. Each keyword argument that is specified is the name of a state argument a state must posess to be considered to be a member of the category. The value of the keyword arguments are patterns that must match the value of the state argument. """ self.args = args self.name = name def equiv(self, other): """ A Category is equivalent to another Category if: - The other category mandates the same or more arguments - The intersection of the two is nonempty Its an odd relationship, brought about to match expected user behavior """ if self.name != other.name: return False for key, value in self.args.iteritems(): if not other.args.has_key(key): return False if not other.args[key].intersect(value).nonempty(): return False return True def subtract(self, other): """ returns a set of categories that, when combined, include all states that are in `self` but *not* in `other` """ retval = set() for key, value in other.args.iteritems(): args = {} for k, v in self.args.iteritems(): args[k] = v args[key].intersect(value.complement()) if args[key].nonempty(): retval.add(Category(self.name, **args)) def inverse_set(self): """ Returns a set of Categories that will match any state NOT matched by this one (provided it specifies the same or more arguments). """ retval = set() for key, value in self.args.iteritems(): value = value.complement() if not value.nonempty(): continue newargs = self.args.copy() newargs[key] = value retval.add(Category(self.name, **newargs)) return retval def intersect(self, other): """ Returns a new category consisting only of states that are in this category, and the category `other`. """ if self.name != other.name: return None args = {} for key in set(self.args.keys() + other.args.keys()): if not self.args.has_key(key): args[key] = other.args[key] elif not other.args.has_key(key): args[key] = self.args[key] else: val = self.args[key].intersect(other.args[key]) if not val.nonempty(): return None args[key] = val return Category(self.name, **args) def __str__(self): return "%s(%s)" % ( self.name, ", ".join(["%s: %s" % (key, val) \ for key, val in self.args.iteritems()]) ) def __repr__(self): return self.__str__() def __hash__(self): return hash((self.argstup(), self.name)) def __eq__(self, other): if not other.__class__ == Category: return False return self.name == other.name and self.argstup() == other.argstup() def subset_of(self, other): return self.intersect(other) == self def argstup(self): """ Returns arguments as a sorted tuple. """ retval = [] keys = self.args.keys() keys.sort() for key in keys: if self.args[key].full(): continue retval.append((key, self.args[key])) return tuple(retval) def fill(self, info): """ Set any arguments matched by this category that don't have explicitly required values to require the values in `info`. """ args = {} for key, value in self.args.iteritems(): if not info.has_key(key): args[key] = value elif not value.singular(): args[key] = info[key] else: args[key] = value return Category(self.name, **args)