_bunch.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. import sys as _sys
  2. from keyword import iskeyword as _iskeyword
  3. def _validate_names(typename, field_names, extra_field_names):
  4. """
  5. Ensure that all the given names are valid Python identifiers that
  6. do not start with '_'. Also check that there are no duplicates
  7. among field_names + extra_field_names.
  8. """
  9. for name in [typename] + field_names + extra_field_names:
  10. if type(name) is not str:
  11. raise TypeError('typename and all field names must be strings')
  12. if not name.isidentifier():
  13. raise ValueError('typename and all field names must be valid '
  14. f'identifiers: {name!r}')
  15. if _iskeyword(name):
  16. raise ValueError('typename and all field names cannot be a '
  17. f'keyword: {name!r}')
  18. seen = set()
  19. for name in field_names + extra_field_names:
  20. if name.startswith('_'):
  21. raise ValueError('Field names cannot start with an underscore: '
  22. f'{name!r}')
  23. if name in seen:
  24. raise ValueError(f'Duplicate field name: {name!r}')
  25. seen.add(name)
  26. # Note: This code is adapted from CPython:Lib/collections/__init__.py
  27. def _make_tuple_bunch(typename, field_names, extra_field_names=None,
  28. module=None):
  29. """
  30. Create a namedtuple-like class with additional attributes.
  31. This function creates a subclass of tuple that acts like a namedtuple
  32. and that has additional attributes.
  33. The additional attributes are listed in `extra_field_names`. The
  34. values assigned to these attributes are not part of the tuple.
  35. The reason this function exists is to allow functions in SciPy
  36. that currently return a tuple or a namedtuple to returned objects
  37. that have additional attributes, while maintaining backwards
  38. compatibility.
  39. This should only be used to enhance *existing* functions in SciPy.
  40. New functions are free to create objects as return values without
  41. having to maintain backwards compatibility with an old tuple or
  42. namedtuple return value.
  43. Parameters
  44. ----------
  45. typename : str
  46. The name of the type.
  47. field_names : list of str
  48. List of names of the values to be stored in the tuple. These names
  49. will also be attributes of instances, so the values in the tuple
  50. can be accessed by indexing or as attributes. At least one name
  51. is required. See the Notes for additional restrictions.
  52. extra_field_names : list of str, optional
  53. List of names of values that will be stored as attributes of the
  54. object. See the notes for additional restrictions.
  55. Returns
  56. -------
  57. cls : type
  58. The new class.
  59. Notes
  60. -----
  61. There are restrictions on the names that may be used in `field_names`
  62. and `extra_field_names`:
  63. * The names must be unique--no duplicates allowed.
  64. * The names must be valid Python identifiers, and must not begin with
  65. an underscore.
  66. * The names must not be Python keywords (e.g. 'def', 'and', etc., are
  67. not allowed).
  68. Examples
  69. --------
  70. >>> from scipy._lib._bunch import _make_tuple_bunch
  71. Create a class that acts like a namedtuple with length 2 (with field
  72. names `x` and `y`) that will also have the attributes `w` and `beta`:
  73. >>> Result = _make_tuple_bunch('Result', ['x', 'y'], ['w', 'beta'])
  74. `Result` is the new class. We call it with keyword arguments to create
  75. a new instance with given values.
  76. >>> result1 = Result(x=1, y=2, w=99, beta=0.5)
  77. >>> result1
  78. Result(x=1, y=2, w=99, beta=0.5)
  79. `result1` acts like a tuple of length 2:
  80. >>> len(result1)
  81. 2
  82. >>> result1[:]
  83. (1, 2)
  84. The values assigned when the instance was created are available as
  85. attributes:
  86. >>> result1.y
  87. 2
  88. >>> result1.beta
  89. 0.5
  90. """
  91. if len(field_names) == 0:
  92. raise ValueError('field_names must contain at least one name')
  93. if extra_field_names is None:
  94. extra_field_names = []
  95. _validate_names(typename, field_names, extra_field_names)
  96. typename = _sys.intern(str(typename))
  97. field_names = tuple(map(_sys.intern, field_names))
  98. extra_field_names = tuple(map(_sys.intern, extra_field_names))
  99. all_names = field_names + extra_field_names
  100. arg_list = ', '.join(field_names)
  101. full_list = ', '.join(all_names)
  102. repr_fmt = ''.join(('(',
  103. ', '.join(f'{name}=%({name})r' for name in all_names),
  104. ')'))
  105. tuple_new = tuple.__new__
  106. _dict, _tuple, _zip = dict, tuple, zip
  107. # Create all the named tuple methods to be added to the class namespace
  108. s = f"""\
  109. def __new__(_cls, {arg_list}, **extra_fields):
  110. return _tuple_new(_cls, ({arg_list},))
  111. def __init__(self, {arg_list}, **extra_fields):
  112. for key in self._extra_fields:
  113. if key not in extra_fields:
  114. raise TypeError("missing keyword argument '%s'" % (key,))
  115. for key, val in extra_fields.items():
  116. if key not in self._extra_fields:
  117. raise TypeError("unexpected keyword argument '%s'" % (key,))
  118. self.__dict__[key] = val
  119. def __setattr__(self, key, val):
  120. if key in {repr(field_names)}:
  121. raise AttributeError("can't set attribute %r of class %r"
  122. % (key, self.__class__.__name__))
  123. else:
  124. self.__dict__[key] = val
  125. """
  126. del arg_list
  127. namespace = {'_tuple_new': tuple_new,
  128. '__builtins__': dict(TypeError=TypeError,
  129. AttributeError=AttributeError),
  130. '__name__': f'namedtuple_{typename}'}
  131. exec(s, namespace)
  132. __new__ = namespace['__new__']
  133. __new__.__doc__ = f'Create new instance of {typename}({full_list})'
  134. __init__ = namespace['__init__']
  135. __init__.__doc__ = f'Instantiate instance of {typename}({full_list})'
  136. __setattr__ = namespace['__setattr__']
  137. def __repr__(self):
  138. 'Return a nicely formatted representation string'
  139. return self.__class__.__name__ + repr_fmt % self._asdict()
  140. def _asdict(self):
  141. 'Return a new dict which maps field names to their values.'
  142. out = _dict(_zip(self._fields, self))
  143. out.update(self.__dict__)
  144. return out
  145. def __getnewargs_ex__(self):
  146. 'Return self as a plain tuple. Used by copy and pickle.'
  147. return _tuple(self), self.__dict__
  148. # Modify function metadata to help with introspection and debugging
  149. for method in (__new__, __repr__, _asdict, __getnewargs_ex__):
  150. method.__qualname__ = f'{typename}.{method.__name__}'
  151. # Build-up the class namespace dictionary
  152. # and use type() to build the result class
  153. class_namespace = {
  154. '__doc__': f'{typename}({full_list})',
  155. '_fields': field_names,
  156. '__new__': __new__,
  157. '__init__': __init__,
  158. '__repr__': __repr__,
  159. '__setattr__': __setattr__,
  160. '_asdict': _asdict,
  161. '_extra_fields': extra_field_names,
  162. '__getnewargs_ex__': __getnewargs_ex__,
  163. }
  164. for index, name in enumerate(field_names):
  165. def _get(self, index=index):
  166. return self[index]
  167. class_namespace[name] = property(_get)
  168. for name in extra_field_names:
  169. def _get(self, name=name):
  170. return self.__dict__[name]
  171. class_namespace[name] = property(_get)
  172. result = type(typename, (tuple,), class_namespace)
  173. # For pickling to work, the __module__ variable needs to be set to the
  174. # frame where the named tuple is created. Bypass this step in environments
  175. # where sys._getframe is not defined (Jython for example) or sys._getframe
  176. # is not defined for arguments greater than 0 (IronPython), or where the
  177. # user has specified a particular module.
  178. if module is None:
  179. try:
  180. module = _sys._getframe(1).f_globals.get('__name__', '__main__')
  181. except (AttributeError, ValueError):
  182. pass
  183. if module is not None:
  184. result.__module__ = module
  185. __new__.__module__ = module
  186. return result