diff options
author | Rob Crittenden <rcritten@redhat.com> | 2009-01-22 10:42:41 -0500 |
---|---|---|
committer | Rob Crittenden <rcritten@redhat.com> | 2009-01-22 10:42:41 -0500 |
commit | c2967a675a288e7d31374229fd974d0cb9966f2c (patch) | |
tree | 58be8ca6319f4660d9f18b97a37b9c0c56104d02 /ipalib/base.py | |
parent | 2b8b87b4d6c3b4389a0a7bf48c225035c53e7ad1 (diff) | |
parent | 5d82e3b35a8fb2d4c25f282cddad557a7650197c (diff) | |
download | freeipa.git-c2967a675a288e7d31374229fd974d0cb9966f2c.tar.gz freeipa.git-c2967a675a288e7d31374229fd974d0cb9966f2c.tar.xz freeipa.git-c2967a675a288e7d31374229fd974d0cb9966f2c.zip |
Merge branch 'master' of git://fedorapeople.org/~jderose/freeipa2
Diffstat (limited to 'ipalib/base.py')
-rw-r--r-- | ipalib/base.py | 486 |
1 files changed, 486 insertions, 0 deletions
diff --git a/ipalib/base.py b/ipalib/base.py new file mode 100644 index 00000000..bff8f195 --- /dev/null +++ b/ipalib/base.py @@ -0,0 +1,486 @@ +# Authors: +# Jason Gerard DeRose <jderose@redhat.com> +# +# Copyright (C) 2008 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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; version 2 only +# +# 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 + +""" +Foundational classes and functions. +""" + +import re +from constants import NAME_REGEX, NAME_ERROR +from constants import TYPE_ERROR, SET_ERROR, DEL_ERROR, OVERRIDE_ERROR + + +class ReadOnly(object): + """ + Base class for classes that can be locked into a read-only state. + + Be forewarned that Python does not offer true read-only attributes for + user-defined classes. Do *not* rely upon the read-only-ness of this + class for security purposes! + + The point of this class is not to make it impossible to set or to delete + attributes after an instance is locked, but to make it impossible to do so + *accidentally*. Rather than constantly reminding our programmers of things + like, for example, "Don't set any attributes on this ``FooBar`` instance + because doing so wont be thread-safe", this class offers a real way to + enforce read-only attribute usage. + + For example, before a `ReadOnly` instance is locked, you can set and delete + its attributes as normal: + + >>> class Person(ReadOnly): + ... pass + ... + >>> p = Person() + >>> p.name = 'John Doe' + >>> p.phone = '123-456-7890' + >>> del p.phone + + But after an instance is locked, you cannot set its attributes: + + >>> p.__islocked__() # Is this instance locked? + False + >>> p.__lock__() # This will lock the instance + >>> p.__islocked__() + True + >>> p.department = 'Engineering' + Traceback (most recent call last): + ... + AttributeError: locked: cannot set Person.department to 'Engineering' + + Nor can you deleted its attributes: + + >>> del p.name + Traceback (most recent call last): + ... + AttributeError: locked: cannot delete Person.name + + However, as noted at the start, there are still obscure ways in which + attributes can be set or deleted on a locked `ReadOnly` instance. For + example: + + >>> object.__setattr__(p, 'department', 'Engineering') + >>> p.department + 'Engineering' + >>> object.__delattr__(p, 'name') + >>> hasattr(p, 'name') + False + + But again, the point is that a programmer would never employ the above + techniques *accidentally*. + + Lastly, this example aside, you should use the `lock()` function rather + than the `ReadOnly.__lock__()` method. And likewise, you should + use the `islocked()` function rather than the `ReadOnly.__islocked__()` + method. For example: + + >>> readonly = ReadOnly() + >>> islocked(readonly) + False + >>> lock(readonly) is readonly # lock() returns the instance + True + >>> islocked(readonly) + True + """ + + __locked = False + + def __lock__(self): + """ + Put this instance into a read-only state. + + After the instance has been locked, attempting to set or delete an + attribute will raise an AttributeError. + """ + assert self.__locked is False, '__lock__() can only be called once' + self.__locked = True + + def __islocked__(self): + """ + Return True if instance is locked, otherwise False. + """ + return self.__locked + + def __setattr__(self, name, value): + """ + If unlocked, set attribute named ``name`` to ``value``. + + If this instance is locked, an AttributeError will be raised. + + :param name: Name of attribute to set. + :param value: Value to assign to attribute. + """ + if self.__locked: + raise AttributeError( + SET_ERROR % (self.__class__.__name__, name, value) + ) + return object.__setattr__(self, name, value) + + def __delattr__(self, name): + """ + If unlocked, delete attribute named ``name``. + + If this instance is locked, an AttributeError will be raised. + + :param name: Name of attribute to delete. + """ + if self.__locked: + raise AttributeError( + DEL_ERROR % (self.__class__.__name__, name) + ) + return object.__delattr__(self, name) + + +def lock(instance): + """ + Lock an instance of the `ReadOnly` class or similar. + + This function can be used to lock instances of any class that implements + the same locking API as the `ReadOnly` class. For example, this function + can lock instances of the `config.Env` class. + + So that this function can be easily used within an assignment, ``instance`` + is returned after it is locked. For example: + + >>> readonly = ReadOnly() + >>> readonly is lock(readonly) + True + >>> readonly.attr = 'This wont work' + Traceback (most recent call last): + ... + AttributeError: locked: cannot set ReadOnly.attr to 'This wont work' + + Also see the `islocked()` function. + + :param instance: The instance of `ReadOnly` (or similar) to lock. + """ + assert instance.__islocked__() is False, 'already locked: %r' % instance + instance.__lock__() + assert instance.__islocked__() is True, 'failed to lock: %r' % instance + return instance + + +def islocked(instance): + """ + Return ``True`` if ``instance`` is locked. + + This function can be used on an instance of the `ReadOnly` class or an + instance of any other class implemented the same locking API. + + For example: + + >>> readonly = ReadOnly() + >>> islocked(readonly) + False + >>> readonly.__lock__() + >>> islocked(readonly) + True + + Also see the `lock()` function. + + :param instance: The instance of `ReadOnly` (or similar) to interrogate. + """ + assert ( + hasattr(instance, '__lock__') and callable(instance.__lock__) + ), 'no __lock__() method: %r' % instance + return instance.__islocked__() + + +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: + + >>> check_name('MyName') + Traceback (most recent call last): + ... + ValueError: name must match '^[a-z][_a-z0-9]*[a-z0-9]$'; got 'MyName' + + Also, this function will raise a ``TypeError`` if ``name`` is not an + ``str`` instance. For example: + + >>> check_name(u'my_name') + Traceback (most recent call last): + ... + TypeError: name: need a <type 'str'>; got u'my_name' (a <type 'unicode'>) + + So that `check_name()` can be easily used within an assignment, ``name`` + is returned unchanged if it passes the check. For example: + + >>> n = check_name('my_name') + >>> n + 'my_name' + + :param name: Identifier to test. + """ + if type(name) is not str: + raise TypeError( + TYPE_ERROR % ('name', str, name, type(name)) + ) + if re.match(NAME_REGEX, name) is None: + raise ValueError( + 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) + + We can then access ``my_member`` both as an attribute and as a dictionary + item: + + >>> 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* + (as opposed to the member names). Think of it 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, simpler way to do it + [Member(0), Member(1), Member(2)] + + Another convenience method is `NameSpace.__todict__()`, which will return + a copy of the ``dict`` mapping the member names to the members. + For example: + + >>> ns.__todict__() + {'member1': Member(1), 'member0': Member(0), 'member2': Member(2)} + + As `NameSpace.__init__()` locks the instance, `NameSpace` instances are + read-only from the get-go. An ``AttributeError`` is raised if you try to + set *any* attribute on a `NameSpace` instance. 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) |