[Subversion] / Contextual / Contextual.txt  

View of /Contextual/Contextual.txt

Parent Directory | Revision Log
Revision: 2511 - (download)
Sun Mar 9 17:58:19 2008 UTC (14 years, 4 months ago) by pje
File size: 22703 byte(s)
Bug fixes for state initialization and registry wildcard lookups.
===================================================================
Creating Ultralight Frameworks with "Contextual" (``peak.context``)
===================================================================

-----------------
Developer's Guide
-----------------

The ``Contextual`` library provides a safe replacement for the use of global
(or thread-local) variables, making it easy to create non-invasive frameworks
that are still highly testable and configurable.  Code based on ``Contextual``
is also safe for use in event-driven, coroutine, or "co-operative" multitasking
systems such as Twisted and ``peak.events``, because an entire thread's context
can be easily saved and restored when switching between different logical
threads, tasks, or coroutines.

Although ``Contextual`` works with Python versions 2.3 and up, the examples in
this guide are written using Python 2.5 and the ``with`` statement for ease of
reading.  If you're using an older version of, you'll need to consult the
section below on `Support for Python 2.3/2.4`_ before you try using any of the
examples that use the ``with`` statement.

Also, under Python 2.5, you'll need to do a ``__future__`` import at the top
of every module that uses the ``with`` statement, and of course you'll need
to import the ``Contextual`` library too:

    >>> from __future__ import with_statement
    >>> from peak import context


Overview
========

Global variables (including so-called "singletons") are attractive to use,
because they're so *convenient* to use.  But they're bug-prone for two reasons:

1. They're global (i.e., shared by the entire program), and

2. They're variable (i.e., can be changed at any time)!

The ``Contextual`` library lets you replace "global variables" with "contextual
values" (and "singletons" with "services").  These tools are almost as easy to
use as their "global" counterparts, but are less bug-prone becuase they're
neither "global" nor "variable"!

First, while global variables and singletons are stored in modules, contextual
values and services are stored in a "context".  By default, each thread has its
own current context, so you don't have to explicitly keep track of what context
you're in.

This gives you the convenience of global variables (i.e., you don't need to
pass them around everywhere), but without necessarily sharing the values
throughout the entire program.  Thus, they are not truly "global".

Second, contextual values and services are not "variable" either, because they
can only be set *once* within a given context (rather like variables in a
"pure functional" language).  This makes them 100% thread-safe, and free of
"race conditions" even in non-threaded code.

To "change" a value, you must create a new context (e.g. using the Python 2.5
``with:`` statement), and then assign the new value in the new context.  Then,
when the context is exited, the previous value is restored.


Safe Configuration Using Settings
=================================

The simplest type of "contextual" value is a ``context.setting``.  Settings
consist of a default "input" value, and a function for transforming input
values to the form needed when using the setting.  (For example, you might have
a setting whose output value is numeric, but which allows string inputs.)  In
the simplest case, however, a setting function can simply return its input
unchanged.

Settings are designed to be a safe replacement for module-level configuration
variables.  Instead of one program-wide value for a setting, individual code
paths can safely alter the current configuration temporarily, without affecting
any other code (including code in other threads) that needs to change or use
those settings.  Settings are also useful as replacements for rarely-changed
parameters that must be passed through a lot of function or method calls.

Let's look at two simple settings::

    >>> @context.setting
    ... def speed(value=16):
    ...     """Speed of duplicating CDs"""
    ...     return float(value)

    >>> @context.setting
    ... def resolution(value=300):
    ...     """DPI for printing CD labels"""
    ...     return int(value)

Settings look, for all intents and purposes, like normal functions, but they
are actually specialized objects that wrap the supplied function.

When called, a setting returns its "current" value.  By default, this will be
the result of applying the original function to its default input value::

    >>> speed()     # returns the default value
    16.0
    >>> resolution()
    300

However, settings can be "changed" in a controlled fashion, by entering a new
context, and using the ``<<=`` operator to set a new input value.  For
example::

    >>> with context.new():  # enter a new child context
    ...     speed <<= 48     # set speed's input to 48 in the current context
    ...     print speed()
    ...     print resolution()  # inherits the default from the outer context
    48.0
    300

    >>> speed() # outside the block, goes back to the old value
    16.0


Within a given context, however, a setting is guaranteed to only have **one**
readable value.  You can set it to the *same* (i.e. equal) input value as many
times as you like::

    >>> speed <<= 16    # setting to the existing value, ok
    >>> speed <<= 16    # still ok

But you cannot *change* the value, once it has been read in a given context::
    
    >>> speed <<= 48    # changing the value, not ok!
    Traceback (most recent call last):
      ...
    InputConflict: (speed, 16, 48)

