diff --git a/index/README.txt b/index/README.txt new file mode 100644 index 0000000..5aafa53 --- /dev/null +++ b/index/README.txt @@ -0,0 +1,56 @@ +======================================== +Indexed Collections for Various Purposes +======================================== + +Multikey Dictionaries +===================== + +A MultiKeyDict is a dictionary that expects its keys to be tuples. + + >>> from cybertools.index.multikey import MultiKeyDict + >>> registry = MultiKeyDict() + + >>> registry[('index.html',)] = 'global index.html' + + >>> registry[('index.html',)] + 'global index.html' + +So this would be nothing special - any dictionary is able to provide this +functionality; but a MultiKeyDict has some fallback mechanisms for retrieving +objects only partly fitting the requested key: + + >>> registry.get(('index.html', 'topic', 'zope3', 'Custom')) + 'global index.html' + + >>> registry[('index.html', 'topic', 'zope3', 'Custom')] + 'global index.html' + + >>> registry[('index.html', 'topic',)] = 'index.html for type "topic"' + + >>> registry[('index.html', 'topic', 'zope3', 'Custom')] + 'index.html for type "topic"' + +It is also possible to keep intermediate parts of a key variable by +setting them to None: + + >>> registry[('index.html', None, None, 'Custom')] = 'Global index.html for Custom skin' + +The more on the left a matching key part is the higher is its priority: + + >>> registry[('index.html', 'topic', 'zope3', 'Custom')] + 'index.html for type "topic"' + + >>> registry[('index.html', 'task', 'bugfixes', 'Custom')] + 'Global index.html for Custom skin' + + + >>> registry[('edit.html', 'topic', 'zope3', 'Custom')] = 'very special edit.html' + + >>> registry[('index.html', 'task', 'bugfixes', 'Custom')] + 'Global index.html for Custom skin' + + >>> registry[('index.html', 'topic', 'zope3', 'Custom')] + 'index.html for type "topic"' + + >>> registry.get(('edit.html', 'task', 'bugfixes', 'Custom')) + diff --git a/index/__init__.py b/index/__init__.py new file mode 100644 index 0000000..38314f3 --- /dev/null +++ b/index/__init__.py @@ -0,0 +1,3 @@ +""" +$Id$ +""" diff --git a/index/multikey.py b/index/multikey.py new file mode 100644 index 0000000..d0fd80d --- /dev/null +++ b/index/multikey.py @@ -0,0 +1,84 @@ +# +# Copyright (c) 2006 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 +# + +""" +Dictionaries with multiple keys. + +$Id$ +""" + +_not_found = object() +_default = object() + +class MultiKeyDict(dict): + + def __init__(self, **kw): + super(MultiKeyDict, self).__init__(**kw) + self.singleKeyDict = {} + + def __setitem__(self, key, value): + assert type(key) is tuple + super(MultiKeyDict, self).__setitem__(key, value) + for n, k in enumerate(key): + if k: + entry = self.singleKeyDict.setdefault((n, k), []) + if value not in entry: + entry.append((key, value)) + + def __getitem__(self, key): + r = self.get(key, _default) + if r is _default: + raise KeyError(key) + return r + + def get(self, key, default=None): + assert type(key) is tuple + kl = list(key) + while kl: + r = super(MultiKeyDict, self).get(tuple(kl), _not_found) + if r is not _not_found: + return r + kl.pop() + return default + + def get(self, key, default=None): + assert type(key) is tuple + firstTry = super(MultiKeyDict, self).get(key, _not_found) + # fast return for full match: + if firstTry is not _not_found: + return firstTry + collector = {} + for n, k in enumerate(key): + rList = self.singleKeyDict.get((n, k), []) + for r in rList: + skip = False + for nx, kx in enumerate(r[0]): + if kx and kx != key[nx]: # if stored key elements are present + skip = True # they must match + break + if skip: + continue + entry = collector.setdefault(r[1], []) + entry.append(n) + if not collector: + return default + #print 'collector', collector + results = sorted((-len(value), value, o) for o, value in collector.items()) + #print 'sorted', results + return results[0][2] + diff --git a/index/tests.py b/index/tests.py new file mode 100755 index 0000000..b9e2484 --- /dev/null +++ b/index/tests.py @@ -0,0 +1,30 @@ +#! /usr/bin/python + +""" +Tests for the 'cybertools.index' package. + +$Id$ +""" + +import unittest, doctest +from zope.testing.doctestunit import DocFileSuite + +from cybertools.index import multikey + + +class Test(unittest.TestCase): + "Basic tests for the index package." + + def testBasicStuff(self): + pass + + +def test_suite(): + flags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS + return unittest.TestSuite(( + DocFileSuite('README.txt', optionflags=flags), + unittest.makeSuite(Test), + )) + +if __name__ == '__main__': + unittest.main(defaultTest='test_suite')