summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--ipalib/base.py240
-rw-r--r--tests/test_ipalib/test_base.py166
2 files changed, 404 insertions, 2 deletions
diff --git a/ipalib/base.py b/ipalib/base.py
index e427b747e..e3d08208c 100644
--- a/ipalib/base.py
+++ b/ipalib/base.py
@@ -23,7 +23,7 @@ Low-level functions and abstract base classes.
import re
from constants import NAME_REGEX, NAME_ERROR
-from constants import TYPE_ERROR, SET_ERROR, DEL_ERROR
+from constants import TYPE_ERROR, SET_ERROR, DEL_ERROR, OVERRIDE_ERROR
class ReadOnly(object):
@@ -189,6 +189,10 @@ def check_name(name):
"""
Verify that ``name`` is suitable for a `NameSpace` member name.
+ In short, ``name`` must be a valid lower-case Python identifier that
+ neither starts nor ends with an underscore. Otherwise an exception is
+ raised.
+
This function will raise a ``ValueError`` if ``name`` does not match the
`constants.NAME_REGEX` regular expression. For example:
@@ -223,3 +227,237 @@ def check_name(name):
NAME_ERROR % (NAME_REGEX, name)
)
return name
+
+
+class NameSpace(ReadOnly):
+ """
+ A read-only name-space with handy container behaviours.
+
+ A `NameSpace` instance is an ordered, immutable mapping object whose values
+ can also be accessed as attributes. A `NameSpace` instance is constructed
+ from an iterable providing its *members*, which are simply arbitrary objects
+ with a ``name`` attribute whose value:
+
+ 1. Is unique among the members
+
+ 2. Passes the `check_name()` function
+
+ Beyond that, no restrictions are placed on the members: they can be
+ classes or instances, and of any type.
+
+ The members can be accessed as attributes on the `NameSpace` instance or
+ through a dictionary interface. For example, say we create a `NameSpace`
+ instance from a list containing a single member, like this:
+
+ >>> class my_member(object):
+ ... name = 'my_name'
+ ...
+ >>> namespace = NameSpace([my_member])
+ >>> namespace
+ NameSpace(<1 member>, sort=True)
+ >>> my_member is namespace.my_name # As an attribute
+ True
+ >>> my_member is namespace['my_name'] # As dictionary item
+ True
+
+ For a more detailed example, say we create a `NameSpace` instance from a
+ generator like this:
+
+ >>> class Member(object):
+ ... def __init__(self, i):
+ ... self.i = i
+ ... self.name = 'member%d' % i
+ ... def __repr__(self):
+ ... return 'Member(%d)' % self.i
+ ...
+ >>> ns = NameSpace(Member(i) for i in xrange(3))
+ >>> ns
+ NameSpace(<3 members>, sort=True)
+
+ As above, the members can be accessed as attributes and as dictionary items:
+
+ >>> ns.member0 is ns['member0']
+ True
+ >>> ns.member1 is ns['member1']
+ True
+ >>> ns.member2 is ns['member2']
+ True
+
+ Members can also be accessed by index and by slice. For example:
+
+ >>> ns[0]
+ Member(0)
+ >>> ns[-1]
+ Member(2)
+ >>> ns[1:]
+ (Member(1), Member(2))
+
+ (Note that slicing a `NameSpace` returns a ``tuple``.)
+
+ `NameSpace` instances provide standard container emulation for membership
+ testing, counting, and iteration. For example:
+
+ >>> 'member3' in ns # Is there a member named 'member3'?
+ False
+ >>> 'member2' in ns # But there is a member named 'member2'
+ True
+ >>> len(ns) # The number of members
+ 3
+ >>> list(ns) # Iterate through the member names
+ ['member0', 'member1', 'member2']
+
+ Although not a standard container feature, the `NameSpace.__call__()` method
+ provides a convenient (and efficient) way to iterate through the members,
+ like an ordered version of the ``dict.itervalues()`` method. For example:
+
+ >>> list(ns[name] for name in ns) # One way to do it
+ [Member(0), Member(1), Member(2)]
+ >>> list(ns()) # A more efficient, less verbose way to do it
+ [Member(0), Member(1), Member(2)]
+
+ As another convenience, the `NameSpace.__todict__()` method will return copy
+ of the ``dict`` mapping the member names to the members. For example:
+
+ >>> ns.__todict__()
+ {'member1': Member(1), 'member0': Member(0), 'member2': Member(2)}
+
+
+ `NameSpace.__init__()` locks the instance, so `NameSpace` instances are
+ read-only from the get-go. For example:
+
+ >>> ns.member3 = Member(3) # Lets add that missing 'member3'
+ Traceback (most recent call last):
+ ...
+ AttributeError: locked: cannot set NameSpace.member3 to Member(3)
+
+ (For information on the locking protocol, see the `ReadOnly` class, of which
+ `NameSpace` is a subclass.)
+
+ By default the members will be sorted alphabetically by the member name.
+ For example:
+
+ >>> sorted_ns = NameSpace([Member(7), Member(3), Member(5)])
+ >>> sorted_ns
+ NameSpace(<3 members>, sort=True)
+ >>> list(sorted_ns)
+ ['member3', 'member5', 'member7']
+ >>> sorted_ns[0]
+ Member(3)
+
+ But if the instance is created with the ``sort=False`` keyword argument, the
+ original order of the members is preserved. For example:
+
+ >>> unsorted_ns = NameSpace([Member(7), Member(3), Member(5)], sort=False)
+ >>> unsorted_ns
+ NameSpace(<3 members>, sort=False)
+ >>> list(unsorted_ns)
+ ['member7', 'member3', 'member5']
+ >>> unsorted_ns[0]
+ Member(7)
+
+ The `NameSpace` class is used in many places throughout freeIPA. For a few
+ examples, see the `plugable.API` and the `frontend.Command` classes.
+ """
+
+ def __init__(self, members, sort=True):
+ """
+ :param members: An iterable providing the members.
+ :param sort: Whether to sort the members by member name.
+ """
+ if type(sort) is not bool:
+ raise TypeError(
+ TYPE_ERROR % ('sort', bool, sort, type(sort))
+ )
+ self.__sort = sort
+ if sort:
+ self.__members = tuple(
+ sorted(members, key=lambda m: m.name)
+ )
+ else:
+ self.__members = tuple(members)
+ self.__names = tuple(m.name for m in self.__members)
+ self.__map = dict()
+ for member in self.__members:
+ name = check_name(member.name)
+ if name in self.__map:
+ raise AttributeError(OVERRIDE_ERROR %
+ (self.__class__.__name__, name, self.__map[name], member)
+ )
+ assert not hasattr(self, name), 'Ouch! Has attribute %r' % name
+ self.__map[name] = member
+ setattr(self, name, member)
+ lock(self)
+
+ def __len__(self):
+ """
+ Return the number of members.
+ """
+ return len(self.__members)
+
+ def __iter__(self):
+ """
+ Iterate through the member names.
+
+ If this instance was created with ``sort=False``, the names will be in
+ the same order as the members were passed to the constructor; otherwise
+ the names will be in alphabetical order (which is the default).
+
+ This method is like an ordered version of ``dict.iterkeys()``.
+ """
+ for name in self.__names:
+ yield name
+
+ def __call__(self):
+ """
+ Iterate through the members.
+
+ If this instance was created with ``sort=False``, the members will be
+ in the same order as they were passed to the constructor; otherwise the
+ members will be in alphabetical order by name (which is the default).
+
+ This method is like an ordered version of ``dict.itervalues()``.
+ """
+ for member in self.__members:
+ yield member
+
+ def __contains__(self, name):
+ """
+ Return ``True`` if namespace has a member named ``name``.
+ """
+ return name in self.__map
+
+ def __getitem__(self, key):
+ """
+ Return a member by name or index, or return a slice of members.
+
+ :param key: The name or index of a member, or a slice object.
+ """
+ if type(key) is str:
+ return self.__map[key]
+ if type(key) in (int, slice):
+ return self.__members[key]
+ raise TypeError(
+ TYPE_ERROR % ('key', (str, int, slice), key, type(key))
+ )
+
+ def __repr__(self):
+ """
+ Return a pseudo-valid expression that could create this instance.
+ """
+ cnt = len(self)
+ if cnt == 1:
+ m = 'member'
+ else:
+ m = 'members'
+ return '%s(<%d %s>, sort=%r)' % (
+ self.__class__.__name__,
+ cnt,
+ m,
+ self.__sort,
+ )
+
+ def __todict__(self):
+ """
+ Return a copy of the private dict mapping member name to member.
+ """
+ return dict(self.__map)
diff --git a/tests/test_ipalib/test_base.py b/tests/test_ipalib/test_base.py
index 87e4c063d..ce88f23f8 100644
--- a/tests/test_ipalib/test_base.py
+++ b/tests/test_ipalib/test_base.py
@@ -23,7 +23,7 @@ Test the `ipalib.base` module.
from tests.util import ClassChecker, raises
from ipalib.constants import NAME_REGEX, NAME_ERROR
-from ipalib.constants import TYPE_ERROR, SET_ERROR, DEL_ERROR
+from ipalib.constants import TYPE_ERROR, SET_ERROR, DEL_ERROR, OVERRIDE_ERROR
from ipalib import base
@@ -186,3 +186,167 @@ def test_check_name():
for name in okay:
e = raises(ValueError, f, name.upper())
assert str(e) == NAME_ERROR % (NAME_REGEX, name.upper())
+
+
+def membername(i):
+ return 'member%03d' % i
+
+
+class DummyMember(object):
+ def __init__(self, i):
+ self.i = i
+ self.name = membername(i)
+
+
+def gen_members(*indexes):
+ return tuple(DummyMember(i) for i in indexes)
+
+
+class test_NameSpace(ClassChecker):
+ """
+ Test the `ipalib.base.NameSpace` class.
+ """
+ _cls = base.NameSpace
+
+ def new(self, count, sort=True):
+ members = tuple(DummyMember(i) for i in xrange(count, 0, -1))
+ assert len(members) == count
+ o = self.cls(members, sort=sort)
+ return (o, members)
+
+ def test_init(self):
+ """
+ Test the `ipalib.base.NameSpace.__init__` method.
+ """
+ o = self.cls([])
+ assert len(o) == 0
+ assert list(o) == []
+ assert list(o()) == []
+
+ # Test members as attribute and item:
+ for cnt in (3, 42):
+ for sort in (True, False):
+ (o, members) = self.new(cnt, sort=sort)
+ assert len(members) == cnt
+ for m in members:
+ assert getattr(o, m.name) is m
+ assert o[m.name] is m
+
+ # Test that TypeError is raised if sort is not a bool:
+ e = raises(TypeError, self.cls, [], sort=None)
+ assert str(e) == TYPE_ERROR % ('sort', bool, None, type(None))
+
+ # Test that AttributeError is raised with duplicate member name:
+ members = gen_members(0, 1, 2, 1, 3)
+ e = raises(AttributeError, self.cls, members)
+ assert str(e) == OVERRIDE_ERROR % (
+ 'NameSpace', membername(1), members[1], members[3]
+ )
+
+ def test_len(self):
+ """
+ Test the `ipalib.base.NameSpace.__len__` method.
+ """
+ for count in (5, 18, 127):
+ (o, members) = self.new(count)
+ assert len(o) == count
+ (o, members) = self.new(count, sort=False)
+ assert len(o) == count
+
+ def test_iter(self):
+ """
+ Test the `ipalib.base.NameSpace.__iter__` method.
+ """
+ (o, members) = self.new(25)
+ assert list(o) == sorted(m.name for m in members)
+ (o, members) = self.new(25, sort=False)
+ assert list(o) == list(m.name for m in members)
+
+ def test_call(self):
+ """
+ Test the `ipalib.base.NameSpace.__call__` method.
+ """
+ (o, members) = self.new(25)
+ assert list(o()) == sorted(members, key=lambda m: m.name)
+ (o, members) = self.new(25, sort=False)
+ assert tuple(o()) == members
+
+ def test_contains(self):
+ """
+ Test the `ipalib.base.NameSpace.__contains__` method.
+ """
+ yes = (99, 3, 777)
+ no = (9, 333, 77)
+ for sort in (True, False):
+ members = gen_members(*yes)
+ o = self.cls(members, sort=sort)
+ for i in yes:
+ assert membername(i) in o
+ assert membername(i).upper() not in o
+ for i in no:
+ assert membername(i) not in o
+
+ def test_getitem(self):
+ """
+ Test the `ipalib.base.NameSpace.__getitem__` method.
+ """
+ cnt = 17
+ for sort in (True, False):
+ (o, members) = self.new(cnt, sort=sort)
+ assert len(members) == cnt
+ if sort is True:
+ members = tuple(sorted(members, key=lambda m: m.name))
+
+ # Test str keys:
+ for m in members:
+ assert o[m.name] is m
+ e = raises(KeyError, o.__getitem__, 'nope')
+
+ # Test int indexes:
+ for i in xrange(cnt):
+ assert o[i] is members[i]
+ e = raises(IndexError, o.__getitem__, cnt)
+
+ # Test negative int indexes:
+ for i in xrange(1, cnt + 1):
+ assert o[-i] is members[-i]
+ e = raises(IndexError, o.__getitem__, -(cnt + 1))
+
+ # Test slicing:
+ assert o[3:] == members[3:]
+ assert o[:10] == members[:10]
+ assert o[3:10] == members[3:10]
+ assert o[-9:] == members[-9:]
+ assert o[:-4] == members[:-4]
+ assert o[-9:-4] == members[-9:-4]
+
+ # Test that TypeError is raised with wrong type
+ e = raises(TypeError, o.__getitem__, 3.0)
+ assert str(e) == TYPE_ERROR % ('key', (str, int, slice), 3.0, float)
+
+ def test_repr(self):
+ """
+ Test the `ipalib.base.NameSpace.__repr__` method.
+ """
+ for cnt in (0, 1, 2):
+ for sort in (True, False):
+ (o, members) = self.new(cnt, sort=sort)
+ if cnt == 1:
+ assert repr(o) == \
+ 'NameSpace(<%d member>, sort=%r)' % (cnt, sort)
+ else:
+ assert repr(o) == \
+ 'NameSpace(<%d members>, sort=%r)' % (cnt, sort)
+
+ def test_todict(self):
+ """
+ Test the `ipalib.base.NameSpace.__todict__` method.
+ """
+ for cnt in (3, 101):
+ for sort in (True, False):
+ (o, members) = self.new(cnt, sort=sort)
+ d = o.__todict__()
+ assert d == dict((m.name, m) for m in members)
+
+ # Test that a copy is returned:
+ assert o.__todict__() is not d