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  ...     initialized = False
60  ...     def __init__(self, **kw):
61  ...         if self.initialized:
62  ...             raise SystemError('__init__ called too many times')
63  ...         self.initialized = True
64  ...         self.__dict__.update(kw)
65  ...     def squared(self):
66  ...         return self.number ** 2
67
68This can be useful to support default values, methods and
69initialization.  Note that if you define an __init__ method, it will be
70called each time the local object is used in a separate thread.  This
71is necessary to initialize each thread's dictionary.
72
73Now if we create a local object:
74
75  >>> mydata = MyLocal(color='red')
76
77Now we have a default number:
78
79  >>> mydata.number
80  2
81
82an initial color:
83
84  >>> mydata.color
85  'red'
86  >>> del mydata.color
87
88And a method that operates on the data:
89
90  >>> mydata.squared()
91  4
92
93As before, we can access the data in a separate thread:
94
95  >>> log = []
96  >>> thread = threading.Thread(target=f)
97  >>> thread.start()
98  >>> thread.join()
99  >>> log
100  [[('color', 'red'), ('initialized', True)], 11]
101
102without affecting this thread's data:
103
104  >>> mydata.number
105  2
106  >>> mydata.color
107  Traceback (most recent call last):
108  ...
109  AttributeError: 'MyLocal' object has no attribute 'color'
110
111Note that subclasses can define slots, but they are not thread
112local. They are shared across threads:
113
114  >>> class MyLocal(local):
115  ...     __slots__ = 'number'
116
117  >>> mydata = MyLocal()
118  >>> mydata.number = 42
119  >>> mydata.color = 'red'
120
121So, the separate thread:
122
123  >>> thread = threading.Thread(target=f)
124  >>> thread.start()
125  >>> thread.join()
126
127affects what we see:
128
129  >>> mydata.number
130  11
131
132>>> del mydata
133"""
134
135from weakref import ref
136from contextlib import contextmanager
137
138__all__ = ["local"]
139
140# We need to use objects from the threading module, but the threading
141# module may also want to use our `local` class, if support for locals
142# isn't compiled in to the `thread` module.  This creates potential problems
143# with circular imports.  For that reason, we don't import `threading`
144# until the bottom of this file (a hack sufficient to worm around the
145# potential problems).  Note that all platforms on CPython do have support
146# for locals in the `thread` module, and there is no circular import problem
147# then, so problems introduced by fiddling the order of imports here won't
148# manifest.
149
150class _localimpl:
151    """A class managing thread-local dicts"""
152    __slots__ = 'key', 'dicts', 'localargs', 'locallock', '__weakref__'
153
154    def __init__(self):
155        # The key used in the Thread objects' attribute dicts.
156        # We keep it a string for speed but make it unlikely to clash with
157        # a "real" attribute.
158        self.key = '_threading_local._localimpl.' + str(id(self))
159        # { id(Thread) -> (ref(Thread), thread-local dict) }
160        self.dicts = {}
161
162    def get_dict(self):
163        """Return the dict for the current thread. Raises KeyError if none
164        defined."""
165        thread = current_thread()
166        return self.dicts[id(thread)][1]
167
168    def create_dict(self):
169        """Create a new dict for the current thread, and return it."""
170        localdict = {}
171        key = self.key
172        thread = current_thread()
173        idt = id(thread)
174        def local_deleted(_, key=key):
175            # When the localimpl is deleted, remove the thread attribute.
176            thread = wrthread()
177            if thread is not None:
178                del thread.__dict__[key]
179        def thread_deleted(_, idt=idt):
180            # When the thread is deleted, remove the local dict.
181            # Note that this is suboptimal if the thread object gets
182            # caught in a reference loop. We would like to be called
183            # as soon as the OS-level thread ends instead.
184            local = wrlocal()
185            if local is not None:
186                dct = local.dicts.pop(idt)
187        wrlocal = ref(self, local_deleted)
188        wrthread = ref(thread, thread_deleted)
189        thread.__dict__[key] = wrlocal
190        self.dicts[idt] = wrthread, localdict
191        return localdict
192
193
194@contextmanager
195def _patch(self):
196    impl = object.__getattribute__(self, '_local__impl')
197    try:
198        dct = impl.get_dict()
199    except KeyError:
200        dct = impl.create_dict()
201        args, kw = impl.localargs
202        self.__init__(*args, **kw)
203    with impl.locallock:
204        object.__setattr__(self, '__dict__', dct)
205        yield
206
207
208class local:
209    __slots__ = '_local__impl', '__dict__'
210
211    def __new__(cls, *args, **kw):
212        if (args or kw) and (cls.__init__ is object.__init__):
213            raise TypeError("Initialization arguments are not supported")
214        self = object.__new__(cls)
215        impl = _localimpl()
216        impl.localargs = (args, kw)
217        impl.locallock = RLock()
218        object.__setattr__(self, '_local__impl', impl)
219        # We need to create the thread dict in anticipation of
220        # __init__ being called, to make sure we don't call it
221        # again ourselves.
222        impl.create_dict()
223        return self
224
225    def __getattribute__(self, name):
226        with _patch(self):
227            return object.__getattribute__(self, name)
228
229    def __setattr__(self, name, value):
230        if name == '__dict__':
231            raise AttributeError(
232                "%r object attribute '__dict__' is read-only"
233                % self.__class__.__name__)
234        with _patch(self):
235            return object.__setattr__(self, name, value)
236
237    def __delattr__(self, name):
238        if name == '__dict__':
239            raise AttributeError(
240                "%r object attribute '__dict__' is read-only"
241                % self.__class__.__name__)
242        with _patch(self):
243            return object.__delattr__(self, name)
244
245
246from threading import current_thread, RLock
247