~divmod-dev/divmod.org/trunk

« back to all changes in this revision

Viewing changes to Epsilon/epsilon/structlike.py

  • Committer: Jean-Paul Calderone
  • Date: 2014-06-29 20:33:04 UTC
  • mfrom: (2749.1.1 remove-epsilon-1325289)
  • Revision ID: exarkun@twistedmatrix.com-20140629203304-gdkmbwl1suei4m97
mergeĀ lp:~exarkun/divmod.org/remove-epsilon-1325289

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# -*- test-case-name: epsilon.test.test_structlike -*-
2
 
 
3
 
"""
4
 
This module implements convenience objects for classes which have initializers
5
 
and repr()s that describe a fixed set of attributes.
6
 
"""
7
 
 
8
 
from twisted.python import context
9
 
 
10
 
_NOT_SPECIFIED = object()
11
 
 
12
 
 
13
 
class _RecursiveReprer(object):
14
 
    """
15
 
    This object maintains state so that repr()s can tell when they are
16
 
    recursing and not do so.
17
 
    """
18
 
    def __init__(self):
19
 
        self.active = {}
20
 
 
21
 
    def recursiveRepr(self, stuff, thunk=repr):
22
 
        """
23
 
        Recursive repr().
24
 
        """
25
 
        ID = id(stuff)
26
 
        if ID in self.active:
27
 
            return '%s(...)' % (stuff.__class__.__name__,)
28
 
        else:
29
 
            try:
30
 
                self.active[ID] = stuff
31
 
                return thunk(stuff)
32
 
            finally:
33
 
                del self.active[ID]
34
 
 
35
 
 
36
 
def _contextualize(contextFactory, contextReceiver):
37
 
    """
38
 
    Invoke a callable with an argument derived from the current execution
39
 
    context (L{twisted.python.context}), or automatically created if none is
40
 
    yet present in the current context.
41
 
 
42
 
    This function, with a better name and documentation, should probably be
43
 
    somewhere in L{twisted.python.context}.  Calling context.get() and
44
 
    context.call() individually is perilous because you always have to handle
45
 
    the case where the value you're looking for isn't present; this idiom
46
 
    forces you to supply some behavior for that case.
47
 
 
48
 
    @param contextFactory: An object which is both a 0-arg callable and
49
 
    hashable; used to look up the value in the context, set the value in the
50
 
    context, and create the value (by being called).
51
 
 
52
 
    @param contextReceiver: A function that receives the value created or
53
 
    identified by contextFactory.  It is a 1-arg callable object, called with
54
 
    the result of calling the contextFactory, or retrieving the contextFactory
55
 
    from the context.
56
 
    """
57
 
    value = context.get(contextFactory, _NOT_SPECIFIED)
58
 
    if value is not _NOT_SPECIFIED:
59
 
        return contextReceiver(value)
60
 
    else:
61
 
        return context.call({contextFactory: contextFactory()},
62
 
                            _contextualize, contextFactory, contextReceiver)
63
 
 
64
 
 
65
 
 
66
 
class StructBehavior(object):
67
 
    __names__ = []
68
 
    __defaults__ = []
69
 
 
70
 
    def __init__(self, *args, **kw):
71
 
        super(StructBehavior, self).__init__()
72
 
 
73
 
        # Turn all the args into kwargs
74
 
        if len(args) > len(self.__names__):
75
 
            raise TypeError(
76
 
                "Got %d positional arguments but expected no more than %d" %
77
 
                (len(args), len(self.__names__)))
78
 
 
79
 
        for n, v in zip(self.__names__, args):
80
 
            if n in kw:
81
 
                raise TypeError("Got multiple values for argument " + n)
82
 
            kw[n] = v
83
 
 
84
 
        # Fill in defaults
85
 
        for n, v in zip(self.__names__[::-1], self.__defaults__[::-1]):
86
 
            if n not in kw:
87
 
                kw[n] = v
