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