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
« 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
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}')
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)
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.
37 This function creates a subclass of tuple that acts like a namedtuple
38 and that has additional attributes.
40 The additional attributes are listed in `extra_field_names`. The
41 values assigned to these attributes are not part of the tuple.
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.
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.
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.
66 Returns
67 -------
68 cls : type
69 The new class.
71 Notes
72 -----
73 There are restrictions on the names that may be used in `field_names`
74 and `extra_field_names`:
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).
82 Examples
83 --------
84 >>> from scipy._lib._bunch import _make_tuple_bunch
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`:
89 >>> Result = _make_tuple_bunch('Result', ['x', 'y'], ['w', 'beta'])
91 `Result` is the new class. We call it with keyword arguments to create
92 a new instance with given values.
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)
98 `result1` acts like a tuple of length 2:
100 >>> len(result1)
101 2
102 >>> result1[:]
103 (1, 2)
105 The values assigned when the instance was created are available as
106 attributes:
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')
116 if extra_field_names is None:
117 extra_field_names = []
118 _validate_names(typename, field_names, extra_field_names)
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))
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
133 # Create all the named tuple methods to be added to the class namespace
135 s = f"""\
136def __new__(_cls, {arg_list}, **extra_fields):
137 return _tuple_new(_cls, ({arg_list},))
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
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__']
167 def __repr__(self):
168 'Return a nicely formatted representation string'
169 return self.__class__.__name__ + repr_fmt % self._asdict()
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
177 def __getnewargs_ex__(self):
178 'Return self as a plain tuple. Used by copy and pickle.'
179 return _tuple(self), self.__dict__
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__}'
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):
200 def _get(self, index=index):
201 return self[index]
202 class_namespace[name] = property(_get)
203 for name in extra_field_names:
205 def _get(self, name=name):
206 return self.__dict__[name]
207 class_namespace[name] = property(_get)
209 result = type(typename, (tuple,), class_namespace)
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
225 return result