205 lines
7.0 KiB
Python
205 lines
7.0 KiB
Python
|
#! /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()
|