As you can see, we get an ``InputConflict`` error showing that an input value
of ``16`` for ``speed`` has already been read in the current context, and an
attempt was made to change it to ``48``.

Remember, settings are not the same thing as "variables": they can only "vary"
in a controlled way.  If you want to "change" a setting, you must do so in
a context where that setting has not yet been used.  As you'll see in this next
example, it's the act of *using* a setting that makes it unchangeable in that
context::

    >>> with context.new():
    ...     speed <<= 77    # change as many times as we like, because the
    ...     speed <<= 99    # input hasn't been *used* yet in this context
    ...     speed <<= 66
    ...     print speed()   # but now it can't be changed ever again!
    ...
    ...     try:
    ...         speed <<= 8
    ...     except context.InputConflict:
    ...         print "Caught a conflict"
    ...
    ...     with context.new():
    ...         speed <<= 99    # but we can change it in this *new* context!
    ...         speed <<= 54    # as many times as we like
    ...         print speed()   # ...until it's read, of course
    ...
    ...     print speed()   # back to 66, as we're back in this context
    ... 
    ...     with context.new():
    ...         print speed()   # and of course it's 66 here too, inherited
    ...                         # from our containing context
    ...         
    66.0
    Caught a conflict
    54.0
    66.0
    66.0
    
This "write many until read once" behavior makes it impossible to have
setting initialization bugs where one piece of code reads one value for a
setting, and then another piece of code changes the setting to something else.


Creating and Using Services
===========================

Here's a simple "counter" service implementation::

    >>> class Counter(context.Service):
    ...     value = 0
    ...     def inc(self):
    ...         self.value += 1

You can get the current instance of the service using its ``get()``
classmethod::

    >>> count = Counter.get()   # get the current instance
    >>> count.value
    0
    >>> count.inc()
    >>> count.value
    1

By default, each thread will have its own unique instance, that's created upon
first use, and is cached thereafter::

    >>> count is Counter.get()  # still using the same one
    True

    >>> def run_in_another_thread():
    ...     c2 = Counter.get()
    ...     print c2 is count           # this will be different
    ...     print c2 is Counter.get()   # but the same as long as thread runs

    >>> import threading
    >>> t = threading.Thread(target=run_in_another_thread)

    >>> t.start(); t.join() # run and wait for it to finish
    False
    True

You can replace the current instance with another instance, by using the
``with:`` statement (or a special function in older versions of Python; see
the section below on `Support for Python 2.3/2.4`_ for more info)::

    >>> with Counter.new():
    ...     print count is Counter.get()
    False

    >>> count is Counter.get()  # old value is restored
    True


Easy Access Shortcuts
---------------------

Although up to this point we've been using ``Counter.get()`` to get the current
``Counter`` instance, note that we can also just use class attributes and
methods of ``Counter``, and they will automatically be redirected to the
corresponding attributes and methods of the current instance::

    >>> Counter.value
    1
    >>> Counter.inc()
    >>> Counter.value
    2
    >>> Counter.value = 42
    >>> Counter.get().value
    42

Any attribute that is defined in the service class that is *not* a
staticmethod, classmethod, or language-defined class attribute (like
``__new__`` and ``__slots__``) is automatically set up to redirect to the
instances, when retrieved from the class.

In other words, ``Counter.inc`` and ``Counter.value`` are just
shorter ways to spell ``Counter.get().inc``, and ``Counter.get().value``.  The
same shorthand works for getting, setting, or deleting class attributes.

Note, however, that you cannot use this shorthand form when performing
"in-place" operations such as ``+=``, because this would replace the class
with the operation's result.  That is, a statement like ``Counter += 1`` would
delete the ``Counter`` class and replace it with something else!

Therefore, you must always use ``.get()`` before performing an in-place
operation, e.g.::

    service = MyService.get()
    service += 42

This will not change the current instance of the service or delete the class,
but instead will just rebind the variable named ``service`` to point to the
operation's result.  Even this approach is confusing, however, so it's best
to just avoid defining or using in-place operators on your service classes.


Service Replacements
--------------------

You can create alternative implementations for a service, and substitute
them for the original service.  They do *not* have to be subclasses of the
original service for this to work, but they should of course be a suitable
replacement (i.e., implement the same interface), and they *must* use the
``context.replaces()`` class decorator to specify what service they're a
replacement for (even if they are a subclass of that service class).

