[Subversion] / Contextual / context_tests.txt  

View of /Contextual/context_tests.txt

Parent Directory | Revision Log
Revision: 2511 - (download)
Sun Mar 9 17:58:19 2008 UTC (16 years, 1 month ago) by pje
File size: 31968 byte(s)
Bug fixes for state initialization and registry wildcard lookups.
==========================
``peak.context`` Internals
==========================

    >>> from peak.context import State, lookup, setting, registry, wildcard
    >>> from peak import context


Thread testing utility::

    >>> def run_in_thread(func):
    ...     from threading import Thread
    ...     t = Thread(target = func)
    ...     t.start()
    ...     t.join()


State Objects
=============

Separate, ongoing states exist for each thread, with the default state for
each thread being a child of ``State.root``::

    >>> State.get()
    <...State object at ...>

    >>> State.get() is State.get()
    True

    >>> State.parent is State.root
    True

    >>> my_state = State.get()
    >>> def test_other_thread():
    ...     print my_state is State.get()
    ...     print State.parent is State.root
    >>> run_in_thread(test_other_thread)
    False
    True

States allow setting and getting rules for values::

    >>> s = State()
    >>> v = setting(lambda value=42: value)
    >>> s[v] = 2
    >>> s[v] = 3

But rules can't be changed once they've been used/read::

    >>> s[v]
    3

    >>> s[v] = 3
    >>> s[v] = 4
    Traceback (most recent call last):
      ...
    InputConflict: (<lambda>, 3, 4)

``new()`` and ``empty()`` return new child and root states, respectively::

    >>> context.new() is not context.new()
    True
    >>> context.new().parent is State.get()
    True
    >>> context.empty() is not context.empty()
    True
    >>> context.empty().parent is State.root
    True
    >>> isinstance(context.new(), State)
    True
    >>> isinstance(context.empty(), State)
    True


Settings
========

"Value" settings can be created from functions returning default values::

    >>> def a_setting(value=42): return value
    >>> a_setting = setting(a_setting)

If a rule isn't established for a setting in a state, its default value is
used::

    >>> s[a_setting]
    42

And of course it can't be changed except to the same value afterward::

    >>> s[a_setting] = 42
    >>> s[a_setting] = 43
    Traceback (most recent call last):
      ...
    InputConflict: (...a_setting..., 42, 43)


Child States and Setting Inheritance
====================================

States can have child states, that inherit their rules' settings::

    >>> parent = State()
    >>> child = parent.child()
    >>> child.parent is parent
    True

    >>> parent[a_setting] = 99
    >>> child[a_setting]
    99

States can be swapped in and out of current-ness::

    >>> original = State.get()
    >>> old = parent.swap()
    >>> old is original
    True
    >>> State.get() is parent
    True
    >>> old.swap() is parent
    True
    >>> State.get() is old
    True

Calling a setting returns its value in the current state::

    >>> a_setting()
    42

    >>> old = child.swap()
    >>> a_setting()
    99


Scoped States (using __enter__ and __exit__)
============================================

States have ``__enter__`` and ``__exit__`` methods so they can be used as
context managers for "with" statements.  Once these methods are used, the
state can no longer be used for any other purpose.  A strict nesting of states
being entered or exited is also enforced; a state may have no more than one
entered state nested underneath it.  (This nesting is dynamic and has nothing
to do with parent/child relationships between the states.)

Many possible error conditions are checked.  For example, you can't __exit__
a state that hasn't been entered yet::

    >>> s = State()
    >>> old = State.get()

    >>> s.__exit__(None, None, None)
    Traceback (most recent call last):
      ...
    ScopeError: State hasn't been entered yet

    >>> State.get() is old
    True

Enter an inactive state that already has a nested state under it::

    >>> s.__enter__() is s
    True

    >>> s is State.get()
    True

    >>> old.__enter__()
    Traceback (most recent call last):
      ...
    ScopeError: State already has an active child

Re-enter a previously-entered state:::

    >>> s.__enter__()
    Traceback (most recent call last):
      ...
    ScopeError: Can't re-enter a previously-entered state

