Spaces:
Sleeping
Sleeping
# -*- coding: utf-8 -*- | |
# Copyright (c) 2013, Mahmoud Hashemi | |
# | |
# Redistribution and use in source and binary forms, with or without | |
# modification, are permitted provided that the following conditions are | |
# met: | |
# | |
# * Redistributions of source code must retain the above copyright | |
# notice, this list of conditions and the following disclaimer. | |
# | |
# * Redistributions in binary form must reproduce the above | |
# copyright notice, this list of conditions and the following | |
# disclaimer in the documentation and/or other materials provided | |
# with the distribution. | |
# | |
# * The names of the contributors may not be used to endorse or | |
# promote products derived from this software without specific | |
# prior written permission. | |
# | |
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
"""\ | |
The ``namedutils`` module defines two lightweight container types: | |
:class:`namedtuple` and :class:`namedlist`. Both are subtypes of built-in | |
sequence types, which are very fast and efficient. They simply add | |
named attribute accessors for specific indexes within themselves. | |
The :class:`namedtuple` is identical to the built-in | |
:class:`collections.namedtuple`, with a couple of enhancements, | |
including a ``__repr__`` more suitable to inheritance. | |
The :class:`namedlist` is the mutable counterpart to the | |
:class:`namedtuple`, and is much faster and lighter-weight than | |
full-blown :class:`object`. Consider this if you're implementing nodes | |
in a tree, graph, or other mutable data structure. If you want an even | |
skinnier approach, you'll probably have to look to C. | |
""" | |
from __future__ import print_function | |
import sys as _sys | |
try: | |
from collections import OrderedDict | |
except ImportError: | |
# backwards compatibility (2.6 has no OrderedDict) | |
OrderedDict = dict | |
from keyword import iskeyword as _iskeyword | |
from operator import itemgetter as _itemgetter | |
try: | |
basestring | |
def exec_(code, global_env): | |
exec("exec code in global_env") | |
except NameError: | |
basestring = (str, bytes) # Python 3 compat | |
def exec_(code, global_env): | |
exec(code, global_env) | |
__all__ = ['namedlist', 'namedtuple'] | |
# Tiny templates | |
_repr_tmpl = '{name}=%r' | |
_imm_field_tmpl = '''\ | |
{name} = _property(_itemgetter({index:d}), doc='Alias for field {index:d}') | |
''' | |
_m_field_tmpl = '''\ | |
{name} = _property(_itemgetter({index:d}), _itemsetter({index:d}), doc='Alias for field {index:d}') | |
''' | |
################################################################# | |
### namedtuple | |
################################################################# | |
_namedtuple_tmpl = '''\ | |
class {typename}(tuple): | |
'{typename}({arg_list})' | |
__slots__ = () | |
_fields = {field_names!r} | |
def __new__(_cls, {arg_list}): # TODO: tweak sig to make more extensible | |
'Create new instance of {typename}({arg_list})' | |
return _tuple.__new__(_cls, ({arg_list})) | |
@classmethod | |
def _make(cls, iterable, new=_tuple.__new__, len=len): | |
'Make a new {typename} object from a sequence or iterable' | |
result = new(cls, iterable) | |
if len(result) != {num_fields:d}: | |
raise TypeError('Expected {num_fields:d}' | |
' arguments, got %d' % len(result)) | |
return result | |
def __repr__(self): | |
'Return a nicely formatted representation string' | |
tmpl = self.__class__.__name__ + '({repr_fmt})' | |
return tmpl % self | |
def _asdict(self): | |
'Return a new OrderedDict which maps field names to their values' | |
return OrderedDict(zip(self._fields, self)) | |
def _replace(_self, **kwds): | |
'Return a new {typename} object replacing field(s) with new values' | |
result = _self._make(map(kwds.pop, {field_names!r}, _self)) | |
if kwds: | |
raise ValueError('Got unexpected field names: %r' % kwds.keys()) | |
return result | |
def __getnewargs__(self): | |
'Return self as a plain tuple. Used by copy and pickle.' | |
return tuple(self) | |
__dict__ = _property(_asdict) | |
def __getstate__(self): | |
'Exclude the OrderedDict from pickling' # wat | |
pass | |
{field_defs} | |
''' | |
def namedtuple(typename, field_names, verbose=False, rename=False): | |
"""Returns a new subclass of tuple with named fields. | |
>>> Point = namedtuple('Point', ['x', 'y']) | |
>>> Point.__doc__ # docstring for the new class | |
'Point(x, y)' | |
>>> p = Point(11, y=22) # instantiate with pos args or keywords | |
>>> p[0] + p[1] # indexable like a plain tuple | |
33 | |
>>> x, y = p # unpack like a regular tuple | |
>>> x, y | |
(11, 22) | |
>>> p.x + p.y # fields also accessible by name | |
33 | |
>>> d = p._asdict() # convert to a dictionary | |
>>> d['x'] | |
11 | |
>>> Point(**d) # convert from a dictionary | |
Point(x=11, y=22) | |
>>> p._replace(x=100) # _replace() is like str.replace() but targets named fields | |
Point(x=100, y=22) | |
""" | |
# Validate the field names. At the user's option, either generate an error | |
# message or automatically replace the field name with a valid name. | |
if isinstance(field_names, basestring): | |
field_names = field_names.replace(',', ' ').split() | |
field_names = [str(x) for x in field_names] | |
if rename: | |
seen = set() | |
for index, name in enumerate(field_names): | |
if (not all(c.isalnum() or c == '_' for c in name) | |
or _iskeyword(name) | |
or not name | |
or name[0].isdigit() | |
or name.startswith('_') | |
or name in seen): | |
field_names[index] = '_%d' % index | |
seen.add(name) | |
for name in [typename] + field_names: | |
if not all(c.isalnum() or c == '_' for c in name): | |
raise ValueError('Type names and field names can only contain ' | |
'alphanumeric characters and underscores: %r' | |
% name) | |
if _iskeyword(name): | |
raise ValueError('Type names and field names cannot be a ' | |
'keyword: %r' % name) | |
if name[0].isdigit(): | |
raise ValueError('Type names and field names cannot start with ' | |
'a number: %r' % name) | |
seen = set() | |
for name in field_names: | |
if name.startswith('_') and not rename: | |
raise ValueError('Field names cannot start with an underscore: ' | |
'%r' % name) | |
if name in seen: | |
raise ValueError('Encountered duplicate field name: %r' % name) | |
seen.add(name) | |
# Fill-in the class template | |
fmt_kw = {'typename': typename} | |
fmt_kw['field_names'] = tuple(field_names) | |
fmt_kw['num_fields'] = len(field_names) | |
fmt_kw['arg_list'] = repr(tuple(field_names)).replace("'", "")[1:-1] | |
fmt_kw['repr_fmt'] = ', '.join(_repr_tmpl.format(name=name) | |
for name in field_names) | |
fmt_kw['field_defs'] = '\n'.join(_imm_field_tmpl.format(index=index, name=name) | |
for index, name in enumerate(field_names)) | |
class_definition = _namedtuple_tmpl.format(**fmt_kw) | |
if verbose: | |
print(class_definition) | |
# Execute the template string in a temporary namespace and support | |
# tracing utilities by setting a value for frame.f_globals['__name__'] | |
namespace = dict(_itemgetter=_itemgetter, | |
__name__='namedtuple_%s' % typename, | |
OrderedDict=OrderedDict, | |
_property=property, | |
_tuple=tuple) | |
try: | |
exec_(class_definition, namespace) | |
except SyntaxError as e: | |
raise SyntaxError(e.message + ':\n' + class_definition) | |
result = namespace[typename] | |
# For pickling to work, the __module__ variable needs to be set to the frame | |
# where the named tuple is created. Bypass this step in environments where | |
# sys._getframe is not defined (Jython for example) or sys._getframe is not | |
# defined for arguments greater than 0 (IronPython). | |
try: | |
frame = _sys._getframe(1) | |
result.__module__ = frame.f_globals.get('__name__', '__main__') | |
except (AttributeError, ValueError): | |
pass | |
return result | |
################################################################# | |
### namedlist | |
################################################################# | |
_namedlist_tmpl = '''\ | |
class {typename}(list): | |
'{typename}({arg_list})' | |
__slots__ = () | |
_fields = {field_names!r} | |
def __new__(_cls, {arg_list}): # TODO: tweak sig to make more extensible | |
'Create new instance of {typename}({arg_list})' | |
return _list.__new__(_cls, ({arg_list})) | |
def __init__(self, {arg_list}): # tuple didn't need this but list does | |
return _list.__init__(self, ({arg_list})) | |
@classmethod | |
def _make(cls, iterable, new=_list, len=len): | |
'Make a new {typename} object from a sequence or iterable' | |
# why did this function exist? why not just star the | |
# iterable like below? | |
result = cls(*iterable) | |
if len(result) != {num_fields:d}: | |
raise TypeError('Expected {num_fields:d} arguments,' | |
' got %d' % len(result)) | |
return result | |
def __repr__(self): | |
'Return a nicely formatted representation string' | |
tmpl = self.__class__.__name__ + '({repr_fmt})' | |
return tmpl % tuple(self) | |
def _asdict(self): | |
'Return a new OrderedDict which maps field names to their values' | |
return OrderedDict(zip(self._fields, self)) | |
def _replace(_self, **kwds): | |
'Return a new {typename} object replacing field(s) with new values' | |
result = _self._make(map(kwds.pop, {field_names!r}, _self)) | |
if kwds: | |
raise ValueError('Got unexpected field names: %r' % kwds.keys()) | |
return result | |
def __getnewargs__(self): | |
'Return self as a plain list. Used by copy and pickle.' | |
return tuple(self) | |
__dict__ = _property(_asdict) | |
def __getstate__(self): | |
'Exclude the OrderedDict from pickling' # wat | |
pass | |
{field_defs} | |
''' | |
def namedlist(typename, field_names, verbose=False, rename=False): | |
"""Returns a new subclass of list with named fields. | |
>>> Point = namedlist('Point', ['x', 'y']) | |
>>> Point.__doc__ # docstring for the new class | |
'Point(x, y)' | |
>>> p = Point(11, y=22) # instantiate with pos args or keywords | |
>>> p[0] + p[1] # indexable like a plain list | |
33 | |
>>> x, y = p # unpack like a regular list | |
>>> x, y | |
(11, 22) | |
>>> p.x + p.y # fields also accessible by name | |
33 | |
>>> d = p._asdict() # convert to a dictionary | |
>>> d['x'] | |
11 | |
>>> Point(**d) # convert from a dictionary | |
Point(x=11, y=22) | |
>>> p._replace(x=100) # _replace() is like str.replace() but targets named fields | |
Point(x=100, y=22) | |
""" | |
# Validate the field names. At the user's option, either generate an error | |
# message or automatically replace the field name with a valid name. | |
if isinstance(field_names, basestring): | |
field_names = field_names.replace(',', ' ').split() | |
field_names = [str(x) for x in field_names] | |
if rename: | |
seen = set() | |
for index, name in enumerate(field_names): | |
if (not all(c.isalnum() or c == '_' for c in name) | |
or _iskeyword(name) | |
or not name | |
or name[0].isdigit() | |
or name.startswith('_') | |
or name in seen): | |
field_names[index] = '_%d' % index | |
seen.add(name) | |
for name in [typename] + field_names: | |
if not all(c.isalnum() or c == '_' for c in name): | |
raise ValueError('Type names and field names can only contain ' | |
'alphanumeric characters and underscores: %r' | |
% name) | |
if _iskeyword(name): | |
raise ValueError('Type names and field names cannot be a ' | |
'keyword: %r' % name) | |
if name[0].isdigit(): | |
raise ValueError('Type names and field names cannot start with ' | |
'a number: %r' % name) | |
seen = set() | |
for name in field_names: | |
if name.startswith('_') and not rename: | |
raise ValueError('Field names cannot start with an underscore: ' | |
'%r' % name) | |
if name in seen: | |
raise ValueError('Encountered duplicate field name: %r' % name) | |
seen.add(name) | |
# Fill-in the class template | |
fmt_kw = {'typename': typename} | |
fmt_kw['field_names'] = tuple(field_names) | |
fmt_kw['num_fields'] = len(field_names) | |
fmt_kw['arg_list'] = repr(tuple(field_names)).replace("'", "")[1:-1] | |
fmt_kw['repr_fmt'] = ', '.join(_repr_tmpl.format(name=name) | |
for name in field_names) | |
fmt_kw['field_defs'] = '\n'.join(_m_field_tmpl.format(index=index, name=name) | |
for index, name in enumerate(field_names)) | |
class_definition = _namedlist_tmpl.format(**fmt_kw) | |
if verbose: | |
print(class_definition) | |
def _itemsetter(key): | |
def _itemsetter(obj, value): | |
obj[key] = value | |
return _itemsetter | |
# Execute the template string in a temporary namespace and support | |
# tracing utilities by setting a value for frame.f_globals['__name__'] | |
namespace = dict(_itemgetter=_itemgetter, | |
_itemsetter=_itemsetter, | |
__name__='namedlist_%s' % typename, | |
OrderedDict=OrderedDict, | |
_property=property, | |
_list=list) | |
try: | |
exec_(class_definition, namespace) | |
except SyntaxError as e: | |
raise SyntaxError(e.message + ':\n' + class_definition) | |
result = namespace[typename] | |
# For pickling to work, the __module__ variable needs to be set to | |
# the frame where the named list is created. Bypass this step in | |
# environments where sys._getframe is not defined (Jython for | |
# example) or sys._getframe is not defined for arguments greater | |
# than 0 (IronPython). | |
try: | |
frame = _sys._getframe(1) | |
result.__module__ = frame.f_globals.get('__name__', '__main__') | |
except (AttributeError, ValueError): | |
pass | |
return result | |