Skip to content

Commit 67a392d

Browse files
committed
dynassign: add support for mutating dynamic bindings
SRFI-39 and Racket's parameterize have this feature, now we do too. Resolves #10.
1 parent 5718511 commit 67a392d

File tree

2 files changed

+101
-15
lines changed

2 files changed

+101
-15
lines changed

unpythonic/dynassign.py

Lines changed: 61 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from collections import ChainMap
88
from collections.abc import Container, Sized, Iterable, Mapping
99

10-
# LEG rule for dynvars: allow a global definition with make_dynvar(a=...)
10+
# LEG rule for dynvars: allow a global definition (shared between threads) with make_dynvar(a=...)
1111
_global_dynvars = {}
1212

1313
_L = threading.local()
@@ -55,15 +55,14 @@ def _refresh(self):
5555
self.maps = list(reversed(_getstack())) + [_global_dynvars]
5656

5757
class _Env(object):
58-
"""This module exports a single object instance, ``dyn``, which provides
59-
dynamic assignment (like Racket's ``parameterize``; akin to Common Lisp's
60-
special variables).
58+
"""This module exports a singleton, ``dyn``, which provides dynamic assignment
59+
(like Racket's ``parameterize``; akin to Common Lisp's special variables.).
6160
6261
For implicitly passing stuff through several layers of function calls,
6362
in cases where a lexical closure is not the right tool for the job
6463
(i.e. when some of the functions are defined elsewhere in the code).
6564
66-
- Dynamic variables are set by ``with dyn.let()``.
65+
- Dynamic variables are introduced by ``with dyn.let()``.
6766
6867
- The created dynamic variables exist while the with block is executing,
6968
and fall out of scope when the with block exits. (I.e. dynamic variables
@@ -77,6 +76,12 @@ class _Env(object):
7776
threads, that can be used to set default values for dynamic variables.
7877
See ``make_dynvar``.
7978
79+
- An existing dynamic variable ``x`` can be mutated by assigning to ``dyn.x``,
80+
or by calling ``dyn.update(x=...)`` (syntax similar to ``let``, for mass updates).
81+
The variable is mutated in the nearest enclosing dynamic scope that has that
82+
name bound. If the name is not bound in any dynamic scope, ``AttributeError``
83+
is raised.
84+
8085
Similar to (parameterize) in Racket.
8186
8287
Example::
@@ -104,26 +109,69 @@ def main():
104109
105110
https://stackoverflow.com/questions/2001138/how-to-create-dynamical-scoped-variables-in-python
106111
"""
107-
def __getattr__(self, name):
108-
# Essentially asdict() and look up, but without creating the ChainMap every time.
112+
def _resolve(self, name):
113+
# Essentially asdict() and look up, but without creating the ChainMap
114+
# every time _resolve() is called.
109115
for scope in reversed(_getstack()):
110116
if name in scope:
111-
return scope[name]
117+
return scope
112118
if name in _global_dynvars: # default value from make_dynvar
113-
return _global_dynvars[name]
119+
return _global_dynvars
114120
raise AttributeError("dynamic variable '{:s}' is not defined".format(name))
115121

122+
def __getattr__(self, name):
123+
"""Read the value of a dynamic binding."""
124+
scope = self._resolve(name)
125+
return scope[name]
126+
127+
def __setattr__(self, name, value):
128+
"""Update an existing dynamic binding.
129+
130+
The update occurs in the closest enclosing dynamic scope that has
131+
``name`` bound.
132+
133+
If the name cannot be found in any dynamic scope, ``AttributeError`` is
134+
raised.
135+
136+
**CAUTION**: Use carefully, if at all. Stealth updates of dynamic
137+
variables defined in enclosing dynamic scopes can destroy readability.
138+
"""
139+
scope = self._resolve(name)
140+
scope[name] = value
141+
116142
def let(self, **bindings):
117143
"""Introduce dynamic bindings.
118144
119145
Context manager; usage is ``with dyn.let(name=value, ...):``
120146
147+
When binding a name that already exists in an enclosing dynamic scope,
148+
the inner binding shadows the enclosing one for the dynamic extent of
149+
the ``with dyn.let``.
150+
151+
This dynamic binding is the main advantage of dynamic assignment,
152+
as opposed to simple global variables.
153+
121154
See ``dyn``.
122155
"""
123156
return _EnvBlock(bindings)
124157

125-
def __setattr__(self, name, value):
126-
raise AttributeError("dynamic variables can only be set using 'with dyn.let()'")
158+
def update(self, **bindings):
159+
"""Mass-update existing dynamic bindings.
160+
161+
For each binding, the update occurs in the closest enclosing dynamic
162+
scope that has a binding with that name.
163+
164+
If at least one of the names cannot be found in any dynamic scope, the
165+
update is canceled (without changes) and ``AttributeError`` is raised.
166+
167+
**CAUTION**: Like ``__setattr__``, but for mass updates, so the same
168+
caution applies. Use carefully, if at all.
169+
"""
170+
# validate, and resolve scopes (let AttributeError propagate)
171+
scopes = {k: self._resolve(k) for k in bindings}
172+
for k, v in bindings.items():
173+
scope = scopes[k]
174+
scope[k] = v
127175

128176
# membership test (in, not in)
129177
def __contains__(self, name):
@@ -165,9 +213,7 @@ def __len__(self):
165213
# subscripting
166214
def __getitem__(self, k):
167215
return getattr(self, k)
168-
169216
def __setitem__(self, k, v):
170-
# writing not supported, but should behave consistently with setattr.
171217
setattr(self, k, v)
172218

173219
# pretty-printing
@@ -176,14 +222,14 @@ def __repr__(self):
176222
return "<dyn object at 0x{:x}: {{{:s}}}>".format(id(self), ", ".join(bindings))
177223

178224
def make_dynvar(**bindings):
179-
"""Create and set default value for dynamic variables.
225+
"""Create a dynamic variable and set its default value.
180226
181227
The default value is used when ``dyn`` is queried for the value outside the
182228
dynamic extent of any ``with dyn.let()`` blocks.
183229
184230
This is convenient for eliminating the need for ``if "x" in dyn``
185231
checks, since the variable will always be there (after the global
186-
definition has been executed).
232+
``make_dynvar`` call has been executed).
187233
188234
The kwargs should be ``name=value`` pairs. Note ``value`` is mandatory,
189235
since the whole point of this function is to assign a value. If you need

unpythonic/test/test_dynassign.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,46 @@ def noimplicits(kvs):
7070
assert noimplicits(dyn.items()) == (("a", 1), ("b", 2))
7171
assert noimplicits(dyn.items()) == ()
7272

73+
# update existing dynamic bindings (by mutation)
74+
D2 = {"c": 3, "d": 4}
75+
with dyn.let(**D):
76+
with dyn.let(**D2):
77+
assert noimplicits(dyn.items()) == (("a", 1), ("b", 2), ("c", 3), ("d", 4))
78+
dyn.c = 23
79+
assert noimplicits(dyn.items()) == (("a", 1), ("b", 2), ("c", 23), ("d", 4))
80+
dyn.a = 42 # update occurs in the nearest enclosing dynamic scope that has the name bound
81+
assert noimplicits(dyn.items()) == (("a", 42), ("b", 2), ("c", 23), ("d", 4))
82+
try:
83+
dyn.e = 5
84+
except AttributeError:
85+
pass
86+
else:
87+
assert False, "trying to update an unbound dynamic variable should be an error"
88+
assert noimplicits(dyn.items()) == (("a", 42), ("b", 2))
89+
assert noimplicits(dyn.items()) == ()
90+
91+
# update in the presence of shadowing
92+
with dyn.let(**D):
93+
with dyn.let(**D):
94+
assert noimplicits(dyn.items()) == (("a", 1), ("b", 2))
95+
dyn.a = 42
96+
assert noimplicits(dyn.items()) == (("a", 42), ("b", 2))
97+
# the inner "a" was updated, the outer one remains untouched
98+
assert noimplicits(dyn.items()) == (("a", 1), ("b", 2))
99+
assert noimplicits(dyn.items()) == ()
100+
101+
# mass update
102+
with dyn.let(**D):
103+
with dyn.let(**D2):
104+
assert noimplicits(dyn.items()) == (("a", 1), ("b", 2), ("c", 3), ("d", 4))
105+
dyn.update(a=-1, b=-2, c=-3, d=-4)
106+
assert noimplicits(dyn.items()) == (("a", -1), ("b", -2), ("c", -3), ("d", -4))
107+
assert noimplicits(dyn.items()) == (("a", -1), ("b", -2))
108+
dyn.update(a=10, b=20)
109+
assert noimplicits(dyn.items()) == (("a", 10), ("b", 20))
110+
assert noimplicits(dyn.items()) == ()
111+
112+
# default values
73113
make_dynvar(im_always_there=True)
74114
with dyn.let(a=1, b=2):
75115
assert noimplicits(dyn.items()) == (("a", 1), ("b", 2),

0 commit comments

Comments
 (0)