blob: 646aed6ffd7705ebf926d1efe787587d4fc60838 [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
64exec(
65 # Python 2.x metaclass declaration syntax
66 """class _NamedTupleABC(object):
67 '''The abstract base class + mix-in for named tuples.'''
68 __metaclass__ = _NamedTupleABCMeta
69 _fields = abstractproperty()""" if version_info[0] < 3 else
70 # Python 3.x metaclass declaration syntax
71 """class _NamedTupleABC(metaclass=_NamedTupleABCMeta):
72 '''The abstract base class + mix-in for named tuples.'''
73 _fields = abstractproperty()"""
74)
75
76
77_namedtuple.abc = _NamedTupleABC
78#_NamedTupleABC.register(type(version_info)) # (and similar, in the future...)
79
80@wraps(_namedtuple)
81def namedtuple(*args, **kwargs):
82 '''Named tuple factory with namedtuple.abc subclass registration.'''
83 cls = _namedtuple(*args, **kwargs)
84 _NamedTupleABC.register(cls)
85 return cls
86
87collections.namedtuple = namedtuple
88
89
90
91
92if __name__ == '__main__':
93
94 '''Examples and explanations'''
95
96 # Simple usage
97
98 class MyRecord(namedtuple.abc):
99 _fields = 'x y z' # such form will be transformed into ('x', 'y', 'z')
100 def _my_custom_method(self):
101 return list(self._asdict().items())
102 # (the '_fields' attribute belongs to the named tuple public API anyway)
103
104 rec = MyRecord(1, 2, 3)
105 print(rec)
106 print(rec._my_custom_method())
107 print(rec._replace(y=222))
108 print(rec._replace(y=222)._my_custom_method())
109
110 # Custom abstract classes...
111
112 class MyAbstractRecord(namedtuple.abc):
113 def _my_custom_method(self):
114 return list(self._asdict().items())
115
116 try:
117 MyAbstractRecord() # (abstract classes cannot be instantiated)
118 except TypeError as exc:
119 print(exc)
120
121 class AnotherAbstractRecord(MyAbstractRecord):
122 def __str__(self):
123 return '<<<{0}>>>'.format(super(AnotherAbstractRecord,
124 self).__str__())
125
126 # ...and their non-abstract subclasses
127
128 class MyRecord2(MyAbstractRecord):
129 _fields = 'a, b'
130
131 class MyRecord3(AnotherAbstractRecord):
132 _fields = 'p', 'q', 'r'
133
134 rec2 = MyRecord2('foo', 'bar')
135 print(rec2)
136 print(rec2._my_custom_method())
137 print(rec2._replace(b=222))
138 print(rec2._replace(b=222)._my_custom_method())
139
140 rec3 = MyRecord3('foo', 'bar', 'baz')
141 print(rec3)
142 print(rec3._my_custom_method())
143 print(rec3._replace(q=222))
144 print(rec3._replace(q=222)._my_custom_method())
145
146 # You can also subclass non-abstract ones...
147
148 class MyRecord33(MyRecord3):
149 def __str__(self):
150 return '< {0!r}, ..., {0!r} >'.format(self.p, self.r)
151
152 rec33 = MyRecord33('foo', 'bar', 'baz')
153 print(rec33)
154 print(rec33._my_custom_method())
155 print(rec33._replace(q=222))
156 print(rec33._replace(q=222)._my_custom_method())
157
158 # ...and even override the magic '_fields' attribute again
159
160 class MyRecord345(MyRecord3):
161 _fields = 'e f g h i j k'
162
163 rec345 = MyRecord345(1, 2, 3, 4, 3, 2, 1)
164 print(rec345)
165 print(rec345._my_custom_method())
166 print(rec345._replace(f=222))
167 print(rec345._replace(f=222)._my_custom_method())
168
169 # Mixing-in some other classes is also possible:
170
171 class MyMixIn(object):
172 def method(self):
173 return "MyMixIn.method() called"
174 def _my_custom_method(self):
175 return "MyMixIn._my_custom_method() called"
176 def count(self, item):
177 return "MyMixIn.count({0}) called".format(item)
178 def _asdict(self): # (cannot override a namedtuple method, see below)
179 return "MyMixIn._asdict() called"
180
181 class MyRecord4(MyRecord33, MyMixIn): # mix-in on the right
182 _fields = 'j k l x'
183
184 class MyRecord5(MyMixIn, MyRecord33): # mix-in on the left
185 _fields = 'j k l x y'
186
187 rec4 = MyRecord4(1, 2, 3, 2)
188 print(rec4)
189 print(rec4.method())
190 print(rec4._my_custom_method()) # MyRecord33's
191 print(rec4.count(2)) # tuple's
192 print(rec4._replace(k=222))
193 print(rec4._replace(k=222).method())
194 print(rec4._replace(k=222)._my_custom_method()) # MyRecord33's
195 print(rec4._replace(k=222).count(8)) # tuple's
196
197 rec5 = MyRecord5(1, 2, 3, 2, 1)
198 print(rec5)
199 print(rec5.method())
200 print(rec5._my_custom_method()) # MyMixIn's
201 print(rec5.count(2)) # MyMixIn's
202 print(rec5._replace(k=222))
203 print(rec5._replace(k=222).method())
204 print(rec5._replace(k=222)._my_custom_method()) # MyMixIn's
205 print(rec5._replace(k=222).count(2)) # MyMixIn's
206
207 # Note that behavior: the standard namedtuple methods cannot be
208 # overridden by a foreign mix-in -- even if the mix-in is declared
209 # as the leftmost base class (but, obviously, you can override them
210 # in the defined class or its subclasses):
211
212 print(rec4._asdict()) # (returns a dict, not "MyMixIn._asdict() called")
213 print(rec5._asdict()) # (returns a dict, not "MyMixIn._asdict() called")
214
215 class MyRecord6(MyRecord33):
216 _fields = 'j k l x y z'
217 def _asdict(self):
218 return "MyRecord6._asdict() called"
219 rec6 = MyRecord6(1, 2, 3, 1, 2, 3)
220 print(rec6._asdict()) # (this returns "MyRecord6._asdict() called")
221
222 # All that record classes are real subclasses of namedtuple.abc:
223
224 assert issubclass(MyRecord, namedtuple.abc)
225 assert issubclass(MyAbstractRecord, namedtuple.abc)
226 assert issubclass(AnotherAbstractRecord, namedtuple.abc)
227 assert issubclass(MyRecord2, namedtuple.abc)
228 assert issubclass(MyRecord3, namedtuple.abc)
229 assert issubclass(MyRecord33, namedtuple.abc)
230 assert issubclass(MyRecord345, namedtuple.abc)
231 assert issubclass(MyRecord4, namedtuple.abc)
232 assert issubclass(MyRecord5, namedtuple.abc)
233 assert issubclass(MyRecord6, namedtuple.abc)
234
235 # ...but abstract ones are not subclasses of tuple
236 # (and this is what you probably want):
237
238 assert not issubclass(MyAbstractRecord, tuple)
239 assert not issubclass(AnotherAbstractRecord, tuple)
240
241 assert issubclass(MyRecord, tuple)
242 assert issubclass(MyRecord2, tuple)
243 assert issubclass(MyRecord3, tuple)
244 assert issubclass(MyRecord33, tuple)
245 assert issubclass(MyRecord345, tuple)
246 assert issubclass(MyRecord4, tuple)
247 assert issubclass(MyRecord5, tuple)
248 assert issubclass(MyRecord6, tuple)
249
250 # Named tuple classes created with namedtuple() factory function
251 # (in the "traditional" way) are registered as "virtual" subclasses
252 # of namedtuple.abc:
253
254 MyTuple = namedtuple('MyTuple', 'a b c')
255 mt = MyTuple(1, 2, 3)
256 assert issubclass(MyTuple, namedtuple.abc)
257 assert isinstance(mt, namedtuple.abc)