From 174a0ab4f1f6b80a004bcd28ef9cc0feda7609c3 Mon Sep 17 00:00:00 2001 From: helmutm Date: Thu, 4 Jan 2007 14:09:48 +0000 Subject: [PATCH] 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 --- util/README.txt | 26 ++++++++++++ util/__init__.py | 3 ++ util/adapter.py | 52 +++++++++++++++++++++++ util/adapter.txt | 102 ++++++++++++++++++++++++++++++++++++++++++++++ util/property.py | 41 +++++++++++++++++++ util/property.txt | 54 ++++++++++++++++++++++++ util/tests.py | 26 ++++++++++++ 7 files changed, 304 insertions(+) create mode 100644 util/README.txt create mode 100644 util/__init__.py create mode 100644 util/adapter.py create mode 100644 util/adapter.txt create mode 100644 util/property.py create mode 100644 util/property.txt create mode 100755 util/tests.py diff --git a/util/README.txt b/util/README.txt new file mode 100644 index 0000000..ff14ef4 --- /dev/null +++ b/util/README.txt @@ -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 .py file with an implementation and a +corresponding .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. diff --git a/util/__init__.py b/util/__init__.py new file mode 100644 index 0000000..38314f3 --- /dev/null +++ b/util/__init__.py @@ -0,0 +1,3 @@ +""" +$Id$ +""" diff --git a/util/adapter.py b/util/adapter.py new file mode 100644 index 0000000..1717a3f --- /dev/null +++ b/util/adapter.py @@ -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 diff --git a/util/adapter.txt b/util/adapter.txt new file mode 100644 index 0000000..3afdd1f --- /dev/null +++ b/util/adapter.txt @@ -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 + + diff --git a/util/property.py b/util/property.py new file mode 100644 index 0000000..0881d4a --- /dev/null +++ b/util/property.py @@ -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 diff --git a/util/property.txt b/util/property.txt new file mode 100644 index 0000000..62629f0 --- /dev/null +++ b/util/property.txt @@ -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 + diff --git a/util/tests.py b/util/tests.py new file mode 100755 index 0000000..71de297 --- /dev/null +++ b/util/tests.py @@ -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')