1*4882a593Smuzhiyun# http://code.activestate.com/recipes/577629-namedtupleabc-abstract-base-class-mix-in-for-named/ 2*4882a593Smuzhiyun# Copyright (c) 2011 Jan Kaliszewski (zuo). Available under the MIT License. 3*4882a593Smuzhiyun# 4*4882a593Smuzhiyun# SPDX-License-Identifier: MIT 5*4882a593Smuzhiyun# 6*4882a593Smuzhiyun 7*4882a593Smuzhiyun""" 8*4882a593Smuzhiyunnamedtuple_with_abc.py: 9*4882a593Smuzhiyun* named tuple mix-in + ABC (abstract base class) recipe, 10*4882a593Smuzhiyun* works under Python 2.6, 2.7 as well as 3.x. 11*4882a593Smuzhiyun 12*4882a593SmuzhiyunImport this module to patch collections.namedtuple() factory function 13*4882a593Smuzhiyun-- enriching it with the 'abc' attribute (an abstract base class + mix-in 14*4882a593Smuzhiyunfor named tuples) and decorating it with a wrapper that registers each 15*4882a593Smuzhiyunnewly created named tuple as a subclass of namedtuple.abc. 16*4882a593Smuzhiyun 17*4882a593SmuzhiyunHow to import: 18*4882a593Smuzhiyun import collections, namedtuple_with_abc 19*4882a593Smuzhiyunor: 20*4882a593Smuzhiyun import namedtuple_with_abc 21*4882a593Smuzhiyun from collections import namedtuple 22*4882a593Smuzhiyun # ^ in this variant you must import namedtuple function 23*4882a593Smuzhiyun # *after* importing namedtuple_with_abc module 24*4882a593Smuzhiyunor simply: 25*4882a593Smuzhiyun from namedtuple_with_abc import namedtuple 26*4882a593Smuzhiyun 27*4882a593SmuzhiyunSimple usage example: 28*4882a593Smuzhiyun class Credentials(namedtuple.abc): 29*4882a593Smuzhiyun _fields = 'username password' 30*4882a593Smuzhiyun def __str__(self): 31*4882a593Smuzhiyun return ('{0.__class__.__name__}' 32*4882a593Smuzhiyun '(username={0.username}, password=...)'.format(self)) 33*4882a593Smuzhiyun print(Credentials("alice", "Alice's password")) 34*4882a593Smuzhiyun 35*4882a593SmuzhiyunFor more advanced examples -- see below the "if __name__ == '__main__':". 36*4882a593Smuzhiyun""" 37*4882a593Smuzhiyun 38*4882a593Smuzhiyunimport collections 39*4882a593Smuzhiyunfrom abc import ABCMeta, abstractproperty 40*4882a593Smuzhiyunfrom functools import wraps 41*4882a593Smuzhiyunfrom sys import version_info 42*4882a593Smuzhiyun 43*4882a593Smuzhiyun__all__ = ('namedtuple',) 44*4882a593Smuzhiyun_namedtuple = collections.namedtuple 45*4882a593Smuzhiyun 46*4882a593Smuzhiyun 47*4882a593Smuzhiyunclass _NamedTupleABCMeta(ABCMeta): 48*4882a593Smuzhiyun '''The metaclass for the abstract base class + mix-in for named tuples.''' 49*4882a593Smuzhiyun def __new__(mcls, name, bases, namespace): 50*4882a593Smuzhiyun fields = namespace.get('_fields') 51*4882a593Smuzhiyun for base in bases: 52*4882a593Smuzhiyun if fields is not None: 53*4882a593Smuzhiyun break 54*4882a593Smuzhiyun fields = getattr(base, '_fields', None) 55*4882a593Smuzhiyun if not isinstance(fields, abstractproperty): 56*4882a593Smuzhiyun basetuple = _namedtuple(name, fields) 57*4882a593Smuzhiyun bases = (basetuple,) + bases 58*4882a593Smuzhiyun namespace.pop('_fields', None) 59*4882a593Smuzhiyun namespace.setdefault('__doc__', basetuple.__doc__) 60*4882a593Smuzhiyun namespace.setdefault('__slots__', ()) 61*4882a593Smuzhiyun return ABCMeta.__new__(mcls, name, bases, namespace) 62*4882a593Smuzhiyun 63*4882a593Smuzhiyun 64*4882a593Smuzhiyunclass _NamedTupleABC(metaclass=_NamedTupleABCMeta): 65*4882a593Smuzhiyun '''The abstract base class + mix-in for named tuples.''' 66*4882a593Smuzhiyun _fields = abstractproperty() 67*4882a593Smuzhiyun 68*4882a593Smuzhiyun 69*4882a593Smuzhiyun_namedtuple.abc = _NamedTupleABC 70*4882a593Smuzhiyun#_NamedTupleABC.register(type(version_info)) # (and similar, in the future...) 71*4882a593Smuzhiyun 72*4882a593Smuzhiyun@wraps(_namedtuple) 73*4882a593Smuzhiyundef namedtuple(*args, **kwargs): 74*4882a593Smuzhiyun '''Named tuple factory with namedtuple.abc subclass registration.''' 75*4882a593Smuzhiyun cls = _namedtuple(*args, **kwargs) 76*4882a593Smuzhiyun _NamedTupleABC.register(cls) 77*4882a593Smuzhiyun return cls 78*4882a593Smuzhiyun 79*4882a593Smuzhiyuncollections.namedtuple = namedtuple 80*4882a593Smuzhiyun 81*4882a593Smuzhiyun 82*4882a593Smuzhiyun 83*4882a593Smuzhiyun 84*4882a593Smuzhiyunif __name__ == '__main__': 85*4882a593Smuzhiyun 86*4882a593Smuzhiyun '''Examples and explanations''' 87*4882a593Smuzhiyun 88*4882a593Smuzhiyun # Simple usage 89*4882a593Smuzhiyun 90*4882a593Smuzhiyun class MyRecord(namedtuple.abc): 91*4882a593Smuzhiyun _fields = 'x y z' # such form will be transformed into ('x', 'y', 'z') 92*4882a593Smuzhiyun def _my_custom_method(self): 93*4882a593Smuzhiyun return list(self._asdict().items()) 94*4882a593Smuzhiyun # (the '_fields' attribute belongs to the named tuple public API anyway) 95*4882a593Smuzhiyun 96*4882a593Smuzhiyun rec = MyRecord(1, 2, 3) 97*4882a593Smuzhiyun print(rec) 98*4882a593Smuzhiyun print(rec._my_custom_method()) 99*4882a593Smuzhiyun print(rec._replace(y=222)) 100*4882a593Smuzhiyun print(rec._replace(y=222)._my_custom_method()) 101*4882a593Smuzhiyun 102*4882a593Smuzhiyun # Custom abstract classes... 103*4882a593Smuzhiyun 104*4882a593Smuzhiyun class MyAbstractRecord(namedtuple.abc): 105*4882a593Smuzhiyun def _my_custom_method(self): 106*4882a593Smuzhiyun return list(self._asdict().items()) 107*4882a593Smuzhiyun 108*4882a593Smuzhiyun try: 109*4882a593Smuzhiyun MyAbstractRecord() # (abstract classes cannot be instantiated) 110*4882a593Smuzhiyun except TypeError as exc: 111*4882a593Smuzhiyun print(exc) 112*4882a593Smuzhiyun 113*4882a593Smuzhiyun class AnotherAbstractRecord(MyAbstractRecord): 114*4882a593Smuzhiyun def __str__(self): 115*4882a593Smuzhiyun return '<<<{0}>>>'.format(super(AnotherAbstractRecord, 116*4882a593Smuzhiyun self).__str__()) 117*4882a593Smuzhiyun 118*4882a593Smuzhiyun # ...and their non-abstract subclasses 119*4882a593Smuzhiyun 120*4882a593Smuzhiyun class MyRecord2(MyAbstractRecord): 121*4882a593Smuzhiyun _fields = 'a, b' 122*4882a593Smuzhiyun 123*4882a593Smuzhiyun class MyRecord3(AnotherAbstractRecord): 124*4882a593Smuzhiyun _fields = 'p', 'q', 'r' 125*4882a593Smuzhiyun 126*4882a593Smuzhiyun rec2 = MyRecord2('foo', 'bar') 127*4882a593Smuzhiyun print(rec2) 128*4882a593Smuzhiyun print(rec2._my_custom_method()) 129*4882a593Smuzhiyun print(rec2._replace(b=222)) 130*4882a593Smuzhiyun print(rec2._replace(b=222)._my_custom_method()) 131*4882a593Smuzhiyun 132*4882a593Smuzhiyun rec3 = MyRecord3('foo', 'bar', 'baz') 133*4882a593Smuzhiyun print(rec3) 134*4882a593Smuzhiyun print(rec3._my_custom_method()) 135*4882a593Smuzhiyun print(rec3._replace(q=222)) 136*4882a593Smuzhiyun print(rec3._replace(q=222)._my_custom_method()) 137*4882a593Smuzhiyun 138*4882a593Smuzhiyun # You can also subclass non-abstract ones... 139*4882a593Smuzhiyun 140*4882a593Smuzhiyun class MyRecord33(MyRecord3): 141*4882a593Smuzhiyun def __str__(self): 142*4882a593Smuzhiyun return '< {0!r}, ..., {0!r} >'.format(self.p, self.r) 143*4882a593Smuzhiyun 144*4882a593Smuzhiyun rec33 = MyRecord33('foo', 'bar', 'baz') 145*4882a593Smuzhiyun print(rec33) 146*4882a593Smuzhiyun print(rec33._my_custom_method()) 147*4882a593Smuzhiyun print(rec33._replace(q=222)) 148*4882a593Smuzhiyun print(rec33._replace(q=222)._my_custom_method()) 149*4882a593Smuzhiyun 150*4882a593Smuzhiyun # ...and even override the magic '_fields' attribute again 151*4882a593Smuzhiyun 152*4882a593Smuzhiyun class MyRecord345(MyRecord3): 153*4882a593Smuzhiyun _fields = 'e f g h i j k' 154*4882a593Smuzhiyun 155*4882a593Smuzhiyun rec345 = MyRecord345(1, 2, 3, 4, 3, 2, 1) 156*4882a593Smuzhiyun print(rec345) 157*4882a593Smuzhiyun print(rec345._my_custom_method()) 158*4882a593Smuzhiyun print(rec345._replace(f=222)) 159*4882a593Smuzhiyun print(rec345._replace(f=222)._my_custom_method()) 160*4882a593Smuzhiyun 161*4882a593Smuzhiyun # Mixing-in some other classes is also possible: 162*4882a593Smuzhiyun 163*4882a593Smuzhiyun class MyMixIn(object): 164*4882a593Smuzhiyun def method(self): 165*4882a593Smuzhiyun return "MyMixIn.method() called" 166*4882a593Smuzhiyun def _my_custom_method(self): 167*4882a593Smuzhiyun return "MyMixIn._my_custom_method() called" 168*4882a593Smuzhiyun def count(self, item): 169*4882a593Smuzhiyun return "MyMixIn.count({0}) called".format(item) 170*4882a593Smuzhiyun def _asdict(self): # (cannot override a namedtuple method, see below) 171*4882a593Smuzhiyun return "MyMixIn._asdict() called" 172*4882a593Smuzhiyun 173*4882a593Smuzhiyun class MyRecord4(MyRecord33, MyMixIn): # mix-in on the right 174*4882a593Smuzhiyun _fields = 'j k l x' 175*4882a593Smuzhiyun 176*4882a593Smuzhiyun class MyRecord5(MyMixIn, MyRecord33): # mix-in on the left 177*4882a593Smuzhiyun _fields = 'j k l x y' 178*4882a593Smuzhiyun 179*4882a593Smuzhiyun rec4 = MyRecord4(1, 2, 3, 2) 180*4882a593Smuzhiyun print(rec4) 181*4882a593Smuzhiyun print(rec4.method()) 182*4882a593Smuzhiyun print(rec4._my_custom_method()) # MyRecord33's 183*4882a593Smuzhiyun print(rec4.count(2)) # tuple's 184*4882a593Smuzhiyun print(rec4._replace(k=222)) 185*4882a593Smuzhiyun print(rec4._replace(k=222).method()) 186*4882a593Smuzhiyun print(rec4._replace(k=222)._my_custom_method()) # MyRecord33's 187*4882a593Smuzhiyun print(rec4._replace(k=222).count(8)) # tuple's 188*4882a593Smuzhiyun 189*4882a593Smuzhiyun rec5 = MyRecord5(1, 2, 3, 2, 1) 190*4882a593Smuzhiyun print(rec5) 191*4882a593Smuzhiyun print(rec5.method()) 192*4882a593Smuzhiyun print(rec5._my_custom_method()) # MyMixIn's 193*4882a593Smuzhiyun print(rec5.count(2)) # MyMixIn's 194*4882a593Smuzhiyun print(rec5._replace(k=222)) 195*4882a593Smuzhiyun print(rec5._replace(k=222).method()) 196*4882a593Smuzhiyun print(rec5._replace(k=222)._my_custom_method()) # MyMixIn's 197*4882a593Smuzhiyun print(rec5._replace(k=222).count(2)) # MyMixIn's 198*4882a593Smuzhiyun 199*4882a593Smuzhiyun # Note that behavior: the standard namedtuple methods cannot be 200*4882a593Smuzhiyun # overridden by a foreign mix-in -- even if the mix-in is declared 201*4882a593Smuzhiyun # as the leftmost base class (but, obviously, you can override them 202*4882a593Smuzhiyun # in the defined class or its subclasses): 203*4882a593Smuzhiyun 204*4882a593Smuzhiyun print(rec4._asdict()) # (returns a dict, not "MyMixIn._asdict() called") 205*4882a593Smuzhiyun print(rec5._asdict()) # (returns a dict, not "MyMixIn._asdict() called") 206*4882a593Smuzhiyun 207*4882a593Smuzhiyun class MyRecord6(MyRecord33): 208*4882a593Smuzhiyun _fields = 'j k l x y z' 209*4882a593Smuzhiyun def _asdict(self): 210*4882a593Smuzhiyun return "MyRecord6._asdict() called" 211*4882a593Smuzhiyun rec6 = MyRecord6(1, 2, 3, 1, 2, 3) 212*4882a593Smuzhiyun print(rec6._asdict()) # (this returns "MyRecord6._asdict() called") 213*4882a593Smuzhiyun 214*4882a593Smuzhiyun # All that record classes are real subclasses of namedtuple.abc: 215*4882a593Smuzhiyun 216*4882a593Smuzhiyun assert issubclass(MyRecord, namedtuple.abc) 217*4882a593Smuzhiyun assert issubclass(MyAbstractRecord, namedtuple.abc) 218*4882a593Smuzhiyun assert issubclass(AnotherAbstractRecord, namedtuple.abc) 219*4882a593Smuzhiyun assert issubclass(MyRecord2, namedtuple.abc) 220*4882a593Smuzhiyun assert issubclass(MyRecord3, namedtuple.abc) 221*4882a593Smuzhiyun assert issubclass(MyRecord33, namedtuple.abc) 222*4882a593Smuzhiyun assert issubclass(MyRecord345, namedtuple.abc) 223*4882a593Smuzhiyun assert issubclass(MyRecord4, namedtuple.abc) 224*4882a593Smuzhiyun assert issubclass(MyRecord5, namedtuple.abc) 225*4882a593Smuzhiyun assert issubclass(MyRecord6, namedtuple.abc) 226*4882a593Smuzhiyun 227*4882a593Smuzhiyun # ...but abstract ones are not subclasses of tuple 228*4882a593Smuzhiyun # (and this is what you probably want): 229*4882a593Smuzhiyun 230*4882a593Smuzhiyun assert not issubclass(MyAbstractRecord, tuple) 231*4882a593Smuzhiyun assert not issubclass(AnotherAbstractRecord, tuple) 232*4882a593Smuzhiyun 233*4882a593Smuzhiyun assert issubclass(MyRecord, tuple) 234*4882a593Smuzhiyun assert issubclass(MyRecord2, tuple) 235*4882a593Smuzhiyun assert issubclass(MyRecord3, tuple) 236*4882a593Smuzhiyun assert issubclass(MyRecord33, tuple) 237*4882a593Smuzhiyun assert issubclass(MyRecord345, tuple) 238*4882a593Smuzhiyun assert issubclass(MyRecord4, tuple) 239*4882a593Smuzhiyun assert issubclass(MyRecord5, tuple) 240*4882a593Smuzhiyun assert issubclass(MyRecord6, tuple) 241*4882a593Smuzhiyun 242*4882a593Smuzhiyun # Named tuple classes created with namedtuple() factory function 243*4882a593Smuzhiyun # (in the "traditional" way) are registered as "virtual" subclasses 244*4882a593Smuzhiyun # of namedtuple.abc: 245*4882a593Smuzhiyun 246*4882a593Smuzhiyun MyTuple = namedtuple('MyTuple', 'a b c') 247*4882a593Smuzhiyun mt = MyTuple(1, 2, 3) 248*4882a593Smuzhiyun assert issubclass(MyTuple, namedtuple.abc) 249*4882a593Smuzhiyun assert isinstance(mt, namedtuple.abc) 250