summaryrefslogtreecommitdiff
path: root/pytest/pfod.py
diff options
context:
space:
mode:
Diffstat (limited to 'pytest/pfod.py')
-rw-r--r--pytest/pfod.py204
1 files changed, 204 insertions, 0 deletions
diff --git a/pytest/pfod.py b/pytest/pfod.py
new file mode 100644
index 000000000000..6167354e88cc
--- /dev/null
+++ b/pytest/pfod.py
@@ -0,0 +1,204 @@
+#! /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()