blob: e46dbf08499631e049628d5678ff4e3e46b65c6a [file] [log] [blame]
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001# http://code.activestate.com/recipes/577629-namedtupleabc-abstract-base-class-mix-in-for-named/
Patrick Williamsc124f4f2015-09-15 14:41:29 -05002# Copyright (c) 2011 Jan Kaliszewski (zuo). Available under the MIT License.
Brad Bishopc342db32019-05-15 21:57:59 -04003#
4# SPDX-License-Identifier: MIT
5#
Patrick Williamsc124f4f2015-09-15 14:41:29 -05006
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
Andrew Geisslerc9f78652020-09-18 14:11:35 -050064class _NamedTupleABC(metaclass=_NamedTupleABCMeta):
65 '''The abstract base class + mix-in for named tuples.'''
66 _fields = abstractproperty()
Patrick Williamsc124f4f2015-09-15 14:41:29 -050067
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)