1. Szymon Wróblewski
  2. ppenet


ppenet / ppenet / structtype.py

# based on recordtype recipe code
# http://code.activestate.com/recipes/576555/

__all__ = ['structtype']

import sys
import struct
from textwrap import dedent
from keyword import iskeyword

def structtype(typename, field_names, format=None, verbose=False, **default_kwds):
    '''Returns a new class with named fields and abilities of struct.Struct.

    @keyword field_defaults: A mapping from (a subset of) field names to
        default values.
    @keyword default: If provided, the default value for all fields without an
        explicit default in `field_defaults`.

    >>> Point = structtype('Point', 'x y', 'BB', default=0)
    >>> Point.__doc__           # docstring for the new class
    'Point(x, y)'
    >>> Point()                 # instantiate with defaults
    Point(x=0, y=0)
    >>> p = Point(11, y=22)     # instantiate with positional args or keywords
    >>> p[0] + p.y              # accessible by name and index
    >>> p.x = 100; p[1] =200    # modifiable by name and index
    >>> p
    Point(x=100, y=200)
    >>> x, y = p               # unpack
    >>> x, y
    (100, 200)
    >>> d = p.todict()         # convert to a dictionary
    >>> d['x']
    >>> Point(**d) == p        # convert from a dictionary
    # Parse and validate the field names.  Validation serves two purposes,
    # generating informative error messages and preventing template
    # injection attacks.
    if isinstance(field_names, basestring):
        # names separated by whitespace and/or commas
        field_names = field_names.replace(',', ' ').split()
    field_names = tuple(map(str, field_names))
    if not field_names:
        raise ValueError('Records must have at least one field')
    for name in (typename,) + field_names:
        if not min(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_names = set()
    for name in field_names:
        if name.startswith('_'):
            raise ValueError('Field names cannot start with an underscore: %r'
                             % name)
        if name in seen_names:
            raise ValueError('Encountered duplicate field name: %r' % name)
    # determine the func_defaults of __init__
    field_defaults = default_kwds.pop('field_defaults', {})
    if 'default' in default_kwds:
        default = default_kwds.pop('default')
        init_defaults = tuple(field_defaults.get(f, default) for f in field_names)
    elif not field_defaults:
        init_defaults = None
        default_fields = field_names[-len(field_defaults):]
        if set(default_fields) != set(field_defaults):
            raise ValueError('Missing default parameter values')
        init_defaults = tuple(field_defaults[f] for f in default_fields)
    if default_kwds:
        raise ValueError('Invalid keyword arguments: %s' % default_kwds)
    # Check format parameter
    if format:
            struct.pack(format, *(init_defaults if init_defaults\
                                  else (0,) * len(field_names)))
            raise ValueError('Invalid format arguments: %s' % format)

    # Create and fill-in the class template
    numfields = len(field_names)
    argtxt = ', '.join(field_names)
    reprtxt = ', '.join('%s=%%r' % f for f in field_names)
    dicttxt = ', '.join('%r: self.%s' % (f, f) for f in field_names)
    tupletxt = repr(tuple('self.%s' % f for f in field_names)).replace("'", '')
    inittxt = '; '.join('self.%s=%s' % (f, f) for f in field_names)
    itertxt = '; '.join('yield self.%s' % f for f in field_names)
    eqtxt = ' and '.join('self.%s==other.%s' % (f, f) for f in field_names)
    structtxt = '''
    __struct = struct.Struct('%(format)s')
    format = __struct.format
    size = __struct.size

    def pack(self):
        return self.__struct.pack(%(tupletxt)s)

    def pack_into(self, buffer, offset=0):
        return self.__struct.pack_into(buffer, offset, %(tupletxt)s)

    def unpack(self, string):
        %(tupletxt)s = self.__struct.unpack(string)

    def unpack_from(self, buffer, offset=0):
        %(tupletxt)s = self.__struct.unpack_from(buffer, offset)
    ''' % {'format': format, 'tupletxt': tupletxt[1:-1]} if format else ''
    template = dedent('''
        class %(typename)s(object):

            __slots__  = %(field_names)r
            field_names = __slots__

            def __init__(self, %(argtxt)s):

            def __len__(self):
                return %(numfields)d

            def __iter__(self):

            def __getitem__(self, index):
                return getattr(self, self.__slots__[index])

            def __setitem__(self, index, value):
                return setattr(self, self.__slots__[index], value)

            def todict(self):
                'Return a new dict which maps field names to their values'
                return {%(dicttxt)s}

            def __repr__(self):
                return '%(typename)s(%(reprtxt)s)' %% %(tupletxt)s

            def __eq__(self, other):
                return isinstance(other, self.__class__) and %(eqtxt)s

            def __ne__(self, other):
                return not self==other

            def __getstate__(self):
                return %(tupletxt)s

            def __setstate__(self, state):
                %(tupletxt)s = state

    ''') % locals()
    # Execute the template string in a temporary namespace
    namespace = {'struct': struct}
        exec template in namespace
        if verbose:
            print template
    except SyntaxError, e:
        raise SyntaxError(e.message + ':\n' + template)
    cls = namespace[typename]
    cls.__init__.im_func.func_defaults = init_defaults
    # 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).
    if hasattr(sys, '_getframe') and sys.platform != 'cli':
        cls.__module__ = sys._getframe(1).f_globals['__name__']
    return cls

if __name__ == '__main__':
    import doctest
    TestResults = structtype('TestResults', 'failed, attempted')
    print TestResults(*doctest.testmod())