Exit a state that still has an active nested state::

    >>> sub = s.child()
    >>> sub.__enter__() is sub
    True

    >>> s.__exit__(None, None, None)
    Traceback (most recent call last):
      ...
    ScopeError: Nested state(s) haven't exited yet

Or enter a state when the current state already has a nested state::

    >>> s.swap() is sub
    True

    >>> sub2 = State()
    >>> sub2.__enter__()
    Traceback (most recent call last):
      ...
    ScopeError: Current state already has an active child

    >>> sub.swap() is s
    True

And note that a state's contents are deleted when it's exited::

    >>> def dummy(): pass
    >>> def on_delete(wr):
    ...     print "dummy deleted"
    >>> import weakref
    >>> aRef = weakref.ref(dummy, on_delete)
    >>> dlist = [dummy]
    >>> def aKey(expr=lambda:None): return expr()
    >>> aKey = setting(aKey)
    >>> sub[aKey] = dlist.pop
    >>> lookup(aKey) is dummy
    True
    >>> dlist
    []
    >>> del dummy

    >>> sub.__exit__(None, None, None)
    dummy deleted

You can register functions that will be called back when a state is exited,
but not if the state has already exited::

    >>> def atexit(typ, val, tb):
    ...     print "exiting", typ, val, tb

    >>> sub.on_exit(atexit)
    Traceback (most recent call last):
      ...
    ScopeError: State already exited

Or hasn't been entered yet::

    >>> State.get() is s
    True

    >>> sub2.on_exit(atexit)
    Traceback (most recent call last):
      ...
    ScopeError: State hasn't been entered yet

But once it has been entered, you can register the same function as many times
as you like, but it's only run once  (XXX run-once guarantee isn't thread-safe
yet)::

    >>> sub2.__enter__() is sub2
    True

    >>> sub2.on_exit(atexit)
    >>> sub2.on_exit(atexit)    # register twice, only run once

And the callback is run at exit time, passing the same values::

    >>> sub2.__exit__(None, None, None)
    exiting None None None

    >>> old.swap() is s
    True

Of course, you can't exit a state that isn't the current state::

    >>> s.__exit__(None, None, None)
    Traceback (most recent call last):
      ...
    ScopeError: Can't exit a non-current state

And exit callbacks are not allowed to perform any dynamic lookups, since the
state has already been cleared.::

    >>> s.swap() is old
    True

    >>> def try_a_lookup(typ, val, tb):
    ...     lookup(aKey)    # this will fail, because scope is closed
    >>> def print_error(typ, val, tb):
    ...     import traceback
    ...     print '-->', ''.join(traceback.format_exception(typ, val, tb)), '<--'

    >>> s.on_exit(try_a_lookup)
    >>> s.on_exit(print_error)

    >>> s.__exit__(None, None, None)
    --> Traceback (most recent call last):
      ...
    DynamicRuleError: ('default rule or exit function tried to read dynamic state', ...aKey...)
    <--

But the exit operation will be succeed, anyway, no matter how many errors
take place during the callbacks::

    >>> State.get() is old
    True

And once exited, you can't exit again, of course::

    >>> s.__exit__(None, None, None)
    Traceback (most recent call last):
      ...
    ScopeError: State already exited

Nor can you ever make that state active again, either by switching to it or
re-entering it::

    >>> s.swap()
    Traceback (most recent call last):
      ...
    ScopeError: Can't switch to an exited state

    >>> State.get() is old
    True

    >>> s.__enter__()
    Traceback (most recent call last):
      ...
    ScopeError: Can't re-enter a previously-entered state

Nor can you enter a state that is already the current state::

    >>> State.get() is old
    True

    >>> old.__enter__()
    Traceback (most recent call last):
      ...
    ScopeError: State is already current

There is a root state, that is the parent of every empty state::

    >>> State().parent is State.root
    True

