1#! /usr/bin/env python 2 3from __future__ import print_function 4 5__all__ = ['pfod', 'OrderedDict'] 6 7### shameless stealing from namedtuple here 8 9""" 10pfod - prefilled OrderedDict 11 12This is basically a hybrid of a class and an OrderedDict, 13or, sort of a data-only class. When an instance of the 14class is created, all its fields are set to None if not 15initialized. 16 17Because it is an OrderedDict you can add extra fields to an 18instance, and they will be in inst.keys(). Because it 19behaves in a class-like way, if the keys are 'foo' and 'bar' 20you can write print(inst.foo) or inst.bar = 3. Setting an 21attribute that does not currently exist causes a new key 22to be added to the instance. 23""" 24 25import sys as _sys 26from keyword import iskeyword as _iskeyword 27from collections import OrderedDict 28from collections import deque as _deque 29 30_class_template = '''\ 31class {typename}(OrderedDict): 32 '{typename}({arg_list})' 33 __slots__ = () 34 35 _fields = {field_names!r} 36 37 def __init__(self, *args, **kwargs): 38 'Create new instance of {typename}()' 39 super({typename}, self).__init__() 40 args = _deque(args) 41 for field in self._fields: 42 if field in kwargs: 43 self[field] = kwargs.pop(field) 44 elif len(args) > 0: 45 self[field] = args.popleft() 46 else: 47 self[field] = None 48 if len(kwargs): 49 raise TypeError('unexpected kwargs %s' % kwargs.keys()) 50 if len(args): 51 raise TypeError('unconsumed args %r' % tuple(args)) 52 53 def _copy(self): 54 'copy to new instance' 55 new = {typename}() 56 new.update(self) 57 return new 58 59 def __getattr__(self, attr): 60 if attr in self: 61 return self[attr] 62 raise AttributeError('%r object has no attribute %r' % 63 (self.__class__.__name__, attr)) 64 65 def __setattr__(self, attr, val): 66 if attr.startswith('_OrderedDict_'): 67 super({typename}, self).__setattr__(attr, val) 68 else: 69 self[attr] = val 70 71 def __repr__(self): 72 'Return a nicely formatted representation string' 73 return '{typename}({repr_fmt})'.format(**self) 74''' 75 76_repr_template = '{name}={{{name}!r}}' 77 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! 80if _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""") 87else: 88 # py3k: just make an alias for builtin function exec 89 exec("_exec = exec") 90 91def pfod(typename, field_names, verbose=False, rename=False): 92 """ 93 Return a new subclass of OrderedDict with named fields. 94 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. 98 99 When creating an instance of the new class, fields 100 that are not initialized are set to None. 101 102 >>> Point = pfod('Point', ['x', 'y']) 103 >>> Point.__doc__ # docstring for the new class 104 'Point(x, y)' 105 >>> p = Point(11, y=22) # instantiate with positional args or keywords 106 >>> p 107 Point(x=11, y=22) 108 >>> p['x'] + p['y'] # indexable 109 33 110 >>> p.x + p.y # fields also accessable by name 111 33 112 >>> p._copy() 113 Point(x=11, y=22) 114 >>> p2 = Point() 115 >>> p2.extra = 2 116 >>> p2 117 Point(x=None, y=None) 118 >>> p2.extra 119 2 120 >>> p2['extra'] 121 2 122 """ 123 124 # Validate the field names. At the user's option, either generate an error 125 if _sys.version_info[0] >= 3: 126 string_type = str 127 else: 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) 134 if rename: 135 seen = set() 136 for index, name in enumerate(field_names): 137 if (not all(c.isalnum() or c=='_' for c in name) 138 or _iskeyword(name) 139 or not name 140 or name[0].isdigit() 141 or name.startswith('_') 142 or name in seen): 143 field_names[index] = '_%d' % index 144 seen.add(name) 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) 151 if _iskeyword(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) 157 seen = set() 158 for name in field_names: 159 if name.startswith('_OrderedDict_'): 160 raise ValueError('Field names cannot start with _OrderedDict_: ' 161 '%r' % name) 162 if name.startswith('_') and not rename: 163 raise ValueError('Field names cannot start with an underscore: ' 164 '%r' % name) 165 if name in seen: 166 raise ValueError('Encountered duplicate field name: %r' % name) 167 seen.add(name) 168 169 # Fill-in the class template 170 class_definition = _class_template.format( 171 typename = typename, 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), 176 ) 177 if verbose: 178 print(class_definition, 179 file=verbose if isinstance(verbose, file) else _sys.stdout) 180 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) 185 try: 186 _exec(class_definition, namespace, namespace) 187 except SyntaxError as e: 188 raise SyntaxError(e.message + ':\n' + class_definition) 189 result = namespace[typename] 190 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). 195 try: 196 result.__module__ = _sys._getframe(1).f_globals.get('__name__', '__main__') 197 except (AttributeError, ValueError): 198 pass 199 200 return result 201 202if __name__ == '__main__': 203 import doctest 204 doctest.testmod() 205