79
79
* Can convert instance into a dict simply by casting: ``dict(john)``
80
80
81
81
* Can set new items/attributes on an instance, even if they weren't previously defined.
82
-
82
+
83
+ * NOTE: You cannot edit an original namedtuple field defined on the type, those remain read only
83
84
84
85
There are three functions available for working with ``dictable_namedtuple`` classes/instances,
85
86
each for different purposes.
203
204
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
204
205
205
206
"""
207
+ import inspect
206
208
import sys
207
209
from collections import namedtuple
208
210
from typing import Dict , Optional , NamedTuple , Union , Type
211
+ import logging
212
+
213
+ log = logging .getLogger (__name__ )
209
214
210
215
211
216
class DictObject (dict ):
@@ -414,100 +419,117 @@ class type, but you set this to ``Man``, then this will return a ``Man`` class t
414
419
module = named_type .__module__ if module is None else module
415
420
read_only = kwargs .pop ('read_only' , False )
416
421
417
- class DictableNamedTuple (named_type ):
418
- """
419
- Customized :func:`.namedtuple` class - part of :func:`privex.helpers.common.dictable_namedtuple`
422
+ _dt = make_dict_tuple (typename , ' ' .join (named_type ._fields ), read_only = read_only )
420
423
421
- Unlike the normal :func:`.namedtuple` class, this class supports extra functionality:
424
+ if module is None :
425
+ try :
426
+ module = sys ._getframe (1 ).f_globals .get ('__name__' , '__main__' )
427
+ except (AttributeError , ValueError ):
428
+ pass
422
429
423
- * Can access fields via item/key: ``john['first_name']``
430
+ if module is not None :
431
+ _dt .__module__ = module
432
+ return _dt
424
433
425
- * Can convert instance into a dict simply by casting: ``dict(john)``
426
434
427
- * Can set new items/attributes on an instance, even if they weren't previously defined.
428
-
429
- * NOTE: You cannot edit an original namedtuple field defined on the type, those remain read only
430
-
431
- **Example - creating dictable_namedtuple, adding new item 'middle_name', and casting to dict**::
432
-
433
- >>> from privex.helpers import dictable_namedtuple
434
- >>> Person = dictable_namedtuple('Person', 'first_name last_name')
435
- >>> john = Person('John', 'Doe')
436
- >>> john
437
- Person(first_name='John', last_name='Doe')
438
- >>> john['middle_name'] = 'Davis'
439
- >>> john
440
- first_name='John', last_name='Doe', middle_name='Davis'
441
- Person(first_name='John', last_name='Doe', middle_name='Davis')
442
- >>> dict(john)
443
- {'first_name': 'John', 'last_name': 'Doe', 'middle_name': 'Davis'}
444
-
435
+ def make_dict_tuple (typename , field_names , * args , ** kwargs ):
436
+ """
437
+ Generates a :func:`collections.namedtuple` type, with added / modified methods injected to make it
438
+ into a ``dictable_namedtuple``.
439
+
440
+ Note: You probably want to be using :func:`.dictable_namedtuple` instead of calling this directly.
441
+ """
442
+ read_only = kwargs .pop ('read_only' , False )
443
+ module = kwargs .pop ('module' , None )
444
+
445
+ # Create a namedtuple type to use as a base
446
+ BaseNT = namedtuple (typename , field_names , ** kwargs )
447
+
448
+ def __init__ (self , * args , ** kwargs ):
449
+ self .__dict__ ['_extra_items' ] = dict ()
450
+ for i , a in enumerate (list (args )):
451
+ self .__dict__ [self ._fields [i ]] = a
452
+ for k , a in kwargs .items ():
453
+ self .__dict__ [k ] = a
454
+
455
+ def __iter__ (self ):
456
+ """This ``__iter__`` method allows for casting a dictable_namedtuple instance using ``dict(my_nt)``"""
457
+ for k in self ._fields : yield (k , getattr (self , k ),)
458
+
459
+ def __getitem__ (self , item ):
460
+ """Handles when a dictable_namedtuple instance is accessed like ``my_nt['abc']`` or ``my_nt[0]``"""
461
+ if type (item ) is int :
462
+ return self .__dict__ [self ._fields [item ]]
463
+ return getattr (self , item )
464
+
465
+ def __getattr__ (self , item ):
466
+ """Handles when a dictable_namedtuple instance is accessed like ``my_nt.abcd``"""
467
+ try :
468
+ _v = object .__getattribute__ (self , '_extra_items' )
469
+ return _v [item ]
470
+ except (KeyError , AttributeError ):
471
+ return object .__getattribute__ (self , item )
472
+
473
+ def __setitem__ (self , key , value ):
474
+ """Handles when a dictable_namedtuple instance is accessed like ``my_nt['abc'] = 'def'``"""
475
+ if hasattr (self , key ):
476
+ return tuple .__setattr__ (self , key , value )
477
+ if self ._READ_ONLY :
478
+ raise KeyError (f"{ self .__class__ .__name__ } is read only. You cannot set a non-existent field." )
479
+ self ._extra_items [key ] = value
480
+ if key not in self ._fields :
481
+ tuple .__setattr__ (self , '_fields' , self ._fields + (key ,))
482
+
483
+ def __setattr__ (self , key , value ):
484
+ """Handles when a dictable_namedtuple instance is accessed like ``my_nt.abcd = 'def'``"""
485
+ if key in ['_extra_items' , '_fields' ] or key in self ._fields :
486
+ return tuple .__setattr__ (self , key , value )
487
+ if self ._READ_ONLY :
488
+ raise AttributeError (f"{ self .__class__ .__name__ } is read only. You cannot set a non-existent field." )
489
+ self ._extra_items [key ] = value
490
+ if key not in self ._fields :
491
+ tuple .__setattr__ (self , '_fields' , self ._fields + (key ,))
492
+
493
+ def _asdict (self ):
445
494
"""
446
- _READ_ONLY = read_only
447
-
448
- def __init__ (self , * args , ** kwargs ):
449
- self ._extra_items = DictObject ()
450
-
451
- def __iter__ (self ):
452
- """This ``__iter__`` method allows for casting a dictable_namedtuple instance using ``dict(my_nt)``"""
453
- for k in self ._fields : yield (k , getattr (self , k ),)
454
-
455
- def __getitem__ (self , item ):
456
- """Handles when a dictable_namedtuple instance is accessed like ``my_nt['abc']`` or ``my_nt[0]``"""
457
- if type (item ) is int :
458
- return self .__getattribute__ (self ._fields [item ])
459
- try :
460
- return self ._extra_items [item ]
461
- except (KeyError , AttributeError ):
462
- return self .__getattribute__ (item )
463
-
464
- def __getattribute__ (self , item ):
465
- """Handles when a dictable_namedtuple instance is accessed like ``my_nt.abcd``"""
466
- try :
467
- v = super ().__getattribute__ ('_extra_items' )
468
- v = v [item ]
469
- except (KeyError , AttributeError ):
470
- v = super ().__getattribute__ (item )
471
- return v
472
-
473
- def __setitem__ (self , key , value ):
474
- """Handles when a dictable_namedtuple instance is accessed like ``my_nt['abc'] = 'def'``"""
475
- if hasattr (self , key ):
476
- return self .__setattr__ (key , value )
477
- if self ._READ_ONLY :
478
- raise KeyError (f"{ self .__class__ .__name__ } is read only. You cannot set a non-existent field." )
479
- self ._extra_items [key ] = value
480
- if key not in self ._fields :
481
- self ._fields = self ._fields + (key ,)
482
-
483
- def __setattr__ (self , key , value ):
484
- """Handles when a dictable_namedtuple instance is accessed like ``my_nt.abcd = 'def'``"""
485
- if key in ['_extra_items' , '_fields' ] or key in self ._fields :
486
- return super ().__setattr__ (key , value )
487
- if self ._READ_ONLY :
488
- raise AttributeError (f"{ self .__class__ .__name__ } is read only. You cannot set a non-existent field." )
489
- self ._extra_items [key ] = value
490
- if key not in self ._fields :
491
- self ._fields = self ._fields + (key ,)
492
-
493
- def _asdict (self ):
494
- """
495
- The original namedtuple ``_asdict`` doesn't work with our :meth:`.__iter__`, so we override it
496
- for compatibility. Simply calls ``return dict(self)`` to convert the instance to a dict.
497
- """
498
- return dict (self )
499
-
500
- def __repr__ (self ):
501
- _n = ', ' .join (f"{ name } ='{ getattr (self , name )} '" for name in self ._fields )
502
- return f"{ self .__class__ .__name__ } ({ _n } )"
495
+ The original namedtuple ``_asdict`` doesn't work with our :meth:`.__iter__`, so we override it
496
+ for compatibility. Simply calls ``return dict(self)`` to convert the instance to a dict.
497
+ """
498
+ return dict (self )
499
+
500
+ def __repr__ (self ):
501
+ _n = ', ' .join (f"{ name } ='{ getattr (self , name )} '" for name in self ._fields )
502
+ return f"{ self .__class__ .__name__ } ({ _n } )"
503
+
504
+ # Inject our methods defined above into the namedtuple type BaseNT
505
+ BaseNT .__getattr__ = __getattr__
506
+ BaseNT .__getitem__ = __getitem__
507
+ BaseNT .__setitem__ = __setitem__
508
+ BaseNT .__setattr__ = __setattr__
509
+ BaseNT ._asdict = _asdict
510
+ BaseNT .__repr__ = __repr__
511
+ BaseNT .__iter__ = __iter__
512
+ BaseNT .__init__ = __init__
513
+ BaseNT ._READ_ONLY = read_only
514
+
515
+ # Create a class for BaseNT with tuple + object mixins, allowing things like __dict__ to function properly
516
+ # and allowing for tuple.__setattr__ / object.__getattribute__ calls.
517
+ class K (BaseNT , tuple , object ):
518
+ pass
519
+
520
+ # Get the calling module so we can overwrite the module name of the class.
521
+ if module is None :
522
+ try :
523
+ module = sys ._getframe (1 ).f_globals .get ('__name__' , '__main__' )
524
+ except (AttributeError , ValueError ):
525
+ pass
503
526
504
- DictableNamedTuple .__name__ = typename
505
- DictableNamedTuple .__qualname__ = typename
506
- DictableNamedTuple .__bases__ = (named_type , tuple )
527
+ # Overwrite the type name + module to match the originally requested typename
528
+ K .__name__ = BaseNT .__name__
529
+ K .__qualname__ = BaseNT .__qualname__
530
+ K .__module__ = module
507
531
508
- if module is not None :
509
- DictableNamedTuple .__module__ = module
510
- return DictableNamedTuple
532
+ return K
511
533
512
534
513
535
def dictable_namedtuple (typename , field_names , * args , ** kwargs ) -> Union [Type [namedtuple ], dict ]:
@@ -602,8 +624,6 @@ def dictable_namedtuple(typename, field_names, *args, **kwargs) -> Union[Type[na
602
624
"""
603
625
module = kwargs .get ('module' , None )
604
626
read_only = kwargs .pop ('read_only' , False )
605
- # First we create a namedtuple "type" using the same arguments we were passed
606
- BaseNT = namedtuple (typename , field_names , * args , ** kwargs )
607
627
608
628
# As per namedtuple's comment block, we need to set __module__ to the frame
609
629
# where the named tuple is created, otherwise it can't be pickled properly.
@@ -613,11 +633,8 @@ def dictable_namedtuple(typename, field_names, *args, **kwargs) -> Union[Type[na
613
633
module = sys ._getframe (1 ).f_globals .get ('__name__' , '__main__' )
614
634
except (AttributeError , ValueError ):
615
635
pass
616
- # Then we create a class which inherits the namedtuple type we've created, so we can easily
617
- # override / add new attributes and methods
618
- # DictableNamedTuple = convert_dictable_namedtuple(BaseNT, typename=typename, module=module)
619
- DictableNamedTuple = subclass_dictable_namedtuple (BaseNT , module = module , read_only = read_only , * args , ** kwargs )
620
- return DictableNamedTuple
636
+
637
+ return make_dict_tuple (typename , field_names , module = module , read_only = read_only , * args , ** kwargs )
621
638
622
639
623
640
class Dictable :
0 commit comments