blob: c8e1d55c15d6cd6e3327c4fb7e05065f9336b963 [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/
2#!/usr/bin/env python
3# Copyright (c) 2011 Jan Kaliszewski (zuo). Available under the MIT License.
Brad Bishopc342db32019-05-15 21:57:59 -04004#
5# SPDX-License-Identifier: MIT
6#
Patrick Williamsc124f4f2015-09-15 14:41:29 -05007
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)