Skip to content

Commit 5033c55

Browse files
authored
Merge pull request #73 from geoadmin/fix-thread-local
PB-1920: Fix AttributeError: '_thread._local' object has no attribute 'data' - #patch
2 parents 629a983 + 34c036d commit 5033c55

File tree

2 files changed

+46
-1
lines changed

2 files changed

+46
-1
lines changed

logging_utilities/context/thread_context.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,30 +14,54 @@ class ThreadMappingContext(BaseContext):
1414

1515
def __init__(self):
1616
self.__local = threading.local()
17-
self.__local.data = {}
17+
self.ensure_data()
18+
19+
def ensure_data(self):
20+
"""Ensure the current thread has a `data` attribute in its local storage.
21+
22+
The `threading.local()` object provides each thread with its own independent attribute
23+
namespace. Attributes created in one thread are not visible to other threads. This means
24+
that even if `data` was initialized in the thread where this object was constructed,
25+
new threads will not automatically have a `data` attribute since the constructor is not
26+
run again.
27+
28+
Calling this method guarantees that `self.__local.data` exists in the *current* thread,
29+
creating an empty dictionary if needed. It must be invoked on every access path
30+
(e.g., __getitem__, __iter__).
31+
"""
32+
if not hasattr(self.__local, 'data'):
33+
self.__local.data = {}
1834

1935
def __str__(self):
36+
self.ensure_data()
2037
return str(self.__local.data)
2138

2239
def __getitem__(self, __key):
40+
self.ensure_data()
2341
return self.__local.data[__key]
2442

2543
def __setitem__(self, __key, __value):
44+
self.ensure_data()
2645
self.__local.data[__key] = __value
2746

2847
def __delitem__(self, __key):
48+
self.ensure_data()
2949
del self.__local.data[__key]
3050

3151
def __len__(self):
52+
self.ensure_data()
3253
return len(self.__local.data)
3354

3455
def __iter__(self):
56+
self.ensure_data()
3557
return self.__local.data.__iter__()
3658

3759
def __contains__(self, __o):
60+
self.ensure_data()
3861
return self.__local.data.__contains__(__o)
3962

4063
def init(self, data=None):
64+
self.ensure_data()
4165
if data is None:
4266
self.__local.data = {}
4367
else:
@@ -46,18 +70,23 @@ def init(self, data=None):
4670
self.__local.data = data
4771

4872
def get(self, key, default=None):
73+
self.ensure_data()
4974
return self.__local.data.get(key, default)
5075

5176
def pop(self, key, default=__marker):
77+
self.ensure_data()
5278
if default == self.__marker:
5379
return self.__local.data.pop(key)
5480
return self.__local.data.pop(key, default)
5581

5682
def set(self, key, value):
83+
self.ensure_data()
5784
self.__local.data[key] = value
5885

5986
def delete(self, key):
87+
self.ensure_data()
6088
del self.__local.data[key]
6189

6290
def clear(self):
91+
self.ensure_data()
6392
self.__local.data = {}

tests/test_logging_context.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import unittest
33
from concurrent.futures import ThreadPoolExecutor
44
from concurrent.futures import as_completed
5+
from threading import Thread
56

67
from logging_utilities.context import get_logging_context
78
from logging_utilities.context import remove_logging_context
@@ -125,6 +126,21 @@ def test_thread_context_str(self):
125126
ctx.init({'a': 1, 'b': 2, 'c': 'my string'})
126127
self.assertEqual(str(ctx), "{'a': 1, 'b': 2, 'c': 'my string'}")
127128

129+
def test_thread_context_local_data(self):
130+
ctx = ThreadMappingContext()
131+
ctx['thread'] = 'main'
132+
results = {}
133+
134+
def worker():
135+
assert 'thread' not in ctx
136+
ctx['thread'] = 'worker'
137+
138+
t = Thread(target=worker)
139+
t.start()
140+
t.join()
141+
142+
assert ctx['thread'] == 'main'
143+
128144

129145
class LoggingContextTest(unittest.TestCase):
130146

0 commit comments

Comments
 (0)