But it cannot be switched to, entered, or exited::

    >>> State.root.swap()
    Traceback (most recent call last):
      ...
    NotImplementedError: Can't switch to the root state

    >>> State.root.__enter__()
    Traceback (most recent call last):
      ...
    NotImplementedError: Can't enter the root state

    >>> State.root.__exit__(None, None, None)
    Traceback (most recent call last):
      ...
    NotImplementedError: Can't exit the root state


Expressions and ``lookup``
=========================

"Expression" settings can be created from functions returning values::

    >>> def a_param(expr=lambda:42): return expr()
    >>> a_param = setting(a_param)

    >>> a_param()
    42

    >>> old = parent.child().swap()
    >>> State[a_param] = object
    >>> a_param()
    <object object at ...>
    >>> o1 = a_param()
    >>> o2 = a_param()
    >>> o1 is o2
    True

The ``lookup()`` function returns the current value for any key, either static
or dynamic::

    >>> lookup(a_param) is a_param()
    True

    >>> lookup(a_setting) is a_setting()
    True

    >>> new = old.swap()

    >>> lookup(a_setting), lookup(a_param)
    (99, 42)


Rule Purity
===========

Looking up non-rule values during default calculation of a rule is forbidden::

    >>> def s2(suffix, value=None):
    ...     return a_param()
    >>> s2 = registry(s2)
    >>> lookup(s2)
    Traceback (most recent call last):
      ...
    DynamicRuleError: ('default rule or exit function tried to read dynamic state', ...a_param...)


As is changing the active state::

    >>> def s3(suffix, value=None):
    ...     return parent.swap()
    >>> s3 = registry(s3)
    >>> lookup(s3)
    Traceback (most recent call last):
      ...
    DynamicRuleError: default rule or exit function tried to change states

    >>> a_param()
    42


Value Sharing
=============

Computed values should rise to the highest part of the state where they can
be determined to still be based on identical rules::

    >>> root = State()
    >>> s1 = root.child()
    >>> s2 = s1.child()
    >>> s3 = s2.child()
    >>> s4 = s3.child()

    >>> def lookupIn(state, key):
    ...     old = state.swap()
    ...     try: return lookup(key)
    ...     finally: old.swap()

    >>> f1 = lambda expr=None:object()
    >>> f2 = object    # similar result, but unequal factory

    >>> k1 = setting(f1)
    >>> s3[k1] = f2

Looking up k1 in s4 propagates the value to s3 and its children, because the
rule is established there::

    >>> lookupIn(s4, k1) is lookupIn(s3, k1) is lookupIn(s3.child(), k1)
    True

But since s3 has a different rule from s2, the value doesn't propagate any
further up::

    >>> lookupIn(s3, k1) is lookupIn(s2, k1)    # different factories
    False

Instead, s2 shares its value with s1 and root::

    >>> lookupIn(s2, k1) is lookupIn(s1, k1) is lookupIn(root, k1)  # same ones
    True

A value can only be shared as high as the lowest of its (recursive)
dependencies.  So, if we define a new expression whose factory at the root is
based on a lookup of the previous dynamic expression::

    >>> f2 = lambda expr=object: expr()
    >>> k2 = setting(f2)
    >>> root[k2] = lambda: [k1()]

Then its propagation scope should be the same as those of k1::

    >>> lookupIn(s3, k2) is lookupIn(s2, k2)
    False

    >>> lookupIn(s4, k2) is lookupIn(s3, k2) is lookupIn(s3.child(), k2)
    True

    >>> lookupIn(s4, k2) is not lookupIn(s2, k2)
    True

And a dynamic dependency on k2 should follow the same rules too::

    >>> k9 = setting(lambda expr=None: k2())
    >>> lookupIn(s3, k9) is lookupIn(s2, k9)
    False

The cases shown so far only show dynamic dependency on an already-known value.
We also need to verify dynamic dependency on an as-yet-unknown value::

    >>> k3 = setting(f2)
    >>> k4 = setting(f2)
    >>> s2[k3] = lambda: object()
    >>> root[k4] = lambda: [k3()]

    >>> lookupIn(s3, k4) is lookupIn(s2, k4) is not lookupIn(s1, k4)
    True

