Welcome, guest | Sign In | My Account | Store | Cart
# collections.namedtuple, backported to Python 2.4, with a twist.
#
# This should be tested against Python 2.4 through 2.7, but is not expected
# to work in Python 3.


from operator import itemgetter as _itemgetter
from keyword import iskeyword as _iskeyword
import sys as _sys

try:
    all, any
except NameError:
    # Only needed in Python 2.4.
    def all(iterable):
        for element in iterable:
            if not element:
                return False
        return True

    def any(iterable):
        for element in iterable:
            if element:
                return True
        return False


def _check_name(name):
    """Check type or field name is valid.

    Returns an error message if name is not valid, otherwise the
    empty string.
    """
    if not name:
        err = "names must not be empty"
    elif name[0].isdigit():
        err = "name '%s' cannot start with a digit" % name
    elif name.startswith('_'):
        err = "name '%s' cannot start with an underscore" % name
    elif not all(c.isalnum() or c == '_' for c in name):
        err = ("name '%s' must only contain alphanumeric characters"
               " and underscores" % name)
    elif _iskeyword(name):
        err = "name '%s' is a reserved keyword" % name
    else:
        err = ""
    return err


def namedtuple(typename, field_names, verbose=False, rename=False):
    """Returns a new subclass of tuple with named fields.

    >>> Point = namedtuple('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[0] + p[1]                     # indexable like a plain tuple
    33
    >>> x, y = p                        # unpack like a regular tuple
    >>> x, y
    (11, 22)
    >>> p.x + p.y                       # fields also accessable by name
    33
    >>> d = p._asdict()                 # convert to a dictionary
    >>> d['x']
    11
    >>> Point(**d)                      # convert from a dictionary
    Point(x=11, y=22)
    >>> p._replace(x=100)               # _replace() is like str.replace() but targets named fields
    Point(x=100, y=22)

    """
    if not isinstance(typename, basestring):
        raise TypeError('typename must be a string, not %r' % type(typename))
    err = _check_name(typename)
    if err:
        raise ValueError(err)

    # Parse and validate the field names. Validation serves two purposes,
    # generating informative error messages and preventing template
    # injection attacks.
    if isinstance(field_names, basestring):
        # Field names separated by whitespace and/or commas.
        field_names = field_names.replace(',', ' ').split()
    field_names = list(map(str, field_names))
    seen = set()
    for i, name in enumerate(field_names):
        err = _check_name(name)
        if not err and name in seen:
            err = "duplicate name '%s'" % name
        if err:
            if rename:
                field_names[i] = "_%d" % i
            else:
                raise ValueError(err)
        else:
            seen.add(name)
    field_names = tuple(field_names)

    # === Dynamically construct the class ===

    # Unlike Raymond Hettinger's original recipe found at
    # http://code.activestate.com/recipes/500261-named-tuples/
    # we use a regular nested class. The only method which needs to be
    # generated dynamically is __new__.

    numfields = len(field_names)
    reprtxt = ', '.join('%s=%%r' % name for name in field_names)
    argtxt = ', '.join(field_names)

    class Inner(tuple):
        # Work around for annoyance: type __doc__ is read-only :-(
        __doc__ = ("%(typename)s(%(argtxt)s)"
                   % {'typename': typename, 'argtxt': argtxt})

        __slots__ = ()
        _fields = field_names

        # Don't decorate with classmethod here. See below.
        def _make(cls, iterable, new=tuple.__new__, len=len):
            """Make a new %s object from a sequence or iterable."""
            result = new(cls, iterable)
            if len(result) != numfields:
                raise TypeError('Expected %d arguments, got %d' % (numfields, len(result)))
            return result

        # Work around for annoyance: classmethod __doc__ is read-only :-(
        _make.__doc__ %= locals()
        _make = classmethod(_make)

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

        def _asdict(self):
            """Return a new dict which maps field names to their values."""
            return dict(zip(self._fields, self))

        def _replace(self, **kwds):
            """Return a new %(typename)s object replacing specified fields with new values."""
            result = self._make(map(kwds.pop, self._fields, self))
            #result = self._make(map(kwds.pop, ('x', 'y'), _self))
            if kwds:
                raise ValueError('Got unexpected field names: %r' % kwds.keys())
            return result

        def __getnewargs__(self):
            return tuple(self)

    # For pickling to work, the __module__ attribute needs to be set to the
    # frame where the named tuple is created.  Bypass this step in enviroments
    # where sys._getframe is not defined (Jython for example) or sys._getframe
    # is not defined for arguments greater than 0 (IronPython).
    try:
        Inner.__module__ = _sys._getframe(1).f_globals.get('__name__', '__main__')
    except (AttributeError, ValueError):
        pass

    # Dynamically create the __new__ method and inject it into the class.
    # We do this using exec because the method argument handling is otherwise
    # too hard. The "cls" parameter is named _cls instead to avoid clashing
    # with a field of that same name.
    ns = {'_new': tuple.__new__}
    template = """def __new__(_cls, %(argtxt)s):
        return _new(_cls, (%(argtxt)s))""" % locals()
    if verbose:
        print template
    exec template in ns, ns
    Inner.__new__ = staticmethod(ns['__new__'])  # NOT classmethod!

    # Inject properties to retrieve items by name.
    for i, name in enumerate(field_names):
        setattr(Inner, name, property(_itemgetter(i)))

    Inner.__dict__['_replace'].__doc__ %= locals()
    Inner.__name__ = typename
    return Inner


if __name__ == '__main__':
    # verify that instances can be pickled
    from cPickle import loads, dumps
    Point = namedtuple('Point', 'x, y', True)
    p = Point(x=10, y=20)
    assert p == loads(dumps(p, -1))

    # test and demonstrate ability to override methods
    class Point(namedtuple('Point', 'x y')):
        @property
        def hypot(self):
            return (self.x ** 2 + self.y ** 2) ** 0.5
        def __str__(self):
            return 'Point: x=%6.3f y=%6.3f hypot=%6.3f' % (self.x, self.y, self.hypot)

    for p in Point(3,4), Point(14,5), Point(9./7,6):
        print (p)

    class Point(namedtuple('Point', 'x y')):
        'Point class with optimized _make() and _replace() without error-checking'
        _make = classmethod(tuple.__new__)
        def _replace(self, _map=map, **kwds):
            return self._make(_map(kwds.get, ('x', 'y'), self))

    print Point(11, 22)._replace(x=100)

    import doctest
    TestResults = namedtuple('TestResults', 'failed attempted')
    print TestResults(*doctest.testmod())

History