For example, let's replace our ``Counter`` with an extended version that counts
by twos, and can optionally increment by arbitrary amounts::

    >>> class ExtendedCounter(context.Service):
    ...     context.replaces(Counter)
    ...     value = 0
    ...     def inc(self):
    ...         self.value += 2
    ...     def inc_by(self, increment):
    ...         self.value += increment

    >>> with ExtendedCounter.new():
    ...     print Counter.value
    ...     Counter.inc()
    ...     print Counter.value
    0
    2

    >>> Counter.value
    42

As you can see, our count went back to 0, because inside the ``with`` block,
the current ``Counter`` is actually the ``ExtendedCounter`` instance.  Its
value also went up by 2.  Outside the ``with`` block, the original ``Counter``
instance became current again, and thus ``Counter.value`` now returns the same
value it did before.

Note, by the way, that although ``ExtendedCounter`` has added a new method, it
is not acessible from the ``Counter`` class namespace, even if an
``ExtendedCounter`` is the current counter::

    >>> with ExtendedCounter.new():
    ...     Counter.inc_by(42)
    Traceback (most recent call last):
      ...
    AttributeError: type object 'Counter' has no attribute 'inc_by'

To access such additional methods, you need to either get an instance of the
service using the ``get()`` classmethod, or use the replacement class
directly::

    >>> with ExtendedCounter.new():
    ...     Counter.get().inc_by(42)
    ...     print Counter.value
    ...     ExtendedCounter.inc_by(24)
    ...     print Counter.value
    42
    66

However, the extra method will produce an attribute error at runtime if the
current service instance doesn't actually have that method.  For example, the
code below fails because the default ``Counter`` service is a ``Counter``,
not an ``ExtendedCounter``::

    >>> ExtendedCounter.inc_by
    Traceback (most recent call last):
      ...
    AttributeError: 'Counter' object has no attribute 'inc_by'


Configuring Replacement Services
--------------------------------

Now, you might be wondering at this point how your programs will ever be able
to use an ``ExtendedCounter``, if the default instance is a ``Counter``.  Will
you have to use a huge assortment of ``with`` statements to set up all the
service implementations you want to use?

Fortunately, no, because Contextual provides a configuration system that can be
used to set up service implementations as well as other types of configuration,
using either its built-in ``.ini`` format, or using whatever other mechanism
you might want.  So you can assemble the configuration of your entire program
into a single object, and then use a single ``with`` statement to execute your
program under it.

Another simple way to configure a context's services is to create a new child
context, and register one or more service factories while inside it::

    >>> with context.new():
    ...     Counter <<= ExtendedCounter
    ...     print type(Counter.get())
    <class 'ExtendedCounter'>

The ``<<=`` operator sets the factory to be used for creating a service
instance in the current context.  (It has no effect on any other execution
context.)  The factory can only be set, however, if it hasn't already been used
in the current context.  For example, in the default context, we had already
used ``Counter``, so we can't change the factory to ``ExtendedCounter`` there::

    >>> Counter <<= ExtendedCounter
    Traceback (most recent call last):
      ...
    InputConflict: (<class 'Counter'>, <...'ExtendedCounter'>)

Note, by the way, that the factory does not need to be a class.  It can be any
zero-argument callable, e.g.:

    >>> def a_factory():
    ...     ctr = Counter()
    ...     ctr.value = 99
    ...     return ctr

    >>> with context.new():
    ...     Counter <<= a_factory
    ...     print Counter.value
    99



Configuration and Settings
==========================

XXX Everything from here down is out of date and needs to be rewritten; don't
    read it unless you want to be confused! XXX