And dynamic dependency on a static value::

    >>> k5 = setting(lambda value=42: value)
    >>> k6 = setting(f2)
    >>> s2[k5] = 99
    >>> root[k6] = lambda: k5()

    >>> lookupIn(s3, k6) is lookupIn(s2, k6) is not lookupIn(s1, k6)
    True

Finally, we need to prove that values are never propagated to the root state,
since they would then be universal and un-removable::

    >>> def k9(expr=object):
    ...     print "calculating"
    ...     return expr()
    >>> k9 = setting(k9)
    >>> lookupIn(root, k9)
    calculating
    <object object ...>

    >>> lookupIn(s2, k9) is lookupIn(s1, k9)
    calculating
    True

    >>> lookupIn(State(), k9) is lookupIn(root, k9)
    calculating
    False



Services
========

    >>> class S1(context.Service): pass
    >>> class S2(context.Service): context.replaces(S1)

    >>> S1.get is S2.get
    True

    >>> S1.get() is S2.get()
    True

    >>> s1 = S1.get()
    >>> type(s1)
    <class 'S1'>

    >>> s2_ = S2.new()
    >>> s2 = s2_.__enter__()
    >>> s2 is S1.get() is S2.get()
    True
    >>> s3_ = S2.new()  # verify that this forces a *new* instance to be made
    >>> s3 = s3_.__enter__()
    >>> s3 is S1.get() is S2.get() is not s2
    True
    >>> s3_.__exit__(None,None,None)
    >>> s2 is S1.get() is S2.get()
    True
    >>> s2_.__exit__(None,None,None)
    >>> s1 is S1.get()
    True

The ``__default__()`` classmethod of a service is called to create its default
instance in a given context::

    >>> class S3(context.Service):
    ...     def __default__(cls):
    ...         print "creating default instance of", cls
    ...         return cls()
    ...     __default__ = classmethod(__default__)

    >>> s3 = S3.get()
    creating default instance of <class 'S3'>

``context.replaces()`` can only be called inside a ``context.Service`` subclass
definition, and only once::

    >>> context.replaces(S1)
    Traceback (most recent call last):
      ...
    SyntaxError: Class decorators may only be used inside a class statement

    >>> class X:
    ...     context.replaces(S1)
    Traceback (most recent call last):
      ...
    TypeError: context.replaces() can only be used in a context.Service subclass

    >>> class X(context.Service):   # doctest: +NORMALIZE_WHITESPACE
    ...     context.replaces(S1)
    ...     context.replaces(S3)
    Traceback (most recent call last):
      ...
    ValueError: replaces() must be used only once per class; there is already a
    value for ``get``: <bound method ...lookup of <class 'S1'>>

Services should be subclassable and super() should be usable in spite of all
the singleton-izing magic, for both instance and class methods::

    >>> class Base(context.Service):
    ...     def test(self, other=None):
    ...         print "hello from Base"
    ...         if other is not None and other is not self:
    ...             print "insanity!"
    ...     t2 = classmethod(test)

    >>> class Sub(Base):
    ...     def test(self):
    ...         print "hello from sub"
    ...         super(Sub, self).test(self)
    ...     def t2(cls):
    ...         print "hello from sub cm"
    ...         super(Sub, cls).t2()
    ...     t2 = classmethod(t2)

    >>> Base.get() is Sub.get()
    False

    >>> Sub.test()
    hello from sub
    hello from Base

    >>> Sub.t2()
    hello from sub cm
    hello from Base

Service attributes should be settable and deletable, mapped to the instance::

    >>> class Dummy(context.Service):
    ...     foo = 42

    >>> Dummy.foo
    42

    >>> Dummy.foo = 99
    >>> Dummy().foo
    42

    >>> Dummy.get().foo
    99

    >>> del Dummy.foo
    >>> Dummy.foo
    42
    >>> Dummy.get().foo
    42

Service classes should be able to have non-string keys in their class
dictionary (so that ``ClassAddOns`` from the "AddOns" package work)::

    >>> class Dummy(context.Service):
    ...     locals()[42]=99


Settings
========

