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