Coverage for /usr/lib/python3/dist-packages/scipy/_lib/_bunch.py: 79%

70 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-14 15:55 +0200

1import sys as _sys 

2from keyword import iskeyword as _iskeyword 

3 

4 

5def _validate_names(typename, field_names, extra_field_names): 

6 """ 

7 Ensure that all the given names are valid Python identifiers that 

8 do not start with '_'. Also check that there are no duplicates 

9 among field_names + extra_field_names. 

10 """ 

11 for name in [typename] + field_names + extra_field_names: 

12 if type(name) is not str: 

13 raise TypeError('typename and all field names must be strings') 

14 if not name.isidentifier(): 

15 raise ValueError('typename and all field names must be valid ' 

16 f'identifiers: {name!r}') 

17 if _iskeyword(name): 

18 raise ValueError('typename and all field names cannot be a ' 

19 f'keyword: {name!r}') 

20 

21 seen = set() 

22 for name in field_names + extra_field_names: 

23 if name.startswith('_'): 

24 raise ValueError('Field names cannot start with an underscore: ' 

25 f'{name!r}') 

26 if name in seen: 

27 raise ValueError(f'Duplicate field name: {name!r}') 

28 seen.add(name) 

29 

30 

31# Note: This code is adapted from CPython:Lib/collections/__init__.py 

32def _make_tuple_bunch(typename, field_names, extra_field_names=None, 

33 module=None): 

34 """ 

35 Create a namedtuple-like class with additional attributes. 

36 

37 This function creates a subclass of tuple that acts like a namedtuple 

38 and that has additional attributes. 

39 

40 The additional attributes are listed in `extra_field_names`. The 

41 values assigned to these attributes are not part of the tuple. 

42 

43 The reason this function exists is to allow functions in SciPy 

44 that currently return a tuple or a namedtuple to returned objects 

45 that have additional attributes, while maintaining backwards 

46 compatibility. 

47 

48 This should only be used to enhance *existing* functions in SciPy. 

49 New functions are free to create objects as return values without 

50 having to maintain backwards compatibility with an old tuple or 

51 namedtuple return value. 

52 

53 Parameters 

54 ---------- 

55 typename : str 

56 The name of the type. 

57 field_names : list of str 

58 List of names of the values to be stored in the tuple. These names 

59 will also be attributes of instances, so the values in the tuple 

60 can be accessed by indexing or as attributes. At least one name 

61 is required. See the Notes for additional restrictions. 

62 extra_field_names : list of str, optional 

63 List of names of values that will be stored as attributes of the 

64 object. See the notes for additional restrictions. 

65 

66 Returns 

67 ------- 

68 cls : type 

69 The new class. 

70 

71 Notes 

72 ----- 

73 There are restrictions on the names that may be used in `field_names` 

74 and `extra_field_names`: 

75 

76 * The names must be unique--no duplicates allowed. 

77 * The names must be valid Python identifiers, and must not begin with 

78 an underscore. 

79 * The names must not be Python keywords (e.g. 'def', 'and', etc., are 

80 not allowed). 

81 

82 Examples 

83 -------- 

84 >>> from scipy._lib._bunch import _make_tuple_bunch 

85 

86 Create a class that acts like a namedtuple with length 2 (with field 

87 names `x` and `y`) that will also have the attributes `w` and `beta`: 

88 

89 >>> Result = _make_tuple_bunch('Result', ['x', 'y'], ['w', 'beta']) 

90 

91 `Result` is the new class. We call it with keyword arguments to create 

92 a new instance with given values. 

93 

94 >>> result1 = Result(x=1, y=2, w=99, beta=0.5) 

95 >>> result1 

96 Result(x=1, y=2, w=99, beta=0.5) 

97 

98 `result1` acts like a tuple of length 2: 

99 

100 >>> len(result1) 

101 2 

102 >>> result1[:] 

103 (1, 2) 

104 

105 The values assigned when the instance was created are available as 

106 attributes: 

107 

108 >>> result1.y 

109 2 

110 >>> result1.beta 

111 0.5 

112 """ 

113 if len(field_names) == 0: 

114 raise ValueError('field_names must contain at least one name') 

115 

116 if extra_field_names is None: 