Settings must be defined with one parameter::

    >>> def val(value=42):
    ...     """some docstring"""
    ...     return value * 2

    >>> val = setting(val)

    >>> help(val)
    Help on function val...:
    <BLANKLINE>
    val()
        some docstring
    <BLANKLINE>

which must be named ``expr`` or ``value``::

    >>> setting(lambda:42)
    Traceback (most recent call last):
      ...
    TypeError: setting function must have exactly 1 argument(s)

    >>> setting(lambda x,y: 42)
    Traceback (most recent call last):
      ...
    TypeError: setting function must have exactly 1 argument(s)

    >>> setting(lambda x: 42)
    Traceback (most recent call last):
      ...
    TypeError: setting function argument 1 must be named 'value' or 'expr'

and have a default value::

    >>> setting(lambda value: 42)
    Traceback (most recent call last):
      ...
    TypeError: setting function must have a default value for last argument


A setting's "input value" is set by State.__setitem__, and is readable with
State.__getitem__::

    >>> s = State()
    >>> s[val] = 99
    >>> s[val]
    99

And its default input value in the root state is the function's default value::

    >>> State.root[val]
    42

Calling a setting returns its output value in the current state (which is
determined by calling the original function on the input value)::

    >>> val()
    84


Configuration Support
=====================

A setting's input value in the current state can be set using ``<<=``::

    >>> old = State().swap()

    >>> val <<= 99
    >>> val <<= 22
    >>> val <<= 77
    >>> State[val]
    77

    >>> val()
    154

    >>> val <<= 77
    >>> val <<= 22
    Traceback (most recent call last):
      ...
    InputConflict: (val, 77, 22)

Likewise for services::

    >>> d1 = Dummy()
    >>> d2 = Dummy()
    >>> Dummy <<= lambda: d1
    >>> Dummy <<= lambda: d2
    >>> Dummy.get() is d2
    True
    >>> Dummy <<= lambda: d1
    Traceback (most recent call last):
      ...
    InputConflict: (<class 'Dummy'>, <...<lambda>...>, <...<lambda>...>)

Done with test::

    >>> s = old.swap()


A setting can turn a configuration string into a value suitable for its own
use, using the ``%`` operator.  If a setting's parameter is ``expr``,
configuration strings and values are treated as zero-argument callables, to be
invoked by the setting's body::

    >>> setting(lambda expr=None: expr()) % "42"
    'lambda: 42'

If a setting's parameter is ``value``, configuration strings and values are
used directly::

    >>> val % "42"
    '42'

For services, configuration strings are always treated as expressions::

    >>> Dummy % "42"
    'lambda: 42'


Registries
==========

Registries must be defined with two parameters: ``suffix`` and either ``value``
or ``expr``::

    >>> registry(lambda x: 42)
    Traceback (most recent call last):
      ...
    TypeError: registry function must have exactly 2 argument(s)

    >>> registry(lambda x, y: 42)
    Traceback (most recent call last):
      ...
    TypeError: registry function argument 1 must be named 'suffix'

    >>> registry(lambda suffix, y: 42)
    Traceback (most recent call last):
      ...
    TypeError: registry function argument 2 must be named 'value' or 'expr'

    >>> registry(lambda suffix, expr: 42)
    Traceback (most recent call last):
      ...
    TypeError: registry function must have a default value for last argument


    >>> def dummy(suffix, value=42):
    ...     """another docstring"""
    ...     if suffix:
    ...         print "looking up", suffix
    ...         return suffix
    ...     return value

    >>> dummy = registry(dummy)

Registry objects format strings according to their second argument, much like
settings::

    >>> dummy % "xyz"
    'xyz'

    >>> registry(lambda suffix, expr=99: 27) % "xyz"
    'lambda: xyz'


Registries can have lookups done on them to retrieve sub-registries by name::

    >>> dummy['foo']
    dummy.foo

And these sub-registries are cached::

    >>> foo = dummy['foo']
    >>> foo is dummy['foo']
    True

