From 922686a3487eeed2245f576cb26006ea018eaa81 Mon Sep 17 00:00:00 2001 From: Douglas RAILLARD Date: Wed, 15 Jan 2020 16:05:44 +0000 Subject: [PATCH] utils/misc: Add tls_property() Similar to a regular property(), with the following differences: * Values are memoized and are threadlocal * The value returned by the property needs to be called (like a weakref) to get the actual value. This level of indirection is needed to allow methods to be implemented in the proxy object without clashing with the value's methods. * If the above is too annoying, a "sub property" can be created with the regular property() behavior (and therefore without the additional methods) using tls_property.basic_property . --- devlib/utils/misc.py | 118 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/devlib/utils/misc.py b/devlib/utils/misc.py index 0738b3e..4f8beb1 100644 --- a/devlib/utils/misc.py +++ b/devlib/utils/misc.py @@ -23,6 +23,7 @@ from contextlib import contextmanager from functools import partial, reduce from itertools import groupby from operator import itemgetter +from weakref import WeakKeyDictionary, WeakSet import ctypes import functools @@ -705,3 +706,120 @@ def batch_contextmanager(f, kwargs_list): for kwargs in kwargs_list: stack.enter_context(f(**kwargs)) yield + +class tls_property: + """ + Use it like `property` decorator, but the result will be memoized per + thread. When the owning thread dies, the values for that thread will be + destroyed. + + In order to get the values, it's necessary to call the object + given by the property. This is necessary in order to be able to add methods + to that object, like :meth:`_BoundTLSProperty.get_all_values`. + + Values can be set and deleted as well, which will be a thread-local set. + """ + + @property + def name(self): + return self.factory.__name__ + + def __init__(self, factory): + self.factory = factory + # Lock accesses to shared WeakKeyDictionary and WeakSet + self.lock = threading.Lock() + + def __get__(self, instance, owner=None): + return _BoundTLSProperty(self, instance, owner) + + def _get_value(self, instance, owner): + tls, values = self._get_tls(instance) + try: + return tls.value + except AttributeError: + # Bind the method to `instance` + f = self.factory.__get__(instance, owner) + obj = f() + tls.value = obj + # Since that's a WeakSet, values will be removed automatically once + # the threading.local variable that holds them is destroyed + with self.lock: + values.add(obj) + return obj + + def _get_all_values(self, instance, owner): + with self.lock: + # Grab a reference to all the objects at the time of the call by + # using a regular set + tls, values = self._get_tls(instance=instance) + return set(values) + + def __set__(self, instance, value): + tls, values = self._get_tls(instance) + tls.value = value + with self.lock: + values.add(value) + + def __delete__(self, instance): + tls, values = self._get_tls(instance) + with self.lock: + values.discard(tls.value) + del tls.value + + def _get_tls(self, instance): + dct = instance.__dict__ + name = self.name + try: + # Using instance.__dict__[self.name] is safe as + # getattr(instance, name) will return the property instead, as + # the property is a descriptor + tls = dct[name] + except KeyError: + with self.lock: + # Double check after taking the lock to avoid a race + if name not in dct: + tls = (threading.local(), WeakSet()) + dct[name] = tls + + return tls + + @property + def basic_property(self): + """ + Return a basic property that can be used to access the TLS value + without having to call it first. + + The drawback is that it's not possible to do anything over than + getting/setting/deleting. + """ + def getter(instance, owner=None): + prop = self.__get__(instance, owner) + return prop() + + return property(getter, self.__set__, self.__delete__) + +class _BoundTLSProperty: + """ + Simple proxy object to allow either calling it to get the TLS value, or get + some other informations by calling methods. + """ + def __init__(self, tls_property, instance, owner): + self.tls_property = tls_property + self.instance = instance + self.owner = owner + + def __call__(self): + return self.tls_property._get_value( + instance=self.instance, + owner=self.owner, + ) + + def get_all_values(self): + """ + Returns all the thread-local values currently in use in the process for + that property for that instance. + """ + return self.tls_property._get_all_values( + instance=self.instance, + owner=self.owner, + )