Skip to content

Commit f6c46c5

Browse files
committed
enh: dyn.asdict live view, now done properly
1 parent 94d0be4 commit f6c46c5

2 files changed

Lines changed: 48 additions & 24 deletions

File tree

unpythonic/dynassign.py

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,30 @@ def _getstack():
3131
_L._stack = _L.default_stack.copy() # copy main thread's current stack
3232
return _L._stack
3333

34+
_observers = {}
3435
class _EnvBlock(object):
3536
def __init__(self, bindings):
3637
self.bindings = bindings
3738
def __enter__(self):
3839
if self.bindings: # optimization, skip pushing an empty scope
3940
_getstack().append(self.bindings)
41+
for o in _observers.values():
42+
o._refresh()
4043
def __exit__(self, t, v, tb):
4144
if self.bindings:
4245
_getstack().pop()
46+
for o in _observers.values():
47+
o._refresh()
48+
49+
class _DynLiveView(ChainMap):
50+
def __init__(self):
51+
super().__init__(self)
52+
self._refresh()
53+
_observers[id(self)] = self
54+
def __del__(self):
55+
del _observers[id(self)]
56+
def _refresh(self):
57+
self.maps = list(reversed(_getstack())) + [_global_dynvars]
4358

4459
class _Env(object):
4560
"""This module exports a single object instance, ``dyn``, which provides
@@ -87,7 +102,7 @@ def main():
87102
88103
main()
89104
90-
Based on StackOverflow answer by Jason Orendorff (2010).
105+
Initial version of this was based on a StackOverflow answer by Jason Orendorff (2010).
91106
92107
https://stackoverflow.com/questions/2001138/how-to-create-dynamical-scoped-variables-in-python
93108
"""
@@ -122,21 +137,21 @@ def __contains__(self, name):
122137

123138
# iteration
124139
def asdict(self):
125-
"""Return a view as a collections.ChainMap.
140+
"""Return a view of dyn as a ``collections.ChainMap``.
126141
127-
Note it will not detect any new scopes (or when old ones exit), because
128-
the list of mappings the view uses is baked in when ``asdict()`` is called.
142+
When new dynamic scopes begin or old ones exit, its ``.maps`` attribute
143+
is automatically updated to reflect the changes.
129144
"""
130-
return ChainMap(*(list(reversed(_getstack())) + [_global_dynvars]))
131-
132-
def __iter__(self):
133-
return iter(self.asdict())
134-
# no __next__, iterating over dict.
145+
return _DynLiveView()
135146

136147
def items(self):
137148
"""Abbreviation for asdict().items()."""
138149
return self.asdict().items()
139150

151+
def __iter__(self):
152+
return iter(self.asdict())
153+
# no __next__, iterating over dict.
154+
140155
def __len__(self):
141156
return len(self.asdict())
142157

unpythonic/test/test_dynassign.py

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -54,32 +54,41 @@ def threadtest(q):
5454
assert False
5555
runtest()
5656

57+
# various parts of unpythonic use dynvars, so get what's there before we insert anything for testing
58+
implicits = [k for k in dyn]
59+
def noimplicits(kvs):
60+
return tuple(sorted((k, v) for k, v in kvs if k not in implicits))
5761
D = {"a": 1, "b": 2}
58-
# various parts of unpythonic use dynvars, so get what's there before we insert anything
59-
implicits = [k for k, v in dyn.items()] # TODO: add a .keys() method?
60-
def noimplicits(dic):
61-
return {k: dic[k] for k in dic if k not in implicits}
6262
with dyn.let(**D):
6363
# membership test
6464
assert "a" in dyn
6565
assert "c" not in dyn
66-
# subscript syntax as an alternative way to refer to items
66+
67+
# subscript syntax as an alternative notation to refer to dynamic vars
6768
assert dyn.a is dyn["a"]
6869

69-
# iteration works like in a dictionary
70-
assert tuple(sorted(k for k in noimplicits(dyn))) == ("a", "b")
70+
assert noimplicits(dyn.items()) == (("a", 1), ("b", 2))
71+
assert noimplicits(dyn.items()) == ()
7172

72-
# items() gives a snapshot, with values read at the time it was called
73-
assert tuple(sorted(noimplicits(dyn).items())) == (("a", 1), ("b", 2))
73+
make_dynvar(im_always_there=True)
74+
with dyn.let(a=1, b=2):
75+
assert noimplicits(dyn.items()) == (("a", 1), ("b", 2),
76+
("im_always_there", True))
77+
assert noimplicits(dyn.items()) == (("im_always_there", True),)
7478

75-
# safer (TOCTTOU) in complex situations to iterate over keys and retrieve the current dyn[k]
76-
assert tuple(sorted({k: dyn[k] for k in noimplicits(dyn)}.items())) == (("a", 1), ("b", 2))
79+
# dyn.asdict() returns a live view, which is essentially a collections.ChainMap
80+
view = dyn.asdict()
81+
assert noimplicits(view.items()) == (("im_always_there", True),)
82+
with dyn.let(a=1, b=2):
83+
assert noimplicits(view.items()) == (("a", 1), ("b", 2),
84+
("im_always_there", True))
7785

78-
make_dynvar(im_always_there=True)
86+
# as does dyn.items() (it's an abbreviation for dyn.asdict().items())
87+
items = dyn.items()
88+
assert noimplicits(items) == (("im_always_there", True),)
7989
with dyn.let(a=1, b=2):
80-
assert tuple(sorted(noimplicits(dyn).items())) == (("a", 1), ("b", 2),
81-
("im_always_there", True))
82-
assert tuple(sorted(noimplicits(dyn).items())) == (("im_always_there", True),)
90+
assert noimplicits(items) == (("a", 1), ("b", 2),
91+
("im_always_there", True))
8392

8493
print("All tests PASSED")
8594

0 commit comments

Comments
 (0)