#!/usr/bin/env python
from operator import itemgetter
from keyword import iskeyword as _iskeyword
import sys as _sys

def namedtuple(typename, field_names):
    """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)

    """

    # 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 = field_names.replace(',', ' ').split() # names separated by whitespace and/or commas
    field_names = tuple(field_names)
    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)
        seen_names.add(name)

    # Create and fill-in the class template
    numfields = len(field_names)

    type_dict = { '__doc__' : '%s(%s)' % (typename, ', '.join(field_names)),
                  '__slots__' : (),
                  '_fields' : field_names
                }

    def new(cls, *args, **kwargs):
        argcount = len(args) + len(kwargs)
        if argcount != numfields:
            raise TypeError('Expected %d arguments, got %d' % (numfields, len(result)))
        authorized = set(field_names)
        for i in kwargs :
            if i not in authorized :
                raise TypeError('Got unexpected field names: %r' % kwds.keys())
        for i in field_names :
            if i in kwargs :
                args += kwargs[i],
        return tuple.__new__(cls, args)
    type_dict['__new__'] = new

    @classmethod
    def _make(cls, iterable, new=tuple.__new__, len=len):
        'Make a new %s object from a sequence or iterable' % typename
        result = new(cls, iterable)
        if len(result) != numfields:
            raise TypeError('Expected %d arguments, got %d' % (numfields, len(result)))
        return result
    type_dict['_make'] = _make

    reprtxt = typename + '(' + ', '.join('%s=%%r' % name for name in field_names) +')'
    def repr_(self):
        return reprtxt % self
    type_dict['__repr__'] = repr_

    def _asdict(self):
        'Return a new dict which maps field names to their values'
        return dict(zip(field_names, self))
    type_dict['_asdict'] = _asdict

    def _replace(self, **kwds):
        'Return a new %s object replacing specified fields with new values' % typename
        result = self._make(map(kwds.pop, field_names, self))
        if kwds:
            raise ValueError('Got unexpected field names: %r' % kwds.keys())
        return result 
    type_dict['_replace'] = _replace

    for i, name in enumerate(field_names):
        type_dict[name] = property(itemgetter(i))

    named_tuple = type(typename, (tuple,), type_dict)

    # For pickling to work, the __module__ variable 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).
    if hasattr(_sys, '_getframe'):
        named_tuple.__module__ = _sys._getframe(1).f_globals['__name__']

    return named_tuple

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

    # 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())
