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

85 statements  

1import sys 

2from operator import itemgetter 

3from keyword import iskeyword 

4 

5 

6def _tuplegetter(index, doc): 

7 return property(itemgetter(index), doc=doc) 

8 

9 

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 """ 

32 

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

39 

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) 

51 

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}') 

61 

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) 

70 

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

78 

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 

88 

89 # Create all the named tuple methods to be added to the class namespace 

90 

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 

102 

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 

110 

111 _make.__func__.__doc__ = (f'Make a new {typename} object from a sequence ' 

112 'or iterable') 

113 

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 

119 

120 _replace.__doc__ = (f'Return a new {typename} object replacing specified ' 

121 'fields with new values') 

122 

123 def __repr__(self): 

124 'Return a nicely formatted representation string' 

125 return self.__class__.__name__ + repr_fmt % self 

126 

127 def _asdict(self): 

128 'Return a new dict which maps field names to their values.' 

129 return _dict(_zip(self._fields, self)) 

130 

131 def __getnewargs__(self): 

132 'Return self as a plain tuple. Used by copy and pickle.' 

133 return _tuple(self) 

134 

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__}' 

145 

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) 

164 

165 result = type(typename, (tuple,), class_namespace) 

166 

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 

179 

180 return result