...but immutable::

    >>> dummy['foo'] = foo  # same value, ok
    
    >>> dummy['foo'] = 99   # different value, not ok!
    Traceback (most recent call last):
      ...
    TypeError: Registries are read-only


And are also available as attributes::

    >>> foo is dummy.foo
    True

    >>> foo.bar is dummy.foo['bar']
    True

    >>> dummy.foo = foo     # same value, ok
    >>> dummy.foo = 99   # different value, not ok!
    Traceback (most recent call last):
      ...
    TypeError: Registries are read-only

    
You can iterate over a registry's keys, and test for membership::

    >>> list(foo)
    ['bar']

    >>> 'bar' in foo
    True

    >>> 'baz' in foo
    False

Dotted names are treated as though they were nested access::

    >>> dummy['foo.bar'] is foo.bar
    True

    >>> 'foo.bar' in dummy
    True


Wildcards
=========

Wildcards are special objects used with registries::

    >>> wildcard(dummy)
    dummy.*

    >>> type(dummy['*'])
    <class 'peak.context.wildcard'>

Wildcards are effectively settings that prefix config strings with
``lambda suffix:``::

    >>> wildcard(dummy) % "blah"
    'lambda suffix: blah'

and don't transform their input values::

    >>> wildcard(dummy).__apply__(dummy, 99)
    99

When a non-root registry's fallback is called, it looks up its parent
registry's wildcard input, and calls it to compute the input for that key::

    >>> def trace(suffix):
    ...     print "tracing", `suffix`

    >>> foo['*'] <<= trace

    >>> foo.bar.__fallback__(None, foo.bar)
    tracing 'bar'

And when a wildcard's fallback is called, it returns a callable that can be
passed a suffix to compute a setting's value::

    >>> foo.bar['*'].__fallback__(None, foo.bar.baz)('x')
    tracing 'bar.x'

    >>> star = foo.bar['*'].__fallback__

    >>> star(None, foo.bar.baz) == star(None, foo.bar.baz)
    True

    >>> foo.bar.baz.__fallback__(None, foo.bar.baz)
    tracing 'bar.baz'

When attached to a non-root registry, their fallback is to look up their parent
wildcard::

    >>> foo['spam.*'].__fallback__(None, foo.bar)('x')
    tracing 'spam.x'

The fallback for a wildcard attached to a root registry, however, is ``None``::

    >>> print dummy['*'].__fallback__(None, foo)
    None

This causes the original target of the lookup to fall back to state-level
inheritance, or the original function::

    >>> dummy.__fallback__(None, foo.bar)
    looking up foo.bar
    'foo.bar'


Registries can be called, passing in a string key and optionally a default
value to return in case the key isn't found::

    >>> help(dummy)
    Help on function dummy...:
    <BLANKLINE>
    dummy(key, default=None)
        another docstring
    <BLANKLINE>

    >>> dummy('xyz.abc', 88)
    88

    >>> dummy('foo')
    looking up foo
    'foo'

    >>> foo.spam <<= 91
    >>> dummy('foo.spam')
    looking up foo.spam
    91

    >>> lookup(dummy)
    42

    >>> lookup(foo['really.really.long.name.needing.multiple.rules'])
    tracing 'really.really.long.name.needing.multiple.rules'
    looking up foo.really.really.long.name.needing.multiple.rules

    >>> sorted(foo)
    ['bar', 'really', 'spam']


Scopes
======

Subclasses of context.Scope can have a ``manage`` method called on their
scoped expressions, and an ``atexit`` method that is called upon scope exit,
as long as ``manage`` has been called without error at least once::

    >>> class MyScope(context.Scope):
    ...     def manage(self, ob):
    ...         print "managing", ob
    ...         return ob
    ...     def atexit(self, typ, val, tb):
    ...         super(MyScope, self).atexit(typ, val, tb)
    ...         print "exiting", typ, val, tb

And are not active unless explicitly created by entering their .new()::

    >>> MyScope.get()
    Traceback (most recent call last):
      ...
    RuntimeError: No MyScope is currently active

    >>> old = State.get()
    >>> new = MyScope.new()

    >>> s = new.__enter__()

