From b403fd822b76a7deffe8110fbeb7993ef3cac3a5 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 14 Aug 2008 17:21:21 +0000 Subject: 159: Added plugable.DictProxy class; added corresponding unit tests; added setitem(), delitem() functions to tstutil --- ipalib/plugable.py | 70 ++++++++++++++++++++++++++++++++++++++++--- ipalib/tests/test_plugable.py | 57 ++++++++++++++++++++++++++++++++++- ipalib/tests/tstutil.py | 16 ++++++++++ 3 files changed, 138 insertions(+), 5 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 1df3f836c..66cb18fe5 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -426,8 +426,8 @@ class NameSpace(ReadOnly): def __contains__(self, name): """ - Returns True if this NameSpace contains a member named ``name``; returns - False otherwise. + Returns True if instance contains a member named ``name``, otherwise + False. :param name: The name of a potential member """ @@ -435,8 +435,10 @@ class NameSpace(ReadOnly): def __getitem__(self, name): """ - If this NameSpace contains a member named ``name``, returns that member; - otherwise raises KeyError. + Returns the member named ``name``. + + Raises KeyError if this NameSpace does not contain a member named + ``name``. :param name: The name of member to retrieve """ @@ -468,6 +470,66 @@ class NameSpace(ReadOnly): return '%s(<%d members>)' % (self.__class__.__name__, len(self)) +class DictProxy(ReadOnly): + """ + A read-only dict whose items can also be accessed as attributes. + + Although a DictProxy is read-only, the underlying dict can change (and is + assumed to). + + One of these is created for each allowed base class in a `Registrar` + instance. + """ + def __init__(self, d): + """ + :param d: The ``dict`` instance to proxy. + """ + self.__d = d + self.__lock__() + assert self.__islocked__() + + def __len__(self): + """ + Returns number of items in underlying ``dict``. + """ + return len(self.__d) + + def __iter__(self): + """ + Iterates through keys of underlying ``dict`` in ascending order. + """ + for name in sorted(self.__d): + yield name + + def __contains__(self, key): + """ + Returns True if underlying dict contains ``key``, False otherwise. + + :param key: The key to query upon. + """ + return key in self.__d + + def __getitem__(self, key): + """ + Returns value from underlying dict corresponding to ``key``. + + :param key: The key of the value to retrieve. + """ + if key in self.__d: + return self.__d[key] + raise KeyError('no item at key %r' % key) + + def __getattr__(self, name): + """ + Returns value from underlying dict corresponding to ``name``. + + :param name: The name of the attribute to retrieve. + """ + if name in self.__d: + return self.__d[name] + raise AttributeError('no attribute %r' % name) + + class Registrar(ReadOnly): def __init__(self, *allowed): """ diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index c65db0154..5c907dc7a 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -21,7 +21,8 @@ Unit tests for `ipalib.plugable` module. """ -from tstutil import raises, getitem, no_set, no_del, read_only +from tstutil import raises, no_set, no_del, read_only +from tstutil import getitem, setitem, delitem from tstutil import ClassChecker from ipalib import plugable, errors @@ -452,6 +453,60 @@ class test_NameSpace(ClassChecker): no_set(ns, name) +class test_DictProxy(ClassChecker): + """ + Tests the `plugable.DictProxy` class. + """ + _cls = plugable.DictProxy + + def test_class(self): + assert self.cls.__bases__ == (plugable.ReadOnly,) + + def test_DictProxy(self): + cnt = 10 + keys = [] + d = dict() + dictproxy = self.cls(d) + for i in xrange(cnt): + key = 'key_%d' % i + val = 'val_%d' % i + keys.append(key) + + # Test thet key does not yet exist + assert len(dictproxy) == i + assert key not in dictproxy + assert not hasattr(dictproxy, key) + raises(KeyError, getitem, dictproxy, key) + raises(AttributeError, getattr, dictproxy, key) + + # Test that items/attributes cannot be set on dictproxy: + raises(TypeError, setitem, dictproxy, key, val) + raises(AttributeError, setattr, dictproxy, key, val) + + # Test that additions in d are reflected in dictproxy: + d[key] = val + assert len(dictproxy) == i + 1 + assert key in dictproxy + assert hasattr(dictproxy, key) + assert dictproxy[key] is val + assert read_only(dictproxy, key) is val + + # Test __iter__ + assert list(dictproxy) == keys + + for key in keys: + # Test that items cannot be deleted through dictproxy: + raises(TypeError, delitem, dictproxy, key) + raises(AttributeError, delattr, dictproxy, key) + + # Test that deletions in d are reflected in dictproxy + del d[key] + assert len(dictproxy) == len(d) + assert key not in dictproxy + raises(KeyError, getitem, dictproxy, key) + raises(AttributeError, getattr, dictproxy, key) + + def test_Registrar(): class Base1(object): pass diff --git a/ipalib/tests/tstutil.py b/ipalib/tests/tstutil.py index 7b3a2d5ea..79e8ae383 100644 --- a/ipalib/tests/tstutil.py +++ b/ipalib/tests/tstutil.py @@ -60,6 +60,22 @@ def getitem(obj, key): return obj[key] +def setitem(obj, key, value): + """ + Works like setattr but for dictionary interface. Uses this in combination + with raises() to test that, for example, TypeError is raised. + """ + obj[key] = value + + +def delitem(obj, key): + """ + Works like delattr but for dictionary interface. Uses this in combination + with raises() to test that, for example, TypeError is raised. + """ + del obj[key] + + def no_set(obj, name, value='some_new_obj'): """ Tests that attribute cannot be set. -- cgit