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            # @@: Should this really be allowed?
0164            return
0165        self.context._restore(self.state_id)
0166        self.restored = True