#! /usr/bin/env python
from __future__ import print_function
__all__ = ['pfod', 'OrderedDict']
### shameless stealing from namedtuple here
"""
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}}'
# Workaround for py2k exec-as-statement, vs py3k exec-as-function.
# Since the syntax differs, we have to exec the definition of _exec!
if _sys.version_info[0] < 3:
# py2k: need a real function. (There is a way to deal with
# this without a function if the py2k is new enough, but this
# works in more cases.)
exec("""def _exec(string, gdict, ldict):
"Python 2: exec string in gdict, ldict"
exec string in gdict, ldict""")
else:
# py3k: just make an alias for builtin function exec
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
"""
# Validate the field names. At the user's option, either generate an error
if _sys.version_info[0] >= 3:
string_type = str
else:
string_type = basestring
# message or automatically replace the field name with a valid name.
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)
# Fill-in the class template
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)
# Execute the template string in a temporary namespace and support
# tracing utilities by setting a value for frame.f_globals['__name__']
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]
# For pickling to work, the __module__ variable needs to be set to the frame
# where the named tuple is created. Bypass this step in environments where
# sys._getframe is not defined (Jython for example) or sys._getframe is not
# defined for arguments greater than 0 (IronPython).
try:
result.__module__ = _sys._getframe(1).f_globals.get('__name__', '__main__')
except (AttributeError, ValueError):
pass
return result
if __name__ == '__main__':
import doctest
doctest.testmod()