codekingpro/portable-devtools
114k
1"""Thread-local objects.2 3(Note that this module provides a Python version of the threading.local4 class. Depending on the version of Python you're using, there may be a5 faster one available. You should always import the `local` class from6 `threading`.)7"""8 9from weakref import ref10from contextlib import contextmanager11 12__all__ = ["local"]13 14# We need to use objects from the threading module, but the threading15# module may also want to use our `local` class, if support for locals16# isn't compiled in to the `thread` module. This creates potential problems17# with circular imports. For that reason, we don't import `threading`18# until the bottom of this file (a hack sufficient to worm around the19# potential problems). Note that all platforms on CPython do have support20# for locals in the `thread` module, and there is no circular import problem21# then, so problems introduced by fiddling the order of imports here won't22# manifest.23 24class _localimpl:25 """A class managing thread-local dicts"""26 __slots__ = 'key', 'dicts', 'localargs', 'locallock', '__weakref__'27 28 def __init__(self):29 # The key used in the Thread objects' attribute dicts.30 # We keep it a string for speed but make it unlikely to clash with31 # a "real" attribute.32 self.key = '_threading_local._localimpl.' + str(id(self))33 # { id(Thread) -> (ref(Thread), thread-local dict) }34 self.dicts = {}35 36 def get_dict(self):37 """Return the dict for the current thread. Raises KeyError if none38 defined."""39 thread = current_thread()40 return self.dicts[id(thread)][1]41 42 def create_dict(self):43 """Create a new dict for the current thread, and return it."""44 localdict = {}45 key = self.key46 thread = current_thread()47 idt = id(thread)48 def local_deleted(_, key=key):49 # When the localimpl is deleted, remove the thread attribute.50 thread = wrthread()51 if thread is not None:52 del thread.__dict__[key]53 def thread_deleted(_, idt=idt):54 # When the thread is deleted, remove the local dict.55 # Note that this is suboptimal if the thread object gets56 # caught in a reference loop. We would like to be called57 # as soon as the OS-level thread ends instead.58 local = wrlocal()59 if local is not None:60 dct = local.dicts.pop(idt)61 wrlocal = ref(self, local_deleted)62 wrthread = ref(thread, thread_deleted)63 thread.__dict__[key] = wrlocal64 self.dicts[idt] = wrthread, localdict65 return localdict66 67 68@contextmanager69def _patch(self):70 impl = object.__getattribute__(self, '_local__impl')71 try:72 dct = impl.get_dict()73 except KeyError:74 dct = impl.create_dict()75 args, kw = impl.localargs76 self.__init__(*args, **kw)77 with impl.locallock:78 object.__setattr__(self, '__dict__', dct)79 yield80 81 82class local:83 __slots__ = '_local__impl', '__dict__'84 85 def __new__(cls, /, *args, **kw):86 if (args or kw) and (cls.__init__ is object.__init__):87 raise TypeError("Initialization arguments are not supported")88 self = object.__new__(cls)89 impl = _localimpl()90 impl.localargs = (args, kw)91 impl.locallock = RLock()92 object.__setattr__(self, '_local__impl', impl)93 # We need to create the thread dict in anticipation of94 # __init__ being called, to make sure we don't call it95 # again ourselves.96 impl.create_dict()97 return self98 99 def __getattribute__(self, name):100 with _patch(self):101 return object.__getattribute__(self, name)102 103 def __setattr__(self, name, value):104 if name == '__dict__':105 raise AttributeError(106 "%r object attribute '__dict__' is read-only"107 % self.__class__.__name__)108 with _patch(self):109 return object.__setattr__(self, name, value)110 111 def __delattr__(self, name):112 if name == '__dict__':113 raise AttributeError(114 "%r object attribute '__dict__' is read-only"115 % self.__class__.__name__)116 with _patch(self):117 return object.__delattr__(self, name)118 119 120from threading import current_thread, RLock121 