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