1*134e1779SJakub Wojciech Klama#! /usr/bin/env python 2*134e1779SJakub Wojciech Klama 3*134e1779SJakub Wojciech Klamafrom __future__ import print_function 4*134e1779SJakub Wojciech Klama 5*134e1779SJakub Wojciech Klama__all__ = ['pfod', 'OrderedDict'] 6*134e1779SJakub Wojciech Klama 7*134e1779SJakub Wojciech Klama### shameless stealing from namedtuple here 8*134e1779SJakub Wojciech Klama 9*134e1779SJakub Wojciech Klama""" 10*134e1779SJakub Wojciech Klamapfod - prefilled OrderedDict 11*134e1779SJakub Wojciech Klama 12*134e1779SJakub Wojciech KlamaThis is basically a hybrid of a class and an OrderedDict, 13*134e1779SJakub Wojciech Klamaor, sort of a data-only class. When an instance of the 14*134e1779SJakub Wojciech Klamaclass is created, all its fields are set to None if not 15*134e1779SJakub Wojciech Klamainitialized. 16*134e1779SJakub Wojciech Klama 17*134e1779SJakub Wojciech KlamaBecause it is an OrderedDict you can add extra fields to an 18*134e1779SJakub Wojciech Klamainstance, and they will be in inst.keys(). Because it 19*134e1779SJakub Wojciech Klamabehaves in a class-like way, if the keys are 'foo' and 'bar' 20*134e1779SJakub Wojciech Klamayou can write print(inst.foo) or inst.bar = 3. Setting an 21*134e1779SJakub Wojciech Klamaattribute that does not currently exist causes a new key 22*134e1779SJakub Wojciech Klamato be added to the instance. 23*134e1779SJakub Wojciech Klama""" 24*134e1779SJakub Wojciech Klama 25*134e1779SJakub Wojciech Klamaimport sys as _sys 26*134e1779SJakub Wojciech Klamafrom keyword import iskeyword as _iskeyword 27*134e1779SJakub Wojciech Klamafrom collections import OrderedDict 28*134e1779SJakub Wojciech Klamafrom collections import deque as _deque 29*134e1779SJakub Wojciech Klama 30*134e1779SJakub Wojciech Klama_class_template = '''\ 31*134e1779SJakub Wojciech Klamaclass {typename}(OrderedDict): 32*134e1779SJakub Wojciech Klama '{typename}({arg_list})' 33*134e1779SJakub Wojciech Klama __slots__ = () 34*134e1779SJakub Wojciech Klama 35*134e1779SJakub Wojciech Klama _fields = {field_names!r} 36*134e1779SJakub Wojciech Klama 37*134e1779SJakub Wojciech Klama def __init__(self, *args, **kwargs): 38*134e1779SJakub Wojciech Klama 'Create new instance of {typename}()' 39*134e1779SJakub Wojciech Klama super({typename}, self).__init__() 40*134e1779SJakub Wojciech Klama args = _deque(args) 41*134e1779SJakub Wojciech Klama for field in self._fields: 42*134e1779SJakub Wojciech Klama if field in kwargs: 43*134e1779SJakub Wojciech Klama self[field] = kwargs.pop(field) 44*134e1779SJakub Wojciech Klama elif len(args) > 0: 45*134e1779SJakub Wojciech Klama self[field] = args.popleft() 46*134e1779SJakub Wojciech Klama else: 47*134e1779SJakub Wojciech Klama self[field] = None 48*134e1779SJakub Wojciech Klama if len(kwargs): 49*134e1779SJakub Wojciech Klama raise TypeError('unexpected kwargs %s' % kwargs.keys()) 50*134e1779SJakub Wojciech Klama if len(args): 51*134e1779SJakub Wojciech Klama raise TypeError('unconsumed args %r' % tuple(args)) 52*134e1779SJakub Wojciech Klama 53*134e1779SJakub Wojciech Klama def _copy(self): 54*134e1779SJakub Wojciech Klama 'copy to new instance' 55*134e1779SJakub Wojciech Klama new = {typename}() 56*134e1779SJakub Wojciech Klama new.update(self) 57*134e1779SJakub Wojciech Klama return new 58*134e1779SJakub Wojciech Klama 59*134e1779SJakub Wojciech Klama def __getattr__(self, attr): 60*134e1779SJakub Wojciech Klama if attr in self: 61*134e1779SJakub Wojciech Klama return self[attr] 62*134e1779SJakub Wojciech Klama raise AttributeError('%r object has no attribute %r' % 63*134e1779SJakub Wojciech Klama (self.__class__.__name__, attr)) 64*134e1779SJakub Wojciech Klama 65*134e1779SJakub Wojciech Klama def __setattr__(self, attr, val): 66*134e1779SJakub Wojciech Klama if attr.startswith('_OrderedDict_'): 67*134e1779SJakub Wojciech Klama super({typename}, self).__setattr__(attr, val) 68*134e1779SJakub Wojciech Klama else: 69*134e1779SJakub Wojciech Klama self[attr] = val 70*134e1779SJakub Wojciech Klama 71*134e1779SJakub Wojciech Klama def __repr__(self): 72*134e1779SJakub Wojciech Klama 'Return a nicely formatted representation string' 73*134e1779SJakub Wojciech Klama return '{typename}({repr_fmt})'.format(**self) 74*134e1779SJakub Wojciech Klama''' 75*134e1779SJakub Wojciech Klama 76*134e1779SJakub Wojciech Klama_repr_template = '{name}={{{name}!r}}' 77*134e1779SJakub Wojciech Klama 78*134e1779SJakub Wojciech Klama# Workaround for py2k exec-as-statement, vs py3k exec-as-function. 79*134e1779SJakub Wojciech Klama# Since the syntax differs, we have to exec the definition of _exec! 80*134e1779SJakub Wojciech Klamaif _sys.version_info[0] < 3: 81*134e1779SJakub Wojciech Klama # py2k: need a real function. (There is a way to deal with 82*134e1779SJakub Wojciech Klama # this without a function if the py2k is new enough, but this 83*134e1779SJakub Wojciech Klama # works in more cases.) 84*134e1779SJakub Wojciech Klama exec("""def _exec(string, gdict, ldict): 85*134e1779SJakub Wojciech Klama "Python 2: exec string in gdict, ldict" 86*134e1779SJakub Wojciech Klama exec string in gdict, ldict""") 87*134e1779SJakub Wojciech Klamaelse: 88*134e1779SJakub Wojciech Klama # py3k: just make an alias for builtin function exec 89*134e1779SJakub Wojciech Klama exec("_exec = exec") 90*134e1779SJakub Wojciech Klama 91*134e1779SJakub Wojciech Klamadef pfod(typename, field_names, verbose=False, rename=False): 92*134e1779SJakub Wojciech Klama """ 93*134e1779SJakub Wojciech Klama Return a new subclass of OrderedDict with named fields. 94*134e1779SJakub Wojciech Klama 95*134e1779SJakub Wojciech Klama Fields are accessible by name. Note that this means 96*134e1779SJakub Wojciech Klama that to copy a PFOD you must use _copy() - field names 97*134e1779SJakub Wojciech Klama may not start with '_' unless they are all numeric. 98*134e1779SJakub Wojciech Klama 99*134e1779SJakub Wojciech Klama When creating an instance of the new class, fields 100*134e1779SJakub Wojciech Klama that are not initialized are set to None. 101*134e1779SJakub Wojciech Klama 102*134e1779SJakub Wojciech Klama >>> Point = pfod('Point', ['x', 'y']) 103*134e1779SJakub Wojciech Klama >>> Point.__doc__ # docstring for the new class 104*134e1779SJakub Wojciech Klama 'Point(x, y)' 105*134e1779SJakub Wojciech Klama >>> p = Point(11, y=22) # instantiate with positional args or keywords 106*134e1779SJakub Wojciech Klama >>> p 107*134e1779SJakub Wojciech Klama Point(x=11, y=22) 108*134e1779SJakub Wojciech Klama >>> p['x'] + p['y'] # indexable 109*134e1779SJakub Wojciech Klama 33 110*134e1779SJakub Wojciech Klama >>> p.x + p.y # fields also accessable by name 111*134e1779SJakub Wojciech Klama 33 112*134e1779SJakub Wojciech Klama >>> p._copy() 113*134e1779SJakub Wojciech Klama Point(x=11, y=22) 114*134e1779SJakub Wojciech Klama >>> p2 = Point() 115*134e1779SJakub Wojciech Klama >>> p2.extra = 2 116*134e1779SJakub Wojciech Klama >>> p2 117*134e1779SJakub Wojciech Klama Point(x=None, y=None) 118*134e1779SJakub Wojciech Klama >>> p2.extra 119*134e1779SJakub Wojciech Klama 2 120*134e1779SJakub Wojciech Klama >>> p2['extra'] 121*134e1779SJakub Wojciech Klama 2 122*134e1779SJakub Wojciech Klama """ 123*134e1779SJakub Wojciech Klama 124*134e1779SJakub Wojciech Klama # Validate the field names. At the user's option, either generate an error 125*134e1779SJakub Wojciech Klama if _sys.version_info[0] >= 3: 126*134e1779SJakub Wojciech Klama string_type = str 127*134e1779SJakub Wojciech Klama else: 128*134e1779SJakub Wojciech Klama string_type = basestring 129*134e1779SJakub Wojciech Klama # message or automatically replace the field name with a valid name. 130*134e1779SJakub Wojciech Klama if isinstance(field_names, string_type): 131*134e1779SJakub Wojciech Klama field_names = field_names.replace(',', ' ').split() 132*134e1779SJakub Wojciech Klama field_names = list(map(str, field_names)) 133*134e1779SJakub Wojciech Klama typename = str(typename) 134*134e1779SJakub Wojciech Klama if rename: 135*134e1779SJakub Wojciech Klama seen = set() 136*134e1779SJakub Wojciech Klama for index, name in enumerate(field_names): 137*134e1779SJakub Wojciech Klama if (not all(c.isalnum() or c=='_' for c in name) 138*134e1779SJakub Wojciech Klama or _iskeyword(name) 139*134e1779SJakub Wojciech Klama or not name 140*134e1779SJakub Wojciech Klama or name[0].isdigit() 141*134e1779SJakub Wojciech Klama or name.startswith('_') 142*134e1779SJakub Wojciech Klama or name in seen): 143*134e1779SJakub Wojciech Klama field_names[index] = '_%d' % index 144*134e1779SJakub Wojciech Klama seen.add(name) 145*134e1779SJakub Wojciech Klama for name in [typename] + field_names: 146*134e1779SJakub Wojciech Klama if type(name) != str: 147*134e1779SJakub Wojciech Klama raise TypeError('Type names and field names must be strings') 148*134e1779SJakub Wojciech Klama if not all(c.isalnum() or c=='_' for c in name): 149*134e1779SJakub Wojciech Klama raise ValueError('Type names and field names can only contain ' 150*134e1779SJakub Wojciech Klama 'alphanumeric characters and underscores: %r' % name) 151*134e1779SJakub Wojciech Klama if _iskeyword(name): 152*134e1779SJakub Wojciech Klama raise ValueError('Type names and field names cannot be a ' 153*134e1779SJakub Wojciech Klama 'keyword: %r' % name) 154*134e1779SJakub Wojciech Klama if name[0].isdigit(): 155*134e1779SJakub Wojciech Klama raise ValueError('Type names and field names cannot start with ' 156*134e1779SJakub Wojciech Klama 'a number: %r' % name) 157*134e1779SJakub Wojciech Klama seen = set() 158*134e1779SJakub Wojciech Klama for name in field_names: 159*134e1779SJakub Wojciech Klama if name.startswith('_OrderedDict_'): 160*134e1779SJakub Wojciech Klama raise ValueError('Field names cannot start with _OrderedDict_: ' 161*134e1779SJakub Wojciech Klama '%r' % name) 162*134e1779SJakub Wojciech Klama if name.startswith('_') and not rename: 163*134e1779SJakub Wojciech Klama raise ValueError('Field names cannot start with an underscore: ' 164*134e1779SJakub Wojciech Klama '%r' % name) 165*134e1779SJakub Wojciech Klama if name in seen: 166*134e1779SJakub Wojciech Klama raise ValueError('Encountered duplicate field name: %r' % name) 167*134e1779SJakub Wojciech Klama seen.add(name) 168*134e1779SJakub Wojciech Klama 169*134e1779SJakub Wojciech Klama # Fill-in the class template 170*134e1779SJakub Wojciech Klama class_definition = _class_template.format( 171*134e1779SJakub Wojciech Klama typename = typename, 172*134e1779SJakub Wojciech Klama field_names = tuple(field_names), 173*134e1779SJakub Wojciech Klama arg_list = repr(tuple(field_names)).replace("'", "")[1:-1], 174*134e1779SJakub Wojciech Klama repr_fmt = ', '.join(_repr_template.format(name=name) 175*134e1779SJakub Wojciech Klama for name in field_names), 176*134e1779SJakub Wojciech Klama ) 177*134e1779SJakub Wojciech Klama if verbose: 178*134e1779SJakub Wojciech Klama print(class_definition, 179*134e1779SJakub Wojciech Klama file=verbose if isinstance(verbose, file) else _sys.stdout) 180*134e1779SJakub Wojciech Klama 181*134e1779SJakub Wojciech Klama # Execute the template string in a temporary namespace and support 182*134e1779SJakub Wojciech Klama # tracing utilities by setting a value for frame.f_globals['__name__'] 183*134e1779SJakub Wojciech Klama namespace = dict(__name__='PFOD%s' % typename, 184*134e1779SJakub Wojciech Klama OrderedDict=OrderedDict, _deque=_deque) 185*134e1779SJakub Wojciech Klama try: 186*134e1779SJakub Wojciech Klama _exec(class_definition, namespace, namespace) 187*134e1779SJakub Wojciech Klama except SyntaxError as e: 188*134e1779SJakub Wojciech Klama raise SyntaxError(e.message + ':\n' + class_definition) 189*134e1779SJakub Wojciech Klama result = namespace[typename] 190*134e1779SJakub Wojciech Klama 191*134e1779SJakub Wojciech Klama # For pickling to work, the __module__ variable needs to be set to the frame 192*134e1779SJakub Wojciech Klama # where the named tuple is created. Bypass this step in environments where 193*134e1779SJakub Wojciech Klama # sys._getframe is not defined (Jython for example) or sys._getframe is not 194*134e1779SJakub Wojciech Klama # defined for arguments greater than 0 (IronPython). 195*134e1779SJakub Wojciech Klama try: 196*134e1779SJakub Wojciech Klama result.__module__ = _sys._getframe(1).f_globals.get('__name__', '__main__') 197*134e1779SJakub Wojciech Klama except (AttributeError, ValueError): 198*134e1779SJakub Wojciech Klama pass 199*134e1779SJakub Wojciech Klama 200*134e1779SJakub Wojciech Klama return result 201*134e1779SJakub Wojciech Klama 202*134e1779SJakub Wojciech Klamaif __name__ == '__main__': 203*134e1779SJakub Wojciech Klama import doctest 204*134e1779SJakub Wojciech Klama doctest.testmod() 205