1"""
2``grizzled.collections.tuple`` contains some useful tuple-related classes
3and functions.
4"""
5__docformat__ = "restructuredtext en"
6
7# ---------------------------------------------------------------------------
8# Imports
9# ---------------------------------------------------------------------------
10
11from operator import itemgetter as _itemgetter
12from keyword import iskeyword as _iskeyword
13import sys as _sys
14
15# ---------------------------------------------------------------------------
16# Exports
17# ---------------------------------------------------------------------------
18
19__all__ = ['namedtuple']
20
21# ---------------------------------------------------------------------------
22# Public Functions
23# ---------------------------------------------------------------------------
24
25def namedtuple(typename, fieldnames, verbose=False):
26    """
27    Returns a new subclass of tuple with named fields. If running under Python
28    2.6 or newer, this function is nothing more than an alias for the
29    standard Python ``collections.namedtuple()`` function. Otherwise,
30    this function is a local implementation, adapted from an
31    `ActiveState namedtuple recipe`_.
32
33    .. _ActiveState namedtuple recipe: http://code.activestate.com/recipes/500261/
34
35    Usage:
36
37    .. python::
38
39        Point = namedtuple('Point', 'x y')
40        p0 = Point(10, 20)
41        p1 = Point(11, y=22)
42        p2 = Point(x=1, y=2)
43        print p2[0]          # prints 1
44        print p1[1]          # prints 22
45        x, y = p0            # x=10, y=20
46        print p0.x           # prints 10
47        d = p2._asdict()     # convert to dictionary
48        print d['x']         # prints 1
49
50    :Parameters:
51        typename : str
52            Name for the returned class
53
54        fieldnames : str or sequence
55            A single string with each field name separated by whitespace and/or
56            commas (e.g., 'x y' or 'x, y'). Alternatively, ``fieldnames`` can
57            be a sequence of strings such as ['x', 'y'].
58
59        verbose : bool
60            If ``True``, the class definition will be printed to standard
61            output before being returned
62
63    :rtype: class
64    :return: The named tuple class
65
66    :raise ValueError: Bad parameters
67    """
68    return _namedtuple(typename, fieldnames, verbose=verbose)
69
70# ---------------------------------------------------------------------------
71# Private
72# ---------------------------------------------------------------------------
73
74def _local_namedtuple(typename, fieldnames, verbose=False):
75    # Parse and validate the field names. Validation serves two purposes,
76    # generating informative error messages and preventing template injection
77    # attacks.
78
79    if isinstance(fieldnames, basestring):
80        # names separated by whitespace and/or commas
81        fieldnames = fieldnames.replace(',', ' ').split()
82
83    fieldnames = tuple(map(str, fieldnames))
84    for name in (typename,) + fieldnames:
85        if not min(c.isalnum() or c=='_' for c in name):
86            raise ValueError('Type names and field names can only contain '
87                             'alphanumeric characters and underscores: %r' % name)
88
89        if _iskeyword(name):
90            raise ValueError('Type names and field names cannot be a keyword: '
91                             '%r' % name)
92
93        if name[0].isdigit():
94            raise ValueError('Type names and field names cannot start with a '
95                             'number: %r' % name)
96
97    seen_names = set()
98    for name in fieldnames:
99        if name.startswith('_'):
100            raise ValueError('Field names cannot start with an underscore: '
101                             '%r' % name)
102        if name in seen_names:
103            raise ValueError('Encountered duplicate field name: %r' % name)
104
105        seen_names.add(name)
106
107    # Create and fill-in the class template
108
109    numfields = len(fieldnames)
110    argtxt = repr(fieldnames).replace("'", "")[1:-1]   # tuple repr without parens or quotes
111    reprtxt = ', '.join('%s=%%r' % name for name in fieldnames)
112    dicttxt = ', '.join('%r: t[%d]' % (name, pos) for pos, name in enumerate(fieldnames))
113    template = '''class %(typename)s(tuple):
114        '%(typename)s(%(argtxt)s)' \n
115        __slots__ = () \n
116        _fields = %(fieldnames)r \n
117        def __new__(cls, %(argtxt)s):
118            return tuple.__new__(cls, (%(argtxt)s)) \n
119        @classmethod
120        def _make(cls, iterable, new=tuple.__new__, len=len):
121            'Make a new %(typename)s object from a sequence or iterable'
122            result = new(cls, iterable)
123            if len(result) != %(numfields)d:
124                raise TypeError('Expected %(numfields)d arguments, got %%d' %% len(result))
125            return result \n
126        def __repr__(self):
127            return '%(typename)s(%(reprtxt)s)' %% self \n
128        def _asdict(t):
129            'Return a new dict which maps field names to their values'
130            return {%(dicttxt)s} \n
131        def _replace(self, **kwds):
132            'Return a new %(typename)s object replacing specified fields with new values'
133            result = self._make(map(kwds.pop, %(fieldnames)r, self))
134            if kwds:
135                raise ValueError('Got unexpected field names: %%r' %% kwds.keys())
136            return result \n\n''' % locals()
137    for i, name in enumerate(fieldnames):
138        template += '        %s = property(itemgetter(%d))\n' % (name, i)
139
140    if verbose:
141        print template
142
143    # Execute the template string in a temporary namespace
144    namespace = dict(itemgetter=_itemgetter)
145    try:
146        exec template in namespace
147    except SyntaxError, e:
148        raise SyntaxError(e.message + ':\n' + template)
149
150    result = namespace[typename]
151
152    # For pickling to work, the __module__ variable needs to be set to the
153    # frame where the named tuple is created. Bypass this step in enviroments
154    # where sys._getframe is not defined (Jython for example).
155
156    if hasattr(_sys, '_getframe') and _sys.platform != 'cli':
157        result.__module__ = _sys._getframe(1).f_globals['__name__']
158
159    return result
160
161# ---------------------------------------------------------------------------
162# Initialization
163# ---------------------------------------------------------------------------
164
165if _sys.hexversion >= 0x2060000:
166    import collections
167    _namedtuple = collections.namedtuple
168else:
169    _namedtuple = _local_namedtuple
170