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