summaryrefslogtreecommitdiff
path: root/bps/unstable/softref.py
diff options
context:
space:
mode:
authorEli Collins <elic@assurancetechnologies.com>2011-01-06 01:11:16 +0000
committerEli Collins <elic@assurancetechnologies.com>2011-01-06 01:11:16 +0000
commit0dd599bbb323387991cd8a3565ea87f36ff0892f (patch)
tree26705624c861c0725d1e8fd8555f84705775e1c2 /bps/unstable/softref.py
downloadpasslib-0dd599bbb323387991cd8a3565ea87f36ff0892f.tar.gz
cloning bps to passlib trunk
Diffstat (limited to 'bps/unstable/softref.py')
-rw-r--r--bps/unstable/softref.py847
1 files changed, 847 insertions, 0 deletions
diff --git a/bps/unstable/softref.py b/bps/unstable/softref.py
new file mode 100644
index 0000000..030d013
--- /dev/null
+++ b/bps/unstable/softref.py
@@ -0,0 +1,847 @@
+"""
+soft reference implementation
+
+this module implements something similar to Java's soft references.
+these are normal python references, but use reference count introspection
+to remove the reference only no other references to the final object
+are left, AND the object has not been used in a long enough time
+to warrant removing the object.
+
+this is mainly useful when maintaining a cache of objects
+which can be regenerated, but only through a (cpu) costly process.
+it's generally useful to keep such objects around in a cache,
+in case they are needed again, but for memory reasons,
+long running processes will generally want to free unused
+objects up after a time.
+
+this class provides ``softref()`` which provides access-time tracking,
+allowing the reference to be freed up after it's remained unused
+for an (application specified) amount of time.
+
+it also contains platform-specific code for detecting low memory conditions,
+and freeing up softrefs more aggressively in this case,
+in the hopes of staving off out-of-memory conditions.
+"""
+#=================================================================================
+#imports
+#=================================================================================
+from __future__ import with_statement
+#core
+import logging
+import sys
+import threading
+from itertools import count as itercount
+from time import time as cur_time
+import logging; log = logging.getLogger(__name__)
+import UserDict
+from weakref import ref as make_weakref
+from warnings import warn
+#site
+#pkg
+#local
+log = logging.getLogger(__name__)
+__all__ = [
+ #main entry point
+ "softref",
+
+ #collector control & configuration
+ "collect", "enable", "disable", "is_enabled",
+ "get_config", "set_config",
+
+ #introspection
+ "get_softref_count",
+ "get_softrefs",
+
+ #other helpers
+ 'SoftValueDict',
+## 'KeyedSoftRef',
+# 'get_memory_usage',
+]
+
+#=================================================================================
+#platform specific code for detecting memory levels
+#=================================================================================
+
+#NOTE: this is mainly used a helper for the collector to detect low memory conditions.
+
+if sys.platform == "linux2":
+ #use /proc/meminfo
+ import re
+
+ _memtotal_re = re.compile("^MemTotal:\s+(\d+)\s*kb$", re.I|re.M)
+ _memfree_re = re.compile("^MemFree:\s+(\d+)\s*kb$", re.I|re.M)
+ _buffers_re = re.compile("^Buffers:\s+(\d+)\s*kb$", re.I|re.M)
+ _cached_re = re.compile("^Cached:\s+(\d+)\s*kb$", re.I|re.M)
+
+ def get_memory_usage():
+ try:
+ with file("/proc/meminfo") as fh:
+ data = fh.read()
+ memtotal = int(_memtotal_re.search(data).group(1))
+ memfree = int(_memfree_re.search(data).group(1))
+ buffers = int(_buffers_re.search(data).group(1))
+ cached = int(_cached_re.search(data).group(1))
+ avail = memfree + buffers + cached
+ assert 0 <= avail <= memtotal
+ return memtotal, avail
+ except:
+ #this is a sign something has gone wrong :|
+ log.error("error reading /proc/meminfo", exc_info=True)
+ return (-1, -1)
+
+elif sys.platform == "win32":
+ #implementation taken from http://code.activestate.com/recipes/511491/
+ #TODO: check if this will work with cygwin platform
+ import ctypes
+ kernel32 = ctypes.windll.kernel32
+ c_ulong = ctypes.c_ulong
+ class MEMORYSTATUS(ctypes.Structure):
+ _fields_ = [
+ ('dwLength', c_ulong),
+ ('dwMemoryLoad', c_ulong),
+ ('dwTotalPhys', c_ulong),
+ ('dwAvailPhys', c_ulong),
+ ('dwTotalPageFile', c_ulong),
+ ('dwAvailPageFile', c_ulong),
+ ('dwTotalVirtual', c_ulong),
+ ('dwAvailVirtual', c_ulong)
+ ]
+ def get_memory_usage():
+ memoryStatus = MEMORYSTATUS()
+ memoryStatus.dwLength = ctypes.sizeof(MEMORYSTATUS)
+ kernel32.GlobalMemoryStatus(ctypes.byref(memoryStatus))
+ #XXX: does availphy correspond properly to linux's free-buffers-cache ?
+ return (memoryStatus.dwTotalPhys//1024, memoryStatus.dwAvailPhys//1024)
+else:
+ #TODO: would like to support more platforms (esp OS X)
+ warn("disabled low memory detection, not implemented for " + sys.platform + " platform")
+ def get_memory_usage():
+ return (-1,-1)
+
+get_memory_usage.__doc__ = """return memory usage
+
+Return current memory usage as tuple
+``(total physical memory, available physical memory)``.
+
+All measurments in kilobytes.
+
+Available physical memory counts
+os buffers & cache as "available",
+where possible for the implementation.
+
+Currently supports linux & win32 platforms,
+if run on other platforms, will return
+tuple where all values are -1.
+
+If an error occurs on a supported platform,
+it will be logged, and a tuple of -1 values will be returned.
+"""
+
+#=================================================================================
+#soft ref collector - implemented as singleton of private class
+#=================================================================================
+
+#log used by collector
+clog = logging.getLogger(__name__ + ".collector")
+
+#NOTE: design of this module attempts to put as much work in the collector,
+# and as little work in the creation & access of the softrefs,
+# since the collector is designed to run in another thread anyways.
+
+class _SoftRefCollector(object):
+ """tracks softref objects, handles collecting them when needed"""
+ #=================================================================================
+ #instance attrs
+ #=================================================================================
+
+ #--------------------------------------------------------------------
+ #configuration
+ #--------------------------------------------------------------------
+ default_min_age = 600 #default delay from last access time before a softref can be released
+ default_max_age = -1 #default delay from last access time before softref will be released even w/o lowmem condition
+
+ collect_frequency = 300 #how often collector should run
+
+ lowmem_abs = 25 * 1024 #lowmem level in kilobytes
+ lowmem_pct = .05 #lowmem level as % of total mem
+ #low memory is calculated as max(lowmem_abs, physmem * lowmem_pct)
+ #this way there's a floor to the lowmem level for low memory systems (eg <512mb),
+ #but it scales so lowmem doesn't get hit as often for systems with more memory (eg 4gb)
+ #values may need tuning
+
+ #--------------------------------------------------------------------
+ #softref state
+ #--------------------------------------------------------------------
+ targetmap = None #map of id(target) -> [ weakref(softref(target)) ... ]
+ #since id will only be re-used once target is dereferenced,
+ #and that won't happen as long as softrefs exist,
+ #ids should never conflict
+ last_collect = 0 #timestamp of last time collect() ran
+
+ #--------------------------------------------------------------------
+ #threading
+ #--------------------------------------------------------------------
+ lock = None #threading lock for instance state
+ thread = None #thread collector uses to run in the background
+ thread_stop = None #Event used to signal collector thread that it should halt
+
+ #=================================================================================
+ #init
+ #=================================================================================
+ def __init__(self):
+ self.targetmap = {}
+ self.lock = threading.Lock() #lock for changes
+ #XXX: if softref's onrelease tried to create a new softref,
+ # it'll block on this lock.. in that case, this should be made to an RLock.
+ # if this happens, could either do it permanently, or make an use_rlock() method
+ self.thread_stop = threading.Event()
+
+ #=================================================================================
+ #softref interface
+ #=================================================================================
+ def add(self, sref):
+ "add a new softref instance"
+ #NOTE: instances *must* be compatible with softref type,
+ # namely its' _target _atime min_age attrs
+
+ #TODO: some target types (eg: int, str, bool, None) should never have softref expire,
+ # and we shouldn't even bother tracking them. should find nice way to have fallback in that case.
+ # bucket system would be good, could just never add them to initial bucket.
+
+ target_id = id(sref._target)
+ sref_wr = make_weakref(sref)
+ targetmap = self.targetmap
+ with self.lock:
+ srlist = targetmap.get(target_id)
+ if srlist is None:
+ targetmap[target_id] = [ sref_wr ]
+ else:
+ srlist.append(sref_wr)
+
+ #=================================================================================
+ #introspection
+ #=================================================================================
+ @property
+ def next_collect(self):
+ "time next collection is scheduled"
+ return self.last_collect + self.collect_frequency
+
+ def count(self, target):
+ with self.lock:
+ srlist = self.targetmap.get(id(target))
+ count = 0
+ if srlist:
+ for sref_wr in srlist:
+ if sref_wr():
+ count += 1
+ return count
+
+ def refs(self, target):
+ with self.lock:
+ srlist = self.targetmap.get(id(target))
+ out = []
+ if srlist:
+ for sref_wr in srlist:
+ sref = sref_wr()
+ if sref:
+ out.append(sref)
+ return out
+
+ #=================================================================================
+ #collector
+ #=================================================================================
+ def collect(self):
+ #TODO: rework this scan into using generational buckets (ala gc module),
+ # keyed off of target id, that way long-lived ones don't have to be scanned as often.
+ with self.lock:
+ targetmap = self.targetmap
+ lowmem = self._check_lowmem()
+ clog.info("collecting soft refs... targets=%d lowmem=%r", len(targetmap), lowmem)
+ purge_keys = set()
+ cur = cur_time()
+ #call collect_entry for all of targetmap
+ helper = self._collect_entry
+ for target_id, srlist in targetmap.iteritems():
+ if helper(target_id, srlist, cur, lowmem):
+ purge_keys.add(target_id)
+ #purge any keys we identified previously, just to free up even more memory
+ for target_id in purge_keys:
+ del targetmap[target_id]
+ self.last_collect = cur
+ count = len(purge_keys)
+ clog.info("released %d targets", count)
+ return count
+
+ def _check_lowmem(self):
+ "check if we're running in low memory condition"
+ #TOOD:
+ # - sliding scale, doing more as lowmem approaches? ie, freeing only some eligible softrefs
+ # - have collector prefer softrefs w/ older atimes in that case
+ # - weighting for different instances / types, allowing "heavy" classes to be preferred to be freed?
+ # - generational buckets
+ # - other schemes?
+ total, free = get_memory_usage()
+ if total == -1: #not available or error occurred
+ clog.debug("disabling lowmem check")
+ #just to stop spamming, let's not call get mem again
+ self._check_lowmem = lambda : False
+ return False
+ threshold = int(max(self.lowmem_abs, total * self.lowmem_pct))
+ clog.debug("system memory: total=%r lowmem_threshold=%r free=%r", total, threshold, free)
+ return free <= threshold
+
+ def _collect_entry(self, target_id, srlist, cur, lowmem):
+ "run collect algorithm for specified entry, return True if it needs removing"
+
+ #NOTE: could consolidate min_age, max_age (and maybe atime)
+ #into a single record stored w/in collector,
+ #instead of storing separately in each softref.
+
+ #scan existing softrefs, working out latest atime & softrefcount
+ atime = 0
+ min_age = self.default_min_age
+ max_age = self.default_max_age
+ srefs = [] #accumulate hard refs to sref objects, so weakrefs don't vanish while in this loop
+ for sref_wr in srlist:
+ sref = sref_wr()
+ if sref is None:
+ #TODO: could purge sref_wr here for extra mem,
+ #but probably efficient enough for most cases
+ #to just purge whole targetmap entry once target_wr is gone
+ continue
+ srefs.append(sref)
+ if sref._atime > atime:
+ atime = sref._atime
+ if sref.min_age > min_age:
+ min_age = sref.min_age
+ if sref.max_age is not None and (max_age == -1 or sref.max_age < max_age):
+ max_age = sref.max_age
+
+ #check if any softrefs is still around
+ if not srefs:
+ #all references to softref objects dropped before they were purged
+ clog.debug("softrefs vanished: %r", target_id)
+ return True
+
+ #decide if this one should be released yet
+ age = cur-atime
+ if age <= min_age:
+ return False
+ if not lowmem and (max_age == -1 or age < max_age):
+ return False
+
+ #sanity check on target
+ assert all(id(sref._target) == target_id for sref in srefs), "targetmap corrupted: target=%r srefs=%r" % (target_id, srefs)
+ target = srefs[0]._target
+
+ #now check how many hardrefs are out there,
+ #after ignoring the following hard refs:
+ # +1 reference in this frame's 'target' var
+ # +1 reference in getrefcount() call
+ # +N references held by '_target' attr of srefs
+ # any more, and it's external.
+ # any less, and it's runtimeerror, cause one of the above was missing.
+ offset = 2+len(srefs)
+ rc = sys.getrefcount(target)
+ if rc < offset:
+ raise RuntimeError, "too few references to target: %r rc=%r offset=%r" % (target, rc, offset)
+ if rc > offset:
+ #(rc-offset) hardrefs still out there, so don't purge softref
+ return False
+
+ #ok, time to release softref
+ clog.info("releasing softrefs: %r", target)
+ for sref in reversed(srefs): #NOTE: reversed so handlers called LIFO, same as weakref module
+ sref._target = None #just so existing softrefs return None
+ h = sref._onrelease
+ if h is not None:
+ try:
+ h(sref)
+ except:
+ clog.error("error in softref onrelease callback: %r %r", target, onrelease)
+ sys.excepthook(*sys.exc_info())
+
+ #schedule whole entry for removal
+ return True
+
+ #=================================================================================
+ #collector thread
+ #=================================================================================
+ def is_enabled(self):
+ "return true if collector thread is running, else false"
+ if not self.thread or not self.thread.isAlive():
+ return False
+ if self.thread_stop.isSet():
+ return None #special value indicating thread is still running but will stop soon. call disable() to ensure it's stopped.
+ return True
+
+ def enable(self):
+ "start collector thread if not running"
+ if self.thread and self.thread.isAlive():
+ if not self.thread_stop.isSet():
+ return True
+ #wait til thread has exited
+ self.thread.join()
+ self.thread_stop.clear()
+ thread = threading.Thread(target=self._collector_loop, name="[softref collector]")
+ thread.setDaemon(True)
+ clog.debug("softref collector thread launched")
+ thread.run()
+
+ def _collector_loop(self):
+ "main loop used by collector thread"
+ clog.info("softref collector thread started")
+ #XXX: should we check for errors and have a cooldown period before trying again?
+ while True:
+ #wait for stop event OR time for next collection
+ delay = max(.05, self.next_collect - cur_time())
+ self.thread_stop.wait(delay)
+
+ #check if we've been signalled to stop
+ if self.thread_stop.isSet():
+ clog.info("softref collector thread stopped")
+ return
+
+ #run collector
+ clog.info("collecting softrefs")
+ self.collect()
+
+ def disable(self):
+ "stop collector thread if running"
+ #NOTE: this shouldn't be called if self.lock is held,
+ #otherwise might deadlock if we join while
+ #other thread is trying to acquire lock.
+
+ #signal thread should stop
+ self.thread_stop.set()
+
+ #then join til it does
+ if self.thread and self.thread.isAlive():
+ self.thread.join()
+ self.thread = None
+
+ #=================================================================================
+ #eoc
+ #=================================================================================
+
+#=================================================================================
+#single collector and public interface to it
+#=================================================================================
+
+_collector = _SoftRefCollector()
+
+#----------------------------------------------------------------------
+#configuration collector
+#----------------------------------------------------------------------
+def set_config(default_max_age=None, default_min_age=None, collect_frequency=None,
+ lowmem_pct=None, lowmem_abs=None):
+ """update various collector config options.
+
+ :kwd default_min_age:
+ change minimum age (seconds since last access)
+ before a softref is eligible to be released
+ by the collector.
+
+ softrefs can specify this per-instance.
+
+ defaults to 10 minutes.
+
+ :kwd default_max_age:
+ change maximum age (seconds since last access)
+ before a softref will be purged by collector
+ even if there isn't a low memory condition.
+
+ softrefs can specify this per-instance.
+
+ defaults to -1 minutes,
+ which indicates there is no max age.
+
+ :kwd collect_frequency:
+ how often collector thread calls collect(),
+ measured in seconds.
+
+ defaults to every 5 minutes.
+
+ :kwd lowmem_pct:
+ if free memory drops below this amount (as percent of total memory),
+ the collector considers the system to be low on memory,
+ and becomes agressive in purging softrefs.
+
+ the actual low memory threshold is ``max(lowmem_abs, phymem * lowmem_pct)``,
+ providing a floor to the lowmem threshold so it will function acceptably
+ on systems with a small amount of physical memory.
+
+ defaults to .05 percent (~100 Mb on a 2Gb system).
+
+ :kwd lowmem_abs:
+ minimum free memory threshold (in kilobytes).
+ see lowmem_pct for more details.
+
+ defaults to 25 Mb (~.05 percent on a 512 Mb system).
+
+ .. note::
+ default values subject to change as internal algorithm
+ is being refined (currently doesn't account for system memory usage, etc)
+
+ most changes will take affect the next time collect() runs,
+ with the exception of collect_frequency, which will take effect
+ next time the collector thread wakes up.
+ """
+ global _collector
+ if default_max_age is not None:
+ if default_max_age <= 0 and default_max_age != -1:
+ raise ValueError, "default_max_age must be -1 or positive value"
+ _collector.default_max_age = default_max_age
+ if default_min_age is not None:
+ if default_min_age < 0:
+ raise ValueError, "default_min_age must be >= 0"
+ _collector.default_min_age = default_min_age
+ if collect_frequency is not None:
+ if collector_frequency <= 0:
+ raise ValueError, "collector frequency must be > 0"
+ _collector.collect_frequency = collect_frequency
+ if lowmem_pct is not None:
+ if lowmem_pct < 0 or lowmem_pct >= 1:
+ raise ValueError, "lowmem_pct must be between [0.0,1.0)"
+ _collector.lowmem_pct = lowmem_pct
+ if lowmem_abs is not None:
+ if lowmem_abs < 0:
+ raise ValueError, "lowmem_abs must be >= 0"
+ _collector.lowmem_abs = lowmem_abs
+
+def get_config():
+ "return dict of current collector config options, corresponding to :func:`set_config` kwds"
+ global _collector
+ return dict(
+ (k,getattr(_collector,k))
+ for k in ["default_min_age", "default_max_age", "collect_frequency",
+ "lowmem_pct", "lowmem_abs"]
+ )
+
+#----------------------------------------------------------------------
+#running collector
+#----------------------------------------------------------------------
+def is_enabled():
+ "check if softref collector thread is running"
+ return _collector.is_enabled()
+
+def enable():
+ "ensure softref collector thread is running"
+ return _collector.enable()
+
+def disable():
+ "ensure softref collector thread is not running (will block until thread terminates)"
+ return _collector.disable()
+
+def collect():
+ "force a run of the softref collector immediately"
+ return _collector.collect()
+
+#----------------------------------------------------------------------
+#introspection of softref information
+#----------------------------------------------------------------------
+def get_softref_count(target):
+ "return number of soft refs attached to target"
+ return _collector.count(target)
+
+def get_softrefs(target):
+ "return all softref instances attached to target"
+ return _collector.refs(target)
+
+def get_hardref_count(target):
+ "return number of hard refs attached to target (include 1 ref for this function call)"
+ rc = sys.getrefcount(target)
+ sc = get_softref_count(target)
+ return rc-sc-1
+
+#=================================================================================
+#softref constructor
+#=================================================================================
+class softref(object):
+ """create a softref to another object
+
+ :arg target: object this should hold softref to
+ :arg onrelease:
+ optional callback to invoke ``onrelease(sref)``
+ if softref to target is released before this object
+ is dereferenced.
+ :arg min_age:
+ override default min_age for this target
+ :arg max_age:
+ override default max_age for this target
+
+ :returns:
+ a new softref instance.
+ calling it will return original target, or ``None``,
+ same as a weakref.
+ """
+
+ #TODO: provide hook which can prevent softref from being freed
+ #(eg if object shouldn't be freed if it's in a certain state, etc)
+
+ #=================================================================================
+ #instance attrs
+ #=================================================================================
+ __slots__ = ["__weakref__", "_target", "_atime", "_onrelease", "min_age", "max_age"]
+
+ #store quick links to collector
+ _collector_lock = _collector.lock
+ _collector_add = _collector.add
+
+ #=================================================================================
+ #instance methods
+ #=================================================================================
+ #TODO: could override __new__ for cls=softref and only 'target' param,
+ # let it cache things for us.
+
+ def __init__(self, target, onrelease=None, min_age=None, max_age=None):
+ self._target = target
+ self._onrelease = onrelease
+ self.min_age = min_age
+ self.max_age = max_age
+ self._atime = cur_time()
+ self._collector_add(self) #register new softref with collector
+
+ ##@property
+ ##def atime(self): return self._atime
+
+ ##def touch(self):
+ ## self._atime = cur_time()
+
+ def __call__(self):
+ self._atime = cur_time() #NOTE: doing this outside lock cause it can't hurt, and might catch collector in it's tracks
+
+ #NOTE: have to lock collector while we're creating new hardref,
+ #or collector might see N hard refs, this thread creates hard ref N+1,
+ #and then collector purges softref, causing this thread
+ #to have a hard ref after softref was purged (which is against
+ #how this module wants softrefs to behave)
+ with self._collector_lock:
+ return self._target
+
+ def __repr__(self):
+ target = self._target
+ if target is None:
+ return "<softref at 0x%x; dead>"% (id(self),)
+ else:
+ return "<softref at 0x%x; to '%s' at 0x%x>" % (id(self), type(target).__name__, id(target))
+
+ def __eq__(self, other):
+ return self._target == other
+
+ def __ne__(self, other):
+ return self._target != other
+
+ #=================================================================================
+ #eoc
+ #=================================================================================
+
+#=================================================================================
+#soft value dict
+#=================================================================================
+#NOTE: this is cloned from py26 weakref.WeakValueDict, and adapted for softrefs...
+
+class SoftValueDictionary(UserDict.UserDict):
+ """Mapping class that references values using softref.
+
+ Entries in the dictionary will be discarded when no strong
+ reference to the value exists anymore
+ """
+ # We inherit the constructor without worrying about the input
+ # dictionary; since it uses our .update() method, we get the right
+ # checks (if the other dictionary is a WeakValueDictionary,
+ # objects are unwrapped on the way out, and we always wrap on the
+ # way in).
+
+ min_age = None
+ max_age = None
+
+ def __init__(self, source=None, min_age=None, max_age=None):
+ if min_age:
+ self.min_age = min_age
+ if max_age:
+ self.max_age = max_age
+ def remove(sr, selfref=make_weakref(self)):
+ self = selfref()
+ if self is not None:
+ del self.data[sr.key]
+ self._remove = remove
+ if source is None:
+ args = ()
+ else:
+ args = (source,)
+ UserDict.UserDict.__init__(self, *args)
+
+ ##def touch(self, key):
+ ## "helper to update softref atime for value attached to key"
+ ## self.data[key].touch()
+
+ def __getitem__(self, key):
+ o = self.data[key]()
+ if o is None:
+ raise KeyError, key
+ else:
+ return o
+
+ def __contains__(self, key):
+ try:
+ o = self.data[key]()
+ except KeyError:
+ return False
+ return o is not None
+
+ def has_key(self, key):
+ try:
+ o = self.data[key]()
+ except KeyError:
+ return False
+ return o is not None
+
+ def __repr__(self):
+ return "<SoftValueDictionary at %s>" % id(self)
+
+ def __setitem__(self, key, value):
+ self.data[key] = KeyedSoftRef(key, value, self._remove, self.min_age, self.max_age)
+
+ def copy(self):
+ new = SoftValueDictionary()
+ new.min_age = self.min_age
+ new.max_age = self.max_age
+ for key, sr in self.data.items():
+ o = sr()
+ if o is not None:
+ new[key] = o
+ return new
+
+ def get(self, key, default=None):
+ try:
+ sr = self.data[key]
+ except KeyError:
+ return default
+ else:
+ o = sr()
+ if o is None:
+ # This should only happen
+ return default
+ else:
+ return o
+
+ def items(self):
+ L = []
+ for key, sr in self.data.items():
+ o = sr()
+ if o is not None:
+ L.append((key, o))
+ return L
+
+ def iteritems(self):
+ for sr in self.data.itervalues():
+ value = ws()
+ if value is not None:
+ yield ws.key, value
+
+ def iterkeys(self):
+ return self.data.iterkeys()
+
+ def __iter__(self):
+ return self.data.iterkeys()
+
+ def itervaluerefs(self):
+ """Return an iterator that yields the weak references to the values.
+
+ The references are not guaranteed to be 'live' at the time
+ they are used, so the result of calling the references needs
+ to be checked before being used. This can be used to avoid
+ creating references that will cause the garbage collector to
+ keep the values around longer than needed.
+
+ """
+ return self.data.itervalues()
+
+ def itervalues(self):
+ for wr in self.data.itervalues():
+ obj = wr()
+ if obj is not None:
+ yield obj
+
+ def popitem(self):
+ while 1:
+ key, wr = self.data.popitem()
+ o = wr()
+ if o is not None:
+ return key, o
+
+ def pop(self, key, *args):
+ try:
+ o = self.data.pop(key)()
+ except KeyError:
+ if args:
+ return args[0]
+ raise
+ if o is None:
+ raise KeyError, key
+ else:
+ return o
+
+ def setdefault(self, key, default=None):
+ try:
+ wr = self.data[key]
+ except KeyError:
+ self.data[key] = KeyedSoftRef(key, default, self._remove, self.min_age, self.max_age)
+ return default
+ else:
+ return wr()
+
+ def update(self, dict=None, **kwargs):
+ d = self.data
+ if dict is not None:
+ if not hasattr(dict, "items"):
+ dict = type({})(dict)
+ for key, o in dict.items():
+ d[key] = KeyedSoftRef(key, o, self._remove, self.min_age, self.max_age)
+ if len(kwargs):
+ self.update(kwargs)
+
+ def valuerefs(self):
+ """Return a list of weak references to the values.
+
+ The references are not guaranteed to be 'live' at the time
+ they are used, so the result of calling the references needs
+ to be checked before being used. This can be used to avoid
+ creating references that will cause the garbage collector to
+ keep the values around longer than needed.
+
+ """
+ return self.data.values()
+
+ def values(self):
+ L = []
+ for wr in self.data.values():
+ o = wr()
+ if o is not None:
+ L.append(o)
+ return L
+
+class KeyedSoftRef(softref):
+ """Specialized reference that includes a key corresponding to the value.
+
+ This is used in the SoftValueDictionary to avoid having to create
+ a function object for each key stored in the mapping. A shared
+ callback object can use the 'key' attribute of a KeyedSoftRef instead
+ of getting a reference to the key from an enclosing scope.
+
+ """
+
+ __slots__ = "key",
+
+ def __new__(cls, key, target, onrelease=None, min_age=None, max_age=None):
+ self = softref.__new__(cls, target, onrelease, min_age, max_age)
+ self.key = key
+ return self
+
+ def __init__(self, key, target, onrelease=None, min_age=None, max_age=None):
+ super(KeyedSoftRef, self).__init__(target, onrelease, min_age, max_age)
+
+#=================================================================================
+#eof
+#=================================================================================