123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400 |
- # -*- 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
|