88
 
 
89
 
        for n in self.__names__:
90
 
            if n not in kw:
91
 
                raise TypeError('Specify a value for %r' % (n,))
92
 
            setattr(self, n, kw.pop(n))
93
 
 
94
 
        if kw:
95
 
            raise TypeError('Got unexpected arguments: ' + ', '.join(kw))
96
 
 
97
 
 
98
 
    def __repr__(self):
99
 
        """
100
 
        Generate a string representation.
101
 
        """
102
 
 
103
 
        def doit(rr):
104
 
            def _recordrepr(self2):
105
 
                """
106
 
                Internal implementation of repr() for this record.
107
 
                """
108
 
                return '%s(%s)' % (
109
 
                    self.__class__.__name__,
110
 
                    ', '.join(["%s=%s" %
111
 
                               (n, repr(getattr(self, n, None)))
112
 
                               for n in self.__names__]))
113
 
            return rr.recursiveRepr(self, _recordrepr)
114
 
        return _contextualize(_RecursiveReprer, doit)
115
 
 
116
 
 
117
 
def record(*a, **kw):
118
 
    """
119
 
    Are you tired of typing class declarations that look like this::
120
 
 
121
 
        class StuffInfo:
122
 
            def __init__(self, a=None, b=None, c=None, d=None, e=None,
123
 
                         f=None, g=None, h=None, i=None, j=None):
124
 
                self.a = a
125
 
                self.b = b
126
 
                self.c = c
127
 
                self.d = d
128
 
                # ...
129
 
 
130
 
    Epsilon can help!  That's right - for a limited time only, this function
131
 
    returns a class which provides a shortcut.  The above can be simplified
132
 
    to::
133
 
 
134
 
        StuffInfo = record(a=None, b=None, c=None, d=None, e=None,
135
 
                           f=None, g=None, h=None, i=None, j=None)
136
 
 
137
 
    if the arguments are required, rather than having defaults, it could be
138
 
    even shorter::
139
 
 
140
 
        StuffInfo = record('a b c d e f g h i j')
141
 
 
142
 
    Put more formally: C{record} optionally takes one positional argument, a
143
 
    L{str} representing attribute names as whitespace-separated identifiers; it
144
 
    also takes an arbitrary number of keyword arguments, which map attribute
145
 
    names to their default values.  If no positional argument is provided, the
146
 
    names of attributes will be inferred from the names of the defaults
147
 
    instead.
148
 
    """
149
 
    if len(a) == 1:
150
 
        attributeNames = a[0].split()
151
 
    elif len(a) == 0:
152
 
        if not kw:
153
 
            raise TypeError("Attempted to define a record with no attributes.")
154
 
        attributeNames = kw.keys()
155
 
        attributeNames.sort()
156
 
    else:
157
 
        raise TypeError(
158
 
            "record must be called with zero or one positional arguments")
159
 
 
160
 
    # Work like Python: allow defaults specified backwards from the end
161
 
    defaults = []
162
 
    for attributeName in attributeNames:
163
 
        default = kw.pop(attributeName, _NOT_SPECIFIED)
164
 
        if defaults:
165
 
            if default is _NOT_SPECIFIED:
166
 
                raise TypeError(
167
 
                    "You must specify default values like in Python; "
168
 
                    "backwards from the end of the argument list, "
169
 
                    "with no gaps")
170
 
            else:
171
 
                defaults.append(default)
172
 
        elif default is not _NOT_SPECIFIED:
173
 
            defaults.append(default)
174
 
        else:
175
 
            # This space left intentionally blank.
176
 
            pass
177
 
    if kw:
178
 
        raise TypeError("The following defaults did not apply: %r" % (kw,))
179
 
 
180
 
    return type('Record<%s>' % (' '.join(attributeNames),),
181
 
                (StructBehavior,),
182
 
                dict(__names__=attributeNames,
183
 
                     __defaults__=defaults))