The state you'll be in is a child of the original state::

    >>> State.parent is old
    True

And settings defined using the class' ``.resource`` and ``.resource_registry``
decorators will be passed through the ``manage`` method::

    >>> e = MyScope.resource(lambda value=23: value)
    >>> e()
    managing 23
    23

    >>> def e2(suffix, value=23):
    ...     if suffix:
    ...         return suffix
    ...     return value
    >>> e2 = MyScope.resource_registry(e2)
    >>> lookup(e2)
    managing 23
    23
    >>> lookup(e2.foo)
    managing foo
    'foo'

And are always calculated strictly within the state where the scope was
created::

    >>> this = State.get()
    >>> def check_state(value=None):
    ...     print State.get() is this
    >>> check = MyScope.resource(check_state)
    >>> check()
    True
    managing None

    >>> check2 = MyScope.resource(check_state)
    >>> context.call_with(State.child())(lambda x: check2())
    True
    managing None

Which means that you can't access such an expression from a state where its
rule has been changed, as that would create a dependency inversion::

    >>> s = State.child()
    >>> s is s.__enter__()
    True

    >>> check2 <<= lambda:check_state()
    >>> check2()
    Traceback (most recent call last):
      ...
    ScopeError: Redefined rule in sub-state

    >>> s.__exit__(None, None, None)
    >>> new.__exit__(None, None, None)
    exiting None None None

TODO: prove that manage() has to be called without an error to enable atexit()


Scoped Settings
===============

Scope's management wraps the application of a setting's function body::

    >>> def res(value=42):
    ...     print "initializing"
    ...     return value

    >>> res = context.Scope.resource(res)

    >>> s = context.Scope().new()
    >>> ss = s.__enter__()

    >>> res()
    initializing
    42
    >>> res()
    42

    >>> s.__exit__(None, None, None)


Resources
=========

``@context.resource`` is a decorator that turns a zero-argument function into
a dynamic variable, whose default value in a new context is the function's
return value when called within the relevant scope (``Action`` by default)::

    >>> def my_res(value=42):
    ...     """A resource"""
    ...     return value   # default value

    >>> my_res = context.resource(my_res)

    >>> help(my_res)
    Help on function my_res...:
    <BLANKLINE>
    my_res()
        A resource
    <BLANKLINE>

    >>> my_res()
    Traceback (most recent call last):
      ...
    RuntimeError: No Action is currently active

    >>> a = context.Action.new()
    >>> aa = a.__enter__()

    >>> my_res()
    42

    >>> a.__exit__(None, None, None)


Actions
=======

    >>> print context.Action.get()
    Traceback (most recent call last):
      ...
    RuntimeError: No Action is currently active

    >>> anew = context.Action.new()

    >>> class TestResource(object):
    ...     def __enter__(self):
    ...         print "Setting up"
    ...         return self
    ...     def __exit__(self, typ, val, tb):
    ...         print "Tearing down", map(str,(typ,val,tb))

    >>> res = context.resource(lambda expr=TestResource: expr())

    >>> res()
    Traceback (most recent call last):
      ...
    RuntimeError: No Action is currently active

    >>> act = anew.__enter__()
    >>> context.Action.get() is act
    True

    >>> r1 = res()
    Setting up

    >>> r1
    <TestResource object...>

    >>> r2 = res()
    >>> r1 is r2
    True

    >>> anew.__exit__(None,None,None)
    Tearing down ['None', 'None', 'None']

    >>> print context.Action.get()
    Traceback (most recent call last):
      ...
    RuntimeError: No Action is currently active

    >>> anew = context.Action.new()
    >>> act = anew.__enter__()
    >>> r = res()
    Setting up
    >>> r is res()
    True
    >>> old = State.child().swap()
    >>> r is res()
    True
    >>> old = old.swap()

    >>> r3 = res()
    >>> r4 = res()
    >>> r3 is r4
    True
    >>> r3 is r2
    False

    >>> anew.__exit__(TypeError,TypeError("Foo"),None)
    Tearing down [...'exceptions.TypeError'..., 'Foo', ...]

    >>> def my_factory():
    ...     print "Factory running"
    ...     return 42

    >>> anew = context.Action.new()
    >>> act = anew.__enter__()
    >>> State[res] = my_factory

    >>> res()
    Factory running
    42
    >>> res()
    42

    >>> class Failure(object):
    ...     def __enter__(self): raise RuntimeError("Foo!")
    ...     def __exit__(self,*exc):
    ...         raise AssertionError("This shouldn't get called!")

    >>> res2 = context.resource(lambda expr=Failure: expr())
    >>> res2()
    Traceback (most recent call last):
      ...
    RuntimeError: Foo!

    >>> class Success(object):
    ...     def __enter__(self):
    ...         print "entering"
    ...         return 99
    ...     def __exit__(self, *exc):
    ...         print "exiting", exc

    >>> res3 = context.resource(lambda expr=Success: expr())
    >>> res3()
    entering
    99
    >>> anew.__exit__(None,None,None)  # no __exit__ for failed __enter__
    exiting (None, None, None)


