@@ -60,6 +60,77 @@ def _process_count(count):
6060 return int (count )
6161
6262
63+ class _BootstrappedCachedProperty (misc_tools .OwnNameDiscoveringDescriptor ):
64+ '''
65+ A property that is calculated only once for an object, and then cached.
66+
67+ This is defined here in `bagging.py` because we can't import the canonical
68+ `CachedProperty` because of an import loop.
69+
70+ Usage:
71+
72+ class MyObject:
73+
74+ # ... Regular definitions here
75+
76+ def _get_personality(self):
77+ print('Calculating personality...')
78+ time.sleep(5) # Time consuming process that creates personality
79+ return 'Nice person'
80+
81+ personality = CachedProperty(_get_personality)
82+
83+ You can also put in a value as the first argument if you'd like to have it
84+ returned instead of using a getter. (It can be a tobag static value like
85+ `0`). If this value happens to be a callable but you'd still like it to be
86+ used as a static value, use `force_value_not_getter=True`.
87+ '''
88+ def __init__ (self , getter_or_value , doc = None , name = None ,
89+ force_value_not_getter = False ):
90+ '''
91+ Construct the cached property.
92+
93+ `getter_or_value` may be either a function that takes the parent object
94+ and returns the value of the property, or the value of the property
95+ itself, (as long as it's not a callable.)
96+
97+ You may optionally pass in the name that this property has in the
98+ class; this will save a bit of processing later.
99+ '''
100+ misc_tools .OwnNameDiscoveringDescriptor .__init__ (self , name = name )
101+ if callable (getter_or_value ) and not force_value_not_getter :
102+ self .getter = getter_or_value
103+ else :
104+ self .getter = lambda thing : getter_or_value
105+ self .__doc__ = doc or getattr (self .getter , '__doc__' , None )
106+
107+
108+ def __get__ (self , obj , our_type = None ):
109+
110+ if obj is None :
111+ # We're being accessed from the class itself, not from an object
112+ return self
113+
114+ value = self .getter (obj )
115+
116+ setattr (obj , self .get_our_name (obj , our_type = our_type ), value )
117+
118+ return value
119+
120+
121+ def __call__ (self , method_function ):
122+ '''
123+ Decorate method to use value of `CachedProperty` as a context manager.
124+ '''
125+ def inner (same_method_function , self_obj , * args , ** kwargs ):
126+ with getattr (self_obj , self .get_our_name (self_obj )):
127+ return method_function (self_obj , * args , ** kwargs )
128+ return decorator_tools .decorator (inner , method_function )
129+
130+
131+ def __repr__ (self ):
132+ return '<%s: %s>' % (type (self ).__name__ , self .our_name or self .getter )
133+
63134
64135class _BaseBagMixin :
65136 '''Mixin for `FrozenBag` and `FrozenOrderedBag`.'''
@@ -125,6 +196,13 @@ def elements(self):
125196 def __contains__ (self , item ):
126197 return (self [item ] >= 1 )
127198
199+ n_elements = property (lambda self : sum (self .values ()))
200+
201+ @property
202+ def frozen_bag_bag (self ):
203+ from .frozen_bag_bag import FrozenBagBag
204+ return FrozenBagBag (self .values ())
205+
128206 def __or__ (self , other ):
129207 '''
130208 Get the maximum of value in either of the input bags.
@@ -248,28 +326,6 @@ def __pow__(self, other, modulo=None):
248326 __bool__ = lambda self : any (True for element in self .elements ())
249327
250328
251- _n_elements = None
252- @property
253- def n_elements (self ):
254- # Implemented as a poor man's `CachedProperty` because we can't use the
255- # real `CachedProperty` due to circular import.
256- if self ._n_elements is None :
257- self ._n_elements = sum (self .values ())
258- return self ._n_elements
259- _n_elements = None
260-
261-
262- _frozen_bag_bag = None
263- @property
264- def frozen_bag_bag (self ):
265- # Implemented as a poor man's `CachedProperty` because we can't use the
266- # real `CachedProperty` due to circular import.
267- from .frozen_bag_bag import FrozenBagBag
268- if self ._frozen_bag_bag is None :
269- self ._frozen_bag_bag = FrozenBagBag (self .values ())
270- return self ._frozen_bag_bag
271-
272-
273329 # We define all the comparison methods manually instead of using
274330 # `total_ordering` because `total_ordering` assumes that >= means (> and
275331 # ==) while we, in `FrozenOrderedBag`, don't have that hold because ==
@@ -492,6 +548,15 @@ def __eq__(self, other):
492548 index = misc_tools .ProxyProperty ('._dict.index' )
493549
494550
551+ class _FrozenBagMixin :
552+
553+ n_elements = _BootstrappedCachedProperty (lambda self : sum (self .values ()))
554+
555+ @_BootstrappedCachedProperty
556+ def frozen_bag_bag (self ):
557+ from .frozen_bag_bag import FrozenBagBag
558+ return FrozenBagBag (self .values ())
559+
495560
496561
497562class _BaseDictDelegator (collections .MutableMapping ):
@@ -600,7 +665,7 @@ def popitem(self, last=True):
600665
601666
602667
603- class FrozenBag (_BaseBagMixin , FrozenDict ):
668+ class FrozenBag (_BaseBagMixin , _FrozenBagMixin , FrozenDict ):
604669 '''
605670 An immutable bag that counts items.
606671
@@ -626,7 +691,7 @@ def __hash__(self):
626691 return hash ((type (self ), frozenset (self .items ())))
627692
628693
629- class FrozenOrderedBag (_OrderedBagMixin , _BaseBagMixin ,
694+ class FrozenOrderedBag (_OrderedBagMixin , _FrozenBagMixin , _BaseBagMixin ,
630695 FrozenOrderedDict ):
631696 '''
632697 An immutable, ordered bag that counts items.
0 commit comments