Skip to content

Commit 1a0f0f5

Browse files
committed
dynassign: make _DynLiveView a singleton
This lets us get rid of the thread-local observer registry, as well as the destructor of _DynLiveView, since there is now always exactly one observer monitoring _EnvBlock enter/exit events. Concurrent iteration of the singleton's items() has been tested to work correctly. Removing the destructor improves PyPy compatibility, since in PyPy __del__ methods may be called at an arbitrary time later (at next garbage collection time; no reference counting in PyPy's GC), if at all. Also, __del__ methods are not very pythonic. Good riddance! This also gets rid of a mysterious bug involving the REPL server (see comment in the deleted destructor code).
1 parent 2658ede commit 1a0f0f5

File tree

2 files changed

+11
-26
lines changed

2 files changed

+11
-26
lines changed

unpythonic/dynassign.py

Lines changed: 4 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -27,47 +27,25 @@ def _getstack():
2727
_L._stack = _mainthread_stack.copy()
2828
return _L._stack
2929

30-
def _getobservers():
31-
if not hasattr(_L, "_observers"):
32-
_L._observers = {}
33-
return _L._observers
34-
3530
class _EnvBlock(object):
3631
def __init__(self, bindings):
3732
self.bindings = bindings
3833
def __enter__(self):
3934
if self.bindings: # optimization, skip pushing an empty scope
4035
_getstack().append(self.bindings)
41-
for o in _getobservers().values():
42-
o._refresh()
36+
_DynLiveView._refresh()
4337
def __exit__(self, t, v, tb):
4438
if self.bindings:
4539
_getstack().pop()
46-
for o in _getobservers().values():
47-
o._refresh()
40+
_DynLiveView._refresh()
4841

4942
class _DynLiveView(ChainMap):
5043
def __init__(self):
5144
super().__init__(self)
5245
self._refresh()
53-
_getobservers()[id(self)] = self
54-
def __del__(self):
55-
# No idea how, but our REPL server can trigger a KeyError here
56-
# if the user views `help()`, which causes the client to get stuck.
57-
# Then pressing `q` in the server console to quit the help, and then
58-
# asking the REPL client (which is now responsive again) to disconnect
59-
# (Ctrl+D), triggers the `KeyError` when the server cleans up the
60-
# disconnected session.
61-
#
62-
# Anyway, if `id(self)` is not in the current thread's observers,
63-
# we don't need to do anything here, so the Right Thing to do is
64-
# to absorb `KeyError` if it occurs.
65-
try:
66-
del _getobservers()[id(self)]
67-
except KeyError:
68-
pass
6946
def _refresh(self):
7047
self.maps = list(reversed(_getstack())) + [_global_dynvars]
48+
_DynLiveView = _DynLiveView() # singleton
7149

7250
class _Env(object):
7351
"""This module exports a singleton, ``dyn``, which provides dynamic assignment
@@ -211,7 +189,7 @@ def asdict(self):
211189
When new dynamic scopes begin or old ones exit, its ``.maps`` attribute
212190
is automatically updated to reflect the changes.
213191
"""
214-
return _DynLiveView()
192+
return _DynLiveView
215193

216194
def __iter__(self):
217195
return iter(self.asdict())

unpythonic/test/test_dynassign.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,13 @@ def noimplicits(kvs):
130130
assert noimplicits(items) == (("a", 1), ("b", 2),
131131
("im_always_there", True))
132132

133+
# the view is a singleton, so let's test it supports concurrent iteration correctly.
134+
view = dyn.asdict()
135+
i1 = iter(view.items())
136+
i2 = iter(view.items())
137+
for a, b in zip(i1, i2):
138+
assert a == b
139+
133140
print("All tests PASSED")
134141

135142
if __name__ == '__main__':

0 commit comments

Comments
 (0)