diff options
Diffstat (limited to 'pytest/pfod.py')
-rw-r--r-- | pytest/pfod.py | 204 |
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() |