1# 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# SPDX-License-Identifier: MIT
6#
7
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)
259