mirror of
https://salsa.debian.org/freeipa-team/freeipa.git
synced 2024-12-29 10:21:18 -06:00
7493d781df
The changes include: * Change license blobs in source files to mention GPLv3+ not GPLv2 only * Add GPLv3+ license text * Package COPYING not LICENSE as the license blobs (even the old ones) mention COPYING specifically, it is also more common, I think https://fedorahosted.org/freeipa/ticket/239
487 lines
15 KiB
Python
487 lines
15 KiB
Python
# 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, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# 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, see <http://www.gnu.org/licenses/>.
|
|
|
|
"""
|
|
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]$|^[a-z]$'; 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, name_attr='name'):
|
|
"""
|
|
: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: getattr(m, name_attr))
|
|
)
|
|
else:
|
|
self.__members = tuple(members)
|
|
self.__names = tuple(getattr(m, name_attr) for m in self.__members)
|
|
self.__map = dict()
|
|
for member in self.__members:
|
|
name = check_name(getattr(member, name_attr))
|
|
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 isinstance(key, basestring):
|
|
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)
|