mirror of
https://salsa.debian.org/freeipa-team/freeipa.git
synced 2024-12-23 07:33:27 -06:00
Copied plugable.NameSpace to base.NameSpace and made many docstring and unit test improvements
This commit is contained in:
parent
57dae28d9c
commit
86325bf4eb
240
ipalib/base.py
240
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)
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user