|
import sys as _sys |
|
from keyword import iskeyword as _iskeyword |
|
|
|
|
|
def _validate_names(typename, field_names, extra_field_names): |
|
""" |
|
Ensure that all the given names are valid Python identifiers that |
|
do not start with '_'. Also check that there are no duplicates |
|
among field_names + extra_field_names. |
|
""" |
|
for name in [typename] + field_names + extra_field_names: |
|
if not isinstance(name, str): |
|
raise TypeError('typename and all field names must be strings') |
|
if not name.isidentifier(): |
|
raise ValueError('typename and all field names must be valid ' |
|
f'identifiers: {name!r}') |
|
if _iskeyword(name): |
|
raise ValueError('typename and all field names cannot be a ' |
|
f'keyword: {name!r}') |
|
|
|
seen = set() |
|
for name in field_names + extra_field_names: |
|
if name.startswith('_'): |
|
raise ValueError('Field names cannot start with an underscore: ' |
|
f'{name!r}') |
|
if name in seen: |
|
raise ValueError(f'Duplicate field name: {name!r}') |
|
seen.add(name) |
|
|
|
|
|
|
|
def _make_tuple_bunch(typename, field_names, extra_field_names=None, |
|
module=None): |
|
""" |
|
Create a namedtuple-like class with additional attributes. |
|
|
|
This function creates a subclass of tuple that acts like a namedtuple |
|
and that has additional attributes. |
|
|
|
The additional attributes are listed in `extra_field_names`. The |
|
values assigned to these attributes are not part of the tuple. |
|
|
|
The reason this function exists is to allow functions in SciPy |
|
that currently return a tuple or a namedtuple to returned objects |
|
that have additional attributes, while maintaining backwards |
|
compatibility. |
|
|
|
This should only be used to enhance *existing* functions in SciPy. |
|
New functions are free to create objects as return values without |
|
having to maintain backwards compatibility with an old tuple or |
|
namedtuple return value. |
|
|
|
Parameters |
|
---------- |
|
typename : str |
|
The name of the type. |
|
field_names : list of str |
|
List of names of the values to be stored in the tuple. These names |
|
will also be attributes of instances, so the values in the tuple |
|
can be accessed by indexing or as attributes. At least one name |
|
is required. See the Notes for additional restrictions. |
|
extra_field_names : list of str, optional |
|
List of names of values that will be stored as attributes of the |
|
object. See the notes for additional restrictions. |
|
|
|
Returns |
|
------- |
|
cls : type |
|
The new class. |
|
|
|
Notes |
|
----- |
|
There are restrictions on the names that may be used in `field_names` |
|
and `extra_field_names`: |
|
|
|
* The names must be unique--no duplicates allowed. |
|
* The names must be valid Python identifiers, and must not begin with |
|
an underscore. |
|
* The names must not be Python keywords (e.g. 'def', 'and', etc., are |
|
not allowed). |
|
|
|
Examples |
|
-------- |
|
>>> from scipy._lib._bunch import _make_tuple_bunch |
|
|
|
Create a class that acts like a namedtuple with length 2 (with field |
|
names `x` and `y`) that will also have the attributes `w` and `beta`: |
|
|
|
>>> Result = _make_tuple_bunch('Result', ['x', 'y'], ['w', 'beta']) |
|
|
|
`Result` is the new class. We call it with keyword arguments to create |
|
a new instance with given values. |
|
|
|
>>> result1 = Result(x=1, y=2, w=99, beta=0.5) |
|
>>> result1 |
|
Result(x=1, y=2, w=99, beta=0.5) |
|
|
|
`result1` acts like a tuple of length 2: |
|
|
|
>>> len(result1) |
|
2 |
|
>>> result1[:] |
|
(1, 2) |
|
|
|
The values assigned when the instance was created are available as |
|
attributes: |
|
|
|
>>> result1.y |
|
2 |
|
>>> result1.beta |
|
0.5 |
|
""" |
|
if len(field_names) == 0: |
|
raise ValueError('field_names must contain at least one name') |
|
|
|
if extra_field_names is None: |
|
extra_field_names = [] |
|
_validate_names(typename, field_names, extra_field_names) |
|
|
|
typename = _sys.intern(str(typename)) |
|
field_names = tuple(map(_sys.intern, field_names)) |
|
extra_field_names = tuple(map(_sys.intern, extra_field_names)) |
|
|
|
all_names = field_names + extra_field_names |
|
arg_list = ', '.join(field_names) |
|
full_list = ', '.join(all_names) |
|
repr_fmt = ''.join(('(', |
|
', '.join(f'{name}=%({name})r' for name in all_names), |
|
')')) |
|
tuple_new = tuple.__new__ |
|
_dict, _tuple, _zip = dict, tuple, zip |
|
|
|
|
|
|
|
s = f"""\ |
|
def __new__(_cls, {arg_list}, **extra_fields): |
|
return _tuple_new(_cls, ({arg_list},)) |
|
|
|
def __init__(self, {arg_list}, **extra_fields): |
|
for key in self._extra_fields: |
|
if key not in extra_fields: |
|
raise TypeError("missing keyword argument '%s'" % (key,)) |
|
for key, val in extra_fields.items(): |
|
if key not in self._extra_fields: |
|
raise TypeError("unexpected keyword argument '%s'" % (key,)) |
|
self.__dict__[key] = val |
|
|
|
def __setattr__(self, key, val): |
|
if key in {repr(field_names)}: |
|
raise AttributeError("can't set attribute %r of class %r" |
|
% (key, self.__class__.__name__)) |
|
else: |
|
self.__dict__[key] = val |
|
""" |
|
del arg_list |
|
namespace = {'_tuple_new': tuple_new, |
|
'__builtins__': dict(TypeError=TypeError, |
|
AttributeError=AttributeError), |
|
'__name__': f'namedtuple_{typename}'} |
|
exec(s, namespace) |
|
__new__ = namespace['__new__'] |
|
__new__.__doc__ = f'Create new instance of {typename}({full_list})' |
|
__init__ = namespace['__init__'] |
|
__init__.__doc__ = f'Instantiate instance of {typename}({full_list})' |
|
__setattr__ = namespace['__setattr__'] |
|
|
|
def __repr__(self): |
|
'Return a nicely formatted representation string' |
|
return self.__class__.__name__ + repr_fmt % self._asdict() |
|
|
|
def _asdict(self): |
|
'Return a new dict which maps field names to their values.' |
|
out = _dict(_zip(self._fields, self)) |
|
out.update(self.__dict__) |
|
return out |
|
|
|
def __getnewargs_ex__(self): |
|
'Return self as a plain tuple. Used by copy and pickle.' |
|
return _tuple(self), self.__dict__ |
|
|
|
|
|
for method in (__new__, __repr__, _asdict, __getnewargs_ex__): |
|
method.__qualname__ = f'{typename}.{method.__name__}' |
|
|
|
|
|
|
|
class_namespace = { |
|
'__doc__': f'{typename}({full_list})', |
|
'_fields': field_names, |
|
'__new__': __new__, |
|
'__init__': __init__, |
|
'__repr__': __repr__, |
|
'__setattr__': __setattr__, |
|
'_asdict': _asdict, |
|
'_extra_fields': extra_field_names, |
|
'__getnewargs_ex__': __getnewargs_ex__, |
|
} |
|
for index, name in enumerate(field_names): |
|
|
|
def _get(self, index=index): |
|
return self[index] |
|
class_namespace[name] = property(_get) |
|
for name in extra_field_names: |
|
|
|
def _get(self, name=name): |
|
return self.__dict__[name] |
|
class_namespace[name] = property(_get) |
|
|
|
result = type(typename, (tuple,), class_namespace) |
|
|
|
|
|
|
|
|
|
|
|
|
|
if module is None: |
|
try: |
|
module = _sys._getframe(1).f_globals.get('__name__', '__main__') |
|
except (AttributeError, ValueError): |
|
pass |
|
if module is not None: |
|
result.__module__ = module |
|
__new__.__module__ = module |
|
|
|
return result |
|
|