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:
parent
0dda2f4b12
commit
174a0ab4f1
7 changed files with 304 additions and 0 deletions
26
util/README.txt
Normal file
26
util/README.txt
Normal 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
3
util/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
"""
|
||||||
|
$Id$
|
||||||
|
"""
|
52
util/adapter.py
Normal file
52
util/adapter.py
Normal 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
102
util/adapter.txt
Normal 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
41
util/property.py
Normal 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
54
util/property.txt
Normal 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
26
util/tests.py
Executable 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')
|
Loading…
Add table
Reference in a new issue