3 from __future__ import print_function
5 __all__ = ['pfod', 'OrderedDict']
7 ### shameless stealing from namedtuple here
10 pfod - prefilled OrderedDict
12 This is basically a hybrid of a class and an OrderedDict,
13 or, sort of a data-only class. When an instance of the
14 class is created, all its fields are set to None if not
17 Because it is an OrderedDict you can add extra fields to an
18 instance, and they will be in inst.keys(). Because it
19 behaves in a class-like way, if the keys are 'foo' and 'bar'
20 you can write print(inst.foo) or inst.bar = 3. Setting an
21 attribute that does not currently exist causes a new key
22 to be added to the instance.
26 from keyword import iskeyword as _iskeyword
27 from collections import OrderedDict
28 from collections import deque as _deque
30 _class_template = '''\
31 class {typename}(OrderedDict):
32 '{typename}({arg_list})'
35 _fields = {field_names!r}
37 def __init__(self, *args, **kwargs):
38 'Create new instance of {typename}()'
39 super({typename}, self).__init__()
41 for field in self._fields:
43 self[field] = kwargs.pop(field)
45 self[field] = args.popleft()
49 raise TypeError('unexpected kwargs %s' % kwargs.keys())
51 raise TypeError('unconsumed args %r' % tuple(args))
54 'copy to new instance'
59 def __getattr__(self, attr):
62 raise AttributeError('%r object has no attribute %r' %
63 (self.__class__.__name__, attr))
65 def __setattr__(self, attr, val):
66 if attr.startswith('_OrderedDict_'):
67 super({typename}, self).__setattr__(attr, val)
72 'Return a nicely formatted representation string'
73 return '{typename}({repr_fmt})'.format(**self)
76 _repr_template = '{name}={{{name}!r}}'
78 # Workaround for py2k exec-as-statement, vs py3k exec-as-function.
79 # Since the syntax differs, we have to exec the definition of _exec!
80 if _sys.version_info[0] < 3:
81 # py2k: need a real function. (There is a way to deal with
82 # this without a function if the py2k is new enough, but this
83 # works in more cases.)
84 exec("""def _exec(string, gdict, ldict):
85 "Python 2: exec string in gdict, ldict"
86 exec string in gdict, ldict""")
88 # py3k: just make an alias for builtin function exec
91 def pfod(typename, field_names, verbose=False, rename=False):
93 Return a new subclass of OrderedDict with named fields.
95 Fields are accessible by name. Note that this means
96 that to copy a PFOD you must use _copy() - field names
97 may not start with '_' unless they are all numeric.
99 When creating an instance of the new class, fields
100 that are not initialized are set to None.
102 >>> Point = pfod('Point', ['x', 'y'])
103 >>> Point.__doc__ # docstring for the new class
105 >>> p = Point(11, y=22) # instantiate with positional args or keywords
108 >>> p['x'] + p['y'] # indexable
110 >>> p.x + p.y # fields also accessable by name
117 Point(x=None, y=None)
124 # Validate the field names. At the user's option, either generate an error
125 if _sys.version_info[0] >= 3:
128 string_type = basestring
129 # message or automatically replace the field name with a valid name.
130 if isinstance(field_names, string_type):
131 field_names = field_names.replace(',', ' ').split()
132 field_names = list(map(str, field_names))
133 typename = str(typename)
136 for index, name in enumerate(field_names):
137 if (not all(c.isalnum() or c=='_' for c in name)
141 or name.startswith('_')
143 field_names[index] = '_%d' % index
145 for name in [typename] + field_names:
146 if type(name) != str:
147 raise TypeError('Type names and field names must be strings')
148 if not all(c.isalnum() or c=='_' for c in name):
149 raise ValueError('Type names and field names can only contain '
150 'alphanumeric characters and underscores: %r' % name)
152 raise ValueError('Type names and field names cannot be a '
153 'keyword: %r' % name)
154 if name[0].isdigit():
155 raise ValueError('Type names and field names cannot start with '
156 'a number: %r' % name)
158 for name in field_names:
159 if name.startswith('_OrderedDict_'):
160 raise ValueError('Field names cannot start with _OrderedDict_: '
162 if name.startswith('_') and not rename:
163 raise ValueError('Field names cannot start with an underscore: '
166 raise ValueError('Encountered duplicate field name: %r' % name)
169 # Fill-in the class template
170 class_definition = _class_template.format(
172 field_names = tuple(field_names),
173 arg_list = repr(tuple(field_names)).replace("'", "")[1:-1],
174 repr_fmt = ', '.join(_repr_template.format(name=name)
175 for name in field_names),
178 print(class_definition,
179 file=verbose if isinstance(verbose, file) else _sys.stdout)
181 # Execute the template string in a temporary namespace and support
182 # tracing utilities by setting a value for frame.f_globals['__name__']
183 namespace = dict(__name__='PFOD%s' % typename,
184 OrderedDict=OrderedDict, _deque=_deque)
186 _exec(class_definition, namespace, namespace)
187 except SyntaxError as e:
188 raise SyntaxError(e.message + ':\n' + class_definition)
189 result = namespace[typename]
191 # For pickling to work, the __module__ variable needs to be set to the frame
192 # where the named tuple is created. Bypass this step in environments where
193 # sys._getframe is not defined (Jython for example) or sys._getframe is not
194 # defined for arguments greater than 0 (IronPython).
196 result.__module__ = _sys._getframe(1).f_globals.get('__name__', '__main__')
197 except (AttributeError, ValueError):
202 if __name__ == '__main__':