forked from julycoding/The-Art-Of-Programming-By-July-2nd
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathrcdtype.py
146 lines (126 loc) · 5.56 KB
/
rcdtype.py
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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
__all__ = ['recordtype']
import sys
from textwrap import dedent
from keyword import iskeyword
def recordtype(typename, field_names, verbose=False, **default_kwds):
'''Returns a new class with named fields.
@keyword field_defaults: A mapping from (a subset of) field names to default
values.
@keyword default: If provided, the default value for all fields without an
explicit default in `field_defaults`.
>>> Point = recordtype('Point', 'x y', default=0)
>>> Point.__doc__ # docstring for the new class
'Point(x, y)'
>>> Point() # instantiate with defaults
Point(x=0, y=0)
>>> p = Point(11, y=22) # instantiate with positional args or keywords
>>> p[0] + p.y # accessible by name and index
33
>>> p.x = 100; p[1] =200 # modifiable by name and index
>>> p
Point(x=100, y=200)
>>> x, y = p # unpack
>>> x, y
(100, 200)
>>> d = p.todict() # convert to a dictionary
>>> d['x']
100
>>> Point(**d) == p # convert from a dictionary
True
'''
# Parse and validate the field names. Validation serves two purposes,
# generating informative error messages and preventing template injection attacks.
if isinstance(field_names, basestring):
# names separated by whitespace and/or commas
field_names = field_names.replace(',', ' ').split()
field_names = tuple(map(str, field_names))
if not field_names:
raise ValueError('Records must have at least one field')
for name in (typename,) + field_names:
if not min(c.isalnum() or c=='_' for c in name):
raise ValueError('Type names and field names can only contain '
'alphanumeric characters and underscores: %r' % name)
if iskeyword(name):
raise ValueError('Type names and field names cannot be a keyword: %r'
% name)
if name[0].isdigit():
raise ValueError('Type names and field names cannot start with a '
'number: %r' % name)
seen_names = set()
for name in field_names:
if name.startswith('_'):
raise ValueError('Field names cannot start with an underscore: %r'
% name)
if name in seen_names:
raise ValueError('Encountered duplicate field name: %r' % name)
seen_names.add(name)
# determine the func_defaults of __init__
field_defaults = default_kwds.pop('field_defaults', {})
if 'default' in default_kwds:
default = default_kwds.pop('default')
init_defaults = tuple(field_defaults.get(f,default) for f in field_names)
elif not field_defaults:
init_defaults = None
else:
default_fields = field_names[-len(field_defaults):]
if set(default_fields) != set(field_defaults):
raise ValueError('Missing default parameter values')
init_defaults = tuple(field_defaults[f] for f in default_fields)
if default_kwds:
raise ValueError('Invalid keyword arguments: %s' % default_kwds)
# Create and fill-in the class template
numfields = len(field_names)
argtxt = ', '.join(field_names)
reprtxt = ', '.join('%s=%%r' % f for f in field_names)
dicttxt = ', '.join('%r: self.%s' % (f,f) for f in field_names)
tupletxt = repr(tuple('self.%s' % f for f in field_names)).replace("'",'')
inittxt = '; '.join('self.%s=%s' % (f,f) for f in field_names)
itertxt = '; '.join('yield self.%s' % f for f in field_names)
eqtxt = ' and '.join('self.%s==other.%s' % (f,f) for f in field_names)
template = dedent('''
class %(typename)s(object):
'%(typename)s(%(argtxt)s)'
__slots__ = %(field_names)r
def __init__(self, %(argtxt)s):
%(inittxt)s
def __len__(self):
return %(numfields)d
def __iter__(self):
%(itertxt)s
def __getitem__(self, index):
return getattr(self, self.__slots__[index])
def __setitem__(self, index, value):
return setattr(self, self.__slots__[index], value)
def todict(self):
'Return a new dict which maps field names to their values'
return {%(dicttxt)s}
def __repr__(self):
return '%(typename)s(%(reprtxt)s)' %% %(tupletxt)s
def __eq__(self, other):
return isinstance(other, self.__class__) and %(eqtxt)s
def __ne__(self, other):
return not self==other
def __getstate__(self):
return %(tupletxt)s
def __setstate__(self, state):
%(tupletxt)s = state
''') % locals()
# Execute the template string in a temporary namespace
namespace = {}
try:
exec template in namespace
if verbose: print template
except SyntaxError, e:
raise SyntaxError(e.message + ':\n' + template)
cls = namespace[typename]
cls.__init__.im_func.func_defaults = init_defaults
# For pickling to work, the __module__ variable needs to be set to the frame
# where the named tuple is created. Bypass this step in enviroments where
# sys._getframe is not defined (Jython for example).
if hasattr(sys, '_getframe') and sys.platform != 'cli':
cls.__module__ = sys._getframe(1).f_globals['__name__']
return cls
if __name__ == '__main__':
import doctest
TestResults = recordtype('TestResults', 'failed, attempted')
print TestResults(*doctest.testmod())