The environment variables of processes get inherited by their children who can modify them and pass them on to their children. The tree of processes is similar to the call tree of a running Python process, but a similar mechanism like the environment variables is missing. I'm offering a solution for this; each stack frame takes the place of a process, hence I call it StackEnv. It comes in handy if you want to pass information from one frame to a frame far below without patching all the calls in between to pass the data (which might belong to a framework you don't want to change).
Usecases:
You call a framework which calls back your code before returning (e. g. in passed objects you provide). You want to pass some information to your code without relying on global variables or similar constructs which weren't thread-safe nor re-entrant.
You want to pass pseudo-global but in fact situation-related information (e. g. the verbosity based on the situation the call comes from) without handing the information down in each and every call which can happen below yours.
You want to give called methods the option to override the decision of the caller method regarding this information. (E. g. the caller decides that verbosity should be True, but the called method then calls another method and decides that the verbosity in this case should be False.)
Alike processes, called frames cannot influence the values for calling frames (but of course mutable values can be used to pass information upwards; this normal kind of "abuse" isn't prevented).
Importing:
from stackEnv import stackEnv
Setting a stackEnv variable:
stackEnv.verbose = True
Using the value of a stackEnv variable:
if stackEnv.verbose: print "being verbose now"
Testing a stackEnv variable:
if 'verbose' in stackEnv: print "having information on verbosity"
Overriding a stackEnv variable's value for the rest of this frame and its calls:
stackEnv.verbose = False
Removing a stackEnv variable for the rest of this frame and its calls:
del stackEnv.verbose
Some more useful API of this class can be found in the unit test included.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 | #!/usr/bin/env python
"""
stackEnv -- a module providing a stack-based environment to automatically
transfer some arguments (stackEnv) to called functions without
passing them explicitly in the call, thus forming a kind of
hierarchical environment as known from Unix processes
"""
import sys
import unittest
### internals
##############################
_PREFIX = '__STACKENV:'
def _internalName(name):
"internally used name for the stackEnv variable of the given name"
return _PREFIX + name
def _internalDelMarker(name):
"""
internally used name to marker that the stackEnv variable of the
given name is deleted below this frame
"""
return _PREFIX + '!' + name
### low level
##############################
def setStackEnv(frame, name, value):
"""
set the stackEnv variable of the given name to the given value
in the given frame
"""
frame.f_locals[_internalName(name)] = value
delMarker = _internalDelMarker(name)
if delMarker in frame.f_locals:
del frame.f_locals[delMarker]
def delStackEnv(frame, name):
"""
mark the stackEnv variable of the given name as deleted below the
given frame
"""
frame.f_locals[_internalDelMarker(name)] = True
class NoSuchStackEnv(Exception):
"""
Exception stating that the a stackEnv variable for the given name
does not exist
"""
pass
def getStackEnv(frame, name):
"""
return the stackEnv variable value of the given name
"""
internalName = _internalName(name)
delMarker = _internalDelMarker(name)
walker = frame
while walker:
if delMarker in walker.f_locals: # explicitly deleted in this frame?
raise NoSuchStackEnv(name)
try:
return walker.f_locals[internalName]
except KeyError: # not in this frame
walker = walker.f_back
# not found anywhere in the frames above
raise NoSuchStackEnv(name)
def yieldAllStackEnvItems(frame):
"yield all stackEnv variable names and values in use above the given frame"
found = set([])
walker = frame
while walker:
for name, value in walker.f_locals.iteritems():
if name.startswith(_PREFIX):
name = name[len(_PREFIX):] # strip off prefix
if name.startswith('!'): # del marker?
found.add(name[1:]) # just store it (w/o '!') as found
elif name not in found: # new name?
yield name, value
found.add(name)
walker = walker.f_back
### high level
##############################
def setStackEnvs(**kwargs):
"set a bunch of stackEnv variables in one step via kwargs"
backFrame = sys._getframe().f_back
for name, value in kwargs.iteritems():
setStackEnv(backFrame, name, value)
class StackEnv(dict):
"""
The singleton instance of this class is the main interface to the stackEnv.
See the unit test below for example usage (which is as simple as possible).
"""
def __str__(self):
return '{ %s }' % ', '.join('%r: %r' % item for item in self.iteritems())
def __repr__(self):
return '{ %s }' % ', '.join('%r: %r' % item for item in self.iteritems())
def iteritems(self):
return yieldAllStackEnvItems(sys._getframe().f_back)
def items(self):
return [ item for item in self.iteritems() ]
def __iter__(self):
for name, value in yieldAllStackEnvItems(sys._getframe().f_back):
yield name
def __setattr__(self, name, value):
setStackEnv(sys._getframe().f_back, name, value)
# TODO: def __iadd__() etc.
def __delattr__(self, name):
delStackEnv(sys._getframe().f_back, name)
def __getattr__(self, name):
return getStackEnv(sys._getframe().f_back, name)
def __contains__(self, name):
try:
getStackEnv(sys._getframe().f_back, name)
return True
except NoSuchStackEnv:
return False
def clear(self):
# collect the names first to prevent changes during iteration:
names = [ name
for name, value in yieldAllStackEnvItems(sys._getframe().f_back) ]
for name in names:
delStackEnv(sys._getframe().f_back, name)
stackEnv = StackEnv()
### Unit tests
###########################################
class Test(unittest.TestCase):
def runTest(self):
def b():
self.assertEqual(42, stackEnv.a)
self.assertFalse('b' in stackEnv)
stackEnv.a = 43
stackEnv.b = 24
self.assertEqual(43, stackEnv.a)
self.assertTrue('b' in stackEnv)
self.assertTrue(24, stackEnv.b)
def a():
self.assertTrue(41, stackEnv.a)
self.assertTrue('b' in stackEnv)
self.assertEqual(23, stackEnv.b)
stackEnv.a = 42
del stackEnv.b
self.assertEqual(42, stackEnv.a)
self.assertFalse('b' in stackEnv)
b()
self.assertEqual(42, stackEnv.a)
self.assertFalse('b' in stackEnv)
for version in ('short', 'long'):
if version == 'long':
stackEnv.a = 41
stackEnv.b = 23
elif version == 'short':
setStackEnvs(a=41, b=23)
self.assertEqual(41, stackEnv.a)
self.assertTrue('b' in stackEnv)
self.assertEqual(23, stackEnv.b)
a()
self.assertEqual(41, stackEnv.a)
self.assertTrue('b' in stackEnv)
self.assertEqual(23, stackEnv.b)
stackEnv.clear()
self.assertFalse('a' in stackEnv)
self.assertFalse('b' in stackEnv)
def main(argv):
unittest.main(argv=argv)
return 0
if __name__ == '__main__':
sys.exit(main(sys.argv))
|
Neat! I haven't played with yours yet, but it reminds me of PJE's Contextual or Crosscuts (which walks the stack like you are doing). Thanks for sharing, Alfe.