summaryrefslogtreecommitdiffstats
path: root/category.py
blob: c718002a4be56822f17091608d8345031e7cd842 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
__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)