summaryrefslogtreecommitdiffstats
path: root/ipalib/base.py
diff options
context:
space:
mode:
authorRob Crittenden <rcritten@redhat.com>2009-01-22 10:42:41 -0500
committerRob Crittenden <rcritten@redhat.com>2009-01-22 10:42:41 -0500
commitc2967a675a288e7d31374229fd974d0cb9966f2c (patch)
tree58be8ca6319f4660d9f18b97a37b9c0c56104d02 /ipalib/base.py
parent2b8b87b4d6c3b4389a0a7bf48c225035c53e7ad1 (diff)
parent5d82e3b35a8fb2d4c25f282cddad557a7650197c (diff)
downloadfreeipa.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.py486
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)