from __future__ import print_function
__all__ = ['pfod', 'OrderedDict']
"""
pfod - prefilled OrderedDict
This is basically a hybrid of a class and an OrderedDict,
or, sort of a data-only class. When an instance of the
class is created, all its fields are set to None if not
initialized.
Because it is an OrderedDict you can add extra fields to an
instance, and they will be in inst.keys(). Because it
behaves in a class-like way, if the keys are 'foo' and 'bar'
you can write print(inst.foo) or inst.bar = 3. Setting an
attribute that does not currently exist causes a new key
to be added to the instance.
"""
import sys as _sys
from keyword import iskeyword as _iskeyword
from collections import OrderedDict
from collections import deque as _deque
_class_template = '''\
class {typename}(OrderedDict):
'{typename}({arg_list})'
__slots__ = ()
_fields = {field_names!r}
def __init__(self, *args, **kwargs):
'Create new instance of {typename}()'
super({typename}, self).__init__()
args = _deque(args)
for field in self._fields:
if field in kwargs:
self[field] = kwargs.pop(field)
elif len(args) > 0:
self[field] = args.popleft()
else:
self[field] = None
if len(kwargs):
raise TypeError('unexpected kwargs %s' % kwargs.keys())
if len(args):
raise TypeError('unconsumed args %r' % tuple(args))
def _copy(self):
'copy to new instance'
new = {typename}()
new.update(self)
return new
def __getattr__(self, attr):
if attr in self:
return self[attr]
raise AttributeError('%r object has no attribute %r' %
(self.__class__.__name__, attr))
def __setattr__(self, attr, val):
if attr.startswith('_OrderedDict_'):
super({typename}, self).__setattr__(attr, val)
else:
self[attr] = val
def __repr__(self):
'Return a nicely formatted representation string'
return '{typename}({repr_fmt})'.format(**self)
'''
_repr_template = '{name}={{{name}!r}}'
if _sys.version_info[0] < 3:
exec("""def _exec(string, gdict, ldict):
"Python 2: exec string in gdict, ldict"
exec string in gdict, ldict""")
else:
exec("_exec = exec")
def pfod(typename, field_names, verbose=False, rename=False):
"""
Return a new subclass of OrderedDict with named fields.
Fields are accessible by name. Note that this means
that to copy a PFOD you must use _copy() - field names
may not start with '_' unless they are all numeric.
When creating an instance of the new class, fields
that are not initialized are set to None.
>>> Point = pfod('Point', ['x', 'y'])
>>> Point.__doc__ # docstring for the new class
'Point(x, y)'
>>> p = Point(11, y=22) # instantiate with positional args or keywords
>>> p
Point(x=11, y=22)
>>> p['x'] + p['y'] # indexable
33
>>> p.x + p.y # fields also accessable by name
33
>>> p._copy()
Point(x=11, y=22)
>>> p2 = Point()
>>> p2.extra = 2
>>> p2
Point(x=None, y=None)
>>> p2.extra
2
>>> p2['extra']
2
"""
if _sys.version_info[0] >= 3:
string_type = str
else:
string_type = basestring
if isinstance(field_names, string_type):
field_names = field_names.replace(',', ' ').split()
field_names = list(map(str, field_names))
typename = str(typename)
if rename:
seen = set()
for index, name in enumerate(field_names):
if (not all(c.isalnum() or c=='_' for c in name)
or _iskeyword(name)
or not name
or name[0].isdigit()
or name.startswith('_')
or name in seen):
field_names[index] = '_%d' % index
seen.add(name)
for name in [typename] + field_names:
if type(name) != str:
raise TypeError('Type names and field names must be strings')
if not all(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 = set()
for name in field_names:
if name.startswith('_OrderedDict_'):
raise ValueError('Field names cannot start with _OrderedDict_: '
'%r' % name)
if name.startswith('_') and not rename:
raise ValueError('Field names cannot start with an underscore: '
'%r' % name)
if name in seen:
raise ValueError('Encountered duplicate field name: %r' % name)
seen.add(name)
class_definition = _class_template.format(
typename = typename,
field_names = tuple(field_names),
arg_list = repr(tuple(field_names)).replace("'", "")[1:-1],
repr_fmt = ', '.join(_repr_template.format(name=name)
for name in field_names),
)
if verbose:
print(class_definition,
file=verbose if isinstance(verbose, file) else _sys.stdout)
namespace = dict(__name__='PFOD%s' % typename,
OrderedDict=OrderedDict, _deque=_deque)
try:
_exec(class_definition, namespace, namespace)
except SyntaxError as e:
raise SyntaxError(e.message + ':\n' + class_definition)
result = namespace[typename]
try:
result.__module__ = _sys._getframe(1).f_globals.get('__name__', '__main__')
except (AttributeError, ValueError):
pass
return result
if __name__ == '__main__':
import doctest
doctest.testmod()