117 extra_field_names = [] 

118 _validate_names(typename, field_names, extra_field_names) 

119 

120 typename = _sys.intern(str(typename)) 

121 field_names = tuple(map(_sys.intern, field_names)) 

122 extra_field_names = tuple(map(_sys.intern, extra_field_names)) 

123 

124 all_names = field_names + extra_field_names 

125 arg_list = ', '.join(field_names) 

126 full_list = ', '.join(all_names) 

127 repr_fmt = ''.join(('(', 

128 ', '.join(f'{name}=%({name})r' for name in all_names), 

129 ')')) 

130 tuple_new = tuple.__new__ 

131 _dict, _tuple, _zip = dict, tuple, zip 

132 

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

134 

135 s = f"""\ 

136def __new__(_cls, {arg_list}, **extra_fields): 

137 return _tuple_new(_cls, ({arg_list},)) 

138 

139def __init__(self, {arg_list}, **extra_fields): 

140 for key in self._extra_fields: 

141 if key not in extra_fields: 

142 raise TypeError("missing keyword argument '%s'" % (key,)) 

143 for key, val in extra_fields.items(): 

144 if key not in self._extra_fields: 

145 raise TypeError("unexpected keyword argument '%s'" % (key,)) 

146 self.__dict__[key] = val 

147 

148def __setattr__(self, key, val): 

149 if key in {repr(field_names)}: 

150 raise AttributeError("can't set attribute %r of class %r" 

151 % (key, self.__class__.__name__)) 

152 else: 

153 self.__dict__[key] = val 

154""" 

155 del arg_list 

156 namespace = {'_tuple_new': tuple_new, 

157 '__builtins__': dict(TypeError=TypeError, 

158 AttributeError=AttributeError), 

159 '__name__': f'namedtuple_{typename}'} 

160 exec(s, namespace) 

161 __new__ = namespace['__new__'] 

162 __new__.__doc__ = f'Create new instance of {typename}({full_list})' 

163 __init__ = namespace['__init__'] 

164 __init__.__doc__ = f'Instantiate instance of {typename}({full_list})' 

165 __setattr__ = namespace['__setattr__'] 

166 

167 def __repr__(self): 

168 'Return a nicely formatted representation string' 

169 return self.__class__.__name__ + repr_fmt % self._asdict() 

170 

171 def _asdict(self): 

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

173 out = _dict(_zip(self._fields, self)) 

174 out.update(self.__dict__) 

175 return out 

176 

177 def __getnewargs_ex__(self): 

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

179 return _tuple(self), self.__dict__ 

180 

181 # Modify function metadata to help with introspection and debugging 

182 for method in (__new__, __repr__, _asdict, __getnewargs_ex__): 

183 method.__qualname__ = f'{typename}.{method.__name__}' 

184 

185 # Build-up the class namespace dictionary 

186 # and use type() to build the result class 

187 class_namespace = { 

188 '__doc__': f'{typename}({full_list})', 

189 '_fields': field_names, 

190 '__new__': __new__, 

191 '__init__': __init__, 

192 '__repr__': __repr__, 

193 '__setattr__': __setattr__, 

194 '_asdict': _asdict, 

195 '_extra_fields': extra_field_names, 

196 '__getnewargs_ex__': __getnewargs_ex__, 

197 } 

198 for index, name in enumerate(field_names): 

199 

200 def _get(self, index=index): 

201 return self[index] 

202 class_namespace[name] = property(_get) 

203 for name in extra_field_names: 

204 

205 def _get(self, name=name): 

206 return self.__dict__[name] 

207 class_namespace[name] = property(_get) 

208 

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

210 

211 # For pickling to work, the __module__ variable needs to be set to the 

212 # frame where the named tuple is created. Bypass this step in environments 

213 # where sys._getframe is not defined (Jython for example) or sys._getframe 

214 # is not defined for arguments greater than 0 (IronPython), or where the 

215 # user has specified a particular module. 

216 if module is None: 

217 try: 

218 module = _sys._getframe(1).f_globals.get('__name__', '__main__') 

219 except (AttributeError, ValueError): 

220 pass 

221 if module is not None: 

222 result.__module__ = module 

223 __new__.__module__ = module 

224 

225 return result