0001"""
0002A dynamic-scope-like system, aka fluid variables.
0003
0004The idea behind dynamic scoped variables is for when, at one level,
0005you want to change the behavior of something you call. Except you
0006can't pass in any new arguments (e.g., there's some function or object
0007inbetween you and the thing you want to change), or you can't predict
0008exactly what you will want to change.
0009
0010You should use it like::
0011
0012 context = Context()
0013
0014 def do_stuff():
0015 state = context.set(inside='do_stuff')
0016 try:
0017 do stuff...
0018 finally:
0019 state.restore()
0020
0021Then ``context.inside`` will be set to ``'do_stuff'`` inside that try
0022block. If a value isn't set, you'll get an attribute error.
0023
0024Note that all values are thread local; this means you cannot use a
0025context object to pass information to another thread. In a
0026single-thread environment it doesn't really matter.
0027
0028Typically you will create ``Context`` instances for your application,
0029environment, etc. These should be global module-level variables, that
0030may be imported by any interested module; each instance is a namespace
0031of its own.
0032
0033Sometimes it's nice to have default values, instead of getting
0034attribute errors. This makes it easier to put in new variables that
0035are intended to be used elsewhere, without having to use
0036``getattr(context, 'var', default)`` to avoid AttributeErrors. There
0037are two ways (that can be used together) to do this.
0038
0039First, when instantiating a ``Context`` object, you can give it a
0040``default`` value. If given, then all variables will default to that
0041value. ``None`` is a typical value for that.
0042
0043Another is ``context.set_default(**vars)``, which will set only those
0044variables to default values. This will not effect the stack of
0045scopes, but will only add defaults.
0046
0047
0048When Python 2.5 comes out, this syntax would certainly be useful::
0049
0050 with context(page='view'):
0051 do stuff...
0052
0053And ``page`` will be set to ``'view'`` only inside that ``with``
0054block.
0055"""
0056
0057from formencode.util import threadinglocal
0058from itertools import count
0059
0060__all__ = ['Context', 'ContextRestoreError']
0061
0062_restore_ids = count()
0063
0064class _NoDefault:
0065 pass
0066
0067class ContextRestoreError(Exception):
0068 """
0069 Raised when something is restored out-of-order.
0070 """
0071
0072class Context(object):
0073
0074 def __init__(self, default=_NoDefault):
0075 self.__dict__['_local'] = threadinglocal.local()
0076 self.__dict__['_default'] = default
0077
0078 def __getattr__(self, attr):
0079 if attr.startswith('_'):
0080 raise AttributeError
0081 try:
0082 stack = self._local.stack
0083 except AttributeError:
0084 stack = []
0085 for i in range(len(stack)-1, -1, -1):
0086 if attr in stack[i][0]:
0087 return stack[i][0][attr]
0088 if self._default is _NoDefault:
0089 raise AttributeError(
0090 "The attribute %s has not been set on %r"
0091 % (attr, self))
0092 return self._default
0093
0094 def __setattr__(self, attr, value):
0095 raise AttributeError(
0096 "You can only write attribute on context object with the .set() method")
0097
0098 def set(self, **kw):
0099 state_id = _restore_ids.next()
0100 try:
0101 stack = self._local.stack
0102 except AttributeError:
0103 stack = self._local.stack = [({}, -1)]
0104 restorer = RestoreState(self, state_id)
0105 stack.append((kw, state_id))
0106 return restorer
0107
0108 def _restore(self, state_id):
0109 try:
0110 stack = self._local.stack
0111 except AttributeError:
0112 raise ContextRestoreError(
0113 "Tried to restore context %r (to state ID %s) but no variables have been set in context"
0114 % (self, state_id))
0115 if stack[-1][1] == -1:
0116 raise ContextRestoreError(
0117 "Out of order restoration of context %r (to state ID %s); the stack state is empty"
0118 % (self, state_id))
0119 if stack[-1][1] != state_id:
0120 raise ContextRestoreError(
0121 "Out of order restoration of context %r (to state ID %s) when last state is %s"
0122 % (self, state_id, stack[-1][1]))
0123 stack.pop()
0124
0125 def set_default(self, **kw):
0126 try:
0127 stack = self._local.stack
0128 except AttributeError:
0129 stack = self._local.stack = [({}, -1)]
0130 stack[0][0].update(kw)
0131
0132 def __repr__(self):
0133 try:
0134 stack = self._local.stack
0135 except AttributeError:
0136 stack = []
0137 myid = hex(abs(id(self)))[2:]
0138 if not stack:
0139 return '<%s %s (empty)>' % (self.__class__.__name__, myid)
0140 cur = {}
0141 for vars, state_id in stack:
0142 cur.update(vars)
0143 keys = cur.keys()
0144 keys.sort()
0145 varlist = []
0146 for key in keys:
0147 rep = repr(cur[key])
0148 if len(rep) > 10:
0149 rep = rep[:9]+'...'+rep[-1]
0150 varlist.append('%s=%s' % (key, rep))
0151 return '<%s %s %s>' % (
0152 self.__class__.__name__, myid, ' '.join(varlist))
0153
0154class RestoreState(object):
0155
0156 def __init__(self, context, state_id):
0157 self.state_id = state_id
0158 self.context = context
0159 self.restored = False
0160
0161 def restore(self):
0162 if self.restored:
0163
0164 return
0165 self.context._restore(self.state_id)
0166 self.restored = True