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