1# http://code.activestate.com/recipes/577629-namedtupleabc-abstract-base-class-mix-in-for-named/
2# Copyright (c) 2011 Jan Kaliszewski (zuo). Available under the MIT License.
3#
4# SPDX-License-Identifier: MIT
5#
6
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
64class _NamedTupleABC(metaclass=_NamedTupleABCMeta):
65    '''The abstract base class + mix-in for named tuples.'''
66    _fields = abstractproperty()
67
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)
250