Source Lines
============

    >>> from peak.context import Source, Line

    >>> s = Source("<test>", "def foo():\n  def bar(): return 42\n  return bar")
    >>> s
    Source('<test>')

    >>> list(s)
    ['def foo():\n', '  def bar(): return 42\n', '  return bar']

    >>> exec s.compile('exec')
    >>> bar = foo()
    >>> bar.func_code.co_firstlineno
    2

    >>> s[1].strip()
    'def bar(): return 42'

    >>> _.line
    2

    >>> (s[1]+'\n').splitlines()[1].line
    3

    >>> exec(s[1].strip().compile('exec'))
    >>> bar.func_code.co_firstlineno
    2

    >>> l = Line("lambda: (\n lambda: \n lambda: 42)", s, 19)
    >>> f = eval(l.compile('eval'))
    >>> f.func_code.co_firstlineno
    19
    >>> f().func_code.co_firstlineno
    20
    >>> f()().func_code.co_firstlineno
    21
    >>> f()()()
    42

    >>> from pkg_resources import yield_lines
    >>> list(yield_lines(s))
    ['def foo():', 'def bar(): return 42', 'return bar']

    >>> _[2].line
    3


PEP 343 Implementation Tests
============================

An example context manager::

    >>> def demo_manager(value):
    ...     print "before"
    ...
    ...     try:
    ...         yield value
    ...         context.reraise()   # propagate error, if any
    ...     except TypeError, exc:
    ...         print "caught TypeError"
    ...     print "after"
    >>> demo_manager = context.manager(demo_manager)

Applied to a simple function using ``with_()``::

    >>> def something(ob):
    ...     print "got", ob
    ...     return 42
    >>> context.with_(demo_manager(99), something)
    before
    got 99
    after
    42

Errors in the called function get passed to ``__exit__()`` and reraised::

    >>> def fail(v):
    ...     raise TypeError("foo")

    >>> context.with_(demo_manager(57), fail)
    before
    caught TypeError
    after

    >>> def fail(v):
    ...     raise KeyError("foo")
    >>> try:
    ...     print context.with_(demo_manager(57), fail)
    ... except KeyError:
    ...     print "KeyError escaped!"
    before
    KeyError escaped!

You don't have to decorate a new function to use ``call_with`` or ``with_()``::

    >>> def something(ob):
    ...     print "got", ob
    ...     return 42

Just wrap the context object, then pass the result the function to call::

    >>> context.call_with(demo_manager(None))(something)
    before
    got None
    after
    42

Or use ``with_()``, passing in the context and the function in one call::

    >>> context.with_(demo_manager(None),something)
    before
    got None
    after
    42

Finally, notice that ``__enter__`` may return a different object, which will be
passed in to the called function as its sole argument::

    >>> context.call_with(demo_manager(99))(something)
    before
    got 99
    after
    42

    >>> context.with_(demo_manager(99),something)
    before
    got 99
    after
    42


cvs-admin@eby-sarna.com

Powered by ViewCVS 1.0-dev

ViewCVS and CVS Help