Coverage for casanova/casanova/_namedtuple.py: 69%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1import sys
2from operator import itemgetter
3from keyword import iskeyword
6def _tuplegetter(index, doc):
7 return property(itemgetter(index), doc=doc)
10# From: https://github.com/python/cpython/blob/master/Lib/collections/__init__.py
11def future_namedtuple(typename, field_names, *, rename=False, defaults=None, module=None):
12 """Returns a new subclass of tuple with named fields.
13 >>> Point = namedtuple('Point', ['x', 'y'])
14 >>> Point.__doc__ # docstring for the new class
15 'Point(x, y)'
16 >>> p = Point(11, y=22) # instantiate with positional args or keywords
17 >>> p[0] + p[1] # indexable like a plain tuple
18 33
19 >>> x, y = p # unpack like a regular tuple
20 >>> x, y
21 (11, 22)
22 >>> p.x + p.y # fields also accessible by name
23 33
24 >>> d = p._asdict() # convert to a dictionary
25 >>> d['x']
26 11
27 >>> Point(**d) # convert from a dictionary
28 Point(x=11, y=22)
29 >>> p._replace(x=100) # _replace() is like str.replace() but targets named fields
30 Point(x=100, y=22)
31 """
33 # Validate the field names. At the user's option, either generate an error
34 # message or automatically replace the field name with a valid name.
35 if isinstance(field_names, str):
36 field_names = field_names.replace(',', ' ').split()
37 field_names = list(map(str, field_names))
38 typename = sys.intern(str(typename))
40 if rename:
41 seen = set()
42 for index, name in enumerate(field_names):
43 if (
44 not name.isidentifier() or
45 iskeyword(name) or
46 name.startswith('_') or
47 name in seen
48 ):
49 field_names[index] = f'_{index}'
50 seen.add(name)
52 for name in [typename] + field_names:
53 if type(name) is not str:
54 raise TypeError('Type names and field names must be strings')
55 if not name.isidentifier():
56 raise ValueError('Type names and field names must be valid '
57 f'identifiers: {name!r}')
58 if iskeyword(name):
59 raise ValueError('Type names and field names cannot be a '
60 f'keyword: {name!r}')
62 seen = set()
63 for name in field_names:
64 if name.startswith('_') and not rename:
65 raise ValueError('Field names cannot start with an underscore: '
66 f'{name!r}')
67 if name in seen:
68 raise ValueError(f'Encountered duplicate field name: {name!r}')
69 seen.add(name)
71 field_defaults = {}
72 if defaults is not None:
73 defaults = tuple(defaults)
74 if len(defaults) > len(field_names):
75 raise TypeError('Got more default values than field names')
76 field_defaults = dict(reversed(list(zip(reversed(field_names),
77 reversed(defaults)))))
79 # Variables used in the methods and docstrings
80 field_names = tuple(map(sys.intern, field_names))
81 num_fields = len(field_names)
82 arg_list = ', '.join(field_names)
83 if num_fields == 1:
84 arg_list += ','
85 repr_fmt = '(' + ', '.join(f'{name}=%r' for name in field_names) + ')'
86 tuple_new = tuple.__new__
87 _dict, _tuple, _len, _map, _zip = dict, tuple, len, map, zip
89 # Create all the named tuple methods to be added to the class namespace
91 namespace = {
92 '_tuple_new': tuple_new,
93 '__builtins__': {},
94 '__name__': f'namedtuple_{typename}',
95 }
96 code = f'lambda _cls, {arg_list}: _tuple_new(_cls, ({arg_list}))'
97 __new__ = eval(code, namespace)
98 __new__.__name__ = '__new__'
99 __new__.__doc__ = f'Create new instance of {typename}({arg_list})'
100 if defaults is not None:
101 __new__.__defaults__ = defaults
103 @classmethod
104 def _make(cls, iterable):
105 result = tuple_new(cls, iterable)
106 if _len(result) != num_fields:
107 raise TypeError(
108 f'Expected {num_fields} arguments, got {len(result)}')
109 return result
111 _make.__func__.__doc__ = (f'Make a new {typename} object from a sequence '
112 'or iterable')
114 def _replace(self, **kwds):
115 result = self._make(_map(kwds.pop, field_names, self))
116 if kwds:
117 raise ValueError(f'Got unexpected field names: {list(kwds)!r}')
118 return result
120 _replace.__doc__ = (f'Return a new {typename} object replacing specified '
121 'fields with new values')
123 def __repr__(self):
124 'Return a nicely formatted representation string'
125 return self.__class__.__name__ + repr_fmt % self
127 def _asdict(self):
128 'Return a new dict which maps field names to their values.'
129 return _dict(_zip(self._fields, self))
131 def __getnewargs__(self):
132 'Return self as a plain tuple. Used by copy and pickle.'
133 return _tuple(self)
135 # Modify function metadata to help with introspection and debugging
136 for method in (
137 __new__,
138 _make.__func__,
139 _replace,
140 __repr__,
141 _asdict,
142 __getnewargs__,
143 ):
144 method.__qualname__ = f'{typename}.{method.__name__}'
146 # Build-up the class namespace dictionary
147 # and use type() to build the result class
148 class_namespace = {
149 '__doc__': f'{typename}({arg_list})',
150 '__slots__': (),
151 '_fields': field_names,
152 '_field_defaults': field_defaults,
153 '__new__': __new__,
154 '_make': _make,
155 '_replace': _replace,
156 '__repr__': __repr__,
157 '_asdict': _asdict,
158 '__getnewargs__': __getnewargs__,
159 '__match_args__': field_names,
160 }
161 for index, name in enumerate(field_names):
162 doc = sys.intern(f'Alias for field number {index}')
163 class_namespace[name] = _tuplegetter(index, doc)
165 result = type(typename, (tuple,), class_namespace)
167 # For pickling to work, the __module__ variable needs to be set to the frame
168 # where the named tuple is created. Bypass this step in environments where
169 # sys._getframe is not defined (Jython for example) or sys._getframe is not
170 # defined for arguments greater than 0 (IronPython), or where the user has
171 # specified a particular module.
172 if module is None:
173 try:
174 module = sys._getframe(1).f_globals.get('__name__', '__main__')
175 except (AttributeError, ValueError):
176 pass
177 if module is not None:
178 result.__module__ = module
180 return result