Skip to content
This repository has been archived by the owner on Mar 23, 2023. It is now read-only.

namedtuple excluded from collections module #29

Open
selik opened this issue Jan 5, 2017 · 0 comments
Open

namedtuple excluded from collections module #29

selik opened this issue Jan 5, 2017 · 0 comments

Comments

@selik
Copy link

selik commented Jan 5, 2017

I read that you don't want to have things execing. Here's a rough sketch of what a namedtuple might look like without exec.

class NamedTuple(tuple):
    'tuple with named attribute aliases for elements'

    def __new__(cls, *args, **kwds):
        'Create new instance of {typename}'
        excess = set(kwds) - set(cls._fields)
        if excess:
            raise TypeError('Unexpected keyword arguments %r' % excess)
        kwds = sorted(kwds.items(), key=lambda pair: cls._fields.index(pair[0]))
        return tuple.__new__(cls, args + tuple(v for k, v in kwds))

    @classmethod
    def _make(cls, iterable, new=tuple.__new__, len=len):
        'Make a new {typename} object from a sequence or iterable'
        n = len(cls._fields)
        result = new(cls, iterable)
        if len(result) != n:
            raise TypeError('Expected %d arguments, got %d' % (n, len(result)))
        return result

    def __repr__(self):
        'Return a nicely formatted representation string'
        names = self._fields
        values = [getattr(self, name) for name in self._fields]
        attrstring = ', '.join('%s=%r' % (k, v) for k, v in zip(names, values))
        return '%s(%s)' % (type(self).__name__, attrstring)

    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 specified fields with new values'
        result = _self._make(map(kwds.pop, _self._fields, _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'
        pass



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 positional 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 = map(str, field_names)
    typename = str(typename)
    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 type(name) != str:
            raise TypeError('Type names and field names must be strings')
        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)

    if verbose:
        raise NotImplementedError('verbose mode irrelevant to this implementation')

    bases = (NamedTuple,)
    field = lambda i: property(itemgetter(i), doc='Alias for field number %d' % i)
    attributes = {name: field(i) for i, name in enumerate(field_names)}
    attributes['__slots__'] = ()
    attributes['_fields'] = tuple(field_names)
    attributes['__doc__'] = '%s(%s)' % (typename, str(field_names).replace("'", '')[1:-1])
    result = type(typename, bases, attributes)

    return result

I copy-pasted from the CPython stdlib, deleted the exec and fiddled until it seemed to work.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant