Spaces:
Sleeping
Sleeping
from ._common import * | |
from ._constant import NamedConstant | |
import sys as _sys | |
__all__ = [ | |
'TupleSize', 'NamedTuple', | |
] | |
# NamedTuple | |
class NamedTupleDict(OrderedDict): | |
"""Track field order and ensure field names are not reused. | |
NamedTupleMeta will use the names found in self._field_names to translate | |
to indices. | |
""" | |
def __init__(self, *args, **kwds): | |
self._field_names = [] | |
super(NamedTupleDict, self).__init__(*args, **kwds) | |
def __setitem__(self, key, value): | |
"""Records anything not dundered or not a descriptor. | |
If a field name is used twice, an error is raised. | |
Single underscore (sunder) names are reserved. | |
""" | |
if is_sunder(key): | |
if key not in ('_size_', '_order_', '_fields_', '_review_'): | |
raise ValueError( | |
'_sunder_ names, such as %r, are reserved for future NamedTuple use' | |
% (key, ) | |
) | |
elif is_dunder(key): | |
if key == '__order__': | |
key = '_order_' | |
elif key in self._field_names: | |
# overwriting a field? | |
raise TypeError('attempt to reuse field name: %r' % (key, )) | |
elif not is_descriptor(value): | |
if key in self: | |
# field overwriting a descriptor? | |
raise TypeError('%s already defined as: %r' % (key, self[key])) | |
self._field_names.append(key) | |
super(NamedTupleDict, self).__setitem__(key, value) | |
class _TupleAttributeAtIndex(object): | |
def __init__(self, name, index, doc, default): | |
self.name = name | |
self.index = index | |
if doc is undefined: | |
doc = None | |
self.__doc__ = doc | |
self.default = default | |
def __get__(self, instance, owner): | |
if instance is None: | |
return self | |
if len(instance) <= self.index: | |
raise AttributeError('%s instance has no value for %s' % (instance.__class__.__name__, self.name)) | |
return instance[self.index] | |
def __repr__(self): | |
return '%s(%d)' % (self.__class__.__name__, self.index) | |
class undefined(object): | |
def __repr__(self): | |
return 'undefined' | |
def __bool__(self): | |
return False | |
__nonzero__ = __bool__ | |
undefined = undefined() | |
class TupleSize(NamedConstant): | |
fixed = constant('fixed', 'tuple length is static') | |
minimum = constant('minimum', 'tuple must be at least x long (x is calculated during creation') | |
variable = constant('variable', 'tuple length can be anything') | |
class NamedTupleMeta(type): | |
"""Metaclass for NamedTuple""" | |
def __prepare__(metacls, cls, bases, size=undefined, **kwds): | |
return NamedTupleDict() | |
def __init__(cls, *args , **kwds): | |
super(NamedTupleMeta, cls).__init__(*args) | |
def __new__(metacls, cls, bases, clsdict, size=undefined, **kwds): | |
if bases == (object, ): | |
bases = (tuple, object) | |
elif tuple not in bases: | |
if object in bases: | |
index = bases.index(object) | |
bases = bases[:index] + (tuple, ) + bases[index:] | |
else: | |
bases = bases + (tuple, ) | |
# include any fields from base classes | |
base_dict = NamedTupleDict() | |
namedtuple_bases = [] | |
for base in bases: | |
if isinstance(base, NamedTupleMeta): | |
namedtuple_bases.append(base) | |
i = 0 | |
if namedtuple_bases: | |
for name, index, doc, default in metacls._convert_fields(*namedtuple_bases): | |
base_dict[name] = index, doc, default | |
i = max(i, index) | |
# construct properly ordered dict with normalized indexes | |
for k, v in clsdict.items(): | |
base_dict[k] = v | |
original_dict = base_dict | |
if size is not undefined and '_size_' in original_dict: | |
raise TypeError('_size_ cannot be set if "size" is passed in header') | |
add_order = isinstance(clsdict, NamedTupleDict) | |
clsdict = NamedTupleDict() | |
clsdict.setdefault('_size_', size or TupleSize.fixed) | |
unnumbered = OrderedDict() | |
numbered = OrderedDict() | |
_order_ = original_dict.pop('_order_', []) | |
if _order_ : | |
_order_ = _order_.replace(',',' ').split() | |
add_order = False | |
# and process this class | |
for k, v in original_dict.items(): | |
if k not in original_dict._field_names: | |
clsdict[k] = v | |
else: | |
# TODO:normalize v here | |
if isinstance(v, baseinteger): | |
# assume an offset | |
v = v, undefined, undefined | |
i = v[0] + 1 | |
target = numbered | |
elif isinstance(v, basestring): | |
# assume a docstring | |
if add_order: | |
v = i, v, undefined | |
i += 1 | |
target = numbered | |
else: | |
v = undefined, v, undefined | |
target = unnumbered | |
elif isinstance(v, tuple) and len(v) in (2, 3) and isinstance(v[0], baseinteger) and isinstance(v[1], (basestring, NoneType)): | |
# assume an offset, a docstring, and (maybe) a default | |
if len(v) == 2: | |
v = v + (undefined, ) | |
v = v | |
i = v[0] + 1 | |
target = numbered | |
elif isinstance(v, tuple) and len(v) in (1, 2) and isinstance(v[0], (basestring, NoneType)): | |
# assume a docstring, and (maybe) a default | |
if len(v) == 1: | |
v = v + (undefined, ) | |
if add_order: | |
v = (i, ) + v | |
i += 1 | |
target = numbered | |
else: | |
v = (undefined, ) + v | |
target = unnumbered | |
else: | |
# refuse to guess further | |
raise ValueError('not sure what to do with %s=%r (should be OFFSET [, DOC [, DEFAULT]])' % (k, v)) | |
target[k] = v | |
# all index values have been normalized | |
# deal with _order_ (or lack thereof) | |
fields = [] | |
aliases = [] | |
seen = set() | |
max_len = 0 | |
if not _order_: | |
if unnumbered: | |
raise ValueError("_order_ not specified and OFFSETs not declared for %r" % (unnumbered.keys(), )) | |
for name, (index, doc, default) in sorted(numbered.items(), key=lambda nv: (nv[1][0], nv[0])): | |
if index in seen: | |
aliases.append(name) | |
else: | |
fields.append(name) | |
seen.add(index) | |
max_len = max(max_len, index + 1) | |
offsets = numbered | |
else: | |
# check if any unnumbered not in _order_ | |
missing = set(unnumbered) - set(_order_) | |
if missing: | |
raise ValueError("unable to order fields: %s (use _order_ or specify OFFSET" % missing) | |
offsets = OrderedDict() | |
# if any unnumbered, number them from their position in _order_ | |
i = 0 | |
for k in _order_: | |
try: | |
index, doc, default = unnumbered.pop(k, None) or numbered.pop(k) | |
except IndexError: | |
raise ValueError('%s (from _order_) not found in %s' % (k, cls)) | |
if index is not undefined: | |
i = index | |
if i in seen: | |
aliases.append(k) | |
else: | |
fields.append(k) | |
seen.add(i) | |
offsets[k] = i, doc, default | |
i += 1 | |
max_len = max(max_len, i) | |
# now handle anything in numbered | |
for k, (index, doc, default) in sorted(numbered.items(), key=lambda nv: (nv[1][0], nv[0])): | |
if index in seen: | |
aliases.append(k) | |
else: | |
fields.append(k) | |
seen.add(index) | |
offsets[k] = index, doc, default | |
max_len = max(max_len, index+1) | |
# at this point fields and aliases should be ordered lists, offsets should be an | |
# OrdededDict with each value an int, str or None or undefined, default or None or undefined | |
assert len(fields) + len(aliases) == len(offsets), "number of fields + aliases != number of offsets" | |
assert set(fields) & set(offsets) == set(fields), "some fields are not in offsets: %s" % set(fields) & set(offsets) | |
assert set(aliases) & set(offsets) == set(aliases), "some aliases are not in offsets: %s" % set(aliases) & set(offsets) | |
for name, (index, doc, default) in offsets.items(): | |
assert isinstance(index, baseinteger), "index for %s is not an int (%s:%r)" % (name, type(index), index) | |
assert isinstance(doc, (basestring, NoneType)) or doc is undefined, "doc is not a str, None, nor undefined (%s:%r)" % (name, type(doc), doc) | |
# create descriptors for fields | |
for name, (index, doc, default) in offsets.items(): | |
clsdict[name] = _TupleAttributeAtIndex(name, index, doc, default) | |
clsdict['__slots__'] = () | |
# create our new NamedTuple type | |
namedtuple_class = super(NamedTupleMeta, metacls).__new__(metacls, cls, bases, clsdict) | |
namedtuple_class._fields_ = fields | |
namedtuple_class._aliases_ = aliases | |
namedtuple_class._defined_len_ = max_len | |
return namedtuple_class | |
def _convert_fields(*namedtuples): | |
"create list of index, doc, default triplets for cls in namedtuples" | |
all_fields = [] | |
for cls in namedtuples: | |
base = len(all_fields) | |
for field in cls._fields_: | |
desc = getattr(cls, field) | |
all_fields.append((field, base+desc.index, desc.__doc__, desc.default)) | |
return all_fields | |
def __add__(cls, other): | |
"A new NamedTuple is created by concatenating the _fields_ and adjusting the descriptors" | |
if not isinstance(other, NamedTupleMeta): | |
return NotImplemented | |
return NamedTupleMeta('%s%s' % (cls.__name__, other.__name__), (cls, other), {}) | |
def __call__(cls, *args, **kwds): | |
"""Creates a new NamedTuple class or an instance of a NamedTuple subclass. | |
NamedTuple should have args of (class_name, names, module) | |
`names` can be: | |
* A string containing member names, separated either with spaces or | |
commas. Values are auto-numbered from 1. | |
* An iterable of member names. Values are auto-numbered from 1. | |
* An iterable of (member name, value) pairs. | |
* A mapping of member name -> value. | |
`module`, if set, will be stored in the new class' __module__ attribute; | |
Note: if `module` is not set this routine will attempt to discover the | |
calling module by walking the frame stack; if this is unsuccessful | |
the resulting class will not be pickleable. | |
subclass should have whatever arguments and/or keywords will be used to create an | |
instance of the subclass | |
""" | |
if cls is NamedTuple or cls._defined_len_ == 0: | |
original_args = args | |
original_kwds = kwds.copy() | |
# create a new subclass | |
try: | |
if 'class_name' in kwds: | |
class_name = kwds.pop('class_name') | |
else: | |
class_name, args = args[0], args[1:] | |
if 'names' in kwds: | |
names = kwds.pop('names') | |
else: | |
names, args = args[0], args[1:] | |
if 'module' in kwds: | |
module = kwds.pop('module') | |
elif args: | |
module, args = args[0], args[1:] | |
else: | |
module = None | |
if 'type' in kwds: | |
type = kwds.pop('type') | |
elif args: | |
type, args = args[0], args[1:] | |
else: | |
type = None | |
except IndexError: | |
raise TypeError('too few arguments to NamedTuple: %s, %s' % (original_args, original_kwds)) | |
if args or kwds: | |
raise TypeError('too many arguments to NamedTuple: %s, %s' % (original_args, original_kwds)) | |
if PY2: | |
# if class_name is unicode, attempt a conversion to ASCII | |
if isinstance(class_name, unicode): | |
try: | |
class_name = class_name.encode('ascii') | |
except UnicodeEncodeError: | |
raise TypeError('%r is not representable in ASCII' % (class_name, )) | |
# quick exit if names is a NamedTuple | |
if isinstance(names, NamedTupleMeta): | |
names.__name__ = class_name | |
if type is not None and type not in names.__bases__: | |
names.__bases__ = (type, ) + names.__bases__ | |
return names | |
metacls = cls.__class__ | |
bases = (cls, ) | |
clsdict = metacls.__prepare__(class_name, bases) | |
# special processing needed for names? | |
if isinstance(names, basestring): | |
names = names.replace(',', ' ').split() | |
if isinstance(names, (tuple, list)) and isinstance(names[0], basestring): | |
names = [(e, i) for (i, e) in enumerate(names)] | |
# Here, names is either an iterable of (name, index) or (name, index, doc, default) or a mapping. | |
item = None # in case names is empty | |
for item in names: | |
if isinstance(item, basestring): | |
# mapping | |
field_name, field_index = item, names[item] | |
else: | |
# non-mapping | |
if len(item) == 2: | |
field_name, field_index = item | |
else: | |
field_name, field_index = item[0], item[1:] | |
clsdict[field_name] = field_index | |
if type is not None: | |
if not isinstance(type, tuple): | |
type = (type, ) | |
bases = type + bases | |
namedtuple_class = metacls.__new__(metacls, class_name, bases, clsdict) | |
# TODO: replace the frame hack if a blessed way to know the calling | |
# module is ever developed | |
if module is None: | |
try: | |
module = _sys._getframe(1).f_globals['__name__'] | |
except (AttributeError, ValueError, KeyError): | |
pass | |
if module is None: | |
make_class_unpicklable(namedtuple_class) | |
else: | |
namedtuple_class.__module__ = module | |
return namedtuple_class | |
else: | |
# instantiate a subclass | |
namedtuple_instance = cls.__new__(cls, *args, **kwds) | |
if isinstance(namedtuple_instance, cls): | |
namedtuple_instance.__init__(*args, **kwds) | |
return namedtuple_instance | |
def __fields__(cls): | |
return list(cls._fields_) | |
# collections.namedtuple compatibility | |
_fields = __fields__ | |
def __aliases__(cls): | |
return list(cls._aliases_) | |
def __repr__(cls): | |
return "<NamedTuple %r>" % (cls.__name__, ) | |
namedtuple_dict = _Addendum( | |
dict=NamedTupleMeta.__prepare__('NamedTuple', (object, )), | |
doc="NamedTuple base class.\n\n Derive from this class to define new NamedTuples.\n\n", | |
ns=globals(), | |
) | |
def __new__(cls, *args, **kwds): | |
if cls._size_ is TupleSize.fixed and len(args) > cls._defined_len_: | |
raise TypeError('%d fields expected, %d received' % (cls._defined_len_, len(args))) | |
unknown = set(kwds) - set(cls._fields_) - set(cls._aliases_) | |
if unknown: | |
raise TypeError('unknown fields: %r' % (unknown, )) | |
final_args = list(args) + [undefined] * (len(cls.__fields__) - len(args)) | |
for field, value in kwds.items(): | |
index = getattr(cls, field).index | |
if final_args[index] != undefined: | |
raise TypeError('field %s specified more than once' % field) | |
final_args[index] = value | |
cls._review_(final_args) | |
missing = [] | |
for index, value in enumerate(final_args): | |
if value is undefined: | |
# look for default values | |
name = cls.__fields__[index] | |
default = getattr(cls, name).default | |
if default is undefined: | |
missing.append(name) | |
else: | |
final_args[index] = default | |
if missing: | |
if cls._size_ in (TupleSize.fixed, TupleSize.minimum): | |
raise TypeError('values not provided for field(s): %s' % ', '.join(missing)) | |
while final_args and final_args[-1] is undefined: | |
final_args.pop() | |
missing.pop() | |
if cls._size_ is not TupleSize.variable or undefined in final_args: | |
raise TypeError('values not provided for field(s): %s' % ', '.join(missing)) | |
return tuple.__new__(cls, tuple(final_args)) | |
def __reduce_ex__(self, proto): | |
return self.__class__, tuple(getattr(self, f) for f in self._fields_) | |
def __repr__(self): | |
if len(self) == len(self._fields_): | |
return "%s(%s)" % ( | |
self.__class__.__name__, ', '.join(['%s=%r' % (f, o) for f, o in zip(self._fields_, self)]) | |
) | |
else: | |
return '%s(%s)' % (self.__class__.__name__, ', '.join([repr(o) for o in self])) | |
def __str__(self): | |
return "%s(%s)" % ( | |
self.__class__.__name__, ', '.join(['%r' % (getattr(self, f), ) for f in self._fields_]) | |
) | |
def _fields_(self): | |
return list(self.__class__._fields_) | |
# compatibility methods with stdlib namedtuple | |
def __aliases__(self): | |
return list(self.__class__._aliases_) | |
def _fields(self): | |
return list(self.__class__._fields_) | |
def _make(cls, iterable, new=None, len=None): | |
return cls.__new__(cls, *iterable) | |
def _asdict(self): | |
return OrderedDict(zip(self._fields_, self)) | |
def _replace(self, **kwds): | |
current = self._asdict() | |
current.update(kwds) | |
return self.__class__(**current) | |
def _review_(cls, final_args): | |
pass | |
NamedTuple = NamedTupleMeta('NamedTuple', (object, ), namedtuple_dict.resolve()) | |
del namedtuple_dict | |