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