256 lines
8.5 KiB
Python
256 lines
8.5 KiB
Python
|
# http://code.activestate.com/recipes/577629-namedtupleabc-abstract-base-class-mix-in-for-named/
|
||
|
#!/usr/bin/env python
|
||
|
# Copyright (c) 2011 Jan Kaliszewski (zuo). Available under the MIT License.
|
||
|
|
||
|
"""
|
||
|
namedtuple_with_abc.py:
|
||
|
* named tuple mix-in + ABC (abstract base class) recipe,
|
||
|
* works under Python 2.6, 2.7 as well as 3.x.
|
||
|
|
||
|
Import this module to patch collections.namedtuple() factory function
|
||
|
-- enriching it with the 'abc' attribute (an abstract base class + mix-in
|
||
|
for named tuples) and decorating it with a wrapper that registers each
|
||
|
newly created named tuple as a subclass of namedtuple.abc.
|
||
|
|
||
|
How to import:
|
||
|
import collections, namedtuple_with_abc
|
||
|
or:
|
||
|
import namedtuple_with_abc
|
||
|
from collections import namedtuple
|
||
|
# ^ in this variant you must import namedtuple function
|
||
|
# *after* importing namedtuple_with_abc module
|
||
|
or simply:
|
||
|
from namedtuple_with_abc import namedtuple
|
||
|
|
||
|
Simple usage example:
|
||
|
class Credentials(namedtuple.abc):
|
||
|
_fields = 'username password'
|
||
|
def __str__(self):
|
||
|
return ('{0.__class__.__name__}'
|
||
|
'(username={0.username}, password=...)'.format(self))
|
||
|
print(Credentials("alice", "Alice's password"))
|
||
|
|
||
|
For more advanced examples -- see below the "if __name__ == '__main__':".
|
||
|
"""
|
||
|
|
||
|
import collections
|
||
|
from abc import ABCMeta, abstractproperty
|
||
|
from functools import wraps
|
||
|
from sys import version_info
|
||
|
|
||
|
__all__ = ('namedtuple',)
|
||
|
_namedtuple = collections.namedtuple
|
||
|
|
||
|
|
||
|
class _NamedTupleABCMeta(ABCMeta):
|
||
|
'''The metaclass for the abstract base class + mix-in for named tuples.'''
|
||
|
def __new__(mcls, name, bases, namespace):
|
||
|
fields = namespace.get('_fields')
|
||
|
for base in bases:
|
||
|
if fields is not None:
|
||
|
break
|
||
|
fields = getattr(base, '_fields', None)
|
||
|
if not isinstance(fields, abstractproperty):
|
||
|
basetuple = _namedtuple(name, fields)
|
||
|
bases = (basetuple,) + bases
|
||
|
namespace.pop('_fields', None)
|
||
|
namespace.setdefault('__doc__', basetuple.__doc__)
|
||
|
namespace.setdefault('__slots__', ())
|
||
|
return ABCMeta.__new__(mcls, name, bases, namespace)
|
||
|
|
||
|
|
||
|
exec(
|
||
|
# Python 2.x metaclass declaration syntax
|
||
|
"""class _NamedTupleABC(object):
|
||
|
'''The abstract base class + mix-in for named tuples.'''
|
||
|
__metaclass__ = _NamedTupleABCMeta
|
||
|
_fields = abstractproperty()""" if version_info[0] < 3 else
|
||
|
# Python 3.x metaclass declaration syntax
|
||
|
"""class _NamedTupleABC(metaclass=_NamedTupleABCMeta):
|
||
|
'''The abstract base class + mix-in for named tuples.'''
|
||
|
_fields = abstractproperty()"""
|
||
|
)
|
||
|
|
||
|
|
||
|
_namedtuple.abc = _NamedTupleABC
|
||
|
#_NamedTupleABC.register(type(version_info)) # (and similar, in the future...)
|
||
|
|
||
|
@wraps(_namedtuple)
|
||
|
def namedtuple(*args, **kwargs):
|
||
|
'''Named tuple factory with namedtuple.abc subclass registration.'''
|
||
|
cls = _namedtuple(*args, **kwargs)
|
||
|
_NamedTupleABC.register(cls)
|
||
|
return cls
|
||
|
|
||
|
collections.namedtuple = namedtuple
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
if __name__ == '__main__':
|
||
|
|
||
|
'''Examples and explanations'''
|
||
|
|
||
|
# Simple usage
|
||
|
|
||
|
class MyRecord(namedtuple.abc):
|
||
|
_fields = 'x y z' # such form will be transformed into ('x', 'y', 'z')
|
||
|
def _my_custom_method(self):
|
||
|
return list(self._asdict().items())
|
||
|
# (the '_fields' attribute belongs to the named tuple public API anyway)
|
||
|
|
||
|
rec = MyRecord(1, 2, 3)
|
||
|
print(rec)
|
||
|
print(rec._my_custom_method())
|
||
|
print(rec._replace(y=222))
|
||
|
print(rec._replace(y=222)._my_custom_method())
|
||
|
|
||
|
# Custom abstract classes...
|
||
|
|
||
|
class MyAbstractRecord(namedtuple.abc):
|
||
|
def _my_custom_method(self):
|
||
|
return list(self._asdict().items())
|
||
|
|
||
|
try:
|
||
|
MyAbstractRecord() # (abstract classes cannot be instantiated)
|
||
|
except TypeError as exc:
|
||
|
print(exc)
|
||
|
|
||
|
class AnotherAbstractRecord(MyAbstractRecord):
|
||
|
def __str__(self):
|
||
|
return '<<<{0}>>>'.format(super(AnotherAbstractRecord,
|
||
|
self).__str__())
|
||
|
|
||
|
# ...and their non-abstract subclasses
|
||
|
|
||
|
class MyRecord2(MyAbstractRecord):
|
||
|
_fields = 'a, b'
|
||
|
|
||
|
class MyRecord3(AnotherAbstractRecord):
|
||
|
_fields = 'p', 'q', 'r'
|
||
|
|
||
|
rec2 = MyRecord2('foo', 'bar')
|
||
|
print(rec2)
|
||
|
print(rec2._my_custom_method())
|
||
|
print(rec2._replace(b=222))
|
||
|
print(rec2._replace(b=222)._my_custom_method())
|
||
|
|
||
|
rec3 = MyRecord3('foo', 'bar', 'baz')
|
||
|
print(rec3)
|
||
|
print(rec3._my_custom_method())
|
||
|
print(rec3._replace(q=222))
|
||
|
print(rec3._replace(q=222)._my_custom_method())
|
||
|
|
||
|
# You can also subclass non-abstract ones...
|
||
|
|
||
|
class MyRecord33(MyRecord3):
|
||
|
def __str__(self):
|
||
|
return '< {0!r}, ..., {0!r} >'.format(self.p, self.r)
|
||
|
|
||
|
rec33 = MyRecord33('foo', 'bar', 'baz')
|
||
|
print(rec33)
|
||
|
print(rec33._my_custom_method())
|
||
|
print(rec33._replace(q=222))
|
||
|
print(rec33._replace(q=222)._my_custom_method())
|
||
|
|
||
|
# ...and even override the magic '_fields' attribute again
|
||
|
|
||
|
class MyRecord345(MyRecord3):
|
||
|
_fields = 'e f g h i j k'
|
||
|
|
||
|
rec345 = MyRecord345(1, 2, 3, 4, 3, 2, 1)
|
||
|
print(rec345)
|
||
|
print(rec345._my_custom_method())
|
||
|
print(rec345._replace(f=222))
|
||
|
print(rec345._replace(f=222)._my_custom_method())
|
||
|
|
||
|
# Mixing-in some other classes is also possible:
|
||
|
|
||
|
class MyMixIn(object):
|
||
|
def method(self):
|
||
|
return "MyMixIn.method() called"
|
||
|
def _my_custom_method(self):
|
||
|
return "MyMixIn._my_custom_method() called"
|
||
|
def count(self, item):
|
||
|
return "MyMixIn.count({0}) called".format(item)
|
||
|
def _asdict(self): # (cannot override a namedtuple method, see below)
|
||
|
return "MyMixIn._asdict() called"
|
||
|
|
||
|
class MyRecord4(MyRecord33, MyMixIn): # mix-in on the right
|
||
|
_fields = 'j k l x'
|
||
|
|
||
|
class MyRecord5(MyMixIn, MyRecord33): # mix-in on the left
|
||
|
_fields = 'j k l x y'
|
||
|
|
||
|
rec4 = MyRecord4(1, 2, 3, 2)
|
||
|
print(rec4)
|
||
|
print(rec4.method())
|
||
|
print(rec4._my_custom_method()) # MyRecord33's
|
||
|
print(rec4.count(2)) # tuple's
|
||
|
print(rec4._replace(k=222))
|
||
|
print(rec4._replace(k=222).method())
|
||
|
print(rec4._replace(k=222)._my_custom_method()) # MyRecord33's
|
||
|
print(rec4._replace(k=222).count(8)) # tuple's
|
||
|
|
||
|
rec5 = MyRecord5(1, 2, 3, 2, 1)
|
||
|
print(rec5)
|
||
|
print(rec5.method())
|
||
|
print(rec5._my_custom_method()) # MyMixIn's
|
||
|
print(rec5.count(2)) # MyMixIn's
|
||
|
print(rec5._replace(k=222))
|
||
|
print(rec5._replace(k=222).method())
|
||
|
print(rec5._replace(k=222)._my_custom_method()) # MyMixIn's
|
||
|
print(rec5._replace(k=222).count(2)) # MyMixIn's
|
||
|
|
||
|
# Note that behavior: the standard namedtuple methods cannot be
|
||
|
# overridden by a foreign mix-in -- even if the mix-in is declared
|
||
|
# as the leftmost base class (but, obviously, you can override them
|
||
|
# in the defined class or its subclasses):
|
||
|
|
||
|
print(rec4._asdict()) # (returns a dict, not "MyMixIn._asdict() called")
|
||
|
print(rec5._asdict()) # (returns a dict, not "MyMixIn._asdict() called")
|
||
|
|
||
|
class MyRecord6(MyRecord33):
|
||
|
_fields = 'j k l x y z'
|
||
|
def _asdict(self):
|
||
|
return "MyRecord6._asdict() called"
|
||
|
rec6 = MyRecord6(1, 2, 3, 1, 2, 3)
|
||
|
print(rec6._asdict()) # (this returns "MyRecord6._asdict() called")
|
||
|
|
||
|
# All that record classes are real subclasses of namedtuple.abc:
|
||
|
|
||
|
assert issubclass(MyRecord, namedtuple.abc)
|
||
|
assert issubclass(MyAbstractRecord, namedtuple.abc)
|
||
|
assert issubclass(AnotherAbstractRecord, namedtuple.abc)
|
||
|
assert issubclass(MyRecord2, namedtuple.abc)
|
||
|
assert issubclass(MyRecord3, namedtuple.abc)
|
||
|
assert issubclass(MyRecord33, namedtuple.abc)
|
||
|
assert issubclass(MyRecord345, namedtuple.abc)
|
||
|
assert issubclass(MyRecord4, namedtuple.abc)
|
||
|
assert issubclass(MyRecord5, namedtuple.abc)
|
||
|
assert issubclass(MyRecord6, namedtuple.abc)
|
||
|
|
||
|
# ...but abstract ones are not subclasses of tuple
|
||
|
# (and this is what you probably want):
|
||
|
|
||
|
assert not issubclass(MyAbstractRecord, tuple)
|
||
|
assert not issubclass(AnotherAbstractRecord, tuple)
|
||
|
|
||
|
assert issubclass(MyRecord, tuple)
|
||
|
assert issubclass(MyRecord2, tuple)
|
||
|
assert issubclass(MyRecord3, tuple)
|
||
|
assert issubclass(MyRecord33, tuple)
|
||
|
assert issubclass(MyRecord345, tuple)
|
||
|
assert issubclass(MyRecord4, tuple)
|
||
|
assert issubclass(MyRecord5, tuple)
|
||
|
assert issubclass(MyRecord6, tuple)
|
||
|
|
||
|
# Named tuple classes created with namedtuple() factory function
|
||
|
# (in the "traditional" way) are registered as "virtual" subclasses
|
||
|
# of namedtuple.abc:
|
||
|
|
||
|
MyTuple = namedtuple('MyTuple', 'a b c')
|
||
|
mt = MyTuple(1, 2, 3)
|
||
|
assert issubclass(MyTuple, namedtuple.abc)
|
||
|
assert isinstance(mt, namedtuple.abc)
|