e6aee98fb49a065b4990488bd7019f559b75b925
[sawsim.git] / pysawsim / _collections.py
1 from operator import itemgetter as _itemgetter
2 from keyword import iskeyword as _iskeyword
3 import sys as _sys
4
5 def namedtuple(typename, field_names, verbose=False):
6     """Returns a new subclass of tuple with named fields.
7
8     >>> Point = namedtuple('Point', 'x y')
9     >>> Point.__doc__                   # docstring for the new class
10     'Point(x, y)'
11     >>> p = Point(11, y=22)             # instantiate with positional args or keywords
12     >>> p[0] + p[1]                     # indexable like a plain tuple
13     33
14     >>> x, y = p                        # unpack like a regular tuple
15     >>> x, y
16     (11, 22)
17     >>> p.x + p.y                       # fields also accessable by name
18     33
19     >>> d = p._asdict()                 # convert to a dictionary
20     >>> d['x']
21     11
22     >>> Point(**d)                      # convert from a dictionary
23     Point(x=11, y=22)
24     >>> p._replace(x=100)               # _replace() is like str.replace() but targets named fields
25     Point(x=100, y=22)
26
27     """
28
29     # Parse and validate the field names.  Validation serves two purposes,
30     # generating informative error messages and preventing template injection attacks.
31     if isinstance(field_names, basestring):
32         field_names = field_names.replace(',', ' ').split() # names separated by whitespace and/or commas
33     field_names = tuple(map(str, field_names))
34     for name in (typename,) + field_names:
35         if not all(c.isalnum() or c=='_' for c in name):
36             raise ValueError('Type names and field names can only contain alphanumeric characters and underscores: %r' % name)
37         if _iskeyword(name):
38             raise ValueError('Type names and field names cannot be a keyword: %r' % name)
39         if name[0].isdigit():
40             raise ValueError('Type names and field names cannot start with a number: %r' % name)
41     seen_names = set()
42     for name in field_names:
43         if name.startswith('_'):
44             raise ValueError('Field names cannot start with an underscore: %r' % name)
45         if name in seen_names:
46             raise ValueError('Encountered duplicate field name: %r' % name)
47         seen_names.add(name)
48
49     # Create and fill-in the class template
50     numfields = len(field_names)
51     argtxt = repr(field_names).replace("'", "")[1:-1]   # tuple repr without parens or quotes
52     reprtxt = ', '.join('%s=%%r' % name for name in field_names)
53     dicttxt = ', '.join('%r: t[%d]' % (name, pos) for pos, name in enumerate(field_names))
54     template = '''class %(typename)s(tuple):
55         '%(typename)s(%(argtxt)s)' \n
56         __slots__ = () \n
57         _fields = %(field_names)r \n
58         def __new__(_cls, %(argtxt)s):
59             return _tuple.__new__(_cls, (%(argtxt)s)) \n
60         @classmethod
61         def _make(cls, iterable, new=tuple.__new__, len=len):
62             'Make a new %(typename)s object from a sequence or iterable'
63             result = new(cls, iterable)
64             if len(result) != %(numfields)d:
65                 raise TypeError('Expected %(numfields)d arguments, got %%d' %% len(result))
66             return result \n
67         def __repr__(self):
68             return '%(typename)s(%(reprtxt)s)' %% self \n
69         def _asdict(t):
70             'Return a new dict which maps field names to their values'
71             return {%(dicttxt)s} \n
72         def _replace(_self, **kwds):
73             'Return a new %(typename)s object replacing specified fields with new values'
74             result = _self._make(map(kwds.pop, %(field_names)r, _self))
75             if kwds:
76                 raise ValueError('Got unexpected field names: %%r' %% kwds.keys())
77             return result \n
78         def __getnewargs__(self):
79             return tuple(self) \n\n''' % locals()
80     for i, name in enumerate(field_names):
81         template += '        %s = _property(_itemgetter(%d))\n' % (name, i)
82     if verbose:
83         print template
84
85     # Execute the template string in a temporary namespace and
86     # support tracing utilities by setting a value for frame.f_globals['__name__']
87     namespace = dict(_itemgetter=_itemgetter, __name__='namedtuple_%s' % typename,
88                      _property=property, _tuple=tuple)
89     try:
90         exec template in namespace
91     except SyntaxError, e:
92         raise SyntaxError(e.message + ':\n' + template)
93     result = namespace[typename]
94
95     # For pickling to work, the __module__ variable needs to be set to the frame
96     # where the named tuple is created.  Bypass this step in enviroments where
97     # sys._getframe is not defined (Jython for example).
98     if hasattr(_sys, '_getframe'):
99         result.__module__ = _sys._getframe(1).f_globals.get('__name__', '__main__')
100
101     return result