Skip to content

Commit 1bb83f8

Browse files
committed
Add ThreadLocalBox and Shim.
ThreadLocalBox is like box, but the contents are thread-local. Shim holds a box (or a ThreadLocalBox), and redirects attribute accesses on the shim to whatever object happens to currently be in the box. (E.g. the combo with ThreadLocalBox can be used to redirect stdin/stdout only in particular threads.)
1 parent 00e7c19 commit 1bb83f8

File tree

2 files changed

+99
-4
lines changed

2 files changed

+99
-4
lines changed

unpythonic/collections.py

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
# -*- coding: utf-8 -*-
22
"""Additional containers and container utilities."""
33

4-
__all__ = ["box", "unbox", "frozendict", "roview", "view", "ShadowedSequence",
4+
__all__ = ["box", "ThreadLocalBox", "unbox", "Shim",
5+
"frozendict", "roview", "view", "ShadowedSequence",
56
"mogrify",
67
"get_abcs", "in_slice", "index_in_slice",
78
"SequenceView", "MutableSequenceView"] # ABCs
@@ -16,6 +17,7 @@
1617
MappingView
1718
from inspect import isclass
1819
from operator import lt, le, ge, gt
20+
import threading
1921

2022
from .llist import cons
2123
from .misc import getattrrec
@@ -201,7 +203,6 @@ def __eq__(self, other):
201203
def set(self, x):
202204
"""Store a new value in the box, replacing the old one.
203205
204-
Syntactic sugar for assigning to the attribute `.x`.
205206
As a convenience, returns the new value.
206207
207208
Since a function call is an expression, you can use this form
@@ -219,6 +220,39 @@ def __lshift__(self, x):
219220
for a `box`, so we just return the new value.)
220221
"""
221222
return self.set(x)
223+
def get(self):
224+
"""Return the value currently in the box.
225+
226+
The syntactic sugar for `b.get()` is `unbox(b)`.
227+
"""
228+
return self.x
229+
230+
# We re-implement instead of making `box` use an `env` as a place
231+
# so that the thread-locality feature is pay-as-you-go (no loss in
232+
# performance for the regular, non-thread-local `box`.)
233+
class ThreadLocalBox(box):
234+
"""Like box, but the store is thread-local."""
235+
def __init__(self, x=None):
236+
self.storage = threading.local()
237+
self.storage.x = x
238+
def __repr__(self):
239+
"""**WARNING**: the repr shows only the content seen by the current thread."""
240+
return "ThreadLocalBox({})".format(repr(self.storage.x))
241+
def __contains__(self, x):
242+
return self.storage.x == x
243+
def __iter__(self):
244+
return (x for x in (self.storage.x,))
245+
def __len(self):
246+
return 1
247+
def __eq__(self, other):
248+
return other == self.storage.x
249+
def set(self, x):
250+
self.storage.x = x
251+
return x
252+
def __lshift__(self, x):
253+
return self.set(x)
254+
def get(self):
255+
return self.storage.x
222256

223257
def unbox(b):
224258
"""Return the value from inside the box b.
@@ -230,7 +264,32 @@ def unbox(b):
230264
"""
231265
if not isinstance(b, box):
232266
raise TypeError("Expected box, got {} with value '{}'".format(type(b), b))
233-
return b.x
267+
return b.get()
268+
269+
class Shim:
270+
"""Attribute access redirector.
271+
272+
Hold a target object inside a box. When an attribute of this object
273+
is accessed (whether to get or set it), redirect that attribute
274+
access to the target currently inside the box.
275+
276+
The point is that the target may be switched at any time, simply by sending
277+
a new value into the box instance you gave to the `Shim` constructor.
278+
279+
Another use case is to combo with `ThreadLocalBox`, e.g. to redirect
280+
stdin/stdout only when used from some specific threads.
281+
"""
282+
def __init__(self, thebox):
283+
"""thebox: a `box` instance that will hold the target."""
284+
self._box = thebox
285+
def __getattr__(self, k):
286+
thing = unbox(self._box)
287+
return getattr(thing, k)
288+
def __setattr__(self, k, v):
289+
if k == "_box":
290+
return super().__setattr__(k, v)
291+
thing = unbox(self._box)
292+
return setattr(thing, k, v)
234293

235294
_the_empty_frozendict = None
236295
class frozendict:

unpythonic/test/test_collections.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22

33
from collections.abc import Mapping, MutableMapping, Hashable, Container, Iterable, Sized
44
from pickle import dumps, loads
5+
import threading
56

6-
from ..collections import box, unbox, frozendict, view, roview, ShadowedSequence, mogrify
7+
from ..collections import box, ThreadLocalBox, Shim, unbox, frozendict, view, roview, ShadowedSequence, mogrify
78

89
def test():
910
# box: mutable single-item container à la Racket
@@ -65,6 +66,41 @@ def f(b):
6566
b2 = loads(dumps(b1)) # pickling
6667
assert b2 == b1
6768

69+
# ThreadLocalBox: like box, but with thread-local contents
70+
tlb = ThreadLocalBox(42)
71+
assert unbox(tlb) == 42
72+
def test_threadlocalbox_worker():
73+
tlb << 17
74+
assert unbox(tlb) == 17
75+
t = threading.Thread(target=test_threadlocalbox_worker)
76+
t.start()
77+
t.join()
78+
assert unbox(tlb) == 42 # In the main thread, this box still has the original value.
79+
80+
# Shim: redirect attribute accesses.
81+
#
82+
# The shim holds a box. Attribute accesses on the shim are redirected
83+
# to whatever object currently happens to be inside the box.
84+
class TestTarget:
85+
def __init__(self, x):
86+
self.x = x
87+
def getme(self):
88+
return self.x
89+
b5 = box(TestTarget(21))
90+
s = Shim(b5) # This is modular so we could use a ThreadLocalBox just as well.
91+
assert hasattr(s, "x")
92+
assert hasattr(s, "getme")
93+
assert s.x == 21
94+
assert s.getme() == 21
95+
s.y = "hi from injected attribute" # We can also add or rebind attributes through the shim.
96+
assert unbox(b5).y == "hi from injected attribute"
97+
s.y = "hi again"
98+
assert unbox(b5).y == "hi again"
99+
b5 << TestTarget(42) # After we send a different object into the box held by the shim...
100+
assert s.x == 42 # ...the shim accesses the new object.
101+
assert s.getme() == 42
102+
assert not hasattr(s, "y") # The new TestTarget instance doesn't have "y".
103+
68104
# frozendict: like frozenset, but for dictionaries
69105
d3 = frozendict({'a': 1, 'b': 2})
70106
assert d3['a'] == 1

0 commit comments

Comments
 (0)