1"""Thread-local objects.
2
3(Note that this module provides a Python version of the threading.local
4 class.  Depending on the version of Python you're using, there may be a
5 faster one available.  You should always import the `local` class from
6 `threading`.)
7
8Thread-local objects support the management of thread-local data.
9If you have data that you want to be local to a thread, simply create
10a thread-local object and use its attributes:
11
12  >>> mydata = local()
13  >>> mydata.number = 42
14  >>> mydata.number
15  42
16
17You can also access the local-object's dictionary:
18
19  >>> mydata.__dict__
20  {'number': 42}
21  >>> mydata.__dict__.setdefault('widgets', [])
22  []
23  >>> mydata.widgets
24  []
25
26What's important about thread-local objects is that their data are
27local to a thread. If we access the data in a different thread:
28
29  >>> log = []
30  >>> def f():
31  ...     items = sorted(mydata.__dict__.items())
32  ...     log.append(items)
33  ...     mydata.number = 11
34  ...     log.append(mydata.number)
35
36  >>> import threading
37  >>> thread = threading.Thread(target=f)
38  >>> thread.start()
39  >>> thread.join()
40  >>> log
41  [[], 11]
42
43we get different data.  Furthermore, changes made in the other thread
44don't affect data seen in this thread:
45
46  >>> mydata.number
47  42
48
49Of course, values you get from a local object, including a __dict__
50attribute, are for whatever thread was current at the time the
51attribute was read.  For that reason, you generally don't want to save
52these values across threads, as they apply only to the thread they
53came from.
54
55You can create custom local objects by subclassing the local class:
56
57  >>> class MyLocal(local):
58  ...     number = 2
59  ...     def __init__(self, /, **kw):
60  ...         self.__dict__.update(kw)
61  ...     def squared(self):
62  ...         return self.number ** 2
63
64This can be useful to support default values, methods and
65initialization.  Note that if you define an __init__ method, it will be
66called each time the local object is used in a separate thread.  This
67is necessary to initialize each thread's dictionary.
68
69Now if we create a local object:
70
71  >>> mydata = MyLocal(color='red')
72
73Now we have a default number:
74
75  >>> mydata.number
76  2
77
78an initial color:
79
80  >>> mydata.color
81  'red'
82  >>> del mydata.color
83
84And a method that operates on the data:
85
86  >>> mydata.squared()
87  4
88
89As before, we can access the data in a separate thread:
90
91  >>> log = []
92  >>> thread = threading.Thread(target=f)
93  >>> thread.start()
94  >>> thread.join()
95  >>> log
96  [[('color', 'red')], 11]
97
98without affecting this thread's data:
99
100  >>> mydata.number
101  2
102  >>> mydata.color
103  Traceback (most recent call last):
104  ...
105  AttributeError: 'MyLocal' object has no attribute 'color'
106
107Note that subclasses can define slots, but they are not thread
108local. They are shared across threads:
109
110  >>> class MyLocal(local):
111  ...     __slots__ = 'number'
112
113  >>> mydata = MyLocal()
114  >>> mydata.number = 42
115  >>> mydata.color = 'red'
116
117So, the separate thread:
118
119  >>> thread = threading.Thread(target=f)
120  >>> thread.start()
121  >>> thread.join()
122
123affects what we see:
124
125  >>> mydata.number
126  11
127
128>>> del mydata
129"""
130
131from weakref import ref
132from contextlib import contextmanager
133
134__all__ = ["local"]
135
136# We need to use objects from the threading module, but the threading
137# module may also want to use our `local` class, if support for locals
138# isn't compiled in to the `thread` module.  This creates potential problems
139# with circular imports.  For that reason, we don't import `threading`
140# until the bottom of this file (a hack sufficient to worm around the
141# potential problems).  Note that all platforms on CPython do have support
142# for locals in the `thread` module, and there is no circular import problem
143# then, so problems introduced by fiddling the order of imports here won't
144# manifest.
145
146class _localimpl:
147    """A class managing thread-local dicts"""
148    __slots__ = 'key', 'dicts', 'localargs', 'locallock', '__weakref__'
149
150    def __init__(self):
151        # The key used in the Thread objects' attribute dicts.
152        # We keep it a string for speed but make it unlikely to clash with
153        # a "real" attribute.
154        self.key = '_threading_local._localimpl.' + str(id(self))
155        # { id(Thread) -> (ref(Thread), thread-local dict) }
156        self.dicts = {}
157
158    def get_dict(self):
159        """Return the dict for the current thread. Raises KeyError if none
160        defined."""
161        thread = current_thread()
162        return self.dicts[id(thread)][1]
163
164    def create_dict(self):
165        """Create a new dict for the current thread, and return it."""
166        localdict = {}
167        key = self.key
168        thread = current_thread()
169        idt = id(thread)
170        def local_deleted(_, key=key):
171            # When the localimpl is deleted, remove the thread attribute.
172            thread = wrthread()
173            if thread is not None:
174                del thread.__dict__[key]
175        def thread_deleted(_, idt=idt):
176            # When the thread is deleted, remove the local dict.
177            # Note that this is suboptimal if the thread object gets
178            # caught in a reference loop. We would like to be called
179            # as soon as the OS-level thread ends instead.
180            local = wrlocal()
181            if local is not None:
182                dct = local.dicts.pop(idt)
183        wrlocal = ref(self, local_deleted)
184        wrthread = ref(thread, thread_deleted)
185        thread.__dict__[key] = wrlocal
186        self.dicts[idt] = wrthread, localdict
187        return localdict
188
189
190@contextmanager
191def _patch(self):
192    impl = object.__getattribute__(self, '_local__impl')
193    try:
194        dct = impl.get_dict()
195    except KeyError:
196        dct = impl.create_dict()
197        args, kw = impl.localargs
198        self.__init__(*args, **kw)
199    with impl.locallock:
200        object.__setattr__(self, '__dict__', dct)
201        yield
202
203
204class local:
205    __slots__ = '_local__impl', '__dict__'
206
207    def __new__(cls, /, *args, **kw):
208        if (args or kw) and (cls.__init__ is object.__init__):
209            raise TypeError("Initialization arguments are not supported")
210        self = object.__new__(cls)
211        impl = _localimpl()
212        impl.localargs = (args, kw)
213        impl.locallock = RLock()
214        object.__setattr__(self, '_local__impl', impl)
215        # We need to create the thread dict in anticipation of
216        # __init__ being called, to make sure we don't call it
217        # again ourselves.
218        impl.create_dict()
219        return self
220
221    def __getattribute__(self, name):
222        with _patch(self):
223            return object.__getattribute__(self, name)
224
225    def __setattr__(self, name, value):
226        if name == '__dict__':
227            raise AttributeError(
228                "%r object attribute '__dict__' is read-only"
229                % self.__class__.__name__)
230        with _patch(self):
231            return object.__setattr__(self, name, value)
232
233    def __delattr__(self, name):
234        if name == '__dict__':
235            raise AttributeError(
236                "%r object attribute '__dict__' is read-only"
237                % self.__class__.__name__)
238        with _patch(self):
239            return object.__delattr__(self, name)
240
241
242from threading import current_thread, RLock
243