-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy paththinrecord.py
202 lines (165 loc) · 6.29 KB
/
thinrecord.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
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
"""A lightweight mutable container factory.
This is a lightweight struct that can be used when working with very
large numbers of objects. thinrecord instances do not carry around a
dict, and cannot define their own attributes. You can, however, add
methods to the class object and, with caveats, subclass the class to
add additional methods: http://stackoverflow.com/a/1816648/3109503
tl;dr subclasses must use __slots__, don't repeat variable names.
Based on collections.namedtuple, with inspiration and tests from:
https://bitbucket.org/ericvsmith/recordtype
"""
__all__ = ['thinrecord']
__author__ = "Isaac Levy <[email protected]>"
__version__ = "0.0.1"
import six as _six
import sys as _sys
from keyword import iskeyword as _iskeyword
NO_DEFAULT = object()
def _check_name(name):
err_id = 'Type names and field names'
if not isinstance(name, _six.string_types):
raise ValueError('{} must be a string (type: {!r}): '
'{!r}'.format(err_id, type(name), name))
if not name:
raise ValueError('{} cannot be empty.'.format(err_id))
if not all(c.isalnum() or c=='_' for c in name):
raise ValueError('{} can only contain alphanumerics and underscores: '
'{!r}'.format(err_id, name))
if _iskeyword(name):
raise ValueError('{} cannot be a keyword: {!r}'.format(err_id, name))
if name[0].isdigit():
raise ValueError('{} cannot start with a number: '
'{!r}'.format(err_id, name))
def thinrecord(typename, fields, default=NO_DEFAULT,
ignore_extra_kwargs=True):
# field_names must be a string or an iterable, consisting of fieldname
# strings or 2-tuples. Each 2-tuple is of the form (fieldname,
# default).
_check_name(typename)
if isinstance(fields, _six.string_types):
fields = fields.replace(',', ' ').split()
field_defaults, field_names, fields_seen = {}, [], set()
for field in fields:
if isinstance(field, _six.string_types):
field_name = field
cur_default = default
else:
try:
field_name, cur_default = field
except TypeError:
raise ValueError('Field must be string or iterable: {!r}'.format(field))
_check_name(field_name)
if field_name in ('_fields', '_items', '_update'):
raise ValueError('field name conflicts with helper method: '
'{!r}'.format(field_name))
if field_name in fields_seen:
raise ValueError('Duplicate field name: {}'.format(field_name))
fields_seen.add(field_name)
field_names.append(field_name)
if cur_default is not NO_DEFAULT:
field_defaults[field_name] = cur_default
# Create and fill-in the class template.
default_name_prefix = '_default_val_for_'
argtxt = ', '.join(field_names) # "x, y, ..."
quoted_argtxt = ', '.join("'{}'".format(f) for f in field_names)
if len(field_names) == 1:
quoted_argtxt += ','
initargs = []
for f_name in field_names:
if f_name in field_defaults:
initargs.append('{}={}'.format(f_name, default_name_prefix + f_name))
else:
initargs.append(f_name)
if ignore_extra_kwargs:
initargs.append('**_unused_kwargs')
initargs = ', '.join(initargs) # "x, y=_default_val_for_y, **_unused_kwargs"
if field_names:
initbody = '\n '.join('self.{0} = {0}'.format(f) for f in field_names)
else:
initbody = 'pass'
reprtxt = ', '.join('{}={{!r}}'.format(f) for f in field_names)
template = '''
try:
from collections import OrderedDict as _MaybeOrderedDict
except ImportError:
_MaybeOrderedDict = dict
try:
from __builtins__ import property as _property, list as _list, tuple as _tuple
except ImportError:
_property, _tuple, _list = property, tuple, list
class {typename}(object):
'{typename}({argtxt})'
__slots__ = ({quoted_argtxt})
_fields = __slots__
def __init__(self, {initargs}):
{initbody}
def __len__(self):
return {num_fields}
def __iter__(self):
"""Iterate through values."""
for var in self._fields:
yield getattr(self, var)
def _items(self):
"""A fresh list of pairs (key, val)."""
return zip(self._fields, self)
def _update(self, **kwargs):
for k, v in kwargs:
setattr(self, k, v)
@_property
def __dict__(self):
return _MaybeOrderedDict(self._items())
def __repr__(self):
return '{typename}(' + '{reprtxt}'.format(*self) + ')'
def __eq__(self, other):
return isinstance(other, self.__class__) and _tuple(self) == _tuple(other)
def __ne__(self, other):
return not self.__eq__(other)
def __lt__(self, other):
if isinstance(other, self.__class__):
return _tuple(self) < _tuple(other)
raise TypeError('Unorderable types ({typename}, {{!s}})'.format(
other.__class__.__name__))
def __ge__(self, other):
return not self.__lt__(other)
def __le__(self, other):
if isinstance(other, self.__class__):
return _tuple(self) <= _tuple(other)
raise TypeError('Unorderable types ({typename}, {{!s}})'.format(
other.__class__.__name__))
def __gt__(self, other):
return not self.__le__(other)
def __hash__(self):
raise TypeError('Unhashable type: {typename}')
def __getstate__(self):
return _tuple(self)
def __setstate__(self, state):
self.__init__(*state)
def __getitem__(self, idx):
return _tuple(self)[idx]
def __setitem__(self, idx, value):
if isinstance(idx, slice):
raise TypeError('{typename} does not support assignment by slice.')
else:
setattr(self, self._fields[idx], value)
'''.format(
typename=typename,
argtxt=argtxt,
quoted_argtxt=quoted_argtxt,
initargs=initargs,
initbody=initbody,
reprtxt=reprtxt,
num_fields=len(field_names))
# Execute the template string in a temporary namespace.
namespace = {'__name__': 'thinrecord_' + typename}
for name, default in _six.iteritems(field_defaults):
namespace[default_name_prefix + name] = default
_six.exec_(template, namespace)
cls = namespace[typename]
cls._source = template
# 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.get('__name__', '__main__')
return cls