77from collections import ChainMap
88from 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
5757class _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
178224def 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
0 commit comments