Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 1 | # http://code.activestate.com/recipes/577629-namedtupleabc-abstract-base-class-mix-in-for-named/ |
Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 2 | # Copyright (c) 2011 Jan Kaliszewski (zuo). Available under the MIT License. |
Brad Bishop | c342db3 | 2019-05-15 21:57:59 -0400 | [diff] [blame] | 3 | # |
| 4 | # SPDX-License-Identifier: MIT |
| 5 | # |
Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 6 | |
| 7 | """ |
| 8 | namedtuple_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 | |
| 12 | Import this module to patch collections.namedtuple() factory function |
| 13 | -- enriching it with the 'abc' attribute (an abstract base class + mix-in |
| 14 | for named tuples) and decorating it with a wrapper that registers each |
| 15 | newly created named tuple as a subclass of namedtuple.abc. |
| 16 | |
| 17 | How to import: |
| 18 | import collections, namedtuple_with_abc |
| 19 | or: |
| 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 |
| 24 | or simply: |
| 25 | from namedtuple_with_abc import namedtuple |
| 26 | |
| 27 | Simple 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 | |
| 35 | For more advanced examples -- see below the "if __name__ == '__main__':". |
| 36 | """ |
| 37 | |
| 38 | import collections |
| 39 | from abc import ABCMeta, abstractproperty |
| 40 | from functools import wraps |
| 41 | from sys import version_info |
| 42 | |
| 43 | __all__ = ('namedtuple',) |
| 44 | _namedtuple = collections.namedtuple |
| 45 | |
| 46 | |
| 47 | class _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 | |
Andrew Geissler | c9f7865 | 2020-09-18 14:11:35 -0500 | [diff] [blame] | 64 | class _NamedTupleABC(metaclass=_NamedTupleABCMeta): |
| 65 | '''The abstract base class + mix-in for named tuples.''' |
| 66 | _fields = abstractproperty() |
Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 67 | |
| 68 | |
| 69 | _namedtuple.abc = _NamedTupleABC |
| 70 | #_NamedTupleABC.register(type(version_info)) # (and similar, in the future...) |
| 71 | |
| 72 | @wraps(_namedtuple) |
| 73 | def 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 | |
| 79 | collections.namedtuple = namedtuple |
| 80 | |
| 81 | |
| 82 | |
| 83 | |
| 84 | if __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) |