Welcome, guest | Sign In | My Account | Store | Cart

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:

  1. 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.

  2. 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.

  3. 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.

Python, 195 lines
  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))

1 comment

Kevin Edwards 11 years, 10 months ago  # | flag

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.