# -*- coding: iso-8859-1 -*-
"""delegation module.
The underlying philosophy
-------------------------
Suppose you are about to develop a reusable component offering a secure
login service. This component might need two different sorts of features:
say, a mandatory procedure to check login and password, and some optional
others, e.g. a special procedure to be called when the maximal number of
login attempts is reached. Your component implements a default procedure
to check credentials against, say, the ``/etc/password`` file, and does not
have any limit on the number of retries after failure.
However and since this is a reusable component you wish that the procedure
that checks credentials, the maximum number of failures and the action to
be taken can be conveniently changed at runtime.
One approach consists in making these parameters settable in your
component ; this will work well, but this may not be really convenient:
- for you, as the developper of the reusable component, because this
make the API of the component all the more cumbersome since the number
of possible external "tune-ups" grows
- for the users of your component, for which it may be handier to be
able to provide a single object gathering its own mechanisms to tune
the reusable component [1]_. A good reason for this is when the some
processings involved depends on some other objects the reusable
component has no way to know about, and/or if answers to some questions
cannot be determined but at runtime.
The delegation offers you a general mechanism to solve these two issues.
The DelegateWrapper class allows the following features:
- it makes it simple to have a single place to specify and document what
can be changed along with specific documentation: this is the
delegate's interface.
- The users of your component do not have to implement *all*
methods of the delegate's API, they only provide whatever methods is
adequate for their own use [2]_.
- It can also be used to notify a delegate when some actions are about
to be done, have been processed, etc., thus making it possible to
keep an external object in sync. with the reusable component's
internal state, without violating encapsulation.
.. [1] of course, you may run into cases where such a design is inaccurate.
All this is obviously a general discussion which might be inadequate given
a specific architecture or design philosophy!
.. [2] except if you enforce your own rules, such as "if the delegates
provides methodOne it should provide methodTwo as well", but this an other
story (see DelegateWrapper.respondsTo())
How to use
----------
We will stick on the example exposed above to explain how delegation is
achieved with this module.
First, define a general "interface": a class, which defines and
documents the methods a delegate for your component might implement:
.. code-block:: Python
class LoginComponentDelegate:
'Delegate for the reusable LoginComponent'
def checkCredentials(self, login, password):
'''
Implement this method if you want to replace the default checking
mechanism by yours. This method should raise InvalidCredentials
if parameters 'login' and 'password' do not match an valid user.
Parameters: both 'login' and 'password' are strings.
'''
def maximumNumberOfFailures(self):
'''
Implement this method to ...
A return value equal to '-1' means no limit.
'''
def numberOfFailuresHasReachedItsMaximum(self):
'''
...
'''
The `delegation` module supplies one class, `DelegateWrapper`, to handle
delegate. Its role is to inspect a delegate's interface...
To use it, your reusable component's class will look like:
.. code-block:: Python
class LoginComponent:
def __init__(self):
'... Initialization ...'
self._delegate=DelegateWrapper(LoginComponentDelegate)
self._nbOfFailures=0
def setDelegate(self, object):
'''Sets the login component's delegate'''
self._delegate.setDelegateObject(object)
def validateCredentials(self, login, password):
'''
delegate's
'''
try:
if self._delegate.respondsTo('checkCredentials'):
# DelegateWrapper acts as a proxy for the wrapped object
self._delegate.checkCredentials(login, password)
else:
# the default mechanism, raising InvalidCredentials if no user
# can be found
except:
self._nbOfFailures+=1
if self._nbOfFailures>=self.maximumNumberOfFailures():
[...you get the idea...]
raise
def maximumNumberOfFailures(self):
'''
Defaults to -1 (no limit). You can change this limit by implementing
the delegate's method 'maximumNumberOfFailures'
'''
if self._delegate.respondsTo('maximumNumberOfFailures'):
return self._delegate.maximumNumberOfFailures()
else:
return -1
[...]
The delegate itself, as it will be provided by the users of the reusable
component, can be an instance of any class. You should have noticed in the
sample code above that the `DelegateWrapper` acts as a proxy : it
automatically forwards any function call it cannot service (see Gotchas,
below) to the wrapped object --the real delegate). Note, however, that the
`DelegateWrapper` is **not** an "absolute" proxy: the forwarded messages
are __only__ those which are declared in the delegate's interface.
Gotchas
-------
There are several points which you should be aware of when using this
class, some of which should be indicated in your own documentation:
- the DelegateWrapper works by inspecting the interface-class and
registering the signatures of its methods. By signature we mean: name,
parameters *and* their default values, if any. These signatures are then
compared to those of the delegate object when it is supplied. Thus:
- if a delegate object does implement a function whose name matches
one of the delegate's API but which uses different number of
parameters, the same number of parameter with different names, or
the same number of parameters and the same names but with different
default values, this function will not be considered as an valid
delegate method (hence 'respondsTo(thatMethodName)' will be false).
- The DelegateWrapper determines the mapping between the delegate's
API and the delegate object when this object is supplied
('setDelegateObject()') ; this mapping determines the answer for
message 'respondsTo' as well as whether that message should be
forwarded to the delegate by the DelegateWrapper.
Thus, if the delegate object can change at runtime, e.g. not
responding to a message at some point of runtime and being able to
service the same message later, that message will never be
considered available. In that case, the provider for the delegate
object should take care to call the component's 'setDelegate()'
method each time the delegate object changes --this ensures that the
mapping between the delegate's API and the object's capabilities is
correctly recomputed.
- The `DelegateWrapper` itself declares some methods:
`DelegateWrapper.respondsTo`, `DelegateWrapper.delegateObject`,
`DelegateWrapper.setDelegateObject`,
`DelegateWrapper.reevaluateDelegateObject`. If the delegate's interface uses
at least one of these methods, the automatic forwarding of method calls is
automatically disabled. In that case, a warnings is issued (warning level:
``RuntimeWarning``) when the delegate object is set. You can still send
messages to the delegate object by sending the messages to the
`DelegateWrapper.delegateObject()`.
Licence
-------
::
Copyright (c) 2001-2004, Sebastien Bigaret
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of the software's copyright holder, Sebastien
Bigaret, nor the names of its contributors may be used to endorse or
promote products derived from this software without specific prior
written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
CVS information
---------------
$Id: delegation.py 937 2004-08-02 20:54:47Z sbigaret $
"""
__version__='$Revision: 937 $'[11:-3] # original revision
__authors__=('Sebastien Bigaret <sbigaret@users.sourceforge.net>', )
from warnings import warn
import inspect, types
class DelegateWrapper:
"""
The DelegateWrapper:
"""
_delegate=None
def __init__(self, interface, object=None):
"""
Initializes the DelegateWrapper
:Parameters:
- `interface`: a class object declaring the methods a delegate may
choose to implement
- `object`: optional delegate object. The delegate object can be set or
changed later with `setDelegateObject()`
"""
if not inspect.isclass(interface):
raise ValueError, 'Invalid parameter interface: should be a class'
self._interfaceSpec={}
self._delegateCapabilities={}
self._proxyDelegate=1
self._interface=interface
self._compileInterface()
if object:
self.setDelegateObject(object)
def delegateObject(self):
"""
Returns the delegate object
"""
return self._delegate
def respondsTo(self, interfaceMethodName):
"""
Tells whether the wrapped delegate object can responds to the method
`interfaceMethodName` as defined in the delegate's interface.
Returned value is ``None`` for false, ``1`` (integer) for true
"""
return self._delegateCapabilities.get(interfaceMethodName)
def reevaluateDelegateObject(self):
"""
"""
if not self._delegate:
return
self._delegateCapabilities={}
functions=self._getClassMethodsSignatures(self._delegate.__class__)
for nameSignature in functions:
name, signature=nameSignature
if signature==self._interfaceSpec.get(name):
self._delegateCapabilities[name]=1
def setDelegateObject(self, object):
"""
Sets the delegate object
"""
if not getattr(object, '__class__', None):
raise ValueError, 'Invalid parameter object: it should be an instance'
self._delegate=object
self.reevaluateDelegateObject()
def __getattr__(self, name): # proxy object's method only
if not self._proxyDelegate:
raise AttributeError, 'Automatic forward of method calls is disabled. '
if self._delegateCapabilities.get(name):
return getattr(self._delegate, name)
raise AttributeError
# Private methods
def _compileInterface(self):
"""
Inspect the delegate's interface and extracts the necessary methods' names
and signatures
"""
functions=self._getClassMethodsSignatures(self._interface)
dict={}
pbFuncNames=[]
self._proxyDelegate=1
for nameSignature in functions:
name, signature=nameSignature
dict[name]=signature
if name in ('delegate', 'respondsTo', 'reevaluateDelegateObject', 'setDelegate'):
self._proxyDelegate=0
pbFuncNames.append(name)
if not self._proxyDelegate:
warn('DelegateWrapper will not be able to proxy calls to the delegate '\
"object: the interface declares the methods %s which are also "\
"methods of the DelegateWrapper itself.\n\nProxy calls disabled "\
"(see documentation for details)"%str(pbFuncNames),
RuntimeWarning)
self._interfaceSpec=dict
return
def _getClassMethodsSignatures(self, aClass):
"""
Generic function returning a sequence made of couples
(<functionName>, <functionSignature>), each couple representing one of
`aClass` 's methods.
An example of such a couple is (from ``tests/test_delegation.py``)::
('canTheWorldBeChanged', "(['self', 'isElvisAlive'], None, None, (1,))")
See also: `inspect.getargspec()`
"""
isfunction=inspect.isfunction
classDict=aClass.__dict__
methods=[(name, str(inspect.getargspec(func)))
for name, func in map(None,classDict.keys(),classDict.values())
if isfunction(func)]
return tuple(methods)
|