Settings
    ``context.setting`` objects provide thread-safe access to stateless
    configuration settings.  Settings consist of a default "input" value, and
    a function for transforming input values to the form needed when using the
    setting.  (For example, the function might take a factory function as
    input, and call it to produce the output, or it might convert strings to
    numeric or date values, etc.)

    Settings can have different input values in different contexts, but once
    the setting's input or output value has been retrieved or computed in a
    given context, it can no longer be changed in that context.  This is a
    major improvement over global variables, as it prevents bugs due to
    changing settings after another part of the program has already used it.

    Of course, this restriction only applies to a single context; each context
    can have a different value for any given setting, and it's easy to create
    a context that inherits its default settings from an existing context (so
    you don't have to copy the settings you want to keep.

Services
    Services are like singletons, but have a separate instance for each thread
    and can easily be replaced as needed for e.g. testing or co-operative
    multitasking.  Services are created by subclassing ``context.Service``, and
    the resulting class's ``get()`` classmethod will return the current
    instance.  Alternative implementations of a service can be defined and used
    to replace the default implementation in a given context.

    Services are also similar to settings, in that they take an input value
    (which must be 

    (But as with
    settings, the choice of replacement cannot be changed in a context where
    the

    , but as with settings, the choice of
    implementation cannot be changed within

    Services also work like settings, in that you cannot  whose input value is a class or
    factory for creating instances of the service, and its

Transaction Resources
    The ``context.Action`` service manages access to objects that may need to
    be recycled, saved, or reverted at the completion of an action such as
    a transaction, command, web request, or other atomic operation.  Requesting
    a resource (such as a database connection) causes it to be registered with
    the current ``Action``, so that it will be notified of the success or
    failure of the action.  This then allows the
    resource to commit or rollback as appropriate, return connections to
    connection pools or close them, etc.

    The boundaries of an action are indicated by the scope of a ``with:``
    statement (or substitute).  E.g.::

        from peak.context import Action

        @Action.resource
        def db_conn(value="default connection string"):
            conn = db_open(value)
            return conn

        with context.Action.new():
            # any resources used here will be notified of the
            # success or failure of this block of code

            # for example, the opened db connection will get __enter__
            # called here:
            c = db_conn()
            # and then its __exit__ will be called when the block is finished

In addition, ``Contextual`` offers some related convenience features:

``State`` objects
    State objects can be used to explicitly manipulate the current context,
    e.g. to swap out the current state of all settings, resources, and
    services, thereby creating either a new blank slate (e.g.
    for tests) or swapping between multiple independent tasks in the same
    thread (e.g. to do co-operative multitasking).

Registries
    Normally, configuration settings and action resources are defined
    statically in code.  But sometimes you want to create a dynamic namespace
    where any name could potentially identify a valid setting or resource,
    possibly with some kind of rules to compute default values when they
    haven't been explicitly set.  The ``registry`` and ``resource_registry``
    decorators let you create such namespaces for settings and resources.

Configuration Files
    ``Contextual`` offers an extensible ``.ini``-style configuration file
    format that can be used to configure arbitrary settings and resources
    across any number of modules, thereby giving you global configuration
    of an entire application from a single configuration system.

    The loaded configuration is lazy, which means that it doesn't force
    importing of any of the modules being configured, and doesn't compute any
    values until they're actually needed.  Thus, a configuration file can
    include optional settings for modules that may not even be installed!

    Although the ``.ini`` syntax is a fixed property of the configuration
    loader, the semantics can be extended to create custom section handlers
    that interpret their contents differently -- and this can even be done
    from within the ``.ini`` files themselves, because the handlers are looked
    up using the same configuration services that everything else uses.

"with"-statement Emulation and Tools
    Finally, ``Contextual`` offers a variety of decorators, base classes, and
    utility functions that are useful for either emulating the "with" statement
    in older versions of Python, and/or working with it in Python 2.5+.






Actions and Resources
=====================


Namespaces and Configuration Files
==================================


Dynamic Parameters
==================


The Application Object
======================


Co-operative Multi-tasking
--------------------------


-------------
API Reference
-------------

Service
replaces()

@value
@resource
@expression

Scope

using, only, State

InputConflict
DynamicRuleError

Action

@namespace

lookup


Support for Python 2.3/2.4
==========================

The "with" statement is only available in Python 2.5 and up, but Contextual is
intended to be usable with Python 2.3 and up.  So, Contextual include a few
functions that let you emulate most of the "with" statement's functionality
in Python 2.3 and 2.4.  You should use them in place of the Python 2.5 features
if you want your code to work on Python 2.3 or 2.4.

Here's a simple translation table::

    Python 2.5+               Python 2.4 (for 2.3, put decorators after funcs)
    ------------              ------------------------------------------------
    with x as y:              @context.call_with(x)
        print y               def do_it(y):
                                  print y

    with x as y:              z = context.with_(x,f)
        z = f(y)

    @contextmanager           @context.manager
    def transacted():         def transacted():
        begin_txn()               begin_txn()
        try:                      try:
            yield                     yield None
        except:                       context.reraise()
            abort_txn()           except:
            raise                     abort_txn()
        else:                         raise
            commit_txn()          else:
                                      commit_txn()

The biggest difference between these constructs (apart from performance) is
that the "with" statement lets you rebind variables in the surrounding scope,
because the code in the block is still in the same scope.  Since our emulation
for Python 2.4 uses functions, they can't rebind variables from the surrounding
scope, and thus must make do with side effects and/or return values.

Notice also that we allow definition of context managers using generators, much
like in Python 2.5, but you must *always* call ``context.reraise()`` following
your ``yield None``, so that any error raised by the "with" block will be
propagated.  (Unless, of course, you want to suppress it.)



cvs-admin@eby-sarna.com

Powered by ViewCVS 1.0-dev

ViewCVS and CVS Help