created util package with a simple adapter factory and lazy properties

git-svn-id: svn://svn.cy55.de/Zope3/src/cybertools/trunk@1540 fd906abe-77d9-0310-91a1-e0d9ade77398
This commit is contained in:
helmutm 2007-01-04 14:09:48 +00:00
parent 0dda2f4b12
commit 174a0ab4f1
7 changed files with 304 additions and 0 deletions

26
util/README.txt Normal file
View file

@ -0,0 +1,26 @@
================
Common Utilities
================
$Id$
This package contains a set of miscellaneous utility modules that
are to small for creating a separate package for them.
As each utility is developed further it may eventually get its own
package some time.
Usually each utility has a <name>.py file with an implementation and a
corresponding <name>.txt file with a description that can be run as a
doctest.
The doctests can be run by issuing
python cybertools/util/tests.py
in the directory above the cybertools directory or via
bin/test -vs cybertools.util
from within a Zope 3 instance that contains the cybertools package in
its python path.

3
util/__init__.py Normal file
View file

@ -0,0 +1,3 @@
"""
$Id$
"""

52
util/adapter.py Normal file
View file

@ -0,0 +1,52 @@
#
# Copyright (c) 2007 Helmut Merz helmutm@cy55.de
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
"""
A simple adapter framework.
$Id$
"""
class AdapterFactory(object):
def __init__(self):
self._registry = {}
def register(self, adapter, adapted, name=''):
""" Register `adapter` class for objects of class `adapted`.
Optionally associate the adapter with a `name`; thus there
can be more than one adapter for one class.
"""
self._registry[(adapted, name)] = adapter
def __call__(self, obj, name=''):
""" Return an adapter instance on `obj` with the `name` given.
if obj is a class use this class for the adapter lookup, else
use obj's class.
If there isn't an adapter directly for the class
check also for its base classes.
"""
class_ = type(obj) is type and obj or obj.__class__
adapter = None
while adapter is None and class_:
adapter = self._registry.get((class_, name))
if adapter is None:
bases = class_.__bases__
class_ = bases and bases[0] or None
return adapter is not None and adapter(obj) or None

102
util/adapter.txt Normal file
View file

@ -0,0 +1,102 @@
==========================
A Simple Adapter Framework
==========================
$Id$
To work with adapters we need at least two classes, one for objects
that we want to adapt to (the adapter's `context`) and one for the adapters.
>>> class Content(object):
... pass
>>> class StateAdapter(object):
... def __init__(self, context):
... self.context = context
... def setState(self, state):
... self.context._state = state
... def getState(self):
... return getattr(self.context, '_state', 'initial')
In order to use the adapters in a flexible way we create adapter objects
by using an adapter factory. The stateful factory is intended to create
adapters that allow setting and retrieving the state of objects.
>>> from cybertools.util.adapter import AdapterFactory
>>> stateful = AdapterFactory()
We can now register our StateAdapter class with the `stateful` AdapterFactory.
We have to tell the factory which class of adapted objects a certain
adapter class is responsible for.
>>> stateful.register(StateAdapter, Content)
Now we are ready to create an object of the Content class
>>> c1 = Content()
and a corresponding `stateful` adapter:
>>> c1State = stateful(c1)
The adapter can now be used to access the Content object's state:
>>> c1State.getState()
'initial'
>>> c1State.setState('visible')
>>> c1State.getState()
'visible'
The procedure also works with subclasses of the Content class.
>>> class SpecialContent(Content):
... pass
>>> sc1 = SpecialContent()
>>> sc1State = stateful(sc1)
>>> sc1State.getState()
'initial'
>>> sc1State.setState('public')
>>> sc1State.getState()
'public'
But if we try to use an adapter factory for objects for which there is
no adapter registered we get back None.
>>> class UnknownContent(object):
... pass
>>> uc1 = UnknownContent()
>>> uc1State = stateful(uc1)
>>> uc1State is None
True
Class adapters as factories
---------------------------
There is a special case that can be handled with an adapter factory:
creating objects via an adapter to a class. An example for this is
a class that creates objects by loading them from a file or database,
so this would be a storage adapter class.
In our example we avoid reading from disk or from a database but just
create the object wanted by using the context's constructor.
>>> class ContentLoader(object):
... def __init__(self, context):
... self.context = context
... def load(self):
... return Content()
>>> storage = AdapterFactory()
>>> storage.register(ContentLoader, Content)
In this case we get the adapter from the adapter factory by providing
it with the context class (instead of an instance of it like in the
example above.
>>> loader = storage(Content)
>>> c2 = loader.load()
>>> c2
<Content object ...>

41
util/property.py Normal file
View file

@ -0,0 +1,41 @@
#
# Copyright (c) 2007 Helmut Merz helmutm@cy55.de
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
"""
Advanced properties, esp the lazy one...
Based on zope.cachedescriptors.property.Lazy
$Id$
"""
class lzprop(object):
"""Declare lazy properties that are calculated only if needed,
and just once.
See property.txt for example usage.
"""
def __init__(self, func):
self.func = func
def __get__(self, inst, class_):
if inst is None: return self
value = self.func(inst)
inst.__dict__[self.func.__name__] = value
return value

54
util/property.txt Normal file
View file

@ -0,0 +1,54 @@
================
Smart Properties
================
$Id$
lzprop
======
The `lzprop` decorator allows the declaration of lazy properties - attributes
that are calculated only on first access, and then just once. This is
extremely useful when working with objects of a limited lifetime (e.g.
adapters) that provide results from expensive calculations.
We use a simple class with one lazy property that tracks the calculation
by printing an informative message:
>>> from cybertools.util.property import lzprop
>>> class Demo(object):
... base = 6
... @lzprop
... def value(self):
... print 'calculating'
... return self.base * 7
>>> demo = Demo()
When we first access the `value` attribute the corresponding method will be
called:
>>> demo.value
calculating
42
On subsequent accesses the previously calculated value will be returned:
>>> demo.value
42
>>> demo.base = 15
>>> demo.value
42
Let's make sure the value is really calculated upon first access (and not
already during compilation or object creation):
>>> demo2 = Demo()
>>> demo2.base = 15
>>> demo2.value
calculating
105
>>> demo2.value
105

26
util/tests.py Executable file
View file

@ -0,0 +1,26 @@
# $Id$
import unittest
import doctest
import cybertools.util.property
class Test(unittest.TestCase):
"Basic tests for modules in the util package."
def testBasicStuff(self):
pass
def test_suite():
flags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS
return unittest.TestSuite((
#unittest.makeSuite(Test), # we don't need this
#doctest.DocTestSuite(cybertools.util.property, optionflags=flags),
doctest.DocFileSuite('adapter.txt', optionflags=flags),
doctest.DocFileSuite('property.txt', optionflags=flags),
))
if __name__ == '__main__':
unittest.main(defaultTest='test_suite')