####################################################################### # namedlist is similar to collections.namedtuple, but supports default # values and is writable. Also contains an implementation of # namedtuple, which is the same as collections.namedtuple supporting # defaults. # # Copyright 2011-2016 True Blade Systems, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # Notes: # See http://code.activestate.com/recipes/576555/ for a similar # concept. # ######################################################################## __all__ = ['namedtuple'] # All of this hassle with ast is solely to provide a decent __init__ # function, that takes all of the right arguments and defaults. But # it's worth it to get all of the normal python error messages. # For other functions, like __repr__, we don't bother. __init__ is # the only function where we really need the argument processing, # because __init__ is the only function whose signature will vary # per class. import ast as _ast import sys as _sys import copy as _copy import operator as _operator import itertools as _itertools from keyword import iskeyword as _iskeyword import collections as _collections import abc as _abc try: _OrderedDict = _collections.OrderedDict except AttributeError: _OrderedDict = None ######################################################################## # Validate and possibly sanitize the field and type names. class _NameChecker(object): def __init__(self, typename): self.seen_fields = set() self._check_common(typename, 'Type') def check_field_name(self, fieldname, rename, idx): try: self._check_common(fieldname, 'Field') self._check_specific_to_fields(fieldname) except ValueError as ex: if rename: return '_' + str(idx) else: raise self.seen_fields.add(fieldname) return fieldname def _check_common(self, name, type_of_name): # tests that are common to both field names and the type name if len(name) == 0: raise ValueError('{0} names cannot be zero ' 'length: {1!r}'.format(type_of_name, name)) if not name.isidentifier(): raise ValueError('{0} names names must be valid ' 'identifiers: {1!r}'.format(type_of_name, name)) if _iskeyword(name): raise ValueError('{0} names cannot be a keyword: ' '{1!r}'.format(type_of_name, name)) def _check_specific_to_fields(self, name): # these tests don't apply for the typename, just the fieldnames if name in self.seen_fields: raise ValueError('Encountered duplicate field name: ' '{0!r}'.format(name)) if name.startswith('_'): raise ValueError('Field names cannot start with an underscore: ' '{0!r}'.format(name)) ######################################################################## # Returns a function with name 'name', that calls another function 'chain_fn' # This is used to create the __init__ function with the right argument names and defaults, that # calls into _init to do the real work. # The new function takes args as arguments, with defaults as given. def _make_fn(name, chain_fn, args): args_with_self = ['_self'] + list(args) arguments = [_ast.Name(id=arg, ctx=_ast.Load()) for arg in args_with_self] parameters = _ast.arguments(args=[_ast.arg(arg=arg) for arg in args_with_self], kwonlyargs=[], defaults=[], kw_defaults=[]) module_node = _ast.Module(body=[_ast.FunctionDef(name=name, args=parameters, body=[_ast.Return(value=_ast.Call(func=_ast.Name(id='_chain', ctx=_ast.Load()), args=arguments, keywords=[]))], decorator_list=[])]) module_node = _ast.fix_missing_locations(module_node) # compile the ast code = compile(module_node, '', 'exec') # and eval it in the right context globals_ = {'_chain': chain_fn} locals_ = {} eval(code, globals_, locals_) # extract our function from the newly created module return locals_[name] ######################################################################## # Produce a docstring for the class. def _build_docstring(typename, fields): return '{0}({1})'.format(typename, ', '.join(fields)) ######################################################################## # Given the typename, fields_names, default, and the rename flag, # return a tuple of fields and a list of defaults. def _fields(typename, field_names, rename): # field_names must be a string or an iterable, consisting of fieldname # strings or 2-tuples. Each 2-tuple is of the form (fieldname, # default). # Validates field and type names. name_checker = _NameChecker(typename) if isinstance(field_names, str): # No per-field defaults. So it's like a collections.namedtuple, # but with a possible default value. field_names = field_names.replace(',', ' ').split() # Parse and validate the field names. # field_names is now an iterable. Walk through it, # sanitizing as needed, and add to fields. return [name_checker.check_field_name(str(field_name), rename, idx) for idx, field_name in enumerate(field_names)] ######################################################################## # Common member functions for the generated classes. def _repr(self): return '{0}({1})'.format(self.__class__.__name__, ', '.join('{0}={1!r}'.format(name, getattr(self, name)) for name in self._fields)) def _asdict(self): # In 2.6, return a dict. # Otherwise, return an OrderedDict t = _OrderedDict if _OrderedDict is not None else dict return t(zip(self._fields, self)) # Set up methods and fields shared by namedlist and namedtuple def _common_fields(fields, docstr): type_dict = {'__repr__': _repr, '__dict__': property(_asdict), '__doc__': docstr, '_asdict': _asdict, '_fields': fields} # See collections.namedtuple for a description of # what's happening here. # _getframe(2) instead of 1, because we're now inside # another function. try: type_dict['__module__'] = _sys._getframe(2).f_globals.get('__name__', '__main__') except (AttributeError, ValueError): pass return type_dict ######################################################################## # namedtuple methods def _nt_new(cls, *args): # sets all of the fields to their passed in values assert len(args) == len(cls._fields) values = [value for _, value in _get_values(cls._fields, args)] return tuple.__new__(cls, values) def _nt_replace(_self, **kwds): result = _self._make(map(kwds.pop, _self._fields, _self)) if kwds: raise ValueError('Got unexpected field names: %r' % list(kwds)) return result def _nt_make(cls, iterable, new=tuple.__new__): result = new(cls, iterable) if len(result) != len(cls._fields): raise TypeError('Expected {0} arguments, got {1}'.format(len(cls._fields), len(result))) return result def _nt_getnewargs(self): 'Return self as a plain tuple. Used by copy and pickle.' return tuple(self) def _nt_getstate(self): 'Exclude the OrderedDict from pickling' return None def _get_values(fields, args): # Returns [(fieldname, value)]. If the value is a FACTORY, call it. assert len(fields) == len(args) return [(fieldname, value) for fieldname, value in zip(fields, args)] ######################################################################## # The actual namedtuple factory function. def namedtuple(typename, field_names, rename=False): fields = _fields(typename, field_names, rename) type_dict = {'__new__': _make_fn('__new__', _nt_new, fields), '__getnewargs__': _nt_getnewargs, '__getstate__': _nt_getstate, '_replace': _nt_replace, '_make': classmethod(_nt_make), '__slots__': ()} type_dict.update(_common_fields(fields, _build_docstring(typename, fields))) # Create each field property. for idx, field in enumerate(fields): type_dict[field] = property(_operator.itemgetter(idx), doc='Alias for field number {0}'.format(idx)) # Create the new type object. return type(typename, (tuple,), type_dict)