From 556abfaf0becbeafa5cad50b2b2866a76e587156 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 18 Jul 2008 17:51:34 +0000 Subject: 1: Started roughing out ipalib package --- ipalib/__init__.py | 18 +++++++++++++++++ ipalib/base.py | 45 ++++++++++++++++++++++++++++++++++++++++++ ipalib/exceptions.py | 50 +++++++++++++++++++++++++++++++++++++++++++++++ ipalib/tests/__init__.py | 22 +++++++++++++++++++++ ipalib/tests/test_base.py | 4 ++++ 5 files changed, 139 insertions(+) create mode 100644 ipalib/__init__.py create mode 100644 ipalib/base.py create mode 100644 ipalib/exceptions.py create mode 100644 ipalib/tests/__init__.py create mode 100644 ipalib/tests/test_base.py (limited to 'ipalib') diff --git a/ipalib/__init__.py b/ipalib/__init__.py new file mode 100644 index 00000000..8eb39d3c --- /dev/null +++ b/ipalib/__init__.py @@ -0,0 +1,18 @@ +# Authors: +# Jason Gerard DeRose +# +# 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 diff --git a/ipalib/base.py b/ipalib/base.py new file mode 100644 index 00000000..fabc1acf --- /dev/null +++ b/ipalib/base.py @@ -0,0 +1,45 @@ +class Command(object): + def normalize(self, kw): + raise NotImplementedError + + def validate(self, kw): + raise NotImplementedError + + def execute(self, kw): + raise NotImplementedError + + def __call__(self, **kw): + kw = self.normalize(kw) + invalid = self.validate(kw) + if invalid: + return invalid + return self.execute(kw) + + + +class Argument(object): + pass + + +class NameSpace(object): + def __init__(self): + pass + + + + +class API(object): + def __init__(self): + self.__c = object() + self.__o = object() + + def __get_c(self): + return self.__c + c = property(__get_c) + + def __get_o(self): + return self.__o + o = property(__get_o) + + def register_command(self, name, callback, override=False): + pass diff --git a/ipalib/exceptions.py b/ipalib/exceptions.py new file mode 100644 index 00000000..d42ab9f0 --- /dev/null +++ b/ipalib/exceptions.py @@ -0,0 +1,50 @@ +# Authors: +# Jason Gerard DeRose +# +# 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 + +""" +All custom exceptions raised by `ipalib` package. +""" + +class IPAException(Exception): + """ + Use this base class for your custom IPA exceptions unless there is a + specific reason to subclass from AttributeError, KeyError, etc. + """ + format = None + + def __init__(self, *args, **kw): + self.args = args + self.kw = kw + + def __str__(self): + """ + Returns the string representation of this exception. + """ + if self.format is None: + if len(self.args) == 1: + return unicode(self.args[0]) + return unicode(self.args) + if len(self.args) > 0: + return self.format % self.args + return self.format % self.kw + + + +class CommandOverride(IPAException): + format = 'Cannot override command %r' diff --git a/ipalib/tests/__init__.py b/ipalib/tests/__init__.py new file mode 100644 index 00000000..d3658c45 --- /dev/null +++ b/ipalib/tests/__init__.py @@ -0,0 +1,22 @@ +# Authors: +# Jason Gerard DeRose +# +# 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 + +""" +Unit tests for `ipalib` package. +""" diff --git a/ipalib/tests/test_base.py b/ipalib/tests/test_base.py new file mode 100644 index 00000000..73b85c2a --- /dev/null +++ b/ipalib/tests/test_base.py @@ -0,0 +1,4 @@ +from ipalib import base + +def test_stuff(): + pass -- cgit From 00f4da79a900ae2af1db82e4e697bd2552cdabc5 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 18 Jul 2008 20:31:12 +0000 Subject: 2: Got basics of NameSpace working, added corresponding unit tests --- ipalib/__init__.py | 4 +++ ipalib/base.py | 47 ++++++++++++++++++++++++-- ipalib/exceptions.py | 17 +++++----- ipalib/tests/test_base.py | 85 +++++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 138 insertions(+), 15 deletions(-) (limited to 'ipalib') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index 8eb39d3c..ddce3ac9 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -16,3 +16,7 @@ # 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 + +""" +IPA library. +""" diff --git a/ipalib/base.py b/ipalib/base.py index fabc1acf..70cfe567 100644 --- a/ipalib/base.py +++ b/ipalib/base.py @@ -1,3 +1,29 @@ +# Authors: +# Jason Gerard DeRose +# +# 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 + +""" +Base classes in for plug-in architecture and generative API. +""" + +from exceptions import NameSpaceError + + class Command(object): def normalize(self, kw): raise NotImplementedError @@ -16,14 +42,29 @@ class Command(object): return self.execute(kw) - class Argument(object): pass class NameSpace(object): - def __init__(self): - pass + def __init__(self, kw): + assert isinstance(kw, dict) + self.__kw = dict(kw) + for (key, value) in self.__kw.items(): + assert not key.startswith('_') + setattr(self, key, value) + self.__keys = sorted(self.__kw) + + def __getitem__(self, key): + return self.__kw[key] + + def __iter__(self): + for key in self.__keys: + yield key + + + + diff --git a/ipalib/exceptions.py b/ipalib/exceptions.py index d42ab9f0..d1f3ab9e 100644 --- a/ipalib/exceptions.py +++ b/ipalib/exceptions.py @@ -2,7 +2,7 @@ # Jason Gerard DeRose # # Copyright (C) 2008 Red Hat -# see file 'COPYING' for use and warranty information +# see file 'COPYING' for use and warranty inmsgion # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License as @@ -21,12 +21,12 @@ All custom exceptions raised by `ipalib` package. """ -class IPAException(Exception): +class IPAError(Exception): """ Use this base class for your custom IPA exceptions unless there is a specific reason to subclass from AttributeError, KeyError, etc. """ - format = None + msg = None def __init__(self, *args, **kw): self.args = args @@ -36,15 +36,14 @@ class IPAException(Exception): """ Returns the string representation of this exception. """ - if self.format is None: + if self.msg is None: if len(self.args) == 1: return unicode(self.args[0]) return unicode(self.args) if len(self.args) > 0: - return self.format % self.args - return self.format % self.kw + return self.msg % self.args + return self.msg % self.kw - -class CommandOverride(IPAException): - format = 'Cannot override command %r' +class NameSpaceError(IPAError): + msg = 'Cannot set %r: NameSpace does not allow attribute setting' diff --git a/ipalib/tests/test_base.py b/ipalib/tests/test_base.py index 73b85c2a..432b9120 100644 --- a/ipalib/tests/test_base.py +++ b/ipalib/tests/test_base.py @@ -1,4 +1,83 @@ -from ipalib import base +# Authors: +# Jason Gerard DeRose +# +# 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 -def test_stuff(): - pass +""" +Unit tests for `ipalib.base` module. +""" + +from ipalib import base, exceptions + + +class test_NameSpace(): + """ + Unit tests for `NameSpace` class. + """ + + def ns(self, kw): + """ + Returns a new NameSpace instance. + """ + return base.NameSpace(kw) + + def kw(self): + """ + Returns standard test kw dict suitable for passing to + NameSpace.__init__(). + """ + return dict( + attr_a='Hello', + attr_b='all', + attr_c='yall!', + ) + + def std(self): + """ + Returns standard (kw, ns) tuple. + """ + kw = self.kw() + ns = self.ns(kw) + return (kw, ns) + + def test_public(self): + """ + Test that NameSpace instance created with empty dict has no public + attributes. + """ + ns = self.ns({}) + assert list(ns) == [] + for name in dir(ns): + assert name.startswith('_') or name.startswith('_NameSpace__') + + def test_iter(self): + """ + Test that __iter__() method returns sorted list of attribute names. + """ + (kw, ns) = self.std() + assert list(ns) == sorted(kw) + assert [ns[k] for k in ns] == ['Hello', 'all', 'yall!'] + + def test_dict_vs_attr(self): + """ + Tests NameSpace.__getitem__() and NameSpace.__getattr__() return the + same values. + """ + (kw, ns) = self.std() + for (key, val) in kw.items(): + assert ns[key] is val + assert getattr(ns, key) is val -- cgit From 5470a0d29a9131a5b95e6092df898ee579600e07 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sat, 19 Jul 2008 00:56:09 +0000 Subject: 3: Finished NameSpace and cerresponding unit tests --- ipalib/base.py | 78 +++++++++++++++++++++++++++++++++++++---- ipalib/exceptions.py | 2 +- ipalib/tests/test_base.py | 88 +++++++++++++++++++++++++++++++++++++++++------ 3 files changed, 150 insertions(+), 18 deletions(-) (limited to 'ipalib') diff --git a/ipalib/base.py b/ipalib/base.py index 70cfe567..eb84dd12 100644 --- a/ipalib/base.py +++ b/ipalib/base.py @@ -18,10 +18,10 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ -Base classes in for plug-in architecture and generative API. +Base classes for plug-in architecture and generative API. """ -from exceptions import NameSpaceError +from exceptions import SetAttributeError class Command(object): @@ -47,26 +47,90 @@ class Argument(object): class NameSpace(object): + """ + A read-only namespace of (key, value) pairs that can be accessed + both as instance attributes and as dictionary items. For example: + + >>> ns = NameSpace(dict(my_message='Hello world!')) + >>> ns.my_message + 'Hello world!' + >>> ns['my_message'] + 'Hello world!' + + Keep in mind that Python doesn't offer true ready-only attributes. A + NameSpace is read-only in that it prevents programmers from + *accidentally* setting its attributes, but a motivated programmer can + still set them. + + For example, setting an attribute the normal way will raise an exception: + + >>> ns.my_message = 'some new value' + (raises ipalib.exceptions.SetAttributeError) + + But a programmer could still set the attribute like this: + + >>> ns.__dict__['my_message'] = 'some new value' + + You should especially not implement a security feature that relies upon + NameSpace being strictly read-only. + """ + + __locked = False # Whether __setattr__ has been locked + def __init__(self, kw): + """ + The single constructor argument `kw` is a dict of the (key, value) + pairs to be in this NameSpace instance. + """ assert isinstance(kw, dict) self.__kw = dict(kw) for (key, value) in self.__kw.items(): assert not key.startswith('_') setattr(self, key, value) self.__keys = sorted(self.__kw) + self.__locked = True + + def __setattr__(self, name, value): + """ + Raises an exception if trying to set an attribute after the + NameSpace has been locked; otherwise calls object.__setattr__(). + """ + if self.__locked: + raise SetAttributeError(name) + super(NameSpace, self).__setattr__(name, value) def __getitem__(self, key): + """ + Returns item from namespace named `key`. + """ return self.__kw[key] - def __iter__(self): - for key in self.__keys: - yield key - - + def __hasitem__(self, key): + """ + Returns True if namespace has an item named `key`. + """ + return key in self.__kw + def __iter__(self): + """ + Yields the names in this NameSpace in ascending order. + For example: + >>> ns = NameSpace(dict(attr_b='world', attr_a='hello')) + >>> list(ns) + ['attr_a', 'attr_b'] + >>> [ns[k] for k in ns] + ['hello', 'world'] + """ + for key in self.__keys: + yield key + def __len__(self): + """ + Returns number of items in this NameSpace. + """ + return len(self.__keys) class API(object): diff --git a/ipalib/exceptions.py b/ipalib/exceptions.py index d1f3ab9e..2c1e5a55 100644 --- a/ipalib/exceptions.py +++ b/ipalib/exceptions.py @@ -45,5 +45,5 @@ class IPAError(Exception): return self.msg % self.kw -class NameSpaceError(IPAError): +class SetAttributeError(IPAError): msg = 'Cannot set %r: NameSpace does not allow attribute setting' diff --git a/ipalib/tests/test_base.py b/ipalib/tests/test_base.py index 432b9120..42cb89a1 100644 --- a/ipalib/tests/test_base.py +++ b/ipalib/tests/test_base.py @@ -56,28 +56,96 @@ class test_NameSpace(): def test_public(self): """ - Test that NameSpace instance created with empty dict has no public - attributes. + Tests that a NameSpace instance created with empty dict has no public + attributes (that would then conflict with names we want to assign to + the NameSpace). Also tests that a NameSpace instance created with a + non-empty dict has no unexpected public methods. """ ns = self.ns({}) assert list(ns) == [] + assert len(ns) == 0 for name in dir(ns): - assert name.startswith('_') or name.startswith('_NameSpace__') + assert name.startswith('__') or name.startswith('_NameSpace__') + (kw, ns) = self.std() + keys = set(kw) + for name in dir(ns): + assert ( + name.startswith('__') or + name.startswith('_NameSpace__') or + name in keys + ) + + def test_dict_vs_attr(self): + """ + Tests that NameSpace.__getitem__() and NameSpace.__getattr__() return + the same values. + """ + (kw, ns) = self.std() + assert len(kw) > 0 + assert len(kw) == len(list(ns)) + for (key, val) in kw.items(): + assert ns[key] is val + assert getattr(ns, key) is val + + def test_setattr(self): + """ + Tests that attributes cannot be set on NameSpace instance. + """ + (kw, ns) = self.std() + value = 'new value' + for key in kw: + raised = False + try: + setattr(ns, key, value) + except exceptions.SetAttributeError: + raised = True + assert raised + assert getattr(ns, key, None) != value + assert ns[key] != value + + def test_setitem(self): + """ + Tests that attributes cannot be set via NameSpace dict interface. + """ + (kw, ns) = self.std() + value = 'new value' + for key in kw: + raised = False + try: + ns[key] = value + except TypeError: + raised = True + assert raised + assert getattr(ns, key, None) != value + assert ns[key] != value + + def test_hasitem(self): + """ + Test __hasitem__() membership method. + """ + (kw, ns) = self.std() + nope = [ + 'attr_d', + 'attr_e', + 'whatever', + ] + for key in kw: + assert key in ns + for key in nope: + assert key not in kw + assert key not in ns def test_iter(self): """ - Test that __iter__() method returns sorted list of attribute names. + Tests that __iter__() method returns sorted list of attribute names. """ (kw, ns) = self.std() assert list(ns) == sorted(kw) assert [ns[k] for k in ns] == ['Hello', 'all', 'yall!'] - def test_dict_vs_attr(self): + def test_len(self): """ - Tests NameSpace.__getitem__() and NameSpace.__getattr__() return the - same values. + Test __len__() method. """ (kw, ns) = self.std() - for (key, val) in kw.items(): - assert ns[key] is val - assert getattr(ns, key) is val + assert len(kw) == len(ns) == 3 -- cgit From ef7594ffe1bad349dc539f69ee90708460999a71 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sat, 19 Jul 2008 04:28:03 +0000 Subject: 4: Got basics of API.register_command() working; added corresponding unit tests --- ipalib/__init__.py | 4 +++ ipalib/base.py | 70 ++++++++++++++++++++++++++++++++++------------- ipalib/exceptions.py | 8 ++++++ ipalib/tests/test_base.py | 65 ++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 127 insertions(+), 20 deletions(-) (limited to 'ipalib') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index ddce3ac9..1337d812 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -20,3 +20,7 @@ """ IPA library. """ + +import base + +api = base.API() diff --git a/ipalib/base.py b/ipalib/base.py index eb84dd12..96dd300d 100644 --- a/ipalib/base.py +++ b/ipalib/base.py @@ -21,10 +21,24 @@ Base classes for plug-in architecture and generative API. """ -from exceptions import SetAttributeError +import inspect +import exceptions -class Command(object): +class Named(object): + #def __init__(self, prefix): + # clsname = self.__class__.__name__ + def __get_name(self): + return self.__class__.__name__ + name = property(__get_name) + + def __get_cli_name(self): + return self.name.replace('_', '-') + cli_name = property(__get_cli_name) + + +class Command(Named): + def normalize(self, kw): raise NotImplementedError @@ -35,11 +49,11 @@ class Command(object): raise NotImplementedError def __call__(self, **kw): - kw = self.normalize(kw) - invalid = self.validate(kw) + normalized = self.normalize(kw) + invalid = self.validate(normalized) if invalid: return invalid - return self.execute(kw) + return self.execute(normalize) class Argument(object): @@ -65,7 +79,7 @@ class NameSpace(object): For example, setting an attribute the normal way will raise an exception: >>> ns.my_message = 'some new value' - (raises ipalib.exceptions.SetAttributeError) + (raises exceptions.SetAttributeError) But a programmer could still set the attribute like this: @@ -96,7 +110,7 @@ class NameSpace(object): NameSpace has been locked; otherwise calls object.__setattr__(). """ if self.__locked: - raise SetAttributeError(name) + raise exceptions.SetAttributeError(name) super(NameSpace, self).__setattr__(name, value) def __getitem__(self, key): @@ -134,17 +148,35 @@ class NameSpace(object): class API(object): - def __init__(self): - self.__c = object() - self.__o = object() - - def __get_c(self): - return self.__c - c = property(__get_c) + __commands = None + __objects = None + __locked = False - def __get_o(self): - return self.__o - o = property(__get_o) - - def register_command(self, name, callback, override=False): + def __init__(self): + self.__c = {} # Proposed commands + self.__o = {} # Proposed objects + + def __get_objects(self): + return self.__objects + objects = property(__get_objects) + + def __get_commands(self): + return self.__commands + commands = property(__get_commands) + + def __merge(self, target, base, cls, override): + assert type(target) is dict + assert inspect.isclass(base) + assert inspect.isclass(cls) + assert type(override) is bool + if not issubclass(cls, base): + raise exceptions.RegistrationError( + cls, + '%s.%s' % (base.__module__, base.__name__) + ) + + def register_command(self, cls, override=False): + self.__merge(self.__c, Command, cls, override) + + def finalize(self): pass diff --git a/ipalib/exceptions.py b/ipalib/exceptions.py index 2c1e5a55..752a1e20 100644 --- a/ipalib/exceptions.py +++ b/ipalib/exceptions.py @@ -47,3 +47,11 @@ class IPAError(Exception): class SetAttributeError(IPAError): msg = 'Cannot set %r: NameSpace does not allow attribute setting' + + +class OverrideError(IPAError): + msg = 'Unexpected override of %r; use override=True if intended' + + +class RegistrationError(IPAError): + msg = '%r is not a subclass of %s' diff --git a/ipalib/tests/test_base.py b/ipalib/tests/test_base.py index 42cb89a1..7a998f3c 100644 --- a/ipalib/tests/test_base.py +++ b/ipalib/tests/test_base.py @@ -24,7 +24,23 @@ Unit tests for `ipalib.base` module. from ipalib import base, exceptions -class test_NameSpace(): +def read_only(obj, name): + """ + Check that a given property is read-only. + Returns the value of the property. + """ + assert isinstance(obj, object) + assert hasattr(obj, name) + raised = False + try: + setattr(obj, name, 'some new obj') + except AttributeError: + raised = True + assert raised + return getattr(obj, name) + + +class test_NameSpace: """ Unit tests for `NameSpace` class. """ @@ -149,3 +165,50 @@ class test_NameSpace(): """ (kw, ns) = self.std() assert len(kw) == len(ns) == 3 + + +class test_Command: + def new(self): + return base.Command() + + def test_fresh(self): + c = self.new() + + + +class test_API: + """ + Unit tests for `API` class. + """ + + def new(self): + """ + Returns a new API instance. + """ + return base.API() + + def test_fresh(self): + """ + Test expectations of a fresh API instance. + """ + api = self.new() + assert read_only(api, 'objects') is None + assert read_only(api, 'objects') is None + + def test_register_command(self): + class my_command(base.Command): + pass + class another_command(base.Command): + pass + api = self.new() + + api.register_command(my_command) + + # Check that RegistrationError is raised passing something not + # sub-classed from Command: + raised = False + try: + api.register_command(object) + except exceptions.RegistrationError: + raised = True + assert raised -- cgit From e8257ad5311a4011625ed28bf6b308b1a9b43776 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sat, 19 Jul 2008 06:03:34 +0000 Subject: 5: Fleshed out base.Named, added corresponding unit tests --- ipalib/base.py | 27 +++++++++++++----- ipalib/exceptions.py | 4 +++ ipalib/tests/test_base.py | 71 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 95 insertions(+), 7 deletions(-) (limited to 'ipalib') diff --git a/ipalib/base.py b/ipalib/base.py index 96dd300d..c7a0cf99 100644 --- a/ipalib/base.py +++ b/ipalib/base.py @@ -26,18 +26,31 @@ import exceptions class Named(object): - #def __init__(self, prefix): - # clsname = self.__class__.__name__ + prefix = None + + @classmethod + def clsname(cls): + return cls.__name__ + + def __init__(self): + clsname = self.clsname() + assert type(self.prefix) is str + prefix = self.prefix + '_' + if not clsname.startswith(prefix): + raise exceptions.PrefixError(clsname, prefix) + self.__name = clsname[len(prefix):] + self.__name_cli = self.__name.replace('_', '-') + def __get_name(self): - return self.__class__.__name__ + return self.__name name = property(__get_name) - def __get_cli_name(self): - return self.name.replace('_', '-') - cli_name = property(__get_cli_name) + def __get_name_cli(self): + return self.__name_cli + name_cli = property(__get_name_cli) -class Command(Named): +class Command(object): def normalize(self, kw): raise NotImplementedError diff --git a/ipalib/exceptions.py b/ipalib/exceptions.py index 752a1e20..4150d712 100644 --- a/ipalib/exceptions.py +++ b/ipalib/exceptions.py @@ -55,3 +55,7 @@ class OverrideError(IPAError): class RegistrationError(IPAError): msg = '%r is not a subclass of %s' + + +class PrefixError(IPAError): + msg = 'class name %r must start with %r' diff --git a/ipalib/tests/test_base.py b/ipalib/tests/test_base.py index 7a998f3c..e0e2d8e8 100644 --- a/ipalib/tests/test_base.py +++ b/ipalib/tests/test_base.py @@ -40,6 +40,77 @@ def read_only(obj, name): return getattr(obj, name) +class ClassChecker(object): + cls = None # Override this is subclasses + + def new(self, *args, **kw): + return self.cls(*args, **kw) + + def args(self): + return [] + + def kw(self): + return {} + + def std(self): + return self.new(*self.args(), **self.kw()) + + +class test_Named: + """ + Unit tests for `Named` class. + """ + cls = base.Named + + def new(self): + class tst_verb_object(self.cls): + prefix = 'tst' + return tst_verb_object() + + def test_prefix(self): + """ + Test prefix exception. + """ + # Test Example class: + class Example(self.cls): + prefix = 'eg' + + # Two test subclasses: + class do_stuff(Example): + pass + class eg_do_stuff(Example): + pass + + # Test that PrefixError is raised with incorrectly named subclass: + raised = False + try: + do_stuff() + except exceptions.PrefixError: + raised = True + assert raised + + # Test that correctly named subclass works: + eg_do_stuff() + + def test_name(self): + """ + Test Named.name property. + """ + obj = self.new() + assert read_only(obj, 'name') == 'verb_object' + + def test_name_cli(self): + """ + Test Named.name_cli property. + """ + obj = self.new() + assert read_only(obj, 'name_cli') == 'verb-object' + + + + + + class test_NameSpace: """ Unit tests for `NameSpace` class. -- cgit From 91adc9c2d060b65d96a8515d08fc7192be79da83 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sat, 19 Jul 2008 07:43:48 +0000 Subject: 6: Fleshed out API.register_command, made correpsonding unit tests much more rigorous --- ipalib/base.py | 45 ++++++++++++++++++++----------------- ipalib/exceptions.py | 12 ++++++---- ipalib/tests/test_base.py | 57 ++++++++++++++++++++++++++++++++++++----------- 3 files changed, 77 insertions(+), 37 deletions(-) (limited to 'ipalib') diff --git a/ipalib/base.py b/ipalib/base.py index c7a0cf99..97fb7c90 100644 --- a/ipalib/base.py +++ b/ipalib/base.py @@ -28,12 +28,8 @@ import exceptions class Named(object): prefix = None - @classmethod - def clsname(cls): - return cls.__name__ - def __init__(self): - clsname = self.clsname() + clsname = self.__class__.__name__ assert type(self.prefix) is str prefix = self.prefix + '_' if not clsname.startswith(prefix): @@ -50,7 +46,8 @@ class Named(object): name_cli = property(__get_name_cli) -class Command(object): +class Command(Named): + prefix = 'cmd' def normalize(self, kw): raise NotImplementedError @@ -92,7 +89,7 @@ class NameSpace(object): For example, setting an attribute the normal way will raise an exception: >>> ns.my_message = 'some new value' - (raises exceptions.SetAttributeError) + (raises exceptions.SetError) But a programmer could still set the attribute like this: @@ -123,7 +120,7 @@ class NameSpace(object): NameSpace has been locked; otherwise calls object.__setattr__(). """ if self.__locked: - raise exceptions.SetAttributeError(name) + raise exceptions.SetError(name) super(NameSpace, self).__setattr__(name, value) def __getitem__(self, key): @@ -166,8 +163,9 @@ class API(object): __locked = False def __init__(self): - self.__c = {} # Proposed commands - self.__o = {} # Proposed objects + self.__classes = set() + self.__names = set() + self.__stage = {} def __get_objects(self): return self.__objects @@ -177,19 +175,26 @@ class API(object): return self.__commands commands = property(__get_commands) - def __merge(self, target, base, cls, override): - assert type(target) is dict - assert inspect.isclass(base) - assert inspect.isclass(cls) + def __merge(self, base, cls, override): + assert issubclass(base, Named) assert type(override) is bool - if not issubclass(cls, base): - raise exceptions.RegistrationError( - cls, - '%s.%s' % (base.__module__, base.__name__) - ) + if not (inspect.isclass(cls) and issubclass(cls, base)): + raise exceptions.RegistrationError(cls, base.__name__) + if cls in self.__classes: + raise exceptions.DuplicateError(cls.__name__, id(cls)) + if cls.__name__ in self.__names and not override: + raise exceptions.OverrideError(cls.__name__) + self.__classes.add(cls) + self.__names.add(cls.__name__) + if base not in self.__stage: + self.__stage[base.prefix] = {} + self.__stage[base.prefix][cls.__name__] = cls + def register_command(self, cls, override=False): - self.__merge(self.__c, Command, cls, override) + self.__merge(Command, cls, override) def finalize(self): pass + #i = cls() + #assert cls.__name__ == (base.prefix + '_' + i.name) diff --git a/ipalib/exceptions.py b/ipalib/exceptions.py index 4150d712..4584c1ee 100644 --- a/ipalib/exceptions.py +++ b/ipalib/exceptions.py @@ -45,16 +45,20 @@ class IPAError(Exception): return self.msg % self.kw -class SetAttributeError(IPAError): - msg = 'Cannot set %r: NameSpace does not allow attribute setting' +class SetError(IPAError): + msg = 'setting %r, but NameSpace does not allow attribute setting' class OverrideError(IPAError): - msg = 'Unexpected override of %r; use override=True if intended' + msg = 'unexpected override of %r (use override=True if intended)' + + +class DuplicateError(IPAError): + msg = 'class %r at %d was already registered' class RegistrationError(IPAError): - msg = '%r is not a subclass of %s' + msg = '%r must be a subclass of %s' class PrefixError(IPAError): diff --git a/ipalib/tests/test_base.py b/ipalib/tests/test_base.py index e0e2d8e8..bf727ed6 100644 --- a/ipalib/tests/test_base.py +++ b/ipalib/tests/test_base.py @@ -184,7 +184,7 @@ class test_NameSpace: raised = False try: setattr(ns, key, value) - except exceptions.SetAttributeError: + except exceptions.SetError: raised = True assert raised assert getattr(ns, key, None) != value @@ -238,13 +238,17 @@ class test_NameSpace: assert len(kw) == len(ns) == 3 -class test_Command: - def new(self): - return base.Command() +class test_Command(ClassChecker): + class cmd_some_command(base.Command): + pass + cls = cmd_some_command def test_fresh(self): c = self.new() - + assert isinstance(c, base.Named) + assert c.name == 'some_command' + assert c.name_cli == 'some-command' + assert callable(c) class test_API: @@ -267,19 +271,46 @@ class test_API: assert read_only(api, 'objects') is None def test_register_command(self): - class my_command(base.Command): + api = self.new() + + class cmd_my_command(base.Command): pass - class another_command(base.Command): + class cmd_another_command(base.Command): pass - api = self.new() - api.register_command(my_command) + # Check that RegistrationError is raised when registering anything + # other than a subclass of Command: + for obj in [object, cmd_my_command()]: + raised = False + try: + api.register_command(obj) + except exceptions.RegistrationError: + raised = True + assert raised - # Check that RegistrationError is raised passing something not - # sub-classed from Command: + # Check that command registration works: + api.register_command(cmd_my_command) + api.register_command(cmd_another_command) + + # Check that DuplicateError is raised when registering the same class + # twice: + raised = False + try: + api.register_command(cmd_my_command) + except exceptions.DuplicateError: + raised = True + assert raised + + # Check that OverrideError is raised when registering same name + # without override = True: + class cmd_my_command(base.Command): + pass raised = False try: - api.register_command(object) - except exceptions.RegistrationError: + api.register_command(cmd_my_command) + except exceptions.OverrideError: raised = True assert raised + + # Check that override=True works: + api.register_command(cmd_my_command, override=True) -- cgit From 26c9f4c8818e9904dab838ac95839c0d527219b8 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sat, 19 Jul 2008 08:31:46 +0000 Subject: 7: Roughed out API.finalize(); added corresponding unit tests --- ipalib/base.py | 27 +++++++++++++++++---------- ipalib/tests/test_base.py | 28 ++++++++++++++++++++++++++-- 2 files changed, 43 insertions(+), 12 deletions(-) (limited to 'ipalib') diff --git a/ipalib/base.py b/ipalib/base.py index 97fb7c90..51324f93 100644 --- a/ipalib/base.py +++ b/ipalib/base.py @@ -158,7 +158,7 @@ class NameSpace(object): class API(object): - __commands = None + __cmd = None __objects = None __locked = False @@ -171,9 +171,9 @@ class API(object): return self.__objects objects = property(__get_objects) - def __get_commands(self): - return self.__commands - commands = property(__get_commands) + def __get_cmd(self): + return self.__cmd + cmd = property(__get_cmd) def __merge(self, base, cls, override): assert issubclass(base, Named) @@ -184,17 +184,24 @@ class API(object): raise exceptions.DuplicateError(cls.__name__, id(cls)) if cls.__name__ in self.__names and not override: raise exceptions.OverrideError(cls.__name__) + prefix = base.prefix + assert cls.__name__.startswith(prefix) self.__classes.add(cls) self.__names.add(cls.__name__) - if base not in self.__stage: - self.__stage[base.prefix] = {} - self.__stage[base.prefix][cls.__name__] = cls + if prefix not in self.__stage: + self.__stage[prefix] = {} + self.__stage[prefix][cls.__name__] = cls def register_command(self, cls, override=False): self.__merge(Command, cls, override) def finalize(self): - pass - #i = cls() - #assert cls.__name__ == (base.prefix + '_' + i.name) + for (prefix, d) in self.__stage.items(): + n = {} + for cls in d.values(): + i = cls() + assert cls.__name__ == (prefix + '_' + i.name) + n[i.name] = i + if prefix == 'cmd': + self.__cmd = NameSpace(n) diff --git a/ipalib/tests/test_base.py b/ipalib/tests/test_base.py index bf727ed6..81794d7a 100644 --- a/ipalib/tests/test_base.py +++ b/ipalib/tests/test_base.py @@ -267,8 +267,7 @@ class test_API: Test expectations of a fresh API instance. """ api = self.new() - assert read_only(api, 'objects') is None - assert read_only(api, 'objects') is None + assert read_only(api, 'cmd') is None def test_register_command(self): api = self.new() @@ -314,3 +313,28 @@ class test_API: # Check that override=True works: api.register_command(cmd_my_command, override=True) + + def test_finalize(self): + api = self.new() + assert read_only(api, 'cmd') is None + + class cmd_my_command(base.Command): + pass + class cmd_another_command(base.Command): + pass + + api.register_command(cmd_my_command) + api.register_command(cmd_another_command) + + api.finalize() + + cmd = read_only(api, 'cmd') + assert isinstance(cmd, base.NameSpace) + assert api.cmd is cmd + + assert len(cmd) == 2 + assert list(cmd) == ['another_command', 'my_command'] + assert isinstance(cmd.my_command, cmd_my_command) + assert cmd.my_command is cmd['my_command'] + assert isinstance(cmd.another_command, cmd_another_command) + assert cmd.another_command is cmd['another_command'] -- cgit From e76160b01db52f9e750a605983eb85ae97305629 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sat, 19 Jul 2008 21:51:07 +0000 Subject: 8: Experimental work on more OO definition of what gets pluged into API.commands --- ipalib/base.py | 8 +++- ipalib/crud.py | 83 ++++++++++++++++++++++++++++++++++ ipalib/identity.py | 54 ++++++++++++++++++++++ ipalib/tests/test_crud.py | 111 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 254 insertions(+), 2 deletions(-) create mode 100644 ipalib/crud.py create mode 100644 ipalib/identity.py create mode 100644 ipalib/tests/test_crud.py (limited to 'ipalib') diff --git a/ipalib/base.py b/ipalib/base.py index 51324f93..62949eef 100644 --- a/ipalib/base.py +++ b/ipalib/base.py @@ -101,7 +101,7 @@ class NameSpace(object): __locked = False # Whether __setattr__ has been locked - def __init__(self, kw): + def __init__(self, kw, order=None): """ The single constructor argument `kw` is a dict of the (key, value) pairs to be in this NameSpace instance. @@ -111,7 +111,11 @@ class NameSpace(object): for (key, value) in self.__kw.items(): assert not key.startswith('_') setattr(self, key, value) - self.__keys = sorted(self.__kw) + if order is None: + self.__keys = sorted(self.__kw) + else: + self.__keys = list(order) + assert set(self.__keys) == set(self.__kw) self.__locked = True def __setattr__(self, name, value): diff --git a/ipalib/crud.py b/ipalib/crud.py new file mode 100644 index 00000000..1be72767 --- /dev/null +++ b/ipalib/crud.py @@ -0,0 +1,83 @@ +# Authors: +# Jason Gerard DeRose +# +# 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 + +""" + +""" + +from base import NameSpace + +class Named(object): + def __get_name(self): + return self.__class__.__name__ + name = property(__get_name) + +class ObjectMember(Named): + def __init__(self, obj): + self.__obj = obj + + def __get_obj(self): + return self.__obj + obj = property(__get_obj) + + +class Command(ObjectMember): + def __get_full_name(self): + return '%s_%s' % (self.name, self.obj.name) + full_name = property(__get_full_name) + +class Attribute(ObjectMember): + def __get_full_name(self): + return '%s_%s' % (self.obj.name, self.name) + full_name = property(__get_full_name) + + +class Object(Named): + def __init__(self): + self.__commands = self.__build_ns(self.get_commands) + self.__attributes = self.__build_ns(self.get_attributes, True) + + def __get_commands(self): + return self.__commands + commands = property(__get_commands) + + def __get_attributes(self): + return self.__attributes + attributes = property(__get_attributes) + + def __build_ns(self, callback, preserve=False): + d = {} + o = [] + for cls in callback(): + i = cls(self) + assert i.name not in d + d[i.name] = i + o.append(i.name) + if preserve: + return NameSpace(d, order=o) + return NameSpace(d) + + def __get_commands(self): + return + + def get_commands(self): + raise NotImplementedError + + def get_attributes(self): + raise NotImplementedError diff --git a/ipalib/identity.py b/ipalib/identity.py new file mode 100644 index 00000000..00caa20b --- /dev/null +++ b/ipalib/identity.py @@ -0,0 +1,54 @@ +# hidden +# read only +# editable + + + + + def get_label(self, _): + return _('Title') # Enum? + + def get_label(self, _): + return _('First Name') + + def get_label(self, _): + return _('Last Name') + + def get_label(self, _): + return _('Full Name') # Autofill + + def get_label(self, _): + return _('Display Name') # Autofill + + def get_label(self, _): + return _('Initials') # generated/ro? + + def get_label(self, _): + return _('Account Status') # Enum (active, inactive) + + def get_label(self, _): + return _('Login') + + def get_label(self, _): + return _('Password') + + def get_label(self, _): # Same field as above, special interface + return _('Confirm Password') + + def get_label(self, _): + return _('UID') #ro + + def get_label(self, _): + return _('GID') #ro + + def get_label(self, _): + return _('Home Directory') #ro + + def get_label(self, _): + return _('Login Shell') + + def get_label(self, _): + return _('GECOS') + + def get_label(self, _): + return _('') diff --git a/ipalib/tests/test_crud.py b/ipalib/tests/test_crud.py new file mode 100644 index 00000000..12ee1009 --- /dev/null +++ b/ipalib/tests/test_crud.py @@ -0,0 +1,111 @@ +# Authors: +# Jason Gerard DeRose +# +# 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 + +""" +Unit tests for `ipalib.crud` module. +""" + +from ipalib import crud, base, exceptions + +class create(crud.Command): + pass + +class retrieve(crud.Command): + pass + +class update(crud.Command): + pass + +class delete(crud.Command): + pass + +class givenName(crud.Attribute): + pass + +class sn(crud.Attribute): + pass + +class login(crud.Attribute): + pass + + +class user(crud.Object): + def get_commands(self): + return [ + create, + retrieve, + update, + delete, + ] + + def get_attributes(self): + return [ + givenName, + sn, + login, + ] + + +def test_Named(): + class named_class(crud.Named): + pass + + n = named_class() + assert n.name == 'named_class' + + +def test_Command(): + class user(object): + name = 'user' + class add(crud.Command): + pass + i = add(user()) + assert i.name == 'add' + assert i.full_name == 'add_user' + + +def test_Object(): + i = user() + assert i.name == 'user' + + # Test commands: + commands = i.commands + assert isinstance(commands, base.NameSpace) + assert list(commands) == ['create', 'delete', 'retrieve', 'update'] + assert len(commands) == 4 + for name in commands: + cls = globals()[name] + cmd = commands[name] + assert type(cmd) is cls + assert getattr(commands, name) is cmd + assert cmd.name == name + assert cmd.full_name == ('%s_user' % name) + + # Test attributes: + attributes = i.attributes + assert isinstance(attributes, base.NameSpace) + assert list(attributes) == ['givenName', 'sn', 'login'] + assert len(attributes) == 3 + for name in attributes: + cls = globals()[name] + attr = attributes[name] + assert type(attr) is cls + assert getattr(attributes, name) is attr + assert attr.name == name + assert attr.full_name == ('user_%s' % name) -- cgit From ccd8eb3373b0b195c1bc6efd8650320419c709a6 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sat, 19 Jul 2008 23:40:23 +0000 Subject: 9: Reorganized new work and unit tests based around base.Object being the plugin definining unit --- ipalib/base.py | 113 +++++++++++++++++++-------------- ipalib/crud.py | 71 +++++++-------------- ipalib/tests/test_base.py | 155 ++++++++++++++++++++++++++-------------------- ipalib/tests/test_crud.py | 89 -------------------------- 4 files changed, 177 insertions(+), 251 deletions(-) (limited to 'ipalib') diff --git a/ipalib/base.py b/ipalib/base.py index 62949eef..cac6797f 100644 --- a/ipalib/base.py +++ b/ipalib/base.py @@ -25,51 +25,6 @@ import inspect import exceptions -class Named(object): - prefix = None - - def __init__(self): - clsname = self.__class__.__name__ - assert type(self.prefix) is str - prefix = self.prefix + '_' - if not clsname.startswith(prefix): - raise exceptions.PrefixError(clsname, prefix) - self.__name = clsname[len(prefix):] - self.__name_cli = self.__name.replace('_', '-') - - def __get_name(self): - return self.__name - name = property(__get_name) - - def __get_name_cli(self): - return self.__name_cli - name_cli = property(__get_name_cli) - - -class Command(Named): - prefix = 'cmd' - - def normalize(self, kw): - raise NotImplementedError - - def validate(self, kw): - raise NotImplementedError - - def execute(self, kw): - raise NotImplementedError - - def __call__(self, **kw): - normalized = self.normalize(kw) - invalid = self.validate(normalized) - if invalid: - return invalid - return self.execute(normalize) - - -class Argument(object): - pass - - class NameSpace(object): """ A read-only namespace of (key, value) pairs that can be accessed @@ -103,8 +58,10 @@ class NameSpace(object): def __init__(self, kw, order=None): """ - The single constructor argument `kw` is a dict of the (key, value) - pairs to be in this NameSpace instance. + The `kw` argument is a dict of the (key, value) pairs to be in this + NameSpace instance. The optional `order` keyword argument specifies + the order of the keys in this namespace; if omitted, the default is + to sort the keys in ascending order. """ assert isinstance(kw, dict) self.__kw = dict(kw) @@ -141,7 +98,8 @@ class NameSpace(object): def __iter__(self): """ - Yields the names in this NameSpace in ascending order. + Yields the names in this NameSpace in ascending order, or in the + the order specified in `order` kw arg. For example: @@ -161,6 +119,65 @@ class NameSpace(object): return len(self.__keys) +class Named(object): + def __get_name(self): + return self.__class__.__name__ + name = property(__get_name) + + +class ObjectMember(Named): + def __init__(self, obj): + self.__obj = obj + + def __get_obj(self): + return self.__obj + obj = property(__get_obj) + + +class Command(ObjectMember): + def __get_full_name(self): + return '%s_%s' % (self.name, self.obj.name) + full_name = property(__get_full_name) + + +class Attribute(ObjectMember): + def __get_full_name(self): + return '%s_%s' % (self.obj.name, self.name) + full_name = property(__get_full_name) + + +class Object(Named): + def __init__(self): + self.__commands = self.__build_ns(self.get_commands) + self.__attributes = self.__build_ns(self.get_attributes, True) + + def __get_commands(self): + return self.__commands + commands = property(__get_commands) + + def __get_attributes(self): + return self.__attributes + attributes = property(__get_attributes) + + def __build_ns(self, callback, preserve=False): + d = {} + o = [] + for cls in callback(): + i = cls(self) + assert i.name not in d + d[i.name] = i + o.append(i.name) + if preserve: + return NameSpace(d, order=o) + return NameSpace(d) + + def get_commands(self): + raise NotImplementedError + + def get_attributes(self): + raise NotImplementedError + + class API(object): __cmd = None __objects = None diff --git a/ipalib/crud.py b/ipalib/crud.py index 1be72767..afbad47a 100644 --- a/ipalib/crud.py +++ b/ipalib/crud.py @@ -18,66 +18,43 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ - +Base classes for objects with CRUD functionality. """ -from base import NameSpace - -class Named(object): - def __get_name(self): - return self.__class__.__name__ - name = property(__get_name) - -class ObjectMember(Named): - def __init__(self, obj): - self.__obj = obj +import base - def __get_obj(self): - return self.__obj - obj = property(__get_obj) +class create(base.Command): + pass -class Command(ObjectMember): - def __get_full_name(self): - return '%s_%s' % (self.name, self.obj.name) - full_name = property(__get_full_name) +class retrieve(base.Command): + pass -class Attribute(ObjectMember): - def __get_full_name(self): - return '%s_%s' % (self.obj.name, self.name) - full_name = property(__get_full_name) +class update(base.Command): + pass -class Object(Named): - def __init__(self): - self.__commands = self.__build_ns(self.get_commands) - self.__attributes = self.__build_ns(self.get_attributes, True) - def __get_commands(self): - return self.__commands - commands = property(__get_commands) +class delete(base.Command): + pass - def __get_attributes(self): - return self.__attributes - attributes = property(__get_attributes) - def __build_ns(self, callback, preserve=False): - d = {} - o = [] - for cls in callback(): - i = cls(self) - assert i.name not in d - d[i.name] = i - o.append(i.name) - if preserve: - return NameSpace(d, order=o) - return NameSpace(d) +class search(base.Command): + pass - def __get_commands(self): - return +class user(base.Object): def get_commands(self): - raise NotImplementedError + return [ + create, + retrieve, + update, + delete, + ] def get_attributes(self): - raise NotImplementedError + return [ + givenName, + sn, + login, + ] diff --git a/ipalib/tests/test_base.py b/ipalib/tests/test_base.py index 81794d7a..31b10994 100644 --- a/ipalib/tests/test_base.py +++ b/ipalib/tests/test_base.py @@ -56,61 +56,6 @@ class ClassChecker(object): return self.new(*self.args(), **self.kw()) -class test_Named: - """ - Unit tests for `Named` class. - """ - cls = base.Named - - def new(self): - class tst_verb_object(self.cls): - prefix = 'tst' - return tst_verb_object() - - def test_prefix(self): - """ - Test prefix exception. - """ - # Test Example class: - class Example(self.cls): - prefix = 'eg' - - # Two test subclasses: - class do_stuff(Example): - pass - class eg_do_stuff(Example): - pass - - # Test that PrefixError is raised with incorrectly named subclass: - raised = False - try: - do_stuff() - except exceptions.PrefixError: - raised = True - assert raised - - # Test that correctly named subclass works: - eg_do_stuff() - - def test_name(self): - """ - Test Named.name property. - """ - obj = self.new() - assert read_only(obj, 'name') == 'verb_object' - - def test_name_cli(self): - """ - Test Named.name_cli property. - """ - obj = self.new() - assert read_only(obj, 'name_cli') == 'verb-object' - - - - - - class test_NameSpace: """ Unit tests for `NameSpace` class. @@ -238,17 +183,93 @@ class test_NameSpace: assert len(kw) == len(ns) == 3 -class test_Command(ClassChecker): - class cmd_some_command(base.Command): +def test_Command(): + class user(object): + name = 'user' + class add(base.Command): + pass + i = add(user()) + assert i.name == 'add' + assert i.full_name == 'add_user' + + +def test_Attribute(): + class user(object): + name = 'user' + class sn(base.Attribute): pass - cls = cmd_some_command + i = sn(user()) + assert i.name == 'sn' + assert i.full_name == 'user_sn' + + +def test_Object(): + class create(base.Command): + pass + + class retrieve(base.Command): + pass + + class update(base.Command): + pass + + class delete(base.Command): + pass + + class givenName(base.Attribute): + pass + + class sn(base.Attribute): + pass + + class login(base.Attribute): + pass + - def test_fresh(self): - c = self.new() - assert isinstance(c, base.Named) - assert c.name == 'some_command' - assert c.name_cli == 'some-command' - assert callable(c) + class user(base.Object): + def get_commands(self): + return [ + create, + retrieve, + update, + delete, + ] + + def get_attributes(self): + return [ + givenName, + sn, + login, + ] + + i = user() + assert i.name == 'user' + + # Test commands: + commands = i.commands + assert isinstance(commands, base.NameSpace) + assert list(commands) == ['create', 'delete', 'retrieve', 'update'] + assert len(commands) == 4 + for name in commands: + cls = locals()[name] + cmd = commands[name] + assert type(cmd) is cls + assert getattr(commands, name) is cmd + assert cmd.name == name + assert cmd.full_name == ('%s_user' % name) + + # Test attributes: + attributes = i.attributes + assert isinstance(attributes, base.NameSpace) + assert list(attributes) == ['givenName', 'sn', 'login'] + assert len(attributes) == 3 + for name in attributes: + cls = locals()[name] + attr = attributes[name] + assert type(attr) is cls + assert getattr(attributes, name) is attr + assert attr.name == name + assert attr.full_name == ('user_%s' % name) class test_API: @@ -262,14 +283,14 @@ class test_API: """ return base.API() - def test_fresh(self): + def dont_fresh(self): """ Test expectations of a fresh API instance. """ api = self.new() assert read_only(api, 'cmd') is None - def test_register_command(self): + def dont_register_command(self): api = self.new() class cmd_my_command(base.Command): @@ -314,7 +335,7 @@ class test_API: # Check that override=True works: api.register_command(cmd_my_command, override=True) - def test_finalize(self): + def dont_finalize(self): api = self.new() assert read_only(api, 'cmd') is None diff --git a/ipalib/tests/test_crud.py b/ipalib/tests/test_crud.py index 12ee1009..99113c4a 100644 --- a/ipalib/tests/test_crud.py +++ b/ipalib/tests/test_crud.py @@ -20,92 +20,3 @@ """ Unit tests for `ipalib.crud` module. """ - -from ipalib import crud, base, exceptions - -class create(crud.Command): - pass - -class retrieve(crud.Command): - pass - -class update(crud.Command): - pass - -class delete(crud.Command): - pass - -class givenName(crud.Attribute): - pass - -class sn(crud.Attribute): - pass - -class login(crud.Attribute): - pass - - -class user(crud.Object): - def get_commands(self): - return [ - create, - retrieve, - update, - delete, - ] - - def get_attributes(self): - return [ - givenName, - sn, - login, - ] - - -def test_Named(): - class named_class(crud.Named): - pass - - n = named_class() - assert n.name == 'named_class' - - -def test_Command(): - class user(object): - name = 'user' - class add(crud.Command): - pass - i = add(user()) - assert i.name == 'add' - assert i.full_name == 'add_user' - - -def test_Object(): - i = user() - assert i.name == 'user' - - # Test commands: - commands = i.commands - assert isinstance(commands, base.NameSpace) - assert list(commands) == ['create', 'delete', 'retrieve', 'update'] - assert len(commands) == 4 - for name in commands: - cls = globals()[name] - cmd = commands[name] - assert type(cmd) is cls - assert getattr(commands, name) is cmd - assert cmd.name == name - assert cmd.full_name == ('%s_user' % name) - - # Test attributes: - attributes = i.attributes - assert isinstance(attributes, base.NameSpace) - assert list(attributes) == ['givenName', 'sn', 'login'] - assert len(attributes) == 3 - for name in attributes: - cls = globals()[name] - attr = attributes[name] - assert type(attr) is cls - assert getattr(attributes, name) is attr - assert attr.name == name - assert attr.full_name == ('user_%s' % name) -- cgit From 7acf12e988f45d503d7d93f03f706618f7696504 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sun, 20 Jul 2008 01:29:59 +0000 Subject: 10: Updated base.API to reflect the fact that base.Object is now the new unit of plugin functionality; updated corresponding unit tests --- ipalib/__init__.py | 4 -- ipalib/base.py | 67 ++++++++++++++---------------- ipalib/crud.py | 24 +++++------ ipalib/tests/test_base.py | 101 +++++++++++++++++++++++----------------------- 4 files changed, 90 insertions(+), 106 deletions(-) (limited to 'ipalib') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index 1337d812..ddce3ac9 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -20,7 +20,3 @@ """ IPA library. """ - -import base - -api = base.API() diff --git a/ipalib/base.py b/ipalib/base.py index cac6797f..3cadc70e 100644 --- a/ipalib/base.py +++ b/ipalib/base.py @@ -112,6 +112,14 @@ class NameSpace(object): for key in self.__keys: yield key + def __call__(self): + """ + Iterates through the values in this NameSpace in the same order as + the keys. + """ + for key in self.__keys: + yield self.__kw[key] + def __len__(self): """ Returns number of items in this NameSpace. @@ -172,57 +180,42 @@ class Object(Named): return NameSpace(d) def get_commands(self): - raise NotImplementedError + return [] def get_attributes(self): - raise NotImplementedError + return [] class API(object): - __cmd = None __objects = None - __locked = False + __commands = None def __init__(self): - self.__classes = set() - self.__names = set() - self.__stage = {} + self.__obj_d = {} def __get_objects(self): return self.__objects objects = property(__get_objects) - def __get_cmd(self): - return self.__cmd - cmd = property(__get_cmd) + def __get_commands(self): + return self.__commands + commands = property(__get_commands) - def __merge(self, base, cls, override): - assert issubclass(base, Named) + def register_object(self, cls, override=False): assert type(override) is bool - if not (inspect.isclass(cls) and issubclass(cls, base)): - raise exceptions.RegistrationError(cls, base.__name__) - if cls in self.__classes: - raise exceptions.DuplicateError(cls.__name__, id(cls)) - if cls.__name__ in self.__names and not override: - raise exceptions.OverrideError(cls.__name__) - prefix = base.prefix - assert cls.__name__.startswith(prefix) - self.__classes.add(cls) - self.__names.add(cls.__name__) - if prefix not in self.__stage: - self.__stage[prefix] = {} - self.__stage[prefix][cls.__name__] = cls - - - def register_command(self, cls, override=False): - self.__merge(Command, cls, override) + if not (inspect.isclass(cls) and issubclass(cls, Object)): + raise exceptions.RegistrationError(cls, 'Object') + obj = cls() + if obj.name in self.__obj_d and not override: + raise exceptions.OverrideError(obj.name) + self.__obj_d[obj.name] = obj def finalize(self): - for (prefix, d) in self.__stage.items(): - n = {} - for cls in d.values(): - i = cls() - assert cls.__name__ == (prefix + '_' + i.name) - n[i.name] = i - if prefix == 'cmd': - self.__cmd = NameSpace(n) + cmd_d = {} + for obj in self.__obj_d.values(): + for cmd in obj.commands(): + assert cmd.full_name not in cmd_d + cmd_d[cmd.full_name] = cmd + self.__commands = NameSpace(cmd_d) + self.__objects = NameSpace(self.__obj_d) + self.__obj_d = None diff --git a/ipalib/crud.py b/ipalib/crud.py index afbad47a..c20b4cc1 100644 --- a/ipalib/crud.py +++ b/ipalib/crud.py @@ -23,15 +23,15 @@ Base classes for objects with CRUD functionality. import base -class create(base.Command): +class add(base.Command): pass -class retrieve(base.Command): +class get(base.Command): pass -class update(base.Command): +class edit(base.Command): pass @@ -39,22 +39,16 @@ class delete(base.Command): pass -class search(base.Command): +class find(base.Command): pass -class user(base.Object): +class CrudLike(base.Object): def get_commands(self): return [ - create, - retrieve, - update, + add, + get, + edit, delete, - ] - - def get_attributes(self): - return [ - givenName, - sn, - login, + find, ] diff --git a/ipalib/tests/test_base.py b/ipalib/tests/test_base.py index 31b10994..aefe4fa3 100644 --- a/ipalib/tests/test_base.py +++ b/ipalib/tests/test_base.py @@ -21,7 +21,7 @@ Unit tests for `ipalib.base` module. """ -from ipalib import base, exceptions +from ipalib import base, exceptions, crud def read_only(obj, name): @@ -225,7 +225,6 @@ def test_Object(): class login(base.Attribute): pass - class user(base.Object): def get_commands(self): return [ @@ -283,79 +282,81 @@ class test_API: """ return base.API() - def dont_fresh(self): + def test_fresh(self): """ Test expectations of a fresh API instance. """ api = self.new() - assert read_only(api, 'cmd') is None + assert read_only(api, 'objects') is None + assert read_only(api, 'commands') is None - def dont_register_command(self): + def test_register_exception(self): + """ + Check that RegistrationError is raised when registering anything + other than a subclass of Command. + """ api = self.new() - class cmd_my_command(base.Command): - pass - class cmd_another_command(base.Command): + class my_command(base.Command): pass - # Check that RegistrationError is raised when registering anything - # other than a subclass of Command: - for obj in [object, cmd_my_command()]: + for obj in [object, my_command]: raised = False try: - api.register_command(obj) + api.register_object(obj) except exceptions.RegistrationError: raised = True assert raised - # Check that command registration works: - api.register_command(cmd_my_command) - api.register_command(cmd_another_command) - - # Check that DuplicateError is raised when registering the same class - # twice: - raised = False - try: - api.register_command(cmd_my_command) - except exceptions.DuplicateError: - raised = True - assert raised + def test_override_exception(self): + class some_object(base.Object): + def get_commands(self): + return [] + def get_attributes(self): + return [] - # Check that OverrideError is raised when registering same name - # without override = True: - class cmd_my_command(base.Command): - pass + api = self.new() + api.register_object(some_object) raised = False try: - api.register_command(cmd_my_command) + api.register_object(some_object) except exceptions.OverrideError: raised = True assert raised + api.register_object(some_object, override=True) - # Check that override=True works: - api.register_command(cmd_my_command, override=True) - - def dont_finalize(self): - api = self.new() - assert read_only(api, 'cmd') is None - - class cmd_my_command(base.Command): + def test_finalize(self): + class user(crud.CrudLike): pass - class cmd_another_command(base.Command): + class group(crud.CrudLike): + pass + class service(crud.CrudLike): pass - api.register_command(cmd_my_command) - api.register_command(cmd_another_command) + names = list(user().commands) + assert len(names) == 5 + full_names = set() + for o in ['user', 'group', 'service']: + full_names.update('%s_%s' % (v, o) for v in names) + assert len(full_names) == 15 - api.finalize() - cmd = read_only(api, 'cmd') - assert isinstance(cmd, base.NameSpace) - assert api.cmd is cmd + api = self.new() + api.register_object(user) + api.register_object(group) + api.register_object(service) + api.finalize() - assert len(cmd) == 2 - assert list(cmd) == ['another_command', 'my_command'] - assert isinstance(cmd.my_command, cmd_my_command) - assert cmd.my_command is cmd['my_command'] - assert isinstance(cmd.another_command, cmd_another_command) - assert cmd.another_command is cmd['another_command'] + # Test API.objects property: + objects = read_only(api, 'objects') + assert type(objects) is base.NameSpace + assert objects is api.objects # Same instance must be returned + assert len(objects) is 3 + assert list(objects) == ['group', 'service', 'user'] + + # Test API.commands property: + commands = read_only(api, 'commands') + assert type(commands) is base.NameSpace + assert commands is api.commands # Same instance must be returned + assert len(commands) is 15 + assert list(commands) == sorted(full_names) -- cgit From 700d58ac1e29378569f2f9ac1a4fe39c8747aeba Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sun, 20 Jul 2008 02:03:15 +0000 Subject: 11: Added submodules needed to triger the plugin loading, etc., so I can start work on the cli demo --- ipalib/base.py | 1 + ipalib/crud.py | 10 ++-------- ipalib/plugins.py | 39 +++++++++++++++++++++++++++++++++++++++ ipalib/run.py | 28 ++++++++++++++++++++++++++++ ipalib/startup.py | 31 +++++++++++++++++++++++++++++++ 5 files changed, 101 insertions(+), 8 deletions(-) create mode 100644 ipalib/plugins.py create mode 100644 ipalib/run.py create mode 100644 ipalib/startup.py (limited to 'ipalib') diff --git a/ipalib/base.py b/ipalib/base.py index 3cadc70e..537146b4 100644 --- a/ipalib/base.py +++ b/ipalib/base.py @@ -212,6 +212,7 @@ class API(object): def finalize(self): cmd_d = {} + cmd_l = {} for obj in self.__obj_d.values(): for cmd in obj.commands(): assert cmd.full_name not in cmd_d diff --git a/ipalib/crud.py b/ipalib/crud.py index c20b4cc1..2ae736f5 100644 --- a/ipalib/crud.py +++ b/ipalib/crud.py @@ -26,29 +26,23 @@ import base class add(base.Command): pass - -class get(base.Command): +class find(base.Command): pass - class edit(base.Command): pass - class delete(base.Command): pass -class find(base.Command): - pass class CrudLike(base.Object): def get_commands(self): return [ add, - get, + find, edit, delete, - find, ] diff --git a/ipalib/plugins.py b/ipalib/plugins.py new file mode 100644 index 00000000..a78755ab --- /dev/null +++ b/ipalib/plugins.py @@ -0,0 +1,39 @@ +# Authors: +# Jason Gerard DeRose +# +# 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 + +""" +Some example plugins. +""" + +import crud +from run import api + +class user(crud.CrudLike): + pass +api.register_object(user) + + +class group(crud.CrudLike): + pass +api.register_object(group) + + +class service(crud.CrudLike): + pass +api.register_object(service) diff --git a/ipalib/run.py b/ipalib/run.py new file mode 100644 index 00000000..ffaa655f --- /dev/null +++ b/ipalib/run.py @@ -0,0 +1,28 @@ +# Authors: +# Jason Gerard DeRose +# +# 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 + +""" +Standard run-time instances of importard classes. This is where plugins +should access the registration API. +""" + +import base + +# The standard API instance +api = base.API() diff --git a/ipalib/startup.py b/ipalib/startup.py new file mode 100644 index 00000000..cfeb57b1 --- /dev/null +++ b/ipalib/startup.py @@ -0,0 +1,31 @@ +# Authors: +# Jason Gerard DeRose +# +# 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 + +""" +Importing this module causes the plugins to be loaded and the API to be +generated. + +This is not in __init__.py so that imported other IPA modules doesn't cause +unnecessary side effects (needed for unit tests, among other things). +""" + +from run import api +import plugins + +api.finalize() -- cgit From 739e67ca7d34e05fd3c6c8078c4b5d7134504be1 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sun, 20 Jul 2008 02:40:49 +0000 Subject: 12: Fixed test_base.test_API.test_finalize() unit tests now that CrudLike has 4 commands, not 5 --- ipalib/tests/test_base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'ipalib') diff --git a/ipalib/tests/test_base.py b/ipalib/tests/test_base.py index aefe4fa3..818b7713 100644 --- a/ipalib/tests/test_base.py +++ b/ipalib/tests/test_base.py @@ -334,11 +334,11 @@ class test_API: pass names = list(user().commands) - assert len(names) == 5 + assert len(names) == 4 full_names = set() for o in ['user', 'group', 'service']: full_names.update('%s_%s' % (v, o) for v in names) - assert len(full_names) == 15 + assert len(full_names) == 12 api = self.new() @@ -358,5 +358,5 @@ class test_API: commands = read_only(api, 'commands') assert type(commands) is base.NameSpace assert commands is api.commands # Same instance must be returned - assert len(commands) is 15 + assert len(commands) is 12 assert list(commands) == sorted(full_names) -- cgit From 370282819d7839e6e5091c019d6ff1b606add066 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sun, 20 Jul 2008 03:32:22 +0000 Subject: 13: Starting playing around with 'ipa' cli script --- ipalib/base.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/base.py b/ipalib/base.py index 537146b4..2769efd3 100644 --- a/ipalib/base.py +++ b/ipalib/base.py @@ -94,7 +94,7 @@ class NameSpace(object): """ Returns True if namespace has an item named `key`. """ - return key in self.__kw + return key.replace('-', '_') in self.__kw def __iter__(self): """ @@ -132,6 +132,10 @@ class Named(object): return self.__class__.__name__ name = property(__get_name) + def __get_doc(self): + return self.__class__.__doc__ + doc = property(__get_doc) + class ObjectMember(Named): def __init__(self, obj): @@ -189,6 +193,7 @@ class Object(Named): class API(object): __objects = None __commands = None + __max_cmd_len = None def __init__(self): self.__obj_d = {} @@ -201,6 +206,14 @@ class API(object): return self.__commands commands = property(__get_commands) + def __get_max_cmd_len(self): + if self.__max_cmd_len is None: + if self.__commands is None: + return 0 + self.__max_cmd_len = max(len(n) for n in self.__commands) + return self.__max_cmd_len + max_cmd_len = property(__get_max_cmd_len) + def register_object(self, cls, override=False): assert type(override) is bool if not (inspect.isclass(cls) and issubclass(cls, Object)): -- cgit From c2df39156979ea5a01901b97504c1de276364dfc Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sun, 20 Jul 2008 03:48:36 +0000 Subject: 14: Added Named.cli property that returns name.replace('_', '-'); Named.doc property now does a strip() to make it more user-friendly; added test_Named unit tests which somehow got dropped, uppdated with new Named properties --- ipalib/base.py | 6 +++++- ipalib/tests/test_base.py | 11 +++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/base.py b/ipalib/base.py index 2769efd3..ece446cf 100644 --- a/ipalib/base.py +++ b/ipalib/base.py @@ -132,8 +132,12 @@ class Named(object): return self.__class__.__name__ name = property(__get_name) + def __get_cli(self): + return self.name.replace('_', '-') + cli = property(__get_cli) + def __get_doc(self): - return self.__class__.__doc__ + return self.__class__.__doc__.strip() doc = property(__get_doc) diff --git a/ipalib/tests/test_base.py b/ipalib/tests/test_base.py index 818b7713..0dfd3438 100644 --- a/ipalib/tests/test_base.py +++ b/ipalib/tests/test_base.py @@ -183,6 +183,17 @@ class test_NameSpace: assert len(kw) == len(ns) == 3 +def test_Named(): + class named_class(base.Named): + """ + This class is so introspective! + """ + i = named_class() + assert i.name == 'named_class' + assert i.cli == 'named-class' + assert i.doc == 'This class is so introspective!' + + def test_Command(): class user(object): name = 'user' -- cgit From 0cb26ef3ec68739a888f4295103210d301c2f9a8 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sun, 20 Jul 2008 07:09:28 +0000 Subject: 15: Added ipalib.base2 module where I'm experimenting with a 3rd approach that is a hybrid of the first two: a decoupled late binding OO strategy --- ipalib/base2.py | 102 ++++++++++++++++++++++++++++++++ ipalib/exceptions.py | 4 ++ ipalib/tests/test_base2.py | 142 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 248 insertions(+) create mode 100644 ipalib/base2.py create mode 100644 ipalib/tests/test_base2.py (limited to 'ipalib') diff --git a/ipalib/base2.py b/ipalib/base2.py new file mode 100644 index 00000000..fa5536bd --- /dev/null +++ b/ipalib/base2.py @@ -0,0 +1,102 @@ +# Authors: +# Jason Gerard DeRose +# +# 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 + +""" +Base classes for plug-in architecture and generative API. +""" + +import inspect +import exceptions +from base import NameSpace + + +class Named(object): + def __get_name(self): + return self.__class__.__name__ + name = property(__get_name) + + +class WithObj(Named): + _obj = None + __obj = None + + def __get_obj(self): + return self.__obj + def __set_obj(self, obj): + if self.__obj is not None: + raise exceptions.TwiceSetError(self.__class__.__name__, 'obj') + assert isinstance(obj, Named) + assert isinstance(self._obj, str) + assert obj.name == self._obj + self.__obj = obj + assert self.obj is obj + obj = property(__get_obj, __set_obj) + + +class Command(WithObj): + pass + +class Property(WithObj): + pass + +class Object(Named): + pass + + +class Registrar(object): + __object = None + __commands = None + __properties = None + + def __init__(self): + self.__tmp_objects = {} + self.__tmp_commands = {} + self.__tmp_properties = {} + + def __get_objects(self): + return self.__objects + objects = property(__get_objects) + + def __get_commands(self): + return self.__commands + commands = property(__get_commands) + + def __get_target(self, i): + if isinstance(i, Object): + return (self.__tmp_objects, i.name) + if isinstance(i, Command): + return (self.__tmp_commands, i.name) + assert isinstance(i, Property) + + + def register(self, cls): + assert inspect.isclass(cls) + assert issubclass(cls, Named) + i = cls() + (target, key) = self.__get_target(i) + target[key] = i + + def finalize(self): + for cmd in self.__tmp_commands.values(): + if cmd._obj is None: + continue + obj = self.__tmp_objects[cmd._obj] + cmd.obj = obj + self.__objects = NameSpace(self.__tmp_objects) + self.__commands = NameSpace(self.__tmp_commands) diff --git a/ipalib/exceptions.py b/ipalib/exceptions.py index 4584c1ee..76c7da8c 100644 --- a/ipalib/exceptions.py +++ b/ipalib/exceptions.py @@ -63,3 +63,7 @@ class RegistrationError(IPAError): class PrefixError(IPAError): msg = 'class name %r must start with %r' + + +class TwiceSetError(IPAError): + msg = '%s.%s cannot be set twice' diff --git a/ipalib/tests/test_base2.py b/ipalib/tests/test_base2.py new file mode 100644 index 00000000..cdf96bdb --- /dev/null +++ b/ipalib/tests/test_base2.py @@ -0,0 +1,142 @@ +# Authors: +# Jason Gerard DeRose +# +# 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 + +""" +Unit tests for `ipalib.base` module. +""" + +from ipalib import base2 as base +from ipalib import exceptions + + +def read_only(obj, name): + """ + Check that a given property is read-only. + Returns the value of the property. + """ + assert isinstance(obj, object) + assert hasattr(obj, name) + raised = False + try: + setattr(obj, name, 'some new obj') + except AttributeError: + raised = True + assert raised + return getattr(obj, name) + + +class ClassChecker(object): + cls = None # Override this is subclasses + + def new(self, *args, **kw): + return self.cls(*args, **kw) + + def args(self): + return [] + + def kw(self): + return {} + + def std(self): + return self.new(*self.args(), **self.kw()) + + + +def test_Named(): + class named_class(base.Named): + pass + + i = named_class() + assert i.name == 'named_class' + + +def test_WithObj(): + class some_object(base.Named): + pass + + class another_object(base.Named): + pass + + class some_command(base.WithObj): + _obj = 'some_object' + + obj = some_object() + cmd = some_command() + + # Test that it can be set: + assert cmd.obj is None + cmd.obj = obj + assert cmd.obj is obj + + # Test that it cannot be set twice: + raised = False + try: + cmd.obj = obj + except exceptions.TwiceSetError: + raised = True + assert raised + + # Test that it can't be set with the wrong name: + obj = another_object() + cmd = some_command() + raised = False + try: + cmd.obj = obj + except AssertionError: + raised = True + assert raised + + +def test_Registar(): + class adduser(base.Command): + _obj = 'user' + class moduser(base.Command): + _obj = 'user' + class deluser(base.Command): + _obj = 'user' + class finduser(base.Command): + _obj = 'user' + class kinit(base.Command): + pass + class user(base.Object): + pass + class group(base.Object): + pass + + r = base.Registrar() + r.register(adduser) + r.register(moduser) + r.register(deluser) + r.register(finduser) + r.register(kinit) + r.register(user) + r.register(group) + + r.finalize() + assert len(r.commands) == 5 + assert len(r.objects) == 2 + + obj = r.objects.user + assert type(obj) is user + for name in ['adduser', 'moduser', 'deluser', 'finduser']: + cmd = r.commands[name] + assert type(cmd) is locals()[name] + assert cmd.obj is obj + + assert r.commands.kinit.obj is None -- cgit From cf32ac337062d76babb8efbf217c4045138b686a Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sun, 20 Jul 2008 07:20:00 +0000 Subject: 16: Changed base2.WithObj.__set_obj() slightly so that its gets locked into read-only even when _obj is None --- ipalib/base2.py | 25 ++++++++++++++++--------- ipalib/tests/test_base2.py | 8 ++++++++ 2 files changed, 24 insertions(+), 9 deletions(-) (limited to 'ipalib') diff --git a/ipalib/base2.py b/ipalib/base2.py index fa5536bd..98c0a180 100644 --- a/ipalib/base2.py +++ b/ipalib/base2.py @@ -35,17 +35,23 @@ class Named(object): class WithObj(Named): _obj = None __obj = None + __obj_locked = False def __get_obj(self): return self.__obj def __set_obj(self, obj): - if self.__obj is not None: + if self.__obj_locked: raise exceptions.TwiceSetError(self.__class__.__name__, 'obj') - assert isinstance(obj, Named) - assert isinstance(self._obj, str) - assert obj.name == self._obj - self.__obj = obj - assert self.obj is obj + self.__obj_locked = True + if obj is None: + assert self.__obj is None + assert self.obj is None + else: + assert isinstance(obj, Named) + assert isinstance(self._obj, str) + assert obj.name == self._obj + self.__obj = obj + assert self.obj is obj obj = property(__get_obj, __set_obj) @@ -95,8 +101,9 @@ class Registrar(object): def finalize(self): for cmd in self.__tmp_commands.values(): if cmd._obj is None: - continue - obj = self.__tmp_objects[cmd._obj] - cmd.obj = obj + cmd.obj = None + else: + obj = self.__tmp_objects[cmd._obj] + cmd.obj = obj self.__objects = NameSpace(self.__tmp_objects) self.__commands = NameSpace(self.__tmp_commands) diff --git a/ipalib/tests/test_base2.py b/ipalib/tests/test_base2.py index cdf96bdb..398f6c63 100644 --- a/ipalib/tests/test_base2.py +++ b/ipalib/tests/test_base2.py @@ -140,3 +140,11 @@ def test_Registar(): assert cmd.obj is obj assert r.commands.kinit.obj is None + + for cmd in r.commands(): + raised = False + try: + cmd.obj = None + except exceptions.TwiceSetError: + raised = True + assert raised -- cgit From 66cd39f51991abbff1c5a8c08fa8c9b1f358284a Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sun, 20 Jul 2008 17:33:17 +0000 Subject: 17: Registar.finalize() now sets the commands property on each object with commands --- ipalib/base2.py | 47 +++++++++++++++++++++++++++++++++++++++++++++- ipalib/tests/test_base2.py | 5 +++++ 2 files changed, 51 insertions(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/base2.py b/ipalib/base2.py index 98c0a180..827996e9 100644 --- a/ipalib/base2.py +++ b/ipalib/base2.py @@ -62,7 +62,48 @@ class Property(WithObj): pass class Object(Named): - pass + __commands = None + + def __get_commands(self): + return self.__commands + def __set_commands(self, commands): + if self.__commands is not None: + raise exceptions.TwiceSetError( + self.__class__.__name__, 'commands' + ) + assert type(commands) is NameSpace + self.__commands = commands + assert self.commands is commands + commands = property(__get_commands, __set_commands) + + +class Collector(object): + def __init__(self): + self.__d = {} + self.globals = [] + + def __getitem__(self, key): + assert isinstance(key, str) + if key not in self.__d: + self.__d[key] = [] + return self.__d[key] + + def __iter__(self): + for key in self.__d: + yield key + + def add(self, i): + assert isinstance(i, WithObj) + if i._obj is None: + self.globals.append(i) + else: + self[i._obj].append(i) + + def namespaces(self): + for key in self: + d = dict((i.name, i) for i in self[key]) + yield (key, NameSpace(d)) + class Registrar(object): @@ -99,11 +140,15 @@ class Registrar(object): target[key] = i def finalize(self): + obj_cmd = Collector() for cmd in self.__tmp_commands.values(): if cmd._obj is None: cmd.obj = None else: obj = self.__tmp_objects[cmd._obj] cmd.obj = obj + obj_cmd.add(cmd) self.__objects = NameSpace(self.__tmp_objects) self.__commands = NameSpace(self.__tmp_commands) + for (key, ns) in obj_cmd.namespaces(): + self.objects[key].commands = ns diff --git a/ipalib/tests/test_base2.py b/ipalib/tests/test_base2.py index 398f6c63..0cebdade 100644 --- a/ipalib/tests/test_base2.py +++ b/ipalib/tests/test_base2.py @@ -148,3 +148,8 @@ def test_Registar(): except exceptions.TwiceSetError: raised = True assert raised + + u = r.objects.user + assert isinstance(u.commands, base.NameSpace) + assert len(u.commands) == 4 + assert list(u.commands) == ['adduser', 'deluser', 'finduser', 'moduser'] -- cgit From 9b3e2f5cec773e06815fc85511f0c38410993edc Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sun, 20 Jul 2008 18:10:08 +0000 Subject: 18: Moved base2 stuff into base --- ipalib/base.py | 169 +++++++++++++++++++-------------- ipalib/crud.py | 27 ++---- ipalib/plugins.py | 23 +++-- ipalib/tests/test_base.py | 237 ++++++++++++++-------------------------------- 4 files changed, 194 insertions(+), 262 deletions(-) (limited to 'ipalib') diff --git a/ipalib/base.py b/ipalib/base.py index ece446cf..4731a872 100644 --- a/ipalib/base.py +++ b/ipalib/base.py @@ -127,80 +127,96 @@ class NameSpace(object): return len(self.__keys) + class Named(object): def __get_name(self): return self.__class__.__name__ name = property(__get_name) - def __get_cli(self): - return self.name.replace('_', '-') - cli = property(__get_cli) - - def __get_doc(self): - return self.__class__.__doc__.strip() - doc = property(__get_doc) - -class ObjectMember(Named): - def __init__(self, obj): - self.__obj = obj +class WithObj(Named): + _obj = None + __obj = None + __obj_locked = False def __get_obj(self): return self.__obj - obj = property(__get_obj) - - -class Command(ObjectMember): - def __get_full_name(self): - return '%s_%s' % (self.name, self.obj.name) - full_name = property(__get_full_name) + def __set_obj(self, obj): + if self.__obj_locked: + raise exceptions.TwiceSetError(self.__class__.__name__, 'obj') + self.__obj_locked = True + if obj is None: + assert self.__obj is None + assert self.obj is None + else: + assert isinstance(obj, Named) + assert isinstance(self._obj, str) + assert obj.name == self._obj + self.__obj = obj + assert self.obj is obj + obj = property(__get_obj, __set_obj) -class Attribute(ObjectMember): - def __get_full_name(self): - return '%s_%s' % (self.obj.name, self.name) - full_name = property(__get_full_name) +class Command(WithObj): + pass +class Property(WithObj): + pass class Object(Named): - def __init__(self): - self.__commands = self.__build_ns(self.get_commands) - self.__attributes = self.__build_ns(self.get_attributes, True) + __commands = None def __get_commands(self): return self.__commands - commands = property(__get_commands) + def __set_commands(self, commands): + if self.__commands is not None: + raise exceptions.TwiceSetError( + self.__class__.__name__, 'commands' + ) + assert type(commands) is NameSpace + self.__commands = commands + assert self.commands is commands + commands = property(__get_commands, __set_commands) + + +class Collector(object): + def __init__(self): + self.__d = {} + self.globals = [] - def __get_attributes(self): - return self.__attributes - attributes = property(__get_attributes) + def __getitem__(self, key): + assert isinstance(key, str) + if key not in self.__d: + self.__d[key] = [] + return self.__d[key] - def __build_ns(self, callback, preserve=False): - d = {} - o = [] - for cls in callback(): - i = cls(self) - assert i.name not in d - d[i.name] = i - o.append(i.name) - if preserve: - return NameSpace(d, order=o) - return NameSpace(d) + def __iter__(self): + for key in self.__d: + yield key - def get_commands(self): - return [] + def add(self, i): + assert isinstance(i, WithObj) + if i._obj is None: + self.globals.append(i) + else: + self[i._obj].append(i) + + def namespaces(self): + for key in self: + d = dict((i.name, i) for i in self[key]) + yield (key, NameSpace(d)) - def get_attributes(self): - return [] -class API(object): - __objects = None +class Registrar(object): + __object = None __commands = None - __max_cmd_len = None + __properties = None def __init__(self): - self.__obj_d = {} + self.__tmp_objects = {} + self.__tmp_commands = {} + self.__tmp_properties = {} def __get_objects(self): return self.__objects @@ -210,30 +226,43 @@ class API(object): return self.__commands commands = property(__get_commands) + def __get_target(self, i): + if isinstance(i, Object): + return (self.__tmp_objects, i.name) + if isinstance(i, Command): + return (self.__tmp_commands, i.name) + assert isinstance(i, Property) + + + def register(self, cls): + assert inspect.isclass(cls) + assert issubclass(cls, Named) + i = cls() + (target, key) = self.__get_target(i) + target[key] = i + + def finalize(self): + obj_cmd = Collector() + for cmd in self.__tmp_commands.values(): + if cmd._obj is None: + cmd.obj = None + else: + obj = self.__tmp_objects[cmd._obj] + cmd.obj = obj + obj_cmd.add(cmd) + self.__objects = NameSpace(self.__tmp_objects) + self.__commands = NameSpace(self.__tmp_commands) + for (key, ns) in obj_cmd.namespaces(): + self.objects[key].commands = ns + + +class API(Registrar): + __max_cmd_len = None + def __get_max_cmd_len(self): if self.__max_cmd_len is None: - if self.__commands is None: + if self.commands is None: return 0 - self.__max_cmd_len = max(len(n) for n in self.__commands) + self.__max_cmd_len = max(len(n) for n in self.commands) return self.__max_cmd_len max_cmd_len = property(__get_max_cmd_len) - - def register_object(self, cls, override=False): - assert type(override) is bool - if not (inspect.isclass(cls) and issubclass(cls, Object)): - raise exceptions.RegistrationError(cls, 'Object') - obj = cls() - if obj.name in self.__obj_d and not override: - raise exceptions.OverrideError(obj.name) - self.__obj_d[obj.name] = obj - - def finalize(self): - cmd_d = {} - cmd_l = {} - for obj in self.__obj_d.values(): - for cmd in obj.commands(): - assert cmd.full_name not in cmd_d - cmd_d[cmd.full_name] = cmd - self.__commands = NameSpace(cmd_d) - self.__objects = NameSpace(self.__obj_d) - self.__obj_d = None diff --git a/ipalib/crud.py b/ipalib/crud.py index 2ae736f5..89f7001b 100644 --- a/ipalib/crud.py +++ b/ipalib/crud.py @@ -23,26 +23,15 @@ Base classes for objects with CRUD functionality. import base -class add(base.Command): - pass -class find(base.Command): - pass +class Add(base.Command): + pass -class edit(base.Command): - pass +class Del(base.Command): + pass -class delete(base.Command): - pass +class Mod(base.Command): + pass - - - -class CrudLike(base.Object): - def get_commands(self): - return [ - add, - find, - edit, - delete, - ] +class Find(base.Command): + pass diff --git a/ipalib/plugins.py b/ipalib/plugins.py index a78755ab..21a60835 100644 --- a/ipalib/plugins.py +++ b/ipalib/plugins.py @@ -22,18 +22,25 @@ Some example plugins. """ import crud +import base from run import api -class user(crud.CrudLike): +class user(base.Object): pass -api.register_object(user) +api.register(user) +class adduser(crud.Add): + _obj = 'user' +api.register(adduser) -class group(crud.CrudLike): - pass -api.register_object(group) +class deluser(crud.Del): + _obj = 'user' +api.register(deluser) +class moduser(crud.Mod): + _obj = 'user' +api.register(moduser) -class service(crud.CrudLike): - pass -api.register_object(service) +class finduser(crud.Find): + _obj = 'user' +api.register(finduser) diff --git a/ipalib/tests/test_base.py b/ipalib/tests/test_base.py index 0dfd3438..d06847a2 100644 --- a/ipalib/tests/test_base.py +++ b/ipalib/tests/test_base.py @@ -185,189 +185,96 @@ class test_NameSpace: def test_Named(): class named_class(base.Named): - """ - This class is so introspective! - """ + pass + i = named_class() assert i.name == 'named_class' - assert i.cli == 'named-class' - assert i.doc == 'This class is so introspective!' -def test_Command(): - class user(object): - name = 'user' - class add(base.Command): +def test_WithObj(): + class some_object(base.Named): pass - i = add(user()) - assert i.name == 'add' - assert i.full_name == 'add_user' - -def test_Attribute(): - class user(object): - name = 'user' - class sn(base.Attribute): + class another_object(base.Named): pass - i = sn(user()) - assert i.name == 'sn' - assert i.full_name == 'user_sn' + class some_command(base.WithObj): + _obj = 'some_object' -def test_Object(): - class create(base.Command): - pass + obj = some_object() + cmd = some_command() - class retrieve(base.Command): - pass + # Test that it can be set: + assert cmd.obj is None + cmd.obj = obj + assert cmd.obj is obj - class update(base.Command): - pass - - class delete(base.Command): - pass + # Test that it cannot be set twice: + raised = False + try: + cmd.obj = obj + except exceptions.TwiceSetError: + raised = True + assert raised - class givenName(base.Attribute): - pass + # Test that it can't be set with the wrong name: + obj = another_object() + cmd = some_command() + raised = False + try: + cmd.obj = obj + except AssertionError: + raised = True + assert raised - class sn(base.Attribute): - pass - class login(base.Attribute): +def test_Registar(): + class adduser(base.Command): + _obj = 'user' + class moduser(base.Command): + _obj = 'user' + class deluser(base.Command): + _obj = 'user' + class finduser(base.Command): + _obj = 'user' + class kinit(base.Command): pass - class user(base.Object): - def get_commands(self): - return [ - create, - retrieve, - update, - delete, - ] - - def get_attributes(self): - return [ - givenName, - sn, - login, - ] - - i = user() - assert i.name == 'user' - - # Test commands: - commands = i.commands - assert isinstance(commands, base.NameSpace) - assert list(commands) == ['create', 'delete', 'retrieve', 'update'] - assert len(commands) == 4 - for name in commands: - cls = locals()[name] - cmd = commands[name] - assert type(cmd) is cls - assert getattr(commands, name) is cmd - assert cmd.name == name - assert cmd.full_name == ('%s_user' % name) - - # Test attributes: - attributes = i.attributes - assert isinstance(attributes, base.NameSpace) - assert list(attributes) == ['givenName', 'sn', 'login'] - assert len(attributes) == 3 - for name in attributes: - cls = locals()[name] - attr = attributes[name] - assert type(attr) is cls - assert getattr(attributes, name) is attr - assert attr.name == name - assert attr.full_name == ('user_%s' % name) - - -class test_API: - """ - Unit tests for `API` class. - """ - - def new(self): - """ - Returns a new API instance. - """ - return base.API() - - def test_fresh(self): - """ - Test expectations of a fresh API instance. - """ - api = self.new() - assert read_only(api, 'objects') is None - assert read_only(api, 'commands') is None - - def test_register_exception(self): - """ - Check that RegistrationError is raised when registering anything - other than a subclass of Command. - """ - api = self.new() - - class my_command(base.Command): - pass - - for obj in [object, my_command]: - raised = False - try: - api.register_object(obj) - except exceptions.RegistrationError: - raised = True - assert raised - - def test_override_exception(self): - class some_object(base.Object): - def get_commands(self): - return [] - def get_attributes(self): - return [] + pass + class group(base.Object): + pass - api = self.new() - api.register_object(some_object) + r = base.Registrar() + r.register(adduser) + r.register(moduser) + r.register(deluser) + r.register(finduser) + r.register(kinit) + r.register(user) + r.register(group) + + r.finalize() + assert len(r.commands) == 5 + assert len(r.objects) == 2 + + obj = r.objects.user + assert type(obj) is user + for name in ['adduser', 'moduser', 'deluser', 'finduser']: + cmd = r.commands[name] + assert type(cmd) is locals()[name] + assert cmd.obj is obj + + assert r.commands.kinit.obj is None + + for cmd in r.commands(): raised = False try: - api.register_object(some_object) - except exceptions.OverrideError: + cmd.obj = None + except exceptions.TwiceSetError: raised = True assert raised - api.register_object(some_object, override=True) - - def test_finalize(self): - class user(crud.CrudLike): - pass - class group(crud.CrudLike): - pass - class service(crud.CrudLike): - pass - - names = list(user().commands) - assert len(names) == 4 - full_names = set() - for o in ['user', 'group', 'service']: - full_names.update('%s_%s' % (v, o) for v in names) - assert len(full_names) == 12 - - - api = self.new() - api.register_object(user) - api.register_object(group) - api.register_object(service) - api.finalize() - - # Test API.objects property: - objects = read_only(api, 'objects') - assert type(objects) is base.NameSpace - assert objects is api.objects # Same instance must be returned - assert len(objects) is 3 - assert list(objects) == ['group', 'service', 'user'] - - # Test API.commands property: - commands = read_only(api, 'commands') - assert type(commands) is base.NameSpace - assert commands is api.commands # Same instance must be returned - assert len(commands) is 12 - assert list(commands) == sorted(full_names) + + u = r.objects.user + assert isinstance(u.commands, base.NameSpace) + assert len(u.commands) == 4 + assert list(u.commands) == ['adduser', 'deluser', 'finduser', 'moduser'] -- cgit From 89e9b7eaeca5e96d0fdeddf76fd74a2176d96d6f Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sun, 20 Jul 2008 18:10:56 +0000 Subject: 19: Removed depreciated base2.py and test_base2.py files --- ipalib/base2.py | 154 -------------------------------------------- ipalib/tests/test_base2.py | 155 --------------------------------------------- 2 files changed, 309 deletions(-) delete mode 100644 ipalib/base2.py delete mode 100644 ipalib/tests/test_base2.py (limited to 'ipalib') diff --git a/ipalib/base2.py b/ipalib/base2.py deleted file mode 100644 index 827996e9..00000000 --- a/ipalib/base2.py +++ /dev/null @@ -1,154 +0,0 @@ -# Authors: -# Jason Gerard DeRose -# -# 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 - -""" -Base classes for plug-in architecture and generative API. -""" - -import inspect -import exceptions -from base import NameSpace - - -class Named(object): - def __get_name(self): - return self.__class__.__name__ - name = property(__get_name) - - -class WithObj(Named): - _obj = None - __obj = None - __obj_locked = False - - def __get_obj(self): - return self.__obj - def __set_obj(self, obj): - if self.__obj_locked: - raise exceptions.TwiceSetError(self.__class__.__name__, 'obj') - self.__obj_locked = True - if obj is None: - assert self.__obj is None - assert self.obj is None - else: - assert isinstance(obj, Named) - assert isinstance(self._obj, str) - assert obj.name == self._obj - self.__obj = obj - assert self.obj is obj - obj = property(__get_obj, __set_obj) - - -class Command(WithObj): - pass - -class Property(WithObj): - pass - -class Object(Named): - __commands = None - - def __get_commands(self): - return self.__commands - def __set_commands(self, commands): - if self.__commands is not None: - raise exceptions.TwiceSetError( - self.__class__.__name__, 'commands' - ) - assert type(commands) is NameSpace - self.__commands = commands - assert self.commands is commands - commands = property(__get_commands, __set_commands) - - -class Collector(object): - def __init__(self): - self.__d = {} - self.globals = [] - - def __getitem__(self, key): - assert isinstance(key, str) - if key not in self.__d: - self.__d[key] = [] - return self.__d[key] - - def __iter__(self): - for key in self.__d: - yield key - - def add(self, i): - assert isinstance(i, WithObj) - if i._obj is None: - self.globals.append(i) - else: - self[i._obj].append(i) - - def namespaces(self): - for key in self: - d = dict((i.name, i) for i in self[key]) - yield (key, NameSpace(d)) - - - -class Registrar(object): - __object = None - __commands = None - __properties = None - - def __init__(self): - self.__tmp_objects = {} - self.__tmp_commands = {} - self.__tmp_properties = {} - - def __get_objects(self): - return self.__objects - objects = property(__get_objects) - - def __get_commands(self): - return self.__commands - commands = property(__get_commands) - - def __get_target(self, i): - if isinstance(i, Object): - return (self.__tmp_objects, i.name) - if isinstance(i, Command): - return (self.__tmp_commands, i.name) - assert isinstance(i, Property) - - - def register(self, cls): - assert inspect.isclass(cls) - assert issubclass(cls, Named) - i = cls() - (target, key) = self.__get_target(i) - target[key] = i - - def finalize(self): - obj_cmd = Collector() - for cmd in self.__tmp_commands.values(): - if cmd._obj is None: - cmd.obj = None - else: - obj = self.__tmp_objects[cmd._obj] - cmd.obj = obj - obj_cmd.add(cmd) - self.__objects = NameSpace(self.__tmp_objects) - self.__commands = NameSpace(self.__tmp_commands) - for (key, ns) in obj_cmd.namespaces(): - self.objects[key].commands = ns diff --git a/ipalib/tests/test_base2.py b/ipalib/tests/test_base2.py deleted file mode 100644 index 0cebdade..00000000 --- a/ipalib/tests/test_base2.py +++ /dev/null @@ -1,155 +0,0 @@ -# Authors: -# Jason Gerard DeRose -# -# 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 - -""" -Unit tests for `ipalib.base` module. -""" - -from ipalib import base2 as base -from ipalib import exceptions - - -def read_only(obj, name): - """ - Check that a given property is read-only. - Returns the value of the property. - """ - assert isinstance(obj, object) - assert hasattr(obj, name) - raised = False - try: - setattr(obj, name, 'some new obj') - except AttributeError: - raised = True - assert raised - return getattr(obj, name) - - -class ClassChecker(object): - cls = None # Override this is subclasses - - def new(self, *args, **kw): - return self.cls(*args, **kw) - - def args(self): - return [] - - def kw(self): - return {} - - def std(self): - return self.new(*self.args(), **self.kw()) - - - -def test_Named(): - class named_class(base.Named): - pass - - i = named_class() - assert i.name == 'named_class' - - -def test_WithObj(): - class some_object(base.Named): - pass - - class another_object(base.Named): - pass - - class some_command(base.WithObj): - _obj = 'some_object' - - obj = some_object() - cmd = some_command() - - # Test that it can be set: - assert cmd.obj is None - cmd.obj = obj - assert cmd.obj is obj - - # Test that it cannot be set twice: - raised = False - try: - cmd.obj = obj - except exceptions.TwiceSetError: - raised = True - assert raised - - # Test that it can't be set with the wrong name: - obj = another_object() - cmd = some_command() - raised = False - try: - cmd.obj = obj - except AssertionError: - raised = True - assert raised - - -def test_Registar(): - class adduser(base.Command): - _obj = 'user' - class moduser(base.Command): - _obj = 'user' - class deluser(base.Command): - _obj = 'user' - class finduser(base.Command): - _obj = 'user' - class kinit(base.Command): - pass - class user(base.Object): - pass - class group(base.Object): - pass - - r = base.Registrar() - r.register(adduser) - r.register(moduser) - r.register(deluser) - r.register(finduser) - r.register(kinit) - r.register(user) - r.register(group) - - r.finalize() - assert len(r.commands) == 5 - assert len(r.objects) == 2 - - obj = r.objects.user - assert type(obj) is user - for name in ['adduser', 'moduser', 'deluser', 'finduser']: - cmd = r.commands[name] - assert type(cmd) is locals()[name] - assert cmd.obj is obj - - assert r.commands.kinit.obj is None - - for cmd in r.commands(): - raised = False - try: - cmd.obj = None - except exceptions.TwiceSetError: - raised = True - assert raised - - u = r.objects.user - assert isinstance(u.commands, base.NameSpace) - assert len(u.commands) == 4 - assert list(u.commands) == ['adduser', 'deluser', 'finduser', 'moduser'] -- cgit From 14339cfae01b843949d0f9972670f56f952a5faa Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sun, 20 Jul 2008 18:36:02 +0000 Subject: 20: Updated example plugins, added '_api_' command to ipa script with prints the api --- ipalib/base.py | 3 ++- ipalib/plugins.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/base.py b/ipalib/base.py index 4731a872..3c302369 100644 --- a/ipalib/base.py +++ b/ipalib/base.py @@ -158,7 +158,8 @@ class WithObj(Named): class Command(WithObj): - pass + def __call__(self): + print 'You called %s()' % self.name class Property(WithObj): pass diff --git a/ipalib/plugins.py b/ipalib/plugins.py index 21a60835..a28ac2fb 100644 --- a/ipalib/plugins.py +++ b/ipalib/plugins.py @@ -44,3 +44,46 @@ api.register(moduser) class finduser(crud.Find): _obj = 'user' api.register(finduser) + + + +class group(base.Object): + pass +api.register(group) + +class addgroup(crud.Add): + _obj = 'group' +api.register(addgroup) + +class delgroup(crud.Del): + _obj = 'group' +api.register(delgroup) + +class modgroup(crud.Mod): + _obj = 'group' +api.register(modgroup) + +class findgroup(crud.Find): + _obj = 'group' +api.register(findgroup) + + +class service(base.Object): + pass +api.register(service) + +class addservice(crud.Add): + _obj = 'service' +api.register(addservice) + +class delservice(crud.Del): + _obj = 'service' +api.register(delservice) + +class modservice(crud.Mod): + _obj = 'service' +api.register(modservice) + +class findservice(crud.Find): + _obj = 'service' +api.register(findservice) -- cgit From f3faaf2d29e57733a4d1c2a05534add46d6491bc Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sun, 20 Jul 2008 18:55:53 +0000 Subject: 22: Named.name property now calls _get_name() at first evaluation to make changing the behaviour in subclasses easier --- ipalib/base.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/base.py b/ipalib/base.py index 3c302369..09355f41 100644 --- a/ipalib/base.py +++ b/ipalib/base.py @@ -129,8 +129,15 @@ class NameSpace(object): class Named(object): - def __get_name(self): + __name = None + + def _get_name(self): return self.__class__.__name__ + + def __get_name(self): + if self.__name is None: + self.__name = self._get_name() + return self.__name name = property(__get_name) -- cgit From 6f58880dcd61493eb37bd4f55bc0a7dabd0c5e54 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sun, 20 Jul 2008 21:42:35 +0000 Subject: 23: Added base.Attribute class that determins the object association via class naming convention instead of through the _obj attribute --- ipalib/base.py | 76 +++++++++++++++++++++++++++++++++++++++++++++-- ipalib/tests/test_base.py | 71 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 145 insertions(+), 2 deletions(-) (limited to 'ipalib') diff --git a/ipalib/base.py b/ipalib/base.py index 09355f41..b4d20450 100644 --- a/ipalib/base.py +++ b/ipalib/base.py @@ -21,6 +21,7 @@ Base classes for plug-in architecture and generative API. """ +import re import inspect import exceptions @@ -141,6 +142,53 @@ class Named(object): name = property(__get_name) +class AbstractCommand(object): + def __call__(self): + print 'You called %s()' % self.name + +class Attribute(Named): + __locked = False + __obj = None + + def __init__(self): + m = re.match('^([a-z]+)__([a-z]+)$', self.__class__.__name__) + assert m + self.__obj_name = m.group(1) + self.__attr_name = m.group(2) + + def __get_obj(self): + return self.__obj + obj = property(__get_obj) + + def set_obj(self, obj=None): + if self.__locked: + raise exceptions.TwiceSetError(self.__class__.__name__, 'obj') + self.__locked = True + if obj is None: + return + assert isinstance(obj, Object) + assert obj.name == self.__obj_name + self.__obj = obj + + def __get_obj_name(self): + return self.__obj_name + obj_name = property(__get_obj_name) + + def __get_attr_name(self): + return self.__attr_name + attr_name = property(__get_attr_name) + + +class Method(AbstractCommand, Attribute): + def _get_name(self): + return '%s_%s' % (self.attr_name, self.obj_name) + + +class Property(Attribute): + def _get_name(self): + return self.attr_name + + class WithObj(Named): _obj = None __obj = None @@ -168,8 +216,7 @@ class Command(WithObj): def __call__(self): print 'You called %s()' % self.name -class Property(WithObj): - pass + class Object(Named): __commands = None @@ -187,6 +234,31 @@ class Object(Named): commands = property(__get_commands, __set_commands) + + +class AttributeCollector(object): + def __init__(self): + self.__d = {} + + def __getitem__(self, key): + assert isinstance(key, str) + if key not in self.__d: + self.__d[key] = {} + return self.__d[key] + + def __iter__(self): + for key in self.__d: + yield key + + def add(self, i): + assert isinstance(i, Attribute) + self[i.obj_name][i.attr_name] = i + + def namespaces(self): + for key in self: + yield (key, NameSpace(self[key])) + + class Collector(object): def __init__(self): self.__d = {} diff --git a/ipalib/tests/test_base.py b/ipalib/tests/test_base.py index d06847a2..9d2c1b05 100644 --- a/ipalib/tests/test_base.py +++ b/ipalib/tests/test_base.py @@ -191,6 +191,77 @@ def test_Named(): assert i.name == 'named_class' +def test_Attribute(): + class user__add(base.Attribute): + pass + i = user__add() + assert i.obj_name == 'user' + assert i.attr_name == 'add' + assert read_only(i, 'obj') is None + class user(base.Object): + pass + u = user() + i.set_obj(u) + assert read_only(i, 'obj') is u + raised = False + try: + i.set_obj(u) + except exceptions.TwiceSetError: + raised = True + assert raised + + +def test_Method(): + class user__mod(base.Method): + pass + i = user__mod() + assert isinstance(i, base.Attribute) + assert isinstance(i, base.AbstractCommand) + assert i.obj_name == 'user' + assert i.attr_name == 'mod' + assert i.name == 'mod_user' + + +def test_Property(): + class user__firstname(base.Property): + pass + i = user__firstname() + assert isinstance(i, base.Attribute) + assert i.obj_name == 'user' + assert i.attr_name == 'firstname' + assert i.name == 'firstname' + + +def test_AttributeCollector(): + class user__add(base.Attribute): + pass + class user__mod(base.Attribute): + pass + class group__add(base.Attribute): + pass + u_a = user__add() + u_m = user__mod() + g_a = group__add() + + ac = base.AttributeCollector() + ac.add(u_a) + ac.add(u_m) + ac.add(g_a) + + assert set(ac) == set(['user', 'group']) + + u = ac['user'] + assert set(u) == set(['add', 'mod']) + assert set(u.values()) == set([u_a, u_m]) + + g = ac['group'] + assert g.keys() == ['add'] + assert g.values() == [g_a] + + + + + def test_WithObj(): class some_object(base.Named): pass -- cgit From 15c419de124d3f85f18ce96bb412e7c533fb3b4c Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sun, 20 Jul 2008 23:09:29 +0000 Subject: 24: Ported Registar to changes around Attribute; updated unit tests --- ipalib/base.py | 153 +++++++++++++++++++++------------------------- ipalib/tests/test_base.py | 144 +++++++++++++++++++++---------------------- 2 files changed, 140 insertions(+), 157 deletions(-) (limited to 'ipalib') diff --git a/ipalib/base.py b/ipalib/base.py index b4d20450..a62d5812 100644 --- a/ipalib/base.py +++ b/ipalib/base.py @@ -158,17 +158,13 @@ class Attribute(Named): def __get_obj(self): return self.__obj - obj = property(__get_obj) - - def set_obj(self, obj=None): - if self.__locked: + def __set_obj(self, obj): + if self.__obj is not None: raise exceptions.TwiceSetError(self.__class__.__name__, 'obj') - self.__locked = True - if obj is None: - return assert isinstance(obj, Object) - assert obj.name == self.__obj_name self.__obj = obj + assert self.obj is obj + obj = property(__get_obj, __set_obj) def __get_obj_name(self): return self.__obj_name @@ -189,50 +185,37 @@ class Property(Attribute): return self.attr_name -class WithObj(Named): - _obj = None - __obj = None - __obj_locked = False - - def __get_obj(self): - return self.__obj - def __set_obj(self, obj): - if self.__obj_locked: - raise exceptions.TwiceSetError(self.__class__.__name__, 'obj') - self.__obj_locked = True - if obj is None: - assert self.__obj is None - assert self.obj is None - else: - assert isinstance(obj, Named) - assert isinstance(self._obj, str) - assert obj.name == self._obj - self.__obj = obj - assert self.obj is obj - obj = property(__get_obj, __set_obj) - - -class Command(WithObj): - def __call__(self): - print 'You called %s()' % self.name - +class Command(AbstractCommand, Named): + pass class Object(Named): - __commands = None + __methods = None + __properties = None - def __get_commands(self): - return self.__commands - def __set_commands(self, commands): - if self.__commands is not None: + def __get_methods(self): + return self.__methods + def __set_methods(self, methods): + if self.__methods is not None: raise exceptions.TwiceSetError( - self.__class__.__name__, 'commands' + self.__class__.__name__, 'methods' ) - assert type(commands) is NameSpace - self.__commands = commands - assert self.commands is commands - commands = property(__get_commands, __set_commands) - + assert type(methods) is NameSpace + self.__methods = methods + assert self.methods is methods + methods = property(__get_methods, __set_methods) + + def __get_properties(self): + return self.__properties + def __set_properties(self, properties): + if self.__properties is not None: + raise exceptions.TwiceSetError( + self.__class__.__name__, 'properties' + ) + assert type(properties) is NameSpace + self.__properties = properties + assert self.properties is properties + properties = property(__get_properties, __set_properties) @@ -262,41 +245,32 @@ class AttributeCollector(object): class Collector(object): def __init__(self): self.__d = {} - self.globals = [] - def __getitem__(self, key): - assert isinstance(key, str) - if key not in self.__d: - self.__d[key] = [] - return self.__d[key] + def __get_d(self): + return dict(self.__d) + d = property(__get_d) def __iter__(self): for key in self.__d: yield key def add(self, i): - assert isinstance(i, WithObj) - if i._obj is None: - self.globals.append(i) - else: - self[i._obj].append(i) - - def namespaces(self): - for key in self: - d = dict((i.name, i) for i in self[key]) - yield (key, NameSpace(d)) + assert isinstance(i, Named) + self.__d[i.name] = i + def ns(self): + return NameSpace(self.__d) class Registrar(object): - __object = None + __objects = None __commands = None - __properties = None def __init__(self): - self.__tmp_objects = {} - self.__tmp_commands = {} - self.__tmp_properties = {} + self.__tmp_commands = Collector() + self.__tmp_objects = Collector() + self.__tmp_methods = AttributeCollector() + self.__tmp_properties = AttributeCollector() def __get_objects(self): return self.__objects @@ -307,33 +281,44 @@ class Registrar(object): commands = property(__get_commands) def __get_target(self, i): - if isinstance(i, Object): - return (self.__tmp_objects, i.name) if isinstance(i, Command): - return (self.__tmp_commands, i.name) + return self.__tmp_commands + if isinstance(i, Object): + return self.__tmp_objects + if isinstance(i, Method): + return self.__tmp_methods assert isinstance(i, Property) + return self.__tmp_properties def register(self, cls): assert inspect.isclass(cls) assert issubclass(cls, Named) i = cls() - (target, key) = self.__get_target(i) - target[key] = i + self.__get_target(i).add(i) + def finalize(self): - obj_cmd = Collector() - for cmd in self.__tmp_commands.values(): - if cmd._obj is None: - cmd.obj = None - else: - obj = self.__tmp_objects[cmd._obj] - cmd.obj = obj - obj_cmd.add(cmd) - self.__objects = NameSpace(self.__tmp_objects) - self.__commands = NameSpace(self.__tmp_commands) - for (key, ns) in obj_cmd.namespaces(): - self.objects[key].commands = ns + self.__objects = self.__tmp_objects.ns() + for (key, ns) in self.__tmp_methods.namespaces(): + self.__objects[key].methods = ns + for (key, ns) in self.__tmp_properties.namespaces(): + self.__objects[key].properties = ns + commands = self.__tmp_commands.d + for obj in self.__objects(): + assert isinstance(obj, Object) + if obj.methods is None: + obj.methods = NameSpace({}) + if obj.properties is None: + obj.properties = NameSpace({}) + for m in obj.methods(): + m.obj = obj + assert m.name not in commands + commands[m.name] = m + for p in obj.properties(): + p.obj = obj + self.__commands = NameSpace(commands) + class API(Registrar): diff --git a/ipalib/tests/test_base.py b/ipalib/tests/test_base.py index 9d2c1b05..7affd997 100644 --- a/ipalib/tests/test_base.py +++ b/ipalib/tests/test_base.py @@ -197,15 +197,15 @@ def test_Attribute(): i = user__add() assert i.obj_name == 'user' assert i.attr_name == 'add' - assert read_only(i, 'obj') is None + assert i.obj is None class user(base.Object): pass u = user() - i.set_obj(u) - assert read_only(i, 'obj') is u + i.obj = u + assert i.obj is u raised = False try: - i.set_obj(u) + i.obj = u except exceptions.TwiceSetError: raised = True assert raised @@ -232,6 +232,15 @@ def test_Property(): assert i.name == 'firstname' +def test_Command(): + class dostuff(base.Command): + pass + i = dostuff() + assert isinstance(i, base.AbstractCommand) + assert i.name == 'dostuff' + + + def test_AttributeCollector(): class user__add(base.Attribute): pass @@ -259,93 +268,82 @@ def test_AttributeCollector(): assert g.values() == [g_a] - - - -def test_WithObj(): - class some_object(base.Named): +def test_Collector(): + class user(base.Object): pass - - class another_object(base.Named): + class group(base.Object): pass + u = user() + g = group() + c = base.Collector() + c.add(u) + c.add(g) + ns = c.ns() + assert isinstance(ns, base.NameSpace) + assert set(ns) == set(['user', 'group']) + assert ns.user is u + assert ns.group is g - class some_command(base.WithObj): - _obj = 'some_object' - obj = some_object() - cmd = some_command() - # Test that it can be set: - assert cmd.obj is None - cmd.obj = obj - assert cmd.obj is obj - - # Test that it cannot be set twice: - raised = False - try: - cmd.obj = obj - except exceptions.TwiceSetError: - raised = True - assert raised - - # Test that it can't be set with the wrong name: - obj = another_object() - cmd = some_command() - raised = False - try: - cmd.obj = obj - except AssertionError: - raised = True - assert raised def test_Registar(): - class adduser(base.Command): - _obj = 'user' - class moduser(base.Command): - _obj = 'user' - class deluser(base.Command): - _obj = 'user' - class finduser(base.Command): - _obj = 'user' class kinit(base.Command): pass + class user__add(base.Method): + pass + class user__del(base.Method): + pass + class user__firstname(base.Property): + pass + class user__lastname(base.Property): + pass + class user__login(base.Property): + pass class user(base.Object): pass class group(base.Object): pass r = base.Registrar() - r.register(adduser) - r.register(moduser) - r.register(deluser) - r.register(finduser) + assert read_only(r, 'objects') is None + assert read_only(r, 'commands') is None + + r.register(kinit) + r.register(user__add) + r.register(user__del) + r.register(user__firstname) + r.register(user__lastname) + r.register(user__login) r.register(user) r.register(group) r.finalize() - assert len(r.commands) == 5 - assert len(r.objects) == 2 - - obj = r.objects.user - assert type(obj) is user - for name in ['adduser', 'moduser', 'deluser', 'finduser']: - cmd = r.commands[name] - assert type(cmd) is locals()[name] - assert cmd.obj is obj - - assert r.commands.kinit.obj is None - - for cmd in r.commands(): - raised = False - try: - cmd.obj = None - except exceptions.TwiceSetError: - raised = True - assert raised - - u = r.objects.user - assert isinstance(u.commands, base.NameSpace) - assert len(u.commands) == 4 - assert list(u.commands) == ['adduser', 'deluser', 'finduser', 'moduser'] + + objects = read_only(r, 'objects') + assert isinstance(objects, base.NameSpace) + assert len(objects) == 2 + assert list(objects) == ['group', 'user'] + assert type(objects.user) is user + assert type(objects.group) is group + + u = objects.user + assert len(u.methods) == 2 + assert list(u.methods) == ['add', 'del'] + assert len(u.properties) == 3 + assert list(u.properties) == ['firstname', 'lastname', 'login'] + + for m in u.methods(): + assert m.obj is u + for p in u.properties(): + assert p.obj is u + + g = objects.group + assert len(g.methods) == 0 + assert len(g.properties) == 0 + + + assert len(r.commands) == 3 + assert list(r.commands) == sorted(['kinit', 'add_user', 'del_user']) -- cgit From 48c7da47c78c5b5f97dc01a7593313943aef7b6e Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sun, 20 Jul 2008 23:43:16 +0000 Subject: 25: Updated plugin examples, ipa script --- ipalib/crud.py | 8 ++--- ipalib/plugins.py | 104 ++++++++++++++++++++++++++++++++---------------------- 2 files changed, 66 insertions(+), 46 deletions(-) (limited to 'ipalib') diff --git a/ipalib/crud.py b/ipalib/crud.py index 89f7001b..b61239d2 100644 --- a/ipalib/crud.py +++ b/ipalib/crud.py @@ -24,14 +24,14 @@ Base classes for objects with CRUD functionality. import base -class Add(base.Command): +class Add(base.Method): pass -class Del(base.Command): +class Del(base.Method): pass -class Mod(base.Command): +class Mod(base.Method): pass -class Find(base.Command): +class Find(base.Method): pass diff --git a/ipalib/plugins.py b/ipalib/plugins.py index a28ac2fb..7c1dcf91 100644 --- a/ipalib/plugins.py +++ b/ipalib/plugins.py @@ -25,65 +25,85 @@ import crud import base from run import api -class user(base.Object): + +# Register some methods for the 'user' object: +class user__add(crud.Add): pass -api.register(user) +api.register(user__add) -class adduser(crud.Add): - _obj = 'user' -api.register(adduser) +class user__del(crud.Del): + pass +api.register(user__del) -class deluser(crud.Del): - _obj = 'user' -api.register(deluser) +class user__mod(crud.Mod): + pass +api.register(user__mod) -class moduser(crud.Mod): - _obj = 'user' -api.register(moduser) +class user__find(crud.Find): + pass +api.register(user__find) -class finduser(crud.Find): - _obj = 'user' -api.register(finduser) +# Register some properties for the 'user' object: +class user__firstname(base.Property): + pass +api.register(user__firstname) +class user__lastname(base.Property): + pass +api.register(user__lastname) -class group(base.Object): +class user__lastname(base.Property): pass -api.register(group) +api.register(user__lastname) -class addgroup(crud.Add): - _obj = 'group' -api.register(addgroup) -class delgroup(crud.Del): - _obj = 'group' -api.register(delgroup) +# Register some methods for the 'group' object: +class group__add(crud.Add): + pass +api.register(group__add) + +class group__del(crud.Del): + pass +api.register(group__del) -class modgroup(crud.Mod): - _obj = 'group' -api.register(modgroup) +class group__mod(crud.Mod): + pass +api.register(group__mod) -class findgroup(crud.Find): - _obj = 'group' -api.register(findgroup) +class group__find(crud.Find): + pass +api.register(group__find) -class service(base.Object): +# Register some methods for the 'service' object +class service__add(crud.Add): pass -api.register(service) +api.register(service__add) + +class service__del(crud.Del): + pass +api.register(service__del) + +class service__mod(crud.Mod): + pass +api.register(service__mod) + +class service__find(crud.Find): + pass +api.register(service__find) -class addservice(crud.Add): - _obj = 'service' -api.register(addservice) -class delservice(crud.Del): - _obj = 'service' -api.register(delservice) +# And to emphasis that the registration order doesn't matter, +# we'll register the objects last: +class group(base.Object): + pass +api.register(group) -class modservice(crud.Mod): - _obj = 'service' -api.register(modservice) +class service(base.Object): + pass +api.register(service) -class findservice(crud.Find): - _obj = 'service' -api.register(findservice) +class user(base.Object): + pass +api.register(user) -- cgit From 7273d48169a6c0dabc1bfb0f42bafb06515fdac9 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 21 Jul 2008 01:44:59 +0000 Subject: 26: Added AbstractCommand.get_doc() method to return the gettext translated summary of command; added get_doc() method to all example --- ipalib/base.py | 29 +++++++++++++++++++++++++- ipalib/plugins.py | 61 +++++++++++++++++++++++++++++++++++++++---------------- 2 files changed, 72 insertions(+), 18 deletions(-) (limited to 'ipalib') diff --git a/ipalib/base.py b/ipalib/base.py index a62d5812..522b13b1 100644 --- a/ipalib/base.py +++ b/ipalib/base.py @@ -95,7 +95,7 @@ class NameSpace(object): """ Returns True if namespace has an item named `key`. """ - return key.replace('-', '_') in self.__kw + return bool(key in self.__kw) def __iter__(self): """ @@ -135,17 +135,44 @@ class Named(object): def _get_name(self): return self.__class__.__name__ + def __get_loc(self): + cls = self.__class__ + return '%s.%s' % (cls.__module__, cls.__name__) + loc = property(__get_loc) + def __get_name(self): if self.__name is None: self.__name = self._get_name() return self.__name name = property(__get_name) + def __get_cli_name(self): + return self.name.replace('_', '-') + cli_name = property(__get_cli_name) + class AbstractCommand(object): def __call__(self): print 'You called %s()' % self.name + def get_doc(self, _): + """ + This should return a gettext translated summarary of the command. + + For example, if you were documenting the 'add-user' command, you're + method would look something like this. + + >>> def get_doc(self, _): + >>> return _('add new user') + """ + raise NotImplementedError('%s.%s.%s()' % ( + self.__class__.__module__, + self.__class__.__name__, + 'get_doc', + ) + ) + + class Attribute(Named): __locked = False __obj = None diff --git a/ipalib/plugins.py b/ipalib/plugins.py index 7c1dcf91..85f3a9f4 100644 --- a/ipalib/plugins.py +++ b/ipalib/plugins.py @@ -26,21 +26,37 @@ import base from run import api +# Hypothetical functional commands (not associated with any object): +class krbtest(base.Command): + def get_doc(self, _): + return _('test your Kerberos ticket') +api.register(krbtest) + +class discover(base.Command): + def get_doc(self, _): + return _('discover IPA servers on network') +api.register(discover) + + # Register some methods for the 'user' object: class user__add(crud.Add): - pass + def get_doc(self, _): + return _('add new user') api.register(user__add) class user__del(crud.Del): - pass + def get_doc(self, _): + return _('delete existing user') api.register(user__del) class user__mod(crud.Mod): - pass + def get_doc(self, _): + return _('edit existing user') api.register(user__mod) class user__find(crud.Find): - pass + def get_doc(self, _): + return _('search for users') api.register(user__find) @@ -53,57 +69,68 @@ class user__lastname(base.Property): pass api.register(user__lastname) -class user__lastname(base.Property): +class user__login(base.Property): pass -api.register(user__lastname) +api.register(user__login) # Register some methods for the 'group' object: class group__add(crud.Add): - pass + def get_doc(self, _): + return _('add new group') api.register(group__add) class group__del(crud.Del): - pass + def get_doc(self, _): + return _('delete existing group') api.register(group__del) class group__mod(crud.Mod): - pass + def get_doc(self, _): + return _('exit existing group') api.register(group__mod) class group__find(crud.Find): - pass + def get_doc(self, _): + return _('search for groups') api.register(group__find) # Register some methods for the 'service' object class service__add(crud.Add): - pass + def get_doc(self, _): + return _('add new service') api.register(service__add) class service__del(crud.Del): - pass + def get_doc(self, _): + return _('delete existing service') api.register(service__del) class service__mod(crud.Mod): - pass + def get_doc(self, _): + return _('edit existing service') api.register(service__mod) class service__find(crud.Find): - pass + def get_doc(self, _): + return _('search for services') api.register(service__find) # And to emphasis that the registration order doesn't matter, # we'll register the objects last: class group(base.Object): - pass + def get_doc(self, _): + return _('') api.register(group) class service(base.Object): - pass + def get_doc(self, _): + return _('') api.register(service) class user(base.Object): - pass + def get_doc(self, _): + return _('') api.register(user) -- cgit From 0c574d830062d7957c2c65081e3e66fc0bb41759 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 21 Jul 2008 01:58:22 +0000 Subject: 27: Added quick hack for replace('-', '_') problem I'm having --- ipalib/base.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/base.py b/ipalib/base.py index 522b13b1..aa867018 100644 --- a/ipalib/base.py +++ b/ipalib/base.py @@ -153,7 +153,10 @@ class Named(object): class AbstractCommand(object): def __call__(self): - print 'You called %s()' % self.name + print 'You called %s.%s()' % ( + self.__class__.__module__, + self.__class__.__name__ + ) def get_doc(self, _): """ -- cgit From fc33f5d359573cd977d168ea1fbed97cdc55c992 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 22 Jul 2008 06:41:33 +0000 Subject: 28: Added new base.Register class that is a more generic way of doing the plugin registration and doesn't itself instatiate any plugins; added corresponding unit tests --- ipalib/base.py | 58 ++++++++++++++++++++++++++++++++++++++++++++ ipalib/exceptions.py | 2 +- ipalib/tests/test_base.py | 61 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 120 insertions(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/base.py b/ipalib/base.py index aa867018..6209139f 100644 --- a/ipalib/base.py +++ b/ipalib/base.py @@ -292,6 +292,64 @@ class Collector(object): return NameSpace(self.__d) +class Proxy(object): + def __init__(self, d): + self.__d = d + + def __getattr__(self, name): + if name not in self.__d: + raise AttributeError(name) + return self.__d[name] + + + +class Register(object): + __allowed = ( + Command, + Object, + Method, + Property, + ) + + def __init__(self): + self.__d = {} + for base in self.__allowed: + assert inspect.isclass(base) + assert base.__name__ not in self.__d + sub_d = {} + self.__d[base.__name__] = sub_d + setattr(self, base.__name__, Proxy(sub_d)) + + def __iter__(self): + for key in self.__d: + yield key + + def __getitem__(self, key): + return dict(self.__d[key]) + + def items(self): + for key in self: + yield (key, self[key]) + + def __findbase(self, cls): + if not inspect.isclass(cls): + raise exceptions.RegistrationError('not a class', cls) + for base in self.__allowed: + if issubclass(cls, base): + return base + raise exceptions.RegistrationError( + 'not subclass of an allowed base', + cls, + ) + + def __call__(self, cls): + base = self.__findbase(cls) + ns = self.__d[base.__name__] + assert cls.__name__ not in ns + ns[cls.__name__] = cls + + + class Registrar(object): __objects = None __commands = None diff --git a/ipalib/exceptions.py b/ipalib/exceptions.py index 76c7da8c..4c307177 100644 --- a/ipalib/exceptions.py +++ b/ipalib/exceptions.py @@ -58,7 +58,7 @@ class DuplicateError(IPAError): class RegistrationError(IPAError): - msg = '%r must be a subclass of %s' + msg = '%s: %r' class PrefixError(IPAError): diff --git a/ipalib/tests/test_base.py b/ipalib/tests/test_base.py index 7affd997..f11f1f4f 100644 --- a/ipalib/tests/test_base.py +++ b/ipalib/tests/test_base.py @@ -347,3 +347,64 @@ def test_Registar(): assert len(r.commands) == 3 assert list(r.commands) == sorted(['kinit', 'add_user', 'del_user']) + + +class test_Register(): + r = base.Register() + + assert set(r) == set(['Command', 'Object', 'Method', 'Property']) + + + class wrong_base(object): + pass + + class krbtest(base.Command): + pass + + class user(base.Object): + pass + + class user__add(base.Method): + pass + + class user__firstname(base.Property): + pass + + + + + #r(wrong_base) + #r(user()) + + # Check that exception is raised trying to register an instance of a + # class of a correct base: + raised = False + try: + r(user()) + except exceptions.RegistrationError: + raised = True + + # Check that exception is raised trying to register class of wrong base: + raised = False + try: + r(wrong_base) + except exceptions.RegistrationError: + raised = True + assert raised + + # Check that added a valid class works + for cls in (krbtest, user, user__add, user__firstname): + r(cls) + key = cls.__bases__[0].__name__ + d = r[key] + assert d.keys() == [cls.__name__] + assert d.values() == [cls] + # Check that a copy is returned + d2 = r[key] + assert d2 == d + assert d2 is not d + p = getattr(r, key) + assert isinstance(p, base.Proxy) + # Check that same instance is returned + assert p is getattr(r, key) + assert getattr(p, cls.__name__) is cls -- cgit From 8b64314359950801f1b3220f655261bcee2ead85 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 25 Jul 2008 03:17:24 +0000 Subject: 29: Some experimentation to make the Registar more generalized --- ipalib/base.py | 94 +++++++++++++++++++++++--- ipalib/tests/test_base.py | 164 +++++++++++++++++++++++++--------------------- 2 files changed, 175 insertions(+), 83 deletions(-) (limited to 'ipalib') diff --git a/ipalib/base.py b/ipalib/base.py index 6209139f..63bb940b 100644 --- a/ipalib/base.py +++ b/ipalib/base.py @@ -303,7 +303,7 @@ class Proxy(object): -class Register(object): +class Registrar(object): __allowed = ( Command, Object, @@ -311,8 +311,13 @@ class Register(object): Property, ) - def __init__(self): - self.__d = {} + def __init__(self, d=None): + if d is None: + self.__d = {} + else: + assert isinstance(d, dict) + assert d == {} + self.__d = d for base in self.__allowed: assert inspect.isclass(base) assert base.__name__ not in self.__d @@ -349,10 +354,25 @@ class Register(object): ns[cls.__name__] = cls + def get_instances(self, base_name): + for cls in self[base_name].values(): + yield cls() + + def get_attrs(self, base_name): + d = {} + for i in self.get_instances(base_name): + if i.obj_name not in d: + d[i.obj_name] = [] + d[i.obj_name].append(i) + return d + + + + + + +class RegistrarOld(object): -class Registrar(object): - __objects = None - __commands = None def __init__(self): self.__tmp_commands = Collector() @@ -368,6 +388,7 @@ class Registrar(object): return self.__commands commands = property(__get_commands) + def __get_target(self, i): if isinstance(i, Command): return self.__tmp_commands @@ -409,13 +430,70 @@ class Registrar(object): -class API(Registrar): +class API(object): __max_cmd_len = None + __objects = None + __commands = None + + def __init__(self, registrar): + assert isinstance(registrar, Registrar) + self.__r = registrar + + def __get_objects(self): + return self.__objects + objects = property(__get_objects) + + def __get_commands(self): + return self.__commands + commands = property(__get_commands) def __get_max_cmd_len(self): if self.__max_cmd_len is None: if self.commands is None: - return 0 + return None self.__max_cmd_len = max(len(n) for n in self.commands) return self.__max_cmd_len max_cmd_len = property(__get_max_cmd_len) + + def __items(self, base, name): + for cls in self.__r[base].values(): + i = cls() + yield (getattr(i, name), i) + + def __namespace(self, base, name): + return NameSpace(dict(self.__items(base, name))) + + + + def finalize(self): + self.__objects = self.__namespace('Object', 'name') + + m = {} + for obj in self.__objects(): + if obj.name not in m: + m[obj.name] = {} + + for cls in self.__r['Method'].values(): + meth = cls() + assert meth.obj_name in m + + return + + for (key, ns) in self.__tmp_methods.namespaces(): + self.__objects[key].methods = ns + for (key, ns) in self.__tmp_properties.namespaces(): + self.__objects[key].properties = ns + commands = self.__tmp_commands.d + for obj in self.__objects(): + assert isinstance(obj, Object) + if obj.methods is None: + obj.methods = NameSpace({}) + if obj.properties is None: + obj.properties = NameSpace({}) + for m in obj.methods(): + m.obj = obj + assert m.name not in commands + commands[m.name] = m + for p in obj.properties(): + p.obj = obj + self.__commands = NameSpace(commands) diff --git a/ipalib/tests/test_base.py b/ipalib/tests/test_base.py index f11f1f4f..8f6a0313 100644 --- a/ipalib/tests/test_base.py +++ b/ipalib/tests/test_base.py @@ -285,10 +285,82 @@ def test_Collector(): assert ns.group is g +class test_Registrar(): + r = base.Registrar() + allowed = set(['Command', 'Object', 'Method', 'Property']) + assert set(r) == allowed + + # Some test classes: + class wrong_base(object): + pass + class krbtest(base.Command): + pass + class user(base.Object): + pass + class user__add(base.Method): + pass + class user__firstname(base.Property): + pass + + # Check that exception is raised trying to register an instance of a + # class of a correct base: + raised = False + try: + r(user()) + except exceptions.RegistrationError: + raised = True + + # Check that exception is raised trying to register class of wrong base: + raised = False + try: + r(wrong_base) + except exceptions.RegistrationError: + raised = True + assert raised + + # Check that adding a valid class works + for cls in (krbtest, user, user__add, user__firstname): + r(cls) + key = cls.__bases__[0].__name__ + d = r[key] + assert d.keys() == [cls.__name__] + assert d.values() == [cls] + # Check that a copy is returned + d2 = r[key] + assert d2 == d + assert d2 is not d + p = getattr(r, key) + assert isinstance(p, base.Proxy) + # Check that same instance is returned + assert p is getattr(r, key) + assert getattr(p, cls.__name__) is cls + + for base_name in allowed: + for i in r.get_instances(base_name): + assert isinstance(i, getattr(base, base_name)) + + + m = r.get_attrs('Method') + assert isinstance(m, dict) + assert len(m) == 1 + assert len(m['user']) == 1 + assert isinstance(m['user'][0], user__add) + + p = r.get_attrs('Property') + assert isinstance(p, dict) + assert len(p) == 1 + assert len(p['user']) == 1 + assert isinstance(p['user'][0], user__firstname) + -def test_Registar(): + + +def test_API(): + r = base.Registrar() + api = base.API(r) + class kinit(base.Command): pass class user__add(base.Method): @@ -306,29 +378,32 @@ def test_Registar(): class group(base.Object): pass - r = base.Registrar() - assert read_only(r, 'objects') is None - assert read_only(r, 'commands') is None + assert read_only(api, 'objects') is None + assert read_only(api, 'commands') is None + assert read_only(api, 'max_cmd_len') is None + + r(kinit) + r(user__add) + r(user__del) + r(user__firstname) + r(user__lastname) + r(user__login) + r(user) + r(group) - r.register(kinit) - r.register(user__add) - r.register(user__del) - r.register(user__firstname) - r.register(user__lastname) - r.register(user__login) - r.register(user) - r.register(group) + api.finalize() - r.finalize() - objects = read_only(r, 'objects') + objects = read_only(api, 'objects') assert isinstance(objects, base.NameSpace) assert len(objects) == 2 assert list(objects) == ['group', 'user'] assert type(objects.user) is user assert type(objects.group) is group + return + u = objects.user assert len(u.methods) == 2 assert list(u.methods) == ['add', 'del'] @@ -347,64 +422,3 @@ def test_Registar(): assert len(r.commands) == 3 assert list(r.commands) == sorted(['kinit', 'add_user', 'del_user']) - - -class test_Register(): - r = base.Register() - - assert set(r) == set(['Command', 'Object', 'Method', 'Property']) - - - class wrong_base(object): - pass - - class krbtest(base.Command): - pass - - class user(base.Object): - pass - - class user__add(base.Method): - pass - - class user__firstname(base.Property): - pass - - - - - #r(wrong_base) - #r(user()) - - # Check that exception is raised trying to register an instance of a - # class of a correct base: - raised = False - try: - r(user()) - except exceptions.RegistrationError: - raised = True - - # Check that exception is raised trying to register class of wrong base: - raised = False - try: - r(wrong_base) - except exceptions.RegistrationError: - raised = True - assert raised - - # Check that added a valid class works - for cls in (krbtest, user, user__add, user__firstname): - r(cls) - key = cls.__bases__[0].__name__ - d = r[key] - assert d.keys() == [cls.__name__] - assert d.values() == [cls] - # Check that a copy is returned - d2 = r[key] - assert d2 == d - assert d2 is not d - p = getattr(r, key) - assert isinstance(p, base.Proxy) - # Check that same instance is returned - assert p is getattr(r, key) - assert getattr(p, cls.__name__) is cls -- cgit From bc1675dc3853748064dbf1485bf58bce0e344add Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 28 Jul 2008 04:34:25 +0000 Subject: 30: Added plugable module with more generic implementation of Registrar; added corresponding unit tests --- ipalib/exceptions.py | 72 +++++++++++++++++++++++--- ipalib/plugable.py | 95 ++++++++++++++++++++++++++++++++++ ipalib/tests/test_plugable.py | 115 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 275 insertions(+), 7 deletions(-) create mode 100644 ipalib/plugable.py create mode 100644 ipalib/tests/test_plugable.py (limited to 'ipalib') diff --git a/ipalib/exceptions.py b/ipalib/exceptions.py index 4c307177..376a7a56 100644 --- a/ipalib/exceptions.py +++ b/ipalib/exceptions.py @@ -45,24 +45,82 @@ class IPAError(Exception): return self.msg % self.kw + + + class SetError(IPAError): msg = 'setting %r, but NameSpace does not allow attribute setting' -class OverrideError(IPAError): - msg = 'unexpected override of %r (use override=True if intended)' -class DuplicateError(IPAError): - msg = 'class %r at %d was already registered' + class RegistrationError(IPAError): - msg = '%s: %r' + """ + Base class for errors that occur during plugin registration. + """ + + +class SubclassError(RegistrationError): + """ + Raised when registering a plugin that is not a subclass of one of the + allowed bases. + """ + msg = 'plugin %r not subclass of any base in %r' + + def __init__(self, cls, allowed): + self.cls = cls + self.allowed = allowed + + def __str__(self): + return self.msg % (self.cls, self.allowed) + + +class DuplicateError(RegistrationError): + """ + Raised when registering a plugin whose exact class has already been + registered. + """ + msg = '%r at %d was already registered' + def __init__(self, cls): + self.cls = cls + + def __str__(self): + return self.msg % (self.cls, id(self.cls)) + + +class OverrideError(RegistrationError): + """ + Raised when override=False yet registering a plugin that overrides an + existing plugin in the same namespace. + """ + msg = 'unexpected override of %s.%s with %r (use override=True if intended)' + + def __init__(self, base, cls): + self.base = base + self.cls = cls + + def __str__(self): + return self.msg % (self.base.__name__, self.cls.__name__, self.cls) + + +class MissingOverrideError(RegistrationError): + """ + Raised when override=True yet no preexisting plugin with the same name + and base has been registered. + """ + msg = '%s.%s has not been registered, cannot override with %r' + + def __init__(self, base, cls): + self.base = base + self.cls = cls + + def __str__(self): + return self.msg % (self.base.__name__, self.cls.__name__, self.cls) -class PrefixError(IPAError): - msg = 'class name %r must start with %r' class TwiceSetError(IPAError): diff --git a/ipalib/plugable.py b/ipalib/plugable.py new file mode 100644 index 00000000..ba2241b9 --- /dev/null +++ b/ipalib/plugable.py @@ -0,0 +1,95 @@ +# Authors: +# Jason Gerard DeRose +# +# 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 + +""" +Utility classes for registering plugins, base classe for writing plugins. +""" + +import inspect +import exceptions + + + +class Registrar(object): + def __init__(self, *allowed): + """ + `*allowed` is a list of the base classes plugins can be subclassed + from. + """ + self.__allowed = frozenset(allowed) + self.__d = {} + self.__registered = set() + assert len(self.__allowed) == len(allowed) + for base in self.__allowed: + assert inspect.isclass(base) + assert base.__name__ not in self.__d + self.__d[base.__name__] = {} + + def __findbase(self, cls): + """ + If `cls` is a subclass of a base in self.__allowed, returns that + base; otherwise raises SubclassError. + """ + assert inspect.isclass(cls) + for base in self.__allowed: + if issubclass(cls, base): + return base + raise exceptions.SubclassError(cls, self.__allowed) + + def __call__(self, cls, override=False): + """ + Register the plugin `cls`. + """ + if not inspect.isclass(cls): + raise TypeError('plugin must be a class: %r' % cls) + + # Find the base class or raise SubclassError: + base = self.__findbase(cls) + sub_d = self.__d[base.__name__] + + # Raise DuplicateError if this exact class was already registered: + if cls in self.__registered: + raise exceptions.DuplicateError(cls) + + # Check override: + if cls.__name__ in sub_d: + # Must use override=True to override: + if not override: + raise exceptions.OverrideError(base, cls) + else: + # There was nothing already registered to override: + if override: + raise exceptions.MissingOverrideError(base, cls) + + # The plugin is okay, add to __registered and sub_d: + self.__registered.add(cls) + sub_d[cls.__name__] = cls + + def __getitem__(self, name): + """ + Returns a copy of the namespace dict of the base class named `name`. + """ + return dict(self.__d[name]) + + def __iter__(self): + """ + Iterates through the names of the allowed base classes. + """ + for key in self.__d: + yield key diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py new file mode 100644 index 00000000..1ba02113 --- /dev/null +++ b/ipalib/tests/test_plugable.py @@ -0,0 +1,115 @@ +# Authors: +# Jason Gerard DeRose +# +# 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 + +""" +Unit tests for `ipalib.plugable` module. +""" + +from ipalib import plugable, exceptions + + +def test_Registrar(): + class Base1(object): + pass + class Base2(object): + pass + class Base3(object): + pass + class plugin1(Base1): + pass + class plugin2(Base2): + pass + class plugin3(Base3): + pass + + # Test creation of Registrar: + r = plugable.Registrar(Base1, Base2) + assert sorted(r) == ['Base1', 'Base2'] + + # Check that TypeError is raised trying to register something that isn't + # a class: + raised = False + try: + r(plugin1()) + except TypeError: + raised = True + assert raised + + # Check that SubclassError is raised trying to register a class that is + # not a subclass of an allowed base: + raised = False + try: + r(plugin3) + except exceptions.SubclassError: + raised = True + assert raised + + # Check that registration works + r(plugin1) + sub_d = r['Base1'] + assert len(sub_d) == 1 + assert sub_d['plugin1'] is plugin1 + # Check that a copy is returned + assert sub_d is not r['Base1'] + assert sub_d == r['Base1'] + + # Check that DuplicateError is raised trying to register exact class + # again: + raised = False + try: + r(plugin1) + except exceptions.DuplicateError: + raised = True + assert raised + + # Check that OverrideError is raised trying to register class with same + # name and same base: + orig1 = plugin1 + class base1_extended(Base1): + pass + class plugin1(base1_extended): + pass + raised = False + try: + r(plugin1) + except exceptions.OverrideError: + raised = True + assert raised + + # Check that overriding works + r(plugin1, override=True) + sub_d = r['Base1'] + assert len(sub_d) == 1 + assert sub_d['plugin1'] is plugin1 + assert sub_d['plugin1'] is not orig1 + + # Check that MissingOverrideError is raised trying to override a name + # not yet registerd: + raised = False + try: + r(plugin2, override=True) + except exceptions.MissingOverrideError: + raised = True + assert raised + + # Check that additional plugin can be registered: + r(plugin2) + sub_d = r['Base2'] + assert len(sub_d) == 1 + assert sub_d['plugin2'] is plugin2 -- cgit From d7569a84b94ab304a1b7f353ea71c15061ebd5d4 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 31 Jul 2008 18:57:10 +0000 Subject: 31: Renamed exceptions.py to errors.py --- ipalib/base.py | 16 +++--- ipalib/errors.py | 127 ++++++++++++++++++++++++++++++++++++++++++ ipalib/exceptions.py | 127 ------------------------------------------ ipalib/plugable.py | 10 ++-- ipalib/tests/test_base.py | 10 ++-- ipalib/tests/test_plugable.py | 10 ++-- 6 files changed, 150 insertions(+), 150 deletions(-) create mode 100644 ipalib/errors.py delete mode 100644 ipalib/exceptions.py (limited to 'ipalib') diff --git a/ipalib/base.py b/ipalib/base.py index 63bb940b..ae9dfae4 100644 --- a/ipalib/base.py +++ b/ipalib/base.py @@ -23,7 +23,7 @@ Base classes for plug-in architecture and generative API. import re import inspect -import exceptions +import errors class NameSpace(object): @@ -45,7 +45,7 @@ class NameSpace(object): For example, setting an attribute the normal way will raise an exception: >>> ns.my_message = 'some new value' - (raises exceptions.SetError) + (raises errors.SetError) But a programmer could still set the attribute like this: @@ -82,7 +82,7 @@ class NameSpace(object): NameSpace has been locked; otherwise calls object.__setattr__(). """ if self.__locked: - raise exceptions.SetError(name) + raise errors.SetError(name) super(NameSpace, self).__setattr__(name, value) def __getitem__(self, key): @@ -190,7 +190,7 @@ class Attribute(Named): return self.__obj def __set_obj(self, obj): if self.__obj is not None: - raise exceptions.TwiceSetError(self.__class__.__name__, 'obj') + raise errors.TwiceSetError(self.__class__.__name__, 'obj') assert isinstance(obj, Object) self.__obj = obj assert self.obj is obj @@ -227,7 +227,7 @@ class Object(Named): return self.__methods def __set_methods(self, methods): if self.__methods is not None: - raise exceptions.TwiceSetError( + raise errors.TwiceSetError( self.__class__.__name__, 'methods' ) assert type(methods) is NameSpace @@ -239,7 +239,7 @@ class Object(Named): return self.__properties def __set_properties(self, properties): if self.__properties is not None: - raise exceptions.TwiceSetError( + raise errors.TwiceSetError( self.__class__.__name__, 'properties' ) assert type(properties) is NameSpace @@ -338,11 +338,11 @@ class Registrar(object): def __findbase(self, cls): if not inspect.isclass(cls): - raise exceptions.RegistrationError('not a class', cls) + raise errors.RegistrationError('not a class', cls) for base in self.__allowed: if issubclass(cls, base): return base - raise exceptions.RegistrationError( + raise errors.RegistrationError( 'not subclass of an allowed base', cls, ) diff --git a/ipalib/errors.py b/ipalib/errors.py new file mode 100644 index 00000000..53a0870e --- /dev/null +++ b/ipalib/errors.py @@ -0,0 +1,127 @@ +# Authors: +# Jason Gerard DeRose +# +# Copyright (C) 2008 Red Hat +# see file 'COPYING' for use and warranty inmsgion +# +# 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 + +""" +All custom errors raised by `ipalib` package. +""" + +class IPAError(Exception): + """ + Use this base class for your custom IPA errors unless there is a + specific reason to subclass from AttributeError, KeyError, etc. + """ + msg = None + + def __init__(self, *args, **kw): + self.args = args + self.kw = kw + + def __str__(self): + """ + Returns the string representation of this exception. + """ + if self.msg is None: + if len(self.args) == 1: + return unicode(self.args[0]) + return unicode(self.args) + if len(self.args) > 0: + return self.msg % self.args + return self.msg % self.kw + + + + + +class SetError(IPAError): + msg = 'setting %r, but NameSpace does not allow attribute setting' + + + + + + + +class RegistrationError(IPAError): + """ + Base class for errors that occur during plugin registration. + """ + + +class SubclassError(RegistrationError): + """ + Raised when registering a plugin that is not a subclass of one of the + allowed bases. + """ + msg = 'plugin %r not subclass of any base in %r' + + def __init__(self, cls, allowed): + self.cls = cls + self.allowed = allowed + + def __str__(self): + return self.msg % (self.cls, self.allowed) + + +class DuplicateError(RegistrationError): + """ + Raised when registering a plugin whose exact class has already been + registered. + """ + msg = '%r at %d was already registered' + + def __init__(self, cls): + self.cls = cls + + def __str__(self): + return self.msg % (self.cls, id(self.cls)) + + +class OverrideError(RegistrationError): + """ + Raised when override=False yet registering a plugin that overrides an + existing plugin in the same namespace. + """ + msg = 'unexpected override of %s.%s with %r (use override=True if intended)' + + def __init__(self, base, cls): + self.base = base + self.cls = cls + + def __str__(self): + return self.msg % (self.base.__name__, self.cls.__name__, self.cls) + + +class MissingOverrideError(RegistrationError): + """ + Raised when override=True yet no preexisting plugin with the same name + and base has been registered. + """ + msg = '%s.%s has not been registered, cannot override with %r' + + def __init__(self, base, cls): + self.base = base + self.cls = cls + + def __str__(self): + return self.msg % (self.base.__name__, self.cls.__name__, self.cls) + + + +class TwiceSetError(IPAError): + msg = '%s.%s cannot be set twice' diff --git a/ipalib/exceptions.py b/ipalib/exceptions.py deleted file mode 100644 index 376a7a56..00000000 --- a/ipalib/exceptions.py +++ /dev/null @@ -1,127 +0,0 @@ -# Authors: -# Jason Gerard DeRose -# -# Copyright (C) 2008 Red Hat -# see file 'COPYING' for use and warranty inmsgion -# -# 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 - -""" -All custom exceptions raised by `ipalib` package. -""" - -class IPAError(Exception): - """ - Use this base class for your custom IPA exceptions unless there is a - specific reason to subclass from AttributeError, KeyError, etc. - """ - msg = None - - def __init__(self, *args, **kw): - self.args = args - self.kw = kw - - def __str__(self): - """ - Returns the string representation of this exception. - """ - if self.msg is None: - if len(self.args) == 1: - return unicode(self.args[0]) - return unicode(self.args) - if len(self.args) > 0: - return self.msg % self.args - return self.msg % self.kw - - - - - -class SetError(IPAError): - msg = 'setting %r, but NameSpace does not allow attribute setting' - - - - - - - -class RegistrationError(IPAError): - """ - Base class for errors that occur during plugin registration. - """ - - -class SubclassError(RegistrationError): - """ - Raised when registering a plugin that is not a subclass of one of the - allowed bases. - """ - msg = 'plugin %r not subclass of any base in %r' - - def __init__(self, cls, allowed): - self.cls = cls - self.allowed = allowed - - def __str__(self): - return self.msg % (self.cls, self.allowed) - - -class DuplicateError(RegistrationError): - """ - Raised when registering a plugin whose exact class has already been - registered. - """ - msg = '%r at %d was already registered' - - def __init__(self, cls): - self.cls = cls - - def __str__(self): - return self.msg % (self.cls, id(self.cls)) - - -class OverrideError(RegistrationError): - """ - Raised when override=False yet registering a plugin that overrides an - existing plugin in the same namespace. - """ - msg = 'unexpected override of %s.%s with %r (use override=True if intended)' - - def __init__(self, base, cls): - self.base = base - self.cls = cls - - def __str__(self): - return self.msg % (self.base.__name__, self.cls.__name__, self.cls) - - -class MissingOverrideError(RegistrationError): - """ - Raised when override=True yet no preexisting plugin with the same name - and base has been registered. - """ - msg = '%s.%s has not been registered, cannot override with %r' - - def __init__(self, base, cls): - self.base = base - self.cls = cls - - def __str__(self): - return self.msg % (self.base.__name__, self.cls.__name__, self.cls) - - - -class TwiceSetError(IPAError): - msg = '%s.%s cannot be set twice' diff --git a/ipalib/plugable.py b/ipalib/plugable.py index ba2241b9..0de31d82 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -22,7 +22,7 @@ Utility classes for registering plugins, base classe for writing plugins. """ import inspect -import exceptions +import errors @@ -50,7 +50,7 @@ class Registrar(object): for base in self.__allowed: if issubclass(cls, base): return base - raise exceptions.SubclassError(cls, self.__allowed) + raise errors.SubclassError(cls, self.__allowed) def __call__(self, cls, override=False): """ @@ -65,17 +65,17 @@ class Registrar(object): # Raise DuplicateError if this exact class was already registered: if cls in self.__registered: - raise exceptions.DuplicateError(cls) + raise errors.DuplicateError(cls) # Check override: if cls.__name__ in sub_d: # Must use override=True to override: if not override: - raise exceptions.OverrideError(base, cls) + raise errors.OverrideError(base, cls) else: # There was nothing already registered to override: if override: - raise exceptions.MissingOverrideError(base, cls) + raise errors.MissingOverrideError(base, cls) # The plugin is okay, add to __registered and sub_d: self.__registered.add(cls) diff --git a/ipalib/tests/test_base.py b/ipalib/tests/test_base.py index 8f6a0313..da9de7a0 100644 --- a/ipalib/tests/test_base.py +++ b/ipalib/tests/test_base.py @@ -21,7 +21,7 @@ Unit tests for `ipalib.base` module. """ -from ipalib import base, exceptions, crud +from ipalib import base, errors, crud def read_only(obj, name): @@ -129,7 +129,7 @@ class test_NameSpace: raised = False try: setattr(ns, key, value) - except exceptions.SetError: + except errors.SetError: raised = True assert raised assert getattr(ns, key, None) != value @@ -206,7 +206,7 @@ def test_Attribute(): raised = False try: i.obj = u - except exceptions.TwiceSetError: + except errors.TwiceSetError: raised = True assert raised @@ -307,14 +307,14 @@ class test_Registrar(): raised = False try: r(user()) - except exceptions.RegistrationError: + except errors.RegistrationError: raised = True # Check that exception is raised trying to register class of wrong base: raised = False try: r(wrong_base) - except exceptions.RegistrationError: + except errors.RegistrationError: raised = True assert raised diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index 1ba02113..f0bdeb4a 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -21,7 +21,7 @@ Unit tests for `ipalib.plugable` module. """ -from ipalib import plugable, exceptions +from ipalib import plugable, errors def test_Registrar(): @@ -56,7 +56,7 @@ def test_Registrar(): raised = False try: r(plugin3) - except exceptions.SubclassError: + except errors.SubclassError: raised = True assert raised @@ -74,7 +74,7 @@ def test_Registrar(): raised = False try: r(plugin1) - except exceptions.DuplicateError: + except errors.DuplicateError: raised = True assert raised @@ -88,7 +88,7 @@ def test_Registrar(): raised = False try: r(plugin1) - except exceptions.OverrideError: + except errors.OverrideError: raised = True assert raised @@ -104,7 +104,7 @@ def test_Registrar(): raised = False try: r(plugin2, override=True) - except exceptions.MissingOverrideError: + except errors.MissingOverrideError: raised = True assert raised -- cgit From a131ebf72469d416d4c08e23a7f3ac70854b237b Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 31 Jul 2008 22:36:15 +0000 Subject: 32: Added Plugin and Proxy base classes in plugable module, along with to_cli() and from_cli() functions; added correspending unit tests --- ipalib/plugable.py | 84 +++++++++++++++++++++++++++++++++++++++++++ ipalib/tests/test_plugable.py | 47 ++++++++++++++++++++++++ 2 files changed, 131 insertions(+) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 0de31d82..054b12db 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -21,10 +21,94 @@ Utility classes for registering plugins, base classe for writing plugins. """ + import inspect import errors +def to_cli(name): + assert isinstance(name, basestring) + return name.replace('__', '.').replace('_', '-') + +def from_cli(cli_name): + assert isinstance(cli_name, basestring) + return cli_name.replace('-', '_').replace('.', '__') + + +class Plugin(object): + """ + Base class for all plugins. + """ + + def __get_name(self): + """ + Returns the class name of this instance. + """ + return self.__class__.__name__ + name = property(__get_name) + + def __repr__(self): + """ + Returns a valid Python expression that could create this plugin + instance given the appropriate environment. + """ + return '%s.%s()' % ( + self.__class__.__module__, + self.__class__.__name__ + ) + + +class Proxy(object): + """ + Used to only export certain attributes into the dynamic API. + + Subclasses must list names of attributes to be proxied in the __slots__ + class attribute. + """ + + __slots__ = ( + '__obj', + 'name', + 'cli_name', + ) + + def __init__(self, obj, proxy_name=None): + """ + Proxy attributes on `obj`. + """ + if proxy_name is None: + proxy_name = obj.name + assert isinstance(proxy_name, str) + object.__setattr__(self, '_Proxy__obj', obj) + object.__setattr__(self, 'name', proxy_name) + object.__setattr__(self, 'cli_name', to_cli(proxy_name)) + for name in self.__slots__: + object.__setattr__(self, name, getattr(obj, name)) + + def __setattr__(self, name, value): + """ + Proxy instances are read-only. This raises an AttributeError + anytime an attempt is made to set an attribute. + """ + raise AttributeError('cannot set %s.%s' % + (self.__class__.__name__, name) + ) + + def __delattr__(self, name): + """ + Proxy instances are read-only. This raises an AttributeError + anytime an attempt is made to delete an attribute. + """ + raise AttributeError('cannot del %s.%s' % + (self.__class__.__name__, name) + ) + + def __repr__(self): + return '%s(%r)' % (self.__class__.__name__, self.__obj) + + def __str__(self): + return self.cli_name + class Registrar(object): def __init__(self, *allowed): diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index f0bdeb4a..0421e72b 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -24,6 +24,53 @@ Unit tests for `ipalib.plugable` module. from ipalib import plugable, errors +def test_to_cli(): + f = plugable.to_cli + assert f('initialize') == 'initialize' + assert f('find_everything') == 'find-everything' + assert f('user__add') == 'user.add' + assert f('meta_service__do_something') == 'meta-service.do-something' + + +def test_from_cli(): + f = plugable.from_cli + assert f('initialize') == 'initialize' + assert f('find-everything') == 'find_everything' + assert f('user.add') == 'user__add' + assert f('meta-service.do-something') == 'meta_service__do_something' + + +def test_Plugin(): + p = plugable.Plugin() + assert p.name == 'Plugin' + assert repr(p) == '%s.Plugin()' % plugable.__name__ + + class some_plugin(plugable.Plugin): + pass + p = some_plugin() + assert p.name == 'some_plugin' + assert repr(p) == '%s.some_plugin()' % __name__ + + +def test_Proxy(): + class CommandProxy(plugable.Proxy): + __slots__ = ( + 'get_label', + '__call__', + ) + + class Command(plugable.Plugin): + def get_label(self): + return 'Add User' + def __call__(self, *argv, **kw): + return (argv, kw) + + i = Command() + p = CommandProxy(i, 'hello') + assert '__dict__' not in dir(p) + #assert repr(p) == 'CommandProxy(%s.Command())' % __name__ + + def test_Registrar(): class Base1(object): pass -- cgit From f53dec2600f95246a72fa3c847a485d2a94edfa7 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 1 Aug 2008 01:47:49 +0000 Subject: 33: Finished unit tests for plugable.Proxy --- ipalib/plugable.py | 17 +++++++--- ipalib/tests/test_plugable.py | 75 +++++++++++++++++++++++++++++++++++++------ 2 files changed, 78 insertions(+), 14 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 054b12db..de5f3f8f 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -27,10 +27,19 @@ import errors def to_cli(name): - assert isinstance(name, basestring) + """ + Takes a Python identifier and transforms it into form suitable for the + Command Line Interface. + """ + assert isinstance(name, str) return name.replace('__', '.').replace('_', '-') + def from_cli(cli_name): + """ + Takes a string from the Command Line Interface and transforms it into a + Python identifier. + """ assert isinstance(cli_name, basestring) return cli_name.replace('-', '_').replace('.', '__') @@ -69,7 +78,6 @@ class Proxy(object): __slots__ = ( '__obj', 'name', - 'cli_name', ) def __init__(self, obj, proxy_name=None): @@ -77,11 +85,10 @@ class Proxy(object): Proxy attributes on `obj`. """ if proxy_name is None: - proxy_name = obj.name + proxy_name = obj.__class__.__name__ assert isinstance(proxy_name, str) object.__setattr__(self, '_Proxy__obj', obj) object.__setattr__(self, 'name', proxy_name) - object.__setattr__(self, 'cli_name', to_cli(proxy_name)) for name in self.__slots__: object.__setattr__(self, name, getattr(obj, name)) @@ -107,7 +114,7 @@ class Proxy(object): return '%s(%r)' % (self.__class__.__name__, self.__obj) def __str__(self): - return self.cli_name + return to_cli(self.name) class Registrar(object): diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index 0421e72b..023bf45f 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -55,20 +55,77 @@ def test_Plugin(): def test_Proxy(): class CommandProxy(plugable.Proxy): __slots__ = ( - 'get_label', + 'validate', '__call__', ) - class Command(plugable.Plugin): - def get_label(self): - return 'Add User' - def __call__(self, *argv, **kw): - return (argv, kw) + class do_something(object): + def __repr__(self): + return '' - i = Command() - p = CommandProxy(i, 'hello') + def __call__(self, arg): + return arg + 1 + + def validate(self, arg): + return arg + 2 + + def not_public(self, arg): + return arg + 3 + + # Test basic Proxy functionality + i = do_something() + p = CommandProxy(i) assert '__dict__' not in dir(p) - #assert repr(p) == 'CommandProxy(%s.Command())' % __name__ + assert p.name == 'do_something' + assert str(p) == 'do-something' + assert repr(p) == 'CommandProxy()' + assert p(1) == 2 + assert p.validate(1) == 3 + + # Test that proxy_name can be overriden: + i = do_something() + p = CommandProxy(i, proxy_name='user__add') + assert '__dict__' not in dir(p) + assert p.name == 'user__add' + assert str(p) == 'user.add' + assert repr(p) == 'CommandProxy()' + assert p(1) == 2 + assert p.validate(1) == 3 + + # Test that attributes not listed in __slots__ are not present: + name = 'not_public' + i = do_something() + p = CommandProxy(i) + assert getattr(i, name)(1) == 4 + raised = False + try: + getattr(p, name) + except AttributeError: + raised = True + assert raised + + # Test that attributes are read-only: + name = 'validate' + i = do_something() + p = CommandProxy(i) + assert getattr(p, name)(1) == 3 + raised = False + try: + # Test __setattr__() + setattr(p, name, 'new_object') + except AttributeError: + raised = True + assert raised + raised = False + try: + # Test __delattr__() + delattr(p, name) + except AttributeError: + raised = True + assert raised + + + def test_Registrar(): -- cgit From 31fc955355ac8d873b82d129021f599f820c2694 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 1 Aug 2008 03:12:17 +0000 Subject: 34: Added tests.unit_common with frequently used utility functions; split ro __setattr__, __delattr__ methods out of Proxy and into new ReadOnly base class; added corresponding unit tests --- ipalib/plugable.py | 45 ++++++++++++++++++------------- ipalib/tests/test_plugable.py | 18 +++++++++++++ ipalib/tests/unit_common.py | 63 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+), 19 deletions(-) create mode 100644 ipalib/tests/unit_common.py (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index de5f3f8f..e74809cd 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -67,7 +67,32 @@ class Plugin(object): ) -class Proxy(object): +class ReadOnly(object): + """ + Base class for classes with read-only attributes. + """ + __slots__ = tuple() + + def __setattr__(self, name, value): + """ + This raises an AttributeError anytime an attempt is made to set an + attribute. + """ + raise AttributeError('read-only: cannot set %s.%s' % + (self.__class__.__name__, name) + ) + + def __delattr__(self, name): + """ + This raises an AttributeError anytime an attempt is made to delete an + attribute. + """ + raise AttributeError('read-only: cannot del %s.%s' % + (self.__class__.__name__, name) + ) + + +class Proxy(ReadOnly): """ Used to only export certain attributes into the dynamic API. @@ -92,24 +117,6 @@ class Proxy(object): for name in self.__slots__: object.__setattr__(self, name, getattr(obj, name)) - def __setattr__(self, name, value): - """ - Proxy instances are read-only. This raises an AttributeError - anytime an attempt is made to set an attribute. - """ - raise AttributeError('cannot set %s.%s' % - (self.__class__.__name__, name) - ) - - def __delattr__(self, name): - """ - Proxy instances are read-only. This raises an AttributeError - anytime an attempt is made to delete an attribute. - """ - raise AttributeError('cannot del %s.%s' % - (self.__class__.__name__, name) - ) - def __repr__(self): return '%s(%r)' % (self.__class__.__name__, self.__obj) diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index 023bf45f..e69060b3 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -21,6 +21,7 @@ Unit tests for `ipalib.plugable` module. """ +import unit_common as uc from ipalib import plugable, errors @@ -52,6 +53,23 @@ def test_Plugin(): assert repr(p) == '%s.some_plugin()' % __name__ +def test_ReadOnly(): + obj = plugable.ReadOnly() + names = ['not_an_attribute', 'an_attribute'] + for name in names: + uc.no_set(obj, name) + uc.no_del(obj, name) + + class some_ro_class(plugable.ReadOnly): + def __init__(self): + object.__setattr__(self, 'an_attribute', 'Hello world!') + obj = some_ro_class() + for name in names: + uc.no_set(obj, name) + uc.no_del(obj, name) + assert uc.read_only(obj, 'an_attribute') == 'Hello world!' + + def test_Proxy(): class CommandProxy(plugable.Proxy): __slots__ = ( diff --git a/ipalib/tests/unit_common.py b/ipalib/tests/unit_common.py new file mode 100644 index 00000000..f5c7f0f9 --- /dev/null +++ b/ipalib/tests/unit_common.py @@ -0,0 +1,63 @@ +# Authors: +# Jason Gerard DeRose +# +# 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 + +""" +Utility functions for unit tests. +""" + + +def no_set(obj, name): + """ + Tests that attribute cannot be set. + """ + raised = False + try: + setattr(obj, name, 'some_new_obj') + except AttributeError: + raised = True + assert raised + + +def no_del(obj, name): + """ + Tests that attribute cannot be deleted. + """ + raised = False + try: + delattr(obj, name) + except AttributeError: + raised = True + assert raised + + +def read_only(obj, name): + """ + Tests that attribute is read-only. Returns attribute. + """ + assert isinstance(obj, object) + assert hasattr(obj, name) + + # Test that it cannot be set: + no_set(obj, name) + + # Test that it cannot be deleted: + no_del(obj, name) + + # Return the attribute + return getattr(obj, name) -- cgit From 8a964d02b5a7c0c8a002f05597f0d1c1aabf569c Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 1 Aug 2008 03:15:50 +0000 Subject: 35: Renamed unit_common.py to tstutil.py --- ipalib/tests/test_plugable.py | 12 ++++----- ipalib/tests/tstutil.py | 63 +++++++++++++++++++++++++++++++++++++++++++ ipalib/tests/unit_common.py | 63 ------------------------------------------- 3 files changed, 69 insertions(+), 69 deletions(-) create mode 100644 ipalib/tests/tstutil.py delete mode 100644 ipalib/tests/unit_common.py (limited to 'ipalib') diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index e69060b3..8c0b4b42 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -21,7 +21,7 @@ Unit tests for `ipalib.plugable` module. """ -import unit_common as uc +import tstutil from ipalib import plugable, errors @@ -57,17 +57,17 @@ def test_ReadOnly(): obj = plugable.ReadOnly() names = ['not_an_attribute', 'an_attribute'] for name in names: - uc.no_set(obj, name) - uc.no_del(obj, name) + tstutil.no_set(obj, name) + tstutil.no_del(obj, name) class some_ro_class(plugable.ReadOnly): def __init__(self): object.__setattr__(self, 'an_attribute', 'Hello world!') obj = some_ro_class() for name in names: - uc.no_set(obj, name) - uc.no_del(obj, name) - assert uc.read_only(obj, 'an_attribute') == 'Hello world!' + tstutil.no_set(obj, name) + tstutil.no_del(obj, name) + assert tstutil.read_only(obj, 'an_attribute') == 'Hello world!' def test_Proxy(): diff --git a/ipalib/tests/tstutil.py b/ipalib/tests/tstutil.py new file mode 100644 index 00000000..1c93f138 --- /dev/null +++ b/ipalib/tests/tstutil.py @@ -0,0 +1,63 @@ +# Authors: +# Jason Gerard DeRose +# +# 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 + +""" +Utility functions for the unit tests. +""" + + +def no_set(obj, name): + """ + Tests that attribute cannot be set. + """ + raised = False + try: + setattr(obj, name, 'some_new_obj') + except AttributeError: + raised = True + assert raised + + +def no_del(obj, name): + """ + Tests that attribute cannot be deleted. + """ + raised = False + try: + delattr(obj, name) + except AttributeError: + raised = True + assert raised + + +def read_only(obj, name): + """ + Tests that attribute is read-only. Returns attribute. + """ + assert isinstance(obj, object) + assert hasattr(obj, name) + + # Test that it cannot be set: + no_set(obj, name) + + # Test that it cannot be deleted: + no_del(obj, name) + + # Return the attribute + return getattr(obj, name) diff --git a/ipalib/tests/unit_common.py b/ipalib/tests/unit_common.py deleted file mode 100644 index f5c7f0f9..00000000 --- a/ipalib/tests/unit_common.py +++ /dev/null @@ -1,63 +0,0 @@ -# Authors: -# Jason Gerard DeRose -# -# 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 - -""" -Utility functions for unit tests. -""" - - -def no_set(obj, name): - """ - Tests that attribute cannot be set. - """ - raised = False - try: - setattr(obj, name, 'some_new_obj') - except AttributeError: - raised = True - assert raised - - -def no_del(obj, name): - """ - Tests that attribute cannot be deleted. - """ - raised = False - try: - delattr(obj, name) - except AttributeError: - raised = True - assert raised - - -def read_only(obj, name): - """ - Tests that attribute is read-only. Returns attribute. - """ - assert isinstance(obj, object) - assert hasattr(obj, name) - - # Test that it cannot be set: - no_set(obj, name) - - # Test that it cannot be deleted: - no_del(obj, name) - - # Return the attribute - return getattr(obj, name) -- cgit From 4ac7ad99d6bee9b35015d5967feed24f25506d1a Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 1 Aug 2008 05:44:11 +0000 Subject: 36: Added more functionality to tests.tstutil; added corresponding tests.test_tstutil unit tests --- ipalib/tests/test_tstutil.py | 148 +++++++++++++++++++++++++++++++++++++++++++ ipalib/tests/tstutil.py | 46 +++++++++----- 2 files changed, 178 insertions(+), 16 deletions(-) create mode 100644 ipalib/tests/test_tstutil.py (limited to 'ipalib') diff --git a/ipalib/tests/test_tstutil.py b/ipalib/tests/test_tstutil.py new file mode 100644 index 00000000..73713d51 --- /dev/null +++ b/ipalib/tests/test_tstutil.py @@ -0,0 +1,148 @@ +# Authors: +# Jason Gerard DeRose +# +# 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 + +""" +Unit tests for test-helper `tests.tstutil` module. +""" + +import tstutil + + +class Prop(object): + def __init__(self, *ops): + self.__ops = frozenset(ops) + self.__prop = 'prop value' + + def __get_prop(self): + if 'get' not in self.__ops: + raise AttributeError('get prop') + return self.__prop + + def __set_prop(self, value): + if 'set' not in self.__ops: + raise AttributeError('set prop') + self.__prop = value + + def __del_prop(self): + if 'del' not in self.__ops: + raise AttributeError('del prop') + self.__prop = None + + prop = property(__get_prop, __set_prop, __del_prop) + + +def test_yes_raised(): + f = tstutil.yes_raises + + class SomeError(Exception): + pass + + class AnotherError(Exception): + pass + + def callback1(): + 'raises correct exception' + raise SomeError() + + def callback2(): + 'raises wrong exception' + raise AnotherError() + + def callback3(): + 'raises no exception' + + f(SomeError, callback1) + + raised = False + try: + f(SomeError, callback2) + except AnotherError: + raised = True + assert raised + + raised = False + try: + f(SomeError, callback3) + except tstutil.ExceptionNotRaised: + raised = True + assert raised + + +def test_no_set(): + # Tests that it works when prop cannot be set: + tstutil.no_set(Prop('get', 'del'), 'prop') + + # Tests that ExceptionNotRaised is raised when prop *can* be set: + raised = False + try: + tstutil.no_set(Prop('set'), 'prop') + except tstutil.ExceptionNotRaised: + raised = True + assert raised + + +def test_no_del(): + # Tests that it works when prop cannot be deleted: + tstutil.no_del(Prop('get', 'set'), 'prop') + + # Tests that ExceptionNotRaised is raised when prop *can* be set: + raised = False + try: + tstutil.no_del(Prop('del'), 'prop') + except tstutil.ExceptionNotRaised: + raised = True + assert raised + + +def test_read_only(): + # Test that it works when prop is read only: + assert tstutil.read_only(Prop('get'), 'prop') == 'prop value' + + # Test that ExceptionNotRaised is raised when prop can be set: + raised = False + try: + tstutil.read_only(Prop('get', 'set'), 'prop') + except tstutil.ExceptionNotRaised: + raised = True + assert raised + + # Test that ExceptionNotRaised is raised when prop can be deleted: + raised = False + try: + tstutil.read_only(Prop('get', 'del'), 'prop') + except tstutil.ExceptionNotRaised: + raised = True + assert raised + + # Test that ExceptionNotRaised is raised when prop can be both set and + # deleted: + raised = False + try: + tstutil.read_only(Prop('get', 'del'), 'prop') + except tstutil.ExceptionNotRaised: + raised = True + assert raised + + # Test that AttributeError is raised when prop can't be read: + raised = False + try: + tstutil.read_only(Prop(), 'prop') + except AttributeError: + raised = True + assert raised diff --git a/ipalib/tests/tstutil.py b/ipalib/tests/tstutil.py index 1c93f138..b9c6e15d 100644 --- a/ipalib/tests/tstutil.py +++ b/ipalib/tests/tstutil.py @@ -21,40 +21,54 @@ Utility functions for the unit tests. """ +class ExceptionNotRaised(Exception): + """ + Exception raised when an *expected* exception is *not* raised during a + unit test. + """ + msg = 'expected %s' + + def __init__(self, expected): + self.expected = expected + + def __str__(self): + return self.msg % self.expected.__name__ -def no_set(obj, name): + +def yes_raises(exception, callback, *args, **kw): """ - Tests that attribute cannot be set. + Tests that the expected exception is raised; raises ExceptionNotRaised + if test fails. """ raised = False try: - setattr(obj, name, 'some_new_obj') - except AttributeError: + callback(*args, **kw) + except exception: raised = True - assert raised + if not raised: + raise ExceptionNotRaised(exception) + + +def no_set(obj, name, value='some_new_obj'): + """ + Tests that attribute cannot be set. + """ + yes_raises(AttributeError, setattr, obj, name, value) def no_del(obj, name): """ Tests that attribute cannot be deleted. """ - raised = False - try: - delattr(obj, name) - except AttributeError: - raised = True - assert raised + yes_raises(AttributeError, delattr, obj, name) -def read_only(obj, name): +def read_only(obj, name, value='some_new_obj'): """ Tests that attribute is read-only. Returns attribute. """ - assert isinstance(obj, object) - assert hasattr(obj, name) - # Test that it cannot be set: - no_set(obj, name) + no_set(obj, name, value) # Test that it cannot be deleted: no_del(obj, name) -- cgit From 5eac2ea15fbef4fcd2f0a182e41bd4f6f5725d2a Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 1 Aug 2008 06:02:29 +0000 Subject: 37: Renamed tstutil.yes_raises() to raises(); changed test_plugable.py to use raises() throughout --- ipalib/tests/test_plugable.py | 60 +++++-------------------------------------- ipalib/tests/test_tstutil.py | 2 +- ipalib/tests/tstutil.py | 6 ++--- 3 files changed, 11 insertions(+), 57 deletions(-) (limited to 'ipalib') diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index 8c0b4b42..fc2e9a67 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -115,35 +115,14 @@ def test_Proxy(): i = do_something() p = CommandProxy(i) assert getattr(i, name)(1) == 4 - raised = False - try: - getattr(p, name) - except AttributeError: - raised = True - assert raised + tstutil.raises(AttributeError, getattr, p, name) # Test that attributes are read-only: name = 'validate' i = do_something() p = CommandProxy(i) assert getattr(p, name)(1) == 3 - raised = False - try: - # Test __setattr__() - setattr(p, name, 'new_object') - except AttributeError: - raised = True - assert raised - raised = False - try: - # Test __delattr__() - delattr(p, name) - except AttributeError: - raised = True - assert raised - - - + assert tstutil.read_only(p, name)(1) == 3 def test_Registrar(): @@ -166,21 +145,11 @@ def test_Registrar(): # Check that TypeError is raised trying to register something that isn't # a class: - raised = False - try: - r(plugin1()) - except TypeError: - raised = True - assert raised + tstutil.raises(TypeError, r, plugin1()) # Check that SubclassError is raised trying to register a class that is # not a subclass of an allowed base: - raised = False - try: - r(plugin3) - except errors.SubclassError: - raised = True - assert raised + tstutil.raises(errors.SubclassError, r, plugin3) # Check that registration works r(plugin1) @@ -193,12 +162,7 @@ def test_Registrar(): # Check that DuplicateError is raised trying to register exact class # again: - raised = False - try: - r(plugin1) - except errors.DuplicateError: - raised = True - assert raised + tstutil.raises(errors.DuplicateError, r, plugin1) # Check that OverrideError is raised trying to register class with same # name and same base: @@ -207,12 +171,7 @@ def test_Registrar(): pass class plugin1(base1_extended): pass - raised = False - try: - r(plugin1) - except errors.OverrideError: - raised = True - assert raised + tstutil.raises(errors.OverrideError, r, plugin1) # Check that overriding works r(plugin1, override=True) @@ -223,12 +182,7 @@ def test_Registrar(): # Check that MissingOverrideError is raised trying to override a name # not yet registerd: - raised = False - try: - r(plugin2, override=True) - except errors.MissingOverrideError: - raised = True - assert raised + tstutil.raises(errors.MissingOverrideError, r, plugin2, override=True) # Check that additional plugin can be registered: r(plugin2) diff --git a/ipalib/tests/test_tstutil.py b/ipalib/tests/test_tstutil.py index 73713d51..a4c72364 100644 --- a/ipalib/tests/test_tstutil.py +++ b/ipalib/tests/test_tstutil.py @@ -48,7 +48,7 @@ class Prop(object): def test_yes_raised(): - f = tstutil.yes_raises + f = tstutil.raises class SomeError(Exception): pass diff --git a/ipalib/tests/tstutil.py b/ipalib/tests/tstutil.py index b9c6e15d..37b7745f 100644 --- a/ipalib/tests/tstutil.py +++ b/ipalib/tests/tstutil.py @@ -35,7 +35,7 @@ class ExceptionNotRaised(Exception): return self.msg % self.expected.__name__ -def yes_raises(exception, callback, *args, **kw): +def raises(exception, callback, *args, **kw): """ Tests that the expected exception is raised; raises ExceptionNotRaised if test fails. @@ -53,14 +53,14 @@ def no_set(obj, name, value='some_new_obj'): """ Tests that attribute cannot be set. """ - yes_raises(AttributeError, setattr, obj, name, value) + raises(AttributeError, setattr, obj, name, value) def no_del(obj, name): """ Tests that attribute cannot be deleted. """ - yes_raises(AttributeError, delattr, obj, name) + raises(AttributeError, delattr, obj, name) def read_only(obj, name, value='some_new_obj'): -- cgit From 8881e4a543e9f1f1edda2d1cc935c020950214e6 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 1 Aug 2008 06:44:30 +0000 Subject: 38: dict interface of Registrar now works with both classes and strings as the key --- ipalib/plugable.py | 102 +++++++++++++++++++++++++++++++++++++++--- ipalib/tests/test_plugable.py | 37 +++++++++------ 2 files changed, 120 insertions(+), 19 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index e74809cd..9e94e96b 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -124,6 +124,83 @@ class Proxy(ReadOnly): return to_cli(self.name) +class NameSpace(ReadOnly): + """ + A read-only namespace of (key, value) pairs that can be accessed + both as instance attributes and as dictionary items. + """ + + def __init__(self, kw): + """ + The `kw` argument is a dict of the (key, value) pairs to be in this + NameSpace instance. The optional `order` keyword argument specifies + the order of the keys in this namespace; if omitted, the default is + to sort the keys in ascending order. + """ + assert isinstance(kw, dict) + self.__kw = dict(kw) + for (key, value) in self.__kw.items(): + assert not key.startswith('_') + setattr(self, key, value) + if order is None: + self.__keys = sorted(self.__kw) + else: + self.__keys = list(order) + assert set(self.__keys) == set(self.__kw) + self.__locked = True + + def __setattr__(self, name, value): + """ + Raises an exception if trying to set an attribute after the + NameSpace has been locked; otherwise calls object.__setattr__(). + """ + if self.__locked: + raise errors.SetError(name) + super(NameSpace, self).__setattr__(name, value) + + def __getitem__(self, key): + """ + Returns item from namespace named `key`. + """ + return self.__kw[key] + + def __hasitem__(self, key): + """ + Returns True if namespace has an item named `key`. + """ + return bool(key in self.__kw) + + def __iter__(self): + """ + Yields the names in this NameSpace in ascending order, or in the + the order specified in `order` kw arg. + + For example: + + >>> ns = NameSpace(dict(attr_b='world', attr_a='hello')) + >>> list(ns) + ['attr_a', 'attr_b'] + >>> [ns[k] for k in ns] + ['hello', 'world'] + """ + for key in self.__keys: + yield key + + def __call__(self): + """ + Iterates through the values in this NameSpace in the same order as + the keys. + """ + for key in self.__keys: + yield self.__kw[key] + + def __len__(self): + """ + Returns number of items in this NameSpace. + """ + return len(self.__keys) + + class Registrar(object): def __init__(self, *allowed): """ @@ -179,15 +256,30 @@ class Registrar(object): self.__registered.add(cls) sub_d[cls.__name__] = cls - def __getitem__(self, name): + def __getitem__(self, item): """ Returns a copy of the namespace dict of the base class named `name`. """ - return dict(self.__d[name]) + if inspect.isclass(item): + if item not in self.__allowed: + raise KeyError(repr(item)) + key = item.__name__ + else: + key = item + return dict(self.__d[key]) + + def __contains__(self, item): + """ + Returns True if a base class named `name` is in this Registrar. + """ + if inspect.isclass(item): + return item in self.__allowed + return item in self.__d def __iter__(self): """ - Iterates through the names of the allowed base classes. + Iterates through a (base, registered_plugins) tuple for each allowed + base. """ - for key in self.__d: - yield key + for base in self.__allowed: + yield (base, self.__d[base.__name__].values()) diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index fc2e9a67..a24bddfd 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -21,7 +21,7 @@ Unit tests for `ipalib.plugable` module. """ -import tstutil +from tstutil import raises, no_set, no_del, read_only from ipalib import plugable, errors @@ -57,20 +57,22 @@ def test_ReadOnly(): obj = plugable.ReadOnly() names = ['not_an_attribute', 'an_attribute'] for name in names: - tstutil.no_set(obj, name) - tstutil.no_del(obj, name) + no_set(obj, name) + no_del(obj, name) class some_ro_class(plugable.ReadOnly): def __init__(self): object.__setattr__(self, 'an_attribute', 'Hello world!') obj = some_ro_class() for name in names: - tstutil.no_set(obj, name) - tstutil.no_del(obj, name) - assert tstutil.read_only(obj, 'an_attribute') == 'Hello world!' + no_set(obj, name) + no_del(obj, name) + assert read_only(obj, 'an_attribute') == 'Hello world!' def test_Proxy(): + assert issubclass(plugable.Proxy, plugable.ReadOnly) + class CommandProxy(plugable.Proxy): __slots__ = ( 'validate', @@ -115,14 +117,14 @@ def test_Proxy(): i = do_something() p = CommandProxy(i) assert getattr(i, name)(1) == 4 - tstutil.raises(AttributeError, getattr, p, name) + raises(AttributeError, getattr, p, name) # Test that attributes are read-only: name = 'validate' i = do_something() p = CommandProxy(i) assert getattr(p, name)(1) == 3 - assert tstutil.read_only(p, name)(1) == 3 + assert read_only(p, name)(1) == 3 def test_Registrar(): @@ -141,15 +143,22 @@ def test_Registrar(): # Test creation of Registrar: r = plugable.Registrar(Base1, Base2) - assert sorted(r) == ['Base1', 'Base2'] + + # Test __hasitem__, __getitem__: + for base in [Base1, Base2]: + assert base in r + assert base.__name__ in r + assert r[base] == {} + assert r[base.__name__] == {} + # Check that TypeError is raised trying to register something that isn't # a class: - tstutil.raises(TypeError, r, plugin1()) + raises(TypeError, r, plugin1()) # Check that SubclassError is raised trying to register a class that is # not a subclass of an allowed base: - tstutil.raises(errors.SubclassError, r, plugin3) + raises(errors.SubclassError, r, plugin3) # Check that registration works r(plugin1) @@ -162,7 +171,7 @@ def test_Registrar(): # Check that DuplicateError is raised trying to register exact class # again: - tstutil.raises(errors.DuplicateError, r, plugin1) + raises(errors.DuplicateError, r, plugin1) # Check that OverrideError is raised trying to register class with same # name and same base: @@ -171,7 +180,7 @@ def test_Registrar(): pass class plugin1(base1_extended): pass - tstutil.raises(errors.OverrideError, r, plugin1) + raises(errors.OverrideError, r, plugin1) # Check that overriding works r(plugin1, override=True) @@ -182,7 +191,7 @@ def test_Registrar(): # Check that MissingOverrideError is raised trying to override a name # not yet registerd: - tstutil.raises(errors.MissingOverrideError, r, plugin2, override=True) + raises(errors.MissingOverrideError, r, plugin2, override=True) # Check that additional plugin can be registered: r(plugin2) -- cgit From 4fe8e52ecb61088bcff2a7c91db454621d6755f1 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 1 Aug 2008 07:00:08 +0000 Subject: 39: Added unit tests for Registrar.__iter__() --- ipalib/tests/test_plugable.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) (limited to 'ipalib') diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index a24bddfd..99c9a4f6 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -198,3 +198,41 @@ def test_Registrar(): sub_d = r['Base2'] assert len(sub_d) == 1 assert sub_d['plugin2'] is plugin2 + + + # Setup to test __iter__: + class plugin1a(Base1): + pass + r(plugin1a) + + class plugin1b(Base1): + pass + r(plugin1b) + + class plugin2a(Base2): + pass + r(plugin2a) + + class plugin2b(Base2): + pass + r(plugin2b) + + m = { + 'Base1': set([plugin1, plugin1a, plugin1b]), + 'Base2': set([plugin2, plugin2a, plugin2b]), + } + + # Now test __iter__: + for (base, plugins) in r: + assert base in [Base1, Base2] + assert set(plugins) == m[base.__name__] + assert len(list(r)) == 2 + + # Again test __hasitem__, __getitem__: + for base in [Base1, Base2]: + assert base in r + assert base.__name__ in r + d = dict((p.__name__, p) for p in m[base.__name__]) + assert len(d) == 3 + assert r[base] == d + assert r[base.__name__] == d -- cgit From f3762a76c0824296e90385eac27455aaf06af32d Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 1 Aug 2008 20:42:35 +0000 Subject: 40: Rewrote dictionary interface for plugable.NameSpace to better suite new architecture --- ipalib/plugable.py | 89 +++++++++++++++++-------------------------- ipalib/tests/test_plugable.py | 62 +++++++++++++++++++++++++++++- ipalib/tests/tstutil.py | 8 ++++ 3 files changed, 104 insertions(+), 55 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 9e94e96b..4923c621 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -130,75 +130,56 @@ class NameSpace(ReadOnly): both as instance attributes and as dictionary items. """ - def __init__(self, kw): - """ - The `kw` argument is a dict of the (key, value) pairs to be in this - NameSpace instance. The optional `order` keyword argument specifies - the order of the keys in this namespace; if omitted, the default is - to sort the keys in ascending order. - """ - assert isinstance(kw, dict) - self.__kw = dict(kw) - for (key, value) in self.__kw.items(): - assert not key.startswith('_') - setattr(self, key, value) - if order is None: - self.__keys = sorted(self.__kw) - else: - self.__keys = list(order) - assert set(self.__keys) == set(self.__kw) - self.__locked = True - - def __setattr__(self, name, value): + def __init__(self, items): """ - Raises an exception if trying to set an attribute after the - NameSpace has been locked; otherwise calls object.__setattr__(). """ - if self.__locked: - raise errors.SetError(name) - super(NameSpace, self).__setattr__(name, value) + object.__setattr__(self, '_NameSpace__items', tuple(items)) - def __getitem__(self, key): - """ - Returns item from namespace named `key`. - """ - return self.__kw[key] + # dict mapping Python name to item: + object.__setattr__(self, '_NameSpace__pname', {}) - def __hasitem__(self, key): - """ - Returns True if namespace has an item named `key`. - """ - return bool(key in self.__kw) + # dict mapping human-readibly name to item: + object.__setattr__(self, '_NameSpace__hname', {}) + + for item in self.__items: + for (key, d) in [ + (item.name, self.__pname), + (str(item), self.__hname), + ]: + assert key not in d + d[key] = item def __iter__(self): """ - Yields the names in this NameSpace in ascending order, or in the - the order specified in `order` kw arg. - - For example: + Iterates through the items in this NameSpace in the same order they + were passed in the contructor. + """ + for item in self.__items: + yield item - >>> ns = NameSpace(dict(attr_b='world', attr_a='hello')) - >>> list(ns) - ['attr_a', 'attr_b'] - >>> [ns[k] for k in ns] - ['hello', 'world'] + def __len__(self): + """ + Returns number of items in this NameSpace. """ - for key in self.__keys: - yield key + return len(self.__items) - def __call__(self): + def __contains__(self, key): """ - Iterates through the values in this NameSpace in the same order as - the keys. + Returns True if an item with pname or hname `key` is in this + NameSpace. """ - for key in self.__keys: - yield self.__kw[key] + return (key in self.__pname) or (key in self.__hname) - def __len__(self): + def __getitem__(self, key): """ - Returns number of items in this NameSpace. + Returns item with pname or hname `key`; otherwise raises KeyError. """ - return len(self.__keys) + if key in self.__pname: + return self.__pname[key] + if key in self.__hname: + return self.__hname[key] + raise KeyError('NameSpace has no item for key %r' % key) + class Registrar(object): diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index 99c9a4f6..4f92889c 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -21,7 +21,7 @@ Unit tests for `ipalib.plugable` module. """ -from tstutil import raises, no_set, no_del, read_only +from tstutil import raises, getitem, no_set, no_del, read_only from ipalib import plugable, errors @@ -236,3 +236,63 @@ def test_Registrar(): assert len(d) == 3 assert r[base] == d assert r[base.__name__] == d + + +def test_NameSpace(): + assert issubclass(plugable.NameSpace, plugable.ReadOnly) + + class DummyProxy(object): + def __init__(self, name): + self.__name = name + + def __get_name(self): + return self.__name + name = property(__get_name) + + def __str__(self): + return plugable.to_cli(self.__name) + + def get_name(i): + return 'noun_verb%d' % i + + def get_cli(i): + return 'noun-verb%d' % i + + def get_proxies(n): + for i in xrange(n): + yield DummyProxy(get_name(i)) + + cnt = 20 + ns = plugable.NameSpace(get_proxies(cnt)) + + # Test __len__ + assert len(ns) == cnt + + # Test __iter__ + i = None + for (i, item) in enumerate(ns): + assert type(item) is DummyProxy + assert item.name == get_name(i) + assert str(item) == get_cli(i) + assert i == cnt - 1 + + # Test __contains__, __getitem__: + for i in xrange(cnt): + name = get_name(i) + cli = get_cli(i) + assert name in ns + assert cli in ns + item = ns[name] + assert isinstance(item, DummyProxy) + assert item.name == name + assert str(item) == cli + assert ns[name] is item + assert ns[cli] is item + + # Check that KeyError is raised: + name = get_name(cnt) + cli = get_cli(cnt) + assert name not in ns + assert cli not in ns + raises(KeyError, getitem, ns, name) + raises(KeyError, getitem, ns, cli) diff --git a/ipalib/tests/tstutil.py b/ipalib/tests/tstutil.py index 37b7745f..12ca119d 100644 --- a/ipalib/tests/tstutil.py +++ b/ipalib/tests/tstutil.py @@ -49,6 +49,14 @@ def raises(exception, callback, *args, **kw): raise ExceptionNotRaised(exception) +def getitem(obj, key): + """ + Works like getattr but for dictionary interface. Uses this in combination + with raises() to test that, for example, KeyError is raised. + """ + return obj[key] + + def no_set(obj, name, value='some_new_obj'): """ Tests that attribute cannot be set. -- cgit From a0f480a414d2aa3a5f79e77026ff9183c1dd3a48 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 1 Aug 2008 20:58:48 +0000 Subject: 41: New plugable.NameSpace now has attributes set for each member; updated unit tests --- ipalib/plugable.py | 1 + ipalib/tests/test_plugable.py | 10 ++++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 4923c621..9025c1db 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -142,6 +142,7 @@ class NameSpace(ReadOnly): object.__setattr__(self, '_NameSpace__hname', {}) for item in self.__items: + object.__setattr__(self, item.name, item) for (key, d) in [ (item.name, self.__pname), (str(item), self.__hname), diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index 4f92889c..fc7fff98 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -276,7 +276,7 @@ def test_NameSpace(): assert str(item) == get_cli(i) assert i == cnt - 1 - # Test __contains__, __getitem__: + # Test __contains__, __getitem__, getattr(): for i in xrange(cnt): name = get_name(i) cli = get_cli(i) @@ -288,11 +288,17 @@ def test_NameSpace(): assert str(item) == cli assert ns[name] is item assert ns[cli] is item + assert read_only(ns, name) is item - # Check that KeyError is raised: + # Test dir(): + assert set(get_name(i) for i in xrange(cnt)).issubset(set(dir(ns))) + + # Test that KeyError, AttributeError is raised: name = get_name(cnt) cli = get_cli(cnt) assert name not in ns assert cli not in ns raises(KeyError, getitem, ns, name) raises(KeyError, getitem, ns, cli) + raises(AttributeError, getattr, ns, name) + no_set(ns, name) -- cgit From 74f5719078adfcfdf8b98bf97f0828dd150c840d Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 1 Aug 2008 21:25:46 +0000 Subject: 42: plugable.Plugin.__init__() now takes the plugable.API instance as its single argument --- ipalib/plugable.py | 19 ++++++++++++++++++- ipalib/tests/test_plugable.py | 15 +++++++++++---- 2 files changed, 29 insertions(+), 5 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 9025c1db..f298e97e 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -49,6 +49,16 @@ class Plugin(object): Base class for all plugins. """ + def __init__(self, api): + self.__api = api + + def __get_api(self): + """ + Returns the plugable.API object this plugin has been instatiated in. + """ + return self.__api + api = property(__get_api) + def __get_name(self): """ Returns the class name of this instance. @@ -132,6 +142,8 @@ class NameSpace(ReadOnly): def __init__(self, items): """ + `items` should be an iterable providing the members of this + NameSpace. """ object.__setattr__(self, '_NameSpace__items', tuple(items)) @@ -182,7 +194,6 @@ class NameSpace(ReadOnly): raise KeyError('NameSpace has no item for key %r' % key) - class Registrar(object): def __init__(self, *allowed): """ @@ -265,3 +276,9 @@ class Registrar(object): """ for base in self.__allowed: yield (base, self.__d[base.__name__].values()) + + +class API(ReadOnly): + def __init__(self, registrar): + for (base, plugins) in registrar: + pass diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index fc7fff98..a6f1e7cd 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -42,14 +42,17 @@ def test_from_cli(): def test_Plugin(): - p = plugable.Plugin() - assert p.name == 'Plugin' + api = 'the api instance' + p = plugable.Plugin(api) + assert read_only(p, 'api') is api + assert read_only(p, 'name') == 'Plugin' assert repr(p) == '%s.Plugin()' % plugable.__name__ class some_plugin(plugable.Plugin): pass - p = some_plugin() - assert p.name == 'some_plugin' + p = some_plugin(api) + assert read_only(p, 'api') is api + assert read_only(p, 'name') == 'some_plugin' assert repr(p) == '%s.some_plugin()' % __name__ @@ -302,3 +305,7 @@ def test_NameSpace(): raises(KeyError, getitem, ns, cli) raises(AttributeError, getattr, ns, name) no_set(ns, name) + + +def test_API(): + assert issubclass(plugable.API, plugable.ReadOnly) -- cgit From c3bf5ad8579e6f09aba558a68de947b2be398619 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 5 Aug 2008 03:21:52 +0000 Subject: 43: Fleshed out new plugable.API class; added corresponding unit tests --- ipalib/plugable.py | 11 ++++++- ipalib/tests/test_plugable.py | 67 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index f298e97e..32cbe033 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -281,4 +281,13 @@ class Registrar(object): class API(ReadOnly): def __init__(self, registrar): for (base, plugins) in registrar: - pass + ns = NameSpace(self.__plugin_iter(base, plugins)) + assert not hasattr(self, base.__name__) + object.__setattr__(self, base.__name__, ns) + + def __plugin_iter(self, base, plugins): + assert issubclass(base.proxy, Proxy) + for cls in plugins: + plugin = cls(self) + assert plugin.api is self + yield base.proxy(plugin) diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index a6f1e7cd..f5d469af 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -309,3 +309,70 @@ def test_NameSpace(): def test_API(): assert issubclass(plugable.API, plugable.ReadOnly) + + # Setup the test plugins, create the Registrar: + class ExampleProxy(plugable.Proxy): + __slots__ = ['method'] + + class base0(plugable.Plugin): + proxy = ExampleProxy + + def method(self, n): + return n + + class base1(plugable.Plugin): + proxy = ExampleProxy + + def method(self, n): + return n + 1 + + r = plugable.Registrar(base0, base1) + + class base0_plugin0(base0): + pass + r(base0_plugin0) + + class base0_plugin1(base0): + pass + r(base0_plugin1) + + class base0_plugin2(base0): + pass + r(base0_plugin2) + + class base1_plugin0(base1): + pass + r(base1_plugin0) + + class base1_plugin1(base1): + pass + r(base1_plugin1) + + class base1_plugin2(base1): + pass + r(base1_plugin2) + + registrants = tuple(r) + + # Test API instance: + api = plugable.API(r) + + def get_base(b): + return 'base%d' % b + + def get_plugin(b, p): + return 'base%d_plugin%d' % (b, p) + + for b in xrange(2): + base_name = get_base(b) + ns = getattr(api, base_name) + assert isinstance(ns, plugable.NameSpace) + assert read_only(api, base_name) is ns + assert len(ns) == 3 + for p in xrange(3): + plugin_name = get_plugin(b, p) + proxy = ns[plugin_name] + assert isinstance(proxy, ExampleProxy) + assert proxy.name == plugin_name + assert read_only(ns, plugin_name) is proxy + assert read_only(proxy, 'method')(7) == 7 + b -- cgit From 42c53b2a5345560e2583e3d7686b29cde812d52b Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 5 Aug 2008 04:24:19 +0000 Subject: 44: Added Plugin.finalize() method called by API after all plugin instances are created; updated corresponding unit tests --- ipalib/plugable.py | 25 ++++++++++++++++++++----- ipalib/tests/test_plugable.py | 16 ++++++++++++---- 2 files changed, 32 insertions(+), 9 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 32cbe033..cafb8c50 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -49,16 +49,27 @@ class Plugin(object): Base class for all plugins. """ - def __init__(self, api): - self.__api = api + __api = None def __get_api(self): """ - Returns the plugable.API object this plugin has been instatiated in. + Returns the plugable.API instance passed to Plugin.finalize(), or + or returns None if finalize() has not yet been called. """ return self.__api api = property(__get_api) + def finalize(self, api): + """ + After all the plugins are instantiated, the plugable.API calls this + method, passing itself as the only argument. This is where plugins + should check that other plugins they depend upon have actually be + loaded. + """ + assert self.__api is None, 'finalize() can only be called once' + assert api is not None, 'finalize() argument cannot be None' + self.__api = api + def __get_name(self): """ Returns the class name of this instance. @@ -280,14 +291,18 @@ class Registrar(object): class API(ReadOnly): def __init__(self, registrar): + object.__setattr__(self, '_API__plugins', []) for (base, plugins) in registrar: ns = NameSpace(self.__plugin_iter(base, plugins)) assert not hasattr(self, base.__name__) object.__setattr__(self, base.__name__, ns) + for plugin in self.__plugins: + plugin.finalize(self) + assert plugin.api is self def __plugin_iter(self, base, plugins): assert issubclass(base.proxy, Proxy) for cls in plugins: - plugin = cls(self) - assert plugin.api is self + plugin = cls() + self.__plugins.append(plugin) yield base.proxy(plugin) diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index f5d469af..27841e2e 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -43,17 +43,25 @@ def test_from_cli(): def test_Plugin(): api = 'the api instance' - p = plugable.Plugin(api) - assert read_only(p, 'api') is api + p = plugable.Plugin() assert read_only(p, 'name') == 'Plugin' assert repr(p) == '%s.Plugin()' % plugable.__name__ + assert read_only(p, 'api') is None + raises(AssertionError, p.finalize, None) + p.finalize(api) + assert read_only(p, 'api') is api + raises(AssertionError, p.finalize, api) class some_plugin(plugable.Plugin): pass - p = some_plugin(api) - assert read_only(p, 'api') is api + p = some_plugin() assert read_only(p, 'name') == 'some_plugin' assert repr(p) == '%s.some_plugin()' % __name__ + assert read_only(p, 'api') is None + raises(AssertionError, p.finalize, None) + p.finalize(api) + assert read_only(p, 'api') is api + raises(AssertionError, p.finalize, api) def test_ReadOnly(): -- cgit From 2b3c2238f6ecb5fc496acc50fc81f5b658d23c4b Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 5 Aug 2008 04:40:44 +0000 Subject: 45: Fixed docstring typo in plugable.__doc__ --- ipalib/plugable.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index cafb8c50..80349090 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -18,10 +18,9 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ -Utility classes for registering plugins, base classe for writing plugins. +Utility classes for registering plugins, base classes for writing plugins. """ - import inspect import errors -- cgit From d134b483066ae9d3a7e76d6e491f0f91eba6a954 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 5 Aug 2008 05:12:09 +0000 Subject: 46: plugable.API now takes allowed base class in __init__ and creates Registrar at API.register, thereby coupling the two; updated correspending unit tests --- ipalib/plugable.py | 10 ++++++++-- ipalib/tests/test_plugable.py | 9 +++++---- 2 files changed, 13 insertions(+), 6 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 80349090..70743f5a 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -289,9 +289,15 @@ class Registrar(object): class API(ReadOnly): - def __init__(self, registrar): + def __init__(self, *allowed): + object.__setattr__(self, 'register', Registrar(*allowed)) object.__setattr__(self, '_API__plugins', []) - for (base, plugins) in registrar: + + def __call__(self): + """ + Finalize the registration, instantiate the plugins. + """ + for (base, plugins) in self.register: ns = NameSpace(self.__plugin_iter(base, plugins)) assert not hasattr(self, base.__name__) object.__setattr__(self, base.__name__, ns) diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index 27841e2e..2e87ea2c 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -334,7 +334,10 @@ def test_API(): def method(self, n): return n + 1 - r = plugable.Registrar(base0, base1) + api = plugable.API(base0, base1) + r = api.register + assert isinstance(r, plugable.Registrar) + assert read_only(api, 'register') is r class base0_plugin0(base0): pass @@ -360,10 +363,8 @@ def test_API(): pass r(base1_plugin2) - registrants = tuple(r) - # Test API instance: - api = plugable.API(r) + api() # Calling instance performs finalization def get_base(b): return 'base%d' % b -- cgit From 56fa454fdd229524999127a5b89cc7c9077b9bd6 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 5 Aug 2008 06:33:09 +0000 Subject: 47: Added plugable.check_identifier() function; added corresponding unit tests --- ipalib/errors.py | 5 ++++- ipalib/plugable.py | 11 +++++++++++ ipalib/tests/test_plugable.py | 24 ++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/errors.py b/ipalib/errors.py index 53a0870e..e9f78477 100644 --- a/ipalib/errors.py +++ b/ipalib/errors.py @@ -56,13 +56,16 @@ class SetError(IPAError): - class RegistrationError(IPAError): """ Base class for errors that occur during plugin registration. """ +class NameSpaceError(RegistrationError): + msg = 'name %r does not re.match %r' + + class SubclassError(RegistrationError): """ Raised when registering a plugin that is not a subclass of one of the diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 70743f5a..8f2cbc27 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -21,6 +21,7 @@ Utility classes for registering plugins, base classes for writing plugins. """ +import re import inspect import errors @@ -43,6 +44,16 @@ def from_cli(cli_name): return cli_name.replace('-', '_').replace('.', '__') +def check_identifier(name): + """ + Raises errors.NameSpaceError if `name` is not a valid Python identifier + suitable for use in a NameSpace. + """ + regex = r'^[a-z][_a-z0-9]*[a-z0-9]$' + if re.match(regex, name) is None: + raise errors.NameSpaceError(name, regex) + + class Plugin(object): """ Base class for all plugins. diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index 2e87ea2c..1fa34bd5 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -41,6 +41,30 @@ def test_from_cli(): assert f('meta-service.do-something') == 'meta_service__do_something' +def test_valid_identifier(): + f = plugable.check_identifier + okay = [ + 'user_add', + 'stuff2junk', + 'sixty9', + ] + nope = [ + '_user_add', + '__user_add', + 'user_add_', + 'user_add__', + '_user_add_', + '__user_add__', + '60nine', + ] + for name in okay: + f(name) + for name in nope: + raises(errors.NameSpaceError, f, name) + for name in okay: + raises(errors.NameSpaceError, f, name.upper()) + + def test_Plugin(): api = 'the api instance' p = plugable.Plugin() -- cgit From 907107001bb0db662a743cb678c2adda3381d7b7 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 5 Aug 2008 07:39:50 +0000 Subject: 48: Added public.py with base classes for 'public' api used for XML-RPC, CLI, and UI; added corresponding unit stests --- ipalib/public.py | 81 +++++++++++++++++++++++++++++++++++++++++++++ ipalib/tests/test_public.py | 66 ++++++++++++++++++++++++++++++++++++ 2 files changed, 147 insertions(+) create mode 100644 ipalib/public.py create mode 100644 ipalib/tests/test_public.py (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py new file mode 100644 index 00000000..99d89000 --- /dev/null +++ b/ipalib/public.py @@ -0,0 +1,81 @@ +# Authors: +# Jason Gerard DeRose +# +# 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 + +""" +Base classes for the public plugable.API instance, which the XML-RPC, CLI, +and UI all use. +""" + +import re +import plugable + + +class cmd_proxy(plugable.Proxy): + __slots__ = ( + 'get_label', + 'get_summary', + 'get_help', + 'get_options', + ) + + +class cmd(plugable.Plugin): + proxy = cmd_proxy + + +class obj(plugable.Plugin): + pass + + +class attr(plugable.Plugin): + __obj = None + + def __init__(self): + m = re.match('^([a-z]+)_([a-z]+)$', self.__class__.__name__) + assert m + self.__obj_name = m.group(1) + self.__attr_name = m.group(2) + + def __get_obj_name(self): + return self.__obj_name + obj_name = property(__get_obj_name) + + def __get_attr_name(self): + return self.__attr_name + attr_name = property(__get_attr_name) + + def __get_obj(self): + """ + Returns the obj instance this attribute is associated with, or None + if no association has been set. + """ + return self.__obj + obj = property(__get_obj) + + def finalize(self, api): + super(attr, self).finalize(api) + self.__obj = api.obj[self.obj_name] + + +class mthd(attr, cmd): + pass + + +class prop(attr): + pass diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py new file mode 100644 index 00000000..5ea18430 --- /dev/null +++ b/ipalib/tests/test_public.py @@ -0,0 +1,66 @@ +# Authors: +# Jason Gerard DeRose +# +# 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 + +""" +Unit tests for `ipalib.public` module. +""" + +from tstutil import raises, getitem, no_set, no_del, read_only +from ipalib import public, plugable, errors + + +def test_cmd(): + cls = public.cmd + assert issubclass(cls, plugable.Plugin) + assert cls.proxy is public.cmd_proxy + + +def test_obj(): + cls = public.obj + assert issubclass(cls, plugable.Plugin) + + +def test_attr(): + cls = public.attr + assert issubclass(cls, plugable.Plugin) + + class api(object): + obj = dict(user='the user obj') + + class user_add(cls): + pass + + i = user_add() + assert read_only(i, 'obj_name') == 'user' + assert read_only(i, 'attr_name') == 'add' + assert read_only(i, 'obj') is None + i.finalize(api) + assert read_only(i, 'api') is api + assert read_only(i, 'obj') == 'the user obj' + + +def test_mthd(): + cls = public.mthd + assert issubclass(cls, public.attr) + assert issubclass(cls, public.cmd) + + +def test_prop(): + cls = public.prop + assert issubclass(cls, public.attr) -- cgit From f193dcba277e10b204f0e70a6cb0bcf3a661b8dc Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 5 Aug 2008 21:10:49 +0000 Subject: 49: Added public.PublicAPI class; added some basic unit tests for same --- ipalib/public.py | 26 +++++++++++++++++++------- ipalib/run.py | 4 ++-- ipalib/tests/test_public.py | 17 +++++++++++++++++ 3 files changed, 38 insertions(+), 9 deletions(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index 99d89000..b9f858fb 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -26,25 +26,32 @@ import re import plugable -class cmd_proxy(plugable.Proxy): +class generic_proxy(plugable.Proxy): __slots__ = ( 'get_label', - 'get_summary', - 'get_help', - 'get_options', + ) + + +class cmd_proxy(plugable.Proxy): + __slots__ = ( + '__call__', ) class cmd(plugable.Plugin): proxy = cmd_proxy + def __call__(self, *args, **kw): + print repr(self) + class obj(plugable.Plugin): - pass + proxy = generic_proxy class attr(plugable.Plugin): __obj = None + proxy = generic_proxy def __init__(self): m = re.match('^([a-z]+)_([a-z]+)$', self.__class__.__name__) @@ -74,8 +81,13 @@ class attr(plugable.Plugin): class mthd(attr, cmd): - pass + proxy = generic_proxy class prop(attr): - pass + proxy = generic_proxy + + +class PublicAPI(plugable.API): + def __init__(self): + super(PublicAPI, self).__init__(cmd, obj, prop) diff --git a/ipalib/run.py b/ipalib/run.py index ffaa655f..eaaaed9c 100644 --- a/ipalib/run.py +++ b/ipalib/run.py @@ -22,7 +22,7 @@ Standard run-time instances of importard classes. This is where plugins should access the registration API. """ -import base +import public # The standard API instance -api = base.API() +api = public.PublicAPI() diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index 5ea18430..bfe951ab 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -64,3 +64,20 @@ def test_mthd(): def test_prop(): cls = public.prop assert issubclass(cls, public.attr) + + +def test_PublicAPI(): + cls = public.PublicAPI + assert issubclass(cls, plugable.API) + + api = cls() + + class cmd1(public.cmd): + pass + api.register(cmd1) + + class cmd2(public.cmd): + pass + api.register(cmd2) + + api() -- cgit From 175dfc121a99ad60523a752e5c600ed809712789 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 5 Aug 2008 21:14:37 +0000 Subject: 50: Moved plugins.py to old_plugins.py --- ipalib/old_plugins.py | 136 ++++++++++++++++++++++++++++++++++++++++++++++++++ ipalib/plugins.py | 136 -------------------------------------------------- 2 files changed, 136 insertions(+), 136 deletions(-) create mode 100644 ipalib/old_plugins.py delete mode 100644 ipalib/plugins.py (limited to 'ipalib') diff --git a/ipalib/old_plugins.py b/ipalib/old_plugins.py new file mode 100644 index 00000000..85f3a9f4 --- /dev/null +++ b/ipalib/old_plugins.py @@ -0,0 +1,136 @@ +# Authors: +# Jason Gerard DeRose +# +# 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 + +""" +Some example plugins. +""" + +import crud +import base +from run import api + + +# Hypothetical functional commands (not associated with any object): +class krbtest(base.Command): + def get_doc(self, _): + return _('test your Kerberos ticket') +api.register(krbtest) + +class discover(base.Command): + def get_doc(self, _): + return _('discover IPA servers on network') +api.register(discover) + + +# Register some methods for the 'user' object: +class user__add(crud.Add): + def get_doc(self, _): + return _('add new user') +api.register(user__add) + +class user__del(crud.Del): + def get_doc(self, _): + return _('delete existing user') +api.register(user__del) + +class user__mod(crud.Mod): + def get_doc(self, _): + return _('edit existing user') +api.register(user__mod) + +class user__find(crud.Find): + def get_doc(self, _): + return _('search for users') +api.register(user__find) + + +# Register some properties for the 'user' object: +class user__firstname(base.Property): + pass +api.register(user__firstname) + +class user__lastname(base.Property): + pass +api.register(user__lastname) + +class user__login(base.Property): + pass +api.register(user__login) + + +# Register some methods for the 'group' object: +class group__add(crud.Add): + def get_doc(self, _): + return _('add new group') +api.register(group__add) + +class group__del(crud.Del): + def get_doc(self, _): + return _('delete existing group') +api.register(group__del) + +class group__mod(crud.Mod): + def get_doc(self, _): + return _('exit existing group') +api.register(group__mod) + +class group__find(crud.Find): + def get_doc(self, _): + return _('search for groups') +api.register(group__find) + + +# Register some methods for the 'service' object +class service__add(crud.Add): + def get_doc(self, _): + return _('add new service') +api.register(service__add) + +class service__del(crud.Del): + def get_doc(self, _): + return _('delete existing service') +api.register(service__del) + +class service__mod(crud.Mod): + def get_doc(self, _): + return _('edit existing service') +api.register(service__mod) + +class service__find(crud.Find): + def get_doc(self, _): + return _('search for services') +api.register(service__find) + + +# And to emphasis that the registration order doesn't matter, +# we'll register the objects last: +class group(base.Object): + def get_doc(self, _): + return _('') +api.register(group) + +class service(base.Object): + def get_doc(self, _): + return _('') +api.register(service) + +class user(base.Object): + def get_doc(self, _): + return _('') +api.register(user) diff --git a/ipalib/plugins.py b/ipalib/plugins.py deleted file mode 100644 index 85f3a9f4..00000000 --- a/ipalib/plugins.py +++ /dev/null @@ -1,136 +0,0 @@ -# Authors: -# Jason Gerard DeRose -# -# 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 - -""" -Some example plugins. -""" - -import crud -import base -from run import api - - -# Hypothetical functional commands (not associated with any object): -class krbtest(base.Command): - def get_doc(self, _): - return _('test your Kerberos ticket') -api.register(krbtest) - -class discover(base.Command): - def get_doc(self, _): - return _('discover IPA servers on network') -api.register(discover) - - -# Register some methods for the 'user' object: -class user__add(crud.Add): - def get_doc(self, _): - return _('add new user') -api.register(user__add) - -class user__del(crud.Del): - def get_doc(self, _): - return _('delete existing user') -api.register(user__del) - -class user__mod(crud.Mod): - def get_doc(self, _): - return _('edit existing user') -api.register(user__mod) - -class user__find(crud.Find): - def get_doc(self, _): - return _('search for users') -api.register(user__find) - - -# Register some properties for the 'user' object: -class user__firstname(base.Property): - pass -api.register(user__firstname) - -class user__lastname(base.Property): - pass -api.register(user__lastname) - -class user__login(base.Property): - pass -api.register(user__login) - - -# Register some methods for the 'group' object: -class group__add(crud.Add): - def get_doc(self, _): - return _('add new group') -api.register(group__add) - -class group__del(crud.Del): - def get_doc(self, _): - return _('delete existing group') -api.register(group__del) - -class group__mod(crud.Mod): - def get_doc(self, _): - return _('exit existing group') -api.register(group__mod) - -class group__find(crud.Find): - def get_doc(self, _): - return _('search for groups') -api.register(group__find) - - -# Register some methods for the 'service' object -class service__add(crud.Add): - def get_doc(self, _): - return _('add new service') -api.register(service__add) - -class service__del(crud.Del): - def get_doc(self, _): - return _('delete existing service') -api.register(service__del) - -class service__mod(crud.Mod): - def get_doc(self, _): - return _('edit existing service') -api.register(service__mod) - -class service__find(crud.Find): - def get_doc(self, _): - return _('search for services') -api.register(service__find) - - -# And to emphasis that the registration order doesn't matter, -# we'll register the objects last: -class group(base.Object): - def get_doc(self, _): - return _('') -api.register(group) - -class service(base.Object): - def get_doc(self, _): - return _('') -api.register(service) - -class user(base.Object): - def get_doc(self, _): - return _('') -api.register(user) -- cgit From 1fce1487f9ba85aeee36178c4efadbc983b612cc Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 5 Aug 2008 21:28:29 +0000 Subject: 51: Moved old_plugins.py back to plugins.py --- ipalib/old_plugins.py | 136 -------------------------------------------------- ipalib/plugins.py | 136 ++++++++++++++++++++++++++++++++++++++++++++++++++ ipalib/startup.py | 2 +- 3 files changed, 137 insertions(+), 137 deletions(-) delete mode 100644 ipalib/old_plugins.py create mode 100644 ipalib/plugins.py (limited to 'ipalib') diff --git a/ipalib/old_plugins.py b/ipalib/old_plugins.py deleted file mode 100644 index 85f3a9f4..00000000 --- a/ipalib/old_plugins.py +++ /dev/null @@ -1,136 +0,0 @@ -# Authors: -# Jason Gerard DeRose -# -# 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 - -""" -Some example plugins. -""" - -import crud -import base -from run import api - - -# Hypothetical functional commands (not associated with any object): -class krbtest(base.Command): - def get_doc(self, _): - return _('test your Kerberos ticket') -api.register(krbtest) - -class discover(base.Command): - def get_doc(self, _): - return _('discover IPA servers on network') -api.register(discover) - - -# Register some methods for the 'user' object: -class user__add(crud.Add): - def get_doc(self, _): - return _('add new user') -api.register(user__add) - -class user__del(crud.Del): - def get_doc(self, _): - return _('delete existing user') -api.register(user__del) - -class user__mod(crud.Mod): - def get_doc(self, _): - return _('edit existing user') -api.register(user__mod) - -class user__find(crud.Find): - def get_doc(self, _): - return _('search for users') -api.register(user__find) - - -# Register some properties for the 'user' object: -class user__firstname(base.Property): - pass -api.register(user__firstname) - -class user__lastname(base.Property): - pass -api.register(user__lastname) - -class user__login(base.Property): - pass -api.register(user__login) - - -# Register some methods for the 'group' object: -class group__add(crud.Add): - def get_doc(self, _): - return _('add new group') -api.register(group__add) - -class group__del(crud.Del): - def get_doc(self, _): - return _('delete existing group') -api.register(group__del) - -class group__mod(crud.Mod): - def get_doc(self, _): - return _('exit existing group') -api.register(group__mod) - -class group__find(crud.Find): - def get_doc(self, _): - return _('search for groups') -api.register(group__find) - - -# Register some methods for the 'service' object -class service__add(crud.Add): - def get_doc(self, _): - return _('add new service') -api.register(service__add) - -class service__del(crud.Del): - def get_doc(self, _): - return _('delete existing service') -api.register(service__del) - -class service__mod(crud.Mod): - def get_doc(self, _): - return _('edit existing service') -api.register(service__mod) - -class service__find(crud.Find): - def get_doc(self, _): - return _('search for services') -api.register(service__find) - - -# And to emphasis that the registration order doesn't matter, -# we'll register the objects last: -class group(base.Object): - def get_doc(self, _): - return _('') -api.register(group) - -class service(base.Object): - def get_doc(self, _): - return _('') -api.register(service) - -class user(base.Object): - def get_doc(self, _): - return _('') -api.register(user) diff --git a/ipalib/plugins.py b/ipalib/plugins.py new file mode 100644 index 00000000..85f3a9f4 --- /dev/null +++ b/ipalib/plugins.py @@ -0,0 +1,136 @@ +# Authors: +# Jason Gerard DeRose +# +# 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 + +""" +Some example plugins. +""" + +import crud +import base +from run import api + + +# Hypothetical functional commands (not associated with any object): +class krbtest(base.Command): + def get_doc(self, _): + return _('test your Kerberos ticket') +api.register(krbtest) + +class discover(base.Command): + def get_doc(self, _): + return _('discover IPA servers on network') +api.register(discover) + + +# Register some methods for the 'user' object: +class user__add(crud.Add): + def get_doc(self, _): + return _('add new user') +api.register(user__add) + +class user__del(crud.Del): + def get_doc(self, _): + return _('delete existing user') +api.register(user__del) + +class user__mod(crud.Mod): + def get_doc(self, _): + return _('edit existing user') +api.register(user__mod) + +class user__find(crud.Find): + def get_doc(self, _): + return _('search for users') +api.register(user__find) + + +# Register some properties for the 'user' object: +class user__firstname(base.Property): + pass +api.register(user__firstname) + +class user__lastname(base.Property): + pass +api.register(user__lastname) + +class user__login(base.Property): + pass +api.register(user__login) + + +# Register some methods for the 'group' object: +class group__add(crud.Add): + def get_doc(self, _): + return _('add new group') +api.register(group__add) + +class group__del(crud.Del): + def get_doc(self, _): + return _('delete existing group') +api.register(group__del) + +class group__mod(crud.Mod): + def get_doc(self, _): + return _('exit existing group') +api.register(group__mod) + +class group__find(crud.Find): + def get_doc(self, _): + return _('search for groups') +api.register(group__find) + + +# Register some methods for the 'service' object +class service__add(crud.Add): + def get_doc(self, _): + return _('add new service') +api.register(service__add) + +class service__del(crud.Del): + def get_doc(self, _): + return _('delete existing service') +api.register(service__del) + +class service__mod(crud.Mod): + def get_doc(self, _): + return _('edit existing service') +api.register(service__mod) + +class service__find(crud.Find): + def get_doc(self, _): + return _('search for services') +api.register(service__find) + + +# And to emphasis that the registration order doesn't matter, +# we'll register the objects last: +class group(base.Object): + def get_doc(self, _): + return _('') +api.register(group) + +class service(base.Object): + def get_doc(self, _): + return _('') +api.register(service) + +class user(base.Object): + def get_doc(self, _): + return _('') +api.register(user) diff --git a/ipalib/startup.py b/ipalib/startup.py index cfeb57b1..edc14405 100644 --- a/ipalib/startup.py +++ b/ipalib/startup.py @@ -28,4 +28,4 @@ unnecessary side effects (needed for unit tests, among other things). from run import api import plugins -api.finalize() +api() -- cgit From 159207514fadfacb6e1df9713abd2c61c24d7b77 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 5 Aug 2008 22:21:57 +0000 Subject: 52: Got cli working against new framework --- ipalib/plugable.py | 5 +-- ipalib/plugins.py | 75 +++++++++++++++++++++---------------------- ipalib/public.py | 20 +++++++++++- ipalib/tests/test_plugable.py | 4 +-- 4 files changed, 61 insertions(+), 43 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 8f2cbc27..2c5fd18c 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -92,7 +92,7 @@ class Plugin(object): Returns a valid Python expression that could create this plugin instance given the appropriate environment. """ - return '%s.%s()' % ( + return '%s.%s' % ( self.__class__.__module__, self.__class__.__name__ ) @@ -296,7 +296,8 @@ class Registrar(object): base. """ for base in self.__allowed: - yield (base, self.__d[base.__name__].values()) + sub_d = self.__d[base.__name__] + yield (base, tuple(sub_d[k] for k in sorted(sub_d))) class API(ReadOnly): diff --git a/ipalib/plugins.py b/ipalib/plugins.py index 85f3a9f4..7495acc2 100644 --- a/ipalib/plugins.py +++ b/ipalib/plugins.py @@ -21,116 +21,115 @@ Some example plugins. """ -import crud -import base +import public from run import api # Hypothetical functional commands (not associated with any object): -class krbtest(base.Command): +class krbtest(public.cmd): def get_doc(self, _): return _('test your Kerberos ticket') api.register(krbtest) -class discover(base.Command): +class discover(public.cmd): def get_doc(self, _): return _('discover IPA servers on network') api.register(discover) # Register some methods for the 'user' object: -class user__add(crud.Add): +class user_add(public.mthd): def get_doc(self, _): return _('add new user') -api.register(user__add) +api.register(user_add) -class user__del(crud.Del): +class user_del(public.mthd): def get_doc(self, _): return _('delete existing user') -api.register(user__del) +api.register(user_del) -class user__mod(crud.Mod): +class user_mod(public.mthd): def get_doc(self, _): return _('edit existing user') -api.register(user__mod) +api.register(user_mod) -class user__find(crud.Find): +class user_find(public.mthd): def get_doc(self, _): return _('search for users') -api.register(user__find) +api.register(user_find) # Register some properties for the 'user' object: -class user__firstname(base.Property): +class user_firstname(public.prop): pass -api.register(user__firstname) +api.register(user_firstname) -class user__lastname(base.Property): +class user_lastname(public.prop): pass -api.register(user__lastname) +api.register(user_lastname) -class user__login(base.Property): +class user_login(public.prop): pass -api.register(user__login) +api.register(user_login) # Register some methods for the 'group' object: -class group__add(crud.Add): +class group_add(public.mthd): def get_doc(self, _): return _('add new group') -api.register(group__add) +api.register(group_add) -class group__del(crud.Del): +class group_del(public.mthd): def get_doc(self, _): return _('delete existing group') -api.register(group__del) +api.register(group_del) -class group__mod(crud.Mod): +class group_mod(public.mthd): def get_doc(self, _): - return _('exit existing group') -api.register(group__mod) + return _('edit existing group') +api.register(group_mod) -class group__find(crud.Find): +class group_find(public.mthd): def get_doc(self, _): return _('search for groups') -api.register(group__find) +api.register(group_find) # Register some methods for the 'service' object -class service__add(crud.Add): +class service_add(public.mthd): def get_doc(self, _): return _('add new service') -api.register(service__add) +api.register(service_add) -class service__del(crud.Del): +class service_del(public.mthd): def get_doc(self, _): return _('delete existing service') -api.register(service__del) +api.register(service_del) -class service__mod(crud.Mod): +class service_mod(public.mthd): def get_doc(self, _): return _('edit existing service') -api.register(service__mod) +api.register(service_mod) -class service__find(crud.Find): +class service_find(public.mthd): def get_doc(self, _): return _('search for services') -api.register(service__find) +api.register(service_find) # And to emphasis that the registration order doesn't matter, # we'll register the objects last: -class group(base.Object): +class group(public.obj): def get_doc(self, _): return _('') api.register(group) -class service(base.Object): +class service(public.obj): def get_doc(self, _): return _('') api.register(service) -class user(base.Object): +class user(public.obj): def get_doc(self, _): return _('') api.register(user) diff --git a/ipalib/public.py b/ipalib/public.py index b9f858fb..3bcf697f 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -28,19 +28,23 @@ import plugable class generic_proxy(plugable.Proxy): __slots__ = ( - 'get_label', + 'get_doc', ) class cmd_proxy(plugable.Proxy): __slots__ = ( '__call__', + 'get_doc', ) class cmd(plugable.Plugin): proxy = cmd_proxy + def get_doc(self, _): + raise NotImplementedError('%s.get_doc()' % self.name) + def __call__(self, *args, **kw): print repr(self) @@ -87,7 +91,21 @@ class mthd(attr, cmd): class prop(attr): proxy = generic_proxy + def get_doc(self, _): + return _('prop doc') + class PublicAPI(plugable.API): + __max_cmd_len = None + def __init__(self): super(PublicAPI, self).__init__(cmd, obj, prop) + + def __get_max_cmd_len(self): + if self.__max_cmd_len is None: + if not hasattr(self, 'cmd'): + return None + max_cmd_len = max(len(str(cmd)) for cmd in self.cmd) + object.__setattr__(self, '_PublicAPI__max_cmd_len', max_cmd_len) + return self.__max_cmd_len + max_cmd_len = property(__get_max_cmd_len) diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index 1fa34bd5..a9a6492d 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -69,7 +69,7 @@ def test_Plugin(): api = 'the api instance' p = plugable.Plugin() assert read_only(p, 'name') == 'Plugin' - assert repr(p) == '%s.Plugin()' % plugable.__name__ + assert repr(p) == '%s.Plugin' % plugable.__name__ assert read_only(p, 'api') is None raises(AssertionError, p.finalize, None) p.finalize(api) @@ -80,7 +80,7 @@ def test_Plugin(): pass p = some_plugin() assert read_only(p, 'name') == 'some_plugin' - assert repr(p) == '%s.some_plugin()' % __name__ + assert repr(p) == '%s.some_plugin' % __name__ assert read_only(p, 'api') is None raises(AssertionError, p.finalize, None) p.finalize(api) -- cgit From f31f7813febf0665a072d474166ea883bc7365dc Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 5 Aug 2008 23:34:59 +0000 Subject: 53: Changed plugable.Registar so the same plugin can be added to in the ns for more than one base (for cmd and mthd) --- ipalib/plugable.py | 39 ++++++++++++++++++++++----------------- ipalib/public.py | 2 +- 2 files changed, 23 insertions(+), 18 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 2c5fd18c..a8996cf2 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -236,10 +236,13 @@ class Registrar(object): base; otherwise raises SubclassError. """ assert inspect.isclass(cls) + found = False for base in self.__allowed: if issubclass(cls, base): - return base - raise errors.SubclassError(cls, self.__allowed) + found = True + yield base + if not found: + raise errors.SubclassError(cls, self.__allowed) def __call__(self, cls, override=False): """ @@ -248,27 +251,29 @@ class Registrar(object): if not inspect.isclass(cls): raise TypeError('plugin must be a class: %r' % cls) - # Find the base class or raise SubclassError: - base = self.__findbase(cls) - sub_d = self.__d[base.__name__] - # Raise DuplicateError if this exact class was already registered: if cls in self.__registered: raise errors.DuplicateError(cls) - # Check override: - if cls.__name__ in sub_d: - # Must use override=True to override: - if not override: - raise errors.OverrideError(base, cls) - else: - # There was nothing already registered to override: - if override: - raise errors.MissingOverrideError(base, cls) + # Find the base class or raise SubclassError: + for base in self.__findbase(cls): + sub_d = self.__d[base.__name__] + + # Check override: + if cls.__name__ in sub_d: + # Must use override=True to override: + if not override: + raise errors.OverrideError(base, cls) + else: + # There was nothing already registered to override: + if override: + raise errors.MissingOverrideError(base, cls) + + # The plugin is okay, add to sub_d: + sub_d[cls.__name__] = cls - # The plugin is okay, add to __registered and sub_d: + # The plugin is okay, add to __registered: self.__registered.add(cls) - sub_d[cls.__name__] = cls def __getitem__(self, item): """ diff --git a/ipalib/public.py b/ipalib/public.py index 3bcf697f..e5e579f1 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -99,7 +99,7 @@ class PublicAPI(plugable.API): __max_cmd_len = None def __init__(self): - super(PublicAPI, self).__init__(cmd, obj, prop) + super(PublicAPI, self).__init__(cmd, obj, mthd, prop) def __get_max_cmd_len(self): if self.__max_cmd_len is None: -- cgit From c6f69e1c66b86f8f375a3c561922a42fdc0b1afb Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 6 Aug 2008 02:00:18 +0000 Subject: 54: Added plugable.Proxy._clone() method; fleshed out public.obj; updated unit tests; port ipa script --- ipalib/plugable.py | 26 ++++++++++++++++++++-- ipalib/public.py | 52 ++++++++++++++++++++++++++++++++++++++++--- ipalib/tests/test_plugable.py | 7 ++++++ 3 files changed, 80 insertions(+), 5 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index a8996cf2..6e6c6973 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -145,8 +145,8 @@ class Proxy(ReadOnly): assert isinstance(proxy_name, str) object.__setattr__(self, '_Proxy__obj', obj) object.__setattr__(self, 'name', proxy_name) - for name in self.__slots__: - object.__setattr__(self, name, getattr(obj, name)) + #for name in self.__slots__: + # object.__setattr__(self, name, getattr(obj, name)) def __repr__(self): return '%s(%r)' % (self.__class__.__name__, self.__obj) @@ -154,6 +154,18 @@ class Proxy(ReadOnly): def __str__(self): return to_cli(self.name) + def _clone(self, new_name): + return self.__class__(self.__obj, proxy_name=new_name) + + def __getattr__(self, name): + if name in self.__slots__: + return getattr(self.__obj, name) + raise AttributeError('attribute %r not in %s.__slots__' % ( + name, + self.__class__.__name__ + ) + ) + class NameSpace(ReadOnly): """ @@ -161,6 +173,8 @@ class NameSpace(ReadOnly): both as instance attributes and as dictionary items. """ + __max_len = None + def __init__(self, items): """ `items` should be an iterable providing the members of this @@ -214,6 +228,14 @@ class NameSpace(ReadOnly): return self.__hname[key] raise KeyError('NameSpace has no item for key %r' % key) + def __call__(self): + if self.__max_len is None: + ml = max(len(k) for k in self.__pname) + object.__setattr__(self, '_NameSpace__max_len', ml) + return self.__max_len + + + class Registrar(object): def __init__(self, *allowed): diff --git a/ipalib/public.py b/ipalib/public.py index e5e579f1..941011b6 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -49,10 +49,45 @@ class cmd(plugable.Plugin): print repr(self) +class obj_proxy(plugable.Proxy): + __slots__ = ( + 'mthd', + 'prop', + ) + + class obj(plugable.Plugin): - proxy = generic_proxy + proxy = obj_proxy + __mthd = None + __prop = None + + def __get_mthd(self): + return self.__mthd + mthd = property(__get_mthd) + + def __get_prop(self): + return self.__prop + prop = property(__get_prop) + + def finalize(self, api): + super(obj, self).finalize(api) + self.__mthd = self.__create_ns('mthd') + self.__prop = self.__create_ns('prop') + + def __create_ns(self, name): + return plugable.NameSpace(self.__filter(name)) + + def __filter(self, name): + for i in getattr(self.api, name): + if i.obj_name == self.name: + yield i._clone(i.attr_name) +ATTR_SLOTS = ( + 'obj_name', + 'attr_name', +) + class attr(plugable.Plugin): __obj = None proxy = generic_proxy @@ -84,12 +119,23 @@ class attr(plugable.Plugin): self.__obj = api.obj[self.obj_name] +class mthd_proxy(plugable.Proxy): + __slots__ = ( + '__call__', + 'get_doc', + ) + ATTR_SLOTS + class mthd(attr, cmd): - proxy = generic_proxy + proxy = mthd_proxy +class prop_proxy(plugable.Proxy): + __slots__ = ( + 'get_doc', + ) + ATTR_SLOTS + class prop(attr): - proxy = generic_proxy + proxy = prop_proxy def get_doc(self, _): return _('prop doc') diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index a9a6492d..6949fdeb 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -161,6 +161,13 @@ def test_Proxy(): assert getattr(p, name)(1) == 3 assert read_only(p, name)(1) == 3 + # Test cloning: + i = do_something() + p = CommandProxy(i) + c = p._clone('do_a_thing') + assert isinstance(c, CommandProxy) + assert c.name == 'do_a_thing' + def test_Registrar(): class Base1(object): -- cgit From 277685439c91f496df9510e02418da01160df0ea Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 6 Aug 2008 03:27:00 +0000 Subject: 55: Cleaned up print_api() function in ipa script --- ipalib/plugable.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 6e6c6973..e017a8a4 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -175,12 +175,13 @@ class NameSpace(ReadOnly): __max_len = None - def __init__(self, items): + def __init__(self, items, base=None): """ `items` should be an iterable providing the members of this NameSpace. """ object.__setattr__(self, '_NameSpace__items', tuple(items)) + object.__setattr__(self, '_NameSpace__base', base) # dict mapping Python name to item: object.__setattr__(self, '_NameSpace__pname', {}) @@ -234,6 +235,14 @@ class NameSpace(ReadOnly): object.__setattr__(self, '_NameSpace__max_len', ml) return self.__max_len + def __repr__(self): + if self.__base is None: + base = repr(self.__base) + else: + base = '%s.%s' % (self.__base.__module__, self.__base.__name__) + return '%s(*proxies, base=%s)' % (self.__class__.__name__, base) + + @@ -329,6 +338,8 @@ class Registrar(object): class API(ReadOnly): def __init__(self, *allowed): + keys = tuple(b.__name__ for b in allowed) + object.__setattr__(self, '_API__keys', keys) object.__setattr__(self, 'register', Registrar(*allowed)) object.__setattr__(self, '_API__plugins', []) @@ -337,7 +348,7 @@ class API(ReadOnly): Finalize the registration, instantiate the plugins. """ for (base, plugins) in self.register: - ns = NameSpace(self.__plugin_iter(base, plugins)) + ns = NameSpace(self.__plugin_iter(base, plugins), base=base) assert not hasattr(self, base.__name__) object.__setattr__(self, base.__name__, ns) for plugin in self.__plugins: @@ -350,3 +361,7 @@ class API(ReadOnly): plugin = cls() self.__plugins.append(plugin) yield base.proxy(plugin) + + def __iter__(self): + for key in self.__keys: + yield key -- cgit From 8865f516dfbffaee0da679c47aa2709ec8f5d80f Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 6 Aug 2008 03:51:33 +0000 Subject: 56: Fixed Proxy.__call__ --- ipalib/plugable.py | 3 +++ 1 file changed, 3 insertions(+) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index e017a8a4..801ef7ab 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -132,6 +132,7 @@ class Proxy(ReadOnly): """ __slots__ = ( + '__call__', '__obj', 'name', ) @@ -145,6 +146,8 @@ class Proxy(ReadOnly): assert isinstance(proxy_name, str) object.__setattr__(self, '_Proxy__obj', obj) object.__setattr__(self, 'name', proxy_name) + if callable(obj): + object.__setattr__(self, '__call__', obj.__call__) #for name in self.__slots__: # object.__setattr__(self, name, getattr(obj, name)) -- cgit From e618d99bc7adb47b724aebf67ea85e59c520e10d Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 6 Aug 2008 03:58:15 +0000 Subject: 57: to_cli() function no longer replaces '__' with '.'; from_cli() function no longer replaces '.' with '__'; updated unit tests --- ipalib/plugable.py | 6 +++--- ipalib/tests/test_plugable.py | 14 +++++--------- 2 files changed, 8 insertions(+), 12 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 801ef7ab..01adc613 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -32,7 +32,7 @@ def to_cli(name): Command Line Interface. """ assert isinstance(name, str) - return name.replace('__', '.').replace('_', '-') + return name.replace('_', '-') def from_cli(cli_name): @@ -41,7 +41,7 @@ def from_cli(cli_name): Python identifier. """ assert isinstance(cli_name, basestring) - return cli_name.replace('-', '_').replace('.', '__') + return cli_name.replace('-', '_') def check_identifier(name): @@ -143,7 +143,7 @@ class Proxy(ReadOnly): """ if proxy_name is None: proxy_name = obj.__class__.__name__ - assert isinstance(proxy_name, str) + check_identifier(proxy_name) object.__setattr__(self, '_Proxy__obj', obj) object.__setattr__(self, 'name', proxy_name) if callable(obj): diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index 6949fdeb..6ca4050b 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -28,17 +28,13 @@ from ipalib import plugable, errors def test_to_cli(): f = plugable.to_cli assert f('initialize') == 'initialize' - assert f('find_everything') == 'find-everything' - assert f('user__add') == 'user.add' - assert f('meta_service__do_something') == 'meta-service.do-something' + assert f('user_add') == 'user-add' def test_from_cli(): f = plugable.from_cli assert f('initialize') == 'initialize' - assert f('find-everything') == 'find_everything' - assert f('user.add') == 'user__add' - assert f('meta-service.do-something') == 'meta_service__do_something' + assert f('user-add') == 'user_add' def test_valid_identifier(): @@ -139,10 +135,10 @@ def test_Proxy(): # Test that proxy_name can be overriden: i = do_something() - p = CommandProxy(i, proxy_name='user__add') + p = CommandProxy(i, proxy_name='user_add') assert '__dict__' not in dir(p) - assert p.name == 'user__add' - assert str(p) == 'user.add' + assert p.name == 'user_add' + assert str(p) == 'user-add' assert repr(p) == 'CommandProxy()' assert p(1) == 2 assert p.validate(1) == 3 -- cgit From 2081987186a533bd6c953c8d48dfcfd193802e44 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 6 Aug 2008 14:22:38 +0000 Subject: 58: A bit of docstring cleanup in plugable.py --- ipalib/plugable.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 01adc613..7602bce3 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -18,7 +18,7 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ -Utility classes for registering plugins, base classes for writing plugins. +Base classes for plug-in architecture and generative API. """ import re @@ -89,8 +89,7 @@ class Plugin(object): def __repr__(self): """ - Returns a valid Python expression that could create this plugin - instance given the appropriate environment. + Returns a fully qualified representation of the class. """ return '%s.%s' % ( self.__class__.__module__, @@ -125,7 +124,7 @@ class ReadOnly(object): class Proxy(ReadOnly): """ - Used to only export certain attributes into the dynamic API. + Used to only export certain attributes into the generative API. Subclasses must list names of attributes to be proxied in the __slots__ class attribute. -- cgit From 62d2cd65f22f748ec8db3d56d1baa9a533d4f11d Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 6 Aug 2008 14:27:33 +0000 Subject: 59: Removed NameSpace.__call__ method (returned max_len) --- ipalib/plugable.py | 11 ----------- 1 file changed, 11 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 7602bce3..fe4a4531 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -175,8 +175,6 @@ class NameSpace(ReadOnly): both as instance attributes and as dictionary items. """ - __max_len = None - def __init__(self, items, base=None): """ `items` should be an iterable providing the members of this @@ -231,12 +229,6 @@ class NameSpace(ReadOnly): return self.__hname[key] raise KeyError('NameSpace has no item for key %r' % key) - def __call__(self): - if self.__max_len is None: - ml = max(len(k) for k in self.__pname) - object.__setattr__(self, '_NameSpace__max_len', ml) - return self.__max_len - def __repr__(self): if self.__base is None: base = repr(self.__base) @@ -245,9 +237,6 @@ class NameSpace(ReadOnly): return '%s(*proxies, base=%s)' % (self.__class__.__name__, base) - - - class Registrar(object): def __init__(self, *allowed): """ -- cgit From 293b31ac75cd4f72c5d4a62ffc82df83c70f564f Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 6 Aug 2008 14:30:21 +0000 Subject: 60: Remeved depreciated base.py, crud.py; remeved corresponding test_base.py, test_crud.py --- ipalib/base.py | 499 ---------------------------------------------- ipalib/crud.py | 37 ---- ipalib/tests/test_base.py | 424 --------------------------------------- ipalib/tests/test_crud.py | 22 -- 4 files changed, 982 deletions(-) delete mode 100644 ipalib/base.py delete mode 100644 ipalib/crud.py delete mode 100644 ipalib/tests/test_base.py delete mode 100644 ipalib/tests/test_crud.py (limited to 'ipalib') diff --git a/ipalib/base.py b/ipalib/base.py deleted file mode 100644 index ae9dfae4..00000000 --- a/ipalib/base.py +++ /dev/null @@ -1,499 +0,0 @@ -# Authors: -# Jason Gerard DeRose -# -# 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 - -""" -Base classes for plug-in architecture and generative API. -""" - -import re -import inspect -import errors - - -class NameSpace(object): - """ - A read-only namespace of (key, value) pairs that can be accessed - both as instance attributes and as dictionary items. For example: - - >>> ns = NameSpace(dict(my_message='Hello world!')) - >>> ns.my_message - 'Hello world!' - >>> ns['my_message'] - 'Hello world!' - - Keep in mind that Python doesn't offer true ready-only attributes. A - NameSpace is read-only in that it prevents programmers from - *accidentally* setting its attributes, but a motivated programmer can - still set them. - - For example, setting an attribute the normal way will raise an exception: - - >>> ns.my_message = 'some new value' - (raises errors.SetError) - - But a programmer could still set the attribute like this: - - >>> ns.__dict__['my_message'] = 'some new value' - - You should especially not implement a security feature that relies upon - NameSpace being strictly read-only. - """ - - __locked = False # Whether __setattr__ has been locked - - def __init__(self, kw, order=None): - """ - The `kw` argument is a dict of the (key, value) pairs to be in this - NameSpace instance. The optional `order` keyword argument specifies - the order of the keys in this namespace; if omitted, the default is - to sort the keys in ascending order. - """ - assert isinstance(kw, dict) - self.__kw = dict(kw) - for (key, value) in self.__kw.items(): - assert not key.startswith('_') - setattr(self, key, value) - if order is None: - self.__keys = sorted(self.__kw) - else: - self.__keys = list(order) - assert set(self.__keys) == set(self.__kw) - self.__locked = True - - def __setattr__(self, name, value): - """ - Raises an exception if trying to set an attribute after the - NameSpace has been locked; otherwise calls object.__setattr__(). - """ - if self.__locked: - raise errors.SetError(name) - super(NameSpace, self).__setattr__(name, value) - - def __getitem__(self, key): - """ - Returns item from namespace named `key`. - """ - return self.__kw[key] - - def __hasitem__(self, key): - """ - Returns True if namespace has an item named `key`. - """ - return bool(key in self.__kw) - - def __iter__(self): - """ - Yields the names in this NameSpace in ascending order, or in the - the order specified in `order` kw arg. - - For example: - - >>> ns = NameSpace(dict(attr_b='world', attr_a='hello')) - >>> list(ns) - ['attr_a', 'attr_b'] - >>> [ns[k] for k in ns] - ['hello', 'world'] - """ - for key in self.__keys: - yield key - - def __call__(self): - """ - Iterates through the values in this NameSpace in the same order as - the keys. - """ - for key in self.__keys: - yield self.__kw[key] - - def __len__(self): - """ - Returns number of items in this NameSpace. - """ - return len(self.__keys) - - - -class Named(object): - __name = None - - def _get_name(self): - return self.__class__.__name__ - - def __get_loc(self): - cls = self.__class__ - return '%s.%s' % (cls.__module__, cls.__name__) - loc = property(__get_loc) - - def __get_name(self): - if self.__name is None: - self.__name = self._get_name() - return self.__name - name = property(__get_name) - - def __get_cli_name(self): - return self.name.replace('_', '-') - cli_name = property(__get_cli_name) - - -class AbstractCommand(object): - def __call__(self): - print 'You called %s.%s()' % ( - self.__class__.__module__, - self.__class__.__name__ - ) - - def get_doc(self, _): - """ - This should return a gettext translated summarary of the command. - - For example, if you were documenting the 'add-user' command, you're - method would look something like this. - - >>> def get_doc(self, _): - >>> return _('add new user') - """ - raise NotImplementedError('%s.%s.%s()' % ( - self.__class__.__module__, - self.__class__.__name__, - 'get_doc', - ) - ) - - -class Attribute(Named): - __locked = False - __obj = None - - def __init__(self): - m = re.match('^([a-z]+)__([a-z]+)$', self.__class__.__name__) - assert m - self.__obj_name = m.group(1) - self.__attr_name = m.group(2) - - def __get_obj(self): - return self.__obj - def __set_obj(self, obj): - if self.__obj is not None: - raise errors.TwiceSetError(self.__class__.__name__, 'obj') - assert isinstance(obj, Object) - self.__obj = obj - assert self.obj is obj - obj = property(__get_obj, __set_obj) - - def __get_obj_name(self): - return self.__obj_name - obj_name = property(__get_obj_name) - - def __get_attr_name(self): - return self.__attr_name - attr_name = property(__get_attr_name) - - -class Method(AbstractCommand, Attribute): - def _get_name(self): - return '%s_%s' % (self.attr_name, self.obj_name) - - -class Property(Attribute): - def _get_name(self): - return self.attr_name - - -class Command(AbstractCommand, Named): - pass - - -class Object(Named): - __methods = None - __properties = None - - def __get_methods(self): - return self.__methods - def __set_methods(self, methods): - if self.__methods is not None: - raise errors.TwiceSetError( - self.__class__.__name__, 'methods' - ) - assert type(methods) is NameSpace - self.__methods = methods - assert self.methods is methods - methods = property(__get_methods, __set_methods) - - def __get_properties(self): - return self.__properties - def __set_properties(self, properties): - if self.__properties is not None: - raise errors.TwiceSetError( - self.__class__.__name__, 'properties' - ) - assert type(properties) is NameSpace - self.__properties = properties - assert self.properties is properties - properties = property(__get_properties, __set_properties) - - - -class AttributeCollector(object): - def __init__(self): - self.__d = {} - - def __getitem__(self, key): - assert isinstance(key, str) - if key not in self.__d: - self.__d[key] = {} - return self.__d[key] - - def __iter__(self): - for key in self.__d: - yield key - - def add(self, i): - assert isinstance(i, Attribute) - self[i.obj_name][i.attr_name] = i - - def namespaces(self): - for key in self: - yield (key, NameSpace(self[key])) - - -class Collector(object): - def __init__(self): - self.__d = {} - - def __get_d(self): - return dict(self.__d) - d = property(__get_d) - - def __iter__(self): - for key in self.__d: - yield key - - def add(self, i): - assert isinstance(i, Named) - self.__d[i.name] = i - - def ns(self): - return NameSpace(self.__d) - - -class Proxy(object): - def __init__(self, d): - self.__d = d - - def __getattr__(self, name): - if name not in self.__d: - raise AttributeError(name) - return self.__d[name] - - - -class Registrar(object): - __allowed = ( - Command, - Object, - Method, - Property, - ) - - def __init__(self, d=None): - if d is None: - self.__d = {} - else: - assert isinstance(d, dict) - assert d == {} - self.__d = d - for base in self.__allowed: - assert inspect.isclass(base) - assert base.__name__ not in self.__d - sub_d = {} - self.__d[base.__name__] = sub_d - setattr(self, base.__name__, Proxy(sub_d)) - - def __iter__(self): - for key in self.__d: - yield key - - def __getitem__(self, key): - return dict(self.__d[key]) - - def items(self): - for key in self: - yield (key, self[key]) - - def __findbase(self, cls): - if not inspect.isclass(cls): - raise errors.RegistrationError('not a class', cls) - for base in self.__allowed: - if issubclass(cls, base): - return base - raise errors.RegistrationError( - 'not subclass of an allowed base', - cls, - ) - - def __call__(self, cls): - base = self.__findbase(cls) - ns = self.__d[base.__name__] - assert cls.__name__ not in ns - ns[cls.__name__] = cls - - - def get_instances(self, base_name): - for cls in self[base_name].values(): - yield cls() - - def get_attrs(self, base_name): - d = {} - for i in self.get_instances(base_name): - if i.obj_name not in d: - d[i.obj_name] = [] - d[i.obj_name].append(i) - return d - - - - - - -class RegistrarOld(object): - - - def __init__(self): - self.__tmp_commands = Collector() - self.__tmp_objects = Collector() - self.__tmp_methods = AttributeCollector() - self.__tmp_properties = AttributeCollector() - - def __get_objects(self): - return self.__objects - objects = property(__get_objects) - - def __get_commands(self): - return self.__commands - commands = property(__get_commands) - - - def __get_target(self, i): - if isinstance(i, Command): - return self.__tmp_commands - if isinstance(i, Object): - return self.__tmp_objects - if isinstance(i, Method): - return self.__tmp_methods - assert isinstance(i, Property) - return self.__tmp_properties - - - def register(self, cls): - assert inspect.isclass(cls) - assert issubclass(cls, Named) - i = cls() - self.__get_target(i).add(i) - - - def finalize(self): - self.__objects = self.__tmp_objects.ns() - for (key, ns) in self.__tmp_methods.namespaces(): - self.__objects[key].methods = ns - for (key, ns) in self.__tmp_properties.namespaces(): - self.__objects[key].properties = ns - commands = self.__tmp_commands.d - for obj in self.__objects(): - assert isinstance(obj, Object) - if obj.methods is None: - obj.methods = NameSpace({}) - if obj.properties is None: - obj.properties = NameSpace({}) - for m in obj.methods(): - m.obj = obj - assert m.name not in commands - commands[m.name] = m - for p in obj.properties(): - p.obj = obj - self.__commands = NameSpace(commands) - - - -class API(object): - __max_cmd_len = None - __objects = None - __commands = None - - def __init__(self, registrar): - assert isinstance(registrar, Registrar) - self.__r = registrar - - def __get_objects(self): - return self.__objects - objects = property(__get_objects) - - def __get_commands(self): - return self.__commands - commands = property(__get_commands) - - def __get_max_cmd_len(self): - if self.__max_cmd_len is None: - if self.commands is None: - return None - self.__max_cmd_len = max(len(n) for n in self.commands) - return self.__max_cmd_len - max_cmd_len = property(__get_max_cmd_len) - - def __items(self, base, name): - for cls in self.__r[base].values(): - i = cls() - yield (getattr(i, name), i) - - def __namespace(self, base, name): - return NameSpace(dict(self.__items(base, name))) - - - - def finalize(self): - self.__objects = self.__namespace('Object', 'name') - - m = {} - for obj in self.__objects(): - if obj.name not in m: - m[obj.name] = {} - - for cls in self.__r['Method'].values(): - meth = cls() - assert meth.obj_name in m - - return - - for (key, ns) in self.__tmp_methods.namespaces(): - self.__objects[key].methods = ns - for (key, ns) in self.__tmp_properties.namespaces(): - self.__objects[key].properties = ns - commands = self.__tmp_commands.d - for obj in self.__objects(): - assert isinstance(obj, Object) - if obj.methods is None: - obj.methods = NameSpace({}) - if obj.properties is None: - obj.properties = NameSpace({}) - for m in obj.methods(): - m.obj = obj - assert m.name not in commands - commands[m.name] = m - for p in obj.properties(): - p.obj = obj - self.__commands = NameSpace(commands) diff --git a/ipalib/crud.py b/ipalib/crud.py deleted file mode 100644 index b61239d2..00000000 --- a/ipalib/crud.py +++ /dev/null @@ -1,37 +0,0 @@ -# Authors: -# Jason Gerard DeRose -# -# 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 - -""" -Base classes for objects with CRUD functionality. -""" - -import base - - -class Add(base.Method): - pass - -class Del(base.Method): - pass - -class Mod(base.Method): - pass - -class Find(base.Method): - pass diff --git a/ipalib/tests/test_base.py b/ipalib/tests/test_base.py deleted file mode 100644 index da9de7a0..00000000 --- a/ipalib/tests/test_base.py +++ /dev/null @@ -1,424 +0,0 @@ -# Authors: -# Jason Gerard DeRose -# -# 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 - -""" -Unit tests for `ipalib.base` module. -""" - -from ipalib import base, errors, crud - - -def read_only(obj, name): - """ - Check that a given property is read-only. - Returns the value of the property. - """ - assert isinstance(obj, object) - assert hasattr(obj, name) - raised = False - try: - setattr(obj, name, 'some new obj') - except AttributeError: - raised = True - assert raised - return getattr(obj, name) - - -class ClassChecker(object): - cls = None # Override this is subclasses - - def new(self, *args, **kw): - return self.cls(*args, **kw) - - def args(self): - return [] - - def kw(self): - return {} - - def std(self): - return self.new(*self.args(), **self.kw()) - - -class test_NameSpace: - """ - Unit tests for `NameSpace` class. - """ - - def ns(self, kw): - """ - Returns a new NameSpace instance. - """ - return base.NameSpace(kw) - - def kw(self): - """ - Returns standard test kw dict suitable for passing to - NameSpace.__init__(). - """ - return dict( - attr_a='Hello', - attr_b='all', - attr_c='yall!', - ) - - def std(self): - """ - Returns standard (kw, ns) tuple. - """ - kw = self.kw() - ns = self.ns(kw) - return (kw, ns) - - def test_public(self): - """ - Tests that a NameSpace instance created with empty dict has no public - attributes (that would then conflict with names we want to assign to - the NameSpace). Also tests that a NameSpace instance created with a - non-empty dict has no unexpected public methods. - """ - ns = self.ns({}) - assert list(ns) == [] - assert len(ns) == 0 - for name in dir(ns): - assert name.startswith('__') or name.startswith('_NameSpace__') - (kw, ns) = self.std() - keys = set(kw) - for name in dir(ns): - assert ( - name.startswith('__') or - name.startswith('_NameSpace__') or - name in keys - ) - - def test_dict_vs_attr(self): - """ - Tests that NameSpace.__getitem__() and NameSpace.__getattr__() return - the same values. - """ - (kw, ns) = self.std() - assert len(kw) > 0 - assert len(kw) == len(list(ns)) - for (key, val) in kw.items(): - assert ns[key] is val - assert getattr(ns, key) is val - - def test_setattr(self): - """ - Tests that attributes cannot be set on NameSpace instance. - """ - (kw, ns) = self.std() - value = 'new value' - for key in kw: - raised = False - try: - setattr(ns, key, value) - except errors.SetError: - raised = True - assert raised - assert getattr(ns, key, None) != value - assert ns[key] != value - - def test_setitem(self): - """ - Tests that attributes cannot be set via NameSpace dict interface. - """ - (kw, ns) = self.std() - value = 'new value' - for key in kw: - raised = False - try: - ns[key] = value - except TypeError: - raised = True - assert raised - assert getattr(ns, key, None) != value - assert ns[key] != value - - def test_hasitem(self): - """ - Test __hasitem__() membership method. - """ - (kw, ns) = self.std() - nope = [ - 'attr_d', - 'attr_e', - 'whatever', - ] - for key in kw: - assert key in ns - for key in nope: - assert key not in kw - assert key not in ns - - def test_iter(self): - """ - Tests that __iter__() method returns sorted list of attribute names. - """ - (kw, ns) = self.std() - assert list(ns) == sorted(kw) - assert [ns[k] for k in ns] == ['Hello', 'all', 'yall!'] - - def test_len(self): - """ - Test __len__() method. - """ - (kw, ns) = self.std() - assert len(kw) == len(ns) == 3 - - -def test_Named(): - class named_class(base.Named): - pass - - i = named_class() - assert i.name == 'named_class' - - -def test_Attribute(): - class user__add(base.Attribute): - pass - i = user__add() - assert i.obj_name == 'user' - assert i.attr_name == 'add' - assert i.obj is None - class user(base.Object): - pass - u = user() - i.obj = u - assert i.obj is u - raised = False - try: - i.obj = u - except errors.TwiceSetError: - raised = True - assert raised - - -def test_Method(): - class user__mod(base.Method): - pass - i = user__mod() - assert isinstance(i, base.Attribute) - assert isinstance(i, base.AbstractCommand) - assert i.obj_name == 'user' - assert i.attr_name == 'mod' - assert i.name == 'mod_user' - - -def test_Property(): - class user__firstname(base.Property): - pass - i = user__firstname() - assert isinstance(i, base.Attribute) - assert i.obj_name == 'user' - assert i.attr_name == 'firstname' - assert i.name == 'firstname' - - -def test_Command(): - class dostuff(base.Command): - pass - i = dostuff() - assert isinstance(i, base.AbstractCommand) - assert i.name == 'dostuff' - - - -def test_AttributeCollector(): - class user__add(base.Attribute): - pass - class user__mod(base.Attribute): - pass - class group__add(base.Attribute): - pass - u_a = user__add() - u_m = user__mod() - g_a = group__add() - - ac = base.AttributeCollector() - ac.add(u_a) - ac.add(u_m) - ac.add(g_a) - - assert set(ac) == set(['user', 'group']) - - u = ac['user'] - assert set(u) == set(['add', 'mod']) - assert set(u.values()) == set([u_a, u_m]) - - g = ac['group'] - assert g.keys() == ['add'] - assert g.values() == [g_a] - - -def test_Collector(): - class user(base.Object): - pass - class group(base.Object): - pass - u = user() - g = group() - c = base.Collector() - c.add(u) - c.add(g) - ns = c.ns() - assert isinstance(ns, base.NameSpace) - assert set(ns) == set(['user', 'group']) - assert ns.user is u - assert ns.group is g - - -class test_Registrar(): - r = base.Registrar() - allowed = set(['Command', 'Object', 'Method', 'Property']) - assert set(r) == allowed - - # Some test classes: - class wrong_base(object): - pass - class krbtest(base.Command): - pass - class user(base.Object): - pass - class user__add(base.Method): - pass - class user__firstname(base.Property): - pass - - # Check that exception is raised trying to register an instance of a - # class of a correct base: - raised = False - try: - r(user()) - except errors.RegistrationError: - raised = True - - # Check that exception is raised trying to register class of wrong base: - raised = False - try: - r(wrong_base) - except errors.RegistrationError: - raised = True - assert raised - - # Check that adding a valid class works - for cls in (krbtest, user, user__add, user__firstname): - r(cls) - key = cls.__bases__[0].__name__ - d = r[key] - assert d.keys() == [cls.__name__] - assert d.values() == [cls] - # Check that a copy is returned - d2 = r[key] - assert d2 == d - assert d2 is not d - p = getattr(r, key) - assert isinstance(p, base.Proxy) - # Check that same instance is returned - assert p is getattr(r, key) - assert getattr(p, cls.__name__) is cls - - for base_name in allowed: - for i in r.get_instances(base_name): - assert isinstance(i, getattr(base, base_name)) - - - m = r.get_attrs('Method') - assert isinstance(m, dict) - assert len(m) == 1 - assert len(m['user']) == 1 - assert isinstance(m['user'][0], user__add) - - p = r.get_attrs('Property') - assert isinstance(p, dict) - assert len(p) == 1 - assert len(p['user']) == 1 - assert isinstance(p['user'][0], user__firstname) - - - - - - -def test_API(): - r = base.Registrar() - api = base.API(r) - - class kinit(base.Command): - pass - class user__add(base.Method): - pass - class user__del(base.Method): - pass - class user__firstname(base.Property): - pass - class user__lastname(base.Property): - pass - class user__login(base.Property): - pass - class user(base.Object): - pass - class group(base.Object): - pass - - assert read_only(api, 'objects') is None - assert read_only(api, 'commands') is None - assert read_only(api, 'max_cmd_len') is None - - r(kinit) - r(user__add) - r(user__del) - r(user__firstname) - r(user__lastname) - r(user__login) - r(user) - r(group) - - - api.finalize() - - - objects = read_only(api, 'objects') - assert isinstance(objects, base.NameSpace) - assert len(objects) == 2 - assert list(objects) == ['group', 'user'] - assert type(objects.user) is user - assert type(objects.group) is group - - return - - u = objects.user - assert len(u.methods) == 2 - assert list(u.methods) == ['add', 'del'] - assert len(u.properties) == 3 - assert list(u.properties) == ['firstname', 'lastname', 'login'] - - for m in u.methods(): - assert m.obj is u - for p in u.properties(): - assert p.obj is u - - g = objects.group - assert len(g.methods) == 0 - assert len(g.properties) == 0 - - - assert len(r.commands) == 3 - assert list(r.commands) == sorted(['kinit', 'add_user', 'del_user']) diff --git a/ipalib/tests/test_crud.py b/ipalib/tests/test_crud.py deleted file mode 100644 index 99113c4a..00000000 --- a/ipalib/tests/test_crud.py +++ /dev/null @@ -1,22 +0,0 @@ -# Authors: -# Jason Gerard DeRose -# -# 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 - -""" -Unit tests for `ipalib.crud` module. -""" -- cgit From 4e825ba2d9d292af17acdecb2e7f739c3355a464 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 6 Aug 2008 14:59:54 +0000 Subject: 61: Proxy now does a setattr for all callable attributes in __slots__ (and uses __getattr__ for rest --- ipalib/plugable.py | 9 ++++----- ipalib/tests/test_plugable.py | 3 +++ 2 files changed, 7 insertions(+), 5 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index fe4a4531..4a790a37 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -131,7 +131,6 @@ class Proxy(ReadOnly): """ __slots__ = ( - '__call__', '__obj', 'name', ) @@ -145,10 +144,10 @@ class Proxy(ReadOnly): check_identifier(proxy_name) object.__setattr__(self, '_Proxy__obj', obj) object.__setattr__(self, 'name', proxy_name) - if callable(obj): - object.__setattr__(self, '__call__', obj.__call__) - #for name in self.__slots__: - # object.__setattr__(self, name, getattr(obj, name)) + for name in self.__slots__: + attr = getattr(obj, name) + if callable(attr): + object.__setattr__(self, name, attr) def __repr__(self): return '%s(%r)' % (self.__class__.__name__, self.__obj) diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index 6ca4050b..ab9a8665 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -62,6 +62,9 @@ def test_valid_identifier(): def test_Plugin(): + cls = plugable.Plugin + assert type(cls.name) is property + api = 'the api instance' p = plugable.Plugin() assert read_only(p, 'name') == 'Plugin' -- cgit From 495f96a73f20f0d0331099251d2472f216d05cac Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 6 Aug 2008 15:06:39 +0000 Subject: 62: NameSpace no longer takes base=base kwarg --- ipalib/plugable.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 4a790a37..ee65c516 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -174,13 +174,12 @@ class NameSpace(ReadOnly): both as instance attributes and as dictionary items. """ - def __init__(self, items, base=None): + def __init__(self, items): """ `items` should be an iterable providing the members of this NameSpace. """ object.__setattr__(self, '_NameSpace__items', tuple(items)) - object.__setattr__(self, '_NameSpace__base', base) # dict mapping Python name to item: object.__setattr__(self, '_NameSpace__pname', {}) @@ -229,11 +228,7 @@ class NameSpace(ReadOnly): raise KeyError('NameSpace has no item for key %r' % key) def __repr__(self): - if self.__base is None: - base = repr(self.__base) - else: - base = '%s.%s' % (self.__base.__module__, self.__base.__name__) - return '%s(*proxies, base=%s)' % (self.__class__.__name__, base) + return '%s(<%d proxies>)' % (self.__class__.__name__, len(self)) class Registrar(object): @@ -338,7 +333,7 @@ class API(ReadOnly): Finalize the registration, instantiate the plugins. """ for (base, plugins) in self.register: - ns = NameSpace(self.__plugin_iter(base, plugins), base=base) + ns = NameSpace(self.__plugin_iter(base, plugins)) assert not hasattr(self, base.__name__) object.__setattr__(self, base.__name__, ns) for plugin in self.__plugins: -- cgit From 57534ca5a0f5443c80ffba4c1640650a5989c7b8 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 6 Aug 2008 15:54:27 +0000 Subject: 63: Started fleshing out public.cmd --- ipalib/public.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index 941011b6..5c413ab0 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -41,10 +41,35 @@ class cmd_proxy(plugable.Proxy): class cmd(plugable.Plugin): proxy = cmd_proxy + __opt = None def get_doc(self, _): + """ + Returns the gettext translated doc-string for this command. + + For example: + + >>> def get_doc(self, _): + >>> return _('add new user') + """ raise NotImplementedError('%s.get_doc()' % self.name) + def get_options(self): + """ + Returns iterable with opt_proxy objects used to create the opt + NameSpace when __get_opt() is called. + """ + raise NotImplementedError('%s.get_options()' % self.name) + + def __get_opt(self): + """ + Returns the NameSpace containing opt_proxy objects. + """ + if self.__opt is None: + self.__opt = plugable.NameSpace(self.get_options()) + return self.__opt + opt = property(__get_opt) + def __call__(self, *args, **kw): print repr(self) -- cgit From 0c7769473ca01facdcb1768868bfd053e726fddf Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 6 Aug 2008 20:38:07 +0000 Subject: 64: Almost finish with Proxy2, where base class is passed to __init__ and methods use @export decorator; added corresponding unit tests --- ipalib/plugable.py | 73 +++++++++++++++++++++++++++++++++++++++++++ ipalib/tests/test_plugable.py | 71 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 144 insertions(+) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index ee65c516..43dd50ca 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -25,6 +25,29 @@ import re import inspect import errors +EXPORT_FLAG = 'exported' + +def export(obj): + """ + Decorator function to set the 'exported' flag to True. + + For example: + + >>> @export + >>> def my_func(): + >>> pass + >>> assert my_func.exported is True + """ + assert not hasattr(obj, EXPORT_FLAG) + setattr(obj, EXPORT_FLAG, True) + return obj + +def is_exported(obj): + """ + Returns True if `obj` as an 'exported' attribute that is True. + """ + return getattr(obj, EXPORT_FLAG, False) is True + def to_cli(name): """ @@ -168,6 +191,56 @@ class Proxy(ReadOnly): ) +class Proxy2(ReadOnly): + def __init__(self, base, target): + if not inspect.isclass(base): + raise TypeError('arg1 must be a class, got %r' % base) + if not isinstance(target, base): + raise ValueError('arg2 must be instance of arg1, got %r' % target) + object.__setattr__(self, 'base', base) + object.__setattr__(self, '_Proxy2__target', target) + object.__setattr__(self, '_Proxy2__props', dict()) + + names = [] # The names of exported attributes + # This matches implied property fget methods like '_get_user' + r = re.compile(r'^_get_([a-z][_a-z0-9]*[a-z0-9])$') + for name in dir(base): + match = r.match(name) + if name != '__call__' and name.startswith('_') and not match: + continue # Skip '_SomeClass__private', etc. + base_attr = getattr(base, name) + if is_exported(base_attr): + target_attr = getattr(target, name) + assert not hasattr(self, name), 'Cannot override %r' % name + object.__setattr__(self, name, target_attr) + names.append(name) + if match: + assert callable(target_attr), '%s must be callable' % name + key = match.group(1) + assert not hasattr(self, key), ( + '%r cannot override %r' % (name, key) + ) + self.__props[key] = target_attr + object.__setattr__(self, '_Proxy2__names', tuple(names)) + + def __call__(self, *args, **kw): + return self.__target(*args, **kw) + + def __iter__(self): + for name in self.__names: + yield name + + def __getattr__(self, name): + if name in self.__props: + return self.__props[name]() + raise AttributeError(name) + + + + + + + class NameSpace(ReadOnly): """ A read-only namespace of (key, value) pairs that can be accessed diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index ab9a8665..40e98ed3 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -168,6 +168,77 @@ def test_Proxy(): assert c.name == 'do_a_thing' +def test_Proxy2(): + cls = plugable.Proxy2 + export = plugable.export + assert issubclass(cls, plugable.ReadOnly) + + # Setup: + class base(object): + @export + def public_0(self): + return 'public_0' + + @export + def public_1(self): + return 'public_1' + + @export + def _get_some_prop(self): + return 'ya got it' + + def __call__(self, caller): + return 'ya called it, %s.' % caller + + def private_0(self): + return 'private_0' + + def private_1(self): + return 'private_1' + + class plugin(base): + pass + + # Test that TypeError is raised when base is not a class: + raises(TypeError, cls, base(), None) + + # Test that ValueError is raised when target is not instance of base: + raises(ValueError, cls, base, object()) + + # Test with correct arguments: + i = plugin() + p = cls(base, i) + assert read_only(p, 'base') is base + assert list(p) == ['_get_some_prop', 'public_0', 'public_1'] + + # Test normal methods: + for n in xrange(2): + pub = 'public_%d' % n + priv = 'private_%d' % n + assert getattr(i, pub)() == pub + assert getattr(p, pub)() == pub + assert getattr(i, priv)() == priv + assert not hasattr(p, priv) + + # Test __call__: + value = 'ya called it, dude.' + assert i('dude') == value + assert p('dude') == value + + # Test implied property: + fget = '_get_some_prop' + name = 'some_prop' + value = 'ya got it' + assert getattr(i, fget)() == value + assert getattr(p, fget)() == value + assert getattr(p, name) == value + + + + + + + def test_Registrar(): class Base1(object): pass -- cgit From f13f1226b4b798fd901ece6b9a37c06ca25c3c2e Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 6 Aug 2008 21:54:56 +0000 Subject: 65: Finished simplified Proxy2 class; updated unit tests --- ipalib/errors.py | 3 -- ipalib/plugable.py | 80 +++++++++++++------------------------------ ipalib/tests/test_plugable.py | 31 ++++++++--------- 3 files changed, 38 insertions(+), 76 deletions(-) (limited to 'ipalib') diff --git a/ipalib/errors.py b/ipalib/errors.py index e9f78477..b86ffcdb 100644 --- a/ipalib/errors.py +++ b/ipalib/errors.py @@ -53,9 +53,6 @@ class SetError(IPAError): - - - class RegistrationError(IPAError): """ Base class for errors that occur during plugin registration. diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 43dd50ca..bf0f52b4 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -25,29 +25,6 @@ import re import inspect import errors -EXPORT_FLAG = 'exported' - -def export(obj): - """ - Decorator function to set the 'exported' flag to True. - - For example: - - >>> @export - >>> def my_func(): - >>> pass - >>> assert my_func.exported is True - """ - assert not hasattr(obj, EXPORT_FLAG) - setattr(obj, EXPORT_FLAG, True) - return obj - -def is_exported(obj): - """ - Returns True if `obj` as an 'exported' attribute that is True. - """ - return getattr(obj, EXPORT_FLAG, False) is True - def to_cli(name): """ @@ -192,53 +169,42 @@ class Proxy(ReadOnly): class Proxy2(ReadOnly): - def __init__(self, base, target): + __slots__ = ( + 'base', + 'name', + '__target', + ) + def __init__(self, base, target, name_attr='name'): if not inspect.isclass(base): raise TypeError('arg1 must be a class, got %r' % base) if not isinstance(target, base): raise ValueError('arg2 must be instance of arg1, got %r' % target) object.__setattr__(self, 'base', base) object.__setattr__(self, '_Proxy2__target', target) - object.__setattr__(self, '_Proxy2__props', dict()) - - names = [] # The names of exported attributes - # This matches implied property fget methods like '_get_user' - r = re.compile(r'^_get_([a-z][_a-z0-9]*[a-z0-9])$') - for name in dir(base): - match = r.match(name) - if name != '__call__' and name.startswith('_') and not match: - continue # Skip '_SomeClass__private', etc. - base_attr = getattr(base, name) - if is_exported(base_attr): - target_attr = getattr(target, name) - assert not hasattr(self, name), 'Cannot override %r' % name - object.__setattr__(self, name, target_attr) - names.append(name) - if match: - assert callable(target_attr), '%s must be callable' % name - key = match.group(1) - assert not hasattr(self, key), ( - '%r cannot override %r' % (name, key) - ) - self.__props[key] = target_attr - object.__setattr__(self, '_Proxy2__names', tuple(names)) - def __call__(self, *args, **kw): - return self.__target(*args, **kw) + # Check base.public + assert type(self.base.public) is frozenset + + # Check name + object.__setattr__(self, 'name', getattr(target, name_attr)) + check_identifier(self.name) def __iter__(self): - for name in self.__names: + for name in sorted(self.base.public): yield name - def __getattr__(self, name): - if name in self.__props: - return self.__props[name]() - raise AttributeError(name) - - - + def __getitem__(self, key): + if key in self.base.public: + return getattr(self.__target, key) + raise KeyError('no proxy attribute %r' % key) + def __getattr__(self, name): + if name in self.base.public: + return getattr(self.__target, name) + raise AttributeError('no proxy attribute %r' % name) + def __call__(self, *args, **kw): + return self['__call__'](*args, **kw) class NameSpace(ReadOnly): diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index 40e98ed3..383e068e 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -170,23 +170,22 @@ def test_Proxy(): def test_Proxy2(): cls = plugable.Proxy2 - export = plugable.export assert issubclass(cls, plugable.ReadOnly) # Setup: class base(object): - @export + public = frozenset(( + 'public_0', + 'public_1', + '__call__', + )) + def public_0(self): return 'public_0' - @export def public_1(self): return 'public_1' - @export - def _get_some_prop(self): - return 'ya got it' - def __call__(self, caller): return 'ya called it, %s.' % caller @@ -197,7 +196,8 @@ def test_Proxy2(): return 'private_1' class plugin(base): - pass + name = 'user_add' + attr_name = 'add' # Test that TypeError is raised when base is not a class: raises(TypeError, cls, base(), None) @@ -209,7 +209,8 @@ def test_Proxy2(): i = plugin() p = cls(base, i) assert read_only(p, 'base') is base - assert list(p) == ['_get_some_prop', 'public_0', 'public_1'] + assert read_only(p, 'name') is 'user_add' + assert list(p) == sorted(base.public) # Test normal methods: for n in xrange(2): @@ -217,6 +218,7 @@ def test_Proxy2(): priv = 'private_%d' % n assert getattr(i, pub)() == pub assert getattr(p, pub)() == pub + assert hasattr(p, pub) assert getattr(i, priv)() == priv assert not hasattr(p, priv) @@ -224,14 +226,11 @@ def test_Proxy2(): value = 'ya called it, dude.' assert i('dude') == value assert p('dude') == value + assert callable(p) - # Test implied property: - fget = '_get_some_prop' - name = 'some_prop' - value = 'ya got it' - assert getattr(i, fget)() == value - assert getattr(p, fget)() == value - assert getattr(p, name) == value + # Test name_attr='name' kw arg + i = plugin() + p = cls(base, i) -- cgit From e63453a85816ee71617c89c4933ee85a605d58a4 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 6 Aug 2008 22:59:50 +0000 Subject: 66: Added NameSpace2 (bit simpler than NameSpace, better suited to Proxy2); added corresponding unit tests --- ipalib/plugable.py | 51 +++++++++++++++++++++++++++++++++++++++ ipalib/tests/test_plugable.py | 55 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index bf0f52b4..1a186b61 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -207,6 +207,57 @@ class Proxy2(ReadOnly): return self['__call__'](*args, **kw) +class NameSpace2(ReadOnly): + """ + A read-only namespace of (key, value) pairs that can be accessed + both as instance attributes and as dictionary items. + """ + + def __init__(self, proxies): + """ + NameSpace2 + """ + object.__setattr__(self, '_NameSpace2__proxies', tuple(proxies)) + object.__setattr__(self, '_NameSpace2__d', dict()) + for proxy in self.__proxies: + assert isinstance(proxy, Proxy2) + assert proxy.name not in self.__d + self.__d[proxy.name] = proxy + assert not hasattr(self, proxy.name) + object.__setattr__(self, proxy.name, proxy) + + def __iter__(self): + """ + Iterates through the proxies in this NameSpace in the same order they + were passed in the contructor. + """ + for proxy in self.__proxies: + yield proxy + + def __len__(self): + """ + Returns number of proxies in this NameSpace. + """ + return len(self.__proxies) + + def __contains__(self, key): + """ + Returns True if a proxy named `key` is in this NameSpace. + """ + return key in self.__d + + def __getitem__(self, key): + """ + Returns proxy named `key`; otherwise raises KeyError. + """ + if key in self.__d: + return self.__d[key] + raise KeyError('NameSpace has no item for key %r' % key) + + def __repr__(self): + return '%s(<%d proxies>)' % (self.__class__.__name__, len(self)) + + class NameSpace(ReadOnly): """ A read-only namespace of (key, value) pairs that can be accessed diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index 383e068e..8ad45864 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -233,7 +233,62 @@ def test_Proxy2(): p = cls(base, i) +def test_NameSpace2(): + cls = plugable.NameSpace2 + assert issubclass(cls, plugable.ReadOnly) + + class base(object): + public = frozenset(( + 'plusplus', + )) + + def plusplus(self, n): + return n + 1 + + class plugin(base): + def __init__(self, name): + self.name = name + def get_name(i): + return 'noun_verb%d' % i + + def get_proxies(n): + for i in xrange(n): + yield plugable.Proxy2(base, plugin(get_name(i))) + + cnt = 20 + ns = cls(get_proxies(cnt)) + + # Test __len__ + assert len(ns) == cnt + + # Test __iter__ + i = None + for (i, proxy) in enumerate(ns): + assert type(proxy) is plugable.Proxy2 + assert proxy.name == get_name(i) + assert i == cnt - 1 + + # Test __contains__, __getitem__, getattr(): + proxies = frozenset(ns) + for i in xrange(cnt): + name = get_name(i) + assert name in ns + proxy = ns[name] + assert proxy.name == name + assert type(proxy) is plugable.Proxy2 + assert proxy in proxies + assert read_only(ns, name) is proxy + + # Test dir(): + assert set(get_name(i) for i in xrange(cnt)).issubset(set(dir(ns))) + + # Test that KeyError, AttributeError is raised: + name = get_name(cnt) + assert name not in ns + raises(KeyError, getitem, ns, name) + raises(AttributeError, getattr, ns, name) + no_set(ns, name) -- cgit From 03bad04e7bdf6bf02eca13e0b3af3beb587fdc3d Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 6 Aug 2008 23:22:29 +0000 Subject: 67: Deleted NameSpace, Proxy; renamed NameSpace2, Proxy2 to NameSpace, Proxy --- ipalib/plugable.py | 123 +++------------------------------ ipalib/tests/test_plugable.py | 157 +++++------------------------------------- 2 files changed, 27 insertions(+), 253 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 1a186b61..769e5617 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -123,52 +123,6 @@ class ReadOnly(object): class Proxy(ReadOnly): - """ - Used to only export certain attributes into the generative API. - - Subclasses must list names of attributes to be proxied in the __slots__ - class attribute. - """ - - __slots__ = ( - '__obj', - 'name', - ) - - def __init__(self, obj, proxy_name=None): - """ - Proxy attributes on `obj`. - """ - if proxy_name is None: - proxy_name = obj.__class__.__name__ - check_identifier(proxy_name) - object.__setattr__(self, '_Proxy__obj', obj) - object.__setattr__(self, 'name', proxy_name) - for name in self.__slots__: - attr = getattr(obj, name) - if callable(attr): - object.__setattr__(self, name, attr) - - def __repr__(self): - return '%s(%r)' % (self.__class__.__name__, self.__obj) - - def __str__(self): - return to_cli(self.name) - - def _clone(self, new_name): - return self.__class__(self.__obj, proxy_name=new_name) - - def __getattr__(self, name): - if name in self.__slots__: - return getattr(self.__obj, name) - raise AttributeError('attribute %r not in %s.__slots__' % ( - name, - self.__class__.__name__ - ) - ) - - -class Proxy2(ReadOnly): __slots__ = ( 'base', 'name', @@ -180,7 +134,7 @@ class Proxy2(ReadOnly): if not isinstance(target, base): raise ValueError('arg2 must be instance of arg1, got %r' % target) object.__setattr__(self, 'base', base) - object.__setattr__(self, '_Proxy2__target', target) + object.__setattr__(self, '_Proxy__target', target) # Check base.public assert type(self.base.public) is frozenset @@ -206,8 +160,11 @@ class Proxy2(ReadOnly): def __call__(self, *args, **kw): return self['__call__'](*args, **kw) + def _clone(self, name_attr): + return self.__class__(self.base, self.__target, name_attr) + -class NameSpace2(ReadOnly): +class NameSpace(ReadOnly): """ A read-only namespace of (key, value) pairs that can be accessed both as instance attributes and as dictionary items. @@ -215,12 +172,12 @@ class NameSpace2(ReadOnly): def __init__(self, proxies): """ - NameSpace2 + NameSpace """ - object.__setattr__(self, '_NameSpace2__proxies', tuple(proxies)) - object.__setattr__(self, '_NameSpace2__d', dict()) + object.__setattr__(self, '_NameSpace__proxies', tuple(proxies)) + object.__setattr__(self, '_NameSpace__d', dict()) for proxy in self.__proxies: - assert isinstance(proxy, Proxy2) + assert isinstance(proxy, Proxy) assert proxy.name not in self.__d self.__d[proxy.name] = proxy assert not hasattr(self, proxy.name) @@ -258,68 +215,6 @@ class NameSpace2(ReadOnly): return '%s(<%d proxies>)' % (self.__class__.__name__, len(self)) -class NameSpace(ReadOnly): - """ - A read-only namespace of (key, value) pairs that can be accessed - both as instance attributes and as dictionary items. - """ - - def __init__(self, items): - """ - `items` should be an iterable providing the members of this - NameSpace. - """ - object.__setattr__(self, '_NameSpace__items', tuple(items)) - - # dict mapping Python name to item: - object.__setattr__(self, '_NameSpace__pname', {}) - - # dict mapping human-readibly name to item: - object.__setattr__(self, '_NameSpace__hname', {}) - - for item in self.__items: - object.__setattr__(self, item.name, item) - for (key, d) in [ - (item.name, self.__pname), - (str(item), self.__hname), - ]: - assert key not in d - d[key] = item - - def __iter__(self): - """ - Iterates through the items in this NameSpace in the same order they - were passed in the contructor. - """ - for item in self.__items: - yield item - - def __len__(self): - """ - Returns number of items in this NameSpace. - """ - return len(self.__items) - - def __contains__(self, key): - """ - Returns True if an item with pname or hname `key` is in this - NameSpace. - """ - return (key in self.__pname) or (key in self.__hname) - - def __getitem__(self, key): - """ - Returns item with pname or hname `key`; otherwise raises KeyError. - """ - if key in self.__pname: - return self.__pname[key] - if key in self.__hname: - return self.__hname[key] - raise KeyError('NameSpace has no item for key %r' % key) - - def __repr__(self): - return '%s(<%d proxies>)' % (self.__class__.__name__, len(self)) - class Registrar(object): def __init__(self, *allowed): diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index 8ad45864..0e170564 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -105,71 +105,7 @@ def test_ReadOnly(): def test_Proxy(): - assert issubclass(plugable.Proxy, plugable.ReadOnly) - - class CommandProxy(plugable.Proxy): - __slots__ = ( - 'validate', - '__call__', - ) - - class do_something(object): - def __repr__(self): - return '' - - def __call__(self, arg): - return arg + 1 - - def validate(self, arg): - return arg + 2 - - def not_public(self, arg): - return arg + 3 - - # Test basic Proxy functionality - i = do_something() - p = CommandProxy(i) - assert '__dict__' not in dir(p) - assert p.name == 'do_something' - assert str(p) == 'do-something' - assert repr(p) == 'CommandProxy()' - assert p(1) == 2 - assert p.validate(1) == 3 - - # Test that proxy_name can be overriden: - i = do_something() - p = CommandProxy(i, proxy_name='user_add') - assert '__dict__' not in dir(p) - assert p.name == 'user_add' - assert str(p) == 'user-add' - assert repr(p) == 'CommandProxy()' - assert p(1) == 2 - assert p.validate(1) == 3 - - # Test that attributes not listed in __slots__ are not present: - name = 'not_public' - i = do_something() - p = CommandProxy(i) - assert getattr(i, name)(1) == 4 - raises(AttributeError, getattr, p, name) - - # Test that attributes are read-only: - name = 'validate' - i = do_something() - p = CommandProxy(i) - assert getattr(p, name)(1) == 3 - assert read_only(p, name)(1) == 3 - - # Test cloning: - i = do_something() - p = CommandProxy(i) - c = p._clone('do_a_thing') - assert isinstance(c, CommandProxy) - assert c.name == 'do_a_thing' - - -def test_Proxy2(): - cls = plugable.Proxy2 + cls = plugable.Proxy assert issubclass(cls, plugable.ReadOnly) # Setup: @@ -209,7 +145,7 @@ def test_Proxy2(): i = plugin() p = cls(base, i) assert read_only(p, 'base') is base - assert read_only(p, 'name') is 'user_add' + assert read_only(p, 'name') == 'user_add' assert list(p) == sorted(base.public) # Test normal methods: @@ -230,11 +166,22 @@ def test_Proxy2(): # Test name_attr='name' kw arg i = plugin() + p = cls(base, i, 'attr_name') + assert read_only(p, 'name') == 'add' + + # Test _clone(): + i = plugin() p = cls(base, i) + assert read_only(p, 'name') == 'user_add' + c = p._clone('attr_name') + assert isinstance(c, cls) + assert read_only(c, 'name') == 'add' + assert c is not p + assert c('whoever') == p('whoever') -def test_NameSpace2(): - cls = plugable.NameSpace2 +def test_NameSpace(): + cls = plugable.NameSpace assert issubclass(cls, plugable.ReadOnly) class base(object): @@ -254,7 +201,7 @@ def test_NameSpace2(): def get_proxies(n): for i in xrange(n): - yield plugable.Proxy2(base, plugin(get_name(i))) + yield plugable.Proxy(base, plugin(get_name(i))) cnt = 20 ns = cls(get_proxies(cnt)) @@ -265,7 +212,7 @@ def test_NameSpace2(): # Test __iter__ i = None for (i, proxy) in enumerate(ns): - assert type(proxy) is plugable.Proxy2 + assert type(proxy) is plugable.Proxy assert proxy.name == get_name(i) assert i == cnt - 1 @@ -276,7 +223,7 @@ def test_NameSpace2(): assert name in ns proxy = ns[name] assert proxy.name == name - assert type(proxy) is plugable.Proxy2 + assert type(proxy) is plugable.Proxy assert proxy in proxies assert read_only(ns, name) is proxy @@ -291,8 +238,6 @@ def test_NameSpace2(): no_set(ns, name) - - def test_Registrar(): class Base1(object): pass @@ -404,72 +349,6 @@ def test_Registrar(): assert r[base.__name__] == d -def test_NameSpace(): - assert issubclass(plugable.NameSpace, plugable.ReadOnly) - - class DummyProxy(object): - def __init__(self, name): - self.__name = name - - def __get_name(self): - return self.__name - name = property(__get_name) - - def __str__(self): - return plugable.to_cli(self.__name) - - def get_name(i): - return 'noun_verb%d' % i - - def get_cli(i): - return 'noun-verb%d' % i - - def get_proxies(n): - for i in xrange(n): - yield DummyProxy(get_name(i)) - - cnt = 20 - ns = plugable.NameSpace(get_proxies(cnt)) - - # Test __len__ - assert len(ns) == cnt - - # Test __iter__ - i = None - for (i, item) in enumerate(ns): - assert type(item) is DummyProxy - assert item.name == get_name(i) - assert str(item) == get_cli(i) - assert i == cnt - 1 - - # Test __contains__, __getitem__, getattr(): - for i in xrange(cnt): - name = get_name(i) - cli = get_cli(i) - assert name in ns - assert cli in ns - item = ns[name] - assert isinstance(item, DummyProxy) - assert item.name == name - assert str(item) == cli - assert ns[name] is item - assert ns[cli] is item - assert read_only(ns, name) is item - - # Test dir(): - assert set(get_name(i) for i in xrange(cnt)).issubset(set(dir(ns))) - - # Test that KeyError, AttributeError is raised: - name = get_name(cnt) - cli = get_cli(cnt) - assert name not in ns - assert cli not in ns - raises(KeyError, getitem, ns, name) - raises(KeyError, getitem, ns, cli) - raises(AttributeError, getattr, ns, name) - no_set(ns, name) - - def test_API(): assert issubclass(plugable.API, plugable.ReadOnly) -- cgit From 7335af8a9eb4b5ab6a0884f686a51a050464320b Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 7 Aug 2008 00:14:38 +0000 Subject: 68: Ported to changes in NameSpace, Proxy; updated unit tests --- ipalib/plugable.py | 32 ++++++++++++++++--------- ipalib/public.py | 55 +++++++++++++------------------------------ ipalib/tests/test_plugable.py | 15 ++++++------ ipalib/tests/test_public.py | 1 - 4 files changed, 45 insertions(+), 58 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 769e5617..91a9143d 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -127,6 +127,7 @@ class Proxy(ReadOnly): 'base', 'name', '__target', + '__name_attr', ) def __init__(self, base, target, name_attr='name'): if not inspect.isclass(base): @@ -135,6 +136,7 @@ class Proxy(ReadOnly): raise ValueError('arg2 must be instance of arg1, got %r' % target) object.__setattr__(self, 'base', base) object.__setattr__(self, '_Proxy__target', target) + object.__setattr__(self, '_Proxy__name_attr', name_attr) # Check base.public assert type(self.base.public) is frozenset @@ -163,6 +165,14 @@ class Proxy(ReadOnly): def _clone(self, name_attr): return self.__class__(self.base, self.__target, name_attr) + def __repr__(self): + return '%s(%s, %r, %r)' % ( + self.__class__.__name__, + self.base.__name__, + self.__target, + self.__name_attr, + ) + class NameSpace(ReadOnly): """ @@ -311,27 +321,27 @@ class API(ReadOnly): keys = tuple(b.__name__ for b in allowed) object.__setattr__(self, '_API__keys', keys) object.__setattr__(self, 'register', Registrar(*allowed)) - object.__setattr__(self, '_API__plugins', []) def __call__(self): """ Finalize the registration, instantiate the plugins. """ - for (base, plugins) in self.register: - ns = NameSpace(self.__plugin_iter(base, plugins)) + d = {} + def plugin_iter(base, classes): + for cls in classes: + if cls not in d: + d[cls] = cls() + plugin = d[cls] + yield Proxy(base, plugin) + + for (base, classes) in self.register: + ns = NameSpace(plugin_iter(base, classes)) assert not hasattr(self, base.__name__) object.__setattr__(self, base.__name__, ns) - for plugin in self.__plugins: + for plugin in d.values(): plugin.finalize(self) assert plugin.api is self - def __plugin_iter(self, base, plugins): - assert issubclass(base.proxy, Proxy) - for cls in plugins: - plugin = cls() - self.__plugins.append(plugin) - yield base.proxy(plugin) - def __iter__(self): for key in self.__keys: yield key diff --git a/ipalib/public.py b/ipalib/public.py index 5c413ab0..692a6d3c 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -26,21 +26,13 @@ import re import plugable -class generic_proxy(plugable.Proxy): - __slots__ = ( - 'get_doc', - ) - - -class cmd_proxy(plugable.Proxy): - __slots__ = ( +class cmd(plugable.Plugin): + public = frozenset(( '__call__', 'get_doc', - ) + 'opt', - -class cmd(plugable.Plugin): - proxy = cmd_proxy + )) __opt = None def get_doc(self, _): @@ -74,15 +66,11 @@ class cmd(plugable.Plugin): print repr(self) -class obj_proxy(plugable.Proxy): - __slots__ = ( +class obj(plugable.Plugin): + public = frozenset(( 'mthd', 'prop', - ) - - -class obj(plugable.Plugin): - proxy = obj_proxy + )) __mthd = None __prop = None @@ -105,17 +93,11 @@ class obj(plugable.Plugin): def __filter(self, name): for i in getattr(self.api, name): if i.obj_name == self.name: - yield i._clone(i.attr_name) + yield i._clone('attr_name') -ATTR_SLOTS = ( - 'obj_name', - 'attr_name', -) - class attr(plugable.Plugin): __obj = None - proxy = generic_proxy def __init__(self): m = re.match('^([a-z]+)_([a-z]+)$', self.__class__.__name__) @@ -144,23 +126,18 @@ class attr(plugable.Plugin): self.__obj = api.obj[self.obj_name] -class mthd_proxy(plugable.Proxy): - __slots__ = ( - '__call__', - 'get_doc', - ) + ATTR_SLOTS - class mthd(attr, cmd): - proxy = mthd_proxy + public = frozenset(( + 'obj', + 'obj_name', + )) -class prop_proxy(plugable.Proxy): - __slots__ = ( - 'get_doc', - ) + ATTR_SLOTS - class prop(attr): - proxy = prop_proxy + public = frozenset(( + 'obj', + 'obj_name', + )) def get_doc(self, _): return _('prop doc') diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index 0e170564..ebc3a78f 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -352,18 +352,19 @@ def test_Registrar(): def test_API(): assert issubclass(plugable.API, plugable.ReadOnly) - # Setup the test plugins, create the Registrar: - class ExampleProxy(plugable.Proxy): - __slots__ = ['method'] - + # Setup the test bases, create the API: class base0(plugable.Plugin): - proxy = ExampleProxy + public = frozenset(( + 'method', + )) def method(self, n): return n class base1(plugable.Plugin): - proxy = ExampleProxy + public = frozenset(( + 'method', + )) def method(self, n): return n + 1 @@ -415,7 +416,7 @@ def test_API(): for p in xrange(3): plugin_name = get_plugin(b, p) proxy = ns[plugin_name] - assert isinstance(proxy, ExampleProxy) + assert isinstance(proxy, plugable.Proxy) assert proxy.name == plugin_name assert read_only(ns, plugin_name) is proxy assert read_only(proxy, 'method')(7) == 7 + b diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index bfe951ab..0985658e 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -28,7 +28,6 @@ from ipalib import public, plugable, errors def test_cmd(): cls = public.cmd assert issubclass(cls, plugable.Plugin) - assert cls.proxy is public.cmd_proxy def test_obj(): -- cgit From 19dbd5714167cca0cd48cfd73052a6d896ebc5a1 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 7 Aug 2008 00:21:50 +0000 Subject: 69: Made Proxy.base a private attribute; updated unit tests --- ipalib/plugable.py | 25 ++++++++++++++----------- ipalib/tests/test_plugable.py | 1 - 2 files changed, 14 insertions(+), 12 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 91a9143d..9cf313fa 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -124,38 +124,41 @@ class ReadOnly(object): class Proxy(ReadOnly): __slots__ = ( - 'base', - 'name', + '__base', '__target', '__name_attr', + '__public', + 'name', ) + def __init__(self, base, target, name_attr='name'): if not inspect.isclass(base): raise TypeError('arg1 must be a class, got %r' % base) if not isinstance(target, base): raise ValueError('arg2 must be instance of arg1, got %r' % target) - object.__setattr__(self, 'base', base) + object.__setattr__(self, '_Proxy__base', base) object.__setattr__(self, '_Proxy__target', target) object.__setattr__(self, '_Proxy__name_attr', name_attr) + object.__setattr__(self, '_Proxy__public', base.public) + object.__setattr__(self, 'name', getattr(target, name_attr)) - # Check base.public - assert type(self.base.public) is frozenset + # Check __public + assert type(self.__public) is frozenset # Check name - object.__setattr__(self, 'name', getattr(target, name_attr)) check_identifier(self.name) def __iter__(self): - for name in sorted(self.base.public): + for name in sorted(self.__public): yield name def __getitem__(self, key): - if key in self.base.public: + if key in self.__public: return getattr(self.__target, key) raise KeyError('no proxy attribute %r' % key) def __getattr__(self, name): - if name in self.base.public: + if name in self.__public: return getattr(self.__target, name) raise AttributeError('no proxy attribute %r' % name) @@ -163,12 +166,12 @@ class Proxy(ReadOnly): return self['__call__'](*args, **kw) def _clone(self, name_attr): - return self.__class__(self.base, self.__target, name_attr) + return self.__class__(self.__base, self.__target, name_attr) def __repr__(self): return '%s(%s, %r, %r)' % ( self.__class__.__name__, - self.base.__name__, + self.__base.__name__, self.__target, self.__name_attr, ) diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index ebc3a78f..57827787 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -144,7 +144,6 @@ def test_Proxy(): # Test with correct arguments: i = plugin() p = cls(base, i) - assert read_only(p, 'base') is base assert read_only(p, 'name') == 'user_add' assert list(p) == sorted(base.public) -- cgit From 778a019129b919b4856fc54e2f9d58209685f159 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 7 Aug 2008 00:35:51 +0000 Subject: 70: Plugin.__repr__ now again returns 'module_name.class_name()' form; updated unit test --- ipalib/plugable.py | 2 +- ipalib/tests/test_plugable.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 9cf313fa..a60105a4 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -91,7 +91,7 @@ class Plugin(object): """ Returns a fully qualified representation of the class. """ - return '%s.%s' % ( + return '%s.%s()' % ( self.__class__.__module__, self.__class__.__name__ ) diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index 57827787..f529f4c2 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -68,7 +68,7 @@ def test_Plugin(): api = 'the api instance' p = plugable.Plugin() assert read_only(p, 'name') == 'Plugin' - assert repr(p) == '%s.Plugin' % plugable.__name__ + assert repr(p) == '%s.Plugin()' % plugable.__name__ assert read_only(p, 'api') is None raises(AssertionError, p.finalize, None) p.finalize(api) @@ -79,7 +79,7 @@ def test_Plugin(): pass p = some_plugin() assert read_only(p, 'name') == 'some_plugin' - assert repr(p) == '%s.some_plugin' % __name__ + assert repr(p) == '%s.some_plugin()' % __name__ assert read_only(p, 'api') is None raises(AssertionError, p.finalize, None) p.finalize(api) -- cgit From f904cb0422194dc55cf74366145b2cf20299b657 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 7 Aug 2008 00:51:34 +0000 Subject: 71: Proxy now uses base.__public__ instead of base.public; updated unit tests --- ipalib/plugable.py | 3 +-- ipalib/public.py | 8 ++++---- ipalib/tests/test_plugable.py | 10 +++++----- 3 files changed, 10 insertions(+), 11 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index a60105a4..62d228ca 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -139,7 +139,7 @@ class Proxy(ReadOnly): object.__setattr__(self, '_Proxy__base', base) object.__setattr__(self, '_Proxy__target', target) object.__setattr__(self, '_Proxy__name_attr', name_attr) - object.__setattr__(self, '_Proxy__public', base.public) + object.__setattr__(self, '_Proxy__public', base.__public__) object.__setattr__(self, 'name', getattr(target, name_attr)) # Check __public @@ -228,7 +228,6 @@ class NameSpace(ReadOnly): return '%s(<%d proxies>)' % (self.__class__.__name__, len(self)) - class Registrar(object): def __init__(self, *allowed): """ diff --git a/ipalib/public.py b/ipalib/public.py index 692a6d3c..e6cd278a 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -27,7 +27,7 @@ import plugable class cmd(plugable.Plugin): - public = frozenset(( + __public__ = frozenset(( '__call__', 'get_doc', 'opt', @@ -67,7 +67,7 @@ class cmd(plugable.Plugin): class obj(plugable.Plugin): - public = frozenset(( + __public__ = frozenset(( 'mthd', 'prop', )) @@ -127,14 +127,14 @@ class attr(plugable.Plugin): class mthd(attr, cmd): - public = frozenset(( + __public__ = frozenset(( 'obj', 'obj_name', )) class prop(attr): - public = frozenset(( + __public__ = frozenset(( 'obj', 'obj_name', )) diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index f529f4c2..668d3406 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -110,7 +110,7 @@ def test_Proxy(): # Setup: class base(object): - public = frozenset(( + __public__ = frozenset(( 'public_0', 'public_1', '__call__', @@ -145,7 +145,7 @@ def test_Proxy(): i = plugin() p = cls(base, i) assert read_only(p, 'name') == 'user_add' - assert list(p) == sorted(base.public) + assert list(p) == sorted(base.__public__) # Test normal methods: for n in xrange(2): @@ -184,7 +184,7 @@ def test_NameSpace(): assert issubclass(cls, plugable.ReadOnly) class base(object): - public = frozenset(( + __public__ = frozenset(( 'plusplus', )) @@ -353,7 +353,7 @@ def test_API(): # Setup the test bases, create the API: class base0(plugable.Plugin): - public = frozenset(( + __public__ = frozenset(( 'method', )) @@ -361,7 +361,7 @@ def test_API(): return n class base1(plugable.Plugin): - public = frozenset(( + __public__ = frozenset(( 'method', )) -- cgit From fadbae642053565be1d10bc5d6b40b151a97ff16 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 7 Aug 2008 03:38:49 +0000 Subject: 72: Started work on public.opt class; added corresponding unit tests --- ipalib/errors.py | 20 ++++++++++++++++++++ ipalib/public.py | 30 +++++++++++++++++++++++++++++- ipalib/tests/test_public.py | 36 ++++++++++++++++++++++++++++++++++++ ipalib/tests/tstutil.py | 9 ++++++++- 4 files changed, 93 insertions(+), 2 deletions(-) (limited to 'ipalib') diff --git a/ipalib/errors.py b/ipalib/errors.py index b86ffcdb..6b1a898a 100644 --- a/ipalib/errors.py +++ b/ipalib/errors.py @@ -45,6 +45,26 @@ class IPAError(Exception): return self.msg % self.kw +class ValidationError(IPAError): + msg = 'invalid %r value %r: %s' + + def __init__(self, name, value, error): + self.name = name + self.value = value + self.error = error + super(ValidationError, self).__init__(name, value, error) + +class NormalizationError(ValidationError): + def __init__(self, name, value, type): + self.type = type + super(NormalizationError, self).__init__(name, value, + 'not %r' % type + ) + + + +class ValidationRuleError(ValidationError): + msg = '%r is invalid %r: %s' diff --git a/ipalib/public.py b/ipalib/public.py index e6cd278a..5806d0eb 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -24,10 +24,35 @@ and UI all use. import re import plugable +import errors + + +class opt(plugable.ReadOnly): + __public__ = frozenset(( + 'normalize', + 'validate', + 'default', + 'required', + 'type', + )) + + def normalize(self, value): + try: + return self.type(value) + except (TypeError, ValueError): + raise errors.NormalizationError( + self.__class__.__name__, value, self.type + ) + + + + class cmd(plugable.Plugin): __public__ = frozenset(( + 'normalize', + 'autofill', '__call__', 'get_doc', 'opt', @@ -63,7 +88,10 @@ class cmd(plugable.Plugin): opt = property(__get_opt) def __call__(self, *args, **kw): - print repr(self) + (args, kw) = self.normalize(*args, **kw) + (args, kw) = self.autofill(*args, **kw) + self.validate(*args, **kw) + class obj(plugable.Plugin): diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index 0985658e..faffd02e 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -25,6 +25,41 @@ from tstutil import raises, getitem, no_set, no_del, read_only from ipalib import public, plugable, errors +def test_opt(): + cls = public.opt + assert issubclass(cls, plugable.ReadOnly) + + class int_opt(cls): + type = int + + i = int_opt() + + # Test with values that can't be converted: + nope = ( + '7.0' + 'whatever', + object, + None, + ) + for val in nope: + e = raises(errors.NormalizationError, i.normalize, val) + assert isinstance(e, errors.ValidationError) + assert e.name == 'int_opt' + assert e.value == val + assert e.error == "not " + assert e.type is int + # Test with values that can be converted: + okay = ( + 7, + 7.0, + 7.2, + 7L, + '7', + ' 7 ', + ) + for val in okay: + assert i.normalize(val) == 7 + def test_cmd(): cls = public.cmd assert issubclass(cls, plugable.Plugin) @@ -35,6 +70,7 @@ def test_obj(): assert issubclass(cls, plugable.Plugin) + def test_attr(): cls = public.attr assert issubclass(cls, plugable.Plugin) diff --git a/ipalib/tests/tstutil.py b/ipalib/tests/tstutil.py index 12ca119d..cdac4547 100644 --- a/ipalib/tests/tstutil.py +++ b/ipalib/tests/tstutil.py @@ -43,10 +43,11 @@ def raises(exception, callback, *args, **kw): raised = False try: callback(*args, **kw) - except exception: + except exception, e: raised = True if not raised: raise ExceptionNotRaised(exception) + return e def getitem(obj, key): @@ -83,3 +84,9 @@ def read_only(obj, name, value='some_new_obj'): # Return the attribute return getattr(obj, name) + + +class ClassChecker(object): + + def new(self, *args, **kw): + return self.cls(*args, **kw) -- cgit From 8a6041b7978d370418e99df8b9fc06b2055a39e6 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 7 Aug 2008 04:51:21 +0000 Subject: 73: Started work on validation rules for opt; added corresponding unit tests --- ipalib/public.py | 26 ++++++++++ ipalib/tests/test_public.py | 122 ++++++++++++++++++++++++++++++++------------ ipalib/tests/tstutil.py | 7 +++ 3 files changed, 121 insertions(+), 34 deletions(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index 5806d0eb..b43e0af9 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -27,6 +27,19 @@ import plugable import errors +RULE_FLAG = 'validation_rule' + +def rule(obj): + assert not hasattr(obj, RULE_FLAG) + setattr(obj, RULE_FLAG, True) + return obj + +def is_rule(obj): + return getattr(obj, RULE_FLAG, False) is True + + + + class opt(plugable.ReadOnly): __public__ = frozenset(( 'normalize', @@ -35,6 +48,7 @@ class opt(plugable.ReadOnly): 'required', 'type', )) + __rules = None def normalize(self, value): try: @@ -44,6 +58,18 @@ class opt(plugable.ReadOnly): self.__class__.__name__, value, self.type ) + def __get_rules(self): + if self.__rules is None: + self.__rules = tuple(self.__rules_iter()) + return self.__rules + rules = property(__get_rules) + + def __rules_iter(self): + pass + + def validate(self, value): + pass + diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index faffd02e..122f489d 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -25,40 +25,94 @@ from tstutil import raises, getitem, no_set, no_del, read_only from ipalib import public, plugable, errors -def test_opt(): - cls = public.opt - assert issubclass(cls, plugable.ReadOnly) - - class int_opt(cls): - type = int - - i = int_opt() - - # Test with values that can't be converted: - nope = ( - '7.0' - 'whatever', - object, - None, - ) - for val in nope: - e = raises(errors.NormalizationError, i.normalize, val) - assert isinstance(e, errors.ValidationError) - assert e.name == 'int_opt' - assert e.value == val - assert e.error == "not " - assert e.type is int - # Test with values that can be converted: - okay = ( - 7, - 7.0, - 7.2, - 7L, - '7', - ' 7 ', - ) - for val in okay: - assert i.normalize(val) == 7 +def test_RULE_FLAG(): + assert public.RULE_FLAG == 'validation_rule' + + +def test_rule(): + flag = public.RULE_FLAG + rule = public.rule + def my_func(): + pass + assert not hasattr(my_func, flag) + rule(my_func) + assert getattr(my_func, flag) is True + @rule + def my_func2(): + pass + assert getattr(my_func2, flag) is True + + +def test_is_rule(): + is_rule = public.is_rule + flag = public.RULE_FLAG + + class example(object): + def __init__(self, value): + if value is not None: + assert value in (True, False) + setattr(self, flag, value) + + obj = example(True) + assert getattr(obj, flag) is True + assert is_rule(obj) + + obj = example(False) + assert getattr(obj, flag) is False + assert not is_rule(obj) + + obj = example(None) + assert not hasattr(obj, flag) + assert not is_rule(obj) + + + + + + +class test_opt(): + def cls(self): + return public.opt + + def sub(self): + class int_opt(self.cls()): + type = int + return int_opt + + def test_class(self): + cls = self.cls() + assert issubclass(cls, plugable.ReadOnly) + + def test_normalize(self): + sub = self.sub() + + i = sub() + + # Test with values that can't be converted: + nope = ( + '7.0' + 'whatever', + object, + None, + ) + for val in nope: + e = raises(errors.NormalizationError, i.normalize, val) + assert isinstance(e, errors.ValidationError) + assert e.name == 'int_opt' + assert e.value == val + assert e.error == "not " + assert e.type is int + # Test with values that can be converted: + okay = ( + 7, + 7.0, + 7.2, + 7L, + '7', + ' 7 ', + ) + for val in okay: + assert i.normalize(val) == 7 def test_cmd(): cls = public.cmd diff --git a/ipalib/tests/tstutil.py b/ipalib/tests/tstutil.py index cdac4547..d2de3b86 100644 --- a/ipalib/tests/tstutil.py +++ b/ipalib/tests/tstutil.py @@ -86,7 +86,14 @@ def read_only(obj, name, value='some_new_obj'): return getattr(obj, name) +def is_prop(prop): + return type(prop) is property + + class ClassChecker(object): def new(self, *args, **kw): return self.cls(*args, **kw) + + def get_sub(self): + raise NotImplementedError('get_sub()') -- cgit From 2cc88a7a3233d6bd70ec174ba976d093dc7f0d98 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 7 Aug 2008 05:02:56 +0000 Subject: 74: Finished opt.__rules_iter(); is_rule(obj) now returns False if obj is not callable; updated unit tests --- ipalib/public.py | 11 +++++++---- ipalib/tests/test_public.py | 23 ++++++++--------------- 2 files changed, 15 insertions(+), 19 deletions(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index b43e0af9..baae83e4 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -35,9 +35,7 @@ def rule(obj): return obj def is_rule(obj): - return getattr(obj, RULE_FLAG, False) is True - - + return callable(obj) and getattr(obj, RULE_FLAG, False) is True class opt(plugable.ReadOnly): @@ -65,7 +63,12 @@ class opt(plugable.ReadOnly): rules = property(__get_rules) def __rules_iter(self): - pass + for name in dir(self): + if name.startswith('_'): + continue + attr = getattr(self, name) + if is_rule(attr): + yield attr def validate(self, value): pass diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index 122f489d..ef2ded17 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -47,27 +47,20 @@ def test_is_rule(): is_rule = public.is_rule flag = public.RULE_FLAG - class example(object): + class no_call(object): def __init__(self, value): if value is not None: assert value in (True, False) setattr(self, flag, value) - obj = example(True) - assert getattr(obj, flag) is True - assert is_rule(obj) - - obj = example(False) - assert getattr(obj, flag) is False - assert not is_rule(obj) - - obj = example(None) - assert not hasattr(obj, flag) - assert not is_rule(obj) - - - + class call(no_call): + def __call__(self): + pass + assert is_rule(call(True)) + assert not is_rule(no_call(True)) + assert not is_rule(call(False)) + assert not is_rule(call(None)) class test_opt(): -- cgit From 8cbd8343be843e2972b0f59250c148973f26a091 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 7 Aug 2008 06:02:46 +0000 Subject: 75: Fixed opt.__rules_iter; added corresponding unit tests --- ipalib/public.py | 18 ++++++++++++------ ipalib/tests/test_public.py | 26 ++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 6 deletions(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index baae83e4..9467feaf 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -58,17 +58,24 @@ class opt(plugable.ReadOnly): def __get_rules(self): if self.__rules is None: - self.__rules = tuple(self.__rules_iter()) + rules = sorted( + self.__rules_iter(), + key=lambda f: getattr(f, '__name__'), + ) + object.__setattr__(self, '_opt__rules', tuple(rules)) return self.__rules rules = property(__get_rules) def __rules_iter(self): - for name in dir(self): + for name in dir(self.__class__): if name.startswith('_'): continue - attr = getattr(self, name) - if is_rule(attr): - yield attr + base_attr = getattr(self.__class__, name) + if is_rule(base_attr): + attr = getattr(self, name) + if is_rule(attr): + yield attr + def validate(self, value): pass @@ -77,7 +84,6 @@ class opt(plugable.ReadOnly): - class cmd(plugable.Plugin): __public__ = frozenset(( 'normalize', diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index ef2ded17..87d6d104 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -68,10 +68,36 @@ class test_opt(): return public.opt def sub(self): + rule = public.rule class int_opt(self.cls()): type = int + @rule + def rule_a(self, value): + if value == 'a': + return 'cannot be a' + @rule + def rule_b(self, value): + if value == 'b': + return 'cannot be b' + @rule + def rule_c(self, value): + if value == 'c': + return 'cannot be c' return int_opt + def test_rules(self): + """ + Test the rules property. + """ + i = self.sub()() + def i_attr(l): + return getattr(i, 'rule_%s' % l) + letters = ('a', 'b', 'c') + rules = tuple(i_attr(l) for l in letters) + assert i.rules == rules + + + def test_class(self): cls = self.cls() assert issubclass(cls, plugable.ReadOnly) -- cgit From 14a0658464b0a4696a2788692610a7fdade2fdbd Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 7 Aug 2008 06:23:02 +0000 Subject: 76: Fleshed out opt.validate(); added corresponding unit tests --- ipalib/errors.py | 8 +++++--- ipalib/public.py | 14 ++++++++++++-- ipalib/tests/test_public.py | 41 ++++++++++++++++++++++++++--------------- 3 files changed, 43 insertions(+), 20 deletions(-) (limited to 'ipalib') diff --git a/ipalib/errors.py b/ipalib/errors.py index 6b1a898a..f1162827 100644 --- a/ipalib/errors.py +++ b/ipalib/errors.py @@ -54,6 +54,7 @@ class ValidationError(IPAError): self.error = error super(ValidationError, self).__init__(name, value, error) + class NormalizationError(ValidationError): def __init__(self, name, value, type): self.type = type @@ -62,9 +63,10 @@ class NormalizationError(ValidationError): ) - -class ValidationRuleError(ValidationError): - msg = '%r is invalid %r: %s' +class RuleError(ValidationError): + def __init__(self, name, value, rule, error): + self.rule = rule + super(RuleError, self).__init__(name, value, error) diff --git a/ipalib/public.py b/ipalib/public.py index 9467feaf..358bd076 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -76,9 +76,19 @@ class opt(plugable.ReadOnly): if is_rule(attr): yield attr - def validate(self, value): - pass + for rule in self.rules: + msg = rule(value) + if msg is None: + continue + raise errors.RuleError( + self.__class__.__name__, + value, + rule, + msg, + ) + + diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index 87d6d104..57cb2a77 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -72,29 +72,40 @@ class test_opt(): class int_opt(self.cls()): type = int @rule - def rule_a(self, value): - if value == 'a': - return 'cannot be a' + def rule_0(self, value): + if value == 0: + return 'cannot be 0' @rule - def rule_b(self, value): - if value == 'b': - return 'cannot be b' + def rule_1(self, value): + if value == 1: + return 'cannot be 1' @rule - def rule_c(self, value): - if value == 'c': - return 'cannot be c' + def rule_2(self, value): + if value == 2: + return 'cannot be 2' return int_opt def test_rules(self): """ Test the rules property. """ - i = self.sub()() - def i_attr(l): - return getattr(i, 'rule_%s' % l) - letters = ('a', 'b', 'c') - rules = tuple(i_attr(l) for l in letters) - assert i.rules == rules + o = self.sub()() + def get_rule(i): + return getattr(o, 'rule_%d' % i) + rules = tuple(get_rule(i) for i in xrange(3)) + assert o.rules == rules + + def test_validation(self): + """ + Test the validation method. + """ + o = self.sub()() + o.validate(9) + for i in xrange(3): + e = raises(errors.RuleError, o.validate, i) + assert e.error == 'cannot be %d' % i + + -- cgit From 62dc9a78fd919193a53d4490e6ac03ec889efa3c Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 7 Aug 2008 23:11:02 +0000 Subject: 77: A few stylistic changes --- ipalib/public.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index 358bd076..2c5d31ae 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -79,14 +79,13 @@ class opt(plugable.ReadOnly): def validate(self, value): for rule in self.rules: msg = rule(value) - if msg is None: - continue - raise errors.RuleError( - self.__class__.__name__, - value, - rule, - msg, - ) + if msg is not None: + raise errors.RuleError( + self.__class__.__name__, + value, + rule, + msg, + ) @@ -136,6 +135,7 @@ class cmd(plugable.Plugin): (args, kw) = self.normalize(*args, **kw) (args, kw) = self.autofill(*args, **kw) self.validate(*args, **kw) + self.execute(*args, **kw) -- cgit From 9ee10d383dc03649c3358ab7a296872fac528ada Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 8 Aug 2008 04:47:42 +0000 Subject: 78: Renamed opt to option; started fleshing out cmd more --- ipalib/public.py | 47 ++++++++++++++++++++++++++++++++++++++++----- ipalib/tests/test_public.py | 21 ++++++++++---------- 2 files changed, 53 insertions(+), 15 deletions(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index 2c5d31ae..f6bad7ec 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -38,7 +38,7 @@ def is_rule(obj): return callable(obj) and getattr(obj, RULE_FLAG, False) is True -class opt(plugable.ReadOnly): +class option(object): __public__ = frozenset(( 'normalize', 'validate', @@ -49,6 +49,14 @@ class opt(plugable.ReadOnly): __rules = None def normalize(self, value): + """ + Normalize an input value. The base class implementation only does + type coercion, but subclasses might do other normalization (e.g., a + str option might strip leading and trailing white-space). + + If value cannot be normalized, NormalizationError is raised, which + is a subclass of ValidationError. + """ try: return self.type(value) except (TypeError, ValueError): @@ -57,16 +65,23 @@ class opt(plugable.ReadOnly): ) def __get_rules(self): + """ + Returns the tuple of rule methods used for input validation. This + tuple is lazily initialized the first time the property is accessed. + """ if self.__rules is None: - rules = sorted( + self.__rules = tuple(sorted( self.__rules_iter(), key=lambda f: getattr(f, '__name__'), - ) - object.__setattr__(self, '_opt__rules', tuple(rules)) + )) return self.__rules rules = property(__get_rules) def __rules_iter(self): + """ + Iterates through the attributes in this instance to retrieve the + methods implemented validation rules. + """ for name in dir(self.__class__): if name.startswith('_'): continue @@ -131,7 +146,29 @@ class cmd(plugable.Plugin): return self.__opt opt = property(__get_opt) - def __call__(self, *args, **kw): + + def normalize_iter(self, kw): + for (key, value) in kw.items(): + if key in self.options: + yield ( + key, self.options[key].normalize(value) + ) + else: + yield (key, value) + + def normalize(self, **kw): + return dict(self.normalize_iter(kw)) + + def validate(self, **kw): + for (key, value) in kw.items(): + if key in self.options: + self.options.validate(value) + + + + + + def __call__(self, **kw): (args, kw) = self.normalize(*args, **kw) (args, kw) = self.autofill(*args, **kw) self.validate(*args, **kw) diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index 57cb2a77..3ef7ad08 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -63,9 +63,9 @@ def test_is_rule(): assert not is_rule(call(None)) -class test_opt(): +class test_option(): def cls(self): - return public.opt + return public.option def sub(self): rule = public.rule @@ -85,11 +85,20 @@ class test_opt(): return 'cannot be 2' return int_opt + def test_class(self): + """ + Perform some tests on the class (not an instance). + """ + cls = self.cls() + #assert issubclass(cls, plugable.ReadOnly) + assert type(cls.rules) is property + def test_rules(self): """ Test the rules property. """ o = self.sub()() + assert len(o.rules) == 3 def get_rule(i): return getattr(o, 'rule_%d' % i) rules = tuple(get_rule(i) for i in xrange(3)) @@ -105,14 +114,6 @@ class test_opt(): e = raises(errors.RuleError, o.validate, i) assert e.error == 'cannot be %d' % i - - - - - def test_class(self): - cls = self.cls() - assert issubclass(cls, plugable.ReadOnly) - def test_normalize(self): sub = self.sub() -- cgit From b3fc5f9a41685f40da0702f860e6182182783150 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 8 Aug 2008 06:18:12 +0000 Subject: 79: More work on option and cmd --- ipalib/public.py | 50 ++++++++++++++++++++++++++++----------------- ipalib/tests/test_public.py | 47 +++++++++++++++++++++--------------------- 2 files changed, 55 insertions(+), 42 deletions(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index f6bad7ec..071d905d 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -39,6 +39,10 @@ def is_rule(obj): class option(object): + """ + The option class represents a kw argument from a command. + """ + __public__ = frozenset(( 'normalize', 'validate', @@ -64,6 +68,21 @@ class option(object): self.__class__.__name__, value, self.type ) + def validate(self, value): + """ + Calls each validation rule and if any rule fails, raises RuleError, + which is a subclass of ValidationError. + """ + for rule in self.rules: + msg = rule(value) + if msg is not None: + raise errors.RuleError( + self.__class__.__name__, + value, + rule, + msg, + ) + def __get_rules(self): """ Returns the tuple of rule methods used for input validation. This @@ -91,21 +110,12 @@ class option(object): if is_rule(attr): yield attr - def validate(self, value): - for rule in self.rules: - msg = rule(value) - if msg is not None: - raise errors.RuleError( - self.__class__.__name__, - value, - rule, - msg, - ) - - - - - + def default(self, **kw): + """ + Returns a default or auto-completed value for this option. If no + default is available, this method should return None. + """ + return None class cmd(plugable.Plugin): @@ -146,7 +156,6 @@ class cmd(plugable.Plugin): return self.__opt opt = property(__get_opt) - def normalize_iter(self, kw): for (key, value) in kw.items(): if key in self.options: @@ -164,9 +173,12 @@ class cmd(plugable.Plugin): if key in self.options: self.options.validate(value) - - - + def default(self, **kw): + for opt in self.options: + if opt.name not in kw: + value = opt.default(**kw) + if value is not None: + kw[opt.name] = value def __call__(self, **kw): (args, kw) = self.normalize(*args, **kw) diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index 3ef7ad08..f05a9c31 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -93,32 +93,9 @@ class test_option(): #assert issubclass(cls, plugable.ReadOnly) assert type(cls.rules) is property - def test_rules(self): - """ - Test the rules property. - """ - o = self.sub()() - assert len(o.rules) == 3 - def get_rule(i): - return getattr(o, 'rule_%d' % i) - rules = tuple(get_rule(i) for i in xrange(3)) - assert o.rules == rules - - def test_validation(self): - """ - Test the validation method. - """ - o = self.sub()() - o.validate(9) - for i in xrange(3): - e = raises(errors.RuleError, o.validate, i) - assert e.error == 'cannot be %d' % i - def test_normalize(self): sub = self.sub() - i = sub() - # Test with values that can't be converted: nope = ( '7.0' @@ -145,6 +122,30 @@ class test_option(): for val in okay: assert i.normalize(val) == 7 + def test_rules(self): + """ + Test the rules property. + """ + o = self.sub()() + assert len(o.rules) == 3 + def get_rule(i): + return getattr(o, 'rule_%d' % i) + rules = tuple(get_rule(i) for i in xrange(3)) + assert o.rules == rules + + def test_validation(self): + """ + Test the validation method. + """ + o = self.sub()() + o.validate(9) + for i in xrange(3): + e = raises(errors.RuleError, o.validate, i) + assert e.error == 'cannot be %d' % i + assert e.value == i + + + def test_cmd(): cls = public.cmd assert issubclass(cls, plugable.Plugin) -- cgit From f656e31a7ee366c57d959e4a3e4b9a935eb2cc07 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 8 Aug 2008 16:43:24 +0000 Subject: 80: Cleaned up docstring for option.normalize() --- ipalib/public.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index 071d905d..07f03ef8 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -52,14 +52,17 @@ class option(object): )) __rules = None + # type = unicode, int, float # Set in subclass + def normalize(self, value): """ - Normalize an input value. The base class implementation only does - type coercion, but subclasses might do other normalization (e.g., a - str option might strip leading and trailing white-space). + Returns the normalized form of `value`. If `value` cannot be + normalized, NormalizationError is raised, which is a subclass of + ValidationError. - If value cannot be normalized, NormalizationError is raised, which - is a subclass of ValidationError. + The base class implementation only does type coercion, but subclasses + might do other normalization (e.g., a unicode option might strip + leading and trailing white-space). """ try: return self.type(value) -- cgit From 8e468248155947075689e6d01c3ab90fbd9f1643 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 8 Aug 2008 17:11:29 +0000 Subject: 81: Switch from tab to 4-space indentation --- ipalib/errors.py | 160 ++++----- ipalib/identity.py | 64 ++-- ipalib/plugable.py | 592 ++++++++++++++++----------------- ipalib/plugins.py | 74 ++--- ipalib/public.py | 434 ++++++++++++------------- ipalib/tests/test_plugable.py | 738 +++++++++++++++++++++--------------------- ipalib/tests/test_public.py | 274 ++++++++-------- ipalib/tests/test_tstutil.py | 190 +++++------ ipalib/tests/tstutil.py | 96 +++--- 9 files changed, 1311 insertions(+), 1311 deletions(-) (limited to 'ipalib') diff --git a/ipalib/errors.py b/ipalib/errors.py index f1162827..ee0b931b 100644 --- a/ipalib/errors.py +++ b/ipalib/errors.py @@ -22,128 +22,128 @@ All custom errors raised by `ipalib` package. """ class IPAError(Exception): - """ - Use this base class for your custom IPA errors unless there is a - specific reason to subclass from AttributeError, KeyError, etc. - """ - msg = None - - def __init__(self, *args, **kw): - self.args = args - self.kw = kw - - def __str__(self): - """ - Returns the string representation of this exception. - """ - if self.msg is None: - if len(self.args) == 1: - return unicode(self.args[0]) - return unicode(self.args) - if len(self.args) > 0: - return self.msg % self.args - return self.msg % self.kw + """ + Use this base class for your custom IPA errors unless there is a + specific reason to subclass from AttributeError, KeyError, etc. + """ + msg = None + + def __init__(self, *args, **kw): + self.args = args + self.kw = kw + + def __str__(self): + """ + Returns the string representation of this exception. + """ + if self.msg is None: + if len(self.args) == 1: + return unicode(self.args[0]) + return unicode(self.args) + if len(self.args) > 0: + return self.msg % self.args + return self.msg % self.kw class ValidationError(IPAError): - msg = 'invalid %r value %r: %s' + msg = 'invalid %r value %r: %s' - def __init__(self, name, value, error): - self.name = name - self.value = value - self.error = error - super(ValidationError, self).__init__(name, value, error) + def __init__(self, name, value, error): + self.name = name + self.value = value + self.error = error + super(ValidationError, self).__init__(name, value, error) class NormalizationError(ValidationError): - def __init__(self, name, value, type): - self.type = type - super(NormalizationError, self).__init__(name, value, - 'not %r' % type - ) + def __init__(self, name, value, type): + self.type = type + super(NormalizationError, self).__init__(name, value, + 'not %r' % type + ) class RuleError(ValidationError): - def __init__(self, name, value, rule, error): - self.rule = rule - super(RuleError, self).__init__(name, value, error) + def __init__(self, name, value, rule, error): + self.rule = rule + super(RuleError, self).__init__(name, value, error) class SetError(IPAError): - msg = 'setting %r, but NameSpace does not allow attribute setting' + msg = 'setting %r, but NameSpace does not allow attribute setting' class RegistrationError(IPAError): - """ - Base class for errors that occur during plugin registration. - """ + """ + Base class for errors that occur during plugin registration. + """ class NameSpaceError(RegistrationError): - msg = 'name %r does not re.match %r' + msg = 'name %r does not re.match %r' class SubclassError(RegistrationError): - """ - Raised when registering a plugin that is not a subclass of one of the - allowed bases. - """ - msg = 'plugin %r not subclass of any base in %r' + """ + Raised when registering a plugin that is not a subclass of one of the + allowed bases. + """ + msg = 'plugin %r not subclass of any base in %r' - def __init__(self, cls, allowed): - self.cls = cls - self.allowed = allowed + def __init__(self, cls, allowed): + self.cls = cls + self.allowed = allowed - def __str__(self): - return self.msg % (self.cls, self.allowed) + def __str__(self): + return self.msg % (self.cls, self.allowed) class DuplicateError(RegistrationError): - """ - Raised when registering a plugin whose exact class has already been - registered. - """ - msg = '%r at %d was already registered' + """ + Raised when registering a plugin whose exact class has already been + registered. + """ + msg = '%r at %d was already registered' - def __init__(self, cls): - self.cls = cls + def __init__(self, cls): + self.cls = cls - def __str__(self): - return self.msg % (self.cls, id(self.cls)) + def __str__(self): + return self.msg % (self.cls, id(self.cls)) class OverrideError(RegistrationError): - """ - Raised when override=False yet registering a plugin that overrides an - existing plugin in the same namespace. - """ - msg = 'unexpected override of %s.%s with %r (use override=True if intended)' + """ + Raised when override=False yet registering a plugin that overrides an + existing plugin in the same namespace. + """ + msg = 'unexpected override of %s.%s with %r (use override=True if intended)' - def __init__(self, base, cls): - self.base = base - self.cls = cls + def __init__(self, base, cls): + self.base = base + self.cls = cls - def __str__(self): - return self.msg % (self.base.__name__, self.cls.__name__, self.cls) + def __str__(self): + return self.msg % (self.base.__name__, self.cls.__name__, self.cls) class MissingOverrideError(RegistrationError): - """ - Raised when override=True yet no preexisting plugin with the same name - and base has been registered. - """ - msg = '%s.%s has not been registered, cannot override with %r' + """ + Raised when override=True yet no preexisting plugin with the same name + and base has been registered. + """ + msg = '%s.%s has not been registered, cannot override with %r' - def __init__(self, base, cls): - self.base = base - self.cls = cls + def __init__(self, base, cls): + self.base = base + self.cls = cls - def __str__(self): - return self.msg % (self.base.__name__, self.cls.__name__, self.cls) + def __str__(self): + return self.msg % (self.base.__name__, self.cls.__name__, self.cls) class TwiceSetError(IPAError): - msg = '%s.%s cannot be set twice' + msg = '%s.%s cannot be set twice' diff --git a/ipalib/identity.py b/ipalib/identity.py index 00caa20b..1239816b 100644 --- a/ipalib/identity.py +++ b/ipalib/identity.py @@ -5,50 +5,50 @@ - def get_label(self, _): - return _('Title') # Enum? + def get_label(self, _): + return _('Title') # Enum? - def get_label(self, _): - return _('First Name') + def get_label(self, _): + return _('First Name') - def get_label(self, _): - return _('Last Name') + def get_label(self, _): + return _('Last Name') - def get_label(self, _): - return _('Full Name') # Autofill + def get_label(self, _): + return _('Full Name') # Autofill - def get_label(self, _): - return _('Display Name') # Autofill + def get_label(self, _): + return _('Display Name') # Autofill - def get_label(self, _): - return _('Initials') # generated/ro? + def get_label(self, _): + return _('Initials') # generated/ro? - def get_label(self, _): - return _('Account Status') # Enum (active, inactive) + def get_label(self, _): + return _('Account Status') # Enum (active, inactive) - def get_label(self, _): - return _('Login') + def get_label(self, _): + return _('Login') - def get_label(self, _): - return _('Password') + def get_label(self, _): + return _('Password') - def get_label(self, _): # Same field as above, special interface - return _('Confirm Password') + def get_label(self, _): # Same field as above, special interface + return _('Confirm Password') - def get_label(self, _): - return _('UID') #ro + def get_label(self, _): + return _('UID') #ro - def get_label(self, _): - return _('GID') #ro + def get_label(self, _): + return _('GID') #ro - def get_label(self, _): - return _('Home Directory') #ro + def get_label(self, _): + return _('Home Directory') #ro - def get_label(self, _): - return _('Login Shell') + def get_label(self, _): + return _('Login Shell') - def get_label(self, _): - return _('GECOS') + def get_label(self, _): + return _('GECOS') - def get_label(self, _): - return _('') + def get_label(self, _): + return _('') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 62d228ca..c3eb409b 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -27,323 +27,323 @@ import errors def to_cli(name): - """ - Takes a Python identifier and transforms it into form suitable for the - Command Line Interface. - """ - assert isinstance(name, str) - return name.replace('_', '-') + """ + Takes a Python identifier and transforms it into form suitable for the + Command Line Interface. + """ + assert isinstance(name, str) + return name.replace('_', '-') def from_cli(cli_name): - """ - Takes a string from the Command Line Interface and transforms it into a - Python identifier. - """ - assert isinstance(cli_name, basestring) - return cli_name.replace('-', '_') + """ + Takes a string from the Command Line Interface and transforms it into a + Python identifier. + """ + assert isinstance(cli_name, basestring) + return cli_name.replace('-', '_') def check_identifier(name): - """ - Raises errors.NameSpaceError if `name` is not a valid Python identifier - suitable for use in a NameSpace. - """ - regex = r'^[a-z][_a-z0-9]*[a-z0-9]$' - if re.match(regex, name) is None: - raise errors.NameSpaceError(name, regex) + """ + Raises errors.NameSpaceError if `name` is not a valid Python identifier + suitable for use in a NameSpace. + """ + regex = r'^[a-z][_a-z0-9]*[a-z0-9]$' + if re.match(regex, name) is None: + raise errors.NameSpaceError(name, regex) class Plugin(object): - """ - Base class for all plugins. - """ - - __api = None - - def __get_api(self): - """ - Returns the plugable.API instance passed to Plugin.finalize(), or - or returns None if finalize() has not yet been called. - """ - return self.__api - api = property(__get_api) - - def finalize(self, api): - """ - After all the plugins are instantiated, the plugable.API calls this - method, passing itself as the only argument. This is where plugins - should check that other plugins they depend upon have actually be - loaded. - """ - assert self.__api is None, 'finalize() can only be called once' - assert api is not None, 'finalize() argument cannot be None' - self.__api = api - - def __get_name(self): - """ - Returns the class name of this instance. - """ - return self.__class__.__name__ - name = property(__get_name) - - def __repr__(self): - """ - Returns a fully qualified representation of the class. - """ - return '%s.%s()' % ( - self.__class__.__module__, - self.__class__.__name__ - ) + """ + Base class for all plugins. + """ + + __api = None + + def __get_api(self): + """ + Returns the plugable.API instance passed to Plugin.finalize(), or + or returns None if finalize() has not yet been called. + """ + return self.__api + api = property(__get_api) + + def finalize(self, api): + """ + After all the plugins are instantiated, the plugable.API calls this + method, passing itself as the only argument. This is where plugins + should check that other plugins they depend upon have actually be + loaded. + """ + assert self.__api is None, 'finalize() can only be called once' + assert api is not None, 'finalize() argument cannot be None' + self.__api = api + + def __get_name(self): + """ + Returns the class name of this instance. + """ + return self.__class__.__name__ + name = property(__get_name) + + def __repr__(self): + """ + Returns a fully qualified representation of the class. + """ + return '%s.%s()' % ( + self.__class__.__module__, + self.__class__.__name__ + ) class ReadOnly(object): - """ - Base class for classes with read-only attributes. - """ - __slots__ = tuple() - - def __setattr__(self, name, value): - """ - This raises an AttributeError anytime an attempt is made to set an - attribute. - """ - raise AttributeError('read-only: cannot set %s.%s' % - (self.__class__.__name__, name) - ) - - def __delattr__(self, name): - """ - This raises an AttributeError anytime an attempt is made to delete an - attribute. - """ - raise AttributeError('read-only: cannot del %s.%s' % - (self.__class__.__name__, name) - ) + """ + Base class for classes with read-only attributes. + """ + __slots__ = tuple() + + def __setattr__(self, name, value): + """ + This raises an AttributeError anytime an attempt is made to set an + attribute. + """ + raise AttributeError('read-only: cannot set %s.%s' % + (self.__class__.__name__, name) + ) + + def __delattr__(self, name): + """ + This raises an AttributeError anytime an attempt is made to delete an + attribute. + """ + raise AttributeError('read-only: cannot del %s.%s' % + (self.__class__.__name__, name) + ) class Proxy(ReadOnly): - __slots__ = ( - '__base', - '__target', - '__name_attr', - '__public', - 'name', - ) - - def __init__(self, base, target, name_attr='name'): - if not inspect.isclass(base): - raise TypeError('arg1 must be a class, got %r' % base) - if not isinstance(target, base): - raise ValueError('arg2 must be instance of arg1, got %r' % target) - object.__setattr__(self, '_Proxy__base', base) - object.__setattr__(self, '_Proxy__target', target) - object.__setattr__(self, '_Proxy__name_attr', name_attr) - object.__setattr__(self, '_Proxy__public', base.__public__) - object.__setattr__(self, 'name', getattr(target, name_attr)) - - # Check __public - assert type(self.__public) is frozenset - - # Check name - check_identifier(self.name) - - def __iter__(self): - for name in sorted(self.__public): - yield name - - def __getitem__(self, key): - if key in self.__public: - return getattr(self.__target, key) - raise KeyError('no proxy attribute %r' % key) - - def __getattr__(self, name): - if name in self.__public: - return getattr(self.__target, name) - raise AttributeError('no proxy attribute %r' % name) - - def __call__(self, *args, **kw): - return self['__call__'](*args, **kw) - - def _clone(self, name_attr): - return self.__class__(self.__base, self.__target, name_attr) - - def __repr__(self): - return '%s(%s, %r, %r)' % ( - self.__class__.__name__, - self.__base.__name__, - self.__target, - self.__name_attr, - ) + __slots__ = ( + '__base', + '__target', + '__name_attr', + '__public', + 'name', + ) + + def __init__(self, base, target, name_attr='name'): + if not inspect.isclass(base): + raise TypeError('arg1 must be a class, got %r' % base) + if not isinstance(target, base): + raise ValueError('arg2 must be instance of arg1, got %r' % target) + object.__setattr__(self, '_Proxy__base', base) + object.__setattr__(self, '_Proxy__target', target) + object.__setattr__(self, '_Proxy__name_attr', name_attr) + object.__setattr__(self, '_Proxy__public', base.__public__) + object.__setattr__(self, 'name', getattr(target, name_attr)) + + # Check __public + assert type(self.__public) is frozenset + + # Check name + check_identifier(self.name) + + def __iter__(self): + for name in sorted(self.__public): + yield name + + def __getitem__(self, key): + if key in self.__public: + return getattr(self.__target, key) + raise KeyError('no proxy attribute %r' % key) + + def __getattr__(self, name): + if name in self.__public: + return getattr(self.__target, name) + raise AttributeError('no proxy attribute %r' % name) + + def __call__(self, *args, **kw): + return self['__call__'](*args, **kw) + + def _clone(self, name_attr): + return self.__class__(self.__base, self.__target, name_attr) + + def __repr__(self): + return '%s(%s, %r, %r)' % ( + self.__class__.__name__, + self.__base.__name__, + self.__target, + self.__name_attr, + ) class NameSpace(ReadOnly): - """ - A read-only namespace of (key, value) pairs that can be accessed - both as instance attributes and as dictionary items. - """ - - def __init__(self, proxies): - """ - NameSpace - """ - object.__setattr__(self, '_NameSpace__proxies', tuple(proxies)) - object.__setattr__(self, '_NameSpace__d', dict()) - for proxy in self.__proxies: - assert isinstance(proxy, Proxy) - assert proxy.name not in self.__d - self.__d[proxy.name] = proxy - assert not hasattr(self, proxy.name) - object.__setattr__(self, proxy.name, proxy) - - def __iter__(self): - """ - Iterates through the proxies in this NameSpace in the same order they - were passed in the contructor. - """ - for proxy in self.__proxies: - yield proxy - - def __len__(self): - """ - Returns number of proxies in this NameSpace. - """ - return len(self.__proxies) - - def __contains__(self, key): - """ - Returns True if a proxy named `key` is in this NameSpace. - """ - return key in self.__d - - def __getitem__(self, key): - """ - Returns proxy named `key`; otherwise raises KeyError. - """ - if key in self.__d: - return self.__d[key] - raise KeyError('NameSpace has no item for key %r' % key) - - def __repr__(self): - return '%s(<%d proxies>)' % (self.__class__.__name__, len(self)) + """ + A read-only namespace of (key, value) pairs that can be accessed + both as instance attributes and as dictionary items. + """ + + def __init__(self, proxies): + """ + NameSpace + """ + object.__setattr__(self, '_NameSpace__proxies', tuple(proxies)) + object.__setattr__(self, '_NameSpace__d', dict()) + for proxy in self.__proxies: + assert isinstance(proxy, Proxy) + assert proxy.name not in self.__d + self.__d[proxy.name] = proxy + assert not hasattr(self, proxy.name) + object.__setattr__(self, proxy.name, proxy) + + def __iter__(self): + """ + Iterates through the proxies in this NameSpace in the same order they + were passed in the contructor. + """ + for proxy in self.__proxies: + yield proxy + + def __len__(self): + """ + Returns number of proxies in this NameSpace. + """ + return len(self.__proxies) + + def __contains__(self, key): + """ + Returns True if a proxy named `key` is in this NameSpace. + """ + return key in self.__d + + def __getitem__(self, key): + """ + Returns proxy named `key`; otherwise raises KeyError. + """ + if key in self.__d: + return self.__d[key] + raise KeyError('NameSpace has no item for key %r' % key) + + def __repr__(self): + return '%s(<%d proxies>)' % (self.__class__.__name__, len(self)) class Registrar(object): - def __init__(self, *allowed): - """ - `*allowed` is a list of the base classes plugins can be subclassed - from. - """ - self.__allowed = frozenset(allowed) - self.__d = {} - self.__registered = set() - assert len(self.__allowed) == len(allowed) - for base in self.__allowed: - assert inspect.isclass(base) - assert base.__name__ not in self.__d - self.__d[base.__name__] = {} - - def __findbase(self, cls): - """ - If `cls` is a subclass of a base in self.__allowed, returns that - base; otherwise raises SubclassError. - """ - assert inspect.isclass(cls) - found = False - for base in self.__allowed: - if issubclass(cls, base): - found = True - yield base - if not found: - raise errors.SubclassError(cls, self.__allowed) - - def __call__(self, cls, override=False): - """ - Register the plugin `cls`. - """ - if not inspect.isclass(cls): - raise TypeError('plugin must be a class: %r' % cls) - - # Raise DuplicateError if this exact class was already registered: - if cls in self.__registered: - raise errors.DuplicateError(cls) - - # Find the base class or raise SubclassError: - for base in self.__findbase(cls): - sub_d = self.__d[base.__name__] - - # Check override: - if cls.__name__ in sub_d: - # Must use override=True to override: - if not override: - raise errors.OverrideError(base, cls) - else: - # There was nothing already registered to override: - if override: - raise errors.MissingOverrideError(base, cls) - - # The plugin is okay, add to sub_d: - sub_d[cls.__name__] = cls - - # The plugin is okay, add to __registered: - self.__registered.add(cls) - - def __getitem__(self, item): - """ - Returns a copy of the namespace dict of the base class named `name`. - """ - if inspect.isclass(item): - if item not in self.__allowed: - raise KeyError(repr(item)) - key = item.__name__ - else: - key = item - return dict(self.__d[key]) - - def __contains__(self, item): - """ - Returns True if a base class named `name` is in this Registrar. - """ - if inspect.isclass(item): - return item in self.__allowed - return item in self.__d - - def __iter__(self): - """ - Iterates through a (base, registered_plugins) tuple for each allowed - base. - """ - for base in self.__allowed: - sub_d = self.__d[base.__name__] - yield (base, tuple(sub_d[k] for k in sorted(sub_d))) + def __init__(self, *allowed): + """ + `*allowed` is a list of the base classes plugins can be subclassed + from. + """ + self.__allowed = frozenset(allowed) + self.__d = {} + self.__registered = set() + assert len(self.__allowed) == len(allowed) + for base in self.__allowed: + assert inspect.isclass(base) + assert base.__name__ not in self.__d + self.__d[base.__name__] = {} + + def __findbase(self, cls): + """ + If `cls` is a subclass of a base in self.__allowed, returns that + base; otherwise raises SubclassError. + """ + assert inspect.isclass(cls) + found = False + for base in self.__allowed: + if issubclass(cls, base): + found = True + yield base + if not found: + raise errors.SubclassError(cls, self.__allowed) + + def __call__(self, cls, override=False): + """ + Register the plugin `cls`. + """ + if not inspect.isclass(cls): + raise TypeError('plugin must be a class: %r' % cls) + + # Raise DuplicateError if this exact class was already registered: + if cls in self.__registered: + raise errors.DuplicateError(cls) + + # Find the base class or raise SubclassError: + for base in self.__findbase(cls): + sub_d = self.__d[base.__name__] + + # Check override: + if cls.__name__ in sub_d: + # Must use override=True to override: + if not override: + raise errors.OverrideError(base, cls) + else: + # There was nothing already registered to override: + if override: + raise errors.MissingOverrideError(base, cls) + + # The plugin is okay, add to sub_d: + sub_d[cls.__name__] = cls + + # The plugin is okay, add to __registered: + self.__registered.add(cls) + + def __getitem__(self, item): + """ + Returns a copy of the namespace dict of the base class named `name`. + """ + if inspect.isclass(item): + if item not in self.__allowed: + raise KeyError(repr(item)) + key = item.__name__ + else: + key = item + return dict(self.__d[key]) + + def __contains__(self, item): + """ + Returns True if a base class named `name` is in this Registrar. + """ + if inspect.isclass(item): + return item in self.__allowed + return item in self.__d + + def __iter__(self): + """ + Iterates through a (base, registered_plugins) tuple for each allowed + base. + """ + for base in self.__allowed: + sub_d = self.__d[base.__name__] + yield (base, tuple(sub_d[k] for k in sorted(sub_d))) class API(ReadOnly): - def __init__(self, *allowed): - keys = tuple(b.__name__ for b in allowed) - object.__setattr__(self, '_API__keys', keys) - object.__setattr__(self, 'register', Registrar(*allowed)) - - def __call__(self): - """ - Finalize the registration, instantiate the plugins. - """ - d = {} - def plugin_iter(base, classes): - for cls in classes: - if cls not in d: - d[cls] = cls() - plugin = d[cls] - yield Proxy(base, plugin) - - for (base, classes) in self.register: - ns = NameSpace(plugin_iter(base, classes)) - assert not hasattr(self, base.__name__) - object.__setattr__(self, base.__name__, ns) - for plugin in d.values(): - plugin.finalize(self) - assert plugin.api is self - - def __iter__(self): - for key in self.__keys: - yield key + def __init__(self, *allowed): + keys = tuple(b.__name__ for b in allowed) + object.__setattr__(self, '_API__keys', keys) + object.__setattr__(self, 'register', Registrar(*allowed)) + + def __call__(self): + """ + Finalize the registration, instantiate the plugins. + """ + d = {} + def plugin_iter(base, classes): + for cls in classes: + if cls not in d: + d[cls] = cls() + plugin = d[cls] + yield Proxy(base, plugin) + + for (base, classes) in self.register: + ns = NameSpace(plugin_iter(base, classes)) + assert not hasattr(self, base.__name__) + object.__setattr__(self, base.__name__, ns) + for plugin in d.values(): + plugin.finalize(self) + assert plugin.api is self + + def __iter__(self): + for key in self.__keys: + yield key diff --git a/ipalib/plugins.py b/ipalib/plugins.py index 7495acc2..cf1eaf8d 100644 --- a/ipalib/plugins.py +++ b/ipalib/plugins.py @@ -27,109 +27,109 @@ from run import api # Hypothetical functional commands (not associated with any object): class krbtest(public.cmd): - def get_doc(self, _): - return _('test your Kerberos ticket') + def get_doc(self, _): + return _('test your Kerberos ticket') api.register(krbtest) class discover(public.cmd): - def get_doc(self, _): - return _('discover IPA servers on network') + def get_doc(self, _): + return _('discover IPA servers on network') api.register(discover) # Register some methods for the 'user' object: class user_add(public.mthd): - def get_doc(self, _): - return _('add new user') + def get_doc(self, _): + return _('add new user') api.register(user_add) class user_del(public.mthd): - def get_doc(self, _): - return _('delete existing user') + def get_doc(self, _): + return _('delete existing user') api.register(user_del) class user_mod(public.mthd): - def get_doc(self, _): - return _('edit existing user') + def get_doc(self, _): + return _('edit existing user') api.register(user_mod) class user_find(public.mthd): - def get_doc(self, _): - return _('search for users') + def get_doc(self, _): + return _('search for users') api.register(user_find) # Register some properties for the 'user' object: class user_firstname(public.prop): - pass + pass api.register(user_firstname) class user_lastname(public.prop): - pass + pass api.register(user_lastname) class user_login(public.prop): - pass + pass api.register(user_login) # Register some methods for the 'group' object: class group_add(public.mthd): - def get_doc(self, _): - return _('add new group') + def get_doc(self, _): + return _('add new group') api.register(group_add) class group_del(public.mthd): - def get_doc(self, _): - return _('delete existing group') + def get_doc(self, _): + return _('delete existing group') api.register(group_del) class group_mod(public.mthd): - def get_doc(self, _): - return _('edit existing group') + def get_doc(self, _): + return _('edit existing group') api.register(group_mod) class group_find(public.mthd): - def get_doc(self, _): - return _('search for groups') + def get_doc(self, _): + return _('search for groups') api.register(group_find) # Register some methods for the 'service' object class service_add(public.mthd): - def get_doc(self, _): - return _('add new service') + def get_doc(self, _): + return _('add new service') api.register(service_add) class service_del(public.mthd): - def get_doc(self, _): - return _('delete existing service') + def get_doc(self, _): + return _('delete existing service') api.register(service_del) class service_mod(public.mthd): - def get_doc(self, _): - return _('edit existing service') + def get_doc(self, _): + return _('edit existing service') api.register(service_mod) class service_find(public.mthd): - def get_doc(self, _): - return _('search for services') + def get_doc(self, _): + return _('search for services') api.register(service_find) # And to emphasis that the registration order doesn't matter, # we'll register the objects last: class group(public.obj): - def get_doc(self, _): - return _('') + def get_doc(self, _): + return _('') api.register(group) class service(public.obj): - def get_doc(self, _): - return _('') + def get_doc(self, _): + return _('') api.register(service) class user(public.obj): - def get_doc(self, _): - return _('') + def get_doc(self, _): + return _('') api.register(user) diff --git a/ipalib/public.py b/ipalib/public.py index 07f03ef8..6f7f2154 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -30,255 +30,255 @@ import errors RULE_FLAG = 'validation_rule' def rule(obj): - assert not hasattr(obj, RULE_FLAG) - setattr(obj, RULE_FLAG, True) - return obj + assert not hasattr(obj, RULE_FLAG) + setattr(obj, RULE_FLAG, True) + return obj def is_rule(obj): - return callable(obj) and getattr(obj, RULE_FLAG, False) is True + return callable(obj) and getattr(obj, RULE_FLAG, False) is True class option(object): - """ - The option class represents a kw argument from a command. - """ - - __public__ = frozenset(( - 'normalize', - 'validate', - 'default', - 'required', - 'type', - )) - __rules = None - - # type = unicode, int, float # Set in subclass - - def normalize(self, value): - """ - Returns the normalized form of `value`. If `value` cannot be - normalized, NormalizationError is raised, which is a subclass of - ValidationError. - - The base class implementation only does type coercion, but subclasses - might do other normalization (e.g., a unicode option might strip - leading and trailing white-space). - """ - try: - return self.type(value) - except (TypeError, ValueError): - raise errors.NormalizationError( - self.__class__.__name__, value, self.type - ) - - def validate(self, value): - """ - Calls each validation rule and if any rule fails, raises RuleError, - which is a subclass of ValidationError. - """ - for rule in self.rules: - msg = rule(value) - if msg is not None: - raise errors.RuleError( - self.__class__.__name__, - value, - rule, - msg, - ) - - def __get_rules(self): - """ - Returns the tuple of rule methods used for input validation. This - tuple is lazily initialized the first time the property is accessed. - """ - if self.__rules is None: - self.__rules = tuple(sorted( - self.__rules_iter(), - key=lambda f: getattr(f, '__name__'), - )) - return self.__rules - rules = property(__get_rules) - - def __rules_iter(self): - """ - Iterates through the attributes in this instance to retrieve the - methods implemented validation rules. - """ - for name in dir(self.__class__): - if name.startswith('_'): - continue - base_attr = getattr(self.__class__, name) - if is_rule(base_attr): - attr = getattr(self, name) - if is_rule(attr): - yield attr - - def default(self, **kw): - """ - Returns a default or auto-completed value for this option. If no - default is available, this method should return None. - """ - return None + """ + The option class represents a kw argument from a command. + """ + + __public__ = frozenset(( + 'normalize', + 'validate', + 'default', + 'required', + 'type', + )) + __rules = None + + # type = unicode, int, float # Set in subclass + + def normalize(self, value): + """ + Returns the normalized form of `value`. If `value` cannot be + normalized, NormalizationError is raised, which is a subclass of + ValidationError. + + The base class implementation only does type coercion, but subclasses + might do other normalization (e.g., a unicode option might strip + leading and trailing white-space). + """ + try: + return self.type(value) + except (TypeError, ValueError): + raise errors.NormalizationError( + self.__class__.__name__, value, self.type + ) + + def validate(self, value): + """ + Calls each validation rule and if any rule fails, raises RuleError, + which is a subclass of ValidationError. + """ + for rule in self.rules: + msg = rule(value) + if msg is not None: + raise errors.RuleError( + self.__class__.__name__, + value, + rule, + msg, + ) + + def __get_rules(self): + """ + Returns the tuple of rule methods used for input validation. This + tuple is lazily initialized the first time the property is accessed. + """ + if self.__rules is None: + self.__rules = tuple(sorted( + self.__rules_iter(), + key=lambda f: getattr(f, '__name__'), + )) + return self.__rules + rules = property(__get_rules) + + def __rules_iter(self): + """ + Iterates through the attributes in this instance to retrieve the + methods implemented validation rules. + """ + for name in dir(self.__class__): + if name.startswith('_'): + continue + base_attr = getattr(self.__class__, name) + if is_rule(base_attr): + attr = getattr(self, name) + if is_rule(attr): + yield attr + + def default(self, **kw): + """ + Returns a default or auto-completed value for this option. If no + default is available, this method should return None. + """ + return None class cmd(plugable.Plugin): - __public__ = frozenset(( - 'normalize', - 'autofill', - '__call__', - 'get_doc', - 'opt', - - )) - __opt = None - - def get_doc(self, _): - """ - Returns the gettext translated doc-string for this command. - - For example: - - >>> def get_doc(self, _): - >>> return _('add new user') - """ - raise NotImplementedError('%s.get_doc()' % self.name) - - def get_options(self): - """ - Returns iterable with opt_proxy objects used to create the opt - NameSpace when __get_opt() is called. - """ - raise NotImplementedError('%s.get_options()' % self.name) - - def __get_opt(self): - """ - Returns the NameSpace containing opt_proxy objects. - """ - if self.__opt is None: - self.__opt = plugable.NameSpace(self.get_options()) - return self.__opt - opt = property(__get_opt) - - def normalize_iter(self, kw): - for (key, value) in kw.items(): - if key in self.options: - yield ( - key, self.options[key].normalize(value) - ) - else: - yield (key, value) - - def normalize(self, **kw): - return dict(self.normalize_iter(kw)) - - def validate(self, **kw): - for (key, value) in kw.items(): - if key in self.options: - self.options.validate(value) - - def default(self, **kw): - for opt in self.options: - if opt.name not in kw: - value = opt.default(**kw) - if value is not None: - kw[opt.name] = value - - def __call__(self, **kw): - (args, kw) = self.normalize(*args, **kw) - (args, kw) = self.autofill(*args, **kw) - self.validate(*args, **kw) - self.execute(*args, **kw) + __public__ = frozenset(( + 'normalize', + 'autofill', + '__call__', + 'get_doc', + 'opt', + + )) + __opt = None + + def get_doc(self, _): + """ + Returns the gettext translated doc-string for this command. + + For example: + + >>> def get_doc(self, _): + >>> return _('add new user') + """ + raise NotImplementedError('%s.get_doc()' % self.name) + + def get_options(self): + """ + Returns iterable with opt_proxy objects used to create the opt + NameSpace when __get_opt() is called. + """ + raise NotImplementedError('%s.get_options()' % self.name) + + def __get_opt(self): + """ + Returns the NameSpace containing opt_proxy objects. + """ + if self.__opt is None: + self.__opt = plugable.NameSpace(self.get_options()) + return self.__opt + opt = property(__get_opt) + + def normalize_iter(self, kw): + for (key, value) in kw.items(): + if key in self.options: + yield ( + key, self.options[key].normalize(value) + ) + else: + yield (key, value) + + def normalize(self, **kw): + return dict(self.normalize_iter(kw)) + + def validate(self, **kw): + for (key, value) in kw.items(): + if key in self.options: + self.options.validate(value) + + def default(self, **kw): + for opt in self.options: + if opt.name not in kw: + value = opt.default(**kw) + if value is not None: + kw[opt.name] = value + + def __call__(self, **kw): + (args, kw) = self.normalize(*args, **kw) + (args, kw) = self.autofill(*args, **kw) + self.validate(*args, **kw) + self.execute(*args, **kw) class obj(plugable.Plugin): - __public__ = frozenset(( - 'mthd', - 'prop', - )) - __mthd = None - __prop = None + __public__ = frozenset(( + 'mthd', + 'prop', + )) + __mthd = None + __prop = None - def __get_mthd(self): - return self.__mthd - mthd = property(__get_mthd) + def __get_mthd(self): + return self.__mthd + mthd = property(__get_mthd) - def __get_prop(self): - return self.__prop - prop = property(__get_prop) + def __get_prop(self): + return self.__prop + prop = property(__get_prop) - def finalize(self, api): - super(obj, self).finalize(api) - self.__mthd = self.__create_ns('mthd') - self.__prop = self.__create_ns('prop') + def finalize(self, api): + super(obj, self).finalize(api) + self.__mthd = self.__create_ns('mthd') + self.__prop = self.__create_ns('prop') - def __create_ns(self, name): - return plugable.NameSpace(self.__filter(name)) + def __create_ns(self, name): + return plugable.NameSpace(self.__filter(name)) - def __filter(self, name): - for i in getattr(self.api, name): - if i.obj_name == self.name: - yield i._clone('attr_name') + def __filter(self, name): + for i in getattr(self.api, name): + if i.obj_name == self.name: + yield i._clone('attr_name') class attr(plugable.Plugin): - __obj = None + __obj = None - def __init__(self): - m = re.match('^([a-z]+)_([a-z]+)$', self.__class__.__name__) - assert m - self.__obj_name = m.group(1) - self.__attr_name = m.group(2) + def __init__(self): + m = re.match('^([a-z]+)_([a-z]+)$', self.__class__.__name__) + assert m + self.__obj_name = m.group(1) + self.__attr_name = m.group(2) - def __get_obj_name(self): - return self.__obj_name - obj_name = property(__get_obj_name) + def __get_obj_name(self): + return self.__obj_name + obj_name = property(__get_obj_name) - def __get_attr_name(self): - return self.__attr_name - attr_name = property(__get_attr_name) + def __get_attr_name(self): + return self.__attr_name + attr_name = property(__get_attr_name) - def __get_obj(self): - """ - Returns the obj instance this attribute is associated with, or None - if no association has been set. - """ - return self.__obj - obj = property(__get_obj) + def __get_obj(self): + """ + Returns the obj instance this attribute is associated with, or None + if no association has been set. + """ + return self.__obj + obj = property(__get_obj) - def finalize(self, api): - super(attr, self).finalize(api) - self.__obj = api.obj[self.obj_name] + def finalize(self, api): + super(attr, self).finalize(api) + self.__obj = api.obj[self.obj_name] class mthd(attr, cmd): - __public__ = frozenset(( - 'obj', - 'obj_name', - )) + __public__ = frozenset(( + 'obj', + 'obj_name', + )) class prop(attr): - __public__ = frozenset(( - 'obj', - 'obj_name', - )) + __public__ = frozenset(( + 'obj', + 'obj_name', + )) - def get_doc(self, _): - return _('prop doc') + def get_doc(self, _): + return _('prop doc') class PublicAPI(plugable.API): - __max_cmd_len = None - - def __init__(self): - super(PublicAPI, self).__init__(cmd, obj, mthd, prop) - - def __get_max_cmd_len(self): - if self.__max_cmd_len is None: - if not hasattr(self, 'cmd'): - return None - max_cmd_len = max(len(str(cmd)) for cmd in self.cmd) - object.__setattr__(self, '_PublicAPI__max_cmd_len', max_cmd_len) - return self.__max_cmd_len - max_cmd_len = property(__get_max_cmd_len) + __max_cmd_len = None + + def __init__(self): + super(PublicAPI, self).__init__(cmd, obj, mthd, prop) + + def __get_max_cmd_len(self): + if self.__max_cmd_len is None: + if not hasattr(self, 'cmd'): + return None + max_cmd_len = max(len(str(cmd)) for cmd in self.cmd) + object.__setattr__(self, '_PublicAPI__max_cmd_len', max_cmd_len) + return self.__max_cmd_len + max_cmd_len = property(__get_max_cmd_len) diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index 668d3406..232fbd71 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -26,396 +26,396 @@ from ipalib import plugable, errors def test_to_cli(): - f = plugable.to_cli - assert f('initialize') == 'initialize' - assert f('user_add') == 'user-add' + f = plugable.to_cli + assert f('initialize') == 'initialize' + assert f('user_add') == 'user-add' def test_from_cli(): - f = plugable.from_cli - assert f('initialize') == 'initialize' - assert f('user-add') == 'user_add' + f = plugable.from_cli + assert f('initialize') == 'initialize' + assert f('user-add') == 'user_add' def test_valid_identifier(): - f = plugable.check_identifier - okay = [ - 'user_add', - 'stuff2junk', - 'sixty9', - ] - nope = [ - '_user_add', - '__user_add', - 'user_add_', - 'user_add__', - '_user_add_', - '__user_add__', - '60nine', - ] - for name in okay: - f(name) - for name in nope: - raises(errors.NameSpaceError, f, name) - for name in okay: - raises(errors.NameSpaceError, f, name.upper()) + f = plugable.check_identifier + okay = [ + 'user_add', + 'stuff2junk', + 'sixty9', + ] + nope = [ + '_user_add', + '__user_add', + 'user_add_', + 'user_add__', + '_user_add_', + '__user_add__', + '60nine', + ] + for name in okay: + f(name) + for name in nope: + raises(errors.NameSpaceError, f, name) + for name in okay: + raises(errors.NameSpaceError, f, name.upper()) def test_Plugin(): - cls = plugable.Plugin - assert type(cls.name) is property - - api = 'the api instance' - p = plugable.Plugin() - assert read_only(p, 'name') == 'Plugin' - assert repr(p) == '%s.Plugin()' % plugable.__name__ - assert read_only(p, 'api') is None - raises(AssertionError, p.finalize, None) - p.finalize(api) - assert read_only(p, 'api') is api - raises(AssertionError, p.finalize, api) - - class some_plugin(plugable.Plugin): - pass - p = some_plugin() - assert read_only(p, 'name') == 'some_plugin' - assert repr(p) == '%s.some_plugin()' % __name__ - assert read_only(p, 'api') is None - raises(AssertionError, p.finalize, None) - p.finalize(api) - assert read_only(p, 'api') is api - raises(AssertionError, p.finalize, api) + cls = plugable.Plugin + assert type(cls.name) is property + + api = 'the api instance' + p = plugable.Plugin() + assert read_only(p, 'name') == 'Plugin' + assert repr(p) == '%s.Plugin()' % plugable.__name__ + assert read_only(p, 'api') is None + raises(AssertionError, p.finalize, None) + p.finalize(api) + assert read_only(p, 'api') is api + raises(AssertionError, p.finalize, api) + + class some_plugin(plugable.Plugin): + pass + p = some_plugin() + assert read_only(p, 'name') == 'some_plugin' + assert repr(p) == '%s.some_plugin()' % __name__ + assert read_only(p, 'api') is None + raises(AssertionError, p.finalize, None) + p.finalize(api) + assert read_only(p, 'api') is api + raises(AssertionError, p.finalize, api) def test_ReadOnly(): - obj = plugable.ReadOnly() - names = ['not_an_attribute', 'an_attribute'] - for name in names: - no_set(obj, name) - no_del(obj, name) - - class some_ro_class(plugable.ReadOnly): - def __init__(self): - object.__setattr__(self, 'an_attribute', 'Hello world!') - obj = some_ro_class() - for name in names: - no_set(obj, name) - no_del(obj, name) - assert read_only(obj, 'an_attribute') == 'Hello world!' + obj = plugable.ReadOnly() + names = ['not_an_attribute', 'an_attribute'] + for name in names: + no_set(obj, name) + no_del(obj, name) + + class some_ro_class(plugable.ReadOnly): + def __init__(self): + object.__setattr__(self, 'an_attribute', 'Hello world!') + obj = some_ro_class() + for name in names: + no_set(obj, name) + no_del(obj, name) + assert read_only(obj, 'an_attribute') == 'Hello world!' def test_Proxy(): - cls = plugable.Proxy - assert issubclass(cls, plugable.ReadOnly) - - # Setup: - class base(object): - __public__ = frozenset(( - 'public_0', - 'public_1', - '__call__', - )) - - def public_0(self): - return 'public_0' - - def public_1(self): - return 'public_1' - - def __call__(self, caller): - return 'ya called it, %s.' % caller - - def private_0(self): - return 'private_0' - - def private_1(self): - return 'private_1' - - class plugin(base): - name = 'user_add' - attr_name = 'add' - - # Test that TypeError is raised when base is not a class: - raises(TypeError, cls, base(), None) - - # Test that ValueError is raised when target is not instance of base: - raises(ValueError, cls, base, object()) - - # Test with correct arguments: - i = plugin() - p = cls(base, i) - assert read_only(p, 'name') == 'user_add' - assert list(p) == sorted(base.__public__) - - # Test normal methods: - for n in xrange(2): - pub = 'public_%d' % n - priv = 'private_%d' % n - assert getattr(i, pub)() == pub - assert getattr(p, pub)() == pub - assert hasattr(p, pub) - assert getattr(i, priv)() == priv - assert not hasattr(p, priv) - - # Test __call__: - value = 'ya called it, dude.' - assert i('dude') == value - assert p('dude') == value - assert callable(p) - - # Test name_attr='name' kw arg - i = plugin() - p = cls(base, i, 'attr_name') - assert read_only(p, 'name') == 'add' - - # Test _clone(): - i = plugin() - p = cls(base, i) - assert read_only(p, 'name') == 'user_add' - c = p._clone('attr_name') - assert isinstance(c, cls) - assert read_only(c, 'name') == 'add' - assert c is not p - assert c('whoever') == p('whoever') + cls = plugable.Proxy + assert issubclass(cls, plugable.ReadOnly) + + # Setup: + class base(object): + __public__ = frozenset(( + 'public_0', + 'public_1', + '__call__', + )) + + def public_0(self): + return 'public_0' + + def public_1(self): + return 'public_1' + + def __call__(self, caller): + return 'ya called it, %s.' % caller + + def private_0(self): + return 'private_0' + + def private_1(self): + return 'private_1' + + class plugin(base): + name = 'user_add' + attr_name = 'add' + + # Test that TypeError is raised when base is not a class: + raises(TypeError, cls, base(), None) + + # Test that ValueError is raised when target is not instance of base: + raises(ValueError, cls, base, object()) + + # Test with correct arguments: + i = plugin() + p = cls(base, i) + assert read_only(p, 'name') == 'user_add' + assert list(p) == sorted(base.__public__) + + # Test normal methods: + for n in xrange(2): + pub = 'public_%d' % n + priv = 'private_%d' % n + assert getattr(i, pub)() == pub + assert getattr(p, pub)() == pub + assert hasattr(p, pub) + assert getattr(i, priv)() == priv + assert not hasattr(p, priv) + + # Test __call__: + value = 'ya called it, dude.' + assert i('dude') == value + assert p('dude') == value + assert callable(p) + + # Test name_attr='name' kw arg + i = plugin() + p = cls(base, i, 'attr_name') + assert read_only(p, 'name') == 'add' + + # Test _clone(): + i = plugin() + p = cls(base, i) + assert read_only(p, 'name') == 'user_add' + c = p._clone('attr_name') + assert isinstance(c, cls) + assert read_only(c, 'name') == 'add' + assert c is not p + assert c('whoever') == p('whoever') def test_NameSpace(): - cls = plugable.NameSpace - assert issubclass(cls, plugable.ReadOnly) - - class base(object): - __public__ = frozenset(( - 'plusplus', - )) - - def plusplus(self, n): - return n + 1 - - class plugin(base): - def __init__(self, name): - self.name = name - - def get_name(i): - return 'noun_verb%d' % i - - def get_proxies(n): - for i in xrange(n): - yield plugable.Proxy(base, plugin(get_name(i))) - - cnt = 20 - ns = cls(get_proxies(cnt)) - - # Test __len__ - assert len(ns) == cnt - - # Test __iter__ - i = None - for (i, proxy) in enumerate(ns): - assert type(proxy) is plugable.Proxy - assert proxy.name == get_name(i) - assert i == cnt - 1 - - # Test __contains__, __getitem__, getattr(): - proxies = frozenset(ns) - for i in xrange(cnt): - name = get_name(i) - assert name in ns - proxy = ns[name] - assert proxy.name == name - assert type(proxy) is plugable.Proxy - assert proxy in proxies - assert read_only(ns, name) is proxy - - # Test dir(): - assert set(get_name(i) for i in xrange(cnt)).issubset(set(dir(ns))) - - # Test that KeyError, AttributeError is raised: - name = get_name(cnt) - assert name not in ns - raises(KeyError, getitem, ns, name) - raises(AttributeError, getattr, ns, name) - no_set(ns, name) + cls = plugable.NameSpace + assert issubclass(cls, plugable.ReadOnly) + + class base(object): + __public__ = frozenset(( + 'plusplus', + )) + + def plusplus(self, n): + return n + 1 + + class plugin(base): + def __init__(self, name): + self.name = name + + def get_name(i): + return 'noun_verb%d' % i + + def get_proxies(n): + for i in xrange(n): + yield plugable.Proxy(base, plugin(get_name(i))) + + cnt = 20 + ns = cls(get_proxies(cnt)) + + # Test __len__ + assert len(ns) == cnt + + # Test __iter__ + i = None + for (i, proxy) in enumerate(ns): + assert type(proxy) is plugable.Proxy + assert proxy.name == get_name(i) + assert i == cnt - 1 + + # Test __contains__, __getitem__, getattr(): + proxies = frozenset(ns) + for i in xrange(cnt): + name = get_name(i) + assert name in ns + proxy = ns[name] + assert proxy.name == name + assert type(proxy) is plugable.Proxy + assert proxy in proxies + assert read_only(ns, name) is proxy + + # Test dir(): + assert set(get_name(i) for i in xrange(cnt)).issubset(set(dir(ns))) + + # Test that KeyError, AttributeError is raised: + name = get_name(cnt) + assert name not in ns + raises(KeyError, getitem, ns, name) + raises(AttributeError, getattr, ns, name) + no_set(ns, name) def test_Registrar(): - class Base1(object): - pass - class Base2(object): - pass - class Base3(object): - pass - class plugin1(Base1): - pass - class plugin2(Base2): - pass - class plugin3(Base3): - pass - - # Test creation of Registrar: - r = plugable.Registrar(Base1, Base2) - - # Test __hasitem__, __getitem__: - for base in [Base1, Base2]: - assert base in r - assert base.__name__ in r - assert r[base] == {} - assert r[base.__name__] == {} - - - # Check that TypeError is raised trying to register something that isn't - # a class: - raises(TypeError, r, plugin1()) - - # Check that SubclassError is raised trying to register a class that is - # not a subclass of an allowed base: - raises(errors.SubclassError, r, plugin3) - - # Check that registration works - r(plugin1) - sub_d = r['Base1'] - assert len(sub_d) == 1 - assert sub_d['plugin1'] is plugin1 - # Check that a copy is returned - assert sub_d is not r['Base1'] - assert sub_d == r['Base1'] - - # Check that DuplicateError is raised trying to register exact class - # again: - raises(errors.DuplicateError, r, plugin1) - - # Check that OverrideError is raised trying to register class with same - # name and same base: - orig1 = plugin1 - class base1_extended(Base1): - pass - class plugin1(base1_extended): - pass - raises(errors.OverrideError, r, plugin1) - - # Check that overriding works - r(plugin1, override=True) - sub_d = r['Base1'] - assert len(sub_d) == 1 - assert sub_d['plugin1'] is plugin1 - assert sub_d['plugin1'] is not orig1 - - # Check that MissingOverrideError is raised trying to override a name - # not yet registerd: - raises(errors.MissingOverrideError, r, plugin2, override=True) - - # Check that additional plugin can be registered: - r(plugin2) - sub_d = r['Base2'] - assert len(sub_d) == 1 - assert sub_d['plugin2'] is plugin2 - - - # Setup to test __iter__: - class plugin1a(Base1): - pass - r(plugin1a) - - class plugin1b(Base1): - pass - r(plugin1b) - - class plugin2a(Base2): - pass - r(plugin2a) - - class plugin2b(Base2): - pass - r(plugin2b) - - m = { - 'Base1': set([plugin1, plugin1a, plugin1b]), - 'Base2': set([plugin2, plugin2a, plugin2b]), - } - - # Now test __iter__: - for (base, plugins) in r: - assert base in [Base1, Base2] - assert set(plugins) == m[base.__name__] - assert len(list(r)) == 2 - - # Again test __hasitem__, __getitem__: - for base in [Base1, Base2]: - assert base in r - assert base.__name__ in r - d = dict((p.__name__, p) for p in m[base.__name__]) - assert len(d) == 3 - assert r[base] == d - assert r[base.__name__] == d + class Base1(object): + pass + class Base2(object): + pass + class Base3(object): + pass + class plugin1(Base1): + pass + class plugin2(Base2): + pass + class plugin3(Base3): + pass + + # Test creation of Registrar: + r = plugable.Registrar(Base1, Base2) + + # Test __hasitem__, __getitem__: + for base in [Base1, Base2]: + assert base in r + assert base.__name__ in r + assert r[base] == {} + assert r[base.__name__] == {} + + + # Check that TypeError is raised trying to register something that isn't + # a class: + raises(TypeError, r, plugin1()) + + # Check that SubclassError is raised trying to register a class that is + # not a subclass of an allowed base: + raises(errors.SubclassError, r, plugin3) + + # Check that registration works + r(plugin1) + sub_d = r['Base1'] + assert len(sub_d) == 1 + assert sub_d['plugin1'] is plugin1 + # Check that a copy is returned + assert sub_d is not r['Base1'] + assert sub_d == r['Base1'] + + # Check that DuplicateError is raised trying to register exact class + # again: + raises(errors.DuplicateError, r, plugin1) + + # Check that OverrideError is raised trying to register class with same + # name and same base: + orig1 = plugin1 + class base1_extended(Base1): + pass + class plugin1(base1_extended): + pass + raises(errors.OverrideError, r, plugin1) + + # Check that overriding works + r(plugin1, override=True) + sub_d = r['Base1'] + assert len(sub_d) == 1 + assert sub_d['plugin1'] is plugin1 + assert sub_d['plugin1'] is not orig1 + + # Check that MissingOverrideError is raised trying to override a name + # not yet registerd: + raises(errors.MissingOverrideError, r, plugin2, override=True) + + # Check that additional plugin can be registered: + r(plugin2) + sub_d = r['Base2'] + assert len(sub_d) == 1 + assert sub_d['plugin2'] is plugin2 + + + # Setup to test __iter__: + class plugin1a(Base1): + pass + r(plugin1a) + + class plugin1b(Base1): + pass + r(plugin1b) + + class plugin2a(Base2): + pass + r(plugin2a) + + class plugin2b(Base2): + pass + r(plugin2b) + + m = { + 'Base1': set([plugin1, plugin1a, plugin1b]), + 'Base2': set([plugin2, plugin2a, plugin2b]), + } + + # Now test __iter__: + for (base, plugins) in r: + assert base in [Base1, Base2] + assert set(plugins) == m[base.__name__] + assert len(list(r)) == 2 + + # Again test __hasitem__, __getitem__: + for base in [Base1, Base2]: + assert base in r + assert base.__name__ in r + d = dict((p.__name__, p) for p in m[base.__name__]) + assert len(d) == 3 + assert r[base] == d + assert r[base.__name__] == d def test_API(): - assert issubclass(plugable.API, plugable.ReadOnly) - - # Setup the test bases, create the API: - class base0(plugable.Plugin): - __public__ = frozenset(( - 'method', - )) - - def method(self, n): - return n - - class base1(plugable.Plugin): - __public__ = frozenset(( - 'method', - )) - - def method(self, n): - return n + 1 - - api = plugable.API(base0, base1) - r = api.register - assert isinstance(r, plugable.Registrar) - assert read_only(api, 'register') is r - - class base0_plugin0(base0): - pass - r(base0_plugin0) - - class base0_plugin1(base0): - pass - r(base0_plugin1) - - class base0_plugin2(base0): - pass - r(base0_plugin2) - - class base1_plugin0(base1): - pass - r(base1_plugin0) - - class base1_plugin1(base1): - pass - r(base1_plugin1) - - class base1_plugin2(base1): - pass - r(base1_plugin2) - - # Test API instance: - api() # Calling instance performs finalization - - def get_base(b): - return 'base%d' % b - - def get_plugin(b, p): - return 'base%d_plugin%d' % (b, p) - - for b in xrange(2): - base_name = get_base(b) - ns = getattr(api, base_name) - assert isinstance(ns, plugable.NameSpace) - assert read_only(api, base_name) is ns - assert len(ns) == 3 - for p in xrange(3): - plugin_name = get_plugin(b, p) - proxy = ns[plugin_name] - assert isinstance(proxy, plugable.Proxy) - assert proxy.name == plugin_name - assert read_only(ns, plugin_name) is proxy - assert read_only(proxy, 'method')(7) == 7 + b + assert issubclass(plugable.API, plugable.ReadOnly) + + # Setup the test bases, create the API: + class base0(plugable.Plugin): + __public__ = frozenset(( + 'method', + )) + + def method(self, n): + return n + + class base1(plugable.Plugin): + __public__ = frozenset(( + 'method', + )) + + def method(self, n): + return n + 1 + + api = plugable.API(base0, base1) + r = api.register + assert isinstance(r, plugable.Registrar) + assert read_only(api, 'register') is r + + class base0_plugin0(base0): + pass + r(base0_plugin0) + + class base0_plugin1(base0): + pass + r(base0_plugin1) + + class base0_plugin2(base0): + pass + r(base0_plugin2) + + class base1_plugin0(base1): + pass + r(base1_plugin0) + + class base1_plugin1(base1): + pass + r(base1_plugin1) + + class base1_plugin2(base1): + pass + r(base1_plugin2) + + # Test API instance: + api() # Calling instance performs finalization + + def get_base(b): + return 'base%d' % b + + def get_plugin(b, p): + return 'base%d_plugin%d' % (b, p) + + for b in xrange(2): + base_name = get_base(b) + ns = getattr(api, base_name) + assert isinstance(ns, plugable.NameSpace) + assert read_only(api, base_name) is ns + assert len(ns) == 3 + for p in xrange(3): + plugin_name = get_plugin(b, p) + proxy = ns[plugin_name] + assert isinstance(proxy, plugable.Proxy) + assert proxy.name == plugin_name + assert read_only(ns, plugin_name) is proxy + assert read_only(proxy, 'method')(7) == 7 + b diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index f05a9c31..f396eed2 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -26,179 +26,179 @@ from ipalib import public, plugable, errors def test_RULE_FLAG(): - assert public.RULE_FLAG == 'validation_rule' + assert public.RULE_FLAG == 'validation_rule' def test_rule(): - flag = public.RULE_FLAG - rule = public.rule - def my_func(): - pass - assert not hasattr(my_func, flag) - rule(my_func) - assert getattr(my_func, flag) is True - @rule - def my_func2(): - pass - assert getattr(my_func2, flag) is True + flag = public.RULE_FLAG + rule = public.rule + def my_func(): + pass + assert not hasattr(my_func, flag) + rule(my_func) + assert getattr(my_func, flag) is True + @rule + def my_func2(): + pass + assert getattr(my_func2, flag) is True def test_is_rule(): - is_rule = public.is_rule - flag = public.RULE_FLAG + is_rule = public.is_rule + flag = public.RULE_FLAG - class no_call(object): - def __init__(self, value): - if value is not None: - assert value in (True, False) - setattr(self, flag, value) + class no_call(object): + def __init__(self, value): + if value is not None: + assert value in (True, False) + setattr(self, flag, value) - class call(no_call): - def __call__(self): - pass + class call(no_call): + def __call__(self): + pass - assert is_rule(call(True)) - assert not is_rule(no_call(True)) - assert not is_rule(call(False)) - assert not is_rule(call(None)) + assert is_rule(call(True)) + assert not is_rule(no_call(True)) + assert not is_rule(call(False)) + assert not is_rule(call(None)) class test_option(): - def cls(self): - return public.option - - def sub(self): - rule = public.rule - class int_opt(self.cls()): - type = int - @rule - def rule_0(self, value): - if value == 0: - return 'cannot be 0' - @rule - def rule_1(self, value): - if value == 1: - return 'cannot be 1' - @rule - def rule_2(self, value): - if value == 2: - return 'cannot be 2' - return int_opt - - def test_class(self): - """ - Perform some tests on the class (not an instance). - """ - cls = self.cls() - #assert issubclass(cls, plugable.ReadOnly) - assert type(cls.rules) is property - - def test_normalize(self): - sub = self.sub() - i = sub() - # Test with values that can't be converted: - nope = ( - '7.0' - 'whatever', - object, - None, - ) - for val in nope: - e = raises(errors.NormalizationError, i.normalize, val) - assert isinstance(e, errors.ValidationError) - assert e.name == 'int_opt' - assert e.value == val - assert e.error == "not " - assert e.type is int - # Test with values that can be converted: - okay = ( - 7, - 7.0, - 7.2, - 7L, - '7', - ' 7 ', - ) - for val in okay: - assert i.normalize(val) == 7 - - def test_rules(self): - """ - Test the rules property. - """ - o = self.sub()() - assert len(o.rules) == 3 - def get_rule(i): - return getattr(o, 'rule_%d' % i) - rules = tuple(get_rule(i) for i in xrange(3)) - assert o.rules == rules - - def test_validation(self): - """ - Test the validation method. - """ - o = self.sub()() - o.validate(9) - for i in xrange(3): - e = raises(errors.RuleError, o.validate, i) - assert e.error == 'cannot be %d' % i - assert e.value == i + def cls(self): + return public.option + + def sub(self): + rule = public.rule + class int_opt(self.cls()): + type = int + @rule + def rule_0(self, value): + if value == 0: + return 'cannot be 0' + @rule + def rule_1(self, value): + if value == 1: + return 'cannot be 1' + @rule + def rule_2(self, value): + if value == 2: + return 'cannot be 2' + return int_opt + + def test_class(self): + """ + Perform some tests on the class (not an instance). + """ + cls = self.cls() + #assert issubclass(cls, plugable.ReadOnly) + assert type(cls.rules) is property + + def test_normalize(self): + sub = self.sub() + i = sub() + # Test with values that can't be converted: + nope = ( + '7.0' + 'whatever', + object, + None, + ) + for val in nope: + e = raises(errors.NormalizationError, i.normalize, val) + assert isinstance(e, errors.ValidationError) + assert e.name == 'int_opt' + assert e.value == val + assert e.error == "not " + assert e.type is int + # Test with values that can be converted: + okay = ( + 7, + 7.0, + 7.2, + 7L, + '7', + ' 7 ', + ) + for val in okay: + assert i.normalize(val) == 7 + + def test_rules(self): + """ + Test the rules property. + """ + o = self.sub()() + assert len(o.rules) == 3 + def get_rule(i): + return getattr(o, 'rule_%d' % i) + rules = tuple(get_rule(i) for i in xrange(3)) + assert o.rules == rules + + def test_validation(self): + """ + Test the validation method. + """ + o = self.sub()() + o.validate(9) + for i in xrange(3): + e = raises(errors.RuleError, o.validate, i) + assert e.error == 'cannot be %d' % i + assert e.value == i def test_cmd(): - cls = public.cmd - assert issubclass(cls, plugable.Plugin) + cls = public.cmd + assert issubclass(cls, plugable.Plugin) def test_obj(): - cls = public.obj - assert issubclass(cls, plugable.Plugin) + cls = public.obj + assert issubclass(cls, plugable.Plugin) def test_attr(): - cls = public.attr - assert issubclass(cls, plugable.Plugin) + cls = public.attr + assert issubclass(cls, plugable.Plugin) - class api(object): - obj = dict(user='the user obj') + class api(object): + obj = dict(user='the user obj') - class user_add(cls): - pass + class user_add(cls): + pass - i = user_add() - assert read_only(i, 'obj_name') == 'user' - assert read_only(i, 'attr_name') == 'add' - assert read_only(i, 'obj') is None - i.finalize(api) - assert read_only(i, 'api') is api - assert read_only(i, 'obj') == 'the user obj' + i = user_add() + assert read_only(i, 'obj_name') == 'user' + assert read_only(i, 'attr_name') == 'add' + assert read_only(i, 'obj') is None + i.finalize(api) + assert read_only(i, 'api') is api + assert read_only(i, 'obj') == 'the user obj' def test_mthd(): - cls = public.mthd - assert issubclass(cls, public.attr) - assert issubclass(cls, public.cmd) + cls = public.mthd + assert issubclass(cls, public.attr) + assert issubclass(cls, public.cmd) def test_prop(): - cls = public.prop - assert issubclass(cls, public.attr) + cls = public.prop + assert issubclass(cls, public.attr) def test_PublicAPI(): - cls = public.PublicAPI - assert issubclass(cls, plugable.API) + cls = public.PublicAPI + assert issubclass(cls, plugable.API) - api = cls() + api = cls() - class cmd1(public.cmd): - pass - api.register(cmd1) + class cmd1(public.cmd): + pass + api.register(cmd1) - class cmd2(public.cmd): - pass - api.register(cmd2) + class cmd2(public.cmd): + pass + api.register(cmd2) - api() + api() diff --git a/ipalib/tests/test_tstutil.py b/ipalib/tests/test_tstutil.py index a4c72364..76f819e4 100644 --- a/ipalib/tests/test_tstutil.py +++ b/ipalib/tests/test_tstutil.py @@ -25,124 +25,124 @@ import tstutil class Prop(object): - def __init__(self, *ops): - self.__ops = frozenset(ops) - self.__prop = 'prop value' + def __init__(self, *ops): + self.__ops = frozenset(ops) + self.__prop = 'prop value' - def __get_prop(self): - if 'get' not in self.__ops: - raise AttributeError('get prop') - return self.__prop + def __get_prop(self): + if 'get' not in self.__ops: + raise AttributeError('get prop') + return self.__prop - def __set_prop(self, value): - if 'set' not in self.__ops: - raise AttributeError('set prop') - self.__prop = value + def __set_prop(self, value): + if 'set' not in self.__ops: + raise AttributeError('set prop') + self.__prop = value - def __del_prop(self): - if 'del' not in self.__ops: - raise AttributeError('del prop') - self.__prop = None + def __del_prop(self): + if 'del' not in self.__ops: + raise AttributeError('del prop') + self.__prop = None - prop = property(__get_prop, __set_prop, __del_prop) + prop = property(__get_prop, __set_prop, __del_prop) def test_yes_raised(): - f = tstutil.raises + f = tstutil.raises - class SomeError(Exception): - pass + class SomeError(Exception): + pass - class AnotherError(Exception): - pass + class AnotherError(Exception): + pass - def callback1(): - 'raises correct exception' - raise SomeError() + def callback1(): + 'raises correct exception' + raise SomeError() - def callback2(): - 'raises wrong exception' - raise AnotherError() + def callback2(): + 'raises wrong exception' + raise AnotherError() - def callback3(): - 'raises no exception' + def callback3(): + 'raises no exception' - f(SomeError, callback1) + f(SomeError, callback1) - raised = False - try: - f(SomeError, callback2) - except AnotherError: - raised = True - assert raised + raised = False + try: + f(SomeError, callback2) + except AnotherError: + raised = True + assert raised - raised = False - try: - f(SomeError, callback3) - except tstutil.ExceptionNotRaised: - raised = True - assert raised + raised = False + try: + f(SomeError, callback3) + except tstutil.ExceptionNotRaised: + raised = True + assert raised def test_no_set(): - # Tests that it works when prop cannot be set: - tstutil.no_set(Prop('get', 'del'), 'prop') + # Tests that it works when prop cannot be set: + tstutil.no_set(Prop('get', 'del'), 'prop') - # Tests that ExceptionNotRaised is raised when prop *can* be set: - raised = False - try: - tstutil.no_set(Prop('set'), 'prop') - except tstutil.ExceptionNotRaised: - raised = True - assert raised + # Tests that ExceptionNotRaised is raised when prop *can* be set: + raised = False + try: + tstutil.no_set(Prop('set'), 'prop') + except tstutil.ExceptionNotRaised: + raised = True + assert raised def test_no_del(): - # Tests that it works when prop cannot be deleted: - tstutil.no_del(Prop('get', 'set'), 'prop') + # Tests that it works when prop cannot be deleted: + tstutil.no_del(Prop('get', 'set'), 'prop') - # Tests that ExceptionNotRaised is raised when prop *can* be set: - raised = False - try: - tstutil.no_del(Prop('del'), 'prop') - except tstutil.ExceptionNotRaised: - raised = True - assert raised + # Tests that ExceptionNotRaised is raised when prop *can* be set: + raised = False + try: + tstutil.no_del(Prop('del'), 'prop') + except tstutil.ExceptionNotRaised: + raised = True + assert raised def test_read_only(): - # Test that it works when prop is read only: - assert tstutil.read_only(Prop('get'), 'prop') == 'prop value' - - # Test that ExceptionNotRaised is raised when prop can be set: - raised = False - try: - tstutil.read_only(Prop('get', 'set'), 'prop') - except tstutil.ExceptionNotRaised: - raised = True - assert raised - - # Test that ExceptionNotRaised is raised when prop can be deleted: - raised = False - try: - tstutil.read_only(Prop('get', 'del'), 'prop') - except tstutil.ExceptionNotRaised: - raised = True - assert raised - - # Test that ExceptionNotRaised is raised when prop can be both set and - # deleted: - raised = False - try: - tstutil.read_only(Prop('get', 'del'), 'prop') - except tstutil.ExceptionNotRaised: - raised = True - assert raised - - # Test that AttributeError is raised when prop can't be read: - raised = False - try: - tstutil.read_only(Prop(), 'prop') - except AttributeError: - raised = True - assert raised + # Test that it works when prop is read only: + assert tstutil.read_only(Prop('get'), 'prop') == 'prop value' + + # Test that ExceptionNotRaised is raised when prop can be set: + raised = False + try: + tstutil.read_only(Prop('get', 'set'), 'prop') + except tstutil.ExceptionNotRaised: + raised = True + assert raised + + # Test that ExceptionNotRaised is raised when prop can be deleted: + raised = False + try: + tstutil.read_only(Prop('get', 'del'), 'prop') + except tstutil.ExceptionNotRaised: + raised = True + assert raised + + # Test that ExceptionNotRaised is raised when prop can be both set and + # deleted: + raised = False + try: + tstutil.read_only(Prop('get', 'del'), 'prop') + except tstutil.ExceptionNotRaised: + raised = True + assert raised + + # Test that AttributeError is raised when prop can't be read: + raised = False + try: + tstutil.read_only(Prop(), 'prop') + except AttributeError: + raised = True + assert raised diff --git a/ipalib/tests/tstutil.py b/ipalib/tests/tstutil.py index d2de3b86..e3411366 100644 --- a/ipalib/tests/tstutil.py +++ b/ipalib/tests/tstutil.py @@ -22,78 +22,78 @@ Utility functions for the unit tests. """ class ExceptionNotRaised(Exception): - """ - Exception raised when an *expected* exception is *not* raised during a - unit test. - """ - msg = 'expected %s' + """ + Exception raised when an *expected* exception is *not* raised during a + unit test. + """ + msg = 'expected %s' - def __init__(self, expected): - self.expected = expected + def __init__(self, expected): + self.expected = expected - def __str__(self): - return self.msg % self.expected.__name__ + def __str__(self): + return self.msg % self.expected.__name__ def raises(exception, callback, *args, **kw): - """ - Tests that the expected exception is raised; raises ExceptionNotRaised - if test fails. - """ - raised = False - try: - callback(*args, **kw) - except exception, e: - raised = True - if not raised: - raise ExceptionNotRaised(exception) - return e + """ + Tests that the expected exception is raised; raises ExceptionNotRaised + if test fails. + """ + raised = False + try: + callback(*args, **kw) + except exception, e: + raised = True + if not raised: + raise ExceptionNotRaised(exception) + return e def getitem(obj, key): - """ - Works like getattr but for dictionary interface. Uses this in combination - with raises() to test that, for example, KeyError is raised. - """ - return obj[key] + """ + Works like getattr but for dictionary interface. Uses this in combination + with raises() to test that, for example, KeyError is raised. + """ + return obj[key] def no_set(obj, name, value='some_new_obj'): - """ - Tests that attribute cannot be set. - """ - raises(AttributeError, setattr, obj, name, value) + """ + Tests that attribute cannot be set. + """ + raises(AttributeError, setattr, obj, name, value) def no_del(obj, name): - """ - Tests that attribute cannot be deleted. - """ - raises(AttributeError, delattr, obj, name) + """ + Tests that attribute cannot be deleted. + """ + raises(AttributeError, delattr, obj, name) def read_only(obj, name, value='some_new_obj'): - """ - Tests that attribute is read-only. Returns attribute. - """ - # Test that it cannot be set: - no_set(obj, name, value) + """ + Tests that attribute is read-only. Returns attribute. + """ + # Test that it cannot be set: + no_set(obj, name, value) - # Test that it cannot be deleted: - no_del(obj, name) + # Test that it cannot be deleted: + no_del(obj, name) - # Return the attribute - return getattr(obj, name) + # Return the attribute + return getattr(obj, name) def is_prop(prop): - return type(prop) is property + return type(prop) is property class ClassChecker(object): - def new(self, *args, **kw): - return self.cls(*args, **kw) + def new(self, *args, **kw): + return self.cls(*args, **kw) - def get_sub(self): - raise NotImplementedError('get_sub()') + def get_sub(self): + raise NotImplementedError('get_sub()') -- cgit From d171dc90111cad91884c3a1b3afdb8b16b7c289e Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 8 Aug 2008 19:53:45 +0000 Subject: 82: Cleaned up unit tests for public.option; added some doodles in plugable.Base --- ipalib/plugable.py | 17 +++++++++++ ipalib/public.py | 12 ++++++-- ipalib/tests/test_public.py | 72 +++++++++++++++++++++++++++++++-------------- 3 files changed, 77 insertions(+), 24 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index c3eb409b..4032b574 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -54,6 +54,23 @@ def check_identifier(name): raise errors.NameSpaceError(name, regex) +class Abstract(object): + __public__ = frozenset() + + @classmethod + def implements(cls, arg): + assert type(cls.__public__) is frozenset + if isinstance(arg, str): + return arg in cls.__public__ + if type(getattr(arg, '__public__', None)) is frozenset: + return cls.__public__.issuperset(arg.__public__) + if type(arg) is frozenset: + return cls.__public__.issuperset(arg) + raise TypeError( + "must be str, frozenset, or have frozenset '__public__' attribute" + ) + + class Plugin(object): """ Base class for all plugins. diff --git a/ipalib/public.py b/ipalib/public.py index 6f7f2154..1c6f9e7f 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -102,7 +102,7 @@ class option(object): def __rules_iter(self): """ Iterates through the attributes in this instance to retrieve the - methods implemented validation rules. + methods implementing validation rules. """ for name in dir(self.__class__): if name.startswith('_'): @@ -117,6 +117,10 @@ class option(object): """ Returns a default or auto-completed value for this option. If no default is available, this method should return None. + + All the keywords are passed so it's possible to build an + auto-completed value from other options values, e.g., build 'initials' + from 'givenname' + 'sn'. """ return None @@ -177,11 +181,15 @@ class cmd(plugable.Plugin): self.options.validate(value) def default(self, **kw): + d = {} for opt in self.options: if opt.name not in kw: value = opt.default(**kw) if value is not None: - kw[opt.name] = value + d[opt.name] = value + assert not set(kw).intersection(d) + kw.update(d) + return kw def __call__(self, **kw): (args, kw) = self.normalize(*args, **kw) diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index f396eed2..49fbb17f 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -63,13 +63,35 @@ def test_is_rule(): assert not is_rule(call(None)) -class test_option(): - def cls(self): - return public.option +class ClassChecker(object): + __cls = None + __subcls = None - def sub(self): + def __get_cls(self): + if self.__cls is None: + self.__cls = self._cls + return self.__cls + cls = property(__get_cls) + + def __get_subcls(self): + if self.__subcls is None: + self.__subcls = self.get_subcls() + return self.__subcls + subcls = property(__get_subcls) + + def get_subcls(self): + raise NotImplementedError( + self.__class__.__name__, + 'get_subcls()' + ) + + +class test_option(ClassChecker): + _cls = public.option + + def get_subcls(self): rule = public.rule - class int_opt(self.cls()): + class int_opt(self.cls): type = int @rule def rule_0(self, value): @@ -89,13 +111,12 @@ class test_option(): """ Perform some tests on the class (not an instance). """ - cls = self.cls() #assert issubclass(cls, plugable.ReadOnly) - assert type(cls.rules) is property + assert type(self.cls.rules) is property def test_normalize(self): - sub = self.sub() - i = sub() + assert 'normalize' in self.cls.__public__ + o = self.subcls() # Test with values that can't be converted: nope = ( '7.0' @@ -104,7 +125,7 @@ class test_option(): None, ) for val in nope: - e = raises(errors.NormalizationError, i.normalize, val) + e = raises(errors.NormalizationError, o.normalize, val) assert isinstance(e, errors.ValidationError) assert e.name == 'int_opt' assert e.value == val @@ -120,29 +141,36 @@ class test_option(): ' 7 ', ) for val in okay: - assert i.normalize(val) == 7 + assert o.normalize(val) == 7 + + def test_validate(self): + """ + Test the validate method. + """ + assert 'validate' in self.cls.__public__ + o = self.subcls() + o.validate(9) + for i in xrange(3): + e = raises(errors.RuleError, o.validate, i) + assert e.error == 'cannot be %d' % i + assert e.value == i def test_rules(self): """ Test the rules property. """ - o = self.sub()() + o = self.subcls() assert len(o.rules) == 3 def get_rule(i): return getattr(o, 'rule_%d' % i) rules = tuple(get_rule(i) for i in xrange(3)) assert o.rules == rules - def test_validation(self): - """ - Test the validation method. - """ - o = self.sub()() - o.validate(9) - for i in xrange(3): - e = raises(errors.RuleError, o.validate, i) - assert e.error == 'cannot be %d' % i - assert e.value == i + def test_default(self): + assert 'default' in self.cls.__public__ + assert self.cls().default() is None + + -- cgit From b0976a520858ccc1120c2cf5aabfc5e03a065ced Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 8 Aug 2008 20:39:34 +0000 Subject: 83: Added unit tests for plugable.Abstract --- ipalib/tests/test_plugable.py | 50 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) (limited to 'ipalib') diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index 232fbd71..ad1645f1 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -61,6 +61,56 @@ def test_valid_identifier(): raises(errors.NameSpaceError, f, name.upper()) +def test_Abstract(): + cls = plugable.Abstract + + class example(cls): + __public__ = frozenset(( + 'some_method', + 'some_property', + )) + + # Test using str: + assert example.implements('some_method') + assert not example.implements('another_method') + + # Test using frozenset: + assert example.implements(frozenset(['some_method'])) + assert not example.implements( + frozenset(['some_method', 'another_method']) + ) + + # Test using another object/class with __public__ frozenset: + assert example.implements(example) + assert example().implements(example) + assert example.implements(example()) + assert example().implements(example()) + + class subset(cls): + __public__ = frozenset(( + 'some_property', + )) + assert example.implements(subset) + assert not subset.implements(example) + + class superset(cls): + __public__ = frozenset(( + 'some_method', + 'some_property', + 'another_property', + )) + assert not example.implements(superset) + assert superset.implements(example) + + class any_object(object): + __public__ = frozenset(( + 'some_method', + 'some_property', + )) + assert example.implements(any_object) + assert example.implements(any_object()) + + def test_Plugin(): cls = plugable.Plugin assert type(cls.name) is property -- cgit From 6dc60a18c7929e3a1f0fee6aeb06913cc8921ccc Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 8 Aug 2008 20:55:08 +0000 Subject: 84: Renamed Proxy.__public to Proxy.__public__ so it works with Abstract.implements() --- ipalib/plugable.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 4032b574..ad3d5872 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -144,8 +144,8 @@ class Proxy(ReadOnly): '__base', '__target', '__name_attr', - '__public', 'name', + '__public__', ) def __init__(self, base, target, name_attr='name'): @@ -156,26 +156,26 @@ class Proxy(ReadOnly): object.__setattr__(self, '_Proxy__base', base) object.__setattr__(self, '_Proxy__target', target) object.__setattr__(self, '_Proxy__name_attr', name_attr) - object.__setattr__(self, '_Proxy__public', base.__public__) + object.__setattr__(self, '__public__', base.__public__) object.__setattr__(self, 'name', getattr(target, name_attr)) - # Check __public - assert type(self.__public) is frozenset + # Check __public__ + assert type(self.__public__) is frozenset # Check name check_identifier(self.name) def __iter__(self): - for name in sorted(self.__public): + for name in sorted(self.__public__): yield name def __getitem__(self, key): - if key in self.__public: + if key in self.__public__: return getattr(self.__target, key) raise KeyError('no proxy attribute %r' % key) def __getattr__(self, name): - if name in self.__public: + if name in self.__public__: return getattr(self.__target, name) raise AttributeError('no proxy attribute %r' % name) -- cgit From 58a3b1d0915f32326e9387bc4978c53a16e5f217 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 8 Aug 2008 21:28:56 +0000 Subject: 85: Added ReadOnly._lock() method to make class easier to use; updated subclasses and unit tests --- ipalib/plugable.py | 57 ++++++++++++++++++++++++------------------- ipalib/tests/test_plugable.py | 4 ++- 2 files changed, 35 insertions(+), 26 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index ad3d5872..e328f0c3 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -118,25 +118,33 @@ class ReadOnly(object): """ Base class for classes with read-only attributes. """ - __slots__ = tuple() + __locked = False + + def _lock(self): + assert self.__locked is False + self.__locked = True def __setattr__(self, name, value): """ - This raises an AttributeError anytime an attempt is made to set an - attribute. + Raises an AttributeError if ReadOnly._lock() has already been called; + otherwise calls object.__setattr__() """ - raise AttributeError('read-only: cannot set %s.%s' % - (self.__class__.__name__, name) - ) + if self.__locked: + raise AttributeError('read-only: cannot set %s.%s' % + (self.__class__.__name__, name) + ) + return object.__setattr__(self, name, value) def __delattr__(self, name): """ - This raises an AttributeError anytime an attempt is made to delete an - attribute. + Raises an AttributeError if ReadOnly._lock() has already been called; + otherwise calls object.__delattr__() """ - raise AttributeError('read-only: cannot del %s.%s' % - (self.__class__.__name__, name) - ) + if self.__locked: + raise AttributeError('read-only: cannot del %s.%s' % + (self.__class__.__name__, name) + ) + return object.__delattr__(self, name) class Proxy(ReadOnly): @@ -153,17 +161,15 @@ class Proxy(ReadOnly): raise TypeError('arg1 must be a class, got %r' % base) if not isinstance(target, base): raise ValueError('arg2 must be instance of arg1, got %r' % target) - object.__setattr__(self, '_Proxy__base', base) - object.__setattr__(self, '_Proxy__target', target) - object.__setattr__(self, '_Proxy__name_attr', name_attr) - object.__setattr__(self, '__public__', base.__public__) - object.__setattr__(self, 'name', getattr(target, name_attr)) - - # Check __public__ + self.__base = base + self.__target = target + self.__name_attr = name_attr + self.name = getattr(target, name_attr) + self.__public__ = base.__public__ assert type(self.__public__) is frozenset - - # Check name check_identifier(self.name) + self._lock() + def __iter__(self): for name in sorted(self.__public__): @@ -204,14 +210,15 @@ class NameSpace(ReadOnly): """ NameSpace """ - object.__setattr__(self, '_NameSpace__proxies', tuple(proxies)) - object.__setattr__(self, '_NameSpace__d', dict()) + self.__proxies = tuple(proxies) + self.__d = dict() for proxy in self.__proxies: assert isinstance(proxy, Proxy) assert proxy.name not in self.__d self.__d[proxy.name] = proxy assert not hasattr(self, proxy.name) - object.__setattr__(self, proxy.name, proxy) + setattr(self, proxy.name, proxy) + self._lock() def __iter__(self): """ @@ -338,8 +345,8 @@ class Registrar(object): class API(ReadOnly): def __init__(self, *allowed): keys = tuple(b.__name__ for b in allowed) - object.__setattr__(self, '_API__keys', keys) - object.__setattr__(self, 'register', Registrar(*allowed)) + self.register = Registrar(*allowed) + self._lock() def __call__(self): """ diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index ad1645f1..af6ee0c9 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -139,6 +139,7 @@ def test_Plugin(): def test_ReadOnly(): obj = plugable.ReadOnly() + obj._lock() names = ['not_an_attribute', 'an_attribute'] for name in names: no_set(obj, name) @@ -146,7 +147,8 @@ def test_ReadOnly(): class some_ro_class(plugable.ReadOnly): def __init__(self): - object.__setattr__(self, 'an_attribute', 'Hello world!') + self.an_attribute = 'Hello world!' + self._lock() obj = some_ro_class() for name in names: no_set(obj, name) -- cgit From fdfa827a36df87fd6b228fc1560576e268413104 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 8 Aug 2008 21:40:03 +0000 Subject: 86: Actually change *all* tab indentation to 4-space: 'sed s/\t/ /g' --- ipalib/errors.py | 66 +++---- ipalib/identity.py | 32 ++-- ipalib/plugable.py | 410 +++++++++++++++++++++--------------------- ipalib/plugins.py | 34 ++-- ipalib/public.py | 290 +++++++++++++++--------------- ipalib/tests/test_plugable.py | 240 ++++++++++++------------- ipalib/tests/test_public.py | 156 ++++++++-------- ipalib/tests/test_tstutil.py | 68 +++---- ipalib/tests/tstutil.py | 14 +- 9 files changed, 655 insertions(+), 655 deletions(-) (limited to 'ipalib') diff --git a/ipalib/errors.py b/ipalib/errors.py index ee0b931b..1fc0c90c 100644 --- a/ipalib/errors.py +++ b/ipalib/errors.py @@ -29,44 +29,44 @@ class IPAError(Exception): msg = None def __init__(self, *args, **kw): - self.args = args - self.kw = kw + self.args = args + self.kw = kw def __str__(self): - """ - Returns the string representation of this exception. - """ - if self.msg is None: - if len(self.args) == 1: - return unicode(self.args[0]) - return unicode(self.args) - if len(self.args) > 0: - return self.msg % self.args - return self.msg % self.kw + """ + Returns the string representation of this exception. + """ + if self.msg is None: + if len(self.args) == 1: + return unicode(self.args[0]) + return unicode(self.args) + if len(self.args) > 0: + return self.msg % self.args + return self.msg % self.kw class ValidationError(IPAError): msg = 'invalid %r value %r: %s' def __init__(self, name, value, error): - self.name = name - self.value = value - self.error = error - super(ValidationError, self).__init__(name, value, error) + self.name = name + self.value = value + self.error = error + super(ValidationError, self).__init__(name, value, error) class NormalizationError(ValidationError): def __init__(self, name, value, type): - self.type = type - super(NormalizationError, self).__init__(name, value, - 'not %r' % type - ) + self.type = type + super(NormalizationError, self).__init__(name, value, + 'not %r' % type + ) class RuleError(ValidationError): def __init__(self, name, value, rule, error): - self.rule = rule - super(RuleError, self).__init__(name, value, error) + self.rule = rule + super(RuleError, self).__init__(name, value, error) @@ -93,11 +93,11 @@ class SubclassError(RegistrationError): msg = 'plugin %r not subclass of any base in %r' def __init__(self, cls, allowed): - self.cls = cls - self.allowed = allowed + self.cls = cls + self.allowed = allowed def __str__(self): - return self.msg % (self.cls, self.allowed) + return self.msg % (self.cls, self.allowed) class DuplicateError(RegistrationError): @@ -108,10 +108,10 @@ class DuplicateError(RegistrationError): msg = '%r at %d was already registered' def __init__(self, cls): - self.cls = cls + self.cls = cls def __str__(self): - return self.msg % (self.cls, id(self.cls)) + return self.msg % (self.cls, id(self.cls)) class OverrideError(RegistrationError): @@ -122,11 +122,11 @@ class OverrideError(RegistrationError): msg = 'unexpected override of %s.%s with %r (use override=True if intended)' def __init__(self, base, cls): - self.base = base - self.cls = cls + self.base = base + self.cls = cls def __str__(self): - return self.msg % (self.base.__name__, self.cls.__name__, self.cls) + return self.msg % (self.base.__name__, self.cls.__name__, self.cls) class MissingOverrideError(RegistrationError): @@ -137,11 +137,11 @@ class MissingOverrideError(RegistrationError): msg = '%s.%s has not been registered, cannot override with %r' def __init__(self, base, cls): - self.base = base - self.cls = cls + self.base = base + self.cls = cls def __str__(self): - return self.msg % (self.base.__name__, self.cls.__name__, self.cls) + return self.msg % (self.base.__name__, self.cls.__name__, self.cls) diff --git a/ipalib/identity.py b/ipalib/identity.py index 1239816b..50642fec 100644 --- a/ipalib/identity.py +++ b/ipalib/identity.py @@ -6,49 +6,49 @@ def get_label(self, _): - return _('Title') # Enum? + return _('Title') # Enum? def get_label(self, _): - return _('First Name') + return _('First Name') def get_label(self, _): - return _('Last Name') + return _('Last Name') def get_label(self, _): - return _('Full Name') # Autofill + return _('Full Name') # Autofill def get_label(self, _): - return _('Display Name') # Autofill + return _('Display Name') # Autofill def get_label(self, _): - return _('Initials') # generated/ro? + return _('Initials') # generated/ro? def get_label(self, _): - return _('Account Status') # Enum (active, inactive) + return _('Account Status') # Enum (active, inactive) def get_label(self, _): - return _('Login') + return _('Login') def get_label(self, _): - return _('Password') + return _('Password') def get_label(self, _): # Same field as above, special interface - return _('Confirm Password') + return _('Confirm Password') def get_label(self, _): - return _('UID') #ro + return _('UID') #ro def get_label(self, _): - return _('GID') #ro + return _('GID') #ro def get_label(self, _): - return _('Home Directory') #ro + return _('Home Directory') #ro def get_label(self, _): - return _('Login Shell') + return _('Login Shell') def get_label(self, _): - return _('GECOS') + return _('GECOS') def get_label(self, _): - return _('') + return _('') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index e328f0c3..0a6a0caa 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -51,7 +51,7 @@ def check_identifier(name): """ regex = r'^[a-z][_a-z0-9]*[a-z0-9]$' if re.match(regex, name) is None: - raise errors.NameSpaceError(name, regex) + raise errors.NameSpaceError(name, regex) class Abstract(object): @@ -79,39 +79,39 @@ class Plugin(object): __api = None def __get_api(self): - """ - Returns the plugable.API instance passed to Plugin.finalize(), or - or returns None if finalize() has not yet been called. - """ - return self.__api + """ + Returns the plugable.API instance passed to Plugin.finalize(), or + or returns None if finalize() has not yet been called. + """ + return self.__api api = property(__get_api) def finalize(self, api): - """ - After all the plugins are instantiated, the plugable.API calls this - method, passing itself as the only argument. This is where plugins - should check that other plugins they depend upon have actually be - loaded. - """ - assert self.__api is None, 'finalize() can only be called once' - assert api is not None, 'finalize() argument cannot be None' - self.__api = api + """ + After all the plugins are instantiated, the plugable.API calls this + method, passing itself as the only argument. This is where plugins + should check that other plugins they depend upon have actually be + loaded. + """ + assert self.__api is None, 'finalize() can only be called once' + assert api is not None, 'finalize() argument cannot be None' + self.__api = api def __get_name(self): - """ - Returns the class name of this instance. - """ - return self.__class__.__name__ + """ + Returns the class name of this instance. + """ + return self.__class__.__name__ name = property(__get_name) def __repr__(self): - """ - Returns a fully qualified representation of the class. - """ - return '%s.%s()' % ( - self.__class__.__module__, - self.__class__.__name__ - ) + """ + Returns a fully qualified representation of the class. + """ + return '%s.%s()' % ( + self.__class__.__module__, + self.__class__.__name__ + ) class ReadOnly(object): @@ -125,79 +125,79 @@ class ReadOnly(object): self.__locked = True def __setattr__(self, name, value): - """ - Raises an AttributeError if ReadOnly._lock() has already been called; - otherwise calls object.__setattr__() - """ - if self.__locked: - raise AttributeError('read-only: cannot set %s.%s' % - (self.__class__.__name__, name) - ) - return object.__setattr__(self, name, value) + """ + Raises an AttributeError if ReadOnly._lock() has already been called; + otherwise calls object.__setattr__() + """ + if self.__locked: + raise AttributeError('read-only: cannot set %s.%s' % + (self.__class__.__name__, name) + ) + return object.__setattr__(self, name, value) def __delattr__(self, name): - """ - Raises an AttributeError if ReadOnly._lock() has already been called; - otherwise calls object.__delattr__() - """ - if self.__locked: - raise AttributeError('read-only: cannot del %s.%s' % - (self.__class__.__name__, name) - ) + """ + Raises an AttributeError if ReadOnly._lock() has already been called; + otherwise calls object.__delattr__() + """ + if self.__locked: + raise AttributeError('read-only: cannot del %s.%s' % + (self.__class__.__name__, name) + ) return object.__delattr__(self, name) class Proxy(ReadOnly): __slots__ = ( - '__base', - '__target', - '__name_attr', - 'name', - '__public__', + '__base', + '__target', + '__name_attr', + 'name', + '__public__', ) def __init__(self, base, target, name_attr='name'): - if not inspect.isclass(base): - raise TypeError('arg1 must be a class, got %r' % base) - if not isinstance(target, base): - raise ValueError('arg2 must be instance of arg1, got %r' % target) + if not inspect.isclass(base): + raise TypeError('arg1 must be a class, got %r' % base) + if not isinstance(target, base): + raise ValueError('arg2 must be instance of arg1, got %r' % target) self.__base = base - self.__target = target - self.__name_attr = name_attr - self.name = getattr(target, name_attr) - self.__public__ = base.__public__ - assert type(self.__public__) is frozenset - check_identifier(self.name) - self._lock() + self.__target = target + self.__name_attr = name_attr + self.name = getattr(target, name_attr) + self.__public__ = base.__public__ + assert type(self.__public__) is frozenset + check_identifier(self.name) + self._lock() def __iter__(self): - for name in sorted(self.__public__): - yield name + for name in sorted(self.__public__): + yield name def __getitem__(self, key): - if key in self.__public__: - return getattr(self.__target, key) - raise KeyError('no proxy attribute %r' % key) + if key in self.__public__: + return getattr(self.__target, key) + raise KeyError('no proxy attribute %r' % key) def __getattr__(self, name): - if name in self.__public__: - return getattr(self.__target, name) - raise AttributeError('no proxy attribute %r' % name) + if name in self.__public__: + return getattr(self.__target, name) + raise AttributeError('no proxy attribute %r' % name) def __call__(self, *args, **kw): - return self['__call__'](*args, **kw) + return self['__call__'](*args, **kw) def _clone(self, name_attr): - return self.__class__(self.__base, self.__target, name_attr) + return self.__class__(self.__base, self.__target, name_attr) def __repr__(self): - return '%s(%s, %r, %r)' % ( - self.__class__.__name__, - self.__base.__name__, - self.__target, - self.__name_attr, - ) + return '%s(%s, %r, %r)' % ( + self.__class__.__name__, + self.__base.__name__, + self.__target, + self.__name_attr, + ) class NameSpace(ReadOnly): @@ -207,167 +207,167 @@ class NameSpace(ReadOnly): """ def __init__(self, proxies): - """ - NameSpace - """ - self.__proxies = tuple(proxies) - self.__d = dict() - for proxy in self.__proxies: - assert isinstance(proxy, Proxy) - assert proxy.name not in self.__d - self.__d[proxy.name] = proxy - assert not hasattr(self, proxy.name) - setattr(self, proxy.name, proxy) - self._lock() + """ + NameSpace + """ + self.__proxies = tuple(proxies) + self.__d = dict() + for proxy in self.__proxies: + assert isinstance(proxy, Proxy) + assert proxy.name not in self.__d + self.__d[proxy.name] = proxy + assert not hasattr(self, proxy.name) + setattr(self, proxy.name, proxy) + self._lock() def __iter__(self): - """ - Iterates through the proxies in this NameSpace in the same order they - were passed in the contructor. - """ - for proxy in self.__proxies: - yield proxy + """ + Iterates through the proxies in this NameSpace in the same order they + were passed in the contructor. + """ + for proxy in self.__proxies: + yield proxy def __len__(self): - """ - Returns number of proxies in this NameSpace. - """ - return len(self.__proxies) + """ + Returns number of proxies in this NameSpace. + """ + return len(self.__proxies) def __contains__(self, key): - """ - Returns True if a proxy named `key` is in this NameSpace. - """ - return key in self.__d + """ + Returns True if a proxy named `key` is in this NameSpace. + """ + return key in self.__d def __getitem__(self, key): - """ - Returns proxy named `key`; otherwise raises KeyError. - """ - if key in self.__d: - return self.__d[key] - raise KeyError('NameSpace has no item for key %r' % key) + """ + Returns proxy named `key`; otherwise raises KeyError. + """ + if key in self.__d: + return self.__d[key] + raise KeyError('NameSpace has no item for key %r' % key) def __repr__(self): - return '%s(<%d proxies>)' % (self.__class__.__name__, len(self)) + return '%s(<%d proxies>)' % (self.__class__.__name__, len(self)) class Registrar(object): def __init__(self, *allowed): - """ - `*allowed` is a list of the base classes plugins can be subclassed - from. - """ - self.__allowed = frozenset(allowed) - self.__d = {} - self.__registered = set() - assert len(self.__allowed) == len(allowed) - for base in self.__allowed: - assert inspect.isclass(base) - assert base.__name__ not in self.__d - self.__d[base.__name__] = {} + """ + `*allowed` is a list of the base classes plugins can be subclassed + from. + """ + self.__allowed = frozenset(allowed) + self.__d = {} + self.__registered = set() + assert len(self.__allowed) == len(allowed) + for base in self.__allowed: + assert inspect.isclass(base) + assert base.__name__ not in self.__d + self.__d[base.__name__] = {} def __findbase(self, cls): - """ - If `cls` is a subclass of a base in self.__allowed, returns that - base; otherwise raises SubclassError. - """ - assert inspect.isclass(cls) - found = False - for base in self.__allowed: - if issubclass(cls, base): - found = True - yield base - if not found: - raise errors.SubclassError(cls, self.__allowed) + """ + If `cls` is a subclass of a base in self.__allowed, returns that + base; otherwise raises SubclassError. + """ + assert inspect.isclass(cls) + found = False + for base in self.__allowed: + if issubclass(cls, base): + found = True + yield base + if not found: + raise errors.SubclassError(cls, self.__allowed) def __call__(self, cls, override=False): - """ - Register the plugin `cls`. - """ - if not inspect.isclass(cls): - raise TypeError('plugin must be a class: %r' % cls) - - # Raise DuplicateError if this exact class was already registered: - if cls in self.__registered: - raise errors.DuplicateError(cls) - - # Find the base class or raise SubclassError: - for base in self.__findbase(cls): - sub_d = self.__d[base.__name__] - - # Check override: - if cls.__name__ in sub_d: - # Must use override=True to override: - if not override: - raise errors.OverrideError(base, cls) - else: - # There was nothing already registered to override: - if override: - raise errors.MissingOverrideError(base, cls) - - # The plugin is okay, add to sub_d: - sub_d[cls.__name__] = cls - - # The plugin is okay, add to __registered: - self.__registered.add(cls) + """ + Register the plugin `cls`. + """ + if not inspect.isclass(cls): + raise TypeError('plugin must be a class: %r' % cls) + + # Raise DuplicateError if this exact class was already registered: + if cls in self.__registered: + raise errors.DuplicateError(cls) + + # Find the base class or raise SubclassError: + for base in self.__findbase(cls): + sub_d = self.__d[base.__name__] + + # Check override: + if cls.__name__ in sub_d: + # Must use override=True to override: + if not override: + raise errors.OverrideError(base, cls) + else: + # There was nothing already registered to override: + if override: + raise errors.MissingOverrideError(base, cls) + + # The plugin is okay, add to sub_d: + sub_d[cls.__name__] = cls + + # The plugin is okay, add to __registered: + self.__registered.add(cls) def __getitem__(self, item): - """ - Returns a copy of the namespace dict of the base class named `name`. - """ - if inspect.isclass(item): - if item not in self.__allowed: - raise KeyError(repr(item)) - key = item.__name__ - else: - key = item - return dict(self.__d[key]) + """ + Returns a copy of the namespace dict of the base class named `name`. + """ + if inspect.isclass(item): + if item not in self.__allowed: + raise KeyError(repr(item)) + key = item.__name__ + else: + key = item + return dict(self.__d[key]) def __contains__(self, item): - """ - Returns True if a base class named `name` is in this Registrar. - """ - if inspect.isclass(item): - return item in self.__allowed - return item in self.__d + """ + Returns True if a base class named `name` is in this Registrar. + """ + if inspect.isclass(item): + return item in self.__allowed + return item in self.__d def __iter__(self): - """ - Iterates through a (base, registered_plugins) tuple for each allowed - base. - """ - for base in self.__allowed: - sub_d = self.__d[base.__name__] - yield (base, tuple(sub_d[k] for k in sorted(sub_d))) + """ + Iterates through a (base, registered_plugins) tuple for each allowed + base. + """ + for base in self.__allowed: + sub_d = self.__d[base.__name__] + yield (base, tuple(sub_d[k] for k in sorted(sub_d))) class API(ReadOnly): def __init__(self, *allowed): - keys = tuple(b.__name__ for b in allowed) + keys = tuple(b.__name__ for b in allowed) self.register = Registrar(*allowed) self._lock() def __call__(self): - """ - Finalize the registration, instantiate the plugins. - """ - d = {} - def plugin_iter(base, classes): - for cls in classes: - if cls not in d: - d[cls] = cls() - plugin = d[cls] - yield Proxy(base, plugin) - - for (base, classes) in self.register: - ns = NameSpace(plugin_iter(base, classes)) - assert not hasattr(self, base.__name__) - object.__setattr__(self, base.__name__, ns) - for plugin in d.values(): - plugin.finalize(self) - assert plugin.api is self + """ + Finalize the registration, instantiate the plugins. + """ + d = {} + def plugin_iter(base, classes): + for cls in classes: + if cls not in d: + d[cls] = cls() + plugin = d[cls] + yield Proxy(base, plugin) + + for (base, classes) in self.register: + ns = NameSpace(plugin_iter(base, classes)) + assert not hasattr(self, base.__name__) + object.__setattr__(self, base.__name__, ns) + for plugin in d.values(): + plugin.finalize(self) + assert plugin.api is self def __iter__(self): - for key in self.__keys: - yield key + for key in self.__keys: + yield key diff --git a/ipalib/plugins.py b/ipalib/plugins.py index cf1eaf8d..90bc184f 100644 --- a/ipalib/plugins.py +++ b/ipalib/plugins.py @@ -28,34 +28,34 @@ from run import api # Hypothetical functional commands (not associated with any object): class krbtest(public.cmd): def get_doc(self, _): - return _('test your Kerberos ticket') + return _('test your Kerberos ticket') api.register(krbtest) class discover(public.cmd): def get_doc(self, _): - return _('discover IPA servers on network') + return _('discover IPA servers on network') api.register(discover) # Register some methods for the 'user' object: class user_add(public.mthd): def get_doc(self, _): - return _('add new user') + return _('add new user') api.register(user_add) class user_del(public.mthd): def get_doc(self, _): - return _('delete existing user') + return _('delete existing user') api.register(user_del) class user_mod(public.mthd): def get_doc(self, _): - return _('edit existing user') + return _('edit existing user') api.register(user_mod) class user_find(public.mthd): def get_doc(self, _): - return _('search for users') + return _('search for users') api.register(user_find) @@ -76,44 +76,44 @@ api.register(user_login) # Register some methods for the 'group' object: class group_add(public.mthd): def get_doc(self, _): - return _('add new group') + return _('add new group') api.register(group_add) class group_del(public.mthd): def get_doc(self, _): - return _('delete existing group') + return _('delete existing group') api.register(group_del) class group_mod(public.mthd): def get_doc(self, _): - return _('edit existing group') + return _('edit existing group') api.register(group_mod) class group_find(public.mthd): def get_doc(self, _): - return _('search for groups') + return _('search for groups') api.register(group_find) # Register some methods for the 'service' object class service_add(public.mthd): def get_doc(self, _): - return _('add new service') + return _('add new service') api.register(service_add) class service_del(public.mthd): def get_doc(self, _): - return _('delete existing service') + return _('delete existing service') api.register(service_del) class service_mod(public.mthd): def get_doc(self, _): - return _('edit existing service') + return _('edit existing service') api.register(service_mod) class service_find(public.mthd): def get_doc(self, _): - return _('search for services') + return _('search for services') api.register(service_find) @@ -121,15 +121,15 @@ api.register(service_find) # we'll register the objects last: class group(public.obj): def get_doc(self, _): - return _('') + return _('') api.register(group) class service(public.obj): def get_doc(self, _): - return _('') + return _('') api.register(service) class user(public.obj): def get_doc(self, _): - return _('') + return _('') api.register(user) diff --git a/ipalib/public.py b/ipalib/public.py index 1c6f9e7f..7bce4992 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -44,249 +44,249 @@ class option(object): """ __public__ = frozenset(( - 'normalize', - 'validate', - 'default', - 'required', - 'type', + 'normalize', + 'validate', + 'default', + 'required', + 'type', )) __rules = None # type = unicode, int, float # Set in subclass def normalize(self, value): - """ - Returns the normalized form of `value`. If `value` cannot be - normalized, NormalizationError is raised, which is a subclass of - ValidationError. - - The base class implementation only does type coercion, but subclasses - might do other normalization (e.g., a unicode option might strip - leading and trailing white-space). - """ - try: - return self.type(value) - except (TypeError, ValueError): - raise errors.NormalizationError( - self.__class__.__name__, value, self.type - ) + """ + Returns the normalized form of `value`. If `value` cannot be + normalized, NormalizationError is raised, which is a subclass of + ValidationError. + + The base class implementation only does type coercion, but subclasses + might do other normalization (e.g., a unicode option might strip + leading and trailing white-space). + """ + try: + return self.type(value) + except (TypeError, ValueError): + raise errors.NormalizationError( + self.__class__.__name__, value, self.type + ) def validate(self, value): - """ - Calls each validation rule and if any rule fails, raises RuleError, - which is a subclass of ValidationError. - """ - for rule in self.rules: - msg = rule(value) - if msg is not None: - raise errors.RuleError( - self.__class__.__name__, - value, - rule, - msg, - ) + """ + Calls each validation rule and if any rule fails, raises RuleError, + which is a subclass of ValidationError. + """ + for rule in self.rules: + msg = rule(value) + if msg is not None: + raise errors.RuleError( + self.__class__.__name__, + value, + rule, + msg, + ) def __get_rules(self): - """ - Returns the tuple of rule methods used for input validation. This - tuple is lazily initialized the first time the property is accessed. - """ - if self.__rules is None: - self.__rules = tuple(sorted( - self.__rules_iter(), - key=lambda f: getattr(f, '__name__'), - )) - return self.__rules + """ + Returns the tuple of rule methods used for input validation. This + tuple is lazily initialized the first time the property is accessed. + """ + if self.__rules is None: + self.__rules = tuple(sorted( + self.__rules_iter(), + key=lambda f: getattr(f, '__name__'), + )) + return self.__rules rules = property(__get_rules) def __rules_iter(self): - """ - Iterates through the attributes in this instance to retrieve the - methods implementing validation rules. - """ - for name in dir(self.__class__): - if name.startswith('_'): - continue - base_attr = getattr(self.__class__, name) - if is_rule(base_attr): - attr = getattr(self, name) - if is_rule(attr): - yield attr + """ + Iterates through the attributes in this instance to retrieve the + methods implementing validation rules. + """ + for name in dir(self.__class__): + if name.startswith('_'): + continue + base_attr = getattr(self.__class__, name) + if is_rule(base_attr): + attr = getattr(self, name) + if is_rule(attr): + yield attr def default(self, **kw): - """ - Returns a default or auto-completed value for this option. If no - default is available, this method should return None. + """ + Returns a default or auto-completed value for this option. If no + default is available, this method should return None. - All the keywords are passed so it's possible to build an - auto-completed value from other options values, e.g., build 'initials' - from 'givenname' + 'sn'. - """ - return None + All the keywords are passed so it's possible to build an + auto-completed value from other options values, e.g., build 'initials' + from 'givenname' + 'sn'. + """ + return None class cmd(plugable.Plugin): __public__ = frozenset(( - 'normalize', - 'autofill', - '__call__', - 'get_doc', - 'opt', + 'normalize', + 'autofill', + '__call__', + 'get_doc', + 'opt', )) __opt = None def get_doc(self, _): - """ - Returns the gettext translated doc-string for this command. + """ + Returns the gettext translated doc-string for this command. - For example: + For example: - >>> def get_doc(self, _): - >>> return _('add new user') - """ - raise NotImplementedError('%s.get_doc()' % self.name) + >>> def get_doc(self, _): + >>> return _('add new user') + """ + raise NotImplementedError('%s.get_doc()' % self.name) def get_options(self): - """ - Returns iterable with opt_proxy objects used to create the opt - NameSpace when __get_opt() is called. - """ - raise NotImplementedError('%s.get_options()' % self.name) + """ + Returns iterable with opt_proxy objects used to create the opt + NameSpace when __get_opt() is called. + """ + raise NotImplementedError('%s.get_options()' % self.name) def __get_opt(self): - """ - Returns the NameSpace containing opt_proxy objects. - """ - if self.__opt is None: - self.__opt = plugable.NameSpace(self.get_options()) - return self.__opt + """ + Returns the NameSpace containing opt_proxy objects. + """ + if self.__opt is None: + self.__opt = plugable.NameSpace(self.get_options()) + return self.__opt opt = property(__get_opt) def normalize_iter(self, kw): - for (key, value) in kw.items(): - if key in self.options: - yield ( - key, self.options[key].normalize(value) - ) - else: - yield (key, value) + for (key, value) in kw.items(): + if key in self.options: + yield ( + key, self.options[key].normalize(value) + ) + else: + yield (key, value) def normalize(self, **kw): - return dict(self.normalize_iter(kw)) + return dict(self.normalize_iter(kw)) def validate(self, **kw): - for (key, value) in kw.items(): - if key in self.options: - self.options.validate(value) + for (key, value) in kw.items(): + if key in self.options: + self.options.validate(value) def default(self, **kw): d = {} - for opt in self.options: - if opt.name not in kw: - value = opt.default(**kw) - if value is not None: - d[opt.name] = value - assert not set(kw).intersection(d) - kw.update(d) - return kw + for opt in self.options: + if opt.name not in kw: + value = opt.default(**kw) + if value is not None: + d[opt.name] = value + assert not set(kw).intersection(d) + kw.update(d) + return kw def __call__(self, **kw): - (args, kw) = self.normalize(*args, **kw) - (args, kw) = self.autofill(*args, **kw) - self.validate(*args, **kw) - self.execute(*args, **kw) + (args, kw) = self.normalize(*args, **kw) + (args, kw) = self.autofill(*args, **kw) + self.validate(*args, **kw) + self.execute(*args, **kw) class obj(plugable.Plugin): __public__ = frozenset(( - 'mthd', - 'prop', + 'mthd', + 'prop', )) __mthd = None __prop = None def __get_mthd(self): - return self.__mthd + return self.__mthd mthd = property(__get_mthd) def __get_prop(self): - return self.__prop + return self.__prop prop = property(__get_prop) def finalize(self, api): - super(obj, self).finalize(api) - self.__mthd = self.__create_ns('mthd') - self.__prop = self.__create_ns('prop') + super(obj, self).finalize(api) + self.__mthd = self.__create_ns('mthd') + self.__prop = self.__create_ns('prop') def __create_ns(self, name): - return plugable.NameSpace(self.__filter(name)) + return plugable.NameSpace(self.__filter(name)) def __filter(self, name): - for i in getattr(self.api, name): - if i.obj_name == self.name: - yield i._clone('attr_name') + for i in getattr(self.api, name): + if i.obj_name == self.name: + yield i._clone('attr_name') class attr(plugable.Plugin): __obj = None def __init__(self): - m = re.match('^([a-z]+)_([a-z]+)$', self.__class__.__name__) - assert m - self.__obj_name = m.group(1) - self.__attr_name = m.group(2) + m = re.match('^([a-z]+)_([a-z]+)$', self.__class__.__name__) + assert m + self.__obj_name = m.group(1) + self.__attr_name = m.group(2) def __get_obj_name(self): - return self.__obj_name + return self.__obj_name obj_name = property(__get_obj_name) def __get_attr_name(self): - return self.__attr_name + return self.__attr_name attr_name = property(__get_attr_name) def __get_obj(self): - """ - Returns the obj instance this attribute is associated with, or None - if no association has been set. - """ - return self.__obj + """ + Returns the obj instance this attribute is associated with, or None + if no association has been set. + """ + return self.__obj obj = property(__get_obj) def finalize(self, api): - super(attr, self).finalize(api) - self.__obj = api.obj[self.obj_name] + super(attr, self).finalize(api) + self.__obj = api.obj[self.obj_name] class mthd(attr, cmd): __public__ = frozenset(( - 'obj', - 'obj_name', + 'obj', + 'obj_name', )) class prop(attr): __public__ = frozenset(( - 'obj', - 'obj_name', + 'obj', + 'obj_name', )) def get_doc(self, _): - return _('prop doc') + return _('prop doc') class PublicAPI(plugable.API): __max_cmd_len = None def __init__(self): - super(PublicAPI, self).__init__(cmd, obj, mthd, prop) + super(PublicAPI, self).__init__(cmd, obj, mthd, prop) def __get_max_cmd_len(self): - if self.__max_cmd_len is None: - if not hasattr(self, 'cmd'): - return None - max_cmd_len = max(len(str(cmd)) for cmd in self.cmd) - object.__setattr__(self, '_PublicAPI__max_cmd_len', max_cmd_len) - return self.__max_cmd_len + if self.__max_cmd_len is None: + if not hasattr(self, 'cmd'): + return None + max_cmd_len = max(len(str(cmd)) for cmd in self.cmd) + object.__setattr__(self, '_PublicAPI__max_cmd_len', max_cmd_len) + return self.__max_cmd_len max_cmd_len = property(__get_max_cmd_len) diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index af6ee0c9..6a1d8a60 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -40,25 +40,25 @@ def test_from_cli(): def test_valid_identifier(): f = plugable.check_identifier okay = [ - 'user_add', - 'stuff2junk', - 'sixty9', + 'user_add', + 'stuff2junk', + 'sixty9', ] nope = [ - '_user_add', - '__user_add', - 'user_add_', - 'user_add__', - '_user_add_', - '__user_add__', - '60nine', + '_user_add', + '__user_add', + 'user_add_', + 'user_add__', + '_user_add_', + '__user_add__', + '60nine', ] for name in okay: - f(name) + f(name) for name in nope: - raises(errors.NameSpaceError, f, name) + raises(errors.NameSpaceError, f, name) for name in okay: - raises(errors.NameSpaceError, f, name.upper()) + raises(errors.NameSpaceError, f, name.upper()) def test_Abstract(): @@ -126,7 +126,7 @@ def test_Plugin(): raises(AssertionError, p.finalize, api) class some_plugin(plugable.Plugin): - pass + pass p = some_plugin() assert read_only(p, 'name') == 'some_plugin' assert repr(p) == '%s.some_plugin()' % __name__ @@ -142,17 +142,17 @@ def test_ReadOnly(): obj._lock() names = ['not_an_attribute', 'an_attribute'] for name in names: - no_set(obj, name) - no_del(obj, name) + no_set(obj, name) + no_del(obj, name) class some_ro_class(plugable.ReadOnly): - def __init__(self): - self.an_attribute = 'Hello world!' - self._lock() + def __init__(self): + self.an_attribute = 'Hello world!' + self._lock() obj = some_ro_class() for name in names: - no_set(obj, name) - no_del(obj, name) + no_set(obj, name) + no_del(obj, name) assert read_only(obj, 'an_attribute') == 'Hello world!' @@ -162,30 +162,30 @@ def test_Proxy(): # Setup: class base(object): - __public__ = frozenset(( - 'public_0', - 'public_1', - '__call__', - )) + __public__ = frozenset(( + 'public_0', + 'public_1', + '__call__', + )) - def public_0(self): - return 'public_0' + def public_0(self): + return 'public_0' - def public_1(self): - return 'public_1' + def public_1(self): + return 'public_1' - def __call__(self, caller): - return 'ya called it, %s.' % caller + def __call__(self, caller): + return 'ya called it, %s.' % caller - def private_0(self): - return 'private_0' + def private_0(self): + return 'private_0' - def private_1(self): - return 'private_1' + def private_1(self): + return 'private_1' class plugin(base): - name = 'user_add' - attr_name = 'add' + name = 'user_add' + attr_name = 'add' # Test that TypeError is raised when base is not a class: raises(TypeError, cls, base(), None) @@ -201,13 +201,13 @@ def test_Proxy(): # Test normal methods: for n in xrange(2): - pub = 'public_%d' % n - priv = 'private_%d' % n - assert getattr(i, pub)() == pub - assert getattr(p, pub)() == pub - assert hasattr(p, pub) - assert getattr(i, priv)() == priv - assert not hasattr(p, priv) + pub = 'public_%d' % n + priv = 'private_%d' % n + assert getattr(i, pub)() == pub + assert getattr(p, pub)() == pub + assert hasattr(p, pub) + assert getattr(i, priv)() == priv + assert not hasattr(p, priv) # Test __call__: value = 'ya called it, dude.' @@ -236,23 +236,23 @@ def test_NameSpace(): assert issubclass(cls, plugable.ReadOnly) class base(object): - __public__ = frozenset(( - 'plusplus', - )) + __public__ = frozenset(( + 'plusplus', + )) - def plusplus(self, n): - return n + 1 + def plusplus(self, n): + return n + 1 class plugin(base): - def __init__(self, name): - self.name = name + def __init__(self, name): + self.name = name def get_name(i): - return 'noun_verb%d' % i + return 'noun_verb%d' % i def get_proxies(n): - for i in xrange(n): - yield plugable.Proxy(base, plugin(get_name(i))) + for i in xrange(n): + yield plugable.Proxy(base, plugin(get_name(i))) cnt = 20 ns = cls(get_proxies(cnt)) @@ -263,20 +263,20 @@ def test_NameSpace(): # Test __iter__ i = None for (i, proxy) in enumerate(ns): - assert type(proxy) is plugable.Proxy - assert proxy.name == get_name(i) + assert type(proxy) is plugable.Proxy + assert proxy.name == get_name(i) assert i == cnt - 1 # Test __contains__, __getitem__, getattr(): proxies = frozenset(ns) for i in xrange(cnt): - name = get_name(i) - assert name in ns - proxy = ns[name] - assert proxy.name == name - assert type(proxy) is plugable.Proxy - assert proxy in proxies - assert read_only(ns, name) is proxy + name = get_name(i) + assert name in ns + proxy = ns[name] + assert proxy.name == name + assert type(proxy) is plugable.Proxy + assert proxy in proxies + assert read_only(ns, name) is proxy # Test dir(): assert set(get_name(i) for i in xrange(cnt)).issubset(set(dir(ns))) @@ -291,27 +291,27 @@ def test_NameSpace(): def test_Registrar(): class Base1(object): - pass + pass class Base2(object): - pass + pass class Base3(object): - pass + pass class plugin1(Base1): - pass + pass class plugin2(Base2): - pass + pass class plugin3(Base3): - pass + pass # Test creation of Registrar: r = plugable.Registrar(Base1, Base2) # Test __hasitem__, __getitem__: for base in [Base1, Base2]: - assert base in r - assert base.__name__ in r - assert r[base] == {} - assert r[base.__name__] == {} + assert base in r + assert base.__name__ in r + assert r[base] == {} + assert r[base.__name__] == {} # Check that TypeError is raised trying to register something that isn't @@ -339,9 +339,9 @@ def test_Registrar(): # name and same base: orig1 = plugin1 class base1_extended(Base1): - pass + pass class plugin1(base1_extended): - pass + pass raises(errors.OverrideError, r, plugin1) # Check that overriding works @@ -364,40 +364,40 @@ def test_Registrar(): # Setup to test __iter__: class plugin1a(Base1): - pass + pass r(plugin1a) class plugin1b(Base1): - pass + pass r(plugin1b) class plugin2a(Base2): - pass + pass r(plugin2a) class plugin2b(Base2): - pass + pass r(plugin2b) m = { - 'Base1': set([plugin1, plugin1a, plugin1b]), - 'Base2': set([plugin2, plugin2a, plugin2b]), + 'Base1': set([plugin1, plugin1a, plugin1b]), + 'Base2': set([plugin2, plugin2a, plugin2b]), } # Now test __iter__: for (base, plugins) in r: - assert base in [Base1, Base2] - assert set(plugins) == m[base.__name__] + assert base in [Base1, Base2] + assert set(plugins) == m[base.__name__] assert len(list(r)) == 2 # Again test __hasitem__, __getitem__: for base in [Base1, Base2]: - assert base in r - assert base.__name__ in r - d = dict((p.__name__, p) for p in m[base.__name__]) - assert len(d) == 3 - assert r[base] == d - assert r[base.__name__] == d + assert base in r + assert base.__name__ in r + d = dict((p.__name__, p) for p in m[base.__name__]) + assert len(d) == 3 + assert r[base] == d + assert r[base.__name__] == d def test_API(): @@ -405,20 +405,20 @@ def test_API(): # Setup the test bases, create the API: class base0(plugable.Plugin): - __public__ = frozenset(( - 'method', - )) + __public__ = frozenset(( + 'method', + )) - def method(self, n): - return n + def method(self, n): + return n class base1(plugable.Plugin): - __public__ = frozenset(( - 'method', - )) + __public__ = frozenset(( + 'method', + )) - def method(self, n): - return n + 1 + def method(self, n): + return n + 1 api = plugable.API(base0, base1) r = api.register @@ -426,48 +426,48 @@ def test_API(): assert read_only(api, 'register') is r class base0_plugin0(base0): - pass + pass r(base0_plugin0) class base0_plugin1(base0): - pass + pass r(base0_plugin1) class base0_plugin2(base0): - pass + pass r(base0_plugin2) class base1_plugin0(base1): - pass + pass r(base1_plugin0) class base1_plugin1(base1): - pass + pass r(base1_plugin1) class base1_plugin2(base1): - pass + pass r(base1_plugin2) # Test API instance: api() # Calling instance performs finalization def get_base(b): - return 'base%d' % b + return 'base%d' % b def get_plugin(b, p): - return 'base%d_plugin%d' % (b, p) + return 'base%d_plugin%d' % (b, p) for b in xrange(2): - base_name = get_base(b) - ns = getattr(api, base_name) - assert isinstance(ns, plugable.NameSpace) - assert read_only(api, base_name) is ns - assert len(ns) == 3 - for p in xrange(3): - plugin_name = get_plugin(b, p) - proxy = ns[plugin_name] - assert isinstance(proxy, plugable.Proxy) - assert proxy.name == plugin_name - assert read_only(ns, plugin_name) is proxy - assert read_only(proxy, 'method')(7) == 7 + b + base_name = get_base(b) + ns = getattr(api, base_name) + assert isinstance(ns, plugable.NameSpace) + assert read_only(api, base_name) is ns + assert len(ns) == 3 + for p in xrange(3): + plugin_name = get_plugin(b, p) + proxy = ns[plugin_name] + assert isinstance(proxy, plugable.Proxy) + assert proxy.name == plugin_name + assert read_only(ns, plugin_name) is proxy + assert read_only(proxy, 'method')(7) == 7 + b diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index 49fbb17f..6bff9d89 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -33,13 +33,13 @@ def test_rule(): flag = public.RULE_FLAG rule = public.rule def my_func(): - pass + pass assert not hasattr(my_func, flag) rule(my_func) assert getattr(my_func, flag) is True @rule def my_func2(): - pass + pass assert getattr(my_func2, flag) is True @@ -48,14 +48,14 @@ def test_is_rule(): flag = public.RULE_FLAG class no_call(object): - def __init__(self, value): - if value is not None: - assert value in (True, False) - setattr(self, flag, value) + def __init__(self, value): + if value is not None: + assert value in (True, False) + setattr(self, flag, value) class call(no_call): - def __call__(self): - pass + def __call__(self): + pass assert is_rule(call(True)) assert not is_rule(no_call(True)) @@ -90,81 +90,81 @@ class test_option(ClassChecker): _cls = public.option def get_subcls(self): - rule = public.rule - class int_opt(self.cls): - type = int - @rule - def rule_0(self, value): - if value == 0: - return 'cannot be 0' - @rule - def rule_1(self, value): - if value == 1: - return 'cannot be 1' - @rule - def rule_2(self, value): - if value == 2: - return 'cannot be 2' - return int_opt + rule = public.rule + class int_opt(self.cls): + type = int + @rule + def rule_0(self, value): + if value == 0: + return 'cannot be 0' + @rule + def rule_1(self, value): + if value == 1: + return 'cannot be 1' + @rule + def rule_2(self, value): + if value == 2: + return 'cannot be 2' + return int_opt def test_class(self): - """ - Perform some tests on the class (not an instance). - """ - #assert issubclass(cls, plugable.ReadOnly) - assert type(self.cls.rules) is property + """ + Perform some tests on the class (not an instance). + """ + #assert issubclass(cls, plugable.ReadOnly) + assert type(self.cls.rules) is property def test_normalize(self): assert 'normalize' in self.cls.__public__ - o = self.subcls() - # Test with values that can't be converted: - nope = ( - '7.0' - 'whatever', - object, - None, - ) - for val in nope: - e = raises(errors.NormalizationError, o.normalize, val) - assert isinstance(e, errors.ValidationError) - assert e.name == 'int_opt' - assert e.value == val - assert e.error == "not " - assert e.type is int - # Test with values that can be converted: - okay = ( - 7, - 7.0, - 7.2, - 7L, - '7', - ' 7 ', - ) - for val in okay: - assert o.normalize(val) == 7 + o = self.subcls() + # Test with values that can't be converted: + nope = ( + '7.0' + 'whatever', + object, + None, + ) + for val in nope: + e = raises(errors.NormalizationError, o.normalize, val) + assert isinstance(e, errors.ValidationError) + assert e.name == 'int_opt' + assert e.value == val + assert e.error == "not " + assert e.type is int + # Test with values that can be converted: + okay = ( + 7, + 7.0, + 7.2, + 7L, + '7', + ' 7 ', + ) + for val in okay: + assert o.normalize(val) == 7 def test_validate(self): - """ - Test the validate method. - """ - assert 'validate' in self.cls.__public__ - o = self.subcls() - o.validate(9) - for i in xrange(3): - e = raises(errors.RuleError, o.validate, i) - assert e.error == 'cannot be %d' % i - assert e.value == i + """ + Test the validate method. + """ + assert 'validate' in self.cls.__public__ + o = self.subcls() + o.validate(9) + for i in xrange(3): + e = raises(errors.RuleError, o.validate, i) + assert e.error == 'cannot be %d' % i + assert e.value == i def test_rules(self): - """ - Test the rules property. - """ - o = self.subcls() - assert len(o.rules) == 3 - def get_rule(i): - return getattr(o, 'rule_%d' % i) - rules = tuple(get_rule(i) for i in xrange(3)) - assert o.rules == rules + """ + Test the rules property. + """ + o = self.subcls() + assert len(o.rules) == 3 + def get_rule(i): + return getattr(o, 'rule_%d' % i) + rules = tuple(get_rule(i) for i in xrange(3)) + assert o.rules == rules def test_default(self): assert 'default' in self.cls.__public__ @@ -190,10 +190,10 @@ def test_attr(): assert issubclass(cls, plugable.Plugin) class api(object): - obj = dict(user='the user obj') + obj = dict(user='the user obj') class user_add(cls): - pass + pass i = user_add() assert read_only(i, 'obj_name') == 'user' @@ -222,11 +222,11 @@ def test_PublicAPI(): api = cls() class cmd1(public.cmd): - pass + pass api.register(cmd1) class cmd2(public.cmd): - pass + pass api.register(cmd2) api() diff --git a/ipalib/tests/test_tstutil.py b/ipalib/tests/test_tstutil.py index 76f819e4..5916f9d2 100644 --- a/ipalib/tests/test_tstutil.py +++ b/ipalib/tests/test_tstutil.py @@ -26,23 +26,23 @@ import tstutil class Prop(object): def __init__(self, *ops): - self.__ops = frozenset(ops) - self.__prop = 'prop value' + self.__ops = frozenset(ops) + self.__prop = 'prop value' def __get_prop(self): - if 'get' not in self.__ops: - raise AttributeError('get prop') - return self.__prop + if 'get' not in self.__ops: + raise AttributeError('get prop') + return self.__prop def __set_prop(self, value): - if 'set' not in self.__ops: - raise AttributeError('set prop') - self.__prop = value + if 'set' not in self.__ops: + raise AttributeError('set prop') + self.__prop = value def __del_prop(self): - if 'del' not in self.__ops: - raise AttributeError('del prop') - self.__prop = None + if 'del' not in self.__ops: + raise AttributeError('del prop') + self.__prop = None prop = property(__get_prop, __set_prop, __del_prop) @@ -51,36 +51,36 @@ def test_yes_raised(): f = tstutil.raises class SomeError(Exception): - pass + pass class AnotherError(Exception): - pass + pass def callback1(): - 'raises correct exception' - raise SomeError() + 'raises correct exception' + raise SomeError() def callback2(): - 'raises wrong exception' - raise AnotherError() + 'raises wrong exception' + raise AnotherError() def callback3(): - 'raises no exception' + 'raises no exception' f(SomeError, callback1) raised = False try: - f(SomeError, callback2) + f(SomeError, callback2) except AnotherError: - raised = True + raised = True assert raised raised = False try: - f(SomeError, callback3) + f(SomeError, callback3) except tstutil.ExceptionNotRaised: - raised = True + raised = True assert raised @@ -91,9 +91,9 @@ def test_no_set(): # Tests that ExceptionNotRaised is raised when prop *can* be set: raised = False try: - tstutil.no_set(Prop('set'), 'prop') + tstutil.no_set(Prop('set'), 'prop') except tstutil.ExceptionNotRaised: - raised = True + raised = True assert raised @@ -104,9 +104,9 @@ def test_no_del(): # Tests that ExceptionNotRaised is raised when prop *can* be set: raised = False try: - tstutil.no_del(Prop('del'), 'prop') + tstutil.no_del(Prop('del'), 'prop') except tstutil.ExceptionNotRaised: - raised = True + raised = True assert raised @@ -117,32 +117,32 @@ def test_read_only(): # Test that ExceptionNotRaised is raised when prop can be set: raised = False try: - tstutil.read_only(Prop('get', 'set'), 'prop') + tstutil.read_only(Prop('get', 'set'), 'prop') except tstutil.ExceptionNotRaised: - raised = True + raised = True assert raised # Test that ExceptionNotRaised is raised when prop can be deleted: raised = False try: - tstutil.read_only(Prop('get', 'del'), 'prop') + tstutil.read_only(Prop('get', 'del'), 'prop') except tstutil.ExceptionNotRaised: - raised = True + raised = True assert raised # Test that ExceptionNotRaised is raised when prop can be both set and # deleted: raised = False try: - tstutil.read_only(Prop('get', 'del'), 'prop') + tstutil.read_only(Prop('get', 'del'), 'prop') except tstutil.ExceptionNotRaised: - raised = True + raised = True assert raised # Test that AttributeError is raised when prop can't be read: raised = False try: - tstutil.read_only(Prop(), 'prop') + tstutil.read_only(Prop(), 'prop') except AttributeError: - raised = True + raised = True assert raised diff --git a/ipalib/tests/tstutil.py b/ipalib/tests/tstutil.py index e3411366..63c75665 100644 --- a/ipalib/tests/tstutil.py +++ b/ipalib/tests/tstutil.py @@ -29,10 +29,10 @@ class ExceptionNotRaised(Exception): msg = 'expected %s' def __init__(self, expected): - self.expected = expected + self.expected = expected def __str__(self): - return self.msg % self.expected.__name__ + return self.msg % self.expected.__name__ def raises(exception, callback, *args, **kw): @@ -42,11 +42,11 @@ def raises(exception, callback, *args, **kw): """ raised = False try: - callback(*args, **kw) + callback(*args, **kw) except exception, e: - raised = True + raised = True if not raised: - raise ExceptionNotRaised(exception) + raise ExceptionNotRaised(exception) return e @@ -93,7 +93,7 @@ def is_prop(prop): class ClassChecker(object): def new(self, *args, **kw): - return self.cls(*args, **kw) + return self.cls(*args, **kw) def get_sub(self): - raise NotImplementedError('get_sub()') + raise NotImplementedError('get_sub()') -- cgit From 3fe13d5945df224643374da477f68e04d4f443e5 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 8 Aug 2008 21:46:23 +0000 Subject: 87: Moved to_cli(), from_cli() functions from plugable.py into new cli.py file; moved corresponding unit tests into new test_cli.py file --- ipalib/cli.py | 40 ++++++++++++++++++++++++++++++++++++++++ ipalib/plugable.py | 18 ------------------ ipalib/tests/test_cli.py | 36 ++++++++++++++++++++++++++++++++++++ ipalib/tests/test_plugable.py | 12 ------------ 4 files changed, 76 insertions(+), 30 deletions(-) create mode 100644 ipalib/cli.py create mode 100644 ipalib/tests/test_cli.py (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py new file mode 100644 index 00000000..5e257f70 --- /dev/null +++ b/ipalib/cli.py @@ -0,0 +1,40 @@ +# Authors: +# Jason Gerard DeRose +# +# 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 + +""" +Functionality for Command Line Inteface. +""" + + +def to_cli(name): + """ + Takes a Python identifier and transforms it into form suitable for the + Command Line Interface. + """ + assert isinstance(name, str) + return name.replace('_', '-') + + +def from_cli(cli_name): + """ + Takes a string from the Command Line Interface and transforms it into a + Python identifier. + """ + assert isinstance(cli_name, basestring) + return cli_name.replace('-', '_') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 0a6a0caa..ecbd5855 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -26,24 +26,6 @@ import inspect import errors -def to_cli(name): - """ - Takes a Python identifier and transforms it into form suitable for the - Command Line Interface. - """ - assert isinstance(name, str) - return name.replace('_', '-') - - -def from_cli(cli_name): - """ - Takes a string from the Command Line Interface and transforms it into a - Python identifier. - """ - assert isinstance(cli_name, basestring) - return cli_name.replace('-', '_') - - def check_identifier(name): """ Raises errors.NameSpaceError if `name` is not a valid Python identifier diff --git a/ipalib/tests/test_cli.py b/ipalib/tests/test_cli.py new file mode 100644 index 00000000..91bc0a29 --- /dev/null +++ b/ipalib/tests/test_cli.py @@ -0,0 +1,36 @@ +# Authors: +# Jason Gerard DeRose +# +# 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 + +""" +Unit tests for `ipalib.cli` module. +""" + +from ipalib import cli + + +def test_to_cli(): + f = cli.to_cli + assert f('initialize') == 'initialize' + assert f('user_add') == 'user-add' + + +def test_from_cli(): + f = cli.from_cli + assert f('initialize') == 'initialize' + assert f('user-add') == 'user_add' diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index 6a1d8a60..258f7b1d 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -25,18 +25,6 @@ from tstutil import raises, getitem, no_set, no_del, read_only from ipalib import plugable, errors -def test_to_cli(): - f = plugable.to_cli - assert f('initialize') == 'initialize' - assert f('user_add') == 'user-add' - - -def test_from_cli(): - f = plugable.from_cli - assert f('initialize') == 'initialize' - assert f('user-add') == 'user_add' - - def test_valid_identifier(): f = plugable.check_identifier okay = [ -- cgit From 1744723d11b2fbc93f43699f79df40d5d0b9305d Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 8 Aug 2008 21:49:09 +0000 Subject: 88: Renamed ReadOnly._lock() to ReadOnly.__lock__(); updated subclasses and unit tests --- ipalib/plugable.py | 69 ++++++++++++++++++++++--------------------- ipalib/tests/test_plugable.py | 4 +-- 2 files changed, 38 insertions(+), 35 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index ecbd5855..092e3bdd 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -36,6 +36,39 @@ def check_identifier(name): raise errors.NameSpaceError(name, regex) +class ReadOnly(object): + """ + Base class for classes with read-only attributes. + """ + __locked = False + + def __lock__(self): + assert self.__locked is False + self.__locked = True + + def __setattr__(self, name, value): + """ + Raises an AttributeError if ReadOnly.__lock__() has already been called; + otherwise calls object.__setattr__() + """ + if self.__locked: + raise AttributeError('read-only: cannot set %s.%s' % + (self.__class__.__name__, name) + ) + return object.__setattr__(self, name, value) + + def __delattr__(self, name): + """ + Raises an AttributeError if ReadOnly.__lock__() has already been called; + otherwise calls object.__delattr__() + """ + if self.__locked: + raise AttributeError('read-only: cannot del %s.%s' % + (self.__class__.__name__, name) + ) + return object.__delattr__(self, name) + + class Abstract(object): __public__ = frozenset() @@ -96,37 +129,7 @@ class Plugin(object): ) -class ReadOnly(object): - """ - Base class for classes with read-only attributes. - """ - __locked = False - - def _lock(self): - assert self.__locked is False - self.__locked = True - - def __setattr__(self, name, value): - """ - Raises an AttributeError if ReadOnly._lock() has already been called; - otherwise calls object.__setattr__() - """ - if self.__locked: - raise AttributeError('read-only: cannot set %s.%s' % - (self.__class__.__name__, name) - ) - return object.__setattr__(self, name, value) - def __delattr__(self, name): - """ - Raises an AttributeError if ReadOnly._lock() has already been called; - otherwise calls object.__delattr__() - """ - if self.__locked: - raise AttributeError('read-only: cannot del %s.%s' % - (self.__class__.__name__, name) - ) - return object.__delattr__(self, name) class Proxy(ReadOnly): @@ -150,7 +153,7 @@ class Proxy(ReadOnly): self.__public__ = base.__public__ assert type(self.__public__) is frozenset check_identifier(self.name) - self._lock() + self.__lock__() def __iter__(self): @@ -200,7 +203,7 @@ class NameSpace(ReadOnly): self.__d[proxy.name] = proxy assert not hasattr(self, proxy.name) setattr(self, proxy.name, proxy) - self._lock() + self.__lock__() def __iter__(self): """ @@ -328,7 +331,7 @@ class API(ReadOnly): def __init__(self, *allowed): keys = tuple(b.__name__ for b in allowed) self.register = Registrar(*allowed) - self._lock() + self.__lock__() def __call__(self): """ diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index 258f7b1d..91b9f6e8 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -127,7 +127,7 @@ def test_Plugin(): def test_ReadOnly(): obj = plugable.ReadOnly() - obj._lock() + obj.__lock__() names = ['not_an_attribute', 'an_attribute'] for name in names: no_set(obj, name) @@ -136,7 +136,7 @@ def test_ReadOnly(): class some_ro_class(plugable.ReadOnly): def __init__(self): self.an_attribute = 'Hello world!' - self._lock() + self.__lock__() obj = some_ro_class() for name in names: no_set(obj, name) -- cgit From 6f144fbaf062d9644af06fdd11020e3d5d349639 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 8 Aug 2008 22:13:49 +0000 Subject: 89: Moved ClassChecker from test_public.py into tstutil.py; improved unit tests for plugable.ReadOnly --- ipalib/tests/test_plugable.py | 67 ++++++++++++++++++++++++++++++++----------- ipalib/tests/test_public.py | 22 +------------- ipalib/tests/tstutil.py | 30 +++++++++++++++---- 3 files changed, 75 insertions(+), 44 deletions(-) (limited to 'ipalib') diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index 91b9f6e8..1f42aa85 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -22,9 +22,58 @@ Unit tests for `ipalib.plugable` module. """ from tstutil import raises, getitem, no_set, no_del, read_only +from tstutil import ClassChecker from ipalib import plugable, errors +class test_ReadOnly(ClassChecker): + """ + Test the plugable.ReadOnly class + """ + _cls = plugable.ReadOnly + + def test_class(self): + assert self.cls.__bases__ == (object,) + + def test_when_unlocked(self): + """ + Test that default state is unlocked, that setting and deleting + attributes works. + """ + o = self.cls() + + # Setting: + o.hello = 'world' + assert o.hello == 'world' + + # Deleting: + del o.hello + assert not hasattr(o, 'hello') + + def test_when_locked(self): + """ + Test that after __lock__() has been called, setting or deleting an + attribute raises AttributeError. + """ + obj = self.cls() + obj.__lock__() + names = ['not_an_attribute', 'an_attribute'] + for name in names: + no_set(obj, name) + no_del(obj, name) + + class some_ro_class(self.cls): + def __init__(self): + self.an_attribute = 'Hello world!' + self.__lock__() + obj = some_ro_class() + for name in names: + no_set(obj, name) + no_del(obj, name) + assert read_only(obj, 'an_attribute') == 'Hello world!' + + + def test_valid_identifier(): f = plugable.check_identifier okay = [ @@ -125,23 +174,7 @@ def test_Plugin(): raises(AssertionError, p.finalize, api) -def test_ReadOnly(): - obj = plugable.ReadOnly() - obj.__lock__() - names = ['not_an_attribute', 'an_attribute'] - for name in names: - no_set(obj, name) - no_del(obj, name) - - class some_ro_class(plugable.ReadOnly): - def __init__(self): - self.an_attribute = 'Hello world!' - self.__lock__() - obj = some_ro_class() - for name in names: - no_set(obj, name) - no_del(obj, name) - assert read_only(obj, 'an_attribute') == 'Hello world!' + def test_Proxy(): diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index 6bff9d89..83fd65c1 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -21,7 +21,7 @@ Unit tests for `ipalib.public` module. """ -from tstutil import raises, getitem, no_set, no_del, read_only +from tstutil import raises, getitem, no_set, no_del, read_only, ClassChecker from ipalib import public, plugable, errors @@ -63,27 +63,7 @@ def test_is_rule(): assert not is_rule(call(None)) -class ClassChecker(object): - __cls = None - __subcls = None - def __get_cls(self): - if self.__cls is None: - self.__cls = self._cls - return self.__cls - cls = property(__get_cls) - - def __get_subcls(self): - if self.__subcls is None: - self.__subcls = self.get_subcls() - return self.__subcls - subcls = property(__get_subcls) - - def get_subcls(self): - raise NotImplementedError( - self.__class__.__name__, - 'get_subcls()' - ) class test_option(ClassChecker): diff --git a/ipalib/tests/tstutil.py b/ipalib/tests/tstutil.py index 63c75665..7b3a2d5e 100644 --- a/ipalib/tests/tstutil.py +++ b/ipalib/tests/tstutil.py @@ -21,6 +21,8 @@ Utility functions for the unit tests. """ +import inspect + class ExceptionNotRaised(Exception): """ Exception raised when an *expected* exception is *not* raised during a @@ -91,9 +93,25 @@ def is_prop(prop): class ClassChecker(object): - - def new(self, *args, **kw): - return self.cls(*args, **kw) - - def get_sub(self): - raise NotImplementedError('get_sub()') + __cls = None + __subcls = None + + def __get_cls(self): + if self.__cls is None: + self.__cls = self._cls + assert inspect.isclass(self.__cls) + return self.__cls + cls = property(__get_cls) + + def __get_subcls(self): + if self.__subcls is None: + self.__subcls = self.get_subcls() + assert inspect.isclass(self.__subcls) + return self.__subcls + subcls = property(__get_subcls) + + def get_subcls(self): + raise NotImplementedError( + self.__class__.__name__, + 'get_subcls()' + ) -- cgit From 5a1223e94367c4370a94f271ef7e087dbdb02615 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 8 Aug 2008 22:45:09 +0000 Subject: 90: Renamed plugable.Abstract to ProxyTarget, which now subclasses from ReadOnly; updated unit tests --- ipalib/plugable.py | 104 +++++++++++++++------------- ipalib/tests/test_plugable.py | 155 +++++++++++++++++++++++------------------- 2 files changed, 139 insertions(+), 120 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 092e3bdd..3f53fd4a 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -43,7 +43,11 @@ class ReadOnly(object): __locked = False def __lock__(self): - assert self.__locked is False + """ + Puts this instance into a read-only state, after which attempting to + set or delete an attribute will raise AttributeError. + """ + assert self.__locked is False, '__lock__() can only be called once' self.__locked = True def __setattr__(self, name, value): @@ -69,7 +73,7 @@ class ReadOnly(object): return object.__delattr__(self, name) -class Abstract(object): +class ProxyTarget(ReadOnly): __public__ = frozenset() @classmethod @@ -86,52 +90,6 @@ class Abstract(object): ) -class Plugin(object): - """ - Base class for all plugins. - """ - - __api = None - - def __get_api(self): - """ - Returns the plugable.API instance passed to Plugin.finalize(), or - or returns None if finalize() has not yet been called. - """ - return self.__api - api = property(__get_api) - - def finalize(self, api): - """ - After all the plugins are instantiated, the plugable.API calls this - method, passing itself as the only argument. This is where plugins - should check that other plugins they depend upon have actually be - loaded. - """ - assert self.__api is None, 'finalize() can only be called once' - assert api is not None, 'finalize() argument cannot be None' - self.__api = api - - def __get_name(self): - """ - Returns the class name of this instance. - """ - return self.__class__.__name__ - name = property(__get_name) - - def __repr__(self): - """ - Returns a fully qualified representation of the class. - """ - return '%s.%s()' % ( - self.__class__.__module__, - self.__class__.__name__ - ) - - - - - class Proxy(ReadOnly): __slots__ = ( '__base', @@ -155,7 +113,6 @@ class Proxy(ReadOnly): check_identifier(self.name) self.__lock__() - def __iter__(self): for name in sorted(self.__public__): yield name @@ -185,6 +142,55 @@ class Proxy(ReadOnly): ) +class Plugin(object): + """ + Base class for all plugins. + """ + + __api = None + + def __get_api(self): + """ + Returns the plugable.API instance passed to Plugin.finalize(), or + or returns None if finalize() has not yet been called. + """ + return self.__api + api = property(__get_api) + + def finalize(self, api): + """ + After all the plugins are instantiated, the plugable.API calls this + method, passing itself as the only argument. This is where plugins + should check that other plugins they depend upon have actually be + loaded. + """ + assert self.__api is None, 'finalize() can only be called once' + assert api is not None, 'finalize() argument cannot be None' + self.__api = api + + def __get_name(self): + """ + Returns the class name of this instance. + """ + return self.__class__.__name__ + name = property(__get_name) + + def __repr__(self): + """ + Returns a fully qualified representation of the class. + """ + return '%s.%s()' % ( + self.__class__.__module__, + self.__class__.__name__ + ) + + + + + + + + class NameSpace(ReadOnly): """ A read-only namespace of (key, value) pairs that can be accessed diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index 1f42aa85..c3245e77 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -26,6 +26,33 @@ from tstutil import ClassChecker from ipalib import plugable, errors +def test_valid_identifier(): + """ + Test the plugable.valid_identifier function. + """ + f = plugable.check_identifier + okay = [ + 'user_add', + 'stuff2junk', + 'sixty9', + ] + nope = [ + '_user_add', + '__user_add', + 'user_add_', + 'user_add__', + '_user_add_', + '__user_add__', + '60nine', + ] + for name in okay: + f(name) + for name in nope: + raises(errors.NameSpaceError, f, name) + for name in okay: + raises(errors.NameSpaceError, f, name.upper()) + + class test_ReadOnly(ClassChecker): """ Test the plugable.ReadOnly class @@ -34,6 +61,7 @@ class test_ReadOnly(ClassChecker): def test_class(self): assert self.cls.__bases__ == (object,) + assert callable(self.cls.__lock__) def test_when_unlocked(self): """ @@ -73,79 +101,64 @@ class test_ReadOnly(ClassChecker): assert read_only(obj, 'an_attribute') == 'Hello world!' +class test_ProxyTarget(ClassChecker): + """ + Test the plugable.ProxyTarget class. + """ + _cls = plugable.ProxyTarget -def test_valid_identifier(): - f = plugable.check_identifier - okay = [ - 'user_add', - 'stuff2junk', - 'sixty9', - ] - nope = [ - '_user_add', - '__user_add', - 'user_add_', - 'user_add__', - '_user_add_', - '__user_add__', - '60nine', - ] - for name in okay: - f(name) - for name in nope: - raises(errors.NameSpaceError, f, name) - for name in okay: - raises(errors.NameSpaceError, f, name.upper()) - - -def test_Abstract(): - cls = plugable.Abstract - - class example(cls): - __public__ = frozenset(( - 'some_method', - 'some_property', - )) - - # Test using str: - assert example.implements('some_method') - assert not example.implements('another_method') - - # Test using frozenset: - assert example.implements(frozenset(['some_method'])) - assert not example.implements( - frozenset(['some_method', 'another_method']) - ) - - # Test using another object/class with __public__ frozenset: - assert example.implements(example) - assert example().implements(example) - assert example.implements(example()) - assert example().implements(example()) - - class subset(cls): - __public__ = frozenset(( - 'some_property', - )) - assert example.implements(subset) - assert not subset.implements(example) - - class superset(cls): - __public__ = frozenset(( - 'some_method', - 'some_property', - 'another_property', - )) - assert not example.implements(superset) - assert superset.implements(example) + def test_class(self): + assert self.cls.__bases__ == (plugable.ReadOnly,) + assert self.cls.implements(frozenset()) - class any_object(object): - __public__ = frozenset(( - 'some_method', - 'some_property', - )) - assert example.implements(any_object) - assert example.implements(any_object()) + def test_implements(self): + """ + Test the implements() classmethod + """ + class example(self.cls): + __public__ = frozenset(( + 'some_method', + 'some_property', + )) + class superset(self.cls): + __public__ = frozenset(( + 'some_method', + 'some_property', + 'another_property', + )) + class subset(self.cls): + __public__ = frozenset(( + 'some_property', + )) + class any_object(object): + __public__ = frozenset(( + 'some_method', + 'some_property', + )) + + for ex in (example, example()): + # Test using str: + assert ex.implements('some_method') + assert not ex.implements('another_method') + + # Test using frozenset: + assert ex.implements(frozenset(['some_method'])) + assert not ex.implements( + frozenset(['some_method', 'another_method']) + ) + + # Test using another object/class with __public__ frozenset: + assert ex.implements(example) + assert ex.implements(example()) + + assert ex.implements(subset) + assert not subset.implements(ex) + + assert not ex.implements(superset) + assert superset.implements(ex) + + assert ex.implements(any_object) + assert ex.implements(any_object()) def test_Plugin(): -- cgit From e3811f3f45adf977ade6468221368efb7f92294f Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 8 Aug 2008 23:07:22 +0000 Subject: 91: Fleshed out docstrings in plugable.Proxy --- ipalib/plugable.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 3f53fd4a..581f377b 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -91,6 +91,15 @@ class ProxyTarget(ReadOnly): class Proxy(ReadOnly): + """ + Allows access to only certain attributes on its target object (a + ProxyTarget). + + Think of a proxy as an argreement that "I will have at most these + attributes". This is different from (although similar to) an interface, + which can be thought of as an agreement that "I will have at least these + attributes". + """ __slots__ = ( '__base', '__target', @@ -100,6 +109,13 @@ class Proxy(ReadOnly): ) def __init__(self, base, target, name_attr='name'): + """ + `base` - the class defining the __public__ frozenset of attributes to + proxy + `target` - the target of the proxy (must be instance of `base`) + `name_attr` - the name of the str attribute on `target` to assign + to Proxy.name + """ if not inspect.isclass(base): raise TypeError('arg1 must be a class, got %r' % base) if not isinstance(target, base): @@ -114,15 +130,26 @@ class Proxy(ReadOnly): self.__lock__() def __iter__(self): + """ + Iterates though the attribute names this proxy is allowing access to. + """ for name in sorted(self.__public__): yield name def __getitem__(self, key): + """ + If this proxy allowes access to an attribute named `key`, return that + attrribute. + """ if key in self.__public__: return getattr(self.__target, key) raise KeyError('no proxy attribute %r' % key) def __getattr__(self, name): + """ + If this proxy allowes access to an attribute named `name`, return that + attrribute. + """ if name in self.__public__: return getattr(self.__target, name) raise AttributeError('no proxy attribute %r' % name) -- cgit From 45201e31c1b7be7cb770d2e864c307c95e743751 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 8 Aug 2008 23:26:17 +0000 Subject: 92: Added ProxyTarget.name property; added corresponding unit tests --- ipalib/plugable.py | 7 +++++++ ipalib/tests/test_plugable.py | 17 ++++++++++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 581f377b..c5ceeffe 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -76,6 +76,13 @@ class ReadOnly(object): class ProxyTarget(ReadOnly): __public__ = frozenset() + def __get_name(self): + """ + Convenience property to return the class name. + """ + return self.__class__.__name__ + name = property(__get_name) + @classmethod def implements(cls, arg): assert type(cls.__public__) is frozenset diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index c3245e77..25f52099 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -55,7 +55,7 @@ def test_valid_identifier(): class test_ReadOnly(ClassChecker): """ - Test the plugable.ReadOnly class + Test the `ReadOnly` class """ _cls = plugable.ReadOnly @@ -103,17 +103,28 @@ class test_ReadOnly(ClassChecker): class test_ProxyTarget(ClassChecker): """ - Test the plugable.ProxyTarget class. + Test the `ProxyTarget` class. """ _cls = plugable.ProxyTarget def test_class(self): assert self.cls.__bases__ == (plugable.ReadOnly,) + assert type(self.cls.name) is property assert self.cls.implements(frozenset()) + def test_name(self): + """ + Test the `name` property. + """ + assert read_only(self.cls(), 'name') == 'ProxyTarget' + + class some_subclass(self.cls): + pass + assert read_only(some_subclass(), 'name') == 'some_subclass' + def test_implements(self): """ - Test the implements() classmethod + Test the `implements` classmethod. """ class example(self.cls): __public__ = frozenset(( -- cgit From cc5b0174949a4769876a892b210a9faa9683d81e Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sat, 9 Aug 2008 01:06:42 +0000 Subject: 93: Added Proxy.implements() method; addeded corresponding unit tests --- ipalib/plugable.py | 9 +- ipalib/tests/test_plugable.py | 185 ++++++++++++++++++++++++------------------ 2 files changed, 113 insertions(+), 81 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index c5ceeffe..1e1f4a90 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -102,7 +102,7 @@ class Proxy(ReadOnly): Allows access to only certain attributes on its target object (a ProxyTarget). - Think of a proxy as an argreement that "I will have at most these + Think of a proxy as an agreement that "I will have at most these attributes". This is different from (although similar to) an interface, which can be thought of as an agreement that "I will have at least these attributes". @@ -136,9 +136,13 @@ class Proxy(ReadOnly): check_identifier(self.name) self.__lock__() + def implements(self, arg): + return self.__base.implements(arg) + def __iter__(self): """ - Iterates though the attribute names this proxy is allowing access to. + Iterates (in ascending order) though the attribute names this proxy is + allowing access to. """ for name in sorted(self.__public__): yield name @@ -176,6 +180,7 @@ class Proxy(ReadOnly): ) + class Plugin(object): """ Base class for all plugins. diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index 25f52099..faf2fd92 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -28,7 +28,7 @@ from ipalib import plugable, errors def test_valid_identifier(): """ - Test the plugable.valid_identifier function. + Test the `valid_identifier` function. """ f = plugable.check_identifier okay = [ @@ -172,6 +172,111 @@ class test_ProxyTarget(ClassChecker): assert ex.implements(any_object()) +class test_Proxy(ClassChecker): + """ + Test the `Proxy` class. + """ + _cls = plugable.Proxy + + def test_class(self): + assert self.cls.__bases__ == (plugable.ReadOnly,) + + def test_proxy(self): + # Setup: + class base(object): + __public__ = frozenset(( + 'public_0', + 'public_1', + '__call__', + )) + + def public_0(self): + return 'public_0' + + def public_1(self): + return 'public_1' + + def __call__(self, caller): + return 'ya called it, %s.' % caller + + def private_0(self): + return 'private_0' + + def private_1(self): + return 'private_1' + + class plugin(base): + name = 'user_add' + attr_name = 'add' + + # Test that TypeError is raised when base is not a class: + raises(TypeError, self.cls, base(), None) + + # Test that ValueError is raised when target is not instance of base: + raises(ValueError, self.cls, base, object()) + + # Test with correct arguments: + i = plugin() + p = self.cls(base, i) + assert read_only(p, 'name') == 'user_add' + assert list(p) == sorted(base.__public__) + + # Test normal methods: + for n in xrange(2): + pub = 'public_%d' % n + priv = 'private_%d' % n + assert getattr(i, pub)() == pub + assert getattr(p, pub)() == pub + assert hasattr(p, pub) + assert getattr(i, priv)() == priv + assert not hasattr(p, priv) + + # Test __call__: + value = 'ya called it, dude.' + assert i('dude') == value + assert p('dude') == value + assert callable(p) + + # Test name_attr='name' kw arg + i = plugin() + p = self.cls(base, i, 'attr_name') + assert read_only(p, 'name') == 'add' + + # Test _clone(): + i = plugin() + p = self.cls(base, i) + assert read_only(p, 'name') == 'user_add' + c = p._clone('attr_name') + assert isinstance(c, self.cls) + assert read_only(c, 'name') == 'add' + assert c is not p + assert c('whoever') == p('whoever') + + def test_implements(self): + """ + Test the `implements` method. + """ + class base(object): + __public__ = frozenset() + name = 'base' + @classmethod + def implements(cls, arg): + return arg + 7 + + class sub(base): + @classmethod + def implements(cls, arg): + """ + Defined to make sure base.implements() is called, not + target.implements() + """ + return arg + + o = sub() + p = self.cls(base, o) + assert p.implements(3) == 10 + + def test_Plugin(): cls = plugable.Plugin assert type(cls.name) is property @@ -198,84 +303,6 @@ def test_Plugin(): raises(AssertionError, p.finalize, api) - - - -def test_Proxy(): - cls = plugable.Proxy - assert issubclass(cls, plugable.ReadOnly) - - # Setup: - class base(object): - __public__ = frozenset(( - 'public_0', - 'public_1', - '__call__', - )) - - def public_0(self): - return 'public_0' - - def public_1(self): - return 'public_1' - - def __call__(self, caller): - return 'ya called it, %s.' % caller - - def private_0(self): - return 'private_0' - - def private_1(self): - return 'private_1' - - class plugin(base): - name = 'user_add' - attr_name = 'add' - - # Test that TypeError is raised when base is not a class: - raises(TypeError, cls, base(), None) - - # Test that ValueError is raised when target is not instance of base: - raises(ValueError, cls, base, object()) - - # Test with correct arguments: - i = plugin() - p = cls(base, i) - assert read_only(p, 'name') == 'user_add' - assert list(p) == sorted(base.__public__) - - # Test normal methods: - for n in xrange(2): - pub = 'public_%d' % n - priv = 'private_%d' % n - assert getattr(i, pub)() == pub - assert getattr(p, pub)() == pub - assert hasattr(p, pub) - assert getattr(i, priv)() == priv - assert not hasattr(p, priv) - - # Test __call__: - value = 'ya called it, dude.' - assert i('dude') == value - assert p('dude') == value - assert callable(p) - - # Test name_attr='name' kw arg - i = plugin() - p = cls(base, i, 'attr_name') - assert read_only(p, 'name') == 'add' - - # Test _clone(): - i = plugin() - p = cls(base, i) - assert read_only(p, 'name') == 'user_add' - c = p._clone('attr_name') - assert isinstance(c, cls) - assert read_only(c, 'name') == 'add' - assert c is not p - assert c('whoever') == p('whoever') - - def test_NameSpace(): cls = plugable.NameSpace assert issubclass(cls, plugable.ReadOnly) -- cgit From 3495c67d57868a02bafe6f1935d4846cd5615bf5 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sat, 9 Aug 2008 01:46:12 +0000 Subject: 94: Renamed Proxy._clone() method to Proxy.__clone__(); updated unit tests --- ipalib/plugable.py | 10 ++++++++-- ipalib/public.py | 2 +- ipalib/tests/test_plugable.py | 33 ++++++++++++++++++++------------- 3 files changed, 29 insertions(+), 16 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 1e1f4a90..c5ec08ec 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -139,6 +139,14 @@ class Proxy(ReadOnly): def implements(self, arg): return self.__base.implements(arg) + def __clone__(self, name_attr): + """ + Returns a Proxy instance identical to this one except the proxy name + might be derived from a different attribute on the target. The same + base and target will be used. + """ + return self.__class__(self.__base, self.__target, name_attr) + def __iter__(self): """ Iterates (in ascending order) though the attribute names this proxy is @@ -168,8 +176,6 @@ class Proxy(ReadOnly): def __call__(self, *args, **kw): return self['__call__'](*args, **kw) - def _clone(self, name_attr): - return self.__class__(self.__base, self.__target, name_attr) def __repr__(self): return '%s(%s, %r, %r)' % ( diff --git a/ipalib/public.py b/ipalib/public.py index 7bce4992..21403169 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -226,7 +226,7 @@ class obj(plugable.Plugin): def __filter(self, name): for i in getattr(self.api, name): if i.obj_name == self.name: - yield i._clone('attr_name') + yield i.__clone__('attr_name') class attr(plugable.Plugin): diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index faf2fd92..605debe5 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -174,12 +174,12 @@ class test_ProxyTarget(ClassChecker): class test_Proxy(ClassChecker): """ - Test the `Proxy` class. + Tests the `Proxy` class. """ _cls = plugable.Proxy def test_class(self): - assert self.cls.__bases__ == (plugable.ReadOnly,) + assert self.cls.__bases__ == (plugable.ReadOnly,) def test_proxy(self): # Setup: @@ -242,19 +242,9 @@ class test_Proxy(ClassChecker): p = self.cls(base, i, 'attr_name') assert read_only(p, 'name') == 'add' - # Test _clone(): - i = plugin() - p = self.cls(base, i) - assert read_only(p, 'name') == 'user_add' - c = p._clone('attr_name') - assert isinstance(c, self.cls) - assert read_only(c, 'name') == 'add' - assert c is not p - assert c('whoever') == p('whoever') - def test_implements(self): """ - Test the `implements` method. + Tests the `implements` method. """ class base(object): __public__ = frozenset() @@ -276,6 +266,23 @@ class test_Proxy(ClassChecker): p = self.cls(base, o) assert p.implements(3) == 10 + def test_clone(self): + """ + Tests the `__clone__` method. + """ + class base(object): + __public__ = frozenset() + class sub(base): + name = 'some_name' + label = 'another_name' + + p = self.cls(base, sub()) + assert read_only(p, 'name') == 'some_name' + c = p.__clone__('label') + assert isinstance(c, self.cls) + assert c is not p + assert read_only(c, 'name') == 'another_name' + def test_Plugin(): cls = plugable.Plugin -- cgit From 72f3132d2b98a44881ae7001d0001602a66bf8b5 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sat, 9 Aug 2008 04:35:06 +0000 Subject: 95: Improved docstrings for ReadOnly class; added ReadOnly.__islocked__() method; added corresponding unit tests --- ipalib/plugable.py | 39 +++++++++++++++++++++++++++++++++++++++ ipalib/tests/test_plugable.py | 12 ++++++++++++ 2 files changed, 51 insertions(+) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index c5ec08ec..4ce4a9ba 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -39,6 +39,39 @@ def check_identifier(name): class ReadOnly(object): """ Base class for classes with read-only attributes. + + Be forewarned that Python does not offer true read-only user defined + classes. In particular, 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 delete + attributes, but do make it impossible to accidentally do so. The plugins + are not thread-safe: in the server, they are loaded once and the same + instances will be used to process many requests. Therefore, it is + imperative that they not set any instance attributes after they have + been initialized. This base class enforces that policy. + + For example: + + >>> class givenName(ReadOnly): + >>> def __init__(self): + >>> self.whatever = 'some value' # Hasn't been locked yet + >>> self.__lock__() + >>> + >>> def finalize(self, api): + >>> # After the instance has been locked, attributes can still be + >>> # set, but only in a round-about, unconventional way: + >>> object.__setattr__(self, 'api', api) + >>> + >>> def normalize(self, value): + >>> # After the instance has been locked, trying to set an + >>> # attribute in the normal way will raise AttributeError. + >>> self.value = value # Not thread safe! + >>> return self.actually_normalize() + >>> + >>> def actually_normalize(self): + >>> # Again, this is not thread safe: + >>> return unicode(self.value).strip() """ __locked = False @@ -50,6 +83,12 @@ class ReadOnly(object): assert self.__locked is False, '__lock__() can only be called once' self.__locked = True + def __islocked__(self): + """ + Returns True if this instance is locked, False otherwise. + """ + return self.__locked + def __setattr__(self, name, value): """ Raises an AttributeError if ReadOnly.__lock__() has already been called; diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index 605debe5..42453ed5 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -62,6 +62,18 @@ class test_ReadOnly(ClassChecker): def test_class(self): assert self.cls.__bases__ == (object,) assert callable(self.cls.__lock__) + assert callable(self.cls.__islocked__) + + def test_lock(self): + """ + Tests the `__lock__` and `__islocked__` methods. + """ + o = self.cls() + assert o.__islocked__() is False + o.__lock__() + assert o.__islocked__() is True + raises(AssertionError, o.__lock__) # Can only be locked once + assert o.__islocked__() is True # This should still be True def test_when_unlocked(self): """ -- cgit From 409f688ef5ed453708df29913036593f7fa51e41 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sat, 9 Aug 2008 04:37:37 +0000 Subject: 96: Fixed typo is ReadOnly docstring --- ipalib/plugable.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 4ce4a9ba..d8578270 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -45,7 +45,7 @@ class ReadOnly(object): class for security purposes. The point of this class is not to make it impossible to set or delete - attributes, but do make it impossible to accidentally do so. The plugins + attributes, but to make it impossible to accidentally do so. The plugins are not thread-safe: in the server, they are loaded once and the same instances will be used to process many requests. Therefore, it is imperative that they not set any instance attributes after they have -- cgit From 9712eae51ce072962a7e969684ba9e8b4ec19dd9 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sat, 9 Aug 2008 05:19:40 +0000 Subject: 97: Some whitespace and docstring cleanup; Plugin now subclasses from ProxyTarget --- ipalib/plugable.py | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index d8578270..2bef3de7 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -150,8 +150,8 @@ class Proxy(ReadOnly): '__base', '__target', '__name_attr', - 'name', '__public__', + 'name', ) def __init__(self, base, target, name_attr='name'): @@ -169,9 +169,9 @@ class Proxy(ReadOnly): self.__base = base self.__target = target self.__name_attr = name_attr - self.name = getattr(target, name_attr) self.__public__ = base.__public__ assert type(self.__public__) is frozenset + self.name = getattr(target, name_attr) check_identifier(self.name) self.__lock__() @@ -225,8 +225,7 @@ class Proxy(ReadOnly): ) - -class Plugin(object): +class Plugin(ProxyTarget): """ Base class for all plugins. """ @@ -252,16 +251,10 @@ class Plugin(object): assert api is not None, 'finalize() argument cannot be None' self.__api = api - def __get_name(self): - """ - Returns the class name of this instance. - """ - return self.__class__.__name__ - name = property(__get_name) - def __repr__(self): """ - Returns a fully qualified representation of the class. + Returns a fully qualified module_name.class_name() representation that + could be used to contruct this instance. """ return '%s.%s()' % ( self.__class__.__module__, @@ -269,12 +262,6 @@ class Plugin(object): ) - - - - - - class NameSpace(ReadOnly): """ A read-only namespace of (key, value) pairs that can be accessed -- cgit From 5315514f6c773de897c2e74a4ad31bbfeeae2bda Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sat, 9 Aug 2008 18:58:46 +0000 Subject: 98: Completed docstrings in Proxy --- ipalib/plugable.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 2bef3de7..7a571995 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -176,6 +176,13 @@ class Proxy(ReadOnly): self.__lock__() def implements(self, arg): + """ + Returns True if this proxy implements `arg`. Calls the corresponding + classmethod on ProxyTarget. + + Unlike ProxyTarget.implements(), this is not a classmethod as a Proxy + only implements anything as an instance. + """ return self.__base.implements(arg) def __clone__(self, name_attr): @@ -196,8 +203,8 @@ class Proxy(ReadOnly): def __getitem__(self, key): """ - If this proxy allowes access to an attribute named `key`, return that - attrribute. + If this proxy allows access to an attribute named `key`, return that + attribute. """ if key in self.__public__: return getattr(self.__target, key) @@ -205,18 +212,25 @@ class Proxy(ReadOnly): def __getattr__(self, name): """ - If this proxy allowes access to an attribute named `name`, return that - attrribute. + If this proxy allows access to an attribute named `name`, return that + attribute. """ if name in self.__public__: return getattr(self.__target, name) raise AttributeError('no proxy attribute %r' % name) def __call__(self, *args, **kw): + """ + Attempts to call target.__call__(); raises KeyError if `__call__` is + not an attribute this proxy allows access to. + """ return self['__call__'](*args, **kw) - def __repr__(self): + """ + Returns a Python expression that could be used to construct this Proxy + instance given the appropriate environment. + """ return '%s(%s, %r, %r)' % ( self.__class__.__name__, self.__base.__name__, -- cgit From e756e12718a538d82de45fbba3a5e97f3a4d7d7f Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sat, 9 Aug 2008 19:09:10 +0000 Subject: 99: Cleaned up unit tests for plugable.Plugin --- ipalib/plugable.py | 2 +- ipalib/tests/test_plugable.py | 58 +++++++++++++++++++++++++------------------ 2 files changed, 35 insertions(+), 25 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 7a571995..b4a6fb10 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -268,7 +268,7 @@ class Plugin(ProxyTarget): def __repr__(self): """ Returns a fully qualified module_name.class_name() representation that - could be used to contruct this instance. + could be used to construct this Plugin instance. """ return '%s.%s()' % ( self.__class__.__module__, diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index 42453ed5..af52f4ee 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -296,30 +296,40 @@ class test_Proxy(ClassChecker): assert read_only(c, 'name') == 'another_name' -def test_Plugin(): - cls = plugable.Plugin - assert type(cls.name) is property - - api = 'the api instance' - p = plugable.Plugin() - assert read_only(p, 'name') == 'Plugin' - assert repr(p) == '%s.Plugin()' % plugable.__name__ - assert read_only(p, 'api') is None - raises(AssertionError, p.finalize, None) - p.finalize(api) - assert read_only(p, 'api') is api - raises(AssertionError, p.finalize, api) - - class some_plugin(plugable.Plugin): - pass - p = some_plugin() - assert read_only(p, 'name') == 'some_plugin' - assert repr(p) == '%s.some_plugin()' % __name__ - assert read_only(p, 'api') is None - raises(AssertionError, p.finalize, None) - p.finalize(api) - assert read_only(p, 'api') is api - raises(AssertionError, p.finalize, api) +class test_Plugin(ClassChecker): + """ + Tests the `Plugin` class. + """ + _cls = plugable.Plugin + + def test_class(self): + assert self.cls.__bases__ == (plugable.ProxyTarget,) + assert type(self.cls.api) is property + + def test_finalize(self): + """ + Tests the `finalize` method. + """ + api = 'the api instance' + o = self.cls() + assert read_only(o, 'name') == 'Plugin' + assert repr(o) == '%s.Plugin()' % plugable.__name__ + assert read_only(o, 'api') is None + raises(AssertionError, o.finalize, None) + o.finalize(api) + assert read_only(o, 'api') is api + raises(AssertionError, o.finalize, api) + + class some_plugin(self.cls): + pass + sub = some_plugin() + assert read_only(sub, 'name') == 'some_plugin' + assert repr(sub) == '%s.some_plugin()' % __name__ + assert read_only(sub, 'api') is None + raises(AssertionError, sub.finalize, None) + sub.finalize(api) + assert read_only(sub, 'api') is api + raises(AssertionError, sub.finalize, api) def test_NameSpace(): -- cgit From 0e532cd7b30023b10f97690540f4209106d7f832 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sat, 9 Aug 2008 19:28:01 +0000 Subject: 100: Cleaned up NameSpace docstrings; cleanup up NameSpace unit tests --- ipalib/plugable.py | 12 +++-- ipalib/tests/test_plugable.py | 113 ++++++++++++++++++++++-------------------- 2 files changed, 68 insertions(+), 57 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index b4a6fb10..63de6deb 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -278,13 +278,17 @@ class Plugin(ProxyTarget): class NameSpace(ReadOnly): """ - A read-only namespace of (key, value) pairs that can be accessed - both as instance attributes and as dictionary items. + A read-only namespace of Proxy instances. Proxy.name is used to name the + attributes pointing to the Proxy instances, which can also be accesses + through a dictionary interface, for example: + + >>> assert namespace.my_proxy is namespace['my_proxy'] # True """ def __init__(self, proxies): """ - NameSpace + `proxies` - an iterable returning the Proxy instances to be contained + in this NameSpace. """ self.__proxies = tuple(proxies) self.__d = dict() @@ -299,7 +303,7 @@ class NameSpace(ReadOnly): def __iter__(self): """ Iterates through the proxies in this NameSpace in the same order they - were passed in the contructor. + were passed to the constructor. """ for proxy in self.__proxies: yield proxy diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index af52f4ee..b8242dce 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -332,62 +332,69 @@ class test_Plugin(ClassChecker): raises(AssertionError, sub.finalize, api) -def test_NameSpace(): - cls = plugable.NameSpace - assert issubclass(cls, plugable.ReadOnly) +class test_NameSpace(ClassChecker): + """ + Tests the `NameSpace` class. + """ + _cls = plugable.NameSpace - class base(object): - __public__ = frozenset(( - 'plusplus', - )) + def test_class(self): + assert self.cls.__bases__ == (plugable.ReadOnly,) - def plusplus(self, n): - return n + 1 + def test_namespace(self): + class base(object): + __public__ = frozenset(( + 'plusplus', + )) + + def plusplus(self, n): + return n + 1 - class plugin(base): - def __init__(self, name): - self.name = name - - def get_name(i): - return 'noun_verb%d' % i - - def get_proxies(n): - for i in xrange(n): - yield plugable.Proxy(base, plugin(get_name(i))) - - cnt = 20 - ns = cls(get_proxies(cnt)) - - # Test __len__ - assert len(ns) == cnt - - # Test __iter__ - i = None - for (i, proxy) in enumerate(ns): - assert type(proxy) is plugable.Proxy - assert proxy.name == get_name(i) - assert i == cnt - 1 - - # Test __contains__, __getitem__, getattr(): - proxies = frozenset(ns) - for i in xrange(cnt): - name = get_name(i) - assert name in ns - proxy = ns[name] - assert proxy.name == name - assert type(proxy) is plugable.Proxy - assert proxy in proxies - assert read_only(ns, name) is proxy - - # Test dir(): - assert set(get_name(i) for i in xrange(cnt)).issubset(set(dir(ns))) - - # Test that KeyError, AttributeError is raised: - name = get_name(cnt) - assert name not in ns - raises(KeyError, getitem, ns, name) - raises(AttributeError, getattr, ns, name) - no_set(ns, name) + class plugin(base): + def __init__(self, name): + self.name = name + + def get_name(i): + return 'noun_verb%d' % i + + def get_proxies(n): + for i in xrange(n): + yield plugable.Proxy(base, plugin(get_name(i))) + + cnt = 20 + ns = self.cls(get_proxies(cnt)) + assert ns.__islocked__() is True + + # Test __len__ + assert len(ns) == cnt + + # Test __iter__ + i = None + for (i, proxy) in enumerate(ns): + assert type(proxy) is plugable.Proxy + assert proxy.name == get_name(i) + assert i == cnt - 1 + + # Test __contains__, __getitem__, getattr(): + proxies = frozenset(ns) + for i in xrange(cnt): + name = get_name(i) + assert name in ns + proxy = ns[name] + assert proxy.name == name + assert type(proxy) is plugable.Proxy + assert proxy in proxies + assert read_only(ns, name) is proxy + + # Test dir(): + assert set(get_name(i) for i in xrange(cnt)).issubset(dir(ns)) + + # Test that KeyError, AttributeError is raised: + name = get_name(cnt) + assert name not in ns + raises(KeyError, getitem, ns, name) + raises(AttributeError, getattr, ns, name) + no_set(ns, name) def test_Registrar(): -- cgit From 543aea31a4bf85d5843abd808d2200117ff35252 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sat, 9 Aug 2008 19:33:13 +0000 Subject: 101: Registrar now subclasses from ReadOnly --- ipalib/plugable.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 63de6deb..029c8403 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -332,7 +332,7 @@ class NameSpace(ReadOnly): return '%s(<%d proxies>)' % (self.__class__.__name__, len(self)) -class Registrar(object): +class Registrar(ReadOnly): def __init__(self, *allowed): """ `*allowed` is a list of the base classes plugins can be subclassed @@ -346,6 +346,7 @@ class Registrar(object): assert inspect.isclass(base) assert base.__name__ not in self.__d self.__d[base.__name__] = {} + self.__lock__() def __findbase(self, cls): """ -- cgit From 0edb22c9ac70c5acfab51318810f693d59fab955 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sat, 9 Aug 2008 19:39:58 +0000 Subject: 102: After the API instance calls plugin.finalize(), it also calls plugin.__lock__() --- ipalib/plugable.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 029c8403..1341a98f 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -447,6 +447,8 @@ class API(ReadOnly): object.__setattr__(self, base.__name__, ns) for plugin in d.values(): plugin.finalize(self) + plugin.__lock__() + assert plugin.__islocked__() is True assert plugin.api is self def __iter__(self): -- cgit From d7958f3fde94d20be126f4486b8d906eb38446f7 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sat, 9 Aug 2008 19:48:47 +0000 Subject: 103: Fixed missing API.__keys assignment --- ipalib/plugable.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 1341a98f..b607f0fa 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -425,7 +425,7 @@ class Registrar(ReadOnly): class API(ReadOnly): def __init__(self, *allowed): - keys = tuple(b.__name__ for b in allowed) + self.__keys = tuple(b.__name__ for b in allowed) self.register = Registrar(*allowed) self.__lock__() -- cgit From f6b69a590500cf8c141545a1f95d59817eb5a27e Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sun, 10 Aug 2008 22:23:22 +0000 Subject: 104: public.option now subclasses from plugable.Plugin; cleaned up unit tests for option --- ipalib/public.py | 2 +- ipalib/tests/test_public.py | 21 ++++++++++++--------- 2 files changed, 13 insertions(+), 10 deletions(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index 21403169..92dc77af 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -38,7 +38,7 @@ def is_rule(obj): return callable(obj) and getattr(obj, RULE_FLAG, False) is True -class option(object): +class option(plugable.Plugin): """ The option class represents a kw argument from a command. """ diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index 83fd65c1..68f25567 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -63,10 +63,10 @@ def test_is_rule(): assert not is_rule(call(None)) - - - class test_option(ClassChecker): + """ + Tests the option class. + """ _cls = public.option def get_subcls(self): @@ -91,10 +91,13 @@ class test_option(ClassChecker): """ Perform some tests on the class (not an instance). """ - #assert issubclass(cls, plugable.ReadOnly) + assert self.cls.__bases__ == (plugable.Plugin,) assert type(self.cls.rules) is property def test_normalize(self): + """ + Tests the `normalize` method. + """ assert 'normalize' in self.cls.__public__ o = self.subcls() # Test with values that can't be converted: @@ -125,7 +128,7 @@ class test_option(ClassChecker): def test_validate(self): """ - Test the validate method. + Tests the `validate` method. """ assert 'validate' in self.cls.__public__ o = self.subcls() @@ -137,7 +140,7 @@ class test_option(ClassChecker): def test_rules(self): """ - Test the rules property. + Tests the `rules` property. """ o = self.subcls() assert len(o.rules) == 3 @@ -147,13 +150,13 @@ class test_option(ClassChecker): assert o.rules == rules def test_default(self): + """ + Tests the `default` method. + """ assert 'default' in self.cls.__public__ assert self.cls().default() is None - - - def test_cmd(): cls = public.cmd assert issubclass(cls, plugable.Plugin) -- cgit From 879133d28a2da2d675d72a3f4e178f5bc4c82594 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 11 Aug 2008 00:21:12 +0000 Subject: 105: Added a default implementation of cmd.get_options; added corresponding unit tests --- ipalib/public.py | 22 +++++++++++++--------- ipalib/tests/test_public.py | 33 ++++++++++++++++++++++++++++++--- 2 files changed, 43 insertions(+), 12 deletions(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index 92dc77af..baa1496a 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -23,6 +23,7 @@ and UI all use. """ import re +import inspect import plugable import errors @@ -134,7 +135,8 @@ class cmd(plugable.Plugin): 'opt', )) - __opt = None + __options = None + option_classes = tuple() def get_doc(self, _): """ @@ -149,19 +151,21 @@ class cmd(plugable.Plugin): def get_options(self): """ - Returns iterable with opt_proxy objects used to create the opt - NameSpace when __get_opt() is called. + Returns iterable with option proxy objects used to create the option + NameSpace when __get_option() is called. """ - raise NotImplementedError('%s.get_options()' % self.name) + for cls in self.option_classes: + assert inspect.isclass(cls) + yield plugable.Proxy(option, cls()) - def __get_opt(self): + def __get_options(self): """ - Returns the NameSpace containing opt_proxy objects. + Returns the NameSpace containing the option proxy objects. """ - if self.__opt is None: - self.__opt = plugable.NameSpace(self.get_options()) + if self.__options is None: + self.__options = plugable.NameSpace(self.get_options()) return self.__opt - opt = property(__get_opt) + options = property(__get_options) def normalize_iter(self, kw): for (key, value) in kw.items(): diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index 68f25567..91d1f724 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -157,9 +157,36 @@ class test_option(ClassChecker): assert self.cls().default() is None -def test_cmd(): - cls = public.cmd - assert issubclass(cls, plugable.Plugin) +class test_cmd(ClassChecker): + """ + Tests the `cmd` class. + """ + _cls = public.cmd + + def get_subcls(self): + class option0(public.option): + pass + class option1(public.option): + pass + class example(self.cls): + option_classes = (option0, option1) + return example + + def test_class(self): + assert self.cls.__bases__ == (plugable.Plugin,) + assert type(self.cls.options) == property + + def test_get_options(self): + """ + Tests the `get_options` method. + """ + assert list(self.cls().get_options()) == [] + sub = self.subcls() + for (i, proxy) in enumerate(sub.get_options()): + assert isinstance(proxy, plugable.Proxy) + assert read_only(proxy, 'name') == 'option%d' % i + assert proxy.implements(public.option) + assert i == 1 def test_obj(): -- cgit From 8aee8e060c8d155ea0798cb677e784a0a72fa7ab Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 11 Aug 2008 16:29:37 +0000 Subject: 106: Fixed some typos in cmd.__get_options(); added unit tests for cmd.options and cmd.normalize() --- ipalib/public.py | 11 +++++++---- ipalib/tests/test_public.py | 37 +++++++++++++++++++++++++++++++++++-- 2 files changed, 42 insertions(+), 6 deletions(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index baa1496a..10f91693 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -53,7 +53,8 @@ class option(plugable.Plugin): )) __rules = None - # type = unicode, int, float # Set in subclass + type = unicode + required = False def normalize(self, value): """ @@ -132,7 +133,7 @@ class cmd(plugable.Plugin): 'autofill', '__call__', 'get_doc', - 'opt', + 'options', )) __options = None @@ -156,7 +157,9 @@ class cmd(plugable.Plugin): """ for cls in self.option_classes: assert inspect.isclass(cls) - yield plugable.Proxy(option, cls()) + o = cls() + o.__lock__() + yield plugable.Proxy(option, o) def __get_options(self): """ @@ -164,7 +167,7 @@ class cmd(plugable.Plugin): """ if self.__options is None: self.__options = plugable.NameSpace(self.get_options()) - return self.__opt + return self.__options options = property(__get_options) def normalize_iter(self, kw): diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index 91d1f724..7e98b735 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -164,9 +164,12 @@ class test_cmd(ClassChecker): _cls = public.cmd def get_subcls(self): - class option0(public.option): + class my_option(public.option): + def normalize(self, value): + return super(my_option, self).normalize(value).lower() + class option0(my_option): pass - class option1(public.option): + class option1(my_option): pass class example(self.cls): option_classes = (option0, option1) @@ -188,6 +191,36 @@ class test_cmd(ClassChecker): assert proxy.implements(public.option) assert i == 1 + def test_options(self): + """ + Tests the `options` property. + """ + assert 'options' in self.cls.__public__ # Public + sub = self.subcls() + options = sub.options + assert type(options) == plugable.NameSpace + assert len(options) == 2 + for name in ('option0', 'option1'): + assert name in options + proxy = options[name] + assert getattr(options, name) is proxy + assert isinstance(proxy, plugable.Proxy) + assert proxy.name == name + + def test_normalize(self): + """ + Tests the `normalize` method. + """ + assert 'normalize' in self.cls.__public__ # Public + kw = dict( + option0='OPTION0', + option1='OPTION1', + option2='option2', + ) + norm = dict((k, v.lower()) for (k, v) in kw.items()) + sub = self.subcls() + assert sub.normalize(**kw) == norm + def test_obj(): cls = public.obj -- cgit From fd6c215d596912493fa582079a7ec6de45466446 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 11 Aug 2008 17:37:33 +0000 Subject: 107: Some cleanup in cmd; added unit tests for cmd.default() method --- ipalib/public.py | 21 +++++++++++---------- ipalib/tests/test_public.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 10 deletions(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index 10f91693..17d00f04 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -130,7 +130,8 @@ class option(plugable.Plugin): class cmd(plugable.Plugin): __public__ = frozenset(( 'normalize', - 'autofill', + 'default', + 'validate', '__call__', 'get_doc', 'options', @@ -182,11 +183,6 @@ class cmd(plugable.Plugin): def normalize(self, **kw): return dict(self.normalize_iter(kw)) - def validate(self, **kw): - for (key, value) in kw.items(): - if key in self.options: - self.options.validate(value) - def default(self, **kw): d = {} for opt in self.options: @@ -198,11 +194,16 @@ class cmd(plugable.Plugin): kw.update(d) return kw + def validate(self, **kw): + for (key, value) in kw.items(): + if key in self.options: + self.options.validate(value) + def __call__(self, **kw): - (args, kw) = self.normalize(*args, **kw) - (args, kw) = self.autofill(*args, **kw) - self.validate(*args, **kw) - self.execute(*args, **kw) + kw = self.normalize(**kw) + kw = self.default(**kw) + self.validate(**kw) + self.execute(**kw) diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index 7e98b735..8f0ac963 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -167,6 +167,13 @@ class test_cmd(ClassChecker): class my_option(public.option): def normalize(self, value): return super(my_option, self).normalize(value).lower() + @public.rule + def my_rule(self, value): + if value != self.name: + return 'must equal %s' % name + def default(self, **kw): + return kw['default_from'] + class option0(my_option): pass class option1(my_option): @@ -221,6 +228,28 @@ class test_cmd(ClassChecker): sub = self.subcls() assert sub.normalize(**kw) == norm + def test_default(self): + """ + Tests the `default` method. + """ + assert 'default' in self.cls.__public__ # Public + no_fill = dict( + option0='value0', + option1='value1', + whatever='hello world', + ) + fill = dict( + default_from='the default', + ) + filled = dict( + option0='the default', + option1='the default', + default_from='the default', + ) + sub = self.subcls() + assert sub.default(**no_fill) == no_fill + assert sub.default(**fill) == filled + def test_obj(): cls = public.obj -- cgit From 8a6ece2ffbfc142beb1d08e09809c388b3ede160 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 11 Aug 2008 17:57:07 +0000 Subject: 108: Changed cmd.default() so that it now only return dictionary of values for which defaults were generated; updated unit tests --- ipalib/public.py | 19 +++++++++---------- ipalib/tests/test_public.py | 7 +++---- 2 files changed, 12 insertions(+), 14 deletions(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index 17d00f04..d1c4fa2a 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -183,16 +183,15 @@ class cmd(plugable.Plugin): def normalize(self, **kw): return dict(self.normalize_iter(kw)) - def default(self, **kw): - d = {} - for opt in self.options: - if opt.name not in kw: - value = opt.default(**kw) + def default_iter(self, kw): + for option in self.options: + if option.name not in kw: + value = option.default(**kw) if value is not None: - d[opt.name] = value - assert not set(kw).intersection(d) - kw.update(d) - return kw + yield(option.name, value) + + def default(self, **kw): + return dict(self.default_iter(kw)) def validate(self, **kw): for (key, value) in kw.items(): @@ -201,7 +200,7 @@ class cmd(plugable.Plugin): def __call__(self, **kw): kw = self.normalize(**kw) - kw = self.default(**kw) + kw.update(self.default(**kw)) self.validate(**kw) self.execute(**kw) diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index 8f0ac963..4f3d382f 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -241,14 +241,13 @@ class test_cmd(ClassChecker): fill = dict( default_from='the default', ) - filled = dict( + default = dict( option0='the default', option1='the default', - default_from='the default', ) sub = self.subcls() - assert sub.default(**no_fill) == no_fill - assert sub.default(**fill) == filled + assert sub.default(**no_fill) == {} + assert sub.default(**fill) == default def test_obj(): -- cgit From 5313e5a491ceefe866a287cb4c320f0fee0474e2 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 11 Aug 2008 19:11:26 +0000 Subject: 109: Cleanups in cmd; added unit tests for cmd.validate() --- ipalib/public.py | 20 ++++++++++++++++---- ipalib/tests/test_public.py | 20 +++++++++++++++++++- 2 files changed, 35 insertions(+), 5 deletions(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index d1c4fa2a..88b08be5 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -46,13 +46,12 @@ class option(plugable.Plugin): __public__ = frozenset(( 'normalize', - 'validate', 'default', + 'validate', 'required', 'type', )) __rules = None - type = unicode required = False @@ -94,10 +93,11 @@ class option(plugable.Plugin): tuple is lazily initialized the first time the property is accessed. """ if self.__rules is None: - self.__rules = tuple(sorted( + rules = tuple(sorted( self.__rules_iter(), key=lambda f: getattr(f, '__name__'), )) + object.__setattr__(self, '_option__rules', rules) return self.__rules rules = property(__get_rules) @@ -132,6 +132,7 @@ class cmd(plugable.Plugin): 'normalize', 'default', 'validate', + 'execute', '__call__', 'get_doc', 'options', @@ -196,7 +197,18 @@ class cmd(plugable.Plugin): def validate(self, **kw): for (key, value) in kw.items(): if key in self.options: - self.options.validate(value) + self.options[key].validate(value) + + def execute(self, **kw) + pass + + def print_n_call(self, method, kw): + print '%s.%s(%s)' % ( + self.name, + method, + ' '.join('%s=%r' % (k, v) for (k, v) in kw.items()), + ) + getattr(self, method)(**kw) def __call__(self, **kw): kw = self.normalize(**kw) diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index 4f3d382f..81401859 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -170,7 +170,7 @@ class test_cmd(ClassChecker): @public.rule def my_rule(self, value): if value != self.name: - return 'must equal %s' % name + return 'must equal %r' % self.name def default(self, **kw): return kw['default_from'] @@ -249,6 +249,24 @@ class test_cmd(ClassChecker): assert sub.default(**no_fill) == {} assert sub.default(**fill) == default + def test_validate(self): + """ + Tests the `validate` method. + """ + assert 'validate' in self.cls.__public__ # Public + sub = self.subcls() + for name in ('option0', 'option1'): + okay = { + name: name, + 'another_option': 'some value', + } + fail = { + name: 'whatever', + 'another_option': 'some value', + } + sub.validate(**okay) + raises(errors.RuleError, sub.validate, **fail) + def test_obj(): cls = public.obj -- cgit From 92824182911007ce3e9cf4f858f70434594ee5dd Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 11 Aug 2008 19:35:57 +0000 Subject: 110: Started fleshing out more in cli.py --- ipalib/cli.py | 31 +++++++++++++++++++++++++++++++ ipalib/public.py | 5 ++--- ipalib/tests/test_cli.py | 19 +++++++++++++++++++ ipalib/tests/test_public.py | 7 +++++++ 4 files changed, 59 insertions(+), 3 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index 5e257f70..63988337 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -21,6 +21,8 @@ Functionality for Command Line Inteface. """ +import sys +import re def to_cli(name): """ @@ -38,3 +40,32 @@ def from_cli(cli_name): """ assert isinstance(cli_name, basestring) return cli_name.replace('-', '_') + + +class CLI(object): + def __init__(self, api): + self.__api = api + + def __get_api(self): + return self.__api + api = property(__get_api) + + def print_commands(self): + for cmd in self.api.cmd: + print to_cli(cmd.name) + + def run(self): + if len(sys.argv) < 2: + self.print_commands() + print 'Usage: ipa COMMAND [OPTIONS]' + sys.exit(2) + return + name= sys.argv[1] + if name == '_api_': + print_api() + sys.exit() + elif name not in api.cmd: + print_commands() + print 'ipa: ERROR: unknown command %r' % name + sys.exit(2) + api.cmd[name]() diff --git a/ipalib/public.py b/ipalib/public.py index 88b08be5..48e19ff4 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -199,7 +199,7 @@ class cmd(plugable.Plugin): if key in self.options: self.options[key].validate(value) - def execute(self, **kw) + def execute(self, **kw): pass def print_n_call(self, method, kw): @@ -214,8 +214,7 @@ class cmd(plugable.Plugin): kw = self.normalize(**kw) kw.update(self.default(**kw)) self.validate(**kw) - self.execute(**kw) - + return self.execute(**kw) class obj(plugable.Plugin): diff --git a/ipalib/tests/test_cli.py b/ipalib/tests/test_cli.py index 91bc0a29..b47aff3a 100644 --- a/ipalib/tests/test_cli.py +++ b/ipalib/tests/test_cli.py @@ -21,6 +21,7 @@ Unit tests for `ipalib.cli` module. """ +from tstutil import raises, getitem, no_set, no_del, read_only, ClassChecker from ipalib import cli @@ -34,3 +35,21 @@ def test_from_cli(): f = cli.from_cli assert f('initialize') == 'initialize' assert f('user-add') == 'user_add' + + +class test_CLI(ClassChecker): + """ + Tests the `CLI` class. + """ + _cls = cli.CLI + + def test_class(self): + assert type(self.cls.api) is property + + def test_api(self): + """ + Tests the `api` property. + """ + api = 'the plugable.API instance' + o = self.cls(api) + assert read_only(o, 'api') is api diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index 81401859..d6f3dbbe 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -267,6 +267,13 @@ class test_cmd(ClassChecker): sub.validate(**okay) raises(errors.RuleError, sub.validate, **fail) + def test_execute(self): + """ + Tests the `execute` method. + """ + assert 'execute' in self.cls.__public__ # Public + + def test_obj(): cls = public.obj -- cgit From c1a125256b302eceebcee5464f1447fc8e49fdf7 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 11 Aug 2008 21:14:07 +0000 Subject: 111: Minor changes to mthd and prop classes; updated mthd and prop unit tests --- ipalib/public.py | 24 ++++++++++++++---------- ipalib/tests/test_public.py | 23 ++++++++++++++++------- 2 files changed, 30 insertions(+), 17 deletions(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index 48e19ff4..95ba7554 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -136,7 +136,6 @@ class cmd(plugable.Plugin): '__call__', 'get_doc', 'options', - )) __options = None option_classes = tuple() @@ -248,6 +247,10 @@ class obj(plugable.Plugin): class attr(plugable.Plugin): + __public__ = frozenset(( + 'obj', + 'obj_name', + )) __obj = None def __init__(self): @@ -278,17 +281,18 @@ class attr(plugable.Plugin): class mthd(attr, cmd): - __public__ = frozenset(( - 'obj', - 'obj_name', - )) + __public__ = attr.__public__.union(cmd.__public__) + def get_options(self): + for proxy in cmd.get_options(self): + yield proxy + if self.obj is not None and self.obj.prop is not None: + for proxy in self.obj.prop: + yield proxy -class prop(attr): - __public__ = frozenset(( - 'obj', - 'obj_name', - )) + +class prop(attr, option): + __public__ = attr.__public__.union(option.__public__) def get_doc(self, _): return _('prop doc') diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index d6f3dbbe..132cd1cc 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -300,15 +300,24 @@ def test_attr(): assert read_only(i, 'obj') == 'the user obj' -def test_mthd(): - cls = public.mthd - assert issubclass(cls, public.attr) - assert issubclass(cls, public.cmd) +class test_mthd(ClassChecker): + """ + Tests the `mthd` class. + """ + _cls = public.mthd + + def test_class(self): + assert self.cls.__bases__ == (public.attr, public.cmd) + assert self.cls.implements(public.cmd) -def test_prop(): - cls = public.prop - assert issubclass(cls, public.attr) +class test_prop(ClassChecker): + _cls = public.prop + + def test_class(self): + assert self.cls.__bases__ == (public.attr, public.option) + assert self.cls.implements(public.option) + def test_PublicAPI(): -- cgit From afdbc42b2e721012daf7020430353c0686fcc5c3 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 11 Aug 2008 21:38:30 +0000 Subject: 112: More work on cli.py --- ipalib/cli.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index 63988337..e0ba11f8 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -24,6 +24,7 @@ Functionality for Command Line Inteface. import sys import re + def to_cli(name): """ Takes a Python identifier and transforms it into form suitable for the @@ -54,18 +55,23 @@ class CLI(object): for cmd in self.api.cmd: print to_cli(cmd.name) + def __contains__(self, key): + return from_cli(key) in self.api.cmd + + def __getitem__(self, key): + return self.api.cmd[from_cli(key)] + def run(self): if len(sys.argv) < 2: self.print_commands() print 'Usage: ipa COMMAND [OPTIONS]' sys.exit(2) - return - name= sys.argv[1] - if name == '_api_': - print_api() - sys.exit() - elif name not in api.cmd: - print_commands() - print 'ipa: ERROR: unknown command %r' % name + cmd = sys.argv[1] + if cmd not in self: + self.print_commands() + print 'ipa: ERROR: unknown command %r' % cmd sys.exit(2) - api.cmd[name]() + self.run_cmd(cmd) + + def run_cmd(self, cmd): + print self[cmd] -- cgit From 902614a76297f6c3e3d329df6a6a5010765a37f5 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 11 Aug 2008 22:12:23 +0000 Subject: 113: Fixed regex used in attr.__init__(); added unit tests for mthd.get_options() --- ipalib/public.py | 2 +- ipalib/tests/test_public.py | 42 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 2 deletions(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index 95ba7554..57e9c002 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -254,7 +254,7 @@ class attr(plugable.Plugin): __obj = None def __init__(self): - m = re.match('^([a-z]+)_([a-z]+)$', self.__class__.__name__) + m = re.match('^([a-z][a-z0-9]+)_([a-z][a-z0-9]+)$', self.__class__.__name__) assert m self.__obj_name = m.group(1) self.__attr_name = m.group(2) diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index 132cd1cc..4a445140 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -299,6 +299,10 @@ def test_attr(): assert read_only(i, 'api') is api assert read_only(i, 'obj') == 'the user obj' + class example_prop0(cls): + pass + o = example_prop0() + class test_mthd(ClassChecker): """ @@ -310,6 +314,43 @@ class test_mthd(ClassChecker): assert self.cls.__bases__ == (public.attr, public.cmd) assert self.cls.implements(public.cmd) + def get_subcls(self): + class option0(public.option): + pass + class option1(public.option): + pass + class example_prop0(public.prop): + pass + class example_prop1(public.prop): + pass + class example_obj(object): + __prop = None + def __get_prop(self): + if self.__prop is None: + self.__prop = ( + plugable.Proxy(public.prop, example_prop0(), 'attr_name'), + plugable.Proxy(public.prop, example_prop1(), 'attr_name'), + ) + return self.__prop + prop = property(__get_prop) + class noun_verb(self.cls): + option_classes = (option0, option1) + obj = example_obj() + return noun_verb + + def test_get_options(self): + """ + Tests the `get_options` method. + """ + sub = self.subcls() + names = ('option0', 'option1', 'prop0', 'prop1') + proxies = tuple(sub.get_options()) + assert len(proxies) == 4 + for (i, proxy) in enumerate(proxies): + assert proxy.name == names[i] + assert isinstance(proxy, plugable.Proxy) + assert proxy.implements(public.option) + class test_prop(ClassChecker): _cls = public.prop @@ -319,7 +360,6 @@ class test_prop(ClassChecker): assert self.cls.implements(public.option) - def test_PublicAPI(): cls = public.PublicAPI assert issubclass(cls, plugable.API) -- cgit From bc4b26ffca7b48db70006c99ddb6084542b1df88 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 12 Aug 2008 02:03:47 +0000 Subject: 114: Fixed cmd.__get_options(); more work on CLI --- ipalib/cli.py | 19 +++++++++++++++++-- ipalib/plugins.py | 17 ++++++++++------- ipalib/public.py | 5 ++++- 3 files changed, 31 insertions(+), 10 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index e0ba11f8..40edc890 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -21,8 +21,9 @@ Functionality for Command Line Inteface. """ -import sys import re +import sys +import optparse def to_cli(name): @@ -43,6 +44,10 @@ def from_cli(cli_name): return cli_name.replace('-', '_') +def _(arg): + return arg + + class CLI(object): def __init__(self, api): self.__api = api @@ -74,4 +79,14 @@ class CLI(object): self.run_cmd(cmd) def run_cmd(self, cmd): - print self[cmd] + (options, args) = self.build_parser(cmd) + print options + + def build_parser(self, cmd): + parser = optparse.OptionParser() + for option in self[cmd].options: + parser.add_option('--%s' % to_cli(option.name), + help=option.get_doc(_), + ) + + (options, args) parser.parse_args() diff --git a/ipalib/plugins.py b/ipalib/plugins.py index 90bc184f..68731044 100644 --- a/ipalib/plugins.py +++ b/ipalib/plugins.py @@ -60,16 +60,19 @@ api.register(user_find) # Register some properties for the 'user' object: -class user_firstname(public.prop): - pass -api.register(user_firstname) +class user_givenname(public.prop): + def get_doc(self, _): + return _('user first name') +api.register(user_givenname) -class user_lastname(public.prop): - pass -api.register(user_lastname) +class user_sn(public.prop): + def get_doc(self, _): + return _('user last name') +api.register(user_sn) class user_login(public.prop): - pass + def get_doc(self, _): + return _('user login') api.register(user_login) diff --git a/ipalib/public.py b/ipalib/public.py index 57e9c002..e66e1368 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -45,6 +45,7 @@ class option(plugable.Plugin): """ __public__ = frozenset(( + 'get_doc', 'normalize', 'default', 'validate', @@ -167,7 +168,9 @@ class cmd(plugable.Plugin): Returns the NameSpace containing the option proxy objects. """ if self.__options is None: - self.__options = plugable.NameSpace(self.get_options()) + object.__setattr__(self, '_cmd__options', + plugable.NameSpace(self.get_options()), + ) return self.__options options = property(__get_options) -- cgit From 99d7638ff5c5cddb4f23d25ad13ef122476d5679 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 12 Aug 2008 16:49:23 +0000 Subject: 115: CLI now parses out kw args; cmd.__call__() now uses print_n_call() to give feedback on the calling --- ipalib/cli.py | 28 ++++++++++++++-------------- ipalib/public.py | 12 ++++++------ ipalib/tests/test_cli.py | 12 ++++++++++++ 3 files changed, 32 insertions(+), 20 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index 40edc890..ad54d77a 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -76,17 +76,17 @@ class CLI(object): self.print_commands() print 'ipa: ERROR: unknown command %r' % cmd sys.exit(2) - self.run_cmd(cmd) - - def run_cmd(self, cmd): - (options, args) = self.build_parser(cmd) - print options - - def build_parser(self, cmd): - parser = optparse.OptionParser() - for option in self[cmd].options: - parser.add_option('--%s' % to_cli(option.name), - help=option.get_doc(_), - ) - - (options, args) parser.parse_args() + self.run_cmd(cmd, sys.argv[2:]) + + def run_cmd(self, cmd, args): + kw = dict(self.parse_kw(args)) + self[cmd](**kw) + + def parse_kw(self, args): + for arg in args: + m = re.match(r'^--([a-z][-a-z0-9]*)=(.+)$', arg) + if m is not None: + yield ( + from_cli(m.group(1)), + m.group(2), + ) diff --git a/ipalib/public.py b/ipalib/public.py index e66e1368..c1e644d5 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -208,15 +208,15 @@ class cmd(plugable.Plugin): print '%s.%s(%s)' % ( self.name, method, - ' '.join('%s=%r' % (k, v) for (k, v) in kw.items()), + ', '.join('%s=%r' % (k, v) for (k, v) in kw.items()), ) - getattr(self, method)(**kw) + return getattr(self, method)(**kw) def __call__(self, **kw): - kw = self.normalize(**kw) - kw.update(self.default(**kw)) - self.validate(**kw) - return self.execute(**kw) + kw = self.print_n_call('normalize', kw) + kw.update(self.print_n_call('default', kw)) + self.print_n_call('validate', kw) + return self.print_n_call('execute', kw) class obj(plugable.Plugin): diff --git a/ipalib/tests/test_cli.py b/ipalib/tests/test_cli.py index b47aff3a..d56a4b19 100644 --- a/ipalib/tests/test_cli.py +++ b/ipalib/tests/test_cli.py @@ -53,3 +53,15 @@ class test_CLI(ClassChecker): api = 'the plugable.API instance' o = self.cls(api) assert read_only(o, 'api') is api + + def test_parse_kw(self): + """ + Tests the `parse_kw` method. + """ + o = self.cls(None) + kw = dict( + hello='world', + how_are='you', + ) + args = tuple('--%s=%s' % (cli.to_cli(k), v) for (k,v) in kw.items()) + assert dict(o.parse_kw(args)) == kw -- cgit From 7bbeb2db69ffec5a0372e614a3b7eb5194e3b773 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 12 Aug 2008 17:42:21 +0000 Subject: 116: Added a user_initials property plugin to demostrate default() method --- ipalib/plugins.py | 17 ++++++++++++++--- ipalib/public.py | 16 ++++++++++------ 2 files changed, 24 insertions(+), 9 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugins.py b/ipalib/plugins.py index 68731044..f33d69be 100644 --- a/ipalib/plugins.py +++ b/ipalib/plugins.py @@ -60,6 +60,11 @@ api.register(user_find) # Register some properties for the 'user' object: +class user_login(public.prop): + def get_doc(self, _): + return _('user login') +api.register(user_login) + class user_givenname(public.prop): def get_doc(self, _): return _('user first name') @@ -70,10 +75,16 @@ class user_sn(public.prop): return _('user last name') api.register(user_sn) -class user_login(public.prop): +class user_initials(public.prop): def get_doc(self, _): - return _('user login') -api.register(user_login) + return _('user initials') + def default(self, **kw): + givenname = kw.get('givenname', None) + sn = kw.get('sn', None) + if givenname is None or sn is None: + return None + return '%s%s' % (givenname[0], sn[0]) +api.register(user_initials) # Register some methods for the 'group' object: diff --git a/ipalib/public.py b/ipalib/public.py index c1e644d5..86323822 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -184,6 +184,7 @@ class cmd(plugable.Plugin): yield (key, value) def normalize(self, **kw): + self.print_call('normalize', kw) return dict(self.normalize_iter(kw)) def default_iter(self, kw): @@ -194,29 +195,32 @@ class cmd(plugable.Plugin): yield(option.name, value) def default(self, **kw): + self.print_call('default', kw) return dict(self.default_iter(kw)) def validate(self, **kw): + self.print_call('validate', kw) for (key, value) in kw.items(): if key in self.options: self.options[key].validate(value) def execute(self, **kw): + self.print_call('execute', kw) pass - def print_n_call(self, method, kw): + def print_call(self, method, kw): print '%s.%s(%s)' % ( self.name, method, ', '.join('%s=%r' % (k, v) for (k, v) in kw.items()), ) - return getattr(self, method)(**kw) def __call__(self, **kw): - kw = self.print_n_call('normalize', kw) - kw.update(self.print_n_call('default', kw)) - self.print_n_call('validate', kw) - return self.print_n_call('execute', kw) + self.print_call('__call__', kw) + kw = self.normalize(**kw) + kw.update(self.default(**kw)) + self.validate(**kw) + self.execute(**kw) class obj(plugable.Plugin): -- cgit From 4acb7567c424d2d2525ff23f0cede3e4467d0ba1 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 12 Aug 2008 18:02:49 +0000 Subject: 117: Improved readability of cmd.print_call() --- ipalib/public.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index 86323822..0dcf71d6 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -184,7 +184,7 @@ class cmd(plugable.Plugin): yield (key, value) def normalize(self, **kw): - self.print_call('normalize', kw) + self.print_call('normalize', kw, 1) return dict(self.normalize_iter(kw)) def default_iter(self, kw): @@ -195,27 +195,29 @@ class cmd(plugable.Plugin): yield(option.name, value) def default(self, **kw): - self.print_call('default', kw) + self.print_call('default', kw, 1) return dict(self.default_iter(kw)) def validate(self, **kw): - self.print_call('validate', kw) + self.print_call('validate', kw, 1) for (key, value) in kw.items(): if key in self.options: self.options[key].validate(value) def execute(self, **kw): - self.print_call('execute', kw) + self.print_call('execute', kw, 1) pass - def print_call(self, method, kw): - print '%s.%s(%s)' % ( + def print_call(self, method, kw, tab=0): + print '%s%s.%s(%s)\n' % ( + ' ' * (tab *2), self.name, method, ', '.join('%s=%r' % (k, v) for (k, v) in kw.items()), ) def __call__(self, **kw): + print '' self.print_call('__call__', kw) kw = self.normalize(**kw) kw.update(self.default(**kw)) -- cgit From 9ac8a8b49984ec5727b9da4803ae7823a9e44e13 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 12 Aug 2008 19:22:48 +0000 Subject: 118: Added user_login.default() method as another example; print_call() now prints sorted by the keys in ascending order --- ipalib/plugins.py | 16 +++++++++++----- ipalib/public.py | 2 +- 2 files changed, 12 insertions(+), 6 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugins.py b/ipalib/plugins.py index f33d69be..c8eede0b 100644 --- a/ipalib/plugins.py +++ b/ipalib/plugins.py @@ -60,11 +60,6 @@ api.register(user_find) # Register some properties for the 'user' object: -class user_login(public.prop): - def get_doc(self, _): - return _('user login') -api.register(user_login) - class user_givenname(public.prop): def get_doc(self, _): return _('user first name') @@ -75,6 +70,17 @@ class user_sn(public.prop): return _('user last name') api.register(user_sn) +class user_login(public.prop): + def get_doc(self, _): + return _('user login') + def default(self, **kw): + givenname = kw.get('givenname', None) + sn = kw.get('sn', None) + if givenname is None or sn is None: + return None + return ('%s%s' % (givenname[0], sn)).lower() +api.register(user_login) + class user_initials(public.prop): def get_doc(self, _): return _('user initials') diff --git a/ipalib/public.py b/ipalib/public.py index 0dcf71d6..e173bdd5 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -213,7 +213,7 @@ class cmd(plugable.Plugin): ' ' * (tab *2), self.name, method, - ', '.join('%s=%r' % (k, v) for (k, v) in kw.items()), + ', '.join('%s=%r' % (k, kw[k]) for k in sorted(kw)), ) def __call__(self, **kw): -- cgit From 86405236325204cb5750ce79f674a5ab01114fa7 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 12 Aug 2008 21:45:54 +0000 Subject: 119: Added ProxyTarget.implemented_by() classmethod; added corresponding unit tests --- ipalib/plugable.py | 12 ++++++++++++ ipalib/tests/test_plugable.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index b607f0fa..a91063e7 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -135,6 +135,18 @@ class ProxyTarget(ReadOnly): "must be str, frozenset, or have frozenset '__public__' attribute" ) + @classmethod + def implemented_by(cls, arg): + if inspect.isclass(arg): + subclass = arg + else: + subclass = arg.__class__ + assert issubclass(subclass, cls), 'must be subclass of %r' % cls + for name in cls.__public__: + if not hasattr(subclass, name): + return False + return True + class Proxy(ReadOnly): """ diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index b8242dce..8ce98610 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -183,6 +183,49 @@ class test_ProxyTarget(ClassChecker): assert ex.implements(any_object) assert ex.implements(any_object()) + def test_implemented_by(self): + """ + Tests the `implemented_by` classmethod. + """ + class base(self.cls): + __public__ = frozenset(( + 'attr0', + 'attr1', + 'attr2', + )) + + class okay(base): + def attr0(self): + pass + def __get_attr1(self): + assert False # Make sure property isn't accesed on instance + attr1 = property(__get_attr1) + attr2 = 'hello world' + another_attr = 'whatever' + + class fail(base): + def __init__(self): + # Check that class, not instance is inspected: + self.attr2 = 'hello world' + def attr0(self): + pass + def __get_attr1(self): + assert False # Make sure property isn't accesed on instance + attr1 = property(__get_attr1) + another_attr = 'whatever' + + # Test that AssertionError is raised trying to pass something not + # subclass nor instance of base: + raises(AssertionError, base.implemented_by, object) + + # Test on subclass with needed attributes: + assert base.implemented_by(okay) is True + assert base.implemented_by(okay()) is True + + # Test on subclass *without* needed attributes: + assert base.implemented_by(fail) is False + assert base.implemented_by(fail()) is False + class test_Proxy(ClassChecker): """ -- cgit From f767543fe71db6fb840ad8f328158fe0c6d65ad4 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 12 Aug 2008 22:40:06 +0000 Subject: 120: Moved ProxyTarget below Proxy to emphasize relationship with Plugin; added docstrings for ProxyTarget.implements() and implemented_by() classmethods; fixed typo in Plugin.finalize() docstring --- ipalib/plugable.py | 118 ++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 81 insertions(+), 37 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index a91063e7..230e8ee2 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -112,42 +112,6 @@ class ReadOnly(object): return object.__delattr__(self, name) -class ProxyTarget(ReadOnly): - __public__ = frozenset() - - def __get_name(self): - """ - Convenience property to return the class name. - """ - return self.__class__.__name__ - name = property(__get_name) - - @classmethod - def implements(cls, arg): - assert type(cls.__public__) is frozenset - if isinstance(arg, str): - return arg in cls.__public__ - if type(getattr(arg, '__public__', None)) is frozenset: - return cls.__public__.issuperset(arg.__public__) - if type(arg) is frozenset: - return cls.__public__.issuperset(arg) - raise TypeError( - "must be str, frozenset, or have frozenset '__public__' attribute" - ) - - @classmethod - def implemented_by(cls, arg): - if inspect.isclass(arg): - subclass = arg - else: - subclass = arg.__class__ - assert issubclass(subclass, cls), 'must be subclass of %r' % cls - for name in cls.__public__: - if not hasattr(subclass, name): - return False - return True - - class Proxy(ReadOnly): """ Allows access to only certain attributes on its target object (a @@ -251,6 +215,86 @@ class Proxy(ReadOnly): ) +class ProxyTarget(ReadOnly): + __public__ = frozenset() + + def __get_name(self): + """ + Convenience property to return the class name. + """ + return self.__class__.__name__ + name = property(__get_name) + + @classmethod + def implements(cls, arg): + """ + Returns True if this cls.__public__ frozenset contains `arg`; + returns False otherwise. + + There are three different ways this can be called: + + 1. With a argument, e.g.: + + >>> class base(ProxyTarget): + >>> __public__ = frozenset(['some_attr', 'another_attr']) + >>> base.implements('some_attr') + True + >>> base.implements('an_unknown_attribute') + False + + 2. With a argument, e.g.: + + >>> base.implements(frozenset(['some_attr'])) + True + >>> base.implements(frozenset(['some_attr', 'an_unknown_attribute'])) + False + + 3. With any object that has a `__public__` attribute that is + , e.g.: + + >>> class whatever(object): + >>> __public__ = frozenset(['another_attr']) + >>> base.implements(whatever) + True + + Unlike ProxyTarget.implemented_by(), this returns an abstract answer + because only the __public__ frozenset is checked... a ProxyTarget + need not itself have attributes for all names in __public__ + (subclasses might provide them). + """ + assert type(cls.__public__) is frozenset + if isinstance(arg, str): + return arg in cls.__public__ + if type(getattr(arg, '__public__', None)) is frozenset: + return cls.__public__.issuperset(arg.__public__) + if type(arg) is frozenset: + return cls.__public__.issuperset(arg) + raise TypeError( + "must be str, frozenset, or have frozenset '__public__' attribute" + ) + + @classmethod + def implemented_by(cls, arg): + """ + Returns True if (1) `arg` is an instance of or subclass of this class, + and (2) `arg` (or `arg.__class__` if instance) has an attribute for + each name in this class's __public__ frozenset; returns False + otherwise. + + Unlike ProxyTarget.implements(), this returns a concrete answer + because the attributes of the subclass are checked. + """ + if inspect.isclass(arg): + subclass = arg + else: + subclass = arg.__class__ + assert issubclass(subclass, cls), 'must be subclass of %r' % cls + for name in cls.__public__: + if not hasattr(subclass, name): + return False + return True + + class Plugin(ProxyTarget): """ Base class for all plugins. @@ -270,7 +314,7 @@ class Plugin(ProxyTarget): """ After all the plugins are instantiated, the plugable.API calls this method, passing itself as the only argument. This is where plugins - should check that other plugins they depend upon have actually be + should check that other plugins they depend upon have actually been loaded. """ assert self.__api is None, 'finalize() can only be called once' -- cgit From b72cfa5dcc488f3b497fa05a88985cc8f790cc00 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 12 Aug 2008 22:52:37 +0000 Subject: 121: Renamed API.__call__() method to API.finalize() --- ipalib/plugable.py | 2 +- ipalib/startup.py | 2 +- ipalib/tests/test_plugable.py | 2 +- ipalib/tests/test_public.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 230e8ee2..8241d8ea 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -485,7 +485,7 @@ class API(ReadOnly): self.register = Registrar(*allowed) self.__lock__() - def __call__(self): + def finalize(self): """ Finalize the registration, instantiate the plugins. """ diff --git a/ipalib/startup.py b/ipalib/startup.py index edc14405..cfeb57b1 100644 --- a/ipalib/startup.py +++ b/ipalib/startup.py @@ -28,4 +28,4 @@ unnecessary side effects (needed for unit tests, among other things). from run import api import plugins -api() +api.finalize() diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index 8ce98610..3b082082 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -601,7 +601,7 @@ def test_API(): r(base1_plugin2) # Test API instance: - api() # Calling instance performs finalization + api.finalize() def get_base(b): return 'base%d' % b diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index 4a445140..eb19f28a 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -374,4 +374,4 @@ def test_PublicAPI(): pass api.register(cmd2) - api() + api.finalize() -- cgit From 64054a673c23b543450741fa11333bc627efeca3 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 12 Aug 2008 23:33:02 +0000 Subject: 122: The dictorary interface to CLI now has keys build using to_cli(), rather than converting at each call --- ipalib/cli.py | 17 +++++++++++++++-- ipalib/startup.py | 2 -- 2 files changed, 15 insertions(+), 4 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index ad54d77a..a0b8800f 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -49,6 +49,8 @@ def _(arg): class CLI(object): + __d = None + def __init__(self, api): self.__api = api @@ -61,12 +63,23 @@ class CLI(object): print to_cli(cmd.name) def __contains__(self, key): - return from_cli(key) in self.api.cmd + assert self.__d is not None, 'you must call finalize() first' + return key in self.__d def __getitem__(self, key): - return self.api.cmd[from_cli(key)] + assert self.__d is not None, 'you must call finalize() first' + return self.__d[key] + + def finalize(self): + api = self.api + api.finalize() + def d_iter(): + for cmd in api.cmd: + yield (to_cli(cmd.name), cmd) + self.__d = dict(d_iter()) def run(self): + self.finalize() if len(sys.argv) < 2: self.print_commands() print 'Usage: ipa COMMAND [OPTIONS]' diff --git a/ipalib/startup.py b/ipalib/startup.py index cfeb57b1..4879a8e6 100644 --- a/ipalib/startup.py +++ b/ipalib/startup.py @@ -27,5 +27,3 @@ unnecessary side effects (needed for unit tests, among other things). from run import api import plugins - -api.finalize() -- cgit From 7db3aae1b26588b3650dae442b07dca0f33ab0c8 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 12 Aug 2008 23:40:36 +0000 Subject: 123: API.finalize() now raises AssetionError if called more than once; added corresponding unit tests --- ipalib/plugable.py | 4 ++++ ipalib/tests/test_plugable.py | 3 +++ 2 files changed, 7 insertions(+) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 8241d8ea..71f03357 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -480,6 +480,8 @@ class Registrar(ReadOnly): class API(ReadOnly): + __finalized = False + def __init__(self, *allowed): self.__keys = tuple(b.__name__ for b in allowed) self.register = Registrar(*allowed) @@ -489,6 +491,7 @@ class API(ReadOnly): """ Finalize the registration, instantiate the plugins. """ + assert not self.__finalized, 'finalize() can only be called once' d = {} def plugin_iter(base, classes): for cls in classes: @@ -506,6 +509,7 @@ class API(ReadOnly): plugin.__lock__() assert plugin.__islocked__() is True assert plugin.api is self + object.__setattr__(self, '_API__finalized', True) def __iter__(self): for key in self.__keys: diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index 3b082082..89bb948e 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -622,3 +622,6 @@ def test_API(): assert proxy.name == plugin_name assert read_only(ns, plugin_name) is proxy assert read_only(proxy, 'method')(7) == 7 + b + + # Test that calling finilize again raises AssertionError: + raises(AssertionError, api.finalize) -- cgit From 70cbe10624e685f1dac6a898665048972665b97f Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 12 Aug 2008 23:45:36 +0000 Subject: 124: Fixed case in example in ReadOnly class docstring --- ipalib/plugable.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 71f03357..89eb423b 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -53,7 +53,7 @@ class ReadOnly(object): For example: - >>> class givenName(ReadOnly): + >>> class givenname(ReadOnly): >>> def __init__(self): >>> self.whatever = 'some value' # Hasn't been locked yet >>> self.__lock__() -- cgit From d5b0bc1b544a137b6767b13f88f5e688dab15226 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 13 Aug 2008 00:40:13 +0000 Subject: 125: Added some generic auto-import stuff --- ipalib/Plugins/__init__.py | 24 +++++++++++++++++++ ipalib/Plugins/mod1.py | 24 +++++++++++++++++++ ipalib/Plugins/mod2.py | 24 +++++++++++++++++++ ipalib/util.py | 57 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 129 insertions(+) create mode 100644 ipalib/Plugins/__init__.py create mode 100644 ipalib/Plugins/mod1.py create mode 100644 ipalib/Plugins/mod2.py create mode 100644 ipalib/util.py (limited to 'ipalib') diff --git a/ipalib/Plugins/__init__.py b/ipalib/Plugins/__init__.py new file mode 100644 index 00000000..e6325649 --- /dev/null +++ b/ipalib/Plugins/__init__.py @@ -0,0 +1,24 @@ +# Authors: +# Jason Gerard DeRose +# +# 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 + +""" +Sub-package containing all internal plugins. +""" + +print 'imported Plugins' diff --git a/ipalib/Plugins/mod1.py b/ipalib/Plugins/mod1.py new file mode 100644 index 00000000..2dd36d57 --- /dev/null +++ b/ipalib/Plugins/mod1.py @@ -0,0 +1,24 @@ +# Authors: +# Jason Gerard DeRose +# +# 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 + +""" +Test module. +""" + +print 'loaded mod1' diff --git a/ipalib/Plugins/mod2.py b/ipalib/Plugins/mod2.py new file mode 100644 index 00000000..6c75c5cd --- /dev/null +++ b/ipalib/Plugins/mod2.py @@ -0,0 +1,24 @@ +# Authors: +# Jason Gerard DeRose +# +# 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 + +""" +Test module. +""" + +print 'loaded mod2' diff --git a/ipalib/util.py b/ipalib/util.py new file mode 100644 index 00000000..b6240951 --- /dev/null +++ b/ipalib/util.py @@ -0,0 +1,57 @@ +# Authors: +# Jason Gerard DeRose +# +# 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 + +""" + +""" + +import os +from os import path + +__import__('Plugins', globals(), locals(), [], -1) + + + +def import_plugins(): + plugins = 'Plugins' + d = path.join(path.dirname(path.abspath(__file__)), plugins) + assert path.isdir(d) and not path.islink(d), 'not regular dir: %r' % d + print d + suffix = '.py' + for name in os.listdir(d): + if not name.endswith(suffix): + continue + if name.startswith('__init__.'): + continue + print name + mod = name[:len(suffix)+1] + __import__( + '%s.%s' % (plugins, mod), + globals(), + locals(), + [], + -1 + ) + + + + +if __name__ == '__main__': + pass + import_plugins() -- cgit From f8d9a62c29c603d9784498c77bc03dd74400d616 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 13 Aug 2008 00:43:23 +0000 Subject: 126: Renamed plugins.py to Plugins/example.py --- ipalib/Plugins/example.py | 155 ++++++++++++++++++++++++++++++++++++++++++++++ ipalib/plugins.py | 155 ---------------------------------------------- 2 files changed, 155 insertions(+), 155 deletions(-) create mode 100644 ipalib/Plugins/example.py delete mode 100644 ipalib/plugins.py (limited to 'ipalib') diff --git a/ipalib/Plugins/example.py b/ipalib/Plugins/example.py new file mode 100644 index 00000000..c8eede0b --- /dev/null +++ b/ipalib/Plugins/example.py @@ -0,0 +1,155 @@ +# Authors: +# Jason Gerard DeRose +# +# 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 + +""" +Some example plugins. +""" + +import public +from run import api + + +# Hypothetical functional commands (not associated with any object): +class krbtest(public.cmd): + def get_doc(self, _): + return _('test your Kerberos ticket') +api.register(krbtest) + +class discover(public.cmd): + def get_doc(self, _): + return _('discover IPA servers on network') +api.register(discover) + + +# Register some methods for the 'user' object: +class user_add(public.mthd): + def get_doc(self, _): + return _('add new user') +api.register(user_add) + +class user_del(public.mthd): + def get_doc(self, _): + return _('delete existing user') +api.register(user_del) + +class user_mod(public.mthd): + def get_doc(self, _): + return _('edit existing user') +api.register(user_mod) + +class user_find(public.mthd): + def get_doc(self, _): + return _('search for users') +api.register(user_find) + + +# Register some properties for the 'user' object: +class user_givenname(public.prop): + def get_doc(self, _): + return _('user first name') +api.register(user_givenname) + +class user_sn(public.prop): + def get_doc(self, _): + return _('user last name') +api.register(user_sn) + +class user_login(public.prop): + def get_doc(self, _): + return _('user login') + def default(self, **kw): + givenname = kw.get('givenname', None) + sn = kw.get('sn', None) + if givenname is None or sn is None: + return None + return ('%s%s' % (givenname[0], sn)).lower() +api.register(user_login) + +class user_initials(public.prop): + def get_doc(self, _): + return _('user initials') + def default(self, **kw): + givenname = kw.get('givenname', None) + sn = kw.get('sn', None) + if givenname is None or sn is None: + return None + return '%s%s' % (givenname[0], sn[0]) +api.register(user_initials) + + +# Register some methods for the 'group' object: +class group_add(public.mthd): + def get_doc(self, _): + return _('add new group') +api.register(group_add) + +class group_del(public.mthd): + def get_doc(self, _): + return _('delete existing group') +api.register(group_del) + +class group_mod(public.mthd): + def get_doc(self, _): + return _('edit existing group') +api.register(group_mod) + +class group_find(public.mthd): + def get_doc(self, _): + return _('search for groups') +api.register(group_find) + + +# Register some methods for the 'service' object +class service_add(public.mthd): + def get_doc(self, _): + return _('add new service') +api.register(service_add) + +class service_del(public.mthd): + def get_doc(self, _): + return _('delete existing service') +api.register(service_del) + +class service_mod(public.mthd): + def get_doc(self, _): + return _('edit existing service') +api.register(service_mod) + +class service_find(public.mthd): + def get_doc(self, _): + return _('search for services') +api.register(service_find) + + +# And to emphasis that the registration order doesn't matter, +# we'll register the objects last: +class group(public.obj): + def get_doc(self, _): + return _('') +api.register(group) + +class service(public.obj): + def get_doc(self, _): + return _('') +api.register(service) + +class user(public.obj): + def get_doc(self, _): + return _('') +api.register(user) diff --git a/ipalib/plugins.py b/ipalib/plugins.py deleted file mode 100644 index c8eede0b..00000000 --- a/ipalib/plugins.py +++ /dev/null @@ -1,155 +0,0 @@ -# Authors: -# Jason Gerard DeRose -# -# 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 - -""" -Some example plugins. -""" - -import public -from run import api - - -# Hypothetical functional commands (not associated with any object): -class krbtest(public.cmd): - def get_doc(self, _): - return _('test your Kerberos ticket') -api.register(krbtest) - -class discover(public.cmd): - def get_doc(self, _): - return _('discover IPA servers on network') -api.register(discover) - - -# Register some methods for the 'user' object: -class user_add(public.mthd): - def get_doc(self, _): - return _('add new user') -api.register(user_add) - -class user_del(public.mthd): - def get_doc(self, _): - return _('delete existing user') -api.register(user_del) - -class user_mod(public.mthd): - def get_doc(self, _): - return _('edit existing user') -api.register(user_mod) - -class user_find(public.mthd): - def get_doc(self, _): - return _('search for users') -api.register(user_find) - - -# Register some properties for the 'user' object: -class user_givenname(public.prop): - def get_doc(self, _): - return _('user first name') -api.register(user_givenname) - -class user_sn(public.prop): - def get_doc(self, _): - return _('user last name') -api.register(user_sn) - -class user_login(public.prop): - def get_doc(self, _): - return _('user login') - def default(self, **kw): - givenname = kw.get('givenname', None) - sn = kw.get('sn', None) - if givenname is None or sn is None: - return None - return ('%s%s' % (givenname[0], sn)).lower() -api.register(user_login) - -class user_initials(public.prop): - def get_doc(self, _): - return _('user initials') - def default(self, **kw): - givenname = kw.get('givenname', None) - sn = kw.get('sn', None) - if givenname is None or sn is None: - return None - return '%s%s' % (givenname[0], sn[0]) -api.register(user_initials) - - -# Register some methods for the 'group' object: -class group_add(public.mthd): - def get_doc(self, _): - return _('add new group') -api.register(group_add) - -class group_del(public.mthd): - def get_doc(self, _): - return _('delete existing group') -api.register(group_del) - -class group_mod(public.mthd): - def get_doc(self, _): - return _('edit existing group') -api.register(group_mod) - -class group_find(public.mthd): - def get_doc(self, _): - return _('search for groups') -api.register(group_find) - - -# Register some methods for the 'service' object -class service_add(public.mthd): - def get_doc(self, _): - return _('add new service') -api.register(service_add) - -class service_del(public.mthd): - def get_doc(self, _): - return _('delete existing service') -api.register(service_del) - -class service_mod(public.mthd): - def get_doc(self, _): - return _('edit existing service') -api.register(service_mod) - -class service_find(public.mthd): - def get_doc(self, _): - return _('search for services') -api.register(service_find) - - -# And to emphasis that the registration order doesn't matter, -# we'll register the objects last: -class group(public.obj): - def get_doc(self, _): - return _('') -api.register(group) - -class service(public.obj): - def get_doc(self, _): - return _('') -api.register(service) - -class user(public.obj): - def get_doc(self, _): - return _('') -api.register(user) -- cgit From ba6cb2bf924327a580e1b410924f4f6419a75d16 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 13 Aug 2008 00:47:54 +0000 Subject: 127: Removed depreciated identity.py --- ipalib/identity.py | 54 ------------------------------------------------------ 1 file changed, 54 deletions(-) delete mode 100644 ipalib/identity.py (limited to 'ipalib') diff --git a/ipalib/identity.py b/ipalib/identity.py deleted file mode 100644 index 50642fec..00000000 --- a/ipalib/identity.py +++ /dev/null @@ -1,54 +0,0 @@ -# hidden -# read only -# editable - - - - - def get_label(self, _): - return _('Title') # Enum? - - def get_label(self, _): - return _('First Name') - - def get_label(self, _): - return _('Last Name') - - def get_label(self, _): - return _('Full Name') # Autofill - - def get_label(self, _): - return _('Display Name') # Autofill - - def get_label(self, _): - return _('Initials') # generated/ro? - - def get_label(self, _): - return _('Account Status') # Enum (active, inactive) - - def get_label(self, _): - return _('Login') - - def get_label(self, _): - return _('Password') - - def get_label(self, _): # Same field as above, special interface - return _('Confirm Password') - - def get_label(self, _): - return _('UID') #ro - - def get_label(self, _): - return _('GID') #ro - - def get_label(self, _): - return _('Home Directory') #ro - - def get_label(self, _): - return _('Login Shell') - - def get_label(self, _): - return _('GECOS') - - def get_label(self, _): - return _('') -- cgit From e9b715f2c430940d94bfedd6dbf9b1010eb22ffc Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 13 Aug 2008 00:56:46 +0000 Subject: 128: Fixed startup.py, example.py to work with new plugin locations --- ipalib/Plugins/__init__.py | 2 +- ipalib/Plugins/example.py | 5 +++-- ipalib/startup.py | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) (limited to 'ipalib') diff --git a/ipalib/Plugins/__init__.py b/ipalib/Plugins/__init__.py index e6325649..743eedb9 100644 --- a/ipalib/Plugins/__init__.py +++ b/ipalib/Plugins/__init__.py @@ -21,4 +21,4 @@ Sub-package containing all internal plugins. """ -print 'imported Plugins' +import example diff --git a/ipalib/Plugins/example.py b/ipalib/Plugins/example.py index c8eede0b..49b3d49c 100644 --- a/ipalib/Plugins/example.py +++ b/ipalib/Plugins/example.py @@ -21,8 +21,9 @@ Some example plugins. """ -import public -from run import api + +from ipalib import public +from ipalib.run import api # Hypothetical functional commands (not associated with any object): diff --git a/ipalib/startup.py b/ipalib/startup.py index 4879a8e6..305295c7 100644 --- a/ipalib/startup.py +++ b/ipalib/startup.py @@ -21,9 +21,9 @@ Importing this module causes the plugins to be loaded and the API to be generated. -This is not in __init__.py so that imported other IPA modules doesn't cause +This is not in __init__.py so that importing other IPA modules doesn't cause unnecessary side effects (needed for unit tests, among other things). """ from run import api -import plugins +import Plugins -- cgit From 8212be5c4bac78d7be8e6153b5c13bbb969d00f5 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 13 Aug 2008 00:58:49 +0000 Subject: 129: Deleted the test mod1.py, mod2.py files --- ipalib/Plugins/mod1.py | 24 ------------------------ ipalib/Plugins/mod2.py | 24 ------------------------ 2 files changed, 48 deletions(-) delete mode 100644 ipalib/Plugins/mod1.py delete mode 100644 ipalib/Plugins/mod2.py (limited to 'ipalib') diff --git a/ipalib/Plugins/mod1.py b/ipalib/Plugins/mod1.py deleted file mode 100644 index 2dd36d57..00000000 --- a/ipalib/Plugins/mod1.py +++ /dev/null @@ -1,24 +0,0 @@ -# Authors: -# Jason Gerard DeRose -# -# 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 - -""" -Test module. -""" - -print 'loaded mod1' diff --git a/ipalib/Plugins/mod2.py b/ipalib/Plugins/mod2.py deleted file mode 100644 index 6c75c5cd..00000000 --- a/ipalib/Plugins/mod2.py +++ /dev/null @@ -1,24 +0,0 @@ -# Authors: -# Jason Gerard DeRose -# -# 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 - -""" -Test module. -""" - -print 'loaded mod2' -- cgit From 13a3de7442570c47da2ccc76c15a0e6b0d9edea1 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 13 Aug 2008 01:03:32 +0000 Subject: 130: Renamed startup.py to load_plugins.py --- ipalib/load_plugins.py | 27 +++++++++++++++++++++++++++ ipalib/startup.py | 29 ----------------------------- 2 files changed, 27 insertions(+), 29 deletions(-) create mode 100644 ipalib/load_plugins.py delete mode 100644 ipalib/startup.py (limited to 'ipalib') diff --git a/ipalib/load_plugins.py b/ipalib/load_plugins.py new file mode 100644 index 00000000..a477ba25 --- /dev/null +++ b/ipalib/load_plugins.py @@ -0,0 +1,27 @@ +# Authors: +# Jason Gerard DeRose +# +# 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 + +""" +Importing this module causes the plugins to be loaded. + +This is not in __init__.py so that importing other ipalib or its other +modules does not cause unnecessary side effects. +""" + +import Plugins diff --git a/ipalib/startup.py b/ipalib/startup.py deleted file mode 100644 index 305295c7..00000000 --- a/ipalib/startup.py +++ /dev/null @@ -1,29 +0,0 @@ -# Authors: -# Jason Gerard DeRose -# -# 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 - -""" -Importing this module causes the plugins to be loaded and the API to be -generated. - -This is not in __init__.py so that importing other IPA modules doesn't cause -unnecessary side effects (needed for unit tests, among other things). -""" - -from run import api -import Plugins -- cgit From 12d662c1b76fc5971e23471617ebdf2a14ea9cfa Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 13 Aug 2008 01:18:00 +0000 Subject: 131: Renamed Plugins/ to plugins/ --- ipalib/Plugins/__init__.py | 24 ------- ipalib/Plugins/example.py | 156 --------------------------------------------- ipalib/load_plugins.py | 2 +- ipalib/plugins/__init__.py | 24 +++++++ ipalib/plugins/example.py | 156 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 181 insertions(+), 181 deletions(-) delete mode 100644 ipalib/Plugins/__init__.py delete mode 100644 ipalib/Plugins/example.py create mode 100644 ipalib/plugins/__init__.py create mode 100644 ipalib/plugins/example.py (limited to 'ipalib') diff --git a/ipalib/Plugins/__init__.py b/ipalib/Plugins/__init__.py deleted file mode 100644 index 743eedb9..00000000 --- a/ipalib/Plugins/__init__.py +++ /dev/null @@ -1,24 +0,0 @@ -# Authors: -# Jason Gerard DeRose -# -# 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 - -""" -Sub-package containing all internal plugins. -""" - -import example diff --git a/ipalib/Plugins/example.py b/ipalib/Plugins/example.py deleted file mode 100644 index 49b3d49c..00000000 --- a/ipalib/Plugins/example.py +++ /dev/null @@ -1,156 +0,0 @@ -# Authors: -# Jason Gerard DeRose -# -# 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 - -""" -Some example plugins. -""" - - -from ipalib import public -from ipalib.run import api - - -# Hypothetical functional commands (not associated with any object): -class krbtest(public.cmd): - def get_doc(self, _): - return _('test your Kerberos ticket') -api.register(krbtest) - -class discover(public.cmd): - def get_doc(self, _): - return _('discover IPA servers on network') -api.register(discover) - - -# Register some methods for the 'user' object: -class user_add(public.mthd): - def get_doc(self, _): - return _('add new user') -api.register(user_add) - -class user_del(public.mthd): - def get_doc(self, _): - return _('delete existing user') -api.register(user_del) - -class user_mod(public.mthd): - def get_doc(self, _): - return _('edit existing user') -api.register(user_mod) - -class user_find(public.mthd): - def get_doc(self, _): - return _('search for users') -api.register(user_find) - - -# Register some properties for the 'user' object: -class user_givenname(public.prop): - def get_doc(self, _): - return _('user first name') -api.register(user_givenname) - -class user_sn(public.prop): - def get_doc(self, _): - return _('user last name') -api.register(user_sn) - -class user_login(public.prop): - def get_doc(self, _): - return _('user login') - def default(self, **kw): - givenname = kw.get('givenname', None) - sn = kw.get('sn', None) - if givenname is None or sn is None: - return None - return ('%s%s' % (givenname[0], sn)).lower() -api.register(user_login) - -class user_initials(public.prop): - def get_doc(self, _): - return _('user initials') - def default(self, **kw): - givenname = kw.get('givenname', None) - sn = kw.get('sn', None) - if givenname is None or sn is None: - return None - return '%s%s' % (givenname[0], sn[0]) -api.register(user_initials) - - -# Register some methods for the 'group' object: -class group_add(public.mthd): - def get_doc(self, _): - return _('add new group') -api.register(group_add) - -class group_del(public.mthd): - def get_doc(self, _): - return _('delete existing group') -api.register(group_del) - -class group_mod(public.mthd): - def get_doc(self, _): - return _('edit existing group') -api.register(group_mod) - -class group_find(public.mthd): - def get_doc(self, _): - return _('search for groups') -api.register(group_find) - - -# Register some methods for the 'service' object -class service_add(public.mthd): - def get_doc(self, _): - return _('add new service') -api.register(service_add) - -class service_del(public.mthd): - def get_doc(self, _): - return _('delete existing service') -api.register(service_del) - -class service_mod(public.mthd): - def get_doc(self, _): - return _('edit existing service') -api.register(service_mod) - -class service_find(public.mthd): - def get_doc(self, _): - return _('search for services') -api.register(service_find) - - -# And to emphasis that the registration order doesn't matter, -# we'll register the objects last: -class group(public.obj): - def get_doc(self, _): - return _('') -api.register(group) - -class service(public.obj): - def get_doc(self, _): - return _('') -api.register(service) - -class user(public.obj): - def get_doc(self, _): - return _('') -api.register(user) diff --git a/ipalib/load_plugins.py b/ipalib/load_plugins.py index a477ba25..5663d0e8 100644 --- a/ipalib/load_plugins.py +++ b/ipalib/load_plugins.py @@ -24,4 +24,4 @@ This is not in __init__.py so that importing other ipalib or its other modules does not cause unnecessary side effects. """ -import Plugins +import plugins diff --git a/ipalib/plugins/__init__.py b/ipalib/plugins/__init__.py new file mode 100644 index 00000000..743eedb9 --- /dev/null +++ b/ipalib/plugins/__init__.py @@ -0,0 +1,24 @@ +# Authors: +# Jason Gerard DeRose +# +# 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 + +""" +Sub-package containing all internal plugins. +""" + +import example diff --git a/ipalib/plugins/example.py b/ipalib/plugins/example.py new file mode 100644 index 00000000..49b3d49c --- /dev/null +++ b/ipalib/plugins/example.py @@ -0,0 +1,156 @@ +# Authors: +# Jason Gerard DeRose +# +# 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 + +""" +Some example plugins. +""" + + +from ipalib import public +from ipalib.run import api + + +# Hypothetical functional commands (not associated with any object): +class krbtest(public.cmd): + def get_doc(self, _): + return _('test your Kerberos ticket') +api.register(krbtest) + +class discover(public.cmd): + def get_doc(self, _): + return _('discover IPA servers on network') +api.register(discover) + + +# Register some methods for the 'user' object: +class user_add(public.mthd): + def get_doc(self, _): + return _('add new user') +api.register(user_add) + +class user_del(public.mthd): + def get_doc(self, _): + return _('delete existing user') +api.register(user_del) + +class user_mod(public.mthd): + def get_doc(self, _): + return _('edit existing user') +api.register(user_mod) + +class user_find(public.mthd): + def get_doc(self, _): + return _('search for users') +api.register(user_find) + + +# Register some properties for the 'user' object: +class user_givenname(public.prop): + def get_doc(self, _): + return _('user first name') +api.register(user_givenname) + +class user_sn(public.prop): + def get_doc(self, _): + return _('user last name') +api.register(user_sn) + +class user_login(public.prop): + def get_doc(self, _): + return _('user login') + def default(self, **kw): + givenname = kw.get('givenname', None) + sn = kw.get('sn', None) + if givenname is None or sn is None: + return None + return ('%s%s' % (givenname[0], sn)).lower() +api.register(user_login) + +class user_initials(public.prop): + def get_doc(self, _): + return _('user initials') + def default(self, **kw): + givenname = kw.get('givenname', None) + sn = kw.get('sn', None) + if givenname is None or sn is None: + return None + return '%s%s' % (givenname[0], sn[0]) +api.register(user_initials) + + +# Register some methods for the 'group' object: +class group_add(public.mthd): + def get_doc(self, _): + return _('add new group') +api.register(group_add) + +class group_del(public.mthd): + def get_doc(self, _): + return _('delete existing group') +api.register(group_del) + +class group_mod(public.mthd): + def get_doc(self, _): + return _('edit existing group') +api.register(group_mod) + +class group_find(public.mthd): + def get_doc(self, _): + return _('search for groups') +api.register(group_find) + + +# Register some methods for the 'service' object +class service_add(public.mthd): + def get_doc(self, _): + return _('add new service') +api.register(service_add) + +class service_del(public.mthd): + def get_doc(self, _): + return _('delete existing service') +api.register(service_del) + +class service_mod(public.mthd): + def get_doc(self, _): + return _('edit existing service') +api.register(service_mod) + +class service_find(public.mthd): + def get_doc(self, _): + return _('search for services') +api.register(service_find) + + +# And to emphasis that the registration order doesn't matter, +# we'll register the objects last: +class group(public.obj): + def get_doc(self, _): + return _('') +api.register(group) + +class service(public.obj): + def get_doc(self, _): + return _('') +api.register(service) + +class user(public.obj): + def get_doc(self, _): + return _('') +api.register(user) -- cgit From 66bbe8bf2f253faa69c0556b47812a4a91e24a63 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 13 Aug 2008 01:20:01 +0000 Subject: 132: Removed test util.py file --- ipalib/util.py | 57 --------------------------------------------------------- 1 file changed, 57 deletions(-) delete mode 100644 ipalib/util.py (limited to 'ipalib') diff --git a/ipalib/util.py b/ipalib/util.py deleted file mode 100644 index b6240951..00000000 --- a/ipalib/util.py +++ /dev/null @@ -1,57 +0,0 @@ -# Authors: -# Jason Gerard DeRose -# -# 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 - -""" - -""" - -import os -from os import path - -__import__('Plugins', globals(), locals(), [], -1) - - - -def import_plugins(): - plugins = 'Plugins' - d = path.join(path.dirname(path.abspath(__file__)), plugins) - assert path.isdir(d) and not path.islink(d), 'not regular dir: %r' % d - print d - suffix = '.py' - for name in os.listdir(d): - if not name.endswith(suffix): - continue - if name.startswith('__init__.'): - continue - print name - mod = name[:len(suffix)+1] - __import__( - '%s.%s' % (plugins, mod), - globals(), - locals(), - [], - -1 - ) - - - - -if __name__ == '__main__': - pass - import_plugins() -- cgit From c0b5069fa07889496786523c46b5b15181c26fee Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 13 Aug 2008 01:26:30 +0000 Subject: 133: Renamed run.py to api.py --- ipalib/api.py | 27 +++++++++++++++++++++++++++ ipalib/load_plugins.py | 7 +++++-- ipalib/plugins/example.py | 2 +- ipalib/run.py | 28 ---------------------------- 4 files changed, 33 insertions(+), 31 deletions(-) create mode 100644 ipalib/api.py delete mode 100644 ipalib/run.py (limited to 'ipalib') diff --git a/ipalib/api.py b/ipalib/api.py new file mode 100644 index 00000000..f6820fd9 --- /dev/null +++ b/ipalib/api.py @@ -0,0 +1,27 @@ +# Authors: +# Jason Gerard DeRose +# +# 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 + +""" +Standard instances of plugable.API and its subclasses. +""" + +import public + +# The standard API instance +api = public.PublicAPI() diff --git a/ipalib/load_plugins.py b/ipalib/load_plugins.py index 5663d0e8..7863a24b 100644 --- a/ipalib/load_plugins.py +++ b/ipalib/load_plugins.py @@ -20,8 +20,11 @@ """ Importing this module causes the plugins to be loaded. -This is not in __init__.py so that importing other ipalib or its other -modules does not cause unnecessary side effects. +This is not in __init__.py so that importing ipalib or its other sub-modules +does not cause unnecessary side effects. + +Eventually this will also load the out-of tree plugins, but for now it just +loads the internal plugins. """ import plugins diff --git a/ipalib/plugins/example.py b/ipalib/plugins/example.py index 49b3d49c..caf963d2 100644 --- a/ipalib/plugins/example.py +++ b/ipalib/plugins/example.py @@ -23,7 +23,7 @@ Some example plugins. from ipalib import public -from ipalib.run import api +from ipalib.api import api # Hypothetical functional commands (not associated with any object): diff --git a/ipalib/run.py b/ipalib/run.py deleted file mode 100644 index eaaaed9c..00000000 --- a/ipalib/run.py +++ /dev/null @@ -1,28 +0,0 @@ -# Authors: -# Jason Gerard DeRose -# -# 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 - -""" -Standard run-time instances of importard classes. This is where plugins -should access the registration API. -""" - -import public - -# The standard API instance -api = public.PublicAPI() -- cgit From 0b5efa2a62623e09c7e8e5e97e0feafbc5e19823 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 13 Aug 2008 01:52:17 +0000 Subject: 134: Added CLI.mcl (Max Command Length) property; added corresponding unit tests --- ipalib/cli.py | 12 ++++++++++++ ipalib/tests/test_cli.py | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index a0b8800f..898f385b 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -50,6 +50,7 @@ def _(arg): class CLI(object): __d = None + __mcl = None def __init__(self, api): self.__api = api @@ -103,3 +104,14 @@ class CLI(object): from_cli(m.group(1)), m.group(2), ) + + def __get_mcl(self): + """ + Returns the Max Command Length. + """ + if self.__mcl is None: + if self.__d is None: + return None + self.__mcl = max(len(k) for k in self.__d) + return self.__mcl + mcl = property(__get_mcl) diff --git a/ipalib/tests/test_cli.py b/ipalib/tests/test_cli.py index d56a4b19..c523e4e8 100644 --- a/ipalib/tests/test_cli.py +++ b/ipalib/tests/test_cli.py @@ -37,6 +37,35 @@ def test_from_cli(): assert f('user-add') == 'user_add' +def get_cmd_name(i): + return 'cmd_%d' % i + +class DummyCmd(object): + def __init__(self, name): + self.__name = name + + def __get_name(self): + return self.__name + name = property(__get_name) + +class DummyAPI(object): + def __init__(self, cnt): + self.__cmd = tuple(self.__cmd_iter(cnt)) + + def __get_cmd(self): + return self.__cmd + cmd = property(__get_cmd) + + def __cmd_iter(self, cnt): + for i in xrange(cnt): + yield DummyCmd(get_cmd_name(i)) + + def finalize(self): + pass + + + + class test_CLI(ClassChecker): """ Tests the `CLI` class. @@ -65,3 +94,15 @@ class test_CLI(ClassChecker): ) args = tuple('--%s=%s' % (cli.to_cli(k), v) for (k,v) in kw.items()) assert dict(o.parse_kw(args)) == kw + + def test_mcl(self): + """ + Tests the `mcl` (Max Command Length) property . + """ + cnt = 100 + api = DummyAPI(cnt) + len(api.cmd) == cnt + o = self.cls(api) + assert o.mcl is None + o.finalize() + assert o.mcl == 6 # len('cmd_99') -- cgit From 76df64954d468e00197b8e26f685f51ce096adb0 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 13 Aug 2008 02:00:31 +0000 Subject: 135: Added unit test for CLI dictoinary interface --- ipalib/tests/test_cli.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) (limited to 'ipalib') diff --git a/ipalib/tests/test_cli.py b/ipalib/tests/test_cli.py index c523e4e8..7892227d 100644 --- a/ipalib/tests/test_cli.py +++ b/ipalib/tests/test_cli.py @@ -106,3 +106,19 @@ class test_CLI(ClassChecker): assert o.mcl is None o.finalize() assert o.mcl == 6 # len('cmd_99') + + def test_dict(self): + """ + Tests the `__contains__` and `__getitem__` methods. + """ + cnt = 25 + api = DummyAPI(cnt) + assert len(api.cmd) == cnt + o = self.cls(api) + o.finalize() + for cmd in api.cmd: + key = cli.to_cli(cmd.name) + assert key in o + assert o[key] is cmd + assert cmd.name not in o + raises(KeyError, getitem, o, cmd.name) -- cgit From 05cefc2af69ceb0df6b03c8e3cae4510024f1d02 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 13 Aug 2008 02:10:09 +0000 Subject: 136: CLI.print_commands() now prints cmd.get_doc() as well --- ipalib/cli.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index 898f385b..f7b19801 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -61,7 +61,10 @@ class CLI(object): def print_commands(self): for cmd in self.api.cmd: - print to_cli(cmd.name) + print ' %s %s' % ( + to_cli(cmd.name).ljust(self.mcl), + cmd.get_doc(_), + ) def __contains__(self, key): assert self.__d is not None, 'you must call finalize() first' -- cgit From 69f7132365c4f369068b8e09921cd29ea92f3754 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 13 Aug 2008 02:12:08 +0000 Subject: 137: Removed depreciated PublicAPI.max_cmd_len property --- ipalib/public.py | 11 ----------- 1 file changed, 11 deletions(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index e173bdd5..798b258c 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -308,16 +308,5 @@ class prop(attr, option): class PublicAPI(plugable.API): - __max_cmd_len = None - def __init__(self): super(PublicAPI, self).__init__(cmd, obj, mthd, prop) - - def __get_max_cmd_len(self): - if self.__max_cmd_len is None: - if not hasattr(self, 'cmd'): - return None - max_cmd_len = max(len(str(cmd)) for cmd in self.cmd) - object.__setattr__(self, '_PublicAPI__max_cmd_len', max_cmd_len) - return self.__max_cmd_len - max_cmd_len = property(__get_max_cmd_len) -- cgit From 0fed74b56d1940f84e7b64c3661f21baabcb4616 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 13 Aug 2008 02:34:36 +0000 Subject: 138: Added ProxyTarget.doc property; CLI.print_commands() now uses cmd.doc instead of cmd.get_doc() --- ipalib/cli.py | 2 +- ipalib/plugable.py | 13 +++++++-- ipalib/plugins/example.py | 63 +++++++++++++++---------------------------- ipalib/public.py | 1 - ipalib/tests/test_plugable.py | 19 ++++++++++--- 5 files changed, 49 insertions(+), 49 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index f7b19801..7ae8ae3b 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -63,7 +63,7 @@ class CLI(object): for cmd in self.api.cmd: print ' %s %s' % ( to_cli(cmd.name).ljust(self.mcl), - cmd.get_doc(_), + cmd.doc, ) def __contains__(self, key): diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 89eb423b..fbe5e638 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -128,6 +128,7 @@ class Proxy(ReadOnly): '__name_attr', '__public__', 'name', + 'doc', ) def __init__(self, base, target, name_attr='name'): @@ -146,10 +147,11 @@ class Proxy(ReadOnly): self.__target = target self.__name_attr = name_attr self.__public__ = base.__public__ - assert type(self.__public__) is frozenset self.name = getattr(target, name_attr) - check_identifier(self.name) + self.doc = target.doc self.__lock__() + assert type(self.__public__) is frozenset + check_identifier(self.name) def implements(self, arg): """ @@ -225,6 +227,13 @@ class ProxyTarget(ReadOnly): return self.__class__.__name__ name = property(__get_name) + def __get_doc(self): + """ + Convenience property to return the class docstring. + """ + return self.__class__.__doc__ + doc = property(__get_doc) + @classmethod def implements(cls, arg): """ diff --git a/ipalib/plugins/example.py b/ipalib/plugins/example.py index caf963d2..770ed33b 100644 --- a/ipalib/plugins/example.py +++ b/ipalib/plugins/example.py @@ -28,52 +28,43 @@ from ipalib.api import api # Hypothetical functional commands (not associated with any object): class krbtest(public.cmd): - def get_doc(self, _): - return _('test your Kerberos ticket') + 'test your Kerberos ticket' api.register(krbtest) class discover(public.cmd): - def get_doc(self, _): - return _('discover IPA servers on network') + 'discover IPA servers on network' api.register(discover) # Register some methods for the 'user' object: class user_add(public.mthd): - def get_doc(self, _): - return _('add new user') + 'add new user' api.register(user_add) class user_del(public.mthd): - def get_doc(self, _): - return _('delete existing user') + 'delete existing user' api.register(user_del) class user_mod(public.mthd): - def get_doc(self, _): - return _('edit existing user') + 'edit existing user' api.register(user_mod) class user_find(public.mthd): - def get_doc(self, _): - return _('search for users') + 'search for users' api.register(user_find) # Register some properties for the 'user' object: class user_givenname(public.prop): - def get_doc(self, _): - return _('user first name') + 'user first name' api.register(user_givenname) class user_sn(public.prop): - def get_doc(self, _): - return _('user last name') + 'user last name' api.register(user_sn) class user_login(public.prop): - def get_doc(self, _): - return _('user login') + 'user login' def default(self, **kw): givenname = kw.get('givenname', None) sn = kw.get('sn', None) @@ -83,8 +74,7 @@ class user_login(public.prop): api.register(user_login) class user_initials(public.prop): - def get_doc(self, _): - return _('user initials') + 'user initials' def default(self, **kw): givenname = kw.get('givenname', None) sn = kw.get('sn', None) @@ -96,61 +86,50 @@ api.register(user_initials) # Register some methods for the 'group' object: class group_add(public.mthd): - def get_doc(self, _): - return _('add new group') + 'add new group' api.register(group_add) class group_del(public.mthd): - def get_doc(self, _): - return _('delete existing group') + 'delete existing group' api.register(group_del) class group_mod(public.mthd): - def get_doc(self, _): - return _('edit existing group') + 'edit existing group' api.register(group_mod) class group_find(public.mthd): - def get_doc(self, _): - return _('search for groups') + 'search for groups' api.register(group_find) # Register some methods for the 'service' object class service_add(public.mthd): - def get_doc(self, _): - return _('add new service') + 'add new service' api.register(service_add) class service_del(public.mthd): - def get_doc(self, _): - return _('delete existing service') + 'delete existing service' api.register(service_del) class service_mod(public.mthd): - def get_doc(self, _): - return _('edit existing service') + 'edit existing service' api.register(service_mod) class service_find(public.mthd): - def get_doc(self, _): - return _('search for services') + 'search for services' api.register(service_find) # And to emphasis that the registration order doesn't matter, # we'll register the objects last: class group(public.obj): - def get_doc(self, _): - return _('') + 'group object' api.register(group) class service(public.obj): - def get_doc(self, _): - return _('') + 'service object' api.register(service) class user(public.obj): - def get_doc(self, _): - return _('') + 'user object' api.register(user) diff --git a/ipalib/public.py b/ipalib/public.py index 798b258c..9677358f 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -45,7 +45,6 @@ class option(plugable.Plugin): """ __public__ = frozenset(( - 'get_doc', 'normalize', 'default', 'validate', diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index 89bb948e..ba90c203 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -126,7 +126,7 @@ class test_ProxyTarget(ClassChecker): def test_name(self): """ - Test the `name` property. + Tests the `name` property. """ assert read_only(self.cls(), 'name') == 'ProxyTarget' @@ -134,9 +134,17 @@ class test_ProxyTarget(ClassChecker): pass assert read_only(some_subclass(), 'name') == 'some_subclass' + def test_doc(self): + """ + Tests the `doc` property. + """ + class some_subclass(self.cls): + 'here is the doc string' + assert read_only(some_subclass(), 'doc') == 'here is the doc string' + def test_implements(self): """ - Test the `implements` classmethod. + Tests the `implements` classmethod. """ class example(self.cls): __public__ = frozenset(( @@ -263,6 +271,7 @@ class test_Proxy(ClassChecker): class plugin(base): name = 'user_add' attr_name = 'add' + doc = 'add a new user' # Test that TypeError is raised when base is not a class: raises(TypeError, self.cls, base(), None) @@ -273,7 +282,8 @@ class test_Proxy(ClassChecker): # Test with correct arguments: i = plugin() p = self.cls(base, i) - assert read_only(p, 'name') == 'user_add' + assert read_only(p, 'name') is plugin.name + assert read_only(p, 'doc') == plugin.doc assert list(p) == sorted(base.__public__) # Test normal methods: @@ -304,6 +314,7 @@ class test_Proxy(ClassChecker): class base(object): __public__ = frozenset() name = 'base' + doc = 'doc' @classmethod def implements(cls, arg): return arg + 7 @@ -329,6 +340,7 @@ class test_Proxy(ClassChecker): __public__ = frozenset() class sub(base): name = 'some_name' + doc = 'doc' label = 'another_name' p = self.cls(base, sub()) @@ -389,6 +401,7 @@ class test_NameSpace(ClassChecker): __public__ = frozenset(( 'plusplus', )) + doc = 'doc' def plusplus(self, n): return n + 1 -- cgit From b9fa9dc2403d979f59a9963635392bb895bb8138 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 13 Aug 2008 03:15:00 +0000 Subject: 139: Removed dummy gettext _() func from cli.py; improved CLI.print_commands() --- ipalib/cli.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index 7ae8ae3b..d6b2e7c1 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -23,7 +23,7 @@ Functionality for Command Line Inteface. import re import sys -import optparse +import public def to_cli(name): @@ -44,10 +44,6 @@ def from_cli(cli_name): return cli_name.replace('-', '_') -def _(arg): - return arg - - class CLI(object): __d = None __mcl = None @@ -60,8 +56,9 @@ class CLI(object): api = property(__get_api) def print_commands(self): + print 'Available Commands:' for cmd in self.api.cmd: - print ' %s %s' % ( + print ' %s %s' % ( to_cli(cmd.name).ljust(self.mcl), cmd.doc, ) -- cgit From 14cdb57b507c7af188551be23044a905218ab120 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 13 Aug 2008 04:02:39 +0000 Subject: 140: Added a skeleton help command in cli.py --- ipalib/cli.py | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index d6b2e7c1..6f0305d6 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -44,6 +44,10 @@ def from_cli(cli_name): return cli_name.replace('-', '_') +class help(public.cmd): + 'display help on command' + + class CLI(object): __d = None __mcl = None @@ -73,6 +77,7 @@ class CLI(object): def finalize(self): api = self.api + api.register(help) api.finalize() def d_iter(): for cmd in api.cmd: -- cgit From c9072183a68efa9577ba9b6a028f9b0c2557b9ab Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 13 Aug 2008 04:10:23 +0000 Subject: 141: Fixed unit tests for CLI.finalize() --- ipalib/tests/test_cli.py | 3 +++ 1 file changed, 3 insertions(+) (limited to 'ipalib') diff --git a/ipalib/tests/test_cli.py b/ipalib/tests/test_cli.py index 7892227d..e80dee43 100644 --- a/ipalib/tests/test_cli.py +++ b/ipalib/tests/test_cli.py @@ -63,6 +63,9 @@ class DummyAPI(object): def finalize(self): pass + def register(self, *args, **kw): + pass + -- cgit From 47fed6c4c2ec509f1283bf18c4aad6eff9a4b756 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 13 Aug 2008 04:11:26 +0000 Subject: 142: python2.4: Fixed custom exceptions in errors.py as exceptions in Python2.4 are not new-style classes --- ipalib/errors.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'ipalib') diff --git a/ipalib/errors.py b/ipalib/errors.py index 1fc0c90c..47e0a3ed 100644 --- a/ipalib/errors.py +++ b/ipalib/errors.py @@ -52,13 +52,13 @@ class ValidationError(IPAError): self.name = name self.value = value self.error = error - super(ValidationError, self).__init__(name, value, error) + IPAError.__init__(self, name, value, error) class NormalizationError(ValidationError): def __init__(self, name, value, type): self.type = type - super(NormalizationError, self).__init__(name, value, + ValidationError.__init__(self, name, value, 'not %r' % type ) @@ -66,7 +66,7 @@ class NormalizationError(ValidationError): class RuleError(ValidationError): def __init__(self, name, value, rule, error): self.rule = rule - super(RuleError, self).__init__(name, value, error) + ValidationError.__init__(self, name, value, error) -- cgit From b4ad681f410ee5be56b0b02f73306aa49e5c668a Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 13 Aug 2008 05:14:12 +0000 Subject: 143: Added errors.RequirementError exception; cmd.validate() now raises RequirementError if a required option is missing --- ipalib/errors.py | 12 ++++++++++++ ipalib/public.py | 10 +++++++--- ipalib/tests/test_public.py | 37 ++++++++++++++++++++++++------------- 3 files changed, 43 insertions(+), 16 deletions(-) (limited to 'ipalib') diff --git a/ipalib/errors.py b/ipalib/errors.py index 47e0a3ed..afc61dd8 100644 --- a/ipalib/errors.py +++ b/ipalib/errors.py @@ -64,11 +64,23 @@ class NormalizationError(ValidationError): class RuleError(ValidationError): + """ + Raised when a required option was not provided. + """ def __init__(self, name, value, rule, error): self.rule = rule ValidationError.__init__(self, name, value, error) +class RequirementError(ValidationError): + """ + Raised when a required option was not provided. + """ + def __init__(self, name): + ValidationError.__init__(self, name, None, + 'missing required value' + ) + class SetError(IPAError): msg = 'setting %r, but NameSpace does not allow attribute setting' diff --git a/ipalib/public.py b/ipalib/public.py index 9677358f..4c085ba3 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -199,9 +199,13 @@ class cmd(plugable.Plugin): def validate(self, **kw): self.print_call('validate', kw, 1) - for (key, value) in kw.items(): - if key in self.options: - self.options[key].validate(value) + for opt in self.options: + value = kw.get(opt.name, None) + if value is None: + if opt.required: + raise errors.RequirementError(opt.name) + continue + opt.validate(value) def execute(self, **kw): self.print_call('execute', kw, 1) diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index eb19f28a..a331316d 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -177,7 +177,7 @@ class test_cmd(ClassChecker): class option0(my_option): pass class option1(my_option): - pass + required = True class example(self.cls): option_classes = (option0, option1) return example @@ -254,18 +254,30 @@ class test_cmd(ClassChecker): Tests the `validate` method. """ assert 'validate' in self.cls.__public__ # Public + sub = self.subcls() - for name in ('option0', 'option1'): - okay = { - name: name, - 'another_option': 'some value', - } - fail = { - name: 'whatever', - 'another_option': 'some value', - } - sub.validate(**okay) - raises(errors.RuleError, sub.validate, **fail) + + # Check with valid args + okay = dict( + option0='option0', + option1='option1', + another_option='some value', + ) + sub.validate(**okay) + + # Check with an invalid arg + fail = dict(okay) + fail['option0'] = 'whatever' + raises(errors.RuleError, sub.validate, **fail) + + # Check with a missing required arg + fail = dict(okay) + fail.pop('option1') + raises(errors.RequirementError, sub.validate, **fail) + + # Check with missing *not* required arg + okay.pop('option0') + sub.validate(**okay) def test_execute(self): """ @@ -274,7 +286,6 @@ class test_cmd(ClassChecker): assert 'execute' in self.cls.__public__ # Public - def test_obj(): cls = public.obj assert issubclass(cls, plugable.Plugin) -- cgit From 6924d5e25e237244e20554c380454a4029a0288f Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 13 Aug 2008 05:25:00 +0000 Subject: 144: Made properties in example plugins all required --- ipalib/plugins/example.py | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'ipalib') diff --git a/ipalib/plugins/example.py b/ipalib/plugins/example.py index 770ed33b..b5bd4733 100644 --- a/ipalib/plugins/example.py +++ b/ipalib/plugins/example.py @@ -57,14 +57,17 @@ api.register(user_find) # Register some properties for the 'user' object: class user_givenname(public.prop): 'user first name' + required = True api.register(user_givenname) class user_sn(public.prop): 'user last name' + required = True api.register(user_sn) class user_login(public.prop): 'user login' + required = True def default(self, **kw): givenname = kw.get('givenname', None) sn = kw.get('sn', None) @@ -75,6 +78,7 @@ api.register(user_login) class user_initials(public.prop): 'user initials' + required = True def default(self, **kw): givenname = kw.get('givenname', None) sn = kw.get('sn', None) -- cgit From 337c9964d42066368460da9a7c0d770142e2d1c3 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 13 Aug 2008 06:25:42 +0000 Subject: 145: Added new CLI.parse() method; added corresponding unit tests --- ipalib/cli.py | 16 +++++++++++++++- ipalib/tests/test_cli.py | 17 +++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index 6f0305d6..bf96d369 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -97,7 +97,9 @@ class CLI(object): sys.exit(2) self.run_cmd(cmd, sys.argv[2:]) - def run_cmd(self, cmd, args): + def run_cmd(self, cmd, given): + print self.parse(given) + sys.exit(0) kw = dict(self.parse_kw(args)) self[cmd](**kw) @@ -110,6 +112,18 @@ class CLI(object): m.group(2), ) + def parse(self, given): + args = [] + kw = {} + for g in given: + m = re.match(r'^--([a-z][-a-z0-9]*)=(.+)$', g) + if m: + kw[from_cli(m.group(1))] = m.group(2) + else: + args.append(g) + return (args, kw) + + def __get_mcl(self): """ Returns the Max Command Length. diff --git a/ipalib/tests/test_cli.py b/ipalib/tests/test_cli.py index e80dee43..4e2942d7 100644 --- a/ipalib/tests/test_cli.py +++ b/ipalib/tests/test_cli.py @@ -98,6 +98,23 @@ class test_CLI(ClassChecker): args = tuple('--%s=%s' % (cli.to_cli(k), v) for (k,v) in kw.items()) assert dict(o.parse_kw(args)) == kw + def test_parse(self): + """ + Tests the `parse` method. + """ + o = self.cls(None) + args = ['hello', 'naughty', 'nurse'] + kw = dict( + first_name='Naughty', + last_name='Nurse', + ) + opts = ['--%s=%s' % (k.replace('_', '-'), v) for (k, v) in kw.items()] + assert o.parse(args + []) == (args, {}) + assert o.parse(opts + []) == ([], kw) + assert o.parse(args + opts) == (args, kw) + assert o.parse(opts + args) == (args, kw) + + def test_mcl(self): """ Tests the `mcl` (Max Command Length) property . -- cgit From d422ef1134d81123b059574d13060811534e1d0d Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 13 Aug 2008 06:40:25 +0000 Subject: 146: Removed CLI.parse_kw() method and corresponding unit tests --- ipalib/cli.py | 16 ++-------------- ipalib/tests/test_cli.py | 13 ------------- 2 files changed, 2 insertions(+), 27 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index bf96d369..d0cf1017 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -98,19 +98,8 @@ class CLI(object): self.run_cmd(cmd, sys.argv[2:]) def run_cmd(self, cmd, given): - print self.parse(given) - sys.exit(0) - kw = dict(self.parse_kw(args)) - self[cmd](**kw) - - def parse_kw(self, args): - for arg in args: - m = re.match(r'^--([a-z][-a-z0-9]*)=(.+)$', arg) - if m is not None: - yield ( - from_cli(m.group(1)), - m.group(2), - ) + (args, kw) = self.parse(given) + self[cmd](*args, **kw) def parse(self, given): args = [] @@ -123,7 +112,6 @@ class CLI(object): args.append(g) return (args, kw) - def __get_mcl(self): """ Returns the Max Command Length. diff --git a/ipalib/tests/test_cli.py b/ipalib/tests/test_cli.py index 4e2942d7..ad02c645 100644 --- a/ipalib/tests/test_cli.py +++ b/ipalib/tests/test_cli.py @@ -86,18 +86,6 @@ class test_CLI(ClassChecker): o = self.cls(api) assert read_only(o, 'api') is api - def test_parse_kw(self): - """ - Tests the `parse_kw` method. - """ - o = self.cls(None) - kw = dict( - hello='world', - how_are='you', - ) - args = tuple('--%s=%s' % (cli.to_cli(k), v) for (k,v) in kw.items()) - assert dict(o.parse_kw(args)) == kw - def test_parse(self): """ Tests the `parse` method. @@ -114,7 +102,6 @@ class test_CLI(ClassChecker): assert o.parse(args + opts) == (args, kw) assert o.parse(opts + args) == (args, kw) - def test_mcl(self): """ Tests the `mcl` (Max Command Length) property . -- cgit From 024345e1f05e8a07517388deb83a45f8e8fd0a63 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 13 Aug 2008 06:41:39 +0000 Subject: 147: Changed cmd calling signature to __call__(self, *args, **kw) --- ipalib/public.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index 4c085ba3..15f08b5b 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -219,7 +219,7 @@ class cmd(plugable.Plugin): ', '.join('%s=%r' % (k, kw[k]) for k in sorted(kw)), ) - def __call__(self, **kw): + def __call__(self, *args, **kw): print '' self.print_call('__call__', kw) kw = self.normalize(**kw) -- cgit From fe7440735d8527b281f642056f11579b428dce0f Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 13 Aug 2008 07:20:10 +0000 Subject: 148: Added some basic out put for cli.help.__call__() method --- ipalib/cli.py | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index d0cf1017..d2583f57 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -46,6 +46,11 @@ def from_cli(cli_name): class help(public.cmd): 'display help on command' + def __call__(self, key): + if from_cli(key) not in self.api.cmd: + print 'help: no such command %r' % key + sys.exit(2) + print 'Help on command %r:' % key class CLI(object): -- cgit From d95133b66f64a2e4f1c8aafa5ff9183c6acfe9a7 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 14 Aug 2008 01:09:11 +0000 Subject: 149: CLI.run() now does an arg.decode('utf-8') for args in sys.argv so that non-ascii characters work --- ipalib/cli.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index d2583f57..d17d12bc 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -40,8 +40,7 @@ def from_cli(cli_name): Takes a string from the Command Line Interface and transforms it into a Python identifier. """ - assert isinstance(cli_name, basestring) - return cli_name.replace('-', '_') + return str(cli_name).replace('-', '_') class help(public.cmd): @@ -100,7 +99,7 @@ class CLI(object): self.print_commands() print 'ipa: ERROR: unknown command %r' % cmd sys.exit(2) - self.run_cmd(cmd, sys.argv[2:]) + self.run_cmd(cmd, (s.decode('utf-8') for s in sys.argv[2:])) def run_cmd(self, cmd, given): (args, kw) = self.parse(given) -- cgit From ba8d32a110f3dc96b091df9a2520f60c99ac26ba Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 14 Aug 2008 05:46:20 +0000 Subject: 150: NameSpace.__iter__() now iterates through the names, not the members; added NameSpace.__call__() method which iterates through the members; NameSpace no longer requires members to be Proxy instances; updated unit tests and affected code; cleaned up NameSpace docstrings and switch to epydoc param docstrings --- ipalib/cli.py | 4 +- ipalib/plugable.py | 145 ++++++++++++++++++++++++++++++------------ ipalib/public.py | 4 +- ipalib/tests/test_cli.py | 6 +- ipalib/tests/test_plugable.py | 17 +++-- 5 files changed, 122 insertions(+), 54 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index d17d12bc..8f09e90c 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -65,7 +65,7 @@ class CLI(object): def print_commands(self): print 'Available Commands:' - for cmd in self.api.cmd: + for cmd in self.api.cmd(): print ' %s %s' % ( to_cli(cmd.name).ljust(self.mcl), cmd.doc, @@ -84,7 +84,7 @@ class CLI(object): api.register(help) api.finalize() def d_iter(): - for cmd in api.cmd: + for cmd in api.cmd(): yield (to_cli(cmd.name), cmd) self.__d = dict(d_iter()) diff --git a/ipalib/plugable.py b/ipalib/plugable.py index fbe5e638..ec97722a 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -26,16 +26,6 @@ import inspect import errors -def check_identifier(name): - """ - Raises errors.NameSpaceError if `name` is not a valid Python identifier - suitable for use in a NameSpace. - """ - regex = r'^[a-z][_a-z0-9]*[a-z0-9]$' - if re.match(regex, name) is None: - raise errors.NameSpaceError(name, regex) - - class ReadOnly(object): """ Base class for classes with read-only attributes. @@ -151,7 +141,6 @@ class Proxy(ReadOnly): self.doc = target.doc self.__lock__() assert type(self.__public__) is frozenset - check_identifier(self.name) def implements(self, arg): """ @@ -341,60 +330,132 @@ class Plugin(ProxyTarget): ) -class NameSpace(ReadOnly): +def check_name(name): """ - A read-only namespace of Proxy instances. Proxy.name is used to name the - attributes pointing to the Proxy instances, which can also be accesses - through a dictionary interface, for example: + Raises errors.NameSpaceError if `name` is not a valid Python identifier + suitable for use in a NameSpace. + """ + assert type(name) is str, 'must be %r' % str + regex = r'^[a-z][_a-z0-9]*[a-z0-9]$' + if re.match(regex, name) is None: + raise errors.NameSpaceError(name, regex) + return name - >>> assert namespace.my_proxy is namespace['my_proxy'] # True + +class NameSpace(ReadOnly): + """ + A read-only namespace with handy container behaviours. + + Each member of a NameSpace instance must have a `name` attribute whose + value 1) is unique among the members and 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, assuming `obj` is a member in + the NameSpace instance `namespace`: + + >>> obj is getattr(namespace, obj.name) # As attribute + True + >>> obj is namespace[obj.name] # As dictionary item + True + + Here is a more detailed example: + + >>> class member(object): + ... def __init__(self, i): + ... self.name = 'member_%d' % i + ... + >>> def get_members(cnt): + ... for i in xrange(cnt): + ... yield member(i) + ... + >>> namespace = NameSpace(get_members(2)) + >>> namespace.member_0 is namespace['member_0'] + True + >>> len(namespace) # Returns the number of members in namespace + 2 + >>> list(namespace) # As iterable, iterates through the member names + ['member_0', 'member_1'] + >>> list(namespace()) # Calling a NameSpace iterates through the members + [<__main__.member object at 0x836710>, <__main__.member object at 0x836750>] + >>> 'member_1' in namespace # NameSpace.__contains__() + True """ - def __init__(self, proxies): + def __init__(self, members): """ - `proxies` - an iterable returning the Proxy instances to be contained - in this NameSpace. + @type members: iterable + @param members: An iterable providing the members. """ - self.__proxies = tuple(proxies) self.__d = dict() - for proxy in self.__proxies: - assert isinstance(proxy, Proxy) - assert proxy.name not in self.__d - self.__d[proxy.name] = proxy - assert not hasattr(self, proxy.name) - setattr(self, proxy.name, proxy) + self.__names = tuple(self.__member_iter(members)) self.__lock__() + assert set(self.__d) == set(self.__names) - def __iter__(self): + def __member_iter(self, members): """ - Iterates through the proxies in this NameSpace in the same order they - were passed to the constructor. + Helper method used only from __init__(). """ - for proxy in self.__proxies: - yield proxy + for member in members: + name = check_name(member.name) + assert not ( + name in self.__d or hasattr(self, name) + ), 'already has member named %r' % name + self.__d[name] = member + setattr(self, name, member) + yield name def __len__(self): """ - Returns number of proxies in this NameSpace. + Returns the number of members in this NameSpace. """ - return len(self.__proxies) + return len(self.__d) - def __contains__(self, key): + def __contains__(self, name): """ - Returns True if a proxy named `key` is in this NameSpace. + Returns True if this NameSpace contains a member named `name`; returns + False otherwise. + + @type name: str + @param name: The name of a potential member """ - return key in self.__d + return name in self.__d - def __getitem__(self, key): + def __getitem__(self, name): + """ + If this NameSpace contains a member named `name`, returns that member; + otherwise raises KeyError. + + @type name: str + @param name: The name of member to retrieve + """ + if name in self.__d: + return self.__d[name] + raise KeyError('NameSpace has no member named %r' % name) + + def __iter__(self): """ - Returns proxy named `key`; otherwise raises KeyError. + Iterates through the member names in the same order as the members + were passed to the constructor. """ - if key in self.__d: - return self.__d[key] - raise KeyError('NameSpace has no item for key %r' % key) + for name in self.__names: + yield name + + def __call__(self): + """ + Iterates through the members in the same order they were passed to the + constructor. + """ + for name in self.__names: + yield self.__d[name] def __repr__(self): - return '%s(<%d proxies>)' % (self.__class__.__name__, len(self)) + """ + Returns pseudo-valid Python expression that could be used to construct + this NameSpace instance. + """ + return '%s(<%d members>)' % (self.__class__.__name__, len(self)) class Registrar(ReadOnly): diff --git a/ipalib/public.py b/ipalib/public.py index 15f08b5b..d23aa7ac 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -187,7 +187,7 @@ class cmd(plugable.Plugin): return dict(self.normalize_iter(kw)) def default_iter(self, kw): - for option in self.options: + for option in self.options(): if option.name not in kw: value = option.default(**kw) if value is not None: @@ -199,7 +199,7 @@ class cmd(plugable.Plugin): def validate(self, **kw): self.print_call('validate', kw, 1) - for opt in self.options: + for opt in self.options(): value = kw.get(opt.name, None) if value is None: if opt.required: diff --git a/ipalib/tests/test_cli.py b/ipalib/tests/test_cli.py index ad02c645..4c14c0dd 100644 --- a/ipalib/tests/test_cli.py +++ b/ipalib/tests/test_cli.py @@ -22,7 +22,7 @@ Unit tests for `ipalib.cli` module. """ from tstutil import raises, getitem, no_set, no_del, read_only, ClassChecker -from ipalib import cli +from ipalib import cli, plugable def test_to_cli(): @@ -50,7 +50,7 @@ class DummyCmd(object): class DummyAPI(object): def __init__(self, cnt): - self.__cmd = tuple(self.__cmd_iter(cnt)) + self.__cmd = plugable.NameSpace(self.__cmd_iter(cnt)) def __get_cmd(self): return self.__cmd @@ -123,7 +123,7 @@ class test_CLI(ClassChecker): assert len(api.cmd) == cnt o = self.cls(api) o.finalize() - for cmd in api.cmd: + for cmd in api.cmd(): key = cli.to_cli(cmd.name) assert key in o assert o[key] is cmd diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index ba90c203..bf1ef91c 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -26,11 +26,11 @@ from tstutil import ClassChecker from ipalib import plugable, errors -def test_valid_identifier(): +def test_check_name(): """ - Test the `valid_identifier` function. + Tests the `check_name` function. """ - f = plugable.check_identifier + f = plugable.check_name okay = [ 'user_add', 'stuff2junk', @@ -426,13 +426,20 @@ class test_NameSpace(ClassChecker): # Test __iter__ i = None - for (i, proxy) in enumerate(ns): + for (i, key) in enumerate(ns): + assert type(key) is str + assert key == get_name(i) + assert i == cnt - 1 + + # Test __call__ + i = None + for (i, proxy) in enumerate(ns()): assert type(proxy) is plugable.Proxy assert proxy.name == get_name(i) assert i == cnt - 1 # Test __contains__, __getitem__, getattr(): - proxies = frozenset(ns) + proxies = frozenset(ns()) for i in xrange(cnt): name = get_name(i) assert name in ns -- cgit From 3b9df638cece87fda609777c27b15c689945e45b Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 14 Aug 2008 05:54:15 +0000 Subject: 151: Rearanged tests in test_plugable.py to match definition order in plugable.py --- ipalib/tests/test_plugable.py | 286 +++++++++++++++++++++--------------------- 1 file changed, 143 insertions(+), 143 deletions(-) (limited to 'ipalib') diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index bf1ef91c..141b2b2b 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -26,33 +26,6 @@ from tstutil import ClassChecker from ipalib import plugable, errors -def test_check_name(): - """ - Tests the `check_name` function. - """ - f = plugable.check_name - okay = [ - 'user_add', - 'stuff2junk', - 'sixty9', - ] - nope = [ - '_user_add', - '__user_add', - 'user_add_', - 'user_add__', - '_user_add_', - '__user_add__', - '60nine', - ] - for name in okay: - f(name) - for name in nope: - raises(errors.NameSpaceError, f, name) - for name in okay: - raises(errors.NameSpaceError, f, name.upper()) - - class test_ReadOnly(ClassChecker): """ Test the `ReadOnly` class @@ -113,6 +86,122 @@ class test_ReadOnly(ClassChecker): assert read_only(obj, 'an_attribute') == 'Hello world!' +class test_Proxy(ClassChecker): + """ + Tests the `Proxy` class. + """ + _cls = plugable.Proxy + + def test_class(self): + assert self.cls.__bases__ == (plugable.ReadOnly,) + + def test_proxy(self): + # Setup: + class base(object): + __public__ = frozenset(( + 'public_0', + 'public_1', + '__call__', + )) + + def public_0(self): + return 'public_0' + + def public_1(self): + return 'public_1' + + def __call__(self, caller): + return 'ya called it, %s.' % caller + + def private_0(self): + return 'private_0' + + def private_1(self): + return 'private_1' + + class plugin(base): + name = 'user_add' + attr_name = 'add' + doc = 'add a new user' + + # Test that TypeError is raised when base is not a class: + raises(TypeError, self.cls, base(), None) + + # Test that ValueError is raised when target is not instance of base: + raises(ValueError, self.cls, base, object()) + + # Test with correct arguments: + i = plugin() + p = self.cls(base, i) + assert read_only(p, 'name') is plugin.name + assert read_only(p, 'doc') == plugin.doc + assert list(p) == sorted(base.__public__) + + # Test normal methods: + for n in xrange(2): + pub = 'public_%d' % n + priv = 'private_%d' % n + assert getattr(i, pub)() == pub + assert getattr(p, pub)() == pub + assert hasattr(p, pub) + assert getattr(i, priv)() == priv + assert not hasattr(p, priv) + + # Test __call__: + value = 'ya called it, dude.' + assert i('dude') == value + assert p('dude') == value + assert callable(p) + + # Test name_attr='name' kw arg + i = plugin() + p = self.cls(base, i, 'attr_name') + assert read_only(p, 'name') == 'add' + + def test_implements(self): + """ + Tests the `implements` method. + """ + class base(object): + __public__ = frozenset() + name = 'base' + doc = 'doc' + @classmethod + def implements(cls, arg): + return arg + 7 + + class sub(base): + @classmethod + def implements(cls, arg): + """ + Defined to make sure base.implements() is called, not + target.implements() + """ + return arg + + o = sub() + p = self.cls(base, o) + assert p.implements(3) == 10 + + def test_clone(self): + """ + Tests the `__clone__` method. + """ + class base(object): + __public__ = frozenset() + class sub(base): + name = 'some_name' + doc = 'doc' + label = 'another_name' + + p = self.cls(base, sub()) + assert read_only(p, 'name') == 'some_name' + c = p.__clone__('label') + assert isinstance(c, self.cls) + assert c is not p + assert read_only(c, 'name') == 'another_name' + + class test_ProxyTarget(ClassChecker): """ Test the `ProxyTarget` class. @@ -235,122 +324,6 @@ class test_ProxyTarget(ClassChecker): assert base.implemented_by(fail()) is False -class test_Proxy(ClassChecker): - """ - Tests the `Proxy` class. - """ - _cls = plugable.Proxy - - def test_class(self): - assert self.cls.__bases__ == (plugable.ReadOnly,) - - def test_proxy(self): - # Setup: - class base(object): - __public__ = frozenset(( - 'public_0', - 'public_1', - '__call__', - )) - - def public_0(self): - return 'public_0' - - def public_1(self): - return 'public_1' - - def __call__(self, caller): - return 'ya called it, %s.' % caller - - def private_0(self): - return 'private_0' - - def private_1(self): - return 'private_1' - - class plugin(base): - name = 'user_add' - attr_name = 'add' - doc = 'add a new user' - - # Test that TypeError is raised when base is not a class: - raises(TypeError, self.cls, base(), None) - - # Test that ValueError is raised when target is not instance of base: - raises(ValueError, self.cls, base, object()) - - # Test with correct arguments: - i = plugin() - p = self.cls(base, i) - assert read_only(p, 'name') is plugin.name - assert read_only(p, 'doc') == plugin.doc - assert list(p) == sorted(base.__public__) - - # Test normal methods: - for n in xrange(2): - pub = 'public_%d' % n - priv = 'private_%d' % n - assert getattr(i, pub)() == pub - assert getattr(p, pub)() == pub - assert hasattr(p, pub) - assert getattr(i, priv)() == priv - assert not hasattr(p, priv) - - # Test __call__: - value = 'ya called it, dude.' - assert i('dude') == value - assert p('dude') == value - assert callable(p) - - # Test name_attr='name' kw arg - i = plugin() - p = self.cls(base, i, 'attr_name') - assert read_only(p, 'name') == 'add' - - def test_implements(self): - """ - Tests the `implements` method. - """ - class base(object): - __public__ = frozenset() - name = 'base' - doc = 'doc' - @classmethod - def implements(cls, arg): - return arg + 7 - - class sub(base): - @classmethod - def implements(cls, arg): - """ - Defined to make sure base.implements() is called, not - target.implements() - """ - return arg - - o = sub() - p = self.cls(base, o) - assert p.implements(3) == 10 - - def test_clone(self): - """ - Tests the `__clone__` method. - """ - class base(object): - __public__ = frozenset() - class sub(base): - name = 'some_name' - doc = 'doc' - label = 'another_name' - - p = self.cls(base, sub()) - assert read_only(p, 'name') == 'some_name' - c = p.__clone__('label') - assert isinstance(c, self.cls) - assert c is not p - assert read_only(c, 'name') == 'another_name' - - class test_Plugin(ClassChecker): """ Tests the `Plugin` class. @@ -387,6 +360,33 @@ class test_Plugin(ClassChecker): raises(AssertionError, sub.finalize, api) +def test_check_name(): + """ + Tests the `check_name` function. + """ + f = plugable.check_name + okay = [ + 'user_add', + 'stuff2junk', + 'sixty9', + ] + nope = [ + '_user_add', + '__user_add', + 'user_add_', + 'user_add__', + '_user_add_', + '__user_add__', + '60nine', + ] + for name in okay: + f(name) + for name in nope: + raises(errors.NameSpaceError, f, name) + for name in okay: + raises(errors.NameSpaceError, f, name.upper()) + + class test_NameSpace(ClassChecker): """ Tests the `NameSpace` class. -- cgit From b10fc16113311ae1d651af2edd94de87724fed2a Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 14 Aug 2008 05:56:01 +0000 Subject: 152: Updated unit tests check_name() now that it returns the name --- ipalib/tests/test_plugable.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index 141b2b2b..ddb8fed0 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -380,7 +380,7 @@ def test_check_name(): '60nine', ] for name in okay: - f(name) + assert name is f(name) for name in nope: raises(errors.NameSpaceError, f, name) for name in okay: -- cgit From a59d6698d2a13792210bcaeac1ee79e255fd8f1c Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 14 Aug 2008 06:53:05 +0000 Subject: 153: Started cleaning up docstrings in Proxy and also experimented with restructuredtext formatting --- ipalib/plugable.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index ec97722a..8ab5e249 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -104,8 +104,7 @@ class ReadOnly(object): class Proxy(ReadOnly): """ - Allows access to only certain attributes on its target object (a - ProxyTarget). + Allows access to only certain attributes on its target object. Think of a proxy as an agreement that "I will have at most these attributes". This is different from (although similar to) an interface, @@ -123,16 +122,19 @@ class Proxy(ReadOnly): def __init__(self, base, target, name_attr='name'): """ - `base` - the class defining the __public__ frozenset of attributes to - proxy - `target` - the target of the proxy (must be instance of `base`) - `name_attr` - the name of the str attribute on `target` to assign - to Proxy.name + :param base: A subclass of `Plugin`. + :param target: An instance ``base`` or a subclass of ``base``. + :param name_attr: The name of the attribute on ``target`` from which + to derive ``self.name``. """ if not inspect.isclass(base): - raise TypeError('arg1 must be a class, got %r' % base) + raise TypeError( + '`base` must be a class, got %r' % base + ) if not isinstance(target, base): - raise ValueError('arg2 must be instance of arg1, got %r' % target) + raise ValueError( + '`target` must be an instance of `base`, got %r' % target + ) self.__base = base self.__target = target self.__name_attr = name_attr -- cgit From 00f4272662e56a98fe498cc8f5761cc15bcd3825 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 14 Aug 2008 07:10:07 +0000 Subject: 154: Merged ProxyTarget functionality into Plugin to make things a bit clearer --- ipalib/plugable.py | 245 ++++++++++++++++++++-------------------- ipalib/tests/test_plugable.py | 255 ++++++++++++++++++++---------------------- 2 files changed, 245 insertions(+), 255 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 8ab5e249..645b2d16 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -102,9 +102,130 @@ class ReadOnly(object): return object.__delattr__(self, name) +class Plugin(ReadOnly): + """ + Base class for all plugins. + """ + __public__ = frozenset() + __api = None + + def __get_name(self): + """ + Convenience property to return the class name. + """ + return self.__class__.__name__ + name = property(__get_name) + + def __get_doc(self): + """ + Convenience property to return the class docstring. + """ + return self.__class__.__doc__ + doc = property(__get_doc) + + def __get_api(self): + """ + Returns the `API` instance passed to `finalize`, or + or returns None if `finalize` has not yet been called. + """ + return self.__api + api = property(__get_api) + + @classmethod + def implements(cls, arg): + """ + Returns True if this cls.__public__ frozenset contains `arg`; + returns False otherwise. + + There are three different ways this can be called: + + 1. With a argument, e.g.: + + >>> class base(ProxyTarget): + >>> __public__ = frozenset(['some_attr', 'another_attr']) + >>> base.implements('some_attr') + True + >>> base.implements('an_unknown_attribute') + False + + 2. With a argument, e.g.: + + >>> base.implements(frozenset(['some_attr'])) + True + >>> base.implements(frozenset(['some_attr', 'an_unknown_attribute'])) + False + + 3. With any object that has a `__public__` attribute that is + , e.g.: + + >>> class whatever(object): + >>> __public__ = frozenset(['another_attr']) + >>> base.implements(whatever) + True + + Unlike ProxyTarget.implemented_by(), this returns an abstract answer + because only the __public__ frozenset is checked... a ProxyTarget + need not itself have attributes for all names in __public__ + (subclasses might provide them). + """ + assert type(cls.__public__) is frozenset + if isinstance(arg, str): + return arg in cls.__public__ + if type(getattr(arg, '__public__', None)) is frozenset: + return cls.__public__.issuperset(arg.__public__) + if type(arg) is frozenset: + return cls.__public__.issuperset(arg) + raise TypeError( + "must be str, frozenset, or have frozenset '__public__' attribute" + ) + + @classmethod + def implemented_by(cls, arg): + """ + Returns True if (1) `arg` is an instance of or subclass of this class, + and (2) `arg` (or `arg.__class__` if instance) has an attribute for + each name in this class's __public__ frozenset; returns False + otherwise. + + Unlike ProxyTarget.implements(), this returns a concrete answer + because the attributes of the subclass are checked. + """ + if inspect.isclass(arg): + subclass = arg + else: + subclass = arg.__class__ + assert issubclass(subclass, cls), 'must be subclass of %r' % cls + for name in cls.__public__: + if not hasattr(subclass, name): + return False + return True + + def finalize(self, api): + """ + After all the plugins are instantiated, `API` calls this method, + passing itself as the only argument. This is where plugins should + check that other plugins they depend upon have actually been loaded. + + :param api: An `API` instance. + """ + assert self.__api is None, 'finalize() can only be called once' + assert api is not None, 'finalize() argument cannot be None' + self.__api = api + + def __repr__(self): + """ + Returns a fully qualified module_name.class_name() representation that + could be used to construct this Plugin instance. + """ + return '%s.%s()' % ( + self.__class__.__module__, + self.__class__.__name__ + ) + + class Proxy(ReadOnly): """ - Allows access to only certain attributes on its target object. + Allows access to only certain attributes on a `Plugin`. Think of a proxy as an agreement that "I will have at most these attributes". This is different from (although similar to) an interface, @@ -208,128 +329,6 @@ class Proxy(ReadOnly): ) -class ProxyTarget(ReadOnly): - __public__ = frozenset() - - def __get_name(self): - """ - Convenience property to return the class name. - """ - return self.__class__.__name__ - name = property(__get_name) - - def __get_doc(self): - """ - Convenience property to return the class docstring. - """ - return self.__class__.__doc__ - doc = property(__get_doc) - - @classmethod - def implements(cls, arg): - """ - Returns True if this cls.__public__ frozenset contains `arg`; - returns False otherwise. - - There are three different ways this can be called: - - 1. With a argument, e.g.: - - >>> class base(ProxyTarget): - >>> __public__ = frozenset(['some_attr', 'another_attr']) - >>> base.implements('some_attr') - True - >>> base.implements('an_unknown_attribute') - False - - 2. With a argument, e.g.: - - >>> base.implements(frozenset(['some_attr'])) - True - >>> base.implements(frozenset(['some_attr', 'an_unknown_attribute'])) - False - - 3. With any object that has a `__public__` attribute that is - , e.g.: - - >>> class whatever(object): - >>> __public__ = frozenset(['another_attr']) - >>> base.implements(whatever) - True - - Unlike ProxyTarget.implemented_by(), this returns an abstract answer - because only the __public__ frozenset is checked... a ProxyTarget - need not itself have attributes for all names in __public__ - (subclasses might provide them). - """ - assert type(cls.__public__) is frozenset - if isinstance(arg, str): - return arg in cls.__public__ - if type(getattr(arg, '__public__', None)) is frozenset: - return cls.__public__.issuperset(arg.__public__) - if type(arg) is frozenset: - return cls.__public__.issuperset(arg) - raise TypeError( - "must be str, frozenset, or have frozenset '__public__' attribute" - ) - - @classmethod - def implemented_by(cls, arg): - """ - Returns True if (1) `arg` is an instance of or subclass of this class, - and (2) `arg` (or `arg.__class__` if instance) has an attribute for - each name in this class's __public__ frozenset; returns False - otherwise. - - Unlike ProxyTarget.implements(), this returns a concrete answer - because the attributes of the subclass are checked. - """ - if inspect.isclass(arg): - subclass = arg - else: - subclass = arg.__class__ - assert issubclass(subclass, cls), 'must be subclass of %r' % cls - for name in cls.__public__: - if not hasattr(subclass, name): - return False - return True - - -class Plugin(ProxyTarget): - """ - Base class for all plugins. - """ - - __api = None - - def __get_api(self): - """ - Returns the plugable.API instance passed to Plugin.finalize(), or - or returns None if finalize() has not yet been called. - """ - return self.__api - api = property(__get_api) - - def finalize(self, api): - """ - After all the plugins are instantiated, the plugable.API calls this - method, passing itself as the only argument. This is where plugins - should check that other plugins they depend upon have actually been - loaded. - """ - assert self.__api is None, 'finalize() can only be called once' - assert api is not None, 'finalize() argument cannot be None' - self.__api = api - - def __repr__(self): - """ - Returns a fully qualified module_name.class_name() representation that - could be used to construct this Plugin instance. - """ - return '%s.%s()' % ( - self.__class__.__module__, - self.__class__.__name__ - ) def check_name(name): diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index ddb8fed0..aa449b71 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -86,138 +86,24 @@ class test_ReadOnly(ClassChecker): assert read_only(obj, 'an_attribute') == 'Hello world!' -class test_Proxy(ClassChecker): - """ - Tests the `Proxy` class. - """ - _cls = plugable.Proxy - - def test_class(self): - assert self.cls.__bases__ == (plugable.ReadOnly,) - - def test_proxy(self): - # Setup: - class base(object): - __public__ = frozenset(( - 'public_0', - 'public_1', - '__call__', - )) - - def public_0(self): - return 'public_0' - - def public_1(self): - return 'public_1' - - def __call__(self, caller): - return 'ya called it, %s.' % caller - - def private_0(self): - return 'private_0' - - def private_1(self): - return 'private_1' - - class plugin(base): - name = 'user_add' - attr_name = 'add' - doc = 'add a new user' - - # Test that TypeError is raised when base is not a class: - raises(TypeError, self.cls, base(), None) - - # Test that ValueError is raised when target is not instance of base: - raises(ValueError, self.cls, base, object()) - - # Test with correct arguments: - i = plugin() - p = self.cls(base, i) - assert read_only(p, 'name') is plugin.name - assert read_only(p, 'doc') == plugin.doc - assert list(p) == sorted(base.__public__) - - # Test normal methods: - for n in xrange(2): - pub = 'public_%d' % n - priv = 'private_%d' % n - assert getattr(i, pub)() == pub - assert getattr(p, pub)() == pub - assert hasattr(p, pub) - assert getattr(i, priv)() == priv - assert not hasattr(p, priv) - - # Test __call__: - value = 'ya called it, dude.' - assert i('dude') == value - assert p('dude') == value - assert callable(p) - - # Test name_attr='name' kw arg - i = plugin() - p = self.cls(base, i, 'attr_name') - assert read_only(p, 'name') == 'add' - - def test_implements(self): - """ - Tests the `implements` method. - """ - class base(object): - __public__ = frozenset() - name = 'base' - doc = 'doc' - @classmethod - def implements(cls, arg): - return arg + 7 - - class sub(base): - @classmethod - def implements(cls, arg): - """ - Defined to make sure base.implements() is called, not - target.implements() - """ - return arg - - o = sub() - p = self.cls(base, o) - assert p.implements(3) == 10 - - def test_clone(self): - """ - Tests the `__clone__` method. - """ - class base(object): - __public__ = frozenset() - class sub(base): - name = 'some_name' - doc = 'doc' - label = 'another_name' - - p = self.cls(base, sub()) - assert read_only(p, 'name') == 'some_name' - c = p.__clone__('label') - assert isinstance(c, self.cls) - assert c is not p - assert read_only(c, 'name') == 'another_name' - - -class test_ProxyTarget(ClassChecker): +class test_Plugin(ClassChecker): """ - Test the `ProxyTarget` class. + Tests the `Plugin` class. """ - _cls = plugable.ProxyTarget + _cls = plugable.Plugin def test_class(self): assert self.cls.__bases__ == (plugable.ReadOnly,) + assert self.cls.__public__ == frozenset() assert type(self.cls.name) is property - assert self.cls.implements(frozenset()) + assert type(self.cls.doc) is property + assert type(self.cls.api) is property def test_name(self): """ Tests the `name` property. """ - assert read_only(self.cls(), 'name') == 'ProxyTarget' + assert read_only(self.cls(), 'name') == 'Plugin' class some_subclass(self.cls): pass @@ -323,17 +209,6 @@ class test_ProxyTarget(ClassChecker): assert base.implemented_by(fail) is False assert base.implemented_by(fail()) is False - -class test_Plugin(ClassChecker): - """ - Tests the `Plugin` class. - """ - _cls = plugable.Plugin - - def test_class(self): - assert self.cls.__bases__ == (plugable.ProxyTarget,) - assert type(self.cls.api) is property - def test_finalize(self): """ Tests the `finalize` method. @@ -360,6 +235,122 @@ class test_Plugin(ClassChecker): raises(AssertionError, sub.finalize, api) +class test_Proxy(ClassChecker): + """ + Tests the `Proxy` class. + """ + _cls = plugable.Proxy + + def test_class(self): + assert self.cls.__bases__ == (plugable.ReadOnly,) + + def test_proxy(self): + # Setup: + class base(object): + __public__ = frozenset(( + 'public_0', + 'public_1', + '__call__', + )) + + def public_0(self): + return 'public_0' + + def public_1(self): + return 'public_1' + + def __call__(self, caller): + return 'ya called it, %s.' % caller + + def private_0(self): + return 'private_0' + + def private_1(self): + return 'private_1' + + class plugin(base): + name = 'user_add' + attr_name = 'add' + doc = 'add a new user' + + # Test that TypeError is raised when base is not a class: + raises(TypeError, self.cls, base(), None) + + # Test that ValueError is raised when target is not instance of base: + raises(ValueError, self.cls, base, object()) + + # Test with correct arguments: + i = plugin() + p = self.cls(base, i) + assert read_only(p, 'name') is plugin.name + assert read_only(p, 'doc') == plugin.doc + assert list(p) == sorted(base.__public__) + + # Test normal methods: + for n in xrange(2): + pub = 'public_%d' % n + priv = 'private_%d' % n + assert getattr(i, pub)() == pub + assert getattr(p, pub)() == pub + assert hasattr(p, pub) + assert getattr(i, priv)() == priv + assert not hasattr(p, priv) + + # Test __call__: + value = 'ya called it, dude.' + assert i('dude') == value + assert p('dude') == value + assert callable(p) + + # Test name_attr='name' kw arg + i = plugin() + p = self.cls(base, i, 'attr_name') + assert read_only(p, 'name') == 'add' + + def test_implements(self): + """ + Tests the `implements` method. + """ + class base(object): + __public__ = frozenset() + name = 'base' + doc = 'doc' + @classmethod + def implements(cls, arg): + return arg + 7 + + class sub(base): + @classmethod + def implements(cls, arg): + """ + Defined to make sure base.implements() is called, not + target.implements() + """ + return arg + + o = sub() + p = self.cls(base, o) + assert p.implements(3) == 10 + + def test_clone(self): + """ + Tests the `__clone__` method. + """ + class base(object): + __public__ = frozenset() + class sub(base): + name = 'some_name' + doc = 'doc' + label = 'another_name' + + p = self.cls(base, sub()) + assert read_only(p, 'name') == 'some_name' + c = p.__clone__('label') + assert isinstance(c, self.cls) + assert c is not p + assert read_only(c, 'name') == 'another_name' + + def test_check_name(): """ Tests the `check_name` function. -- cgit From 8c27f4c2ded1dfef8dcf406f4cfd89ba1e532e92 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 14 Aug 2008 07:43:43 +0000 Subject: 155: More docstring cleanup in plugable.py --- ipalib/plugable.py | 55 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 29 insertions(+), 26 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 645b2d16..5392c239 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -18,7 +18,7 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ -Base classes for plug-in architecture and generative API. +Base classes for plugin architecture. """ import re @@ -81,8 +81,8 @@ class ReadOnly(object): def __setattr__(self, name, value): """ - Raises an AttributeError if ReadOnly.__lock__() has already been called; - otherwise calls object.__setattr__() + Raises an AttributeError if `ReadOnly.__lock__()` has already been + called; otherwise calls object.__setattr__(). """ if self.__locked: raise AttributeError('read-only: cannot set %s.%s' % @@ -92,8 +92,8 @@ class ReadOnly(object): def __delattr__(self, name): """ - Raises an AttributeError if ReadOnly.__lock__() has already been called; - otherwise calls object.__delattr__() + Raises an AttributeError if `ReadOnly.__lock__()` has already been + called; otherwise calls object.__delattr__(). """ if self.__locked: raise AttributeError('read-only: cannot del %s.%s' % @@ -125,8 +125,8 @@ class Plugin(ReadOnly): def __get_api(self): """ - Returns the `API` instance passed to `finalize`, or - or returns None if `finalize` has not yet been called. + Returns the `API` instance passed to `finalize()`, or + or returns None if `finalize()` has not yet been called. """ return self.__api api = property(__get_api) @@ -329,12 +329,12 @@ class Proxy(ReadOnly): ) - - def check_name(name): """ - Raises errors.NameSpaceError if `name` is not a valid Python identifier - suitable for use in a NameSpace. + Raises `errors.NameSpaceError` if ``name`` is not a valid Python identifier + suitable for use in a `NameSpace`. + + :param name: Identifier to test. """ assert type(name) is str, 'must be %r' % str regex = r'^[a-z][_a-z0-9]*[a-z0-9]$' @@ -347,14 +347,18 @@ class NameSpace(ReadOnly): """ A read-only namespace with handy container behaviours. - Each member of a NameSpace instance must have a `name` attribute whose - value 1) is unique among the members and 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. + Each member of a NameSpace instance must have 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, assuming `obj` is a member in - the NameSpace instance `namespace`: + through a dictionary interface. For example, assuming ``obj`` is a member + in the NameSpace instance ``namespace``: >>> obj is getattr(namespace, obj.name) # As attribute True @@ -386,8 +390,7 @@ class NameSpace(ReadOnly): def __init__(self, members): """ - @type members: iterable - @param members: An iterable providing the members. + :param members: An iterable providing the members. """ self.__d = dict() self.__names = tuple(self.__member_iter(members)) @@ -396,7 +399,9 @@ class NameSpace(ReadOnly): def __member_iter(self, members): """ - Helper method used only from __init__(). + Helper method called only from `NameSpace.__init__()`. + + :param members: Same iterable passed to `NameSpace.__init__()`. """ for member in members: name = check_name(member.name) @@ -415,21 +420,19 @@ class NameSpace(ReadOnly): def __contains__(self, name): """ - Returns True if this NameSpace contains a member named `name`; returns + Returns True if this NameSpace contains a member named ``name``; returns False otherwise. - @type name: str - @param name: The name of a potential member + :param name: The name of a potential member """ return name in self.__d def __getitem__(self, name): """ - If this NameSpace contains a member named `name`, returns that member; + If this NameSpace contains a member named ``name``, returns that member; otherwise raises KeyError. - @type name: str - @param name: The name of member to retrieve + :param name: The name of member to retrieve """ if name in self.__d: return self.__d[name] -- cgit From f0dfb9f873ccafcc77b34e36f03723d73f9c5e0c Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 14 Aug 2008 08:28:48 +0000 Subject: 156: Fixed all broken docstring cross references --- ipalib/plugable.py | 33 ++++++++++++++++++++------------- ipalib/tests/test_cli.py | 16 +++++++++++----- ipalib/tests/test_plugable.py | 27 ++++++++++++++------------- ipalib/tests/test_public.py | 28 ++++++++++++++-------------- 4 files changed, 59 insertions(+), 45 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 5392c239..9fb3c079 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -182,13 +182,19 @@ class Plugin(ReadOnly): @classmethod def implemented_by(cls, arg): """ - Returns True if (1) `arg` is an instance of or subclass of this class, - and (2) `arg` (or `arg.__class__` if instance) has an attribute for - each name in this class's __public__ frozenset; returns False - otherwise. + Returns True if. - Unlike ProxyTarget.implements(), this returns a concrete answer - because the attributes of the subclass are checked. + 1. ``arg`` is an instance of or subclass of this class, and + + 2. ``arg`` (or ``arg.__class__`` if instance) has an attribute for + each name in this class's ``__public__`` frozenset + + Otherwise, returns False. + + Unlike `Plugin.implements`, this returns a concrete answer because + the attributes of the subclass are checked. + + :param arg: An instance of or subclass of this class. """ if inspect.isclass(arg): subclass = arg @@ -465,8 +471,8 @@ class NameSpace(ReadOnly): class Registrar(ReadOnly): def __init__(self, *allowed): """ - `*allowed` is a list of the base classes plugins can be subclassed - from. + :param *allowed: Base classes from which plugins accepted by this + Registrar must subclass. """ self.__allowed = frozenset(allowed) self.__d = {} @@ -480,8 +486,8 @@ class Registrar(ReadOnly): def __findbase(self, cls): """ - If `cls` is a subclass of a base in self.__allowed, returns that - base; otherwise raises SubclassError. + If ``cls`` is a subclass of a base in self.__allowed, returns that + base; otherwise raises `errors.SubclassError`. """ assert inspect.isclass(cls) found = False @@ -494,7 +500,7 @@ class Registrar(ReadOnly): def __call__(self, cls, override=False): """ - Register the plugin `cls`. + Register the plugin ``cls``. """ if not inspect.isclass(cls): raise TypeError('plugin must be a class: %r' % cls) @@ -525,7 +531,8 @@ class Registrar(ReadOnly): def __getitem__(self, item): """ - Returns a copy of the namespace dict of the base class named `name`. + Returns a copy of the namespace dict of the base class named + ``name``. """ if inspect.isclass(item): if item not in self.__allowed: @@ -537,7 +544,7 @@ class Registrar(ReadOnly): def __contains__(self, item): """ - Returns True if a base class named `name` is in this Registrar. + Returns True if a base class named ``name`` is in this Registrar. """ if inspect.isclass(item): return item in self.__allowed diff --git a/ipalib/tests/test_cli.py b/ipalib/tests/test_cli.py index 4c14c0dd..a5ee8a94 100644 --- a/ipalib/tests/test_cli.py +++ b/ipalib/tests/test_cli.py @@ -26,12 +26,18 @@ from ipalib import cli, plugable def test_to_cli(): + """ + Tests the `cli.to_cli` function. + """ f = cli.to_cli assert f('initialize') == 'initialize' assert f('user_add') == 'user-add' def test_from_cli(): + """ + Tests the `cli.from_cli` function. + """ f = cli.from_cli assert f('initialize') == 'initialize' assert f('user-add') == 'user_add' @@ -71,7 +77,7 @@ class DummyAPI(object): class test_CLI(ClassChecker): """ - Tests the `CLI` class. + Tests the `cli.CLI` class. """ _cls = cli.CLI @@ -80,7 +86,7 @@ class test_CLI(ClassChecker): def test_api(self): """ - Tests the `api` property. + Tests the `cli.CLI.api` property. """ api = 'the plugable.API instance' o = self.cls(api) @@ -88,7 +94,7 @@ class test_CLI(ClassChecker): def test_parse(self): """ - Tests the `parse` method. + Tests the `cli.CLI.parse` method. """ o = self.cls(None) args = ['hello', 'naughty', 'nurse'] @@ -104,7 +110,7 @@ class test_CLI(ClassChecker): def test_mcl(self): """ - Tests the `mcl` (Max Command Length) property . + Tests the `cli.CLI.mcl` (Max Command Length) property . """ cnt = 100 api = DummyAPI(cnt) @@ -116,7 +122,7 @@ class test_CLI(ClassChecker): def test_dict(self): """ - Tests the `__contains__` and `__getitem__` methods. + Tests the `cli.CLI.__contains__` and `cli.CLI.__getitem__` methods. """ cnt = 25 api = DummyAPI(cnt) diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index aa449b71..c65db015 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -28,7 +28,7 @@ from ipalib import plugable, errors class test_ReadOnly(ClassChecker): """ - Test the `ReadOnly` class + Test the `plugable.ReadOnly` class """ _cls = plugable.ReadOnly @@ -39,7 +39,8 @@ class test_ReadOnly(ClassChecker): def test_lock(self): """ - Tests the `__lock__` and `__islocked__` methods. + Tests the `plugable.ReadOnly.__lock__` and + `plugable.ReadOnly.__islocked__` methods. """ o = self.cls() assert o.__islocked__() is False @@ -88,7 +89,7 @@ class test_ReadOnly(ClassChecker): class test_Plugin(ClassChecker): """ - Tests the `Plugin` class. + Tests the `plugable.Plugin` class. """ _cls = plugable.Plugin @@ -101,7 +102,7 @@ class test_Plugin(ClassChecker): def test_name(self): """ - Tests the `name` property. + Tests the `plugable.Plugin.name` property. """ assert read_only(self.cls(), 'name') == 'Plugin' @@ -111,7 +112,7 @@ class test_Plugin(ClassChecker): def test_doc(self): """ - Tests the `doc` property. + Tests the `plugable.Plugin.doc` property. """ class some_subclass(self.cls): 'here is the doc string' @@ -119,7 +120,7 @@ class test_Plugin(ClassChecker): def test_implements(self): """ - Tests the `implements` classmethod. + Tests the `plugable.Plugin.implements` classmethod. """ class example(self.cls): __public__ = frozenset(( @@ -168,7 +169,7 @@ class test_Plugin(ClassChecker): def test_implemented_by(self): """ - Tests the `implemented_by` classmethod. + Tests the `plugable.Plugin.implemented_by` classmethod. """ class base(self.cls): __public__ = frozenset(( @@ -211,7 +212,7 @@ class test_Plugin(ClassChecker): def test_finalize(self): """ - Tests the `finalize` method. + Tests the `plugable.Plugin.finalize` method. """ api = 'the api instance' o = self.cls() @@ -237,7 +238,7 @@ class test_Plugin(ClassChecker): class test_Proxy(ClassChecker): """ - Tests the `Proxy` class. + Tests the `plugable.Proxy` class. """ _cls = plugable.Proxy @@ -309,7 +310,7 @@ class test_Proxy(ClassChecker): def test_implements(self): """ - Tests the `implements` method. + Tests the `plugable.Proxy.implements` method. """ class base(object): __public__ = frozenset() @@ -334,7 +335,7 @@ class test_Proxy(ClassChecker): def test_clone(self): """ - Tests the `__clone__` method. + Tests the `plugable.Proxy.__clone__` method. """ class base(object): __public__ = frozenset() @@ -353,7 +354,7 @@ class test_Proxy(ClassChecker): def test_check_name(): """ - Tests the `check_name` function. + Tests the `plugable.check_name` function. """ f = plugable.check_name okay = [ @@ -380,7 +381,7 @@ def test_check_name(): class test_NameSpace(ClassChecker): """ - Tests the `NameSpace` class. + Tests the `plugable.NameSpace` class. """ _cls = plugable.NameSpace diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index a331316d..2c111c38 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -65,7 +65,7 @@ def test_is_rule(): class test_option(ClassChecker): """ - Tests the option class. + Tests the `public.option` class. """ _cls = public.option @@ -96,7 +96,7 @@ class test_option(ClassChecker): def test_normalize(self): """ - Tests the `normalize` method. + Tests the `public.option.normalize` method. """ assert 'normalize' in self.cls.__public__ o = self.subcls() @@ -128,7 +128,7 @@ class test_option(ClassChecker): def test_validate(self): """ - Tests the `validate` method. + Tests the `public.option.validate` method. """ assert 'validate' in self.cls.__public__ o = self.subcls() @@ -140,7 +140,7 @@ class test_option(ClassChecker): def test_rules(self): """ - Tests the `rules` property. + Tests the `public.option.rules` property. """ o = self.subcls() assert len(o.rules) == 3 @@ -151,7 +151,7 @@ class test_option(ClassChecker): def test_default(self): """ - Tests the `default` method. + Tests the `public.option.default` method. """ assert 'default' in self.cls.__public__ assert self.cls().default() is None @@ -159,7 +159,7 @@ class test_option(ClassChecker): class test_cmd(ClassChecker): """ - Tests the `cmd` class. + Tests the `public.cmd` class. """ _cls = public.cmd @@ -188,7 +188,7 @@ class test_cmd(ClassChecker): def test_get_options(self): """ - Tests the `get_options` method. + Tests the `public.cmd.get_options` method. """ assert list(self.cls().get_options()) == [] sub = self.subcls() @@ -200,7 +200,7 @@ class test_cmd(ClassChecker): def test_options(self): """ - Tests the `options` property. + Tests the `public.cmd.options` property. """ assert 'options' in self.cls.__public__ # Public sub = self.subcls() @@ -216,7 +216,7 @@ class test_cmd(ClassChecker): def test_normalize(self): """ - Tests the `normalize` method. + Tests the `public.cmd.normalize` method. """ assert 'normalize' in self.cls.__public__ # Public kw = dict( @@ -230,7 +230,7 @@ class test_cmd(ClassChecker): def test_default(self): """ - Tests the `default` method. + Tests the `public.cmd.default` method. """ assert 'default' in self.cls.__public__ # Public no_fill = dict( @@ -251,7 +251,7 @@ class test_cmd(ClassChecker): def test_validate(self): """ - Tests the `validate` method. + Tests the `public.cmd.validate` method. """ assert 'validate' in self.cls.__public__ # Public @@ -281,7 +281,7 @@ class test_cmd(ClassChecker): def test_execute(self): """ - Tests the `execute` method. + Tests the `public.cmd.execute` method. """ assert 'execute' in self.cls.__public__ # Public @@ -317,7 +317,7 @@ def test_attr(): class test_mthd(ClassChecker): """ - Tests the `mthd` class. + Tests the `public.mthd` class. """ _cls = public.mthd @@ -351,7 +351,7 @@ class test_mthd(ClassChecker): def test_get_options(self): """ - Tests the `get_options` method. + Tests the `public.mthd.get_options` method. """ sub = self.subcls() names = ('option0', 'option1', 'prop0', 'prop1') -- cgit From a3dc04ade4c8b640a881519144f009b70c6e4cfd Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 14 Aug 2008 09:01:02 +0000 Subject: 157: More docstring cleanup; fixed remaining epydoc warnings --- ipalib/__init__.py | 11 ++++++++++- ipalib/plugable.py | 14 +++++++------- 2 files changed, 17 insertions(+), 8 deletions(-) (limited to 'ipalib') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index ddce3ac9..309bd2e2 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -18,5 +18,14 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ -IPA library. +The IPA Library. + +To learn about the library, you should probably read the code in this order: + + 1. Start with the `ipalib.plugable` module + + 2. Then read the `ipalib.public` module + +Some of the plugin architecture was inspired by ``bzr``, so you might also +read http://bazaar-vcs.org/WritingPlugins """ diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 9fb3c079..ba4be6be 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -18,7 +18,7 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ -Base classes for plugin architecture. +Generic plugin framework. """ import re @@ -139,7 +139,7 @@ class Plugin(ReadOnly): There are three different ways this can be called: - 1. With a argument, e.g.: + With a argument, e.g.: >>> class base(ProxyTarget): >>> __public__ = frozenset(['some_attr', 'another_attr']) @@ -148,14 +148,14 @@ class Plugin(ReadOnly): >>> base.implements('an_unknown_attribute') False - 2. With a argument, e.g.: + With a argument, e.g.: >>> base.implements(frozenset(['some_attr'])) True >>> base.implements(frozenset(['some_attr', 'an_unknown_attribute'])) False - 3. With any object that has a `__public__` attribute that is + With any object that has a `__public__` attribute that is , e.g.: >>> class whatever(object): @@ -182,12 +182,12 @@ class Plugin(ReadOnly): @classmethod def implemented_by(cls, arg): """ - Returns True if. + Returns True if: 1. ``arg`` is an instance of or subclass of this class, and 2. ``arg`` (or ``arg.__class__`` if instance) has an attribute for - each name in this class's ``__public__`` frozenset + each name in this class's ``__public__`` frozenset Otherwise, returns False. @@ -471,7 +471,7 @@ class NameSpace(ReadOnly): class Registrar(ReadOnly): def __init__(self, *allowed): """ - :param *allowed: Base classes from which plugins accepted by this + :param allowed: Base classes from which plugins accepted by this Registrar must subclass. """ self.__allowed = frozenset(allowed) -- cgit From ca53615dddd487230c3e40231cb02467e19388d7 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 14 Aug 2008 09:38:28 +0000 Subject: 158: Name local arg 'cls' to 'klass' in Registrar methods to avoid confusion with classmethods; some docstring improvement in Registrar --- ipalib/plugable.py | 46 ++++++++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 20 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index ba4be6be..1df3f836 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -364,7 +364,7 @@ class NameSpace(ReadOnly): The members can be accessed as attributes on the NameSpace instance or through a dictionary interface. For example, assuming ``obj`` is a member - in the NameSpace instance ``namespace``: + in the NameSpace instance ``namespace``, you could do this: >>> obj is getattr(namespace, obj.name) # As attribute True @@ -477,57 +477,63 @@ class Registrar(ReadOnly): self.__allowed = frozenset(allowed) self.__d = {} self.__registered = set() - assert len(self.__allowed) == len(allowed) for base in self.__allowed: assert inspect.isclass(base) assert base.__name__ not in self.__d self.__d[base.__name__] = {} self.__lock__() - def __findbase(self, cls): + def __findbases(self, klass): """ - If ``cls`` is a subclass of a base in self.__allowed, returns that - base; otherwise raises `errors.SubclassError`. + Iterates through allowed bases that ``klass`` is a subclass of. + + Raises `errors.SubclassError` if ``klass`` is not a subclass of any + allowed base. + + :param klass: The class to find bases for. """ - assert inspect.isclass(cls) + assert inspect.isclass(klass) found = False for base in self.__allowed: - if issubclass(cls, base): + if issubclass(klass, base): found = True yield base if not found: - raise errors.SubclassError(cls, self.__allowed) + raise errors.SubclassError(klass, self.__allowed) - def __call__(self, cls, override=False): + def __call__(self, klass, override=False): """ - Register the plugin ``cls``. + Register the plugin ``klass``. + + :param klass: A subclass of `Plugin` to attempt to register. + :param override: If true, override an already registered plugin. """ - if not inspect.isclass(cls): - raise TypeError('plugin must be a class: %r' % cls) + if not inspect.isclass(klass): + raise TypeError('plugin must be a class: %r' % klass) # Raise DuplicateError if this exact class was already registered: - if cls in self.__registered: - raise errors.DuplicateError(cls) + if klass in self.__registered: + raise errors.DuplicateError(klass) # Find the base class or raise SubclassError: - for base in self.__findbase(cls): + for base in self.__findbases(klass): sub_d = self.__d[base.__name__] # Check override: - if cls.__name__ in sub_d: + if klass.__name__ in sub_d: # Must use override=True to override: if not override: - raise errors.OverrideError(base, cls) + raise errors.OverrideError(base, klass) else: # There was nothing already registered to override: if override: - raise errors.MissingOverrideError(base, cls) + raise errors.MissingOverrideError(base, klass) # The plugin is okay, add to sub_d: - sub_d[cls.__name__] = cls + sub_d[klass.__name__] = klass # The plugin is okay, add to __registered: - self.__registered.add(cls) + self.__registered.add(klass) def __getitem__(self, item): """ -- cgit From b403fd822b76a7deffe8110fbeb7993ef3cac3a5 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 14 Aug 2008 17:21:21 +0000 Subject: 159: Added plugable.DictProxy class; added corresponding unit tests; added setitem(), delitem() functions to tstutil --- ipalib/plugable.py | 70 ++++++++++++++++++++++++++++++++++++++++--- ipalib/tests/test_plugable.py | 57 ++++++++++++++++++++++++++++++++++- ipalib/tests/tstutil.py | 16 ++++++++++ 3 files changed, 138 insertions(+), 5 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 1df3f836..66cb18fe 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -426,8 +426,8 @@ class NameSpace(ReadOnly): def __contains__(self, name): """ - Returns True if this NameSpace contains a member named ``name``; returns - False otherwise. + Returns True if instance contains a member named ``name``, otherwise + False. :param name: The name of a potential member """ @@ -435,8 +435,10 @@ class NameSpace(ReadOnly): def __getitem__(self, name): """ - If this NameSpace contains a member named ``name``, returns that member; - otherwise raises KeyError. + Returns the member named ``name``. + + Raises KeyError if this NameSpace does not contain a member named + ``name``. :param name: The name of member to retrieve """ @@ -468,6 +470,66 @@ class NameSpace(ReadOnly): return '%s(<%d members>)' % (self.__class__.__name__, len(self)) +class DictProxy(ReadOnly): + """ + A read-only dict whose items can also be accessed as attributes. + + Although a DictProxy is read-only, the underlying dict can change (and is + assumed to). + + One of these is created for each allowed base class in a `Registrar` + instance. + """ + def __init__(self, d): + """ + :param d: The ``dict`` instance to proxy. + """ + self.__d = d + self.__lock__() + assert self.__islocked__() + + def __len__(self): + """ + Returns number of items in underlying ``dict``. + """ + return len(self.__d) + + def __iter__(self): + """ + Iterates through keys of underlying ``dict`` in ascending order. + """ + for name in sorted(self.__d): + yield name + + def __contains__(self, key): + """ + Returns True if underlying dict contains ``key``, False otherwise. + + :param key: The key to query upon. + """ + return key in self.__d + + def __getitem__(self, key): + """ + Returns value from underlying dict corresponding to ``key``. + + :param key: The key of the value to retrieve. + """ + if key in self.__d: + return self.__d[key] + raise KeyError('no item at key %r' % key) + + def __getattr__(self, name): + """ + Returns value from underlying dict corresponding to ``name``. + + :param name: The name of the attribute to retrieve. + """ + if name in self.__d: + return self.__d[name] + raise AttributeError('no attribute %r' % name) + + class Registrar(ReadOnly): def __init__(self, *allowed): """ diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index c65db015..5c907dc7 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -21,7 +21,8 @@ Unit tests for `ipalib.plugable` module. """ -from tstutil import raises, getitem, no_set, no_del, read_only +from tstutil import raises, no_set, no_del, read_only +from tstutil import getitem, setitem, delitem from tstutil import ClassChecker from ipalib import plugable, errors @@ -452,6 +453,60 @@ class test_NameSpace(ClassChecker): no_set(ns, name) +class test_DictProxy(ClassChecker): + """ + Tests the `plugable.DictProxy` class. + """ + _cls = plugable.DictProxy + + def test_class(self): + assert self.cls.__bases__ == (plugable.ReadOnly,) + + def test_DictProxy(self): + cnt = 10 + keys = [] + d = dict() + dictproxy = self.cls(d) + for i in xrange(cnt): + key = 'key_%d' % i + val = 'val_%d' % i + keys.append(key) + + # Test thet key does not yet exist + assert len(dictproxy) == i + assert key not in dictproxy + assert not hasattr(dictproxy, key) + raises(KeyError, getitem, dictproxy, key) + raises(AttributeError, getattr, dictproxy, key) + + # Test that items/attributes cannot be set on dictproxy: + raises(TypeError, setitem, dictproxy, key, val) + raises(AttributeError, setattr, dictproxy, key, val) + + # Test that additions in d are reflected in dictproxy: + d[key] = val + assert len(dictproxy) == i + 1 + assert key in dictproxy + assert hasattr(dictproxy, key) + assert dictproxy[key] is val + assert read_only(dictproxy, key) is val + + # Test __iter__ + assert list(dictproxy) == keys + + for key in keys: + # Test that items cannot be deleted through dictproxy: + raises(TypeError, delitem, dictproxy, key) + raises(AttributeError, delattr, dictproxy, key) + + # Test that deletions in d are reflected in dictproxy + del d[key] + assert len(dictproxy) == len(d) + assert key not in dictproxy + raises(KeyError, getitem, dictproxy, key) + raises(AttributeError, getattr, dictproxy, key) + + def test_Registrar(): class Base1(object): pass diff --git a/ipalib/tests/tstutil.py b/ipalib/tests/tstutil.py index 7b3a2d5e..79e8ae38 100644 --- a/ipalib/tests/tstutil.py +++ b/ipalib/tests/tstutil.py @@ -60,6 +60,22 @@ def getitem(obj, key): return obj[key] +def setitem(obj, key, value): + """ + Works like setattr but for dictionary interface. Uses this in combination + with raises() to test that, for example, TypeError is raised. + """ + obj[key] = value + + +def delitem(obj, key): + """ + Works like delattr but for dictionary interface. Uses this in combination + with raises() to test that, for example, TypeError is raised. + """ + del obj[key] + + def no_set(obj, name, value='some_new_obj'): """ Tests that attribute cannot be set. -- cgit From 87cad5078a3c9ef7a978c85905309ee7d3ec194d Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 14 Aug 2008 17:29:13 +0000 Subject: 160: DictProxy now checks type of d in __init__(); updated unit tests --- ipalib/plugable.py | 1 + ipalib/tests/test_plugable.py | 2 ++ 2 files changed, 3 insertions(+) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 66cb18fe..ffef8d64 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -484,6 +484,7 @@ class DictProxy(ReadOnly): """ :param d: The ``dict`` instance to proxy. """ + assert type(d) is dict, '`d` must be %r, got %r' % (dict, type(d)) self.__d = d self.__lock__() assert self.__islocked__() diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index 5c907dc7..2854ee6a 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -461,6 +461,8 @@ class test_DictProxy(ClassChecker): def test_class(self): assert self.cls.__bases__ == (plugable.ReadOnly,) + for non_dict in ('hello', 69, object): + raises(AssertionError, self.cls, non_dict) def test_DictProxy(self): cnt = 10 -- cgit From 7c64c8b95457c3aed1a3243ef1c22c303697a057 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 14 Aug 2008 18:50:21 +0000 Subject: 161: Registrar now takes advantage of DictProxy; updated corresponding unit tests --- ipalib/plugable.py | 78 ++++++++++++++++++++++++++++--------------- ipalib/tests/test_plugable.py | 46 ++++++++++++------------- 2 files changed, 74 insertions(+), 50 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index ffef8d64..b663a7ea 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -102,6 +102,16 @@ class ReadOnly(object): return object.__delattr__(self, name) +def lock(obj): + """ + Convenience function to lock a `ReadOnly` instance. + """ + assert isinstance(obj, ReadOnly) + obj.__lock__() + assert obj.__islocked__() + return obj + + class Plugin(ReadOnly): """ Base class for all plugins. @@ -477,8 +487,7 @@ class DictProxy(ReadOnly): Although a DictProxy is read-only, the underlying dict can change (and is assumed to). - One of these is created for each allowed base class in a `Registrar` - instance. + One of these is created for each allowed base in a `Registrar` instance. """ def __init__(self, d): """ @@ -486,8 +495,7 @@ class DictProxy(ReadOnly): """ assert type(d) is dict, '`d` must be %r, got %r' % (dict, type(d)) self.__d = d - self.__lock__() - assert self.__islocked__() + lock(self) def __len__(self): """ @@ -532,19 +540,44 @@ class DictProxy(ReadOnly): class Registrar(ReadOnly): + """ + Collects plugin classes as they are registered. + + The Registrar does not instantiate plugins... it only implements the + override logic and stores the plugins in a namespace per allowed base + class. + + The plugins are instantiated when `API.finalize()` is called. + """ def __init__(self, *allowed): """ :param allowed: Base classes from which plugins accepted by this Registrar must subclass. """ - self.__allowed = frozenset(allowed) + + class Val(ReadOnly): + """ + Internal class used so that only one mapping is needed. + """ + def __init__(self, base): + assert inspect.isclass(base) + self.base = base + self.name = base.__name__ + self.sub_d = dict() + self.dictproxy = DictProxy(self.sub_d) + lock(self) + + self.__allowed = allowed self.__d = {} self.__registered = set() for base in self.__allowed: - assert inspect.isclass(base) - assert base.__name__ not in self.__d - self.__d[base.__name__] = {} - self.__lock__() + val = Val(base) + assert not ( + val.name in self.__d or hasattr(self, val.name) + ) + self.__d[val.name] = val + setattr(self, val.name, val.dictproxy) + lock(self) def __findbases(self, klass): """ @@ -580,7 +613,7 @@ class Registrar(ReadOnly): # Find the base class or raise SubclassError: for base in self.__findbases(klass): - sub_d = self.__d[base.__name__] + sub_d = self.__d[base.__name__].sub_d # Check override: if klass.__name__ in sub_d: @@ -598,26 +631,19 @@ class Registrar(ReadOnly): # The plugin is okay, add to __registered: self.__registered.add(klass) - def __getitem__(self, item): + def __getitem__(self, key): """ - Returns a copy of the namespace dict of the base class named - ``name``. + Returns the DictProxy for plugins subclassed from the base named ``key``. """ - if inspect.isclass(item): - if item not in self.__allowed: - raise KeyError(repr(item)) - key = item.__name__ - else: - key = item - return dict(self.__d[key]) + if key not in self.__d: + raise KeyError('no base class named %r' % key) + return self.__d[key].dictproxy - def __contains__(self, item): + def __contains__(self, key): """ - Returns True if a base class named ``name`` is in this Registrar. + Returns True if a base class named ``key`` is in this Registrar. """ - if inspect.isclass(item): - return item in self.__allowed - return item in self.__d + return key in self.__d def __iter__(self): """ @@ -625,7 +651,7 @@ class Registrar(ReadOnly): base. """ for base in self.__allowed: - sub_d = self.__d[base.__name__] + sub_d = self.__d[base.__name__].sub_d yield (base, tuple(sub_d[k] for k in sorted(sub_d))) diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index 2854ee6a..b64cf305 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -528,11 +528,10 @@ def test_Registrar(): # Test __hasitem__, __getitem__: for base in [Base1, Base2]: - assert base in r assert base.__name__ in r - assert r[base] == {} - assert r[base.__name__] == {} - + dp = r[base.__name__] + assert type(dp) is plugable.DictProxy + assert len(dp) == 0 # Check that TypeError is raised trying to register something that isn't # a class: @@ -544,12 +543,12 @@ def test_Registrar(): # Check that registration works r(plugin1) - sub_d = r['Base1'] - assert len(sub_d) == 1 - assert sub_d['plugin1'] is plugin1 - # Check that a copy is returned - assert sub_d is not r['Base1'] - assert sub_d == r['Base1'] + dp = r['Base1'] + assert type(dp) is plugable.DictProxy + assert len(dp) == 1 + assert r.Base1 is dp + assert dp['plugin1'] is plugin1 + assert dp.plugin1 is plugin1 # Check that DuplicateError is raised trying to register exact class # again: @@ -566,21 +565,19 @@ def test_Registrar(): # Check that overriding works r(plugin1, override=True) - sub_d = r['Base1'] - assert len(sub_d) == 1 - assert sub_d['plugin1'] is plugin1 - assert sub_d['plugin1'] is not orig1 + assert len(r.Base1) == 1 + assert r.Base1.plugin1 is plugin1 + assert r.Base1.plugin1 is not orig1 # Check that MissingOverrideError is raised trying to override a name # not yet registerd: raises(errors.MissingOverrideError, r, plugin2, override=True) - # Check that additional plugin can be registered: + # Test that another plugin can be registered: + assert len(r.Base2) == 0 r(plugin2) - sub_d = r['Base2'] - assert len(sub_d) == 1 - assert sub_d['plugin2'] is plugin2 - + assert len(r.Base2) == 1 + assert r.Base2.plugin2 is plugin2 # Setup to test __iter__: class plugin1a(Base1): @@ -612,12 +609,13 @@ def test_Registrar(): # Again test __hasitem__, __getitem__: for base in [Base1, Base2]: - assert base in r assert base.__name__ in r - d = dict((p.__name__, p) for p in m[base.__name__]) - assert len(d) == 3 - assert r[base] == d - assert r[base.__name__] == d + dp = r[base.__name__] + assert len(dp) == 3 + for key in dp: + klass = dp[key] + assert getattr(dp, key) is klass + assert issubclass(klass, base) def test_API(): -- cgit From f423f2c9f0634d3b123eaaae8b13afd83cc0cf94 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 14 Aug 2008 18:59:12 +0000 Subject: 162: Added link to container emulation documentation in plugable.py docstring --- ipalib/plugable.py | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index b663a7ea..30a4a5f0 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -19,6 +19,10 @@ """ Generic plugin framework. + +The classes in this module make heavy use of Python container emulation. If +you are unfamiliar with this Python feature, see +http://docs.python.org/ref/sequence-types.html """ import re -- cgit From 43c04f1cd356a46aab6720c64e8d15900b46bfdf Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 14 Aug 2008 19:36:54 +0000 Subject: 163: Docstring improvement for ipalib/__init__.py and plugable.py --- ipalib/__init__.py | 8 +++++--- ipalib/plugable.py | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) (limited to 'ipalib') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index 309bd2e2..84b529e1 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -20,11 +20,13 @@ """ The IPA Library. -To learn about the library, you should probably read the code in this order: +To learn about the ``ipalib`` library, you should read the code in this order: - 1. Start with the `ipalib.plugable` module + 1. Learn about the plugin framework in `ipalib.plugable` - 2. Then read the `ipalib.public` module + 2. Learn about the public api in `ipalib.public` + + 3. Look at some example plugins in `ipalib.plugins.example` Some of the plugin architecture was inspired by ``bzr``, so you might also read http://bazaar-vcs.org/WritingPlugins diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 30a4a5f0..60a8c548 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -18,7 +18,7 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ -Generic plugin framework. +Implementation of the plugin framework. The classes in this module make heavy use of Python container emulation. If you are unfamiliar with this Python feature, see -- cgit From d229a764749b37aded48ed6eec230df9105a62b0 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 14 Aug 2008 20:32:35 +0000 Subject: 165: Added unit tests for plugable.lock() function; replaced occurances of 'self.__lock__()' with 'lock(self)' in plugable.py --- ipalib/plugable.py | 28 +++++++++++++++++----------- ipalib/tests/test_plugable.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 11 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 60a8c548..5bfe5977 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -18,7 +18,7 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ -Implementation of the plugin framework. +Plugin framework. The classes in this module make heavy use of Python container emulation. If you are unfamiliar with this Python feature, see @@ -50,7 +50,7 @@ class ReadOnly(object): >>> class givenname(ReadOnly): >>> def __init__(self): >>> self.whatever = 'some value' # Hasn't been locked yet - >>> self.__lock__() + >>> lock(self) >>> >>> def finalize(self, api): >>> # After the instance has been locked, attributes can still be @@ -106,14 +106,20 @@ class ReadOnly(object): return object.__delattr__(self, name) -def lock(obj): +def lock(readonly): """ - Convenience function to lock a `ReadOnly` instance. + Locks a `ReadOnly` instance. + + This is mostly a convenience function to call `ReadOnly.__lock__()`. It + also verifies that the locking worked using `ReadOnly.__islocked__()` + + :param readonly: An instance of the `ReadOnly` class. """ - assert isinstance(obj, ReadOnly) - obj.__lock__() - assert obj.__islocked__() - return obj + if not isinstance(readonly, ReadOnly): + raise ValueError('not a ReadOnly instance: %r' % readonly) + readonly.__lock__() + assert readonly.__islocked__(), 'Ouch! The locking failed?' + return readonly class Plugin(ReadOnly): @@ -282,7 +288,7 @@ class Proxy(ReadOnly): self.__public__ = base.__public__ self.name = getattr(target, name_attr) self.doc = target.doc - self.__lock__() + lock(self) assert type(self.__public__) is frozenset def implements(self, arg): @@ -414,7 +420,7 @@ class NameSpace(ReadOnly): """ self.__d = dict() self.__names = tuple(self.__member_iter(members)) - self.__lock__() + lock(self) assert set(self.__d) == set(self.__names) def __member_iter(self, members): @@ -665,7 +671,7 @@ class API(ReadOnly): def __init__(self, *allowed): self.__keys = tuple(b.__name__ for b in allowed) self.register = Registrar(*allowed) - self.__lock__() + lock(self) def finalize(self): """ diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index b64cf305..839451b5 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -88,6 +88,35 @@ class test_ReadOnly(ClassChecker): assert read_only(obj, 'an_attribute') == 'Hello world!' +def test_lock(): + """ + Tests the `plugable.lock` function. + """ + f = plugable.lock + + # Test on a ReadOnly instance: + o = plugable.ReadOnly() + assert not o.__islocked__() + assert f(o) is o + assert o.__islocked__() + + # Test on something not subclassed from ReadOnly: + class not_subclass(object): + def __lock__(self): + pass + def __islocked__(self): + return True + o = not_subclass() + raises(ValueError, f, o) + + # Test that it checks __islocked__(): + class subclass(plugable.ReadOnly): + def __islocked__(self): + return False + o = subclass() + raises(AssertionError, f, o) + + class test_Plugin(ClassChecker): """ Tests the `plugable.Plugin` class. -- cgit From 5f38daf6dee93c7b80b79b6c915ce6916c79fcdc Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 14 Aug 2008 21:40:37 +0000 Subject: 167: In API.finalize(), lock(plugin) is used instead of plugin.__lock__(); more docstring improvements in plugable.py --- ipalib/plugable.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 5bfe5977..e82167ab 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -357,8 +357,10 @@ class Proxy(ReadOnly): def check_name(name): """ - Raises `errors.NameSpaceError` if ``name`` is not a valid Python identifier - suitable for use in a `NameSpace`. + Verifies that ``name`` is suitable for a `NameSpace` member name. + + Raises `errors.NameSpaceError` if ``name`` is not a valid Python + identifier suitable for use as the name of `NameSpace` member. :param name: Identifier to test. """ @@ -666,6 +668,9 @@ class Registrar(ReadOnly): class API(ReadOnly): + """ + Dynamic API object through which `Plugin` instances are accessed. + """ __finalized = False def __init__(self, *allowed): @@ -692,8 +697,7 @@ class API(ReadOnly): object.__setattr__(self, base.__name__, ns) for plugin in d.values(): plugin.finalize(self) - plugin.__lock__() - assert plugin.__islocked__() is True + lock(plugin) assert plugin.api is self object.__setattr__(self, '_API__finalized', True) -- cgit From 07cd5372779fe88b1dd6c252e157b48a944c4669 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 14 Aug 2008 22:13:42 +0000 Subject: 168: plugable.API now implements the all the usual container methods --- ipalib/plugable.py | 51 ++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 40 insertions(+), 11 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index e82167ab..8d6a8047 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -674,7 +674,7 @@ class API(ReadOnly): __finalized = False def __init__(self, *allowed): - self.__keys = tuple(b.__name__ for b in allowed) + self.__d = dict() self.register = Registrar(*allowed) lock(self) @@ -683,24 +683,53 @@ class API(ReadOnly): Finalize the registration, instantiate the plugins. """ assert not self.__finalized, 'finalize() can only be called once' - d = {} + + instances = {} def plugin_iter(base, classes): - for cls in classes: - if cls not in d: - d[cls] = cls() - plugin = d[cls] + for klass in classes: + if klass not in instances: + instances[klass] = klass() + plugin = instances[klass] yield Proxy(base, plugin) for (base, classes) in self.register: - ns = NameSpace(plugin_iter(base, classes)) - assert not hasattr(self, base.__name__) - object.__setattr__(self, base.__name__, ns) - for plugin in d.values(): + namespace = NameSpace(plugin_iter(base, classes)) + name = base.__name__ + assert not ( + name in self.__d or hasattr(self, name) + ) + self.__d[name] = namespace + object.__setattr__(self, name, namespace) + + for plugin in instances.values(): plugin.finalize(self) lock(plugin) assert plugin.api is self object.__setattr__(self, '_API__finalized', True) + def __len__(self): + """ + Returns the number of namespaces in this API. + """ + return len(self.__d) + def __iter__(self): - for key in self.__keys: + """ + Iterates through the names of the namespaces in this API. + """ + for key in sorted(self.__d): yield key + + def __contains__(self, key): + """ + Returns True if this API contains a `NameSpace` named ``key``. + """ + return key in self.__d + + def __getitem__(self, key): + """ + Returns the `NameSpace` instance named ``key``. + """ + if key in self.__d: + return self.__d[key] + raise KeyError('API has no NameSpace %r' % key) -- cgit From 88a5b3ae2587ef71efecc1b59eb9ec94e09cacad Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 14 Aug 2008 23:49:36 +0000 Subject: 169: Renamed DictProxy to MagicDict --- ipalib/plugable.py | 8 ++++---- ipalib/tests/test_plugable.py | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 8d6a8047..45c73354 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -492,11 +492,11 @@ class NameSpace(ReadOnly): return '%s(<%d members>)' % (self.__class__.__name__, len(self)) -class DictProxy(ReadOnly): +class MagicDict(ReadOnly): """ A read-only dict whose items can also be accessed as attributes. - Although a DictProxy is read-only, the underlying dict can change (and is + Although a MagicDict is read-only, the underlying dict can change (and is assumed to). One of these is created for each allowed base in a `Registrar` instance. @@ -576,7 +576,7 @@ class Registrar(ReadOnly): self.base = base self.name = base.__name__ self.sub_d = dict() - self.dictproxy = DictProxy(self.sub_d) + self.dictproxy = MagicDict(self.sub_d) lock(self) self.__allowed = allowed @@ -645,7 +645,7 @@ class Registrar(ReadOnly): def __getitem__(self, key): """ - Returns the DictProxy for plugins subclassed from the base named ``key``. + Returns the MagicDict for plugins subclassed from the base named ``key``. """ if key not in self.__d: raise KeyError('no base class named %r' % key) diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index 839451b5..ba665447 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -482,18 +482,18 @@ class test_NameSpace(ClassChecker): no_set(ns, name) -class test_DictProxy(ClassChecker): +class test_MagicDict(ClassChecker): """ - Tests the `plugable.DictProxy` class. + Tests the `plugable.MagicDict` class. """ - _cls = plugable.DictProxy + _cls = plugable.MagicDict def test_class(self): assert self.cls.__bases__ == (plugable.ReadOnly,) for non_dict in ('hello', 69, object): raises(AssertionError, self.cls, non_dict) - def test_DictProxy(self): + def test_MagicDict(self): cnt = 10 keys = [] d = dict() @@ -559,7 +559,7 @@ def test_Registrar(): for base in [Base1, Base2]: assert base.__name__ in r dp = r[base.__name__] - assert type(dp) is plugable.DictProxy + assert type(dp) is plugable.MagicDict assert len(dp) == 0 # Check that TypeError is raised trying to register something that isn't @@ -573,7 +573,7 @@ def test_Registrar(): # Check that registration works r(plugin1) dp = r['Base1'] - assert type(dp) is plugable.DictProxy + assert type(dp) is plugable.MagicDict assert len(dp) == 1 assert r.Base1 is dp assert dp['plugin1'] is plugin1 -- cgit From f6c2181eebf6e6bd794eaca8b78d3b35ad3be4e4 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 15 Aug 2008 01:04:19 +0000 Subject: 170: Added SetProxy and DictProxy classes to plugable so container emulation can be consolidated --- ipalib/plugable.py | 33 ++++++++++++++++ ipalib/tests/test_plugable.py | 88 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 45c73354..0d8286a4 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -122,6 +122,39 @@ def lock(readonly): return readonly +class SetProxy(ReadOnly): + def __init__(self, s): + allowed = (set, frozenset, dict) + if type(s) not in allowed: + raise TypeError('%r not in %r' % (type(s), allowed)) + self.__s = s + lock(self) + + def __len__(self): + return len(self.__s) + + def __iter__(self): + for key in sorted(self.__s): + yield key + + def __contains__(self, key): + return key in self.__s + + +class DictProxy(SetProxy): + def __init__(self, d): + if type(d) is not dict: + raise TypeError('%r is not %r' % (type(d), dict)) + self.__d = d + super(DictProxy, self).__init__(d) + + def __getitem__(self, key): + """ + Returns the value + """ + return self.__d[key] + + class Plugin(ReadOnly): """ Base class for all plugins. diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index ba665447..0410bf31 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -117,6 +117,94 @@ def test_lock(): raises(AssertionError, f, o) +class test_SetProxy(ClassChecker): + """ + Tests the `plugable.SetProxy` class. + """ + _cls = plugable.SetProxy + + def test_class(self): + assert self.cls.__bases__ == (plugable.ReadOnly,) + + def test_init(self): + okay = (set, frozenset, dict) + fail = (list, tuple) + for t in okay: + self.cls(t()) + raises(TypeError, self.cls, t) + for t in fail: + raises(TypeError, self.cls, t()) + raises(TypeError, self.cls, t) + + def test_SetProxy(self): + def get_key(i): + return 'key_%d' % i + + cnt = 10 + target = set() + proxy = self.cls(target) + for i in xrange(cnt): + key = get_key(i) + + # Check initial state + assert len(proxy) == len(target) + assert list(proxy) == sorted(target) + assert key not in proxy + assert key not in target + + # Add and test again + target.add(key) + assert len(proxy) == len(target) + assert list(proxy) == sorted(target) + assert key in proxy + assert key in target + + +class test_DictProxy(ClassChecker): + """ + Tests the `plugable.DictProxy` class. + """ + _cls = plugable.DictProxy + + def test_class(self): + assert self.cls.__bases__ == (plugable.SetProxy,) + + def test_init(self): + self.cls(dict()) + raises(TypeError, self.cls, dict) + fail = (set, frozenset, list, tuple) + for t in fail: + raises(TypeError, self.cls, t()) + raises(TypeError, self.cls, t) + + def test_DictProxy(self): + def get_kv(i): + return ( + 'key_%d' % i, + 'val_%d' % i, + ) + cnt = 10 + target = dict() + proxy = self.cls(target) + for i in xrange(cnt): + (key, val) = get_kv(i) + + # Check initial state + assert len(proxy) == len(target) + assert list(proxy) == sorted(target) + assert key not in proxy + raises(KeyError, getitem, proxy, key) + + # Add and test again + target[key] = val + assert len(proxy) == len(target) + assert list(proxy) == sorted(target) + + # Verify TypeError is raised trying to set/del via proxy + raises(TypeError, setitem, proxy, key, val) + raises(TypeError, delitem, proxy, key) + + class test_Plugin(ClassChecker): """ Tests the `plugable.Plugin` class. -- cgit From e43a5c642e1717c9309e8747e5433ab85abf2779 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 15 Aug 2008 01:24:51 +0000 Subject: 171: MagicDict now subclasses from DictProxy; updated unit tests --- ipalib/plugable.py | 88 ++++++++++++--------------------- ipalib/tests/test_plugable.py | 112 +++++++++++++++++++++--------------------- 2 files changed, 87 insertions(+), 113 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 0d8286a4..ba9b6973 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -123,6 +123,13 @@ def lock(readonly): class SetProxy(ReadOnly): + """ + A read-only proxy to an underlying set. + + Although the underlying set cannot be changed through the SetProxy, + the set can change and is expected to (unless the underlying set is a + frozen set). + """ def __init__(self, s): allowed = (set, frozenset, dict) if type(s) not in allowed: @@ -142,6 +149,9 @@ class SetProxy(ReadOnly): class DictProxy(SetProxy): + """ + A read-only proxy to an underlying dict. + """ def __init__(self, d): if type(d) is not dict: raise TypeError('%r is not %r' % (type(d), dict)) @@ -150,11 +160,31 @@ class DictProxy(SetProxy): def __getitem__(self, key): """ - Returns the value + Returns the value corresponding to ``key``. """ return self.__d[key] +class MagicDict(DictProxy): + """ + A read-only dict whose items can also be accessed as attributes. + + Although a MagicDict is read-only, the underlying dict can change (and is + assumed to). + + One of these is created for each allowed base in a `Registrar` instance. + """ + + def __getattr__(self, name): + """ + Returns the value corresponding to ``name``. + """ + try: + return self[name] + except KeyError: + raise AttributeError('no attribute %r' % name) + + class Plugin(ReadOnly): """ Base class for all plugins. @@ -525,63 +555,7 @@ class NameSpace(ReadOnly): return '%s(<%d members>)' % (self.__class__.__name__, len(self)) -class MagicDict(ReadOnly): - """ - A read-only dict whose items can also be accessed as attributes. - - Although a MagicDict is read-only, the underlying dict can change (and is - assumed to). - One of these is created for each allowed base in a `Registrar` instance. - """ - def __init__(self, d): - """ - :param d: The ``dict`` instance to proxy. - """ - assert type(d) is dict, '`d` must be %r, got %r' % (dict, type(d)) - self.__d = d - lock(self) - - def __len__(self): - """ - Returns number of items in underlying ``dict``. - """ - return len(self.__d) - - def __iter__(self): - """ - Iterates through keys of underlying ``dict`` in ascending order. - """ - for name in sorted(self.__d): - yield name - - def __contains__(self, key): - """ - Returns True if underlying dict contains ``key``, False otherwise. - - :param key: The key to query upon. - """ - return key in self.__d - - def __getitem__(self, key): - """ - Returns value from underlying dict corresponding to ``key``. - - :param key: The key of the value to retrieve. - """ - if key in self.__d: - return self.__d[key] - raise KeyError('no item at key %r' % key) - - def __getattr__(self, name): - """ - Returns value from underlying dict corresponding to ``name``. - - :param name: The name of the attribute to retrieve. - """ - if name in self.__d: - return self.__d[name] - raise AttributeError('no attribute %r' % name) class Registrar(ReadOnly): diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index 0410bf31..6f2385c9 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -205,6 +205,62 @@ class test_DictProxy(ClassChecker): raises(TypeError, delitem, proxy, key) +class test_MagicDict(ClassChecker): + """ + Tests the `plugable.MagicDict` class. + """ + _cls = plugable.MagicDict + + def test_class(self): + assert self.cls.__bases__ == (plugable.DictProxy,) + for non_dict in ('hello', 69, object): + raises(TypeError, self.cls, non_dict) + + def test_MagicDict(self): + cnt = 10 + keys = [] + d = dict() + dictproxy = self.cls(d) + for i in xrange(cnt): + key = 'key_%d' % i + val = 'val_%d' % i + keys.append(key) + + # Test thet key does not yet exist + assert len(dictproxy) == i + assert key not in dictproxy + assert not hasattr(dictproxy, key) + raises(KeyError, getitem, dictproxy, key) + raises(AttributeError, getattr, dictproxy, key) + + # Test that items/attributes cannot be set on dictproxy: + raises(TypeError, setitem, dictproxy, key, val) + raises(AttributeError, setattr, dictproxy, key, val) + + # Test that additions in d are reflected in dictproxy: + d[key] = val + assert len(dictproxy) == i + 1 + assert key in dictproxy + assert hasattr(dictproxy, key) + assert dictproxy[key] is val + assert read_only(dictproxy, key) is val + + # Test __iter__ + assert list(dictproxy) == keys + + for key in keys: + # Test that items cannot be deleted through dictproxy: + raises(TypeError, delitem, dictproxy, key) + raises(AttributeError, delattr, dictproxy, key) + + # Test that deletions in d are reflected in dictproxy + del d[key] + assert len(dictproxy) == len(d) + assert key not in dictproxy + raises(KeyError, getitem, dictproxy, key) + raises(AttributeError, getattr, dictproxy, key) + + class test_Plugin(ClassChecker): """ Tests the `plugable.Plugin` class. @@ -570,62 +626,6 @@ class test_NameSpace(ClassChecker): no_set(ns, name) -class test_MagicDict(ClassChecker): - """ - Tests the `plugable.MagicDict` class. - """ - _cls = plugable.MagicDict - - def test_class(self): - assert self.cls.__bases__ == (plugable.ReadOnly,) - for non_dict in ('hello', 69, object): - raises(AssertionError, self.cls, non_dict) - - def test_MagicDict(self): - cnt = 10 - keys = [] - d = dict() - dictproxy = self.cls(d) - for i in xrange(cnt): - key = 'key_%d' % i - val = 'val_%d' % i - keys.append(key) - - # Test thet key does not yet exist - assert len(dictproxy) == i - assert key not in dictproxy - assert not hasattr(dictproxy, key) - raises(KeyError, getitem, dictproxy, key) - raises(AttributeError, getattr, dictproxy, key) - - # Test that items/attributes cannot be set on dictproxy: - raises(TypeError, setitem, dictproxy, key, val) - raises(AttributeError, setattr, dictproxy, key, val) - - # Test that additions in d are reflected in dictproxy: - d[key] = val - assert len(dictproxy) == i + 1 - assert key in dictproxy - assert hasattr(dictproxy, key) - assert dictproxy[key] is val - assert read_only(dictproxy, key) is val - - # Test __iter__ - assert list(dictproxy) == keys - - for key in keys: - # Test that items cannot be deleted through dictproxy: - raises(TypeError, delitem, dictproxy, key) - raises(AttributeError, delattr, dictproxy, key) - - # Test that deletions in d are reflected in dictproxy - del d[key] - assert len(dictproxy) == len(d) - assert key not in dictproxy - raises(KeyError, getitem, dictproxy, key) - raises(AttributeError, getattr, dictproxy, key) - - def test_Registrar(): class Base1(object): pass -- cgit From 1a92bdf29b3c65d7b9bd1c61d9eda0f98a70ecfa Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 15 Aug 2008 01:32:20 +0000 Subject: 172: API now subclasses from DictProxy --- ipalib/plugable.py | 31 ++----------------------------- 1 file changed, 2 insertions(+), 29 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index ba9b6973..4661aa1e 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -674,7 +674,7 @@ class Registrar(ReadOnly): yield (base, tuple(sub_d[k] for k in sorted(sub_d))) -class API(ReadOnly): +class API(DictProxy): """ Dynamic API object through which `Plugin` instances are accessed. """ @@ -683,7 +683,7 @@ class API(ReadOnly): def __init__(self, *allowed): self.__d = dict() self.register = Registrar(*allowed) - lock(self) + super(API, self).__init__(self.__d) def finalize(self): """ @@ -713,30 +713,3 @@ class API(ReadOnly): lock(plugin) assert plugin.api is self object.__setattr__(self, '_API__finalized', True) - - def __len__(self): - """ - Returns the number of namespaces in this API. - """ - return len(self.__d) - - def __iter__(self): - """ - Iterates through the names of the namespaces in this API. - """ - for key in sorted(self.__d): - yield key - - def __contains__(self, key): - """ - Returns True if this API contains a `NameSpace` named ``key``. - """ - return key in self.__d - - def __getitem__(self, key): - """ - Returns the `NameSpace` instance named ``key``. - """ - if key in self.__d: - return self.__d[key] - raise KeyError('API has no NameSpace %r' % key) -- cgit From 3e3b596f68957f46efa5af4b957c8add50fca8b6 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 15 Aug 2008 01:46:11 +0000 Subject: 173: NameSpace now subclasses from DictProxy --- ipalib/plugable.py | 62 +++++++------------------------------------ ipalib/tests/test_plugable.py | 4 +-- 2 files changed, 11 insertions(+), 55 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 4661aa1e..7d42d4a0 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -434,7 +434,7 @@ def check_name(name): return name -class NameSpace(ReadOnly): +class NameSpace(DictProxy): """ A read-only namespace with handy container behaviours. @@ -483,10 +483,9 @@ class NameSpace(ReadOnly): """ :param members: An iterable providing the members. """ - self.__d = dict() - self.__names = tuple(self.__member_iter(members)) - lock(self) - assert set(self.__d) == set(self.__names) + super(NameSpace, self).__init__( + dict(self.__member_iter(members)) + ) def __member_iter(self, members): """ @@ -496,56 +495,16 @@ class NameSpace(ReadOnly): """ for member in members: name = check_name(member.name) - assert not ( - name in self.__d or hasattr(self, name) - ), 'already has member named %r' % name - self.__d[name] = member + assert not hasattr(self, name), 'already has attribute %r' % name setattr(self, name, member) - yield name - - def __len__(self): - """ - Returns the number of members in this NameSpace. - """ - return len(self.__d) - - def __contains__(self, name): - """ - Returns True if instance contains a member named ``name``, otherwise - False. - - :param name: The name of a potential member - """ - return name in self.__d - - def __getitem__(self, name): - """ - Returns the member named ``name``. - - Raises KeyError if this NameSpace does not contain a member named - ``name``. - - :param name: The name of member to retrieve - """ - if name in self.__d: - return self.__d[name] - raise KeyError('NameSpace has no member named %r' % name) - - def __iter__(self): - """ - Iterates through the member names in the same order as the members - were passed to the constructor. - """ - for name in self.__names: - yield name + yield (name, member) def __call__(self): """ - Iterates through the members in the same order they were passed to the - constructor. + Iterates through the members of this NameSpace. """ - for name in self.__names: - yield self.__d[name] + for key in self: + yield self[key] def __repr__(self): """ @@ -555,9 +514,6 @@ class NameSpace(ReadOnly): return '%s(<%d members>)' % (self.__class__.__name__, len(self)) - - - class Registrar(ReadOnly): """ Collects plugin classes as they are registered. diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index 6f2385c9..00d20cc9 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -560,7 +560,7 @@ class test_NameSpace(ClassChecker): _cls = plugable.NameSpace def test_class(self): - assert self.cls.__bases__ == (plugable.ReadOnly,) + assert self.cls.__bases__ == (plugable.DictProxy,) def test_namespace(self): class base(object): @@ -583,7 +583,7 @@ class test_NameSpace(ClassChecker): for i in xrange(n): yield plugable.Proxy(base, plugin(get_name(i))) - cnt = 20 + cnt = 10 ns = self.cls(get_proxies(cnt)) assert ns.__islocked__() is True -- cgit From ec0596b429d10b8659ea21a051fd98e047aece46 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 15 Aug 2008 03:24:37 +0000 Subject: 174: Fleshed out docstrings for SetProxy, DictProxy, and MagicDict --- ipalib/__init__.py | 2 +- ipalib/plugable.py | 59 +++++++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 50 insertions(+), 11 deletions(-) (limited to 'ipalib') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index 84b529e1..4d96c2d6 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -24,7 +24,7 @@ To learn about the ``ipalib`` library, you should read the code in this order: 1. Learn about the plugin framework in `ipalib.plugable` - 2. Learn about the public api in `ipalib.public` + 2. Learn about the public API in `ipalib.public` 3. Look at some example plugins in `ipalib.plugins.example` diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 7d42d4a0..e1731156 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -124,13 +124,17 @@ def lock(readonly): class SetProxy(ReadOnly): """ - A read-only proxy to an underlying set. + A read-only container with set/sequence behaviour. - Although the underlying set cannot be changed through the SetProxy, - the set can change and is expected to (unless the underlying set is a - frozen set). + This container acts as a proxy to an actual set-like object (a set, + frozenset, or dict) that is passed to the constructor. To the extent + possible in Python, this underlying set-like object cannot be modified + through the SetProxy... which just means you wont do it accidentally. """ def __init__(self, s): + """ + :param s: The target set-like object (a set, frozenset, or dict) + """ allowed = (set, frozenset, dict) if type(s) not in allowed: raise TypeError('%r not in %r' % (type(s), allowed)) @@ -138,21 +142,43 @@ class SetProxy(ReadOnly): lock(self) def __len__(self): + """ + Returns the number of items in this container. + """ return len(self.__s) def __iter__(self): + """ + Iterates (in ascending order) through the items (or keys) in this + container. + """ for key in sorted(self.__s): yield key def __contains__(self, key): + """ + Returns True if this container contains ``key``, False otherwise. + + :param key: The item (or key) to test for membership. + """ return key in self.__s class DictProxy(SetProxy): """ - A read-only proxy to an underlying dict. + A read-only container with mapping behaviour. + + This container acts as a proxy to an actual mapping object (a dict) that + is passed to the constructor. To the extent possible in Python, this + underlying mapping object cannot be modified through the DictProxy... + which just means you wont do it accidentally. + + Also see `SetProxy`. """ def __init__(self, d): + """ + :param d: The target mapping object (a dict) + """ if type(d) is not dict: raise TypeError('%r is not %r' % (type(d), dict)) self.__d = d @@ -161,28 +187,41 @@ class DictProxy(SetProxy): def __getitem__(self, key): """ Returns the value corresponding to ``key``. + + :param key: The key of the value you wish to retrieve. """ return self.__d[key] class MagicDict(DictProxy): """ - A read-only dict whose items can also be accessed as attributes. + A read-only mapping container whose values can also be accessed as + attributes. - Although a MagicDict is read-only, the underlying dict can change (and is - assumed to). + For example, assuming ``magic`` is a MagicDict instance that contains the + key ``name``, you could do this: - One of these is created for each allowed base in a `Registrar` instance. + >>> magic[name] is getattr(magic, name) + True + + This container acts as a proxy to an actual mapping object (a dict) that + is passed to the constructor. To the extent possible in Python, this + underlying mapping object cannot be modified through the MagicDict... + which just means you wont do it accidentally. + + Also see `DictProxy` and `SetProxy`. """ def __getattr__(self, name): """ Returns the value corresponding to ``name``. + + :param name: The name of the attribute you wish to retrieve. """ try: return self[name] except KeyError: - raise AttributeError('no attribute %r' % name) + raise AttributeError('no magic attribute %r' % name) class Plugin(ReadOnly): -- cgit From 233293fb4a60d57e60bce67035a88f57b2cbf751 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 15 Aug 2008 03:32:38 +0000 Subject: 175: Renamed Proxy to PluginProxy --- ipalib/plugable.py | 4 ++-- ipalib/public.py | 2 +- ipalib/tests/test_plugable.py | 16 ++++++++-------- ipalib/tests/test_public.py | 10 +++++----- 4 files changed, 16 insertions(+), 16 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index e1731156..a891bab5 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -351,7 +351,7 @@ class Plugin(ReadOnly): ) -class Proxy(ReadOnly): +class PluginProxy(ReadOnly): """ Allows access to only certain attributes on a `Plugin`. @@ -692,7 +692,7 @@ class API(DictProxy): if klass not in instances: instances[klass] = klass() plugin = instances[klass] - yield Proxy(base, plugin) + yield PluginProxy(base, plugin) for (base, classes) in self.register: namespace = NameSpace(plugin_iter(base, classes)) diff --git a/ipalib/public.py b/ipalib/public.py index d23aa7ac..1663a3b4 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -160,7 +160,7 @@ class cmd(plugable.Plugin): assert inspect.isclass(cls) o = cls() o.__lock__() - yield plugable.Proxy(option, o) + yield plugable.PluginProxy(option, o) def __get_options(self): """ diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index 00d20cc9..f92f941c 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -412,9 +412,9 @@ class test_Plugin(ClassChecker): class test_Proxy(ClassChecker): """ - Tests the `plugable.Proxy` class. + Tests the `plugable.PluginProxy` class. """ - _cls = plugable.Proxy + _cls = plugable.PluginProxy def test_class(self): assert self.cls.__bases__ == (plugable.ReadOnly,) @@ -484,7 +484,7 @@ class test_Proxy(ClassChecker): def test_implements(self): """ - Tests the `plugable.Proxy.implements` method. + Tests the `plugable.PluginProxy.implements` method. """ class base(object): __public__ = frozenset() @@ -509,7 +509,7 @@ class test_Proxy(ClassChecker): def test_clone(self): """ - Tests the `plugable.Proxy.__clone__` method. + Tests the `plugable.PluginProxy.__clone__` method. """ class base(object): __public__ = frozenset() @@ -581,7 +581,7 @@ class test_NameSpace(ClassChecker): def get_proxies(n): for i in xrange(n): - yield plugable.Proxy(base, plugin(get_name(i))) + yield plugable.PluginProxy(base, plugin(get_name(i))) cnt = 10 ns = self.cls(get_proxies(cnt)) @@ -600,7 +600,7 @@ class test_NameSpace(ClassChecker): # Test __call__ i = None for (i, proxy) in enumerate(ns()): - assert type(proxy) is plugable.Proxy + assert type(proxy) is plugable.PluginProxy assert proxy.name == get_name(i) assert i == cnt - 1 @@ -611,7 +611,7 @@ class test_NameSpace(ClassChecker): assert name in ns proxy = ns[name] assert proxy.name == name - assert type(proxy) is plugable.Proxy + assert type(proxy) is plugable.PluginProxy assert proxy in proxies assert read_only(ns, name) is proxy @@ -802,7 +802,7 @@ def test_API(): for p in xrange(3): plugin_name = get_plugin(b, p) proxy = ns[plugin_name] - assert isinstance(proxy, plugable.Proxy) + assert isinstance(proxy, plugable.PluginProxy) assert proxy.name == plugin_name assert read_only(ns, plugin_name) is proxy assert read_only(proxy, 'method')(7) == 7 + b diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index 2c111c38..968e067f 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -193,7 +193,7 @@ class test_cmd(ClassChecker): assert list(self.cls().get_options()) == [] sub = self.subcls() for (i, proxy) in enumerate(sub.get_options()): - assert isinstance(proxy, plugable.Proxy) + assert isinstance(proxy, plugable.PluginProxy) assert read_only(proxy, 'name') == 'option%d' % i assert proxy.implements(public.option) assert i == 1 @@ -211,7 +211,7 @@ class test_cmd(ClassChecker): assert name in options proxy = options[name] assert getattr(options, name) is proxy - assert isinstance(proxy, plugable.Proxy) + assert isinstance(proxy, plugable.PluginProxy) assert proxy.name == name def test_normalize(self): @@ -339,8 +339,8 @@ class test_mthd(ClassChecker): def __get_prop(self): if self.__prop is None: self.__prop = ( - plugable.Proxy(public.prop, example_prop0(), 'attr_name'), - plugable.Proxy(public.prop, example_prop1(), 'attr_name'), + plugable.PluginProxy(public.prop, example_prop0(), 'attr_name'), + plugable.PluginProxy(public.prop, example_prop1(), 'attr_name'), ) return self.__prop prop = property(__get_prop) @@ -359,7 +359,7 @@ class test_mthd(ClassChecker): assert len(proxies) == 4 for (i, proxy) in enumerate(proxies): assert proxy.name == names[i] - assert isinstance(proxy, plugable.Proxy) + assert isinstance(proxy, plugable.PluginProxy) assert proxy.implements(public.option) -- cgit From db8099febcb9c385eadfc4461dafa32df31bcbc0 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 15 Aug 2008 03:41:17 +0000 Subject: 176: PluginProxy now subclasses from SetProxy --- ipalib/plugable.py | 22 +++++++--------------- ipalib/tests/test_plugable.py | 4 ++-- 2 files changed, 9 insertions(+), 17 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index a891bab5..c58114a9 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -351,7 +351,7 @@ class Plugin(ReadOnly): ) -class PluginProxy(ReadOnly): +class PluginProxy(SetProxy): """ Allows access to only certain attributes on a `Plugin`. @@ -390,8 +390,8 @@ class PluginProxy(ReadOnly): self.__public__ = base.__public__ self.name = getattr(target, name_attr) self.doc = target.doc - lock(self) assert type(self.__public__) is frozenset + super(PluginProxy, self).__init__(self.__public__) def implements(self, arg): """ @@ -411,31 +411,23 @@ class PluginProxy(ReadOnly): """ return self.__class__(self.__base, self.__target, name_attr) - def __iter__(self): - """ - Iterates (in ascending order) though the attribute names this proxy is - allowing access to. - """ - for name in sorted(self.__public__): - yield name - def __getitem__(self, key): """ - If this proxy allows access to an attribute named `key`, return that + If this proxy allows access to an attribute named ``key``, return that attribute. """ if key in self.__public__: return getattr(self.__target, key) - raise KeyError('no proxy attribute %r' % key) + raise KeyError('no public attribute %r' % key) def __getattr__(self, name): """ - If this proxy allows access to an attribute named `name`, return that - attribute. + If this proxy allows access to an attribute named ``name``, return + that attribute. """ if name in self.__public__: return getattr(self.__target, name) - raise AttributeError('no proxy attribute %r' % name) + raise AttributeError('no public attribute %r' % name) def __call__(self, *args, **kw): """ diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index f92f941c..aeb3ddf6 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -410,14 +410,14 @@ class test_Plugin(ClassChecker): raises(AssertionError, sub.finalize, api) -class test_Proxy(ClassChecker): +class test_PluginProxy(ClassChecker): """ Tests the `plugable.PluginProxy` class. """ _cls = plugable.PluginProxy def test_class(self): - assert self.cls.__bases__ == (plugable.ReadOnly,) + assert self.cls.__bases__ == (plugable.SetProxy,) def test_proxy(self): # Setup: -- cgit From 5ed58fdb4213908b406fe625d0727ecc15dbd1cf Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 15 Aug 2008 03:45:07 +0000 Subject: 177: Docstring cleanup in NameSpace.__call__() --- ipalib/plugable.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index c58114a9..b8d2b390 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -532,7 +532,8 @@ class NameSpace(DictProxy): def __call__(self): """ - Iterates through the members of this NameSpace. + Iterates (in ascending order by name) through the members in this + NameSpace. """ for key in self: yield self[key] -- cgit From a24f2121d553644513dc90d423b9ac968de34bc2 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 15 Aug 2008 05:07:17 +0000 Subject: 178: Registrar now subclasses from DictProxy; made Registrar.__iter__ behave same as the other container emulation in plugable.py, and made the dictorary interface return the base and the attribute interface return the MagicDict; updated API class and unit tests --- ipalib/plugable.py | 85 +++++++++++++------------------------------ ipalib/tests/test_plugable.py | 49 +++++++++++-------------- 2 files changed, 47 insertions(+), 87 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index b8d2b390..57ab8bc7 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -546,7 +546,7 @@ class NameSpace(DictProxy): return '%s(<%d members>)' % (self.__class__.__name__, len(self)) -class Registrar(ReadOnly): +class Registrar(DictProxy): """ Collects plugin classes as they are registered. @@ -561,30 +561,19 @@ class Registrar(ReadOnly): :param allowed: Base classes from which plugins accepted by this Registrar must subclass. """ - - class Val(ReadOnly): - """ - Internal class used so that only one mapping is needed. - """ - def __init__(self, base): - assert inspect.isclass(base) - self.base = base - self.name = base.__name__ - self.sub_d = dict() - self.dictproxy = MagicDict(self.sub_d) - lock(self) - - self.__allowed = allowed - self.__d = {} + self.__allowed = dict((base, {}) for base in allowed) self.__registered = set() - for base in self.__allowed: - val = Val(base) - assert not ( - val.name in self.__d or hasattr(self, val.name) - ) - self.__d[val.name] = val - setattr(self, val.name, val.dictproxy) - lock(self) + super(Registrar, self).__init__( + dict(self.__base_iter()) + ) + + def __base_iter(self): + for (base, sub_d) in self.__allowed.iteritems(): + assert inspect.isclass(base) + name = base.__name__ + assert not hasattr(self, name) + setattr(self, name, MagicDict(sub_d)) + yield (name, base) def __findbases(self, klass): """ @@ -597,12 +586,12 @@ class Registrar(ReadOnly): """ assert inspect.isclass(klass) found = False - for base in self.__allowed: + for (base, sub_d) in self.__allowed.iteritems(): if issubclass(klass, base): found = True - yield base + yield (base, sub_d) if not found: - raise errors.SubclassError(klass, self.__allowed) + raise errors.SubclassError(klass, self.__allowed.keys()) def __call__(self, klass, override=False): """ @@ -619,17 +608,15 @@ class Registrar(ReadOnly): raise errors.DuplicateError(klass) # Find the base class or raise SubclassError: - for base in self.__findbases(klass): - sub_d = self.__d[base.__name__].sub_d - + for (base, sub_d) in self.__findbases(klass): # Check override: if klass.__name__ in sub_d: - # Must use override=True to override: if not override: + # Must use override=True to override: raise errors.OverrideError(base, klass) else: - # There was nothing already registered to override: if override: + # There was nothing already registered to override: raise errors.MissingOverrideError(base, klass) # The plugin is okay, add to sub_d: @@ -638,29 +625,6 @@ class Registrar(ReadOnly): # The plugin is okay, add to __registered: self.__registered.add(klass) - def __getitem__(self, key): - """ - Returns the MagicDict for plugins subclassed from the base named ``key``. - """ - if key not in self.__d: - raise KeyError('no base class named %r' % key) - return self.__d[key].dictproxy - - def __contains__(self, key): - """ - Returns True if a base class named ``key`` is in this Registrar. - """ - return key in self.__d - - def __iter__(self): - """ - Iterates through a (base, registered_plugins) tuple for each allowed - base. - """ - for base in self.__allowed: - sub_d = self.__d[base.__name__].sub_d - yield (base, tuple(sub_d[k] for k in sorted(sub_d))) - class API(DictProxy): """ @@ -687,16 +651,19 @@ class API(DictProxy): plugin = instances[klass] yield PluginProxy(base, plugin) - for (base, classes) in self.register: - namespace = NameSpace(plugin_iter(base, classes)) - name = base.__name__ + for name in self.register: + base = self.register[name] + magic = getattr(self.register, name) + namespace = NameSpace( + plugin_iter(base, (magic[k] for k in magic)) + ) assert not ( name in self.__d or hasattr(self, name) ) self.__d[name] = namespace object.__setattr__(self, name, namespace) - for plugin in instances.values(): + for plugin in instances.itervalues(): plugin.finalize(self) lock(plugin) assert plugin.api is self diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index aeb3ddf6..aece3fb8 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -643,12 +643,17 @@ def test_Registrar(): # Test creation of Registrar: r = plugable.Registrar(Base1, Base2) + # Test __iter__: + assert list(r) == ['Base1', 'Base2'] + # Test __hasitem__, __getitem__: for base in [Base1, Base2]: - assert base.__name__ in r - dp = r[base.__name__] - assert type(dp) is plugable.MagicDict - assert len(dp) == 0 + name = base.__name__ + assert name in r + assert r[name] is base + magic = getattr(r, name) + assert type(magic) is plugable.MagicDict + assert len(magic) == 0 # Check that TypeError is raised trying to register something that isn't # a class: @@ -660,12 +665,9 @@ def test_Registrar(): # Check that registration works r(plugin1) - dp = r['Base1'] - assert type(dp) is plugable.MagicDict - assert len(dp) == 1 - assert r.Base1 is dp - assert dp['plugin1'] is plugin1 - assert dp.plugin1 is plugin1 + assert len(r.Base1) == 1 + assert r.Base1['plugin1'] is plugin1 + assert r.Base1.plugin1 is plugin1 # Check that DuplicateError is raised trying to register exact class # again: @@ -696,7 +698,7 @@ def test_Registrar(): assert len(r.Base2) == 1 assert r.Base2.plugin2 is plugin2 - # Setup to test __iter__: + # Setup to test more registration: class plugin1a(Base1): pass r(plugin1a) @@ -713,25 +715,16 @@ def test_Registrar(): pass r(plugin2b) - m = { - 'Base1': set([plugin1, plugin1a, plugin1b]), - 'Base2': set([plugin2, plugin2a, plugin2b]), - } - - # Now test __iter__: - for (base, plugins) in r: - assert base in [Base1, Base2] - assert set(plugins) == m[base.__name__] - assert len(list(r)) == 2 - # Again test __hasitem__, __getitem__: for base in [Base1, Base2]: - assert base.__name__ in r - dp = r[base.__name__] - assert len(dp) == 3 - for key in dp: - klass = dp[key] - assert getattr(dp, key) is klass + name = base.__name__ + assert name in r + assert r[name] is base + magic = getattr(r, name) + assert len(magic) == 3 + for key in magic: + klass = magic[key] + assert getattr(magic, key) is klass assert issubclass(klass, base) -- cgit From ab10f0843be45529925a226dc54a9fd0a30ad159 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 15 Aug 2008 05:19:02 +0000 Subject: 179: DictProxy now has __call__() method that iterates through the values; removed __call__() method from NameSpace as it subclasses from DictProxys; DictProxy unit tests now test __call__() --- ipalib/plugable.py | 16 ++++++++-------- ipalib/tests/test_plugable.py | 2 ++ 2 files changed, 10 insertions(+), 8 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 57ab8bc7..811a5527 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -192,6 +192,14 @@ class DictProxy(SetProxy): """ return self.__d[key] + def __call__(self): + """ + Iterates (in ascending order by key) through the values in this + container. + """ + for key in self: + yield self.__d[key] + class MagicDict(DictProxy): """ @@ -530,14 +538,6 @@ class NameSpace(DictProxy): setattr(self, name, member) yield (name, member) - def __call__(self): - """ - Iterates (in ascending order by name) through the members in this - NameSpace. - """ - for key in self: - yield self[key] - def __repr__(self): """ Returns pseudo-valid Python expression that could be used to construct diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index aece3fb8..44067b80 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -192,6 +192,7 @@ class test_DictProxy(ClassChecker): # Check initial state assert len(proxy) == len(target) assert list(proxy) == sorted(target) + assert list(proxy()) == [target[k] for k in sorted(target)] assert key not in proxy raises(KeyError, getitem, proxy, key) @@ -199,6 +200,7 @@ class test_DictProxy(ClassChecker): target[key] = val assert len(proxy) == len(target) assert list(proxy) == sorted(target) + assert list(proxy()) == [target[k] for k in sorted(target)] # Verify TypeError is raised trying to set/del via proxy raises(TypeError, setitem, proxy, key, val) -- cgit From 594e7512b749b8aa98ca6b90d313f1e05dfaa327 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 15 Aug 2008 19:15:24 +0000 Subject: 180: Fixed a few things in public.py that were broken by the changed NameSpace iter behaiviour --- ipalib/public.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index 1663a3b4..9a6c02ef 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -253,9 +253,11 @@ class obj(plugable.Plugin): return plugable.NameSpace(self.__filter(name)) def __filter(self, name): - for i in getattr(self.api, name): - if i.obj_name == self.name: - yield i.__clone__('attr_name') + namespace = getattr(self.api, name) + assert type(namespace) is plugable.NameSpace + for proxy in namespace(): # Like dict.itervalues() + if proxy.obj_name == self.name: + yield proxy.__clone__('attr_name') class attr(plugable.Plugin): @@ -299,7 +301,7 @@ class mthd(attr, cmd): for proxy in cmd.get_options(self): yield proxy if self.obj is not None and self.obj.prop is not None: - for proxy in self.obj.prop: + for proxy in self.obj.prop(): yield proxy -- cgit From 99450358af821b269d46581750d20730fb5c9e9f Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 15 Aug 2008 19:19:42 +0000 Subject: 181: Changed docstrings on example plugins to use itial capital --- ipalib/plugins/example.py | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugins/example.py b/ipalib/plugins/example.py index b5bd4733..3350e2b9 100644 --- a/ipalib/plugins/example.py +++ b/ipalib/plugins/example.py @@ -28,45 +28,45 @@ from ipalib.api import api # Hypothetical functional commands (not associated with any object): class krbtest(public.cmd): - 'test your Kerberos ticket' + 'Test your Kerberos ticket' api.register(krbtest) class discover(public.cmd): - 'discover IPA servers on network' + 'Discover IPA servers on network' api.register(discover) # Register some methods for the 'user' object: class user_add(public.mthd): - 'add new user' + 'Add new user' api.register(user_add) class user_del(public.mthd): - 'delete existing user' + 'Delete existing user' api.register(user_del) class user_mod(public.mthd): - 'edit existing user' + 'Edit existing user' api.register(user_mod) class user_find(public.mthd): - 'search for users' + 'Search for users' api.register(user_find) # Register some properties for the 'user' object: class user_givenname(public.prop): - 'user first name' + 'User first name' required = True api.register(user_givenname) class user_sn(public.prop): - 'user last name' + 'User last name' required = True api.register(user_sn) class user_login(public.prop): - 'user login' + 'User login' required = True def default(self, **kw): givenname = kw.get('givenname', None) @@ -77,7 +77,7 @@ class user_login(public.prop): api.register(user_login) class user_initials(public.prop): - 'user initials' + 'User initials' required = True def default(self, **kw): givenname = kw.get('givenname', None) @@ -90,50 +90,50 @@ api.register(user_initials) # Register some methods for the 'group' object: class group_add(public.mthd): - 'add new group' + 'Add new group' api.register(group_add) class group_del(public.mthd): - 'delete existing group' + 'Delete existing group' api.register(group_del) class group_mod(public.mthd): - 'edit existing group' + 'Edit existing group' api.register(group_mod) class group_find(public.mthd): - 'search for groups' + 'Search for groups' api.register(group_find) # Register some methods for the 'service' object class service_add(public.mthd): - 'add new service' + 'Add new service' api.register(service_add) class service_del(public.mthd): - 'delete existing service' + 'Delete existing service' api.register(service_del) class service_mod(public.mthd): - 'edit existing service' + 'Edit existing service' api.register(service_mod) class service_find(public.mthd): - 'search for services' + 'Search for services' api.register(service_find) # And to emphasis that the registration order doesn't matter, # we'll register the objects last: class group(public.obj): - 'group object' + 'Group object' api.register(group) class service(public.obj): - 'service object' + 'Service object' api.register(service) class user(public.obj): - 'user object' + 'User object' api.register(user) -- cgit From b0ec8fe551bc5f454aa1babeab31a424fd8c9abe Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 15 Aug 2008 19:49:04 +0000 Subject: 182: Renamed plublic.cmd base class to Command --- ipalib/cli.py | 8 ++++---- ipalib/plugins/example.py | 4 ++-- ipalib/public.py | 12 ++++++------ ipalib/tests/test_cli.py | 12 ++++++------ ipalib/tests/test_public.py | 36 ++++++++++++++++++++---------------- 5 files changed, 38 insertions(+), 34 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index 8f09e90c..1ad53058 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -43,10 +43,10 @@ def from_cli(cli_name): return str(cli_name).replace('-', '_') -class help(public.cmd): +class help(public.Command): 'display help on command' def __call__(self, key): - if from_cli(key) not in self.api.cmd: + if from_cli(key) not in self.api.Command: print 'help: no such command %r' % key sys.exit(2) print 'Help on command %r:' % key @@ -65,7 +65,7 @@ class CLI(object): def print_commands(self): print 'Available Commands:' - for cmd in self.api.cmd(): + for cmd in self.api.Command(): print ' %s %s' % ( to_cli(cmd.name).ljust(self.mcl), cmd.doc, @@ -84,7 +84,7 @@ class CLI(object): api.register(help) api.finalize() def d_iter(): - for cmd in api.cmd(): + for cmd in api.Command(): yield (to_cli(cmd.name), cmd) self.__d = dict(d_iter()) diff --git a/ipalib/plugins/example.py b/ipalib/plugins/example.py index 3350e2b9..8a89ca43 100644 --- a/ipalib/plugins/example.py +++ b/ipalib/plugins/example.py @@ -27,11 +27,11 @@ from ipalib.api import api # Hypothetical functional commands (not associated with any object): -class krbtest(public.cmd): +class krbtest(public.Command): 'Test your Kerberos ticket' api.register(krbtest) -class discover(public.cmd): +class discover(public.Command): 'Discover IPA servers on network' api.register(discover) diff --git a/ipalib/public.py b/ipalib/public.py index 9a6c02ef..ff8bd8b0 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -127,7 +127,7 @@ class option(plugable.Plugin): return None -class cmd(plugable.Plugin): +class Command(plugable.Plugin): __public__ = frozenset(( 'normalize', 'default', @@ -167,7 +167,7 @@ class cmd(plugable.Plugin): Returns the NameSpace containing the option proxy objects. """ if self.__options is None: - object.__setattr__(self, '_cmd__options', + object.__setattr__(self, '_Command__options', plugable.NameSpace(self.get_options()), ) return self.__options @@ -294,11 +294,11 @@ class attr(plugable.Plugin): self.__obj = api.obj[self.obj_name] -class mthd(attr, cmd): - __public__ = attr.__public__.union(cmd.__public__) +class mthd(attr, Command): + __public__ = attr.__public__.union(Command.__public__) def get_options(self): - for proxy in cmd.get_options(self): + for proxy in Command.get_options(self): yield proxy if self.obj is not None and self.obj.prop is not None: for proxy in self.obj.prop(): @@ -314,4 +314,4 @@ class prop(attr, option): class PublicAPI(plugable.API): def __init__(self): - super(PublicAPI, self).__init__(cmd, obj, mthd, prop) + super(PublicAPI, self).__init__(Command, obj, mthd, prop) diff --git a/ipalib/tests/test_cli.py b/ipalib/tests/test_cli.py index a5ee8a94..2c65bd06 100644 --- a/ipalib/tests/test_cli.py +++ b/ipalib/tests/test_cli.py @@ -46,7 +46,7 @@ def test_from_cli(): def get_cmd_name(i): return 'cmd_%d' % i -class DummyCmd(object): +class DummyCommand(object): def __init__(self, name): self.__name = name @@ -60,11 +60,11 @@ class DummyAPI(object): def __get_cmd(self): return self.__cmd - cmd = property(__get_cmd) + Command = property(__get_cmd) def __cmd_iter(self, cnt): for i in xrange(cnt): - yield DummyCmd(get_cmd_name(i)) + yield DummyCommand(get_cmd_name(i)) def finalize(self): pass @@ -114,7 +114,7 @@ class test_CLI(ClassChecker): """ cnt = 100 api = DummyAPI(cnt) - len(api.cmd) == cnt + len(api.Command) == cnt o = self.cls(api) assert o.mcl is None o.finalize() @@ -126,10 +126,10 @@ class test_CLI(ClassChecker): """ cnt = 25 api = DummyAPI(cnt) - assert len(api.cmd) == cnt + assert len(api.Command) == cnt o = self.cls(api) o.finalize() - for cmd in api.cmd(): + for cmd in api.Command(): key = cli.to_cli(cmd.name) assert key in o assert o[key] is cmd diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index 968e067f..d053081d 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -159,9 +159,9 @@ class test_option(ClassChecker): class test_cmd(ClassChecker): """ - Tests the `public.cmd` class. + Tests the `public.Command` class. """ - _cls = public.cmd + _cls = public.Command def get_subcls(self): class my_option(public.option): @@ -188,7 +188,7 @@ class test_cmd(ClassChecker): def test_get_options(self): """ - Tests the `public.cmd.get_options` method. + Tests the `public.Command.get_options` method. """ assert list(self.cls().get_options()) == [] sub = self.subcls() @@ -200,7 +200,7 @@ class test_cmd(ClassChecker): def test_options(self): """ - Tests the `public.cmd.options` property. + Tests the `public.Command.options` property. """ assert 'options' in self.cls.__public__ # Public sub = self.subcls() @@ -216,7 +216,7 @@ class test_cmd(ClassChecker): def test_normalize(self): """ - Tests the `public.cmd.normalize` method. + Tests the `public.Command.normalize` method. """ assert 'normalize' in self.cls.__public__ # Public kw = dict( @@ -230,7 +230,7 @@ class test_cmd(ClassChecker): def test_default(self): """ - Tests the `public.cmd.default` method. + Tests the `public.Command.default` method. """ assert 'default' in self.cls.__public__ # Public no_fill = dict( @@ -251,7 +251,7 @@ class test_cmd(ClassChecker): def test_validate(self): """ - Tests the `public.cmd.validate` method. + Tests the `public.Command.validate` method. """ assert 'validate' in self.cls.__public__ # Public @@ -281,7 +281,7 @@ class test_cmd(ClassChecker): def test_execute(self): """ - Tests the `public.cmd.execute` method. + Tests the `public.Command.execute` method. """ assert 'execute' in self.cls.__public__ # Public @@ -322,8 +322,8 @@ class test_mthd(ClassChecker): _cls = public.mthd def test_class(self): - assert self.cls.__bases__ == (public.attr, public.cmd) - assert self.cls.implements(public.cmd) + assert self.cls.__bases__ == (public.attr, public.Command) + assert self.cls.implements(public.Command) def get_subcls(self): class option0(public.option): @@ -338,10 +338,14 @@ class test_mthd(ClassChecker): __prop = None def __get_prop(self): if self.__prop is None: - self.__prop = ( - plugable.PluginProxy(public.prop, example_prop0(), 'attr_name'), - plugable.PluginProxy(public.prop, example_prop1(), 'attr_name'), - ) + self.__prop = plugable.NameSpace([ + plugable.PluginProxy( + public.prop, example_prop0(), 'attr_name' + ), + plugable.PluginProxy( + public.prop, example_prop1(), 'attr_name' + ), + ]) return self.__prop prop = property(__get_prop) class noun_verb(self.cls): @@ -377,11 +381,11 @@ def test_PublicAPI(): api = cls() - class cmd1(public.cmd): + class cmd1(public.Command): pass api.register(cmd1) - class cmd2(public.cmd): + class cmd2(public.Command): pass api.register(cmd2) -- cgit From cad924168eebbb3618205651f8c7a30bf00fe47d Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 22 Aug 2008 20:07:17 +0000 Subject: 183: Added public.DefaultFrom class; added corresponding unit tests --- ipalib/public.py | 21 +++++++++++++++++++++ ipalib/tests/test_public.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index ff8bd8b0..19a678fb 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -25,6 +25,7 @@ and UI all use. import re import inspect import plugable +from plugable import lock import errors @@ -39,6 +40,26 @@ def is_rule(obj): return callable(obj) and getattr(obj, RULE_FLAG, False) is True +class DefaultFrom(plugable.ReadOnly): + def __init__(self, callback, *keys): + assert callable(callback), 'not a callable: %r' % callback + self.callback = callback + self.keys = keys + lock(self) + + def __call__(self, **kw): + vals = tuple(kw.get(k, None) for k in self.keys) + if None in vals: + return None + try: + ret = self.callback(*vals) + except Exception: + return None + if isinstance(ret, basestring): + return ret + return None + + class option(plugable.Plugin): """ The option class represents a kw argument from a command. diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index d053081d..1a496a16 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -63,6 +63,41 @@ def test_is_rule(): assert not is_rule(call(None)) +class test_DefaltFrom(ClassChecker): + """ + Tests the `public.DefaltFrom` class. + """ + _cls = public.DefaultFrom + + def test_class(self): + assert self.cls.__bases__ == (plugable.ReadOnly,) + + def test_init(self): + def callback(*args): + return args + keys = ('givenname', 'sn') + o = self.cls(callback, *keys) + assert read_only(o, 'callback') is callback + assert read_only(o, 'keys') == keys + + def test_call(self): + def callback(givenname, sn): + return givenname[0] + sn[0] + keys = ('givenname', 'sn') + o = self.cls(callback, *keys) + kw = dict( + givenname='John', + sn='Public', + hello='world', + ) + assert o(**kw) == 'JP' + assert o() is None + for key in ('givenname', 'sn'): + kw_copy = dict(kw) + del kw_copy[key] + assert o(**kw_copy) is None + + class test_option(ClassChecker): """ Tests the `public.option` class. -- cgit From a1b5d928fbf989a45c0fabb599d25e80964aacee Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 22 Aug 2008 20:23:19 +0000 Subject: 184: Renamed public.mthd class to Method --- ipalib/plugins/example.py | 24 ++++++++++++------------ ipalib/public.py | 16 ++++++++-------- ipalib/tests/test_public.py | 8 ++++---- 3 files changed, 24 insertions(+), 24 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugins/example.py b/ipalib/plugins/example.py index 8a89ca43..30f0a70f 100644 --- a/ipalib/plugins/example.py +++ b/ipalib/plugins/example.py @@ -37,19 +37,19 @@ api.register(discover) # Register some methods for the 'user' object: -class user_add(public.mthd): +class user_add(public.Method): 'Add new user' api.register(user_add) -class user_del(public.mthd): +class user_del(public.Method): 'Delete existing user' api.register(user_del) -class user_mod(public.mthd): +class user_mod(public.Method): 'Edit existing user' api.register(user_mod) -class user_find(public.mthd): +class user_find(public.Method): 'Search for users' api.register(user_find) @@ -89,37 +89,37 @@ api.register(user_initials) # Register some methods for the 'group' object: -class group_add(public.mthd): +class group_add(public.Method): 'Add new group' api.register(group_add) -class group_del(public.mthd): +class group_del(public.Method): 'Delete existing group' api.register(group_del) -class group_mod(public.mthd): +class group_mod(public.Method): 'Edit existing group' api.register(group_mod) -class group_find(public.mthd): +class group_find(public.Method): 'Search for groups' api.register(group_find) # Register some methods for the 'service' object -class service_add(public.mthd): +class service_add(public.Method): 'Add new service' api.register(service_add) -class service_del(public.mthd): +class service_del(public.Method): 'Delete existing service' api.register(service_del) -class service_mod(public.mthd): +class service_mod(public.Method): 'Edit existing service' api.register(service_mod) -class service_find(public.mthd): +class service_find(public.Method): 'Search for services' api.register(service_find) diff --git a/ipalib/public.py b/ipalib/public.py index 19a678fb..1c51df49 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -251,15 +251,15 @@ class Command(plugable.Plugin): class obj(plugable.Plugin): __public__ = frozenset(( - 'mthd', + 'Method', 'prop', )) - __mthd = None + __Method = None __prop = None - def __get_mthd(self): - return self.__mthd - mthd = property(__get_mthd) + def __get_Method(self): + return self.__Method + Method = property(__get_Method) def __get_prop(self): return self.__prop @@ -267,7 +267,7 @@ class obj(plugable.Plugin): def finalize(self, api): super(obj, self).finalize(api) - self.__mthd = self.__create_ns('mthd') + self.__Method = self.__create_ns('Method') self.__prop = self.__create_ns('prop') def __create_ns(self, name): @@ -315,7 +315,7 @@ class attr(plugable.Plugin): self.__obj = api.obj[self.obj_name] -class mthd(attr, Command): +class Method(attr, Command): __public__ = attr.__public__.union(Command.__public__) def get_options(self): @@ -335,4 +335,4 @@ class prop(attr, option): class PublicAPI(plugable.API): def __init__(self): - super(PublicAPI, self).__init__(Command, obj, mthd, prop) + super(PublicAPI, self).__init__(Command, obj, Method, prop) diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index 1a496a16..089fcfe9 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -350,11 +350,11 @@ def test_attr(): o = example_prop0() -class test_mthd(ClassChecker): +class test_Method(ClassChecker): """ - Tests the `public.mthd` class. + Tests the `public.Method` class. """ - _cls = public.mthd + _cls = public.Method def test_class(self): assert self.cls.__bases__ == (public.attr, public.Command) @@ -390,7 +390,7 @@ class test_mthd(ClassChecker): def test_get_options(self): """ - Tests the `public.mthd.get_options` method. + Tests the `public.Method.get_options` method. """ sub = self.subcls() names = ('option0', 'option1', 'prop0', 'prop1') -- cgit From 5bf6a9eb097fbaf1c048a4487e6ca6b9605b9f05 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 22 Aug 2008 20:32:23 +0000 Subject: 185: Renamed public.prop to Property --- ipalib/plugins/example.py | 8 ++++---- ipalib/public.py | 22 +++++++++++----------- ipalib/tests/test_public.py | 12 ++++++------ 3 files changed, 21 insertions(+), 21 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugins/example.py b/ipalib/plugins/example.py index 30f0a70f..cff144ba 100644 --- a/ipalib/plugins/example.py +++ b/ipalib/plugins/example.py @@ -55,17 +55,17 @@ api.register(user_find) # Register some properties for the 'user' object: -class user_givenname(public.prop): +class user_givenname(public.Property): 'User first name' required = True api.register(user_givenname) -class user_sn(public.prop): +class user_sn(public.Property): 'User last name' required = True api.register(user_sn) -class user_login(public.prop): +class user_login(public.Property): 'User login' required = True def default(self, **kw): @@ -76,7 +76,7 @@ class user_login(public.prop): return ('%s%s' % (givenname[0], sn)).lower() api.register(user_login) -class user_initials(public.prop): +class user_initials(public.Property): 'User initials' required = True def default(self, **kw): diff --git a/ipalib/public.py b/ipalib/public.py index 1c51df49..4b988b51 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -252,23 +252,23 @@ class Command(plugable.Plugin): class obj(plugable.Plugin): __public__ = frozenset(( 'Method', - 'prop', + 'Property', )) __Method = None - __prop = None + __Property = None def __get_Method(self): return self.__Method Method = property(__get_Method) - def __get_prop(self): - return self.__prop - prop = property(__get_prop) + def __get_Property(self): + return self.__Property + Property = property(__get_Property) def finalize(self, api): super(obj, self).finalize(api) self.__Method = self.__create_ns('Method') - self.__prop = self.__create_ns('prop') + self.__Property = self.__create_ns('Property') def __create_ns(self, name): return plugable.NameSpace(self.__filter(name)) @@ -321,18 +321,18 @@ class Method(attr, Command): def get_options(self): for proxy in Command.get_options(self): yield proxy - if self.obj is not None and self.obj.prop is not None: - for proxy in self.obj.prop(): + if self.obj is not None and self.obj.Property is not None: + for proxy in self.obj.Property(): yield proxy -class prop(attr, option): +class Property(attr, option): __public__ = attr.__public__.union(option.__public__) def get_doc(self, _): - return _('prop doc') + return _('Property doc') class PublicAPI(plugable.API): def __init__(self): - super(PublicAPI, self).__init__(Command, obj, Method, prop) + super(PublicAPI, self).__init__(Command, obj, Method, Property) diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index 089fcfe9..dc004254 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -365,9 +365,9 @@ class test_Method(ClassChecker): pass class option1(public.option): pass - class example_prop0(public.prop): + class example_prop0(public.Property): pass - class example_prop1(public.prop): + class example_prop1(public.Property): pass class example_obj(object): __prop = None @@ -375,14 +375,14 @@ class test_Method(ClassChecker): if self.__prop is None: self.__prop = plugable.NameSpace([ plugable.PluginProxy( - public.prop, example_prop0(), 'attr_name' + public.Property, example_prop0(), 'attr_name' ), plugable.PluginProxy( - public.prop, example_prop1(), 'attr_name' + public.Property, example_prop1(), 'attr_name' ), ]) return self.__prop - prop = property(__get_prop) + Property = property(__get_prop) class noun_verb(self.cls): option_classes = (option0, option1) obj = example_obj() @@ -403,7 +403,7 @@ class test_Method(ClassChecker): class test_prop(ClassChecker): - _cls = public.prop + _cls = public.Property def test_class(self): assert self.cls.__bases__ == (public.attr, public.option) -- cgit From f60fa06ce23d25042d258bdb7ae1b782993b1e42 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 22 Aug 2008 21:27:25 +0000 Subject: 186: Renamed public.attr to Attribute; reworked public.Attribute unit tests using ClassChecker --- ipalib/public.py | 12 +++++----- ipalib/tests/test_public.py | 53 +++++++++++++++++++++++++++------------------ 2 files changed, 38 insertions(+), 27 deletions(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index 4b988b51..c0d67cd3 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -281,7 +281,7 @@ class obj(plugable.Plugin): yield proxy.__clone__('attr_name') -class attr(plugable.Plugin): +class Attribute(plugable.Plugin): __public__ = frozenset(( 'obj', 'obj_name', @@ -311,12 +311,12 @@ class attr(plugable.Plugin): obj = property(__get_obj) def finalize(self, api): - super(attr, self).finalize(api) + super(Attribute, self).finalize(api) self.__obj = api.obj[self.obj_name] -class Method(attr, Command): - __public__ = attr.__public__.union(Command.__public__) +class Method(Attribute, Command): + __public__ = Attribute.__public__.union(Command.__public__) def get_options(self): for proxy in Command.get_options(self): @@ -326,8 +326,8 @@ class Method(attr, Command): yield proxy -class Property(attr, option): - __public__ = attr.__public__.union(option.__public__) +class Property(Attribute, option): + __public__ = Attribute.__public__.union(option.__public__) def get_doc(self, _): return _('Property doc') diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index dc004254..a6ae7d8e 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -327,27 +327,38 @@ def test_obj(): -def test_attr(): - cls = public.attr - assert issubclass(cls, plugable.Plugin) - - class api(object): - obj = dict(user='the user obj') - - class user_add(cls): - pass +class test_Attribute(ClassChecker): + """ + Tests the `public.Attribute` class. + """ + _cls = public.Attribute - i = user_add() - assert read_only(i, 'obj_name') == 'user' - assert read_only(i, 'attr_name') == 'add' - assert read_only(i, 'obj') is None - i.finalize(api) - assert read_only(i, 'api') is api - assert read_only(i, 'obj') == 'the user obj' + def test_class(self): + assert self.cls.__bases__ == (plugable.Plugin,) + assert type(self.cls.obj) is property + assert type(self.cls.obj_name) is property + assert type(self.cls.attr_name) is property - class example_prop0(cls): - pass - o = example_prop0() + def test_init(self): + class user_add(self.cls): + pass + o = user_add() + assert read_only(o, 'obj') is None + assert read_only(o, 'obj_name') == 'user' + assert read_only(o, 'attr_name') == 'add' + + def test_finalize(self): + user_obj = 'The user public.Object instance' + class api(object): + obj = dict(user=user_obj) + class user_add(self.cls): + pass + o = user_add() + assert read_only(o, 'api') is None + assert read_only(o, 'obj') is None + o.finalize(api) + assert read_only(o, 'api') is api + assert read_only(o, 'obj') is user_obj class test_Method(ClassChecker): @@ -357,7 +368,7 @@ class test_Method(ClassChecker): _cls = public.Method def test_class(self): - assert self.cls.__bases__ == (public.attr, public.Command) + assert self.cls.__bases__ == (public.Attribute, public.Command) assert self.cls.implements(public.Command) def get_subcls(self): @@ -406,7 +417,7 @@ class test_prop(ClassChecker): _cls = public.Property def test_class(self): - assert self.cls.__bases__ == (public.attr, public.option) + assert self.cls.__bases__ == (public.Attribute, public.option) assert self.cls.implements(public.option) -- cgit From af52671e132818deaf266cd434338aff11064f01 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 22 Aug 2008 21:50:53 +0000 Subject: 187: Renamed plubic.obj to Object; reworked plublic.Object unit tests to use ClassChecker --- ipalib/plugins/example.py | 6 +++--- ipalib/public.py | 20 ++++++++++---------- ipalib/tests/test_public.py | 19 +++++++++++++++---- 3 files changed, 28 insertions(+), 17 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugins/example.py b/ipalib/plugins/example.py index cff144ba..f7a5fe70 100644 --- a/ipalib/plugins/example.py +++ b/ipalib/plugins/example.py @@ -126,14 +126,14 @@ api.register(service_find) # And to emphasis that the registration order doesn't matter, # we'll register the objects last: -class group(public.obj): +class group(public.Object): 'Group object' api.register(group) -class service(public.obj): +class service(public.Object): 'Service object' api.register(service) -class user(public.obj): +class user(public.Object): 'User object' api.register(user) diff --git a/ipalib/public.py b/ipalib/public.py index c0d67cd3..c0ca5255 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -249,7 +249,7 @@ class Command(plugable.Plugin): self.execute(**kw) -class obj(plugable.Plugin): +class Object(plugable.Plugin): __public__ = frozenset(( 'Method', 'Property', @@ -266,17 +266,17 @@ class obj(plugable.Plugin): Property = property(__get_Property) def finalize(self, api): - super(obj, self).finalize(api) - self.__Method = self.__create_ns('Method') - self.__Property = self.__create_ns('Property') + super(Object, self).finalize(api) + self.__Method = self.__create_namespace('Method') + self.__Property = self.__create_namespace('Property') - def __create_ns(self, name): - return plugable.NameSpace(self.__filter(name)) + def __create_namespace(self, name): + return plugable.NameSpace(self.__filter_members(name)) - def __filter(self, name): + def __filter_members(self, name): namespace = getattr(self.api, name) assert type(namespace) is plugable.NameSpace - for proxy in namespace(): # Like dict.itervalues() + for proxy in namespace(): # Equivalent to dict.itervalues() if proxy.obj_name == self.name: yield proxy.__clone__('attr_name') @@ -312,7 +312,7 @@ class Attribute(plugable.Plugin): def finalize(self, api): super(Attribute, self).finalize(api) - self.__obj = api.obj[self.obj_name] + self.__obj = api.Object[self.obj_name] class Method(Attribute, Command): @@ -335,4 +335,4 @@ class Property(Attribute, option): class PublicAPI(plugable.API): def __init__(self): - super(PublicAPI, self).__init__(Command, obj, Method, Property) + super(PublicAPI, self).__init__(Command, Object, Method, Property) diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index a6ae7d8e..95bb7b9c 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -321,10 +321,21 @@ class test_cmd(ClassChecker): assert 'execute' in self.cls.__public__ # Public -def test_obj(): - cls = public.obj - assert issubclass(cls, plugable.Plugin) +class test_Object(ClassChecker): + """ + Tests the `public.Object` class. + """ + _cls = public.Object + + def test_class(self): + assert self.cls.__bases__ == (plugable.Plugin,) + assert type(self.cls.Method) is property + assert type(self.cls.Property) is property + def test_init(self): + o = self.cls() + assert read_only(o, 'Method') is None + assert read_only(o, 'Property') is None class test_Attribute(ClassChecker): @@ -350,7 +361,7 @@ class test_Attribute(ClassChecker): def test_finalize(self): user_obj = 'The user public.Object instance' class api(object): - obj = dict(user=user_obj) + Object = dict(user=user_obj) class user_add(self.cls): pass o = user_add() -- cgit From c646dfdddeefe286379245066493821ca5cac5da Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 22 Aug 2008 22:49:56 +0000 Subject: 188: Added unit tests for public.Object.finalize() method --- ipalib/public.py | 5 +++- ipalib/tests/test_public.py | 65 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index c0ca5255..78e2a7ea 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -289,7 +289,10 @@ class Attribute(plugable.Plugin): __obj = None def __init__(self): - m = re.match('^([a-z][a-z0-9]+)_([a-z][a-z0-9]+)$', self.__class__.__name__) + m = re.match( + '^([a-z][a-z0-9]+)_([a-z][a-z0-9]+)$', + self.__class__.__name__ + ) assert m self.__obj_name = m.group(1) self.__attr_name = m.group(2) diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index 95bb7b9c..4fd24f8d 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -333,10 +333,75 @@ class test_Object(ClassChecker): assert type(self.cls.Property) is property def test_init(self): + """ + Tests the `public.Object.__init__` method. + """ o = self.cls() assert read_only(o, 'Method') is None assert read_only(o, 'Property') is None + def test_finalize(self): + """ + Tests the `public.Object.finalize` method. + """ + # Setup for test: + class DummyAttribute(object): + def __init__(self, obj_name, attr_name, name=None): + self.obj_name = obj_name + self.attr_name = attr_name + if name is None: + self.name = '%s_%s' % (obj_name, attr_name) + else: + self.name = name + def __clone__(self, attr_name): + return self.__class__( + self.obj_name, + self.attr_name, + getattr(self, attr_name) + ) + + def get_attributes(cnt, format): + for name in ['other', 'user', 'another']: + for i in xrange(cnt): + yield DummyAttribute(name, format % i) + + cnt = 10 + formats = dict( + Method='method_%d', + Property='property_%d', + ) + + class api(object): + Method = plugable.NameSpace( + get_attributes(cnt, formats['Method']) + ) + Property = plugable.NameSpace( + get_attributes(cnt, formats['Property']) + ) + assert len(api.Method) == cnt * 3 + assert len(api.Property) == cnt * 3 + + class user(self.cls): + pass + + # Actually perform test: + o = user() + o.finalize(api) + assert read_only(o, 'api') is api + for name in ['Method', 'Property']: + namespace = getattr(o, name) + assert isinstance(namespace, plugable.NameSpace) + assert len(namespace) == cnt + f = formats[name] + for i in xrange(cnt): + attr_name = f % i + attr = namespace[attr_name] + assert isinstance(attr, DummyAttribute) + assert attr is getattr(namespace, attr_name) + assert attr.obj_name == 'user' + assert attr.attr_name == attr_name + assert attr.name == attr_name + class test_Attribute(ClassChecker): """ -- cgit From 513bbb32b989906795d281738d90fc113192b217 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 25 Aug 2008 22:10:23 +0000 Subject: 190: Renamed public.option class to public.Option --- ipalib/public.py | 16 ++++++++-------- ipalib/tests/test_public.py | 30 +++++++++++++++--------------- 2 files changed, 23 insertions(+), 23 deletions(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index 78e2a7ea..06f5b2b5 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -60,9 +60,9 @@ class DefaultFrom(plugable.ReadOnly): return None -class option(plugable.Plugin): +class Option(plugable.Plugin): """ - The option class represents a kw argument from a command. + The Option class represents a kw argument from a `Command`. """ __public__ = frozenset(( @@ -118,7 +118,7 @@ class option(plugable.Plugin): self.__rules_iter(), key=lambda f: getattr(f, '__name__'), )) - object.__setattr__(self, '_option__rules', rules) + object.__setattr__(self, '_Option__rules', rules) return self.__rules rules = property(__get_rules) @@ -138,11 +138,11 @@ class option(plugable.Plugin): def default(self, **kw): """ - Returns a default or auto-completed value for this option. If no + Returns a default or auto-completed value for this Option. If no default is available, this method should return None. All the keywords are passed so it's possible to build an - auto-completed value from other options values, e.g., build 'initials' + auto-completed value from other Options values, e.g., build 'initials' from 'givenname' + 'sn'. """ return None @@ -181,7 +181,7 @@ class Command(plugable.Plugin): assert inspect.isclass(cls) o = cls() o.__lock__() - yield plugable.PluginProxy(option, o) + yield plugable.PluginProxy(Option, o) def __get_options(self): """ @@ -329,8 +329,8 @@ class Method(Attribute, Command): yield proxy -class Property(Attribute, option): - __public__ = Attribute.__public__.union(option.__public__) +class Property(Attribute, Option): + __public__ = Attribute.__public__.union(Option.__public__) def get_doc(self, _): return _('Property doc') diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index 4fd24f8d..ea054255 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -98,11 +98,11 @@ class test_DefaltFrom(ClassChecker): assert o(**kw_copy) is None -class test_option(ClassChecker): +class test_Option(ClassChecker): """ - Tests the `public.option` class. + Tests the `public.Option` class. """ - _cls = public.option + _cls = public.Option def get_subcls(self): rule = public.rule @@ -131,7 +131,7 @@ class test_option(ClassChecker): def test_normalize(self): """ - Tests the `public.option.normalize` method. + Tests the `public.Option.normalize` method. """ assert 'normalize' in self.cls.__public__ o = self.subcls() @@ -163,7 +163,7 @@ class test_option(ClassChecker): def test_validate(self): """ - Tests the `public.option.validate` method. + Tests the `public.Option.validate` method. """ assert 'validate' in self.cls.__public__ o = self.subcls() @@ -175,7 +175,7 @@ class test_option(ClassChecker): def test_rules(self): """ - Tests the `public.option.rules` property. + Tests the `public.Option.rules` property. """ o = self.subcls() assert len(o.rules) == 3 @@ -186,20 +186,20 @@ class test_option(ClassChecker): def test_default(self): """ - Tests the `public.option.default` method. + Tests the `public.Option.default` method. """ assert 'default' in self.cls.__public__ assert self.cls().default() is None -class test_cmd(ClassChecker): +class test_Command(ClassChecker): """ Tests the `public.Command` class. """ _cls = public.Command def get_subcls(self): - class my_option(public.option): + class my_option(public.Option): def normalize(self, value): return super(my_option, self).normalize(value).lower() @public.rule @@ -230,7 +230,7 @@ class test_cmd(ClassChecker): for (i, proxy) in enumerate(sub.get_options()): assert isinstance(proxy, plugable.PluginProxy) assert read_only(proxy, 'name') == 'option%d' % i - assert proxy.implements(public.option) + assert proxy.implements(public.Option) assert i == 1 def test_options(self): @@ -448,9 +448,9 @@ class test_Method(ClassChecker): assert self.cls.implements(public.Command) def get_subcls(self): - class option0(public.option): + class option0(public.Option): pass - class option1(public.option): + class option1(public.Option): pass class example_prop0(public.Property): pass @@ -486,15 +486,15 @@ class test_Method(ClassChecker): for (i, proxy) in enumerate(proxies): assert proxy.name == names[i] assert isinstance(proxy, plugable.PluginProxy) - assert proxy.implements(public.option) + assert proxy.implements(public.Option) class test_prop(ClassChecker): _cls = public.Property def test_class(self): - assert self.cls.__bases__ == (public.Attribute, public.option) - assert self.cls.implements(public.option) + assert self.cls.__bases__ == (public.Attribute, public.Option) + assert self.cls.implements(public.Option) def test_PublicAPI(): -- cgit From 2fc3819beca86c3d19d85e2f5777af3566305175 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 25 Aug 2008 23:35:29 +0000 Subject: 191: Removed ipalib/api.py module; standard plugable.API instance is now in ipalib.__init__.py --- ipalib/__init__.py | 10 ++++++++++ ipalib/api.py | 27 --------------------------- ipalib/plugins/example.py | 2 +- 3 files changed, 11 insertions(+), 28 deletions(-) delete mode 100644 ipalib/api.py (limited to 'ipalib') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index 4d96c2d6..2436d9b1 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -31,3 +31,13 @@ To learn about the ``ipalib`` library, you should read the code in this order: Some of the plugin architecture was inspired by ``bzr``, so you might also read http://bazaar-vcs.org/WritingPlugins """ + +import plugable +import public + +api = plugable.API( + public.Command, + public.Object, + public.Method, + public.Property, +) diff --git a/ipalib/api.py b/ipalib/api.py deleted file mode 100644 index f6820fd9..00000000 --- a/ipalib/api.py +++ /dev/null @@ -1,27 +0,0 @@ -# Authors: -# Jason Gerard DeRose -# -# 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 - -""" -Standard instances of plugable.API and its subclasses. -""" - -import public - -# The standard API instance -api = public.PublicAPI() diff --git a/ipalib/plugins/example.py b/ipalib/plugins/example.py index f7a5fe70..4f960564 100644 --- a/ipalib/plugins/example.py +++ b/ipalib/plugins/example.py @@ -23,7 +23,7 @@ Some example plugins. from ipalib import public -from ipalib.api import api +from ipalib import api # Hypothetical functional commands (not associated with any object): -- cgit From 0d35c96f1afd8b058a6431b944130e5d7bfe41bb Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 26 Aug 2008 00:04:15 +0000 Subject: 192: Added a quick console example to docstring in ipalib/__init__.py --- ipalib/__init__.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) (limited to 'ipalib') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index 2436d9b1..7f2249ef 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -30,6 +30,30 @@ To learn about the ``ipalib`` library, you should read the code in this order: Some of the plugin architecture was inspired by ``bzr``, so you might also read http://bazaar-vcs.org/WritingPlugins + +Here is a short console example on using the plugable API: + +>>> from ipalib import api +>>> list(api.register) # Plugins must subclass from one of these base classes: +['Command', 'Method', 'Object', 'Property'] +>>> 'user_add' in api.register.Command # Has 'user_add' been registered? +False +>>> import ipalib.load_plugins # This causes all plugins to be loaded +>>> 'user_add' in api.register.Command # Yes, 'user_add' has been registered: +True +>>> list(api) # API is empty till finalize() is called: +[] +>>> api.finalize() # Instantiates plugins, builds API namespaces: +>>> list(api) # Lists the namespaces in the API: +['Command', 'Method', 'Object', 'Property'] +>>> 'user_add' in api.Command # Yes, the 'user_add' command exists: +True +>>> api['Command'] is api.Command # Access as dict item or as attribute: +True +>>> list(api.Command) # List available commands: +['discover', 'group_add', 'group_del', 'group_find', 'group_mod', 'krbtest', 'service_add', 'service_del', 'service_find', 'service_mod', 'user_add', 'user_del', 'user_find', 'user_mod'] +>>> list(api.Command.user_add) # List public methods for user_add: +['__call__', 'default', 'execute', 'get_doc', 'normalize', 'options', 'validate'] """ import plugable -- cgit From 45a6ee50b10c5800cc9a8fd95bf55d9a734b0fe9 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 26 Aug 2008 00:06:00 +0000 Subject: 193: Removed depreciated public.PublicAPI class; removed corresponding unit tests --- ipalib/public.py | 5 ----- ipalib/tests/test_public.py | 17 ----------------- 2 files changed, 22 deletions(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index 06f5b2b5..d008e9d5 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -334,8 +334,3 @@ class Property(Attribute, Option): def get_doc(self, _): return _('Property doc') - - -class PublicAPI(plugable.API): - def __init__(self): - super(PublicAPI, self).__init__(Command, Object, Method, Property) diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index ea054255..37a92cbe 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -495,20 +495,3 @@ class test_prop(ClassChecker): def test_class(self): assert self.cls.__bases__ == (public.Attribute, public.Option) assert self.cls.implements(public.Option) - - -def test_PublicAPI(): - cls = public.PublicAPI - assert issubclass(cls, plugable.API) - - api = cls() - - class cmd1(public.Command): - pass - api.register(cmd1) - - class cmd2(public.Command): - pass - api.register(cmd2) - - api.finalize() -- cgit From 5157d8fc50bab1bd5ac1ebe2c006faaf910fef31 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 26 Aug 2008 00:28:43 +0000 Subject: 194: Removed like to Bazaar Plugin doc as it's not very relevant --- ipalib/__init__.py | 3 --- 1 file changed, 3 deletions(-) (limited to 'ipalib') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index 7f2249ef..a5fc3f11 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -28,9 +28,6 @@ To learn about the ``ipalib`` library, you should read the code in this order: 3. Look at some example plugins in `ipalib.plugins.example` -Some of the plugin architecture was inspired by ``bzr``, so you might also -read http://bazaar-vcs.org/WritingPlugins - Here is a short console example on using the plugable API: >>> from ipalib import api -- cgit From 87fabaa7177140cf0a934e0f05a6a4b4295fc1d0 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 26 Aug 2008 01:07:24 +0000 Subject: 195: Started on docstring for public.DefaultFrom --- ipalib/public.py | 20 ++++++++++++++++++++ ipalib/tests/test_public.py | 6 ++++++ 2 files changed, 26 insertions(+) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index d008e9d5..edb4641f 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -41,8 +41,28 @@ def is_rule(obj): class DefaultFrom(plugable.ReadOnly): + """ + Derives a default for one value using other supplied values. + + Here is an example: + + >>> df = DefaultFrom(lambda f, l: f[0] + l[0], 'first', 'last') + >>> df(first='John', last='Doe') # Both keys + 'JD' + >>> df() is None # Returns None if any key is missing + True + >>> df(first='John', middle='Q') is None # Still returns None + True + """ def __init__(self, callback, *keys): + """ + :param callback: The callable to call when all ``keys`` are present. + :param keys: The keys used to map from keyword to position arguments. + """ assert callable(callback), 'not a callable: %r' % callback + assert len(keys) > 0, 'must have at least one key' + for key in keys: + assert type(key) is str, 'not an str: %r' % key self.callback = callback self.keys = keys lock(self) diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index 37a92cbe..d809b074 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -73,6 +73,9 @@ class test_DefaltFrom(ClassChecker): assert self.cls.__bases__ == (plugable.ReadOnly,) def test_init(self): + """ + Tests the `public.DefaultFrom.__init__` method. + """ def callback(*args): return args keys = ('givenname', 'sn') @@ -81,6 +84,9 @@ class test_DefaltFrom(ClassChecker): assert read_only(o, 'keys') == keys def test_call(self): + """ + Tests the `public.DefaultFrom.__call__` method. + """ def callback(givenname, sn): return givenname[0] + sn[0] keys = ('givenname', 'sn') -- cgit From 0755c218ffdedafbeb9b0a19750704205b4f0b65 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 26 Aug 2008 16:52:46 +0000 Subject: 196: DefaultFrom.__call__() now returns values from callback even if not basestring; small work on DefaultFrom docstrings --- ipalib/public.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index edb4641f..bb592d2f 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -44,7 +44,8 @@ class DefaultFrom(plugable.ReadOnly): """ Derives a default for one value using other supplied values. - Here is an example: + Here is an example that constructs a user's initials from his first + and last name: >>> df = DefaultFrom(lambda f, l: f[0] + l[0], 'first', 'last') >>> df(first='John', last='Doe') # Both keys @@ -68,16 +69,18 @@ class DefaultFrom(plugable.ReadOnly): lock(self) def __call__(self, **kw): + """ + If all keys are present, calls the callback; otherwise returns None. + + :param kw: The keyword arguments. + """ vals = tuple(kw.get(k, None) for k in self.keys) if None in vals: return None try: - ret = self.callback(*vals) + return self.callback(*vals) except Exception: return None - if isinstance(ret, basestring): - return ret - return None class Option(plugable.Plugin): -- cgit From 0e47948695680047490ebdc55ca3fa4f7347640f Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 26 Aug 2008 19:02:24 +0000 Subject: 197: Added new public.Option.get_default() method that calls Option.default_from() if it's a DefaultFrom instance, and otherwise returns Option.default (the static default value) --- ipalib/public.py | 25 +++++++++++++------------ ipalib/tests/test_public.py | 19 ++++++++++++++++--- 2 files changed, 29 insertions(+), 15 deletions(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index bb592d2f..9e999879 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -90,14 +90,19 @@ class Option(plugable.Plugin): __public__ = frozenset(( 'normalize', - 'default', + 'get_default', 'validate', - 'required', 'type', + 'required', + 'default', + 'default_from', )) __rules = None type = unicode required = False + default = None + default_from = None + def normalize(self, value): """ @@ -159,16 +164,12 @@ class Option(plugable.Plugin): if is_rule(attr): yield attr - def default(self, **kw): - """ - Returns a default or auto-completed value for this Option. If no - default is available, this method should return None. - - All the keywords are passed so it's possible to build an - auto-completed value from other Options values, e.g., build 'initials' - from 'givenname' + 'sn'. - """ - return None + def get_default(self, **kw): + if type(self.default_from) is DefaultFrom: + default = self.default_from(**kw) + if default is not None: + return default + return self.default class Command(plugable.Plugin): diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index d809b074..609ac33e 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -190,12 +190,25 @@ class test_Option(ClassChecker): rules = tuple(get_rule(i) for i in xrange(3)) assert o.rules == rules - def test_default(self): + def test_get_default(self): """ - Tests the `public.Option.default` method. + Tests the `public.Option.get_default` method. """ + assert 'get_default' in self.cls.__public__ assert 'default' in self.cls.__public__ - assert self.cls().default() is None + assert 'default_from' in self.cls.__public__ + assert self.cls().get_default() is None + class subclass(self.cls): + default = 3 + default_from = public.DefaultFrom( + lambda a,b: a * b, + 'key0', 'key1' + ) + o = subclass() + assert o.get_default() == 3 + assert o.get_default(key0=2, key1=5) == 10 + assert o.get_default(key0=7) == 3 + class test_Command(ClassChecker): -- cgit From 845c0d8bc6bbd05a871d7b002415f83ab745cbd2 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 26 Aug 2008 19:13:55 +0000 Subject: 198: Renamed Command.default() to Command.get_default(); Command.get_default_iter() now correctly calls Option.get_default() instead of Option.default() --- ipalib/public.py | 12 ++++++------ ipalib/tests/test_public.py | 16 ++++++++-------- 2 files changed, 14 insertions(+), 14 deletions(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index 9e999879..78d4a7a4 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -175,7 +175,7 @@ class Option(plugable.Plugin): class Command(plugable.Plugin): __public__ = frozenset(( 'normalize', - 'default', + 'get_default', 'validate', 'execute', '__call__', @@ -231,16 +231,16 @@ class Command(plugable.Plugin): self.print_call('normalize', kw, 1) return dict(self.normalize_iter(kw)) - def default_iter(self, kw): + def get_default_iter(self, kw): for option in self.options(): if option.name not in kw: - value = option.default(**kw) + value = option.get_default(**kw) if value is not None: yield(option.name, value) - def default(self, **kw): + def get_default(self, **kw): self.print_call('default', kw, 1) - return dict(self.default_iter(kw)) + return dict(self.get_default_iter(kw)) def validate(self, **kw): self.print_call('validate', kw, 1) @@ -268,7 +268,7 @@ class Command(plugable.Plugin): print '' self.print_call('__call__', kw) kw = self.normalize(**kw) - kw.update(self.default(**kw)) + kw.update(self.get_default(**kw)) self.validate(**kw) self.execute(**kw) diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index 609ac33e..3ed1f5da 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -210,7 +210,6 @@ class test_Option(ClassChecker): assert o.get_default(key0=7) == 3 - class test_Command(ClassChecker): """ Tests the `public.Command` class. @@ -225,8 +224,9 @@ class test_Command(ClassChecker): def my_rule(self, value): if value != self.name: return 'must equal %r' % self.name - def default(self, **kw): - return kw['default_from'] + default_from = public.DefaultFrom( + lambda arg: arg, 'default_from' + ) class option0(my_option): pass @@ -282,11 +282,11 @@ class test_Command(ClassChecker): sub = self.subcls() assert sub.normalize(**kw) == norm - def test_default(self): + def test_get_default(self): """ - Tests the `public.Command.default` method. + Tests the `public.Command.get_default` method. """ - assert 'default' in self.cls.__public__ # Public + assert 'get_default' in self.cls.__public__ # Public no_fill = dict( option0='value0', option1='value1', @@ -300,8 +300,8 @@ class test_Command(ClassChecker): option1='the default', ) sub = self.subcls() - assert sub.default(**no_fill) == {} - assert sub.default(**fill) == default + assert sub.get_default(**no_fill) == {} + assert sub.get_default(**fill) == default def test_validate(self): """ -- cgit From 6226837eeae1ff8dca4a7bfe470337936c08dbaa Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 26 Aug 2008 19:23:50 +0000 Subject: 199: Updated user_login and user_initials example plugins to use Option.default_from --- ipalib/plugins/example.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugins/example.py b/ipalib/plugins/example.py index 4f960564..3456b1dc 100644 --- a/ipalib/plugins/example.py +++ b/ipalib/plugins/example.py @@ -68,23 +68,19 @@ api.register(user_sn) class user_login(public.Property): 'User login' required = True - def default(self, **kw): - givenname = kw.get('givenname', None) - sn = kw.get('sn', None) - if givenname is None or sn is None: - return None - return ('%s%s' % (givenname[0], sn)).lower() + default_from = public.DefaultFrom( + lambda first, last: (first[0] + last).lower(), + 'givenname', 'sn' + ) api.register(user_login) class user_initials(public.Property): 'User initials' required = True - def default(self, **kw): - givenname = kw.get('givenname', None) - sn = kw.get('sn', None) - if givenname is None or sn is None: - return None - return '%s%s' % (givenname[0], sn[0]) + default_from = public.DefaultFrom( + lambda first, last: first[0] + last[0], + 'givenname', 'sn' + ) api.register(user_initials) -- cgit From 74a3cf8d2860665cc37a8f2787f950246c0ba10e Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 26 Aug 2008 19:43:56 +0000 Subject: 200: Added plugins/override.py with an example of overriding a plugin --- ipalib/plugins/__init__.py | 1 + ipalib/plugins/override.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 ipalib/plugins/override.py (limited to 'ipalib') diff --git a/ipalib/plugins/__init__.py b/ipalib/plugins/__init__.py index 743eedb9..91b56733 100644 --- a/ipalib/plugins/__init__.py +++ b/ipalib/plugins/__init__.py @@ -22,3 +22,4 @@ Sub-package containing all internal plugins. """ import example +import override diff --git a/ipalib/plugins/override.py b/ipalib/plugins/override.py new file mode 100644 index 00000000..bc5666c2 --- /dev/null +++ b/ipalib/plugins/override.py @@ -0,0 +1,33 @@ +# Authors: +# Jason Gerard DeRose +# +# 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 + +""" +An example of overriding a plugin. + +This example depends upon the order that the plugins/ modules are imported +in plugins/__init__.py, which will likely change in the near future. +""" + +from ipalib import public +from ipalib import api + +if 'user_mod' in api.register.Method: + class user_mod(api.register.Method.user_mod): + '(override) Edit existing user' + api.register(user_mod, override=True) -- cgit From 330c17730c056428c70bd4612799cac44306f513 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 27 Aug 2008 00:25:33 +0000 Subject: 201: Added new cli command 'console' that starts a custom interactive Python console --- ipalib/cli.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index 1ad53058..989c24f6 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -23,6 +23,7 @@ Functionality for Command Line Inteface. import re import sys +import code import public @@ -44,7 +45,7 @@ def from_cli(cli_name): class help(public.Command): - 'display help on command' + 'Display help on command' def __call__(self, key): if from_cli(key) not in self.api.Command: print 'help: no such command %r' % key @@ -52,6 +53,16 @@ class help(public.Command): print 'Help on command %r:' % key +class console(public.Command): + 'Start IPA Interactive Python Console' + + def __call__(self): + code.interact( + '(Custom IPA Interactive Python Console)', + local=dict(api=self.api) + ) + + class CLI(object): __d = None __mcl = None @@ -82,6 +93,7 @@ class CLI(object): def finalize(self): api = self.api api.register(help) + api.register(console) api.finalize() def d_iter(): for cmd in api.Command(): -- cgit From 6b214cbccf5211078e3c2b3386c036d50c262f94 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 27 Aug 2008 20:09:19 +0000 Subject: 202: Started work on type classes in ipa_types module; added corresponding unit tests --- ipalib/ipa_types.py | 103 ++++++++++++++++++++++++++++++++++++++ ipalib/tests/test_ipa_types.py | 110 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 213 insertions(+) create mode 100644 ipalib/ipa_types.py create mode 100644 ipalib/tests/test_ipa_types.py (limited to 'ipalib') diff --git a/ipalib/ipa_types.py b/ipalib/ipa_types.py new file mode 100644 index 00000000..9a34a6b6 --- /dev/null +++ b/ipalib/ipa_types.py @@ -0,0 +1,103 @@ +# Authors: +# Jason Gerard DeRose +# +# 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 + +""" +Type system for coercing and normalizing input values. +""" + +from plugable import ReadOnly, lock +import errors + + +class Type(ReadOnly): + """ + Base class for all IPA types. + """ + + type = None # Override in base class + + def convert(self, value): + return self.type(value) + + def __get_name(self): + """ + Convenience property to return the class name. + """ + return self.__class__.__name__ + name = property(__get_name) + + +class Int(Type): + type = int + + def __init__(self, min_value=None, max_value=None): + args = (min_value, max_value) + for arg in args: + if not (arg is None or type(arg) is int): + raise TypeError('Must be an int or None: %r' % arg) + if None not in args and min_value >= max_value: + raise ValueError( + 'min_value not less than max_value: %r, %r' % ( + min_value, max_value + ) + ) + self.min_value = min_value + self.max_value = max_value + lock(self) + + def validate(self, value): + if type(value) is not self.type: + return 'Must be an integer' + if self.min_value is not None and value < self.min_value: + return 'Cannot be smaller than %d' % self.min_value + if self.max_value is not None and value > self.max_value: + return 'Cannot be larger than %d' % self.max_value + + +def check_min_max(min_name, min_value, max_name, max_value): + assert type(min_name) is str, 'min_name must be an str' + assert type(max_name) is str, 'max_name must be an str' + for (name, value) in [(min_name, min_value), (max_name, max_value)]: + if not (value is None or type(value) is int): + raise TypeError( + '%s must be an int or None, got: %r' % (name, value) + ) +# if None not in (min_value, max_value) and min_value >= max_value: +# raise ValueError( +# 'min_value not less than max_value: %r, %r' % ( +# min_value, max_value +# ) +# ) + + +class Unicode(Type): + def __init__(self, min_length=None, max_length=None, pattern=None): + integers = (min_length, max_length) + for i in integers: + if not (i is None or type(i) is int): + raise TypeError('Must be an int or None: %r' % i) + if None not in integers and min_value >= max_value: + raise ValueError( + 'min_value not less than max_value: %r, %r' % ( + min_value, max_value + ) + ) + self.min_length = min_length + self.max_length = max_length + self.pattern = pattern diff --git a/ipalib/tests/test_ipa_types.py b/ipalib/tests/test_ipa_types.py new file mode 100644 index 00000000..6ecef2a5 --- /dev/null +++ b/ipalib/tests/test_ipa_types.py @@ -0,0 +1,110 @@ +# Authors: +# Jason Gerard DeRose +# +# 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 + +""" +Unit tests for `ipalib.ipa_types` module. +""" + +from tstutil import raises, getitem, no_set, no_del, read_only, ClassChecker +from ipalib import ipa_types, errors, plugable + + +def test_check_min_max(): + """ + Tests the `ipa_types.check_min_max` function. + """ + f = ipa_types.check_min_max + fail_type = [ + '10', + 10.0, + 10L, + True, + False, + object, + ] + for value in fail_type: + e = raises(TypeError, f, 'low', value, 'high', None) + assert str(e) == 'low must be an int or None, got: %r' % value + e = raises(TypeError, f, 'low', None, 'high', value) + assert str(e) == 'high must be an int or None, got: %r' % value + + + +class test_Type(ClassChecker): + """ + Tests the `ipa_types.Type` class. + """ + _cls = ipa_types.Type + + def test_class(self): + assert self.cls.__bases__ == (plugable.ReadOnly,) + + + +class test_Int(ClassChecker): + _cls = ipa_types.Int + + def test_init(self): + o = self.cls() + assert o.min_value is None + assert o.max_value is None + okay = [ + (None, -5), + (-20, None), + (-20, -5), + ] + fail_type = [ + (None, 10L), + (5L, None), + (None, '10'), + ('5', None), + ] + fail_value = [ + (10, 5), + (5, -5), + (-5, -10), + ] + for (l, h) in okay: + o = self.cls(min_value=l, max_value=h) + assert o.min_value is l + assert o.max_value is h + for (l, h) in fail_type: + raises(TypeError, self.cls, min_value=l, max_value=h) + for (l, h) in fail_value: + raises(ValueError, self.cls, min_value=l, max_value=h) + + def test_validate(self): + o = self.cls(min_value=2, max_value=7) + assert o.validate(2) is None + assert o.validate(5) is None + assert o.validate(7) is None + assert o.validate(1) == 'Cannot be smaller than 2' + assert o.validate(8) == 'Cannot be larger than 7' + for val in ['5', 5.0, 5L, None, True, False, object]: + assert o.validate(val) == 'Must be an integer' + + +class test_Unicode(ClassChecker): + _cls = ipa_types.Unicode + + def test_init(self): + o = self.cls() + assert o.min_length is None + assert o.max_length is None + assert o.pattern is None -- cgit From 89ea3acd0add73049a9ff7e73a0fdd646c7c9894 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 27 Aug 2008 21:20:19 +0000 Subject: 203: Finished ipa_types.check_min_max() function; added corresponding unit tests; ipa_types.Int now uses check_min_max() --- ipalib/ipa_types.py | 47 ++++++++++++++------------------ ipalib/tests/test_ipa_types.py | 62 +++++++++++++++++++++++++++++++----------- 2 files changed, 67 insertions(+), 42 deletions(-) (limited to 'ipalib') diff --git a/ipalib/ipa_types.py b/ipalib/ipa_types.py index 9a34a6b6..a03b8822 100644 --- a/ipalib/ipa_types.py +++ b/ipalib/ipa_types.py @@ -25,6 +25,26 @@ from plugable import ReadOnly, lock import errors +def check_min_max(min_value, max_value, min_name, max_name): + assert type(min_name) is str, 'min_name must be an str' + assert type(max_name) is str, 'max_name must be an str' + for (name, value) in [(min_name, min_value), (max_name, max_value)]: + if not (value is None or type(value) is int): + raise TypeError( + '`%s` must be an int or None, got: %r' % (name, value) + ) + if None not in (min_value, max_value) and min_value >= max_value: + d = dict( + k0=min_name, + v0=min_value, + k1=max_name, + v1=max_value, + ) + raise ValueError( + '%(k1)s > %(k0)s: %(k0)s=%(v0)r, %(k1)s=%(v1)r' % d + ) + + class Type(ReadOnly): """ Base class for all IPA types. @@ -47,16 +67,7 @@ class Int(Type): type = int def __init__(self, min_value=None, max_value=None): - args = (min_value, max_value) - for arg in args: - if not (arg is None or type(arg) is int): - raise TypeError('Must be an int or None: %r' % arg) - if None not in args and min_value >= max_value: - raise ValueError( - 'min_value not less than max_value: %r, %r' % ( - min_value, max_value - ) - ) + check_min_max(min_value, max_value, 'min_value', 'max_value') self.min_value = min_value self.max_value = max_value lock(self) @@ -70,22 +81,6 @@ class Int(Type): return 'Cannot be larger than %d' % self.max_value -def check_min_max(min_name, min_value, max_name, max_value): - assert type(min_name) is str, 'min_name must be an str' - assert type(max_name) is str, 'max_name must be an str' - for (name, value) in [(min_name, min_value), (max_name, max_value)]: - if not (value is None or type(value) is int): - raise TypeError( - '%s must be an int or None, got: %r' % (name, value) - ) -# if None not in (min_value, max_value) and min_value >= max_value: -# raise ValueError( -# 'min_value not less than max_value: %r, %r' % ( -# min_value, max_value -# ) -# ) - - class Unicode(Type): def __init__(self, min_length=None, max_length=None, pattern=None): integers = (min_length, max_length) diff --git a/ipalib/tests/test_ipa_types.py b/ipalib/tests/test_ipa_types.py index 6ecef2a5..87063ef6 100644 --- a/ipalib/tests/test_ipa_types.py +++ b/ipalib/tests/test_ipa_types.py @@ -30,6 +30,13 @@ def test_check_min_max(): Tests the `ipa_types.check_min_max` function. """ f = ipa_types.check_min_max + okay = [ + (None, -5), + (-20, None), + (-20, -5), + ] + for (l, h) in okay: + assert f(l, h, 'low', 'high') is None fail_type = [ '10', 10.0, @@ -39,11 +46,18 @@ def test_check_min_max(): object, ] for value in fail_type: - e = raises(TypeError, f, 'low', value, 'high', None) - assert str(e) == 'low must be an int or None, got: %r' % value - e = raises(TypeError, f, 'low', None, 'high', value) - assert str(e) == 'high must be an int or None, got: %r' % value - + e = raises(TypeError, f, value, None, 'low', 'high') + assert str(e) == '`low` must be an int or None, got: %r' % value + e = raises(TypeError, f, None, value, 'low', 'high') + assert str(e) == '`high` must be an int or None, got: %r' % value + fail_value = [ + (10, 5), + (-5, -10), + (5, -10), + ] + for (l, h) in fail_value: + e = raises(ValueError, f, l, h, 'low', 'high') + assert str(e) == 'high > low: low=%r, high=%r' % (l, h) class test_Type(ClassChecker): @@ -56,36 +70,52 @@ class test_Type(ClassChecker): assert self.cls.__bases__ == (plugable.ReadOnly,) - class test_Int(ClassChecker): _cls = ipa_types.Int + def test_class(self): + assert self.cls.__bases__ == (ipa_types.Type,) + assert self.cls.type is int + def test_init(self): o = self.cls() + assert o.name == 'Int' assert o.min_value is None assert o.max_value is None + okay = [ (None, -5), (-20, None), (-20, -5), ] + for (l, h) in okay: + o = self.cls(min_value=l, max_value=h) + assert o.min_value is l + assert o.max_value is h + fail_type = [ - (None, 10L), - (5L, None), - (None, '10'), - ('5', None), + '10', + 10.0, + 10L, + True, + False, + object, ] + for value in fail_type: + e = raises(TypeError, self.cls, min_value=value) + assert str(e) == ( + '`min_value` must be an int or None, got: %r' % value + ) + e = raises(TypeError, self.cls, max_value=value) + assert str(e) == ( + '`max_value` must be an int or None, got: %r' % value + ) + fail_value = [ (10, 5), (5, -5), (-5, -10), ] - for (l, h) in okay: - o = self.cls(min_value=l, max_value=h) - assert o.min_value is l - assert o.max_value is h - for (l, h) in fail_type: - raises(TypeError, self.cls, min_value=l, max_value=h) for (l, h) in fail_value: raises(ValueError, self.cls, min_value=l, max_value=h) -- cgit From e6cecfdcf299db564a9055ad69b1c0bc75d4af31 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 27 Aug 2008 21:52:13 +0000 Subject: 204: Fixed logic error in check_min_max(); started work on argument validation for Unicode --- ipalib/ipa_types.py | 24 ++++++++++++------------ ipalib/tests/test_ipa_types.py | 19 ++++++++++++++----- 2 files changed, 26 insertions(+), 17 deletions(-) (limited to 'ipalib') diff --git a/ipalib/ipa_types.py b/ipalib/ipa_types.py index a03b8822..d94a44fc 100644 --- a/ipalib/ipa_types.py +++ b/ipalib/ipa_types.py @@ -21,6 +21,7 @@ Type system for coercing and normalizing input values. """ +import re from plugable import ReadOnly, lock import errors @@ -31,9 +32,9 @@ def check_min_max(min_value, max_value, min_name, max_name): for (name, value) in [(min_name, min_value), (max_name, max_value)]: if not (value is None or type(value) is int): raise TypeError( - '`%s` must be an int or None, got: %r' % (name, value) + '%s must be an int or None, got: %r' % (name, value) ) - if None not in (min_value, max_value) and min_value >= max_value: + if None not in (min_value, max_value) and min_value > max_value: d = dict( k0=min_name, v0=min_value, @@ -41,7 +42,7 @@ def check_min_max(min_value, max_value, min_name, max_name): v1=max_value, ) raise ValueError( - '%(k1)s > %(k0)s: %(k0)s=%(v0)r, %(k1)s=%(v1)r' % d + '%(k0)s > %(k1)s: %(k0)s=%(v0)r, %(k1)s=%(v1)r' % d ) @@ -82,17 +83,16 @@ class Int(Type): class Unicode(Type): - def __init__(self, min_length=None, max_length=None, pattern=None): - integers = (min_length, max_length) - for i in integers: - if not (i is None or type(i) is int): - raise TypeError('Must be an int or None: %r' % i) - if None not in integers and min_value >= max_value: + def __init__(self, length=None,min_length=None, max_length=None, pattern=None): + check_min_max(min_length, max_length, 'min_length', 'max_length') + if min_length is not None and min_length < 0: raise ValueError( - 'min_value not less than max_value: %r, %r' % ( - min_value, max_value - ) + 'min_length must zero or greater, got: %r' % min_length ) self.min_length = min_length self.max_length = max_length self.pattern = pattern + if pattern is None: + self.regex = None + else: + self.regex = re.compile(pattern) diff --git a/ipalib/tests/test_ipa_types.py b/ipalib/tests/test_ipa_types.py index 87063ef6..03346684 100644 --- a/ipalib/tests/test_ipa_types.py +++ b/ipalib/tests/test_ipa_types.py @@ -47,9 +47,9 @@ def test_check_min_max(): ] for value in fail_type: e = raises(TypeError, f, value, None, 'low', 'high') - assert str(e) == '`low` must be an int or None, got: %r' % value + assert str(e) == 'low must be an int or None, got: %r' % value e = raises(TypeError, f, None, value, 'low', 'high') - assert str(e) == '`high` must be an int or None, got: %r' % value + assert str(e) == 'high must be an int or None, got: %r' % value fail_value = [ (10, 5), (-5, -10), @@ -57,7 +57,7 @@ def test_check_min_max(): ] for (l, h) in fail_value: e = raises(ValueError, f, l, h, 'low', 'high') - assert str(e) == 'high > low: low=%r, high=%r' % (l, h) + assert str(e) == 'low > high: low=%r, high=%r' % (l, h) class test_Type(ClassChecker): @@ -104,11 +104,11 @@ class test_Int(ClassChecker): for value in fail_type: e = raises(TypeError, self.cls, min_value=value) assert str(e) == ( - '`min_value` must be an int or None, got: %r' % value + 'min_value must be an int or None, got: %r' % value ) e = raises(TypeError, self.cls, max_value=value) assert str(e) == ( - '`max_value` must be an int or None, got: %r' % value + 'max_value must be an int or None, got: %r' % value ) fail_value = [ @@ -135,6 +135,15 @@ class test_Unicode(ClassChecker): def test_init(self): o = self.cls() + assert o.name == 'Unicode' assert o.min_length is None assert o.max_length is None assert o.pattern is None + assert o.regex is None + + okay = ( + (0, 5, r'(hello|world)'), + (8, 8, r'\d{4}'), + ) + for (l, h, pat) in okay: + o = self.cls(min_length=l, max_length=h, pattern=pat) -- cgit From 2984041d00f509b34a6ba7f0f0f79135ba6842a3 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 27 Aug 2008 22:26:35 +0000 Subject: 205: Continued work on Unicode.__init__() and corresponding unit tests --- ipalib/ipa_types.py | 10 ++++++--- ipalib/tests/test_ipa_types.py | 51 +++++++++++++++++++++++++++++++++++++----- 2 files changed, 53 insertions(+), 8 deletions(-) (limited to 'ipalib') diff --git a/ipalib/ipa_types.py b/ipalib/ipa_types.py index d94a44fc..f0baf1ba 100644 --- a/ipalib/ipa_types.py +++ b/ipalib/ipa_types.py @@ -83,11 +83,15 @@ class Int(Type): class Unicode(Type): - def __init__(self, length=None,min_length=None, max_length=None, pattern=None): + def __init__(self, min_length=None, max_length=None, pattern=None): check_min_max(min_length, max_length, 'min_length', 'max_length') if min_length is not None and min_length < 0: - raise ValueError( - 'min_length must zero or greater, got: %r' % min_length + raise ValueError('min_length must be >= 0, got: %r' % min_length) + if max_length is not None and max_length < 1: + raise ValueError('max_length must be >= 1, got: %r' % max_length) + if not (pattern is None or isinstance(pattern, basestring)): + raise TypeError( + 'pattern must be a basestring or None, got: %r' % pattern ) self.min_length = min_length self.max_length = max_length diff --git a/ipalib/tests/test_ipa_types.py b/ipalib/tests/test_ipa_types.py index 03346684..8e4f379f 100644 --- a/ipalib/tests/test_ipa_types.py +++ b/ipalib/tests/test_ipa_types.py @@ -117,7 +117,10 @@ class test_Int(ClassChecker): (-5, -10), ] for (l, h) in fail_value: - raises(ValueError, self.cls, min_value=l, max_value=h) + e = raises(ValueError, self.cls, min_value=l, max_value=h) + assert str(e) == ( + 'min_value > max_value: min_value=%d, max_value=%d' % (l, h) + ) def test_validate(self): o = self.cls(min_value=2, max_value=7) @@ -141,9 +144,47 @@ class test_Unicode(ClassChecker): assert o.pattern is None assert o.regex is None + # Test min_length, max_length: okay = ( - (0, 5, r'(hello|world)'), - (8, 8, r'\d{4}'), + (0, 1), + (8, 8), ) - for (l, h, pat) in okay: - o = self.cls(min_length=l, max_length=h, pattern=pat) + for (l, h) in okay: + o = self.cls(min_length=l, max_length=h) + assert o.min_length == l + assert o.max_length == h + + fail_type = [ + '10', + 10.0, + 10L, + True, + False, + object, + ] + for value in fail_type: + e = raises(TypeError, self.cls, min_length=value) + assert str(e) == ( + 'min_length must be an int or None, got: %r' % value + ) + e = raises(TypeError, self.cls, max_length=value) + assert str(e) == ( + 'max_length must be an int or None, got: %r' % value + ) + + fail_value = [ + (10, 5), + (5, -5), + (0, -10), + ] + for (l, h) in fail_value: + e = raises(ValueError, self.cls, min_length=l, max_length=h) + assert str(e) == ( + 'min_length > max_length: min_length=%d, max_length=%d' % (l, h) + ) + + for (key, lower) in [('min_length', 0), ('max_length', 1)]: + value = lower - 1 + kw = {key: value} + e = raises(ValueError, self.cls, **kw) + assert str(e) == '%s must be >= %d, got: %d' % (key, lower, value) -- cgit From 8fbc01ca864df332afe16ed51ea661ae88892d8b Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 27 Aug 2008 22:56:51 +0000 Subject: 206: Finished unit tests for Unicode.__init__() --- ipalib/tests/test_ipa_types.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) (limited to 'ipalib') diff --git a/ipalib/tests/test_ipa_types.py b/ipalib/tests/test_ipa_types.py index 8e4f379f..d78160db 100644 --- a/ipalib/tests/test_ipa_types.py +++ b/ipalib/tests/test_ipa_types.py @@ -188,3 +188,34 @@ class test_Unicode(ClassChecker): kw = {key: value} e = raises(ValueError, self.cls, **kw) assert str(e) == '%s must be >= %d, got: %d' % (key, lower, value) + + # Test pattern: + okay = [ + '(hello|world)', + u'(take the blue pill|take the red pill)', + ] + for value in okay: + o = self.cls(pattern=value) + assert o.pattern is value + assert o.regex is not None + + fail = [ + 42, + True, + False, + object, + ] + for value in fail: + e = raises(TypeError, self.cls, pattern=value) + assert str(e) == ( + 'pattern must be a basestring or None, got: %r' % value + ) + + # Test regex: + pat = '^(hello|world)$' + o = self.cls(pattern=pat) + for value in ('hello', 'world'): + m = o.regex.match(value) + assert m.group(1) == value + for value in ('hello beautiful', 'world!'): + assert o.regex.match(value) is None -- cgit From 5da1d4bb86fadc12e2becf05239b0177d42ce454 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 27 Aug 2008 23:40:34 +0000 Subject: 207: Added Unicode.validate() method and corresponding unit tests --- ipalib/ipa_types.py | 16 ++++++++++++++++ ipalib/tests/test_ipa_types.py | 17 ++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/ipa_types.py b/ipalib/ipa_types.py index f0baf1ba..3cae7af5 100644 --- a/ipalib/ipa_types.py +++ b/ipalib/ipa_types.py @@ -83,6 +83,8 @@ class Int(Type): class Unicode(Type): + type = unicode + def __init__(self, min_length=None, max_length=None, pattern=None): check_min_max(min_length, max_length, 'min_length', 'max_length') if min_length is not None and min_length < 0: @@ -100,3 +102,17 @@ class Unicode(Type): self.regex = None else: self.regex = re.compile(pattern) + lock(self) + + def validate(self, value): + if type(value) is not self.type: + return 'Must be a string' + + if self.regex and self.regex.match(value) is None: + return 'Must match %r' % self.pattern + + if self.min_length is not None and len(value) < self.min_length: + return 'Must be at least %d characters long' % self.min_length + + if self.max_length is not None and len(value) > self.max_length: + return 'Can be at most %d characters long' % self.max_length diff --git a/ipalib/tests/test_ipa_types.py b/ipalib/tests/test_ipa_types.py index d78160db..71d8d6f3 100644 --- a/ipalib/tests/test_ipa_types.py +++ b/ipalib/tests/test_ipa_types.py @@ -136,9 +136,13 @@ class test_Int(ClassChecker): class test_Unicode(ClassChecker): _cls = ipa_types.Unicode + def test_class(self): + assert self.cls.__bases__ == (ipa_types.Type,) + assert self.cls.type is unicode + def test_init(self): o = self.cls() - assert o.name == 'Unicode' + assert read_only(o, 'name') == 'Unicode' assert o.min_length is None assert o.max_length is None assert o.pattern is None @@ -219,3 +223,14 @@ class test_Unicode(ClassChecker): assert m.group(1) == value for value in ('hello beautiful', 'world!'): assert o.regex.match(value) is None + + def test_validate(self): + pat = '^a_*b$' + o = self.cls(min_length=3, max_length=4, pattern=pat) + assert o.validate(u'a_b') is None + assert o.validate(u'a__b') is None + assert o.validate('a_b') == 'Must be a string' + assert o.validate(u'ab') == 'Must be at least 3 characters long' + assert o.validate(u'a___b') == 'Can be at most 4 characters long' + assert o.validate(u'a-b') == 'Must match %r' % pat + assert o.validate(u'a--b') == 'Must match %r' % pat -- cgit From 2b01bdc1121bf7dee1296bc3b8bdf8443d54d202 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 28 Aug 2008 01:38:29 +0000 Subject: 209: Added Type.__call__() method; fleshed out Type.convert() method; added corresponding unit tests --- ipalib/ipa_types.py | 16 +++++++++++++--- ipalib/tests/test_ipa_types.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 3 deletions(-) (limited to 'ipalib') diff --git a/ipalib/ipa_types.py b/ipalib/ipa_types.py index 3cae7af5..5bcea534 100644 --- a/ipalib/ipa_types.py +++ b/ipalib/ipa_types.py @@ -53,9 +53,6 @@ class Type(ReadOnly): type = None # Override in base class - def convert(self, value): - return self.type(value) - def __get_name(self): """ Convenience property to return the class name. @@ -63,6 +60,19 @@ class Type(ReadOnly): return self.__class__.__name__ name = property(__get_name) + def convert(self, value): + try: + return self.type(value) + except (TypeError, ValueError): + return None + + def __call__(self, value): + if value is None: + raise TypeError('value cannot be None') + if type(value) is self.type: + return value + return self.convert(value) + class Int(Type): type = int diff --git a/ipalib/tests/test_ipa_types.py b/ipalib/tests/test_ipa_types.py index 71d8d6f3..657a99bd 100644 --- a/ipalib/tests/test_ipa_types.py +++ b/ipalib/tests/test_ipa_types.py @@ -122,6 +122,36 @@ class test_Int(ClassChecker): 'min_value > max_value: min_value=%d, max_value=%d' % (l, h) ) + + def test_call(self): + o = self.cls() + + # Test calling with None + e = raises(TypeError, o, None) + assert str(e) == 'value cannot be None' + + # Test with values that can be converted: + okay = [ + 3, + '3', + ' 3 ', + 3L, + 3.0, + ] + for value in okay: + assert o(value) == 3 + + # Test with values that cannot be converted: + fail = [ + object, + '3.0', + '3L', + 'whatever', + ] + for value in fail: + assert o(value) is None + + def test_validate(self): o = self.cls(min_value=2, max_value=7) assert o.validate(2) is None -- cgit From 039b9a2a9b8e4a5a81d338b642869cefa6a6d8e4 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 28 Aug 2008 02:02:03 +0000 Subject: 210: Type.__init__() now takes the type as the first argument, does not use subclass attribute; updated Int, Unicode, and their unit tests accordingly --- ipalib/ipa_types.py | 17 ++++++++++------- ipalib/tests/test_ipa_types.py | 21 +++++++++++---------- 2 files changed, 21 insertions(+), 17 deletions(-) (limited to 'ipalib') diff --git a/ipalib/ipa_types.py b/ipalib/ipa_types.py index 5bcea534..8d30aaa4 100644 --- a/ipalib/ipa_types.py +++ b/ipalib/ipa_types.py @@ -51,7 +51,14 @@ class Type(ReadOnly): Base class for all IPA types. """ - type = None # Override in base class + def __init__(self, type_): + allowed = (bool, int, float, unicode) + if type_ not in allowed: + raise ValueError( + 'type_ must be in %r, got %r' % (type_, allowed) + ) + self.type = type_ + lock(self) def __get_name(self): """ @@ -75,13 +82,11 @@ class Type(ReadOnly): class Int(Type): - type = int - def __init__(self, min_value=None, max_value=None): check_min_max(min_value, max_value, 'min_value', 'max_value') self.min_value = min_value self.max_value = max_value - lock(self) + super(Int, self).__init__(int) def validate(self, value): if type(value) is not self.type: @@ -93,8 +98,6 @@ class Int(Type): class Unicode(Type): - type = unicode - def __init__(self, min_length=None, max_length=None, pattern=None): check_min_max(min_length, max_length, 'min_length', 'max_length') if min_length is not None and min_length < 0: @@ -112,7 +115,7 @@ class Unicode(Type): self.regex = None else: self.regex = re.compile(pattern) - lock(self) + super(Unicode, self).__init__(unicode) def validate(self, value): if type(value) is not self.type: diff --git a/ipalib/tests/test_ipa_types.py b/ipalib/tests/test_ipa_types.py index 657a99bd..4ca44121 100644 --- a/ipalib/tests/test_ipa_types.py +++ b/ipalib/tests/test_ipa_types.py @@ -75,13 +75,14 @@ class test_Int(ClassChecker): def test_class(self): assert self.cls.__bases__ == (ipa_types.Type,) - assert self.cls.type is int def test_init(self): o = self.cls() - assert o.name == 'Int' - assert o.min_value is None - assert o.max_value is None + assert o.__islocked__() is True + assert read_only(o, 'type') is int + assert read_only(o, 'name') == 'Int' + assert read_only(o, 'min_value') is None + assert read_only(o, 'max_value') is None okay = [ (None, -5), @@ -122,7 +123,6 @@ class test_Int(ClassChecker): 'min_value > max_value: min_value=%d, max_value=%d' % (l, h) ) - def test_call(self): o = self.cls() @@ -168,15 +168,16 @@ class test_Unicode(ClassChecker): def test_class(self): assert self.cls.__bases__ == (ipa_types.Type,) - assert self.cls.type is unicode def test_init(self): o = self.cls() + assert o.__islocked__() is True + assert read_only(o, 'type') is unicode assert read_only(o, 'name') == 'Unicode' - assert o.min_length is None - assert o.max_length is None - assert o.pattern is None - assert o.regex is None + assert read_only(o, 'min_length') is None + assert read_only(o, 'max_length') is None + assert read_only(o, 'pattern') is None + assert read_only(o, 'regex') is None # Test min_length, max_length: okay = ( -- cgit From d121a729aa1b0e97dcf927bfc66eb86a73529863 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 28 Aug 2008 02:45:04 +0000 Subject: 212: Type.__init__() now can also raise TypeError; added unit tests for Type.__init__() --- ipalib/ipa_types.py | 6 +++--- ipalib/tests/test_ipa_types.py | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) (limited to 'ipalib') diff --git a/ipalib/ipa_types.py b/ipalib/ipa_types.py index 8d30aaa4..fe35a9ac 100644 --- a/ipalib/ipa_types.py +++ b/ipalib/ipa_types.py @@ -52,11 +52,11 @@ class Type(ReadOnly): """ def __init__(self, type_): + if type(type_) is not type: + raise TypeError('%r is not %r' % (type(type_), type)) allowed = (bool, int, float, unicode) if type_ not in allowed: - raise ValueError( - 'type_ must be in %r, got %r' % (type_, allowed) - ) + raise ValueError('not an allowed type: %r' % type_) self.type = type_ lock(self) diff --git a/ipalib/tests/test_ipa_types.py b/ipalib/tests/test_ipa_types.py index 4ca44121..f594aabf 100644 --- a/ipalib/tests/test_ipa_types.py +++ b/ipalib/tests/test_ipa_types.py @@ -69,6 +69,24 @@ class test_Type(ClassChecker): def test_class(self): assert self.cls.__bases__ == (plugable.ReadOnly,) + def test_init(self): + okay = (bool, int, float, unicode) + for t in okay: + o = self.cls(t) + assert o.__islocked__() is True + assert read_only(o, 'type') is t + assert read_only(o, 'name') is 'Type' + + type_errors = (None, True, 8, 8.0, u'hello') + for t in type_errors: + e = raises(TypeError, self.cls, t) + assert str(e) == '%r is not %r' % (type(t), type) + + value_errors = (long, complex, str, tuple, list, dict, set, frozenset) + for t in value_errors: + e = raises(ValueError, self.cls, t) + assert str(e) == 'not an allowed type: %r' % t + class test_Int(ClassChecker): _cls = ipa_types.Int -- cgit From b865b30511316f2874de6c95b648e6f653f5a46c Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 28 Aug 2008 04:54:48 +0000 Subject: 213: Added ipa_type.Bool class; added corresponding unit tests --- ipalib/ipa_types.py | 22 ++++++++++++++++++++++ ipalib/tests/test_ipa_types.py | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) (limited to 'ipalib') diff --git a/ipalib/ipa_types.py b/ipalib/ipa_types.py index fe35a9ac..ff62af22 100644 --- a/ipalib/ipa_types.py +++ b/ipalib/ipa_types.py @@ -81,6 +81,28 @@ class Type(ReadOnly): return self.convert(value) +class Bool(Type): + def __init__(self, true='Yes', false='No'): + if true is None: + raise TypeError('`true` cannot be None') + if false is None: + raise TypeError('`false` cannot be None') + if true == false: + raise ValueError( + 'cannot be equal: true=%r, false=%r' % (true, false) + ) + self.true = true + self.false = false + super(Bool, self).__init__(bool) + + def convert(self, value): + if value == self.true: + return True + if value == self.false: + return False + return None + + class Int(Type): def __init__(self, min_value=None, max_value=None): check_min_max(min_value, max_value, 'min_value', 'max_value') diff --git a/ipalib/tests/test_ipa_types.py b/ipalib/tests/test_ipa_types.py index f594aabf..37546d9e 100644 --- a/ipalib/tests/test_ipa_types.py +++ b/ipalib/tests/test_ipa_types.py @@ -88,6 +88,45 @@ class test_Type(ClassChecker): assert str(e) == 'not an allowed type: %r' % t +class test_Bool(ClassChecker): + _cls = ipa_types.Bool + + def test_class(self): + assert self.cls.__bases__ == (ipa_types.Type,) + + def test_init(self): + o = self.cls() + assert o.__islocked__() is True + assert read_only(o, 'type') is bool + assert read_only(o, 'name') == 'Bool' + assert read_only(o, 'true') == 'Yes' + assert read_only(o, 'false') == 'No' + + keys = ('true', 'false') + val = 'some value' + for key in keys: + # Check that kwarg sets appropriate attribute: + o = self.cls(**{key: val}) + assert read_only(o, key) is val + # Check that None raises TypeError: + e = raises(TypeError, self.cls, **{key: None}) + assert str(e) == '`%s` cannot be None' % key + + # Check that ValueError is raise if true == false: + e = raises(ValueError, self.cls, true=1L, false=1.0) + assert str(e) == 'cannot be equal: true=1L, false=1.0' + + def test_call(self): + o = self.cls() + assert o(True) is True + assert o('Yes') is True + assert o(False) is False + assert o('No') is False + for value in (0, 1, 'True', 'False', 'yes', 'no'): + # value is not be converted, so None is returned + assert o(value) is None + + class test_Int(ClassChecker): _cls = ipa_types.Int -- cgit From 07ac867ed39b9539c4667bf51ef32778e5fb01df Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 28 Aug 2008 05:06:13 +0000 Subject: 214: Added ipa_types.Bool.validate() method; added corresponding unit tests --- ipalib/ipa_types.py | 4 ++++ ipalib/tests/test_ipa_types.py | 9 +++++++++ 2 files changed, 13 insertions(+) (limited to 'ipalib') diff --git a/ipalib/ipa_types.py b/ipalib/ipa_types.py index ff62af22..670c4dd6 100644 --- a/ipalib/ipa_types.py +++ b/ipalib/ipa_types.py @@ -102,6 +102,10 @@ class Bool(Type): return False return None + def validate(self, value): + if not (value is True or value is False): + return 'Must be %r or %r' % (self.true, self.false) + class Int(Type): def __init__(self, min_value=None, max_value=None): diff --git a/ipalib/tests/test_ipa_types.py b/ipalib/tests/test_ipa_types.py index 37546d9e..5d31b844 100644 --- a/ipalib/tests/test_ipa_types.py +++ b/ipalib/tests/test_ipa_types.py @@ -126,6 +126,15 @@ class test_Bool(ClassChecker): # value is not be converted, so None is returned assert o(value) is None + def test_validate(self): + t = 'For sure!' + f = 'No way!' + o = self.cls(true=t, false=f) + assert o.validate(True) is None + assert o.validate(False) is None + for value in (t, f, 0, 1, 'True', 'False', 'Yes', 'No'): + assert o.validate(value) == 'Must be %r or %r' % (t, f) + class test_Int(ClassChecker): _cls = ipa_types.Int -- cgit From c83c478ae17991391ad5431062dd987ea5640469 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 28 Aug 2008 06:56:45 +0000 Subject: 215: Added basics of ipa_types.Enum class; added corresponding unit tests --- ipalib/ipa_types.py | 16 ++++++++++++++++ ipalib/tests/test_ipa_types.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) (limited to 'ipalib') diff --git a/ipalib/ipa_types.py b/ipalib/ipa_types.py index 670c4dd6..19950ead 100644 --- a/ipalib/ipa_types.py +++ b/ipalib/ipa_types.py @@ -155,3 +155,19 @@ class Unicode(Type): if self.max_length is not None and len(value) > self.max_length: return 'Can be at most %d characters long' % self.max_length + + +class Enum(Type): + def __init__(self, *values): + if len(values) < 1: + raise ValueError('%s requires at least one value' % self.name) + type_ = type(values[0]) + if type_ not in (unicode, int, float): + raise TypeError( + '%r: %r not unicode, int, nor float' % (values[0], type_) + ) + for val in values[1:]: + if type(val) is not type_: + raise TypeError('%r: %r is not %r' % (val, type(val), type_)) + self.values = values + super(Enum, self).__init__(type_) diff --git a/ipalib/tests/test_ipa_types.py b/ipalib/tests/test_ipa_types.py index 5d31b844..6ae94c41 100644 --- a/ipalib/tests/test_ipa_types.py +++ b/ipalib/tests/test_ipa_types.py @@ -331,3 +331,34 @@ class test_Unicode(ClassChecker): assert o.validate(u'a___b') == 'Can be at most 4 characters long' assert o.validate(u'a-b') == 'Must match %r' % pat assert o.validate(u'a--b') == 'Must match %r' % pat + + +class test_Enum(ClassChecker): + _cls = ipa_types.Enum + + def test_class(self): + assert self.cls.__bases__ == (ipa_types.Type,) + + def test_init(self): + for t in (unicode, int, float): + vals = (t(1),) + o = self.cls(*vals) + assert o.__islocked__() is True + assert read_only(o, 'type') is t + assert read_only(o, 'name') is 'Enum' + assert read_only(o, 'values') == vals + + # Check that ValueError is raised when no values are given: + e = raises(ValueError, self.cls) + assert str(e) == 'Enum requires at least one value' + + # Check that TypeError is raised when type of first value is not + # allowed: + e = raises(TypeError, self.cls, 'hello') + assert str(e) == '%r: %r not unicode, int, nor float' % ('hello', str) + #self.cls('hello') + + # Check that TypeError is raised when subsequent values aren't same + # type as first: + e = raises(TypeError, self.cls, u'hello', 'world') + assert str(e) == '%r: %r is not %r' % ('world', str, unicode) -- cgit From 283c6f8fcec6a4687fd2cc99326a7f2b33e4e8bf Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 28 Aug 2008 07:57:07 +0000 Subject: 216: Added ipa_types.Enum.validate() method; added corresponding unit tests --- ipalib/ipa_types.py | 7 +++++++ ipalib/tests/test_ipa_types.py | 17 ++++++++++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) (limited to 'ipalib') diff --git a/ipalib/ipa_types.py b/ipalib/ipa_types.py index 19950ead..c120b5ab 100644 --- a/ipalib/ipa_types.py +++ b/ipalib/ipa_types.py @@ -170,4 +170,11 @@ class Enum(Type): if type(val) is not type_: raise TypeError('%r: %r is not %r' % (val, type(val), type_)) self.values = values + self.frozenset = frozenset(values) super(Enum, self).__init__(type_) + + def validate(self, value): + if type(value) is not self.type: + return 'Incorrect type' + if value not in self.frozenset: + return 'Invalid value' diff --git a/ipalib/tests/test_ipa_types.py b/ipalib/tests/test_ipa_types.py index 6ae94c41..360478fb 100644 --- a/ipalib/tests/test_ipa_types.py +++ b/ipalib/tests/test_ipa_types.py @@ -341,12 +341,13 @@ class test_Enum(ClassChecker): def test_init(self): for t in (unicode, int, float): - vals = (t(1),) - o = self.cls(*vals) + values = (t(1), t(2), t(3)) + o = self.cls(*values) assert o.__islocked__() is True assert read_only(o, 'type') is t assert read_only(o, 'name') is 'Enum' - assert read_only(o, 'values') == vals + assert read_only(o, 'values') == values + assert read_only(o, 'frozenset') == frozenset(values) # Check that ValueError is raised when no values are given: e = raises(ValueError, self.cls) @@ -362,3 +363,13 @@ class test_Enum(ClassChecker): # type as first: e = raises(TypeError, self.cls, u'hello', 'world') assert str(e) == '%r: %r is not %r' % ('world', str, unicode) + + def test_validate(self): + values = (u'hello', u'naughty', u'nurse') + o = self.cls(*values) + for value in values: + assert o.validate(value) is None + assert o.validate(str(value)) == 'Incorrect type' + for value in (u'one fish', u'two fish'): + assert o.validate(value) == 'Invalid value' + assert o.validate(str(value)) == 'Incorrect type' -- cgit From a6ec94da601273719e44c69c4d7c23776ab30f3a Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 28 Aug 2008 18:31:06 +0000 Subject: 217: Started work on new Option2 class that is more declarative and doesn't require subclassing from Option --- ipalib/public.py | 37 +++++++++++++++++++++++++++++++++++++ ipalib/tests/test_public.py | 29 ++++++++++++++++++++++++++++- 2 files changed, 65 insertions(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index 78d4a7a4..4c2a8dd2 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -27,6 +27,7 @@ import inspect import plugable from plugable import lock import errors +import ipa_types RULE_FLAG = 'validation_rule' @@ -83,6 +84,42 @@ class DefaultFrom(plugable.ReadOnly): return None +class Option2(plugable.ReadOnly): + def __init__(self, name, doc, type_, required=False, multivalue=False, + default=None, default_from=None, normalize=None, rules=tuple() + ): + self.name = name + self.doc = doc + self.type = type_ + self.required = required + self.multivalue = multivalue + self.default = default + self.default_from = default_from + self.__normalize = normalize + self.rules = (type_.validate,) + rules + lock(self) + + def validate_scalar(self, value): + for rule in self.rules: + msg = rule(value) + if msg is not None: + raise errors.RuleError( + self.__class__.__name__, + value, + rule, + msg, + ) + + def validate(self, value): + if self.multivalue: + if type(value) is not tuple: + value = (value,) + for v in value: + self.validate_scalar(v) + else: + self.validate_scalar(value) + + class Option(plugable.Plugin): """ The Option class represents a kw argument from a `Command`. diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index 3ed1f5da..5b5d1d4b 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -22,7 +22,7 @@ Unit tests for `ipalib.public` module. """ from tstutil import raises, getitem, no_set, no_del, read_only, ClassChecker -from ipalib import public, plugable, errors +from ipalib import public, plugable, errors, ipa_types def test_RULE_FLAG(): @@ -104,6 +104,33 @@ class test_DefaltFrom(ClassChecker): assert o(**kw_copy) is None +class test_Option2(ClassChecker): + """ + Tests the `public.Option2` class. + """ + _cls = public.Option2 + + def test_class(self): + assert self.cls.__bases__ == (plugable.ReadOnly,) + + def test_init(self): + name = 'sn', + doc = 'Last Name', + type_ = ipa_types.Unicode() + o = self.cls(name, doc, type_) + assert o.__islocked__() is True + assert read_only(o, 'name') is name + assert read_only(o, 'doc') is doc + assert read_only(o, 'type') is type_ + assert read_only(o, 'required') is False + assert read_only(o, 'multivalue') is False + assert read_only(o, 'default') is None + assert read_only(o, 'default_from') is None + assert read_only(o, 'rules') == (type_.validate,) + + + + class test_Option(ClassChecker): """ Tests the `public.Option` class. -- cgit From 992a5dadbea27b21075617daf4216396d18404de Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 28 Aug 2008 20:30:08 +0000 Subject: 218: Finished unit tests for Option2.validate(), Option2.validate_scalar() --- ipalib/errors.py | 1 + ipalib/public.py | 16 ++++++------- ipalib/tests/test_public.py | 57 ++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 64 insertions(+), 10 deletions(-) (limited to 'ipalib') diff --git a/ipalib/errors.py b/ipalib/errors.py index afc61dd8..a25df091 100644 --- a/ipalib/errors.py +++ b/ipalib/errors.py @@ -67,6 +67,7 @@ class RuleError(ValidationError): """ Raised when a required option was not provided. """ + # FIXME: `rule` should really be after `error` def __init__(self, name, value, rule, error): self.rule = rule ValidationError.__init__(self, name, value, error) diff --git a/ipalib/public.py b/ipalib/public.py index 4c2a8dd2..e0d9e6d2 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -88,6 +88,7 @@ class Option2(plugable.ReadOnly): def __init__(self, name, doc, type_, required=False, multivalue=False, default=None, default_from=None, normalize=None, rules=tuple() ): + self.name = name self.doc = doc self.type = type_ @@ -101,19 +102,16 @@ class Option2(plugable.ReadOnly): def validate_scalar(self, value): for rule in self.rules: - msg = rule(value) - if msg is not None: - raise errors.RuleError( - self.__class__.__name__, - value, - rule, - msg, - ) + error = rule(value) + if error is not None: + raise errors.RuleError(self.name, value, rule, error) def validate(self, value): if self.multivalue: if type(value) is not tuple: - value = (value,) + raise TypeError( + 'when multivalue, value must be tuple; got %r' % value + ) for v in value: self.validate_scalar(v) else: diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index 5b5d1d4b..ed4c75e5 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -128,7 +128,62 @@ class test_Option2(ClassChecker): assert read_only(o, 'default_from') is None assert read_only(o, 'rules') == (type_.validate,) - + def test_validate(self): + # Constructor arguments + name = 'sn' + doc = 'User last name' + type_ = ipa_types.Unicode() + def case_rule(value): + if not value.islower(): + return 'Must be lower case' + my_rules = (case_rule,) + + # Some test values: + okay = u'whatever' + fail_case = u'Whatever' + fail_type = 'whatever' + + # Test validate() and validate_scalar() when multivalue=False: + o = self.cls(name, doc, type_, rules=my_rules) + assert o.rules == (type_.validate, case_rule) + for m in [o.validate, o.validate_scalar]: + # Test a valid value: + m(okay) + # Check that RuleError is raised with wrong case: + e = raises(errors.RuleError, m, fail_case) + assert e.name is name + assert e.value is fail_case + assert e.error == 'Must be lower case' + # Test a RuleError is raise with wrong type: + e = raises(errors.RuleError, m, fail_type) + assert e.name is name + assert e.value is fail_type + assert e.error == 'Must be a string' + + # Test validate() when multivalue=True: + o = self.cls(name, doc, type_, multivalue=True, rules=my_rules) + def check_type_error(value): + e = raises(TypeError, o.validate, value) + assert str(e) == ( + 'when multivalue, value must be tuple; got %r' % value + ) + # Check a valid value: + check_type_error(okay) + o.validate((okay,)) + # Check that RuleError is raised with wrong case: + check_type_error(fail_case) + for value in [(okay, fail_case), (fail_case, okay)]: + e = raises(errors.RuleError, o.validate, value) + assert e.name is name + assert e.value is fail_case + assert e.error == 'Must be lower case' + # Test a RuleError is raise with wrong type: + check_type_error(fail_type) + for value in [(okay, fail_type), (fail_type, okay)]: + e = raises(errors.RuleError, o.validate, value) + assert e.name is name + assert e.value is fail_type + assert e.error == 'Must be a string' class test_Option(ClassChecker): -- cgit From 61b5b7a8488c2e3392905df3f6e7e02b7302fdd8 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 29 Aug 2008 03:17:26 +0000 Subject: 220: Renamed Option2.validate_scalar() to Option2.__validate_scalar(); added Option2.normalize() method; added corresponding unit tests --- ipalib/public.py | 39 +++++++++++---- ipalib/tests/test_public.py | 116 +++++++++++++++++++++++++++++++++++--------- 2 files changed, 124 insertions(+), 31 deletions(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index e0d9e6d2..147596e1 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -86,9 +86,7 @@ class DefaultFrom(plugable.ReadOnly): class Option2(plugable.ReadOnly): def __init__(self, name, doc, type_, required=False, multivalue=False, - default=None, default_from=None, normalize=None, rules=tuple() - ): - + default=None, default_from=None, rules=tuple(), normalize=None): self.name = name self.doc = doc self.type = type_ @@ -100,7 +98,32 @@ class Option2(plugable.ReadOnly): self.rules = (type_.validate,) + rules lock(self) - def validate_scalar(self, value): + def convert(self, value): + if self.multivalue: + if type(value) in (tuple, list): + return tuple(self.type(v) for v in value) + return (self.type(value),) + return self.type(value) + + def __normalize_scalar(self, value): + if value is None: + return None + if type(value) is not self.type.type: + raise TypeError('need a %r; got %r' % (self.type.type, value)) + return self.__normalize(value) + + def normalize(self, value): + if self.__normalize is None: + return value + if self.multivalue: + if value is None: + return None + if type(value) is not tuple: + raise TypeError('multivalue must be a tuple; got %r' % value) + return tuple(self.__normalize_scalar(v) for v in value) + return self.__normalize_scalar(value) + + def __validate_scalar(self, value): for rule in self.rules: error = rule(value) if error is not None: @@ -109,13 +132,11 @@ class Option2(plugable.ReadOnly): def validate(self, value): if self.multivalue: if type(value) is not tuple: - raise TypeError( - 'when multivalue, value must be tuple; got %r' % value - ) + raise TypeError('multivalue must be a tuple; got %r' % value) for v in value: - self.validate_scalar(v) + self.__validate_scalar(v) else: - self.validate_scalar(value) + self.__validate_scalar(value) class Option(plugable.Plugin): diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index ed4c75e5..b34e4875 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -114,6 +114,9 @@ class test_Option2(ClassChecker): assert self.cls.__bases__ == (plugable.ReadOnly,) def test_init(self): + """ + Tests the `public.Option2.__init__` method. + """ name = 'sn', doc = 'Last Name', type_ = ipa_types.Unicode() @@ -128,8 +131,82 @@ class test_Option2(ClassChecker): assert read_only(o, 'default_from') is None assert read_only(o, 'rules') == (type_.validate,) + def test_convert(self): + name = 'sn' + doc = 'User last name' + type_ = ipa_types.Unicode() + class Hello(object): + def __unicode__(self): + return u'hello' + hello = Hello() + values = (u'hello', 'hello', hello) + # Test when multivalue=False: + o = self.cls(name, doc, type_) + for value in values: + new = o.convert(value) + assert new == u'hello' + assert type(new) is unicode + # Test when multivalue=True: + o = self.cls(name, doc, type_, multivalue=True) + for value in values: + for v in (value, (value,)): + new = o.convert(hello) + assert new == (u'hello',) + assert type(new) is tuple + + def test_normalize(self): + """ + Tests the `public.Option2.validate` method. + """ + name = 'sn' + doc = 'User last name' + t = ipa_types.Unicode() + callback = lambda value: value.lower() + orig = u'Hello World' + orig_str = str(orig) + norm = u'hello world' + tup_orig = (orig, norm, u'WONDERFUL!') + tup_norm = (norm, norm, u'wonderful!') + tup_str = (orig_str, orig) + all_values = (None, orig, orig_str, norm, tup_orig, tup_norm, tup_str) + + ## Scenario 1: multivalue=False, normalize=None + o = self.cls(name, doc, t) + for v in all_values: + # When normalize=None, value is returned, no type checking: + assert o.normalize(v) is v + + ## Scenario 2: multivalue=False, normalize=callback + o = self.cls(name, doc, t, normalize=callback) + assert o.normalize(None) is None + for v in (orig, norm): + assert o.normalize(v) == norm + for v in (orig_str, tup_orig, tup_norm, tup_str): # Not unicode + e = raises(TypeError, o.normalize, v) + assert str(e) == 'need a %r; got %r' % (unicode, v) + + ## Scenario 3: multivalue=True, normalize=None + o = self.cls(name, doc, t, multivalue=True) + for v in all_values: + # When normalize=None, value is returned, no type checking: + assert o.normalize(v) is v + + ## Scenario 4: multivalue=True, normalize=callback + o = self.cls(name, doc, t, multivalue=True, normalize=callback) + assert o.normalize(None) is None + for v in (tup_orig, tup_norm): + assert o.normalize(v) == tup_norm + for v in (orig, orig_str, norm): # Not tuple + e = raises(TypeError, o.normalize, v) + assert str(e) == 'multivalue must be a tuple; got %r' % v + for v in [tup_str, (norm, orig, orig_str)]: # Not unicode + e = raises(TypeError, o.normalize, v) + assert str(e) == 'need a %r; got %r' % (unicode, orig_str) + def test_validate(self): - # Constructor arguments + """ + Tests the `public.Option2.validate` method. + """ name = 'sn' doc = 'User last name' type_ = ipa_types.Unicode() @@ -137,36 +214,31 @@ class test_Option2(ClassChecker): if not value.islower(): return 'Must be lower case' my_rules = (case_rule,) - - # Some test values: okay = u'whatever' fail_case = u'Whatever' fail_type = 'whatever' - # Test validate() and validate_scalar() when multivalue=False: + ## Scenario 1: multivalue=False o = self.cls(name, doc, type_, rules=my_rules) assert o.rules == (type_.validate, case_rule) - for m in [o.validate, o.validate_scalar]: - # Test a valid value: - m(okay) - # Check that RuleError is raised with wrong case: - e = raises(errors.RuleError, m, fail_case) - assert e.name is name - assert e.value is fail_case - assert e.error == 'Must be lower case' - # Test a RuleError is raise with wrong type: - e = raises(errors.RuleError, m, fail_type) - assert e.name is name - assert e.value is fail_type - assert e.error == 'Must be a string' + # Test a valid value: + o.validate(okay) + # Check that RuleError is raised with wrong case: + e = raises(errors.RuleError, o.validate, fail_case) + assert e.name is name + assert e.value is fail_case + assert e.error == 'Must be lower case' + # Test a RuleError is raise with wrong type: + e = raises(errors.RuleError, o.validate, fail_type) + assert e.name is name + assert e.value is fail_type + assert e.error == 'Must be a string' - # Test validate() when multivalue=True: + ## Scenario 2: multivalue=True o = self.cls(name, doc, type_, multivalue=True, rules=my_rules) def check_type_error(value): e = raises(TypeError, o.validate, value) - assert str(e) == ( - 'when multivalue, value must be tuple; got %r' % value - ) + assert str(e) == 'multivalue must be a tuple; got %r' % value # Check a valid value: check_type_error(okay) o.validate((okay,)) @@ -177,7 +249,7 @@ class test_Option2(ClassChecker): assert e.name is name assert e.value is fail_case assert e.error == 'Must be lower case' - # Test a RuleError is raise with wrong type: + # Check that RuleError is raise with wrong type: check_type_error(fail_type) for value in [(okay, fail_type), (fail_type, okay)]: e = raises(errors.RuleError, o.validate, value) -- cgit From 8dc0e263dac8bf4074e48fcca593a00cadec03e0 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 29 Aug 2008 03:48:33 +0000 Subject: 221: Added errors.IPATypeError exception; added new test_errors.py module with corresponding unit tests --- ipalib/errors.py | 20 ++++++++++++++++++++ ipalib/tests/test_errors.py | 46 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 ipalib/tests/test_errors.py (limited to 'ipalib') diff --git a/ipalib/errors.py b/ipalib/errors.py index a25df091..70128f7c 100644 --- a/ipalib/errors.py +++ b/ipalib/errors.py @@ -21,6 +21,26 @@ All custom errors raised by `ipalib` package. """ + +class IPATypeError(TypeError): + """ + TypeError subclass with standard message for easier debugging. + + Also has two custom attributes: + + ``type`` - The type being tested against. + ``value`` - The offending value that caused the exception. + """ + + format = 'need a %r; got %r' + + def __init__(self, type_, value): + assert type(value) is not type, 'no error: %r, %r' % (type_, value) + self.type = type_ + self.value = value + TypeError.__init__(self, self.format % (self.type, self.value)) + + class IPAError(Exception): """ Use this base class for your custom IPA errors unless there is a diff --git a/ipalib/tests/test_errors.py b/ipalib/tests/test_errors.py new file mode 100644 index 00000000..bca8d3a8 --- /dev/null +++ b/ipalib/tests/test_errors.py @@ -0,0 +1,46 @@ +# Authors: +# Jason Gerard DeRose +# +# 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 + +""" +Unit tests for `ipalib.errors` module. +""" + +from tstutil import raises, getitem, no_set, no_del, read_only, ClassChecker +from ipalib import errors + + +class test_IPATypeError(ClassChecker): + """ + Tests the `errors.IPATypeError` exception. + """ + _cls = errors.IPATypeError + + def test_class(self): + assert self.cls.__bases__ == (TypeError,) + + def test_init(self): + """ + Tests the `errors.IPATypeError.__init__` method. + """ + t = unicode + v = 'hello' + e = self.cls(t, v) + assert e.type is t + assert e.value is v + assert str(e) == 'need a %r; got %r' % (t, v) -- cgit From 03daa91d1c9c355f5f964095371c81d73fb9e08a Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 29 Aug 2008 04:29:29 +0000 Subject: 222: Fixed broken assertion in IPATypeError; did more work on docstrings in same --- ipalib/errors.py | 8 ++++++-- ipalib/tests/test_errors.py | 19 ++++++++++++------- 2 files changed, 18 insertions(+), 9 deletions(-) (limited to 'ipalib') diff --git a/ipalib/errors.py b/ipalib/errors.py index 70128f7c..52225373 100644 --- a/ipalib/errors.py +++ b/ipalib/errors.py @@ -24,18 +24,22 @@ All custom errors raised by `ipalib` package. class IPATypeError(TypeError): """ - TypeError subclass with standard message for easier debugging. + A TypeError subclass with with a standard message. Also has two custom attributes: ``type`` - The type being tested against. ``value`` - The offending value that caused the exception. + + There is no edict that all TypeError should be raised with IPATypeError, + but when it fits, use it... it makes the unit tests faster to write and + the debugging easier to read. """ format = 'need a %r; got %r' def __init__(self, type_, value): - assert type(value) is not type, 'no error: %r, %r' % (type_, value) + assert type(value) is not type_, '%r is a %r' % (value, type_) self.type = type_ self.value = value TypeError.__init__(self, self.format % (self.type, self.value)) diff --git a/ipalib/tests/test_errors.py b/ipalib/tests/test_errors.py index bca8d3a8..48b1b8fe 100644 --- a/ipalib/tests/test_errors.py +++ b/ipalib/tests/test_errors.py @@ -21,7 +21,7 @@ Unit tests for `ipalib.errors` module. """ -from tstutil import raises, getitem, no_set, no_del, read_only, ClassChecker +from tstutil import raises, ClassChecker from ipalib import errors @@ -38,9 +38,14 @@ class test_IPATypeError(ClassChecker): """ Tests the `errors.IPATypeError.__init__` method. """ - t = unicode - v = 'hello' - e = self.cls(t, v) - assert e.type is t - assert e.value is v - assert str(e) == 'need a %r; got %r' % (t, v) + type_ = unicode + okay = 'hello' + e = self.cls(type_, okay) + assert e.type is type_ + assert e.value is okay + assert str(e) == 'need a %r; got %r' % (type_, okay) + + # Check that AssertionError is raised when type(value) is type_: + fail = u'hello' + e = raises(AssertionError, self.cls, type_, fail) + assert str(e) == '%r is a %r' % (fail, type_) -- cgit From 76b30dff15de9eb50f0d9cb00b6df18ecd91a8f5 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 29 Aug 2008 06:04:38 +0000 Subject: 223: IPATypeError takes as first argument, has attribute --- ipalib/errors.py | 29 +++++++++++++++++++++-------- ipalib/tests/test_errors.py | 29 +++++++++++++++++++++-------- 2 files changed, 42 insertions(+), 16 deletions(-) (limited to 'ipalib') diff --git a/ipalib/errors.py b/ipalib/errors.py index 52225373..f88fdd5a 100644 --- a/ipalib/errors.py +++ b/ipalib/errors.py @@ -24,25 +24,38 @@ All custom errors raised by `ipalib` package. class IPATypeError(TypeError): """ - A TypeError subclass with with a standard message. + A TypeError subclass with a helpful message format. - Also has two custom attributes: + IPATypeError has three custom instance attributes: - ``type`` - The type being tested against. - ``value`` - The offending value that caused the exception. + ``name`` - Name of the argument TypeError is being raised for. + + ``type`` - Type that the argument should be. + + ``value`` - Value (of incorrect type) supplied for the argument. There is no edict that all TypeError should be raised with IPATypeError, but when it fits, use it... it makes the unit tests faster to write and the debugging easier to read. + + Here is an example: + + >>> raise IPATypeError('islate', bool, '4 AM') + Traceback (most recent call last): + File "", line 1, in + IPATypeError: islate: need a ; got '4 AM' """ - format = 'need a %r; got %r' + format = '%s: need a %r; got %r' - def __init__(self, type_, value): - assert type(value) is not type_, '%r is a %r' % (value, type_) + def __init__(self, name, type_, value): + assert type(name) is str, self.format % ('name', str, name) + assert type(type_) is type, self.format % ('type_', type, type_) + assert type(value) is not type_, 'value: %r is a %r' % (value, type_) + self.name = name self.type = type_ self.value = value - TypeError.__init__(self, self.format % (self.type, self.value)) + TypeError.__init__(self, self.format % (name, type_, value)) class IPAError(Exception): diff --git a/ipalib/tests/test_errors.py b/ipalib/tests/test_errors.py index 48b1b8fe..2d5fc613 100644 --- a/ipalib/tests/test_errors.py +++ b/ipalib/tests/test_errors.py @@ -38,14 +38,27 @@ class test_IPATypeError(ClassChecker): """ Tests the `errors.IPATypeError.__init__` method. """ + format = '%s: need a %r; got %r' + name = 'message' type_ = unicode - okay = 'hello' - e = self.cls(type_, okay) + value = 'hello world' + e = self.cls(name, type_, value) + assert e.name is name assert e.type is type_ - assert e.value is okay - assert str(e) == 'need a %r; got %r' % (type_, okay) + assert e.value is value + assert str(e) == format % (name, type_, value) - # Check that AssertionError is raised when type(value) is type_: - fail = u'hello' - e = raises(AssertionError, self.cls, type_, fail) - assert str(e) == '%r is a %r' % (fail, type_) + # name not an str: + fail = 42 + e = raises(AssertionError, self.cls, fail, type_, value) + assert str(e) == format % ('name', str, fail) + + # type_ not a type: + fail = unicode() + e = raises(AssertionError, self.cls, name, fail, value) + assert str(e) == format % ('type_', type, fail) + + # type(value) is type_: + fail = u'how are you?' + e = raises(AssertionError, self.cls, name, type_, fail) + assert str(e) == 'value: %r is a %r' % (fail, type_) -- cgit From 44ff0b3d23c0473106a6c0da90cc8d80df98ee78 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 29 Aug 2008 07:05:06 +0000 Subject: 224: Reworked IPATypeError class into raise_TypeError function --- ipalib/errors.py | 41 +++++++++++++++++------------- ipalib/tests/test_errors.py | 62 ++++++++++++++++++++------------------------- 2 files changed, 50 insertions(+), 53 deletions(-) (limited to 'ipalib') diff --git a/ipalib/errors.py b/ipalib/errors.py index f88fdd5a..dea7cd73 100644 --- a/ipalib/errors.py +++ b/ipalib/errors.py @@ -22,40 +22,45 @@ All custom errors raised by `ipalib` package. """ -class IPATypeError(TypeError): +def raise_TypeError(name, type_, value): """ - A TypeError subclass with a helpful message format. + Raises a TypeError with a nicely formatted message and helpful attributes. - IPATypeError has three custom instance attributes: + The TypeError raised will have three custom attributes: - ``name`` - Name of the argument TypeError is being raised for. + ``name`` - The name (identifier) of the argument in question. - ``type`` - Type that the argument should be. + ``type`` - The type expected for the arguement. - ``value`` - Value (of incorrect type) supplied for the argument. + ``value`` - The value (of incorrect type) revieved for argument. - There is no edict that all TypeError should be raised with IPATypeError, + There is no edict that all TypeError should be raised with raise_TypeError, but when it fits, use it... it makes the unit tests faster to write and the debugging easier to read. Here is an example: - >>> raise IPATypeError('islate', bool, '4 AM') + >>> raise_TypeError('message', str, u'Hello.') Traceback (most recent call last): File "", line 1, in - IPATypeError: islate: need a ; got '4 AM' + File "/home/jderose/projects/freeipa2/ipalib/errors.py", line 61, in raise_TypeError + raise e + TypeError: message: need a ; got u'Hello.' + + :param name: The name (identifier) of the argument in question. + :param type_: The type expected for the arguement. + :param value: The value (of incorrect type) revieved for argument. """ format = '%s: need a %r; got %r' - - def __init__(self, name, type_, value): - assert type(name) is str, self.format % ('name', str, name) - assert type(type_) is type, self.format % ('type_', type, type_) - assert type(value) is not type_, 'value: %r is a %r' % (value, type_) - self.name = name - self.type = type_ - self.value = value - TypeError.__init__(self, self.format % (name, type_, value)) + assert type(name) is str, format % ('name', str, name) + assert type(type_) is type, format % ('type_', type, type_) + assert type(value) is not type_, 'value: %r is a %r' % (value, type_) + e = TypeError(format % (name, type_, value)) + setattr(e, 'name', name) + setattr(e, 'type', type_) + setattr(e, 'value', value) + raise e class IPAError(Exception): diff --git a/ipalib/tests/test_errors.py b/ipalib/tests/test_errors.py index 2d5fc613..730502c5 100644 --- a/ipalib/tests/test_errors.py +++ b/ipalib/tests/test_errors.py @@ -25,40 +25,32 @@ from tstutil import raises, ClassChecker from ipalib import errors -class test_IPATypeError(ClassChecker): +def test_raise_TypeError(): """ - Tests the `errors.IPATypeError` exception. + Tests the `errors.raise_TypeError` function. """ - _cls = errors.IPATypeError - - def test_class(self): - assert self.cls.__bases__ == (TypeError,) - - def test_init(self): - """ - Tests the `errors.IPATypeError.__init__` method. - """ - format = '%s: need a %r; got %r' - name = 'message' - type_ = unicode - value = 'hello world' - e = self.cls(name, type_, value) - assert e.name is name - assert e.type is type_ - assert e.value is value - assert str(e) == format % (name, type_, value) - - # name not an str: - fail = 42 - e = raises(AssertionError, self.cls, fail, type_, value) - assert str(e) == format % ('name', str, fail) - - # type_ not a type: - fail = unicode() - e = raises(AssertionError, self.cls, name, fail, value) - assert str(e) == format % ('type_', type, fail) - - # type(value) is type_: - fail = u'how are you?' - e = raises(AssertionError, self.cls, name, type_, fail) - assert str(e) == 'value: %r is a %r' % (fail, type_) + f = errors.raise_TypeError + format = '%s: need a %r; got %r' + name = 'message' + type_ = unicode + value = 'Hello.' + e = raises(TypeError, f, name, type_, value) + assert e.name is name + assert e.type is type_ + assert e.value is value + assert str(e) == format % (name, type_, value) + + # name not an str: + fail = 42 + e = raises(AssertionError, f, fail, type_, value) + assert str(e) == format % ('name', str, fail) + + # type_ not a type: + fail = unicode() + e = raises(AssertionError, f, name, fail, value) + assert str(e) == format % ('type_', type, fail) + + # type(value) is type_: + fail = u'How are you?' + e = raises(AssertionError, f, name, type_, fail) + assert str(e) == 'value: %r is a %r' % (fail, type_) -- cgit From 2fa8d3be74ca45ee5989dd53b7fb818b21d23680 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 29 Aug 2008 23:53:04 +0000 Subject: 225: Added errors.check_type() and errors.check_isinstance() functions; added corresponding unit tests --- ipalib/errors.py | 36 ++++++++++++++----- ipalib/tests/test_errors.py | 88 ++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 108 insertions(+), 16 deletions(-) (limited to 'ipalib') diff --git a/ipalib/errors.py b/ipalib/errors.py index dea7cd73..1c109ed6 100644 --- a/ipalib/errors.py +++ b/ipalib/errors.py @@ -21,6 +21,7 @@ All custom errors raised by `ipalib` package. """ +TYPE_FORMAT = '%s: need a %r; got %r' def raise_TypeError(name, type_, value): """ @@ -30,9 +31,9 @@ def raise_TypeError(name, type_, value): ``name`` - The name (identifier) of the argument in question. - ``type`` - The type expected for the arguement. + ``type`` - The type expected for the argument. - ``value`` - The value (of incorrect type) revieved for argument. + ``value`` - The value (of incorrect type) passed as argument. There is no edict that all TypeError should be raised with raise_TypeError, but when it fits, use it... it makes the unit tests faster to write and @@ -48,21 +49,40 @@ def raise_TypeError(name, type_, value): TypeError: message: need a ; got u'Hello.' :param name: The name (identifier) of the argument in question. - :param type_: The type expected for the arguement. - :param value: The value (of incorrect type) revieved for argument. + :param type_: The type expected for the argument. + :param value: The value (of incorrect type) passed argument. """ - format = '%s: need a %r; got %r' - assert type(name) is str, format % ('name', str, name) - assert type(type_) is type, format % ('type_', type, type_) + assert type(name) is str, TYPE_FORMAT % ('name', str, name) + assert type(type_) is type, TYPE_FORMAT % ('type_', type, type_) assert type(value) is not type_, 'value: %r is a %r' % (value, type_) - e = TypeError(format % (name, type_, value)) + e = TypeError(TYPE_FORMAT % (name, type_, value)) setattr(e, 'name', name) setattr(e, 'type', type_) setattr(e, 'value', value) raise e +def check_type(name, type_, value, allow_None=False): + assert type(name) is str, TYPE_FORMAT % ('name', str, name) + assert type(type_) is type, TYPE_FORMAT % ('type_', type, type_) + assert type(allow_None) is bool, TYPE_FORMAT % ('allow_None', bool, allow_None) + if value is None and allow_None: + return + if type(value) is not type_: + raise_TypeError(name, type_, value) + + +def check_isinstance(name, type_, value, allow_None=False): + assert type(name) is str, TYPE_FORMAT % ('name', str, name) + assert type(type_) is type, TYPE_FORMAT % ('type_', type, type_) + assert type(allow_None) is bool, TYPE_FORMAT % ('allow_None', bool, allow_None) + if value is None and allow_None: + return + if not isinstance(value, type_): + raise_TypeError(name, type_, value) + + class IPAError(Exception): """ Use this base class for your custom IPA errors unless there is a diff --git a/ipalib/tests/test_errors.py b/ipalib/tests/test_errors.py index 730502c5..9d08b5a7 100644 --- a/ipalib/tests/test_errors.py +++ b/ipalib/tests/test_errors.py @@ -25,32 +25,104 @@ from tstutil import raises, ClassChecker from ipalib import errors +type_format = '%s: need a %r; got %r' + +def check_TypeError(f, name, type_, value, **kw): + e = raises(TypeError, f, name, type_, value, **kw) + assert e.name is name + assert e.type is type_ + assert e.value is value + assert str(e) == type_format % (name, type_, value) + + def test_raise_TypeError(): """ Tests the `errors.raise_TypeError` function. """ f = errors.raise_TypeError - format = '%s: need a %r; got %r' name = 'message' type_ = unicode value = 'Hello.' - e = raises(TypeError, f, name, type_, value) - assert e.name is name - assert e.type is type_ - assert e.value is value - assert str(e) == format % (name, type_, value) + + check_TypeError(f, name, type_, value) # name not an str: fail = 42 e = raises(AssertionError, f, fail, type_, value) - assert str(e) == format % ('name', str, fail) + assert str(e) == type_format % ('name', str, fail) # type_ not a type: fail = unicode() e = raises(AssertionError, f, name, fail, value) - assert str(e) == format % ('type_', type, fail) + assert str(e) == type_format % ('type_', type, fail) # type(value) is type_: fail = u'How are you?' e = raises(AssertionError, f, name, type_, fail) assert str(e) == 'value: %r is a %r' % (fail, type_) + + +def test_check_type(): + """ + Tests the `errors.check_type` function. + """ + f = errors.check_type + name = 'greeting' + value = 'How are you?' + + # Should pass: + f(name, str, value) + f(name, str, None, allow_None=True) + + # Should raise TypeError + check_TypeError(f, name, str, None) + check_TypeError(f, name, basestring, value) + check_TypeError(f, name, unicode, value) + + # name not an str + fail = unicode(name) + e = raises(AssertionError, f, fail, str, value) + assert str(e) == type_format % ('name', str, fail) + + # type_ not a type: + fail = 42 + e = raises(AssertionError, f, name, fail, value) + assert str(e) == type_format % ('type_', type, fail) + + # allow_None not a bool: + fail = 0 + e = raises(AssertionError, f, name, str, value, allow_None=fail) + assert str(e) == type_format % ('allow_None', bool, fail) + + +def test_check_isinstance(): + """ + Tests the `errors.check_isinstance` function. + """ + f = errors.check_isinstance + name = 'greeting' + value = 'How are you?' + + # Should pass: + f(name, str, value) + f(name, basestring, value) + f(name, str, None, allow_None=True) + + # Should raise TypeError + check_TypeError(f, name, str, None) + check_TypeError(f, name, unicode, value) + + # name not an str + fail = unicode(name) + e = raises(AssertionError, f, fail, str, value) + assert str(e) == type_format % ('name', str, fail) + + # type_ not a type: + fail = 42 + e = raises(AssertionError, f, name, fail, value) + assert str(e) == type_format % ('type_', type, fail) + + # allow_None not a bool: + fail = 0 + e = raises(AssertionError, f, name, str, value, allow_None=fail) + assert str(e) == type_format % ('allow_None', bool, fail) -- cgit From 5af91df9a58c5066cbd526561886023d5edbfc0f Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 2 Sep 2008 15:15:03 +0000 Subject: 226: check_type() and check_isinstance() now return the value; updated corresponding unit tests --- ipalib/errors.py | 2 ++ ipalib/tests/test_errors.py | 10 +++++----- 2 files changed, 7 insertions(+), 5 deletions(-) (limited to 'ipalib') diff --git a/ipalib/errors.py b/ipalib/errors.py index 1c109ed6..8c1df455 100644 --- a/ipalib/errors.py +++ b/ipalib/errors.py @@ -71,6 +71,7 @@ def check_type(name, type_, value, allow_None=False): return if type(value) is not type_: raise_TypeError(name, type_, value) + return value def check_isinstance(name, type_, value, allow_None=False): @@ -81,6 +82,7 @@ def check_isinstance(name, type_, value, allow_None=False): return if not isinstance(value, type_): raise_TypeError(name, type_, value) + return value class IPAError(Exception): diff --git a/ipalib/tests/test_errors.py b/ipalib/tests/test_errors.py index 9d08b5a7..a68b1174 100644 --- a/ipalib/tests/test_errors.py +++ b/ipalib/tests/test_errors.py @@ -71,8 +71,8 @@ def test_check_type(): value = 'How are you?' # Should pass: - f(name, str, value) - f(name, str, None, allow_None=True) + assert value is f(name, str, value) + assert None is f(name, str, None, allow_None=True) # Should raise TypeError check_TypeError(f, name, str, None) @@ -104,9 +104,9 @@ def test_check_isinstance(): value = 'How are you?' # Should pass: - f(name, str, value) - f(name, basestring, value) - f(name, str, None, allow_None=True) + assert value is f(name, str, value) + assert value is f(name, basestring, value) + assert None is f(name, str, None, allow_None=True) # Should raise TypeError check_TypeError(f, name, str, None) -- cgit From 6697b955eea6c5170cd68fef130d415ef3fa69cc Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 2 Sep 2008 16:42:39 +0000 Subject: 227: check_type() and check_isinstance() now take arguments in (value, type_, name) order so the first two match the built-in isinstance() call signature --- ipalib/errors.py | 26 ++++++------ ipalib/tests/test_errors.py | 97 +++++++++++++++++++++++---------------------- ipalib/tests/test_public.py | 4 +- 3 files changed, 65 insertions(+), 62 deletions(-) (limited to 'ipalib') diff --git a/ipalib/errors.py b/ipalib/errors.py index 8c1df455..1b556c33 100644 --- a/ipalib/errors.py +++ b/ipalib/errors.py @@ -23,17 +23,17 @@ All custom errors raised by `ipalib` package. TYPE_FORMAT = '%s: need a %r; got %r' -def raise_TypeError(name, type_, value): +def raise_TypeError(value, type_, name): """ Raises a TypeError with a nicely formatted message and helpful attributes. The TypeError raised will have three custom attributes: - ``name`` - The name (identifier) of the argument in question. + ``value`` - The value (of incorrect type) passed as argument. ``type`` - The type expected for the argument. - ``value`` - The value (of incorrect type) passed as argument. + ``name`` - The name (identifier) of the argument in question. There is no edict that all TypeError should be raised with raise_TypeError, but when it fits, use it... it makes the unit tests faster to write and @@ -48,40 +48,40 @@ def raise_TypeError(name, type_, value): raise e TypeError: message: need a ; got u'Hello.' - :param name: The name (identifier) of the argument in question. + :param value: The value (of incorrect type) passed as argument. :param type_: The type expected for the argument. - :param value: The value (of incorrect type) passed argument. + :param name: The name (identifier) of the argument in question. """ - assert type(name) is str, TYPE_FORMAT % ('name', str, name) assert type(type_) is type, TYPE_FORMAT % ('type_', type, type_) assert type(value) is not type_, 'value: %r is a %r' % (value, type_) + assert type(name) is str, TYPE_FORMAT % ('name', str, name) e = TypeError(TYPE_FORMAT % (name, type_, value)) - setattr(e, 'name', name) - setattr(e, 'type', type_) setattr(e, 'value', value) + setattr(e, 'type', type_) + setattr(e, 'name', name) raise e -def check_type(name, type_, value, allow_None=False): +def check_type(value, type_, name, allow_None=False): assert type(name) is str, TYPE_FORMAT % ('name', str, name) assert type(type_) is type, TYPE_FORMAT % ('type_', type, type_) assert type(allow_None) is bool, TYPE_FORMAT % ('allow_None', bool, allow_None) if value is None and allow_None: return if type(value) is not type_: - raise_TypeError(name, type_, value) + raise_TypeError(value, type_, name) return value -def check_isinstance(name, type_, value, allow_None=False): - assert type(name) is str, TYPE_FORMAT % ('name', str, name) +def check_isinstance(value, type_, name, allow_None=False): assert type(type_) is type, TYPE_FORMAT % ('type_', type, type_) + assert type(name) is str, TYPE_FORMAT % ('name', str, name) assert type(allow_None) is bool, TYPE_FORMAT % ('allow_None', bool, allow_None) if value is None and allow_None: return if not isinstance(value, type_): - raise_TypeError(name, type_, value) + raise_TypeError(value, type_, name) return value diff --git a/ipalib/tests/test_errors.py b/ipalib/tests/test_errors.py index a68b1174..6ea0e311 100644 --- a/ipalib/tests/test_errors.py +++ b/ipalib/tests/test_errors.py @@ -27,11 +27,12 @@ from ipalib import errors type_format = '%s: need a %r; got %r' -def check_TypeError(f, name, type_, value, **kw): - e = raises(TypeError, f, name, type_, value, **kw) - assert e.name is name - assert e.type is type_ + +def check_TypeError(f, value, type_, name, **kw): + e = raises(TypeError, f, value, type_, name, **kw) assert e.value is value + assert e.type is type_ + assert e.name is name assert str(e) == type_format % (name, type_, value) @@ -40,26 +41,26 @@ def test_raise_TypeError(): Tests the `errors.raise_TypeError` function. """ f = errors.raise_TypeError - name = 'message' - type_ = unicode value = 'Hello.' + type_ = unicode + name = 'message' - check_TypeError(f, name, type_, value) + check_TypeError(f, value, type_, name) - # name not an str: - fail = 42 - e = raises(AssertionError, f, fail, type_, value) - assert str(e) == type_format % ('name', str, fail) + # name not an str + fail_name = 42 + e = raises(AssertionError, f, value, type_, fail_name) + assert str(e) == type_format % ('name', str, fail_name), str(e) # type_ not a type: - fail = unicode() - e = raises(AssertionError, f, name, fail, value) - assert str(e) == type_format % ('type_', type, fail) + fail_type = unicode() + e = raises(AssertionError, f, value, fail_type, name) + assert str(e) == type_format % ('type_', type, fail_type) # type(value) is type_: - fail = u'How are you?' - e = raises(AssertionError, f, name, type_, fail) - assert str(e) == 'value: %r is a %r' % (fail, type_) + fail_value = u'How are you?' + e = raises(AssertionError, f, fail_value, type_, name) + assert str(e) == 'value: %r is a %r' % (fail_value, type_) def test_check_type(): @@ -67,32 +68,33 @@ def test_check_type(): Tests the `errors.check_type` function. """ f = errors.check_type - name = 'greeting' value = 'How are you?' + type_ = str + name = 'greeting' # Should pass: - assert value is f(name, str, value) - assert None is f(name, str, None, allow_None=True) + assert value is f(value, type_, name) + assert None is f(None, type_, name, allow_None=True) # Should raise TypeError - check_TypeError(f, name, str, None) - check_TypeError(f, name, basestring, value) - check_TypeError(f, name, unicode, value) + check_TypeError(f, None, type_, name) + check_TypeError(f, value, basestring, name) + check_TypeError(f, value, unicode, name) # name not an str - fail = unicode(name) - e = raises(AssertionError, f, fail, str, value) - assert str(e) == type_format % ('name', str, fail) + fail_name = unicode(name) + e = raises(AssertionError, f, value, type_, fail_name) + assert str(e) == type_format % ('name', str, fail_name) # type_ not a type: - fail = 42 - e = raises(AssertionError, f, name, fail, value) - assert str(e) == type_format % ('type_', type, fail) + fail_type = 42 + e = raises(AssertionError, f, value, fail_type, name) + assert str(e) == type_format % ('type_', type, fail_type) # allow_None not a bool: - fail = 0 - e = raises(AssertionError, f, name, str, value, allow_None=fail) - assert str(e) == type_format % ('allow_None', bool, fail) + fail_bool = 0 + e = raises(AssertionError, f, value, type_, name, allow_None=fail_bool) + assert str(e) == type_format % ('allow_None', bool, fail_bool) def test_check_isinstance(): @@ -100,29 +102,30 @@ def test_check_isinstance(): Tests the `errors.check_isinstance` function. """ f = errors.check_isinstance - name = 'greeting' value = 'How are you?' + type_ = str + name = 'greeting' # Should pass: - assert value is f(name, str, value) - assert value is f(name, basestring, value) - assert None is f(name, str, None, allow_None=True) + assert value is f(value, type_, name) + assert value is f(value, basestring, name) + assert None is f(None, type_, name, allow_None=True) # Should raise TypeError - check_TypeError(f, name, str, None) - check_TypeError(f, name, unicode, value) + check_TypeError(f, None, type_, name) + check_TypeError(f, value, unicode, name) # name not an str - fail = unicode(name) - e = raises(AssertionError, f, fail, str, value) - assert str(e) == type_format % ('name', str, fail) + fail_name = unicode(name) + e = raises(AssertionError, f, value, type_, fail_name) + assert str(e) == type_format % ('name', str, fail_name) # type_ not a type: - fail = 42 - e = raises(AssertionError, f, name, fail, value) - assert str(e) == type_format % ('type_', type, fail) + fail_type = 42 + e = raises(AssertionError, f, value, fail_type, name) + assert str(e) == type_format % ('type_', type, fail_type) # allow_None not a bool: - fail = 0 - e = raises(AssertionError, f, name, str, value, allow_None=fail) - assert str(e) == type_format % ('allow_None', bool, fail) + fail_bool = 0 + e = raises(AssertionError, f, value, type_, name, allow_None=fail_bool) + assert str(e) == type_format % ('allow_None', bool, fail_bool) diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index b34e4875..84be3c6d 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -117,8 +117,8 @@ class test_Option2(ClassChecker): """ Tests the `public.Option2.__init__` method. """ - name = 'sn', - doc = 'Last Name', + name = 'sn' + doc = 'Last Name' type_ = ipa_types.Unicode() o = self.cls(name, doc, type_) assert o.__islocked__() is True -- cgit From 8b7fe7139dc47a421dd34376374a0ed06dc73f39 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 2 Sep 2008 17:29:01 +0000 Subject: 228: plugable.check_name() now uses errors.check_type() --- ipalib/plugable.py | 3 ++- ipalib/tests/test_plugable.py | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 811a5527..9880b0a0 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -28,6 +28,7 @@ http://docs.python.org/ref/sequence-types.html import re import inspect import errors +from errors import check_type, check_isinstance class ReadOnly(object): @@ -466,7 +467,7 @@ def check_name(name): :param name: Identifier to test. """ - assert type(name) is str, 'must be %r' % str + check_type(name, str, 'name') regex = r'^[a-z][_a-z0-9]*[a-z0-9]$' if re.match(regex, name) is None: raise errors.NameSpaceError(name, regex) diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index 44067b80..ec33989d 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -549,6 +549,8 @@ def test_check_name(): ] for name in okay: assert name is f(name) + e = raises(TypeError, f, unicode(name)) + assert str(e) == errors.TYPE_FORMAT % ('name', str, unicode(name)) for name in nope: raises(errors.NameSpaceError, f, name) for name in okay: -- cgit From f2da06c5cf33a766bf6051acfa772f2dbd4237d8 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 2 Sep 2008 17:41:55 +0000 Subject: 229: Option2.__init__() now uses check_type() --- ipalib/public.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index 147596e1..894a56f3 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -25,8 +25,9 @@ and UI all use. import re import inspect import plugable -from plugable import lock +from plugable import lock, check_name import errors +from errors import check_type, check_isinstance import ipa_types @@ -87,13 +88,14 @@ class DefaultFrom(plugable.ReadOnly): class Option2(plugable.ReadOnly): def __init__(self, name, doc, type_, required=False, multivalue=False, default=None, default_from=None, rules=tuple(), normalize=None): - self.name = name - self.doc = doc - self.type = type_ - self.required = required - self.multivalue = multivalue + self.name = check_name(name) + self.doc = check_type(doc, str, 'doc') + self.type = check_isinstance(type_, ipa_types.Type, 'type_') + self.required = check_type(required, bool, 'required') + self.multivalue = check_type(multivalue, bool, 'multivalue') self.default = default - self.default_from = default_from + self.default_from = check_type(default_from, + DefaultFrom, 'default_from', allow_None=True) self.__normalize = normalize self.rules = (type_.validate,) + rules lock(self) -- cgit From bc08225dcd719eba0134e8a59ea7932fdea8513d Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 2 Sep 2008 17:44:07 +0000 Subject: 230: Renamed allow_None kwarg to allow_none --- ipalib/errors.py | 12 ++++++------ ipalib/public.py | 2 +- ipalib/tests/test_errors.py | 16 ++++++++-------- 3 files changed, 15 insertions(+), 15 deletions(-) (limited to 'ipalib') diff --git a/ipalib/errors.py b/ipalib/errors.py index 1b556c33..d68bac40 100644 --- a/ipalib/errors.py +++ b/ipalib/errors.py @@ -63,22 +63,22 @@ def raise_TypeError(value, type_, name): raise e -def check_type(value, type_, name, allow_None=False): +def check_type(value, type_, name, allow_none=False): assert type(name) is str, TYPE_FORMAT % ('name', str, name) assert type(type_) is type, TYPE_FORMAT % ('type_', type, type_) - assert type(allow_None) is bool, TYPE_FORMAT % ('allow_None', bool, allow_None) - if value is None and allow_None: + assert type(allow_none) is bool, TYPE_FORMAT % ('allow_none', bool, allow_none) + if value is None and allow_none: return if type(value) is not type_: raise_TypeError(value, type_, name) return value -def check_isinstance(value, type_, name, allow_None=False): +def check_isinstance(value, type_, name, allow_none=False): assert type(type_) is type, TYPE_FORMAT % ('type_', type, type_) assert type(name) is str, TYPE_FORMAT % ('name', str, name) - assert type(allow_None) is bool, TYPE_FORMAT % ('allow_None', bool, allow_None) - if value is None and allow_None: + assert type(allow_none) is bool, TYPE_FORMAT % ('allow_none', bool, allow_none) + if value is None and allow_none: return if not isinstance(value, type_): raise_TypeError(value, type_, name) diff --git a/ipalib/public.py b/ipalib/public.py index 894a56f3..ea9a06a3 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -95,7 +95,7 @@ class Option2(plugable.ReadOnly): self.multivalue = check_type(multivalue, bool, 'multivalue') self.default = default self.default_from = check_type(default_from, - DefaultFrom, 'default_from', allow_None=True) + DefaultFrom, 'default_from', allow_none=True) self.__normalize = normalize self.rules = (type_.validate,) + rules lock(self) diff --git a/ipalib/tests/test_errors.py b/ipalib/tests/test_errors.py index 6ea0e311..34b195e8 100644 --- a/ipalib/tests/test_errors.py +++ b/ipalib/tests/test_errors.py @@ -74,7 +74,7 @@ def test_check_type(): # Should pass: assert value is f(value, type_, name) - assert None is f(None, type_, name, allow_None=True) + assert None is f(None, type_, name, allow_none=True) # Should raise TypeError check_TypeError(f, None, type_, name) @@ -91,10 +91,10 @@ def test_check_type(): e = raises(AssertionError, f, value, fail_type, name) assert str(e) == type_format % ('type_', type, fail_type) - # allow_None not a bool: + # allow_none not a bool: fail_bool = 0 - e = raises(AssertionError, f, value, type_, name, allow_None=fail_bool) - assert str(e) == type_format % ('allow_None', bool, fail_bool) + e = raises(AssertionError, f, value, type_, name, allow_none=fail_bool) + assert str(e) == type_format % ('allow_none', bool, fail_bool) def test_check_isinstance(): @@ -109,7 +109,7 @@ def test_check_isinstance(): # Should pass: assert value is f(value, type_, name) assert value is f(value, basestring, name) - assert None is f(None, type_, name, allow_None=True) + assert None is f(None, type_, name, allow_none=True) # Should raise TypeError check_TypeError(f, None, type_, name) @@ -125,7 +125,7 @@ def test_check_isinstance(): e = raises(AssertionError, f, value, fail_type, name) assert str(e) == type_format % ('type_', type, fail_type) - # allow_None not a bool: + # allow_none not a bool: fail_bool = 0 - e = raises(AssertionError, f, value, type_, name, allow_None=fail_bool) - assert str(e) == type_format % ('allow_None', bool, fail_bool) + e = raises(AssertionError, f, value, type_, name, allow_none=fail_bool) + assert str(e) == type_format % ('allow_none', bool, fail_bool) -- cgit From 85f7a08e5d11b3d09b6c36b7a683f39846f8821e Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 2 Sep 2008 19:05:10 +0000 Subject: 231: Added Option2.get_default() method; added corresponding unit tests --- ipalib/public.py | 16 ++++++++++++++-- ipalib/tests/test_public.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index ea9a06a3..cf95af7c 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -86,8 +86,13 @@ class DefaultFrom(plugable.ReadOnly): class Option2(plugable.ReadOnly): - def __init__(self, name, doc, type_, required=False, multivalue=False, - default=None, default_from=None, rules=tuple(), normalize=None): + def __init__(self, name, doc, type_, + required=False, + multivalue=False, + default=None, + default_from=None, + rules=tuple(), + normalize=None): self.name = check_name(name) self.doc = check_type(doc, str, 'doc') self.type = check_isinstance(type_, ipa_types.Type, 'type_') @@ -140,6 +145,13 @@ class Option2(plugable.ReadOnly): else: self.__validate_scalar(value) + def get_default(self, **kw): + if self.default_from is not None: + default = self.default_from(**kw) + if default is not None: + return self.convert(default) + return self.convert(self.default) + class Option(plugable.Plugin): """ diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index 84be3c6d..56da573a 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -257,6 +257,40 @@ class test_Option2(ClassChecker): assert e.value is fail_type assert e.error == 'Must be a string' + def test_get_default(self): + """ + Tests the `public.Option2.get_default` method. + """ + name = 'greeting' + doc = 'User greeting' + type_ = ipa_types.Unicode() + default = u'Hello, world!' + default_from = public.DefaultFrom( + lambda first, last: u'Hello, %s %s!' % (first, last), + 'first', 'last' + ) + + # Scenario 1: multivalue=False + o = self.cls(name, doc, type_, + default=default, + default_from=default_from, + ) + assert o.default is default + assert o.default_from is default_from + assert o.get_default() == default + assert o.get_default(first='John', last='Doe') == 'Hello, John Doe!' + + # Scenario 2: multivalue=True + o = self.cls(name, doc, type_, + default=default, + default_from=default_from, + multivalue=True, + ) + assert o.default is default + assert o.default_from is default_from + assert o.get_default() == (default,) + assert o.get_default(first='John', last='Doe') == ('Hello, John Doe!',) + class test_Option(ClassChecker): """ -- cgit From adf8b9b2d8ac4d5a73ed453fb1c05c28b3efc7b3 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 2 Sep 2008 19:29:00 +0000 Subject: 232: Added Option2.get_values() method; added corresponding unit tests --- ipalib/public.py | 5 +++++ ipalib/tests/test_public.py | 12 ++++++++++++ 2 files changed, 17 insertions(+) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index cf95af7c..c01a88d9 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -152,6 +152,11 @@ class Option2(plugable.ReadOnly): return self.convert(default) return self.convert(self.default) + def get_values(self): + if self.type.name in ('Enum', 'CallbackEnum'): + return self.type.values + return tuple() + class Option(plugable.Plugin): """ diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index 56da573a..be1b9158 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -291,6 +291,18 @@ class test_Option2(ClassChecker): assert o.get_default() == (default,) assert o.get_default(first='John', last='Doe') == ('Hello, John Doe!',) + def test_get_default(self): + """ + Tests the `public.Option2.get_values` method. + """ + name = 'status' + doc = 'Account status' + values = (u'Active', u'Inactive') + o = self.cls(name, doc, ipa_types.Unicode()) + assert o.get_values() == tuple() + o = self.cls(name, doc, ipa_types.Enum(*values)) + assert o.get_values() == values + class test_Option(ClassChecker): """ -- cgit From 2784847b731232c80c9085325b09f1aa72c6afcd Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 2 Sep 2008 20:16:34 +0000 Subject: 233: Removed public.Option class; removed corresponding unit tests --- ipalib/public.py | 126 ++++++++++++-------------------------------- ipalib/tests/test_public.py | 114 ++------------------------------------- 2 files changed, 38 insertions(+), 202 deletions(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index c01a88d9..102f36b9 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -158,95 +158,6 @@ class Option2(plugable.ReadOnly): return tuple() -class Option(plugable.Plugin): - """ - The Option class represents a kw argument from a `Command`. - """ - - __public__ = frozenset(( - 'normalize', - 'get_default', - 'validate', - 'type', - 'required', - 'default', - 'default_from', - )) - __rules = None - type = unicode - required = False - default = None - default_from = None - - - def normalize(self, value): - """ - Returns the normalized form of `value`. If `value` cannot be - normalized, NormalizationError is raised, which is a subclass of - ValidationError. - - The base class implementation only does type coercion, but subclasses - might do other normalization (e.g., a unicode option might strip - leading and trailing white-space). - """ - try: - return self.type(value) - except (TypeError, ValueError): - raise errors.NormalizationError( - self.__class__.__name__, value, self.type - ) - - def validate(self, value): - """ - Calls each validation rule and if any rule fails, raises RuleError, - which is a subclass of ValidationError. - """ - for rule in self.rules: - msg = rule(value) - if msg is not None: - raise errors.RuleError( - self.__class__.__name__, - value, - rule, - msg, - ) - - def __get_rules(self): - """ - Returns the tuple of rule methods used for input validation. This - tuple is lazily initialized the first time the property is accessed. - """ - if self.__rules is None: - rules = tuple(sorted( - self.__rules_iter(), - key=lambda f: getattr(f, '__name__'), - )) - object.__setattr__(self, '_Option__rules', rules) - return self.__rules - rules = property(__get_rules) - - def __rules_iter(self): - """ - Iterates through the attributes in this instance to retrieve the - methods implementing validation rules. - """ - for name in dir(self.__class__): - if name.startswith('_'): - continue - base_attr = getattr(self.__class__, name) - if is_rule(base_attr): - attr = getattr(self, name) - if is_rule(attr): - yield attr - - def get_default(self, **kw): - if type(self.default_from) is DefaultFrom: - default = self.default_from(**kw) - if default is not None: - return default - return self.default - - class Command(plugable.Plugin): __public__ = frozenset(( 'normalize', @@ -428,8 +339,37 @@ class Method(Attribute, Command): yield proxy -class Property(Attribute, Option): - __public__ = Attribute.__public__.union(Option.__public__) +class Property(Attribute): + __public__ = frozenset(( + 'rules', + 'option', + 'type', + )).union(Attribute.__public__) - def get_doc(self, _): - return _('Property doc') + def __get_rules(self): + """ + Returns the tuple of rule methods used for input validation. This + tuple is lazily initialized the first time the property is accessed. + """ + if self.__rules is None: + rules = tuple(sorted( + self.__rules_iter(), + key=lambda f: getattr(f, '__name__'), + )) + object.__setattr__(self, '_Property__rules', rules) + return self.__rules + rules = property(__get_rules) + + def __rules_iter(self): + """ + Iterates through the attributes in this instance to retrieve the + methods implementing validation rules. + """ + for name in dir(self.__class__): + if name.startswith('_'): + continue + base_attr = getattr(self.__class__, name) + if is_rule(base_attr): + attr = getattr(self, name) + if is_rule(attr): + yield attr diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index be1b9158..72b07c4d 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -304,112 +304,6 @@ class test_Option2(ClassChecker): assert o.get_values() == values -class test_Option(ClassChecker): - """ - Tests the `public.Option` class. - """ - _cls = public.Option - - def get_subcls(self): - rule = public.rule - class int_opt(self.cls): - type = int - @rule - def rule_0(self, value): - if value == 0: - return 'cannot be 0' - @rule - def rule_1(self, value): - if value == 1: - return 'cannot be 1' - @rule - def rule_2(self, value): - if value == 2: - return 'cannot be 2' - return int_opt - - def test_class(self): - """ - Perform some tests on the class (not an instance). - """ - assert self.cls.__bases__ == (plugable.Plugin,) - assert type(self.cls.rules) is property - - def test_normalize(self): - """ - Tests the `public.Option.normalize` method. - """ - assert 'normalize' in self.cls.__public__ - o = self.subcls() - # Test with values that can't be converted: - nope = ( - '7.0' - 'whatever', - object, - None, - ) - for val in nope: - e = raises(errors.NormalizationError, o.normalize, val) - assert isinstance(e, errors.ValidationError) - assert e.name == 'int_opt' - assert e.value == val - assert e.error == "not " - assert e.type is int - # Test with values that can be converted: - okay = ( - 7, - 7.0, - 7.2, - 7L, - '7', - ' 7 ', - ) - for val in okay: - assert o.normalize(val) == 7 - - def test_validate(self): - """ - Tests the `public.Option.validate` method. - """ - assert 'validate' in self.cls.__public__ - o = self.subcls() - o.validate(9) - for i in xrange(3): - e = raises(errors.RuleError, o.validate, i) - assert e.error == 'cannot be %d' % i - assert e.value == i - - def test_rules(self): - """ - Tests the `public.Option.rules` property. - """ - o = self.subcls() - assert len(o.rules) == 3 - def get_rule(i): - return getattr(o, 'rule_%d' % i) - rules = tuple(get_rule(i) for i in xrange(3)) - assert o.rules == rules - - def test_get_default(self): - """ - Tests the `public.Option.get_default` method. - """ - assert 'get_default' in self.cls.__public__ - assert 'default' in self.cls.__public__ - assert 'default_from' in self.cls.__public__ - assert self.cls().get_default() is None - class subclass(self.cls): - default = 3 - default_from = public.DefaultFrom( - lambda a,b: a * b, - 'key0', 'key1' - ) - o = subclass() - assert o.get_default() == 3 - assert o.get_default(key0=2, key1=5) == 10 - assert o.get_default(key0=7) == 3 - - class test_Command(ClassChecker): """ Tests the `public.Command` class. @@ -708,9 +602,11 @@ class test_Method(ClassChecker): assert proxy.implements(public.Option) -class test_prop(ClassChecker): +class test_Property(ClassChecker): + """ + Tests the `public.Property` class. + """ _cls = public.Property def test_class(self): - assert self.cls.__bases__ == (public.Attribute, public.Option) - assert self.cls.implements(public.Option) + assert self.cls.__bases__ == (public.Attribute,) -- cgit From 4f4e8e2712269b41f2863f96d31d5e67ad7b4564 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 2 Sep 2008 20:33:08 +0000 Subject: 234: Renamed Option2 to Option --- ipalib/public.py | 2 +- ipalib/tests/test_public.py | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index 102f36b9..d97f519b 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -85,7 +85,7 @@ class DefaultFrom(plugable.ReadOnly): return None -class Option2(plugable.ReadOnly): +class Option(plugable.ReadOnly): def __init__(self, name, doc, type_, required=False, multivalue=False, diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index 72b07c4d..2e732179 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -104,18 +104,18 @@ class test_DefaltFrom(ClassChecker): assert o(**kw_copy) is None -class test_Option2(ClassChecker): +class test_Option(ClassChecker): """ - Tests the `public.Option2` class. + Tests the `public.Option` class. """ - _cls = public.Option2 + _cls = public.Option def test_class(self): assert self.cls.__bases__ == (plugable.ReadOnly,) def test_init(self): """ - Tests the `public.Option2.__init__` method. + Tests the `public.Option.__init__` method. """ name = 'sn' doc = 'Last Name' @@ -156,7 +156,7 @@ class test_Option2(ClassChecker): def test_normalize(self): """ - Tests the `public.Option2.validate` method. + Tests the `public.Option.validate` method. """ name = 'sn' doc = 'User last name' @@ -205,7 +205,7 @@ class test_Option2(ClassChecker): def test_validate(self): """ - Tests the `public.Option2.validate` method. + Tests the `public.Option.validate` method. """ name = 'sn' doc = 'User last name' @@ -259,7 +259,7 @@ class test_Option2(ClassChecker): def test_get_default(self): """ - Tests the `public.Option2.get_default` method. + Tests the `public.Option.get_default` method. """ name = 'greeting' doc = 'User greeting' @@ -293,7 +293,7 @@ class test_Option2(ClassChecker): def test_get_default(self): """ - Tests the `public.Option2.get_values` method. + Tests the `public.Option.get_values` method. """ name = 'status' doc = 'Account status' -- cgit From c9662adcf77314917b71706048d511a0296b938b Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 2 Sep 2008 22:19:39 +0000 Subject: 235: Added Public.option instance attribute; updated corresponding unit tests; disable some broken unit tests --- ipalib/public.py | 66 ++++++++++++++++++++++----------------------- ipalib/tests/test_public.py | 64 +++++++++++++++++++++++++++++++++++-------- 2 files changed, 86 insertions(+), 44 deletions(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index d97f519b..564f6c8b 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -168,8 +168,8 @@ class Command(plugable.Plugin): 'get_doc', 'options', )) - __options = None - option_classes = tuple() + __Option = None + options = tuple() def get_doc(self, _): """ @@ -183,26 +183,18 @@ class Command(plugable.Plugin): raise NotImplementedError('%s.get_doc()' % self.name) def get_options(self): - """ - Returns iterable with option proxy objects used to create the option - NameSpace when __get_option() is called. - """ - for cls in self.option_classes: - assert inspect.isclass(cls) - o = cls() - o.__lock__() - yield plugable.PluginProxy(Option, o) + return self.options - def __get_options(self): + def __get_Option(self): """ - Returns the NameSpace containing the option proxy objects. + Returns the NameSpace containing the Option instances. """ - if self.__options is None: - object.__setattr__(self, '_Command__options', + if self.__Option is None: + object.__setattr__(self, '_Command__Option', plugable.NameSpace(self.get_options()), ) - return self.__options - options = property(__get_options) + return self.__Option + Option = property(__get_Option) def normalize_iter(self, kw): for (key, value) in kw.items(): @@ -332,11 +324,11 @@ class Method(Attribute, Command): __public__ = Attribute.__public__.union(Command.__public__) def get_options(self): - for proxy in Command.get_options(self): - yield proxy + for option in Command.options: + yield option if self.obj is not None and self.obj.Property is not None: for proxy in self.obj.Property(): - yield proxy + yield proxy.option class Property(Attribute): @@ -346,19 +338,27 @@ class Property(Attribute): 'type', )).union(Attribute.__public__) - def __get_rules(self): - """ - Returns the tuple of rule methods used for input validation. This - tuple is lazily initialized the first time the property is accessed. - """ - if self.__rules is None: - rules = tuple(sorted( - self.__rules_iter(), - key=lambda f: getattr(f, '__name__'), - )) - object.__setattr__(self, '_Property__rules', rules) - return self.__rules - rules = property(__get_rules) + type = ipa_types.Unicode() + required = False + multivalue = False + default = None + default_from = None + normalize = None + + def __init__(self): + super(Property, self).__init__() + self.rules = tuple(sorted( + self.__rules_iter(), + key=lambda f: getattr(f, '__name__'), + )) + self.option = Option(self.attr_name, self.doc, self.type, + required=self.required, + multivalue=self.multivalue, + default=self.default, + default_from=self.default_from, + rules=self.rules, + normalize=self.normalize, + ) def __rules_iter(self): """ diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index 2e732179..3000cfe3 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -304,30 +304,47 @@ class test_Option(ClassChecker): assert o.get_values() == values -class test_Command(ClassChecker): +class dont_Command(ClassChecker): """ Tests the `public.Command` class. """ _cls = public.Command def get_subcls(self): - class my_option(public.Option): - def normalize(self, value): - return super(my_option, self).normalize(value).lower() - @public.rule - def my_rule(self, value): + class Rule(object): + def __init__(self, name): + self.name = name + + def __call__(self, value): if value != self.name: return 'must equal %r' % self.name - default_from = public.DefaultFrom( - lambda arg: arg, 'default_from' - ) + + default_from = public.DefaultFrom( + lambda arg: arg, + 'default_from' + ) + normalize = lambda value: value.lower() + type_ = ipa_types.Unicode() class option0(my_option): pass class option1(my_option): required = True + class example(self.cls): - option_classes = (option0, option1) + options = ( + public.Option('option0', 'Option zero', type_, + normalize=normalize, + default_from=default_from, + rules=(Rule('option0'),) + ), + public.Option('option1', 'Option one', type_, + normalize=normalize, + default_from=default_from, + rules=(Rule('option1'),), + required=True, + ), + ) return example def test_class(self): @@ -550,7 +567,7 @@ class test_Attribute(ClassChecker): assert read_only(o, 'obj') is user_obj -class test_Method(ClassChecker): +class dont_Method(ClassChecker): """ Tests the `public.Method` class. """ @@ -608,5 +625,30 @@ class test_Property(ClassChecker): """ _cls = public.Property + def get_subcls(self): + class user_givenname(self.cls): + 'User first name' + + @public.rule + def rule0_lowercase(self, value): + if not value.islower(): + return 'Must be lowercase' + return user_givenname + def test_class(self): assert self.cls.__bases__ == (public.Attribute,) + assert isinstance(self.cls.type, ipa_types.Unicode) + assert self.cls.required is False + assert self.cls.multivalue is False + assert self.cls.default is None + assert self.cls.default_from is None + assert self.cls.normalize is None + + def test_init(self): + o = self.subcls() + assert len(o.rules) == 1 + assert o.rules[0].__name__ == 'rule0_lowercase' + opt = o.option + assert isinstance(opt, public.Option) + assert opt.name == 'givenname' + assert opt.doc == 'User first name' -- cgit From 5bfbbe3c389db94a266c2224089005692bbc0228 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 2 Sep 2008 23:40:44 +0000 Subject: 236: Ported pubic.Command to new Option; updated corresponding unit tests --- ipalib/public.py | 40 +++++++++++++------------------------ ipalib/tests/test_public.py | 48 ++++++++++++++++++++------------------------- 2 files changed, 35 insertions(+), 53 deletions(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index 564f6c8b..7035d381 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -160,28 +160,16 @@ class Option(plugable.ReadOnly): class Command(plugable.Plugin): __public__ = frozenset(( - 'normalize', 'get_default', + 'normalize', 'validate', 'execute', '__call__', - 'get_doc', - 'options', + 'Option', )) __Option = None options = tuple() - def get_doc(self, _): - """ - Returns the gettext translated doc-string for this command. - - For example: - - >>> def get_doc(self, _): - >>> return _('add new user') - """ - raise NotImplementedError('%s.get_doc()' % self.name) - def get_options(self): return self.options @@ -196,21 +184,21 @@ class Command(plugable.Plugin): return self.__Option Option = property(__get_Option) - def normalize_iter(self, kw): + def __normalize_iter(self, kw): for (key, value) in kw.items(): - if key in self.options: + if key in self.Option: yield ( - key, self.options[key].normalize(value) + key, self.Option[key].normalize(value) ) else: yield (key, value) def normalize(self, **kw): self.print_call('normalize', kw, 1) - return dict(self.normalize_iter(kw)) + return dict(self.__normalize_iter(kw)) - def get_default_iter(self, kw): - for option in self.options(): + def __get_default_iter(self, kw): + for option in self.Option(): if option.name not in kw: value = option.get_default(**kw) if value is not None: @@ -218,17 +206,17 @@ class Command(plugable.Plugin): def get_default(self, **kw): self.print_call('default', kw, 1) - return dict(self.get_default_iter(kw)) + return dict(self.__get_default_iter(kw)) def validate(self, **kw): self.print_call('validate', kw, 1) - for opt in self.options(): - value = kw.get(opt.name, None) + for option in self.Option(): + value = kw.get(option.name, None) if value is None: - if opt.required: - raise errors.RequirementError(opt.name) + if option.required: + raise errors.RequirementError(option.name) continue - opt.validate(value) + option.validate(value) def execute(self, **kw): self.print_call('execute', kw, 1) diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index 3000cfe3..fe76a722 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -304,7 +304,7 @@ class test_Option(ClassChecker): assert o.get_values() == values -class dont_Command(ClassChecker): +class test_Command(ClassChecker): """ Tests the `public.Command` class. """ @@ -326,11 +326,6 @@ class dont_Command(ClassChecker): normalize = lambda value: value.lower() type_ = ipa_types.Unicode() - class option0(my_option): - pass - class option1(my_option): - required = True - class example(self.cls): options = ( public.Option('option0', 'Option zero', type_, @@ -349,7 +344,7 @@ class dont_Command(ClassChecker): def test_class(self): assert self.cls.__bases__ == (plugable.Plugin,) - assert type(self.cls.options) == property + assert self.cls.options == tuple() def test_get_options(self): """ @@ -357,27 +352,26 @@ class dont_Command(ClassChecker): """ assert list(self.cls().get_options()) == [] sub = self.subcls() - for (i, proxy) in enumerate(sub.get_options()): - assert isinstance(proxy, plugable.PluginProxy) - assert read_only(proxy, 'name') == 'option%d' % i - assert proxy.implements(public.Option) + for (i, option) in enumerate(sub.get_options()): + assert isinstance(option, public.Option) + assert read_only(option, 'name') == 'option%d' % i assert i == 1 - def test_options(self): + def test_Option(self): """ - Tests the `public.Command.options` property. + Tests the `public.Command.Option` property. """ - assert 'options' in self.cls.__public__ # Public + assert 'Option' in self.cls.__public__ # Public sub = self.subcls() - options = sub.options - assert type(options) == plugable.NameSpace - assert len(options) == 2 + O = sub.Option + assert type(O) is plugable.NameSpace + assert len(O) == 2 for name in ('option0', 'option1'): - assert name in options - proxy = options[name] - assert getattr(options, name) is proxy - assert isinstance(proxy, plugable.PluginProxy) - assert proxy.name == name + assert name in O + option = O[name] + assert getattr(O, name) is option + assert isinstance(option, public.Option) + assert option.name == name def test_normalize(self): """ @@ -385,9 +379,9 @@ class dont_Command(ClassChecker): """ assert 'normalize' in self.cls.__public__ # Public kw = dict( - option0='OPTION0', - option1='OPTION1', - option2='option2', + option0=u'OPTION0', + option1=u'OPTION1', + option2=u'option2', ) norm = dict((k, v.lower()) for (k, v) in kw.items()) sub = self.subcls() @@ -424,8 +418,8 @@ class dont_Command(ClassChecker): # Check with valid args okay = dict( - option0='option0', - option1='option1', + option0=u'option0', + option1=u'option1', another_option='some value', ) sub.validate(**okay) -- cgit From f45dcdd0d0042bac64ca14ca3ba1860d63a110ea Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 3 Sep 2008 00:01:45 +0000 Subject: 237: Ported public.Method to new Option; updated corresponding unit tests --- ipalib/public.py | 2 +- ipalib/tests/test_public.py | 27 +++++++++++++-------------- 2 files changed, 14 insertions(+), 15 deletions(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index 7035d381..72a08d7a 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -312,7 +312,7 @@ class Method(Attribute, Command): __public__ = Attribute.__public__.union(Command.__public__) def get_options(self): - for option in Command.options: + for option in self.options: yield option if self.obj is not None and self.obj.Property is not None: for proxy in self.obj.Property(): diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index fe76a722..e7909a6b 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -561,7 +561,7 @@ class test_Attribute(ClassChecker): assert read_only(o, 'obj') is user_obj -class dont_Method(ClassChecker): +class test_Method(ClassChecker): """ Tests the `public.Method` class. """ @@ -572,14 +572,10 @@ class dont_Method(ClassChecker): assert self.cls.implements(public.Command) def get_subcls(self): - class option0(public.Option): - pass - class option1(public.Option): - pass class example_prop0(public.Property): - pass + 'Prop zero' class example_prop1(public.Property): - pass + 'Prop one' class example_obj(object): __prop = None def __get_prop(self): @@ -594,8 +590,12 @@ class dont_Method(ClassChecker): ]) return self.__prop Property = property(__get_prop) + type_ = ipa_types.Unicode() class noun_verb(self.cls): - option_classes = (option0, option1) + options= ( + public.Option('option0', 'Option zero', type_), + public.Option('option1', 'Option one', type_), + ) obj = example_obj() return noun_verb @@ -605,12 +605,11 @@ class dont_Method(ClassChecker): """ sub = self.subcls() names = ('option0', 'option1', 'prop0', 'prop1') - proxies = tuple(sub.get_options()) - assert len(proxies) == 4 - for (i, proxy) in enumerate(proxies): - assert proxy.name == names[i] - assert isinstance(proxy, plugable.PluginProxy) - assert proxy.implements(public.Option) + options = tuple(sub.get_options()) + assert len(options) == 4 + for (i, option) in enumerate(options): + assert option.name == names[i] + assert isinstance(option, public.Option) class test_Property(ClassChecker): -- cgit From baef0e6f49aaf75c3d71e3afd9cf2a2abcb07152 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 3 Sep 2008 00:15:20 +0000 Subject: 238: Some docstring cleanup & fixes in test_public.py --- ipalib/tests/test_public.py | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) (limited to 'ipalib') diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index e7909a6b..819c3d3f 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -30,6 +30,9 @@ def test_RULE_FLAG(): def test_rule(): + """ + Tests the `public.rule` function. + """ flag = public.RULE_FLAG rule = public.rule def my_func(): @@ -44,6 +47,9 @@ def test_rule(): def test_is_rule(): + """ + Tests the `public.is_rule` function. + """ is_rule = public.is_rule flag = public.RULE_FLAG @@ -63,9 +69,9 @@ def test_is_rule(): assert not is_rule(call(None)) -class test_DefaltFrom(ClassChecker): +class test_DefaultFrom(ClassChecker): """ - Tests the `public.DefaltFrom` class. + Tests the `public.DefaultFrom` class. """ _cls = public.DefaultFrom @@ -132,6 +138,9 @@ class test_Option(ClassChecker): assert read_only(o, 'rules') == (type_.validate,) def test_convert(self): + """ + Tests the `public.Option.convert` method. + """ name = 'sn' doc = 'User last name' type_ = ipa_types.Unicode() @@ -156,7 +165,7 @@ class test_Option(ClassChecker): def test_normalize(self): """ - Tests the `public.Option.validate` method. + Tests the `public.Option.normalize` method. """ name = 'sn' doc = 'User last name' @@ -291,7 +300,7 @@ class test_Option(ClassChecker): assert o.get_default() == (default,) assert o.get_default(first='John', last='Doe') == ('Hello, John Doe!',) - def test_get_default(self): + def test_get_value(self): """ Tests the `public.Option.get_values` method. """ @@ -540,6 +549,9 @@ class test_Attribute(ClassChecker): assert type(self.cls.attr_name) is property def test_init(self): + """ + Tests the `public.Attribute.__init__` method. + """ class user_add(self.cls): pass o = user_add() @@ -548,6 +560,9 @@ class test_Attribute(ClassChecker): assert read_only(o, 'attr_name') == 'add' def test_finalize(self): + """ + Tests the `public.Attribute.finalize` method. + """ user_obj = 'The user public.Object instance' class api(object): Object = dict(user=user_obj) @@ -638,6 +653,9 @@ class test_Property(ClassChecker): assert self.cls.normalize is None def test_init(self): + """ + Tests the `public.Property.__init__` method. + """ o = self.subcls() assert len(o.rules) == 1 assert o.rules[0].__name__ == 'rule0_lowercase' -- cgit From 085ea3f62f37539a279f7d4ade51208fcbe868b9 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 3 Sep 2008 18:32:49 +0000 Subject: 239: Added errors.ConversionError; started big clean up of how ValidationError is raised so it works well with multivalues --- ipalib/errors.py | 8 +++++++ ipalib/ipa_types.py | 10 +++++---- ipalib/public.py | 42 +++++++++++++++++++++--------------- ipalib/tests/test_ipa_types.py | 14 +++++------- ipalib/tests/test_public.py | 48 +++++++++++++++++++----------------------- ipalib/tests/tstutil.py | 14 ++++++++++++ 6 files changed, 80 insertions(+), 56 deletions(-) (limited to 'ipalib') diff --git a/ipalib/errors.py b/ipalib/errors.py index d68bac40..8ecccf2b 100644 --- a/ipalib/errors.py +++ b/ipalib/errors.py @@ -119,6 +119,14 @@ class ValidationError(IPAError): IPAError.__init__(self, name, value, error) +class ConversionError(ValidationError): + def __init__(self, name, value, type_, position): + self.type = type_ + self.position = position + ValidationError.__init__(self, name, value, type_.conversion_error) + + + class NormalizationError(ValidationError): def __init__(self, name, value, type): self.type = type diff --git a/ipalib/ipa_types.py b/ipalib/ipa_types.py index c120b5ab..2da8e0be 100644 --- a/ipalib/ipa_types.py +++ b/ipalib/ipa_types.py @@ -58,6 +58,9 @@ class Type(ReadOnly): if type_ not in allowed: raise ValueError('not an allowed type: %r' % type_) self.type = type_ + # FIXME: This should be replaced with a more user friendly message + # as this is what is returned to the user. + self.conversion_error = 'Must be a %r' % self.type lock(self) def __get_name(self): @@ -73,6 +76,9 @@ class Type(ReadOnly): except (TypeError, ValueError): return None + def validate(self, value): + pass + def __call__(self, value): if value is None: raise TypeError('value cannot be None') @@ -102,10 +108,6 @@ class Bool(Type): return False return None - def validate(self, value): - if not (value is True or value is False): - return 'Must be %r or %r' % (self.true, self.false) - class Int(Type): def __init__(self, min_value=None, max_value=None): diff --git a/ipalib/public.py b/ipalib/public.py index 72a08d7a..24d416c9 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -27,7 +27,7 @@ import inspect import plugable from plugable import lock, check_name import errors -from errors import check_type, check_isinstance +from errors import check_type, check_isinstance, raise_TypeError import ipa_types @@ -105,28 +105,36 @@ class Option(plugable.ReadOnly): self.rules = (type_.validate,) + rules lock(self) + def __convert_scalar(self, value, position=None): + if value is None: + raise TypeError('value cannot be None') + converted = self.type(value) + if converted is None: + raise errors.ConversionError( + self.name, value, self.type, position + ) + return converted + def convert(self, value): if self.multivalue: if type(value) in (tuple, list): - return tuple(self.type(v) for v in value) - return (self.type(value),) - return self.type(value) + return tuple( + self.__convert_scalar(v, i) for (i, v) in enumerate(value) + ) + return (self.__convert_scalar(value, 0),) # tuple + return self.__convert_scalar(value) def __normalize_scalar(self, value): - if value is None: - return None if type(value) is not self.type.type: - raise TypeError('need a %r; got %r' % (self.type.type, value)) + raise_TypeError(value, self.type.type, 'value') return self.__normalize(value) def normalize(self, value): if self.__normalize is None: return value if self.multivalue: - if value is None: - return None if type(value) is not tuple: - raise TypeError('multivalue must be a tuple; got %r' % value) + raise_TypeError(value, tuple, 'value') return tuple(self.__normalize_scalar(v) for v in value) return self.__normalize_scalar(value) @@ -137,6 +145,10 @@ class Option(plugable.ReadOnly): raise errors.RuleError(self.name, value, rule, error) def validate(self, value): + if value is None and self.required: + raise errors.RequirementError(self.name) + else: + return if self.multivalue: if type(value) is not tuple: raise TypeError('multivalue must be a tuple; got %r' % value) @@ -210,13 +222,9 @@ class Command(plugable.Plugin): def validate(self, **kw): self.print_call('validate', kw, 1) - for option in self.Option(): - value = kw.get(option.name, None) - if value is None: - if option.required: - raise errors.RequirementError(option.name) - continue - option.validate(value) + for (key, value) in kw.iteritems(): + if key in self.Option: + self.Option[key].validate(value) def execute(self, **kw): self.print_call('execute', kw, 1) diff --git a/ipalib/tests/test_ipa_types.py b/ipalib/tests/test_ipa_types.py index 360478fb..b8e996a7 100644 --- a/ipalib/tests/test_ipa_types.py +++ b/ipalib/tests/test_ipa_types.py @@ -87,6 +87,11 @@ class test_Type(ClassChecker): e = raises(ValueError, self.cls, t) assert str(e) == 'not an allowed type: %r' % t + def test_validate(self): + o = self.cls(unicode) + for value in (None, u'Hello', 'Hello', 42, False): + assert o.validate(value) is None + class test_Bool(ClassChecker): _cls = ipa_types.Bool @@ -126,15 +131,6 @@ class test_Bool(ClassChecker): # value is not be converted, so None is returned assert o(value) is None - def test_validate(self): - t = 'For sure!' - f = 'No way!' - o = self.cls(true=t, false=f) - assert o.validate(True) is None - assert o.validate(False) is None - for value in (t, f, 0, 1, 'True', 'False', 'Yes', 'No'): - assert o.validate(value) == 'Must be %r or %r' % (t, f) - class test_Int(ClassChecker): _cls = ipa_types.Int diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index 819c3d3f..28970af9 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -22,6 +22,7 @@ Unit tests for `ipalib.public` module. """ from tstutil import raises, getitem, no_set, no_del, read_only, ClassChecker +from tstutil import check_TypeError from ipalib import public, plugable, errors, ipa_types @@ -171,46 +172,41 @@ class test_Option(ClassChecker): doc = 'User last name' t = ipa_types.Unicode() callback = lambda value: value.lower() - orig = u'Hello World' - orig_str = str(orig) - norm = u'hello world' - tup_orig = (orig, norm, u'WONDERFUL!') - tup_norm = (norm, norm, u'wonderful!') - tup_str = (orig_str, orig) - all_values = (None, orig, orig_str, norm, tup_orig, tup_norm, tup_str) - - ## Scenario 1: multivalue=False, normalize=None + values = (None, u'Hello', (u'Hello',), 'hello', ['hello']) + + # Scenario 1: multivalue=False, normalize=None o = self.cls(name, doc, t) - for v in all_values: + for v in values: # When normalize=None, value is returned, no type checking: assert o.normalize(v) is v - ## Scenario 2: multivalue=False, normalize=callback + # Scenario 2: multivalue=False, normalize=callback o = self.cls(name, doc, t, normalize=callback) - assert o.normalize(None) is None - for v in (orig, norm): - assert o.normalize(v) == norm - for v in (orig_str, tup_orig, tup_norm, tup_str): # Not unicode + for v in (u'Hello', u'hello'): # Okay + assert o.normalize(v) == u'hello' + for v in [None, 'hello', (u'Hello',)]: # Not unicode e = raises(TypeError, o.normalize, v) - assert str(e) == 'need a %r; got %r' % (unicode, v) + assert str(e) == errors.TYPE_FORMAT % ('value', unicode, v) + check_TypeError(v, unicode, 'value', o.normalize, v) - ## Scenario 3: multivalue=True, normalize=None + # Scenario 3: multivalue=True, normalize=None o = self.cls(name, doc, t, multivalue=True) - for v in all_values: + for v in values: # When normalize=None, value is returned, no type checking: assert o.normalize(v) is v - ## Scenario 4: multivalue=True, normalize=callback + # Scenario 4: multivalue=True, normalize=callback o = self.cls(name, doc, t, multivalue=True, normalize=callback) - assert o.normalize(None) is None - for v in (tup_orig, tup_norm): - assert o.normalize(v) == tup_norm - for v in (orig, orig_str, norm): # Not tuple + for value in [(u'Hello',), (u'hello',)]: # Okay + assert o.normalize(value) == (u'hello',) + for v in (None, u'Hello', [u'hello']): # Not tuple e = raises(TypeError, o.normalize, v) - assert str(e) == 'multivalue must be a tuple; got %r' % v - for v in [tup_str, (norm, orig, orig_str)]: # Not unicode + assert str(e) == errors.TYPE_FORMAT % ('value', tuple, v) + check_TypeError(v, tuple, 'value', o.normalize, v) + for v in [('Hello',), (u'Hello', 'Hello')]: # Non unicode member e = raises(TypeError, o.normalize, v) - assert str(e) == 'need a %r; got %r' % (unicode, orig_str) + assert str(e) == errors.TYPE_FORMAT % ('value', unicode, 'Hello') + check_TypeError('Hello', unicode, 'value', o.normalize, v) def test_validate(self): """ diff --git a/ipalib/tests/tstutil.py b/ipalib/tests/tstutil.py index 79e8ae38..1bf3eaab 100644 --- a/ipalib/tests/tstutil.py +++ b/ipalib/tests/tstutil.py @@ -22,6 +22,7 @@ Utility functions for the unit tests. """ import inspect +from ipalib import errors class ExceptionNotRaised(Exception): """ @@ -131,3 +132,16 @@ class ClassChecker(object): self.__class__.__name__, 'get_subcls()' ) + + +def check_TypeError(value, type_, name, callback, *args, **kw): + """ + Tests a standard TypeError raised with `errors.raise_TypeError`. + """ + e = raises(TypeError, callback, *args, **kw) + assert e.value == value + assert type(e.value) is type(value) + assert e.type is type_ + assert e.name == name + assert type(e.name) is str + assert str(e) == errors.TYPE_FORMAT % (name, type_, value) -- cgit From 9548b4b951492b3aac175ba351a1dd7e857c017b Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 3 Sep 2008 18:41:46 +0000 Subject: 240: Small change in tstutil.check_TypeError(), cleaned up use of check_TypeError() in test_Option.test_normalize() --- ipalib/tests/test_public.py | 11 +++-------- ipalib/tests/tstutil.py | 4 ++-- 2 files changed, 5 insertions(+), 10 deletions(-) (limited to 'ipalib') diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index 28970af9..c184ac15 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -185,8 +185,6 @@ class test_Option(ClassChecker): for v in (u'Hello', u'hello'): # Okay assert o.normalize(v) == u'hello' for v in [None, 'hello', (u'Hello',)]: # Not unicode - e = raises(TypeError, o.normalize, v) - assert str(e) == errors.TYPE_FORMAT % ('value', unicode, v) check_TypeError(v, unicode, 'value', o.normalize, v) # Scenario 3: multivalue=True, normalize=None @@ -200,13 +198,10 @@ class test_Option(ClassChecker): for value in [(u'Hello',), (u'hello',)]: # Okay assert o.normalize(value) == (u'hello',) for v in (None, u'Hello', [u'hello']): # Not tuple - e = raises(TypeError, o.normalize, v) - assert str(e) == errors.TYPE_FORMAT % ('value', tuple, v) check_TypeError(v, tuple, 'value', o.normalize, v) - for v in [('Hello',), (u'Hello', 'Hello')]: # Non unicode member - e = raises(TypeError, o.normalize, v) - assert str(e) == errors.TYPE_FORMAT % ('value', unicode, 'Hello') - check_TypeError('Hello', unicode, 'value', o.normalize, v) + fail = 'Hello' # Not unicode + for v in [(fail,), (u'Hello', fail)]: # Non unicode member + check_TypeError(fail, unicode, 'value', o.normalize, v) def test_validate(self): """ diff --git a/ipalib/tests/tstutil.py b/ipalib/tests/tstutil.py index 1bf3eaab..7586d08c 100644 --- a/ipalib/tests/tstutil.py +++ b/ipalib/tests/tstutil.py @@ -139,9 +139,9 @@ def check_TypeError(value, type_, name, callback, *args, **kw): Tests a standard TypeError raised with `errors.raise_TypeError`. """ e = raises(TypeError, callback, *args, **kw) - assert e.value == value - assert type(e.value) is type(value) + assert e.value is value assert e.type is type_ assert e.name == name assert type(e.name) is str assert str(e) == errors.TYPE_FORMAT % (name, type_, value) + return e -- cgit From 9b9615df79d27a74b3cefd1dab708c98a5832b71 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 3 Sep 2008 18:48:58 +0000 Subject: 241: Added additional index=None kwarg to errors.ValidationError.__init__() --- ipalib/errors.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/errors.py b/ipalib/errors.py index 8ecccf2b..7629d8f5 100644 --- a/ipalib/errors.py +++ b/ipalib/errors.py @@ -112,10 +112,11 @@ class IPAError(Exception): class ValidationError(IPAError): msg = 'invalid %r value %r: %s' - def __init__(self, name, value, error): + def __init__(self, name, value, error, index=None): self.name = name self.value = value self.error = error + self.index = index IPAError.__init__(self, name, value, error) -- cgit From 5e8f945a1ea2f34f40a5e033801d66162fc63850 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 3 Sep 2008 19:38:39 +0000 Subject: 242: Started cleanup of custom exceptions; added unit tests for errors.IPAError --- ipalib/errors.py | 37 +++++++++++++++++++++---------------- ipalib/public.py | 6 ++++-- ipalib/tests/test_errors.py | 31 +++++++++++++++++++++++++++++++ ipalib/tests/test_public.py | 4 ++-- 4 files changed, 58 insertions(+), 20 deletions(-) (limited to 'ipalib') diff --git a/ipalib/errors.py b/ipalib/errors.py index 7629d8f5..eb08a7be 100644 --- a/ipalib/errors.py +++ b/ipalib/errors.py @@ -87,32 +87,38 @@ def check_isinstance(value, type_, name, allow_none=False): class IPAError(Exception): """ + Base class for all custom IPA errors. + Use this base class for your custom IPA errors unless there is a specific reason to subclass from AttributeError, KeyError, etc. """ - msg = None - def __init__(self, *args, **kw): + format = None + + def __init__(self, *args): self.args = args - self.kw = kw def __str__(self): """ Returns the string representation of this exception. """ - if self.msg is None: - if len(self.args) == 1: - return unicode(self.args[0]) - return unicode(self.args) - if len(self.args) > 0: - return self.msg % self.args - return self.msg % self.kw + return self.format % self.args class ValidationError(IPAError): - msg = 'invalid %r value %r: %s' + """ + Base class for all types of validation errors. + """ + + format = 'invalid %r value %r: %s' def __init__(self, name, value, error, index=None): + """ + :param name: The name of the value that failed validation. + :param value: The value that failed validation. + :param error: The error message describing the failure. + :param index: If multivalue, index of value in multivalue tuple + """ self.name = name self.value = value self.error = error @@ -138,12 +144,11 @@ class NormalizationError(ValidationError): class RuleError(ValidationError): """ - Raised when a required option was not provided. + Raised when a value fails a validation rule. """ - # FIXME: `rule` should really be after `error` - def __init__(self, name, value, rule, error): - self.rule = rule - ValidationError.__init__(self, name, value, error) + def __init__(self, name, value, error, rule, index=None): + self.rule_name = rule.__name__ + ValidationError.__init__(self, name, value, error, index) class RequirementError(ValidationError): diff --git a/ipalib/public.py b/ipalib/public.py index 24d416c9..34acbe6f 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -138,11 +138,13 @@ class Option(plugable.ReadOnly): return tuple(self.__normalize_scalar(v) for v in value) return self.__normalize_scalar(value) - def __validate_scalar(self, value): + def __validate_scalar(self, value, index=None): + if type(value) is not self.type.type: + raise_TypeError(value, self.type.type, 'value') for rule in self.rules: error = rule(value) if error is not None: - raise errors.RuleError(self.name, value, rule, error) + raise errors.RuleError(self.name, value, error, rule) def validate(self, value): if value is None and self.required: diff --git a/ipalib/tests/test_errors.py b/ipalib/tests/test_errors.py index 34b195e8..b0b5483c 100644 --- a/ipalib/tests/test_errors.py +++ b/ipalib/tests/test_errors.py @@ -129,3 +129,34 @@ def test_check_isinstance(): fail_bool = 0 e = raises(AssertionError, f, value, type_, name, allow_none=fail_bool) assert str(e) == type_format % ('allow_none', bool, fail_bool) + + +class test_IPAError(ClassChecker): + """ + Tests the `errors.IPAError` exception. + """ + _cls = errors.IPAError + + def test_class(self): + assert self.cls.__bases__ == (Exception,) + + def test_init(self): + """ + Tests the `errors.IPAError.__init__` method. + """ + args = ('one fish', 'two fish') + e = self.cls(*args) + assert e.args == args + assert self.cls().args == tuple() + + def test_str(self): + """ + Tests the `errors.IPAError.__str__` method. + """ + f = 'The %s color is %s.' + class custom_error(self.cls): + format = f + for args in [('sexiest', 'red'), ('most-batman-like', 'black')]: + e = custom_error(*args) + assert e.args == args + assert str(e) == f % args diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index c184ac15..bbdd37f3 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -203,7 +203,7 @@ class test_Option(ClassChecker): for v in [(fail,), (u'Hello', fail)]: # Non unicode member check_TypeError(fail, unicode, 'value', o.normalize, v) - def test_validate(self): + def dont_validate(self): """ Tests the `public.Option.validate` method. """ @@ -408,7 +408,7 @@ class test_Command(ClassChecker): assert sub.get_default(**no_fill) == {} assert sub.get_default(**fill) == default - def test_validate(self): + def dont_validate(self): """ Tests the `public.Command.validate` method. """ -- cgit From 390c1aa4ba9d2c54ac4c737c128f0561d14b58ab Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 3 Sep 2008 20:05:24 +0000 Subject: 243: Added unit tests for errors.ValidationError --- ipalib/errors.py | 2 ++ ipalib/tests/test_errors.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) (limited to 'ipalib') diff --git a/ipalib/errors.py b/ipalib/errors.py index eb08a7be..cf213d70 100644 --- a/ipalib/errors.py +++ b/ipalib/errors.py @@ -119,6 +119,8 @@ class ValidationError(IPAError): :param error: The error message describing the failure. :param index: If multivalue, index of value in multivalue tuple """ + assert type(name) is str + assert index is None or (type(index) is int and index >= 0) self.name = name self.value = value self.error = error diff --git a/ipalib/tests/test_errors.py b/ipalib/tests/test_errors.py index b0b5483c..83dc6e6e 100644 --- a/ipalib/tests/test_errors.py +++ b/ipalib/tests/test_errors.py @@ -160,3 +160,36 @@ class test_IPAError(ClassChecker): e = custom_error(*args) assert e.args == args assert str(e) == f % args + + +class test_ValidationError(ClassChecker): + """ + Tests the `errors.ValidationError` exception. + """ + _cls = errors.ValidationError + + def test_class(self): + assert self.cls.__bases__ == (errors.IPAError,) + + def test_init(self): + """ + Tests the `errors.ValidationError.__init__` method. + """ + name = 'login' + value = 'Whatever' + error = 'Must be lowercase.' + for index in (None, 3): + e = self.cls(name, value, error, index=index) + assert e.name is name + assert e.value is value + assert e.error is error + assert e.index is index + assert str(e) == 'invalid %r value %r: %s' % (name, value, error) + # Check that index default is None: + assert self.cls(name, value, error).index is None + # Check non str name raises AssertionError: + raises(AssertionError, self.cls, unicode(name), value, error) + # Check non int index raises AssertionError: + raises(AssertionError, self.cls, name, value, error, index=5.0) + # Check negative index raises AssertionError: + raises(AssertionError, self.cls, name, value, error, index=-2) -- cgit From 6f739bcf671ee3028ffeab736e7ea1ff16489907 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 3 Sep 2008 21:53:15 +0000 Subject: 244: Added unit tests for errors.ConversionError --- ipalib/errors.py | 12 ++++++++---- ipalib/tests/test_errors.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 4 deletions(-) (limited to 'ipalib') diff --git a/ipalib/errors.py b/ipalib/errors.py index cf213d70..6f0941e2 100644 --- a/ipalib/errors.py +++ b/ipalib/errors.py @@ -129,11 +129,15 @@ class ValidationError(IPAError): class ConversionError(ValidationError): - def __init__(self, name, value, type_, position): - self.type = type_ - self.position = position - ValidationError.__init__(self, name, value, type_.conversion_error) + """ + Raised when a value cannot be converted to the correct type. + """ + def __init__(self, name, value, type_, index=None): + self.type = type_ + ValidationError.__init__(self, name, value, type_.conversion_error, + index=index, + ) class NormalizationError(ValidationError): diff --git a/ipalib/tests/test_errors.py b/ipalib/tests/test_errors.py index 83dc6e6e..3b89c7ed 100644 --- a/ipalib/tests/test_errors.py +++ b/ipalib/tests/test_errors.py @@ -193,3 +193,33 @@ class test_ValidationError(ClassChecker): raises(AssertionError, self.cls, name, value, error, index=5.0) # Check negative index raises AssertionError: raises(AssertionError, self.cls, name, value, error, index=-2) + + +class test_ConversionError(ClassChecker): + """ + Tests the `errors.ConversionError` exception. + """ + _cls = errors.ConversionError + + def test_class(self): + assert self.cls.__bases__ == (errors.ValidationError,) + + def test_init(self): + """ + Tests the `errors.ConversionError.__init__` method. + """ + name = 'some_arg' + value = '42.0' + class type_(object): + conversion_error = 'Not an integer' + for index in (None, 7): + e = self.cls(name, value, type_, index=index) + assert e.name is name + assert e.value is value + assert e.type is type_ + assert e.error is type_.conversion_error + assert e.index is index + assert str(e) == 'invalid %r value %r: %s' % (name, value, + type_.conversion_error) + # Check that index default is None: + assert self.cls(name, value, type_).index is None -- cgit From 62533bfb2baa2eac7a1361627f90bdda97452605 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 3 Sep 2008 21:55:44 +0000 Subject: 245: Removed depreciated NormalizationError --- ipalib/errors.py | 8 -------- 1 file changed, 8 deletions(-) (limited to 'ipalib') diff --git a/ipalib/errors.py b/ipalib/errors.py index 6f0941e2..de9a43a5 100644 --- a/ipalib/errors.py +++ b/ipalib/errors.py @@ -140,14 +140,6 @@ class ConversionError(ValidationError): ) -class NormalizationError(ValidationError): - def __init__(self, name, value, type): - self.type = type - ValidationError.__init__(self, name, value, - 'not %r' % type - ) - - class RuleError(ValidationError): """ Raised when a value fails a validation rule. -- cgit From 004e989dc493a703b0f85be164409443416bf894 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 3 Sep 2008 22:14:25 +0000 Subject: 246: Added unit tests for errors.RuleError --- ipalib/errors.py | 6 +++--- ipalib/tests/test_errors.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) (limited to 'ipalib') diff --git a/ipalib/errors.py b/ipalib/errors.py index de9a43a5..9f40ddae 100644 --- a/ipalib/errors.py +++ b/ipalib/errors.py @@ -145,8 +145,9 @@ class RuleError(ValidationError): Raised when a value fails a validation rule. """ def __init__(self, name, value, error, rule, index=None): - self.rule_name = rule.__name__ - ValidationError.__init__(self, name, value, error, index) + assert callable(rule) + self.rule = rule + ValidationError.__init__(self, name, value, error, index=index) class RequirementError(ValidationError): @@ -233,6 +234,5 @@ class MissingOverrideError(RegistrationError): return self.msg % (self.base.__name__, self.cls.__name__, self.cls) - class TwiceSetError(IPAError): msg = '%s.%s cannot be set twice' diff --git a/ipalib/tests/test_errors.py b/ipalib/tests/test_errors.py index 3b89c7ed..b1552562 100644 --- a/ipalib/tests/test_errors.py +++ b/ipalib/tests/test_errors.py @@ -223,3 +223,31 @@ class test_ConversionError(ClassChecker): type_.conversion_error) # Check that index default is None: assert self.cls(name, value, type_).index is None + + +class test_RuleError(ClassChecker): + """ + Tests the `errors.RuleError` exception. + """ + _cls = errors.RuleError + + def test_class(self): + assert self.cls.__bases__ == (errors.ValidationError,) + + def test_init(self): + """ + Tests the `errors.RuleError.__init__` method. + """ + name = 'whatever' + value = 'The smallest weird number.' + def my_rule(value): + return 'Value is bad.' + error = my_rule(value) + for index in (None, 42): + e = self.cls(name, value, error, my_rule, index=index) + assert e.name is name + assert e.value is value + assert e.error is error + assert e.rule is my_rule + # Check that index default is None: + assert self.cls(name, value, error, my_rule).index is None -- cgit From 296d59d27a33bedff00e126439730558b4cc93d3 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 3 Sep 2008 22:29:01 +0000 Subject: 247: Added unit tests for errors.RequirementError --- ipalib/errors.py | 7 +++---- ipalib/tests/test_errors.py | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) (limited to 'ipalib') diff --git a/ipalib/errors.py b/ipalib/errors.py index 9f40ddae..fc1b2c49 100644 --- a/ipalib/errors.py +++ b/ipalib/errors.py @@ -19,6 +19,8 @@ """ All custom errors raised by `ipalib` package. + +Also includes a few utility functions for raising exceptions. """ TYPE_FORMAT = '%s: need a %r; got %r' @@ -155,16 +157,13 @@ class RequirementError(ValidationError): Raised when a required option was not provided. """ def __init__(self, name): - ValidationError.__init__(self, name, None, - 'missing required value' - ) + ValidationError.__init__(self, name, None, 'Required') class SetError(IPAError): msg = 'setting %r, but NameSpace does not allow attribute setting' - class RegistrationError(IPAError): """ Base class for errors that occur during plugin registration. diff --git a/ipalib/tests/test_errors.py b/ipalib/tests/test_errors.py index b1552562..7d2df4df 100644 --- a/ipalib/tests/test_errors.py +++ b/ipalib/tests/test_errors.py @@ -251,3 +251,24 @@ class test_RuleError(ClassChecker): assert e.rule is my_rule # Check that index default is None: assert self.cls(name, value, error, my_rule).index is None + + +class test_RequirementError(ClassChecker): + """ + Tests the `errors.RequirementError` exception. + """ + _cls = errors.RequirementError + + def test_class(self): + assert self.cls.__bases__ == (errors.ValidationError,) + + def test_init(self): + """ + Tests the `errors.RequirementError.__init__` method. + """ + name = 'givenname' + e = self.cls(name) + assert e.name is name + assert e.value is None + assert e.error == 'Required' + assert e.index is None -- cgit From 490eaee8a9f35975f7db5739a882894e10fb79a7 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 3 Sep 2008 22:41:53 +0000 Subject: 248: Removed depreciated SetError and TwiceSetError exceptions --- ipalib/errors.py | 8 -------- 1 file changed, 8 deletions(-) (limited to 'ipalib') diff --git a/ipalib/errors.py b/ipalib/errors.py index fc1b2c49..5e8af9d4 100644 --- a/ipalib/errors.py +++ b/ipalib/errors.py @@ -160,10 +160,6 @@ class RequirementError(ValidationError): ValidationError.__init__(self, name, None, 'Required') -class SetError(IPAError): - msg = 'setting %r, but NameSpace does not allow attribute setting' - - class RegistrationError(IPAError): """ Base class for errors that occur during plugin registration. @@ -231,7 +227,3 @@ class MissingOverrideError(RegistrationError): def __str__(self): return self.msg % (self.base.__name__, self.cls.__name__, self.cls) - - -class TwiceSetError(IPAError): - msg = '%s.%s cannot be set twice' -- cgit From b53ff67ff168ee416e2e5ac017d90410feeb53f5 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 3 Sep 2008 23:21:26 +0000 Subject: 249: Fixed Option.__convert_scalar() to user index=None instead of position=None; updated unit tests for Option.convert() to test the ConversionError.index attribute --- ipalib/public.py | 4 ++-- ipalib/tests/test_public.py | 56 ++++++++++++++++++++++++++++++--------------- 2 files changed, 40 insertions(+), 20 deletions(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index 34acbe6f..915a1890 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -105,13 +105,13 @@ class Option(plugable.ReadOnly): self.rules = (type_.validate,) + rules lock(self) - def __convert_scalar(self, value, position=None): + def __convert_scalar(self, value, index=None): if value is None: raise TypeError('value cannot be None') converted = self.type(value) if converted is None: raise errors.ConversionError( - self.name, value, self.type, position + self.name, value, self.type, index=index ) return converted diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index bbdd37f3..ddd99415 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -142,27 +142,47 @@ class test_Option(ClassChecker): """ Tests the `public.Option.convert` method. """ - name = 'sn' - doc = 'User last name' - type_ = ipa_types.Unicode() - class Hello(object): - def __unicode__(self): - return u'hello' - hello = Hello() - values = (u'hello', 'hello', hello) - # Test when multivalue=False: + name = 'some_number' + doc = 'Some number' + type_ = ipa_types.Int() + okay = (7, 7L, 7.0, ' 7 ') + fail = ('7.0', '7L', 'whatever', object) + + # Scenario 1: multivalue=False o = self.cls(name, doc, type_) - for value in values: + e = raises(TypeError, o.convert, None) + assert str(e) == 'value cannot be None' + for value in okay: new = o.convert(value) - assert new == u'hello' - assert type(new) is unicode - # Test when multivalue=True: + assert new == 7 + assert type(new) is int + for value in fail: + e = raises(errors.ConversionError, o.convert, value) + assert e.name is name + assert e.value is value + assert e.error is type_.conversion_error + assert e.index is None + + # Scenario 2: multivalue=True o = self.cls(name, doc, type_, multivalue=True) - for value in values: - for v in (value, (value,)): - new = o.convert(hello) - assert new == (u'hello',) - assert type(new) is tuple + for none in [None, (7, None)]: + e = raises(TypeError, o.convert, none) + assert str(e) == 'value cannot be None' + for value in okay: + assert o.convert((value,)) == (7,) + assert o.convert([value]) == (7,) + assert o.convert(okay) == tuple(int(v) for v in okay) + cnt = 5 + for value in fail: + for i in xrange(cnt): + others = list(7 for x in xrange(cnt)) + others[i] = value + for v in [tuple(others), list(others)]: + e = raises(errors.ConversionError, o.convert, v) + assert e.name is name + assert e.value is value + assert e.error is type_.conversion_error + assert e.index == i def test_normalize(self): """ -- cgit From 7e3664a964e6c0ef0c773ae8eb3ab3ac21385649 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 4 Sep 2008 01:01:40 +0000 Subject: 250: Updated Option.validate to utilize the ValidationError.index attribute; updated unit tests for Option.validate to test use of index attribute --- ipalib/public.py | 14 ++++++-------- ipalib/tests/test_public.py | 40 ++++++++++++++++------------------------ 2 files changed, 22 insertions(+), 32 deletions(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index 915a1890..91f357ca 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -144,18 +144,16 @@ class Option(plugable.ReadOnly): for rule in self.rules: error = rule(value) if error is not None: - raise errors.RuleError(self.name, value, error, rule) + raise errors.RuleError( + self.name, value, error, rule, index=index + ) def validate(self, value): - if value is None and self.required: - raise errors.RequirementError(self.name) - else: - return if self.multivalue: if type(value) is not tuple: - raise TypeError('multivalue must be a tuple; got %r' % value) - for v in value: - self.__validate_scalar(v) + raise_TypeError(value, tuple, 'value') + for (i, v) in enumerate(value): + self.__validate_scalar(v, i) else: self.__validate_scalar(value) diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index ddd99415..0b2ded96 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -223,7 +223,7 @@ class test_Option(ClassChecker): for v in [(fail,), (u'Hello', fail)]: # Non unicode member check_TypeError(fail, unicode, 'value', o.normalize, v) - def dont_validate(self): + def test_validate(self): """ Tests the `public.Option.validate` method. """ @@ -238,44 +238,36 @@ class test_Option(ClassChecker): fail_case = u'Whatever' fail_type = 'whatever' - ## Scenario 1: multivalue=False + # Scenario 1: multivalue=False o = self.cls(name, doc, type_, rules=my_rules) assert o.rules == (type_.validate, case_rule) - # Test a valid value: o.validate(okay) - # Check that RuleError is raised with wrong case: e = raises(errors.RuleError, o.validate, fail_case) assert e.name is name assert e.value is fail_case assert e.error == 'Must be lower case' - # Test a RuleError is raise with wrong type: - e = raises(errors.RuleError, o.validate, fail_type) - assert e.name is name - assert e.value is fail_type - assert e.error == 'Must be a string' + assert e.rule is case_rule + assert e.index is None + check_TypeError(fail_type, unicode, 'value', o.validate, fail_type) ## Scenario 2: multivalue=True o = self.cls(name, doc, type_, multivalue=True, rules=my_rules) - def check_type_error(value): - e = raises(TypeError, o.validate, value) - assert str(e) == 'multivalue must be a tuple; got %r' % value - # Check a valid value: - check_type_error(okay) o.validate((okay,)) - # Check that RuleError is raised with wrong case: - check_type_error(fail_case) - for value in [(okay, fail_case), (fail_case, okay)]: + cnt = 5 + for i in xrange(cnt): + others = list(okay for x in xrange(cnt)) + others[i] = fail_case + value = tuple(others) e = raises(errors.RuleError, o.validate, value) assert e.name is name assert e.value is fail_case assert e.error == 'Must be lower case' - # Check that RuleError is raise with wrong type: - check_type_error(fail_type) - for value in [(okay, fail_type), (fail_type, okay)]: - e = raises(errors.RuleError, o.validate, value) - assert e.name is name - assert e.value is fail_type - assert e.error == 'Must be a string' + assert e.rule is case_rule + assert e.index == i + for not_tuple in (okay, [okay]): + check_TypeError(not_tuple, tuple, 'value', o.validate, not_tuple) + for has_str in [(fail_type,), (okay, fail_type)]: + check_TypeError(fail_type, unicode, 'value', o.validate, has_str) def test_get_default(self): """ -- cgit From 5cdb182ae8e87f4d3e84bbf7875357c101ca605e Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 4 Sep 2008 02:02:06 +0000 Subject: 251: Fixed Command.validate() so it raises RequirementError; updated and re-enabled unit tests for Command.validate() --- ipalib/public.py | 9 ++++++--- ipalib/tests/test_public.py | 22 +++++++++++++--------- 2 files changed, 19 insertions(+), 12 deletions(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index 91f357ca..c64e29d5 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -222,9 +222,12 @@ class Command(plugable.Plugin): def validate(self, **kw): self.print_call('validate', kw, 1) - for (key, value) in kw.iteritems(): - if key in self.Option: - self.Option[key].validate(value) + for option in self.Option(): + value = kw.get(option.name, None) + if value is not None: + option.validate(value) + elif option.required: + raise errors.RequirementError(option.name) def execute(self, **kw): self.print_call('execute', kw, 1) diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index 0b2ded96..012b6253 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -329,7 +329,7 @@ class test_Command(ClassChecker): def __call__(self, value): if value != self.name: - return 'must equal %r' % self.name + return 'must equal %s' % self.name default_from = public.DefaultFrom( lambda arg: arg, @@ -420,7 +420,7 @@ class test_Command(ClassChecker): assert sub.get_default(**no_fill) == {} assert sub.get_default(**fill) == default - def dont_validate(self): + def test_validate(self): """ Tests the `public.Command.validate` method. """ @@ -438,17 +438,21 @@ class test_Command(ClassChecker): # Check with an invalid arg fail = dict(okay) - fail['option0'] = 'whatever' - raises(errors.RuleError, sub.validate, **fail) + fail['option0'] = u'whatever' + e = raises(errors.RuleError, sub.validate, **fail) + assert e.name == 'option0' + assert e.value == u'whatever' + assert e.error == 'must equal option0' + assert e.rule.__class__.__name__ == 'Rule' + assert e.index is None # Check with a missing required arg fail = dict(okay) fail.pop('option1') - raises(errors.RequirementError, sub.validate, **fail) - - # Check with missing *not* required arg - okay.pop('option0') - sub.validate(**okay) + e = raises(errors.RequirementError, sub.validate, **fail) + assert e.name == 'option1' + assert e.value is None + assert e.index is None def test_execute(self): """ -- cgit From cf7e4c1038a5240f00342d284cf7a6be812322b4 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 4 Sep 2008 02:30:40 +0000 Subject: 252: Added Command.convert() method; added corresponding unit tests --- ipalib/public.py | 11 +++++++++++ ipalib/tests/test_public.py | 19 +++++++++++++++++++ 2 files changed, 30 insertions(+) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index c64e29d5..045f1012 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -173,6 +173,7 @@ class Option(plugable.ReadOnly): class Command(plugable.Plugin): __public__ = frozenset(( 'get_default', + 'convert', 'normalize', 'validate', 'execute', @@ -196,6 +197,16 @@ class Command(plugable.Plugin): return self.__Option Option = property(__get_Option) + def __convert_iter(self, kw): + for (key, value) in kw.iteritems(): + if key in self.Option: + yield (key, self.Option[key].convert(value)) + else: + yield (key, value) + + def convert(self, **kw): + return dict(self.__convert_iter(kw)) + def __normalize_iter(self, kw): for (key, value) in kw.items(): if key in self.Option: diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index 012b6253..4df55d2f 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -385,6 +385,25 @@ class test_Command(ClassChecker): assert isinstance(option, public.Option) assert option.name == name + def test_convert(self): + """ + Tests the `public.Command.convert` method. + """ + assert 'convert' in self.cls.__public__ # Public + kw = dict( + option0='option0', + option1='option1', + whatever=False, + also=object, + ) + expected = dict(kw) + expected.update(dict(option0=u'option0', option1=u'option1')) + o = self.subcls() + for (key, value) in o.convert(**kw).iteritems(): + v = expected[key] + assert value == v + assert type(value) is type(v) + def test_normalize(self): """ Tests the `public.Command.normalize` method. -- cgit From a5c6bf179bed52602222b76cf2dcd09f7d461dea Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 4 Sep 2008 02:41:31 +0000 Subject: 253: Fixed error in Option.get_default() where Option.convert() was being called on Option.default; updated corresponding unit tests --- ipalib/public.py | 9 +++------ ipalib/tests/test_public.py | 3 ++- 2 files changed, 5 insertions(+), 7 deletions(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index 045f1012..c2c3a449 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -162,7 +162,7 @@ class Option(plugable.ReadOnly): default = self.default_from(**kw) if default is not None: return self.convert(default) - return self.convert(self.default) + return self.default def get_values(self): if self.type.name in ('Enum', 'CallbackEnum'): @@ -208,16 +208,13 @@ class Command(plugable.Plugin): return dict(self.__convert_iter(kw)) def __normalize_iter(self, kw): - for (key, value) in kw.items(): + for (key, value) in kw.iteritems(): if key in self.Option: - yield ( - key, self.Option[key].normalize(value) - ) + yield (key, self.Option[key].normalize(value)) else: yield (key, value) def normalize(self, **kw): - self.print_call('normalize', kw, 1) return dict(self.__normalize_iter(kw)) def __get_default_iter(self, kw): diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index 4df55d2f..ac1ae818 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -293,6 +293,7 @@ class test_Option(ClassChecker): assert o.get_default(first='John', last='Doe') == 'Hello, John Doe!' # Scenario 2: multivalue=True + default = (default,) o = self.cls(name, doc, type_, default=default, default_from=default_from, @@ -300,7 +301,7 @@ class test_Option(ClassChecker): ) assert o.default is default assert o.default_from is default_from - assert o.get_default() == (default,) + assert o.get_default() == default assert o.get_default(first='John', last='Doe') == ('Hello, John Doe!',) def test_get_value(self): -- cgit From e1f8619d4adbc15415e2959496640c0f707c54fe Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 4 Sep 2008 03:34:16 +0000 Subject: 254: Added public.Application base class; added corresponding unit tests --- ipalib/public.py | 39 +++++++++++++++++++++++++++++++++++++++ ipalib/tests/test_public.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index c2c3a449..c6611ab7 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -380,3 +380,42 @@ class Property(Attribute): attr = getattr(self, name) if is_rule(attr): yield attr + + +class Application(Command): + """ + Base class for commands register by an external application. + + Special commands that only apply to a particular application built atop + `ipalib` should subclass from ``Application``. + + Because ``Application`` subclasses from `Command', plugins that subclass + from ``Application`` with be available in both the ``api.Command`` and + ``api.Application`` namespaces. + """ + + __public__ = frozenset(( + 'application', + )).union(Command.__public__) + __application = None + + def __get_application(self): + """ + Returns external ``application`` object. + """ + return self.__application + def __set_application(self, application): + """ + Sets the external application object to ``application``. + """ + if self.__application is not None: + raise AttributeError( + '%s.application can only be set once' % self.name + ) + if application is None: + raise TypeError( + '%s.application cannot be None' % self.name + ) + object.__setattr__(self, '_Application__application', application) + assert self.application is application + application = property(__get_application, __set_application) diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index ac1ae818..52fb9336 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -690,3 +690,36 @@ class test_Property(ClassChecker): assert isinstance(opt, public.Option) assert opt.name == 'givenname' assert opt.doc == 'User first name' + + +class test_Application(ClassChecker): + """ + Tests the `public.Application` class. + """ + _cls = public.Application + + def test_class(self): + assert self.cls.__bases__ == (public.Command,) + assert type(self.cls.application) is property + + def test_application(self): + """ + Tests the `public.Application.application` property. + """ + assert 'application' in self.cls.__public__ # Public + app = 'The external application' + class example(self.cls): + 'A subclass' + for o in (self.cls(), example()): + assert o.application is None + e = raises(TypeError, setattr, o, 'application', None) + assert str(e) == ( + '%s.application cannot be None' % o.__class__.__name__ + ) + o.application = app + assert o.application is app + e = raises(AttributeError, setattr, o, 'application', app) + assert str(e) == ( + '%s.application can only be set once' % o.__class__.__name__ + ) + assert o.application is app -- cgit From ab81ca56fd336af4b83ef19a6f97dffe0b1a0923 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 4 Sep 2008 04:39:01 +0000 Subject: 255: CLI help, console commands now subclass from public.Application; other tweeking to make CLI utilize Application --- ipalib/__init__.py | 1 + ipalib/cli.py | 18 ++++++++++++------ ipalib/public.py | 6 ++++-- ipalib/tests/test_cli.py | 5 +++-- ipalib/tests/test_public.py | 13 +++++++------ 5 files changed, 27 insertions(+), 16 deletions(-) (limited to 'ipalib') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index a5fc3f11..b0f0a1fc 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -61,4 +61,5 @@ api = plugable.API( public.Object, public.Method, public.Property, + public.Application, ) diff --git a/ipalib/cli.py b/ipalib/cli.py index 989c24f6..e1cbfa78 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -44,7 +44,7 @@ def from_cli(cli_name): return str(cli_name).replace('-', '_') -class help(public.Command): +class help(public.Application): 'Display help on command' def __call__(self, key): if from_cli(key) not in self.api.Command: @@ -53,7 +53,7 @@ class help(public.Command): print 'Help on command %r:' % key -class console(public.Command): +class console(public.Application): 'Start IPA Interactive Python Console' def __call__(self): @@ -95,10 +95,16 @@ class CLI(object): api.register(help) api.register(console) api.finalize() - def d_iter(): - for cmd in api.Command(): - yield (to_cli(cmd.name), cmd) - self.__d = dict(d_iter()) + for a in api.Application(): + a.set_application(self) + self.build_map() + + def build_map(self): + assert self.__d is None + self.__d = dict( + (c.name.replace('_', '-'), c) for c in self.api.Command() + ) + def run(self): self.finalize() diff --git a/ipalib/public.py b/ipalib/public.py index c6611ab7..eb94ac52 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -396,6 +396,7 @@ class Application(Command): __public__ = frozenset(( 'application', + 'set_application' )).union(Command.__public__) __application = None @@ -404,7 +405,9 @@ class Application(Command): Returns external ``application`` object. """ return self.__application - def __set_application(self, application): + application = property(__get_application) + + def set_application(self, application): """ Sets the external application object to ``application``. """ @@ -418,4 +421,3 @@ class Application(Command): ) object.__setattr__(self, '_Application__application', application) assert self.application is application - application = property(__get_application, __set_application) diff --git a/ipalib/tests/test_cli.py b/ipalib/tests/test_cli.py index 2c65bd06..df3f943e 100644 --- a/ipalib/tests/test_cli.py +++ b/ipalib/tests/test_cli.py @@ -75,6 +75,7 @@ class DummyAPI(object): + class test_CLI(ClassChecker): """ Tests the `cli.CLI` class. @@ -117,7 +118,7 @@ class test_CLI(ClassChecker): len(api.Command) == cnt o = self.cls(api) assert o.mcl is None - o.finalize() + o.build_map() assert o.mcl == 6 # len('cmd_99') def test_dict(self): @@ -128,7 +129,7 @@ class test_CLI(ClassChecker): api = DummyAPI(cnt) assert len(api.Command) == cnt o = self.cls(api) - o.finalize() + o.build_map() for cmd in api.Command(): key = cli.to_cli(cmd.name) assert key in o diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index 52fb9336..c071832a 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -707,19 +707,20 @@ class test_Application(ClassChecker): Tests the `public.Application.application` property. """ assert 'application' in self.cls.__public__ # Public + assert 'set_application' in self.cls.__public__ # Public app = 'The external application' class example(self.cls): 'A subclass' for o in (self.cls(), example()): - assert o.application is None - e = raises(TypeError, setattr, o, 'application', None) + assert read_only(o, 'application') is None + e = raises(TypeError, o.set_application, None) assert str(e) == ( '%s.application cannot be None' % o.__class__.__name__ ) - o.application = app - assert o.application is app - e = raises(AttributeError, setattr, o, 'application', app) + o.set_application(app) + assert read_only(o, 'application') is app + e = raises(AttributeError, o.set_application, app) assert str(e) == ( '%s.application can only be set once' % o.__class__.__name__ ) - assert o.application is app + assert read_only(o, 'application') is app -- cgit From b16deabdffd19dcc6f85f3c1f03074484669912c Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 4 Sep 2008 05:18:14 +0000 Subject: 256: Fixed cli.help plugin so it looks up commands in CLI instead of API --- ipalib/cli.py | 15 ++++++++++----- ipalib/plugins/example.py | 8 ++++---- 2 files changed, 14 insertions(+), 9 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index e1cbfa78..5747fd04 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -45,20 +45,25 @@ def from_cli(cli_name): class help(public.Application): - 'Display help on command' + 'Display help on command.' def __call__(self, key): - if from_cli(key) not in self.api.Command: + key = str(key) + if key not in self.application: print 'help: no such command %r' % key sys.exit(2) - print 'Help on command %r:' % key + cmd = self.application[key] + print 'Purpose: %s' % cmd.doc + if len(cmd.Option) > 0: + print '\nOptions:' + print '' class console(public.Application): - 'Start IPA Interactive Python Console' + 'Start the IPA Python console.' def __call__(self): code.interact( - '(Custom IPA Interactive Python Console)', + '(Custom IPA interactive Python console)', local=dict(api=self.api) ) diff --git a/ipalib/plugins/example.py b/ipalib/plugins/example.py index 3456b1dc..ab752976 100644 --- a/ipalib/plugins/example.py +++ b/ipalib/plugins/example.py @@ -38,19 +38,19 @@ api.register(discover) # Register some methods for the 'user' object: class user_add(public.Method): - 'Add new user' + 'Add a new user.' api.register(user_add) class user_del(public.Method): - 'Delete existing user' + 'Delete an existing user.' api.register(user_del) class user_mod(public.Method): - 'Edit existing user' + 'Edit an existing user.' api.register(user_mod) class user_find(public.Method): - 'Search for users' + 'Search for existing users.' api.register(user_find) -- cgit From 01b73e6910cbadd1867256e71fb982209669a8da Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 4 Sep 2008 06:33:57 +0000 Subject: 257: Improved help command, now parsing options with optparse --- ipalib/cli.py | 74 +++++++++++++++++++++++++++++++++--------------- ipalib/tests/test_cli.py | 2 +- 2 files changed, 52 insertions(+), 24 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index 5747fd04..3cecace7 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -24,6 +24,7 @@ Functionality for Command Line Inteface. import re import sys import code +import optparse import public @@ -53,9 +54,7 @@ class help(public.Application): sys.exit(2) cmd = self.application[key] print 'Purpose: %s' % cmd.doc - if len(cmd.Option) > 0: - print '\nOptions:' - print '' + self.application.build_parser(cmd).print_help() class console(public.Application): @@ -68,6 +67,25 @@ class console(public.Application): ) +class KWCollector(object): + def __init__(self): + object.__setattr__(self, '_KWCollector__d', {}) + + def __setattr__(self, name, value): + if name in self.__d: + v = self.__d[name] + if type(v) is tuple: + value = v + (value,) + else: + value = (v, value) + self.__d[name] = value + object.__setattr__(self, name, value) + + def __todict__(self): + return dict(self.__d) + + + class CLI(object): __d = None __mcl = None @@ -115,29 +133,39 @@ class CLI(object): self.finalize() if len(sys.argv) < 2: self.print_commands() - print 'Usage: ipa COMMAND [OPTIONS]' + print 'Usage: ipa COMMAND [ARGS]' sys.exit(2) - cmd = sys.argv[1] - if cmd not in self: + key = sys.argv[1] + if key not in self: self.print_commands() - print 'ipa: ERROR: unknown command %r' % cmd + print 'ipa: ERROR: unknown command %r' % key sys.exit(2) - self.run_cmd(cmd, (s.decode('utf-8') for s in sys.argv[2:])) - - def run_cmd(self, cmd, given): - (args, kw) = self.parse(given) - self[cmd](*args, **kw) - - def parse(self, given): - args = [] - kw = {} - for g in given: - m = re.match(r'^--([a-z][-a-z0-9]*)=(.+)$', g) - if m: - kw[from_cli(m.group(1))] = m.group(2) - else: - args.append(g) - return (args, kw) + self.run_cmd( + self[key], + list(s.decode('utf-8') for s in sys.argv[2:]) + ) + + def run_cmd(self, cmd, argv): + (args, kw) = self.parse(cmd, argv) + cmd(*args, **kw) + + def parse(self, cmd, argv): + parser = self.build_parser(cmd) + (kwc, args) = parser.parse_args(argv, KWCollector()) + return (args, kwc.__todict__()) + + def build_parser(self, cmd): + parser = optparse.OptionParser( + usage='Usage: %%prog %s' % to_cli(cmd.name), + ) + for option in cmd.Option(): + parser.add_option('--%s' % to_cli(option.name), + metavar=option.type.name.upper(), + help=option.doc, + ) + return parser + + def __get_mcl(self): """ diff --git a/ipalib/tests/test_cli.py b/ipalib/tests/test_cli.py index df3f943e..90c66d41 100644 --- a/ipalib/tests/test_cli.py +++ b/ipalib/tests/test_cli.py @@ -93,7 +93,7 @@ class test_CLI(ClassChecker): o = self.cls(api) assert read_only(o, 'api') is api - def test_parse(self): + def dont_parse(self): """ Tests the `cli.CLI.parse` method. """ -- cgit From 71d36aa6a0b9627ae818d116c7240197a62cff74 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 4 Sep 2008 07:18:26 +0000 Subject: 258: Added some experimental features for interactively prompting for values --- ipalib/cli.py | 24 ++++++++++++++++++++++++ ipalib/public.py | 16 ++++++++++++++++ 2 files changed, 40 insertions(+) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index 3cecace7..a495924e 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -147,8 +147,32 @@ class CLI(object): def run_cmd(self, cmd, argv): (args, kw) = self.parse(cmd, argv) + self.run_interactive(cmd, args, kw) + + def run_interactive(self, cmd, args, kw): + for option in cmd.smart_option_order(): + if option.name not in kw: + default = option.get_default(**kw) + if default is None: + prompt = '%s: ' % option.name + else: + prompt = '%s [%s]: ' % (option.name, default) + error = None + while True: + if error is not None: + print '>>> %s: %s' % (option.name, error) + value = raw_input(prompt) + if default is not None and len(value) == 0: + value = default + if len(value) == 0: + error = 'Must supply a value' + else: + kw[option.name] = value + break cmd(*args, **kw) + + def parse(self, cmd, argv): parser = self.build_parser(cmd) (kwc, args) = parser.parse_args(argv, KWCollector()) diff --git a/ipalib/public.py b/ipalib/public.py index eb94ac52..6220acc0 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -169,6 +169,10 @@ class Option(plugable.ReadOnly): return self.type.values return tuple() + def __call__(self, value, **kw): + pass + + class Command(plugable.Plugin): __public__ = frozenset(( @@ -178,6 +182,7 @@ class Command(plugable.Plugin): 'validate', 'execute', '__call__', + 'smart_option_order', 'Option', )) __Option = None @@ -257,6 +262,17 @@ class Command(plugable.Plugin): self.validate(**kw) self.execute(**kw) + def smart_option_order(self): + def get_key(option): + if option.required: + if option.default_from is None: + return 0 + return 1 + return 2 + for option in sorted(self.Option(), key=get_key): + yield option + + class Object(plugable.Plugin): __public__ = frozenset(( -- cgit From c7cd694d4f307e65f8e4cc5fb2e724e5f9700dea Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 4 Sep 2008 07:47:07 +0000 Subject: 259: Option.__normalize_scalar() now raises a TypeError if not isinstance(value, basestring); updated corresponding unit tests --- ipalib/public.py | 4 ++-- ipalib/tests/test_public.py | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index 6220acc0..cc385da1 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -125,8 +125,8 @@ class Option(plugable.ReadOnly): return self.__convert_scalar(value) def __normalize_scalar(self, value): - if type(value) is not self.type.type: - raise_TypeError(value, self.type.type, 'value') + if not isinstance(value, basestring): + raise_TypeError(value, basestring, 'value') return self.__normalize(value) def normalize(self, value): diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index c071832a..6adc393f 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -202,10 +202,10 @@ class test_Option(ClassChecker): # Scenario 2: multivalue=False, normalize=callback o = self.cls(name, doc, t, normalize=callback) - for v in (u'Hello', u'hello'): # Okay - assert o.normalize(v) == u'hello' - for v in [None, 'hello', (u'Hello',)]: # Not unicode - check_TypeError(v, unicode, 'value', o.normalize, v) + for v in (u'Hello', u'hello', 'Hello'): # Okay + assert o.normalize(v) == 'hello' + for v in [None, 42, (u'Hello',)]: # Not basestring + check_TypeError(v, basestring, 'value', o.normalize, v) # Scenario 3: multivalue=True, normalize=None o = self.cls(name, doc, t, multivalue=True) @@ -219,9 +219,9 @@ class test_Option(ClassChecker): assert o.normalize(value) == (u'hello',) for v in (None, u'Hello', [u'hello']): # Not tuple check_TypeError(v, tuple, 'value', o.normalize, v) - fail = 'Hello' # Not unicode + fail = 42 # Not basestring for v in [(fail,), (u'Hello', fail)]: # Non unicode member - check_TypeError(fail, unicode, 'value', o.normalize, v) + check_TypeError(fail, basestring, 'value', o.normalize, v) def test_validate(self): """ -- cgit From 915486dadc476df4915cefdfeb8d61c43664ca60 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 4 Sep 2008 08:16:12 +0000 Subject: 260: Option.normalize() now does same conversion for multivalue as Option.convert() does --- ipalib/cli.py | 15 ++++++++------- ipalib/public.py | 23 +++++++++++++++++------ ipalib/tests/test_public.py | 6 ++---- 3 files changed, 27 insertions(+), 17 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index a495924e..d199f721 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -26,6 +26,7 @@ import sys import code import optparse import public +import errors def to_cli(name): @@ -161,14 +162,14 @@ class CLI(object): while True: if error is not None: print '>>> %s: %s' % (option.name, error) - value = raw_input(prompt) - if default is not None and len(value) == 0: - value = default - if len(value) == 0: - error = 'Must supply a value' - else: - kw[option.name] = value + raw = raw_input(prompt) + try: + value = option(raw) + if value is not None: + kw[option.name] = value break + except errors.ValidationError, e: + error = e.error cmd(*args, **kw) diff --git a/ipalib/public.py b/ipalib/public.py index cc385da1..d1f2ff35 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -127,15 +127,18 @@ class Option(plugable.ReadOnly): def __normalize_scalar(self, value): if not isinstance(value, basestring): raise_TypeError(value, basestring, 'value') - return self.__normalize(value) + try: + return self.__normalize(value) + except Exception: + return value def normalize(self, value): if self.__normalize is None: return value if self.multivalue: - if type(value) is not tuple: - raise_TypeError(value, tuple, 'value') - return tuple(self.__normalize_scalar(v) for v in value) + if type(value) in (tuple, list): + return tuple(self.__normalize_scalar(v) for v in value) + return (self.__normalize_scalar(value),) # tuple return self.__normalize_scalar(value) def __validate_scalar(self, value, index=None): @@ -170,8 +173,16 @@ class Option(plugable.ReadOnly): return tuple() def __call__(self, value, **kw): - pass - + if value in ('', tuple(), []): + value = None + if value is None: + value = self.get_default(**kw) + if value is None: + if self.required: + raise RequirementError(option.name) + return None + else: + pass class Command(plugable.Plugin): diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index 6adc393f..6cdfc5af 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -215,12 +215,10 @@ class test_Option(ClassChecker): # Scenario 4: multivalue=True, normalize=callback o = self.cls(name, doc, t, multivalue=True, normalize=callback) - for value in [(u'Hello',), (u'hello',)]: # Okay + for value in [(u'Hello',), (u'hello',), 'Hello', ['Hello']]: # Okay assert o.normalize(value) == (u'hello',) - for v in (None, u'Hello', [u'hello']): # Not tuple - check_TypeError(v, tuple, 'value', o.normalize, v) fail = 42 # Not basestring - for v in [(fail,), (u'Hello', fail)]: # Non unicode member + for v in [fail, [fail], (u'Hello', fail)]: # Non unicode member check_TypeError(fail, basestring, 'value', o.normalize, v) def test_validate(self): -- cgit From 6f95249d5274258d0935a439407f38030205bf65 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 4 Sep 2008 08:33:41 +0000 Subject: 261: More work on demo using Option.__call__() for interactive input --- ipalib/cli.py | 2 +- ipalib/public.py | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index d199f721..abaef030 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -164,7 +164,7 @@ class CLI(object): print '>>> %s: %s' % (option.name, error) raw = raw_input(prompt) try: - value = option(raw) + value = option(raw, **kw) if value is not None: kw[option.name] = value break diff --git a/ipalib/public.py b/ipalib/public.py index d1f2ff35..7734bf7a 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -164,7 +164,10 @@ class Option(plugable.ReadOnly): if self.default_from is not None: default = self.default_from(**kw) if default is not None: - return self.convert(default) + try: + return self.convert(self.normalize(default)) + except errors.ValidationError: + return None return self.default def get_values(self): @@ -179,10 +182,12 @@ class Option(plugable.ReadOnly): value = self.get_default(**kw) if value is None: if self.required: - raise RequirementError(option.name) + raise errors.RequirementError(self.name) return None else: - pass + value = self.convert(self.normalize(value)) + self.validate(value) + return value class Command(plugable.Plugin): -- cgit From 86035c865514b1f1022bfe68813729ea08439de4 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 4 Sep 2008 08:39:27 +0000 Subject: 262: Fixed crossreference in public.Application docstring --- ipalib/public.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index 7734bf7a..5a70a715 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -421,7 +421,7 @@ class Application(Command): Special commands that only apply to a particular application built atop `ipalib` should subclass from ``Application``. - Because ``Application`` subclasses from `Command', plugins that subclass + Because ``Application`` subclasses from `Command`, plugins that subclass from ``Application`` with be available in both the ``api.Command`` and ``api.Application`` namespaces. """ -- cgit From 6b9ba734e119cbdc92ebc0a1b28d75b405d46bb0 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 4 Sep 2008 09:04:35 +0000 Subject: 263: CLI.print_commands() now seperates Command subclasses from Application subclasses --- ipalib/cli.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index abaef030..7ab0ae8d 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -99,12 +99,22 @@ class CLI(object): api = property(__get_api) def print_commands(self): - print 'Available Commands:' - for cmd in self.api.Command(): - print ' %s %s' % ( - to_cli(cmd.name).ljust(self.mcl), - cmd.doc, - ) + std = set(self.api.Command) - set(self.api.Application) + print '\nStandard IPA commands:' + for key in sorted(std): + cmd = self.api.Command[key] + self.print_cmd(cmd) + print '\nSpecial CLI commands:' + for cmd in self.api.Application(): + self.print_cmd(cmd) + + def print_cmd(self, cmd): + print ' %s %s' % ( + to_cli(cmd.name).ljust(self.mcl), + cmd.doc, + ) + + def __contains__(self, key): assert self.__d is not None, 'you must call finalize() first' @@ -134,7 +144,7 @@ class CLI(object): self.finalize() if len(sys.argv) < 2: self.print_commands() - print 'Usage: ipa COMMAND [ARGS]' + print '\nUsage: ipa COMMAND' sys.exit(2) key = sys.argv[1] if key not in self: -- cgit From 553b0c596d9dc1a955aece1fab28bd0cf3c81119 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 4 Sep 2008 09:22:18 +0000 Subject: 264: Cleaned up docstrings on all example plugins --- ipalib/cli.py | 4 ++-- ipalib/plugins/example.py | 22 +++++++++++----------- ipalib/plugins/override.py | 5 +++-- 3 files changed, 16 insertions(+), 15 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index 7ab0ae8d..25fbec02 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -47,7 +47,7 @@ def from_cli(cli_name): class help(public.Application): - 'Display help on command.' + 'Display help on a command.' def __call__(self, key): key = str(key) if key not in self.application: @@ -59,7 +59,7 @@ class help(public.Application): class console(public.Application): - 'Start the IPA Python console.' + 'Start the IPA interactive Python console.' def __call__(self): code.interact( diff --git a/ipalib/plugins/example.py b/ipalib/plugins/example.py index ab752976..4c62a5de 100644 --- a/ipalib/plugins/example.py +++ b/ipalib/plugins/example.py @@ -28,11 +28,11 @@ from ipalib import api # Hypothetical functional commands (not associated with any object): class krbtest(public.Command): - 'Test your Kerberos ticket' + 'Test your Kerberos ticket.' api.register(krbtest) class discover(public.Command): - 'Discover IPA servers on network' + 'Discover IPA servers on network.' api.register(discover) @@ -50,7 +50,7 @@ class user_mod(public.Method): api.register(user_mod) class user_find(public.Method): - 'Search for existing users.' + 'Search the users.' api.register(user_find) @@ -86,37 +86,37 @@ api.register(user_initials) # Register some methods for the 'group' object: class group_add(public.Method): - 'Add new group' + 'Add a new group.' api.register(group_add) class group_del(public.Method): - 'Delete existing group' + 'Delete an existing group.' api.register(group_del) class group_mod(public.Method): - 'Edit existing group' + 'Edit an existing group.' api.register(group_mod) class group_find(public.Method): - 'Search for groups' + 'Search the groups.' api.register(group_find) # Register some methods for the 'service' object class service_add(public.Method): - 'Add new service' + 'Add a new service.' api.register(service_add) class service_del(public.Method): - 'Delete existing service' + 'Delete an existing service.' api.register(service_del) class service_mod(public.Method): - 'Edit existing service' + 'Edit an existing service.' api.register(service_mod) class service_find(public.Method): - 'Search for services' + 'Search the services.' api.register(service_find) diff --git a/ipalib/plugins/override.py b/ipalib/plugins/override.py index bc5666c2..1255eae7 100644 --- a/ipalib/plugins/override.py +++ b/ipalib/plugins/override.py @@ -28,6 +28,7 @@ from ipalib import public from ipalib import api if 'user_mod' in api.register.Method: - class user_mod(api.register.Method.user_mod): - '(override) Edit existing user' + base = api.register.Method.user_mod + class user_mod(base): + 'Example override, see ipalib/plugins/override.py' api.register(user_mod, override=True) -- cgit From 22d9b8c078844127b8ea8217a9a9ed8f172afb99 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 4 Sep 2008 09:27:28 +0000 Subject: 265: Fixed small formatting error with use of CLI.print_commands() --- ipalib/cli.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index 25fbec02..5ead9837 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -107,6 +107,7 @@ class CLI(object): print '\nSpecial CLI commands:' for cmd in self.api.Application(): self.print_cmd(cmd) + print '' def print_cmd(self, cmd): print ' %s %s' % ( @@ -114,8 +115,6 @@ class CLI(object): cmd.doc, ) - - def __contains__(self, key): assert self.__d is not None, 'you must call finalize() first' return key in self.__d @@ -139,12 +138,11 @@ class CLI(object): (c.name.replace('_', '-'), c) for c in self.api.Command() ) - def run(self): self.finalize() if len(sys.argv) < 2: self.print_commands() - print '\nUsage: ipa COMMAND' + print 'Usage: ipa COMMAND' sys.exit(2) key = sys.argv[1] if key not in self: -- cgit From 641403278e00c30f24d9a6b4938b1e4ab3ecb427 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 4 Sep 2008 18:35:04 +0000 Subject: 266: Started work on new cli.print_api Command --- ipalib/cli.py | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index 5ead9837..05acad92 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -67,6 +67,9 @@ class console(public.Application): local=dict(api=self.api) ) +class print_api(public.Application): + 'Print details on the loaded plugins.' + class KWCollector(object): def __init__(self): @@ -127,6 +130,7 @@ class CLI(object): api = self.api api.register(help) api.register(console) + api.register(print_api) api.finalize() for a in api.Application(): a.set_application(self) -- cgit From e74713a076a72e75d6ca44d12df8500fb5cad8d2 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 8 Sep 2008 21:37:02 +0000 Subject: 267: Finished builtin CLI api command --- ipalib/cli.py | 37 ++++++++++++++++++++++++++++++++----- ipalib/plugable.py | 5 ++--- 2 files changed, 34 insertions(+), 8 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index 05acad92..e4de6031 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -27,6 +27,7 @@ import code import optparse import public import errors +import plugable def to_cli(name): @@ -70,6 +71,37 @@ class console(public.Application): class print_api(public.Application): 'Print details on the loaded plugins.' + def __call__(self): + lines = self.__traverse() + ml = max(len(l[1]) for l in lines) + for line in lines: + if line[0] == 0: + print '' + print '%s%s %r' % ( + ' ' * line[0], + line[1].ljust(ml), + line[2], + ) + + def __traverse(self): + lines = [] + for name in self.api: + namespace = self.api[name] + self.__traverse_namespace(name, namespace, lines) + return lines + + def __traverse_namespace(self, name, namespace, lines, tab=0): + lines.append((tab, name, namespace)) + for member_name in namespace: + member = namespace[member_name] + lines.append((tab + 1, member_name, member)) + if not hasattr(member, '__iter__'): + continue + for n in member: + attr = member[n] + if isinstance(attr, plugable.NameSpace): + self.__traverse_namespace(n, attr, lines, tab + 2) + class KWCollector(object): def __init__(self): @@ -89,7 +121,6 @@ class KWCollector(object): return dict(self.__d) - class CLI(object): __d = None __mcl = None @@ -184,8 +215,6 @@ class CLI(object): error = e.error cmd(*args, **kw) - - def parse(self, cmd, argv): parser = self.build_parser(cmd) (kwc, args) = parser.parse_args(argv, KWCollector()) @@ -202,8 +231,6 @@ class CLI(object): ) return parser - - def __get_mcl(self): """ Returns the Max Command Length. diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 9880b0a0..761d8a95 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -354,7 +354,7 @@ class Plugin(ReadOnly): Returns a fully qualified module_name.class_name() representation that could be used to construct this Plugin instance. """ - return '%s.%s()' % ( + return '%s.%s' % ( self.__class__.__module__, self.__class__.__name__ ) @@ -450,11 +450,10 @@ class PluginProxy(SetProxy): Returns a Python expression that could be used to construct this Proxy instance given the appropriate environment. """ - return '%s(%s, %r, %r)' % ( + return '%s(%s, %r)' % ( self.__class__.__name__, self.__base.__name__, self.__target, - self.__name_attr, ) -- cgit From 7d3d607b557c45c230c1b246386d65e4d07e6493 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 8 Sep 2008 21:40:46 +0000 Subject: 268: Fixed broken unit test for Plugin.__repr__() --- ipalib/tests/test_plugable.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'ipalib') diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index ec33989d..a7c4d61b 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -393,7 +393,7 @@ class test_Plugin(ClassChecker): api = 'the api instance' o = self.cls() assert read_only(o, 'name') == 'Plugin' - assert repr(o) == '%s.Plugin()' % plugable.__name__ + assert repr(o) == '%s.Plugin' % plugable.__name__ assert read_only(o, 'api') is None raises(AssertionError, o.finalize, None) o.finalize(api) @@ -404,7 +404,7 @@ class test_Plugin(ClassChecker): pass sub = some_plugin() assert read_only(sub, 'name') == 'some_plugin' - assert repr(sub) == '%s.some_plugin()' % __name__ + assert repr(sub) == '%s.some_plugin' % __name__ assert read_only(sub, 'api') is None raises(AssertionError, sub.finalize, None) sub.finalize(api) -- cgit From 03fd184e8e7459ac2a0c01c79259054f44721ca2 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 8 Sep 2008 21:42:48 +0000 Subject: 269: Renamed print_api command to show_plugins --- ipalib/cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index e4de6031..db3e0137 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -68,7 +68,7 @@ class console(public.Application): local=dict(api=self.api) ) -class print_api(public.Application): +class show_plugins(public.Application): 'Print details on the loaded plugins.' def __call__(self): @@ -161,7 +161,7 @@ class CLI(object): api = self.api api.register(help) api.register(console) - api.register(print_api) + api.register(show_plugins) api.finalize() for a in api.Application(): a.set_application(self) -- cgit From cb9c44270819c080f899fe7678f7f319686e681d Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 8 Sep 2008 21:44:53 +0000 Subject: 270: show-plugins now only shows namespaces with at least one member --- ipalib/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index db3e0137..048f49ff 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -99,7 +99,7 @@ class show_plugins(public.Application): continue for n in member: attr = member[n] - if isinstance(attr, plugable.NameSpace): + if isinstance(attr, plugable.NameSpace) and len(attr) > 0: self.__traverse_namespace(n, attr, lines, tab + 2) -- cgit From 13f030d91e378064291d2065b547047bb3f175e8 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 8 Sep 2008 21:51:05 +0000 Subject: 271: Improved __repr__ methods for better output from the show-plugins command --- ipalib/plugable.py | 2 +- ipalib/public.py | 8 ++++++++ ipalib/tests/test_plugable.py | 4 ++-- 3 files changed, 11 insertions(+), 3 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 761d8a95..438815bb 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -354,7 +354,7 @@ class Plugin(ReadOnly): Returns a fully qualified module_name.class_name() representation that could be used to construct this Plugin instance. """ - return '%s.%s' % ( + return '%s.%s()' % ( self.__class__.__module__, self.__class__.__name__ ) diff --git a/ipalib/public.py b/ipalib/public.py index 5a70a715..a5385a92 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -189,6 +189,14 @@ class Option(plugable.ReadOnly): self.validate(value) return value + def __repr__(self): + return '%s(%r, %r, %s)' % ( + self.__class__.__name__, + self.name, + self.doc, + self.type.name, + ) + class Command(plugable.Plugin): __public__ = frozenset(( diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index a7c4d61b..ec33989d 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -393,7 +393,7 @@ class test_Plugin(ClassChecker): api = 'the api instance' o = self.cls() assert read_only(o, 'name') == 'Plugin' - assert repr(o) == '%s.Plugin' % plugable.__name__ + assert repr(o) == '%s.Plugin()' % plugable.__name__ assert read_only(o, 'api') is None raises(AssertionError, o.finalize, None) o.finalize(api) @@ -404,7 +404,7 @@ class test_Plugin(ClassChecker): pass sub = some_plugin() assert read_only(sub, 'name') == 'some_plugin' - assert repr(sub) == '%s.some_plugin' % __name__ + assert repr(sub) == '%s.some_plugin()' % __name__ assert read_only(sub, 'api') is None raises(AssertionError, sub.finalize, None) sub.finalize(api) -- cgit From 21a0bab79ec9cddb98d6d3ab478ea48674eeda06 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 9 Sep 2008 01:41:15 +0000 Subject: 272: Add a quick positional arg experiment --- ipalib/cli.py | 27 ++++++++++++++++++++++++++- ipalib/public.py | 2 ++ 2 files changed, 28 insertions(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index 048f49ff..b2251432 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -28,6 +28,7 @@ import optparse import public import errors import plugable +import ipa_types def to_cli(name): @@ -49,6 +50,14 @@ def from_cli(cli_name): class help(public.Application): 'Display help on a command.' + + takes_args = ( + public.Option('command', 'The doc', ipa_types.Unicode(), + required=True, + multivalue=True, + ), + ) + def __call__(self, key): key = str(key) if key not in self.application: @@ -222,7 +231,7 @@ class CLI(object): def build_parser(self, cmd): parser = optparse.OptionParser( - usage='Usage: %%prog %s' % to_cli(cmd.name), + usage=self.get_usage(cmd), ) for option in cmd.Option(): parser.add_option('--%s' % to_cli(option.name), @@ -231,6 +240,22 @@ class CLI(object): ) return parser + def get_usage(self, cmd): + return ' '.join(self.get_usage_iter(cmd)) + + def get_usage_iter(self, cmd): + yield 'Usage: %%prog %s' % to_cli(cmd.name) + for arg in cmd.takes_args: + name = to_cli(arg.name).upper() + if arg.multivalue: + name = '%s...' % name + if arg.required: + yield name + else: + yield '[%s]' % name + + + def __get_mcl(self): """ Returns the Max Command Length. diff --git a/ipalib/public.py b/ipalib/public.py index a5385a92..90b3b88a 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -208,9 +208,11 @@ class Command(plugable.Plugin): '__call__', 'smart_option_order', 'Option', + 'takes_args', )) __Option = None options = tuple() + takes_args = tuple() def get_options(self): return self.options -- cgit From 97b01a48914fbed96b73fe8532bad3b4bd08027e Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 9 Sep 2008 21:18:44 +0000 Subject: 273: Added Command.get_args() method; added corresponding unit tests --- ipalib/public.py | 3 +++ ipalib/tests/test_public.py | 11 +++++++++++ 2 files changed, 14 insertions(+) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index 90b3b88a..b22eff0e 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -214,6 +214,9 @@ class Command(plugable.Plugin): options = tuple() takes_args = tuple() + def get_args(self): + return self.takes_args + def get_options(self): return self.options diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index 6cdfc5af..f9ff3a89 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -357,6 +357,17 @@ class test_Command(ClassChecker): assert self.cls.__bases__ == (plugable.Plugin,) assert self.cls.options == tuple() + def test_get_args(self): + """ + Tests the `public.Command.get_args` method. + """ + assert list(self.cls().get_args()) == [] + args = ('login', 'stuff') + class example(self.cls): + takes_args = args + o = example() + assert o.get_args() is args + def test_get_options(self): """ Tests the `public.Command.get_options` method. -- cgit From 0453aa465f8371aa4baea5c06adad42481553e0a Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 9 Sep 2008 23:10:49 +0000 Subject: 274: NameSpace.__init__() now takes sort=True keyword arument to allow for non-sorted NameSpaces; updated and improved NameSpace unit tests --- ipalib/plugable.py | 36 ++++++++++---- ipalib/tests/test_plugable.py | 107 ++++++++++++++++++------------------------ 2 files changed, 72 insertions(+), 71 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 438815bb..66e5fada 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -518,32 +518,50 @@ class NameSpace(DictProxy): True """ - def __init__(self, members): + def __init__(self, members, sort=True): """ :param members: An iterable providing the members. + :param sort: Whether to sort the members by member name. """ + self.__members = tuple(members) + self.__sort = check_type(sort, bool, 'sort') + names = (m.name for m in self.__members) + if self.__sort: + self.__names = tuple(sorted(names)) + else: + self.__names = tuple(names) super(NameSpace, self).__init__( - dict(self.__member_iter(members)) + dict(self.__member_iter()) ) - def __member_iter(self, members): + def __member_iter(self): """ Helper method called only from `NameSpace.__init__()`. - - :param members: Same iterable passed to `NameSpace.__init__()`. """ - for member in members: + for member in self.__members: name = check_name(member.name) assert not hasattr(self, name), 'already has attribute %r' % name setattr(self, name, member) yield (name, member) + def __iter__(self): + """ + Iterates through member names. + + In this instance was created with ``sort=True``, + """ + for name in self.__names: + yield name + def __repr__(self): """ - Returns pseudo-valid Python expression that could be used to construct - this NameSpace instance. + Returns a pseudo-valid expression that could create this instance. """ - return '%s(<%d members>)' % (self.__class__.__name__, len(self)) + return '%s(<%d members>, sort=%r)' % ( + self.__class__.__name__, + len(self), + self.__sort, + ) class Registrar(DictProxy): diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index ec33989d..62a78e36 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -556,6 +556,11 @@ def test_check_name(): for name in okay: raises(errors.NameSpaceError, f, name.upper()) +class DummyMember(object): + def __init__(self, i): + assert type(i) is int + self.name = 'member_%02d' % i + class test_NameSpace(ClassChecker): """ @@ -566,68 +571,46 @@ class test_NameSpace(ClassChecker): def test_class(self): assert self.cls.__bases__ == (plugable.DictProxy,) - def test_namespace(self): - class base(object): - __public__ = frozenset(( - 'plusplus', - )) - doc = 'doc' - - def plusplus(self, n): - return n + 1 - - class plugin(base): - def __init__(self, name): - self.name = name - - def get_name(i): - return 'noun_verb%d' % i - - def get_proxies(n): - for i in xrange(n): - yield plugable.PluginProxy(base, plugin(get_name(i))) - - cnt = 10 - ns = self.cls(get_proxies(cnt)) - assert ns.__islocked__() is True - - # Test __len__ - assert len(ns) == cnt - - # Test __iter__ - i = None - for (i, key) in enumerate(ns): - assert type(key) is str - assert key == get_name(i) - assert i == cnt - 1 - - # Test __call__ - i = None - for (i, proxy) in enumerate(ns()): - assert type(proxy) is plugable.PluginProxy - assert proxy.name == get_name(i) - assert i == cnt - 1 - - # Test __contains__, __getitem__, getattr(): - proxies = frozenset(ns()) - for i in xrange(cnt): - name = get_name(i) - assert name in ns - proxy = ns[name] - assert proxy.name == name - assert type(proxy) is plugable.PluginProxy - assert proxy in proxies - assert read_only(ns, name) is proxy - - # Test dir(): - assert set(get_name(i) for i in xrange(cnt)).issubset(dir(ns)) - - # Test that KeyError, AttributeError is raised: - name = get_name(cnt) - assert name not in ns - raises(KeyError, getitem, ns, name) - raises(AttributeError, getattr, ns, name) - no_set(ns, name) + def test_init(self): + """ + Tests the `plugable.NameSpace.__init__` method. + """ + o = self.cls(tuple()) + assert list(o) == [] + assert list(o()) == [] + for cnt in (10, 25): + members = tuple(DummyMember(cnt - i) for i in xrange(cnt)) + names = tuple(m.name for m in members) + for sort in (True, False): + o = self.cls(members, sort=sort) + + # Test __len__: + assert len(o) == cnt + + # Test __contains__: + for name in names: + assert name in o + assert ('member_00') not in o + + # Test __iter__, __call__: + if sort: + assert tuple(o) == tuple(sorted(names)) + assert tuple(o()) == tuple( + sorted(members, key=lambda m: m.name) + ) + else: + assert tuple(o) == names + assert tuple(o()) == members + + # Test __getitem__, getattr: + for member in members: + name = member.name + assert o[name] is member + assert read_only(o, name) is member + + # Test __repr__: + assert repr(o) == \ + 'NameSpace(<%d members>, sort=%r)' % (cnt, sort) def test_Registrar(): -- cgit From 349fc660e796841a3d78b82bf4fa195a228da4c4 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 9 Sep 2008 23:46:16 +0000 Subject: 275: Added Command.__check_args(); added basic unit tests for Command.args instance attribute --- ipalib/public.py | 27 +++++++++++++++++++++++++++ ipalib/tests/test_public.py | 8 ++++++++ 2 files changed, 35 insertions(+) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index b22eff0e..003e0d72 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -214,12 +214,39 @@ class Command(plugable.Plugin): options = tuple() takes_args = tuple() + def __init__(self): + self.args = plugable.NameSpace(self.__check_args(), sort=False) + def get_args(self): return self.takes_args def get_options(self): return self.options + def __check_args(self): + optional = False + multivalue = False + for arg in self.get_args(): + if type(arg) is str: + arg = Option(arg, '', ipa_types.Unicode(), required=True) + elif not isinstance(arg, Option): + raise TypeError( + 'arg: need %r or %r; got %r' % (str, Option, arg) + ) + if optional and arg.required: + raise ValueError( + '%s: required argument after optional' % arg.name + ) + if multivalue: + raise ValueError( + '%s: only final argument can be multivalue' % arg.name + ) + if not arg.required: + optional = True + if arg.multivalue: + multivalue = True + yield arg + def __get_Option(self): """ Returns the NameSpace containing the Option instances. diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index f9ff3a89..2683e5dd 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -368,6 +368,14 @@ class test_Command(ClassChecker): o = example() assert o.get_args() is args + def test_args(self): + """ + Tests the ``Command.args`` instance attribute. + """ + ns = self.cls().args + assert type(ns) is plugable.NameSpace + assert len(ns) == 0 + def test_get_options(self): """ Tests the `public.Command.get_options` method. -- cgit From 0215bc8009d7e10f884032e4dfa9cece73c14961 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 10 Sep 2008 00:21:40 +0000 Subject: 276: Option.__init__(): doc is now 3rd kwarg instead of 2nd positional arg; updated unit tests and other affected code --- ipalib/cli.py | 2 +- ipalib/public.py | 11 ++++++----- ipalib/tests/test_public.py | 42 ++++++++++++++++++------------------------ 3 files changed, 25 insertions(+), 30 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index b2251432..a6bc0f1f 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -52,7 +52,7 @@ class help(public.Application): 'Display help on a command.' takes_args = ( - public.Option('command', 'The doc', ipa_types.Unicode(), + public.Option('command', ipa_types.Unicode(), required=True, multivalue=True, ), diff --git a/ipalib/public.py b/ipalib/public.py index 003e0d72..fb7a80b9 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -86,7 +86,8 @@ class DefaultFrom(plugable.ReadOnly): class Option(plugable.ReadOnly): - def __init__(self, name, doc, type_, + def __init__(self, name, type_, + doc='', required=False, multivalue=False, default=None, @@ -190,10 +191,9 @@ class Option(plugable.ReadOnly): return value def __repr__(self): - return '%s(%r, %r, %s)' % ( + return '%s(%r, %s())' % ( self.__class__.__name__, self.name, - self.doc, self.type.name, ) @@ -228,7 +228,7 @@ class Command(plugable.Plugin): multivalue = False for arg in self.get_args(): if type(arg) is str: - arg = Option(arg, '', ipa_types.Unicode(), required=True) + arg = Option(arg, ipa_types.Unicode(), required=True) elif not isinstance(arg, Option): raise TypeError( 'arg: need %r or %r; got %r' % (str, Option, arg) @@ -430,7 +430,8 @@ class Property(Attribute): self.__rules_iter(), key=lambda f: getattr(f, '__name__'), )) - self.option = Option(self.attr_name, self.doc, self.type, + self.option = Option(self.attr_name, self.type, + doc=self.doc, required=self.required, multivalue=self.multivalue, default=self.default, diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index 2683e5dd..73521e91 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -125,13 +125,12 @@ class test_Option(ClassChecker): Tests the `public.Option.__init__` method. """ name = 'sn' - doc = 'Last Name' type_ = ipa_types.Unicode() - o = self.cls(name, doc, type_) + o = self.cls(name, type_) assert o.__islocked__() is True assert read_only(o, 'name') is name - assert read_only(o, 'doc') is doc assert read_only(o, 'type') is type_ + assert read_only(o, 'doc') == '' assert read_only(o, 'required') is False assert read_only(o, 'multivalue') is False assert read_only(o, 'default') is None @@ -143,13 +142,12 @@ class test_Option(ClassChecker): Tests the `public.Option.convert` method. """ name = 'some_number' - doc = 'Some number' type_ = ipa_types.Int() okay = (7, 7L, 7.0, ' 7 ') fail = ('7.0', '7L', 'whatever', object) # Scenario 1: multivalue=False - o = self.cls(name, doc, type_) + o = self.cls(name, type_) e = raises(TypeError, o.convert, None) assert str(e) == 'value cannot be None' for value in okay: @@ -164,7 +162,7 @@ class test_Option(ClassChecker): assert e.index is None # Scenario 2: multivalue=True - o = self.cls(name, doc, type_, multivalue=True) + o = self.cls(name, type_, multivalue=True) for none in [None, (7, None)]: e = raises(TypeError, o.convert, none) assert str(e) == 'value cannot be None' @@ -189,32 +187,31 @@ class test_Option(ClassChecker): Tests the `public.Option.normalize` method. """ name = 'sn' - doc = 'User last name' t = ipa_types.Unicode() callback = lambda value: value.lower() values = (None, u'Hello', (u'Hello',), 'hello', ['hello']) # Scenario 1: multivalue=False, normalize=None - o = self.cls(name, doc, t) + o = self.cls(name, t) for v in values: # When normalize=None, value is returned, no type checking: assert o.normalize(v) is v # Scenario 2: multivalue=False, normalize=callback - o = self.cls(name, doc, t, normalize=callback) + o = self.cls(name, t, normalize=callback) for v in (u'Hello', u'hello', 'Hello'): # Okay assert o.normalize(v) == 'hello' for v in [None, 42, (u'Hello',)]: # Not basestring check_TypeError(v, basestring, 'value', o.normalize, v) # Scenario 3: multivalue=True, normalize=None - o = self.cls(name, doc, t, multivalue=True) + o = self.cls(name, t, multivalue=True) for v in values: # When normalize=None, value is returned, no type checking: assert o.normalize(v) is v # Scenario 4: multivalue=True, normalize=callback - o = self.cls(name, doc, t, multivalue=True, normalize=callback) + o = self.cls(name, t, multivalue=True, normalize=callback) for value in [(u'Hello',), (u'hello',), 'Hello', ['Hello']]: # Okay assert o.normalize(value) == (u'hello',) fail = 42 # Not basestring @@ -226,7 +223,6 @@ class test_Option(ClassChecker): Tests the `public.Option.validate` method. """ name = 'sn' - doc = 'User last name' type_ = ipa_types.Unicode() def case_rule(value): if not value.islower(): @@ -237,7 +233,7 @@ class test_Option(ClassChecker): fail_type = 'whatever' # Scenario 1: multivalue=False - o = self.cls(name, doc, type_, rules=my_rules) + o = self.cls(name, type_, rules=my_rules) assert o.rules == (type_.validate, case_rule) o.validate(okay) e = raises(errors.RuleError, o.validate, fail_case) @@ -249,7 +245,7 @@ class test_Option(ClassChecker): check_TypeError(fail_type, unicode, 'value', o.validate, fail_type) ## Scenario 2: multivalue=True - o = self.cls(name, doc, type_, multivalue=True, rules=my_rules) + o = self.cls(name, type_, multivalue=True, rules=my_rules) o.validate((okay,)) cnt = 5 for i in xrange(cnt): @@ -272,7 +268,6 @@ class test_Option(ClassChecker): Tests the `public.Option.get_default` method. """ name = 'greeting' - doc = 'User greeting' type_ = ipa_types.Unicode() default = u'Hello, world!' default_from = public.DefaultFrom( @@ -281,7 +276,7 @@ class test_Option(ClassChecker): ) # Scenario 1: multivalue=False - o = self.cls(name, doc, type_, + o = self.cls(name, type_, default=default, default_from=default_from, ) @@ -292,7 +287,7 @@ class test_Option(ClassChecker): # Scenario 2: multivalue=True default = (default,) - o = self.cls(name, doc, type_, + o = self.cls(name, type_, default=default, default_from=default_from, multivalue=True, @@ -307,11 +302,10 @@ class test_Option(ClassChecker): Tests the `public.Option.get_values` method. """ name = 'status' - doc = 'Account status' values = (u'Active', u'Inactive') - o = self.cls(name, doc, ipa_types.Unicode()) + o = self.cls(name, ipa_types.Unicode()) assert o.get_values() == tuple() - o = self.cls(name, doc, ipa_types.Enum(*values)) + o = self.cls(name, ipa_types.Enum(*values)) assert o.get_values() == values @@ -339,12 +333,12 @@ class test_Command(ClassChecker): class example(self.cls): options = ( - public.Option('option0', 'Option zero', type_, + public.Option('option0', type_, normalize=normalize, default_from=default_from, rules=(Rule('option0'),) ), - public.Option('option1', 'Option one', type_, + public.Option('option1', type_, normalize=normalize, default_from=default_from, rules=(Rule('option1'),), @@ -652,8 +646,8 @@ class test_Method(ClassChecker): type_ = ipa_types.Unicode() class noun_verb(self.cls): options= ( - public.Option('option0', 'Option zero', type_), - public.Option('option1', 'Option one', type_), + public.Option('option0', type_), + public.Option('option1', type_), ) obj = example_obj() return noun_verb -- cgit From 51b639595858c8395f3beb01659ffe0ea69aaf8b Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 10 Sep 2008 01:03:59 +0000 Subject: 277: Added public.generate_argument() function; added corresponding unit tests --- ipalib/public.py | 15 +++++++++++++++ ipalib/tests/test_public.py | 24 ++++++++++++++++++++++++ 2 files changed, 39 insertions(+) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index fb7a80b9..772490d0 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -198,6 +198,21 @@ class Option(plugable.ReadOnly): ) +def generate_argument(name): + """ + Returns an `Option` instance using argument ``name``. + """ + if name.endswith('?'): + kw = dict(required=False, multivalue=False) + elif name.endswith('*'): + kw = dict(required=False, multivalue=True) + elif name.endswith('+'): + kw = dict(required=True, multivalue=True) + else: + kw = dict(required=True, multivalue=False) + return Option(name.rstrip('?*+'), ipa_types.Unicode(), **kw) + + class Command(plugable.Plugin): __public__ = frozenset(( 'get_default', diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index 73521e91..3841d384 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -309,6 +309,30 @@ class test_Option(ClassChecker): assert o.get_values() == values +def test_generate_argument(): + """ + Tests the `public.generate_argument` function. + """ + f = public.generate_argument + for name in ['arg', 'arg?', 'arg*', 'arg+']: + o = f(name) + assert type(o) is public.Option + assert type(o.type) is ipa_types.Unicode + assert o.name == 'arg' + o = f('arg') + assert o.required is True + assert o.multivalue is False + o = f('arg?') + assert o.required is False + assert o.multivalue is False + o = f('arg*') + assert o.required is False + assert o.multivalue is True + o = f('arg+') + assert o.required is True + assert o.multivalue is True + + class test_Command(ClassChecker): """ Tests the `public.Command` class. -- cgit From 0d3be2f421c3cd4044c4d7616d9426ac58a71ce8 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 10 Sep 2008 01:54:48 +0000 Subject: 278: Completed unit tests for Command.args instance attribute --- ipalib/public.py | 9 ++++++--- ipalib/tests/test_public.py | 22 ++++++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index 772490d0..99c51999 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -204,13 +204,16 @@ def generate_argument(name): """ if name.endswith('?'): kw = dict(required=False, multivalue=False) + name = name[:-1] elif name.endswith('*'): kw = dict(required=False, multivalue=True) + name = name[:-1] elif name.endswith('+'): kw = dict(required=True, multivalue=True) + name = name[:-1] else: kw = dict(required=True, multivalue=False) - return Option(name.rstrip('?*+'), ipa_types.Unicode(), **kw) + return Option(name, ipa_types.Unicode(), **kw) class Command(plugable.Plugin): @@ -223,7 +226,7 @@ class Command(plugable.Plugin): '__call__', 'smart_option_order', 'Option', - 'takes_args', + 'args', )) __Option = None options = tuple() @@ -243,7 +246,7 @@ class Command(plugable.Plugin): multivalue = False for arg in self.get_args(): if type(arg) is str: - arg = Option(arg, ipa_types.Unicode(), required=True) + arg = generate_argument(arg) elif not isinstance(arg, Option): raise TypeError( 'arg: need %r or %r; got %r' % (str, Option, arg) diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index 3841d384..93331f94 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -374,6 +374,7 @@ class test_Command(ClassChecker): def test_class(self): assert self.cls.__bases__ == (plugable.Plugin,) assert self.cls.options == tuple() + assert self.cls.takes_args == tuple() def test_get_args(self): """ @@ -386,13 +387,34 @@ class test_Command(ClassChecker): o = example() assert o.get_args() is args + def __get_instance(self, args=tuple(), options=tuple()): + class example(self.cls): + takes_args = args + takes_options = options + return example() + def test_args(self): """ Tests the ``Command.args`` instance attribute. """ + assert 'args' in self.cls.__public__ # Public ns = self.cls().args assert type(ns) is plugable.NameSpace assert len(ns) == 0 + args = ('destination', 'source?') + ns = self.__get_instance(args=args).args + assert type(ns) is plugable.NameSpace + assert len(ns) == len(args) + assert list(ns) == ['destination', 'source'] + assert type(ns.destination) is public.Option + assert ns.destination.required is True + assert ns.destination.multivalue is False + assert ns.source.required is False + assert ns.source.multivalue is False + + # Test type error: + e = raises(TypeError, self.__get_instance, args=(u'whatever',)) + #assert str(e) == 'arg: need %r or %r; got %r' % (str, public.Option, def test_get_options(self): """ -- cgit From 8062075f847199157910114588ea3c27874bdf35 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 10 Sep 2008 02:02:26 +0000 Subject: 279: Fixed cli and public.Method re new Command.args attribute --- ipalib/cli.py | 11 ++--------- ipalib/public.py | 4 ++++ 2 files changed, 6 insertions(+), 9 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index a6bc0f1f..25a0a5b8 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -51,12 +51,7 @@ def from_cli(cli_name): class help(public.Application): 'Display help on a command.' - takes_args = ( - public.Option('command', ipa_types.Unicode(), - required=True, - multivalue=True, - ), - ) + takes_args = ['command'] def __call__(self, key): key = str(key) @@ -245,7 +240,7 @@ class CLI(object): def get_usage_iter(self, cmd): yield 'Usage: %%prog %s' % to_cli(cmd.name) - for arg in cmd.takes_args: + for arg in cmd.args(): name = to_cli(arg.name).upper() if arg.multivalue: name = '%s...' % name @@ -254,8 +249,6 @@ class CLI(object): else: yield '[%s]' % name - - def __get_mcl(self): """ Returns the Max Command Length. diff --git a/ipalib/public.py b/ipalib/public.py index 99c51999..437531d6 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -420,6 +420,10 @@ class Attribute(plugable.Plugin): class Method(Attribute, Command): __public__ = Attribute.__public__.union(Command.__public__) + def __init__(self): + Attribute.__init__(self) + Command.__init__(self) + def get_options(self): for option in self.options: yield option -- cgit From cbfacf7c2ee7387dc95494c6231fd9b256bb68cd Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 10 Sep 2008 14:46:20 +0000 Subject: 280: Renamed Options.options to takes_options; updated related unit tests --- ipalib/public.py | 6 +++--- ipalib/tests/test_public.py | 46 ++++++++++++++++++++++----------------------- 2 files changed, 26 insertions(+), 26 deletions(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index 437531d6..7f1929f4 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -229,7 +229,7 @@ class Command(plugable.Plugin): 'args', )) __Option = None - options = tuple() + takes_options = tuple() takes_args = tuple() def __init__(self): @@ -239,7 +239,7 @@ class Command(plugable.Plugin): return self.takes_args def get_options(self): - return self.options + return self.takes_options def __check_args(self): optional = False @@ -425,7 +425,7 @@ class Method(Attribute, Command): Command.__init__(self) def get_options(self): - for option in self.options: + for option in self.takes_options: yield option if self.obj is not None and self.obj.Property is not None: for proxy in self.obj.Property(): diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index 93331f94..2c9fbce8 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -356,7 +356,7 @@ class test_Command(ClassChecker): type_ = ipa_types.Unicode() class example(self.cls): - options = ( + takes_options = ( public.Option('option0', type_, normalize=normalize, default_from=default_from, @@ -373,25 +373,35 @@ class test_Command(ClassChecker): def test_class(self): assert self.cls.__bases__ == (plugable.Plugin,) - assert self.cls.options == tuple() + assert self.cls.takes_options == tuple() assert self.cls.takes_args == tuple() + def __get_instance(self, args=tuple(), options=tuple()): + """ + Helper method used to test args and options. + """ + class example(self.cls): + takes_args = args + takes_options = options + return example() + def test_get_args(self): """ Tests the `public.Command.get_args` method. """ assert list(self.cls().get_args()) == [] args = ('login', 'stuff') - class example(self.cls): - takes_args = args - o = example() + o = self.__get_instance(args=args) assert o.get_args() is args - def __get_instance(self, args=tuple(), options=tuple()): - class example(self.cls): - takes_args = args - takes_options = options - return example() + def test_get_options(self): + """ + Tests the `public.Command.get_options` method. + """ + assert list(self.cls().get_options()) == [] + options = ('verbose', 'debug') + o = self.__get_instance(options=options) + assert o.get_options() is options def test_args(self): """ @@ -414,18 +424,8 @@ class test_Command(ClassChecker): # Test type error: e = raises(TypeError, self.__get_instance, args=(u'whatever',)) - #assert str(e) == 'arg: need %r or %r; got %r' % (str, public.Option, - - def test_get_options(self): - """ - Tests the `public.Command.get_options` method. - """ - assert list(self.cls().get_options()) == [] - sub = self.subcls() - for (i, option) in enumerate(sub.get_options()): - assert isinstance(option, public.Option) - assert read_only(option, 'name') == 'option%d' % i - assert i == 1 + assert str(e) == \ + 'arg: need %r or %r; got %r' % (str, public.Option, u'whatever') def test_Option(self): """ @@ -691,7 +691,7 @@ class test_Method(ClassChecker): Property = property(__get_prop) type_ = ipa_types.Unicode() class noun_verb(self.cls): - options= ( + takes_options= ( public.Option('option0', type_), public.Option('option1', type_), ) -- cgit From 2d85a6daa3798ee5feda1b2bda33300b73bed615 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 10 Sep 2008 14:54:01 +0000 Subject: 281: Completed unit tests for Command.args --- ipalib/tests/test_public.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index 2c9fbce8..7c9a0244 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -422,11 +422,19 @@ class test_Command(ClassChecker): assert ns.source.required is False assert ns.source.multivalue is False - # Test type error: + # Test TypeError: e = raises(TypeError, self.__get_instance, args=(u'whatever',)) assert str(e) == \ 'arg: need %r or %r; got %r' % (str, public.Option, u'whatever') + # Test ValueError, required after optional: + e = raises(ValueError, self.__get_instance, args=('arg1?', 'arg2')) + assert str(e) == 'arg2: required argument after optional' + + # Test ValueError, scalar after multivalue: + e = raises(ValueError, self.__get_instance, args=('arg1+', 'arg2')) + assert str(e) == 'arg2: only final argument can be multivalue' + def test_Option(self): """ Tests the `public.Command.Option` property. -- cgit From 7de450363bc56a747a495803d56cc7c4d1323293 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 10 Sep 2008 15:14:26 +0000 Subject: 282: Added Command.__check_options() method; added unit tests for Command.options instance attribute --- ipalib/public.py | 12 ++++++++++++ ipalib/tests/test_public.py | 21 +++++++++++++++++++++ 2 files changed, 33 insertions(+) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index 7f1929f4..7dfcd176 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -227,6 +227,7 @@ class Command(plugable.Plugin): 'smart_option_order', 'Option', 'args', + 'options', )) __Option = None takes_options = tuple() @@ -234,6 +235,7 @@ class Command(plugable.Plugin): def __init__(self): self.args = plugable.NameSpace(self.__check_args(), sort=False) + self.options = plugable.NameSpace(self.__check_options(), sort=False) def get_args(self): return self.takes_args @@ -265,6 +267,16 @@ class Command(plugable.Plugin): multivalue = True yield arg + def __check_options(self): + for option in self.get_options(): + if type(option) is str: + option = generate_argument(option) + elif not isinstance(option, Option): + raise TypeError( + 'option: need %r or %r; got %r' % (str, Option, option) + ) + yield option + def __get_Option(self): """ Returns the NameSpace containing the Option instances. diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index 7c9a0244..1f2efe4b 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -417,6 +417,7 @@ class test_Command(ClassChecker): assert len(ns) == len(args) assert list(ns) == ['destination', 'source'] assert type(ns.destination) is public.Option + assert type(ns.source) is public.Option assert ns.destination.required is True assert ns.destination.multivalue is False assert ns.source.required is False @@ -435,6 +436,26 @@ class test_Command(ClassChecker): e = raises(ValueError, self.__get_instance, args=('arg1+', 'arg2')) assert str(e) == 'arg2: only final argument can be multivalue' + def test_options(self): + """ + Tests the ``Command.options`` instance attribute. + """ + assert 'options' in self.cls.__public__ # Public + ns = self.cls().options + assert type(ns) is plugable.NameSpace + assert len(ns) == 0 + options = ('target', 'files*') + ns = self.__get_instance(options=options).options + assert type(ns) is plugable.NameSpace + assert len(ns) == len(options) + assert list(ns) == ['target', 'files'] + assert type(ns.target) is public.Option + assert type(ns.files) is public.Option + assert ns.target.required is True + assert ns.target.multivalue is False + assert ns.files.required is False + assert ns.files.multivalue is True + def test_Option(self): """ Tests the `public.Command.Option` property. -- cgit From bde377a2da7bb264ee3188a5696bb389af51321d Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 10 Sep 2008 15:16:17 +0000 Subject: 283: Renamed generate_argument() to generate_option() --- ipalib/public.py | 8 ++++---- ipalib/tests/test_public.py | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index 7dfcd176..f20ae6d3 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -198,9 +198,9 @@ class Option(plugable.ReadOnly): ) -def generate_argument(name): +def generate_option(name): """ - Returns an `Option` instance using argument ``name``. + Returns an `Option` instance by parsing ``name``. """ if name.endswith('?'): kw = dict(required=False, multivalue=False) @@ -248,7 +248,7 @@ class Command(plugable.Plugin): multivalue = False for arg in self.get_args(): if type(arg) is str: - arg = generate_argument(arg) + arg = generate_option(arg) elif not isinstance(arg, Option): raise TypeError( 'arg: need %r or %r; got %r' % (str, Option, arg) @@ -270,7 +270,7 @@ class Command(plugable.Plugin): def __check_options(self): for option in self.get_options(): if type(option) is str: - option = generate_argument(option) + option = generate_option(option) elif not isinstance(option, Option): raise TypeError( 'option: need %r or %r; got %r' % (str, Option, option) diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index 1f2efe4b..f805a7a9 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -309,11 +309,11 @@ class test_Option(ClassChecker): assert o.get_values() == values -def test_generate_argument(): +def test_generate_option(): """ - Tests the `public.generate_argument` function. + Tests the `public.generate_option` function. """ - f = public.generate_argument + f = public.generate_option for name in ['arg', 'arg?', 'arg*', 'arg+']: o = f(name) assert type(o) is public.Option -- cgit From 687f60356203a33b7af24842f24570a12d9b2039 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 10 Sep 2008 15:31:34 +0000 Subject: 284: Removed depreciated Command.Option property; removed corresponding unit tests; updated affected code --- ipalib/cli.py | 2 +- ipalib/public.py | 25 +++++++------------------ ipalib/tests/test_public.py | 16 ---------------- 3 files changed, 8 insertions(+), 35 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index 25a0a5b8..54693ffd 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -228,7 +228,7 @@ class CLI(object): parser = optparse.OptionParser( usage=self.get_usage(cmd), ) - for option in cmd.Option(): + for option in cmd.options(): parser.add_option('--%s' % to_cli(option.name), metavar=option.type.name.upper(), help=option.doc, diff --git a/ipalib/public.py b/ipalib/public.py index f20ae6d3..c44d039d 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -277,21 +277,10 @@ class Command(plugable.Plugin): ) yield option - def __get_Option(self): - """ - Returns the NameSpace containing the Option instances. - """ - if self.__Option is None: - object.__setattr__(self, '_Command__Option', - plugable.NameSpace(self.get_options()), - ) - return self.__Option - Option = property(__get_Option) - def __convert_iter(self, kw): for (key, value) in kw.iteritems(): - if key in self.Option: - yield (key, self.Option[key].convert(value)) + if key in self.options: + yield (key, self.options[key].convert(value)) else: yield (key, value) @@ -300,8 +289,8 @@ class Command(plugable.Plugin): def __normalize_iter(self, kw): for (key, value) in kw.iteritems(): - if key in self.Option: - yield (key, self.Option[key].normalize(value)) + if key in self.options: + yield (key, self.options[key].normalize(value)) else: yield (key, value) @@ -309,7 +298,7 @@ class Command(plugable.Plugin): return dict(self.__normalize_iter(kw)) def __get_default_iter(self, kw): - for option in self.Option(): + for option in self.options(): if option.name not in kw: value = option.get_default(**kw) if value is not None: @@ -321,7 +310,7 @@ class Command(plugable.Plugin): def validate(self, **kw): self.print_call('validate', kw, 1) - for option in self.Option(): + for option in self.options(): value = kw.get(option.name, None) if value is not None: option.validate(value) @@ -355,7 +344,7 @@ class Command(plugable.Plugin): return 0 return 1 return 2 - for option in sorted(self.Option(), key=get_key): + for option in sorted(self.options(), key=get_key): yield option diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index f805a7a9..5dcbd84c 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -456,22 +456,6 @@ class test_Command(ClassChecker): assert ns.files.required is False assert ns.files.multivalue is True - def test_Option(self): - """ - Tests the `public.Command.Option` property. - """ - assert 'Option' in self.cls.__public__ # Public - sub = self.subcls() - O = sub.Option - assert type(O) is plugable.NameSpace - assert len(O) == 2 - for name in ('option0', 'option1'): - assert name in O - option = O[name] - assert getattr(O, name) is option - assert isinstance(option, public.Option) - assert option.name == name - def test_convert(self): """ Tests the `public.Command.convert` method. -- cgit From 100492d98a199169a985086c20746dea7ff1fd3e Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 10 Sep 2008 20:05:45 +0000 Subject: 285: Started work on Command.args_to_kw() method; added unit test for functionality so far in args_to_kw() --- ipalib/errors.py | 13 +++++++++++++ ipalib/public.py | 29 +++++++++++++++++++++++++++++ ipalib/tests/test_public.py | 21 +++++++++++++++++++++ 3 files changed, 63 insertions(+) (limited to 'ipalib') diff --git a/ipalib/errors.py b/ipalib/errors.py index 5e8af9d4..a961ecb6 100644 --- a/ipalib/errors.py +++ b/ipalib/errors.py @@ -107,6 +107,19 @@ class IPAError(Exception): return self.format % self.args +class ArgumentError(IPAError): + """ + Raised when a command is called with wrong number of arguments. + """ + + format = '%s %s' + + def __init__(self, command, error): + self.command = command + self.error = error + IPAError.__init__(self, command.name, error) + + class ValidationError(IPAError): """ Base class for all types of validation errors. diff --git a/ipalib/public.py b/ipalib/public.py index c44d039d..8ca97a36 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -236,6 +236,9 @@ class Command(plugable.Plugin): def __init__(self): self.args = plugable.NameSpace(self.__check_args(), sort=False) self.options = plugable.NameSpace(self.__check_options(), sort=False) + self.params = plugable.NameSpace( + tuple(self.args()) + tuple(self.options()), sort=False + ) def get_args(self): return self.takes_args @@ -347,6 +350,32 @@ class Command(plugable.Plugin): for option in sorted(self.options(), key=get_key): yield option + def args_to_kw(self, *args): + Args = tuple(self.args()) + if len(args) > len(Args): + if len(Args) > 0 and not Args[-1].multivalue: + if len(Args) == 1: + error = 'takes at most 1 argument' + else: + error = 'takes at most %d arguments' % len(Args) + raise errors.ArgumentError(self, error) + else: + raise errors.ArgumentError(self, 'takes no arguments') + MinArgs = sum(int(A.required) for A in Args) + if len(args) < MinArgs: + if MinArgs == 1: + error = 'takes at least 1 argument' + else: + error = 'takes at least %d arguments' % MinArgs + raise errors.ArgumentError(self, error) + for (i, Arg) in enumerate(Args): + pass + + + + + + class Object(plugable.Plugin): diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index 5dcbd84c..6662b9d0 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -550,6 +550,27 @@ class test_Command(ClassChecker): """ assert 'execute' in self.cls.__public__ # Public + def test_args_to_kw(self): + o = self.__get_instance() + e = raises(errors.ArgumentError, o.args_to_kw, 1) + assert str(e) == 'example takes no arguments' + + o = self.__get_instance(args=('one?',)) + e = raises(errors.ArgumentError, o.args_to_kw, 1, 2) + assert str(e) == 'example takes at most 1 argument' + + o = self.__get_instance(args=('one', 'two?')) + e = raises(errors.ArgumentError, o.args_to_kw, 1, 2, 3) + assert str(e) == 'example takes at most 2 arguments' + + o = self.__get_instance(args=('one', 'two?')) + e = raises(errors.ArgumentError, o.args_to_kw) + assert str(e) == 'example takes at least 1 argument' + + o = self.__get_instance(args=('one', 'two', 'three?')) + e = raises(errors.ArgumentError, o.args_to_kw, 1) + assert str(e) == 'example takes at least 2 arguments' + class test_Object(ClassChecker): """ -- cgit From 7c40226500daa4a27f4430ef7f94ec4520ab72e5 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 10 Sep 2008 21:34:29 +0000 Subject: 286: Finished Command.args_to_kw(); finished unit tests for args_to_kw() --- ipalib/public.py | 26 ++++++++++++++------------ ipalib/tests/test_public.py | 13 +++++++++++++ 2 files changed, 27 insertions(+), 12 deletions(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index 8ca97a36..4c0255ef 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -352,15 +352,14 @@ class Command(plugable.Plugin): def args_to_kw(self, *args): Args = tuple(self.args()) - if len(args) > len(Args): - if len(Args) > 0 and not Args[-1].multivalue: + if len(Args) == 0 and len(args) > 0: + raise errors.ArgumentError(self, 'takes no arguments') + if len(args) > len(Args) and not Args[-1].multivalue: if len(Args) == 1: error = 'takes at most 1 argument' else: error = 'takes at most %d arguments' % len(Args) raise errors.ArgumentError(self, error) - else: - raise errors.ArgumentError(self, 'takes no arguments') MinArgs = sum(int(A.required) for A in Args) if len(args) < MinArgs: if MinArgs == 1: @@ -368,14 +367,17 @@ class Command(plugable.Plugin): else: error = 'takes at least %d arguments' % MinArgs raise errors.ArgumentError(self, error) - for (i, Arg) in enumerate(Args): - pass - - - - - - + return dict(self.__args_to_kw_iter(args)) + + def __args_to_kw_iter(self, args): + for (i, Arg) in enumerate(self.args()): + if len(args) > i: + if Arg.multivalue: + yield (Arg.name, args[i:]) + else: + yield (Arg.name, args[i]) + else: + assert not Arg.required class Object(plugable.Plugin): diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index 6662b9d0..8bff5a19 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -551,6 +551,19 @@ class test_Command(ClassChecker): assert 'execute' in self.cls.__public__ # Public def test_args_to_kw(self): + o = self.__get_instance(args=('one', 'two?')) + assert o.args_to_kw(1) == dict(one=1) + assert o.args_to_kw(1, 2) == dict(one=1, two=2) + + o = self.__get_instance(args=('one', 'two*')) + assert o.args_to_kw(1) == dict(one=1) + assert o.args_to_kw(1, 2) == dict(one=1, two=(2,)) + assert o.args_to_kw(1, 2, 3) == dict(one=1, two=(2, 3)) + + o = self.__get_instance(args=('one', 'two+')) + assert o.args_to_kw(1, 2) == dict(one=1, two=(2,)) + assert o.args_to_kw(1, 2, 3) == dict(one=1, two=(2, 3)) + o = self.__get_instance() e = raises(errors.ArgumentError, o.args_to_kw, 1) assert str(e) == 'example takes no arguments' -- cgit From c17c5efb4e30c78bb53022e86f74b718dd49e758 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 10 Sep 2008 23:18:29 +0000 Subject: 287: Renamed Command.args_to_kw() to Command.group_args(), which now returns a tuple instead of dict; updated unit tests --- ipalib/public.py | 48 ++++++++++++++++++++++++--------------------- ipalib/tests/test_public.py | 26 ++++++++++++------------ 2 files changed, 39 insertions(+), 35 deletions(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index 4c0255ef..84d321be 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -350,34 +350,38 @@ class Command(plugable.Plugin): for option in sorted(self.options(), key=get_key): yield option - def args_to_kw(self, *args): - Args = tuple(self.args()) - if len(Args) == 0 and len(args) > 0: - raise errors.ArgumentError(self, 'takes no arguments') - if len(args) > len(Args) and not Args[-1].multivalue: - if len(Args) == 1: - error = 'takes at most 1 argument' - else: - error = 'takes at most %d arguments' % len(Args) - raise errors.ArgumentError(self, error) - MinArgs = sum(int(A.required) for A in Args) - if len(args) < MinArgs: - if MinArgs == 1: + def group_args(self, *values): + args = tuple(self.args()) + if len(args) == 0: + if len(values) > 0: + raise errors.ArgumentError(self, 'takes no arguments') + else: + return tuple() + if len(values) > len(args) and not args[-1].multivalue: + if len(args) == 1: + error = 'takes at most 1 argument' + else: + error = 'takes at most %d arguments' % len(args) + raise errors.ArgumentError(self, error) + min_args = sum(int(a.required) for a in args) + if len(values) < min_args: + if min_args == 1: error = 'takes at least 1 argument' else: - error = 'takes at least %d arguments' % MinArgs + error = 'takes at least %d arguments' % min_args raise errors.ArgumentError(self, error) - return dict(self.__args_to_kw_iter(args)) + return tuple(self.__group_args_iter(values, args)) - def __args_to_kw_iter(self, args): - for (i, Arg) in enumerate(self.args()): - if len(args) > i: - if Arg.multivalue: - yield (Arg.name, args[i:]) + def __group_args_iter(self, values, args): + for (i, arg) in enumerate(args): + if len(values) > i: + if arg.multivalue: + yield values[i:] else: - yield (Arg.name, args[i]) + yield values[i] else: - assert not Arg.required + assert not arg.required + yield None class Object(plugable.Plugin): diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index 8bff5a19..aac962b5 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -550,38 +550,38 @@ class test_Command(ClassChecker): """ assert 'execute' in self.cls.__public__ # Public - def test_args_to_kw(self): + def test_group_args(self): o = self.__get_instance(args=('one', 'two?')) - assert o.args_to_kw(1) == dict(one=1) - assert o.args_to_kw(1, 2) == dict(one=1, two=2) + assert o.group_args(1) == (1, None) + assert o.group_args(1, 2) == (1, 2) o = self.__get_instance(args=('one', 'two*')) - assert o.args_to_kw(1) == dict(one=1) - assert o.args_to_kw(1, 2) == dict(one=1, two=(2,)) - assert o.args_to_kw(1, 2, 3) == dict(one=1, two=(2, 3)) + assert o.group_args(1) == (1, None) + assert o.group_args(1, 2) == (1, (2,)) + assert o.group_args(1, 2, 3) == (1, (2, 3)) o = self.__get_instance(args=('one', 'two+')) - assert o.args_to_kw(1, 2) == dict(one=1, two=(2,)) - assert o.args_to_kw(1, 2, 3) == dict(one=1, two=(2, 3)) + assert o.group_args(1, 2) == (1, (2,)) + assert o.group_args(1, 2, 3) == (1, (2, 3)) o = self.__get_instance() - e = raises(errors.ArgumentError, o.args_to_kw, 1) + e = raises(errors.ArgumentError, o.group_args, 1) assert str(e) == 'example takes no arguments' o = self.__get_instance(args=('one?',)) - e = raises(errors.ArgumentError, o.args_to_kw, 1, 2) + e = raises(errors.ArgumentError, o.group_args, 1, 2) assert str(e) == 'example takes at most 1 argument' o = self.__get_instance(args=('one', 'two?')) - e = raises(errors.ArgumentError, o.args_to_kw, 1, 2, 3) + e = raises(errors.ArgumentError, o.group_args, 1, 2, 3) assert str(e) == 'example takes at most 2 arguments' o = self.__get_instance(args=('one', 'two?')) - e = raises(errors.ArgumentError, o.args_to_kw) + e = raises(errors.ArgumentError, o.group_args) assert str(e) == 'example takes at least 1 argument' o = self.__get_instance(args=('one', 'two', 'three?')) - e = raises(errors.ArgumentError, o.args_to_kw, 1) + e = raises(errors.ArgumentError, o.group_args, 1) assert str(e) == 'example takes at least 2 arguments' -- cgit From 23e251a605c8e7cc44450411b841141cc0979638 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 10 Sep 2008 23:33:36 +0000 Subject: 288: CLI now uses Command.group_args() to check for required arguments --- ipalib/cli.py | 8 ++++++++ ipalib/public.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index 54693ffd..594e2812 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -31,6 +31,10 @@ import plugable import ipa_types +def exit_error(error): + sys.exit('ipa: ERROR: %s' % error) + + def to_cli(name): """ Takes a Python identifier and transforms it into form suitable for the @@ -195,6 +199,10 @@ class CLI(object): def run_cmd(self, cmd, argv): (args, kw) = self.parse(cmd, argv) + try: + args = cmd.group_args(*args) + except errors.ArgumentError, e: + exit_error('%s %s' % (to_cli(cmd.name), e.error)) self.run_interactive(cmd, args, kw) def run_interactive(self, cmd, args, kw): diff --git a/ipalib/public.py b/ipalib/public.py index 84d321be..9f12a45b 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -225,9 +225,9 @@ class Command(plugable.Plugin): 'execute', '__call__', 'smart_option_order', - 'Option', 'args', 'options', + 'group_args', )) __Option = None takes_options = tuple() -- cgit From c1ef2d05e881c620d3565d717cfb23029e6e9f4e Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 11 Sep 2008 00:04:49 +0000 Subject: 289: Command.convert(), normalize(), and validate() now use self.params instead of self.options --- ipalib/public.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index 9f12a45b..088e65c5 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -282,8 +282,8 @@ class Command(plugable.Plugin): def __convert_iter(self, kw): for (key, value) in kw.iteritems(): - if key in self.options: - yield (key, self.options[key].convert(value)) + if key in self.params: + yield (key, self.params[key].convert(value)) else: yield (key, value) @@ -292,8 +292,8 @@ class Command(plugable.Plugin): def __normalize_iter(self, kw): for (key, value) in kw.iteritems(): - if key in self.options: - yield (key, self.options[key].normalize(value)) + if key in self.params: + yield (key, self.params[key].normalize(value)) else: yield (key, value) @@ -301,11 +301,11 @@ class Command(plugable.Plugin): return dict(self.__normalize_iter(kw)) def __get_default_iter(self, kw): - for option in self.options(): - if option.name not in kw: - value = option.get_default(**kw) + for param in self.params(): + if param.name not in kw: + value = param.get_default(**kw) if value is not None: - yield(option.name, value) + yield(param.name, value) def get_default(self, **kw): self.print_call('default', kw, 1) @@ -313,12 +313,12 @@ class Command(plugable.Plugin): def validate(self, **kw): self.print_call('validate', kw, 1) - for option in self.options(): - value = kw.get(option.name, None) + for param in self.params(): + value = kw.get(param.name, None) if value is not None: - option.validate(value) - elif option.required: - raise errors.RequirementError(option.name) + param.validate(value) + elif param.required: + raise errors.RequirementError(param.name) def execute(self, **kw): self.print_call('execute', kw, 1) -- cgit From 0e60036bb4db8cf505a3f1009023a09ca2ffe0a1 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 12 Sep 2008 16:36:04 +0000 Subject: 290: Applyied Rob's patch --- ipalib/conn.py | 72 ++++++ ipalib/ipaldap.py | 627 ++++++++++++++++++++++++++++++++++++++++++++++ ipalib/ipautil.py | 190 ++++++++++++++ ipalib/plugins/example.py | 9 +- ipalib/public.py | 2 +- ipalib/servercore.py | 148 +++++++++++ 6 files changed, 1046 insertions(+), 2 deletions(-) create mode 100644 ipalib/conn.py create mode 100644 ipalib/ipaldap.py create mode 100644 ipalib/ipautil.py create mode 100644 ipalib/servercore.py (limited to 'ipalib') diff --git a/ipalib/conn.py b/ipalib/conn.py new file mode 100644 index 00000000..f8f5306f --- /dev/null +++ b/ipalib/conn.py @@ -0,0 +1,72 @@ +# Authors: Rob Crittenden +# +# 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 +# + +import krbV +import threading +import ldap +import ldap.dn +from ipalib import ipaldap + +context = threading.local() + +class IPAConn: + def __init__(self, host, port, krbccache, debug=None): + self._conn = None + + # Save the arguments + self._host = host + self._port = port + self._krbccache = krbccache + self._debug = debug + + self._ctx = krbV.default_context() + + ccache = krbV.CCache(name=krbccache, context=self._ctx) + cprinc = ccache.principal() + + self._conn = ipaldap.IPAdmin(host,port,None,None,None,debug) + + # This will bind the connection + try: + self._conn.set_krbccache(krbccache, cprinc.name) + except ldap.UNWILLING_TO_PERFORM, e: + raise e + except Exception, e: + raise e + + def __del__(self): + # take no chances on unreleased connections + self.releaseConn() + + def getConn(self): + return self._conn + + def releaseConn(self): + if self._conn is None: + return + + self._conn.unbind_s() + self._conn = None + + return + +if __name__ == "__main__": + ipaconn = IPAConn("localhost", 389, "FILE:/tmp/krb5cc_500") + x = ipaconn.getConn().getEntry("dc=example,dc=com", ldap.SCOPE_SUBTREE, "uid=admin", ["cn"]) + print "%s" % x diff --git a/ipalib/ipaldap.py b/ipalib/ipaldap.py new file mode 100644 index 00000000..c1d134a0 --- /dev/null +++ b/ipalib/ipaldap.py @@ -0,0 +1,627 @@ +# Authors: Rich Megginson +# Rob Crittenden 0 + + def hasAttr(self,name): + """Return True if this entry has an attribute named name, False otherwise""" + return self.data and self.data.has_key(name) + + def __getattr__(self,name): + """If name is the name of an LDAP attribute, return the first value for that + attribute - equivalent to getValue - this allows the use of + entry.cn + instead of + entry.getValue('cn') + This also allows us to return None if an attribute is not found rather than + throwing an exception""" + return self.getValue(name) + + def getValues(self,name): + """Get the list (array) of values for the attribute named name""" + return self.data.get(name) + + def getValue(self,name): + """Get the first value for the attribute named name""" + return self.data.get(name,[None])[0] + + def setValue(self,name,*value): + """Value passed in may be a single value, several values, or a single sequence. + For example: + ent.setValue('name', 'value') + ent.setValue('name', 'value1', 'value2', ..., 'valueN') + ent.setValue('name', ['value1', 'value2', ..., 'valueN']) + ent.setValue('name', ('value1', 'value2', ..., 'valueN')) + Since *value is a tuple, we may have to extract a list or tuple from that + tuple as in the last two examples above""" + if isinstance(value[0],list) or isinstance(value[0],tuple): + self.data[name] = value[0] + else: + self.data[name] = value + + setValues = setValue + + def toTupleList(self): + """Convert the attrs and values to a list of 2-tuples. The first element + of the tuple is the attribute name. The second element is either a + single value or a list of values.""" + return self.data.items() + + def __str__(self): + """Convert the Entry to its LDIF representation""" + return self.__repr__() + + # the ldif class base64 encodes some attrs which I would rather see in raw form - to + # encode specific attrs as base64, add them to the list below + ldif.safe_string_re = re.compile('^$') + base64_attrs = ['nsstate', 'krbprincipalkey', 'krbExtraData'] + + def __repr__(self): + """Convert the Entry to its LDIF representation""" + sio = cStringIO.StringIO() + # what's all this then? the unparse method will currently only accept + # a list or a dict, not a class derived from them. self.data is a + # cidict, so unparse barfs on it. I've filed a bug against python-ldap, + # but in the meantime, we have to convert to a plain old dict for printing + # I also don't want to see wrapping, so set the line width really high (1000) + newdata = {} + newdata.update(self.data) + ldif.LDIFWriter(sio,Entry.base64_attrs,1000).unparse(self.dn,newdata) + return sio.getvalue() + +def wrapper(f,name): + """This is the method that wraps all of the methods of the superclass. This seems + to need to be an unbound method, that's why it's outside of IPAdmin. Perhaps there + is some way to do this with the new classmethod or staticmethod of 2.4. + Basically, we replace every call to a method in SimpleLDAPObject (the superclass + of IPAdmin) with a call to inner. The f argument to wrapper is the bound method + of IPAdmin (which is inherited from the superclass). Bound means that it will implicitly + be called with the self argument, it is not in the args list. name is the name of + the method to call. If name is a method that returns entry objects (e.g. result), + we wrap the data returned by an Entry class. If name is a method that takes an entry + argument, we extract the raw data from the entry object to pass in.""" + def inner(*args, **kargs): + if name == 'result': + objtype, data = f(*args, **kargs) + # data is either a 2-tuple or a list of 2-tuples + # print data + if data: + if isinstance(data,tuple): + return objtype, Entry(data) + elif isinstance(data,list): + return objtype, [Entry(x) for x in data] + else: + raise TypeError, "unknown data type %s returned by result" % type(data) + else: + return objtype, data + elif name.startswith('add'): + # the first arg is self + # the second and third arg are the dn and the data to send + # We need to convert the Entry into the format used by + # python-ldap + ent = args[0] + if isinstance(ent,Entry): + return f(ent.dn, ent.toTupleList(), *args[2:]) + else: + return f(*args, **kargs) + else: + return f(*args, **kargs) + return inner + +class LDIFConn(ldif.LDIFParser): + def __init__( + self, + input_file, + ignored_attr_types=None,max_entries=0,process_url_schemes=None + ): + """ + See LDIFParser.__init__() + + Additional Parameters: + all_records + List instance for storing parsed records + """ + self.dndict = {} # maps dn to Entry + self.dnlist = [] # contains entries in order read + myfile = input_file + if isinstance(input_file,str) or isinstance(input_file,unicode): + myfile = open(input_file, "r") + ldif.LDIFParser.__init__(self,myfile,ignored_attr_types,max_entries,process_url_schemes) + self.parse() + if isinstance(input_file,str) or isinstance(input_file,unicode): + myfile.close() + + def handle(self,dn,entry): + """ + Append single record to dictionary of all records. + """ + if not dn: + dn = '' + newentry = Entry((dn, entry)) + self.dndict[IPAdmin.normalizeDN(dn)] = newentry + self.dnlist.append(newentry) + + def get(self,dn): + ndn = IPAdmin.normalizeDN(dn) + return self.dndict.get(ndn, Entry(None)) + +class IPAdmin(SimpleLDAPObject): + + def getDseAttr(self,attrname): + conffile = self.confdir + '/dse.ldif' + dseldif = LDIFConn(conffile) + cnconfig = dseldif.get("cn=config") + if cnconfig: + return cnconfig.getValue(attrname) + return None + + def __initPart2(self): + if self.binddn and len(self.binddn) and not hasattr(self,'sroot'): + try: + ent = self.getEntry('cn=config', ldap.SCOPE_BASE, '(objectclass=*)', + [ 'nsslapd-instancedir', 'nsslapd-errorlog', + 'nsslapd-certdir', 'nsslapd-schemadir' ]) + self.errlog = ent.getValue('nsslapd-errorlog') + self.confdir = ent.getValue('nsslapd-certdir') + if not self.confdir: + self.confdir = ent.getValue('nsslapd-schemadir') + if self.confdir: + self.confdir = os.path.dirname(self.confdir) + ent = self.getEntry('cn=config,cn=ldbm database,cn=plugins,cn=config', + ldap.SCOPE_BASE, '(objectclass=*)', + [ 'nsslapd-directory' ]) + self.dbdir = os.path.dirname(ent.getValue('nsslapd-directory')) + except (ldap.INSUFFICIENT_ACCESS, ldap.CONNECT_ERROR): + pass # usually means + except ldap.LDAPError, e: + print "caught exception ", e + raise + + def __localinit(self): + """If a CA certificate is provided then it is assumed that we are + doing SSL client authentication with proxy auth. + + If a CA certificate is not present then it is assumed that we are + using a forwarded kerberos ticket for SASL auth. SASL provides + its own encryption. + """ + if self.cacert is not None: + SimpleLDAPObject.__init__(self,'ldaps://%s:%d' % (self.host,self.port)) + else: + SimpleLDAPObject.__init__(self,'ldap://%s:%d' % (self.host,self.port)) + + def __init__(self,host,port=389,cacert=None,bindcert=None,bindkey=None,proxydn=None,debug=None): + """We just set our instance variables and wrap the methods - the real + work is done in __localinit and __initPart2 - these are separated + out this way so that we can call them from places other than + instance creation e.g. when we just need to reconnect, not create a + new instance""" + if debug and debug.lower() == "on": + ldap.set_option(ldap.OPT_DEBUG_LEVEL,255) + if cacert is not None: + ldap.set_option(ldap.OPT_X_TLS_CACERTFILE,cacert) + if bindcert is not None: + ldap.set_option(ldap.OPT_X_TLS_CERTFILE,bindcert) + if bindkey is not None: + ldap.set_option(ldap.OPT_X_TLS_KEYFILE,bindkey) + + self.__wrapmethods() + self.port = port + self.host = host + self.cacert = cacert + self.bindcert = bindcert + self.bindkey = bindkey + self.proxydn = proxydn + self.suffixes = {} + self.__localinit() + + def __str__(self): + return self.host + ":" + str(self.port) + + def __get_server_controls(self): + """Create the proxy user server control. The control has the form + 0x04 = Octet String + 4|0x80 sets the length of the string length field at 4 bytes + the struct() gets us the length in bytes of string self.proxydn + self.proxydn is the proxy dn to send""" + + if self.proxydn is not None: + proxydn = chr(0x04) + chr(4|0x80) + struct.pack('l', socket.htonl(len(self.proxydn))) + self.proxydn; + + # Create the proxy control + sctrl=[] + sctrl.append(LDAPControl('2.16.840.1.113730.3.4.18',True,proxydn)) + else: + sctrl=None + + return sctrl + + def toLDAPURL(self): + return "ldap://%s:%d/" % (self.host,self.port) + + def set_proxydn(self, proxydn): + self.proxydn = proxydn + + def set_krbccache(self, krbccache, principal): + if krbccache is not None: + os.environ["KRB5CCNAME"] = krbccache + self.sasl_interactive_bind_s("", sasl_auth) + self.principal = principal + self.proxydn = None + + def do_simple_bind(self, binddn="cn=directory manager", bindpw=""): + self.binddn = binddn + self.bindpwd = bindpw + self.simple_bind_s(binddn, bindpw) + self.__initPart2() + + def getEntry(self,*args): + """This wraps the search function. It is common to just get one entry""" + + sctrl = self.__get_server_controls() + + if sctrl is not None: + self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl) + + try: + res = self.search(*args) + objtype, obj = self.result(res) + except ldap.NO_SUCH_OBJECT, e: + raise e + except ldap.LDAPError, e: + raise e + + if not obj: + raise ldap.NO_SUCH_OBJECT + + elif isinstance(obj,Entry): + return obj + else: # assume list/tuple + return obj[0] + + def getList(self,*args): + """This wraps the search function to find multiple entries.""" + + sctrl = self.__get_server_controls() + if sctrl is not None: + self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl) + + try: + res = self.search(*args) + objtype, obj = self.result(res) + except (ldap.ADMINLIMIT_EXCEEDED, ldap.SIZELIMIT_EXCEEDED), e: + # Too many results returned by search + raise e + except ldap.LDAPError, e: + raise e + + if not obj: + raise ldap.NO_SUCH_OBJECT + + entries = [] + for s in obj: + entries.append(s) + + return entries + + def getListAsync(self,*args): + """This version performs an asynchronous search, to allow + results even if we hit a limit. + + It returns a list: counter followed by the results. + If the results are truncated, counter will be set to -1. + """ + + sctrl = self.__get_server_controls() + if sctrl is not None: + self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl) + + entries = [] + partial = 0 + + try: + msgid = self.search_ext(*args) + objtype, result_list = self.result(msgid, 0) + while result_list: + for result in result_list: + entries.append(result) + objtype, result_list = self.result(msgid, 0) + except (ldap.ADMINLIMIT_EXCEEDED, ldap.SIZELIMIT_EXCEEDED, + ldap.TIMELIMIT_EXCEEDED), e: + partial = 1 + except ldap.LDAPError, e: + raise e + + if not entries: + raise ldap.NO_SUCH_OBJECT + + if partial == 1: + counter = -1 + else: + counter = len(entries) + + return [counter] + entries + + def addEntry(self,*args): + """This wraps the add function. It assumes that the entry is already + populated with all of the desired objectclasses and attributes""" + + sctrl = self.__get_server_controls() + + try: + if sctrl is not None: + self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl) + self.add_s(*args) + except ldap.ALREADY_EXISTS, e: + # duplicate value + raise e + except ldap.LDAPError, e: + raise e + return True + + def updateRDN(self, dn, newrdn): + """Wrap the modrdn function.""" + + sctrl = self.__get_server_controls() + + if dn == newrdn: + # no need to report an error + return True + + try: + if sctrl is not None: + self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl) + self.modrdn_s(dn, newrdn, delold=1) + except ldap.LDAPError, e: + raise e + return True + + def updateEntry(self,dn,oldentry,newentry): + """This wraps the mod function. It assumes that the entry is already + populated with all of the desired objectclasses and attributes""" + + sctrl = self.__get_server_controls() + + modlist = self.generateModList(oldentry, newentry) + + if len(modlist) == 0: + # FIXME: better error + raise SyntaxError("empty modlist") + + try: + if sctrl is not None: + self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl) + self.modify_s(dn, modlist) + # this is raised when a 'delete' attribute isn't found. + # it indicates the previous attribute was removed by another + # update, making the oldentry stale. + except ldap.NO_SUCH_ATTRIBUTE: + # FIXME: better error + raise SyntaxError("mid-air collision") + except ldap.LDAPError, e: + raise e + return True + + def generateModList(self, old_entry, new_entry): + """A mod list generator that computes more precise modification lists + than the python-ldap version. This version purposely generates no + REPLACE operations, to deal with multi-user updates more properly.""" + modlist = [] + + old_entry = ipautil.CIDict(old_entry) + new_entry = ipautil.CIDict(new_entry) + + keys = set(map(string.lower, old_entry.keys())) + keys.update(map(string.lower, new_entry.keys())) + + for key in keys: + new_values = new_entry.get(key, []) + if not(isinstance(new_values,list) or isinstance(new_values,tuple)): + new_values = [new_values] + new_values = filter(lambda value:value!=None, new_values) + new_values = set(new_values) + + old_values = old_entry.get(key, []) + if not(isinstance(old_values,list) or isinstance(old_values,tuple)): + old_values = [old_values] + old_values = filter(lambda value:value!=None, old_values) + old_values = set(old_values) + + adds = list(new_values.difference(old_values)) + removes = list(old_values.difference(new_values)) + + if len(removes) > 0: + modlist.append((ldap.MOD_DELETE, key, removes)) + if len(adds) > 0: + modlist.append((ldap.MOD_ADD, key, adds)) + + return modlist + + def inactivateEntry(self,dn,has_key): + """Rather than deleting entries we mark them as inactive. + has_key defines whether the entry already has nsAccountlock + set so we can determine which type of mod operation to run.""" + + sctrl = self.__get_server_controls() + modlist=[] + + if has_key: + operation = ldap.MOD_REPLACE + else: + operation = ldap.MOD_ADD + + modlist.append((operation, "nsAccountlock", "true")) + + try: + if sctrl is not None: + self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl) + self.modify_s(dn, modlist) + except ldap.LDAPError, e: + raise e + return True + + def deleteEntry(self,*args): + """This wraps the delete function. Use with caution.""" + + sctrl = self.__get_server_controls() + + try: + if sctrl is not None: + self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl) + self.delete_s(*args) + except ldap.LDAPError, e: + raise e + return True + + def modifyPassword(self,dn,oldpass,newpass): + """Set the user password using RFC 3062, LDAP Password Modify Extended + Operation. This ends up calling the IPA password slapi plugin + handler so the Kerberos password gets set properly. + + oldpass is not mandatory + """ + + sctrl = self.__get_server_controls() + + try: + if sctrl is not None: + self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl) + self.passwd_s(dn, oldpass, newpass) + except ldap.LDAPError, e: + raise e + return True + + def __wrapmethods(self): + """This wraps all methods of SimpleLDAPObject, so that we can intercept + the methods that deal with entries. Instead of using a raw list of tuples + of lists of hashes of arrays as the entry object, we want to wrap entries + in an Entry class that provides some useful methods""" + for name in dir(self.__class__.__bases__[0]): + attr = getattr(self, name) + if callable(attr): + setattr(self, name, wrapper(attr, name)) + + def addSchema(self, attr, val): + dn = "cn=schema" + self.modify_s(dn, [(ldap.MOD_ADD, attr, val)]) + + def addAttr(self, *args): + return self.addSchema('attributeTypes', args) + + def addObjClass(self, *args): + return self.addSchema('objectClasses', args) + + ########################### + # Static methods start here + ########################### + def normalizeDN(dn): + # not great, but will do until we use a newer version of python-ldap + # that has DN utilities + ary = ldap.explode_dn(dn.lower()) + return ",".join(ary) + normalizeDN = staticmethod(normalizeDN) + + def getfqdn(name=''): + return socket.getfqdn(name) + getfqdn = staticmethod(getfqdn) + + def getdomainname(name=''): + fqdn = IPAdmin.getfqdn(name) + index = fqdn.find('.') + if index >= 0: + return fqdn[index+1:] + else: + return fqdn + getdomainname = staticmethod(getdomainname) + + def getdefaultsuffix(name=''): + dm = IPAdmin.getdomainname(name) + if dm: + return "dc=" + dm.replace('.', ', dc=') + else: + return 'dc=localdomain' + getdefaultsuffix = staticmethod(getdefaultsuffix) + + def is_a_dn(dn): + """Returns True if the given string is a DN, False otherwise.""" + return (dn.find("=") > 0) + is_a_dn = staticmethod(is_a_dn) + + +def notfound(args): + """Return a string suitable for displaying as an error when a + search returns no results. + + This just returns whatever is after the equals sign""" + if len(args) > 2: + searchfilter = args[2] + try: + target = re.match(r'\(.*=(.*)\)', searchfilter).group(1) + except: + target = searchfilter + return "%s not found" % str(target) + else: + return args[0] diff --git a/ipalib/ipautil.py b/ipalib/ipautil.py new file mode 100644 index 00000000..6b0e2c89 --- /dev/null +++ b/ipalib/ipautil.py @@ -0,0 +1,190 @@ +# Authors: Simo Sorce +# +# Copyright (C) 2007 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 +# + +import string +import xmlrpclib +import re + +def realm_to_suffix(realm_name): + s = realm_name.split(".") + terms = ["dc=" + x.lower() for x in s] + return ",".join(terms) + +class CIDict(dict): + """ + Case-insensitive but case-respecting dictionary. + + This code is derived from python-ldap's cidict.py module, + written by stroeder: http://python-ldap.sourceforge.net/ + + This version extends 'dict' so it works properly with TurboGears. + If you extend UserDict, isinstance(foo, dict) returns false. + """ + + def __init__(self,default=None): + super(CIDict, self).__init__() + self._keys = {} + self.update(default or {}) + + def __getitem__(self,key): + return super(CIDict,self).__getitem__(string.lower(key)) + + def __setitem__(self,key,value): + lower_key = string.lower(key) + self._keys[lower_key] = key + return super(CIDict,self).__setitem__(string.lower(key),value) + + def __delitem__(self,key): + lower_key = string.lower(key) + del self._keys[lower_key] + return super(CIDict,self).__delitem__(string.lower(key)) + + def update(self,dict): + for key in dict.keys(): + self[key] = dict[key] + + def has_key(self,key): + return super(CIDict, self).has_key(string.lower(key)) + + def get(self,key,failobj=None): + try: + return self[key] + except KeyError: + return failobj + + def keys(self): + return self._keys.values() + + def items(self): + result = [] + for k in self._keys.values(): + result.append((k,self[k])) + return result + + def copy(self): + copy = {} + for k in self._keys.values(): + copy[k] = self[k] + return copy + + def iteritems(self): + return self.copy().iteritems() + + def iterkeys(self): + return self.copy().iterkeys() + + def setdefault(self,key,value=None): + try: + return self[key] + except KeyError: + self[key] = value + return value + + def pop(self, key, *args): + try: + value = self[key] + del self[key] + return value + except KeyError: + if len(args) == 1: + return args[0] + raise + + def popitem(self): + (lower_key,value) = super(CIDict,self).popitem() + key = self._keys[lower_key] + del self._keys[lower_key] + + return (key,value) + + +# +# The safe_string_re regexp and needs_base64 function are extracted from the +# python-ldap ldif module, which was +# written by Michael Stroeder +# http://python-ldap.sourceforge.net +# +# It was extracted because ipaldap.py is naughtily reaching into the ldif +# module and squashing this regexp. +# +SAFE_STRING_PATTERN = '(^(\000|\n|\r| |:|<)|[\000\n\r\200-\377]+|[ ]+$)' +safe_string_re = re.compile(SAFE_STRING_PATTERN) + +def needs_base64(s): + """ + returns 1 if s has to be base-64 encoded because of special chars + """ + return not safe_string_re.search(s) is None + + +def wrap_binary_data(data): + """Converts all binary data strings into Binary objects for transport + back over xmlrpc.""" + if isinstance(data, str): + if needs_base64(data): + return xmlrpclib.Binary(data) + else: + return data + elif isinstance(data, list) or isinstance(data,tuple): + retval = [] + for value in data: + retval.append(wrap_binary_data(value)) + return retval + elif isinstance(data, dict): + retval = {} + for (k,v) in data.iteritems(): + retval[k] = wrap_binary_data(v) + return retval + else: + return data + + +def unwrap_binary_data(data): + """Converts all Binary objects back into strings.""" + if isinstance(data, xmlrpclib.Binary): + # The data is decoded by the xmlproxy, but is stored + # in a binary object for us. + return str(data) + elif isinstance(data, str): + return data + elif isinstance(data, list) or isinstance(data,tuple): + retval = [] + for value in data: + retval.append(unwrap_binary_data(value)) + return retval + elif isinstance(data, dict): + retval = {} + for (k,v) in data.iteritems(): + retval[k] = unwrap_binary_data(v) + return retval + else: + return data + +def get_gsserror(e): + """A GSSError exception looks differently in python 2.4 than it does + in python 2.5, deal with it.""" + + try: + primary = e[0] + secondary = e[1] + except: + primary = e[0][0] + secondary = e[0][1] + + return (primary[0], secondary[0]) diff --git a/ipalib/plugins/example.py b/ipalib/plugins/example.py index 4c62a5de..92ef95d5 100644 --- a/ipalib/plugins/example.py +++ b/ipalib/plugins/example.py @@ -24,7 +24,8 @@ Some example plugins. from ipalib import public from ipalib import api - +from ipalib import servercore +import ldap # Hypothetical functional commands (not associated with any object): class krbtest(public.Command): @@ -39,8 +40,11 @@ api.register(discover) # Register some methods for the 'user' object: class user_add(public.Method): 'Add a new user.' + def execute(self, **kw): + return 1 api.register(user_add) + class user_del(public.Method): 'Delete an existing user.' api.register(user_del) @@ -51,6 +55,9 @@ api.register(user_mod) class user_find(public.Method): 'Search the users.' + def execute(self, **kw): + result = servercore.get_sub_entry(servercore.basedn, "uid=%s" % kw['uid'], ["*"]) + return result api.register(user_find) diff --git a/ipalib/public.py b/ipalib/public.py index 088e65c5..31270742 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -338,7 +338,7 @@ class Command(plugable.Plugin): kw = self.normalize(**kw) kw.update(self.get_default(**kw)) self.validate(**kw) - self.execute(**kw) + return self.execute(**kw) def smart_option_order(self): def get_key(option): diff --git a/ipalib/servercore.py b/ipalib/servercore.py new file mode 100644 index 00000000..8626c04b --- /dev/null +++ b/ipalib/servercore.py @@ -0,0 +1,148 @@ +# Authors: Rob Crittenden +# +# Copyright (C) 2007 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 +# + +import sys +sys.path.insert(0, ".") +sys.path.insert(0, "..") +import ldap +from ipalib.conn import context +from ipalib import ipautil + +# temporary +import krbV + +krbctx = krbV.default_context() +realm = krbctx.default_realm +basedn = ipautil.realm_to_suffix(realm) + +def convert_entry(ent): + entry = dict(ent.data) + entry['dn'] = ent.dn + # For now convert single entry lists to a string for the ui. + # TODO: we need to deal with multi-values better + for key,value in entry.iteritems(): + if isinstance(value,list) or isinstance(value,tuple): + if len(value) == 0: + entry[key] = '' + elif len(value) == 1: + entry[key] = value[0] + return entry + +def convert_scalar_values(orig_dict): + """LDAP update dicts expect all values to be a list (except for dn). + This method converts single entries to a list.""" + new_dict={} + for (k,v) in orig_dict.iteritems(): + if not isinstance(v, list) and k != 'dn': + v = [v] + new_dict[k] = v + + return new_dict + + +# TODO: rethink the get_entry vs get_list API calls. +# they currently restrict the data coming back without +# restricting scope. For now adding a get_base/sub_entry() +# calls, but the API isn't great. +def get_entry (base, scope, searchfilter, sattrs=None): + """Get a specific entry (with a parametized scope). + Return as a dict of values. + Multi-valued fields are represented as lists. + """ + ent="" + + ent = context.conn.getConn().getEntry(base, scope, searchfilter, sattrs) + + return convert_entry(ent) + +def get_base_entry (base, searchfilter, sattrs=None): + """Get a specific entry (with a scope of BASE). + Return as a dict of values. + Multi-valued fields are represented as lists. + """ + return get_entry(base, ldap.SCOPE_BASE, searchfilter, sattrs) + +def get_sub_entry (base, searchfilter, sattrs=None): + """Get a specific entry (with a scope of SUB). + Return as a dict of values. + Multi-valued fields are represented as lists. + """ + return get_entry(base, ldap.SCOPE_SUBTREE, searchfilter, sattrs) + +def get_list (base, searchfilter, sattrs=None): + """Gets a list of entries. Each is converted to a dict of values. + Multi-valued fields are represented as lists. + """ + entries = [] + + entries = context.conn.getConn().getList(base, ldap.SCOPE_SUBTREE, searchfilter, sattrs) + + return map(convert_entry, entries) + +def update_entry (oldentry, newentry): + """Update an LDAP entry + + oldentry is a dict + newentry is a dict + """ + oldentry = convert_scalar_values(oldentry) + newentry = convert_scalar_values(newentry) + + # Should be able to get this from either the old or new entry + # but just in case someone has decided to try changing it, use the + # original + try: + moddn = oldentry['dn'] + except KeyError, e: + # FIXME: return a missing DN error message + raise e + + res = context.conn.getConn().updateEntry(moddn, oldentry, newentry) + return res + +def uniq_list(x): + """Return a unique list, preserving order and ignoring case""" + myset = {} + return [set.setdefault(e.lower(),e) for e in x if e.lower() not in myset] + +def get_schema(): + """Retrieves the current LDAP schema from the LDAP server.""" + + schema_entry = get_base_entry("", "objectclass=*", ['dn','subschemasubentry']) + schema_cn = schema_entry.get('subschemasubentry') + schema = get_base_entry(schema_cn, "objectclass=*", ['*']) + + return schema + +def get_objectclasses(): + """Returns a list of available objectclasses that the LDAP + server supports. This parses out the syntax, attributes, etc + and JUST returns a lower-case list of the names.""" + + schema = get_schema() + + objectclasses = schema.get('objectclasses') + + # Convert this list into something more readable + result = [] + for i in range(len(objectclasses)): + oc = objectclasses[i].lower().split(" ") + result.append(oc[3].replace("'","")) + + return result -- cgit From 5a1abcdf4ff3433bcce15cd336660d73ab7c8d5b Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sat, 13 Sep 2008 00:22:01 +0000 Subject: 291: Temporarily reverted Rob's changes in public.py and plugins/examples.py --- ipalib/plugins/example.py | 9 +-------- ipalib/public.py | 2 +- 2 files changed, 2 insertions(+), 9 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugins/example.py b/ipalib/plugins/example.py index 92ef95d5..4c62a5de 100644 --- a/ipalib/plugins/example.py +++ b/ipalib/plugins/example.py @@ -24,8 +24,7 @@ Some example plugins. from ipalib import public from ipalib import api -from ipalib import servercore -import ldap + # Hypothetical functional commands (not associated with any object): class krbtest(public.Command): @@ -40,11 +39,8 @@ api.register(discover) # Register some methods for the 'user' object: class user_add(public.Method): 'Add a new user.' - def execute(self, **kw): - return 1 api.register(user_add) - class user_del(public.Method): 'Delete an existing user.' api.register(user_del) @@ -55,9 +51,6 @@ api.register(user_mod) class user_find(public.Method): 'Search the users.' - def execute(self, **kw): - result = servercore.get_sub_entry(servercore.basedn, "uid=%s" % kw['uid'], ["*"]) - return result api.register(user_find) diff --git a/ipalib/public.py b/ipalib/public.py index 31270742..088e65c5 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -338,7 +338,7 @@ class Command(plugable.Plugin): kw = self.normalize(**kw) kw.update(self.get_default(**kw)) self.validate(**kw) - return self.execute(**kw) + self.execute(**kw) def smart_option_order(self): def get_key(option): -- cgit From 4482c71aeb4f150caeb902c4f784a3b157106c5a Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sun, 14 Sep 2008 23:17:36 +0000 Subject: 292: Added experimental Command.args_to_kw() method --- ipalib/public.py | 39 +++++++++++++++++++++++++++------------ ipalib/tests/test_public.py | 15 +++++++++++++++ 2 files changed, 42 insertions(+), 12 deletions(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index 088e65c5..9a90a97f 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -333,12 +333,13 @@ class Command(plugable.Plugin): ) def __call__(self, *args, **kw): - print '' - self.print_call('__call__', kw) + arg_kw = self.args_to_kw(*args) + assert set(arg_kw).intersection(kw) == set() + kw.update(arg_kw) kw = self.normalize(**kw) + kw = self.convert(**kw) kw.update(self.get_default(**kw)) self.validate(**kw) - self.execute(**kw) def smart_option_order(self): def get_key(option): @@ -373,15 +374,29 @@ class Command(plugable.Plugin): return tuple(self.__group_args_iter(values, args)) def __group_args_iter(self, values, args): - for (i, arg) in enumerate(args): - if len(values) > i: - if arg.multivalue: - yield values[i:] - else: - yield values[i] - else: - assert not arg.required - yield None + for (i, arg) in enumerate(args): + if len(values) > i: + if arg.multivalue: + yield values[i:] + else: + yield values[i] + else: + assert not arg.required + yield None + + def args_to_kw(self, *values): + return dict(self.__args_to_kw_iter(values)) + + def __args_to_kw_iter(self, values): + multivalue = False + for (i, arg) in enumerate(self.args()): + assert not multivalue + if len(values) > i: + if arg.multivalue: + multivalue = True + yield (arg.name, values[i:]) + else: + yield (arg.name, values[i]) class Object(plugable.Plugin): diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index aac962b5..578332ea 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -584,6 +584,21 @@ class test_Command(ClassChecker): e = raises(errors.ArgumentError, o.group_args, 1) assert str(e) == 'example takes at least 2 arguments' + def test_args_to_kw(self): + o = self.__get_instance(args=('one', 'two?')) + assert o.args_to_kw(1) == dict(one=1) + assert o.args_to_kw(1, 2) == dict(one=1, two=2) + + o = self.__get_instance(args=('one', 'two*')) + assert o.args_to_kw(1) == dict(one=1) + assert o.args_to_kw(1, 2) == dict(one=1, two=(2,)) + assert o.args_to_kw(1, 2, 3) == dict(one=1, two=(2, 3)) + + o = self.__get_instance(args=('one', 'two+')) + assert o.args_to_kw(1) == dict(one=1) + assert o.args_to_kw(1, 2) == dict(one=1, two=(2,)) + assert o.args_to_kw(1, 2, 3) == dict(one=1, two=(2, 3)) + class test_Object(ClassChecker): """ -- cgit From f78f3ed0ddd409d8cf5fcfe28fe9062ab610b432 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 18 Sep 2008 19:39:23 +0000 Subject: 293: Added Command.kw_to_args() method; added corresponding unit tests --- ipalib/public.py | 5 +++++ ipalib/tests/test_public.py | 51 ++++++++++++++++++++++++++++----------------- 2 files changed, 37 insertions(+), 19 deletions(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index 9a90a97f..f3626fdf 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -397,6 +397,11 @@ class Command(plugable.Plugin): yield (arg.name, values[i:]) else: yield (arg.name, values[i]) + else: + break + + def kw_to_args(self, **kw): + return tuple(kw.get(name, None) for name in self.args) class Object(plugable.Plugin): diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index 578332ea..29531d69 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -376,7 +376,7 @@ class test_Command(ClassChecker): assert self.cls.takes_options == tuple() assert self.cls.takes_args == tuple() - def __get_instance(self, args=tuple(), options=tuple()): + def get_instance(self, args=tuple(), options=tuple()): """ Helper method used to test args and options. """ @@ -391,7 +391,7 @@ class test_Command(ClassChecker): """ assert list(self.cls().get_args()) == [] args = ('login', 'stuff') - o = self.__get_instance(args=args) + o = self.get_instance(args=args) assert o.get_args() is args def test_get_options(self): @@ -400,7 +400,7 @@ class test_Command(ClassChecker): """ assert list(self.cls().get_options()) == [] options = ('verbose', 'debug') - o = self.__get_instance(options=options) + o = self.get_instance(options=options) assert o.get_options() is options def test_args(self): @@ -412,7 +412,7 @@ class test_Command(ClassChecker): assert type(ns) is plugable.NameSpace assert len(ns) == 0 args = ('destination', 'source?') - ns = self.__get_instance(args=args).args + ns = self.get_instance(args=args).args assert type(ns) is plugable.NameSpace assert len(ns) == len(args) assert list(ns) == ['destination', 'source'] @@ -424,16 +424,16 @@ class test_Command(ClassChecker): assert ns.source.multivalue is False # Test TypeError: - e = raises(TypeError, self.__get_instance, args=(u'whatever',)) + e = raises(TypeError, self.get_instance, args=(u'whatever',)) assert str(e) == \ 'arg: need %r or %r; got %r' % (str, public.Option, u'whatever') # Test ValueError, required after optional: - e = raises(ValueError, self.__get_instance, args=('arg1?', 'arg2')) + e = raises(ValueError, self.get_instance, args=('arg1?', 'arg2')) assert str(e) == 'arg2: required argument after optional' # Test ValueError, scalar after multivalue: - e = raises(ValueError, self.__get_instance, args=('arg1+', 'arg2')) + e = raises(ValueError, self.get_instance, args=('arg1+', 'arg2')) assert str(e) == 'arg2: only final argument can be multivalue' def test_options(self): @@ -445,7 +445,7 @@ class test_Command(ClassChecker): assert type(ns) is plugable.NameSpace assert len(ns) == 0 options = ('target', 'files*') - ns = self.__get_instance(options=options).options + ns = self.get_instance(options=options).options assert type(ns) is plugable.NameSpace assert len(ns) == len(options) assert list(ns) == ['target', 'files'] @@ -551,54 +551,67 @@ class test_Command(ClassChecker): assert 'execute' in self.cls.__public__ # Public def test_group_args(self): - o = self.__get_instance(args=('one', 'two?')) + o = self.get_instance(args=('one', 'two?')) assert o.group_args(1) == (1, None) assert o.group_args(1, 2) == (1, 2) - o = self.__get_instance(args=('one', 'two*')) + o = self.get_instance(args=('one', 'two*')) assert o.group_args(1) == (1, None) assert o.group_args(1, 2) == (1, (2,)) assert o.group_args(1, 2, 3) == (1, (2, 3)) - o = self.__get_instance(args=('one', 'two+')) + o = self.get_instance(args=('one', 'two+')) assert o.group_args(1, 2) == (1, (2,)) assert o.group_args(1, 2, 3) == (1, (2, 3)) - o = self.__get_instance() + o = self.get_instance() e = raises(errors.ArgumentError, o.group_args, 1) assert str(e) == 'example takes no arguments' - o = self.__get_instance(args=('one?',)) + o = self.get_instance(args=('one?',)) e = raises(errors.ArgumentError, o.group_args, 1, 2) assert str(e) == 'example takes at most 1 argument' - o = self.__get_instance(args=('one', 'two?')) + o = self.get_instance(args=('one', 'two?')) e = raises(errors.ArgumentError, o.group_args, 1, 2, 3) assert str(e) == 'example takes at most 2 arguments' - o = self.__get_instance(args=('one', 'two?')) + o = self.get_instance(args=('one', 'two?')) e = raises(errors.ArgumentError, o.group_args) assert str(e) == 'example takes at least 1 argument' - o = self.__get_instance(args=('one', 'two', 'three?')) + o = self.get_instance(args=('one', 'two', 'three?')) e = raises(errors.ArgumentError, o.group_args, 1) assert str(e) == 'example takes at least 2 arguments' def test_args_to_kw(self): - o = self.__get_instance(args=('one', 'two?')) + o = self.get_instance(args=('one', 'two?')) assert o.args_to_kw(1) == dict(one=1) assert o.args_to_kw(1, 2) == dict(one=1, two=2) - o = self.__get_instance(args=('one', 'two*')) + o = self.get_instance(args=('one', 'two*')) assert o.args_to_kw(1) == dict(one=1) assert o.args_to_kw(1, 2) == dict(one=1, two=(2,)) assert o.args_to_kw(1, 2, 3) == dict(one=1, two=(2, 3)) - o = self.__get_instance(args=('one', 'two+')) + o = self.get_instance(args=('one', 'two+')) assert o.args_to_kw(1) == dict(one=1) assert o.args_to_kw(1, 2) == dict(one=1, two=(2,)) assert o.args_to_kw(1, 2, 3) == dict(one=1, two=(2, 3)) + def test_kw_to_args(self): + """ + Tests the `public.Command.kw_to_arg` method. + """ + o = self.get_instance(args=('one', 'two?')) + assert o.kw_to_args() == (None, None) + assert o.kw_to_args(whatever='hello') == (None, None) + assert o.kw_to_args(one='the one') == ('the one', None) + assert o.kw_to_args(two='the two') == (None, 'the two') + assert o.kw_to_args(whatever='hello', two='Two', one='One') == \ + ('One', 'Two') + + class test_Object(ClassChecker): """ -- cgit From 84a721d408c307add34440b7a68a9c7a858c52e3 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 18 Sep 2008 20:35:23 +0000 Subject: 294: NameSpace no longer subclasses from DictProxy; NameSpace.__getitem__() now works with int and slice objects --- ipalib/plugable.py | 65 ++++++++++++++++++++++++++++++++----------- ipalib/tests/test_plugable.py | 2 +- 2 files changed, 50 insertions(+), 17 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 66e5fada..88b60a2b 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -473,7 +473,7 @@ def check_name(name): return name -class NameSpace(DictProxy): +class NameSpace(ReadOnly): """ A read-only namespace with handy container behaviours. @@ -523,36 +523,69 @@ class NameSpace(DictProxy): :param members: An iterable providing the members. :param sort: Whether to sort the members by member name. """ - self.__members = tuple(members) self.__sort = check_type(sort, bool, 'sort') - names = (m.name for m in self.__members) if self.__sort: - self.__names = tuple(sorted(names)) + self.__members = tuple(sorted(members, key=lambda m: m.name)) else: - self.__names = tuple(names) - super(NameSpace, self).__init__( - dict(self.__member_iter()) - ) - - def __member_iter(self): - """ - Helper method called only from `NameSpace.__init__()`. - """ + 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) + assert name not in self.__map, 'already has key %r' % name + self.__map[name] = member assert not hasattr(self, name), 'already has attribute %r' % name setattr(self, name, member) - yield (name, member) + lock(self) + + def __len__(self): + """ + Returns the number of members. + """ + return len(self.__members) def __iter__(self): """ - Iterates through member names. + Iterates through the member names. - In this instance was created with ``sort=True``, + If this instance was created with ``sort=True``, the names will be in + alphabetical order; otherwise the names will be in the same order as + the members were passed to the constructor. """ for name in self.__names: yield name + def __call__(self): + """ + Iterates through the members. + + If this instance was created with ``sort=True``, the members will be + in alphabetical order by name; otherwise the members will be in the + same order as they were passed to the constructor. + """ + for member in self.__members: + yield member + + def __contains__(self, name): + """ + Returns True if namespace has a member named ``name``. + """ + return name in self.__map + + def __getitem__(self, spec): + """ + Returns a member by name or index, or returns a slice of members. + + :param spec: The name or index of a member, or a slice object. + """ + if type(spec) is str: + return self.__map[spec] + if type(spec) in (int, slice): + return self.__members[spec] + raise TypeError( + 'spec: must be %r, %r, or %r; got %r' % (str, int, slice, spec) + ) + def __repr__(self): """ Returns a pseudo-valid expression that could create this instance. diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index 62a78e36..b4461dee 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -569,7 +569,7 @@ class test_NameSpace(ClassChecker): _cls = plugable.NameSpace def test_class(self): - assert self.cls.__bases__ == (plugable.DictProxy,) + assert self.cls.__bases__ == (plugable.ReadOnly,) def test_init(self): """ -- cgit From 5ab45385dbdfdd3e5610f2751b5aeabbc244d446 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 18 Sep 2008 20:48:55 +0000 Subject: 295: Updated NameSpace unit tests to test NameSpace.__getitem__() with int and slice --- ipalib/tests/test_plugable.py | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) (limited to 'ipalib') diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index b4461dee..52665cc6 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -580,9 +580,13 @@ class test_NameSpace(ClassChecker): assert list(o()) == [] for cnt in (10, 25): members = tuple(DummyMember(cnt - i) for i in xrange(cnt)) - names = tuple(m.name for m in members) for sort in (True, False): o = self.cls(members, sort=sort) + if sort: + ordered = tuple(sorted(members, key=lambda m: m.name)) + else: + ordered = members + names = tuple(m.name for m in ordered) # Test __len__: assert len(o) == cnt @@ -593,21 +597,24 @@ class test_NameSpace(ClassChecker): assert ('member_00') not in o # Test __iter__, __call__: - if sort: - assert tuple(o) == tuple(sorted(names)) - assert tuple(o()) == tuple( - sorted(members, key=lambda m: m.name) - ) - else: - assert tuple(o) == names - assert tuple(o()) == members + assert tuple(o) == names + assert tuple(o()) == ordered # Test __getitem__, getattr: - for member in members: + for (i, member) in enumerate(ordered): + assert o[i] is member name = member.name assert o[name] is member assert read_only(o, name) is member + # Test negative indexes: + for i in xrange(1, cnt + 1): + assert o[-i] is ordered[-i] + + # Test slices: + assert o[2:cnt-5] == ordered[2:cnt-5] + assert o[::3] == ordered[::3] + # Test __repr__: assert repr(o) == \ 'NameSpace(<%d members>, sort=%r)' % (cnt, sort) -- cgit From 14eb96493b5b323eeee53b81a91f93508189b918 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 18 Sep 2008 21:23:05 +0000 Subject: 296: Added more to docstrings for NameSpace.__iter_() and NameSpace.__call__() --- ipalib/plugable.py | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 88b60a2b..dfefbe16 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -551,6 +551,8 @@ class NameSpace(ReadOnly): If this instance was created with ``sort=True``, the names will be in alphabetical order; otherwise the names will be in the same order as the members were passed to the constructor. + + This method is like an ordered version of dict.iterkeys(). """ for name in self.__names: yield name @@ -562,6 +564,8 @@ class NameSpace(ReadOnly): If this instance was created with ``sort=True``, the members will be in alphabetical order by name; otherwise the members will be in the same order as they were passed to the constructor. + + This method is like an ordered version of dict.itervalues(). """ for member in self.__members: yield member -- cgit From e524c826db12ffed029d627bd4afcfc03e7f899a Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 18 Sep 2008 21:45:25 +0000 Subject: 297: Added a better example in docstring for ReadOnly --- ipalib/plugable.py | 37 ++++++++++++++++++------------------- 1 file changed, 18 insertions(+), 19 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index dfefbe16..4e356783 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -48,26 +48,25 @@ class ReadOnly(object): For example: - >>> class givenname(ReadOnly): - >>> def __init__(self): - >>> self.whatever = 'some value' # Hasn't been locked yet - >>> lock(self) - >>> - >>> def finalize(self, api): - >>> # After the instance has been locked, attributes can still be - >>> # set, but only in a round-about, unconventional way: - >>> object.__setattr__(self, 'api', api) - >>> - >>> def normalize(self, value): - >>> # After the instance has been locked, trying to set an - >>> # attribute in the normal way will raise AttributeError. - >>> self.value = value # Not thread safe! - >>> return self.actually_normalize() - >>> - >>> def actually_normalize(self): - >>> # Again, this is not thread safe: - >>> return unicode(self.value).strip() + >>> ro = ReadOnly() # Initially unlocked, can setattr, delattr + >>> ro.name = 'John Doe' + >>> ro.message = 'Hello, world!' + >>> del ro.message + >>> ro.__lock__() # Now locked, cannot setattr, delattr + >>> ro.message = 'How are you?' + Traceback (most recent call last): + File "", line 1, in + File "/home/jderose/projects/freeipa2/ipalib/plugable.py", line 93, in __setattr__ + (self.__class__.__name__, name) + AttributeError: read-only: cannot set ReadOnly.message + >>> del ro.name + Traceback (most recent call last): + File "", line 1, in + File "/home/jderose/projects/freeipa2/ipalib/plugable.py", line 104, in __delattr__ + (self.__class__.__name__, name) + AttributeError: read-only: cannot del ReadOnly.name """ + __locked = False def __lock__(self): -- cgit From 81ebe078be56cef4c3d15e40d1b1fad01e67c0c0 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 18 Sep 2008 22:01:04 +0000 Subject: 298: Cleaned up docstrings in ReadOnly methods --- ipalib/plugable.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 4e356783..44653943 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -71,22 +71,25 @@ class ReadOnly(object): def __lock__(self): """ - Puts this instance into a read-only state, after which attempting to - set or delete an attribute will raise AttributeError. + Put this instance into a read-only state. + + After the instance has been locked, attempting to set or delete an + attribute will raise AttributeError. """ assert self.__locked is False, '__lock__() can only be called once' self.__locked = True def __islocked__(self): """ - Returns True if this instance is locked, False otherwise. + Return whether instance is locked. """ return self.__locked def __setattr__(self, name, value): """ - Raises an AttributeError if `ReadOnly.__lock__()` has already been - called; otherwise calls object.__setattr__(). + If unlocked, set attribute named ``name`` to ``value``. + + If this instance is locked, AttributeError will be raised. """ if self.__locked: raise AttributeError('read-only: cannot set %s.%s' % @@ -96,8 +99,9 @@ class ReadOnly(object): def __delattr__(self, name): """ - Raises an AttributeError if `ReadOnly.__lock__()` has already been - called; otherwise calls object.__delattr__(). + If unlocked, delete attribute named ``name``. + + If this instance is locked, AttributeError will be raised. """ if self.__locked: raise AttributeError('read-only: cannot del %s.%s' % -- cgit From ef0d7a71abe0d026b1b79b6dc32d17793a8d7806 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 18 Sep 2008 22:39:48 +0000 Subject: 299: Cleaned up unit tests for ReadOnly class --- ipalib/plugable.py | 2 +- ipalib/tests/test_plugable.py | 69 ++++++++++++++++++++++--------------------- 2 files changed, 36 insertions(+), 35 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 44653943..9db4a5c6 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -81,7 +81,7 @@ class ReadOnly(object): def __islocked__(self): """ - Return whether instance is locked. + Return True if instance is locked, otherwise False. """ return self.__locked diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index 52665cc6..9623f99e 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -40,52 +40,53 @@ class test_ReadOnly(ClassChecker): def test_lock(self): """ - Tests the `plugable.ReadOnly.__lock__` and - `plugable.ReadOnly.__islocked__` methods. + Test the `plugable.ReadOnly.__lock__` method. + """ + o = self.cls() + assert o._ReadOnly__locked is False + o.__lock__() + assert o._ReadOnly__locked is True + e = raises(AssertionError, o.__lock__) # Can only be locked once + assert str(e) == '__lock__() can only be called once' + assert o._ReadOnly__locked is True # This should still be True + + def test_lock(self): + """ + Test the `plugable.ReadOnly.__islocked__` method. """ o = self.cls() assert o.__islocked__() is False o.__lock__() assert o.__islocked__() is True - raises(AssertionError, o.__lock__) # Can only be locked once - assert o.__islocked__() is True # This should still be True - def test_when_unlocked(self): + def test_setattr(self): """ - Test that default state is unlocked, that setting and deleting - attributes works. + Test the `plugable.ReadOnly.__setattr__` method. """ o = self.cls() + o.attr1 = 'Hello, world!' + assert o.attr1 == 'Hello, world!' + o.__lock__() + for name in ('attr1', 'attr2'): + e = raises(AttributeError, setattr, o, name, 'whatever') + assert str(e) == 'read-only: cannot set ReadOnly.%s' % name + assert o.attr1 == 'Hello, world!' - # Setting: - o.hello = 'world' - assert o.hello == 'world' - - # Deleting: - del o.hello - assert not hasattr(o, 'hello') - - def test_when_locked(self): + def test_delattr(self): """ - Test that after __lock__() has been called, setting or deleting an - attribute raises AttributeError. + Test the `plugable.ReadOnly.__delattr__` method. """ - obj = self.cls() - obj.__lock__() - names = ['not_an_attribute', 'an_attribute'] - for name in names: - no_set(obj, name) - no_del(obj, name) - - class some_ro_class(self.cls): - def __init__(self): - self.an_attribute = 'Hello world!' - self.__lock__() - obj = some_ro_class() - for name in names: - no_set(obj, name) - no_del(obj, name) - assert read_only(obj, 'an_attribute') == 'Hello world!' + o = self.cls() + o.attr1 = 'Hello, world!' + o.attr2 = 'How are you?' + assert o.attr1 == 'Hello, world!' + assert o.attr2 == 'How are you?' + del o.attr1 + assert not hasattr(o, 'attr1') + o.__lock__() + e = raises(AttributeError, delattr, o, 'attr2') + assert str(e) == 'read-only: cannot del ReadOnly.attr2' + assert o.attr2 == 'How are you?' def test_lock(): -- cgit From e0b900894fcc884fbde26ba78fa61aedec3843e9 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 18 Sep 2008 23:15:34 +0000 Subject: 300: Added Command.max_args instance attribute; added corresponding unit tests --- ipalib/public.py | 5 ++++- ipalib/tests/test_public.py | 25 ++++++++++++++++++++----- 2 files changed, 24 insertions(+), 6 deletions(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index f3626fdf..c2624f67 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -229,12 +229,15 @@ class Command(plugable.Plugin): 'options', 'group_args', )) - __Option = None takes_options = tuple() takes_args = tuple() def __init__(self): self.args = plugable.NameSpace(self.__check_args(), sort=False) + if len(self.args) == 0 or not self.args[-1].multivalue: + self.max_args = len(self.args) + else: + self.max_args = None self.options = plugable.NameSpace(self.__check_options(), sort=False) self.params = plugable.NameSpace( tuple(self.args()) + tuple(self.options()), sort=False diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index 29531d69..80151fa0 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -371,11 +371,6 @@ class test_Command(ClassChecker): ) return example - def test_class(self): - assert self.cls.__bases__ == (plugable.Plugin,) - assert self.cls.takes_options == tuple() - assert self.cls.takes_args == tuple() - def get_instance(self, args=tuple(), options=tuple()): """ Helper method used to test args and options. @@ -385,6 +380,11 @@ class test_Command(ClassChecker): takes_options = options return example() + def test_class(self): + assert self.cls.__bases__ == (plugable.Plugin,) + assert self.cls.takes_options == tuple() + assert self.cls.takes_args == tuple() + def test_get_args(self): """ Tests the `public.Command.get_args` method. @@ -436,6 +436,21 @@ class test_Command(ClassChecker): e = raises(ValueError, self.get_instance, args=('arg1+', 'arg2')) assert str(e) == 'arg2: only final argument can be multivalue' + def test_max_args(self): + """ + Test the ``Command.max_args`` instance attribute. + """ + o = self.get_instance() + assert o.max_args == 0 + o = self.get_instance(args=('one?',)) + assert o.max_args == 1 + o = self.get_instance(args=('one', 'two?')) + assert o.max_args == 2 + o = self.get_instance(args=('one', 'multi+',)) + assert o.max_args is None + o = self.get_instance(args=('one', 'multi*',)) + assert o.max_args is None + def test_options(self): """ Tests the ``Command.options`` instance attribute. -- cgit From f29c827d06cb455709d3b07baf727913381709ca Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 18 Sep 2008 23:53:23 +0000 Subject: 301: Command.args_to_kw() now raises ArgumentError if more args than max_args are given --- ipalib/public.py | 8 ++++++++ ipalib/tests/test_public.py | 18 ++++++++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index c2624f67..967f88ce 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -388,6 +388,14 @@ class Command(plugable.Plugin): yield None def args_to_kw(self, *values): + if self.max_args is not None and len(values) > self.max_args: + if self.max_args == 0: + raise errors.ArgumentError(self, 'takes no arguments') + if self.max_args == 1: + raise errors.ArgumentError(self, 'takes at most 1 argument') + raise errors.ArgumentError(self, + 'takes at most %d arguments' % len(self.args) + ) return dict(self.__args_to_kw_iter(values)) def __args_to_kw_iter(self, values): diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index 80151fa0..939d59e8 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -600,6 +600,9 @@ class test_Command(ClassChecker): assert str(e) == 'example takes at least 2 arguments' def test_args_to_kw(self): + """ + Test the `public.Command.args_to_kw` method. + """ o = self.get_instance(args=('one', 'two?')) assert o.args_to_kw(1) == dict(one=1) assert o.args_to_kw(1, 2) == dict(one=1, two=2) @@ -614,9 +617,21 @@ class test_Command(ClassChecker): assert o.args_to_kw(1, 2) == dict(one=1, two=(2,)) assert o.args_to_kw(1, 2, 3) == dict(one=1, two=(2, 3)) + o = self.get_instance() + e = raises(errors.ArgumentError, o.args_to_kw, 1) + assert str(e) == 'example takes no arguments' + + o = self.get_instance(args=('one?',)) + e = raises(errors.ArgumentError, o.args_to_kw, 1, 2) + assert str(e) == 'example takes at most 1 argument' + + o = self.get_instance(args=('one', 'two?')) + e = raises(errors.ArgumentError, o.args_to_kw, 1, 2, 3) + assert str(e) == 'example takes at most 2 arguments' + def test_kw_to_args(self): """ - Tests the `public.Command.kw_to_arg` method. + Tests the `public.Command.kw_to_args` method. """ o = self.get_instance(args=('one', 'two?')) assert o.kw_to_args() == (None, None) @@ -627,7 +642,6 @@ class test_Command(ClassChecker): ('One', 'Two') - class test_Object(ClassChecker): """ Tests the `public.Object` class. -- cgit From 1ec4f379f5eb0b77e6ca90c777e2e976c28ddfca Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 19 Sep 2008 00:00:54 +0000 Subject: 302: Removed depreciated Command.group_args() method --- ipalib/public.py | 36 ++---------------------------------- ipalib/tests/test_public.py | 36 ++---------------------------------- 2 files changed, 4 insertions(+), 68 deletions(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index 967f88ce..a8397a54 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -227,7 +227,8 @@ class Command(plugable.Plugin): 'smart_option_order', 'args', 'options', - 'group_args', + 'args_to_kw', + 'kw_to_args', )) takes_options = tuple() takes_args = tuple() @@ -354,39 +355,6 @@ class Command(plugable.Plugin): for option in sorted(self.options(), key=get_key): yield option - def group_args(self, *values): - args = tuple(self.args()) - if len(args) == 0: - if len(values) > 0: - raise errors.ArgumentError(self, 'takes no arguments') - else: - return tuple() - if len(values) > len(args) and not args[-1].multivalue: - if len(args) == 1: - error = 'takes at most 1 argument' - else: - error = 'takes at most %d arguments' % len(args) - raise errors.ArgumentError(self, error) - min_args = sum(int(a.required) for a in args) - if len(values) < min_args: - if min_args == 1: - error = 'takes at least 1 argument' - else: - error = 'takes at least %d arguments' % min_args - raise errors.ArgumentError(self, error) - return tuple(self.__group_args_iter(values, args)) - - def __group_args_iter(self, values, args): - for (i, arg) in enumerate(args): - if len(values) > i: - if arg.multivalue: - yield values[i:] - else: - yield values[i] - else: - assert not arg.required - yield None - def args_to_kw(self, *values): if self.max_args is not None and len(values) > self.max_args: if self.max_args == 0: diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index 939d59e8..f45d1abe 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -565,44 +565,11 @@ class test_Command(ClassChecker): """ assert 'execute' in self.cls.__public__ # Public - def test_group_args(self): - o = self.get_instance(args=('one', 'two?')) - assert o.group_args(1) == (1, None) - assert o.group_args(1, 2) == (1, 2) - - o = self.get_instance(args=('one', 'two*')) - assert o.group_args(1) == (1, None) - assert o.group_args(1, 2) == (1, (2,)) - assert o.group_args(1, 2, 3) == (1, (2, 3)) - - o = self.get_instance(args=('one', 'two+')) - assert o.group_args(1, 2) == (1, (2,)) - assert o.group_args(1, 2, 3) == (1, (2, 3)) - - o = self.get_instance() - e = raises(errors.ArgumentError, o.group_args, 1) - assert str(e) == 'example takes no arguments' - - o = self.get_instance(args=('one?',)) - e = raises(errors.ArgumentError, o.group_args, 1, 2) - assert str(e) == 'example takes at most 1 argument' - - o = self.get_instance(args=('one', 'two?')) - e = raises(errors.ArgumentError, o.group_args, 1, 2, 3) - assert str(e) == 'example takes at most 2 arguments' - - o = self.get_instance(args=('one', 'two?')) - e = raises(errors.ArgumentError, o.group_args) - assert str(e) == 'example takes at least 1 argument' - - o = self.get_instance(args=('one', 'two', 'three?')) - e = raises(errors.ArgumentError, o.group_args, 1) - assert str(e) == 'example takes at least 2 arguments' - def test_args_to_kw(self): """ Test the `public.Command.args_to_kw` method. """ + assert 'args_to_kw' in self.cls.__public__ # Public o = self.get_instance(args=('one', 'two?')) assert o.args_to_kw(1) == dict(one=1) assert o.args_to_kw(1, 2) == dict(one=1, two=2) @@ -633,6 +600,7 @@ class test_Command(ClassChecker): """ Tests the `public.Command.kw_to_args` method. """ + assert 'kw_to_args' in self.cls.__public__ # Public o = self.get_instance(args=('one', 'two?')) assert o.kw_to_args() == (None, None) assert o.kw_to_args(whatever='hello') == (None, None) -- cgit From f8953720c4438c34f5e42ca3949e8078ca777fe4 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sun, 21 Sep 2008 16:59:12 +0000 Subject: 303: Removed Command.smart_option_order() method and moved its logic into Method.get_options() --- ipalib/public.py | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index a8397a54..40b13229 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -227,6 +227,7 @@ class Command(plugable.Plugin): 'smart_option_order', 'args', 'options', + 'params', 'args_to_kw', 'kw_to_args', )) @@ -337,24 +338,15 @@ class Command(plugable.Plugin): ) def __call__(self, *args, **kw): - arg_kw = self.args_to_kw(*args) - assert set(arg_kw).intersection(kw) == set() - kw.update(arg_kw) + if len(args) > 0: + arg_kw = self.args_to_kw(*args) + assert set(arg_kw).intersection(kw) == set() + kw.update(arg_kw) kw = self.normalize(**kw) kw = self.convert(**kw) kw.update(self.get_default(**kw)) self.validate(**kw) - def smart_option_order(self): - def get_key(option): - if option.required: - if option.default_from is None: - return 0 - return 1 - return 2 - for option in sorted(self.options(), key=get_key): - yield option - def args_to_kw(self, *values): if self.max_args is not None and len(values) > self.max_args: if self.max_args == 0: @@ -463,8 +455,15 @@ class Method(Attribute, Command): for option in self.takes_options: yield option if self.obj is not None and self.obj.Property is not None: - for proxy in self.obj.Property(): - yield proxy.option + def get_key(p): + o = p.option + if o.required: + if o.default_from is None: + return 0 + return 1 + return 2 + for prop in sorted(self.obj.Property(), key=get_key): + yield prop.option class Property(Attribute): -- cgit From 4a96ec2dc7975e5c6f76e87be0c62a51d262de32 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sun, 21 Sep 2008 18:50:00 +0000 Subject: 304: args, options, & params namespaces are now created in Command.finalize() instead of Command.__init__(); updated corresponding unit tests --- ipalib/public.py | 6 +++++- ipalib/tests/test_public.py | 24 +++++++++++++++++------- 2 files changed, 22 insertions(+), 8 deletions(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index 40b13229..78fa0983 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -233,8 +233,12 @@ class Command(plugable.Plugin): )) takes_options = tuple() takes_args = tuple() + args = None + options = None + params = None - def __init__(self): + def finalize(self, api): + super(Command, self).finalize(api) self.args = plugable.NameSpace(self.__check_args(), sort=False) if len(self.args) == 0 or not self.args[-1].multivalue: self.max_args = len(self.args) diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index f45d1abe..865a18fd 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -378,7 +378,9 @@ class test_Command(ClassChecker): class example(self.cls): takes_args = args takes_options = options - return example() + o = example() + o.finalize(object) + return o def test_class(self): assert self.cls.__bases__ == (plugable.Plugin,) @@ -408,9 +410,11 @@ class test_Command(ClassChecker): Tests the ``Command.args`` instance attribute. """ assert 'args' in self.cls.__public__ # Public - ns = self.cls().args - assert type(ns) is plugable.NameSpace - assert len(ns) == 0 + assert self.cls().args is None + o = self.cls() + o.finalize(object) + assert type(o.args) is plugable.NameSpace + assert len(o.args) == 0 args = ('destination', 'source?') ns = self.get_instance(args=args).args assert type(ns) is plugable.NameSpace @@ -456,9 +460,11 @@ class test_Command(ClassChecker): Tests the ``Command.options`` instance attribute. """ assert 'options' in self.cls.__public__ # Public - ns = self.cls().options - assert type(ns) is plugable.NameSpace - assert len(ns) == 0 + assert self.cls().options is None + o = self.cls() + o.finalize(object) + assert type(o.options) is plugable.NameSpace + assert len(o.options) == 0 options = ('target', 'files*') ns = self.get_instance(options=options).options assert type(ns) is plugable.NameSpace @@ -485,6 +491,7 @@ class test_Command(ClassChecker): expected = dict(kw) expected.update(dict(option0=u'option0', option1=u'option1')) o = self.subcls() + o.finalize(object) for (key, value) in o.convert(**kw).iteritems(): v = expected[key] assert value == v @@ -502,6 +509,7 @@ class test_Command(ClassChecker): ) norm = dict((k, v.lower()) for (k, v) in kw.items()) sub = self.subcls() + sub.finalize(object) assert sub.normalize(**kw) == norm def test_get_default(self): @@ -522,6 +530,7 @@ class test_Command(ClassChecker): option1='the default', ) sub = self.subcls() + sub.finalize(object) assert sub.get_default(**no_fill) == {} assert sub.get_default(**fill) == default @@ -532,6 +541,7 @@ class test_Command(ClassChecker): assert 'validate' in self.cls.__public__ # Public sub = self.subcls() + sub.finalize(object) # Check with valid args okay = dict( -- cgit From 2d836140064154c461aec1b24ae8d774cbd12444 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sun, 21 Sep 2008 19:00:41 +0000 Subject: 305: Ported cli.py to changes in public.py --- ipalib/cli.py | 37 ++++++++++++++++++++----------------- ipalib/plugins/example.py | 9 +++++++++ 2 files changed, 29 insertions(+), 17 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index 594e2812..b16fe6b5 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -198,39 +198,42 @@ class CLI(object): ) def run_cmd(self, cmd, argv): - (args, kw) = self.parse(cmd, argv) - try: - args = cmd.group_args(*args) - except errors.ArgumentError, e: - exit_error('%s %s' % (to_cli(cmd.name), e.error)) - self.run_interactive(cmd, args, kw) + kw = self.parse(cmd, argv) + self.run_interactive(cmd, kw) - def run_interactive(self, cmd, args, kw): - for option in cmd.smart_option_order(): - if option.name not in kw: - default = option.get_default(**kw) + def run_interactive(self, cmd, kw): + for param in cmd.params(): + if param.name not in kw: + default = param.get_default(**kw) if default is None: - prompt = '%s: ' % option.name + prompt = '%s: ' % param.name else: - prompt = '%s [%s]: ' % (option.name, default) + prompt = '%s [%s]: ' % (param.name, default) error = None while True: if error is not None: - print '>>> %s: %s' % (option.name, error) + print '>>> %s: %s' % (param.name, error) raw = raw_input(prompt) try: - value = option(raw, **kw) + value = param(raw, **kw) if value is not None: - kw[option.name] = value + kw[param.name] = value break except errors.ValidationError, e: error = e.error - cmd(*args, **kw) + cmd(**kw) def parse(self, cmd, argv): parser = self.build_parser(cmd) (kwc, args) = parser.parse_args(argv, KWCollector()) - return (args, kwc.__todict__()) + kw = kwc.__todict__() + try: + arg_kw = cmd.args_to_kw(*args) + except errors.ArgumentError, e: + exit_error('%s %s' % (to_cli(cmd.name), e.error)) + assert set(arg_kw).intersection(kw) == set() + kw.update(arg_kw) + return kw def build_parser(self, cmd): parser = optparse.OptionParser( diff --git a/ipalib/plugins/example.py b/ipalib/plugins/example.py index 4c62a5de..74874c95 100644 --- a/ipalib/plugins/example.py +++ b/ipalib/plugins/example.py @@ -39,6 +39,15 @@ api.register(discover) # Register some methods for the 'user' object: class user_add(public.Method): 'Add a new user.' + + takes_args = ['login'] + + takes_options = [ + 'givenname', + 'sn', + 'initials', + ] + api.register(user_add) class user_del(public.Method): -- cgit From 5872221bd49dda962391ddfb88f22e86bf72afec Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sun, 21 Sep 2008 21:30:19 +0000 Subject: 306: Added Plugin.set_api() method; added corresponding unit tests --- ipalib/plugable.py | 8 ++++++++ ipalib/tests/test_plugable.py | 14 ++++++++++++++ 2 files changed, 22 insertions(+) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 9db4a5c6..19eae504 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -352,6 +352,14 @@ class Plugin(ReadOnly): assert api is not None, 'finalize() argument cannot be None' self.__api = api + def set_api(self, api): + """ + Set reference to `API` instance. + """ + assert self.__api is None, 'set_api() can only be called once' + assert api is not None, 'set_api() argument cannot be None' + self.__api = api + def __repr__(self): """ Returns a fully qualified module_name.class_name() representation that diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index 9623f99e..3972cfa9 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -387,6 +387,20 @@ class test_Plugin(ClassChecker): assert base.implemented_by(fail) is False assert base.implemented_by(fail()) is False + def test_set_api(self): + """ + Tests the `plugable.Plugin.set_api` method. + """ + api = 'the api instance' + o = self.cls() + assert o.api is None + e = raises(AssertionError, o.set_api, None) + assert str(e) == 'set_api() argument cannot be None' + o.set_api(api) + assert o.api is api + e = raises(AssertionError, o.set_api, api) + assert str(e) == 'set_api() can only be called once' + def test_finalize(self): """ Tests the `plugable.Plugin.finalize` method. -- cgit From f73d976bdacae37557f0b2ccfa6da01ea58c685d Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sun, 21 Sep 2008 21:50:56 +0000 Subject: 307: Split Plugin.finalize() into two steps 1) Plugin.set_api() and 2) Plugin.finalize(); updated unit tests --- ipalib/plugable.py | 17 ++++++----------- ipalib/public.py | 13 +++++++------ ipalib/tests/test_plugable.py | 22 +++------------------- ipalib/tests/test_public.py | 26 +++++++++++++------------- 4 files changed, 29 insertions(+), 49 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 19eae504..725833cd 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -340,17 +340,10 @@ class Plugin(ReadOnly): return False return True - def finalize(self, api): + def finalize(self): """ - After all the plugins are instantiated, `API` calls this method, - passing itself as the only argument. This is where plugins should - check that other plugins they depend upon have actually been loaded. - - :param api: An `API` instance. """ - assert self.__api is None, 'finalize() can only be called once' - assert api is not None, 'finalize() argument cannot be None' - self.__api = api + lock(self) def set_api(self, api): """ @@ -730,7 +723,9 @@ class API(DictProxy): object.__setattr__(self, name, namespace) for plugin in instances.itervalues(): - plugin.finalize(self) - lock(plugin) + plugin.set_api(self) assert plugin.api is self + + for plugin in instances.itervalues(): + plugin.finalize() object.__setattr__(self, '_API__finalized', True) diff --git a/ipalib/public.py b/ipalib/public.py index 78fa0983..c4850747 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -237,8 +237,7 @@ class Command(plugable.Plugin): options = None params = None - def finalize(self, api): - super(Command, self).finalize(api) + def finalize(self): self.args = plugable.NameSpace(self.__check_args(), sort=False) if len(self.args) == 0 or not self.args[-1].multivalue: self.max_args = len(self.args) @@ -248,6 +247,7 @@ class Command(plugable.Plugin): self.params = plugable.NameSpace( tuple(self.args()) + tuple(self.options()), sort=False ) + super(Command, self).finalize() def get_args(self): return self.takes_args @@ -395,11 +395,12 @@ class Object(plugable.Plugin): return self.__Property Property = property(__get_Property) - def finalize(self, api): - super(Object, self).finalize(api) + def set_api(self, api): + super(Object, self).set_api(api) self.__Method = self.__create_namespace('Method') self.__Property = self.__create_namespace('Property') + def __create_namespace(self, name): return plugable.NameSpace(self.__filter_members(name)) @@ -443,9 +444,9 @@ class Attribute(plugable.Plugin): return self.__obj obj = property(__get_obj) - def finalize(self, api): - super(Attribute, self).finalize(api) + def set_api(self, api): self.__obj = api.Object[self.obj_name] + super(Attribute, self).set_api(api) class Method(Attribute, Command): diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index 3972cfa9..6c796f9e 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -405,26 +405,10 @@ class test_Plugin(ClassChecker): """ Tests the `plugable.Plugin.finalize` method. """ - api = 'the api instance' o = self.cls() - assert read_only(o, 'name') == 'Plugin' - assert repr(o) == '%s.Plugin()' % plugable.__name__ - assert read_only(o, 'api') is None - raises(AssertionError, o.finalize, None) - o.finalize(api) - assert read_only(o, 'api') is api - raises(AssertionError, o.finalize, api) - - class some_plugin(self.cls): - pass - sub = some_plugin() - assert read_only(sub, 'name') == 'some_plugin' - assert repr(sub) == '%s.some_plugin()' % __name__ - assert read_only(sub, 'api') is None - raises(AssertionError, sub.finalize, None) - sub.finalize(api) - assert read_only(sub, 'api') is api - raises(AssertionError, sub.finalize, api) + assert not o.__islocked__() + o.finalize() + assert o.__islocked__() class test_PluginProxy(ClassChecker): diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index 865a18fd..d963233b 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -379,7 +379,7 @@ class test_Command(ClassChecker): takes_args = args takes_options = options o = example() - o.finalize(object) + o.finalize() return o def test_class(self): @@ -412,7 +412,7 @@ class test_Command(ClassChecker): assert 'args' in self.cls.__public__ # Public assert self.cls().args is None o = self.cls() - o.finalize(object) + o.finalize() assert type(o.args) is plugable.NameSpace assert len(o.args) == 0 args = ('destination', 'source?') @@ -462,7 +462,7 @@ class test_Command(ClassChecker): assert 'options' in self.cls.__public__ # Public assert self.cls().options is None o = self.cls() - o.finalize(object) + o.finalize() assert type(o.options) is plugable.NameSpace assert len(o.options) == 0 options = ('target', 'files*') @@ -491,7 +491,7 @@ class test_Command(ClassChecker): expected = dict(kw) expected.update(dict(option0=u'option0', option1=u'option1')) o = self.subcls() - o.finalize(object) + o.finalize() for (key, value) in o.convert(**kw).iteritems(): v = expected[key] assert value == v @@ -509,7 +509,7 @@ class test_Command(ClassChecker): ) norm = dict((k, v.lower()) for (k, v) in kw.items()) sub = self.subcls() - sub.finalize(object) + sub.finalize() assert sub.normalize(**kw) == norm def test_get_default(self): @@ -530,7 +530,7 @@ class test_Command(ClassChecker): option1='the default', ) sub = self.subcls() - sub.finalize(object) + sub.finalize() assert sub.get_default(**no_fill) == {} assert sub.get_default(**fill) == default @@ -541,7 +541,7 @@ class test_Command(ClassChecker): assert 'validate' in self.cls.__public__ # Public sub = self.subcls() - sub.finalize(object) + sub.finalize() # Check with valid args okay = dict( @@ -639,9 +639,9 @@ class test_Object(ClassChecker): assert read_only(o, 'Method') is None assert read_only(o, 'Property') is None - def test_finalize(self): + def test_set_api(self): """ - Tests the `public.Object.finalize` method. + Tests the `public.Object.set_api` method. """ # Setup for test: class DummyAttribute(object): @@ -685,7 +685,7 @@ class test_Object(ClassChecker): # Actually perform test: o = user() - o.finalize(api) + o.set_api(api) assert read_only(o, 'api') is api for name in ['Method', 'Property']: namespace = getattr(o, name) @@ -725,9 +725,9 @@ class test_Attribute(ClassChecker): assert read_only(o, 'obj_name') == 'user' assert read_only(o, 'attr_name') == 'add' - def test_finalize(self): + def test_set_api(self): """ - Tests the `public.Attribute.finalize` method. + Tests the `public.Attribute.set_api` method. """ user_obj = 'The user public.Object instance' class api(object): @@ -737,7 +737,7 @@ class test_Attribute(ClassChecker): o = user_add() assert read_only(o, 'api') is None assert read_only(o, 'obj') is None - o.finalize(api) + o.set_api(api) assert read_only(o, 'api') is api assert read_only(o, 'obj') is user_obj -- cgit From 47e4606a98ad4dd4ca05a6ede81e6ceb164568ee Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sun, 21 Sep 2008 21:55:21 +0000 Subject: 308: Fixed broken example plugin user_add --- ipalib/plugins/example.py | 9 --------- 1 file changed, 9 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugins/example.py b/ipalib/plugins/example.py index 74874c95..4c62a5de 100644 --- a/ipalib/plugins/example.py +++ b/ipalib/plugins/example.py @@ -39,15 +39,6 @@ api.register(discover) # Register some methods for the 'user' object: class user_add(public.Method): 'Add a new user.' - - takes_args = ['login'] - - takes_options = [ - 'givenname', - 'sn', - 'initials', - ] - api.register(user_add) class user_del(public.Method): -- cgit From 2a708cfebe22869223ab73a6072091e715b04900 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sun, 21 Sep 2008 22:02:33 +0000 Subject: 309: Renamed public.Option to public.Param --- ipalib/public.py | 16 ++++++++-------- ipalib/tests/test_public.py | 40 ++++++++++++++++++++-------------------- 2 files changed, 28 insertions(+), 28 deletions(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index c4850747..a8c16282 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -85,7 +85,7 @@ class DefaultFrom(plugable.ReadOnly): return None -class Option(plugable.ReadOnly): +class Param(plugable.ReadOnly): def __init__(self, name, type_, doc='', required=False, @@ -200,7 +200,7 @@ class Option(plugable.ReadOnly): def generate_option(name): """ - Returns an `Option` instance by parsing ``name``. + Returns an `Param` instance by parsing ``name``. """ if name.endswith('?'): kw = dict(required=False, multivalue=False) @@ -213,7 +213,7 @@ def generate_option(name): name = name[:-1] else: kw = dict(required=True, multivalue=False) - return Option(name, ipa_types.Unicode(), **kw) + return Param(name, ipa_types.Unicode(), **kw) class Command(plugable.Plugin): @@ -261,9 +261,9 @@ class Command(plugable.Plugin): for arg in self.get_args(): if type(arg) is str: arg = generate_option(arg) - elif not isinstance(arg, Option): + elif not isinstance(arg, Param): raise TypeError( - 'arg: need %r or %r; got %r' % (str, Option, arg) + 'arg: need %r or %r; got %r' % (str, Param, arg) ) if optional and arg.required: raise ValueError( @@ -283,9 +283,9 @@ class Command(plugable.Plugin): for option in self.get_options(): if type(option) is str: option = generate_option(option) - elif not isinstance(option, Option): + elif not isinstance(option, Param): raise TypeError( - 'option: need %r or %r; got %r' % (str, Option, option) + 'option: need %r or %r; got %r' % (str, Param, option) ) yield option @@ -491,7 +491,7 @@ class Property(Attribute): self.__rules_iter(), key=lambda f: getattr(f, '__name__'), )) - self.option = Option(self.attr_name, self.type, + self.option = Param(self.attr_name, self.type, doc=self.doc, required=self.required, multivalue=self.multivalue, diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index d963233b..42127ea4 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -113,16 +113,16 @@ class test_DefaultFrom(ClassChecker): class test_Option(ClassChecker): """ - Tests the `public.Option` class. + Tests the `public.Param` class. """ - _cls = public.Option + _cls = public.Param def test_class(self): assert self.cls.__bases__ == (plugable.ReadOnly,) def test_init(self): """ - Tests the `public.Option.__init__` method. + Tests the `public.Param.__init__` method. """ name = 'sn' type_ = ipa_types.Unicode() @@ -139,7 +139,7 @@ class test_Option(ClassChecker): def test_convert(self): """ - Tests the `public.Option.convert` method. + Tests the `public.Param.convert` method. """ name = 'some_number' type_ = ipa_types.Int() @@ -184,7 +184,7 @@ class test_Option(ClassChecker): def test_normalize(self): """ - Tests the `public.Option.normalize` method. + Tests the `public.Param.normalize` method. """ name = 'sn' t = ipa_types.Unicode() @@ -220,7 +220,7 @@ class test_Option(ClassChecker): def test_validate(self): """ - Tests the `public.Option.validate` method. + Tests the `public.Param.validate` method. """ name = 'sn' type_ = ipa_types.Unicode() @@ -265,7 +265,7 @@ class test_Option(ClassChecker): def test_get_default(self): """ - Tests the `public.Option.get_default` method. + Tests the `public.Param.get_default` method. """ name = 'greeting' type_ = ipa_types.Unicode() @@ -299,7 +299,7 @@ class test_Option(ClassChecker): def test_get_value(self): """ - Tests the `public.Option.get_values` method. + Tests the `public.Param.get_values` method. """ name = 'status' values = (u'Active', u'Inactive') @@ -316,7 +316,7 @@ def test_generate_option(): f = public.generate_option for name in ['arg', 'arg?', 'arg*', 'arg+']: o = f(name) - assert type(o) is public.Option + assert type(o) is public.Param assert type(o.type) is ipa_types.Unicode assert o.name == 'arg' o = f('arg') @@ -357,12 +357,12 @@ class test_Command(ClassChecker): class example(self.cls): takes_options = ( - public.Option('option0', type_, + public.Param('option0', type_, normalize=normalize, default_from=default_from, rules=(Rule('option0'),) ), - public.Option('option1', type_, + public.Param('option1', type_, normalize=normalize, default_from=default_from, rules=(Rule('option1'),), @@ -420,8 +420,8 @@ class test_Command(ClassChecker): assert type(ns) is plugable.NameSpace assert len(ns) == len(args) assert list(ns) == ['destination', 'source'] - assert type(ns.destination) is public.Option - assert type(ns.source) is public.Option + assert type(ns.destination) is public.Param + assert type(ns.source) is public.Param assert ns.destination.required is True assert ns.destination.multivalue is False assert ns.source.required is False @@ -430,7 +430,7 @@ class test_Command(ClassChecker): # Test TypeError: e = raises(TypeError, self.get_instance, args=(u'whatever',)) assert str(e) == \ - 'arg: need %r or %r; got %r' % (str, public.Option, u'whatever') + 'arg: need %r or %r; got %r' % (str, public.Param, u'whatever') # Test ValueError, required after optional: e = raises(ValueError, self.get_instance, args=('arg1?', 'arg2')) @@ -470,8 +470,8 @@ class test_Command(ClassChecker): assert type(ns) is plugable.NameSpace assert len(ns) == len(options) assert list(ns) == ['target', 'files'] - assert type(ns.target) is public.Option - assert type(ns.files) is public.Option + assert type(ns.target) is public.Param + assert type(ns.files) is public.Param assert ns.target.required is True assert ns.target.multivalue is False assert ns.files.required is False @@ -774,8 +774,8 @@ class test_Method(ClassChecker): type_ = ipa_types.Unicode() class noun_verb(self.cls): takes_options= ( - public.Option('option0', type_), - public.Option('option1', type_), + public.Param('option0', type_), + public.Param('option1', type_), ) obj = example_obj() return noun_verb @@ -790,7 +790,7 @@ class test_Method(ClassChecker): assert len(options) == 4 for (i, option) in enumerate(options): assert option.name == names[i] - assert isinstance(option, public.Option) + assert isinstance(option, public.Param) class test_Property(ClassChecker): @@ -826,7 +826,7 @@ class test_Property(ClassChecker): assert len(o.rules) == 1 assert o.rules[0].__name__ == 'rule0_lowercase' opt = o.option - assert isinstance(opt, public.Option) + assert isinstance(opt, public.Param) assert opt.name == 'givenname' assert opt.doc == 'User first name' -- cgit From 14e932a8cdac2c1512880b69dc1932853d72261d Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sun, 21 Sep 2008 22:11:14 +0000 Subject: 310: generate_option() function now can accept a Param object, which it returns without modification --- ipalib/public.py | 2 ++ ipalib/tests/test_public.py | 1 + 2 files changed, 3 insertions(+) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index a8c16282..e70103ea 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -202,6 +202,8 @@ def generate_option(name): """ Returns an `Param` instance by parsing ``name``. """ + if type(name) is Param: + return name if name.endswith('?'): kw = dict(required=False, multivalue=False) name = name[:-1] diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index 42127ea4..bd6f3a95 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -319,6 +319,7 @@ def test_generate_option(): assert type(o) is public.Param assert type(o.type) is ipa_types.Unicode assert o.name == 'arg' + assert f(o) is o o = f('arg') assert o.required is True assert o.multivalue is False -- cgit From 952b45f13859a1b10a790f3448bed088a924b280 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sun, 21 Sep 2008 22:18:33 +0000 Subject: 311: Renamed generate_option() to create_param() --- ipalib/public.py | 13 +++++++++---- ipalib/tests/test_public.py | 6 +++--- 2 files changed, 12 insertions(+), 7 deletions(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index e70103ea..97843ef3 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -198,9 +198,14 @@ class Param(plugable.ReadOnly): ) -def generate_option(name): +def create_param(name): """ - Returns an `Param` instance by parsing ``name``. + Create a `Param` instance from a param name. + + If ``name`` is a `Param` instance, it is returned unchanged. + + If ``name`` is a , then ``name`` is parsed and a correpsonding + `Param` instance is created and returned. """ if type(name) is Param: return name @@ -262,7 +267,7 @@ class Command(plugable.Plugin): multivalue = False for arg in self.get_args(): if type(arg) is str: - arg = generate_option(arg) + arg = create_param(arg) elif not isinstance(arg, Param): raise TypeError( 'arg: need %r or %r; got %r' % (str, Param, arg) @@ -284,7 +289,7 @@ class Command(plugable.Plugin): def __check_options(self): for option in self.get_options(): if type(option) is str: - option = generate_option(option) + option = create_param(option) elif not isinstance(option, Param): raise TypeError( 'option: need %r or %r; got %r' % (str, Param, option) diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index bd6f3a95..01eadabd 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -309,11 +309,11 @@ class test_Option(ClassChecker): assert o.get_values() == values -def test_generate_option(): +def test_create_param(): """ - Tests the `public.generate_option` function. + Test the `public.create_param` function. """ - f = public.generate_option + f = public.create_param for name in ['arg', 'arg?', 'arg*', 'arg+']: o = f(name) assert type(o) is public.Param -- cgit From cbcadb89e4211b8b9371cdbfd2f1e8d2c39d7b53 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sun, 21 Sep 2008 22:43:50 +0000 Subject: 312: Cleaned up docstring for create_param(); Command.finalize() now uses create_param() when creating args and options namespaces --- ipalib/public.py | 64 ++++++++++++++++++++++++--------------------- ipalib/tests/test_public.py | 2 +- 2 files changed, 35 insertions(+), 31 deletions(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index 97843ef3..af964ca5 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -198,28 +198,43 @@ class Param(plugable.ReadOnly): ) -def create_param(name): +def create_param(spec): """ - Create a `Param` instance from a param name. + Create a `Param` instance from a param spec string. - If ``name`` is a `Param` instance, it is returned unchanged. + If ``spec`` is a `Param` instance, ``spec`` is returned unchanged. - If ``name`` is a , then ``name`` is parsed and a correpsonding - `Param` instance is created and returned. + If ``spec`` is an str instance, then ``spec`` is parsed and an + appropriate `Param` instance is created and returned. + + The spec string determines the param name, whether the param is required, + and whether the param is multivalue according the following syntax: + + name => required=True, multivalue=False + name? => required=False, multivalue=False + name+ => required=True, multivalue=True + name* => required=False, multivalue=True + + :param spec: A spec string or a `Param` instance. """ - if type(name) is Param: - return name - if name.endswith('?'): + if type(spec) is Param: + return spec + if type(spec) is not str: + raise TypeError( + 'create_param() takes %r or %r; got %r' % (str, Param, spec) + ) + if spec.endswith('?'): kw = dict(required=False, multivalue=False) - name = name[:-1] - elif name.endswith('*'): + name = spec[:-1] + elif spec.endswith('*'): kw = dict(required=False, multivalue=True) - name = name[:-1] - elif name.endswith('+'): + name = spec[:-1] + elif spec.endswith('+'): kw = dict(required=True, multivalue=True) - name = name[:-1] + name = spec[:-1] else: kw = dict(required=True, multivalue=False) + name = spec return Param(name, ipa_types.Unicode(), **kw) @@ -245,12 +260,12 @@ class Command(plugable.Plugin): params = None def finalize(self): - self.args = plugable.NameSpace(self.__check_args(), sort=False) + self.args = plugable.NameSpace(self.__create_args(), sort=False) if len(self.args) == 0 or not self.args[-1].multivalue: self.max_args = len(self.args) else: self.max_args = None - self.options = plugable.NameSpace(self.__check_options(), sort=False) + self.options = plugable.NameSpace(self.__create_options(), sort=False) self.params = plugable.NameSpace( tuple(self.args()) + tuple(self.options()), sort=False ) @@ -262,16 +277,11 @@ class Command(plugable.Plugin): def get_options(self): return self.takes_options - def __check_args(self): + def __create_args(self): optional = False multivalue = False for arg in self.get_args(): - if type(arg) is str: - arg = create_param(arg) - elif not isinstance(arg, Param): - raise TypeError( - 'arg: need %r or %r; got %r' % (str, Param, arg) - ) + arg = create_param(arg) if optional and arg.required: raise ValueError( '%s: required argument after optional' % arg.name @@ -286,15 +296,9 @@ class Command(plugable.Plugin): multivalue = True yield arg - def __check_options(self): + def __create_options(self): for option in self.get_options(): - if type(option) is str: - option = create_param(option) - elif not isinstance(option, Param): - raise TypeError( - 'option: need %r or %r; got %r' % (str, Param, option) - ) - yield option + yield create_param(option) def __convert_iter(self, kw): for (key, value) in kw.iteritems(): diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index 01eadabd..e97bfbc1 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -431,7 +431,7 @@ class test_Command(ClassChecker): # Test TypeError: e = raises(TypeError, self.get_instance, args=(u'whatever',)) assert str(e) == \ - 'arg: need %r or %r; got %r' % (str, public.Param, u'whatever') + 'create_param() takes %r or %r; got %r' % (str, public.Param, u'whatever') # Test ValueError, required after optional: e = raises(ValueError, self.get_instance, args=('arg1?', 'arg2')) -- cgit From 49c1c29df199dfce5d426ebe15003ab3f8431e71 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 22 Sep 2008 00:37:01 +0000 Subject: 313: Added Object.params instance attribute --- ipalib/public.py | 12 +++++++++++- ipalib/tests/test_public.py | 18 ++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index af964ca5..21c6822c 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -394,9 +394,20 @@ class Object(plugable.Plugin): __public__ = frozenset(( 'Method', 'Property', + 'params' )) __Method = None __Property = None + takes_params = tuple() + + def __init__(self): + self.params = plugable.NameSpace( + (create_param(p) for p in self.takes_params), sort=False + ) + + def __create_params(self): + for param in self.takes_params: + yield create_param(param) def __get_Method(self): return self.__Method @@ -411,7 +422,6 @@ class Object(plugable.Plugin): self.__Method = self.__create_namespace('Method') self.__Property = self.__create_namespace('Property') - def __create_namespace(self, name): return plugable.NameSpace(self.__filter_members(name)) diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index e97bfbc1..a7956907 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -702,6 +702,24 @@ class test_Object(ClassChecker): assert attr.attr_name == attr_name assert attr.name == attr_name + def test_params(self): + """ + Test the ``public.Object.params`` instance attribute. + """ + ns = self.cls().params + assert type(ns) is plugable.NameSpace + assert len(ns) == 0 + class example(self.cls): + takes_params = ('banana', 'apple') + ns = example().params + assert type(ns) is plugable.NameSpace + assert len(ns) == 2, repr(ns) + assert list(ns) == ['banana', 'apple'] + for p in ns(): + assert type(p) is public.Param + assert p.required is True + assert p.multivalue is False + class test_Attribute(ClassChecker): """ -- cgit From b206ef684388da64ee1deb37b064510705dd05bc Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 22 Sep 2008 01:28:57 +0000 Subject: 314: Completed some missing features in Command.__call__(); removed depreciated Command.print_call() method --- ipalib/plugable.py | 1 + ipalib/public.py | 20 +++++++------------- 2 files changed, 8 insertions(+), 13 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 725833cd..6e12d5c7 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -690,6 +690,7 @@ class API(DictProxy): Dynamic API object through which `Plugin` instances are accessed. """ __finalized = False + server_context = True def __init__(self, *allowed): self.__d = dict() diff --git a/ipalib/public.py b/ipalib/public.py index 21c6822c..e575ac84 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -258,6 +258,7 @@ class Command(plugable.Plugin): args = None options = None params = None + can_forward = True def finalize(self): self.args = plugable.NameSpace(self.__create_args(), sort=False) @@ -328,11 +329,9 @@ class Command(plugable.Plugin): yield(param.name, value) def get_default(self, **kw): - self.print_call('default', kw, 1) return dict(self.__get_default_iter(kw)) def validate(self, **kw): - self.print_call('validate', kw, 1) for param in self.params(): value = kw.get(param.name, None) if value is not None: @@ -340,17 +339,10 @@ class Command(plugable.Plugin): elif param.required: raise errors.RequirementError(param.name) - def execute(self, **kw): - self.print_call('execute', kw, 1) - pass - - def print_call(self, method, kw, tab=0): - print '%s%s.%s(%s)\n' % ( - ' ' * (tab *2), - self.name, - method, - ', '.join('%s=%r' % (k, kw[k]) for k in sorted(kw)), - ) + def execute(self, *args, **kw): + print '%s.execute():' % self.name + print ' args =', args + print ' kw =', kw def __call__(self, *args, **kw): if len(args) > 0: @@ -361,6 +353,8 @@ class Command(plugable.Plugin): kw = self.convert(**kw) kw.update(self.get_default(**kw)) self.validate(**kw) + args = tuple(kw.pop(name) for name in self.args) + self.execute(*args, **kw) def args_to_kw(self, *values): if self.max_args is not None and len(values) > self.max_args: -- cgit From 024022c2f9a72480864a855e131f291642b0adfe Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 22 Sep 2008 15:33:32 +0000 Subject: 315: Renamed Property.option instance attribute to Property.param --- ipalib/public.py | 11 +++++------ ipalib/tests/test_public.py | 8 ++++---- 2 files changed, 9 insertions(+), 10 deletions(-) (limited to 'ipalib') diff --git a/ipalib/public.py b/ipalib/public.py index e575ac84..678bd2de 100644 --- a/ipalib/public.py +++ b/ipalib/public.py @@ -476,20 +476,19 @@ class Method(Attribute, Command): yield option if self.obj is not None and self.obj.Property is not None: def get_key(p): - o = p.option - if o.required: - if o.default_from is None: + if p.param.required: + if p.param.default_from is None: return 0 return 1 return 2 for prop in sorted(self.obj.Property(), key=get_key): - yield prop.option + yield prop.param class Property(Attribute): __public__ = frozenset(( 'rules', - 'option', + 'param', 'type', )).union(Attribute.__public__) @@ -506,7 +505,7 @@ class Property(Attribute): self.__rules_iter(), key=lambda f: getattr(f, '__name__'), )) - self.option = Param(self.attr_name, self.type, + self.param = Param(self.attr_name, self.type, doc=self.doc, required=self.required, multivalue=self.multivalue, diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py index a7956907..fa78773f 100644 --- a/ipalib/tests/test_public.py +++ b/ipalib/tests/test_public.py @@ -844,10 +844,10 @@ class test_Property(ClassChecker): o = self.subcls() assert len(o.rules) == 1 assert o.rules[0].__name__ == 'rule0_lowercase' - opt = o.option - assert isinstance(opt, public.Param) - assert opt.name == 'givenname' - assert opt.doc == 'User first name' + param = o.param + assert isinstance(param, public.Param) + assert param.name == 'givenname' + assert param.doc == 'User first name' class test_Application(ClassChecker): -- cgit From 2842e85d88f4c6cedfdf41d3cce2ee7449369871 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 23 Sep 2008 23:51:03 +0000 Subject: 317: Renamed public.py to frontend.py; renamed test_public.py to test_frontend.py --- ipalib/frontend.py | 571 +++++++++++++++++++++++++++ ipalib/public.py | 571 --------------------------- ipalib/tests/test_frontend.py | 884 ++++++++++++++++++++++++++++++++++++++++++ ipalib/tests/test_public.py | 884 ------------------------------------------ 4 files changed, 1455 insertions(+), 1455 deletions(-) create mode 100644 ipalib/frontend.py delete mode 100644 ipalib/public.py create mode 100644 ipalib/tests/test_frontend.py delete mode 100644 ipalib/tests/test_public.py (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py new file mode 100644 index 00000000..678bd2de --- /dev/null +++ b/ipalib/frontend.py @@ -0,0 +1,571 @@ +# Authors: +# Jason Gerard DeRose +# +# 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 + +""" +Base classes for the public plugable.API instance, which the XML-RPC, CLI, +and UI all use. +""" + +import re +import inspect +import plugable +from plugable import lock, check_name +import errors +from errors import check_type, check_isinstance, raise_TypeError +import ipa_types + + +RULE_FLAG = 'validation_rule' + +def rule(obj): + assert not hasattr(obj, RULE_FLAG) + setattr(obj, RULE_FLAG, True) + return obj + +def is_rule(obj): + return callable(obj) and getattr(obj, RULE_FLAG, False) is True + + +class DefaultFrom(plugable.ReadOnly): + """ + Derives a default for one value using other supplied values. + + Here is an example that constructs a user's initials from his first + and last name: + + >>> df = DefaultFrom(lambda f, l: f[0] + l[0], 'first', 'last') + >>> df(first='John', last='Doe') # Both keys + 'JD' + >>> df() is None # Returns None if any key is missing + True + >>> df(first='John', middle='Q') is None # Still returns None + True + """ + def __init__(self, callback, *keys): + """ + :param callback: The callable to call when all ``keys`` are present. + :param keys: The keys used to map from keyword to position arguments. + """ + assert callable(callback), 'not a callable: %r' % callback + assert len(keys) > 0, 'must have at least one key' + for key in keys: + assert type(key) is str, 'not an str: %r' % key + self.callback = callback + self.keys = keys + lock(self) + + def __call__(self, **kw): + """ + If all keys are present, calls the callback; otherwise returns None. + + :param kw: The keyword arguments. + """ + vals = tuple(kw.get(k, None) for k in self.keys) + if None in vals: + return None + try: + return self.callback(*vals) + except Exception: + return None + + +class Param(plugable.ReadOnly): + def __init__(self, name, type_, + doc='', + required=False, + multivalue=False, + default=None, + default_from=None, + rules=tuple(), + normalize=None): + self.name = check_name(name) + self.doc = check_type(doc, str, 'doc') + self.type = check_isinstance(type_, ipa_types.Type, 'type_') + self.required = check_type(required, bool, 'required') + self.multivalue = check_type(multivalue, bool, 'multivalue') + self.default = default + self.default_from = check_type(default_from, + DefaultFrom, 'default_from', allow_none=True) + self.__normalize = normalize + self.rules = (type_.validate,) + rules + lock(self) + + def __convert_scalar(self, value, index=None): + if value is None: + raise TypeError('value cannot be None') + converted = self.type(value) + if converted is None: + raise errors.ConversionError( + self.name, value, self.type, index=index + ) + return converted + + def convert(self, value): + if self.multivalue: + if type(value) in (tuple, list): + return tuple( + self.__convert_scalar(v, i) for (i, v) in enumerate(value) + ) + return (self.__convert_scalar(value, 0),) # tuple + return self.__convert_scalar(value) + + def __normalize_scalar(self, value): + if not isinstance(value, basestring): + raise_TypeError(value, basestring, 'value') + try: + return self.__normalize(value) + except Exception: + return value + + def normalize(self, value): + if self.__normalize is None: + return value + if self.multivalue: + if type(value) in (tuple, list): + return tuple(self.__normalize_scalar(v) for v in value) + return (self.__normalize_scalar(value),) # tuple + return self.__normalize_scalar(value) + + def __validate_scalar(self, value, index=None): + if type(value) is not self.type.type: + raise_TypeError(value, self.type.type, 'value') + for rule in self.rules: + error = rule(value) + if error is not None: + raise errors.RuleError( + self.name, value, error, rule, index=index + ) + + def validate(self, value): + if self.multivalue: + if type(value) is not tuple: + raise_TypeError(value, tuple, 'value') + for (i, v) in enumerate(value): + self.__validate_scalar(v, i) + else: + self.__validate_scalar(value) + + def get_default(self, **kw): + if self.default_from is not None: + default = self.default_from(**kw) + if default is not None: + try: + return self.convert(self.normalize(default)) + except errors.ValidationError: + return None + return self.default + + def get_values(self): + if self.type.name in ('Enum', 'CallbackEnum'): + return self.type.values + return tuple() + + def __call__(self, value, **kw): + if value in ('', tuple(), []): + value = None + if value is None: + value = self.get_default(**kw) + if value is None: + if self.required: + raise errors.RequirementError(self.name) + return None + else: + value = self.convert(self.normalize(value)) + self.validate(value) + return value + + def __repr__(self): + return '%s(%r, %s())' % ( + self.__class__.__name__, + self.name, + self.type.name, + ) + + +def create_param(spec): + """ + Create a `Param` instance from a param spec string. + + If ``spec`` is a `Param` instance, ``spec`` is returned unchanged. + + If ``spec`` is an str instance, then ``spec`` is parsed and an + appropriate `Param` instance is created and returned. + + The spec string determines the param name, whether the param is required, + and whether the param is multivalue according the following syntax: + + name => required=True, multivalue=False + name? => required=False, multivalue=False + name+ => required=True, multivalue=True + name* => required=False, multivalue=True + + :param spec: A spec string or a `Param` instance. + """ + if type(spec) is Param: + return spec + if type(spec) is not str: + raise TypeError( + 'create_param() takes %r or %r; got %r' % (str, Param, spec) + ) + if spec.endswith('?'): + kw = dict(required=False, multivalue=False) + name = spec[:-1] + elif spec.endswith('*'): + kw = dict(required=False, multivalue=True) + name = spec[:-1] + elif spec.endswith('+'): + kw = dict(required=True, multivalue=True) + name = spec[:-1] + else: + kw = dict(required=True, multivalue=False) + name = spec + return Param(name, ipa_types.Unicode(), **kw) + + +class Command(plugable.Plugin): + __public__ = frozenset(( + 'get_default', + 'convert', + 'normalize', + 'validate', + 'execute', + '__call__', + 'smart_option_order', + 'args', + 'options', + 'params', + 'args_to_kw', + 'kw_to_args', + )) + takes_options = tuple() + takes_args = tuple() + args = None + options = None + params = None + can_forward = True + + def finalize(self): + self.args = plugable.NameSpace(self.__create_args(), sort=False) + if len(self.args) == 0 or not self.args[-1].multivalue: + self.max_args = len(self.args) + else: + self.max_args = None + self.options = plugable.NameSpace(self.__create_options(), sort=False) + self.params = plugable.NameSpace( + tuple(self.args()) + tuple(self.options()), sort=False + ) + super(Command, self).finalize() + + def get_args(self): + return self.takes_args + + def get_options(self): + return self.takes_options + + def __create_args(self): + optional = False + multivalue = False + for arg in self.get_args(): + arg = create_param(arg) + if optional and arg.required: + raise ValueError( + '%s: required argument after optional' % arg.name + ) + if multivalue: + raise ValueError( + '%s: only final argument can be multivalue' % arg.name + ) + if not arg.required: + optional = True + if arg.multivalue: + multivalue = True + yield arg + + def __create_options(self): + for option in self.get_options(): + yield create_param(option) + + def __convert_iter(self, kw): + for (key, value) in kw.iteritems(): + if key in self.params: + yield (key, self.params[key].convert(value)) + else: + yield (key, value) + + def convert(self, **kw): + return dict(self.__convert_iter(kw)) + + def __normalize_iter(self, kw): + for (key, value) in kw.iteritems(): + if key in self.params: + yield (key, self.params[key].normalize(value)) + else: + yield (key, value) + + def normalize(self, **kw): + return dict(self.__normalize_iter(kw)) + + def __get_default_iter(self, kw): + for param in self.params(): + if param.name not in kw: + value = param.get_default(**kw) + if value is not None: + yield(param.name, value) + + def get_default(self, **kw): + return dict(self.__get_default_iter(kw)) + + def validate(self, **kw): + for param in self.params(): + value = kw.get(param.name, None) + if value is not None: + param.validate(value) + elif param.required: + raise errors.RequirementError(param.name) + + def execute(self, *args, **kw): + print '%s.execute():' % self.name + print ' args =', args + print ' kw =', kw + + def __call__(self, *args, **kw): + if len(args) > 0: + arg_kw = self.args_to_kw(*args) + assert set(arg_kw).intersection(kw) == set() + kw.update(arg_kw) + kw = self.normalize(**kw) + kw = self.convert(**kw) + kw.update(self.get_default(**kw)) + self.validate(**kw) + args = tuple(kw.pop(name) for name in self.args) + self.execute(*args, **kw) + + def args_to_kw(self, *values): + if self.max_args is not None and len(values) > self.max_args: + if self.max_args == 0: + raise errors.ArgumentError(self, 'takes no arguments') + if self.max_args == 1: + raise errors.ArgumentError(self, 'takes at most 1 argument') + raise errors.ArgumentError(self, + 'takes at most %d arguments' % len(self.args) + ) + return dict(self.__args_to_kw_iter(values)) + + def __args_to_kw_iter(self, values): + multivalue = False + for (i, arg) in enumerate(self.args()): + assert not multivalue + if len(values) > i: + if arg.multivalue: + multivalue = True + yield (arg.name, values[i:]) + else: + yield (arg.name, values[i]) + else: + break + + def kw_to_args(self, **kw): + return tuple(kw.get(name, None) for name in self.args) + + +class Object(plugable.Plugin): + __public__ = frozenset(( + 'Method', + 'Property', + 'params' + )) + __Method = None + __Property = None + takes_params = tuple() + + def __init__(self): + self.params = plugable.NameSpace( + (create_param(p) for p in self.takes_params), sort=False + ) + + def __create_params(self): + for param in self.takes_params: + yield create_param(param) + + def __get_Method(self): + return self.__Method + Method = property(__get_Method) + + def __get_Property(self): + return self.__Property + Property = property(__get_Property) + + def set_api(self, api): + super(Object, self).set_api(api) + self.__Method = self.__create_namespace('Method') + self.__Property = self.__create_namespace('Property') + + def __create_namespace(self, name): + return plugable.NameSpace(self.__filter_members(name)) + + def __filter_members(self, name): + namespace = getattr(self.api, name) + assert type(namespace) is plugable.NameSpace + for proxy in namespace(): # Equivalent to dict.itervalues() + if proxy.obj_name == self.name: + yield proxy.__clone__('attr_name') + + +class Attribute(plugable.Plugin): + __public__ = frozenset(( + 'obj', + 'obj_name', + )) + __obj = None + + def __init__(self): + m = re.match( + '^([a-z][a-z0-9]+)_([a-z][a-z0-9]+)$', + self.__class__.__name__ + ) + assert m + self.__obj_name = m.group(1) + self.__attr_name = m.group(2) + + def __get_obj_name(self): + return self.__obj_name + obj_name = property(__get_obj_name) + + def __get_attr_name(self): + return self.__attr_name + attr_name = property(__get_attr_name) + + def __get_obj(self): + """ + Returns the obj instance this attribute is associated with, or None + if no association has been set. + """ + return self.__obj + obj = property(__get_obj) + + def set_api(self, api): + self.__obj = api.Object[self.obj_name] + super(Attribute, self).set_api(api) + + +class Method(Attribute, Command): + __public__ = Attribute.__public__.union(Command.__public__) + + def __init__(self): + Attribute.__init__(self) + Command.__init__(self) + + def get_options(self): + for option in self.takes_options: + yield option + if self.obj is not None and self.obj.Property is not None: + def get_key(p): + if p.param.required: + if p.param.default_from is None: + return 0 + return 1 + return 2 + for prop in sorted(self.obj.Property(), key=get_key): + yield prop.param + + +class Property(Attribute): + __public__ = frozenset(( + 'rules', + 'param', + 'type', + )).union(Attribute.__public__) + + type = ipa_types.Unicode() + required = False + multivalue = False + default = None + default_from = None + normalize = None + + def __init__(self): + super(Property, self).__init__() + self.rules = tuple(sorted( + self.__rules_iter(), + key=lambda f: getattr(f, '__name__'), + )) + self.param = Param(self.attr_name, self.type, + doc=self.doc, + required=self.required, + multivalue=self.multivalue, + default=self.default, + default_from=self.default_from, + rules=self.rules, + normalize=self.normalize, + ) + + def __rules_iter(self): + """ + Iterates through the attributes in this instance to retrieve the + methods implementing validation rules. + """ + for name in dir(self.__class__): + if name.startswith('_'): + continue + base_attr = getattr(self.__class__, name) + if is_rule(base_attr): + attr = getattr(self, name) + if is_rule(attr): + yield attr + + +class Application(Command): + """ + Base class for commands register by an external application. + + Special commands that only apply to a particular application built atop + `ipalib` should subclass from ``Application``. + + Because ``Application`` subclasses from `Command`, plugins that subclass + from ``Application`` with be available in both the ``api.Command`` and + ``api.Application`` namespaces. + """ + + __public__ = frozenset(( + 'application', + 'set_application' + )).union(Command.__public__) + __application = None + + def __get_application(self): + """ + Returns external ``application`` object. + """ + return self.__application + application = property(__get_application) + + def set_application(self, application): + """ + Sets the external application object to ``application``. + """ + if self.__application is not None: + raise AttributeError( + '%s.application can only be set once' % self.name + ) + if application is None: + raise TypeError( + '%s.application cannot be None' % self.name + ) + object.__setattr__(self, '_Application__application', application) + assert self.application is application diff --git a/ipalib/public.py b/ipalib/public.py deleted file mode 100644 index 678bd2de..00000000 --- a/ipalib/public.py +++ /dev/null @@ -1,571 +0,0 @@ -# Authors: -# Jason Gerard DeRose -# -# 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 - -""" -Base classes for the public plugable.API instance, which the XML-RPC, CLI, -and UI all use. -""" - -import re -import inspect -import plugable -from plugable import lock, check_name -import errors -from errors import check_type, check_isinstance, raise_TypeError -import ipa_types - - -RULE_FLAG = 'validation_rule' - -def rule(obj): - assert not hasattr(obj, RULE_FLAG) - setattr(obj, RULE_FLAG, True) - return obj - -def is_rule(obj): - return callable(obj) and getattr(obj, RULE_FLAG, False) is True - - -class DefaultFrom(plugable.ReadOnly): - """ - Derives a default for one value using other supplied values. - - Here is an example that constructs a user's initials from his first - and last name: - - >>> df = DefaultFrom(lambda f, l: f[0] + l[0], 'first', 'last') - >>> df(first='John', last='Doe') # Both keys - 'JD' - >>> df() is None # Returns None if any key is missing - True - >>> df(first='John', middle='Q') is None # Still returns None - True - """ - def __init__(self, callback, *keys): - """ - :param callback: The callable to call when all ``keys`` are present. - :param keys: The keys used to map from keyword to position arguments. - """ - assert callable(callback), 'not a callable: %r' % callback - assert len(keys) > 0, 'must have at least one key' - for key in keys: - assert type(key) is str, 'not an str: %r' % key - self.callback = callback - self.keys = keys - lock(self) - - def __call__(self, **kw): - """ - If all keys are present, calls the callback; otherwise returns None. - - :param kw: The keyword arguments. - """ - vals = tuple(kw.get(k, None) for k in self.keys) - if None in vals: - return None - try: - return self.callback(*vals) - except Exception: - return None - - -class Param(plugable.ReadOnly): - def __init__(self, name, type_, - doc='', - required=False, - multivalue=False, - default=None, - default_from=None, - rules=tuple(), - normalize=None): - self.name = check_name(name) - self.doc = check_type(doc, str, 'doc') - self.type = check_isinstance(type_, ipa_types.Type, 'type_') - self.required = check_type(required, bool, 'required') - self.multivalue = check_type(multivalue, bool, 'multivalue') - self.default = default - self.default_from = check_type(default_from, - DefaultFrom, 'default_from', allow_none=True) - self.__normalize = normalize - self.rules = (type_.validate,) + rules - lock(self) - - def __convert_scalar(self, value, index=None): - if value is None: - raise TypeError('value cannot be None') - converted = self.type(value) - if converted is None: - raise errors.ConversionError( - self.name, value, self.type, index=index - ) - return converted - - def convert(self, value): - if self.multivalue: - if type(value) in (tuple, list): - return tuple( - self.__convert_scalar(v, i) for (i, v) in enumerate(value) - ) - return (self.__convert_scalar(value, 0),) # tuple - return self.__convert_scalar(value) - - def __normalize_scalar(self, value): - if not isinstance(value, basestring): - raise_TypeError(value, basestring, 'value') - try: - return self.__normalize(value) - except Exception: - return value - - def normalize(self, value): - if self.__normalize is None: - return value - if self.multivalue: - if type(value) in (tuple, list): - return tuple(self.__normalize_scalar(v) for v in value) - return (self.__normalize_scalar(value),) # tuple - return self.__normalize_scalar(value) - - def __validate_scalar(self, value, index=None): - if type(value) is not self.type.type: - raise_TypeError(value, self.type.type, 'value') - for rule in self.rules: - error = rule(value) - if error is not None: - raise errors.RuleError( - self.name, value, error, rule, index=index - ) - - def validate(self, value): - if self.multivalue: - if type(value) is not tuple: - raise_TypeError(value, tuple, 'value') - for (i, v) in enumerate(value): - self.__validate_scalar(v, i) - else: - self.__validate_scalar(value) - - def get_default(self, **kw): - if self.default_from is not None: - default = self.default_from(**kw) - if default is not None: - try: - return self.convert(self.normalize(default)) - except errors.ValidationError: - return None - return self.default - - def get_values(self): - if self.type.name in ('Enum', 'CallbackEnum'): - return self.type.values - return tuple() - - def __call__(self, value, **kw): - if value in ('', tuple(), []): - value = None - if value is None: - value = self.get_default(**kw) - if value is None: - if self.required: - raise errors.RequirementError(self.name) - return None - else: - value = self.convert(self.normalize(value)) - self.validate(value) - return value - - def __repr__(self): - return '%s(%r, %s())' % ( - self.__class__.__name__, - self.name, - self.type.name, - ) - - -def create_param(spec): - """ - Create a `Param` instance from a param spec string. - - If ``spec`` is a `Param` instance, ``spec`` is returned unchanged. - - If ``spec`` is an str instance, then ``spec`` is parsed and an - appropriate `Param` instance is created and returned. - - The spec string determines the param name, whether the param is required, - and whether the param is multivalue according the following syntax: - - name => required=True, multivalue=False - name? => required=False, multivalue=False - name+ => required=True, multivalue=True - name* => required=False, multivalue=True - - :param spec: A spec string or a `Param` instance. - """ - if type(spec) is Param: - return spec - if type(spec) is not str: - raise TypeError( - 'create_param() takes %r or %r; got %r' % (str, Param, spec) - ) - if spec.endswith('?'): - kw = dict(required=False, multivalue=False) - name = spec[:-1] - elif spec.endswith('*'): - kw = dict(required=False, multivalue=True) - name = spec[:-1] - elif spec.endswith('+'): - kw = dict(required=True, multivalue=True) - name = spec[:-1] - else: - kw = dict(required=True, multivalue=False) - name = spec - return Param(name, ipa_types.Unicode(), **kw) - - -class Command(plugable.Plugin): - __public__ = frozenset(( - 'get_default', - 'convert', - 'normalize', - 'validate', - 'execute', - '__call__', - 'smart_option_order', - 'args', - 'options', - 'params', - 'args_to_kw', - 'kw_to_args', - )) - takes_options = tuple() - takes_args = tuple() - args = None - options = None - params = None - can_forward = True - - def finalize(self): - self.args = plugable.NameSpace(self.__create_args(), sort=False) - if len(self.args) == 0 or not self.args[-1].multivalue: - self.max_args = len(self.args) - else: - self.max_args = None - self.options = plugable.NameSpace(self.__create_options(), sort=False) - self.params = plugable.NameSpace( - tuple(self.args()) + tuple(self.options()), sort=False - ) - super(Command, self).finalize() - - def get_args(self): - return self.takes_args - - def get_options(self): - return self.takes_options - - def __create_args(self): - optional = False - multivalue = False - for arg in self.get_args(): - arg = create_param(arg) - if optional and arg.required: - raise ValueError( - '%s: required argument after optional' % arg.name - ) - if multivalue: - raise ValueError( - '%s: only final argument can be multivalue' % arg.name - ) - if not arg.required: - optional = True - if arg.multivalue: - multivalue = True - yield arg - - def __create_options(self): - for option in self.get_options(): - yield create_param(option) - - def __convert_iter(self, kw): - for (key, value) in kw.iteritems(): - if key in self.params: - yield (key, self.params[key].convert(value)) - else: - yield (key, value) - - def convert(self, **kw): - return dict(self.__convert_iter(kw)) - - def __normalize_iter(self, kw): - for (key, value) in kw.iteritems(): - if key in self.params: - yield (key, self.params[key].normalize(value)) - else: - yield (key, value) - - def normalize(self, **kw): - return dict(self.__normalize_iter(kw)) - - def __get_default_iter(self, kw): - for param in self.params(): - if param.name not in kw: - value = param.get_default(**kw) - if value is not None: - yield(param.name, value) - - def get_default(self, **kw): - return dict(self.__get_default_iter(kw)) - - def validate(self, **kw): - for param in self.params(): - value = kw.get(param.name, None) - if value is not None: - param.validate(value) - elif param.required: - raise errors.RequirementError(param.name) - - def execute(self, *args, **kw): - print '%s.execute():' % self.name - print ' args =', args - print ' kw =', kw - - def __call__(self, *args, **kw): - if len(args) > 0: - arg_kw = self.args_to_kw(*args) - assert set(arg_kw).intersection(kw) == set() - kw.update(arg_kw) - kw = self.normalize(**kw) - kw = self.convert(**kw) - kw.update(self.get_default(**kw)) - self.validate(**kw) - args = tuple(kw.pop(name) for name in self.args) - self.execute(*args, **kw) - - def args_to_kw(self, *values): - if self.max_args is not None and len(values) > self.max_args: - if self.max_args == 0: - raise errors.ArgumentError(self, 'takes no arguments') - if self.max_args == 1: - raise errors.ArgumentError(self, 'takes at most 1 argument') - raise errors.ArgumentError(self, - 'takes at most %d arguments' % len(self.args) - ) - return dict(self.__args_to_kw_iter(values)) - - def __args_to_kw_iter(self, values): - multivalue = False - for (i, arg) in enumerate(self.args()): - assert not multivalue - if len(values) > i: - if arg.multivalue: - multivalue = True - yield (arg.name, values[i:]) - else: - yield (arg.name, values[i]) - else: - break - - def kw_to_args(self, **kw): - return tuple(kw.get(name, None) for name in self.args) - - -class Object(plugable.Plugin): - __public__ = frozenset(( - 'Method', - 'Property', - 'params' - )) - __Method = None - __Property = None - takes_params = tuple() - - def __init__(self): - self.params = plugable.NameSpace( - (create_param(p) for p in self.takes_params), sort=False - ) - - def __create_params(self): - for param in self.takes_params: - yield create_param(param) - - def __get_Method(self): - return self.__Method - Method = property(__get_Method) - - def __get_Property(self): - return self.__Property - Property = property(__get_Property) - - def set_api(self, api): - super(Object, self).set_api(api) - self.__Method = self.__create_namespace('Method') - self.__Property = self.__create_namespace('Property') - - def __create_namespace(self, name): - return plugable.NameSpace(self.__filter_members(name)) - - def __filter_members(self, name): - namespace = getattr(self.api, name) - assert type(namespace) is plugable.NameSpace - for proxy in namespace(): # Equivalent to dict.itervalues() - if proxy.obj_name == self.name: - yield proxy.__clone__('attr_name') - - -class Attribute(plugable.Plugin): - __public__ = frozenset(( - 'obj', - 'obj_name', - )) - __obj = None - - def __init__(self): - m = re.match( - '^([a-z][a-z0-9]+)_([a-z][a-z0-9]+)$', - self.__class__.__name__ - ) - assert m - self.__obj_name = m.group(1) - self.__attr_name = m.group(2) - - def __get_obj_name(self): - return self.__obj_name - obj_name = property(__get_obj_name) - - def __get_attr_name(self): - return self.__attr_name - attr_name = property(__get_attr_name) - - def __get_obj(self): - """ - Returns the obj instance this attribute is associated with, or None - if no association has been set. - """ - return self.__obj - obj = property(__get_obj) - - def set_api(self, api): - self.__obj = api.Object[self.obj_name] - super(Attribute, self).set_api(api) - - -class Method(Attribute, Command): - __public__ = Attribute.__public__.union(Command.__public__) - - def __init__(self): - Attribute.__init__(self) - Command.__init__(self) - - def get_options(self): - for option in self.takes_options: - yield option - if self.obj is not None and self.obj.Property is not None: - def get_key(p): - if p.param.required: - if p.param.default_from is None: - return 0 - return 1 - return 2 - for prop in sorted(self.obj.Property(), key=get_key): - yield prop.param - - -class Property(Attribute): - __public__ = frozenset(( - 'rules', - 'param', - 'type', - )).union(Attribute.__public__) - - type = ipa_types.Unicode() - required = False - multivalue = False - default = None - default_from = None - normalize = None - - def __init__(self): - super(Property, self).__init__() - self.rules = tuple(sorted( - self.__rules_iter(), - key=lambda f: getattr(f, '__name__'), - )) - self.param = Param(self.attr_name, self.type, - doc=self.doc, - required=self.required, - multivalue=self.multivalue, - default=self.default, - default_from=self.default_from, - rules=self.rules, - normalize=self.normalize, - ) - - def __rules_iter(self): - """ - Iterates through the attributes in this instance to retrieve the - methods implementing validation rules. - """ - for name in dir(self.__class__): - if name.startswith('_'): - continue - base_attr = getattr(self.__class__, name) - if is_rule(base_attr): - attr = getattr(self, name) - if is_rule(attr): - yield attr - - -class Application(Command): - """ - Base class for commands register by an external application. - - Special commands that only apply to a particular application built atop - `ipalib` should subclass from ``Application``. - - Because ``Application`` subclasses from `Command`, plugins that subclass - from ``Application`` with be available in both the ``api.Command`` and - ``api.Application`` namespaces. - """ - - __public__ = frozenset(( - 'application', - 'set_application' - )).union(Command.__public__) - __application = None - - def __get_application(self): - """ - Returns external ``application`` object. - """ - return self.__application - application = property(__get_application) - - def set_application(self, application): - """ - Sets the external application object to ``application``. - """ - if self.__application is not None: - raise AttributeError( - '%s.application can only be set once' % self.name - ) - if application is None: - raise TypeError( - '%s.application cannot be None' % self.name - ) - object.__setattr__(self, '_Application__application', application) - assert self.application is application diff --git a/ipalib/tests/test_frontend.py b/ipalib/tests/test_frontend.py new file mode 100644 index 00000000..fa78773f --- /dev/null +++ b/ipalib/tests/test_frontend.py @@ -0,0 +1,884 @@ +# Authors: +# Jason Gerard DeRose +# +# 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 + +""" +Unit tests for `ipalib.public` module. +""" + +from tstutil import raises, getitem, no_set, no_del, read_only, ClassChecker +from tstutil import check_TypeError +from ipalib import public, plugable, errors, ipa_types + + +def test_RULE_FLAG(): + assert public.RULE_FLAG == 'validation_rule' + + +def test_rule(): + """ + Tests the `public.rule` function. + """ + flag = public.RULE_FLAG + rule = public.rule + def my_func(): + pass + assert not hasattr(my_func, flag) + rule(my_func) + assert getattr(my_func, flag) is True + @rule + def my_func2(): + pass + assert getattr(my_func2, flag) is True + + +def test_is_rule(): + """ + Tests the `public.is_rule` function. + """ + is_rule = public.is_rule + flag = public.RULE_FLAG + + class no_call(object): + def __init__(self, value): + if value is not None: + assert value in (True, False) + setattr(self, flag, value) + + class call(no_call): + def __call__(self): + pass + + assert is_rule(call(True)) + assert not is_rule(no_call(True)) + assert not is_rule(call(False)) + assert not is_rule(call(None)) + + +class test_DefaultFrom(ClassChecker): + """ + Tests the `public.DefaultFrom` class. + """ + _cls = public.DefaultFrom + + def test_class(self): + assert self.cls.__bases__ == (plugable.ReadOnly,) + + def test_init(self): + """ + Tests the `public.DefaultFrom.__init__` method. + """ + def callback(*args): + return args + keys = ('givenname', 'sn') + o = self.cls(callback, *keys) + assert read_only(o, 'callback') is callback + assert read_only(o, 'keys') == keys + + def test_call(self): + """ + Tests the `public.DefaultFrom.__call__` method. + """ + def callback(givenname, sn): + return givenname[0] + sn[0] + keys = ('givenname', 'sn') + o = self.cls(callback, *keys) + kw = dict( + givenname='John', + sn='Public', + hello='world', + ) + assert o(**kw) == 'JP' + assert o() is None + for key in ('givenname', 'sn'): + kw_copy = dict(kw) + del kw_copy[key] + assert o(**kw_copy) is None + + +class test_Option(ClassChecker): + """ + Tests the `public.Param` class. + """ + _cls = public.Param + + def test_class(self): + assert self.cls.__bases__ == (plugable.ReadOnly,) + + def test_init(self): + """ + Tests the `public.Param.__init__` method. + """ + name = 'sn' + type_ = ipa_types.Unicode() + o = self.cls(name, type_) + assert o.__islocked__() is True + assert read_only(o, 'name') is name + assert read_only(o, 'type') is type_ + assert read_only(o, 'doc') == '' + assert read_only(o, 'required') is False + assert read_only(o, 'multivalue') is False + assert read_only(o, 'default') is None + assert read_only(o, 'default_from') is None + assert read_only(o, 'rules') == (type_.validate,) + + def test_convert(self): + """ + Tests the `public.Param.convert` method. + """ + name = 'some_number' + type_ = ipa_types.Int() + okay = (7, 7L, 7.0, ' 7 ') + fail = ('7.0', '7L', 'whatever', object) + + # Scenario 1: multivalue=False + o = self.cls(name, type_) + e = raises(TypeError, o.convert, None) + assert str(e) == 'value cannot be None' + for value in okay: + new = o.convert(value) + assert new == 7 + assert type(new) is int + for value in fail: + e = raises(errors.ConversionError, o.convert, value) + assert e.name is name + assert e.value is value + assert e.error is type_.conversion_error + assert e.index is None + + # Scenario 2: multivalue=True + o = self.cls(name, type_, multivalue=True) + for none in [None, (7, None)]: + e = raises(TypeError, o.convert, none) + assert str(e) == 'value cannot be None' + for value in okay: + assert o.convert((value,)) == (7,) + assert o.convert([value]) == (7,) + assert o.convert(okay) == tuple(int(v) for v in okay) + cnt = 5 + for value in fail: + for i in xrange(cnt): + others = list(7 for x in xrange(cnt)) + others[i] = value + for v in [tuple(others), list(others)]: + e = raises(errors.ConversionError, o.convert, v) + assert e.name is name + assert e.value is value + assert e.error is type_.conversion_error + assert e.index == i + + def test_normalize(self): + """ + Tests the `public.Param.normalize` method. + """ + name = 'sn' + t = ipa_types.Unicode() + callback = lambda value: value.lower() + values = (None, u'Hello', (u'Hello',), 'hello', ['hello']) + + # Scenario 1: multivalue=False, normalize=None + o = self.cls(name, t) + for v in values: + # When normalize=None, value is returned, no type checking: + assert o.normalize(v) is v + + # Scenario 2: multivalue=False, normalize=callback + o = self.cls(name, t, normalize=callback) + for v in (u'Hello', u'hello', 'Hello'): # Okay + assert o.normalize(v) == 'hello' + for v in [None, 42, (u'Hello',)]: # Not basestring + check_TypeError(v, basestring, 'value', o.normalize, v) + + # Scenario 3: multivalue=True, normalize=None + o = self.cls(name, t, multivalue=True) + for v in values: + # When normalize=None, value is returned, no type checking: + assert o.normalize(v) is v + + # Scenario 4: multivalue=True, normalize=callback + o = self.cls(name, t, multivalue=True, normalize=callback) + for value in [(u'Hello',), (u'hello',), 'Hello', ['Hello']]: # Okay + assert o.normalize(value) == (u'hello',) + fail = 42 # Not basestring + for v in [fail, [fail], (u'Hello', fail)]: # Non unicode member + check_TypeError(fail, basestring, 'value', o.normalize, v) + + def test_validate(self): + """ + Tests the `public.Param.validate` method. + """ + name = 'sn' + type_ = ipa_types.Unicode() + def case_rule(value): + if not value.islower(): + return 'Must be lower case' + my_rules = (case_rule,) + okay = u'whatever' + fail_case = u'Whatever' + fail_type = 'whatever' + + # Scenario 1: multivalue=False + o = self.cls(name, type_, rules=my_rules) + assert o.rules == (type_.validate, case_rule) + o.validate(okay) + e = raises(errors.RuleError, o.validate, fail_case) + assert e.name is name + assert e.value is fail_case + assert e.error == 'Must be lower case' + assert e.rule is case_rule + assert e.index is None + check_TypeError(fail_type, unicode, 'value', o.validate, fail_type) + + ## Scenario 2: multivalue=True + o = self.cls(name, type_, multivalue=True, rules=my_rules) + o.validate((okay,)) + cnt = 5 + for i in xrange(cnt): + others = list(okay for x in xrange(cnt)) + others[i] = fail_case + value = tuple(others) + e = raises(errors.RuleError, o.validate, value) + assert e.name is name + assert e.value is fail_case + assert e.error == 'Must be lower case' + assert e.rule is case_rule + assert e.index == i + for not_tuple in (okay, [okay]): + check_TypeError(not_tuple, tuple, 'value', o.validate, not_tuple) + for has_str in [(fail_type,), (okay, fail_type)]: + check_TypeError(fail_type, unicode, 'value', o.validate, has_str) + + def test_get_default(self): + """ + Tests the `public.Param.get_default` method. + """ + name = 'greeting' + type_ = ipa_types.Unicode() + default = u'Hello, world!' + default_from = public.DefaultFrom( + lambda first, last: u'Hello, %s %s!' % (first, last), + 'first', 'last' + ) + + # Scenario 1: multivalue=False + o = self.cls(name, type_, + default=default, + default_from=default_from, + ) + assert o.default is default + assert o.default_from is default_from + assert o.get_default() == default + assert o.get_default(first='John', last='Doe') == 'Hello, John Doe!' + + # Scenario 2: multivalue=True + default = (default,) + o = self.cls(name, type_, + default=default, + default_from=default_from, + multivalue=True, + ) + assert o.default is default + assert o.default_from is default_from + assert o.get_default() == default + assert o.get_default(first='John', last='Doe') == ('Hello, John Doe!',) + + def test_get_value(self): + """ + Tests the `public.Param.get_values` method. + """ + name = 'status' + values = (u'Active', u'Inactive') + o = self.cls(name, ipa_types.Unicode()) + assert o.get_values() == tuple() + o = self.cls(name, ipa_types.Enum(*values)) + assert o.get_values() == values + + +def test_create_param(): + """ + Test the `public.create_param` function. + """ + f = public.create_param + for name in ['arg', 'arg?', 'arg*', 'arg+']: + o = f(name) + assert type(o) is public.Param + assert type(o.type) is ipa_types.Unicode + assert o.name == 'arg' + assert f(o) is o + o = f('arg') + assert o.required is True + assert o.multivalue is False + o = f('arg?') + assert o.required is False + assert o.multivalue is False + o = f('arg*') + assert o.required is False + assert o.multivalue is True + o = f('arg+') + assert o.required is True + assert o.multivalue is True + + +class test_Command(ClassChecker): + """ + Tests the `public.Command` class. + """ + _cls = public.Command + + def get_subcls(self): + class Rule(object): + def __init__(self, name): + self.name = name + + def __call__(self, value): + if value != self.name: + return 'must equal %s' % self.name + + default_from = public.DefaultFrom( + lambda arg: arg, + 'default_from' + ) + normalize = lambda value: value.lower() + type_ = ipa_types.Unicode() + + class example(self.cls): + takes_options = ( + public.Param('option0', type_, + normalize=normalize, + default_from=default_from, + rules=(Rule('option0'),) + ), + public.Param('option1', type_, + normalize=normalize, + default_from=default_from, + rules=(Rule('option1'),), + required=True, + ), + ) + return example + + def get_instance(self, args=tuple(), options=tuple()): + """ + Helper method used to test args and options. + """ + class example(self.cls): + takes_args = args + takes_options = options + o = example() + o.finalize() + return o + + def test_class(self): + assert self.cls.__bases__ == (plugable.Plugin,) + assert self.cls.takes_options == tuple() + assert self.cls.takes_args == tuple() + + def test_get_args(self): + """ + Tests the `public.Command.get_args` method. + """ + assert list(self.cls().get_args()) == [] + args = ('login', 'stuff') + o = self.get_instance(args=args) + assert o.get_args() is args + + def test_get_options(self): + """ + Tests the `public.Command.get_options` method. + """ + assert list(self.cls().get_options()) == [] + options = ('verbose', 'debug') + o = self.get_instance(options=options) + assert o.get_options() is options + + def test_args(self): + """ + Tests the ``Command.args`` instance attribute. + """ + assert 'args' in self.cls.__public__ # Public + assert self.cls().args is None + o = self.cls() + o.finalize() + assert type(o.args) is plugable.NameSpace + assert len(o.args) == 0 + args = ('destination', 'source?') + ns = self.get_instance(args=args).args + assert type(ns) is plugable.NameSpace + assert len(ns) == len(args) + assert list(ns) == ['destination', 'source'] + assert type(ns.destination) is public.Param + assert type(ns.source) is public.Param + assert ns.destination.required is True + assert ns.destination.multivalue is False + assert ns.source.required is False + assert ns.source.multivalue is False + + # Test TypeError: + e = raises(TypeError, self.get_instance, args=(u'whatever',)) + assert str(e) == \ + 'create_param() takes %r or %r; got %r' % (str, public.Param, u'whatever') + + # Test ValueError, required after optional: + e = raises(ValueError, self.get_instance, args=('arg1?', 'arg2')) + assert str(e) == 'arg2: required argument after optional' + + # Test ValueError, scalar after multivalue: + e = raises(ValueError, self.get_instance, args=('arg1+', 'arg2')) + assert str(e) == 'arg2: only final argument can be multivalue' + + def test_max_args(self): + """ + Test the ``Command.max_args`` instance attribute. + """ + o = self.get_instance() + assert o.max_args == 0 + o = self.get_instance(args=('one?',)) + assert o.max_args == 1 + o = self.get_instance(args=('one', 'two?')) + assert o.max_args == 2 + o = self.get_instance(args=('one', 'multi+',)) + assert o.max_args is None + o = self.get_instance(args=('one', 'multi*',)) + assert o.max_args is None + + def test_options(self): + """ + Tests the ``Command.options`` instance attribute. + """ + assert 'options' in self.cls.__public__ # Public + assert self.cls().options is None + o = self.cls() + o.finalize() + assert type(o.options) is plugable.NameSpace + assert len(o.options) == 0 + options = ('target', 'files*') + ns = self.get_instance(options=options).options + assert type(ns) is plugable.NameSpace + assert len(ns) == len(options) + assert list(ns) == ['target', 'files'] + assert type(ns.target) is public.Param + assert type(ns.files) is public.Param + assert ns.target.required is True + assert ns.target.multivalue is False + assert ns.files.required is False + assert ns.files.multivalue is True + + def test_convert(self): + """ + Tests the `public.Command.convert` method. + """ + assert 'convert' in self.cls.__public__ # Public + kw = dict( + option0='option0', + option1='option1', + whatever=False, + also=object, + ) + expected = dict(kw) + expected.update(dict(option0=u'option0', option1=u'option1')) + o = self.subcls() + o.finalize() + for (key, value) in o.convert(**kw).iteritems(): + v = expected[key] + assert value == v + assert type(value) is type(v) + + def test_normalize(self): + """ + Tests the `public.Command.normalize` method. + """ + assert 'normalize' in self.cls.__public__ # Public + kw = dict( + option0=u'OPTION0', + option1=u'OPTION1', + option2=u'option2', + ) + norm = dict((k, v.lower()) for (k, v) in kw.items()) + sub = self.subcls() + sub.finalize() + assert sub.normalize(**kw) == norm + + def test_get_default(self): + """ + Tests the `public.Command.get_default` method. + """ + assert 'get_default' in self.cls.__public__ # Public + no_fill = dict( + option0='value0', + option1='value1', + whatever='hello world', + ) + fill = dict( + default_from='the default', + ) + default = dict( + option0='the default', + option1='the default', + ) + sub = self.subcls() + sub.finalize() + assert sub.get_default(**no_fill) == {} + assert sub.get_default(**fill) == default + + def test_validate(self): + """ + Tests the `public.Command.validate` method. + """ + assert 'validate' in self.cls.__public__ # Public + + sub = self.subcls() + sub.finalize() + + # Check with valid args + okay = dict( + option0=u'option0', + option1=u'option1', + another_option='some value', + ) + sub.validate(**okay) + + # Check with an invalid arg + fail = dict(okay) + fail['option0'] = u'whatever' + e = raises(errors.RuleError, sub.validate, **fail) + assert e.name == 'option0' + assert e.value == u'whatever' + assert e.error == 'must equal option0' + assert e.rule.__class__.__name__ == 'Rule' + assert e.index is None + + # Check with a missing required arg + fail = dict(okay) + fail.pop('option1') + e = raises(errors.RequirementError, sub.validate, **fail) + assert e.name == 'option1' + assert e.value is None + assert e.index is None + + def test_execute(self): + """ + Tests the `public.Command.execute` method. + """ + assert 'execute' in self.cls.__public__ # Public + + def test_args_to_kw(self): + """ + Test the `public.Command.args_to_kw` method. + """ + assert 'args_to_kw' in self.cls.__public__ # Public + o = self.get_instance(args=('one', 'two?')) + assert o.args_to_kw(1) == dict(one=1) + assert o.args_to_kw(1, 2) == dict(one=1, two=2) + + o = self.get_instance(args=('one', 'two*')) + assert o.args_to_kw(1) == dict(one=1) + assert o.args_to_kw(1, 2) == dict(one=1, two=(2,)) + assert o.args_to_kw(1, 2, 3) == dict(one=1, two=(2, 3)) + + o = self.get_instance(args=('one', 'two+')) + assert o.args_to_kw(1) == dict(one=1) + assert o.args_to_kw(1, 2) == dict(one=1, two=(2,)) + assert o.args_to_kw(1, 2, 3) == dict(one=1, two=(2, 3)) + + o = self.get_instance() + e = raises(errors.ArgumentError, o.args_to_kw, 1) + assert str(e) == 'example takes no arguments' + + o = self.get_instance(args=('one?',)) + e = raises(errors.ArgumentError, o.args_to_kw, 1, 2) + assert str(e) == 'example takes at most 1 argument' + + o = self.get_instance(args=('one', 'two?')) + e = raises(errors.ArgumentError, o.args_to_kw, 1, 2, 3) + assert str(e) == 'example takes at most 2 arguments' + + def test_kw_to_args(self): + """ + Tests the `public.Command.kw_to_args` method. + """ + assert 'kw_to_args' in self.cls.__public__ # Public + o = self.get_instance(args=('one', 'two?')) + assert o.kw_to_args() == (None, None) + assert o.kw_to_args(whatever='hello') == (None, None) + assert o.kw_to_args(one='the one') == ('the one', None) + assert o.kw_to_args(two='the two') == (None, 'the two') + assert o.kw_to_args(whatever='hello', two='Two', one='One') == \ + ('One', 'Two') + + +class test_Object(ClassChecker): + """ + Tests the `public.Object` class. + """ + _cls = public.Object + + def test_class(self): + assert self.cls.__bases__ == (plugable.Plugin,) + assert type(self.cls.Method) is property + assert type(self.cls.Property) is property + + def test_init(self): + """ + Tests the `public.Object.__init__` method. + """ + o = self.cls() + assert read_only(o, 'Method') is None + assert read_only(o, 'Property') is None + + def test_set_api(self): + """ + Tests the `public.Object.set_api` method. + """ + # Setup for test: + class DummyAttribute(object): + def __init__(self, obj_name, attr_name, name=None): + self.obj_name = obj_name + self.attr_name = attr_name + if name is None: + self.name = '%s_%s' % (obj_name, attr_name) + else: + self.name = name + def __clone__(self, attr_name): + return self.__class__( + self.obj_name, + self.attr_name, + getattr(self, attr_name) + ) + + def get_attributes(cnt, format): + for name in ['other', 'user', 'another']: + for i in xrange(cnt): + yield DummyAttribute(name, format % i) + + cnt = 10 + formats = dict( + Method='method_%d', + Property='property_%d', + ) + + class api(object): + Method = plugable.NameSpace( + get_attributes(cnt, formats['Method']) + ) + Property = plugable.NameSpace( + get_attributes(cnt, formats['Property']) + ) + assert len(api.Method) == cnt * 3 + assert len(api.Property) == cnt * 3 + + class user(self.cls): + pass + + # Actually perform test: + o = user() + o.set_api(api) + assert read_only(o, 'api') is api + for name in ['Method', 'Property']: + namespace = getattr(o, name) + assert isinstance(namespace, plugable.NameSpace) + assert len(namespace) == cnt + f = formats[name] + for i in xrange(cnt): + attr_name = f % i + attr = namespace[attr_name] + assert isinstance(attr, DummyAttribute) + assert attr is getattr(namespace, attr_name) + assert attr.obj_name == 'user' + assert attr.attr_name == attr_name + assert attr.name == attr_name + + def test_params(self): + """ + Test the ``public.Object.params`` instance attribute. + """ + ns = self.cls().params + assert type(ns) is plugable.NameSpace + assert len(ns) == 0 + class example(self.cls): + takes_params = ('banana', 'apple') + ns = example().params + assert type(ns) is plugable.NameSpace + assert len(ns) == 2, repr(ns) + assert list(ns) == ['banana', 'apple'] + for p in ns(): + assert type(p) is public.Param + assert p.required is True + assert p.multivalue is False + + +class test_Attribute(ClassChecker): + """ + Tests the `public.Attribute` class. + """ + _cls = public.Attribute + + def test_class(self): + assert self.cls.__bases__ == (plugable.Plugin,) + assert type(self.cls.obj) is property + assert type(self.cls.obj_name) is property + assert type(self.cls.attr_name) is property + + def test_init(self): + """ + Tests the `public.Attribute.__init__` method. + """ + class user_add(self.cls): + pass + o = user_add() + assert read_only(o, 'obj') is None + assert read_only(o, 'obj_name') == 'user' + assert read_only(o, 'attr_name') == 'add' + + def test_set_api(self): + """ + Tests the `public.Attribute.set_api` method. + """ + user_obj = 'The user public.Object instance' + class api(object): + Object = dict(user=user_obj) + class user_add(self.cls): + pass + o = user_add() + assert read_only(o, 'api') is None + assert read_only(o, 'obj') is None + o.set_api(api) + assert read_only(o, 'api') is api + assert read_only(o, 'obj') is user_obj + + +class test_Method(ClassChecker): + """ + Tests the `public.Method` class. + """ + _cls = public.Method + + def test_class(self): + assert self.cls.__bases__ == (public.Attribute, public.Command) + assert self.cls.implements(public.Command) + + def get_subcls(self): + class example_prop0(public.Property): + 'Prop zero' + class example_prop1(public.Property): + 'Prop one' + class example_obj(object): + __prop = None + def __get_prop(self): + if self.__prop is None: + self.__prop = plugable.NameSpace([ + plugable.PluginProxy( + public.Property, example_prop0(), 'attr_name' + ), + plugable.PluginProxy( + public.Property, example_prop1(), 'attr_name' + ), + ]) + return self.__prop + Property = property(__get_prop) + type_ = ipa_types.Unicode() + class noun_verb(self.cls): + takes_options= ( + public.Param('option0', type_), + public.Param('option1', type_), + ) + obj = example_obj() + return noun_verb + + def test_get_options(self): + """ + Tests the `public.Method.get_options` method. + """ + sub = self.subcls() + names = ('option0', 'option1', 'prop0', 'prop1') + options = tuple(sub.get_options()) + assert len(options) == 4 + for (i, option) in enumerate(options): + assert option.name == names[i] + assert isinstance(option, public.Param) + + +class test_Property(ClassChecker): + """ + Tests the `public.Property` class. + """ + _cls = public.Property + + def get_subcls(self): + class user_givenname(self.cls): + 'User first name' + + @public.rule + def rule0_lowercase(self, value): + if not value.islower(): + return 'Must be lowercase' + return user_givenname + + def test_class(self): + assert self.cls.__bases__ == (public.Attribute,) + assert isinstance(self.cls.type, ipa_types.Unicode) + assert self.cls.required is False + assert self.cls.multivalue is False + assert self.cls.default is None + assert self.cls.default_from is None + assert self.cls.normalize is None + + def test_init(self): + """ + Tests the `public.Property.__init__` method. + """ + o = self.subcls() + assert len(o.rules) == 1 + assert o.rules[0].__name__ == 'rule0_lowercase' + param = o.param + assert isinstance(param, public.Param) + assert param.name == 'givenname' + assert param.doc == 'User first name' + + +class test_Application(ClassChecker): + """ + Tests the `public.Application` class. + """ + _cls = public.Application + + def test_class(self): + assert self.cls.__bases__ == (public.Command,) + assert type(self.cls.application) is property + + def test_application(self): + """ + Tests the `public.Application.application` property. + """ + assert 'application' in self.cls.__public__ # Public + assert 'set_application' in self.cls.__public__ # Public + app = 'The external application' + class example(self.cls): + 'A subclass' + for o in (self.cls(), example()): + assert read_only(o, 'application') is None + e = raises(TypeError, o.set_application, None) + assert str(e) == ( + '%s.application cannot be None' % o.__class__.__name__ + ) + o.set_application(app) + assert read_only(o, 'application') is app + e = raises(AttributeError, o.set_application, app) + assert str(e) == ( + '%s.application can only be set once' % o.__class__.__name__ + ) + assert read_only(o, 'application') is app diff --git a/ipalib/tests/test_public.py b/ipalib/tests/test_public.py deleted file mode 100644 index fa78773f..00000000 --- a/ipalib/tests/test_public.py +++ /dev/null @@ -1,884 +0,0 @@ -# Authors: -# Jason Gerard DeRose -# -# 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 - -""" -Unit tests for `ipalib.public` module. -""" - -from tstutil import raises, getitem, no_set, no_del, read_only, ClassChecker -from tstutil import check_TypeError -from ipalib import public, plugable, errors, ipa_types - - -def test_RULE_FLAG(): - assert public.RULE_FLAG == 'validation_rule' - - -def test_rule(): - """ - Tests the `public.rule` function. - """ - flag = public.RULE_FLAG - rule = public.rule - def my_func(): - pass - assert not hasattr(my_func, flag) - rule(my_func) - assert getattr(my_func, flag) is True - @rule - def my_func2(): - pass - assert getattr(my_func2, flag) is True - - -def test_is_rule(): - """ - Tests the `public.is_rule` function. - """ - is_rule = public.is_rule - flag = public.RULE_FLAG - - class no_call(object): - def __init__(self, value): - if value is not None: - assert value in (True, False) - setattr(self, flag, value) - - class call(no_call): - def __call__(self): - pass - - assert is_rule(call(True)) - assert not is_rule(no_call(True)) - assert not is_rule(call(False)) - assert not is_rule(call(None)) - - -class test_DefaultFrom(ClassChecker): - """ - Tests the `public.DefaultFrom` class. - """ - _cls = public.DefaultFrom - - def test_class(self): - assert self.cls.__bases__ == (plugable.ReadOnly,) - - def test_init(self): - """ - Tests the `public.DefaultFrom.__init__` method. - """ - def callback(*args): - return args - keys = ('givenname', 'sn') - o = self.cls(callback, *keys) - assert read_only(o, 'callback') is callback - assert read_only(o, 'keys') == keys - - def test_call(self): - """ - Tests the `public.DefaultFrom.__call__` method. - """ - def callback(givenname, sn): - return givenname[0] + sn[0] - keys = ('givenname', 'sn') - o = self.cls(callback, *keys) - kw = dict( - givenname='John', - sn='Public', - hello='world', - ) - assert o(**kw) == 'JP' - assert o() is None - for key in ('givenname', 'sn'): - kw_copy = dict(kw) - del kw_copy[key] - assert o(**kw_copy) is None - - -class test_Option(ClassChecker): - """ - Tests the `public.Param` class. - """ - _cls = public.Param - - def test_class(self): - assert self.cls.__bases__ == (plugable.ReadOnly,) - - def test_init(self): - """ - Tests the `public.Param.__init__` method. - """ - name = 'sn' - type_ = ipa_types.Unicode() - o = self.cls(name, type_) - assert o.__islocked__() is True - assert read_only(o, 'name') is name - assert read_only(o, 'type') is type_ - assert read_only(o, 'doc') == '' - assert read_only(o, 'required') is False - assert read_only(o, 'multivalue') is False - assert read_only(o, 'default') is None - assert read_only(o, 'default_from') is None - assert read_only(o, 'rules') == (type_.validate,) - - def test_convert(self): - """ - Tests the `public.Param.convert` method. - """ - name = 'some_number' - type_ = ipa_types.Int() - okay = (7, 7L, 7.0, ' 7 ') - fail = ('7.0', '7L', 'whatever', object) - - # Scenario 1: multivalue=False - o = self.cls(name, type_) - e = raises(TypeError, o.convert, None) - assert str(e) == 'value cannot be None' - for value in okay: - new = o.convert(value) - assert new == 7 - assert type(new) is int - for value in fail: - e = raises(errors.ConversionError, o.convert, value) - assert e.name is name - assert e.value is value - assert e.error is type_.conversion_error - assert e.index is None - - # Scenario 2: multivalue=True - o = self.cls(name, type_, multivalue=True) - for none in [None, (7, None)]: - e = raises(TypeError, o.convert, none) - assert str(e) == 'value cannot be None' - for value in okay: - assert o.convert((value,)) == (7,) - assert o.convert([value]) == (7,) - assert o.convert(okay) == tuple(int(v) for v in okay) - cnt = 5 - for value in fail: - for i in xrange(cnt): - others = list(7 for x in xrange(cnt)) - others[i] = value - for v in [tuple(others), list(others)]: - e = raises(errors.ConversionError, o.convert, v) - assert e.name is name - assert e.value is value - assert e.error is type_.conversion_error - assert e.index == i - - def test_normalize(self): - """ - Tests the `public.Param.normalize` method. - """ - name = 'sn' - t = ipa_types.Unicode() - callback = lambda value: value.lower() - values = (None, u'Hello', (u'Hello',), 'hello', ['hello']) - - # Scenario 1: multivalue=False, normalize=None - o = self.cls(name, t) - for v in values: - # When normalize=None, value is returned, no type checking: - assert o.normalize(v) is v - - # Scenario 2: multivalue=False, normalize=callback - o = self.cls(name, t, normalize=callback) - for v in (u'Hello', u'hello', 'Hello'): # Okay - assert o.normalize(v) == 'hello' - for v in [None, 42, (u'Hello',)]: # Not basestring - check_TypeError(v, basestring, 'value', o.normalize, v) - - # Scenario 3: multivalue=True, normalize=None - o = self.cls(name, t, multivalue=True) - for v in values: - # When normalize=None, value is returned, no type checking: - assert o.normalize(v) is v - - # Scenario 4: multivalue=True, normalize=callback - o = self.cls(name, t, multivalue=True, normalize=callback) - for value in [(u'Hello',), (u'hello',), 'Hello', ['Hello']]: # Okay - assert o.normalize(value) == (u'hello',) - fail = 42 # Not basestring - for v in [fail, [fail], (u'Hello', fail)]: # Non unicode member - check_TypeError(fail, basestring, 'value', o.normalize, v) - - def test_validate(self): - """ - Tests the `public.Param.validate` method. - """ - name = 'sn' - type_ = ipa_types.Unicode() - def case_rule(value): - if not value.islower(): - return 'Must be lower case' - my_rules = (case_rule,) - okay = u'whatever' - fail_case = u'Whatever' - fail_type = 'whatever' - - # Scenario 1: multivalue=False - o = self.cls(name, type_, rules=my_rules) - assert o.rules == (type_.validate, case_rule) - o.validate(okay) - e = raises(errors.RuleError, o.validate, fail_case) - assert e.name is name - assert e.value is fail_case - assert e.error == 'Must be lower case' - assert e.rule is case_rule - assert e.index is None - check_TypeError(fail_type, unicode, 'value', o.validate, fail_type) - - ## Scenario 2: multivalue=True - o = self.cls(name, type_, multivalue=True, rules=my_rules) - o.validate((okay,)) - cnt = 5 - for i in xrange(cnt): - others = list(okay for x in xrange(cnt)) - others[i] = fail_case - value = tuple(others) - e = raises(errors.RuleError, o.validate, value) - assert e.name is name - assert e.value is fail_case - assert e.error == 'Must be lower case' - assert e.rule is case_rule - assert e.index == i - for not_tuple in (okay, [okay]): - check_TypeError(not_tuple, tuple, 'value', o.validate, not_tuple) - for has_str in [(fail_type,), (okay, fail_type)]: - check_TypeError(fail_type, unicode, 'value', o.validate, has_str) - - def test_get_default(self): - """ - Tests the `public.Param.get_default` method. - """ - name = 'greeting' - type_ = ipa_types.Unicode() - default = u'Hello, world!' - default_from = public.DefaultFrom( - lambda first, last: u'Hello, %s %s!' % (first, last), - 'first', 'last' - ) - - # Scenario 1: multivalue=False - o = self.cls(name, type_, - default=default, - default_from=default_from, - ) - assert o.default is default - assert o.default_from is default_from - assert o.get_default() == default - assert o.get_default(first='John', last='Doe') == 'Hello, John Doe!' - - # Scenario 2: multivalue=True - default = (default,) - o = self.cls(name, type_, - default=default, - default_from=default_from, - multivalue=True, - ) - assert o.default is default - assert o.default_from is default_from - assert o.get_default() == default - assert o.get_default(first='John', last='Doe') == ('Hello, John Doe!',) - - def test_get_value(self): - """ - Tests the `public.Param.get_values` method. - """ - name = 'status' - values = (u'Active', u'Inactive') - o = self.cls(name, ipa_types.Unicode()) - assert o.get_values() == tuple() - o = self.cls(name, ipa_types.Enum(*values)) - assert o.get_values() == values - - -def test_create_param(): - """ - Test the `public.create_param` function. - """ - f = public.create_param - for name in ['arg', 'arg?', 'arg*', 'arg+']: - o = f(name) - assert type(o) is public.Param - assert type(o.type) is ipa_types.Unicode - assert o.name == 'arg' - assert f(o) is o - o = f('arg') - assert o.required is True - assert o.multivalue is False - o = f('arg?') - assert o.required is False - assert o.multivalue is False - o = f('arg*') - assert o.required is False - assert o.multivalue is True - o = f('arg+') - assert o.required is True - assert o.multivalue is True - - -class test_Command(ClassChecker): - """ - Tests the `public.Command` class. - """ - _cls = public.Command - - def get_subcls(self): - class Rule(object): - def __init__(self, name): - self.name = name - - def __call__(self, value): - if value != self.name: - return 'must equal %s' % self.name - - default_from = public.DefaultFrom( - lambda arg: arg, - 'default_from' - ) - normalize = lambda value: value.lower() - type_ = ipa_types.Unicode() - - class example(self.cls): - takes_options = ( - public.Param('option0', type_, - normalize=normalize, - default_from=default_from, - rules=(Rule('option0'),) - ), - public.Param('option1', type_, - normalize=normalize, - default_from=default_from, - rules=(Rule('option1'),), - required=True, - ), - ) - return example - - def get_instance(self, args=tuple(), options=tuple()): - """ - Helper method used to test args and options. - """ - class example(self.cls): - takes_args = args - takes_options = options - o = example() - o.finalize() - return o - - def test_class(self): - assert self.cls.__bases__ == (plugable.Plugin,) - assert self.cls.takes_options == tuple() - assert self.cls.takes_args == tuple() - - def test_get_args(self): - """ - Tests the `public.Command.get_args` method. - """ - assert list(self.cls().get_args()) == [] - args = ('login', 'stuff') - o = self.get_instance(args=args) - assert o.get_args() is args - - def test_get_options(self): - """ - Tests the `public.Command.get_options` method. - """ - assert list(self.cls().get_options()) == [] - options = ('verbose', 'debug') - o = self.get_instance(options=options) - assert o.get_options() is options - - def test_args(self): - """ - Tests the ``Command.args`` instance attribute. - """ - assert 'args' in self.cls.__public__ # Public - assert self.cls().args is None - o = self.cls() - o.finalize() - assert type(o.args) is plugable.NameSpace - assert len(o.args) == 0 - args = ('destination', 'source?') - ns = self.get_instance(args=args).args - assert type(ns) is plugable.NameSpace - assert len(ns) == len(args) - assert list(ns) == ['destination', 'source'] - assert type(ns.destination) is public.Param - assert type(ns.source) is public.Param - assert ns.destination.required is True - assert ns.destination.multivalue is False - assert ns.source.required is False - assert ns.source.multivalue is False - - # Test TypeError: - e = raises(TypeError, self.get_instance, args=(u'whatever',)) - assert str(e) == \ - 'create_param() takes %r or %r; got %r' % (str, public.Param, u'whatever') - - # Test ValueError, required after optional: - e = raises(ValueError, self.get_instance, args=('arg1?', 'arg2')) - assert str(e) == 'arg2: required argument after optional' - - # Test ValueError, scalar after multivalue: - e = raises(ValueError, self.get_instance, args=('arg1+', 'arg2')) - assert str(e) == 'arg2: only final argument can be multivalue' - - def test_max_args(self): - """ - Test the ``Command.max_args`` instance attribute. - """ - o = self.get_instance() - assert o.max_args == 0 - o = self.get_instance(args=('one?',)) - assert o.max_args == 1 - o = self.get_instance(args=('one', 'two?')) - assert o.max_args == 2 - o = self.get_instance(args=('one', 'multi+',)) - assert o.max_args is None - o = self.get_instance(args=('one', 'multi*',)) - assert o.max_args is None - - def test_options(self): - """ - Tests the ``Command.options`` instance attribute. - """ - assert 'options' in self.cls.__public__ # Public - assert self.cls().options is None - o = self.cls() - o.finalize() - assert type(o.options) is plugable.NameSpace - assert len(o.options) == 0 - options = ('target', 'files*') - ns = self.get_instance(options=options).options - assert type(ns) is plugable.NameSpace - assert len(ns) == len(options) - assert list(ns) == ['target', 'files'] - assert type(ns.target) is public.Param - assert type(ns.files) is public.Param - assert ns.target.required is True - assert ns.target.multivalue is False - assert ns.files.required is False - assert ns.files.multivalue is True - - def test_convert(self): - """ - Tests the `public.Command.convert` method. - """ - assert 'convert' in self.cls.__public__ # Public - kw = dict( - option0='option0', - option1='option1', - whatever=False, - also=object, - ) - expected = dict(kw) - expected.update(dict(option0=u'option0', option1=u'option1')) - o = self.subcls() - o.finalize() - for (key, value) in o.convert(**kw).iteritems(): - v = expected[key] - assert value == v - assert type(value) is type(v) - - def test_normalize(self): - """ - Tests the `public.Command.normalize` method. - """ - assert 'normalize' in self.cls.__public__ # Public - kw = dict( - option0=u'OPTION0', - option1=u'OPTION1', - option2=u'option2', - ) - norm = dict((k, v.lower()) for (k, v) in kw.items()) - sub = self.subcls() - sub.finalize() - assert sub.normalize(**kw) == norm - - def test_get_default(self): - """ - Tests the `public.Command.get_default` method. - """ - assert 'get_default' in self.cls.__public__ # Public - no_fill = dict( - option0='value0', - option1='value1', - whatever='hello world', - ) - fill = dict( - default_from='the default', - ) - default = dict( - option0='the default', - option1='the default', - ) - sub = self.subcls() - sub.finalize() - assert sub.get_default(**no_fill) == {} - assert sub.get_default(**fill) == default - - def test_validate(self): - """ - Tests the `public.Command.validate` method. - """ - assert 'validate' in self.cls.__public__ # Public - - sub = self.subcls() - sub.finalize() - - # Check with valid args - okay = dict( - option0=u'option0', - option1=u'option1', - another_option='some value', - ) - sub.validate(**okay) - - # Check with an invalid arg - fail = dict(okay) - fail['option0'] = u'whatever' - e = raises(errors.RuleError, sub.validate, **fail) - assert e.name == 'option0' - assert e.value == u'whatever' - assert e.error == 'must equal option0' - assert e.rule.__class__.__name__ == 'Rule' - assert e.index is None - - # Check with a missing required arg - fail = dict(okay) - fail.pop('option1') - e = raises(errors.RequirementError, sub.validate, **fail) - assert e.name == 'option1' - assert e.value is None - assert e.index is None - - def test_execute(self): - """ - Tests the `public.Command.execute` method. - """ - assert 'execute' in self.cls.__public__ # Public - - def test_args_to_kw(self): - """ - Test the `public.Command.args_to_kw` method. - """ - assert 'args_to_kw' in self.cls.__public__ # Public - o = self.get_instance(args=('one', 'two?')) - assert o.args_to_kw(1) == dict(one=1) - assert o.args_to_kw(1, 2) == dict(one=1, two=2) - - o = self.get_instance(args=('one', 'two*')) - assert o.args_to_kw(1) == dict(one=1) - assert o.args_to_kw(1, 2) == dict(one=1, two=(2,)) - assert o.args_to_kw(1, 2, 3) == dict(one=1, two=(2, 3)) - - o = self.get_instance(args=('one', 'two+')) - assert o.args_to_kw(1) == dict(one=1) - assert o.args_to_kw(1, 2) == dict(one=1, two=(2,)) - assert o.args_to_kw(1, 2, 3) == dict(one=1, two=(2, 3)) - - o = self.get_instance() - e = raises(errors.ArgumentError, o.args_to_kw, 1) - assert str(e) == 'example takes no arguments' - - o = self.get_instance(args=('one?',)) - e = raises(errors.ArgumentError, o.args_to_kw, 1, 2) - assert str(e) == 'example takes at most 1 argument' - - o = self.get_instance(args=('one', 'two?')) - e = raises(errors.ArgumentError, o.args_to_kw, 1, 2, 3) - assert str(e) == 'example takes at most 2 arguments' - - def test_kw_to_args(self): - """ - Tests the `public.Command.kw_to_args` method. - """ - assert 'kw_to_args' in self.cls.__public__ # Public - o = self.get_instance(args=('one', 'two?')) - assert o.kw_to_args() == (None, None) - assert o.kw_to_args(whatever='hello') == (None, None) - assert o.kw_to_args(one='the one') == ('the one', None) - assert o.kw_to_args(two='the two') == (None, 'the two') - assert o.kw_to_args(whatever='hello', two='Two', one='One') == \ - ('One', 'Two') - - -class test_Object(ClassChecker): - """ - Tests the `public.Object` class. - """ - _cls = public.Object - - def test_class(self): - assert self.cls.__bases__ == (plugable.Plugin,) - assert type(self.cls.Method) is property - assert type(self.cls.Property) is property - - def test_init(self): - """ - Tests the `public.Object.__init__` method. - """ - o = self.cls() - assert read_only(o, 'Method') is None - assert read_only(o, 'Property') is None - - def test_set_api(self): - """ - Tests the `public.Object.set_api` method. - """ - # Setup for test: - class DummyAttribute(object): - def __init__(self, obj_name, attr_name, name=None): - self.obj_name = obj_name - self.attr_name = attr_name - if name is None: - self.name = '%s_%s' % (obj_name, attr_name) - else: - self.name = name - def __clone__(self, attr_name): - return self.__class__( - self.obj_name, - self.attr_name, - getattr(self, attr_name) - ) - - def get_attributes(cnt, format): - for name in ['other', 'user', 'another']: - for i in xrange(cnt): - yield DummyAttribute(name, format % i) - - cnt = 10 - formats = dict( - Method='method_%d', - Property='property_%d', - ) - - class api(object): - Method = plugable.NameSpace( - get_attributes(cnt, formats['Method']) - ) - Property = plugable.NameSpace( - get_attributes(cnt, formats['Property']) - ) - assert len(api.Method) == cnt * 3 - assert len(api.Property) == cnt * 3 - - class user(self.cls): - pass - - # Actually perform test: - o = user() - o.set_api(api) - assert read_only(o, 'api') is api - for name in ['Method', 'Property']: - namespace = getattr(o, name) - assert isinstance(namespace, plugable.NameSpace) - assert len(namespace) == cnt - f = formats[name] - for i in xrange(cnt): - attr_name = f % i - attr = namespace[attr_name] - assert isinstance(attr, DummyAttribute) - assert attr is getattr(namespace, attr_name) - assert attr.obj_name == 'user' - assert attr.attr_name == attr_name - assert attr.name == attr_name - - def test_params(self): - """ - Test the ``public.Object.params`` instance attribute. - """ - ns = self.cls().params - assert type(ns) is plugable.NameSpace - assert len(ns) == 0 - class example(self.cls): - takes_params = ('banana', 'apple') - ns = example().params - assert type(ns) is plugable.NameSpace - assert len(ns) == 2, repr(ns) - assert list(ns) == ['banana', 'apple'] - for p in ns(): - assert type(p) is public.Param - assert p.required is True - assert p.multivalue is False - - -class test_Attribute(ClassChecker): - """ - Tests the `public.Attribute` class. - """ - _cls = public.Attribute - - def test_class(self): - assert self.cls.__bases__ == (plugable.Plugin,) - assert type(self.cls.obj) is property - assert type(self.cls.obj_name) is property - assert type(self.cls.attr_name) is property - - def test_init(self): - """ - Tests the `public.Attribute.__init__` method. - """ - class user_add(self.cls): - pass - o = user_add() - assert read_only(o, 'obj') is None - assert read_only(o, 'obj_name') == 'user' - assert read_only(o, 'attr_name') == 'add' - - def test_set_api(self): - """ - Tests the `public.Attribute.set_api` method. - """ - user_obj = 'The user public.Object instance' - class api(object): - Object = dict(user=user_obj) - class user_add(self.cls): - pass - o = user_add() - assert read_only(o, 'api') is None - assert read_only(o, 'obj') is None - o.set_api(api) - assert read_only(o, 'api') is api - assert read_only(o, 'obj') is user_obj - - -class test_Method(ClassChecker): - """ - Tests the `public.Method` class. - """ - _cls = public.Method - - def test_class(self): - assert self.cls.__bases__ == (public.Attribute, public.Command) - assert self.cls.implements(public.Command) - - def get_subcls(self): - class example_prop0(public.Property): - 'Prop zero' - class example_prop1(public.Property): - 'Prop one' - class example_obj(object): - __prop = None - def __get_prop(self): - if self.__prop is None: - self.__prop = plugable.NameSpace([ - plugable.PluginProxy( - public.Property, example_prop0(), 'attr_name' - ), - plugable.PluginProxy( - public.Property, example_prop1(), 'attr_name' - ), - ]) - return self.__prop - Property = property(__get_prop) - type_ = ipa_types.Unicode() - class noun_verb(self.cls): - takes_options= ( - public.Param('option0', type_), - public.Param('option1', type_), - ) - obj = example_obj() - return noun_verb - - def test_get_options(self): - """ - Tests the `public.Method.get_options` method. - """ - sub = self.subcls() - names = ('option0', 'option1', 'prop0', 'prop1') - options = tuple(sub.get_options()) - assert len(options) == 4 - for (i, option) in enumerate(options): - assert option.name == names[i] - assert isinstance(option, public.Param) - - -class test_Property(ClassChecker): - """ - Tests the `public.Property` class. - """ - _cls = public.Property - - def get_subcls(self): - class user_givenname(self.cls): - 'User first name' - - @public.rule - def rule0_lowercase(self, value): - if not value.islower(): - return 'Must be lowercase' - return user_givenname - - def test_class(self): - assert self.cls.__bases__ == (public.Attribute,) - assert isinstance(self.cls.type, ipa_types.Unicode) - assert self.cls.required is False - assert self.cls.multivalue is False - assert self.cls.default is None - assert self.cls.default_from is None - assert self.cls.normalize is None - - def test_init(self): - """ - Tests the `public.Property.__init__` method. - """ - o = self.subcls() - assert len(o.rules) == 1 - assert o.rules[0].__name__ == 'rule0_lowercase' - param = o.param - assert isinstance(param, public.Param) - assert param.name == 'givenname' - assert param.doc == 'User first name' - - -class test_Application(ClassChecker): - """ - Tests the `public.Application` class. - """ - _cls = public.Application - - def test_class(self): - assert self.cls.__bases__ == (public.Command,) - assert type(self.cls.application) is property - - def test_application(self): - """ - Tests the `public.Application.application` property. - """ - assert 'application' in self.cls.__public__ # Public - assert 'set_application' in self.cls.__public__ # Public - app = 'The external application' - class example(self.cls): - 'A subclass' - for o in (self.cls(), example()): - assert read_only(o, 'application') is None - e = raises(TypeError, o.set_application, None) - assert str(e) == ( - '%s.application cannot be None' % o.__class__.__name__ - ) - o.set_application(app) - assert read_only(o, 'application') is app - e = raises(AttributeError, o.set_application, app) - assert str(e) == ( - '%s.application can only be set once' % o.__class__.__name__ - ) - assert read_only(o, 'application') is app -- cgit From 4e8ff5c65675fe7534afa02ce06d6ff73fd024c9 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 00:01:29 +0000 Subject: 318: Renamed all references to 'public' module to 'frontend' --- ipalib/__init__.py | 12 ++-- ipalib/cli.py | 10 +-- ipalib/plugins/example.py | 48 ++++++------- ipalib/plugins/override.py | 1 - ipalib/tests/test_frontend.py | 152 +++++++++++++++++++++--------------------- 5 files changed, 111 insertions(+), 112 deletions(-) (limited to 'ipalib') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index b0f0a1fc..e6548ff8 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -54,12 +54,12 @@ True """ import plugable -import public +import frontend api = plugable.API( - public.Command, - public.Object, - public.Method, - public.Property, - public.Application, + frontend.Command, + frontend.Object, + frontend.Method, + frontend.Property, + frontend.Application, ) diff --git a/ipalib/cli.py b/ipalib/cli.py index b16fe6b5..92c0cbc3 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -18,14 +18,14 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ -Functionality for Command Line Inteface. +Functionality for Command Line Interface. """ import re import sys import code import optparse -import public +import frontend import errors import plugable import ipa_types @@ -52,7 +52,7 @@ def from_cli(cli_name): return str(cli_name).replace('-', '_') -class help(public.Application): +class help(frontend.Application): 'Display help on a command.' takes_args = ['command'] @@ -67,7 +67,7 @@ class help(public.Application): self.application.build_parser(cmd).print_help() -class console(public.Application): +class console(frontend.Application): 'Start the IPA interactive Python console.' def __call__(self): @@ -76,7 +76,7 @@ class console(public.Application): local=dict(api=self.api) ) -class show_plugins(public.Application): +class show_plugins(frontend.Application): 'Print details on the loaded plugins.' def __call__(self): diff --git a/ipalib/plugins/example.py b/ipalib/plugins/example.py index 4c62a5de..24bf5b8f 100644 --- a/ipalib/plugins/example.py +++ b/ipalib/plugins/example.py @@ -22,62 +22,62 @@ Some example plugins. """ -from ipalib import public +from ipalib import frontend from ipalib import api # Hypothetical functional commands (not associated with any object): -class krbtest(public.Command): +class krbtest(frontend.Command): 'Test your Kerberos ticket.' api.register(krbtest) -class discover(public.Command): +class discover(frontend.Command): 'Discover IPA servers on network.' api.register(discover) # Register some methods for the 'user' object: -class user_add(public.Method): +class user_add(frontend.Method): 'Add a new user.' api.register(user_add) -class user_del(public.Method): +class user_del(frontend.Method): 'Delete an existing user.' api.register(user_del) -class user_mod(public.Method): +class user_mod(frontend.Method): 'Edit an existing user.' api.register(user_mod) -class user_find(public.Method): +class user_find(frontend.Method): 'Search the users.' api.register(user_find) # Register some properties for the 'user' object: -class user_givenname(public.Property): +class user_givenname(frontend.Property): 'User first name' required = True api.register(user_givenname) -class user_sn(public.Property): +class user_sn(frontend.Property): 'User last name' required = True api.register(user_sn) -class user_login(public.Property): +class user_login(frontend.Property): 'User login' required = True - default_from = public.DefaultFrom( + default_from = frontend.DefaultFrom( lambda first, last: (first[0] + last).lower(), 'givenname', 'sn' ) api.register(user_login) -class user_initials(public.Property): +class user_initials(frontend.Property): 'User initials' required = True - default_from = public.DefaultFrom( + default_from = frontend.DefaultFrom( lambda first, last: first[0] + last[0], 'givenname', 'sn' ) @@ -85,51 +85,51 @@ api.register(user_initials) # Register some methods for the 'group' object: -class group_add(public.Method): +class group_add(frontend.Method): 'Add a new group.' api.register(group_add) -class group_del(public.Method): +class group_del(frontend.Method): 'Delete an existing group.' api.register(group_del) -class group_mod(public.Method): +class group_mod(frontend.Method): 'Edit an existing group.' api.register(group_mod) -class group_find(public.Method): +class group_find(frontend.Method): 'Search the groups.' api.register(group_find) # Register some methods for the 'service' object -class service_add(public.Method): +class service_add(frontend.Method): 'Add a new service.' api.register(service_add) -class service_del(public.Method): +class service_del(frontend.Method): 'Delete an existing service.' api.register(service_del) -class service_mod(public.Method): +class service_mod(frontend.Method): 'Edit an existing service.' api.register(service_mod) -class service_find(public.Method): +class service_find(frontend.Method): 'Search the services.' api.register(service_find) # And to emphasis that the registration order doesn't matter, # we'll register the objects last: -class group(public.Object): +class group(frontend.Object): 'Group object' api.register(group) -class service(public.Object): +class service(frontend.Object): 'Service object' api.register(service) -class user(public.Object): +class user(frontend.Object): 'User object' api.register(user) diff --git a/ipalib/plugins/override.py b/ipalib/plugins/override.py index 1255eae7..29ec2509 100644 --- a/ipalib/plugins/override.py +++ b/ipalib/plugins/override.py @@ -24,7 +24,6 @@ This example depends upon the order that the plugins/ modules are imported in plugins/__init__.py, which will likely change in the near future. """ -from ipalib import public from ipalib import api if 'user_mod' in api.register.Method: diff --git a/ipalib/tests/test_frontend.py b/ipalib/tests/test_frontend.py index fa78773f..92d24cfb 100644 --- a/ipalib/tests/test_frontend.py +++ b/ipalib/tests/test_frontend.py @@ -23,19 +23,19 @@ Unit tests for `ipalib.public` module. from tstutil import raises, getitem, no_set, no_del, read_only, ClassChecker from tstutil import check_TypeError -from ipalib import public, plugable, errors, ipa_types +from ipalib import frontend, plugable, errors, ipa_types def test_RULE_FLAG(): - assert public.RULE_FLAG == 'validation_rule' + assert frontend.RULE_FLAG == 'validation_rule' def test_rule(): """ - Tests the `public.rule` function. + Tests the `frontend.rule` function. """ - flag = public.RULE_FLAG - rule = public.rule + flag = frontend.RULE_FLAG + rule = frontend.rule def my_func(): pass assert not hasattr(my_func, flag) @@ -49,10 +49,10 @@ def test_rule(): def test_is_rule(): """ - Tests the `public.is_rule` function. + Tests the `frontend.is_rule` function. """ - is_rule = public.is_rule - flag = public.RULE_FLAG + is_rule = frontend.is_rule + flag = frontend.RULE_FLAG class no_call(object): def __init__(self, value): @@ -72,16 +72,16 @@ def test_is_rule(): class test_DefaultFrom(ClassChecker): """ - Tests the `public.DefaultFrom` class. + Tests the `frontend.DefaultFrom` class. """ - _cls = public.DefaultFrom + _cls = frontend.DefaultFrom def test_class(self): assert self.cls.__bases__ == (plugable.ReadOnly,) def test_init(self): """ - Tests the `public.DefaultFrom.__init__` method. + Tests the `frontend.DefaultFrom.__init__` method. """ def callback(*args): return args @@ -92,7 +92,7 @@ class test_DefaultFrom(ClassChecker): def test_call(self): """ - Tests the `public.DefaultFrom.__call__` method. + Tests the `frontend.DefaultFrom.__call__` method. """ def callback(givenname, sn): return givenname[0] + sn[0] @@ -113,16 +113,16 @@ class test_DefaultFrom(ClassChecker): class test_Option(ClassChecker): """ - Tests the `public.Param` class. + Tests the `frontend.Param` class. """ - _cls = public.Param + _cls = frontend.Param def test_class(self): assert self.cls.__bases__ == (plugable.ReadOnly,) def test_init(self): """ - Tests the `public.Param.__init__` method. + Tests the `frontend.Param.__init__` method. """ name = 'sn' type_ = ipa_types.Unicode() @@ -139,7 +139,7 @@ class test_Option(ClassChecker): def test_convert(self): """ - Tests the `public.Param.convert` method. + Tests the `frontend.Param.convert` method. """ name = 'some_number' type_ = ipa_types.Int() @@ -184,7 +184,7 @@ class test_Option(ClassChecker): def test_normalize(self): """ - Tests the `public.Param.normalize` method. + Tests the `frontend.Param.normalize` method. """ name = 'sn' t = ipa_types.Unicode() @@ -220,7 +220,7 @@ class test_Option(ClassChecker): def test_validate(self): """ - Tests the `public.Param.validate` method. + Tests the `frontend.Param.validate` method. """ name = 'sn' type_ = ipa_types.Unicode() @@ -265,12 +265,12 @@ class test_Option(ClassChecker): def test_get_default(self): """ - Tests the `public.Param.get_default` method. + Tests the `frontend.Param.get_default` method. """ name = 'greeting' type_ = ipa_types.Unicode() default = u'Hello, world!' - default_from = public.DefaultFrom( + default_from = frontend.DefaultFrom( lambda first, last: u'Hello, %s %s!' % (first, last), 'first', 'last' ) @@ -299,7 +299,7 @@ class test_Option(ClassChecker): def test_get_value(self): """ - Tests the `public.Param.get_values` method. + Tests the `frontend.Param.get_values` method. """ name = 'status' values = (u'Active', u'Inactive') @@ -311,12 +311,12 @@ class test_Option(ClassChecker): def test_create_param(): """ - Test the `public.create_param` function. + Test the `frontend.create_param` function. """ - f = public.create_param + f = frontend.create_param for name in ['arg', 'arg?', 'arg*', 'arg+']: o = f(name) - assert type(o) is public.Param + assert type(o) is frontend.Param assert type(o.type) is ipa_types.Unicode assert o.name == 'arg' assert f(o) is o @@ -336,9 +336,9 @@ def test_create_param(): class test_Command(ClassChecker): """ - Tests the `public.Command` class. + Tests the `frontend.Command` class. """ - _cls = public.Command + _cls = frontend.Command def get_subcls(self): class Rule(object): @@ -349,7 +349,7 @@ class test_Command(ClassChecker): if value != self.name: return 'must equal %s' % self.name - default_from = public.DefaultFrom( + default_from = frontend.DefaultFrom( lambda arg: arg, 'default_from' ) @@ -358,12 +358,12 @@ class test_Command(ClassChecker): class example(self.cls): takes_options = ( - public.Param('option0', type_, + frontend.Param('option0', type_, normalize=normalize, default_from=default_from, rules=(Rule('option0'),) ), - public.Param('option1', type_, + frontend.Param('option1', type_, normalize=normalize, default_from=default_from, rules=(Rule('option1'),), @@ -390,7 +390,7 @@ class test_Command(ClassChecker): def test_get_args(self): """ - Tests the `public.Command.get_args` method. + Tests the `frontend.Command.get_args` method. """ assert list(self.cls().get_args()) == [] args = ('login', 'stuff') @@ -399,7 +399,7 @@ class test_Command(ClassChecker): def test_get_options(self): """ - Tests the `public.Command.get_options` method. + Tests the `frontend.Command.get_options` method. """ assert list(self.cls().get_options()) == [] options = ('verbose', 'debug') @@ -421,8 +421,8 @@ class test_Command(ClassChecker): assert type(ns) is plugable.NameSpace assert len(ns) == len(args) assert list(ns) == ['destination', 'source'] - assert type(ns.destination) is public.Param - assert type(ns.source) is public.Param + assert type(ns.destination) is frontend.Param + assert type(ns.source) is frontend.Param assert ns.destination.required is True assert ns.destination.multivalue is False assert ns.source.required is False @@ -431,7 +431,7 @@ class test_Command(ClassChecker): # Test TypeError: e = raises(TypeError, self.get_instance, args=(u'whatever',)) assert str(e) == \ - 'create_param() takes %r or %r; got %r' % (str, public.Param, u'whatever') + 'create_param() takes %r or %r; got %r' % (str, frontend.Param, u'whatever') # Test ValueError, required after optional: e = raises(ValueError, self.get_instance, args=('arg1?', 'arg2')) @@ -471,8 +471,8 @@ class test_Command(ClassChecker): assert type(ns) is plugable.NameSpace assert len(ns) == len(options) assert list(ns) == ['target', 'files'] - assert type(ns.target) is public.Param - assert type(ns.files) is public.Param + assert type(ns.target) is frontend.Param + assert type(ns.files) is frontend.Param assert ns.target.required is True assert ns.target.multivalue is False assert ns.files.required is False @@ -480,7 +480,7 @@ class test_Command(ClassChecker): def test_convert(self): """ - Tests the `public.Command.convert` method. + Tests the `frontend.Command.convert` method. """ assert 'convert' in self.cls.__public__ # Public kw = dict( @@ -500,7 +500,7 @@ class test_Command(ClassChecker): def test_normalize(self): """ - Tests the `public.Command.normalize` method. + Tests the `frontend.Command.normalize` method. """ assert 'normalize' in self.cls.__public__ # Public kw = dict( @@ -515,7 +515,7 @@ class test_Command(ClassChecker): def test_get_default(self): """ - Tests the `public.Command.get_default` method. + Tests the `frontend.Command.get_default` method. """ assert 'get_default' in self.cls.__public__ # Public no_fill = dict( @@ -537,7 +537,7 @@ class test_Command(ClassChecker): def test_validate(self): """ - Tests the `public.Command.validate` method. + Tests the `frontend.Command.validate` method. """ assert 'validate' in self.cls.__public__ # Public @@ -572,13 +572,13 @@ class test_Command(ClassChecker): def test_execute(self): """ - Tests the `public.Command.execute` method. + Tests the `frontend.Command.execute` method. """ assert 'execute' in self.cls.__public__ # Public def test_args_to_kw(self): """ - Test the `public.Command.args_to_kw` method. + Test the `frontend.Command.args_to_kw` method. """ assert 'args_to_kw' in self.cls.__public__ # Public o = self.get_instance(args=('one', 'two?')) @@ -609,7 +609,7 @@ class test_Command(ClassChecker): def test_kw_to_args(self): """ - Tests the `public.Command.kw_to_args` method. + Tests the `frontend.Command.kw_to_args` method. """ assert 'kw_to_args' in self.cls.__public__ # Public o = self.get_instance(args=('one', 'two?')) @@ -623,9 +623,9 @@ class test_Command(ClassChecker): class test_Object(ClassChecker): """ - Tests the `public.Object` class. + Tests the `frontend.Object` class. """ - _cls = public.Object + _cls = frontend.Object def test_class(self): assert self.cls.__bases__ == (plugable.Plugin,) @@ -634,7 +634,7 @@ class test_Object(ClassChecker): def test_init(self): """ - Tests the `public.Object.__init__` method. + Tests the `frontend.Object.__init__` method. """ o = self.cls() assert read_only(o, 'Method') is None @@ -642,7 +642,7 @@ class test_Object(ClassChecker): def test_set_api(self): """ - Tests the `public.Object.set_api` method. + Tests the `frontend.Object.set_api` method. """ # Setup for test: class DummyAttribute(object): @@ -704,7 +704,7 @@ class test_Object(ClassChecker): def test_params(self): """ - Test the ``public.Object.params`` instance attribute. + Test the ``frontend.Object.params`` instance attribute. """ ns = self.cls().params assert type(ns) is plugable.NameSpace @@ -716,16 +716,16 @@ class test_Object(ClassChecker): assert len(ns) == 2, repr(ns) assert list(ns) == ['banana', 'apple'] for p in ns(): - assert type(p) is public.Param + assert type(p) is frontend.Param assert p.required is True assert p.multivalue is False class test_Attribute(ClassChecker): """ - Tests the `public.Attribute` class. + Tests the `frontend.Attribute` class. """ - _cls = public.Attribute + _cls = frontend.Attribute def test_class(self): assert self.cls.__bases__ == (plugable.Plugin,) @@ -735,7 +735,7 @@ class test_Attribute(ClassChecker): def test_init(self): """ - Tests the `public.Attribute.__init__` method. + Tests the `frontend.Attribute.__init__` method. """ class user_add(self.cls): pass @@ -746,9 +746,9 @@ class test_Attribute(ClassChecker): def test_set_api(self): """ - Tests the `public.Attribute.set_api` method. + Tests the `frontend.Attribute.set_api` method. """ - user_obj = 'The user public.Object instance' + user_obj = 'The user frontend.Object instance' class api(object): Object = dict(user=user_obj) class user_add(self.cls): @@ -763,18 +763,18 @@ class test_Attribute(ClassChecker): class test_Method(ClassChecker): """ - Tests the `public.Method` class. + Tests the `frontend.Method` class. """ - _cls = public.Method + _cls = frontend.Method def test_class(self): - assert self.cls.__bases__ == (public.Attribute, public.Command) - assert self.cls.implements(public.Command) + assert self.cls.__bases__ == (frontend.Attribute, frontend.Command) + assert self.cls.implements(frontend.Command) def get_subcls(self): - class example_prop0(public.Property): + class example_prop0(frontend.Property): 'Prop zero' - class example_prop1(public.Property): + class example_prop1(frontend.Property): 'Prop one' class example_obj(object): __prop = None @@ -782,10 +782,10 @@ class test_Method(ClassChecker): if self.__prop is None: self.__prop = plugable.NameSpace([ plugable.PluginProxy( - public.Property, example_prop0(), 'attr_name' + frontend.Property, example_prop0(), 'attr_name' ), plugable.PluginProxy( - public.Property, example_prop1(), 'attr_name' + frontend.Property, example_prop1(), 'attr_name' ), ]) return self.__prop @@ -793,15 +793,15 @@ class test_Method(ClassChecker): type_ = ipa_types.Unicode() class noun_verb(self.cls): takes_options= ( - public.Param('option0', type_), - public.Param('option1', type_), + frontend.Param('option0', type_), + frontend.Param('option1', type_), ) obj = example_obj() return noun_verb def test_get_options(self): """ - Tests the `public.Method.get_options` method. + Tests the `frontend.Method.get_options` method. """ sub = self.subcls() names = ('option0', 'option1', 'prop0', 'prop1') @@ -809,27 +809,27 @@ class test_Method(ClassChecker): assert len(options) == 4 for (i, option) in enumerate(options): assert option.name == names[i] - assert isinstance(option, public.Param) + assert isinstance(option, frontend.Param) class test_Property(ClassChecker): """ - Tests the `public.Property` class. + Tests the `frontend.Property` class. """ - _cls = public.Property + _cls = frontend.Property def get_subcls(self): class user_givenname(self.cls): 'User first name' - @public.rule + @frontend.rule def rule0_lowercase(self, value): if not value.islower(): return 'Must be lowercase' return user_givenname def test_class(self): - assert self.cls.__bases__ == (public.Attribute,) + assert self.cls.__bases__ == (frontend.Attribute,) assert isinstance(self.cls.type, ipa_types.Unicode) assert self.cls.required is False assert self.cls.multivalue is False @@ -839,30 +839,30 @@ class test_Property(ClassChecker): def test_init(self): """ - Tests the `public.Property.__init__` method. + Tests the `frontend.Property.__init__` method. """ o = self.subcls() assert len(o.rules) == 1 assert o.rules[0].__name__ == 'rule0_lowercase' param = o.param - assert isinstance(param, public.Param) + assert isinstance(param, frontend.Param) assert param.name == 'givenname' assert param.doc == 'User first name' class test_Application(ClassChecker): """ - Tests the `public.Application` class. + Tests the `frontend.Application` class. """ - _cls = public.Application + _cls = frontend.Application def test_class(self): - assert self.cls.__bases__ == (public.Command,) + assert self.cls.__bases__ == (frontend.Command,) assert type(self.cls.application) is property def test_application(self): """ - Tests the `public.Application.application` property. + Tests the `frontend.Application.application` property. """ assert 'application' in self.cls.__public__ # Public assert 'set_application' in self.cls.__public__ # Public -- cgit From 81de10f176437053ac47bfee8f5ec81e38f2cf57 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 00:12:35 +0000 Subject: 319: Added new backend and tests.test_backend modules; added place-holder Backend class and corresponding unit tests --- ipalib/backend.py | 27 +++++++++++++++++++++++++++ ipalib/frontend.py | 3 +-- ipalib/tests/test_backend.py | 36 ++++++++++++++++++++++++++++++++++++ ipalib/tests/test_frontend.py | 2 +- 4 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 ipalib/backend.py create mode 100644 ipalib/tests/test_backend.py (limited to 'ipalib') diff --git a/ipalib/backend.py b/ipalib/backend.py new file mode 100644 index 00000000..053609c5 --- /dev/null +++ b/ipalib/backend.py @@ -0,0 +1,27 @@ +# Authors: +# Jason Gerard DeRose +# +# 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 + +""" +Base classes for all backed-end plugins. +""" + +import plugable + +class Backend(plugable.Plugin): + pass diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 678bd2de..489b874c 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -18,8 +18,7 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ -Base classes for the public plugable.API instance, which the XML-RPC, CLI, -and UI all use. +Base classes for all front-end plugins. """ import re diff --git a/ipalib/tests/test_backend.py b/ipalib/tests/test_backend.py new file mode 100644 index 00000000..2183ed0a --- /dev/null +++ b/ipalib/tests/test_backend.py @@ -0,0 +1,36 @@ +# Authors: +# Jason Gerard DeRose +# +# 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 + +""" +Unit tests for `ipalib.backend` module. +""" + +from ipalib import backend, plugable, errors +from tstutil import ClassChecker + + +class test_Backend(ClassChecker): + """ + Test the `backend.Backend` class. + """ + + _cls = backend.Backend + + def test_class(self): + assert self.cls.__bases__ == (plugable.Plugin,) diff --git a/ipalib/tests/test_frontend.py b/ipalib/tests/test_frontend.py index 92d24cfb..d4a70f15 100644 --- a/ipalib/tests/test_frontend.py +++ b/ipalib/tests/test_frontend.py @@ -18,7 +18,7 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ -Unit tests for `ipalib.public` module. +Unit tests for `ipalib.frontend` module. """ from tstutil import raises, getitem, no_set, no_del, read_only, ClassChecker -- cgit From f3aaf65f1c4bbee31dae9431423ab88a15eba990 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 00:44:41 +0000 Subject: 320: plugable.API now respects the Plugin.__proxy__ flag; added test for plugins without proxy to unit tests for API --- ipalib/backend.py | 6 +++++- ipalib/plugable.py | 6 +++++- ipalib/tests/test_backend.py | 1 + ipalib/tests/test_plugable.py | 19 +++++++++++++++++++ 4 files changed, 30 insertions(+), 2 deletions(-) (limited to 'ipalib') diff --git a/ipalib/backend.py b/ipalib/backend.py index 053609c5..82ed14f3 100644 --- a/ipalib/backend.py +++ b/ipalib/backend.py @@ -24,4 +24,8 @@ Base classes for all backed-end plugins. import plugable class Backend(plugable.Plugin): - pass + """ + Base class for all backend plugins. + """ + + __proxy__ = False # Backend plugins are not wrapped in a PluginProxy diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 6e12d5c7..f883eb12 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -241,6 +241,7 @@ class Plugin(ReadOnly): Base class for all plugins. """ __public__ = frozenset() + __proxy__ = True __api = None def __get_name(self): @@ -709,7 +710,10 @@ class API(DictProxy): if klass not in instances: instances[klass] = klass() plugin = instances[klass] - yield PluginProxy(base, plugin) + if base.__proxy__: + yield PluginProxy(base, plugin) + else: + yield plugin for name in self.register: base = self.register[name] diff --git a/ipalib/tests/test_backend.py b/ipalib/tests/test_backend.py index 2183ed0a..967e9fdf 100644 --- a/ipalib/tests/test_backend.py +++ b/ipalib/tests/test_backend.py @@ -34,3 +34,4 @@ class test_Backend(ClassChecker): def test_class(self): assert self.cls.__bases__ == (plugable.Plugin,) + assert self.cls.__proxy__ is False diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index 6c796f9e..02d35cde 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -795,3 +795,22 @@ def test_API(): # Test that calling finilize again raises AssertionError: raises(AssertionError, api.finalize) + + # Test with base class that doesn't request a proxy + class NoProxy(plugable.Plugin): + __proxy__ = False + api = plugable.API(NoProxy) + class plugin0(NoProxy): + pass + api.register(plugin0) + class plugin1(NoProxy): + pass + api.register(plugin1) + api.finalize() + names = ['plugin0', 'plugin1'] + assert list(api.NoProxy) == names + for name in names: + plugin = api.NoProxy[name] + assert getattr(api.NoProxy, name) is plugin + assert isinstance(plugin, plugable.Plugin) + assert not isinstance(plugin, plugable.PluginProxy) -- cgit From c38b90d5d4de1c6d351ce1d9fd94555376d6dda7 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 01:36:54 +0000 Subject: 321: Standard ipalib.api attribute now accepts plugins from the Backend base class --- ipalib/__init__.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'ipalib') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index e6548ff8..216d654f 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -55,6 +55,7 @@ True import plugable import frontend +import backend api = plugable.API( frontend.Command, @@ -62,4 +63,5 @@ api = plugable.API( frontend.Method, frontend.Property, frontend.Application, + backend.Backend, ) -- cgit From 4dbbf5656d4db96068ca6c936120827e52ba5ad8 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 01:43:51 +0000 Subject: 322: Updated ipalib package docstring, replacing cross reference to 'public' with 'frontend' --- ipalib/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'ipalib') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index 216d654f..6c129a41 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -24,9 +24,11 @@ To learn about the ``ipalib`` library, you should read the code in this order: 1. Learn about the plugin framework in `ipalib.plugable` - 2. Learn about the public API in `ipalib.public` + 2. Learn about the frontend plugins in `ipalib.frontend` - 3. Look at some example plugins in `ipalib.plugins.example` + 3. Learn about the backend plugins in `ipalib.backend` + + 4. Look at some example plugins in `ipalib.plugins.example` Here is a short console example on using the plugable API: -- cgit From 19bbc48eb601bb942ed93776c05bf0c326970832 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 02:52:19 +0000 Subject: 323: Added Command.run() method that dispatches to execute() or forward(); added corresponding unit tests --- ipalib/frontend.py | 15 ++++++++++++++- ipalib/plugable.py | 8 ++++++-- ipalib/tests/test_frontend.py | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 3 deletions(-) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 489b874c..11d05d5f 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -343,6 +343,11 @@ class Command(plugable.Plugin): print ' args =', args print ' kw =', kw + def forward(self, *args, **kw): + print '%s.execute():' % self.name + print ' args =', args + print ' kw =', kw + def __call__(self, *args, **kw): if len(args) > 0: arg_kw = self.args_to_kw(*args) @@ -353,7 +358,15 @@ class Command(plugable.Plugin): kw.update(self.get_default(**kw)) self.validate(**kw) args = tuple(kw.pop(name) for name in self.args) - self.execute(*args, **kw) + return self.run(*args, **kw) + + def run(self, *args, **kw): + if self.api.env.in_server_context: + target = self.execute + else: + target = self.forward + object.__setattr__(self, 'run', target) + return target(*args, **kw) def args_to_kw(self, *values): if self.max_args is not None and len(values) > self.max_args: diff --git a/ipalib/plugable.py b/ipalib/plugable.py index f883eb12..8bf90ea8 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -691,11 +691,15 @@ class API(DictProxy): Dynamic API object through which `Plugin` instances are accessed. """ __finalized = False - server_context = True - def __init__(self, *allowed): + def __init__(self, *allowed, **kw): self.__d = dict() self.register = Registrar(*allowed) + default = dict( + in_server_context=True, + ) + default.update(kw) + self.env = MagicDict(default) super(API, self).__init__(self.__d) def finalize(self): diff --git a/ipalib/tests/test_frontend.py b/ipalib/tests/test_frontend.py index d4a70f15..4ddf11b3 100644 --- a/ipalib/tests/test_frontend.py +++ b/ipalib/tests/test_frontend.py @@ -620,6 +620,38 @@ class test_Command(ClassChecker): assert o.kw_to_args(whatever='hello', two='Two', one='One') == \ ('One', 'Two') + def test_run(self): + """ + Test the `frontend.Command.run` method. + """ + class my_cmd(self.cls): + def execute(self, *args, **kw): + return ('execute', args, kw) + + def forward(self, *args, **kw): + return ('forward', args, kw) + + args = ('Hello,', 'world,') + kw = dict(how_are='you', on_this='fine day?') + + # Test in server context: + api = plugable.API(self.cls, in_server_context=True) + api.finalize() + o = my_cmd() + o.set_api(api) + assert o.run.im_func is self.cls.run.im_func + assert ('execute', args, kw) == o.run(*args, **kw) + assert o.run.im_func is my_cmd.execute.im_func + + # Test in non-server context + api = plugable.API(self.cls, in_server_context=False) + api.finalize() + o = my_cmd() + o.set_api(api) + assert o.run.im_func is self.cls.run.im_func + assert ('forward', args, kw) == o.run(*args, **kw) + assert o.run.im_func is my_cmd.forward.im_func + class test_Object(ClassChecker): """ -- cgit From 3bf2da571488b6f1ef27527fa3bff0133b44c2f5 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 03:10:35 +0000 Subject: 324: Removed 'smart_option_order' from Command.__public__; cli commands help, console, and show_plugins now override Command.run() instead of Command.__call__() --- ipalib/cli.py | 7 ++++--- ipalib/frontend.py | 1 - 2 files changed, 4 insertions(+), 4 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index 92c0cbc3..a76c08bc 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -57,7 +57,7 @@ class help(frontend.Application): takes_args = ['command'] - def __call__(self, key): + def run(self, key): key = str(key) if key not in self.application: print 'help: no such command %r' % key @@ -70,16 +70,17 @@ class help(frontend.Application): class console(frontend.Application): 'Start the IPA interactive Python console.' - def __call__(self): + def run(self): code.interact( '(Custom IPA interactive Python console)', local=dict(api=self.api) ) + class show_plugins(frontend.Application): 'Print details on the loaded plugins.' - def __call__(self): + def run(self): lines = self.__traverse() ml = max(len(l[1]) for l in lines) for line in lines: diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 11d05d5f..dbc3a62d 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -245,7 +245,6 @@ class Command(plugable.Plugin): 'validate', 'execute', '__call__', - 'smart_option_order', 'args', 'options', 'params', -- cgit From 3e70c3b56b29dcc9c0f6dd15eee7d4a24945944a Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 04:44:52 +0000 Subject: 325: API.finalize() now creates instance attribtue 'plugins', which is a tuple of PluginInfo objects; renamed show_plugins cli command to namespaces; added new cli command plugins --- ipalib/cli.py | 36 +++++++++++++++++++++++++++----- ipalib/plugable.py | 61 ++++++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 79 insertions(+), 18 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index a76c08bc..7912f1b1 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -77,8 +77,8 @@ class console(frontend.Application): ) -class show_plugins(frontend.Application): - 'Print details on the loaded plugins.' +class namespaces(frontend.Application): + 'Show details of plugable namespaces' def run(self): lines = self.__traverse() @@ -112,6 +112,33 @@ class show_plugins(frontend.Application): self.__traverse_namespace(n, attr, lines, tab + 2) +class plugins(frontend.Application): + """Show all loaded plugins""" + + def run(self): + print '%s:\n' % self.name + for p in sorted(self.api.plugins, key=lambda o: o.plugin): + print ' plugin: %s' % p.plugin + print ' in namespaces: %s' % ', '.join(p.bases) + print '' + if len(self.api.plugins) == 1: + print '1 plugin loaded.' + else: + print '%d plugins loaded.' % len(self.api.plugins) + + + + + +cli_application_commands = ( + help, + console, + namespaces, + plugins, + +) + + class KWCollector(object): def __init__(self): object.__setattr__(self, '_KWCollector__d', {}) @@ -168,9 +195,8 @@ class CLI(object): def finalize(self): api = self.api - api.register(help) - api.register(console) - api.register(show_plugins) + for klass in cli_application_commands: + api.register(klass) api.finalize() for a in api.Application(): a.set_application(self) diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 8bf90ea8..cd130a19 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -708,16 +708,47 @@ class API(DictProxy): """ assert not self.__finalized, 'finalize() can only be called once' - instances = {} - def plugin_iter(base, classes): - for klass in classes: - if klass not in instances: - instances[klass] = klass() - plugin = instances[klass] + class PluginInstance(object): + """ + Represents a plugin instance. + """ + + i = 0 + + def __init__(self, klass): + self.created = self.next() + self.klass = klass + self.instance = klass() + self.bases = [] + + @classmethod + def next(cls): + cls.i += 1 + return cls.i + + class PluginInfo(ReadOnly): + def __init__(self, p): + assert isinstance(p, PluginInstance) + self.created = p.created + self.name = p.klass.__name__ + self.module = str(p.klass.__module__) + self.plugin = '%s.%s' % (self.module, self.name) + self.bases = tuple(b.__name__ for b in p.bases) + lock(self) + + plugins = {} + def plugin_iter(base, subclasses): + for klass in subclasses: + assert issubclass(klass, base) + if klass not in plugins: + plugins[klass] = PluginInstance(klass) + p = plugins[klass] + assert base not in p.bases + p.bases.append(base) if base.__proxy__: - yield PluginProxy(base, plugin) + yield PluginProxy(base, p.instance) else: - yield plugin + yield p.instance for name in self.register: base = self.register[name] @@ -731,10 +762,14 @@ class API(DictProxy): self.__d[name] = namespace object.__setattr__(self, name, namespace) - for plugin in instances.itervalues(): - plugin.set_api(self) - assert plugin.api is self + for p in plugins.itervalues(): + p.instance.set_api(self) + assert p.instance.api is self - for plugin in instances.itervalues(): - plugin.finalize() + for p in plugins.itervalues(): + p.instance.finalize() object.__setattr__(self, '_API__finalized', True) + tuple(PluginInfo(p) for p in plugins.itervalues()) + object.__setattr__(self, 'plugins', + tuple(PluginInfo(p) for p in plugins.itervalues()) + ) -- cgit From f3ac709922c33425c211b79787f1dedc03bb6508 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 05:03:10 +0000 Subject: 326: Made output of plugins cli command nicer --- ipalib/cli.py | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index 7912f1b1..36a5bd1b 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -52,6 +52,23 @@ def from_cli(cli_name): return str(cli_name).replace('-', '_') +class text_ui(frontend.Application): + """ + Base class for CLI commands with special output needs. + """ + + def print_dashed(self, string, top=True, bottom=True): + dashes = '-' * len(string) + if top: + print dashes + print string + if bottom: + print dashes + + def print_name(self, **kw): + self.print_dashed('%s:' % self.name, **kw) + + class help(frontend.Application): 'Display help on a command.' @@ -112,19 +129,24 @@ class namespaces(frontend.Application): self.__traverse_namespace(n, attr, lines, tab + 2) -class plugins(frontend.Application): +class plugins(text_ui): """Show all loaded plugins""" def run(self): - print '%s:\n' % self.name + self.print_name() + first = True for p in sorted(self.api.plugins, key=lambda o: o.plugin): + if first: + first = False + else: + print '' print ' plugin: %s' % p.plugin print ' in namespaces: %s' % ', '.join(p.bases) - print '' if len(self.api.plugins) == 1: - print '1 plugin loaded.' + s = '1 plugin loaded.' else: - print '%d plugins loaded.' % len(self.api.plugins) + s = '%d plugins loaded.' % len(self.api.plugins) + self.print_dashed(s) -- cgit From eaf15d5a52b8438d1a0a5b59a9ace9660a703dce Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 05:35:40 +0000 Subject: 327: Improved formatting on show-api cli command --- ipalib/cli.py | 40 +++++++++++++++++++++++++++++++--------- ipalib/frontend.py | 2 +- 2 files changed, 32 insertions(+), 10 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index 36a5bd1b..8918206f 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -94,26 +94,46 @@ class console(frontend.Application): ) -class namespaces(frontend.Application): - 'Show details of plugable namespaces' - def run(self): - lines = self.__traverse() +class show_api(text_ui): + 'Show attributes on dynamic API object' + + takes_args = ('namespaces*',) + + def run(self, namespaces): + if namespaces is None: + names = tuple(self.api) + else: + for name in namespaces: + if name not in self.api: + exit_error('api has no such namespace: %s' % name) + names = namespaces + lines = self.__traverse(names) ml = max(len(l[1]) for l in lines) + self.print_name() + first = True for line in lines: - if line[0] == 0: + if line[0] == 0 and not first: print '' + if first: + first = False print '%s%s %r' % ( ' ' * line[0], line[1].ljust(ml), line[2], ) + if len(lines) == 1: + s = '1 attribute shown.' + else: + s = '%d attributes show.' % len(lines) + self.print_dashed(s) + - def __traverse(self): + def __traverse(self, names): lines = [] - for name in self.api: + for name in names: namespace = self.api[name] - self.__traverse_namespace(name, namespace, lines) + self.__traverse_namespace('%s' % name, namespace, lines) return lines def __traverse_namespace(self, name, namespace, lines, tab=0): @@ -155,7 +175,7 @@ class plugins(text_ui): cli_application_commands = ( help, console, - namespaces, + show_api, plugins, ) @@ -253,6 +273,8 @@ class CLI(object): def run_interactive(self, cmd, kw): for param in cmd.params(): if param.name not in kw: + if not param.required: + continue default = param.get_default(**kw) if default is None: prompt = '%s: ' % param.name diff --git a/ipalib/frontend.py b/ipalib/frontend.py index dbc3a62d..d4550f87 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -356,7 +356,7 @@ class Command(plugable.Plugin): kw = self.convert(**kw) kw.update(self.get_default(**kw)) self.validate(**kw) - args = tuple(kw.pop(name) for name in self.args) + args = tuple(kw.pop(name, None) for name in self.args) return self.run(*args, **kw) def run(self, *args, **kw): -- cgit From 126b31de5581f4107d7d863f606a9adfa782f88a Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 05:42:31 +0000 Subject: 328: Command.get_default() now returns defaults for all values not present, not just defaults that aren't None --- ipalib/frontend.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index d4550f87..cb18a07a 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -322,9 +322,7 @@ class Command(plugable.Plugin): def __get_default_iter(self, kw): for param in self.params(): if param.name not in kw: - value = param.get_default(**kw) - if value is not None: - yield(param.name, value) + yield (param.name, param.get_default(**kw)) def get_default(self, **kw): return dict(self.__get_default_iter(kw)) @@ -356,7 +354,7 @@ class Command(plugable.Plugin): kw = self.convert(**kw) kw.update(self.get_default(**kw)) self.validate(**kw) - args = tuple(kw.pop(name, None) for name in self.args) + args = tuple(kw.pop(name) for name in self.args) return self.run(*args, **kw) def run(self, *args, **kw): -- cgit From 15b83ab1bf9d94e4f5cf292623d194b6a8094616 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 05:46:49 +0000 Subject: 329: Command.convert() now converts all keys, not just keys in params --- ipalib/frontend.py | 11 +++-------- ipalib/tests/test_frontend.py | 2 -- 2 files changed, 3 insertions(+), 10 deletions(-) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index cb18a07a..fc397530 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -299,15 +299,10 @@ class Command(plugable.Plugin): for option in self.get_options(): yield create_param(option) - def __convert_iter(self, kw): - for (key, value) in kw.iteritems(): - if key in self.params: - yield (key, self.params[key].convert(value)) - else: - yield (key, value) - def convert(self, **kw): - return dict(self.__convert_iter(kw)) + return dict( + (k, self.params[k].convert(v)) for (k, v) in kw.iteritems() + ) def __normalize_iter(self, kw): for (key, value) in kw.iteritems(): diff --git a/ipalib/tests/test_frontend.py b/ipalib/tests/test_frontend.py index 4ddf11b3..db831caa 100644 --- a/ipalib/tests/test_frontend.py +++ b/ipalib/tests/test_frontend.py @@ -486,8 +486,6 @@ class test_Command(ClassChecker): kw = dict( option0='option0', option1='option1', - whatever=False, - also=object, ) expected = dict(kw) expected.update(dict(option0=u'option0', option1=u'option1')) -- cgit From 95abdcd7147399c9bb10adc2a04e41ddc97b2302 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 05:49:30 +0000 Subject: 330: Command.normalize() now normalizes all keys, not just keys in params --- ipalib/frontend.py | 11 +++-------- ipalib/tests/test_frontend.py | 1 - 2 files changed, 3 insertions(+), 9 deletions(-) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index fc397530..f3264d81 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -304,15 +304,10 @@ class Command(plugable.Plugin): (k, self.params[k].convert(v)) for (k, v) in kw.iteritems() ) - def __normalize_iter(self, kw): - for (key, value) in kw.iteritems(): - if key in self.params: - yield (key, self.params[key].normalize(value)) - else: - yield (key, value) - def normalize(self, **kw): - return dict(self.__normalize_iter(kw)) + return dict( + (k, self.params[k].normalize(v)) for (k, v) in kw.iteritems() + ) def __get_default_iter(self, kw): for param in self.params(): diff --git a/ipalib/tests/test_frontend.py b/ipalib/tests/test_frontend.py index db831caa..88232bed 100644 --- a/ipalib/tests/test_frontend.py +++ b/ipalib/tests/test_frontend.py @@ -504,7 +504,6 @@ class test_Command(ClassChecker): kw = dict( option0=u'OPTION0', option1=u'OPTION1', - option2=u'option2', ) norm = dict((k, v.lower()) for (k, v) in kw.items()) sub = self.subcls() -- cgit From d56f4c643b486bfbcb6523a0fe80252343fa594e Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 06:11:46 +0000 Subject: 331: Param.normalize() no longer raises a TypeError when value in not a basestring --- ipalib/frontend.py | 47 +++++++++++++++++++++++++++++-------------- ipalib/tests/test_frontend.py | 6 +++--- 2 files changed, 35 insertions(+), 18 deletions(-) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index f3264d81..59cdf69f 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -105,6 +105,38 @@ class Param(plugable.ReadOnly): self.rules = (type_.validate,) + rules lock(self) + def __normalize_scalar(self, value): + if not isinstance(value, basestring): + return value + try: + return self.__normalize(value) + except StandardError: + return value + + def normalize(self, value): + """ + Normalize ``value`` using normalize callback. + + If this `Param` instance does not have a normalize callback, + ``value`` is returned unchanged. + + If this `Param` instance has a normalize callback and ``value`` is + a basestring, the normalize callback is called and its return value + is returned. + + If ``value`` is not a basestring, or if an exception is caught + when calling the normalize callback, ``value`` is returned unchanged. + + :param value: A proposed value for this parameter. + """ + if self.__normalize is None: + return value + if self.multivalue: + if type(value) in (tuple, list): + return tuple(self.__normalize_scalar(v) for v in value) + return (self.__normalize_scalar(value),) # tuple + return self.__normalize_scalar(value) + def __convert_scalar(self, value, index=None): if value is None: raise TypeError('value cannot be None') @@ -124,22 +156,7 @@ class Param(plugable.ReadOnly): return (self.__convert_scalar(value, 0),) # tuple return self.__convert_scalar(value) - def __normalize_scalar(self, value): - if not isinstance(value, basestring): - raise_TypeError(value, basestring, 'value') - try: - return self.__normalize(value) - except Exception: - return value - def normalize(self, value): - if self.__normalize is None: - return value - if self.multivalue: - if type(value) in (tuple, list): - return tuple(self.__normalize_scalar(v) for v in value) - return (self.__normalize_scalar(value),) # tuple - return self.__normalize_scalar(value) def __validate_scalar(self, value, index=None): if type(value) is not self.type.type: diff --git a/ipalib/tests/test_frontend.py b/ipalib/tests/test_frontend.py index 88232bed..c473ad58 100644 --- a/ipalib/tests/test_frontend.py +++ b/ipalib/tests/test_frontend.py @@ -202,7 +202,7 @@ class test_Option(ClassChecker): for v in (u'Hello', u'hello', 'Hello'): # Okay assert o.normalize(v) == 'hello' for v in [None, 42, (u'Hello',)]: # Not basestring - check_TypeError(v, basestring, 'value', o.normalize, v) + assert o.normalize(v) is v # Scenario 3: multivalue=True, normalize=None o = self.cls(name, t, multivalue=True) @@ -215,8 +215,8 @@ class test_Option(ClassChecker): for value in [(u'Hello',), (u'hello',), 'Hello', ['Hello']]: # Okay assert o.normalize(value) == (u'hello',) fail = 42 # Not basestring - for v in [fail, [fail], (u'Hello', fail)]: # Non unicode member - check_TypeError(fail, basestring, 'value', o.normalize, v) + for v in [[fail], (u'hello', fail)]: # Non unicode member + assert o.normalize(v) == tuple(v) def test_validate(self): """ -- cgit From 6bedb15674ba941c15832ac84387a40ecd2a2879 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 06:25:12 +0000 Subject: 332: Param.normalize() now returns None if multivalue and len() == 0 --- ipalib/frontend.py | 15 ++++++++++----- ipalib/tests/test_frontend.py | 12 +++++++----- 2 files changed, 17 insertions(+), 10 deletions(-) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 59cdf69f..5971f9df 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -105,6 +105,15 @@ class Param(plugable.ReadOnly): self.rules = (type_.validate,) + rules lock(self) + def __if_multivalue(self, value, scalar): + if self.multivalue: + if type(value) in (tuple, list): + if len(value) == 0: + return None + return tuple(scalar(v) for v in value) + return (scalar(value),) # tuple + return scalar(value) + def __normalize_scalar(self, value): if not isinstance(value, basestring): return value @@ -131,11 +140,7 @@ class Param(plugable.ReadOnly): """ if self.__normalize is None: return value - if self.multivalue: - if type(value) in (tuple, list): - return tuple(self.__normalize_scalar(v) for v in value) - return (self.__normalize_scalar(value),) # tuple - return self.__normalize_scalar(value) + return self.__if_multivalue(value, self.__normalize_scalar) def __convert_scalar(self, value, index=None): if value is None: diff --git a/ipalib/tests/test_frontend.py b/ipalib/tests/test_frontend.py index c473ad58..63cc9214 100644 --- a/ipalib/tests/test_frontend.py +++ b/ipalib/tests/test_frontend.py @@ -111,9 +111,9 @@ class test_DefaultFrom(ClassChecker): assert o(**kw_copy) is None -class test_Option(ClassChecker): +class test_Param(ClassChecker): """ - Tests the `frontend.Param` class. + Test the `frontend.Param` class. """ _cls = frontend.Param @@ -122,7 +122,7 @@ class test_Option(ClassChecker): def test_init(self): """ - Tests the `frontend.Param.__init__` method. + Test the `frontend.Param.__init__` method. """ name = 'sn' type_ = ipa_types.Unicode() @@ -139,7 +139,7 @@ class test_Option(ClassChecker): def test_convert(self): """ - Tests the `frontend.Param.convert` method. + Test the `frontend.Param.convert` method. """ name = 'some_number' type_ = ipa_types.Int() @@ -184,7 +184,7 @@ class test_Option(ClassChecker): def test_normalize(self): """ - Tests the `frontend.Param.normalize` method. + Test the `frontend.Param.normalize` method. """ name = 'sn' t = ipa_types.Unicode() @@ -212,6 +212,8 @@ class test_Option(ClassChecker): # Scenario 4: multivalue=True, normalize=callback o = self.cls(name, t, multivalue=True, normalize=callback) + assert o.normalize([]) is None + assert o.normalize(tuple()) is None for value in [(u'Hello',), (u'hello',), 'Hello', ['Hello']]: # Okay assert o.normalize(value) == (u'hello',) fail = 42 # Not basestring -- cgit From 1125d420bdf453a0b51e58a85d447009dd1a99ff Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 06:35:19 +0000 Subject: 333: Param.convert() now uses name Param.__multivalue() helper method as Param.normalize() --- ipalib/frontend.py | 20 ++++++++------------ ipalib/tests/test_frontend.py | 1 + 2 files changed, 9 insertions(+), 12 deletions(-) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 5971f9df..4a84ce98 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -105,16 +105,18 @@ class Param(plugable.ReadOnly): self.rules = (type_.validate,) + rules lock(self) - def __if_multivalue(self, value, scalar): + def __multivalue(self, value, scalar): if self.multivalue: if type(value) in (tuple, list): if len(value) == 0: return None - return tuple(scalar(v) for v in value) - return (scalar(value),) # tuple + return tuple( + scalar(v, i) for (i, v) in enumerate(value) + ) + return (scalar(value, 0),) # tuple return scalar(value) - def __normalize_scalar(self, value): + def __normalize_scalar(self, value, index=None): if not isinstance(value, basestring): return value try: @@ -140,7 +142,7 @@ class Param(plugable.ReadOnly): """ if self.__normalize is None: return value - return self.__if_multivalue(value, self.__normalize_scalar) + return self.__multivalue(value, self.__normalize_scalar) def __convert_scalar(self, value, index=None): if value is None: @@ -153,13 +155,7 @@ class Param(plugable.ReadOnly): return converted def convert(self, value): - if self.multivalue: - if type(value) in (tuple, list): - return tuple( - self.__convert_scalar(v, i) for (i, v) in enumerate(value) - ) - return (self.__convert_scalar(value, 0),) # tuple - return self.__convert_scalar(value) + return self.__multivalue(value, self.__convert_scalar) diff --git a/ipalib/tests/test_frontend.py b/ipalib/tests/test_frontend.py index 63cc9214..d809d1cf 100644 --- a/ipalib/tests/test_frontend.py +++ b/ipalib/tests/test_frontend.py @@ -163,6 +163,7 @@ class test_Param(ClassChecker): # Scenario 2: multivalue=True o = self.cls(name, type_, multivalue=True) + assert o.convert([]) is None for none in [None, (7, None)]: e = raises(TypeError, o.convert, none) assert str(e) == 'value cannot be None' -- cgit From 4215da30ad9de6467abe2c56f7a56f73001060b3 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 06:36:48 +0000 Subject: 334: Renamed Command.__multivalue() helper method to Command.dispatch() --- ipalib/frontend.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 4a84ce98..d53a7d4d 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -105,7 +105,7 @@ class Param(plugable.ReadOnly): self.rules = (type_.validate,) + rules lock(self) - def __multivalue(self, value, scalar): + def __dispatch(self, value, scalar): if self.multivalue: if type(value) in (tuple, list): if len(value) == 0: @@ -142,7 +142,7 @@ class Param(plugable.ReadOnly): """ if self.__normalize is None: return value - return self.__multivalue(value, self.__normalize_scalar) + return self.__dispatch(value, self.__normalize_scalar) def __convert_scalar(self, value, index=None): if value is None: @@ -155,7 +155,7 @@ class Param(plugable.ReadOnly): return converted def convert(self, value): - return self.__multivalue(value, self.__convert_scalar) + return self.__dispatch(value, self.__convert_scalar) -- cgit From e63c462f31bc34c5b19d243492c7644f423d55d0 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 06:48:27 +0000 Subject: 335: If Command.__convert_scalar() is called with None, it now returns None instead of raising TypeError --- ipalib/frontend.py | 11 ++++++++--- ipalib/tests/test_frontend.py | 9 +++------ 2 files changed, 11 insertions(+), 9 deletions(-) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index d53a7d4d..7d75fa17 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -106,6 +106,8 @@ class Param(plugable.ReadOnly): lock(self) def __dispatch(self, value, scalar): + if value is None: + return None if self.multivalue: if type(value) in (tuple, list): if len(value) == 0: @@ -146,7 +148,7 @@ class Param(plugable.ReadOnly): def __convert_scalar(self, value, index=None): if value is None: - raise TypeError('value cannot be None') + return None converted = self.type(value) if converted is None: raise errors.ConversionError( @@ -155,9 +157,12 @@ class Param(plugable.ReadOnly): return converted def convert(self, value): - return self.__dispatch(value, self.__convert_scalar) - + """ + Convert/coerce ``value`` to Python type for this parameter. + :param value: A proposed value for this parameter. + """ + return self.__dispatch(value, self.__convert_scalar) def __validate_scalar(self, value, index=None): if type(value) is not self.type.type: diff --git a/ipalib/tests/test_frontend.py b/ipalib/tests/test_frontend.py index d809d1cf..e6127797 100644 --- a/ipalib/tests/test_frontend.py +++ b/ipalib/tests/test_frontend.py @@ -148,8 +148,7 @@ class test_Param(ClassChecker): # Scenario 1: multivalue=False o = self.cls(name, type_) - e = raises(TypeError, o.convert, None) - assert str(e) == 'value cannot be None' + assert o.convert(None) is None for value in okay: new = o.convert(value) assert new == 7 @@ -163,10 +162,8 @@ class test_Param(ClassChecker): # Scenario 2: multivalue=True o = self.cls(name, type_, multivalue=True) - assert o.convert([]) is None - for none in [None, (7, None)]: - e = raises(TypeError, o.convert, none) - assert str(e) == 'value cannot be None' + for none in (None, tuple(), []): + assert o.convert(none) is None for value in okay: assert o.convert((value,)) == (7,) assert o.convert([value]) == (7,) -- cgit From fb57b919376322160df94aefd84bbebc52a6e53f Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 07:05:43 +0000 Subject: 336: Param.__dispatch() now returns None for any in (None, '', u'', tuple(), []) regardless whether Param is multivalue --- ipalib/frontend.py | 15 +++++++++------ ipalib/tests/test_frontend.py | 15 +++++++++++---- 2 files changed, 20 insertions(+), 10 deletions(-) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 7d75fa17..80c4050e 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -106,12 +106,10 @@ class Param(plugable.ReadOnly): lock(self) def __dispatch(self, value, scalar): - if value is None: - return None + if value in (None, '', tuple(), []): + return if self.multivalue: if type(value) in (tuple, list): - if len(value) == 0: - return None return tuple( scalar(v, i) for (i, v) in enumerate(value) ) @@ -148,7 +146,7 @@ class Param(plugable.ReadOnly): def __convert_scalar(self, value, index=None): if value is None: - return None + return converted = self.type(value) if converted is None: raise errors.ConversionError( @@ -158,7 +156,12 @@ class Param(plugable.ReadOnly): def convert(self, value): """ - Convert/coerce ``value`` to Python type for this parameter. + Convert/coerce ``value`` to Python type for this `Param`. + + If ``value`` can not be converted, ConversionError is raised. + + If ``value`` is None, conversion is not attempted and None is + returned. :param value: A proposed value for this parameter. """ diff --git a/ipalib/tests/test_frontend.py b/ipalib/tests/test_frontend.py index e6127797..58d5fd93 100644 --- a/ipalib/tests/test_frontend.py +++ b/ipalib/tests/test_frontend.py @@ -145,10 +145,12 @@ class test_Param(ClassChecker): type_ = ipa_types.Int() okay = (7, 7L, 7.0, ' 7 ') fail = ('7.0', '7L', 'whatever', object) + none = (None, '', u'', tuple(), []) # Scenario 1: multivalue=False o = self.cls(name, type_) - assert o.convert(None) is None + for n in none: + assert o.convert(n) is None for value in okay: new = o.convert(value) assert new == 7 @@ -162,8 +164,8 @@ class test_Param(ClassChecker): # Scenario 2: multivalue=True o = self.cls(name, type_, multivalue=True) - for none in (None, tuple(), []): - assert o.convert(none) is None + for n in none: + assert o.convert(n) is None for value in okay: assert o.convert((value,)) == (7,) assert o.convert([value]) == (7,) @@ -188,6 +190,7 @@ class test_Param(ClassChecker): t = ipa_types.Unicode() callback = lambda value: value.lower() values = (None, u'Hello', (u'Hello',), 'hello', ['hello']) + none = (None, '', u'', tuple(), []) # Scenario 1: multivalue=False, normalize=None o = self.cls(name, t) @@ -201,6 +204,8 @@ class test_Param(ClassChecker): assert o.normalize(v) == 'hello' for v in [None, 42, (u'Hello',)]: # Not basestring assert o.normalize(v) is v + for n in none: + assert o.normalize(n) is None # Scenario 3: multivalue=True, normalize=None o = self.cls(name, t, multivalue=True) @@ -215,8 +220,10 @@ class test_Param(ClassChecker): for value in [(u'Hello',), (u'hello',), 'Hello', ['Hello']]: # Okay assert o.normalize(value) == (u'hello',) fail = 42 # Not basestring - for v in [[fail], (u'hello', fail)]: # Non unicode member + for v in [[fail], (u'hello', fail)]: # Non basestring member assert o.normalize(v) == tuple(v) + for n in none: + assert o.normalize(n) is None def test_validate(self): """ -- cgit From 744406958df66fb46ec43d80f9d788429953fda4 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 07:56:31 +0000 Subject: 337: Some cleanup in Params; added docstrings for most all Param methods --- ipalib/frontend.py | 67 ++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 55 insertions(+), 12 deletions(-) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 80c4050e..82d96643 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -85,6 +85,8 @@ class DefaultFrom(plugable.ReadOnly): class Param(plugable.ReadOnly): + __nones = (None, '', tuple(), []) + def __init__(self, name, type_, doc='', required=False, @@ -106,7 +108,10 @@ class Param(plugable.ReadOnly): lock(self) def __dispatch(self, value, scalar): - if value in (None, '', tuple(), []): + """ + Helper method used by `normalize` and `convert`. + """ + if value in self.__nones: return if self.multivalue: if type(value) in (tuple, list): @@ -117,6 +122,11 @@ class Param(plugable.ReadOnly): return scalar(value) def __normalize_scalar(self, value, index=None): + """ + Normalize a scalar value. + + This method is called once with each value in multivalue. + """ if not isinstance(value, basestring): return value try: @@ -145,7 +155,12 @@ class Param(plugable.ReadOnly): return self.__dispatch(value, self.__normalize_scalar) def __convert_scalar(self, value, index=None): - if value is None: + """ + Convert a scalar value. + + This method is called once with each value in multivalue. + """ + if value in self.__nones: return converted = self.type(value) if converted is None: @@ -158,7 +173,8 @@ class Param(plugable.ReadOnly): """ Convert/coerce ``value`` to Python type for this `Param`. - If ``value`` can not be converted, ConversionError is raised. + If ``value`` can not be converted, ConversionError is raised, which + is as subclass of ValidationError. If ``value`` is None, conversion is not attempted and None is returned. @@ -168,6 +184,11 @@ class Param(plugable.ReadOnly): return self.__dispatch(value, self.__convert_scalar) def __validate_scalar(self, value, index=None): + """ + Validate a scalar value. + + This method is called once with each value in multivalue. + """ if type(value) is not self.type.type: raise_TypeError(value, self.type.type, 'value') for rule in self.rules: @@ -178,6 +199,18 @@ class Param(plugable.ReadOnly): ) def validate(self, value): + """ + Check validity of a value. + + Each validation rule is called in turn and if any returns and error, + RuleError is raised, which is a subclass of ValidationError. + + :param value: A proposed value for this parameter. + """ + if value is None: + if self.required: + raise errors.RequirementError(self.name) + return if self.multivalue: if type(value) is not tuple: raise_TypeError(value, tuple, 'value') @@ -187,6 +220,22 @@ class Param(plugable.ReadOnly): self.__validate_scalar(value) def get_default(self, **kw): + """ + Return a default value for this parameter. + + If this `Param` instance does not have a default_from() callback, this + method always returns the static Param.default instance attribute. + + On the other hand, if this `Param` instance has a default_from() + callback, the callback is called and its return value is returned + (assuming that value is not None). + + If the default_from() callback returns None, or if an exception is + caught when calling the default_from() callback, the static + Param.default instance attribute is returned. + + :param kw: Optional keyword arguments to pass to default_from(). + """ if self.default_from is not None: default = self.default_from(**kw) if default is not None: @@ -202,18 +251,12 @@ class Param(plugable.ReadOnly): return tuple() def __call__(self, value, **kw): - if value in ('', tuple(), []): - value = None - if value is None: + if value in self.__nones: value = self.get_default(**kw) - if value is None: - if self.required: - raise errors.RequirementError(self.name) - return None else: value = self.convert(self.normalize(value)) - self.validate(value) - return value + self.validate(value) + return value def __repr__(self): return '%s(%r, %s())' % ( -- cgit From 11a07008b896ac995755b2f2a90e6089ca1344a5 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 17:55:29 +0000 Subject: 339: Added parse_param_spec() function and corresponding unit tests --- ipalib/frontend.py | 32 ++++++++++++++++++++++++++++++++ ipalib/tests/test_frontend.py | 12 ++++++++++++ 2 files changed, 44 insertions(+) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 82d96643..1ff62023 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -84,6 +84,38 @@ class DefaultFrom(plugable.ReadOnly): return None +def parse_param_spec(spec): + """ + Parse param spec to get name, required, and multivalue. + + The ``spec`` string determines the param name, whether the param is + required, and whether the param is multivalue according the following + syntax: + + name => required=True, multivalue=False + name? => required=False, multivalue=False + name+ => required=True, multivalue=True + name* => required=False, multivalue=True + + :param spec: A spec string. + """ + if type(spec) is not str: + raise_TypeError(spec, str, 'spec') + if len(spec) < 2: + raise ValueError( + 'param spec must be at least 2 characters; got %r' % spec + ) + _map = { + '?': dict(required=False, multivalue=False), + '*': dict(required=False, multivalue=True), + '+': dict(required=True, multivalue=True), + } + end = spec[-1] + if end in _map: + return (spec[:-1], _map[end]) + return (spec, dict(required=True, multivalue=False)) + + class Param(plugable.ReadOnly): __nones = (None, '', tuple(), []) diff --git a/ipalib/tests/test_frontend.py b/ipalib/tests/test_frontend.py index 58d5fd93..1fe4e376 100644 --- a/ipalib/tests/test_frontend.py +++ b/ipalib/tests/test_frontend.py @@ -111,6 +111,18 @@ class test_DefaultFrom(ClassChecker): assert o(**kw_copy) is None +def test_parse_param_spec(): + """ + Test the `frontend.parse_param_spec` function. + """ + f = frontend.parse_param_spec + + assert f('name') == ('name', dict(required=True, multivalue=False)) + assert f('name?') == ('name', dict(required=False, multivalue=False)) + assert f('name*') == ('name', dict(required=False, multivalue=True)) + assert f('name+') == ('name', dict(required=True, multivalue=True)) + + class test_Param(ClassChecker): """ Test the `frontend.Param` class. -- cgit From 792bf7b1d0f295290aa30bd358d67ecfc7233588 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 18:02:00 +0000 Subject: 340: Changed default for Param.required to True --- ipalib/frontend.py | 12 +++++++++++- ipalib/tests/test_frontend.py | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 1ff62023..92c610c4 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -118,10 +118,20 @@ def parse_param_spec(spec): class Param(plugable.ReadOnly): __nones = (None, '', tuple(), []) + __default = dict( + type=ipa_types.Unicode(), + doc='', + required=True, + multivalue=False, + default=None, + default_from=None, + rules=tuple(), + normalize=None + ) def __init__(self, name, type_, doc='', - required=False, + required=True, multivalue=False, default=None, default_from=None, diff --git a/ipalib/tests/test_frontend.py b/ipalib/tests/test_frontend.py index 1fe4e376..252e4642 100644 --- a/ipalib/tests/test_frontend.py +++ b/ipalib/tests/test_frontend.py @@ -143,7 +143,7 @@ class test_Param(ClassChecker): assert read_only(o, 'name') is name assert read_only(o, 'type') is type_ assert read_only(o, 'doc') == '' - assert read_only(o, 'required') is False + assert read_only(o, 'required') is True assert read_only(o, 'multivalue') is False assert read_only(o, 'default') is None assert read_only(o, 'default_from') is None -- cgit From 06d7fb42ec071974592b35eaab2868c1df8722a5 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 18:27:14 +0000 Subject: 341: Param now only takes type_=ipa_types.Unicode() as an optional positional arg, and the rest as pure kwargs --- ipalib/frontend.py | 45 +++++++++++++++++++++++++++---------------- ipalib/tests/test_frontend.py | 20 +++++++++++++++++++ 2 files changed, 48 insertions(+), 17 deletions(-) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 92c610c4..a880adf6 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -119,7 +119,6 @@ def parse_param_spec(spec): class Param(plugable.ReadOnly): __nones = (None, '', tuple(), []) __default = dict( - type=ipa_types.Unicode(), doc='', required=True, multivalue=False, @@ -129,26 +128,38 @@ class Param(plugable.ReadOnly): normalize=None ) - def __init__(self, name, type_, - doc='', - required=True, - multivalue=False, - default=None, - default_from=None, - rules=tuple(), - normalize=None): + def __init__(self, name, type_=ipa_types.Unicode(), **kw): + if 'required' not in kw and 'multivalue' not in kw: + (name, kw_from_spec) = parse_param_spec(name) + kw.update(kw_from_spec) + default = dict(self.__default) + if not set(default).issuperset(kw): + raise TypeError( + 'no such kwargs: %r' % list(set(kw) - set(default)) + ) + default.update(kw) + self.__kw = default self.name = check_name(name) - self.doc = check_type(doc, str, 'doc') self.type = check_isinstance(type_, ipa_types.Type, 'type_') - self.required = check_type(required, bool, 'required') - self.multivalue = check_type(multivalue, bool, 'multivalue') - self.default = default - self.default_from = check_type(default_from, - DefaultFrom, 'default_from', allow_none=True) - self.__normalize = normalize - self.rules = (type_.validate,) + rules + self.doc = self.__check_type(str, 'doc') + self.required = self.__check_type(bool, 'required') + self.multivalue = self.__check_type(bool, 'multivalue') + self.default = self.__kw['default'] + self.default_from = self.__check_type(DefaultFrom, 'default_from', + allow_none=True + ) + self.__normalize = self.__kw['normalize'] + self.rules = (type_.validate,) + self.__kw['rules'] lock(self) + def __check_type(self, type_, name, allow_none=False): + value = self.__kw[name] + return check_type(value, type_, name, allow_none) + + def __check_isinstance(self, type_, name, allow_none=False): + value = self.__kw[name] + return check_isinstance(value, type_, name, allow_none) + def __dispatch(self, value, scalar): """ Helper method used by `normalize` and `convert`. diff --git a/ipalib/tests/test_frontend.py b/ipalib/tests/test_frontend.py index 252e4642..8b4df3bb 100644 --- a/ipalib/tests/test_frontend.py +++ b/ipalib/tests/test_frontend.py @@ -149,6 +149,26 @@ class test_Param(ClassChecker): assert read_only(o, 'default_from') is None assert read_only(o, 'rules') == (type_.validate,) + # Check default type_: + o = self.cls(name) + assert isinstance(o.type, ipa_types.Unicode) + + # Check param spec parsing: + o = self.cls('name?') + assert o.name == 'name' + assert o.required is False + assert o.multivalue is False + + o = self.cls('name*') + assert o.name == 'name' + assert o.required is False + assert o.multivalue is True + + o = self.cls('name+') + assert o.name == 'name' + assert o.required is True + assert o.multivalue is True + def test_convert(self): """ Test the `frontend.Param.convert` method. -- cgit From 97f0310a4c1648f84b3fbb3ee10043c48975a456 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 18:33:25 +0000 Subject: 342: Added unit test that TypeError is raised when Param() is created with extra kw args --- ipalib/frontend.py | 3 ++- ipalib/tests/test_frontend.py | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index a880adf6..f6626973 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -134,8 +134,9 @@ class Param(plugable.ReadOnly): kw.update(kw_from_spec) default = dict(self.__default) if not set(default).issuperset(kw): + extra = sorted(set(kw) - set(default)) raise TypeError( - 'no such kwargs: %r' % list(set(kw) - set(default)) + 'Param.__init__() takes no such kwargs: %s' % ', '.join(extra) ) default.update(kw) self.__kw = default diff --git a/ipalib/tests/test_frontend.py b/ipalib/tests/test_frontend.py index 8b4df3bb..43c1abf2 100644 --- a/ipalib/tests/test_frontend.py +++ b/ipalib/tests/test_frontend.py @@ -169,6 +169,10 @@ class test_Param(ClassChecker): assert o.required is True assert o.multivalue is True + e = raises(TypeError, self.cls, name, whatever=True, another=False) + assert str(e) == \ + 'Param.__init__() takes no such kwargs: another, whatever' + def test_convert(self): """ Test the `frontend.Param.convert` method. -- cgit From a79434584eaab5692d716368b54572aa2b6be70c Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 18:44:43 +0000 Subject: 343: create_param() function no longer parses the param spec itself but relies on Param.__init__() to do it --- ipalib/frontend.py | 26 ++++---------------------- 1 file changed, 4 insertions(+), 22 deletions(-) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index f6626973..ce43ecea 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -86,7 +86,7 @@ class DefaultFrom(plugable.ReadOnly): def parse_param_spec(spec): """ - Parse param spec to get name, required, and multivalue. + Parse a param spec into to (name, kw). The ``spec`` string determines the param name, whether the param is required, and whether the param is multivalue according the following @@ -322,20 +322,14 @@ class Param(plugable.ReadOnly): def create_param(spec): """ - Create a `Param` instance from a param spec string. + Create a `Param` instance from a param spec. If ``spec`` is a `Param` instance, ``spec`` is returned unchanged. If ``spec`` is an str instance, then ``spec`` is parsed and an appropriate `Param` instance is created and returned. - The spec string determines the param name, whether the param is required, - and whether the param is multivalue according the following syntax: - - name => required=True, multivalue=False - name? => required=False, multivalue=False - name+ => required=True, multivalue=True - name* => required=False, multivalue=True + See `parse_param_spec` for the definition of the spec syntax. :param spec: A spec string or a `Param` instance. """ @@ -345,19 +339,7 @@ def create_param(spec): raise TypeError( 'create_param() takes %r or %r; got %r' % (str, Param, spec) ) - if spec.endswith('?'): - kw = dict(required=False, multivalue=False) - name = spec[:-1] - elif spec.endswith('*'): - kw = dict(required=False, multivalue=True) - name = spec[:-1] - elif spec.endswith('+'): - kw = dict(required=True, multivalue=True) - name = spec[:-1] - else: - kw = dict(required=True, multivalue=False) - name = spec - return Param(name, ipa_types.Unicode(), **kw) + return Param(spec) class Command(plugable.Plugin): -- cgit From f8bb60f02dc3cbb48c2cc6305e095e6936f5a0d6 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 19:45:46 +0000 Subject: 344: Added Param.__clone__() method; added corresponding unit tests --- ipalib/frontend.py | 14 +++++++++++++- ipalib/tests/test_frontend.py | 33 +++++++++++++++++++++++++++++++-- 2 files changed, 44 insertions(+), 3 deletions(-) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index ce43ecea..ff0d3492 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -117,6 +117,9 @@ def parse_param_spec(spec): class Param(plugable.ReadOnly): + """ + A parameter accepted by a `Command`. + """ __nones = (None, '', tuple(), []) __default = dict( doc='', @@ -150,9 +153,18 @@ class Param(plugable.ReadOnly): allow_none=True ) self.__normalize = self.__kw['normalize'] - self.rules = (type_.validate,) + self.__kw['rules'] + self.rules = self.__check_type(tuple, 'rules') + self.all_rules = (type_.validate,) + self.rules lock(self) + def __clone__(self, **override): + """ + Return a new `Param` instance similar to this one. + """ + kw = dict(self.__kw) + kw.update(override) + return self.__class__(self.name, self.type, **kw) + def __check_type(self, type_, name, allow_none=False): value = self.__kw[name] return check_type(value, type_, name, allow_none) diff --git a/ipalib/tests/test_frontend.py b/ipalib/tests/test_frontend.py index 43c1abf2..cc71f1c9 100644 --- a/ipalib/tests/test_frontend.py +++ b/ipalib/tests/test_frontend.py @@ -147,7 +147,8 @@ class test_Param(ClassChecker): assert read_only(o, 'multivalue') is False assert read_only(o, 'default') is None assert read_only(o, 'default_from') is None - assert read_only(o, 'rules') == (type_.validate,) + assert read_only(o, 'rules') == tuple() + assert read_only(o, 'all_rules') == (type_.validate,) # Check default type_: o = self.cls(name) @@ -173,6 +174,33 @@ class test_Param(ClassChecker): assert str(e) == \ 'Param.__init__() takes no such kwargs: another, whatever' + def test_clone(self): + """ + Test the `frontend.Param.__clone__` method. + """ + def compare(o, kw): + for (k, v) in kw.iteritems(): + assert getattr(o, k) == v, (k, v, getattr(o, k)) + default = dict( + required=False, + multivalue=False, + default=None, + default_from=None, + rules=tuple(), + ) + name = 'hair_color?' + type_ = ipa_types.Int() + o = self.cls(name, type_) + compare(o, default) + + override = dict(multivalue=True, default=42) + d = dict(default) + d.update(override) + clone = o.__clone__(**override) + assert clone.name == 'hair_color' + assert clone.type is o.type + compare(clone, d) + def test_convert(self): """ Test the `frontend.Param.convert` method. @@ -277,7 +305,8 @@ class test_Param(ClassChecker): # Scenario 1: multivalue=False o = self.cls(name, type_, rules=my_rules) - assert o.rules == (type_.validate, case_rule) + assert o.rules == my_rules + assert o.all_rules == (type_.validate, case_rule) o.validate(okay) e = raises(errors.RuleError, o.validate, fail_case) assert e.name is name -- cgit From 63a26bd604c3d1421d07fe8737953101409e0fad Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 19:56:07 +0000 Subject: 345: Moved server code from Rob into ipa_server/ package --- ipalib/conn.py | 72 ------ ipalib/ipaldap.py | 627 --------------------------------------------------- ipalib/ipautil.py | 190 ---------------- ipalib/servercore.py | 148 ------------ 4 files changed, 1037 deletions(-) delete mode 100644 ipalib/conn.py delete mode 100644 ipalib/ipaldap.py delete mode 100644 ipalib/ipautil.py delete mode 100644 ipalib/servercore.py (limited to 'ipalib') diff --git a/ipalib/conn.py b/ipalib/conn.py deleted file mode 100644 index f8f5306f..00000000 --- a/ipalib/conn.py +++ /dev/null @@ -1,72 +0,0 @@ -# Authors: Rob Crittenden -# -# 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 -# - -import krbV -import threading -import ldap -import ldap.dn -from ipalib import ipaldap - -context = threading.local() - -class IPAConn: - def __init__(self, host, port, krbccache, debug=None): - self._conn = None - - # Save the arguments - self._host = host - self._port = port - self._krbccache = krbccache - self._debug = debug - - self._ctx = krbV.default_context() - - ccache = krbV.CCache(name=krbccache, context=self._ctx) - cprinc = ccache.principal() - - self._conn = ipaldap.IPAdmin(host,port,None,None,None,debug) - - # This will bind the connection - try: - self._conn.set_krbccache(krbccache, cprinc.name) - except ldap.UNWILLING_TO_PERFORM, e: - raise e - except Exception, e: - raise e - - def __del__(self): - # take no chances on unreleased connections - self.releaseConn() - - def getConn(self): - return self._conn - - def releaseConn(self): - if self._conn is None: - return - - self._conn.unbind_s() - self._conn = None - - return - -if __name__ == "__main__": - ipaconn = IPAConn("localhost", 389, "FILE:/tmp/krb5cc_500") - x = ipaconn.getConn().getEntry("dc=example,dc=com", ldap.SCOPE_SUBTREE, "uid=admin", ["cn"]) - print "%s" % x diff --git a/ipalib/ipaldap.py b/ipalib/ipaldap.py deleted file mode 100644 index c1d134a0..00000000 --- a/ipalib/ipaldap.py +++ /dev/null @@ -1,627 +0,0 @@ -# Authors: Rich Megginson -# Rob Crittenden 0 - - def hasAttr(self,name): - """Return True if this entry has an attribute named name, False otherwise""" - return self.data and self.data.has_key(name) - - def __getattr__(self,name): - """If name is the name of an LDAP attribute, return the first value for that - attribute - equivalent to getValue - this allows the use of - entry.cn - instead of - entry.getValue('cn') - This also allows us to return None if an attribute is not found rather than - throwing an exception""" - return self.getValue(name) - - def getValues(self,name): - """Get the list (array) of values for the attribute named name""" - return self.data.get(name) - - def getValue(self,name): - """Get the first value for the attribute named name""" - return self.data.get(name,[None])[0] - - def setValue(self,name,*value): - """Value passed in may be a single value, several values, or a single sequence. - For example: - ent.setValue('name', 'value') - ent.setValue('name', 'value1', 'value2', ..., 'valueN') - ent.setValue('name', ['value1', 'value2', ..., 'valueN']) - ent.setValue('name', ('value1', 'value2', ..., 'valueN')) - Since *value is a tuple, we may have to extract a list or tuple from that - tuple as in the last two examples above""" - if isinstance(value[0],list) or isinstance(value[0],tuple): - self.data[name] = value[0] - else: - self.data[name] = value - - setValues = setValue - - def toTupleList(self): - """Convert the attrs and values to a list of 2-tuples. The first element - of the tuple is the attribute name. The second element is either a - single value or a list of values.""" - return self.data.items() - - def __str__(self): - """Convert the Entry to its LDIF representation""" - return self.__repr__() - - # the ldif class base64 encodes some attrs which I would rather see in raw form - to - # encode specific attrs as base64, add them to the list below - ldif.safe_string_re = re.compile('^$') - base64_attrs = ['nsstate', 'krbprincipalkey', 'krbExtraData'] - - def __repr__(self): - """Convert the Entry to its LDIF representation""" - sio = cStringIO.StringIO() - # what's all this then? the unparse method will currently only accept - # a list or a dict, not a class derived from them. self.data is a - # cidict, so unparse barfs on it. I've filed a bug against python-ldap, - # but in the meantime, we have to convert to a plain old dict for printing - # I also don't want to see wrapping, so set the line width really high (1000) - newdata = {} - newdata.update(self.data) - ldif.LDIFWriter(sio,Entry.base64_attrs,1000).unparse(self.dn,newdata) - return sio.getvalue() - -def wrapper(f,name): - """This is the method that wraps all of the methods of the superclass. This seems - to need to be an unbound method, that's why it's outside of IPAdmin. Perhaps there - is some way to do this with the new classmethod or staticmethod of 2.4. - Basically, we replace every call to a method in SimpleLDAPObject (the superclass - of IPAdmin) with a call to inner. The f argument to wrapper is the bound method - of IPAdmin (which is inherited from the superclass). Bound means that it will implicitly - be called with the self argument, it is not in the args list. name is the name of - the method to call. If name is a method that returns entry objects (e.g. result), - we wrap the data returned by an Entry class. If name is a method that takes an entry - argument, we extract the raw data from the entry object to pass in.""" - def inner(*args, **kargs): - if name == 'result': - objtype, data = f(*args, **kargs) - # data is either a 2-tuple or a list of 2-tuples - # print data - if data: - if isinstance(data,tuple): - return objtype, Entry(data) - elif isinstance(data,list): - return objtype, [Entry(x) for x in data] - else: - raise TypeError, "unknown data type %s returned by result" % type(data) - else: - return objtype, data - elif name.startswith('add'): - # the first arg is self - # the second and third arg are the dn and the data to send - # We need to convert the Entry into the format used by - # python-ldap - ent = args[0] - if isinstance(ent,Entry): - return f(ent.dn, ent.toTupleList(), *args[2:]) - else: - return f(*args, **kargs) - else: - return f(*args, **kargs) - return inner - -class LDIFConn(ldif.LDIFParser): - def __init__( - self, - input_file, - ignored_attr_types=None,max_entries=0,process_url_schemes=None - ): - """ - See LDIFParser.__init__() - - Additional Parameters: - all_records - List instance for storing parsed records - """ - self.dndict = {} # maps dn to Entry - self.dnlist = [] # contains entries in order read - myfile = input_file - if isinstance(input_file,str) or isinstance(input_file,unicode): - myfile = open(input_file, "r") - ldif.LDIFParser.__init__(self,myfile,ignored_attr_types,max_entries,process_url_schemes) - self.parse() - if isinstance(input_file,str) or isinstance(input_file,unicode): - myfile.close() - - def handle(self,dn,entry): - """ - Append single record to dictionary of all records. - """ - if not dn: - dn = '' - newentry = Entry((dn, entry)) - self.dndict[IPAdmin.normalizeDN(dn)] = newentry - self.dnlist.append(newentry) - - def get(self,dn): - ndn = IPAdmin.normalizeDN(dn) - return self.dndict.get(ndn, Entry(None)) - -class IPAdmin(SimpleLDAPObject): - - def getDseAttr(self,attrname): - conffile = self.confdir + '/dse.ldif' - dseldif = LDIFConn(conffile) - cnconfig = dseldif.get("cn=config") - if cnconfig: - return cnconfig.getValue(attrname) - return None - - def __initPart2(self): - if self.binddn and len(self.binddn) and not hasattr(self,'sroot'): - try: - ent = self.getEntry('cn=config', ldap.SCOPE_BASE, '(objectclass=*)', - [ 'nsslapd-instancedir', 'nsslapd-errorlog', - 'nsslapd-certdir', 'nsslapd-schemadir' ]) - self.errlog = ent.getValue('nsslapd-errorlog') - self.confdir = ent.getValue('nsslapd-certdir') - if not self.confdir: - self.confdir = ent.getValue('nsslapd-schemadir') - if self.confdir: - self.confdir = os.path.dirname(self.confdir) - ent = self.getEntry('cn=config,cn=ldbm database,cn=plugins,cn=config', - ldap.SCOPE_BASE, '(objectclass=*)', - [ 'nsslapd-directory' ]) - self.dbdir = os.path.dirname(ent.getValue('nsslapd-directory')) - except (ldap.INSUFFICIENT_ACCESS, ldap.CONNECT_ERROR): - pass # usually means - except ldap.LDAPError, e: - print "caught exception ", e - raise - - def __localinit(self): - """If a CA certificate is provided then it is assumed that we are - doing SSL client authentication with proxy auth. - - If a CA certificate is not present then it is assumed that we are - using a forwarded kerberos ticket for SASL auth. SASL provides - its own encryption. - """ - if self.cacert is not None: - SimpleLDAPObject.__init__(self,'ldaps://%s:%d' % (self.host,self.port)) - else: - SimpleLDAPObject.__init__(self,'ldap://%s:%d' % (self.host,self.port)) - - def __init__(self,host,port=389,cacert=None,bindcert=None,bindkey=None,proxydn=None,debug=None): - """We just set our instance variables and wrap the methods - the real - work is done in __localinit and __initPart2 - these are separated - out this way so that we can call them from places other than - instance creation e.g. when we just need to reconnect, not create a - new instance""" - if debug and debug.lower() == "on": - ldap.set_option(ldap.OPT_DEBUG_LEVEL,255) - if cacert is not None: - ldap.set_option(ldap.OPT_X_TLS_CACERTFILE,cacert) - if bindcert is not None: - ldap.set_option(ldap.OPT_X_TLS_CERTFILE,bindcert) - if bindkey is not None: - ldap.set_option(ldap.OPT_X_TLS_KEYFILE,bindkey) - - self.__wrapmethods() - self.port = port - self.host = host - self.cacert = cacert - self.bindcert = bindcert - self.bindkey = bindkey - self.proxydn = proxydn - self.suffixes = {} - self.__localinit() - - def __str__(self): - return self.host + ":" + str(self.port) - - def __get_server_controls(self): - """Create the proxy user server control. The control has the form - 0x04 = Octet String - 4|0x80 sets the length of the string length field at 4 bytes - the struct() gets us the length in bytes of string self.proxydn - self.proxydn is the proxy dn to send""" - - if self.proxydn is not None: - proxydn = chr(0x04) + chr(4|0x80) + struct.pack('l', socket.htonl(len(self.proxydn))) + self.proxydn; - - # Create the proxy control - sctrl=[] - sctrl.append(LDAPControl('2.16.840.1.113730.3.4.18',True,proxydn)) - else: - sctrl=None - - return sctrl - - def toLDAPURL(self): - return "ldap://%s:%d/" % (self.host,self.port) - - def set_proxydn(self, proxydn): - self.proxydn = proxydn - - def set_krbccache(self, krbccache, principal): - if krbccache is not None: - os.environ["KRB5CCNAME"] = krbccache - self.sasl_interactive_bind_s("", sasl_auth) - self.principal = principal - self.proxydn = None - - def do_simple_bind(self, binddn="cn=directory manager", bindpw=""): - self.binddn = binddn - self.bindpwd = bindpw - self.simple_bind_s(binddn, bindpw) - self.__initPart2() - - def getEntry(self,*args): - """This wraps the search function. It is common to just get one entry""" - - sctrl = self.__get_server_controls() - - if sctrl is not None: - self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl) - - try: - res = self.search(*args) - objtype, obj = self.result(res) - except ldap.NO_SUCH_OBJECT, e: - raise e - except ldap.LDAPError, e: - raise e - - if not obj: - raise ldap.NO_SUCH_OBJECT - - elif isinstance(obj,Entry): - return obj - else: # assume list/tuple - return obj[0] - - def getList(self,*args): - """This wraps the search function to find multiple entries.""" - - sctrl = self.__get_server_controls() - if sctrl is not None: - self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl) - - try: - res = self.search(*args) - objtype, obj = self.result(res) - except (ldap.ADMINLIMIT_EXCEEDED, ldap.SIZELIMIT_EXCEEDED), e: - # Too many results returned by search - raise e - except ldap.LDAPError, e: - raise e - - if not obj: - raise ldap.NO_SUCH_OBJECT - - entries = [] - for s in obj: - entries.append(s) - - return entries - - def getListAsync(self,*args): - """This version performs an asynchronous search, to allow - results even if we hit a limit. - - It returns a list: counter followed by the results. - If the results are truncated, counter will be set to -1. - """ - - sctrl = self.__get_server_controls() - if sctrl is not None: - self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl) - - entries = [] - partial = 0 - - try: - msgid = self.search_ext(*args) - objtype, result_list = self.result(msgid, 0) - while result_list: - for result in result_list: - entries.append(result) - objtype, result_list = self.result(msgid, 0) - except (ldap.ADMINLIMIT_EXCEEDED, ldap.SIZELIMIT_EXCEEDED, - ldap.TIMELIMIT_EXCEEDED), e: - partial = 1 - except ldap.LDAPError, e: - raise e - - if not entries: - raise ldap.NO_SUCH_OBJECT - - if partial == 1: - counter = -1 - else: - counter = len(entries) - - return [counter] + entries - - def addEntry(self,*args): - """This wraps the add function. It assumes that the entry is already - populated with all of the desired objectclasses and attributes""" - - sctrl = self.__get_server_controls() - - try: - if sctrl is not None: - self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl) - self.add_s(*args) - except ldap.ALREADY_EXISTS, e: - # duplicate value - raise e - except ldap.LDAPError, e: - raise e - return True - - def updateRDN(self, dn, newrdn): - """Wrap the modrdn function.""" - - sctrl = self.__get_server_controls() - - if dn == newrdn: - # no need to report an error - return True - - try: - if sctrl is not None: - self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl) - self.modrdn_s(dn, newrdn, delold=1) - except ldap.LDAPError, e: - raise e - return True - - def updateEntry(self,dn,oldentry,newentry): - """This wraps the mod function. It assumes that the entry is already - populated with all of the desired objectclasses and attributes""" - - sctrl = self.__get_server_controls() - - modlist = self.generateModList(oldentry, newentry) - - if len(modlist) == 0: - # FIXME: better error - raise SyntaxError("empty modlist") - - try: - if sctrl is not None: - self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl) - self.modify_s(dn, modlist) - # this is raised when a 'delete' attribute isn't found. - # it indicates the previous attribute was removed by another - # update, making the oldentry stale. - except ldap.NO_SUCH_ATTRIBUTE: - # FIXME: better error - raise SyntaxError("mid-air collision") - except ldap.LDAPError, e: - raise e - return True - - def generateModList(self, old_entry, new_entry): - """A mod list generator that computes more precise modification lists - than the python-ldap version. This version purposely generates no - REPLACE operations, to deal with multi-user updates more properly.""" - modlist = [] - - old_entry = ipautil.CIDict(old_entry) - new_entry = ipautil.CIDict(new_entry) - - keys = set(map(string.lower, old_entry.keys())) - keys.update(map(string.lower, new_entry.keys())) - - for key in keys: - new_values = new_entry.get(key, []) - if not(isinstance(new_values,list) or isinstance(new_values,tuple)): - new_values = [new_values] - new_values = filter(lambda value:value!=None, new_values) - new_values = set(new_values) - - old_values = old_entry.get(key, []) - if not(isinstance(old_values,list) or isinstance(old_values,tuple)): - old_values = [old_values] - old_values = filter(lambda value:value!=None, old_values) - old_values = set(old_values) - - adds = list(new_values.difference(old_values)) - removes = list(old_values.difference(new_values)) - - if len(removes) > 0: - modlist.append((ldap.MOD_DELETE, key, removes)) - if len(adds) > 0: - modlist.append((ldap.MOD_ADD, key, adds)) - - return modlist - - def inactivateEntry(self,dn,has_key): - """Rather than deleting entries we mark them as inactive. - has_key defines whether the entry already has nsAccountlock - set so we can determine which type of mod operation to run.""" - - sctrl = self.__get_server_controls() - modlist=[] - - if has_key: - operation = ldap.MOD_REPLACE - else: - operation = ldap.MOD_ADD - - modlist.append((operation, "nsAccountlock", "true")) - - try: - if sctrl is not None: - self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl) - self.modify_s(dn, modlist) - except ldap.LDAPError, e: - raise e - return True - - def deleteEntry(self,*args): - """This wraps the delete function. Use with caution.""" - - sctrl = self.__get_server_controls() - - try: - if sctrl is not None: - self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl) - self.delete_s(*args) - except ldap.LDAPError, e: - raise e - return True - - def modifyPassword(self,dn,oldpass,newpass): - """Set the user password using RFC 3062, LDAP Password Modify Extended - Operation. This ends up calling the IPA password slapi plugin - handler so the Kerberos password gets set properly. - - oldpass is not mandatory - """ - - sctrl = self.__get_server_controls() - - try: - if sctrl is not None: - self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl) - self.passwd_s(dn, oldpass, newpass) - except ldap.LDAPError, e: - raise e - return True - - def __wrapmethods(self): - """This wraps all methods of SimpleLDAPObject, so that we can intercept - the methods that deal with entries. Instead of using a raw list of tuples - of lists of hashes of arrays as the entry object, we want to wrap entries - in an Entry class that provides some useful methods""" - for name in dir(self.__class__.__bases__[0]): - attr = getattr(self, name) - if callable(attr): - setattr(self, name, wrapper(attr, name)) - - def addSchema(self, attr, val): - dn = "cn=schema" - self.modify_s(dn, [(ldap.MOD_ADD, attr, val)]) - - def addAttr(self, *args): - return self.addSchema('attributeTypes', args) - - def addObjClass(self, *args): - return self.addSchema('objectClasses', args) - - ########################### - # Static methods start here - ########################### - def normalizeDN(dn): - # not great, but will do until we use a newer version of python-ldap - # that has DN utilities - ary = ldap.explode_dn(dn.lower()) - return ",".join(ary) - normalizeDN = staticmethod(normalizeDN) - - def getfqdn(name=''): - return socket.getfqdn(name) - getfqdn = staticmethod(getfqdn) - - def getdomainname(name=''): - fqdn = IPAdmin.getfqdn(name) - index = fqdn.find('.') - if index >= 0: - return fqdn[index+1:] - else: - return fqdn - getdomainname = staticmethod(getdomainname) - - def getdefaultsuffix(name=''): - dm = IPAdmin.getdomainname(name) - if dm: - return "dc=" + dm.replace('.', ', dc=') - else: - return 'dc=localdomain' - getdefaultsuffix = staticmethod(getdefaultsuffix) - - def is_a_dn(dn): - """Returns True if the given string is a DN, False otherwise.""" - return (dn.find("=") > 0) - is_a_dn = staticmethod(is_a_dn) - - -def notfound(args): - """Return a string suitable for displaying as an error when a - search returns no results. - - This just returns whatever is after the equals sign""" - if len(args) > 2: - searchfilter = args[2] - try: - target = re.match(r'\(.*=(.*)\)', searchfilter).group(1) - except: - target = searchfilter - return "%s not found" % str(target) - else: - return args[0] diff --git a/ipalib/ipautil.py b/ipalib/ipautil.py deleted file mode 100644 index 6b0e2c89..00000000 --- a/ipalib/ipautil.py +++ /dev/null @@ -1,190 +0,0 @@ -# Authors: Simo Sorce -# -# Copyright (C) 2007 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 -# - -import string -import xmlrpclib -import re - -def realm_to_suffix(realm_name): - s = realm_name.split(".") - terms = ["dc=" + x.lower() for x in s] - return ",".join(terms) - -class CIDict(dict): - """ - Case-insensitive but case-respecting dictionary. - - This code is derived from python-ldap's cidict.py module, - written by stroeder: http://python-ldap.sourceforge.net/ - - This version extends 'dict' so it works properly with TurboGears. - If you extend UserDict, isinstance(foo, dict) returns false. - """ - - def __init__(self,default=None): - super(CIDict, self).__init__() - self._keys = {} - self.update(default or {}) - - def __getitem__(self,key): - return super(CIDict,self).__getitem__(string.lower(key)) - - def __setitem__(self,key,value): - lower_key = string.lower(key) - self._keys[lower_key] = key - return super(CIDict,self).__setitem__(string.lower(key),value) - - def __delitem__(self,key): - lower_key = string.lower(key) - del self._keys[lower_key] - return super(CIDict,self).__delitem__(string.lower(key)) - - def update(self,dict): - for key in dict.keys(): - self[key] = dict[key] - - def has_key(self,key): - return super(CIDict, self).has_key(string.lower(key)) - - def get(self,key,failobj=None): - try: - return self[key] - except KeyError: - return failobj - - def keys(self): - return self._keys.values() - - def items(self): - result = [] - for k in self._keys.values(): - result.append((k,self[k])) - return result - - def copy(self): - copy = {} - for k in self._keys.values(): - copy[k] = self[k] - return copy - - def iteritems(self): - return self.copy().iteritems() - - def iterkeys(self): - return self.copy().iterkeys() - - def setdefault(self,key,value=None): - try: - return self[key] - except KeyError: - self[key] = value - return value - - def pop(self, key, *args): - try: - value = self[key] - del self[key] - return value - except KeyError: - if len(args) == 1: - return args[0] - raise - - def popitem(self): - (lower_key,value) = super(CIDict,self).popitem() - key = self._keys[lower_key] - del self._keys[lower_key] - - return (key,value) - - -# -# The safe_string_re regexp and needs_base64 function are extracted from the -# python-ldap ldif module, which was -# written by Michael Stroeder -# http://python-ldap.sourceforge.net -# -# It was extracted because ipaldap.py is naughtily reaching into the ldif -# module and squashing this regexp. -# -SAFE_STRING_PATTERN = '(^(\000|\n|\r| |:|<)|[\000\n\r\200-\377]+|[ ]+$)' -safe_string_re = re.compile(SAFE_STRING_PATTERN) - -def needs_base64(s): - """ - returns 1 if s has to be base-64 encoded because of special chars - """ - return not safe_string_re.search(s) is None - - -def wrap_binary_data(data): - """Converts all binary data strings into Binary objects for transport - back over xmlrpc.""" - if isinstance(data, str): - if needs_base64(data): - return xmlrpclib.Binary(data) - else: - return data - elif isinstance(data, list) or isinstance(data,tuple): - retval = [] - for value in data: - retval.append(wrap_binary_data(value)) - return retval - elif isinstance(data, dict): - retval = {} - for (k,v) in data.iteritems(): - retval[k] = wrap_binary_data(v) - return retval - else: - return data - - -def unwrap_binary_data(data): - """Converts all Binary objects back into strings.""" - if isinstance(data, xmlrpclib.Binary): - # The data is decoded by the xmlproxy, but is stored - # in a binary object for us. - return str(data) - elif isinstance(data, str): - return data - elif isinstance(data, list) or isinstance(data,tuple): - retval = [] - for value in data: - retval.append(unwrap_binary_data(value)) - return retval - elif isinstance(data, dict): - retval = {} - for (k,v) in data.iteritems(): - retval[k] = unwrap_binary_data(v) - return retval - else: - return data - -def get_gsserror(e): - """A GSSError exception looks differently in python 2.4 than it does - in python 2.5, deal with it.""" - - try: - primary = e[0] - secondary = e[1] - except: - primary = e[0][0] - secondary = e[0][1] - - return (primary[0], secondary[0]) diff --git a/ipalib/servercore.py b/ipalib/servercore.py deleted file mode 100644 index 8626c04b..00000000 --- a/ipalib/servercore.py +++ /dev/null @@ -1,148 +0,0 @@ -# Authors: Rob Crittenden -# -# Copyright (C) 2007 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 -# - -import sys -sys.path.insert(0, ".") -sys.path.insert(0, "..") -import ldap -from ipalib.conn import context -from ipalib import ipautil - -# temporary -import krbV - -krbctx = krbV.default_context() -realm = krbctx.default_realm -basedn = ipautil.realm_to_suffix(realm) - -def convert_entry(ent): - entry = dict(ent.data) - entry['dn'] = ent.dn - # For now convert single entry lists to a string for the ui. - # TODO: we need to deal with multi-values better - for key,value in entry.iteritems(): - if isinstance(value,list) or isinstance(value,tuple): - if len(value) == 0: - entry[key] = '' - elif len(value) == 1: - entry[key] = value[0] - return entry - -def convert_scalar_values(orig_dict): - """LDAP update dicts expect all values to be a list (except for dn). - This method converts single entries to a list.""" - new_dict={} - for (k,v) in orig_dict.iteritems(): - if not isinstance(v, list) and k != 'dn': - v = [v] - new_dict[k] = v - - return new_dict - - -# TODO: rethink the get_entry vs get_list API calls. -# they currently restrict the data coming back without -# restricting scope. For now adding a get_base/sub_entry() -# calls, but the API isn't great. -def get_entry (base, scope, searchfilter, sattrs=None): - """Get a specific entry (with a parametized scope). - Return as a dict of values. - Multi-valued fields are represented as lists. - """ - ent="" - - ent = context.conn.getConn().getEntry(base, scope, searchfilter, sattrs) - - return convert_entry(ent) - -def get_base_entry (base, searchfilter, sattrs=None): - """Get a specific entry (with a scope of BASE). - Return as a dict of values. - Multi-valued fields are represented as lists. - """ - return get_entry(base, ldap.SCOPE_BASE, searchfilter, sattrs) - -def get_sub_entry (base, searchfilter, sattrs=None): - """Get a specific entry (with a scope of SUB). - Return as a dict of values. - Multi-valued fields are represented as lists. - """ - return get_entry(base, ldap.SCOPE_SUBTREE, searchfilter, sattrs) - -def get_list (base, searchfilter, sattrs=None): - """Gets a list of entries. Each is converted to a dict of values. - Multi-valued fields are represented as lists. - """ - entries = [] - - entries = context.conn.getConn().getList(base, ldap.SCOPE_SUBTREE, searchfilter, sattrs) - - return map(convert_entry, entries) - -def update_entry (oldentry, newentry): - """Update an LDAP entry - - oldentry is a dict - newentry is a dict - """ - oldentry = convert_scalar_values(oldentry) - newentry = convert_scalar_values(newentry) - - # Should be able to get this from either the old or new entry - # but just in case someone has decided to try changing it, use the - # original - try: - moddn = oldentry['dn'] - except KeyError, e: - # FIXME: return a missing DN error message - raise e - - res = context.conn.getConn().updateEntry(moddn, oldentry, newentry) - return res - -def uniq_list(x): - """Return a unique list, preserving order and ignoring case""" - myset = {} - return [set.setdefault(e.lower(),e) for e in x if e.lower() not in myset] - -def get_schema(): - """Retrieves the current LDAP schema from the LDAP server.""" - - schema_entry = get_base_entry("", "objectclass=*", ['dn','subschemasubentry']) - schema_cn = schema_entry.get('subschemasubentry') - schema = get_base_entry(schema_cn, "objectclass=*", ['*']) - - return schema - -def get_objectclasses(): - """Returns a list of available objectclasses that the LDAP - server supports. This parses out the syntax, attributes, etc - and JUST returns a lower-case list of the names.""" - - schema = get_schema() - - objectclasses = schema.get('objectclasses') - - # Convert this list into something more readable - result = [] - for i in range(len(objectclasses)): - oc = objectclasses[i].lower().split(" ") - result.append(oc[3].replace("'","")) - - return result -- cgit From 5479a349a87458e8e1a7997ad16720778e65be96 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 20:17:53 +0000 Subject: 346: Added skeleton framework for crud.py module and corresponding test_crud.py module --- ipalib/crud.py | 44 ++++++++++++++++++++++++++ ipalib/tests/test_crud.py | 80 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+) create mode 100644 ipalib/crud.py create mode 100644 ipalib/tests/test_crud.py (limited to 'ipalib') diff --git a/ipalib/crud.py b/ipalib/crud.py new file mode 100644 index 00000000..9f410fde --- /dev/null +++ b/ipalib/crud.py @@ -0,0 +1,44 @@ +# Authors: +# Jason Gerard DeRose +# +# 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 + +""" +Base classes for standard CRUD operations. +""" + +import frontend, errors + + +class Add(frontend.Method): + pass + + +class Get(frontend.Method): + pass + + +class Del(frontend.Method): + pass + + +class Mod(frontend.Method): + pass + + +class Find(frontend.Method): + pass diff --git a/ipalib/tests/test_crud.py b/ipalib/tests/test_crud.py new file mode 100644 index 00000000..d708d808 --- /dev/null +++ b/ipalib/tests/test_crud.py @@ -0,0 +1,80 @@ +# Authors: +# Jason Gerard DeRose +# +# 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 + +""" +Unit tests for `ipalib.crud` module. +""" + +from tstutil import read_only, raises, ClassChecker +from ipalib import crud, frontend + + +class test_Add(ClassChecker): + """ + Test the `crud.Add` class. + """ + + _cls = crud.Add + + def test_class(self): + assert self.cls.__bases__ == (frontend.Method,) + + +class test_Get(ClassChecker): + """ + Test the `crud.Get` class. + """ + + _cls = crud.Get + + def test_class(self): + assert self.cls.__bases__ == (frontend.Method,) + + +class test_Del(ClassChecker): + """ + Test the `crud.Del` class. + """ + + _cls = crud.Del + + def test_class(self): + assert self.cls.__bases__ == (frontend.Method,) + + +class test_Mod(ClassChecker): + """ + Test the `crud.Mod` class. + """ + + _cls = crud.Mod + + def test_class(self): + assert self.cls.__bases__ == (frontend.Method,) + + +class test_Find(ClassChecker): + """ + Test the `crud.Find` class. + """ + + _cls = crud.Find + + def test_class(self): + assert self.cls.__bases__ == (frontend.Method,) -- cgit From 566d5ea02a5dfdf6f0da0ce1a3f0bb656604c233 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 21:29:15 +0000 Subject: 347: Added primary_key instance attribute to Param and corresponding kwarg; expanded unit tests for Param.__init__() --- ipalib/frontend.py | 4 +++- ipalib/tests/test_frontend.py | 24 +++++++++++++++++++++--- 2 files changed, 24 insertions(+), 4 deletions(-) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index ff0d3492..7e22a9fa 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -128,7 +128,8 @@ class Param(plugable.ReadOnly): default=None, default_from=None, rules=tuple(), - normalize=None + normalize=None, + primary_key=False, ) def __init__(self, name, type_=ipa_types.Unicode(), **kw): @@ -155,6 +156,7 @@ class Param(plugable.ReadOnly): self.__normalize = self.__kw['normalize'] self.rules = self.__check_type(tuple, 'rules') self.all_rules = (type_.validate,) + self.rules + self.primary_key = self.__check_type(bool, 'primary_key') lock(self) def __clone__(self, **override): diff --git a/ipalib/tests/test_frontend.py b/ipalib/tests/test_frontend.py index cc71f1c9..3d01ed11 100644 --- a/ipalib/tests/test_frontend.py +++ b/ipalib/tests/test_frontend.py @@ -140,6 +140,8 @@ class test_Param(ClassChecker): type_ = ipa_types.Unicode() o = self.cls(name, type_) assert o.__islocked__() is True + + # Test default values assert read_only(o, 'name') is name assert read_only(o, 'type') is type_ assert read_only(o, 'doc') == '' @@ -149,12 +151,28 @@ class test_Param(ClassChecker): assert read_only(o, 'default_from') is None assert read_only(o, 'rules') == tuple() assert read_only(o, 'all_rules') == (type_.validate,) - - # Check default type_: + assert read_only(o, 'primary_key') is False + + # Test all kw args: + assert self.cls(name, doc='the doc').doc == 'the doc' + assert self.cls(name, required=False).required is False + assert self.cls(name, multivalue=True).multivalue is True + assert self.cls(name, default=u'Hello').default == u'Hello' + df = frontend.DefaultFrom(lambda f, l: f + l, + 'first', 'last', + ) + assert self.cls(name, default_from=df).default_from == df + rules = (lambda whatever: 'Not okay!',) + o = self.cls(name, rules=rules) + assert o.rules is rules + assert o.all_rules[1:] == rules + assert self.cls(name, primary_key=True).primary_key is True + + # Test default type_: o = self.cls(name) assert isinstance(o.type, ipa_types.Unicode) - # Check param spec parsing: + # Test param spec parsing: o = self.cls('name?') assert o.name == 'name' assert o.required is False -- cgit From 250a01b5b7ee81b19b5da80f1ef47f1ab9174a64 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 21:46:37 +0000 Subject: 348: If no keys are passed to DefaultFrom.__init__(), the keys from callback.func_code.co_varnames are used; updated DefaultFrom unit tests to test this usage --- ipalib/frontend.py | 20 ++++++++++++-------- ipalib/tests/test_frontend.py | 7 +++++++ 2 files changed, 19 insertions(+), 8 deletions(-) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 7e22a9fa..1e27d93b 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -61,12 +61,16 @@ class DefaultFrom(plugable.ReadOnly): :param callback: The callable to call when all ``keys`` are present. :param keys: The keys used to map from keyword to position arguments. """ - assert callable(callback), 'not a callable: %r' % callback - assert len(keys) > 0, 'must have at least one key' - for key in keys: - assert type(key) is str, 'not an str: %r' % key + if not callable(callback): + raise TypeError('callback must be callable; got %r' % callback) self.callback = callback - self.keys = keys + if len(keys) == 0: + self.keys = callback.func_code.co_varnames + else: + self.keys = keys + for key in self.keys: + if type(key) is not str: + raise_TypeError(key, str, 'keys') lock(self) def __call__(self, **kw): @@ -77,11 +81,11 @@ class DefaultFrom(plugable.ReadOnly): """ vals = tuple(kw.get(k, None) for k in self.keys) if None in vals: - return None + return try: return self.callback(*vals) - except Exception: - return None + except StandardError: + pass def parse_param_spec(spec): diff --git a/ipalib/tests/test_frontend.py b/ipalib/tests/test_frontend.py index 3d01ed11..fb818a4e 100644 --- a/ipalib/tests/test_frontend.py +++ b/ipalib/tests/test_frontend.py @@ -89,6 +89,9 @@ class test_DefaultFrom(ClassChecker): o = self.cls(callback, *keys) assert read_only(o, 'callback') is callback assert read_only(o, 'keys') == keys + lam = lambda first, last: first[0] + last + o = self.cls(lam) + assert read_only(o, 'keys') == ('first', 'last') def test_call(self): """ @@ -109,6 +112,10 @@ class test_DefaultFrom(ClassChecker): kw_copy = dict(kw) del kw_copy[key] assert o(**kw_copy) is None + o = self.cls(lambda first, last: first[0] + last) + assert o(first='john', last='doe') == 'jdoe' + assert o(first='', last='doe') is None + assert o(one='john', two='doe') is None def test_parse_param_spec(): -- cgit From 755ea8d0c26afcd1909994a6d381014d79997a33 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 21:57:34 +0000 Subject: 349: Improved clarity of local variables in Param.__init__() --- ipalib/frontend.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 1e27d93b..6d71a667 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -136,28 +136,28 @@ class Param(plugable.ReadOnly): primary_key=False, ) - def __init__(self, name, type_=ipa_types.Unicode(), **kw): - if 'required' not in kw and 'multivalue' not in kw: + def __init__(self, name, type_=ipa_types.Unicode(), **override): + if not ('required' in override or 'multivalue' in override): (name, kw_from_spec) = parse_param_spec(name) - kw.update(kw_from_spec) - default = dict(self.__default) - if not set(default).issuperset(kw): - extra = sorted(set(kw) - set(default)) + override.update(kw_from_spec) + kw = dict(self.__default) + if not set(kw).issuperset(override): + extra = sorted(set(override) - set(kw)) raise TypeError( 'Param.__init__() takes no such kwargs: %s' % ', '.join(extra) ) - default.update(kw) - self.__kw = default + kw.update(override) + self.__kw = kw self.name = check_name(name) self.type = check_isinstance(type_, ipa_types.Type, 'type_') self.doc = self.__check_type(str, 'doc') self.required = self.__check_type(bool, 'required') self.multivalue = self.__check_type(bool, 'multivalue') - self.default = self.__kw['default'] + self.default = kw['default'] self.default_from = self.__check_type(DefaultFrom, 'default_from', allow_none=True ) - self.__normalize = self.__kw['normalize'] + self.__normalize = kw['normalize'] self.rules = self.__check_type(tuple, 'rules') self.all_rules = (type_.validate,) + self.rules self.primary_key = self.__check_type(bool, 'primary_key') -- cgit From e2a680d7c9ca7416e9e3cffe25835fdee967c995 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 22:05:01 +0000 Subject: 350: If Param default_from kwarg is callable but not a DefaltFrom instances, the instance is created implicity --- ipalib/frontend.py | 9 ++++++--- ipalib/tests/test_frontend.py | 7 ++++++- 2 files changed, 12 insertions(+), 4 deletions(-) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 6d71a667..80579b7b 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -125,7 +125,7 @@ class Param(plugable.ReadOnly): A parameter accepted by a `Command`. """ __nones = (None, '', tuple(), []) - __default = dict( + __defaults = dict( doc='', required=True, multivalue=False, @@ -140,7 +140,7 @@ class Param(plugable.ReadOnly): if not ('required' in override or 'multivalue' in override): (name, kw_from_spec) = parse_param_spec(name) override.update(kw_from_spec) - kw = dict(self.__default) + kw = dict(self.__defaults) if not set(kw).issuperset(override): extra = sorted(set(override) - set(kw)) raise TypeError( @@ -154,7 +154,10 @@ class Param(plugable.ReadOnly): self.required = self.__check_type(bool, 'required') self.multivalue = self.__check_type(bool, 'multivalue') self.default = kw['default'] - self.default_from = self.__check_type(DefaultFrom, 'default_from', + df = kw['default_from'] + if callable(df) and not isinstance(df, DefaultFrom): + df = DefaultFrom(df) + self.default_from = check_type(df, DefaultFrom, 'default_from', allow_none=True ) self.__normalize = kw['normalize'] diff --git a/ipalib/tests/test_frontend.py b/ipalib/tests/test_frontend.py index fb818a4e..06d6b8cb 100644 --- a/ipalib/tests/test_frontend.py +++ b/ipalib/tests/test_frontend.py @@ -168,7 +168,12 @@ class test_Param(ClassChecker): df = frontend.DefaultFrom(lambda f, l: f + l, 'first', 'last', ) - assert self.cls(name, default_from=df).default_from == df + lam = lambda first, last: first + last + for cb in (df, lam): + o = self.cls(name, default_from=cb) + assert type(o.default_from) is frontend.DefaultFrom + assert o.default_from.keys == ('first', 'last') + assert o.default_from.callback('butt', 'erfly') == 'butterfly' rules = (lambda whatever: 'Not okay!',) o = self.cls(name, rules=rules) assert o.rules is rules -- cgit From 3d6ab69b46e5be32af94ecdfb5a696973eeaf7c4 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 22:19:43 +0000 Subject: 351: Removed Object.Method property and added in its place Object.methods instance attribute --- ipalib/frontend.py | 9 +++------ ipalib/tests/test_frontend.py | 9 ++++----- 2 files changed, 7 insertions(+), 11 deletions(-) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 80579b7b..c3b1707b 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -511,11 +511,11 @@ class Command(plugable.Plugin): class Object(plugable.Plugin): __public__ = frozenset(( - 'Method', + 'methods', 'Property', 'params' )) - __Method = None + methods = None __Property = None takes_params = tuple() @@ -528,9 +528,6 @@ class Object(plugable.Plugin): for param in self.takes_params: yield create_param(param) - def __get_Method(self): - return self.__Method - Method = property(__get_Method) def __get_Property(self): return self.__Property @@ -538,7 +535,7 @@ class Object(plugable.Plugin): def set_api(self, api): super(Object, self).set_api(api) - self.__Method = self.__create_namespace('Method') + self.methods = self.__create_namespace('Method') self.__Property = self.__create_namespace('Property') def __create_namespace(self, name): diff --git a/ipalib/tests/test_frontend.py b/ipalib/tests/test_frontend.py index 06d6b8cb..f0ba524d 100644 --- a/ipalib/tests/test_frontend.py +++ b/ipalib/tests/test_frontend.py @@ -760,7 +760,6 @@ class test_Object(ClassChecker): def test_class(self): assert self.cls.__bases__ == (plugable.Plugin,) - assert type(self.cls.Method) is property assert type(self.cls.Property) is property def test_init(self): @@ -768,7 +767,7 @@ class test_Object(ClassChecker): Tests the `frontend.Object.__init__` method. """ o = self.cls() - assert read_only(o, 'Method') is None + assert o.methods is None assert read_only(o, 'Property') is None def test_set_api(self): @@ -798,13 +797,13 @@ class test_Object(ClassChecker): cnt = 10 formats = dict( - Method='method_%d', + methods='method_%d', Property='property_%d', ) class api(object): Method = plugable.NameSpace( - get_attributes(cnt, formats['Method']) + get_attributes(cnt, formats['methods']) ) Property = plugable.NameSpace( get_attributes(cnt, formats['Property']) @@ -819,7 +818,7 @@ class test_Object(ClassChecker): o = user() o.set_api(api) assert read_only(o, 'api') is api - for name in ['Method', 'Property']: + for name in ['methods', 'Property']: namespace = getattr(o, name) assert isinstance(namespace, plugable.NameSpace) assert len(namespace) == cnt -- cgit From c3b09b2116dcbab36098f11c6b3684a6d0e47c08 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 23:19:34 +0000 Subject: 352: Now removed Object.Property property and added in its place Object.properties instance attribute --- ipalib/frontend.py | 15 +++++---------- ipalib/tests/test_frontend.py | 14 ++++++++------ 2 files changed, 13 insertions(+), 16 deletions(-) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index c3b1707b..132e3039 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -512,11 +512,11 @@ class Command(plugable.Plugin): class Object(plugable.Plugin): __public__ = frozenset(( 'methods', - 'Property', + 'properties', 'params' )) methods = None - __Property = None + properties = None takes_params = tuple() def __init__(self): @@ -528,15 +528,10 @@ class Object(plugable.Plugin): for param in self.takes_params: yield create_param(param) - - def __get_Property(self): - return self.__Property - Property = property(__get_Property) - def set_api(self, api): super(Object, self).set_api(api) self.methods = self.__create_namespace('Method') - self.__Property = self.__create_namespace('Property') + self.properties = self.__create_namespace('Property') def __create_namespace(self, name): return plugable.NameSpace(self.__filter_members(name)) @@ -596,14 +591,14 @@ class Method(Attribute, Command): def get_options(self): for option in self.takes_options: yield option - if self.obj is not None and self.obj.Property is not None: + if self.obj is not None and self.obj.properties is not None: def get_key(p): if p.param.required: if p.param.default_from is None: return 0 return 1 return 2 - for prop in sorted(self.obj.Property(), key=get_key): + for prop in sorted(self.obj.properties(), key=get_key): yield prop.param diff --git a/ipalib/tests/test_frontend.py b/ipalib/tests/test_frontend.py index f0ba524d..0109934d 100644 --- a/ipalib/tests/test_frontend.py +++ b/ipalib/tests/test_frontend.py @@ -760,7 +760,9 @@ class test_Object(ClassChecker): def test_class(self): assert self.cls.__bases__ == (plugable.Plugin,) - assert type(self.cls.Property) is property + assert self.cls.methods is None + assert self.cls.properties is None + assert self.cls.takes_params == tuple() def test_init(self): """ @@ -768,7 +770,7 @@ class test_Object(ClassChecker): """ o = self.cls() assert o.methods is None - assert read_only(o, 'Property') is None + assert o.properties is None def test_set_api(self): """ @@ -798,7 +800,7 @@ class test_Object(ClassChecker): cnt = 10 formats = dict( methods='method_%d', - Property='property_%d', + properties='property_%d', ) class api(object): @@ -806,7 +808,7 @@ class test_Object(ClassChecker): get_attributes(cnt, formats['methods']) ) Property = plugable.NameSpace( - get_attributes(cnt, formats['Property']) + get_attributes(cnt, formats['properties']) ) assert len(api.Method) == cnt * 3 assert len(api.Property) == cnt * 3 @@ -818,7 +820,7 @@ class test_Object(ClassChecker): o = user() o.set_api(api) assert read_only(o, 'api') is api - for name in ['methods', 'Property']: + for name in ['methods', 'properties']: namespace = getattr(o, name) assert isinstance(namespace, plugable.NameSpace) assert len(namespace) == cnt @@ -919,7 +921,7 @@ class test_Method(ClassChecker): ), ]) return self.__prop - Property = property(__get_prop) + properties = property(__get_prop) type_ = ipa_types.Unicode() class noun_verb(self.cls): takes_options= ( -- cgit From be2e323bbf3f036777acd6e5e16e03f9e66b2ee8 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 23:29:15 +0000 Subject: 353: The Object.parms instance attribute is now created in Object.set_api() instead of in Object.__init__() --- ipalib/frontend.py | 9 ++++----- ipalib/tests/test_frontend.py | 22 +++++++++++++--------- 2 files changed, 17 insertions(+), 14 deletions(-) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 132e3039..bcd610a5 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -517,13 +517,9 @@ class Object(plugable.Plugin): )) methods = None properties = None + params = None takes_params = tuple() - def __init__(self): - self.params = plugable.NameSpace( - (create_param(p) for p in self.takes_params), sort=False - ) - def __create_params(self): for param in self.takes_params: yield create_param(param) @@ -532,6 +528,9 @@ class Object(plugable.Plugin): super(Object, self).set_api(api) self.methods = self.__create_namespace('Method') self.properties = self.__create_namespace('Property') + self.params = plugable.NameSpace( + (create_param(p) for p in self.takes_params), sort=False + ) def __create_namespace(self, name): return plugable.NameSpace(self.__filter_members(name)) diff --git a/ipalib/tests/test_frontend.py b/ipalib/tests/test_frontend.py index 0109934d..f6bca9b9 100644 --- a/ipalib/tests/test_frontend.py +++ b/ipalib/tests/test_frontend.py @@ -754,7 +754,7 @@ class test_Command(ClassChecker): class test_Object(ClassChecker): """ - Tests the `frontend.Object` class. + Test the `frontend.Object` class. """ _cls = frontend.Object @@ -762,19 +762,22 @@ class test_Object(ClassChecker): assert self.cls.__bases__ == (plugable.Plugin,) assert self.cls.methods is None assert self.cls.properties is None + assert self.cls.params is None assert self.cls.takes_params == tuple() def test_init(self): """ - Tests the `frontend.Object.__init__` method. + Test the `frontend.Object.__init__` method. """ o = self.cls() assert o.methods is None assert o.properties is None + assert o.params is None + assert o.properties is None def test_set_api(self): """ - Tests the `frontend.Object.set_api` method. + Test the `frontend.Object.set_api` method. """ # Setup for test: class DummyAttribute(object): @@ -834,16 +837,17 @@ class test_Object(ClassChecker): assert attr.attr_name == attr_name assert attr.name == attr_name - def test_params(self): - """ - Test the ``frontend.Object.params`` instance attribute. - """ - ns = self.cls().params + # Test params instance attribute + o = self.cls() + o.set_api(api) + ns = o.params assert type(ns) is plugable.NameSpace assert len(ns) == 0 class example(self.cls): takes_params = ('banana', 'apple') - ns = example().params + o = example() + o.set_api(api) + ns = o.params assert type(ns) is plugable.NameSpace assert len(ns) == 2, repr(ns) assert list(ns) == ['banana', 'apple'] -- cgit From f531f7da81864f135ff1a5f7d69e15fbe8a27210 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 24 Sep 2008 23:49:44 +0000 Subject: 354: Added NameSpace.__todict__() method that returns copy of NameSpace.__map; updated NameSpace unit test to also test __todict__() --- ipalib/frontend.py | 9 +++++++-- ipalib/plugable.py | 18 ++++++++++++------ ipalib/tests/test_plugable.py | 1 + 3 files changed, 20 insertions(+), 8 deletions(-) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index bcd610a5..6c5f8c76 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -521,8 +521,13 @@ class Object(plugable.Plugin): takes_params = tuple() def __create_params(self): - for param in self.takes_params: - yield create_param(param) + props = self.properties.__todict__() + for spec in self.takes_params: + if type(spec) is str and spec.rstrip('?*+') in props: + yield props.pop(spec.rstrip('?*+')).param + else: + yield create_param(spec) + def set_api(self, api): super(Object, self).set_api(api) diff --git a/ipalib/plugable.py b/ipalib/plugable.py index cd130a19..e1d728d4 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -545,13 +545,13 @@ class NameSpace(ReadOnly): def __len__(self): """ - Returns the number of members. + Return the number of members. """ return len(self.__members) def __iter__(self): """ - Iterates through the member names. + Iterate through the member names. If this instance was created with ``sort=True``, the names will be in alphabetical order; otherwise the names will be in the same order as @@ -564,7 +564,7 @@ class NameSpace(ReadOnly): def __call__(self): """ - Iterates through the members. + Iterate through the members. If this instance was created with ``sort=True``, the members will be in alphabetical order by name; otherwise the members will be in the @@ -577,13 +577,13 @@ class NameSpace(ReadOnly): def __contains__(self, name): """ - Returns True if namespace has a member named ``name``. + Return True if namespace has a member named ``name``. """ return name in self.__map def __getitem__(self, spec): """ - Returns a member by name or index, or returns a slice of members. + Return a member by name or index, or returns a slice of members. :param spec: The name or index of a member, or a slice object. """ @@ -597,7 +597,7 @@ class NameSpace(ReadOnly): def __repr__(self): """ - Returns a pseudo-valid expression that could create this instance. + Return a pseudo-valid expression that could create this instance. """ return '%s(<%d members>, sort=%r)' % ( self.__class__.__name__, @@ -605,6 +605,12 @@ class NameSpace(ReadOnly): self.__sort, ) + def __todict__(self): + """ + Return a copy of the private dict mapping name to member. + """ + return dict(self.__map) + class Registrar(DictProxy): """ diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index 02d35cde..95d3825f 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -586,6 +586,7 @@ class test_NameSpace(ClassChecker): else: ordered = members names = tuple(m.name for m in ordered) + assert o.__todict__() == dict((o.name, o) for o in ordered) # Test __len__: assert len(o) == cnt -- cgit From 79b33ad3663b91ad7816cf55737faa28603fca70 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 25 Sep 2008 00:00:58 +0000 Subject: 355: Object.set_api() now creates Object.params namespace by merging takes_params and properties together intelegintly --- ipalib/frontend.py | 27 +++++++++++++++++---------- ipalib/tests/test_frontend.py | 2 ++ 2 files changed, 19 insertions(+), 10 deletions(-) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 6c5f8c76..6aa21eb8 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -520,21 +520,12 @@ class Object(plugable.Plugin): params = None takes_params = tuple() - def __create_params(self): - props = self.properties.__todict__() - for spec in self.takes_params: - if type(spec) is str and spec.rstrip('?*+') in props: - yield props.pop(spec.rstrip('?*+')).param - else: - yield create_param(spec) - - def set_api(self, api): super(Object, self).set_api(api) self.methods = self.__create_namespace('Method') self.properties = self.__create_namespace('Property') self.params = plugable.NameSpace( - (create_param(p) for p in self.takes_params), sort=False + self.__create_params(), sort=False ) def __create_namespace(self, name): @@ -547,6 +538,22 @@ class Object(plugable.Plugin): if proxy.obj_name == self.name: yield proxy.__clone__('attr_name') + def __create_params(self): + props = self.properties.__todict__() + for spec in self.takes_params: + if type(spec) is str and spec.rstrip('?*+') in props: + yield props.pop(spec.rstrip('?*+')).param + else: + yield create_param(spec) + def get_key(p): + if p.param.required: + if p.param.default_from is None: + return 0 + return 1 + return 2 + for prop in sorted(props.itervalues(), key=get_key): + yield prop.param + class Attribute(plugable.Plugin): __public__ = frozenset(( diff --git a/ipalib/tests/test_frontend.py b/ipalib/tests/test_frontend.py index f6bca9b9..ec7cccd6 100644 --- a/ipalib/tests/test_frontend.py +++ b/ipalib/tests/test_frontend.py @@ -788,6 +788,8 @@ class test_Object(ClassChecker): self.name = '%s_%s' % (obj_name, attr_name) else: self.name = name + self.param = frontend.create_param(attr_name) + def __clone__(self, attr_name): return self.__class__( self.obj_name, -- cgit From 4747563a802a08863d2195222b2f428e52af8502 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 25 Sep 2008 00:42:38 +0000 Subject: 356: Modified Method.get_options() to now pull from self.obj.params(); updated unit tests for Method.get_options() --- ipalib/frontend.py | 21 +++++++-------- ipalib/plugins/example.py | 61 ++++++++++++++++++++++++------------------- ipalib/tests/test_frontend.py | 20 +++----------- 3 files changed, 47 insertions(+), 55 deletions(-) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 6aa21eb8..9f4f5295 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -541,8 +541,13 @@ class Object(plugable.Plugin): def __create_params(self): props = self.properties.__todict__() for spec in self.takes_params: - if type(spec) is str and spec.rstrip('?*+') in props: - yield props.pop(spec.rstrip('?*+')).param + if type(spec) is str: + key = spec.rstrip('?*+') + else: + assert type(spec) is Param + key = spec.name + if key in props: + yield props.pop(key).param else: yield create_param(spec) def get_key(p): @@ -602,15 +607,9 @@ class Method(Attribute, Command): def get_options(self): for option in self.takes_options: yield option - if self.obj is not None and self.obj.properties is not None: - def get_key(p): - if p.param.required: - if p.param.default_from is None: - return 0 - return 1 - return 2 - for prop in sorted(self.obj.properties(), key=get_key): - yield prop.param + if self.obj is not None and self.obj.params is not None: + for param in self.obj.params(): + yield param class Property(Attribute): diff --git a/ipalib/plugins/example.py b/ipalib/plugins/example.py index 24bf5b8f..143f9f29 100644 --- a/ipalib/plugins/example.py +++ b/ipalib/plugins/example.py @@ -23,6 +23,7 @@ Some example plugins. from ipalib import frontend +from ipalib.frontend import Param from ipalib import api @@ -55,33 +56,33 @@ api.register(user_find) # Register some properties for the 'user' object: -class user_givenname(frontend.Property): - 'User first name' - required = True -api.register(user_givenname) - -class user_sn(frontend.Property): - 'User last name' - required = True -api.register(user_sn) - -class user_login(frontend.Property): - 'User login' - required = True - default_from = frontend.DefaultFrom( - lambda first, last: (first[0] + last).lower(), - 'givenname', 'sn' - ) -api.register(user_login) - -class user_initials(frontend.Property): - 'User initials' - required = True - default_from = frontend.DefaultFrom( - lambda first, last: first[0] + last[0], - 'givenname', 'sn' - ) -api.register(user_initials) +#class user_givenname(frontend.Property): +# 'User first name' +# required = True +#api.register(user_givenname) + +#class user_sn(frontend.Property): +# 'User last name' +# required = True +#api.register(user_sn) + +#class user_login(frontend.Property): +# 'User login' +# required = True +# default_from = frontend.DefaultFrom( +# lambda first, last: (first[0] + last).lower(), +# 'givenname', 'sn' +# ) +#api.register(user_login) + +#class user_initials(frontend.Property): +# 'User initials' +# required = True +# default_from = frontend.DefaultFrom( +# lambda first, last: first[0] + last[0], +# 'givenname', 'sn' +# ) +#api.register(user_initials) # Register some methods for the 'group' object: @@ -132,4 +133,10 @@ api.register(service) class user(frontend.Object): 'User object' + takes_params = ( + 'givenname', + 'sn', + 'uid', + 'krbprincipalname', + ) api.register(user) diff --git a/ipalib/tests/test_frontend.py b/ipalib/tests/test_frontend.py index ec7cccd6..e78aeeb2 100644 --- a/ipalib/tests/test_frontend.py +++ b/ipalib/tests/test_frontend.py @@ -910,24 +910,10 @@ class test_Method(ClassChecker): assert self.cls.implements(frontend.Command) def get_subcls(self): - class example_prop0(frontend.Property): - 'Prop zero' - class example_prop1(frontend.Property): - 'Prop one' class example_obj(object): - __prop = None - def __get_prop(self): - if self.__prop is None: - self.__prop = plugable.NameSpace([ - plugable.PluginProxy( - frontend.Property, example_prop0(), 'attr_name' - ), - plugable.PluginProxy( - frontend.Property, example_prop1(), 'attr_name' - ), - ]) - return self.__prop - properties = property(__get_prop) + params = plugable.NameSpace( + frontend.create_param(n) for n in ('prop0', 'prop1') + ) type_ = ipa_types.Unicode() class noun_verb(self.cls): takes_options= ( -- cgit From e84dd7a69d6f0ab83ec00c4207186ad189ec7cb9 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 25 Sep 2008 00:58:16 +0000 Subject: 357: Some experimenting with the example plugins --- ipalib/plugins/example.py | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugins/example.py b/ipalib/plugins/example.py index 143f9f29..36af33cd 100644 --- a/ipalib/plugins/example.py +++ b/ipalib/plugins/example.py @@ -27,6 +27,26 @@ from ipalib.frontend import Param from ipalib import api +class user(frontend.Object): + 'User object' + takes_params = ( + 'givenname', + 'sn', + Param('uid', + primary_key=True, + default_from=lambda givenname, sn: givenname[0] + sn, + normalize=lambda value: value.lower(), + ), + Param('krbprincipalname', + default_from=lambda uid: '%s@EXAMPLE.COM' % uid, + ), + Param('homedirectory', + default_from=lambda uid: '/home/%s' % uid, + ) + ) +api.register(user) + + # Hypothetical functional commands (not associated with any object): class krbtest(frontend.Command): 'Test your Kerberos ticket.' @@ -130,13 +150,3 @@ api.register(group) class service(frontend.Object): 'Service object' api.register(service) - -class user(frontend.Object): - 'User object' - takes_params = ( - 'givenname', - 'sn', - 'uid', - 'krbprincipalname', - ) -api.register(user) -- cgit From 426742279348765d27ad66c69bea874398ed0ef4 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 25 Sep 2008 01:04:10 +0000 Subject: 358: Cleaned up private methods in Object --- ipalib/frontend.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 9f4f5295..948b047d 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -522,23 +522,24 @@ class Object(plugable.Plugin): def set_api(self, api): super(Object, self).set_api(api) - self.methods = self.__create_namespace('Method') - self.properties = self.__create_namespace('Property') + self.methods = plugable.NameSpace( + self.__get_attrs('Method'), sort=False + ) + self.properties = plugable.NameSpace( + self.__get_attrs('Property'), sort=False + ) self.params = plugable.NameSpace( - self.__create_params(), sort=False + self.__get_params(), sort=False ) - def __create_namespace(self, name): - return plugable.NameSpace(self.__filter_members(name)) - - def __filter_members(self, name): + def __get_attrs(self, name): namespace = getattr(self.api, name) assert type(namespace) is plugable.NameSpace for proxy in namespace(): # Equivalent to dict.itervalues() if proxy.obj_name == self.name: yield proxy.__clone__('attr_name') - def __create_params(self): + def __get_params(self): props = self.properties.__todict__() for spec in self.takes_params: if type(spec) is str: -- cgit From 54c97b494880a3d276e2da69ffde55a3ee475616 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 25 Sep 2008 01:44:53 +0000 Subject: 359: Added Object.primary_key instance attribute; added corresponding unit tests --- ipalib/frontend.py | 12 ++++++++++ ipalib/tests/test_frontend.py | 51 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 948b047d..40220074 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -514,10 +514,12 @@ class Object(plugable.Plugin): 'methods', 'properties', 'params' + 'primary_key', )) methods = None properties = None params = None + primary_key = None takes_params = tuple() def set_api(self, api): @@ -531,6 +533,16 @@ class Object(plugable.Plugin): self.params = plugable.NameSpace( self.__get_params(), sort=False ) + pkeys = filter(lambda p: p.primary_key, self.params()) + if len(pkeys) > 1: + raise ValueError( + '%s (Object) has multiple primary keys: %s' % ( + self.name, + ', '.join(p.name for p in pkeys), + ) + ) + if len(pkeys) == 1: + self.primary_key = pkeys[0] def __get_attrs(self, name): namespace = getattr(self.api, name) diff --git a/ipalib/tests/test_frontend.py b/ipalib/tests/test_frontend.py index e78aeeb2..38c35506 100644 --- a/ipalib/tests/test_frontend.py +++ b/ipalib/tests/test_frontend.py @@ -858,6 +858,57 @@ class test_Object(ClassChecker): assert p.required is True assert p.multivalue is False + def test_primary_key(self): + """ + Test the `frontend.Object.primary_key` attribute. + """ + api = plugable.API( + frontend.Method, + frontend.Property, + ) + api.finalize() + + # Test with no primary keys: + class example1(self.cls): + takes_params = ( + 'one', + 'two', + ) + o = example1() + o.set_api(api) + assert o.primary_key is None + + # Test with 1 primary key: + class example2(self.cls): + takes_params = ( + 'one', + 'two', + frontend.Param('three', + primary_key=True, + ), + 'four', + ) + o = example2() + o.set_api(api) + pk = o.primary_key + assert isinstance(pk, frontend.Param) + assert pk.name == 'three' + assert pk.primary_key is True + assert o.params[2] is o.primary_key + + # Test with multiple primary_key: + class example3(self.cls): + takes_params = ( + frontend.Param('one', primary_key=True), + frontend.Param('two', primary_key=True), + 'three', + frontend.Param('four', primary_key=True), + ) + o = example3() + e = raises(ValueError, o.set_api, api) + assert str(e) == \ + 'example3 (Object) has multiple primary keys: one, two, four' + class test_Attribute(ClassChecker): """ -- cgit From 9f704e001daf760de92c590f69582fc7ffd0c0f2 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 25 Sep 2008 01:52:34 +0000 Subject: 360: Removed Method.get_options() default implementation; cleaned up unit tests for Method --- ipalib/frontend.py | 7 ------- ipalib/tests/test_frontend.py | 36 ++++++++++++------------------------ 2 files changed, 12 insertions(+), 31 deletions(-) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 40220074..5573e944 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -617,13 +617,6 @@ class Method(Attribute, Command): Attribute.__init__(self) Command.__init__(self) - def get_options(self): - for option in self.takes_options: - yield option - if self.obj is not None and self.obj.params is not None: - for param in self.obj.params(): - yield param - class Property(Attribute): __public__ = frozenset(( diff --git a/ipalib/tests/test_frontend.py b/ipalib/tests/test_frontend.py index 38c35506..3f993223 100644 --- a/ipalib/tests/test_frontend.py +++ b/ipalib/tests/test_frontend.py @@ -952,39 +952,27 @@ class test_Attribute(ClassChecker): class test_Method(ClassChecker): """ - Tests the `frontend.Method` class. + Test the `frontend.Method` class. """ _cls = frontend.Method def test_class(self): assert self.cls.__bases__ == (frontend.Attribute, frontend.Command) assert self.cls.implements(frontend.Command) + assert self.cls.implements(frontend.Attribute) - def get_subcls(self): - class example_obj(object): - params = plugable.NameSpace( - frontend.create_param(n) for n in ('prop0', 'prop1') - ) - type_ = ipa_types.Unicode() - class noun_verb(self.cls): - takes_options= ( - frontend.Param('option0', type_), - frontend.Param('option1', type_), - ) - obj = example_obj() - return noun_verb - - def test_get_options(self): + def test_init(self): """ - Tests the `frontend.Method.get_options` method. + Test the `frontend.Method.__init__` method. """ - sub = self.subcls() - names = ('option0', 'option1', 'prop0', 'prop1') - options = tuple(sub.get_options()) - assert len(options) == 4 - for (i, option) in enumerate(options): - assert option.name == names[i] - assert isinstance(option, frontend.Param) + class user_add(self.cls): + pass + o = user_add() + assert o.name == 'user_add' + assert o.obj_name == 'user' + assert o.attr_name == 'add' + assert frontend.Command.implemented_by(o) + assert frontend.Attribute.implemented_by(o) class test_Property(ClassChecker): -- cgit From 023f612921b4d9cbd15e3148d09c02932a61d73e Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 25 Sep 2008 02:13:16 +0000 Subject: 361: Implemented crud.Add.get_options() method; added corresponding unit tests --- ipalib/crud.py | 4 +++- ipalib/frontend.py | 2 +- ipalib/plugable.py | 4 ++-- ipalib/tests/test_crud.py | 32 +++++++++++++++++++++++++++++++- 4 files changed, 37 insertions(+), 5 deletions(-) (limited to 'ipalib') diff --git a/ipalib/crud.py b/ipalib/crud.py index 9f410fde..bdcf3047 100644 --- a/ipalib/crud.py +++ b/ipalib/crud.py @@ -25,7 +25,9 @@ import frontend, errors class Add(frontend.Method): - pass + def get_options(self): + assert 'params' in self.obj, list(self.obj) + return self.obj.params() class Get(frontend.Method): diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 5573e944..6cf9b5d7 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -513,7 +513,7 @@ class Object(plugable.Plugin): __public__ = frozenset(( 'methods', 'properties', - 'params' + 'params', 'primary_key', )) methods = None diff --git a/ipalib/plugable.py b/ipalib/plugable.py index e1d728d4..cc61cbe9 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -432,7 +432,7 @@ class PluginProxy(SetProxy): """ if key in self.__public__: return getattr(self.__target, key) - raise KeyError('no public attribute %r' % key) + raise KeyError('no public attribute %s.%s' % (self.name, key)) def __getattr__(self, name): """ @@ -441,7 +441,7 @@ class PluginProxy(SetProxy): """ if name in self.__public__: return getattr(self.__target, name) - raise AttributeError('no public attribute %r' % name) + raise AttributeError('no public attribute %s.%s' % (self.name, name)) def __call__(self, *args, **kw): """ diff --git a/ipalib/tests/test_crud.py b/ipalib/tests/test_crud.py index d708d808..41eb8834 100644 --- a/ipalib/tests/test_crud.py +++ b/ipalib/tests/test_crud.py @@ -22,7 +22,23 @@ Unit tests for `ipalib.crud` module. """ from tstutil import read_only, raises, ClassChecker -from ipalib import crud, frontend +from ipalib import crud, frontend, plugable + +def get_api(): + api = plugable.API( + frontend.Object, + frontend.Method, + frontend.Property, + ) + class user(frontend.Object): + takes_params = ( + 'givenname', + 'sn', + frontend.Param('uid', primary_key=True), + 'initials', + ) + api.register(user) + return api class test_Add(ClassChecker): @@ -35,6 +51,20 @@ class test_Add(ClassChecker): def test_class(self): assert self.cls.__bases__ == (frontend.Method,) + def test_get_options(self): + """ + Test the `crud.Add.get_options` method. + """ + api = get_api() + class user_add(self.cls): + pass + api.register(user_add) + api.finalize() + assert list(api.Method.user_add.args) == [] + assert list(api.Method.user_add.options) == \ + ['givenname', 'sn', 'uid', 'initials'] + + class test_Get(ClassChecker): """ -- cgit From c303a06a948d1813336161e2546bd85f8edead56 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 25 Sep 2008 03:14:12 +0000 Subject: 362: Implemented Get.get_args() and Del.get_args(); added corresponding unit tests --- ipalib/crud.py | 6 ++++-- ipalib/tests/test_crud.py | 29 ++++++++++++++++++++++++++--- 2 files changed, 30 insertions(+), 5 deletions(-) (limited to 'ipalib') diff --git a/ipalib/crud.py b/ipalib/crud.py index bdcf3047..d6d42494 100644 --- a/ipalib/crud.py +++ b/ipalib/crud.py @@ -31,11 +31,13 @@ class Add(frontend.Method): class Get(frontend.Method): - pass + def get_args(self): + yield self.obj.primary_key class Del(frontend.Method): - pass + def get_args(self): + yield self.obj.primary_key class Mod(frontend.Method): diff --git a/ipalib/tests/test_crud.py b/ipalib/tests/test_crud.py index 41eb8834..2cbf4b16 100644 --- a/ipalib/tests/test_crud.py +++ b/ipalib/tests/test_crud.py @@ -51,9 +51,9 @@ class test_Add(ClassChecker): def test_class(self): assert self.cls.__bases__ == (frontend.Method,) - def test_get_options(self): + def test_options_args(self): """ - Test the `crud.Add.get_options` method. + Test `crud.Add.get_args` and `crud.Add.get_options` methods. """ api = get_api() class user_add(self.cls): @@ -65,7 +65,6 @@ class test_Add(ClassChecker): ['givenname', 'sn', 'uid', 'initials'] - class test_Get(ClassChecker): """ Test the `crud.Get` class. @@ -76,6 +75,18 @@ class test_Get(ClassChecker): def test_class(self): assert self.cls.__bases__ == (frontend.Method,) + def test_options_args(self): + """ + Test `crud.Get.get_args` and `crud.Get.get_options` methods. + """ + api = get_api() + class user_get(self.cls): + pass + api.register(user_get) + api.finalize() + assert list(api.Method.user_get.args) == ['uid'] + assert list(api.Method.user_get.options) == [] + class test_Del(ClassChecker): """ @@ -87,6 +98,18 @@ class test_Del(ClassChecker): def test_class(self): assert self.cls.__bases__ == (frontend.Method,) + def test_options_args(self): + """ + Test `crud.Del.get_args` and `crud.Del.get_options` methods. + """ + api = get_api() + class user_del(self.cls): + pass + api.register(user_del) + api.finalize() + assert list(api.Method.user_del.args) == ['uid'] + assert list(api.Method.user_del.options) == [] + class test_Mod(ClassChecker): """ -- cgit From 152f3089e15eec0ce9f7af07450785114a3fcb6e Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 25 Sep 2008 03:27:40 +0000 Subject: 363: Added Object.params_minus_pk instance attribute --- ipalib/frontend.py | 5 +++++ ipalib/tests/test_frontend.py | 5 +++++ 2 files changed, 10 insertions(+) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 6cf9b5d7..c95397aa 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -515,11 +515,13 @@ class Object(plugable.Plugin): 'properties', 'params', 'primary_key', + 'params_minus_pk', )) methods = None properties = None params = None primary_key = None + params_minus_pk = None takes_params = tuple() def set_api(self, api): @@ -543,6 +545,9 @@ class Object(plugable.Plugin): ) if len(pkeys) == 1: self.primary_key = pkeys[0] + self.params_minus_pk = plugable.NameSpace( + filter(lambda p: not p.primary_key, self.params()), sort=False + ) def __get_attrs(self, name): namespace = getattr(self.api, name) diff --git a/ipalib/tests/test_frontend.py b/ipalib/tests/test_frontend.py index 3f993223..1c36d7bd 100644 --- a/ipalib/tests/test_frontend.py +++ b/ipalib/tests/test_frontend.py @@ -763,6 +763,7 @@ class test_Object(ClassChecker): assert self.cls.methods is None assert self.cls.properties is None assert self.cls.params is None + assert self.cls.params_minus_pk is None assert self.cls.takes_params == tuple() def test_init(self): @@ -773,6 +774,7 @@ class test_Object(ClassChecker): assert o.methods is None assert o.properties is None assert o.params is None + assert o.params_minus_pk is None assert o.properties is None def test_set_api(self): @@ -877,6 +879,7 @@ class test_Object(ClassChecker): o = example1() o.set_api(api) assert o.primary_key is None + assert o.params_minus_pk is None # Test with 1 primary key: class example2(self.cls): @@ -895,6 +898,8 @@ class test_Object(ClassChecker): assert pk.name == 'three' assert pk.primary_key is True assert o.params[2] is o.primary_key + assert isinstance(o.params_minus_pk, plugable.NameSpace) + assert list(o.params_minus_pk) == ['one', 'two', 'four'] # Test with multiple primary_key: class example3(self.cls): -- cgit From ddbe3ae934020fc858f6834a923222c465eba22c Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 25 Sep 2008 03:42:38 +0000 Subject: 364: Implemented Mod.get_args, Mod.get_options(); added corresponding unit tests --- ipalib/crud.py | 9 +++++++-- ipalib/tests/test_crud.py | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) (limited to 'ipalib') diff --git a/ipalib/crud.py b/ipalib/crud.py index d6d42494..40b6bd78 100644 --- a/ipalib/crud.py +++ b/ipalib/crud.py @@ -26,7 +26,6 @@ import frontend, errors class Add(frontend.Method): def get_options(self): - assert 'params' in self.obj, list(self.obj) return self.obj.params() @@ -41,7 +40,13 @@ class Del(frontend.Method): class Mod(frontend.Method): - pass + def get_args(self): + yield self.obj.primary_key + + def get_options(self): + for param in self.obj.params_minus_pk(): + yield param.__clone__(required=False) + class Find(frontend.Method): diff --git a/ipalib/tests/test_crud.py b/ipalib/tests/test_crud.py index 2cbf4b16..3fcef2c7 100644 --- a/ipalib/tests/test_crud.py +++ b/ipalib/tests/test_crud.py @@ -63,6 +63,8 @@ class test_Add(ClassChecker): assert list(api.Method.user_add.args) == [] assert list(api.Method.user_add.options) == \ ['givenname', 'sn', 'uid', 'initials'] + for param in api.Method.user_add.options(): + assert param.required is True class test_Get(ClassChecker): @@ -121,6 +123,22 @@ class test_Mod(ClassChecker): def test_class(self): assert self.cls.__bases__ == (frontend.Method,) + def test_options_args(self): + """ + Test `crud.Mod.get_args` and `crud.Mod.get_options` methods. + """ + api = get_api() + class user_mod(self.cls): + pass + api.register(user_mod) + api.finalize() + assert list(api.Method.user_mod.args) == ['uid'] + assert api.Method.user_mod.args[0].required is True + assert list(api.Method.user_mod.options) == \ + ['givenname', 'sn', 'initials'] + for param in api.Method.user_mod.options(): + assert param.required is False + class test_Find(ClassChecker): """ -- cgit From 55ba8e9d0b86bf25a2cbb7a3a603d796e7a2be2b Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 25 Sep 2008 03:47:22 +0000 Subject: 365: Implemented find.get_args(), find.get_options(); added corresponding unit tests --- ipalib/crud.py | 8 ++++++-- ipalib/tests/test_crud.py | 16 ++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) (limited to 'ipalib') diff --git a/ipalib/crud.py b/ipalib/crud.py index 40b6bd78..5021d06d 100644 --- a/ipalib/crud.py +++ b/ipalib/crud.py @@ -48,6 +48,10 @@ class Mod(frontend.Method): yield param.__clone__(required=False) - class Find(frontend.Method): - pass + def get_args(self): + yield self.obj.primary_key + + def get_options(self): + for param in self.obj.params_minus_pk(): + yield param.__clone__(required=False) diff --git a/ipalib/tests/test_crud.py b/ipalib/tests/test_crud.py index 3fcef2c7..8b1e8a86 100644 --- a/ipalib/tests/test_crud.py +++ b/ipalib/tests/test_crud.py @@ -149,3 +149,19 @@ class test_Find(ClassChecker): def test_class(self): assert self.cls.__bases__ == (frontend.Method,) + + def test_options_args(self): + """ + Test `crud.Find.get_args` and `crud.Find.get_options` methods. + """ + api = get_api() + class user_find(self.cls): + pass + api.register(user_find) + api.finalize() + assert list(api.Method.user_find.args) == ['uid'] + assert api.Method.user_find.args[0].required is True + assert list(api.Method.user_find.options) == \ + ['givenname', 'sn', 'initials'] + for param in api.Method.user_find.options(): + assert param.required is False -- cgit From ac88500382084d3c24a73c15c5fcfe02660383f7 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 25 Sep 2008 03:58:42 +0000 Subject: 366: Ported user_* example Commands to crud base classes; added user_show example command --- ipalib/plugins/example.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugins/example.py b/ipalib/plugins/example.py index 36af33cd..7576d2a1 100644 --- a/ipalib/plugins/example.py +++ b/ipalib/plugins/example.py @@ -23,6 +23,7 @@ Some example plugins. from ipalib import frontend +from ipalib import crud from ipalib.frontend import Param from ipalib import api @@ -58,22 +59,26 @@ api.register(discover) # Register some methods for the 'user' object: -class user_add(frontend.Method): +class user_add(crud.Add): 'Add a new user.' api.register(user_add) -class user_del(frontend.Method): +class user_del(crud.Del): 'Delete an existing user.' api.register(user_del) -class user_mod(frontend.Method): +class user_mod(crud.Mod): 'Edit an existing user.' api.register(user_mod) -class user_find(frontend.Method): +class user_find(crud.Find): 'Search the users.' api.register(user_find) +class user_show(crud.Get): + 'Examine an existing user.' +api.register(user_show) + # Register some properties for the 'user' object: #class user_givenname(frontend.Property): -- cgit From 0c3ebe0befa780485e108bfd85d05fbf6a7bc8e9 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 25 Sep 2008 23:21:41 +0000 Subject: 367: Implementing basics of loading plugins out of tree --- ipalib/load_plugins.py | 45 ++++++++++++++++++++++++++++++++++++++++++++- ipalib/plugins/__init__.py | 3 --- 2 files changed, 44 insertions(+), 4 deletions(-) (limited to 'ipalib') diff --git a/ipalib/load_plugins.py b/ipalib/load_plugins.py index 7863a24b..4352ac65 100644 --- a/ipalib/load_plugins.py +++ b/ipalib/load_plugins.py @@ -27,4 +27,47 @@ Eventually this will also load the out-of tree plugins, but for now it just loads the internal plugins. """ -import plugins +import os +from os import path +import imp +import inspect + + +def load_plugins(src_dir): + """ + Import each Python module found in ``src_dir``. + """ + if not (path.abspath(src_dir) == src_dir and path.isdir(src_dir)): + return + if path.islink(src_dir): + return + suffix = '.py' + for name in sorted(os.listdir(src_dir)): + if not name.endswith(suffix): + continue + py_file = path.join(src_dir, name) + if path.islink(py_file) or not path.isfile(py_file): + continue + module = name[:-len(suffix)] + if module == '__init__': + continue + imp.load_module(module, *imp.find_module(module, [src_dir])) + + +def load_plugins_subpackage(file_in_package): + """ + Load all Python modules found in a plugins/ subpackage. + """ + package_dir = path.dirname(path.abspath(file_in_package)) + plugins_dir = path.join(package_dir, 'plugins') + load_plugins(plugins_dir) + + +load_plugins_subpackage(__file__) +try: + import ipa_server + load_plugins_subpackage(ipa_server.__file__) +except ImportError: + pass + +load_plugins(path.expanduser('~/.freeipa')) diff --git a/ipalib/plugins/__init__.py b/ipalib/plugins/__init__.py index 91b56733..58db94ca 100644 --- a/ipalib/plugins/__init__.py +++ b/ipalib/plugins/__init__.py @@ -20,6 +20,3 @@ """ Sub-package containing all internal plugins. """ - -import example -import override -- cgit From aa45ec616a0c49a9cedd32fb24aa4a56f69a6586 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 26 Sep 2008 02:43:11 +0000 Subject: 369: Added Object.backend attribute used to associated it with a particular backend component --- ipalib/frontend.py | 12 +++++++++++- ipalib/tests/test_frontend.py | 38 ++++++++++++++++++++++++++++++++------ 2 files changed, 43 insertions(+), 7 deletions(-) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index c95397aa..da04cd7a 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -511,17 +511,22 @@ class Command(plugable.Plugin): class Object(plugable.Plugin): __public__ = frozenset(( + 'backend', 'methods', 'properties', 'params', 'primary_key', 'params_minus_pk', )) + backend = None methods = None properties = None params = None primary_key = None params_minus_pk = None + + # Can override in subclasses: + backend_name = None takes_params = tuple() def set_api(self, api): @@ -549,8 +554,13 @@ class Object(plugable.Plugin): filter(lambda p: not p.primary_key, self.params()), sort=False ) + if 'Backend' in self.api and self.backend_name in self.api.Backend: + self.backend = self.api.Backend[self.backend_name] + def __get_attrs(self, name): - namespace = getattr(self.api, name) + if name not in self.api: + return + namespace = self.api[name] assert type(namespace) is plugable.NameSpace for proxy in namespace(): # Equivalent to dict.itervalues() if proxy.obj_name == self.name: diff --git a/ipalib/tests/test_frontend.py b/ipalib/tests/test_frontend.py index 1c36d7bd..d7279c9b 100644 --- a/ipalib/tests/test_frontend.py +++ b/ipalib/tests/test_frontend.py @@ -23,7 +23,7 @@ Unit tests for `ipalib.frontend` module. from tstutil import raises, getitem, no_set, no_del, read_only, ClassChecker from tstutil import check_TypeError -from ipalib import frontend, plugable, errors, ipa_types +from ipalib import frontend, backend, plugable, errors, ipa_types def test_RULE_FLAG(): @@ -760,6 +760,7 @@ class test_Object(ClassChecker): def test_class(self): assert self.cls.__bases__ == (plugable.Plugin,) + assert self.cls.backend is None assert self.cls.methods is None assert self.cls.properties is None assert self.cls.params is None @@ -771,6 +772,7 @@ class test_Object(ClassChecker): Test the `frontend.Object.__init__` method. """ o = self.cls() + assert o.backend is None assert o.methods is None assert o.properties is None assert o.params is None @@ -810,13 +812,16 @@ class test_Object(ClassChecker): properties='property_%d', ) - class api(object): - Method = plugable.NameSpace( + + _d = dict( + Method=plugable.NameSpace( get_attributes(cnt, formats['methods']) - ) - Property = plugable.NameSpace( + ), + Property=plugable.NameSpace( get_attributes(cnt, formats['properties']) - ) + ), + ) + api = plugable.MagicDict(_d) assert len(api.Method) == cnt * 3 assert len(api.Property) == cnt * 3 @@ -914,6 +919,27 @@ class test_Object(ClassChecker): assert str(e) == \ 'example3 (Object) has multiple primary keys: one, two, four' + def test_backend(self): + """ + Test the `frontend.Object.backend` attribute. + """ + api = plugable.API( + frontend.Object, + frontend.Method, + frontend.Property, + backend.Backend, + ) + class ldap(backend.Backend): + whatever = 'It worked!' + api.register(ldap) + class user(frontend.Object): + backend_name = 'ldap' + api.register(user) + api.finalize() + b = api.Object.user.backend + assert isinstance(b, ldap) + assert b.whatever == 'It worked!' + class test_Attribute(ClassChecker): """ -- cgit From 7bbd81d83171c4711a78616688349622ac309b0b Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 26 Sep 2008 22:52:15 +0000 Subject: 370: Added detailed examples to decstring for DefaultFrom class --- ipalib/frontend.py | 76 +++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 66 insertions(+), 10 deletions(-) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index da04cd7a..afc02066 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -43,23 +43,79 @@ def is_rule(obj): class DefaultFrom(plugable.ReadOnly): """ - Derives a default for one value using other supplied values. + Derive a default value from other supplied values. - Here is an example that constructs a user's initials from his first - and last name: + For example, say you wanted to create a default for the user's login from + the user's first and last names. It could be implemented like this: - >>> df = DefaultFrom(lambda f, l: f[0] + l[0], 'first', 'last') - >>> df(first='John', last='Doe') # Both keys - 'JD' - >>> df() is None # Returns None if any key is missing + >>> login = DefaultFrom(lambda first, last: first[0] + last) + >>> login(first='John', last='Doe') + 'JDoe' + + If you do not explicitly provide keys when you create a DefaultFrom + instance, the keys are implicitly derived from your callback by + inspecting ``callback.func_code.co_varnames``. The keys are available + through the ``DefaultFrom.keys`` instance attribute, like this: + + >>> login.keys + ('first', 'last') + + The callback is available through the ``DefaultFrom.callback`` instance + attribute, like this: + + >>> login.callback + at 0x7fdd225cd7d0> + >>> login.callback.func_code.co_varnames # The keys + ('first', 'last') + + The keys can be explicitly provided as optional positional arguments after + the callback. For example, this is equivalent to the ``login`` instance + above: + + >>> login2 = DefaultFrom(lambda a, b: a[0] + b, 'first', 'last') + >>> login2.keys + ('first', 'last') + >>> login2.callback.func_code.co_varnames # Not the keys + ('a', 'b') + >>> login2(first='John', last='Doe') + 'JDoe' + + If any keys are missing when calling your DefaultFrom instance, your + callback is not called and None is returned. For example: + + >>> login(first='John', lastname='Doe') is None True - >>> df(first='John', middle='Q') is None # Still returns None + >>> login() is None True + + Any additional keys are simply ignored, like this: + + >>> login(last='Doe', first='John', middle='Whatever') + 'JDoe' + + As above, because `DefaultFrom.__call__` takes only pure keyword + arguments, they can be supplied in any order. + + Of course, the callback need not be a lambda expression. This third + example is equivalent to both the ``login`` and ``login2`` instances + above: + + >>> def get_login(first, last): + ... return first[0] + last + ... + >>> login3 = DefaultFrom(get_login) + >>> login3.keys + ('first', 'last') + >>> login3.callback.func_code.co_varnames + ('first', 'last') + >>> login3(first='John', last='Doe') + 'JDoe' """ + def __init__(self, callback, *keys): """ - :param callback: The callable to call when all ``keys`` are present. - :param keys: The keys used to map from keyword to position arguments. + :param callback: The callable to call when all keys are present. + :param keys: Optional keys used for source values. """ if not callable(callback): raise TypeError('callback must be callable; got %r' % callback) -- cgit From 031daabcc4bb023ff54bd76dd1418bbe3bcff022 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 26 Sep 2008 23:41:51 +0000 Subject: 371: Added examples to parse_param_spec() docstring and changed syntax guide into a reStructuredText table --- ipalib/frontend.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index afc02066..5fd27116 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -152,10 +152,25 @@ def parse_param_spec(spec): required, and whether the param is multivalue according the following syntax: - name => required=True, multivalue=False - name? => required=False, multivalue=False - name+ => required=True, multivalue=True - name* => required=False, multivalue=True + ====== ===== ======== ========== + Spec Name Required Multivalue + ====== ===== ======== ========== + 'var' 'var' True False + 'var?' 'var' False False + 'var*' 'var' False True + 'var+' 'var' True True + ====== ===== ======== ========== + + For example, + + >>> parse_param_spec('login') + ('login', {'required': True, 'multivalue': False}) + >>> parse_param_spec('gecos?') + ('gecos', {'required': False, 'multivalue': False}) + >>> parse_param_spec('telephone_numbers*') + ('telephone_numbers', {'required': False, 'multivalue': True}) + >>> parse_param_spec('group+') + ('group', {'required': True, 'multivalue': True}) :param spec: A spec string. """ -- cgit From 8901b9a8379c37e6243a24eec9648afa05638785 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sat, 27 Sep 2008 00:31:59 +0000 Subject: 372: Started work on docstring for Param class --- ipalib/frontend.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 5fd27116..77518a96 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -194,17 +194,30 @@ def parse_param_spec(spec): class Param(plugable.ReadOnly): """ A parameter accepted by a `Command`. + + ============ ================= ================== + Keyword Type Default + ============ ================= ================== + type ipa_type.Type ipa_type.Unicode() + doc str '' + required bool True + multivalue bool False + primary_key bool False + normalize callable None + default same as type.type None + default_from callable None + ============ ================= ================== """ __nones = (None, '', tuple(), []) __defaults = dict( doc='', required=True, multivalue=False, + primary_key=False, + normalize=None, default=None, default_from=None, rules=tuple(), - normalize=None, - primary_key=False, ) def __init__(self, name, type_=ipa_types.Unicode(), **override): -- cgit From d77907d2d0ecc33ef4ee4121e10cfef385172b0d Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sat, 27 Sep 2008 01:30:39 +0000 Subject: 373: Replaced type_ optional arg to Param.__init__() with pure kw arg type; updated unit tests and related code --- ipalib/frontend.py | 12 +++++++----- ipalib/tests/test_frontend.py | 42 ++++++++++++++++++++---------------------- 2 files changed, 27 insertions(+), 27 deletions(-) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 77518a96..289f9eec 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -210,6 +210,7 @@ class Param(plugable.ReadOnly): """ __nones = (None, '', tuple(), []) __defaults = dict( + type=ipa_types.Unicode(), doc='', required=True, multivalue=False, @@ -220,7 +221,7 @@ class Param(plugable.ReadOnly): rules=tuple(), ) - def __init__(self, name, type_=ipa_types.Unicode(), **override): + def __init__(self, name, **override): if not ('required' in override or 'multivalue' in override): (name, kw_from_spec) = parse_param_spec(name) override.update(kw_from_spec) @@ -233,7 +234,7 @@ class Param(plugable.ReadOnly): kw.update(override) self.__kw = kw self.name = check_name(name) - self.type = check_isinstance(type_, ipa_types.Type, 'type_') + self.type = self.__check_isinstance(ipa_types.Type, 'type') self.doc = self.__check_type(str, 'doc') self.required = self.__check_type(bool, 'required') self.multivalue = self.__check_type(bool, 'multivalue') @@ -246,7 +247,7 @@ class Param(plugable.ReadOnly): ) self.__normalize = kw['normalize'] self.rules = self.__check_type(tuple, 'rules') - self.all_rules = (type_.validate,) + self.rules + self.all_rules = (self.type.validate,) + self.rules self.primary_key = self.__check_type(bool, 'primary_key') lock(self) @@ -256,7 +257,7 @@ class Param(plugable.ReadOnly): """ kw = dict(self.__kw) kw.update(override) - return self.__class__(self.name, self.type, **kw) + return self.__class__(self.name, **kw) def __check_type(self, type_, name, allow_none=False): value = self.__kw[name] @@ -737,7 +738,8 @@ class Property(Attribute): self.__rules_iter(), key=lambda f: getattr(f, '__name__'), )) - self.param = Param(self.attr_name, self.type, + self.param = Param(self.attr_name, + type=self.type, doc=self.doc, required=self.required, multivalue=self.multivalue, diff --git a/ipalib/tests/test_frontend.py b/ipalib/tests/test_frontend.py index d7279c9b..f0ada896 100644 --- a/ipalib/tests/test_frontend.py +++ b/ipalib/tests/test_frontend.py @@ -144,23 +144,24 @@ class test_Param(ClassChecker): Test the `frontend.Param.__init__` method. """ name = 'sn' - type_ = ipa_types.Unicode() - o = self.cls(name, type_) + o = self.cls(name) assert o.__islocked__() is True # Test default values assert read_only(o, 'name') is name - assert read_only(o, 'type') is type_ + assert isinstance(read_only(o, 'type'), ipa_types.Unicode) assert read_only(o, 'doc') == '' assert read_only(o, 'required') is True assert read_only(o, 'multivalue') is False assert read_only(o, 'default') is None assert read_only(o, 'default_from') is None assert read_only(o, 'rules') == tuple() - assert read_only(o, 'all_rules') == (type_.validate,) + assert len(read_only(o, 'all_rules')) == 1 assert read_only(o, 'primary_key') is False # Test all kw args: + t = ipa_types.Int() + assert self.cls(name, type=t).type is t assert self.cls(name, doc='the doc').doc == 'the doc' assert self.cls(name, required=False).required is False assert self.cls(name, multivalue=True).multivalue is True @@ -220,7 +221,7 @@ class test_Param(ClassChecker): ) name = 'hair_color?' type_ = ipa_types.Int() - o = self.cls(name, type_) + o = self.cls(name, type=type_) compare(o, default) override = dict(multivalue=True, default=42) @@ -242,7 +243,7 @@ class test_Param(ClassChecker): none = (None, '', u'', tuple(), []) # Scenario 1: multivalue=False - o = self.cls(name, type_) + o = self.cls(name, type=type_) for n in none: assert o.convert(n) is None for value in okay: @@ -257,7 +258,7 @@ class test_Param(ClassChecker): assert e.index is None # Scenario 2: multivalue=True - o = self.cls(name, type_, multivalue=True) + o = self.cls(name, type=type_, multivalue=True) for n in none: assert o.convert(n) is None for value in okay: @@ -281,19 +282,18 @@ class test_Param(ClassChecker): Test the `frontend.Param.normalize` method. """ name = 'sn' - t = ipa_types.Unicode() callback = lambda value: value.lower() values = (None, u'Hello', (u'Hello',), 'hello', ['hello']) none = (None, '', u'', tuple(), []) # Scenario 1: multivalue=False, normalize=None - o = self.cls(name, t) + o = self.cls(name) for v in values: # When normalize=None, value is returned, no type checking: assert o.normalize(v) is v # Scenario 2: multivalue=False, normalize=callback - o = self.cls(name, t, normalize=callback) + o = self.cls(name, normalize=callback) for v in (u'Hello', u'hello', 'Hello'): # Okay assert o.normalize(v) == 'hello' for v in [None, 42, (u'Hello',)]: # Not basestring @@ -302,13 +302,13 @@ class test_Param(ClassChecker): assert o.normalize(n) is None # Scenario 3: multivalue=True, normalize=None - o = self.cls(name, t, multivalue=True) + o = self.cls(name, multivalue=True) for v in values: # When normalize=None, value is returned, no type checking: assert o.normalize(v) is v # Scenario 4: multivalue=True, normalize=callback - o = self.cls(name, t, multivalue=True, normalize=callback) + o = self.cls(name, multivalue=True, normalize=callback) assert o.normalize([]) is None assert o.normalize(tuple()) is None for value in [(u'Hello',), (u'hello',), 'Hello', ['Hello']]: # Okay @@ -334,7 +334,7 @@ class test_Param(ClassChecker): fail_type = 'whatever' # Scenario 1: multivalue=False - o = self.cls(name, type_, rules=my_rules) + o = self.cls(name, type=type_, rules=my_rules) assert o.rules == my_rules assert o.all_rules == (type_.validate, case_rule) o.validate(okay) @@ -347,7 +347,7 @@ class test_Param(ClassChecker): check_TypeError(fail_type, unicode, 'value', o.validate, fail_type) ## Scenario 2: multivalue=True - o = self.cls(name, type_, multivalue=True, rules=my_rules) + o = self.cls(name, type=type_, multivalue=True, rules=my_rules) o.validate((okay,)) cnt = 5 for i in xrange(cnt): @@ -370,7 +370,6 @@ class test_Param(ClassChecker): Tests the `frontend.Param.get_default` method. """ name = 'greeting' - type_ = ipa_types.Unicode() default = u'Hello, world!' default_from = frontend.DefaultFrom( lambda first, last: u'Hello, %s %s!' % (first, last), @@ -378,7 +377,7 @@ class test_Param(ClassChecker): ) # Scenario 1: multivalue=False - o = self.cls(name, type_, + o = self.cls(name, default=default, default_from=default_from, ) @@ -389,7 +388,7 @@ class test_Param(ClassChecker): # Scenario 2: multivalue=True default = (default,) - o = self.cls(name, type_, + o = self.cls(name, default=default, default_from=default_from, multivalue=True, @@ -405,9 +404,9 @@ class test_Param(ClassChecker): """ name = 'status' values = (u'Active', u'Inactive') - o = self.cls(name, ipa_types.Unicode()) + o = self.cls(name, type=ipa_types.Unicode()) assert o.get_values() == tuple() - o = self.cls(name, ipa_types.Enum(*values)) + o = self.cls(name, type=ipa_types.Enum(*values)) assert o.get_values() == values @@ -456,16 +455,15 @@ class test_Command(ClassChecker): 'default_from' ) normalize = lambda value: value.lower() - type_ = ipa_types.Unicode() class example(self.cls): takes_options = ( - frontend.Param('option0', type_, + frontend.Param('option0', normalize=normalize, default_from=default_from, rules=(Rule('option0'),) ), - frontend.Param('option1', type_, + frontend.Param('option1', normalize=normalize, default_from=default_from, rules=(Rule('option1'),), -- cgit From afdc72103847fc27efd00f8cc97a7320909ff6a0 Mon Sep 17 00:00:00 2001 From: Martin Nagy Date: Mon, 29 Sep 2008 17:41:30 +0200 Subject: Add support for environment variables, change tests accordingly --- ipalib/__init__.py | 2 ++ ipalib/cli.py | 4 +++ ipalib/config.py | 84 +++++++++++++++++++++++++++++++++++++++++++ ipalib/frontend.py | 2 +- ipalib/plugable.py | 37 +++++++++++++++---- ipalib/plugins/example.py | 17 +++++++++ ipalib/tests/test_crud.py | 3 +- ipalib/tests/test_frontend.py | 8 +++-- ipalib/tests/test_plugable.py | 4 +-- 9 files changed, 148 insertions(+), 13 deletions(-) create mode 100644 ipalib/config.py (limited to 'ipalib') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index 6c129a41..f0d43aad 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -58,8 +58,10 @@ True import plugable import frontend import backend +import config api = plugable.API( + config.default_environment(), frontend.Command, frontend.Object, frontend.Method, diff --git a/ipalib/cli.py b/ipalib/cli.py index 8918206f..1a08cef4 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -30,6 +30,7 @@ import errors import plugable import ipa_types +from ipalib import config def exit_error(error): sys.exit('ipa: ERROR: %s' % error) @@ -256,6 +257,9 @@ class CLI(object): self.print_commands() print 'Usage: ipa COMMAND' sys.exit(2) + # do parsing here, read the conf + conf_dict = config.read_config(self.api.env.conf) + self.api.env.update(conf_dict) key = sys.argv[1] if key not in self: self.print_commands() diff --git a/ipalib/config.py b/ipalib/config.py new file mode 100644 index 00000000..bb345661 --- /dev/null +++ b/ipalib/config.py @@ -0,0 +1,84 @@ +# Authors: +# Martin Nagy +# +# 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 + + +def default_environment(): + default = dict( + conf = '/etc/ipa/ipa.conf', + server_context = True, + query_dns = True, + verbose = False, + servers = LazyIter(myservers), + realm = LazyProp(myrealm), + domain = LazyProp(mydomain), + ) + return default + + +class LazyProp(object): + def __init__(self, func, value=None): + self._func = func + self._value = value + + def set_value(self, value): + self._value = value + + def get_value(self): + if self._value is None: + return self._func() + else: + return self._value + + +# FIXME: make sure to eliminate duplicates +class LazyIter(LazyProp): + def get_value(self): + if self._value is not None: + if type(self._value) is tuple: + for item in self._value: + yield item + else: + yield self._value + for item in self._func(): + yield item + + +def read_config(file): + assert isinstance(file, basestring) + # open the file and read configuration, return a dict + # for now, these are here just for testing purposes + return dict(servers="server.ipatest.com", realm="IPATEST.COM") + + +# these functions are here just to "emulate" dns resolving for now +def mydomain(): + return "ipatest.com" + + +def myrealm(): + return "IPATEST.COM" + + +def myservers(): + # print is here to demonstrate that the querying will occur only when it is + # really needed + print "Querying DNS" + yield "server.ipatest.com" + yield "backup.ipatest.com" + yield "fake.ipatest.com" diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 289f9eec..30f5942b 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -559,7 +559,7 @@ class Command(plugable.Plugin): return self.run(*args, **kw) def run(self, *args, **kw): - if self.api.env.in_server_context: + if self.api.env.server_context: target = self.execute else: target = self.forward diff --git a/ipalib/plugable.py b/ipalib/plugable.py index cc61cbe9..98a74dfa 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -692,20 +692,45 @@ class Registrar(DictProxy): self.__registered.add(klass) +class Environment(dict): + def __getitem__(self, key): + val = super(Environment, self).__getitem__(key) + if hasattr(val, 'get_value'): + return val.get_value() + else: + return val + + def __setitem__(self, key, value): + if key in self: + super_value = super(Environment, self).__getitem__(key) + + if key in self and hasattr(super_value, 'set_value'): + super_value.set_value(value) + else: + super(Environment, self).__setitem__(key, value) + + def __getattr__(self, name): + return self[name] + + def __setattr__(self, name, value): + self[name] = value + + def update(self, d): + assert isinstance(d, dict) + for key, value in d.iteritems(): + self[key] = value + + class API(DictProxy): """ Dynamic API object through which `Plugin` instances are accessed. """ __finalized = False - def __init__(self, *allowed, **kw): + def __init__(self, default_env, *allowed): self.__d = dict() self.register = Registrar(*allowed) - default = dict( - in_server_context=True, - ) - default.update(kw) - self.env = MagicDict(default) + self.env = Environment(default_env) super(API, self).__init__(self.__d) def finalize(self): diff --git a/ipalib/plugins/example.py b/ipalib/plugins/example.py index 7576d2a1..c565d678 100644 --- a/ipalib/plugins/example.py +++ b/ipalib/plugins/example.py @@ -57,6 +57,23 @@ class discover(frontend.Command): 'Discover IPA servers on network.' api.register(discover) +# Command to get the idea how plugins will interact with api.env +class envtest(frontend.Command): + 'Show current environment.' + def run(*args, **kw): + print "" + print "Environment variables:" + for var in api.env: + val = api.env[var] + if var is 'servers': + print "" + print " Servers:" + for item in api.env.servers: + print " %s" % item + print "" + else: + print " %s: %s" % (var, val) +api.register(envtest) # Register some methods for the 'user' object: class user_add(crud.Add): diff --git a/ipalib/tests/test_crud.py b/ipalib/tests/test_crud.py index 8b1e8a86..e95fe509 100644 --- a/ipalib/tests/test_crud.py +++ b/ipalib/tests/test_crud.py @@ -22,10 +22,11 @@ Unit tests for `ipalib.crud` module. """ from tstutil import read_only, raises, ClassChecker -from ipalib import crud, frontend, plugable +from ipalib import crud, frontend, plugable, config def get_api(): api = plugable.API( + config.default_environment(), frontend.Object, frontend.Method, frontend.Property, diff --git a/ipalib/tests/test_frontend.py b/ipalib/tests/test_frontend.py index f0ada896..e3dd04fa 100644 --- a/ipalib/tests/test_frontend.py +++ b/ipalib/tests/test_frontend.py @@ -23,7 +23,7 @@ Unit tests for `ipalib.frontend` module. from tstutil import raises, getitem, no_set, no_del, read_only, ClassChecker from tstutil import check_TypeError -from ipalib import frontend, backend, plugable, errors, ipa_types +from ipalib import frontend, backend, plugable, errors, ipa_types, config def test_RULE_FLAG(): @@ -732,7 +732,7 @@ class test_Command(ClassChecker): kw = dict(how_are='you', on_this='fine day?') # Test in server context: - api = plugable.API(self.cls, in_server_context=True) + api = plugable.API(dict(server_context=True), self.cls) api.finalize() o = my_cmd() o.set_api(api) @@ -741,7 +741,7 @@ class test_Command(ClassChecker): assert o.run.im_func is my_cmd.execute.im_func # Test in non-server context - api = plugable.API(self.cls, in_server_context=False) + api = plugable.API(dict(server_context=False), self.cls) api.finalize() o = my_cmd() o.set_api(api) @@ -868,6 +868,7 @@ class test_Object(ClassChecker): Test the `frontend.Object.primary_key` attribute. """ api = plugable.API( + config.default_environment(), frontend.Method, frontend.Property, ) @@ -922,6 +923,7 @@ class test_Object(ClassChecker): Test the `frontend.Object.backend` attribute. """ api = plugable.API( + config.default_environment(), frontend.Object, frontend.Method, frontend.Property, diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index 95d3825f..9be6b343 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -742,7 +742,7 @@ def test_API(): def method(self, n): return n + 1 - api = plugable.API(base0, base1) + api = plugable.API(dict(), base0, base1) r = api.register assert isinstance(r, plugable.Registrar) assert read_only(api, 'register') is r @@ -800,7 +800,7 @@ def test_API(): # Test with base class that doesn't request a proxy class NoProxy(plugable.Plugin): __proxy__ = False - api = plugable.API(NoProxy) + api = plugable.API(dict(), NoProxy) class plugin0(NoProxy): pass api.register(plugin0) -- cgit From b965e558b5def14c6416beb36dc790cca96c3724 Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Thu, 25 Sep 2008 23:53:53 -0400 Subject: Rebase XML-RPC client and server Fix error handling in server to return exceptions generated in library code --- ipalib/crud.py | 6 +++++- ipalib/plugins/example.py | 9 ++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) (limited to 'ipalib') diff --git a/ipalib/crud.py b/ipalib/crud.py index 5021d06d..813e0c81 100644 --- a/ipalib/crud.py +++ b/ipalib/crud.py @@ -25,8 +25,12 @@ import frontend, errors class Add(frontend.Method): + def get_args(self): + yield self.obj.primary_key + def get_options(self): - return self.obj.params() + for param in self.obj.params_minus_pk(): + yield param.__clone__(required=False) class Get(frontend.Method): diff --git a/ipalib/plugins/example.py b/ipalib/plugins/example.py index c565d678..c7d16160 100644 --- a/ipalib/plugins/example.py +++ b/ipalib/plugins/example.py @@ -26,7 +26,8 @@ from ipalib import frontend from ipalib import crud from ipalib.frontend import Param from ipalib import api - +from ipalib import servercore +import ldap class user(frontend.Object): 'User object' @@ -78,6 +79,8 @@ api.register(envtest) # Register some methods for the 'user' object: class user_add(crud.Add): 'Add a new user.' + def execute(self, *args, **kw): + return 1 api.register(user_add) class user_del(crud.Del): @@ -90,6 +93,10 @@ api.register(user_mod) class user_find(crud.Find): 'Search the users.' + def execute(self, *args, **kw): + uid=args[0] + result = servercore.get_sub_entry(servercore.basedn, "uid=%s" % uid, ["*"]) + return result api.register(user_find) class user_show(crud.Get): -- cgit From 77e6c99f9d8e34e85add7671d89bf7698a4fe5c2 Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Tue, 30 Sep 2008 00:48:53 -0400 Subject: Migrate to new source tree layoute --- ipalib/plugins/example.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugins/example.py b/ipalib/plugins/example.py index c7d16160..6113c117 100644 --- a/ipalib/plugins/example.py +++ b/ipalib/plugins/example.py @@ -21,12 +21,11 @@ Some example plugins. """ - from ipalib import frontend from ipalib import crud from ipalib.frontend import Param from ipalib import api -from ipalib import servercore +from ipa_server import servercore import ldap class user(frontend.Object): -- cgit From 7ee0ccd90d99609f8e85bf0e197ca5a747231fb8 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 30 Sep 2008 20:27:52 -0600 Subject: Fixed unit tests; changed example.py so it doesn't import servercore --- ipalib/crud.py | 2 +- ipalib/plugins/example.py | 2 -- ipalib/tests/test_crud.py | 4 ++-- 3 files changed, 3 insertions(+), 5 deletions(-) (limited to 'ipalib') diff --git a/ipalib/crud.py b/ipalib/crud.py index 813e0c81..9f9d727f 100644 --- a/ipalib/crud.py +++ b/ipalib/crud.py @@ -30,7 +30,7 @@ class Add(frontend.Method): def get_options(self): for param in self.obj.params_minus_pk(): - yield param.__clone__(required=False) + yield param class Get(frontend.Method): diff --git a/ipalib/plugins/example.py b/ipalib/plugins/example.py index 6113c117..e4d7dc10 100644 --- a/ipalib/plugins/example.py +++ b/ipalib/plugins/example.py @@ -25,8 +25,6 @@ from ipalib import frontend from ipalib import crud from ipalib.frontend import Param from ipalib import api -from ipa_server import servercore -import ldap class user(frontend.Object): 'User object' diff --git a/ipalib/tests/test_crud.py b/ipalib/tests/test_crud.py index e95fe509..df85253b 100644 --- a/ipalib/tests/test_crud.py +++ b/ipalib/tests/test_crud.py @@ -61,9 +61,9 @@ class test_Add(ClassChecker): pass api.register(user_add) api.finalize() - assert list(api.Method.user_add.args) == [] + assert list(api.Method.user_add.args) == ['uid'] assert list(api.Method.user_add.options) == \ - ['givenname', 'sn', 'uid', 'initials'] + ['givenname', 'sn', 'initials'] for param in api.Method.user_add.options(): assert param.required is True -- cgit From af6653f6074e43fd6db83111c2b5b55b5b9b56e0 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 1 Oct 2008 15:50:04 -0600 Subject: Added skeleton for kerberos backend --- ipalib/plugins/b_kerberos.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 ipalib/plugins/b_kerberos.py (limited to 'ipalib') diff --git a/ipalib/plugins/b_kerberos.py b/ipalib/plugins/b_kerberos.py new file mode 100644 index 00000000..4b3a9a5b --- /dev/null +++ b/ipalib/plugins/b_kerberos.py @@ -0,0 +1,34 @@ +# Authors: +# Jason Gerard DeRose +# +# 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 + +""" +Kerberos backend. + +This wraps the python-kerberos and python-krbV bindings. +""" + +from ipalib import api +from ipalib.backend import Backend + +class krb(Backend): + """ + Kerberos backend plugin. + """ + +api.register(krb) -- cgit From cc93e45e1309d90e1a47c9bc73c785ffe630edfb Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 1 Oct 2008 15:53:21 -0600 Subject: Removed depreciated override.py module --- ipalib/plugins/override.py | 33 --------------------------------- 1 file changed, 33 deletions(-) delete mode 100644 ipalib/plugins/override.py (limited to 'ipalib') diff --git a/ipalib/plugins/override.py b/ipalib/plugins/override.py deleted file mode 100644 index 29ec2509..00000000 --- a/ipalib/plugins/override.py +++ /dev/null @@ -1,33 +0,0 @@ -# Authors: -# Jason Gerard DeRose -# -# 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 - -""" -An example of overriding a plugin. - -This example depends upon the order that the plugins/ modules are imported -in plugins/__init__.py, which will likely change in the near future. -""" - -from ipalib import api - -if 'user_mod' in api.register.Method: - base = api.register.Method.user_mod - class user_mod(base): - 'Example override, see ipalib/plugins/override.py' - api.register(user_mod, override=True) -- cgit From e963be1dda58494a80198a8d8a1cec5f2c898ca2 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 1 Oct 2008 15:56:04 -0600 Subject: Renamed plugins/example.py to plugins/f_user.py --- ipalib/plugins/example.py | 178 ---------------------------------------------- ipalib/plugins/f_user.py | 178 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 178 insertions(+), 178 deletions(-) delete mode 100644 ipalib/plugins/example.py create mode 100644 ipalib/plugins/f_user.py (limited to 'ipalib') diff --git a/ipalib/plugins/example.py b/ipalib/plugins/example.py deleted file mode 100644 index e4d7dc10..00000000 --- a/ipalib/plugins/example.py +++ /dev/null @@ -1,178 +0,0 @@ -# Authors: -# Jason Gerard DeRose -# -# 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 - -""" -Some example plugins. -""" - -from ipalib import frontend -from ipalib import crud -from ipalib.frontend import Param -from ipalib import api - -class user(frontend.Object): - 'User object' - takes_params = ( - 'givenname', - 'sn', - Param('uid', - primary_key=True, - default_from=lambda givenname, sn: givenname[0] + sn, - normalize=lambda value: value.lower(), - ), - Param('krbprincipalname', - default_from=lambda uid: '%s@EXAMPLE.COM' % uid, - ), - Param('homedirectory', - default_from=lambda uid: '/home/%s' % uid, - ) - ) -api.register(user) - - -# Hypothetical functional commands (not associated with any object): -class krbtest(frontend.Command): - 'Test your Kerberos ticket.' -api.register(krbtest) - -class discover(frontend.Command): - 'Discover IPA servers on network.' -api.register(discover) - -# Command to get the idea how plugins will interact with api.env -class envtest(frontend.Command): - 'Show current environment.' - def run(*args, **kw): - print "" - print "Environment variables:" - for var in api.env: - val = api.env[var] - if var is 'servers': - print "" - print " Servers:" - for item in api.env.servers: - print " %s" % item - print "" - else: - print " %s: %s" % (var, val) -api.register(envtest) - -# Register some methods for the 'user' object: -class user_add(crud.Add): - 'Add a new user.' - def execute(self, *args, **kw): - return 1 -api.register(user_add) - -class user_del(crud.Del): - 'Delete an existing user.' -api.register(user_del) - -class user_mod(crud.Mod): - 'Edit an existing user.' -api.register(user_mod) - -class user_find(crud.Find): - 'Search the users.' - def execute(self, *args, **kw): - uid=args[0] - result = servercore.get_sub_entry(servercore.basedn, "uid=%s" % uid, ["*"]) - return result -api.register(user_find) - -class user_show(crud.Get): - 'Examine an existing user.' -api.register(user_show) - - -# Register some properties for the 'user' object: -#class user_givenname(frontend.Property): -# 'User first name' -# required = True -#api.register(user_givenname) - -#class user_sn(frontend.Property): -# 'User last name' -# required = True -#api.register(user_sn) - -#class user_login(frontend.Property): -# 'User login' -# required = True -# default_from = frontend.DefaultFrom( -# lambda first, last: (first[0] + last).lower(), -# 'givenname', 'sn' -# ) -#api.register(user_login) - -#class user_initials(frontend.Property): -# 'User initials' -# required = True -# default_from = frontend.DefaultFrom( -# lambda first, last: first[0] + last[0], -# 'givenname', 'sn' -# ) -#api.register(user_initials) - - -# Register some methods for the 'group' object: -class group_add(frontend.Method): - 'Add a new group.' -api.register(group_add) - -class group_del(frontend.Method): - 'Delete an existing group.' -api.register(group_del) - -class group_mod(frontend.Method): - 'Edit an existing group.' -api.register(group_mod) - -class group_find(frontend.Method): - 'Search the groups.' -api.register(group_find) - - -# Register some methods for the 'service' object -class service_add(frontend.Method): - 'Add a new service.' -api.register(service_add) - -class service_del(frontend.Method): - 'Delete an existing service.' -api.register(service_del) - -class service_mod(frontend.Method): - 'Edit an existing service.' -api.register(service_mod) - -class service_find(frontend.Method): - 'Search the services.' -api.register(service_find) - - -# And to emphasis that the registration order doesn't matter, -# we'll register the objects last: -class group(frontend.Object): - 'Group object' -api.register(group) - -class service(frontend.Object): - 'Service object' -api.register(service) diff --git a/ipalib/plugins/f_user.py b/ipalib/plugins/f_user.py new file mode 100644 index 00000000..e4d7dc10 --- /dev/null +++ b/ipalib/plugins/f_user.py @@ -0,0 +1,178 @@ +# Authors: +# Jason Gerard DeRose +# +# 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 + +""" +Some example plugins. +""" + +from ipalib import frontend +from ipalib import crud +from ipalib.frontend import Param +from ipalib import api + +class user(frontend.Object): + 'User object' + takes_params = ( + 'givenname', + 'sn', + Param('uid', + primary_key=True, + default_from=lambda givenname, sn: givenname[0] + sn, + normalize=lambda value: value.lower(), + ), + Param('krbprincipalname', + default_from=lambda uid: '%s@EXAMPLE.COM' % uid, + ), + Param('homedirectory', + default_from=lambda uid: '/home/%s' % uid, + ) + ) +api.register(user) + + +# Hypothetical functional commands (not associated with any object): +class krbtest(frontend.Command): + 'Test your Kerberos ticket.' +api.register(krbtest) + +class discover(frontend.Command): + 'Discover IPA servers on network.' +api.register(discover) + +# Command to get the idea how plugins will interact with api.env +class envtest(frontend.Command): + 'Show current environment.' + def run(*args, **kw): + print "" + print "Environment variables:" + for var in api.env: + val = api.env[var] + if var is 'servers': + print "" + print " Servers:" + for item in api.env.servers: + print " %s" % item + print "" + else: + print " %s: %s" % (var, val) +api.register(envtest) + +# Register some methods for the 'user' object: +class user_add(crud.Add): + 'Add a new user.' + def execute(self, *args, **kw): + return 1 +api.register(user_add) + +class user_del(crud.Del): + 'Delete an existing user.' +api.register(user_del) + +class user_mod(crud.Mod): + 'Edit an existing user.' +api.register(user_mod) + +class user_find(crud.Find): + 'Search the users.' + def execute(self, *args, **kw): + uid=args[0] + result = servercore.get_sub_entry(servercore.basedn, "uid=%s" % uid, ["*"]) + return result +api.register(user_find) + +class user_show(crud.Get): + 'Examine an existing user.' +api.register(user_show) + + +# Register some properties for the 'user' object: +#class user_givenname(frontend.Property): +# 'User first name' +# required = True +#api.register(user_givenname) + +#class user_sn(frontend.Property): +# 'User last name' +# required = True +#api.register(user_sn) + +#class user_login(frontend.Property): +# 'User login' +# required = True +# default_from = frontend.DefaultFrom( +# lambda first, last: (first[0] + last).lower(), +# 'givenname', 'sn' +# ) +#api.register(user_login) + +#class user_initials(frontend.Property): +# 'User initials' +# required = True +# default_from = frontend.DefaultFrom( +# lambda first, last: first[0] + last[0], +# 'givenname', 'sn' +# ) +#api.register(user_initials) + + +# Register some methods for the 'group' object: +class group_add(frontend.Method): + 'Add a new group.' +api.register(group_add) + +class group_del(frontend.Method): + 'Delete an existing group.' +api.register(group_del) + +class group_mod(frontend.Method): + 'Edit an existing group.' +api.register(group_mod) + +class group_find(frontend.Method): + 'Search the groups.' +api.register(group_find) + + +# Register some methods for the 'service' object +class service_add(frontend.Method): + 'Add a new service.' +api.register(service_add) + +class service_del(frontend.Method): + 'Delete an existing service.' +api.register(service_del) + +class service_mod(frontend.Method): + 'Edit an existing service.' +api.register(service_mod) + +class service_find(frontend.Method): + 'Search the services.' +api.register(service_find) + + +# And to emphasis that the registration order doesn't matter, +# we'll register the objects last: +class group(frontend.Object): + 'Group object' +api.register(group) + +class service(frontend.Object): + 'Service object' +api.register(service) -- cgit From c846c7d91f0654dc4f52bedb053a79166c8d6adf Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 1 Oct 2008 16:10:41 -0600 Subject: Removed the everything except the envtest command and the user related plugins from f_user.py --- ipalib/plugins/f_user.py | 135 ++++++++++------------------------------------- 1 file changed, 28 insertions(+), 107 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugins/f_user.py b/ipalib/plugins/f_user.py index e4d7dc10..a482a963 100644 --- a/ipalib/plugins/f_user.py +++ b/ipalib/plugins/f_user.py @@ -18,7 +18,7 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ -Some example plugins. +Frontend plugins for user (Identity). """ from ipalib import frontend @@ -26,34 +26,6 @@ from ipalib import crud from ipalib.frontend import Param from ipalib import api -class user(frontend.Object): - 'User object' - takes_params = ( - 'givenname', - 'sn', - Param('uid', - primary_key=True, - default_from=lambda givenname, sn: givenname[0] + sn, - normalize=lambda value: value.lower(), - ), - Param('krbprincipalname', - default_from=lambda uid: '%s@EXAMPLE.COM' % uid, - ), - Param('homedirectory', - default_from=lambda uid: '/home/%s' % uid, - ) - ) -api.register(user) - - -# Hypothetical functional commands (not associated with any object): -class krbtest(frontend.Command): - 'Test your Kerberos ticket.' -api.register(krbtest) - -class discover(frontend.Command): - 'Discover IPA servers on network.' -api.register(discover) # Command to get the idea how plugins will interact with api.env class envtest(frontend.Command): @@ -73,21 +45,46 @@ class envtest(frontend.Command): print " %s: %s" % (var, val) api.register(envtest) -# Register some methods for the 'user' object: + +class user(frontend.Object): + """ + User object. + """ + takes_params = ( + 'givenname', + 'sn', + Param('uid', + primary_key=True, + default_from=lambda givenname, sn: givenname[0] + sn, + normalize=lambda value: value.lower(), + ), + Param('krbprincipalname', + default_from=lambda uid: '%s@EXAMPLE.COM' % uid, + ), + Param('homedirectory', + default_from=lambda uid: '/home/%s' % uid, + ) + ) +api.register(user) + + class user_add(crud.Add): 'Add a new user.' def execute(self, *args, **kw): return 1 api.register(user_add) + class user_del(crud.Del): 'Delete an existing user.' api.register(user_del) + class user_mod(crud.Mod): 'Edit an existing user.' api.register(user_mod) + class user_find(crud.Find): 'Search the users.' def execute(self, *args, **kw): @@ -96,83 +93,7 @@ class user_find(crud.Find): return result api.register(user_find) + class user_show(crud.Get): 'Examine an existing user.' api.register(user_show) - - -# Register some properties for the 'user' object: -#class user_givenname(frontend.Property): -# 'User first name' -# required = True -#api.register(user_givenname) - -#class user_sn(frontend.Property): -# 'User last name' -# required = True -#api.register(user_sn) - -#class user_login(frontend.Property): -# 'User login' -# required = True -# default_from = frontend.DefaultFrom( -# lambda first, last: (first[0] + last).lower(), -# 'givenname', 'sn' -# ) -#api.register(user_login) - -#class user_initials(frontend.Property): -# 'User initials' -# required = True -# default_from = frontend.DefaultFrom( -# lambda first, last: first[0] + last[0], -# 'givenname', 'sn' -# ) -#api.register(user_initials) - - -# Register some methods for the 'group' object: -class group_add(frontend.Method): - 'Add a new group.' -api.register(group_add) - -class group_del(frontend.Method): - 'Delete an existing group.' -api.register(group_del) - -class group_mod(frontend.Method): - 'Edit an existing group.' -api.register(group_mod) - -class group_find(frontend.Method): - 'Search the groups.' -api.register(group_find) - - -# Register some methods for the 'service' object -class service_add(frontend.Method): - 'Add a new service.' -api.register(service_add) - -class service_del(frontend.Method): - 'Delete an existing service.' -api.register(service_del) - -class service_mod(frontend.Method): - 'Edit an existing service.' -api.register(service_mod) - -class service_find(frontend.Method): - 'Search the services.' -api.register(service_find) - - -# And to emphasis that the registration order doesn't matter, -# we'll register the objects last: -class group(frontend.Object): - 'Group object' -api.register(group) - -class service(frontend.Object): - 'Service object' -api.register(service) -- cgit From 2507b7c6741330eafacce63deca630073fb536fd Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 1 Oct 2008 16:17:02 -0600 Subject: Clarified docstrings in */plugins/__init__.py; renamed ipa_server/plugins/ipa_ldap.py to b_ldap.py --- ipalib/plugins/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/plugins/__init__.py b/ipalib/plugins/__init__.py index 58db94ca..544429ef 100644 --- a/ipalib/plugins/__init__.py +++ b/ipalib/plugins/__init__.py @@ -18,5 +18,8 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ -Sub-package containing all internal plugins. +Sub-package containing all core plugins. + +By convention, modules with frontend plugins are named f_*.py and modules +with backend plugins are named b_*.py. """ -- cgit From 0e137110c7f3c543faf9ec4cc7917d6aa81f02a6 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 2 Oct 2008 16:12:19 -0600 Subject: Started on skeleton for xmlrcp client/server --- ipalib/plugins/b_xmlrpc.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 ipalib/plugins/b_xmlrpc.py (limited to 'ipalib') diff --git a/ipalib/plugins/b_xmlrpc.py b/ipalib/plugins/b_xmlrpc.py new file mode 100644 index 00000000..afe76505 --- /dev/null +++ b/ipalib/plugins/b_xmlrpc.py @@ -0,0 +1,39 @@ +# Authors: +# Jason Gerard DeRose +# +# 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 + +""" +XML-RPC client plugin. + +Lightwieght XML-RPC client using Python standard library xmlrpclib. +""" + +import xmlrpclib +from ipalib.backend import Backend +from ipalib import api + +class xmlrpc(Backend): + """ + Kerberos backend plugin. + """ + + def get_client(self): + # FIXME: The server uri should come from self.api.env.server_uri + return xmlrpclib.ServerProxy('http://localhost:8080', allow_none=True) + +api.register(xmlrpc) -- cgit From 6000b6b5c62181d25783b6d45adb2ed6f3928480 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 2 Oct 2008 17:02:24 -0600 Subject: Implemented basic Command.forward() method --- ipalib/frontend.py | 6 +++--- ipalib/plugins/f_user.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 30f5942b..6decb17d 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -542,9 +542,9 @@ class Command(plugable.Plugin): print ' kw =', kw def forward(self, *args, **kw): - print '%s.execute():' % self.name - print ' args =', args - print ' kw =', kw + xmlrpc_client = self.api.Backend.xmlrpc.get_client() + return getattr(xmlrpc_client, self.name)(kw, *args) + def __call__(self, *args, **kw): if len(args) > 0: diff --git a/ipalib/plugins/f_user.py b/ipalib/plugins/f_user.py index a482a963..29f0f8a0 100644 --- a/ipalib/plugins/f_user.py +++ b/ipalib/plugins/f_user.py @@ -87,10 +87,10 @@ api.register(user_mod) class user_find(crud.Find): 'Search the users.' - def execute(self, *args, **kw): - uid=args[0] - result = servercore.get_sub_entry(servercore.basedn, "uid=%s" % uid, ["*"]) - return result +# def execute(self, *args, **kw): +# uid=args[0] +# result = servercore.get_sub_entry(servercore.basedn, "uid=%s" % uid, ["*"]) +# return result api.register(user_find) -- cgit From 149429f3057e3ae934e660e3276c9e8d3c935d17 Mon Sep 17 00:00:00 2001 From: Martin Nagy Date: Thu, 2 Oct 2008 20:24:05 +0200 Subject: Environment is now subclassed from object, rather then dict. Added tests for Environment and config.py --- ipalib/__init__.py | 1 - ipalib/cli.py | 7 ++-- ipalib/config.py | 40 ++++++++++++--------- ipalib/plugable.py | 54 ++++++++++++++++++---------- ipalib/tests/test_crud.py | 2 +- ipalib/tests/test_frontend.py | 10 +++--- ipalib/tests/test_plugable.py | 83 +++++++++++++++++++++++++++++++++++++++++-- ipalib/tests/tstutil.py | 6 ++-- 8 files changed, 152 insertions(+), 51 deletions(-) (limited to 'ipalib') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index f0d43aad..956e4610 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -61,7 +61,6 @@ import backend import config api = plugable.API( - config.default_environment(), frontend.Command, frontend.Object, frontend.Method, diff --git a/ipalib/cli.py b/ipalib/cli.py index 1a08cef4..d66e1e2e 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -29,8 +29,7 @@ import frontend import errors import plugable import ipa_types - -from ipalib import config +import config def exit_error(error): sys.exit('ipa: ERROR: %s' % error) @@ -257,9 +256,7 @@ class CLI(object): self.print_commands() print 'Usage: ipa COMMAND' sys.exit(2) - # do parsing here, read the conf - conf_dict = config.read_config(self.api.env.conf) - self.api.env.update(conf_dict) + self.api.env.update(config.generate_env()) key = sys.argv[1] if key not in self: self.print_commands() diff --git a/ipalib/config.py b/ipalib/config.py index bb345661..73d23c8e 100644 --- a/ipalib/config.py +++ b/ipalib/config.py @@ -17,22 +17,31 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +import types -def default_environment(): +DEFAULT_CONF='/etc/ipa/ipa.conf' + +def generate_env(d={}): default = dict( - conf = '/etc/ipa/ipa.conf', server_context = True, query_dns = True, verbose = False, - servers = LazyIter(myservers), - realm = LazyProp(myrealm), - domain = LazyProp(mydomain), + servers = LazyIter(get_servers), + realm = LazyProp(get_realm), + domain = LazyProp(get_domain), ) + for key, value in d.iteritems(): + if key in default and type(default[key]) in (LazyIter, LazyProp): + default[key].set_value(value) + else: + default[key] = value + return default class LazyProp(object): def __init__(self, func, value=None): + assert isinstance(func, types.FunctionType) self._func = func self._value = value @@ -40,26 +49,26 @@ class LazyProp(object): self._value = value def get_value(self): - if self._value is None: + if self._value == None: return self._func() else: return self._value -# FIXME: make sure to eliminate duplicates class LazyIter(LazyProp): def get_value(self): - if self._value is not None: - if type(self._value) is tuple: + if self._value != None: + if type(self._value) == tuple: for item in self._value: yield item else: yield self._value for item in self._func(): - yield item + if not self._value or item not in self._value: + yield item -def read_config(file): +def read_config(file=DEFAULT_CONF): assert isinstance(file, basestring) # open the file and read configuration, return a dict # for now, these are here just for testing purposes @@ -67,18 +76,15 @@ def read_config(file): # these functions are here just to "emulate" dns resolving for now -def mydomain(): +def get_domain(): return "ipatest.com" -def myrealm(): +def get_realm(): return "IPATEST.COM" -def myservers(): - # print is here to demonstrate that the querying will occur only when it is - # really needed - print "Querying DNS" +def get_servers(): yield "server.ipatest.com" yield "backup.ipatest.com" yield "fake.ipatest.com" diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 98a74dfa..ffe4a11f 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -692,32 +692,50 @@ class Registrar(DictProxy): self.__registered.add(klass) -class Environment(dict): +class Environment(object): + def __init__(self): + object.__setattr__(self, '_Environment__map', {}) + + def __setattr__(self, name, value): + self[name] = value + + def __getattr__(self, name): + return self[name] + + def __delattr__(self, name): + del self[name] + def __getitem__(self, key): - val = super(Environment, self).__getitem__(key) + val = self.__map[key] if hasattr(val, 'get_value'): return val.get_value() else: return val def __setitem__(self, key, value): - if key in self: - super_value = super(Environment, self).__getitem__(key) - - if key in self and hasattr(super_value, 'set_value'): - super_value.set_value(value) - else: - super(Environment, self).__setitem__(key, value) + if key in self or hasattr(self, key): + raise AttributeError('cannot overwrite %s.%s' % + (self.__class__.__name__, key) + ) + self.__map[key] = value + + def __delitem__(self, key): + raise AttributeError('read-only: cannot del %s.%s' % + (self.__class__.__name__, key) + ) - def __getattr__(self, name): - return self[name] + def __contains__(self, key): + return key in self.__map - def __setattr__(self, name, value): - self[name] = value + def __iter__(self): + for key in self.__map: + yield key - def update(self, d): - assert isinstance(d, dict) - for key, value in d.iteritems(): + def update(self, new_vals, ignore_errors = False): + assert type(new_vals) == dict + for key, value in new_vals.iteritems(): + if key in self and ignore_errors: + continue self[key] = value @@ -727,10 +745,10 @@ class API(DictProxy): """ __finalized = False - def __init__(self, default_env, *allowed): + def __init__(self, *allowed): self.__d = dict() self.register = Registrar(*allowed) - self.env = Environment(default_env) + self.env = Environment() super(API, self).__init__(self.__d) def finalize(self): diff --git a/ipalib/tests/test_crud.py b/ipalib/tests/test_crud.py index df85253b..9355f237 100644 --- a/ipalib/tests/test_crud.py +++ b/ipalib/tests/test_crud.py @@ -26,11 +26,11 @@ from ipalib import crud, frontend, plugable, config def get_api(): api = plugable.API( - config.default_environment(), frontend.Object, frontend.Method, frontend.Property, ) + api.env.update(config.generate_env()) class user(frontend.Object): takes_params = ( 'givenname', diff --git a/ipalib/tests/test_frontend.py b/ipalib/tests/test_frontend.py index e3dd04fa..c70cc00d 100644 --- a/ipalib/tests/test_frontend.py +++ b/ipalib/tests/test_frontend.py @@ -732,7 +732,8 @@ class test_Command(ClassChecker): kw = dict(how_are='you', on_this='fine day?') # Test in server context: - api = plugable.API(dict(server_context=True), self.cls) + api = plugable.API(self.cls) + api.env.update(dict(server_context=True)) api.finalize() o = my_cmd() o.set_api(api) @@ -741,7 +742,8 @@ class test_Command(ClassChecker): assert o.run.im_func is my_cmd.execute.im_func # Test in non-server context - api = plugable.API(dict(server_context=False), self.cls) + api = plugable.API(self.cls) + api.env.update(dict(server_context=False)) api.finalize() o = my_cmd() o.set_api(api) @@ -868,10 +870,10 @@ class test_Object(ClassChecker): Test the `frontend.Object.primary_key` attribute. """ api = plugable.API( - config.default_environment(), frontend.Method, frontend.Property, ) + api.env.update(config.generate_env()) api.finalize() # Test with no primary keys: @@ -923,12 +925,12 @@ class test_Object(ClassChecker): Test the `frontend.Object.backend` attribute. """ api = plugable.API( - config.default_environment(), frontend.Object, frontend.Method, frontend.Property, backend.Backend, ) + api.env.update(config.generate_env()) class ldap(backend.Backend): whatever = 'It worked!' api.register(ldap) diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py index 9be6b343..fd3c3c88 100644 --- a/ipalib/tests/test_plugable.py +++ b/ipalib/tests/test_plugable.py @@ -620,6 +620,84 @@ class test_NameSpace(ClassChecker): 'NameSpace(<%d members>, sort=%r)' % (cnt, sort) +def test_Environment(): + """ + Tests the `plugable.Environment` class. + """ + # This has to be the same as iter_cnt + control_cnt = 0 + class prop_class: + def __init__(self, val): + self._val = val + def get_value(self): + return self._val + + class iter_class(prop_class): + # Increment this for each time iter_class yields + iter_cnt = 0 + def get_value(self): + for item in self._val: + self.__class__.iter_cnt += 1 + yield item + + # Tests for basic functionality + basic_tests = ( + ('a', 1), + ('b', 'basic_foo'), + ('c', ('basic_bar', 'basic_baz')), + ) + # Tests with prop classes + prop_tests = ( + ('d', prop_class(2), 2), + ('e', prop_class('prop_foo'), 'prop_foo'), + ('f', prop_class(('prop_bar', 'prop_baz')), ('prop_bar', 'prop_baz')), + ) + # Tests with iter classes + iter_tests = ( + ('g', iter_class((3, 4, 5)), (3, 4, 5)), + ('h', iter_class(('iter_foo', 'iter_bar', 'iter_baz')), + ('iter_foo', 'iter_bar', 'iter_baz') + ), + ) + + # Set all the values + env = plugable.Environment() + for name, val in basic_tests: + env[name] = val + for name, val, dummy in prop_tests: + env[name] = val + for name, val, dummy in iter_tests: + env[name] = val + + # Test if the values are correct + for name, val in basic_tests: + assert env[name] == val + for name, dummy, val in prop_tests: + assert env[name] == val + # Test if the get_value() function is called only when needed + for name, dummy, correct_values in iter_tests: + values_in_env = [] + for val in env[name]: + control_cnt += 1 + assert iter_class.iter_cnt == control_cnt + values_in_env.append(val) + assert tuple(values_in_env) == correct_values + + # Test __setattr__() + env.spam = 'ham' + assert env.spam == 'ham' + + # Test if we throw AttributeError exception when trying to overwrite + # existing value, or delete it + raises(AttributeError, setitem, env, 'a', 1) + raises(AttributeError, setattr, env, 'a', 1) + raises(AttributeError, delitem, env, 'a') + raises(AttributeError, delattr, env, 'a') + raises(AttributeError, plugable.Environment.update, env, dict(a=1000)) + # This should be silently ignored + env.update(dict(a=1000), True) + assert env.a != 1000 + def test_Registrar(): class Base1(object): pass @@ -722,6 +800,7 @@ def test_Registrar(): assert issubclass(klass, base) + def test_API(): assert issubclass(plugable.API, plugable.ReadOnly) @@ -742,7 +821,7 @@ def test_API(): def method(self, n): return n + 1 - api = plugable.API(dict(), base0, base1) + api = plugable.API(base0, base1) r = api.register assert isinstance(r, plugable.Registrar) assert read_only(api, 'register') is r @@ -800,7 +879,7 @@ def test_API(): # Test with base class that doesn't request a proxy class NoProxy(plugable.Plugin): __proxy__ = False - api = plugable.API(dict(), NoProxy) + api = plugable.API(NoProxy) class plugin0(NoProxy): pass api.register(plugin0) diff --git a/ipalib/tests/tstutil.py b/ipalib/tests/tstutil.py index 7586d08c..743716a0 100644 --- a/ipalib/tests/tstutil.py +++ b/ipalib/tests/tstutil.py @@ -55,7 +55,7 @@ def raises(exception, callback, *args, **kw): def getitem(obj, key): """ - Works like getattr but for dictionary interface. Uses this in combination + Works like getattr but for dictionary interface. Use this in combination with raises() to test that, for example, KeyError is raised. """ return obj[key] @@ -63,7 +63,7 @@ def getitem(obj, key): def setitem(obj, key, value): """ - Works like setattr but for dictionary interface. Uses this in combination + Works like setattr but for dictionary interface. Use this in combination with raises() to test that, for example, TypeError is raised. """ obj[key] = value @@ -71,7 +71,7 @@ def setitem(obj, key, value): def delitem(obj, key): """ - Works like delattr but for dictionary interface. Uses this in combination + Works like delattr but for dictionary interface. Use this in combination with raises() to test that, for example, TypeError is raised. """ del obj[key] -- cgit From 993b9f4f63c9868042c96db8c5797a5005331d12 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 2 Oct 2008 17:46:48 -0600 Subject: Command.get_default() now only returns a defaults for required values --- ipalib/frontend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 6decb17d..ed28a4ac 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -522,7 +522,7 @@ class Command(plugable.Plugin): def __get_default_iter(self, kw): for param in self.params(): - if param.name not in kw: + if param.required and kw.get(param.name, None) is None: yield (param.name, param.get_default(**kw)) def get_default(self, **kw): -- cgit From ed3a5855f310d3782bea706c58780f5dc6e96d5d Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 2 Oct 2008 17:51:50 -0600 Subject: -m --- ipalib/frontend.py | 1 - 1 file changed, 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index ed28a4ac..7abb8fb0 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -467,7 +467,6 @@ class Command(plugable.Plugin): args = None options = None params = None - can_forward = True def finalize(self): self.args = plugable.NameSpace(self.__create_args(), sort=False) -- cgit From d84e27f0d41aa13cfa5dd154ee476bd7c5e8b072 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 2 Oct 2008 19:09:13 -0600 Subject: Added ipalib/util.py with xmlrpc_marshal() and xmlrpc_unmarshal() functions; added corresponding unit tests --- ipalib/tests/test_util.py | 49 +++++++++++++++++++++++++++++++++++++++++++++++ ipalib/util.py | 41 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 ipalib/tests/test_util.py create mode 100644 ipalib/util.py (limited to 'ipalib') diff --git a/ipalib/tests/test_util.py b/ipalib/tests/test_util.py new file mode 100644 index 00000000..f8ee0bf4 --- /dev/null +++ b/ipalib/tests/test_util.py @@ -0,0 +1,49 @@ +# Authors: +# Jason Gerard DeRose +# +# 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 + +""" +Unit tests for `ipalib.util` module. +""" + +from tstutil import raises +from ipalib import util + + +def test_xmlrpc_marshal(): + """ + Test the `util.xmlrpc_marshal` function. + """ + f = util.xmlrpc_marshal + assert f() == ({},) + assert f('one', 'two') == ({}, 'one', 'two') + assert f(one=1, two=2) == (dict(one=1, two=2),) + assert f('one', 'two', three=3, four=4) == \ + (dict(three=3, four=4), 'one', 'two') + + +def test_xmlrpc_unmarshal(): + """ + Test the `util.xmlrpc_unmarshal` function. + """ + f = util.xmlrpc_unmarshal + assert f() == (tuple(), {}) + assert f({}, 'one', 'two') == (('one', 'two'), {}) + assert f(dict(one=1, two=2)) == (tuple(), dict(one=1, two=2)) + assert f(dict(three=3, four=4), 'one', 'two') == \ + (('one', 'two'), dict(three=3, four=4)) diff --git a/ipalib/util.py b/ipalib/util.py new file mode 100644 index 00000000..b60bfc8a --- /dev/null +++ b/ipalib/util.py @@ -0,0 +1,41 @@ +# Authors: +# Jason Gerard DeRose +# +# 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 + +""" +Various utility functions. +""" + +def xmlrpc_marshal(*args, **kw): + """ + Marshal (args, kw) into ((kw,) + args). + """ + return ((kw,) + args) + + +def xmlrpc_unmarshal(*params): + """ + Unmarshal (params) into (args, kw). + """ + if len(params) > 0: + kw = params[0] + if type(kw) is not dict: + raise TypeError('first xmlrpc argument must be dict') + else: + kw = {} + return (params[1:], kw) -- cgit From 3ffbaac64cc3a9ab704c707112f59e041986576c Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 2 Oct 2008 19:42:06 -0600 Subject: Backend.xmlrpc and simple-server.py now use the xmlrpc_marshal() and xmlrpc_unmarshal() functions respectively --- ipalib/config.py | 4 ++-- ipalib/frontend.py | 6 ++++-- ipalib/plugins/b_xmlrpc.py | 10 ++++++++++ 3 files changed, 16 insertions(+), 4 deletions(-) (limited to 'ipalib') diff --git a/ipalib/config.py b/ipalib/config.py index 73d23c8e..f327cab7 100644 --- a/ipalib/config.py +++ b/ipalib/config.py @@ -23,7 +23,7 @@ DEFAULT_CONF='/etc/ipa/ipa.conf' def generate_env(d={}): default = dict( - server_context = True, + server_context = False, query_dns = True, verbose = False, servers = LazyIter(get_servers), @@ -35,7 +35,7 @@ def generate_env(d={}): default[key].set_value(value) else: default[key] = value - + return default diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 7abb8fb0..651e4642 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -541,8 +541,10 @@ class Command(plugable.Plugin): print ' kw =', kw def forward(self, *args, **kw): - xmlrpc_client = self.api.Backend.xmlrpc.get_client() - return getattr(xmlrpc_client, self.name)(kw, *args) + """ + Forward call over XML-RPC. + """ + return self.api.Backend.xmlrpc.forward_call(self.name, *args, **kw) def __call__(self, *args, **kw): diff --git a/ipalib/plugins/b_xmlrpc.py b/ipalib/plugins/b_xmlrpc.py index afe76505..61935f01 100644 --- a/ipalib/plugins/b_xmlrpc.py +++ b/ipalib/plugins/b_xmlrpc.py @@ -25,6 +25,7 @@ Lightwieght XML-RPC client using Python standard library xmlrpclib. import xmlrpclib from ipalib.backend import Backend +from ipalib.util import xmlrpc_marshal from ipalib import api class xmlrpc(Backend): @@ -36,4 +37,13 @@ class xmlrpc(Backend): # FIXME: The server uri should come from self.api.env.server_uri return xmlrpclib.ServerProxy('http://localhost:8080', allow_none=True) + def forward_call(self, name, *args, **kw): + """ + Forward a call over XML-RPC to an IPA server. + """ + client = self.get_client() + command = getattr(client, name) + params = xmlrpc_marshal(*args, **kw) + return command(*params) + api.register(xmlrpc) -- cgit From 7e4b0a072e69351496010d7b2151c9b434c8fdb0 Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Sat, 4 Oct 2008 01:50:59 -0400 Subject: Implement user-find and user-add backend functions so they work over XML-RPC Change port to 8880 to not conflict with a running IPA v1 instance Encode incoming values from unicode as utf-8 before sending to LDAP --- ipalib/plugins/b_xmlrpc.py | 3 +- ipalib/plugins/f_user.py | 91 +++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 87 insertions(+), 7 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugins/b_xmlrpc.py b/ipalib/plugins/b_xmlrpc.py index 61935f01..f8dacf5d 100644 --- a/ipalib/plugins/b_xmlrpc.py +++ b/ipalib/plugins/b_xmlrpc.py @@ -35,7 +35,7 @@ class xmlrpc(Backend): def get_client(self): # FIXME: The server uri should come from self.api.env.server_uri - return xmlrpclib.ServerProxy('http://localhost:8080', allow_none=True) + return xmlrpclib.ServerProxy('http://localhost:8888', allow_none=True) def forward_call(self, name, *args, **kw): """ @@ -45,5 +45,6 @@ class xmlrpc(Backend): command = getattr(client, name) params = xmlrpc_marshal(*args, **kw) return command(*params) +# return command(*args, **kw) api.register(xmlrpc) diff --git a/ipalib/plugins/f_user.py b/ipalib/plugins/f_user.py index 29f0f8a0..320666aa 100644 --- a/ipalib/plugins/f_user.py +++ b/ipalib/plugins/f_user.py @@ -25,7 +25,10 @@ from ipalib import frontend from ipalib import crud from ipalib.frontend import Param from ipalib import api - +from ipa_server import servercore +from ipa_server import ipaldap +import ldap +from ipa_server.context import context # Command to get the idea how plugins will interact with api.env class envtest(frontend.Command): @@ -71,7 +74,79 @@ api.register(user) class user_add(crud.Add): 'Add a new user.' def execute(self, *args, **kw): - return 1 + """args[0] = uid of the user to add + kw{container} is the location in the DIT to add the user, not + required + kw otherwise contains all the attributes + """ + # FIXME: ug, really? + if not kw.get('container'): + user_container = servercore.DefaultUserContainer + else: + user_container = kw['container'] + del kw['container'] + + user = kw + + if not isinstance(user, dict): + # FIXME, need proper error + raise SyntaxError + + user['uid'] = args[0] + + # dn is set here, not by the user + try: + del user['dn'] + except KeyError: + pass + + # No need to set empty fields, and they can cause issues when they + # get to LDAP, like: + # TypeError: ('expected a string in the list', None) + for k in user.keys(): + if not user[k] or len(user[k]) == 0 or (isinstance(user[k],list) and len(user[k]) == 1 and '' in user[k]): + del user[k] + + dn="uid=%s,%s,%s" % (ldap.dn.escape_dn_chars(user['uid']), + user_container,servercore.basedn) + + entry = ipaldap.Entry(dn) + + # Let us add in some missing attributes + # FIXME, get config +# if user.get('homedirectory') is None: +# user['homedirectory'] = '%s/%s' % (config.get('ipahomesrootdir'), user.get('uid')) +# user['homedirectory'] = user['homedirectory'].replace('//', '/') +# user['homedirectory'] = user['homedirectory'].rstrip('/') +# if user.get('loginshell') is None: +# user['loginshell'] = config.get('ipadefaultloginshell') + if user.get('gecos') is None: + user['gecos'] = user['uid'] + + # FIXME: add to default group + user['gidNumber'] = "500" + + if user.get('krbprincipalname') is None: + user['krbprincipalname'] = "%s@%s" % (user.get('uid'), self.realm) + + # FIXME. This is a hack so we can request separate First and Last + # name in the GUI. + if user.get('cn') is None: + user['cn'] = "%s %s" % (user.get('givenname'), + user.get('sn')) + + # some required objectclasses + # FIXME + # entry.setValues('objectClass', (config.get('ipauserobjectclasses'))) + entry.setValues('objectClass', ['top', 'person', 'organizationalPerson', 'inetOrgPerson', 'inetUser', 'posixAccount', 'krbPrincipalAux']) + + # fill in our new entry with everything sent by the user + for u in user: + entry.setValues(u, user[u]) + + result = context.conn.getConn().addEntry(entry) + return result + api.register(user_add) @@ -87,10 +162,14 @@ api.register(user_mod) class user_find(crud.Find): 'Search the users.' -# def execute(self, *args, **kw): -# uid=args[0] -# result = servercore.get_sub_entry(servercore.basedn, "uid=%s" % uid, ["*"]) -# return result + def execute(self, *args, **kw): + uid=args[0] + result = servercore.get_sub_entry(servercore.basedn, "uid=%s" % uid, ["*"]) + return result + def forward(self, *args, **kw): + result = super(crud.Find, self).forward(*args, **kw) + for a in result: + print a, ": ", res[a] api.register(user_find) -- cgit From cb795fa14bc2798fd8f1c6e2b87d19432e3f84a1 Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Sat, 4 Oct 2008 05:17:11 -0400 Subject: Add group plugin, routine to get cn=ipaconfig --- ipalib/plugins/f_group.py | 117 ++++++++++++++++++++++++++++++++++++++++++++++ ipalib/plugins/f_user.py | 41 ++++++++++------ 2 files changed, 145 insertions(+), 13 deletions(-) create mode 100644 ipalib/plugins/f_group.py (limited to 'ipalib') diff --git a/ipalib/plugins/f_group.py b/ipalib/plugins/f_group.py new file mode 100644 index 00000000..36cecc33 --- /dev/null +++ b/ipalib/plugins/f_group.py @@ -0,0 +1,117 @@ +# Authors: +# Jason Gerard DeRose +# +# 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 + +""" +Frontend plugins for group (Identity). +""" + +from ipalib import frontend +from ipalib import crud +from ipalib.frontend import Param +from ipalib import api +from ipa_server import servercore +from ipa_server import ipaldap +import ldap +from ipa_server.context import context + + +class group(frontend.Object): + """ + Group object. + """ + takes_params = ( + 'description', + Param('cn', + primary_key=True, + normalize=lambda value: value.lower(), + ) + ) +api.register(group) + + +class group_add(crud.Add): + 'Add a new group.' + def execute(self, *args, **kw): + """args[0] = uid of the group to add + kw{container} is the location in the DIT to add the group, not + required + kw otherwise contains all the attributes + """ + # FIXME: ug, really? + if not kw.get('container'): + group_container = servercore.DefaultGroupContainer + else: + group_container = kw['container'] + del kw['container'] + + group = kw + + group['cn'] = args[0] + + # Get our configuration + config = servercore.get_ipa_config() + + dn="cn=%s,%s,%s" % (ldap.dn.escape_dn_chars(group['cn']), + group_container,servercore.basedn) + + entry = ipaldap.Entry(dn) + + # some required objectclasses + entry.setValues('objectClass', (config.get('ipagroupobjectclasses'))) + + # No need to explicitly set gidNumber. The dna_plugin will do this + # for us if the value isn't provided by the user. + + # fill in our new entry with everything sent by the user + for g in group: + entry.setValues(g, group[g]) + + result = context.conn.getConn().addEntry(entry) + return result + + +api.register(group_add) + + +class group_del(crud.Del): + 'Delete an existing group.' +api.register(group_del) + + +class group_mod(crud.Mod): + 'Edit an existing group.' +api.register(group_mod) + + +class group_find(crud.Find): + 'Search the groups.' + def execute(self, *args, **kw): + cn=args[0] + result = servercore.get_sub_entry(servercore.basedn, "cn=%s" % cn, ["*"]) + return result + def forward(self, *args, **kw): + result = super(crud.Find, self).forward(*args, **kw) + for a in result: + print a, ": ", result[a] +api.register(group_find) + + +class group_show(crud.Get): + 'Examine an existing group.' +api.register(group_show) diff --git a/ipalib/plugins/f_user.py b/ipalib/plugins/f_user.py index 320666aa..0e62b833 100644 --- a/ipalib/plugins/f_user.py +++ b/ipalib/plugins/f_user.py @@ -112,19 +112,35 @@ class user_add(crud.Add): entry = ipaldap.Entry(dn) + # Get our configuration + config = servercore.get_ipa_config() + # Let us add in some missing attributes - # FIXME, get config -# if user.get('homedirectory') is None: -# user['homedirectory'] = '%s/%s' % (config.get('ipahomesrootdir'), user.get('uid')) -# user['homedirectory'] = user['homedirectory'].replace('//', '/') -# user['homedirectory'] = user['homedirectory'].rstrip('/') -# if user.get('loginshell') is None: -# user['loginshell'] = config.get('ipadefaultloginshell') + if user.get('homedirectory') is None: + user['homedirectory'] = '%s/%s' % (config.get('ipahomesrootdir'), user.get('uid')) + user['homedirectory'] = user['homedirectory'].replace('//', '/') + user['homedirectory'] = user['homedirectory'].rstrip('/') + if user.get('loginshell') is None: + user['loginshell'] = config.get('ipadefaultloginshell') if user.get('gecos') is None: user['gecos'] = user['uid'] - # FIXME: add to default group - user['gidNumber'] = "500" + # If uidnumber is blank the the FDS dna_plugin will automatically + # assign the next value. So we don't have to do anything with it. + + group_dn="cn=%s,%s,%s" % (config.get('ipadefaultprimarygroup'), servercore.DefaultGroupContainer, servercore.basedn) + try: + default_group = servercore.get_entry_by_dn(group_dn, ['dn','gidNumber']) + if default_group: + user['gidnumber'] = default_group.get('gidnumber') +# except ipaerror.exception_for(ipaerror.LDAP_DATABASE_ERROR), e: +# raise ipaerror.gen_exception(ipaerror.LDAP_DATABASE_ERROR, message=None, nested_exception=e.detail) +# except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND): +# # Fake an LDAP error so we can return something useful to the user +# raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND, "The default group for new users, '%s', cannot be found." % config.get('ipadefaultprimarygroup')) + except Exception, e: + # FIXME + raise e if user.get('krbprincipalname') is None: user['krbprincipalname'] = "%s@%s" % (user.get('uid'), self.realm) @@ -136,9 +152,8 @@ class user_add(crud.Add): user.get('sn')) # some required objectclasses - # FIXME - # entry.setValues('objectClass', (config.get('ipauserobjectclasses'))) - entry.setValues('objectClass', ['top', 'person', 'organizationalPerson', 'inetOrgPerson', 'inetUser', 'posixAccount', 'krbPrincipalAux']) + entry.setValues('objectClass', (config.get('ipauserobjectclasses'))) + # entry.setValues('objectClass', ['top', 'person', 'organizationalPerson', 'inetOrgPerson', 'inetUser', 'posixAccount', 'krbPrincipalAux']) # fill in our new entry with everything sent by the user for u in user: @@ -169,7 +184,7 @@ class user_find(crud.Find): def forward(self, *args, **kw): result = super(crud.Find, self).forward(*args, **kw) for a in result: - print a, ": ", res[a] + print a, ": ", result[a] api.register(user_find) -- cgit From 69bc5ad77adecaf7d8fde4a6578c3d2f3ef355df Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Tue, 7 Oct 2008 02:10:15 -0400 Subject: Add some more supporting functions Do a little bit more error handling and checking --- ipalib/plugins/b_xmlrpc.py | 8 ++++++-- ipalib/plugins/f_group.py | 3 +-- ipalib/plugins/f_user.py | 14 ++++++++++++-- 3 files changed, 19 insertions(+), 6 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugins/b_xmlrpc.py b/ipalib/plugins/b_xmlrpc.py index f8dacf5d..d7cbd856 100644 --- a/ipalib/plugins/b_xmlrpc.py +++ b/ipalib/plugins/b_xmlrpc.py @@ -24,6 +24,7 @@ Lightwieght XML-RPC client using Python standard library xmlrpclib. """ import xmlrpclib +import socket from ipalib.backend import Backend from ipalib.util import xmlrpc_marshal from ipalib import api @@ -44,7 +45,10 @@ class xmlrpc(Backend): client = self.get_client() command = getattr(client, name) params = xmlrpc_marshal(*args, **kw) - return command(*params) -# return command(*args, **kw) + try: + return command(*params) + except socket.error, e: + print e[1] + return False api.register(xmlrpc) diff --git a/ipalib/plugins/f_group.py b/ipalib/plugins/f_group.py index 36cecc33..c5a37e72 100644 --- a/ipalib/plugins/f_group.py +++ b/ipalib/plugins/f_group.py @@ -28,7 +28,6 @@ from ipalib import api from ipa_server import servercore from ipa_server import ipaldap import ldap -from ipa_server.context import context class group(frontend.Object): @@ -82,7 +81,7 @@ class group_add(crud.Add): for g in group: entry.setValues(g, group[g]) - result = context.conn.getConn().addEntry(entry) + result = servercore.add_entry(entry) return result diff --git a/ipalib/plugins/f_user.py b/ipalib/plugins/f_user.py index 0e62b833..49b6a370 100644 --- a/ipalib/plugins/f_user.py +++ b/ipalib/plugins/f_user.py @@ -28,7 +28,6 @@ from ipalib import api from ipa_server import servercore from ipa_server import ipaldap import ldap -from ipa_server.context import context # Command to get the idea how plugins will interact with api.env class envtest(frontend.Command): @@ -94,6 +93,13 @@ class user_add(crud.Add): user['uid'] = args[0] + if not servercore.is_user_unique(user['uid']): + # FIXME, specific error + raise SyntaxError("user already exists") + if servercore.uid_too_long(user['uid']): + # FIXME, specific error + raise SyntaxError("uid is too long") + # dn is set here, not by the user try: del user['dn'] @@ -159,8 +165,12 @@ class user_add(crud.Add): for u in user: entry.setValues(u, user[u]) - result = context.conn.getConn().addEntry(entry) + result = servercore.add_entry(entry) return result + def forward(self, *args, **kw): + result = super(crud.Add, self).forward(*args, **kw) + if result != False: + print result api.register(user_add) -- cgit From e012e860b472bcb5a00a089e73113fb6989fde20 Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Tue, 7 Oct 2008 04:31:22 -0400 Subject: Implement user-mod --- ipalib/plugins/f_user.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/plugins/f_user.py b/ipalib/plugins/f_user.py index 49b6a370..0b424d35 100644 --- a/ipalib/plugins/f_user.py +++ b/ipalib/plugins/f_user.py @@ -170,7 +170,7 @@ class user_add(crud.Add): def forward(self, *args, **kw): result = super(crud.Add, self).forward(*args, **kw) if result != False: - print result + print "User %s added" % args[0] api.register(user_add) @@ -182,6 +182,25 @@ api.register(user_del) class user_mod(crud.Mod): 'Edit an existing user.' + def execute(self, *args, **kw): + uid=args[0] + result = servercore.get_sub_entry(servercore.basedn, "uid=%s" % uid, ["*"]) + + user = kw + dn = result.get('dn') + del result['dn'] + entry = ipaldap.Entry((dn, servercore.convert_scalar_values(result))) + + for u in user: + entry.setValues(u, user[u]) + + result = servercore.update_entry(entry.toDict()) + + return result + def forward(self, *args, **kw): + result = super(crud.Mod, self).forward(*args, **kw) + if result != False: + print "User %s modified" % args[0] api.register(user_mod) -- cgit From db9d8dd3e0924bb9c7f9c89a56e6b6057dabc710 Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Tue, 7 Oct 2008 06:15:34 -0400 Subject: Implement a real user_find and move existing user_find to user_show --- ipalib/plugins/f_user.py | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugins/f_user.py b/ipalib/plugins/f_user.py index 0b424d35..e560d03c 100644 --- a/ipalib/plugins/f_user.py +++ b/ipalib/plugins/f_user.py @@ -208,15 +208,33 @@ class user_find(crud.Find): 'Search the users.' def execute(self, *args, **kw): uid=args[0] - result = servercore.get_sub_entry(servercore.basedn, "uid=%s" % uid, ["*"]) + result = servercore.find_users(uid, ["*"]) return result def forward(self, *args, **kw): - result = super(crud.Find, self).forward(*args, **kw) - for a in result: - print a, ": ", result[a] + users = super(crud.Find, self).forward(*args, **kw) + counter = users[0] + users = users[1:] + if counter == 0: + print "No entries found for", args[0] + return + elif counter == -1: + print "These results are truncated." + print "Please refine your search and try again." + + for u in users: + for a in u.keys(): + print "%s: %s" % (a, u[a]) api.register(user_find) class user_show(crud.Get): 'Examine an existing user.' + def execute(self, *args, **kw): + uid=args[0] + result = servercore.get_user_by_uid(uid, ["*"]) + return result + def forward(self, *args, **kw): + result = super(crud.Get, self).forward(*args, **kw) + for a in result: + print a, ": ", result[a] api.register(user_show) -- cgit From 4a68c719f03c176bc63a96007c089d0ac7ae5fc1 Mon Sep 17 00:00:00 2001 From: Martin Nagy Date: Fri, 3 Oct 2008 17:08:37 +0200 Subject: Implement config file reading --- ipalib/cli.py | 3 ++- ipalib/config.py | 33 +++++++++++++++++++++++++++------ ipalib/plugins/f_user.py | 4 ++-- 3 files changed, 31 insertions(+), 9 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index d66e1e2e..fc85dcb0 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -256,7 +256,8 @@ class CLI(object): self.print_commands() print 'Usage: ipa COMMAND' sys.exit(2) - self.api.env.update(config.generate_env()) + env_dict = config.read_config() + self.api.env.update(config.generate_env(env_dict)) key = sys.argv[1] if key not in self: self.print_commands() diff --git a/ipalib/config.py b/ipalib/config.py index f327cab7..16bc1371 100644 --- a/ipalib/config.py +++ b/ipalib/config.py @@ -17,7 +17,9 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +from ConfigParser import SafeConfigParser, ParsingError import types +import os DEFAULT_CONF='/etc/ipa/ipa.conf' @@ -26,7 +28,7 @@ def generate_env(d={}): server_context = False, query_dns = True, verbose = False, - servers = LazyIter(get_servers), + server = LazyIter(get_servers), realm = LazyProp(get_realm), domain = LazyProp(get_domain), ) @@ -68,11 +70,30 @@ class LazyIter(LazyProp): yield item -def read_config(file=DEFAULT_CONF): - assert isinstance(file, basestring) - # open the file and read configuration, return a dict - # for now, these are here just for testing purposes - return dict(servers="server.ipatest.com", realm="IPATEST.COM") +def read_config(config_file=DEFAULT_CONF): + assert isinstance(config_file, (basestring, file)) + + parser = SafeConfigParser() + files = [config_file, os.path.expanduser('~/.ipa.conf')] + + for f in files: + try: + if isinstance(f, file): + parser.readfp(f) + else: + parser.read(f) + except ParsingError: + print "Can't read %s" % f + + ret = {} + if parser.has_section('defaults'): + for name, value in parser.items('defaults'): + value = tuple(elem.strip() for elem in value.split(',')) + if len(value) == 1: + value = value[0] + ret[name] = value + + return ret # these functions are here just to "emulate" dns resolving for now diff --git a/ipalib/plugins/f_user.py b/ipalib/plugins/f_user.py index 29f0f8a0..150d48ea 100644 --- a/ipalib/plugins/f_user.py +++ b/ipalib/plugins/f_user.py @@ -35,10 +35,10 @@ class envtest(frontend.Command): print "Environment variables:" for var in api.env: val = api.env[var] - if var is 'servers': + if var is 'server': print "" print " Servers:" - for item in api.env.servers: + for item in api.env.server: print " %s" % item print "" else: -- cgit From 4a1c4a3fe3a568c98b6bab1456993c4163721c5d Mon Sep 17 00:00:00 2001 From: Martin Nagy Date: Fri, 3 Oct 2008 22:13:50 +0200 Subject: Implement argument parsing for the CLI --- ipalib/cli.py | 59 ++++++++++++++++++++++++++++++++++++++++++++++++-------- ipalib/config.py | 10 +++++++--- 2 files changed, 58 insertions(+), 11 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index fc85dcb0..aae4e31c 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -205,6 +205,9 @@ class CLI(object): def __init__(self, api): self.__api = api + self.__all_interactive = False + self.__not_interactive = False + self.__config = None def __get_api(self): return self.__api @@ -219,6 +222,7 @@ class CLI(object): print '\nSpecial CLI commands:' for cmd in self.api.Application(): self.print_cmd(cmd) + print '\nUse the --help option to see all the global options' print '' def print_cmd(self, cmd): @@ -252,20 +256,21 @@ class CLI(object): def run(self): self.finalize() - if len(sys.argv) < 2: + (args, env_dict) = self.parse_globals() + env_dict.update(config.read_config(self.__config)) + self.api.env.update(config.generate_env(env_dict)) + if len(args) < 1: self.print_commands() - print 'Usage: ipa COMMAND' + print 'Usage: ipa [global-options] COMMAND' sys.exit(2) - env_dict = config.read_config() - self.api.env.update(config.generate_env(env_dict)) - key = sys.argv[1] + key = args[0] if key not in self: self.print_commands() print 'ipa: ERROR: unknown command %r' % key sys.exit(2) self.run_cmd( self[key], - list(s.decode('utf-8') for s in sys.argv[2:]) + list(s.decode('utf-8') for s in args[1:]) ) def run_cmd(self, cmd, argv): @@ -276,7 +281,10 @@ class CLI(object): for param in cmd.params(): if param.name not in kw: if not param.required: - continue + if not self.__all_interactive: + continue + elif self.__not_interactive: + exit_error('Not enough arguments given') default = param.get_default(**kw) if default is None: prompt = '%s: ' % param.name @@ -319,11 +327,46 @@ class CLI(object): ) return parser + def parse_globals(self, argv=sys.argv[1:]): + env_dict = {} + parser = optparse.OptionParser() + parser.disable_interspersed_args() + parser.add_option('-a', dest='interactive', action='store_true', + help='Prompt for all missing options interactively') + parser.add_option('-n', dest='interactive', action='store_false', + help='Don\'t prompt for any options interactively') + parser.add_option('-c', dest='config_file', + help='Specify different configuration file') + parser.add_option('-e', dest='environment', + help='Specify or override environment variables') + parser.add_option('-v', dest='verbose', action='store_true', + help='Verbose output') + (options, args) = parser.parse_args(argv) + + if options.interactive == True: + self.__all_interactive = True + elif options.interactive == False: + self.__not_interactive = True + if options.config_file: + self.__config = options.config_file + if options.environment: + for a in options.environment.split(','): + a = a.split('=', 1) + if len(a) < 2: + parser.error('badly specified environment string,'\ + 'use var1=val1[,var2=val2]..') + env_dict[a[0].strip()] = a[1].strip() + if options.verbose != None: + env_dict.update(verbose=True) + + return (args, env_dict) + + def get_usage(self, cmd): return ' '.join(self.get_usage_iter(cmd)) def get_usage_iter(self, cmd): - yield 'Usage: %%prog %s' % to_cli(cmd.name) + yield 'Usage: %%prog [global-options] %s' % to_cli(cmd.name) for arg in cmd.args(): name = to_cli(arg.name).upper() if arg.multivalue: diff --git a/ipalib/config.py b/ipalib/config.py index 16bc1371..a0a33b40 100644 --- a/ipalib/config.py +++ b/ipalib/config.py @@ -28,6 +28,7 @@ def generate_env(d={}): server_context = False, query_dns = True, verbose = False, + interactive = True, server = LazyIter(get_servers), realm = LazyProp(get_realm), domain = LazyProp(get_domain), @@ -70,11 +71,14 @@ class LazyIter(LazyProp): yield item -def read_config(config_file=DEFAULT_CONF): - assert isinstance(config_file, (basestring, file)) +def read_config(config_file=None): + assert config_file == None or isinstance(config_file, (basestring, file)) parser = SafeConfigParser() - files = [config_file, os.path.expanduser('~/.ipa.conf')] + if config_file == None: + files = [DEFAULT_CONF, os.path.expanduser('~/.ipa.conf')] + else: + files = [config_file] for f in files: try: -- cgit From 732785b997bfe109b6cfc5695408a64580f7c85f Mon Sep 17 00:00:00 2001 From: Martin Nagy Date: Fri, 3 Oct 2008 22:27:15 +0200 Subject: Add tests for config.py --- ipalib/tests/test_config.py | 101 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 ipalib/tests/test_config.py (limited to 'ipalib') diff --git a/ipalib/tests/test_config.py b/ipalib/tests/test_config.py new file mode 100644 index 00000000..de7d4c22 --- /dev/null +++ b/ipalib/tests/test_config.py @@ -0,0 +1,101 @@ +# Authors: +# Martin Nagy +# +# 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 + +""" +Unit tests for `ipalib.config` module. +""" + +import types + +from tstutil import raises +from ipalib import config + + +def test_generate_env(): + """ + Test the `config.generate_env` function + """ + + # Make sure we don't overwrite any properties + env = dict( + query_dns = False, + server = ('first', 'second'), + realm = 'myrealm', + ) + d = config.generate_env(env) + assert d['query_dns'] == False + + # Make sure the servers is overwrote properly (that it is still LazyProp) + iter = d['server'].get_value() + assert iter.next() == 'first' + assert iter.next() == 'second' + + +def test_LazyProp(): + """ + Test the `config.LazyProp` class + """ + + def dummy(): + return 1 + + # Basic sanity testing with no initial value + prop = config.LazyProp(dummy) + assert prop.get_value() == 1 + prop.set_value(2) + assert prop.get_value() == 2 + + # Basic sanity testing with initial value + prop = config.LazyProp(dummy, 3) + assert prop.get_value() == 3 + prop.set_value(4) + assert prop.get_value() == 4 + + +def test_LazyIter(): + """ + Test the `config.LazyIter` class + """ + + def dummy(): + yield 1 + yield 2 + + # Basic sanity testing with no initial value + prop = config.LazyIter(dummy) + iter = prop.get_value() + assert iter.next() == 1 + assert iter.next() == 2 + raises(StopIteration, iter.next) + + # Basic sanity testing with initial value + prop = config.LazyIter(dummy, 0) + iter = prop.get_value() + assert iter.next() == 0 + assert iter.next() == 1 + assert iter.next() == 2 + raises(StopIteration, iter.next) + + +def test_read_config(): + """ + Test the `config.read_config` class + """ + + raises(AssertionError, config.read_config, 1) -- cgit From b2b5b904bcc1ab96d5efb992d5630505022d0ecb Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 7 Oct 2008 20:07:16 -0600 Subject: Made package-level docstrings more consistent so they read better in generated documentation --- ipalib/__init__.py | 2 +- ipalib/plugins/b_kerberos.py | 2 +- ipalib/plugins/b_xmlrpc.py | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) (limited to 'ipalib') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index 956e4610..46edb822 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -18,7 +18,7 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ -The IPA Library. +Package containing core library. To learn about the ``ipalib`` library, you should read the code in this order: diff --git a/ipalib/plugins/b_kerberos.py b/ipalib/plugins/b_kerberos.py index 4b3a9a5b..cc820497 100644 --- a/ipalib/plugins/b_kerberos.py +++ b/ipalib/plugins/b_kerberos.py @@ -18,7 +18,7 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ -Kerberos backend. +Backend plugin for Kerberos. This wraps the python-kerberos and python-krbV bindings. """ diff --git a/ipalib/plugins/b_xmlrpc.py b/ipalib/plugins/b_xmlrpc.py index d7cbd856..feb87556 100644 --- a/ipalib/plugins/b_xmlrpc.py +++ b/ipalib/plugins/b_xmlrpc.py @@ -18,9 +18,10 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ -XML-RPC client plugin. +Backend plugin for XML-RPC client. -Lightwieght XML-RPC client using Python standard library xmlrpclib. +This provides a lightwieght XML-RPC client using Python standard library +``xmlrpclib`` module. """ import xmlrpclib -- cgit From 7721443a625b2efd0744ad347c62795e5ba6bb91 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 7 Oct 2008 20:41:15 -0600 Subject: Moved ipalib/tests/ into tests/test_ipalib/ --- ipalib/tests/__init__.py | 22 - ipalib/tests/test_backend.py | 37 -- ipalib/tests/test_cli.py | 138 ----- ipalib/tests/test_config.py | 101 ---- ipalib/tests/test_crud.py | 168 ------- ipalib/tests/test_errors.py | 274 ---------- ipalib/tests/test_frontend.py | 1080 ---------------------------------------- ipalib/tests/test_ipa_types.py | 371 -------------- ipalib/tests/test_plugable.py | 896 --------------------------------- ipalib/tests/test_tstutil.py | 148 ------ ipalib/tests/test_util.py | 49 -- ipalib/tests/tstutil.py | 147 ------ 12 files changed, 3431 deletions(-) delete mode 100644 ipalib/tests/__init__.py delete mode 100644 ipalib/tests/test_backend.py delete mode 100644 ipalib/tests/test_cli.py delete mode 100644 ipalib/tests/test_config.py delete mode 100644 ipalib/tests/test_crud.py delete mode 100644 ipalib/tests/test_errors.py delete mode 100644 ipalib/tests/test_frontend.py delete mode 100644 ipalib/tests/test_ipa_types.py delete mode 100644 ipalib/tests/test_plugable.py delete mode 100644 ipalib/tests/test_tstutil.py delete mode 100644 ipalib/tests/test_util.py delete mode 100644 ipalib/tests/tstutil.py (limited to 'ipalib') diff --git a/ipalib/tests/__init__.py b/ipalib/tests/__init__.py deleted file mode 100644 index d3658c45..00000000 --- a/ipalib/tests/__init__.py +++ /dev/null @@ -1,22 +0,0 @@ -# Authors: -# Jason Gerard DeRose -# -# 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 - -""" -Unit tests for `ipalib` package. -""" diff --git a/ipalib/tests/test_backend.py b/ipalib/tests/test_backend.py deleted file mode 100644 index 967e9fdf..00000000 --- a/ipalib/tests/test_backend.py +++ /dev/null @@ -1,37 +0,0 @@ -# Authors: -# Jason Gerard DeRose -# -# 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 - -""" -Unit tests for `ipalib.backend` module. -""" - -from ipalib import backend, plugable, errors -from tstutil import ClassChecker - - -class test_Backend(ClassChecker): - """ - Test the `backend.Backend` class. - """ - - _cls = backend.Backend - - def test_class(self): - assert self.cls.__bases__ == (plugable.Plugin,) - assert self.cls.__proxy__ is False diff --git a/ipalib/tests/test_cli.py b/ipalib/tests/test_cli.py deleted file mode 100644 index 90c66d41..00000000 --- a/ipalib/tests/test_cli.py +++ /dev/null @@ -1,138 +0,0 @@ -# Authors: -# Jason Gerard DeRose -# -# 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 - -""" -Unit tests for `ipalib.cli` module. -""" - -from tstutil import raises, getitem, no_set, no_del, read_only, ClassChecker -from ipalib import cli, plugable - - -def test_to_cli(): - """ - Tests the `cli.to_cli` function. - """ - f = cli.to_cli - assert f('initialize') == 'initialize' - assert f('user_add') == 'user-add' - - -def test_from_cli(): - """ - Tests the `cli.from_cli` function. - """ - f = cli.from_cli - assert f('initialize') == 'initialize' - assert f('user-add') == 'user_add' - - -def get_cmd_name(i): - return 'cmd_%d' % i - -class DummyCommand(object): - def __init__(self, name): - self.__name = name - - def __get_name(self): - return self.__name - name = property(__get_name) - -class DummyAPI(object): - def __init__(self, cnt): - self.__cmd = plugable.NameSpace(self.__cmd_iter(cnt)) - - def __get_cmd(self): - return self.__cmd - Command = property(__get_cmd) - - def __cmd_iter(self, cnt): - for i in xrange(cnt): - yield DummyCommand(get_cmd_name(i)) - - def finalize(self): - pass - - def register(self, *args, **kw): - pass - - - - - -class test_CLI(ClassChecker): - """ - Tests the `cli.CLI` class. - """ - _cls = cli.CLI - - def test_class(self): - assert type(self.cls.api) is property - - def test_api(self): - """ - Tests the `cli.CLI.api` property. - """ - api = 'the plugable.API instance' - o = self.cls(api) - assert read_only(o, 'api') is api - - def dont_parse(self): - """ - Tests the `cli.CLI.parse` method. - """ - o = self.cls(None) - args = ['hello', 'naughty', 'nurse'] - kw = dict( - first_name='Naughty', - last_name='Nurse', - ) - opts = ['--%s=%s' % (k.replace('_', '-'), v) for (k, v) in kw.items()] - assert o.parse(args + []) == (args, {}) - assert o.parse(opts + []) == ([], kw) - assert o.parse(args + opts) == (args, kw) - assert o.parse(opts + args) == (args, kw) - - def test_mcl(self): - """ - Tests the `cli.CLI.mcl` (Max Command Length) property . - """ - cnt = 100 - api = DummyAPI(cnt) - len(api.Command) == cnt - o = self.cls(api) - assert o.mcl is None - o.build_map() - assert o.mcl == 6 # len('cmd_99') - - def test_dict(self): - """ - Tests the `cli.CLI.__contains__` and `cli.CLI.__getitem__` methods. - """ - cnt = 25 - api = DummyAPI(cnt) - assert len(api.Command) == cnt - o = self.cls(api) - o.build_map() - for cmd in api.Command(): - key = cli.to_cli(cmd.name) - assert key in o - assert o[key] is cmd - assert cmd.name not in o - raises(KeyError, getitem, o, cmd.name) diff --git a/ipalib/tests/test_config.py b/ipalib/tests/test_config.py deleted file mode 100644 index de7d4c22..00000000 --- a/ipalib/tests/test_config.py +++ /dev/null @@ -1,101 +0,0 @@ -# Authors: -# Martin Nagy -# -# 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 - -""" -Unit tests for `ipalib.config` module. -""" - -import types - -from tstutil import raises -from ipalib import config - - -def test_generate_env(): - """ - Test the `config.generate_env` function - """ - - # Make sure we don't overwrite any properties - env = dict( - query_dns = False, - server = ('first', 'second'), - realm = 'myrealm', - ) - d = config.generate_env(env) - assert d['query_dns'] == False - - # Make sure the servers is overwrote properly (that it is still LazyProp) - iter = d['server'].get_value() - assert iter.next() == 'first' - assert iter.next() == 'second' - - -def test_LazyProp(): - """ - Test the `config.LazyProp` class - """ - - def dummy(): - return 1 - - # Basic sanity testing with no initial value - prop = config.LazyProp(dummy) - assert prop.get_value() == 1 - prop.set_value(2) - assert prop.get_value() == 2 - - # Basic sanity testing with initial value - prop = config.LazyProp(dummy, 3) - assert prop.get_value() == 3 - prop.set_value(4) - assert prop.get_value() == 4 - - -def test_LazyIter(): - """ - Test the `config.LazyIter` class - """ - - def dummy(): - yield 1 - yield 2 - - # Basic sanity testing with no initial value - prop = config.LazyIter(dummy) - iter = prop.get_value() - assert iter.next() == 1 - assert iter.next() == 2 - raises(StopIteration, iter.next) - - # Basic sanity testing with initial value - prop = config.LazyIter(dummy, 0) - iter = prop.get_value() - assert iter.next() == 0 - assert iter.next() == 1 - assert iter.next() == 2 - raises(StopIteration, iter.next) - - -def test_read_config(): - """ - Test the `config.read_config` class - """ - - raises(AssertionError, config.read_config, 1) diff --git a/ipalib/tests/test_crud.py b/ipalib/tests/test_crud.py deleted file mode 100644 index 9355f237..00000000 --- a/ipalib/tests/test_crud.py +++ /dev/null @@ -1,168 +0,0 @@ -# Authors: -# Jason Gerard DeRose -# -# 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 - -""" -Unit tests for `ipalib.crud` module. -""" - -from tstutil import read_only, raises, ClassChecker -from ipalib import crud, frontend, plugable, config - -def get_api(): - api = plugable.API( - frontend.Object, - frontend.Method, - frontend.Property, - ) - api.env.update(config.generate_env()) - class user(frontend.Object): - takes_params = ( - 'givenname', - 'sn', - frontend.Param('uid', primary_key=True), - 'initials', - ) - api.register(user) - return api - - -class test_Add(ClassChecker): - """ - Test the `crud.Add` class. - """ - - _cls = crud.Add - - def test_class(self): - assert self.cls.__bases__ == (frontend.Method,) - - def test_options_args(self): - """ - Test `crud.Add.get_args` and `crud.Add.get_options` methods. - """ - api = get_api() - class user_add(self.cls): - pass - api.register(user_add) - api.finalize() - assert list(api.Method.user_add.args) == ['uid'] - assert list(api.Method.user_add.options) == \ - ['givenname', 'sn', 'initials'] - for param in api.Method.user_add.options(): - assert param.required is True - - -class test_Get(ClassChecker): - """ - Test the `crud.Get` class. - """ - - _cls = crud.Get - - def test_class(self): - assert self.cls.__bases__ == (frontend.Method,) - - def test_options_args(self): - """ - Test `crud.Get.get_args` and `crud.Get.get_options` methods. - """ - api = get_api() - class user_get(self.cls): - pass - api.register(user_get) - api.finalize() - assert list(api.Method.user_get.args) == ['uid'] - assert list(api.Method.user_get.options) == [] - - -class test_Del(ClassChecker): - """ - Test the `crud.Del` class. - """ - - _cls = crud.Del - - def test_class(self): - assert self.cls.__bases__ == (frontend.Method,) - - def test_options_args(self): - """ - Test `crud.Del.get_args` and `crud.Del.get_options` methods. - """ - api = get_api() - class user_del(self.cls): - pass - api.register(user_del) - api.finalize() - assert list(api.Method.user_del.args) == ['uid'] - assert list(api.Method.user_del.options) == [] - - -class test_Mod(ClassChecker): - """ - Test the `crud.Mod` class. - """ - - _cls = crud.Mod - - def test_class(self): - assert self.cls.__bases__ == (frontend.Method,) - - def test_options_args(self): - """ - Test `crud.Mod.get_args` and `crud.Mod.get_options` methods. - """ - api = get_api() - class user_mod(self.cls): - pass - api.register(user_mod) - api.finalize() - assert list(api.Method.user_mod.args) == ['uid'] - assert api.Method.user_mod.args[0].required is True - assert list(api.Method.user_mod.options) == \ - ['givenname', 'sn', 'initials'] - for param in api.Method.user_mod.options(): - assert param.required is False - - -class test_Find(ClassChecker): - """ - Test the `crud.Find` class. - """ - - _cls = crud.Find - - def test_class(self): - assert self.cls.__bases__ == (frontend.Method,) - - def test_options_args(self): - """ - Test `crud.Find.get_args` and `crud.Find.get_options` methods. - """ - api = get_api() - class user_find(self.cls): - pass - api.register(user_find) - api.finalize() - assert list(api.Method.user_find.args) == ['uid'] - assert api.Method.user_find.args[0].required is True - assert list(api.Method.user_find.options) == \ - ['givenname', 'sn', 'initials'] - for param in api.Method.user_find.options(): - assert param.required is False diff --git a/ipalib/tests/test_errors.py b/ipalib/tests/test_errors.py deleted file mode 100644 index 7d2df4df..00000000 --- a/ipalib/tests/test_errors.py +++ /dev/null @@ -1,274 +0,0 @@ -# Authors: -# Jason Gerard DeRose -# -# 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 - -""" -Unit tests for `ipalib.errors` module. -""" - -from tstutil import raises, ClassChecker -from ipalib import errors - - -type_format = '%s: need a %r; got %r' - - -def check_TypeError(f, value, type_, name, **kw): - e = raises(TypeError, f, value, type_, name, **kw) - assert e.value is value - assert e.type is type_ - assert e.name is name - assert str(e) == type_format % (name, type_, value) - - -def test_raise_TypeError(): - """ - Tests the `errors.raise_TypeError` function. - """ - f = errors.raise_TypeError - value = 'Hello.' - type_ = unicode - name = 'message' - - check_TypeError(f, value, type_, name) - - # name not an str - fail_name = 42 - e = raises(AssertionError, f, value, type_, fail_name) - assert str(e) == type_format % ('name', str, fail_name), str(e) - - # type_ not a type: - fail_type = unicode() - e = raises(AssertionError, f, value, fail_type, name) - assert str(e) == type_format % ('type_', type, fail_type) - - # type(value) is type_: - fail_value = u'How are you?' - e = raises(AssertionError, f, fail_value, type_, name) - assert str(e) == 'value: %r is a %r' % (fail_value, type_) - - -def test_check_type(): - """ - Tests the `errors.check_type` function. - """ - f = errors.check_type - value = 'How are you?' - type_ = str - name = 'greeting' - - # Should pass: - assert value is f(value, type_, name) - assert None is f(None, type_, name, allow_none=True) - - # Should raise TypeError - check_TypeError(f, None, type_, name) - check_TypeError(f, value, basestring, name) - check_TypeError(f, value, unicode, name) - - # name not an str - fail_name = unicode(name) - e = raises(AssertionError, f, value, type_, fail_name) - assert str(e) == type_format % ('name', str, fail_name) - - # type_ not a type: - fail_type = 42 - e = raises(AssertionError, f, value, fail_type, name) - assert str(e) == type_format % ('type_', type, fail_type) - - # allow_none not a bool: - fail_bool = 0 - e = raises(AssertionError, f, value, type_, name, allow_none=fail_bool) - assert str(e) == type_format % ('allow_none', bool, fail_bool) - - -def test_check_isinstance(): - """ - Tests the `errors.check_isinstance` function. - """ - f = errors.check_isinstance - value = 'How are you?' - type_ = str - name = 'greeting' - - # Should pass: - assert value is f(value, type_, name) - assert value is f(value, basestring, name) - assert None is f(None, type_, name, allow_none=True) - - # Should raise TypeError - check_TypeError(f, None, type_, name) - check_TypeError(f, value, unicode, name) - - # name not an str - fail_name = unicode(name) - e = raises(AssertionError, f, value, type_, fail_name) - assert str(e) == type_format % ('name', str, fail_name) - - # type_ not a type: - fail_type = 42 - e = raises(AssertionError, f, value, fail_type, name) - assert str(e) == type_format % ('type_', type, fail_type) - - # allow_none not a bool: - fail_bool = 0 - e = raises(AssertionError, f, value, type_, name, allow_none=fail_bool) - assert str(e) == type_format % ('allow_none', bool, fail_bool) - - -class test_IPAError(ClassChecker): - """ - Tests the `errors.IPAError` exception. - """ - _cls = errors.IPAError - - def test_class(self): - assert self.cls.__bases__ == (Exception,) - - def test_init(self): - """ - Tests the `errors.IPAError.__init__` method. - """ - args = ('one fish', 'two fish') - e = self.cls(*args) - assert e.args == args - assert self.cls().args == tuple() - - def test_str(self): - """ - Tests the `errors.IPAError.__str__` method. - """ - f = 'The %s color is %s.' - class custom_error(self.cls): - format = f - for args in [('sexiest', 'red'), ('most-batman-like', 'black')]: - e = custom_error(*args) - assert e.args == args - assert str(e) == f % args - - -class test_ValidationError(ClassChecker): - """ - Tests the `errors.ValidationError` exception. - """ - _cls = errors.ValidationError - - def test_class(self): - assert self.cls.__bases__ == (errors.IPAError,) - - def test_init(self): - """ - Tests the `errors.ValidationError.__init__` method. - """ - name = 'login' - value = 'Whatever' - error = 'Must be lowercase.' - for index in (None, 3): - e = self.cls(name, value, error, index=index) - assert e.name is name - assert e.value is value - assert e.error is error - assert e.index is index - assert str(e) == 'invalid %r value %r: %s' % (name, value, error) - # Check that index default is None: - assert self.cls(name, value, error).index is None - # Check non str name raises AssertionError: - raises(AssertionError, self.cls, unicode(name), value, error) - # Check non int index raises AssertionError: - raises(AssertionError, self.cls, name, value, error, index=5.0) - # Check negative index raises AssertionError: - raises(AssertionError, self.cls, name, value, error, index=-2) - - -class test_ConversionError(ClassChecker): - """ - Tests the `errors.ConversionError` exception. - """ - _cls = errors.ConversionError - - def test_class(self): - assert self.cls.__bases__ == (errors.ValidationError,) - - def test_init(self): - """ - Tests the `errors.ConversionError.__init__` method. - """ - name = 'some_arg' - value = '42.0' - class type_(object): - conversion_error = 'Not an integer' - for index in (None, 7): - e = self.cls(name, value, type_, index=index) - assert e.name is name - assert e.value is value - assert e.type is type_ - assert e.error is type_.conversion_error - assert e.index is index - assert str(e) == 'invalid %r value %r: %s' % (name, value, - type_.conversion_error) - # Check that index default is None: - assert self.cls(name, value, type_).index is None - - -class test_RuleError(ClassChecker): - """ - Tests the `errors.RuleError` exception. - """ - _cls = errors.RuleError - - def test_class(self): - assert self.cls.__bases__ == (errors.ValidationError,) - - def test_init(self): - """ - Tests the `errors.RuleError.__init__` method. - """ - name = 'whatever' - value = 'The smallest weird number.' - def my_rule(value): - return 'Value is bad.' - error = my_rule(value) - for index in (None, 42): - e = self.cls(name, value, error, my_rule, index=index) - assert e.name is name - assert e.value is value - assert e.error is error - assert e.rule is my_rule - # Check that index default is None: - assert self.cls(name, value, error, my_rule).index is None - - -class test_RequirementError(ClassChecker): - """ - Tests the `errors.RequirementError` exception. - """ - _cls = errors.RequirementError - - def test_class(self): - assert self.cls.__bases__ == (errors.ValidationError,) - - def test_init(self): - """ - Tests the `errors.RequirementError.__init__` method. - """ - name = 'givenname' - e = self.cls(name) - assert e.name is name - assert e.value is None - assert e.error == 'Required' - assert e.index is None diff --git a/ipalib/tests/test_frontend.py b/ipalib/tests/test_frontend.py deleted file mode 100644 index c70cc00d..00000000 --- a/ipalib/tests/test_frontend.py +++ /dev/null @@ -1,1080 +0,0 @@ -# Authors: -# Jason Gerard DeRose -# -# 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 - -""" -Unit tests for `ipalib.frontend` module. -""" - -from tstutil import raises, getitem, no_set, no_del, read_only, ClassChecker -from tstutil import check_TypeError -from ipalib import frontend, backend, plugable, errors, ipa_types, config - - -def test_RULE_FLAG(): - assert frontend.RULE_FLAG == 'validation_rule' - - -def test_rule(): - """ - Tests the `frontend.rule` function. - """ - flag = frontend.RULE_FLAG - rule = frontend.rule - def my_func(): - pass - assert not hasattr(my_func, flag) - rule(my_func) - assert getattr(my_func, flag) is True - @rule - def my_func2(): - pass - assert getattr(my_func2, flag) is True - - -def test_is_rule(): - """ - Tests the `frontend.is_rule` function. - """ - is_rule = frontend.is_rule - flag = frontend.RULE_FLAG - - class no_call(object): - def __init__(self, value): - if value is not None: - assert value in (True, False) - setattr(self, flag, value) - - class call(no_call): - def __call__(self): - pass - - assert is_rule(call(True)) - assert not is_rule(no_call(True)) - assert not is_rule(call(False)) - assert not is_rule(call(None)) - - -class test_DefaultFrom(ClassChecker): - """ - Tests the `frontend.DefaultFrom` class. - """ - _cls = frontend.DefaultFrom - - def test_class(self): - assert self.cls.__bases__ == (plugable.ReadOnly,) - - def test_init(self): - """ - Tests the `frontend.DefaultFrom.__init__` method. - """ - def callback(*args): - return args - keys = ('givenname', 'sn') - o = self.cls(callback, *keys) - assert read_only(o, 'callback') is callback - assert read_only(o, 'keys') == keys - lam = lambda first, last: first[0] + last - o = self.cls(lam) - assert read_only(o, 'keys') == ('first', 'last') - - def test_call(self): - """ - Tests the `frontend.DefaultFrom.__call__` method. - """ - def callback(givenname, sn): - return givenname[0] + sn[0] - keys = ('givenname', 'sn') - o = self.cls(callback, *keys) - kw = dict( - givenname='John', - sn='Public', - hello='world', - ) - assert o(**kw) == 'JP' - assert o() is None - for key in ('givenname', 'sn'): - kw_copy = dict(kw) - del kw_copy[key] - assert o(**kw_copy) is None - o = self.cls(lambda first, last: first[0] + last) - assert o(first='john', last='doe') == 'jdoe' - assert o(first='', last='doe') is None - assert o(one='john', two='doe') is None - - -def test_parse_param_spec(): - """ - Test the `frontend.parse_param_spec` function. - """ - f = frontend.parse_param_spec - - assert f('name') == ('name', dict(required=True, multivalue=False)) - assert f('name?') == ('name', dict(required=False, multivalue=False)) - assert f('name*') == ('name', dict(required=False, multivalue=True)) - assert f('name+') == ('name', dict(required=True, multivalue=True)) - - -class test_Param(ClassChecker): - """ - Test the `frontend.Param` class. - """ - _cls = frontend.Param - - def test_class(self): - assert self.cls.__bases__ == (plugable.ReadOnly,) - - def test_init(self): - """ - Test the `frontend.Param.__init__` method. - """ - name = 'sn' - o = self.cls(name) - assert o.__islocked__() is True - - # Test default values - assert read_only(o, 'name') is name - assert isinstance(read_only(o, 'type'), ipa_types.Unicode) - assert read_only(o, 'doc') == '' - assert read_only(o, 'required') is True - assert read_only(o, 'multivalue') is False - assert read_only(o, 'default') is None - assert read_only(o, 'default_from') is None - assert read_only(o, 'rules') == tuple() - assert len(read_only(o, 'all_rules')) == 1 - assert read_only(o, 'primary_key') is False - - # Test all kw args: - t = ipa_types.Int() - assert self.cls(name, type=t).type is t - assert self.cls(name, doc='the doc').doc == 'the doc' - assert self.cls(name, required=False).required is False - assert self.cls(name, multivalue=True).multivalue is True - assert self.cls(name, default=u'Hello').default == u'Hello' - df = frontend.DefaultFrom(lambda f, l: f + l, - 'first', 'last', - ) - lam = lambda first, last: first + last - for cb in (df, lam): - o = self.cls(name, default_from=cb) - assert type(o.default_from) is frontend.DefaultFrom - assert o.default_from.keys == ('first', 'last') - assert o.default_from.callback('butt', 'erfly') == 'butterfly' - rules = (lambda whatever: 'Not okay!',) - o = self.cls(name, rules=rules) - assert o.rules is rules - assert o.all_rules[1:] == rules - assert self.cls(name, primary_key=True).primary_key is True - - # Test default type_: - o = self.cls(name) - assert isinstance(o.type, ipa_types.Unicode) - - # Test param spec parsing: - o = self.cls('name?') - assert o.name == 'name' - assert o.required is False - assert o.multivalue is False - - o = self.cls('name*') - assert o.name == 'name' - assert o.required is False - assert o.multivalue is True - - o = self.cls('name+') - assert o.name == 'name' - assert o.required is True - assert o.multivalue is True - - e = raises(TypeError, self.cls, name, whatever=True, another=False) - assert str(e) == \ - 'Param.__init__() takes no such kwargs: another, whatever' - - def test_clone(self): - """ - Test the `frontend.Param.__clone__` method. - """ - def compare(o, kw): - for (k, v) in kw.iteritems(): - assert getattr(o, k) == v, (k, v, getattr(o, k)) - default = dict( - required=False, - multivalue=False, - default=None, - default_from=None, - rules=tuple(), - ) - name = 'hair_color?' - type_ = ipa_types.Int() - o = self.cls(name, type=type_) - compare(o, default) - - override = dict(multivalue=True, default=42) - d = dict(default) - d.update(override) - clone = o.__clone__(**override) - assert clone.name == 'hair_color' - assert clone.type is o.type - compare(clone, d) - - def test_convert(self): - """ - Test the `frontend.Param.convert` method. - """ - name = 'some_number' - type_ = ipa_types.Int() - okay = (7, 7L, 7.0, ' 7 ') - fail = ('7.0', '7L', 'whatever', object) - none = (None, '', u'', tuple(), []) - - # Scenario 1: multivalue=False - o = self.cls(name, type=type_) - for n in none: - assert o.convert(n) is None - for value in okay: - new = o.convert(value) - assert new == 7 - assert type(new) is int - for value in fail: - e = raises(errors.ConversionError, o.convert, value) - assert e.name is name - assert e.value is value - assert e.error is type_.conversion_error - assert e.index is None - - # Scenario 2: multivalue=True - o = self.cls(name, type=type_, multivalue=True) - for n in none: - assert o.convert(n) is None - for value in okay: - assert o.convert((value,)) == (7,) - assert o.convert([value]) == (7,) - assert o.convert(okay) == tuple(int(v) for v in okay) - cnt = 5 - for value in fail: - for i in xrange(cnt): - others = list(7 for x in xrange(cnt)) - others[i] = value - for v in [tuple(others), list(others)]: - e = raises(errors.ConversionError, o.convert, v) - assert e.name is name - assert e.value is value - assert e.error is type_.conversion_error - assert e.index == i - - def test_normalize(self): - """ - Test the `frontend.Param.normalize` method. - """ - name = 'sn' - callback = lambda value: value.lower() - values = (None, u'Hello', (u'Hello',), 'hello', ['hello']) - none = (None, '', u'', tuple(), []) - - # Scenario 1: multivalue=False, normalize=None - o = self.cls(name) - for v in values: - # When normalize=None, value is returned, no type checking: - assert o.normalize(v) is v - - # Scenario 2: multivalue=False, normalize=callback - o = self.cls(name, normalize=callback) - for v in (u'Hello', u'hello', 'Hello'): # Okay - assert o.normalize(v) == 'hello' - for v in [None, 42, (u'Hello',)]: # Not basestring - assert o.normalize(v) is v - for n in none: - assert o.normalize(n) is None - - # Scenario 3: multivalue=True, normalize=None - o = self.cls(name, multivalue=True) - for v in values: - # When normalize=None, value is returned, no type checking: - assert o.normalize(v) is v - - # Scenario 4: multivalue=True, normalize=callback - o = self.cls(name, multivalue=True, normalize=callback) - assert o.normalize([]) is None - assert o.normalize(tuple()) is None - for value in [(u'Hello',), (u'hello',), 'Hello', ['Hello']]: # Okay - assert o.normalize(value) == (u'hello',) - fail = 42 # Not basestring - for v in [[fail], (u'hello', fail)]: # Non basestring member - assert o.normalize(v) == tuple(v) - for n in none: - assert o.normalize(n) is None - - def test_validate(self): - """ - Tests the `frontend.Param.validate` method. - """ - name = 'sn' - type_ = ipa_types.Unicode() - def case_rule(value): - if not value.islower(): - return 'Must be lower case' - my_rules = (case_rule,) - okay = u'whatever' - fail_case = u'Whatever' - fail_type = 'whatever' - - # Scenario 1: multivalue=False - o = self.cls(name, type=type_, rules=my_rules) - assert o.rules == my_rules - assert o.all_rules == (type_.validate, case_rule) - o.validate(okay) - e = raises(errors.RuleError, o.validate, fail_case) - assert e.name is name - assert e.value is fail_case - assert e.error == 'Must be lower case' - assert e.rule is case_rule - assert e.index is None - check_TypeError(fail_type, unicode, 'value', o.validate, fail_type) - - ## Scenario 2: multivalue=True - o = self.cls(name, type=type_, multivalue=True, rules=my_rules) - o.validate((okay,)) - cnt = 5 - for i in xrange(cnt): - others = list(okay for x in xrange(cnt)) - others[i] = fail_case - value = tuple(others) - e = raises(errors.RuleError, o.validate, value) - assert e.name is name - assert e.value is fail_case - assert e.error == 'Must be lower case' - assert e.rule is case_rule - assert e.index == i - for not_tuple in (okay, [okay]): - check_TypeError(not_tuple, tuple, 'value', o.validate, not_tuple) - for has_str in [(fail_type,), (okay, fail_type)]: - check_TypeError(fail_type, unicode, 'value', o.validate, has_str) - - def test_get_default(self): - """ - Tests the `frontend.Param.get_default` method. - """ - name = 'greeting' - default = u'Hello, world!' - default_from = frontend.DefaultFrom( - lambda first, last: u'Hello, %s %s!' % (first, last), - 'first', 'last' - ) - - # Scenario 1: multivalue=False - o = self.cls(name, - default=default, - default_from=default_from, - ) - assert o.default is default - assert o.default_from is default_from - assert o.get_default() == default - assert o.get_default(first='John', last='Doe') == 'Hello, John Doe!' - - # Scenario 2: multivalue=True - default = (default,) - o = self.cls(name, - default=default, - default_from=default_from, - multivalue=True, - ) - assert o.default is default - assert o.default_from is default_from - assert o.get_default() == default - assert o.get_default(first='John', last='Doe') == ('Hello, John Doe!',) - - def test_get_value(self): - """ - Tests the `frontend.Param.get_values` method. - """ - name = 'status' - values = (u'Active', u'Inactive') - o = self.cls(name, type=ipa_types.Unicode()) - assert o.get_values() == tuple() - o = self.cls(name, type=ipa_types.Enum(*values)) - assert o.get_values() == values - - -def test_create_param(): - """ - Test the `frontend.create_param` function. - """ - f = frontend.create_param - for name in ['arg', 'arg?', 'arg*', 'arg+']: - o = f(name) - assert type(o) is frontend.Param - assert type(o.type) is ipa_types.Unicode - assert o.name == 'arg' - assert f(o) is o - o = f('arg') - assert o.required is True - assert o.multivalue is False - o = f('arg?') - assert o.required is False - assert o.multivalue is False - o = f('arg*') - assert o.required is False - assert o.multivalue is True - o = f('arg+') - assert o.required is True - assert o.multivalue is True - - -class test_Command(ClassChecker): - """ - Tests the `frontend.Command` class. - """ - _cls = frontend.Command - - def get_subcls(self): - class Rule(object): - def __init__(self, name): - self.name = name - - def __call__(self, value): - if value != self.name: - return 'must equal %s' % self.name - - default_from = frontend.DefaultFrom( - lambda arg: arg, - 'default_from' - ) - normalize = lambda value: value.lower() - - class example(self.cls): - takes_options = ( - frontend.Param('option0', - normalize=normalize, - default_from=default_from, - rules=(Rule('option0'),) - ), - frontend.Param('option1', - normalize=normalize, - default_from=default_from, - rules=(Rule('option1'),), - required=True, - ), - ) - return example - - def get_instance(self, args=tuple(), options=tuple()): - """ - Helper method used to test args and options. - """ - class example(self.cls): - takes_args = args - takes_options = options - o = example() - o.finalize() - return o - - def test_class(self): - assert self.cls.__bases__ == (plugable.Plugin,) - assert self.cls.takes_options == tuple() - assert self.cls.takes_args == tuple() - - def test_get_args(self): - """ - Tests the `frontend.Command.get_args` method. - """ - assert list(self.cls().get_args()) == [] - args = ('login', 'stuff') - o = self.get_instance(args=args) - assert o.get_args() is args - - def test_get_options(self): - """ - Tests the `frontend.Command.get_options` method. - """ - assert list(self.cls().get_options()) == [] - options = ('verbose', 'debug') - o = self.get_instance(options=options) - assert o.get_options() is options - - def test_args(self): - """ - Tests the ``Command.args`` instance attribute. - """ - assert 'args' in self.cls.__public__ # Public - assert self.cls().args is None - o = self.cls() - o.finalize() - assert type(o.args) is plugable.NameSpace - assert len(o.args) == 0 - args = ('destination', 'source?') - ns = self.get_instance(args=args).args - assert type(ns) is plugable.NameSpace - assert len(ns) == len(args) - assert list(ns) == ['destination', 'source'] - assert type(ns.destination) is frontend.Param - assert type(ns.source) is frontend.Param - assert ns.destination.required is True - assert ns.destination.multivalue is False - assert ns.source.required is False - assert ns.source.multivalue is False - - # Test TypeError: - e = raises(TypeError, self.get_instance, args=(u'whatever',)) - assert str(e) == \ - 'create_param() takes %r or %r; got %r' % (str, frontend.Param, u'whatever') - - # Test ValueError, required after optional: - e = raises(ValueError, self.get_instance, args=('arg1?', 'arg2')) - assert str(e) == 'arg2: required argument after optional' - - # Test ValueError, scalar after multivalue: - e = raises(ValueError, self.get_instance, args=('arg1+', 'arg2')) - assert str(e) == 'arg2: only final argument can be multivalue' - - def test_max_args(self): - """ - Test the ``Command.max_args`` instance attribute. - """ - o = self.get_instance() - assert o.max_args == 0 - o = self.get_instance(args=('one?',)) - assert o.max_args == 1 - o = self.get_instance(args=('one', 'two?')) - assert o.max_args == 2 - o = self.get_instance(args=('one', 'multi+',)) - assert o.max_args is None - o = self.get_instance(args=('one', 'multi*',)) - assert o.max_args is None - - def test_options(self): - """ - Tests the ``Command.options`` instance attribute. - """ - assert 'options' in self.cls.__public__ # Public - assert self.cls().options is None - o = self.cls() - o.finalize() - assert type(o.options) is plugable.NameSpace - assert len(o.options) == 0 - options = ('target', 'files*') - ns = self.get_instance(options=options).options - assert type(ns) is plugable.NameSpace - assert len(ns) == len(options) - assert list(ns) == ['target', 'files'] - assert type(ns.target) is frontend.Param - assert type(ns.files) is frontend.Param - assert ns.target.required is True - assert ns.target.multivalue is False - assert ns.files.required is False - assert ns.files.multivalue is True - - def test_convert(self): - """ - Tests the `frontend.Command.convert` method. - """ - assert 'convert' in self.cls.__public__ # Public - kw = dict( - option0='option0', - option1='option1', - ) - expected = dict(kw) - expected.update(dict(option0=u'option0', option1=u'option1')) - o = self.subcls() - o.finalize() - for (key, value) in o.convert(**kw).iteritems(): - v = expected[key] - assert value == v - assert type(value) is type(v) - - def test_normalize(self): - """ - Tests the `frontend.Command.normalize` method. - """ - assert 'normalize' in self.cls.__public__ # Public - kw = dict( - option0=u'OPTION0', - option1=u'OPTION1', - ) - norm = dict((k, v.lower()) for (k, v) in kw.items()) - sub = self.subcls() - sub.finalize() - assert sub.normalize(**kw) == norm - - def test_get_default(self): - """ - Tests the `frontend.Command.get_default` method. - """ - assert 'get_default' in self.cls.__public__ # Public - no_fill = dict( - option0='value0', - option1='value1', - whatever='hello world', - ) - fill = dict( - default_from='the default', - ) - default = dict( - option0='the default', - option1='the default', - ) - sub = self.subcls() - sub.finalize() - assert sub.get_default(**no_fill) == {} - assert sub.get_default(**fill) == default - - def test_validate(self): - """ - Tests the `frontend.Command.validate` method. - """ - assert 'validate' in self.cls.__public__ # Public - - sub = self.subcls() - sub.finalize() - - # Check with valid args - okay = dict( - option0=u'option0', - option1=u'option1', - another_option='some value', - ) - sub.validate(**okay) - - # Check with an invalid arg - fail = dict(okay) - fail['option0'] = u'whatever' - e = raises(errors.RuleError, sub.validate, **fail) - assert e.name == 'option0' - assert e.value == u'whatever' - assert e.error == 'must equal option0' - assert e.rule.__class__.__name__ == 'Rule' - assert e.index is None - - # Check with a missing required arg - fail = dict(okay) - fail.pop('option1') - e = raises(errors.RequirementError, sub.validate, **fail) - assert e.name == 'option1' - assert e.value is None - assert e.index is None - - def test_execute(self): - """ - Tests the `frontend.Command.execute` method. - """ - assert 'execute' in self.cls.__public__ # Public - - def test_args_to_kw(self): - """ - Test the `frontend.Command.args_to_kw` method. - """ - assert 'args_to_kw' in self.cls.__public__ # Public - o = self.get_instance(args=('one', 'two?')) - assert o.args_to_kw(1) == dict(one=1) - assert o.args_to_kw(1, 2) == dict(one=1, two=2) - - o = self.get_instance(args=('one', 'two*')) - assert o.args_to_kw(1) == dict(one=1) - assert o.args_to_kw(1, 2) == dict(one=1, two=(2,)) - assert o.args_to_kw(1, 2, 3) == dict(one=1, two=(2, 3)) - - o = self.get_instance(args=('one', 'two+')) - assert o.args_to_kw(1) == dict(one=1) - assert o.args_to_kw(1, 2) == dict(one=1, two=(2,)) - assert o.args_to_kw(1, 2, 3) == dict(one=1, two=(2, 3)) - - o = self.get_instance() - e = raises(errors.ArgumentError, o.args_to_kw, 1) - assert str(e) == 'example takes no arguments' - - o = self.get_instance(args=('one?',)) - e = raises(errors.ArgumentError, o.args_to_kw, 1, 2) - assert str(e) == 'example takes at most 1 argument' - - o = self.get_instance(args=('one', 'two?')) - e = raises(errors.ArgumentError, o.args_to_kw, 1, 2, 3) - assert str(e) == 'example takes at most 2 arguments' - - def test_kw_to_args(self): - """ - Tests the `frontend.Command.kw_to_args` method. - """ - assert 'kw_to_args' in self.cls.__public__ # Public - o = self.get_instance(args=('one', 'two?')) - assert o.kw_to_args() == (None, None) - assert o.kw_to_args(whatever='hello') == (None, None) - assert o.kw_to_args(one='the one') == ('the one', None) - assert o.kw_to_args(two='the two') == (None, 'the two') - assert o.kw_to_args(whatever='hello', two='Two', one='One') == \ - ('One', 'Two') - - def test_run(self): - """ - Test the `frontend.Command.run` method. - """ - class my_cmd(self.cls): - def execute(self, *args, **kw): - return ('execute', args, kw) - - def forward(self, *args, **kw): - return ('forward', args, kw) - - args = ('Hello,', 'world,') - kw = dict(how_are='you', on_this='fine day?') - - # Test in server context: - api = plugable.API(self.cls) - api.env.update(dict(server_context=True)) - api.finalize() - o = my_cmd() - o.set_api(api) - assert o.run.im_func is self.cls.run.im_func - assert ('execute', args, kw) == o.run(*args, **kw) - assert o.run.im_func is my_cmd.execute.im_func - - # Test in non-server context - api = plugable.API(self.cls) - api.env.update(dict(server_context=False)) - api.finalize() - o = my_cmd() - o.set_api(api) - assert o.run.im_func is self.cls.run.im_func - assert ('forward', args, kw) == o.run(*args, **kw) - assert o.run.im_func is my_cmd.forward.im_func - - -class test_Object(ClassChecker): - """ - Test the `frontend.Object` class. - """ - _cls = frontend.Object - - def test_class(self): - assert self.cls.__bases__ == (plugable.Plugin,) - assert self.cls.backend is None - assert self.cls.methods is None - assert self.cls.properties is None - assert self.cls.params is None - assert self.cls.params_minus_pk is None - assert self.cls.takes_params == tuple() - - def test_init(self): - """ - Test the `frontend.Object.__init__` method. - """ - o = self.cls() - assert o.backend is None - assert o.methods is None - assert o.properties is None - assert o.params is None - assert o.params_minus_pk is None - assert o.properties is None - - def test_set_api(self): - """ - Test the `frontend.Object.set_api` method. - """ - # Setup for test: - class DummyAttribute(object): - def __init__(self, obj_name, attr_name, name=None): - self.obj_name = obj_name - self.attr_name = attr_name - if name is None: - self.name = '%s_%s' % (obj_name, attr_name) - else: - self.name = name - self.param = frontend.create_param(attr_name) - - def __clone__(self, attr_name): - return self.__class__( - self.obj_name, - self.attr_name, - getattr(self, attr_name) - ) - - def get_attributes(cnt, format): - for name in ['other', 'user', 'another']: - for i in xrange(cnt): - yield DummyAttribute(name, format % i) - - cnt = 10 - formats = dict( - methods='method_%d', - properties='property_%d', - ) - - - _d = dict( - Method=plugable.NameSpace( - get_attributes(cnt, formats['methods']) - ), - Property=plugable.NameSpace( - get_attributes(cnt, formats['properties']) - ), - ) - api = plugable.MagicDict(_d) - assert len(api.Method) == cnt * 3 - assert len(api.Property) == cnt * 3 - - class user(self.cls): - pass - - # Actually perform test: - o = user() - o.set_api(api) - assert read_only(o, 'api') is api - for name in ['methods', 'properties']: - namespace = getattr(o, name) - assert isinstance(namespace, plugable.NameSpace) - assert len(namespace) == cnt - f = formats[name] - for i in xrange(cnt): - attr_name = f % i - attr = namespace[attr_name] - assert isinstance(attr, DummyAttribute) - assert attr is getattr(namespace, attr_name) - assert attr.obj_name == 'user' - assert attr.attr_name == attr_name - assert attr.name == attr_name - - # Test params instance attribute - o = self.cls() - o.set_api(api) - ns = o.params - assert type(ns) is plugable.NameSpace - assert len(ns) == 0 - class example(self.cls): - takes_params = ('banana', 'apple') - o = example() - o.set_api(api) - ns = o.params - assert type(ns) is plugable.NameSpace - assert len(ns) == 2, repr(ns) - assert list(ns) == ['banana', 'apple'] - for p in ns(): - assert type(p) is frontend.Param - assert p.required is True - assert p.multivalue is False - - def test_primary_key(self): - """ - Test the `frontend.Object.primary_key` attribute. - """ - api = plugable.API( - frontend.Method, - frontend.Property, - ) - api.env.update(config.generate_env()) - api.finalize() - - # Test with no primary keys: - class example1(self.cls): - takes_params = ( - 'one', - 'two', - ) - o = example1() - o.set_api(api) - assert o.primary_key is None - assert o.params_minus_pk is None - - # Test with 1 primary key: - class example2(self.cls): - takes_params = ( - 'one', - 'two', - frontend.Param('three', - primary_key=True, - ), - 'four', - ) - o = example2() - o.set_api(api) - pk = o.primary_key - assert isinstance(pk, frontend.Param) - assert pk.name == 'three' - assert pk.primary_key is True - assert o.params[2] is o.primary_key - assert isinstance(o.params_minus_pk, plugable.NameSpace) - assert list(o.params_minus_pk) == ['one', 'two', 'four'] - - # Test with multiple primary_key: - class example3(self.cls): - takes_params = ( - frontend.Param('one', primary_key=True), - frontend.Param('two', primary_key=True), - 'three', - frontend.Param('four', primary_key=True), - ) - o = example3() - e = raises(ValueError, o.set_api, api) - assert str(e) == \ - 'example3 (Object) has multiple primary keys: one, two, four' - - def test_backend(self): - """ - Test the `frontend.Object.backend` attribute. - """ - api = plugable.API( - frontend.Object, - frontend.Method, - frontend.Property, - backend.Backend, - ) - api.env.update(config.generate_env()) - class ldap(backend.Backend): - whatever = 'It worked!' - api.register(ldap) - class user(frontend.Object): - backend_name = 'ldap' - api.register(user) - api.finalize() - b = api.Object.user.backend - assert isinstance(b, ldap) - assert b.whatever == 'It worked!' - - -class test_Attribute(ClassChecker): - """ - Tests the `frontend.Attribute` class. - """ - _cls = frontend.Attribute - - def test_class(self): - assert self.cls.__bases__ == (plugable.Plugin,) - assert type(self.cls.obj) is property - assert type(self.cls.obj_name) is property - assert type(self.cls.attr_name) is property - - def test_init(self): - """ - Tests the `frontend.Attribute.__init__` method. - """ - class user_add(self.cls): - pass - o = user_add() - assert read_only(o, 'obj') is None - assert read_only(o, 'obj_name') == 'user' - assert read_only(o, 'attr_name') == 'add' - - def test_set_api(self): - """ - Tests the `frontend.Attribute.set_api` method. - """ - user_obj = 'The user frontend.Object instance' - class api(object): - Object = dict(user=user_obj) - class user_add(self.cls): - pass - o = user_add() - assert read_only(o, 'api') is None - assert read_only(o, 'obj') is None - o.set_api(api) - assert read_only(o, 'api') is api - assert read_only(o, 'obj') is user_obj - - -class test_Method(ClassChecker): - """ - Test the `frontend.Method` class. - """ - _cls = frontend.Method - - def test_class(self): - assert self.cls.__bases__ == (frontend.Attribute, frontend.Command) - assert self.cls.implements(frontend.Command) - assert self.cls.implements(frontend.Attribute) - - def test_init(self): - """ - Test the `frontend.Method.__init__` method. - """ - class user_add(self.cls): - pass - o = user_add() - assert o.name == 'user_add' - assert o.obj_name == 'user' - assert o.attr_name == 'add' - assert frontend.Command.implemented_by(o) - assert frontend.Attribute.implemented_by(o) - - -class test_Property(ClassChecker): - """ - Tests the `frontend.Property` class. - """ - _cls = frontend.Property - - def get_subcls(self): - class user_givenname(self.cls): - 'User first name' - - @frontend.rule - def rule0_lowercase(self, value): - if not value.islower(): - return 'Must be lowercase' - return user_givenname - - def test_class(self): - assert self.cls.__bases__ == (frontend.Attribute,) - assert isinstance(self.cls.type, ipa_types.Unicode) - assert self.cls.required is False - assert self.cls.multivalue is False - assert self.cls.default is None - assert self.cls.default_from is None - assert self.cls.normalize is None - - def test_init(self): - """ - Tests the `frontend.Property.__init__` method. - """ - o = self.subcls() - assert len(o.rules) == 1 - assert o.rules[0].__name__ == 'rule0_lowercase' - param = o.param - assert isinstance(param, frontend.Param) - assert param.name == 'givenname' - assert param.doc == 'User first name' - - -class test_Application(ClassChecker): - """ - Tests the `frontend.Application` class. - """ - _cls = frontend.Application - - def test_class(self): - assert self.cls.__bases__ == (frontend.Command,) - assert type(self.cls.application) is property - - def test_application(self): - """ - Tests the `frontend.Application.application` property. - """ - assert 'application' in self.cls.__public__ # Public - assert 'set_application' in self.cls.__public__ # Public - app = 'The external application' - class example(self.cls): - 'A subclass' - for o in (self.cls(), example()): - assert read_only(o, 'application') is None - e = raises(TypeError, o.set_application, None) - assert str(e) == ( - '%s.application cannot be None' % o.__class__.__name__ - ) - o.set_application(app) - assert read_only(o, 'application') is app - e = raises(AttributeError, o.set_application, app) - assert str(e) == ( - '%s.application can only be set once' % o.__class__.__name__ - ) - assert read_only(o, 'application') is app diff --git a/ipalib/tests/test_ipa_types.py b/ipalib/tests/test_ipa_types.py deleted file mode 100644 index b8e996a7..00000000 --- a/ipalib/tests/test_ipa_types.py +++ /dev/null @@ -1,371 +0,0 @@ -# Authors: -# Jason Gerard DeRose -# -# 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 - -""" -Unit tests for `ipalib.ipa_types` module. -""" - -from tstutil import raises, getitem, no_set, no_del, read_only, ClassChecker -from ipalib import ipa_types, errors, plugable - - -def test_check_min_max(): - """ - Tests the `ipa_types.check_min_max` function. - """ - f = ipa_types.check_min_max - okay = [ - (None, -5), - (-20, None), - (-20, -5), - ] - for (l, h) in okay: - assert f(l, h, 'low', 'high') is None - fail_type = [ - '10', - 10.0, - 10L, - True, - False, - object, - ] - for value in fail_type: - e = raises(TypeError, f, value, None, 'low', 'high') - assert str(e) == 'low must be an int or None, got: %r' % value - e = raises(TypeError, f, None, value, 'low', 'high') - assert str(e) == 'high must be an int or None, got: %r' % value - fail_value = [ - (10, 5), - (-5, -10), - (5, -10), - ] - for (l, h) in fail_value: - e = raises(ValueError, f, l, h, 'low', 'high') - assert str(e) == 'low > high: low=%r, high=%r' % (l, h) - - -class test_Type(ClassChecker): - """ - Tests the `ipa_types.Type` class. - """ - _cls = ipa_types.Type - - def test_class(self): - assert self.cls.__bases__ == (plugable.ReadOnly,) - - def test_init(self): - okay = (bool, int, float, unicode) - for t in okay: - o = self.cls(t) - assert o.__islocked__() is True - assert read_only(o, 'type') is t - assert read_only(o, 'name') is 'Type' - - type_errors = (None, True, 8, 8.0, u'hello') - for t in type_errors: - e = raises(TypeError, self.cls, t) - assert str(e) == '%r is not %r' % (type(t), type) - - value_errors = (long, complex, str, tuple, list, dict, set, frozenset) - for t in value_errors: - e = raises(ValueError, self.cls, t) - assert str(e) == 'not an allowed type: %r' % t - - def test_validate(self): - o = self.cls(unicode) - for value in (None, u'Hello', 'Hello', 42, False): - assert o.validate(value) is None - - -class test_Bool(ClassChecker): - _cls = ipa_types.Bool - - def test_class(self): - assert self.cls.__bases__ == (ipa_types.Type,) - - def test_init(self): - o = self.cls() - assert o.__islocked__() is True - assert read_only(o, 'type') is bool - assert read_only(o, 'name') == 'Bool' - assert read_only(o, 'true') == 'Yes' - assert read_only(o, 'false') == 'No' - - keys = ('true', 'false') - val = 'some value' - for key in keys: - # Check that kwarg sets appropriate attribute: - o = self.cls(**{key: val}) - assert read_only(o, key) is val - # Check that None raises TypeError: - e = raises(TypeError, self.cls, **{key: None}) - assert str(e) == '`%s` cannot be None' % key - - # Check that ValueError is raise if true == false: - e = raises(ValueError, self.cls, true=1L, false=1.0) - assert str(e) == 'cannot be equal: true=1L, false=1.0' - - def test_call(self): - o = self.cls() - assert o(True) is True - assert o('Yes') is True - assert o(False) is False - assert o('No') is False - for value in (0, 1, 'True', 'False', 'yes', 'no'): - # value is not be converted, so None is returned - assert o(value) is None - - -class test_Int(ClassChecker): - _cls = ipa_types.Int - - def test_class(self): - assert self.cls.__bases__ == (ipa_types.Type,) - - def test_init(self): - o = self.cls() - assert o.__islocked__() is True - assert read_only(o, 'type') is int - assert read_only(o, 'name') == 'Int' - assert read_only(o, 'min_value') is None - assert read_only(o, 'max_value') is None - - okay = [ - (None, -5), - (-20, None), - (-20, -5), - ] - for (l, h) in okay: - o = self.cls(min_value=l, max_value=h) - assert o.min_value is l - assert o.max_value is h - - fail_type = [ - '10', - 10.0, - 10L, - True, - False, - object, - ] - for value in fail_type: - e = raises(TypeError, self.cls, min_value=value) - assert str(e) == ( - 'min_value must be an int or None, got: %r' % value - ) - e = raises(TypeError, self.cls, max_value=value) - assert str(e) == ( - 'max_value must be an int or None, got: %r' % value - ) - - fail_value = [ - (10, 5), - (5, -5), - (-5, -10), - ] - for (l, h) in fail_value: - e = raises(ValueError, self.cls, min_value=l, max_value=h) - assert str(e) == ( - 'min_value > max_value: min_value=%d, max_value=%d' % (l, h) - ) - - def test_call(self): - o = self.cls() - - # Test calling with None - e = raises(TypeError, o, None) - assert str(e) == 'value cannot be None' - - # Test with values that can be converted: - okay = [ - 3, - '3', - ' 3 ', - 3L, - 3.0, - ] - for value in okay: - assert o(value) == 3 - - # Test with values that cannot be converted: - fail = [ - object, - '3.0', - '3L', - 'whatever', - ] - for value in fail: - assert o(value) is None - - - def test_validate(self): - o = self.cls(min_value=2, max_value=7) - assert o.validate(2) is None - assert o.validate(5) is None - assert o.validate(7) is None - assert o.validate(1) == 'Cannot be smaller than 2' - assert o.validate(8) == 'Cannot be larger than 7' - for val in ['5', 5.0, 5L, None, True, False, object]: - assert o.validate(val) == 'Must be an integer' - - -class test_Unicode(ClassChecker): - _cls = ipa_types.Unicode - - def test_class(self): - assert self.cls.__bases__ == (ipa_types.Type,) - - def test_init(self): - o = self.cls() - assert o.__islocked__() is True - assert read_only(o, 'type') is unicode - assert read_only(o, 'name') == 'Unicode' - assert read_only(o, 'min_length') is None - assert read_only(o, 'max_length') is None - assert read_only(o, 'pattern') is None - assert read_only(o, 'regex') is None - - # Test min_length, max_length: - okay = ( - (0, 1), - (8, 8), - ) - for (l, h) in okay: - o = self.cls(min_length=l, max_length=h) - assert o.min_length == l - assert o.max_length == h - - fail_type = [ - '10', - 10.0, - 10L, - True, - False, - object, - ] - for value in fail_type: - e = raises(TypeError, self.cls, min_length=value) - assert str(e) == ( - 'min_length must be an int or None, got: %r' % value - ) - e = raises(TypeError, self.cls, max_length=value) - assert str(e) == ( - 'max_length must be an int or None, got: %r' % value - ) - - fail_value = [ - (10, 5), - (5, -5), - (0, -10), - ] - for (l, h) in fail_value: - e = raises(ValueError, self.cls, min_length=l, max_length=h) - assert str(e) == ( - 'min_length > max_length: min_length=%d, max_length=%d' % (l, h) - ) - - for (key, lower) in [('min_length', 0), ('max_length', 1)]: - value = lower - 1 - kw = {key: value} - e = raises(ValueError, self.cls, **kw) - assert str(e) == '%s must be >= %d, got: %d' % (key, lower, value) - - # Test pattern: - okay = [ - '(hello|world)', - u'(take the blue pill|take the red pill)', - ] - for value in okay: - o = self.cls(pattern=value) - assert o.pattern is value - assert o.regex is not None - - fail = [ - 42, - True, - False, - object, - ] - for value in fail: - e = raises(TypeError, self.cls, pattern=value) - assert str(e) == ( - 'pattern must be a basestring or None, got: %r' % value - ) - - # Test regex: - pat = '^(hello|world)$' - o = self.cls(pattern=pat) - for value in ('hello', 'world'): - m = o.regex.match(value) - assert m.group(1) == value - for value in ('hello beautiful', 'world!'): - assert o.regex.match(value) is None - - def test_validate(self): - pat = '^a_*b$' - o = self.cls(min_length=3, max_length=4, pattern=pat) - assert o.validate(u'a_b') is None - assert o.validate(u'a__b') is None - assert o.validate('a_b') == 'Must be a string' - assert o.validate(u'ab') == 'Must be at least 3 characters long' - assert o.validate(u'a___b') == 'Can be at most 4 characters long' - assert o.validate(u'a-b') == 'Must match %r' % pat - assert o.validate(u'a--b') == 'Must match %r' % pat - - -class test_Enum(ClassChecker): - _cls = ipa_types.Enum - - def test_class(self): - assert self.cls.__bases__ == (ipa_types.Type,) - - def test_init(self): - for t in (unicode, int, float): - values = (t(1), t(2), t(3)) - o = self.cls(*values) - assert o.__islocked__() is True - assert read_only(o, 'type') is t - assert read_only(o, 'name') is 'Enum' - assert read_only(o, 'values') == values - assert read_only(o, 'frozenset') == frozenset(values) - - # Check that ValueError is raised when no values are given: - e = raises(ValueError, self.cls) - assert str(e) == 'Enum requires at least one value' - - # Check that TypeError is raised when type of first value is not - # allowed: - e = raises(TypeError, self.cls, 'hello') - assert str(e) == '%r: %r not unicode, int, nor float' % ('hello', str) - #self.cls('hello') - - # Check that TypeError is raised when subsequent values aren't same - # type as first: - e = raises(TypeError, self.cls, u'hello', 'world') - assert str(e) == '%r: %r is not %r' % ('world', str, unicode) - - def test_validate(self): - values = (u'hello', u'naughty', u'nurse') - o = self.cls(*values) - for value in values: - assert o.validate(value) is None - assert o.validate(str(value)) == 'Incorrect type' - for value in (u'one fish', u'two fish'): - assert o.validate(value) == 'Invalid value' - assert o.validate(str(value)) == 'Incorrect type' diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py deleted file mode 100644 index fd3c3c88..00000000 --- a/ipalib/tests/test_plugable.py +++ /dev/null @@ -1,896 +0,0 @@ -# Authors: -# Jason Gerard DeRose -# -# 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 - -""" -Unit tests for `ipalib.plugable` module. -""" - -from tstutil import raises, no_set, no_del, read_only -from tstutil import getitem, setitem, delitem -from tstutil import ClassChecker -from ipalib import plugable, errors - - -class test_ReadOnly(ClassChecker): - """ - Test the `plugable.ReadOnly` class - """ - _cls = plugable.ReadOnly - - def test_class(self): - assert self.cls.__bases__ == (object,) - assert callable(self.cls.__lock__) - assert callable(self.cls.__islocked__) - - def test_lock(self): - """ - Test the `plugable.ReadOnly.__lock__` method. - """ - o = self.cls() - assert o._ReadOnly__locked is False - o.__lock__() - assert o._ReadOnly__locked is True - e = raises(AssertionError, o.__lock__) # Can only be locked once - assert str(e) == '__lock__() can only be called once' - assert o._ReadOnly__locked is True # This should still be True - - def test_lock(self): - """ - Test the `plugable.ReadOnly.__islocked__` method. - """ - o = self.cls() - assert o.__islocked__() is False - o.__lock__() - assert o.__islocked__() is True - - def test_setattr(self): - """ - Test the `plugable.ReadOnly.__setattr__` method. - """ - o = self.cls() - o.attr1 = 'Hello, world!' - assert o.attr1 == 'Hello, world!' - o.__lock__() - for name in ('attr1', 'attr2'): - e = raises(AttributeError, setattr, o, name, 'whatever') - assert str(e) == 'read-only: cannot set ReadOnly.%s' % name - assert o.attr1 == 'Hello, world!' - - def test_delattr(self): - """ - Test the `plugable.ReadOnly.__delattr__` method. - """ - o = self.cls() - o.attr1 = 'Hello, world!' - o.attr2 = 'How are you?' - assert o.attr1 == 'Hello, world!' - assert o.attr2 == 'How are you?' - del o.attr1 - assert not hasattr(o, 'attr1') - o.__lock__() - e = raises(AttributeError, delattr, o, 'attr2') - assert str(e) == 'read-only: cannot del ReadOnly.attr2' - assert o.attr2 == 'How are you?' - - -def test_lock(): - """ - Tests the `plugable.lock` function. - """ - f = plugable.lock - - # Test on a ReadOnly instance: - o = plugable.ReadOnly() - assert not o.__islocked__() - assert f(o) is o - assert o.__islocked__() - - # Test on something not subclassed from ReadOnly: - class not_subclass(object): - def __lock__(self): - pass - def __islocked__(self): - return True - o = not_subclass() - raises(ValueError, f, o) - - # Test that it checks __islocked__(): - class subclass(plugable.ReadOnly): - def __islocked__(self): - return False - o = subclass() - raises(AssertionError, f, o) - - -class test_SetProxy(ClassChecker): - """ - Tests the `plugable.SetProxy` class. - """ - _cls = plugable.SetProxy - - def test_class(self): - assert self.cls.__bases__ == (plugable.ReadOnly,) - - def test_init(self): - okay = (set, frozenset, dict) - fail = (list, tuple) - for t in okay: - self.cls(t()) - raises(TypeError, self.cls, t) - for t in fail: - raises(TypeError, self.cls, t()) - raises(TypeError, self.cls, t) - - def test_SetProxy(self): - def get_key(i): - return 'key_%d' % i - - cnt = 10 - target = set() - proxy = self.cls(target) - for i in xrange(cnt): - key = get_key(i) - - # Check initial state - assert len(proxy) == len(target) - assert list(proxy) == sorted(target) - assert key not in proxy - assert key not in target - - # Add and test again - target.add(key) - assert len(proxy) == len(target) - assert list(proxy) == sorted(target) - assert key in proxy - assert key in target - - -class test_DictProxy(ClassChecker): - """ - Tests the `plugable.DictProxy` class. - """ - _cls = plugable.DictProxy - - def test_class(self): - assert self.cls.__bases__ == (plugable.SetProxy,) - - def test_init(self): - self.cls(dict()) - raises(TypeError, self.cls, dict) - fail = (set, frozenset, list, tuple) - for t in fail: - raises(TypeError, self.cls, t()) - raises(TypeError, self.cls, t) - - def test_DictProxy(self): - def get_kv(i): - return ( - 'key_%d' % i, - 'val_%d' % i, - ) - cnt = 10 - target = dict() - proxy = self.cls(target) - for i in xrange(cnt): - (key, val) = get_kv(i) - - # Check initial state - assert len(proxy) == len(target) - assert list(proxy) == sorted(target) - assert list(proxy()) == [target[k] for k in sorted(target)] - assert key not in proxy - raises(KeyError, getitem, proxy, key) - - # Add and test again - target[key] = val - assert len(proxy) == len(target) - assert list(proxy) == sorted(target) - assert list(proxy()) == [target[k] for k in sorted(target)] - - # Verify TypeError is raised trying to set/del via proxy - raises(TypeError, setitem, proxy, key, val) - raises(TypeError, delitem, proxy, key) - - -class test_MagicDict(ClassChecker): - """ - Tests the `plugable.MagicDict` class. - """ - _cls = plugable.MagicDict - - def test_class(self): - assert self.cls.__bases__ == (plugable.DictProxy,) - for non_dict in ('hello', 69, object): - raises(TypeError, self.cls, non_dict) - - def test_MagicDict(self): - cnt = 10 - keys = [] - d = dict() - dictproxy = self.cls(d) - for i in xrange(cnt): - key = 'key_%d' % i - val = 'val_%d' % i - keys.append(key) - - # Test thet key does not yet exist - assert len(dictproxy) == i - assert key not in dictproxy - assert not hasattr(dictproxy, key) - raises(KeyError, getitem, dictproxy, key) - raises(AttributeError, getattr, dictproxy, key) - - # Test that items/attributes cannot be set on dictproxy: - raises(TypeError, setitem, dictproxy, key, val) - raises(AttributeError, setattr, dictproxy, key, val) - - # Test that additions in d are reflected in dictproxy: - d[key] = val - assert len(dictproxy) == i + 1 - assert key in dictproxy - assert hasattr(dictproxy, key) - assert dictproxy[key] is val - assert read_only(dictproxy, key) is val - - # Test __iter__ - assert list(dictproxy) == keys - - for key in keys: - # Test that items cannot be deleted through dictproxy: - raises(TypeError, delitem, dictproxy, key) - raises(AttributeError, delattr, dictproxy, key) - - # Test that deletions in d are reflected in dictproxy - del d[key] - assert len(dictproxy) == len(d) - assert key not in dictproxy - raises(KeyError, getitem, dictproxy, key) - raises(AttributeError, getattr, dictproxy, key) - - -class test_Plugin(ClassChecker): - """ - Tests the `plugable.Plugin` class. - """ - _cls = plugable.Plugin - - def test_class(self): - assert self.cls.__bases__ == (plugable.ReadOnly,) - assert self.cls.__public__ == frozenset() - assert type(self.cls.name) is property - assert type(self.cls.doc) is property - assert type(self.cls.api) is property - - def test_name(self): - """ - Tests the `plugable.Plugin.name` property. - """ - assert read_only(self.cls(), 'name') == 'Plugin' - - class some_subclass(self.cls): - pass - assert read_only(some_subclass(), 'name') == 'some_subclass' - - def test_doc(self): - """ - Tests the `plugable.Plugin.doc` property. - """ - class some_subclass(self.cls): - 'here is the doc string' - assert read_only(some_subclass(), 'doc') == 'here is the doc string' - - def test_implements(self): - """ - Tests the `plugable.Plugin.implements` classmethod. - """ - class example(self.cls): - __public__ = frozenset(( - 'some_method', - 'some_property', - )) - class superset(self.cls): - __public__ = frozenset(( - 'some_method', - 'some_property', - 'another_property', - )) - class subset(self.cls): - __public__ = frozenset(( - 'some_property', - )) - class any_object(object): - __public__ = frozenset(( - 'some_method', - 'some_property', - )) - - for ex in (example, example()): - # Test using str: - assert ex.implements('some_method') - assert not ex.implements('another_method') - - # Test using frozenset: - assert ex.implements(frozenset(['some_method'])) - assert not ex.implements( - frozenset(['some_method', 'another_method']) - ) - - # Test using another object/class with __public__ frozenset: - assert ex.implements(example) - assert ex.implements(example()) - - assert ex.implements(subset) - assert not subset.implements(ex) - - assert not ex.implements(superset) - assert superset.implements(ex) - - assert ex.implements(any_object) - assert ex.implements(any_object()) - - def test_implemented_by(self): - """ - Tests the `plugable.Plugin.implemented_by` classmethod. - """ - class base(self.cls): - __public__ = frozenset(( - 'attr0', - 'attr1', - 'attr2', - )) - - class okay(base): - def attr0(self): - pass - def __get_attr1(self): - assert False # Make sure property isn't accesed on instance - attr1 = property(__get_attr1) - attr2 = 'hello world' - another_attr = 'whatever' - - class fail(base): - def __init__(self): - # Check that class, not instance is inspected: - self.attr2 = 'hello world' - def attr0(self): - pass - def __get_attr1(self): - assert False # Make sure property isn't accesed on instance - attr1 = property(__get_attr1) - another_attr = 'whatever' - - # Test that AssertionError is raised trying to pass something not - # subclass nor instance of base: - raises(AssertionError, base.implemented_by, object) - - # Test on subclass with needed attributes: - assert base.implemented_by(okay) is True - assert base.implemented_by(okay()) is True - - # Test on subclass *without* needed attributes: - assert base.implemented_by(fail) is False - assert base.implemented_by(fail()) is False - - def test_set_api(self): - """ - Tests the `plugable.Plugin.set_api` method. - """ - api = 'the api instance' - o = self.cls() - assert o.api is None - e = raises(AssertionError, o.set_api, None) - assert str(e) == 'set_api() argument cannot be None' - o.set_api(api) - assert o.api is api - e = raises(AssertionError, o.set_api, api) - assert str(e) == 'set_api() can only be called once' - - def test_finalize(self): - """ - Tests the `plugable.Plugin.finalize` method. - """ - o = self.cls() - assert not o.__islocked__() - o.finalize() - assert o.__islocked__() - - -class test_PluginProxy(ClassChecker): - """ - Tests the `plugable.PluginProxy` class. - """ - _cls = plugable.PluginProxy - - def test_class(self): - assert self.cls.__bases__ == (plugable.SetProxy,) - - def test_proxy(self): - # Setup: - class base(object): - __public__ = frozenset(( - 'public_0', - 'public_1', - '__call__', - )) - - def public_0(self): - return 'public_0' - - def public_1(self): - return 'public_1' - - def __call__(self, caller): - return 'ya called it, %s.' % caller - - def private_0(self): - return 'private_0' - - def private_1(self): - return 'private_1' - - class plugin(base): - name = 'user_add' - attr_name = 'add' - doc = 'add a new user' - - # Test that TypeError is raised when base is not a class: - raises(TypeError, self.cls, base(), None) - - # Test that ValueError is raised when target is not instance of base: - raises(ValueError, self.cls, base, object()) - - # Test with correct arguments: - i = plugin() - p = self.cls(base, i) - assert read_only(p, 'name') is plugin.name - assert read_only(p, 'doc') == plugin.doc - assert list(p) == sorted(base.__public__) - - # Test normal methods: - for n in xrange(2): - pub = 'public_%d' % n - priv = 'private_%d' % n - assert getattr(i, pub)() == pub - assert getattr(p, pub)() == pub - assert hasattr(p, pub) - assert getattr(i, priv)() == priv - assert not hasattr(p, priv) - - # Test __call__: - value = 'ya called it, dude.' - assert i('dude') == value - assert p('dude') == value - assert callable(p) - - # Test name_attr='name' kw arg - i = plugin() - p = self.cls(base, i, 'attr_name') - assert read_only(p, 'name') == 'add' - - def test_implements(self): - """ - Tests the `plugable.PluginProxy.implements` method. - """ - class base(object): - __public__ = frozenset() - name = 'base' - doc = 'doc' - @classmethod - def implements(cls, arg): - return arg + 7 - - class sub(base): - @classmethod - def implements(cls, arg): - """ - Defined to make sure base.implements() is called, not - target.implements() - """ - return arg - - o = sub() - p = self.cls(base, o) - assert p.implements(3) == 10 - - def test_clone(self): - """ - Tests the `plugable.PluginProxy.__clone__` method. - """ - class base(object): - __public__ = frozenset() - class sub(base): - name = 'some_name' - doc = 'doc' - label = 'another_name' - - p = self.cls(base, sub()) - assert read_only(p, 'name') == 'some_name' - c = p.__clone__('label') - assert isinstance(c, self.cls) - assert c is not p - assert read_only(c, 'name') == 'another_name' - - -def test_check_name(): - """ - Tests the `plugable.check_name` function. - """ - f = plugable.check_name - okay = [ - 'user_add', - 'stuff2junk', - 'sixty9', - ] - nope = [ - '_user_add', - '__user_add', - 'user_add_', - 'user_add__', - '_user_add_', - '__user_add__', - '60nine', - ] - for name in okay: - assert name is f(name) - e = raises(TypeError, f, unicode(name)) - assert str(e) == errors.TYPE_FORMAT % ('name', str, unicode(name)) - for name in nope: - raises(errors.NameSpaceError, f, name) - for name in okay: - raises(errors.NameSpaceError, f, name.upper()) - -class DummyMember(object): - def __init__(self, i): - assert type(i) is int - self.name = 'member_%02d' % i - - -class test_NameSpace(ClassChecker): - """ - Tests the `plugable.NameSpace` class. - """ - _cls = plugable.NameSpace - - def test_class(self): - assert self.cls.__bases__ == (plugable.ReadOnly,) - - def test_init(self): - """ - Tests the `plugable.NameSpace.__init__` method. - """ - o = self.cls(tuple()) - assert list(o) == [] - assert list(o()) == [] - for cnt in (10, 25): - members = tuple(DummyMember(cnt - i) for i in xrange(cnt)) - for sort in (True, False): - o = self.cls(members, sort=sort) - if sort: - ordered = tuple(sorted(members, key=lambda m: m.name)) - else: - ordered = members - names = tuple(m.name for m in ordered) - assert o.__todict__() == dict((o.name, o) for o in ordered) - - # Test __len__: - assert len(o) == cnt - - # Test __contains__: - for name in names: - assert name in o - assert ('member_00') not in o - - # Test __iter__, __call__: - assert tuple(o) == names - assert tuple(o()) == ordered - - # Test __getitem__, getattr: - for (i, member) in enumerate(ordered): - assert o[i] is member - name = member.name - assert o[name] is member - assert read_only(o, name) is member - - # Test negative indexes: - for i in xrange(1, cnt + 1): - assert o[-i] is ordered[-i] - - # Test slices: - assert o[2:cnt-5] == ordered[2:cnt-5] - assert o[::3] == ordered[::3] - - # Test __repr__: - assert repr(o) == \ - 'NameSpace(<%d members>, sort=%r)' % (cnt, sort) - - -def test_Environment(): - """ - Tests the `plugable.Environment` class. - """ - # This has to be the same as iter_cnt - control_cnt = 0 - class prop_class: - def __init__(self, val): - self._val = val - def get_value(self): - return self._val - - class iter_class(prop_class): - # Increment this for each time iter_class yields - iter_cnt = 0 - def get_value(self): - for item in self._val: - self.__class__.iter_cnt += 1 - yield item - - # Tests for basic functionality - basic_tests = ( - ('a', 1), - ('b', 'basic_foo'), - ('c', ('basic_bar', 'basic_baz')), - ) - # Tests with prop classes - prop_tests = ( - ('d', prop_class(2), 2), - ('e', prop_class('prop_foo'), 'prop_foo'), - ('f', prop_class(('prop_bar', 'prop_baz')), ('prop_bar', 'prop_baz')), - ) - # Tests with iter classes - iter_tests = ( - ('g', iter_class((3, 4, 5)), (3, 4, 5)), - ('h', iter_class(('iter_foo', 'iter_bar', 'iter_baz')), - ('iter_foo', 'iter_bar', 'iter_baz') - ), - ) - - # Set all the values - env = plugable.Environment() - for name, val in basic_tests: - env[name] = val - for name, val, dummy in prop_tests: - env[name] = val - for name, val, dummy in iter_tests: - env[name] = val - - # Test if the values are correct - for name, val in basic_tests: - assert env[name] == val - for name, dummy, val in prop_tests: - assert env[name] == val - # Test if the get_value() function is called only when needed - for name, dummy, correct_values in iter_tests: - values_in_env = [] - for val in env[name]: - control_cnt += 1 - assert iter_class.iter_cnt == control_cnt - values_in_env.append(val) - assert tuple(values_in_env) == correct_values - - # Test __setattr__() - env.spam = 'ham' - assert env.spam == 'ham' - - # Test if we throw AttributeError exception when trying to overwrite - # existing value, or delete it - raises(AttributeError, setitem, env, 'a', 1) - raises(AttributeError, setattr, env, 'a', 1) - raises(AttributeError, delitem, env, 'a') - raises(AttributeError, delattr, env, 'a') - raises(AttributeError, plugable.Environment.update, env, dict(a=1000)) - # This should be silently ignored - env.update(dict(a=1000), True) - assert env.a != 1000 - -def test_Registrar(): - class Base1(object): - pass - class Base2(object): - pass - class Base3(object): - pass - class plugin1(Base1): - pass - class plugin2(Base2): - pass - class plugin3(Base3): - pass - - # Test creation of Registrar: - r = plugable.Registrar(Base1, Base2) - - # Test __iter__: - assert list(r) == ['Base1', 'Base2'] - - # Test __hasitem__, __getitem__: - for base in [Base1, Base2]: - name = base.__name__ - assert name in r - assert r[name] is base - magic = getattr(r, name) - assert type(magic) is plugable.MagicDict - assert len(magic) == 0 - - # Check that TypeError is raised trying to register something that isn't - # a class: - raises(TypeError, r, plugin1()) - - # Check that SubclassError is raised trying to register a class that is - # not a subclass of an allowed base: - raises(errors.SubclassError, r, plugin3) - - # Check that registration works - r(plugin1) - assert len(r.Base1) == 1 - assert r.Base1['plugin1'] is plugin1 - assert r.Base1.plugin1 is plugin1 - - # Check that DuplicateError is raised trying to register exact class - # again: - raises(errors.DuplicateError, r, plugin1) - - # Check that OverrideError is raised trying to register class with same - # name and same base: - orig1 = plugin1 - class base1_extended(Base1): - pass - class plugin1(base1_extended): - pass - raises(errors.OverrideError, r, plugin1) - - # Check that overriding works - r(plugin1, override=True) - assert len(r.Base1) == 1 - assert r.Base1.plugin1 is plugin1 - assert r.Base1.plugin1 is not orig1 - - # Check that MissingOverrideError is raised trying to override a name - # not yet registerd: - raises(errors.MissingOverrideError, r, plugin2, override=True) - - # Test that another plugin can be registered: - assert len(r.Base2) == 0 - r(plugin2) - assert len(r.Base2) == 1 - assert r.Base2.plugin2 is plugin2 - - # Setup to test more registration: - class plugin1a(Base1): - pass - r(plugin1a) - - class plugin1b(Base1): - pass - r(plugin1b) - - class plugin2a(Base2): - pass - r(plugin2a) - - class plugin2b(Base2): - pass - r(plugin2b) - - # Again test __hasitem__, __getitem__: - for base in [Base1, Base2]: - name = base.__name__ - assert name in r - assert r[name] is base - magic = getattr(r, name) - assert len(magic) == 3 - for key in magic: - klass = magic[key] - assert getattr(magic, key) is klass - assert issubclass(klass, base) - - - -def test_API(): - assert issubclass(plugable.API, plugable.ReadOnly) - - # Setup the test bases, create the API: - class base0(plugable.Plugin): - __public__ = frozenset(( - 'method', - )) - - def method(self, n): - return n - - class base1(plugable.Plugin): - __public__ = frozenset(( - 'method', - )) - - def method(self, n): - return n + 1 - - api = plugable.API(base0, base1) - r = api.register - assert isinstance(r, plugable.Registrar) - assert read_only(api, 'register') is r - - class base0_plugin0(base0): - pass - r(base0_plugin0) - - class base0_plugin1(base0): - pass - r(base0_plugin1) - - class base0_plugin2(base0): - pass - r(base0_plugin2) - - class base1_plugin0(base1): - pass - r(base1_plugin0) - - class base1_plugin1(base1): - pass - r(base1_plugin1) - - class base1_plugin2(base1): - pass - r(base1_plugin2) - - # Test API instance: - api.finalize() - - def get_base(b): - return 'base%d' % b - - def get_plugin(b, p): - return 'base%d_plugin%d' % (b, p) - - for b in xrange(2): - base_name = get_base(b) - ns = getattr(api, base_name) - assert isinstance(ns, plugable.NameSpace) - assert read_only(api, base_name) is ns - assert len(ns) == 3 - for p in xrange(3): - plugin_name = get_plugin(b, p) - proxy = ns[plugin_name] - assert isinstance(proxy, plugable.PluginProxy) - assert proxy.name == plugin_name - assert read_only(ns, plugin_name) is proxy - assert read_only(proxy, 'method')(7) == 7 + b - - # Test that calling finilize again raises AssertionError: - raises(AssertionError, api.finalize) - - # Test with base class that doesn't request a proxy - class NoProxy(plugable.Plugin): - __proxy__ = False - api = plugable.API(NoProxy) - class plugin0(NoProxy): - pass - api.register(plugin0) - class plugin1(NoProxy): - pass - api.register(plugin1) - api.finalize() - names = ['plugin0', 'plugin1'] - assert list(api.NoProxy) == names - for name in names: - plugin = api.NoProxy[name] - assert getattr(api.NoProxy, name) is plugin - assert isinstance(plugin, plugable.Plugin) - assert not isinstance(plugin, plugable.PluginProxy) diff --git a/ipalib/tests/test_tstutil.py b/ipalib/tests/test_tstutil.py deleted file mode 100644 index 5916f9d2..00000000 --- a/ipalib/tests/test_tstutil.py +++ /dev/null @@ -1,148 +0,0 @@ -# Authors: -# Jason Gerard DeRose -# -# 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 - -""" -Unit tests for test-helper `tests.tstutil` module. -""" - -import tstutil - - -class Prop(object): - def __init__(self, *ops): - self.__ops = frozenset(ops) - self.__prop = 'prop value' - - def __get_prop(self): - if 'get' not in self.__ops: - raise AttributeError('get prop') - return self.__prop - - def __set_prop(self, value): - if 'set' not in self.__ops: - raise AttributeError('set prop') - self.__prop = value - - def __del_prop(self): - if 'del' not in self.__ops: - raise AttributeError('del prop') - self.__prop = None - - prop = property(__get_prop, __set_prop, __del_prop) - - -def test_yes_raised(): - f = tstutil.raises - - class SomeError(Exception): - pass - - class AnotherError(Exception): - pass - - def callback1(): - 'raises correct exception' - raise SomeError() - - def callback2(): - 'raises wrong exception' - raise AnotherError() - - def callback3(): - 'raises no exception' - - f(SomeError, callback1) - - raised = False - try: - f(SomeError, callback2) - except AnotherError: - raised = True - assert raised - - raised = False - try: - f(SomeError, callback3) - except tstutil.ExceptionNotRaised: - raised = True - assert raised - - -def test_no_set(): - # Tests that it works when prop cannot be set: - tstutil.no_set(Prop('get', 'del'), 'prop') - - # Tests that ExceptionNotRaised is raised when prop *can* be set: - raised = False - try: - tstutil.no_set(Prop('set'), 'prop') - except tstutil.ExceptionNotRaised: - raised = True - assert raised - - -def test_no_del(): - # Tests that it works when prop cannot be deleted: - tstutil.no_del(Prop('get', 'set'), 'prop') - - # Tests that ExceptionNotRaised is raised when prop *can* be set: - raised = False - try: - tstutil.no_del(Prop('del'), 'prop') - except tstutil.ExceptionNotRaised: - raised = True - assert raised - - -def test_read_only(): - # Test that it works when prop is read only: - assert tstutil.read_only(Prop('get'), 'prop') == 'prop value' - - # Test that ExceptionNotRaised is raised when prop can be set: - raised = False - try: - tstutil.read_only(Prop('get', 'set'), 'prop') - except tstutil.ExceptionNotRaised: - raised = True - assert raised - - # Test that ExceptionNotRaised is raised when prop can be deleted: - raised = False - try: - tstutil.read_only(Prop('get', 'del'), 'prop') - except tstutil.ExceptionNotRaised: - raised = True - assert raised - - # Test that ExceptionNotRaised is raised when prop can be both set and - # deleted: - raised = False - try: - tstutil.read_only(Prop('get', 'del'), 'prop') - except tstutil.ExceptionNotRaised: - raised = True - assert raised - - # Test that AttributeError is raised when prop can't be read: - raised = False - try: - tstutil.read_only(Prop(), 'prop') - except AttributeError: - raised = True - assert raised diff --git a/ipalib/tests/test_util.py b/ipalib/tests/test_util.py deleted file mode 100644 index f8ee0bf4..00000000 --- a/ipalib/tests/test_util.py +++ /dev/null @@ -1,49 +0,0 @@ -# Authors: -# Jason Gerard DeRose -# -# 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 - -""" -Unit tests for `ipalib.util` module. -""" - -from tstutil import raises -from ipalib import util - - -def test_xmlrpc_marshal(): - """ - Test the `util.xmlrpc_marshal` function. - """ - f = util.xmlrpc_marshal - assert f() == ({},) - assert f('one', 'two') == ({}, 'one', 'two') - assert f(one=1, two=2) == (dict(one=1, two=2),) - assert f('one', 'two', three=3, four=4) == \ - (dict(three=3, four=4), 'one', 'two') - - -def test_xmlrpc_unmarshal(): - """ - Test the `util.xmlrpc_unmarshal` function. - """ - f = util.xmlrpc_unmarshal - assert f() == (tuple(), {}) - assert f({}, 'one', 'two') == (('one', 'two'), {}) - assert f(dict(one=1, two=2)) == (tuple(), dict(one=1, two=2)) - assert f(dict(three=3, four=4), 'one', 'two') == \ - (('one', 'two'), dict(three=3, four=4)) diff --git a/ipalib/tests/tstutil.py b/ipalib/tests/tstutil.py deleted file mode 100644 index 743716a0..00000000 --- a/ipalib/tests/tstutil.py +++ /dev/null @@ -1,147 +0,0 @@ -# Authors: -# Jason Gerard DeRose -# -# 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 - -""" -Utility functions for the unit tests. -""" - -import inspect -from ipalib import errors - -class ExceptionNotRaised(Exception): - """ - Exception raised when an *expected* exception is *not* raised during a - unit test. - """ - msg = 'expected %s' - - def __init__(self, expected): - self.expected = expected - - def __str__(self): - return self.msg % self.expected.__name__ - - -def raises(exception, callback, *args, **kw): - """ - Tests that the expected exception is raised; raises ExceptionNotRaised - if test fails. - """ - raised = False - try: - callback(*args, **kw) - except exception, e: - raised = True - if not raised: - raise ExceptionNotRaised(exception) - return e - - -def getitem(obj, key): - """ - Works like getattr but for dictionary interface. Use this in combination - with raises() to test that, for example, KeyError is raised. - """ - return obj[key] - - -def setitem(obj, key, value): - """ - Works like setattr but for dictionary interface. Use this in combination - with raises() to test that, for example, TypeError is raised. - """ - obj[key] = value - - -def delitem(obj, key): - """ - Works like delattr but for dictionary interface. Use this in combination - with raises() to test that, for example, TypeError is raised. - """ - del obj[key] - - -def no_set(obj, name, value='some_new_obj'): - """ - Tests that attribute cannot be set. - """ - raises(AttributeError, setattr, obj, name, value) - - -def no_del(obj, name): - """ - Tests that attribute cannot be deleted. - """ - raises(AttributeError, delattr, obj, name) - - -def read_only(obj, name, value='some_new_obj'): - """ - Tests that attribute is read-only. Returns attribute. - """ - # Test that it cannot be set: - no_set(obj, name, value) - - # Test that it cannot be deleted: - no_del(obj, name) - - # Return the attribute - return getattr(obj, name) - - -def is_prop(prop): - return type(prop) is property - - -class ClassChecker(object): - __cls = None - __subcls = None - - def __get_cls(self): - if self.__cls is None: - self.__cls = self._cls - assert inspect.isclass(self.__cls) - return self.__cls - cls = property(__get_cls) - - def __get_subcls(self): - if self.__subcls is None: - self.__subcls = self.get_subcls() - assert inspect.isclass(self.__subcls) - return self.__subcls - subcls = property(__get_subcls) - - def get_subcls(self): - raise NotImplementedError( - self.__class__.__name__, - 'get_subcls()' - ) - - -def check_TypeError(value, type_, name, callback, *args, **kw): - """ - Tests a standard TypeError raised with `errors.raise_TypeError`. - """ - e = raises(TypeError, callback, *args, **kw) - assert e.value is value - assert e.type is type_ - assert e.name == name - assert type(e.name) is str - assert str(e) == errors.TYPE_FORMAT % (name, type_, value) - return e -- cgit From fec6fc2e8c373c698966816ee97fe3a660eb503e Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 7 Oct 2008 22:35:45 -0600 Subject: Fixed example in raise_TypeError() docstring (thanks, mnagy) --- ipalib/errors.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'ipalib') diff --git a/ipalib/errors.py b/ipalib/errors.py index a961ecb6..097747ac 100644 --- a/ipalib/errors.py +++ b/ipalib/errors.py @@ -43,12 +43,12 @@ def raise_TypeError(value, type_, name): Here is an example: - >>> raise_TypeError('message', str, u'Hello.') + >>> raise_TypeError(u'Hello, world!', str, 'message') Traceback (most recent call last): File "", line 1, in - File "/home/jderose/projects/freeipa2/ipalib/errors.py", line 61, in raise_TypeError + File "ipalib/errors.py", line 65, in raise_TypeError raise e - TypeError: message: need a ; got u'Hello.' + TypeError: message: need a ; got u'Hello, world!' :param value: The value (of incorrect type) passed as argument. :param type_: The type expected for the argument. -- cgit From b7fe92f44f88cb22b9e229ff7fde5309dfbdd778 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 8 Oct 2008 18:01:22 -0600 Subject: Reorganized Command methods so it is easier to understand and added lots of docstrings --- ipalib/frontend.py | 308 ++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 223 insertions(+), 85 deletions(-) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 651e4642..ce92cf53 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -449,6 +449,27 @@ def create_param(spec): class Command(plugable.Plugin): + """ + A public IPA atomic operation. + + All plugins that subclass from `Command` will be automatically available + as a CLI command and as an XML-RPC method. + + Plugins that subclass from Command are registered in the ``api.Command`` + namespace. For example: + + >>> api = plugable.API(Command) + >>> class my_command(Command): + ... pass + ... + >>> api.register(my_command) + >>> api.finalize() + >>> list(api.Command) + ['my_command'] + >>> api.Command.my_command + PluginProxy(Command, __main__.my_command()) + """ + __public__ = frozenset(( 'get_default', 'convert', @@ -468,66 +489,134 @@ class Command(plugable.Plugin): options = None params = None - def finalize(self): - self.args = plugable.NameSpace(self.__create_args(), sort=False) - if len(self.args) == 0 or not self.args[-1].multivalue: - self.max_args = len(self.args) - else: - self.max_args = None - self.options = plugable.NameSpace(self.__create_options(), sort=False) - self.params = plugable.NameSpace( - tuple(self.args()) + tuple(self.options()), sort=False - ) - super(Command, self).finalize() + def __call__(self, *args, **kw): + """ + Perform validation and then execute the command. - def get_args(self): - return self.takes_args + If not in a server context, the call will be forwarded over + XML-RPC and the executed an the nearest IPA server. + """ + if len(args) > 0: + arg_kw = self.args_to_kw(*args) + assert set(arg_kw).intersection(kw) == set() + kw.update(arg_kw) + kw = self.normalize(**kw) + kw = self.convert(**kw) + kw.update(self.get_default(**kw)) + self.validate(**kw) + args = tuple(kw.pop(name) for name in self.args) + return self.run(*args, **kw) - def get_options(self): - return self.takes_options + def args_to_kw(self, *values): + """ + Map positional into keyword arguments. + """ + if self.max_args is not None and len(values) > self.max_args: + if self.max_args == 0: + raise errors.ArgumentError(self, 'takes no arguments') + if self.max_args == 1: + raise errors.ArgumentError(self, 'takes at most 1 argument') + raise errors.ArgumentError(self, + 'takes at most %d arguments' % len(self.args) + ) + return dict(self.__args_to_kw_iter(values)) - def __create_args(self): - optional = False + def __args_to_kw_iter(self, values): + """ + Generator used by `Command.args_to_kw` method. + """ multivalue = False - for arg in self.get_args(): - arg = create_param(arg) - if optional and arg.required: - raise ValueError( - '%s: required argument after optional' % arg.name - ) - if multivalue: - raise ValueError( - '%s: only final argument can be multivalue' % arg.name - ) - if not arg.required: - optional = True - if arg.multivalue: - multivalue = True - yield arg + for (i, arg) in enumerate(self.args()): + assert not multivalue + if len(values) > i: + if arg.multivalue: + multivalue = True + yield (arg.name, values[i:]) + else: + yield (arg.name, values[i]) + else: + break + + def kw_to_args(self, **kw): + """ + Map keyword into positional arguments. + """ + return tuple(kw.get(name, None) for name in self.args) - def __create_options(self): - for option in self.get_options(): - yield create_param(option) + def normalize(self, **kw): + """ + Return a dictionary of normalized values. - def convert(self, **kw): + For example: + + >>> class my_command(Command): + ... takes_options = ( + ... Param('first', normalize=lambda value: value.lower()), + ... Param('last'), + ... ) + ... + >>> c = my_command() + >>> c.finalize() + >>> c.normalize(first='JOHN', last='DOE') + {'last': 'DOE', 'first': 'john'} + """ return dict( - (k, self.params[k].convert(v)) for (k, v) in kw.iteritems() + (k, self.params[k].normalize(v)) for (k, v) in kw.iteritems() ) - def normalize(self, **kw): + def convert(self, **kw): + """ + Return a dictionary of values converted to correct type. + + >>> from ipalib import ipa_types + >>> class my_command(Command): + ... takes_args = ( + ... Param('one', type=ipa_types.Int()), + ... 'two', + ... ) + ... + >>> c = my_command() + >>> c.finalize() + >>> c.convert(one=1, two=2) + {'two': u'2', 'one': 1} + """ return dict( - (k, self.params[k].normalize(v)) for (k, v) in kw.iteritems() + (k, self.params[k].convert(v)) for (k, v) in kw.iteritems() ) + def get_default(self, **kw): + """ + Return a dictionary of defaults for all missing required values. + + For example: + + >>> class my_command(Command): + ... takes_args = [Param('color', default='Red')] + ... + >>> c = my_command() + >>> c.finalize() + >>> c.get_default() + {'color': 'Red'} + >>> c.get_default(color='Yellow') + {} + """ + return dict(self.__get_default_iter(kw)) + def __get_default_iter(self, kw): + """ + Generator method used by `Command.get_default`. + """ for param in self.params(): if param.required and kw.get(param.name, None) is None: yield (param.name, param.get_default(**kw)) - def get_default(self, **kw): - return dict(self.__get_default_iter(kw)) - def validate(self, **kw): + """ + Validate all values. + + If any value fails the validation, `ipalib.errors.ValidationError` + (or a subclass thereof) will be raised. + """ for param in self.params(): value = kw.get(param.name, None) if value is not None: @@ -535,64 +624,113 @@ class Command(plugable.Plugin): elif param.required: raise errors.RequirementError(param.name) + def run(self, *args, **kw): + """ + Dispatch to `Command.execute` or `Command.forward`. + + If running in a server context, `Command.execute` is called and the + actually work this command performs is executed locally. + + If running in a non-server context, `Command.forward` is called, + which forwards this call over XML-RPC to the exact same command + on the nearest IPA server and the actual work this command + performs is executed remotely. + """ + if self.api.env.server_context: + target = self.execute + else: + target = self.forward + object.__setattr__(self, 'run', target) + return target(*args, **kw) + def execute(self, *args, **kw): + """ + Perform the actual work this command does. + + This method should be implemented only against functionality + in self.api.Backend. For example, a hypothetical + user_add.execute() might be implemented like this: + + >>> class user_add(Command): + ... def execute(self, **kw): + ... return self.api.Backend.ldap.add(**kw) + ... + """ print '%s.execute():' % self.name print ' args =', args print ' kw =', kw def forward(self, *args, **kw): """ - Forward call over XML-RPC. + Forward call over XML-RPC to this same command on server. """ return self.api.Backend.xmlrpc.forward_call(self.name, *args, **kw) + def finalize(self): + """ + Finalize plugin initialization. - def __call__(self, *args, **kw): - if len(args) > 0: - arg_kw = self.args_to_kw(*args) - assert set(arg_kw).intersection(kw) == set() - kw.update(arg_kw) - kw = self.normalize(**kw) - kw = self.convert(**kw) - kw.update(self.get_default(**kw)) - self.validate(**kw) - args = tuple(kw.pop(name) for name in self.args) - return self.run(*args, **kw) - - def run(self, *args, **kw): - if self.api.env.server_context: - target = self.execute + This method creates the ``args``, ``options``, and ``params`` + namespaces. This is not done in `Command.__init__` because + subclasses (like `crud.Add`) might need to access other plugins + loaded in self.api to determine what their custom `Command.get_args` + and `Command.get_options` methods should yield. + """ + self.args = plugable.NameSpace(self.__create_args(), sort=False) + if len(self.args) == 0 or not self.args[-1].multivalue: + self.max_args = len(self.args) else: - target = self.forward - object.__setattr__(self, 'run', target) - return target(*args, **kw) + self.max_args = None + self.options = plugable.NameSpace( + (create_param(spec) for spec in self.get_options()), + sort=False + ) + self.params = plugable.NameSpace( + tuple(self.args()) + tuple(self.options()), sort=False + ) + super(Command, self).finalize() - def args_to_kw(self, *values): - if self.max_args is not None and len(values) > self.max_args: - if self.max_args == 0: - raise errors.ArgumentError(self, 'takes no arguments') - if self.max_args == 1: - raise errors.ArgumentError(self, 'takes at most 1 argument') - raise errors.ArgumentError(self, - 'takes at most %d arguments' % len(self.args) - ) - return dict(self.__args_to_kw_iter(values)) + def get_args(self): + """ + Return iterable with arguments for Command.args namespace. - def __args_to_kw_iter(self, values): - multivalue = False - for (i, arg) in enumerate(self.args()): - assert not multivalue - if len(values) > i: - if arg.multivalue: - multivalue = True - yield (arg.name, values[i:]) - else: - yield (arg.name, values[i]) - else: - break + Subclasses can override this to customize how the arguments + are determined. For an example of why this can be useful, + see `ipalib.crud.Mod`. + """ + return self.takes_args - def kw_to_args(self, **kw): - return tuple(kw.get(name, None) for name in self.args) + def get_options(self): + """ + Return iterable with options for Command.options namespace. + + Subclasses can override this to customize how the options + are determined. For an example of why this can be useful, + see `ipalib.crud.Mod`. + """ + return self.takes_options + + def __create_args(self): + """ + Generator used to create args namespace. + """ + optional = False + multivalue = False + for arg in self.get_args(): + arg = create_param(arg) + if optional and arg.required: + raise ValueError( + '%s: required argument after optional' % arg.name + ) + if multivalue: + raise ValueError( + '%s: only final argument can be multivalue' % arg.name + ) + if not arg.required: + optional = True + if arg.multivalue: + multivalue = True + yield arg class Object(plugable.Plugin): -- cgit From 887016e69d6678892a2ff53735623ce5d413b074 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 8 Oct 2008 18:18:13 -0600 Subject: Base Command.execute() method now raises NotImplementedError; updated unit tests --- ipalib/frontend.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index ce92cf53..639160c1 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -656,9 +656,7 @@ class Command(plugable.Plugin): ... return self.api.Backend.ldap.add(**kw) ... """ - print '%s.execute():' % self.name - print ' args =', args - print ' kw =', kw + raise NotImplementedError('%s.execute()' % self.name) def forward(self, *args, **kw): """ -- cgit From 87390665f6998116ec2429773088c7165e281611 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 9 Oct 2008 11:33:35 -0600 Subject: crud.Add.get_args() and get_options() now yield static values in takes_args, takes_options after the automagic ones --- ipalib/crud.py | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'ipalib') diff --git a/ipalib/crud.py b/ipalib/crud.py index 9f9d727f..1bdd03f8 100644 --- a/ipalib/crud.py +++ b/ipalib/crud.py @@ -27,10 +27,14 @@ import frontend, errors class Add(frontend.Method): def get_args(self): yield self.obj.primary_key + for arg in self.takes_args: + yield arg def get_options(self): for param in self.obj.params_minus_pk(): yield param + for option in self.takes_options: + yield option class Get(frontend.Method): -- cgit From 672c07566df8d838b46edb6b80ba73ade2a27d55 Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Tue, 7 Oct 2008 23:38:00 -0400 Subject: Implement user-del rename is_user_unique() to user_exists() --- ipalib/plugins/f_user.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/plugins/f_user.py b/ipalib/plugins/f_user.py index b006c24b..f87ddea1 100644 --- a/ipalib/plugins/f_user.py +++ b/ipalib/plugins/f_user.py @@ -93,7 +93,7 @@ class user_add(crud.Add): user['uid'] = args[0] - if not servercore.is_user_unique(user['uid']): + if servercore.user_exists(user['uid']): # FIXME, specific error raise SyntaxError("user already exists") if servercore.uid_too_long(user['uid']): @@ -177,6 +177,31 @@ api.register(user_add) class user_del(crud.Del): 'Delete an existing user.' + def execute(self, *args, **kw): + """args[0] = uid of the user to remove + + Delete a user. Not to be confused with inactivate_user. This + makes the entry go away completely. + + uid is the uid of the user to delete + + The memberOf plugin handles removing the user from any other + groups. + """ + uid = args[0] + if uid == "admin": + raise ipaerror.gen_exception(ipaerror.INPUT_ADMIN_REQUIRED) +# logging.info("IPA: delete_user '%s'" % uid) + user = servercore.get_user_by_uid(uid, ['dn', 'uid']) + if not user: + # FIXME, specific error + raise SyntaxError("user doesn't exist") + + return servercore.delete_entry(user['dn']) + def forward(self, *args, **kw): + result = super(crud.Del, self).forward(*args, **kw) + if result != False: + print "User %s removed" % args[0] api.register(user_del) -- cgit From 83bb41faebc0a61269f2869e9123166254fff5b3 Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Wed, 8 Oct 2008 23:31:49 -0400 Subject: Mechanism to convert from xmlrpclib.Fault to an IPAError exception Include slew of new exceptions, not all of which are used yet --- ipalib/errors.py | 137 +++++++++++++++++++++++++++++++++++++++++++++ ipalib/plugins/b_xmlrpc.py | 3 + ipalib/plugins/f_user.py | 48 ++++++++-------- 3 files changed, 163 insertions(+), 25 deletions(-) (limited to 'ipalib') diff --git a/ipalib/errors.py b/ipalib/errors.py index 097747ac..c00db9dc 100644 --- a/ipalib/errors.py +++ b/ipalib/errors.py @@ -240,3 +240,140 @@ class MissingOverrideError(RegistrationError): def __str__(self): return self.msg % (self.base.__name__, self.cls.__name__, self.cls) + +class GenericError(IPAError): + """Base class for our custom exceptions""" + faultCode = 1000 + fromFault = False + def __str__(self): + try: + return str(self.args[0]['args'][0]) + except: + try: + return str(self.args[0]) + except: + return str(self.__dict__) + +class DatabaseError(GenericError): + """A database error has occurred""" + faultCode = 1001 + +class MidairCollision(GenericError): + """Change collided with another change""" + faultCode = 1002 + +class NotFound(GenericError): + """Entry not found""" + faultCode = 1003 + +class Duplicate(GenericError): + """This entry already exists""" + faultCode = 1004 + +class MissingDN(GenericError): + """The distinguished name (DN) is missing""" + faultCode = 1005 + +class EmptyModlist(GenericError): + """No modifications to be performed""" + faultCode = 1006 + +class InputError(GenericError): + """Error on input""" + faultCode = 1007 + +class SameGroupError(InputError): + """You can't add a group to itself""" + faultCode = 1008 + +class AdminsImmutable(InputError): + """The admins group cannot be renamed""" + faultCode = 1009 + +class UsernameTooLong(InputError): + """The requested username is too long""" + faultCode = 1010 + +class PrincipalError(GenericError): + """There is a problem with the kerberos principal""" + faultCode = 1011 + +class MalformedServicePrincipal(PrincipalError): + """The requested service principal is not of the form: service/fully-qualified host name""" + faultCode = 1012 + +class RealmMismatch(PrincipalError): + """The realm for the principal does not match the realm for this IPA server""" + faultCode = 1013 + +class PrincipalRequired(PrincipalError): + """You cannot remove IPA server service principals""" + faultCode = 1014 + +class InactivationError(GenericError): + """This entry cannot be inactivated""" + faultCode = 1015 + +class ConnectionError(GenericError): + """Connection to database failed""" + faultCode = 1016 + +class NoCCacheError(GenericError): + """No Kerberos credentials cache is available. Connection cannot be made""" + faultCode = 1017 + +class GSSAPIError(GenericError): + """GSSAPI Authorization error""" + faultCode = 1018 + +class ServerUnwilling(GenericError): + """Account inactivated. Server is unwilling to perform""" + faultCode = 1018 + +class ConfigurationError(GenericError): + """A configuration error occurred""" + faultCode = 1019 + +class DefaultGroup(ConfigurationError): + """You cannot remove the default users group""" + faultCode = 1020 + +class FunctionDeprecated(GenericError): + """Raised by a deprecated function""" + faultCode = 2000 + +def convertFault(fault): + """Convert a fault to the corresponding Exception type, if possible""" + code = getattr(fault,'faultCode',None) + if code is None: + return fault + for v in globals().values(): + if type(v) == type(Exception) and issubclass(v,GenericError) and \ + code == getattr(v,'faultCode',None): + ret = v(fault.faultString) + ret.fromFault = True + return ret + #otherwise... + return fault + +def listFaults(): + """Return a list of faults + + Returns a list of dictionaries whose keys are: + faultCode: the numeric code used in fault conversion + name: the name of the exception + desc: the description of the exception (docstring) + """ + ret = [] + for n,v in globals().items(): + if type(v) == type(Exception) and issubclass(v,GenericError): + code = getattr(v,'faultCode',None) + if code is None: + continue + info = {} + info['faultCode'] = code + info['name'] = n + info['desc'] = getattr(v,'__doc__',None) + ret.append(info) + ret.sort(lambda a,b: cmp(a['faultCode'],b['faultCode'])) + return ret diff --git a/ipalib/plugins/b_xmlrpc.py b/ipalib/plugins/b_xmlrpc.py index feb87556..442afebf 100644 --- a/ipalib/plugins/b_xmlrpc.py +++ b/ipalib/plugins/b_xmlrpc.py @@ -29,6 +29,7 @@ import socket from ipalib.backend import Backend from ipalib.util import xmlrpc_marshal from ipalib import api +from ipalib import errors class xmlrpc(Backend): """ @@ -51,5 +52,7 @@ class xmlrpc(Backend): except socket.error, e: print e[1] return False + except xmlrpclib.Fault, e: + raise errors.convertFault(e) api.register(xmlrpc) diff --git a/ipalib/plugins/f_user.py b/ipalib/plugins/f_user.py index f87ddea1..8a1c3045 100644 --- a/ipalib/plugins/f_user.py +++ b/ipalib/plugins/f_user.py @@ -25,6 +25,7 @@ from ipalib import frontend from ipalib import crud from ipalib.frontend import Param from ipalib import api +from ipalib import errors from ipa_server import servercore from ipa_server import ipaldap import ldap @@ -32,7 +33,7 @@ import ldap # Command to get the idea how plugins will interact with api.env class envtest(frontend.Command): 'Show current environment.' - def run(*args, **kw): + def run(self, *args, **kw): print "" print "Environment variables:" for var in api.env: @@ -87,18 +88,12 @@ class user_add(crud.Add): user = kw - if not isinstance(user, dict): - # FIXME, need proper error - raise SyntaxError - user['uid'] = args[0] if servercore.user_exists(user['uid']): - # FIXME, specific error - raise SyntaxError("user already exists") + raise errors.Duplicate("user already exists") if servercore.uid_too_long(user['uid']): - # FIXME, specific error - raise SyntaxError("uid is too long") + raise errors.UsernameTooLong # dn is set here, not by the user try: @@ -139,17 +134,15 @@ class user_add(crud.Add): default_group = servercore.get_entry_by_dn(group_dn, ['dn','gidNumber']) if default_group: user['gidnumber'] = default_group.get('gidnumber') -# except ipaerror.exception_for(ipaerror.LDAP_DATABASE_ERROR), e: -# raise ipaerror.gen_exception(ipaerror.LDAP_DATABASE_ERROR, message=None, nested_exception=e.detail) -# except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND): -# # Fake an LDAP error so we can return something useful to the user -# raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND, "The default group for new users, '%s', cannot be found." % config.get('ipadefaultprimarygroup')) + except errors.NotFound: + # Fake an LDAP error so we can return something useful to the user + raise ipalib.NotFound, "The default group for new users, '%s', cannot be found." % config.get('ipadefaultprimarygroup') except Exception, e: - # FIXME + # catch everything else raise e if user.get('krbprincipalname') is None: - user['krbprincipalname'] = "%s@%s" % (user.get('uid'), self.realm) + user['krbprincipalname'] = "%s@%s" % (user.get('uid'), servercore.realm) # FIXME. This is a hack so we can request separate First and Last # name in the GUI. @@ -169,7 +162,7 @@ class user_add(crud.Add): return result def forward(self, *args, **kw): result = super(crud.Add, self).forward(*args, **kw) - if result != False: + if result: print "User %s added" % args[0] api.register(user_add) @@ -190,17 +183,18 @@ class user_del(crud.Del): """ uid = args[0] if uid == "admin": - raise ipaerror.gen_exception(ipaerror.INPUT_ADMIN_REQUIRED) + # FIXME: do we still want a "special" user? + raise SyntaxError("admin required") +# raise ipaerror.gen_exception(ipaerror.INPUT_ADMIN_REQUIRED) # logging.info("IPA: delete_user '%s'" % uid) user = servercore.get_user_by_uid(uid, ['dn', 'uid']) if not user: - # FIXME, specific error - raise SyntaxError("user doesn't exist") + raise errors.NotFound return servercore.delete_entry(user['dn']) def forward(self, *args, **kw): result = super(crud.Del, self).forward(*args, **kw) - if result != False: + if result: print "User %s removed" % args[0] api.register(user_del) @@ -224,7 +218,7 @@ class user_mod(crud.Mod): return result def forward(self, *args, **kw): result = super(crud.Mod, self).forward(*args, **kw) - if result != False: + if result: print "User %s modified" % args[0] api.register(user_mod) @@ -259,7 +253,11 @@ class user_show(crud.Get): result = servercore.get_user_by_uid(uid, ["*"]) return result def forward(self, *args, **kw): - result = super(crud.Get, self).forward(*args, **kw) - for a in result: - print a, ": ", result[a] + try: + result = super(crud.Get, self).forward(*args, **kw) + if not result: return + for a in result: + print a, ": ", result[a] + except errors.NotFound: + print "User %s not found" % args[0] api.register(user_show) -- cgit From 5c07d978659b3f91441a42295531539a1ae8eacc Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Thu, 9 Oct 2008 01:43:23 -0400 Subject: Slight change to how exceptions are handled --- ipalib/errors.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'ipalib') diff --git a/ipalib/errors.py b/ipalib/errors.py index c00db9dc..d0d917f6 100644 --- a/ipalib/errors.py +++ b/ipalib/errors.py @@ -266,7 +266,7 @@ class NotFound(GenericError): """Entry not found""" faultCode = 1003 -class Duplicate(GenericError): +class DuplicateEntry(GenericError): """This entry already exists""" faultCode = 1004 @@ -349,7 +349,7 @@ def convertFault(fault): return fault for v in globals().values(): if type(v) == type(Exception) and issubclass(v,GenericError) and \ - code == getattr(v,'faultCode',None): + code == getattr(v,'faultCode',None): ret = v(fault.faultString) ret.fromFault = True return ret -- cgit From 8a97b3e8a8f437cd99cc7cabbc719368b0247983 Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Thu, 9 Oct 2008 23:11:03 -0400 Subject: Implement group-del --- ipalib/plugins/b_xmlrpc.py | 9 +++++++-- ipalib/plugins/f_group.py | 48 ++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 53 insertions(+), 4 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugins/b_xmlrpc.py b/ipalib/plugins/b_xmlrpc.py index 442afebf..da76aa2b 100644 --- a/ipalib/plugins/b_xmlrpc.py +++ b/ipalib/plugins/b_xmlrpc.py @@ -51,8 +51,13 @@ class xmlrpc(Backend): return command(*params) except socket.error, e: print e[1] - return False except xmlrpclib.Fault, e: - raise errors.convertFault(e) + err = errors.convertFault(e) + code = getattr(err,'faultCode',None) + if code: + print "%s: %s" % (code, getattr(err,'__doc__','')) + else: + raise err + return False api.register(xmlrpc) diff --git a/ipalib/plugins/f_group.py b/ipalib/plugins/f_group.py index c5a37e72..fd56b3ff 100644 --- a/ipalib/plugins/f_group.py +++ b/ipalib/plugins/f_group.py @@ -27,6 +27,7 @@ from ipalib.frontend import Param from ipalib import api from ipa_server import servercore from ipa_server import ipaldap +from ipa_server import ipautil import ldap @@ -83,13 +84,49 @@ class group_add(crud.Add): result = servercore.add_entry(entry) return result - - + def forward(self, *args, **kw): + result = super(crud.Add, self).forward(*args, **kw) + if result: + print "Group %s added" % args[0] api.register(group_add) class group_del(crud.Del): 'Delete an existing group.' + def execute(self, *args, **kw): + """args[0] = dn of the group to remove + + Delete a group + + The memberOf plugin handles removing the group from any other + groups. + """ + group_dn = args[0] + + group = servercore.get_entry_by_dn(group_dn, ['dn', 'cn']) + if group is None: + raise errors.NotFound +# logging.info("IPA: delete_group '%s'" % group_dn) + + # We have 2 special groups, don't allow them to be removed + # FIXME +# if "admins" in group.get('cn') or "editors" in group.get('cn'): +# raise ipaerror.gen_exception(ipaerror.CONFIG_REQUIRED_GROUPS) + + # Don't allow the default user group to be removed + config=servercore.get_ipa_config() + default_group = servercore.get_entry_by_cn(config.get('ipadefaultprimarygroup'), None) + if group_dn == default_group.get('dn'): + raise errors.DefaultGroup + + return servercore.delete_entry(group_dn) + def forward(self, *args, **kw): + group = self.api.Command['group_show'](ipautil.utf8_encode_value(args[0])) + if not group: + print "nothing found" + return False + a = group.get('dn') + result = super(crud.Del, self).forward(a) api.register(group_del) @@ -113,4 +150,11 @@ api.register(group_find) class group_show(crud.Get): 'Examine an existing group.' + def execute(self, *args, **kw): + cn=args[0] + result = servercore.get_sub_entry(servercore.basedn, "cn=%s" % cn, ["*"]) + return result + def forward(self, *args, **kw): + result = super(crud.Get, self).forward(*args, **kw) + return result api.register(group_show) -- cgit From 5d2a99925d4b8f8bb39dfbf4ae797d9845366109 Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Thu, 9 Oct 2008 23:32:28 -0400 Subject: Implement group-mod --- ipalib/plugins/f_group.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) (limited to 'ipalib') diff --git a/ipalib/plugins/f_group.py b/ipalib/plugins/f_group.py index fd56b3ff..eeb18c5c 100644 --- a/ipalib/plugins/f_group.py +++ b/ipalib/plugins/f_group.py @@ -132,6 +132,25 @@ api.register(group_del) class group_mod(crud.Mod): 'Edit an existing group.' + def execute(self, *args, **kw): + group_cn=args[0] + result = servercore.get_entry_by_cn(group_cn, ["*"]) + + group = kw + dn = result.get('dn') + del result['dn'] + entry = ipaldap.Entry((dn, servercore.convert_scalar_values(result))) + + for g in group: + entry.setValues(g, group[g]) + + result = servercore.update_entry(entry.toDict()) + + return result + def forward(self, *args, **kw): + result = super(crud.Mod, self).forward(*args, **kw) + if result: + print "Group %s modified" % args[0] api.register(group_mod) -- cgit From dbe49423ab16ed9ad01166feee22b68b7b05c725 Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Fri, 10 Oct 2008 03:36:39 -0400 Subject: Start service principal plugin --- ipalib/plugins/f_service.py | 155 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 ipalib/plugins/f_service.py (limited to 'ipalib') diff --git a/ipalib/plugins/f_service.py b/ipalib/plugins/f_service.py new file mode 100644 index 00000000..0db1171c --- /dev/null +++ b/ipalib/plugins/f_service.py @@ -0,0 +1,155 @@ +# Authors: +# Jason Gerard DeRose +# +# 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 + +""" +Frontend plugins for service (Identity). +""" + +from ipalib import frontend +from ipalib import crud +from ipalib.frontend import Param +from ipalib import api +from ipalib import errors +from ipalib import ipa_types +from ipa_server import servercore +from ipa_server import ipaldap +import ldap + +class service(frontend.Object): + """ + Service object. + """ + takes_params = ( + Param('principal', primary_key=True), + ) +api.register(service) + + +class service_add(crud.Add): + 'Add a new service.' + """ + my_params = ( + Param('force', type=ipa_types.Bool(), default=False), + ) + def get_options(self): + for param in self.my_params: + yield param + """ + def execute(self, *args, **kw): + """args[0] = service principal to add + kw{force} determines whether we continue on errors + """ + force = kw.get('force', False) + + principal = args[0] + + # Break down the principal into its component parts, which may or + # may not include the realm. + sp = principal.split('/') + if len(sp) != 2: + raise errors.MalformedServicePrincipal + service = sp[0] + + sr = sp[1].split('@') + if len(sr) == 1: + hostname = sr[0].lower() + realm = servercore.realm + elif len(sr) == 2: + hostname = sr[0].lower() + realm = sr[1] + else: + raise MalformedServicePrincipal + + """ + FIXME once DNS client is done + if not force: + fqdn = hostname + "." + rs = dnsclient.query(fqdn, dnsclient.DNS_C_IN, dnsclient.DNS_T_A) + if len(rs) == 0: + logging.debug("IPA: DNS A record lookup failed for '%s'" % hostname) + raise ipaerror.gen_exception(ipaerror.INPUT_NOT_DNS_A_RECORD) + else: + logging.debug("IPA: found %d records for '%s'" % (len(rs), hostname)) + """ + + service_container = servercore.DefaultServiceContainer + + # At some point we'll support multiple realms + if (realm != servercore.realm): + raise errors.RealmMismatch + + # Put the principal back together again + princ_name = service + "/" + hostname + "@" + realm + + dn = "krbprincipalname=%s,%s,%s" % (ldap.dn.escape_dn_chars(princ_name), + service_container,servercore.basedn) + entry = ipaldap.Entry(dn) + + entry.setValues('objectClass', 'krbPrincipal', 'krbPrincipalAux', 'krbTicketPolicyAux') + entry.setValues('krbprincipalname', princ_name) + + result = servercore.add_entry(entry) + return result + def forward(self, *args, **kw): + result = super(crud.Add, self).forward(*args, **kw) + if result: + print "Service %s added" % args[0] +api.register(service_add) + + +class service_del(crud.Del): + 'Delete an existing service.' + def execute(self, *args, **kw): + """args[0] = princial to remove + + Delete a service principal. + + principal is the full DN of the entry to delete. + + This should be called with much care. + """ + principal = args[0] + return False + def forward(self, *args, **kw): + result = super(crud.Del, self).forward(*args, **kw) + if result: + print "Service %s removed" % args[0] +api.register(service_del) + + +class service_mod(crud.Mod): + 'Edit an existing service.' +api.register(service_mod) + + +class service_find(crud.Find): + 'Search the existing services.' +api.register(service_find) + + +class service_show(crud.Get): + 'Examine an existing service.' + def execute(self, *args, **kw): + filter = "(&(objectclass=krbPrincipalAux)(!(objectClass=person))(!(|(krbprincipalname=kadmin/*)(krbprincipalname=K/M@*)(krbprincipalname=krbtgt/*)))(&(|(krbprincipalname=%s))))" % args[0] + result = servercore.get_sub_entry(servercore.basedn, filter, ["*"]) + return result + def forward(self, *args, **kw): + result = super(crud.Get, self).forward(*args, **kw) + return result +api.register(service_show) -- cgit From 42cdca3e8340c9aae721d582a522b7991ea38050 Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Fri, 10 Oct 2008 03:40:52 -0400 Subject: Use new options handler --- ipalib/plugins/f_service.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugins/f_service.py b/ipalib/plugins/f_service.py index 0db1171c..baed5233 100644 --- a/ipalib/plugins/f_service.py +++ b/ipalib/plugins/f_service.py @@ -43,14 +43,9 @@ api.register(service) class service_add(crud.Add): 'Add a new service.' - """ - my_params = ( - Param('force', type=ipa_types.Bool(), default=False), + takes_options = ( + Param('force?', type=ipa_types.Bool(), default=False, doc='Force a service principal name'), ) - def get_options(self): - for param in self.my_params: - yield param - """ def execute(self, *args, **kw): """args[0] = service principal to add kw{force} determines whether we continue on errors -- cgit From 75bad44c27bff471c03ddc86283506f53f47520c Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Fri, 10 Oct 2008 05:23:00 -0400 Subject: Enable the verbose flag to pass thru xmlrpc --- ipalib/plugable.py | 2 ++ ipalib/plugins/b_xmlrpc.py | 6 +++--- ipalib/plugins/f_user.py | 2 ++ 3 files changed, 7 insertions(+), 3 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index ffe4a11f..87f96876 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -738,6 +738,8 @@ class Environment(object): continue self[key] = value + def get(self, name, default=None): + return self.__map.get(name, default) class API(DictProxy): """ diff --git a/ipalib/plugins/b_xmlrpc.py b/ipalib/plugins/b_xmlrpc.py index da76aa2b..618f8385 100644 --- a/ipalib/plugins/b_xmlrpc.py +++ b/ipalib/plugins/b_xmlrpc.py @@ -36,15 +36,15 @@ class xmlrpc(Backend): Kerberos backend plugin. """ - def get_client(self): + def get_client(self, verbose=False): # FIXME: The server uri should come from self.api.env.server_uri - return xmlrpclib.ServerProxy('http://localhost:8888', allow_none=True) + return xmlrpclib.ServerProxy('http://localhost:8888', verbose=verbose) def forward_call(self, name, *args, **kw): """ Forward a call over XML-RPC to an IPA server. """ - client = self.get_client() + client = self.get_client(verbose=api.env.get('verbose', False)) command = getattr(client, name) params = xmlrpc_marshal(*args, **kw) try: diff --git a/ipalib/plugins/f_user.py b/ipalib/plugins/f_user.py index 8a1c3045..9dbc93cb 100644 --- a/ipalib/plugins/f_user.py +++ b/ipalib/plugins/f_user.py @@ -231,6 +231,8 @@ class user_find(crud.Find): return result def forward(self, *args, **kw): users = super(crud.Find, self).forward(*args, **kw) + if not users: + return counter = users[0] users = users[1:] if counter == 0: -- cgit From 250734aea539f0c49c21cb1cdc0310fcbf19f65c Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Fri, 10 Oct 2008 14:38:09 -0400 Subject: Fix syntax error --- ipalib/plugins/f_delegation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/plugins/f_delegation.py b/ipalib/plugins/f_delegation.py index 762df1db..1fb2b4f9 100644 --- a/ipalib/plugins/f_delegation.py +++ b/ipalib/plugins/f_delegation.py @@ -40,7 +40,7 @@ class delegation(frontend.Object): 'target', Param('name', primary_key=True) ) -api.register(user) +api.register(delegation) class delegation_add(crud.Add): -- cgit From 39ad5ccffa60e42904b7f3d2f7a60fef5977f089 Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Sat, 11 Oct 2008 00:49:05 -0400 Subject: Stub out delegations Add ACI class --- ipalib/aci.py | 236 +++++++++++++++++++++++++++++++++++++++++ ipalib/plugins/f_delegation.py | 68 ++++++++++++ 2 files changed, 304 insertions(+) create mode 100755 ipalib/aci.py create mode 100644 ipalib/plugins/f_delegation.py (limited to 'ipalib') diff --git a/ipalib/aci.py b/ipalib/aci.py new file mode 100755 index 00000000..17956812 --- /dev/null +++ b/ipalib/aci.py @@ -0,0 +1,236 @@ +# Authors: +# Rob Crittenden +# +# 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 + +import shlex +import re +import ldap + +# The Python re module doesn't do nested parenthesis + +# Break the ACI into 3 pieces: target, name, permissions/bind_rules +ACIPat = re.compile(r'\s*(\(.*\)+)\s*\(version\s+3.0\s*;\s*acl\s+\"(.*)\"\s*;\s*(.*);\)') + +# Break the permissions/bind_rules out +PermPat = re.compile(r'(\w+)\s*\((.*)\)\s+(.*)') + + +class ACI: + """ + Holds the basic data for an ACI entry, as stored in the cn=accounts + entry in LDAP. Has methods to parse an ACI string and export to an + ACI String. + """ + + # Don't allow arbitrary attributes to be set in our __setattr__ implementation. + _objectattrs = ["name", "orig_acistr", "target", "action", "permissions", + "bindrule"] + + __actions = ["allow", "deny"] + + __permissions = ["read", "write", "add", "delete", "search", "compare", + "selfwrite", "proxy", "all"] + + def __init__(self,acistr=None): + self.name = None + self.orig_acistr = acistr + self.target = {} + self.action = "allow" + self.permissions = ["write"] + self.bindrule = None + if acistr is not None: + self._parse_acistr(acistr) + + def __getitem__(self,key): + """Fake getting attributes by key for sorting""" + if key == 0: + return self.name + if key == 1: + return self.source_group + if key == 2: + return self.dest_group + raise TypeError("Unknown key value %s" % key) + + def __repr__(self): + """An alias for export_to_string()""" + return self.export_to_string() + + def __getattr__(self, name): + """Backwards compatibility for the old ACI class. + The following extra attributes are available: + source_group, dest_group and attrs. + """ + if name == 'source_group': + group = '' + dn = self.bindrule.split('=',1) + if dn[0] == "groupdn": + group = self._remove_quotes(dn[1]) + if group.startswith("ldap:///"): + group = group[8:] + return group + if name == 'dest_group': + group = self.target.get('targetfilter', '') + if group: + g = group.split('=',1)[1] + if g.endswith(')'): + g = g[:-1] + return g + return '' + if name == 'attrs': + return self.target.get('targetattr', None) + raise AttributeError, "object has no attribute '%s'" % name + + def __setattr__(self, name, value): + """Backwards compatibility for the old ACI class. + The following extra attributes are available: + source_group, dest_group and attrs. + """ + if name == 'source_group': + self.__dict__['bindrule'] = 'groupdn="ldap:///%s"' % value + elif name == 'dest_group': + if value.startswith('('): + self.__dict__['target']['targetfilter'] = 'memberOf=%s' % value + else: + self.__dict__['target']['targetfilter'] = '(memberOf=%s)' % value + elif name == 'attrs': + self.__dict__['target']['targetattr'] = value + elif name in self._objectattrs: + self.__dict__[name] = value + else: + raise AttributeError, "object has no attribute '%s'" % name + + def export_to_string(self): + """Output a Directory Server-compatible ACI string""" + self.validate() + aci = "" + for t in self.target: + if isinstance(self.target[t], list): + target = "" + for l in self.target[t]: + target = target + l + " || " + target = target[:-4] + aci = aci + "(%s=\"%s\")" % (t, target) + else: + aci = aci + "(%s=\"%s\")" % (t, self.target[t]) + aci = aci + "(version 3.0;acl \"%s\";%s (%s) %s" % (self.name, self.action, ",".join(self.permissions), self.bindrule) + ";)" + return aci + + def _remove_quotes(self, s): + # Remove leading and trailing quotes + if s.startswith('"'): + s = s[1:] + if s.endswith('"'): + s = s[:-1] + return s + + def _parse_target(self, aci): + lexer = shlex.shlex(aci) + lexer.wordchars = lexer.wordchars + "." + + l = [] + + var = False + for token in lexer: + # We should have the form (a = b)(a = b)... + if token == "(": + var = lexer.next().strip() + operator = lexer.next() + if operator != "=" and operator != "!=": + raise SyntaxError('No operator in target, got %s' % operator) + val = lexer.next().strip() + val = self._remove_quotes(val) + end = lexer.next() + if end != ")": + raise SyntaxError('No end parenthesis in target, got %s' % end) + + if var == 'targetattr': + # Make a string of the form attr || attr || ... into a list + t = re.split('[\W]+', val) + self.target[var] = t + else: + self.target[var] = val + + def _parse_acistr(self, acistr): + acimatch = ACIPat.match(acistr) + if not acimatch or len(acimatch.groups()) < 3: + raise SyntaxError, "malformed ACI" + self._parse_target(acimatch.group(1)) + self.name = acimatch.group(2) + bindperms = PermPat.match(acimatch.group(3)) + if not bindperms or len(bindperms.groups()) < 3: + raise SyntaxError, "malformed ACI" + self.action = bindperms.group(1) + self.permissions = bindperms.group(2).split(',') + self.bindrule = bindperms.group(3) + + def validate(self): + """Do some basic verification that this will produce a + valid LDAP ACI. + + returns True if valid + """ + if not isinstance(self.permissions, list): + raise SyntaxError, "permissions must be a list" + for p in self.permissions: + if not p.lower() in self.__permissions: + raise SyntaxError, "invalid permission: '%s'" % p + if not self.name: + raise SyntaxError, "name must be set" + if not isinstance(self.name, basestring): + raise SyntaxError, "name must be a string" + if not isinstance(self.target, dict) or len(self.target) == 0: + raise SyntaxError, "target must be a non-empty dictionary" + return True + +def extract_group_cns(aci_list, client): + """Extracts all the cn's from a list of aci's and returns them as a hash + from group_dn to group_cn. + + It first tries to cheat by looking at the first rdn for the + group dn. If that's not cn for some reason, it looks up the group.""" + group_dn_to_cn = {} + for aci in aci_list: + for dn in (aci.source_group, aci.dest_group): + if not group_dn_to_cn.has_key(dn): + rdn_list = ldap.explode_dn(dn, 0) + first_rdn = rdn_list[0] + (type,value) = first_rdn.split('=') + if type == "cn": + group_dn_to_cn[dn] = value + else: + try: + group = client.get_entry_by_dn(dn, ['cn']) + group_dn_to_cn[dn] = group.getValue('cn') + except ipaerror.IPAError, e: + group_dn_to_cn[dn] = 'unknown' + + return group_dn_to_cn + +if __name__ == '__main__': + # Pass in an ACI as a string + a = ACI('(targetattr="title")(targetfilter="(memberOf=cn=bar,cn=groups,cn=accounts ,dc=example,dc=com)")(version 3.0;acl "foobar";allow (write) groupdn="ldap:///cn=foo,cn=groups,cn=accounts,dc=example,dc=com";)') + print a + + # Create an ACI in pieces + a = ACI() + a.name ="foobar" + a.source_group="cn=foo,cn=groups,dc=example,dc=org" + a.dest_group="cn=bar,cn=groups,dc=example,dc=org" + a.attrs = ['title'] + a.permissions = ['read','write','add'] + print a diff --git a/ipalib/plugins/f_delegation.py b/ipalib/plugins/f_delegation.py new file mode 100644 index 00000000..762df1db --- /dev/null +++ b/ipalib/plugins/f_delegation.py @@ -0,0 +1,68 @@ +# Authors: +# Rob Crittenden +# +# 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 + +""" +Frontend plugins for delegations. +""" + +from ipalib import frontend +from ipalib import crud +from ipalib.frontend import Param +from ipalib import api +from ipalib import errors +from ipa_server import servercore +from ipa_server import ipaldap +import ldap + +class delegation(frontend.Object): + """ + Delegation object. + """ + takes_params = ( + 'attributes', + 'source', + 'target', + Param('name', primary_key=True) + ) +api.register(user) + + +class delegation_add(crud.Add): + 'Add a new delegation.' +api.register(delegation_add) + + +class delegation_del(crud.Del): + 'Delete an existing delegation.' +api.register(delegation_del) + + +class delegation_mod(crud.Mod): + 'Edit an existing delegation.' +api.register(delegation_mod) + + +class delegation_find(crud.Find): + 'Search for a delegation.' +api.register(delegation_find) + + +class delegation_show(crud.Get): + 'Examine an existing delegation.' +api.register(delegation_show) -- cgit From 225e2b0c939d81b490c955762e125e8afcd5bb94 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 13 Oct 2008 09:50:29 -0600 Subject: Added CrudBackend abstract class defining generic CRUD API --- ipalib/crud.py | 76 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/crud.py b/ipalib/crud.py index 1bdd03f8..5a60ac8c 100644 --- a/ipalib/crud.py +++ b/ipalib/crud.py @@ -21,7 +21,7 @@ Base classes for standard CRUD operations. """ -import frontend, errors +import backend, frontend, errors class Add(frontend.Method): @@ -63,3 +63,77 @@ class Find(frontend.Method): def get_options(self): for param in self.obj.params_minus_pk(): yield param.__clone__(required=False) + + +class CrudBackend(backend.Backend): + """ + Base class defining generic CRUD backend API. + """ + + def create(self, *kw): + """ + Create a new entry. + + This method should take key word arguments representing the + attributes the created entry will have. + + If this methods constructs the primary_key internally, it should raise + an exception if the primary_key was passed. Likewise, if this method + requires the primary_key to be passed in from the caller, it should + raise an exception if the primary key was *not* passed. + + This method should return a dict of the exact entry as it was created + in the backing store, including any automatically created attributes. + """ + raise NotImplementedError('%s.create()' % self.name) + + def retrieve(self, primary_key): + """ + Retrieve an existing entry. + + This method should take a single argument, the primary_key of the + entry in question. + + If such an entry exists, this method should return a dict + representing that entry. If no such entry exists, this method + should return None. + """ + raise NotImplementedError('%s.retrieve()' % self.name) + + def update(self, primary_key, *kw): + """ + Update an existing entry. + + This method should take one required argument, the primary_key of the + entry to modify, plus optional keyword arguments for each of the + attributes being updated. + + This method should return a dict representing the entry as it now + exists in the backing store. If no such entry exists, this method + should return None. + """ + raise NotImplementedError('%s.update()' % self.name) + + def delete(self, primary_key): + """ + Delete an existing entry. + + This method should take one required argument, the primary_key of the + entry to delete. + """ + raise NotImplementedError('%s.delete()' % self.name) + + def search(self, **kw): + """ + Return entries matching specific criteria. + + This method should take keyword arguments representing the search + criteria. If a key is the name of an entry attribute, the value + should be treated as a filter on that attribute. The meaning of + keys outside this namespace is left to the implementation. + + This method should return and iterable containing the matched + entries, where each entry is a dict. If no entries are matched, + this method should return an empty iterable. + """ + raise NotImplementedError('%s.search()' % self.name) -- cgit From 0ebaad646207c8819097124d0e483251d9d5d47d Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Mon, 13 Oct 2008 14:59:48 -0400 Subject: Do a more specific search for the user --- ipalib/plugins/f_user.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/plugins/f_user.py b/ipalib/plugins/f_user.py index 9dbc93cb..573a2a43 100644 --- a/ipalib/plugins/f_user.py +++ b/ipalib/plugins/f_user.py @@ -203,7 +203,9 @@ class user_mod(crud.Mod): 'Edit an existing user.' def execute(self, *args, **kw): uid=args[0] - result = servercore.get_sub_entry(servercore.basedn, "uid=%s" % uid, ["*"]) + + # Get the existing user entry + result = servercore.get_sub_entry("cn=accounts," + servercore.basedn, "uid=%s" % uid, ["*"]) user = kw dn = result.get('dn') -- cgit From fc9f057792cf91a2a2536719ac7f1eb836b1c4a1 Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Mon, 13 Oct 2008 15:01:08 -0400 Subject: Initial implementation of password policy --- ipalib/plugins/f_pwpolicy.py | 100 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 ipalib/plugins/f_pwpolicy.py (limited to 'ipalib') diff --git a/ipalib/plugins/f_pwpolicy.py b/ipalib/plugins/f_pwpolicy.py new file mode 100644 index 00000000..9e5aa3d0 --- /dev/null +++ b/ipalib/plugins/f_pwpolicy.py @@ -0,0 +1,100 @@ +# Authors: +# Rob Crittenden +# +# 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 + +""" +Frontend plugins for password policy. +""" + +from ipalib import frontend +from ipalib import crud +from ipalib.frontend import Param +from ipalib import api +from ipalib import errors +from ipalib import ipa_types +from ipa_server import servercore +from ipa_server import ipaldap +import ldap + + +class pwpolicy_mod(frontend.Command): + 'Edit an existing user.' + # FIXME, switch to more human-readable names at some point + takes_options = ( + Param('krbmaxpwdlife?', type=ipa_types.Int(), doc='Max. Password Lifetime (days)'), + Param('krbminpwdlife?', type=ipa_types.Int(), doc='Min. Password Lifetime (hours)'), + Param('krbpwdhistorylength?', type=ipa_types.Int(), doc='Password History Size'), + Param('krbpwdmindiffchars?', type=ipa_types.Int(), doc='Min. Number of Character Classes'), + Param('krbpwdminlength?', type=ipa_types.Int(), doc='Min. Length of Password'), + ) + def execute(self, *args, **kw): + # Get the existing policy entry + oldpolicy = servercore.get_entry_by_cn("accounts", None) + + # Convert the existing policy into an entry object + dn = oldpolicy.get('dn') + del oldpolicy['dn'] + entry = ipaldap.Entry((dn, servercore.convert_scalar_values(oldpolicy))) + + # FIXME: if the user passed no options should we return something + # more than No modifications to be performed? + + policy = kw + + # The LDAP routines want strings, not ints, so convert a few + # things. Otherwise it sees a string -> int conversion as a change. + for k in policy.iterkeys(): + if k.startswith("krb", 0, 3): + policy[k] = str(policy[k]) + + # Convert hours and days to seconds + if policy.get('krbmaxpwdlife'): + policy['krbmaxpwdlife'] = str(int(policy.get('krbmaxpwdlife')) * 86400) + if policy.get('krbminpwdlife'): + policy['krbminpwdlife'] = str(int(policy.get('krbminpwdlife')) * 3600) + # Update the values passed-in + for p in policy: + # Values need to be strings, not integers + entry.setValues(p, str(policy[p])) + + result = servercore.update_entry(entry.toDict()) + + return result + def forward(self, *args, **kw): + result = super(pwpolicy_mod, self).forward(*args, **kw) + if result: + print "Policy modified" +api.register(pwpolicy_mod) + + +class pwpolicy_show(frontend.Command): + 'Retrieve current password policy' + def execute(self, *args, **kw): + policy = servercore.get_entry_by_cn("accounts", None) + + # convert some values for display purposes + policy['krbmaxpwdlife'] = str(int(policy.get('krbmaxpwdlife')) / 86400) + policy['krbminpwdlife'] = str(int(policy.get('krbminpwdlife')) / 3600) + + return policy + + def forward(self, *args, **kw): + result = super(pwpolicy_show, self).forward(*args, **kw) + if not result: return + print result +api.register(pwpolicy_show) -- cgit From 19465318cebed2a2ae844d33e69728c1eb9fd7d6 Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Mon, 13 Oct 2008 15:17:31 -0400 Subject: Fix up a comment --- ipalib/plugins/f_pwpolicy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/plugins/f_pwpolicy.py b/ipalib/plugins/f_pwpolicy.py index 9e5aa3d0..36e232dc 100644 --- a/ipalib/plugins/f_pwpolicy.py +++ b/ipalib/plugins/f_pwpolicy.py @@ -33,7 +33,7 @@ import ldap class pwpolicy_mod(frontend.Command): - 'Edit an existing user.' + 'Edit existing password policy.' # FIXME, switch to more human-readable names at some point takes_options = ( Param('krbmaxpwdlife?', type=ipa_types.Int(), doc='Max. Password Lifetime (days)'), -- cgit From 6d2705b363e95b5bd692b695cdcbbfcbca6d12b9 Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Mon, 13 Oct 2008 17:17:00 -0400 Subject: Implement user lock and unlock --- ipalib/errors.py | 42 +++++++++++++++++++++++++++++------------- ipalib/plugins/f_user.py | 34 +++++++++++++++++++++++++++++++++- 2 files changed, 62 insertions(+), 14 deletions(-) (limited to 'ipalib') diff --git a/ipalib/errors.py b/ipalib/errors.py index d0d917f6..f1c9e26e 100644 --- a/ipalib/errors.py +++ b/ipalib/errors.py @@ -286,57 +286,73 @@ class SameGroupError(InputError): """You can't add a group to itself""" faultCode = 1008 +class NotGroupMember(InputError): + """This entry is not a member of the group""" + faultCode = 1009 + class AdminsImmutable(InputError): """The admins group cannot be renamed""" - faultCode = 1009 + faultCode = 1010 class UsernameTooLong(InputError): """The requested username is too long""" - faultCode = 1010 + faultCode = 1011 class PrincipalError(GenericError): """There is a problem with the kerberos principal""" - faultCode = 1011 + faultCode = 1012 class MalformedServicePrincipal(PrincipalError): """The requested service principal is not of the form: service/fully-qualified host name""" - faultCode = 1012 + faultCode = 1013 class RealmMismatch(PrincipalError): """The realm for the principal does not match the realm for this IPA server""" - faultCode = 1013 + faultCode = 1014 class PrincipalRequired(PrincipalError): """You cannot remove IPA server service principals""" - faultCode = 1014 + faultCode = 1015 class InactivationError(GenericError): """This entry cannot be inactivated""" - faultCode = 1015 + faultCode = 1016 + +class AlreadyActiveError(InactivationError): + """This entry is already locked""" + faultCode = 1017 + +class AlreadyInactiveError(InactivationError): + """This entry is already unlocked""" + faultCode = 1018 + +class HasNSAccountLock(InactivationError): + """This entry appears to have the nsAccountLock attribute in it so the Class of Service activation/inactivation will not work. You will need to remove the attribute nsAccountLock for this to work.""" + faultCode = 1019 class ConnectionError(GenericError): """Connection to database failed""" - faultCode = 1016 + faultCode = 1020 class NoCCacheError(GenericError): """No Kerberos credentials cache is available. Connection cannot be made""" - faultCode = 1017 + faultCode = 1021 class GSSAPIError(GenericError): """GSSAPI Authorization error""" - faultCode = 1018 + faultCode = 1022 class ServerUnwilling(GenericError): """Account inactivated. Server is unwilling to perform""" - faultCode = 1018 + faultCode = 1023 class ConfigurationError(GenericError): """A configuration error occurred""" - faultCode = 1019 + faultCode = 1024 class DefaultGroup(ConfigurationError): """You cannot remove the default users group""" - faultCode = 1020 + faultCode = 1025 class FunctionDeprecated(GenericError): """Raised by a deprecated function""" diff --git a/ipalib/plugins/f_user.py b/ipalib/plugins/f_user.py index 573a2a43..ff459b3d 100644 --- a/ipalib/plugins/f_user.py +++ b/ipalib/plugins/f_user.py @@ -26,6 +26,7 @@ from ipalib import crud from ipalib.frontend import Param from ipalib import api from ipalib import errors +from ipalib import ipa_types from ipa_server import servercore from ipa_server import ipaldap import ldap @@ -136,7 +137,7 @@ class user_add(crud.Add): user['gidnumber'] = default_group.get('gidnumber') except errors.NotFound: # Fake an LDAP error so we can return something useful to the user - raise ipalib.NotFound, "The default group for new users, '%s', cannot be found." % config.get('ipadefaultprimarygroup') + raise errors.NotFound, "The default group for new users, '%s', cannot be found." % config.get('ipadefaultprimarygroup') except Exception, e: # catch everything else raise e @@ -265,3 +266,34 @@ class user_show(crud.Get): except errors.NotFound: print "User %s not found" % args[0] api.register(user_show) + +class user_lock(frontend.Command): + 'Lock a user account.' + takes_args = ( + Param('uid', primary_key=True), + ) + def execute(self, *args, **kw): + uid = args[0] + user = servercore.get_user_by_uid(uid, ['dn', 'uid']) + return servercore.mark_entry_inactive(user['dn']) + def forward(self, *args, **kw): + result = super(user_lock, self).forward(*args, **kw) + if result: + print "User locked" +api.register(user_lock) + +class user_unlock(frontend.Command): + 'Unlock a user account.' + takes_args = ( + Param('uid', primary_key=True), + ) + def execute(self, *args, **kw): + uid = args[0] + user = servercore.get_user_by_uid(uid, ['dn', 'uid']) + return servercore.mark_entry_active(user['dn']) + def forward(self, *args, **kw): + result = super(user_unlock, self).forward(*args, **kw) + if result: + print "User unlocked" +api.register(user_unlock) + -- cgit From 8674086b8536f64947ca8cdb97d7a1cd3bf1c684 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 13 Oct 2008 17:24:23 -0600 Subject: Param now takes cli_name kwarg that sets Param.cli_name attribute --- ipalib/frontend.py | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 639160c1..4c6a9c8d 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -198,6 +198,7 @@ class Param(plugable.ReadOnly): ============ ================= ================== Keyword Type Default ============ ================= ================== + cli_name str defaults to name type ipa_type.Type ipa_type.Unicode() doc str '' required bool True @@ -210,6 +211,7 @@ class Param(plugable.ReadOnly): """ __nones = (None, '', tuple(), []) __defaults = dict( + cli_name=None, type=ipa_types.Unicode(), doc='', required=True, @@ -226,6 +228,7 @@ class Param(plugable.ReadOnly): (name, kw_from_spec) = parse_param_spec(name) override.update(kw_from_spec) kw = dict(self.__defaults) + kw['cli_name'] = name if not set(kw).issuperset(override): extra = sorted(set(override) - set(kw)) raise TypeError( @@ -234,6 +237,7 @@ class Param(plugable.ReadOnly): kw.update(override) self.__kw = kw self.name = check_name(name) + self.cli_name = check_name(kw.get('cli_name', name)) self.type = self.__check_isinstance(ipa_types.Type, 'type') self.doc = self.__check_type(str, 'doc') self.required = self.__check_type(bool, 'required') -- cgit From b6dcd183a66ca6056f9d23637de0f12aee15efcc Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 13 Oct 2008 20:31:10 -0600 Subject: CLI now maps Param.cli_name to Param.name --- ipalib/cli.py | 5 +++-- ipalib/plugins/f_user.py | 8 ++++---- 2 files changed, 7 insertions(+), 6 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index aae4e31c..378cc4c1 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -321,7 +321,8 @@ class CLI(object): usage=self.get_usage(cmd), ) for option in cmd.options(): - parser.add_option('--%s' % to_cli(option.name), + parser.add_option('--%s' % to_cli(option.cli_name), + dest=option.name, metavar=option.type.name.upper(), help=option.doc, ) @@ -368,7 +369,7 @@ class CLI(object): def get_usage_iter(self, cmd): yield 'Usage: %%prog [global-options] %s' % to_cli(cmd.name) for arg in cmd.args(): - name = to_cli(arg.name).upper() + name = to_cli(arg.cli_name).upper() if arg.multivalue: name = '%s...' % name if arg.required: diff --git a/ipalib/plugins/f_user.py b/ipalib/plugins/f_user.py index ff459b3d..22fb8a27 100644 --- a/ipalib/plugins/f_user.py +++ b/ipalib/plugins/f_user.py @@ -55,9 +55,10 @@ class user(frontend.Object): User object. """ takes_params = ( - 'givenname', - 'sn', + Param('givenname', cli_name='firstname'), + Param('sn', cli_name='lastname'), Param('uid', + cli_name='user', primary_key=True, default_from=lambda givenname, sn: givenname[0] + sn, normalize=lambda value: value.lower(), @@ -78,7 +79,7 @@ class user_add(crud.Add): """args[0] = uid of the user to add kw{container} is the location in the DIT to add the user, not required - kw otherwise contains all the attributes + kw otherwise contains all the attributes """ # FIXME: ug, really? if not kw.get('container'): @@ -296,4 +297,3 @@ class user_unlock(frontend.Command): if result: print "User unlocked" api.register(user_unlock) - -- cgit From 2357360e2a077f56f01e9ce8bc5a21d87fea7675 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 13 Oct 2008 21:53:03 -0600 Subject: Command.params are now sorted the same way as Object.params (make user-add prompt for first, last before login) --- ipalib/frontend.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 4c6a9c8d..50e2dd3e 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -687,8 +687,15 @@ class Command(plugable.Plugin): (create_param(spec) for spec in self.get_options()), sort=False ) + def get_key(p): + if p.required: + if p.default_from is None: + return 0 + return 1 + return 2 self.params = plugable.NameSpace( - tuple(self.args()) + tuple(self.options()), sort=False + sorted(tuple(self.args()) + tuple(self.options()), key=get_key), + sort=False ) super(Command, self).finalize() -- cgit From 22669f1fc2ffb6de8a8d92a64132dd0b31e877b3 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 13 Oct 2008 22:00:18 -0600 Subject: CLI.run_interactive() now uses Param.cli_name instead of Param.name for prompts and errors --- ipalib/cli.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index 378cc4c1..5bebc88d 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -287,13 +287,13 @@ class CLI(object): exit_error('Not enough arguments given') default = param.get_default(**kw) if default is None: - prompt = '%s: ' % param.name + prompt = '%s: ' % param.cli_name else: - prompt = '%s [%s]: ' % (param.name, default) + prompt = '%s [%s]: ' % (param.cli_name, default) error = None while True: if error is not None: - print '>>> %s: %s' % (param.name, error) + print '>>> %s: %s' % (param.cli_name, error) raw = raw_input(prompt) try: value = param(raw, **kw) -- cgit From 446037fd60d59f671b9a402b9111ab041c1c1439 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 13 Oct 2008 23:26:24 -0600 Subject: Added Object.get_dn() method; added corresponding unit tests --- ipalib/frontend.py | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 50e2dd3e..65b053e6 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -750,6 +750,7 @@ class Object(plugable.Plugin): 'params', 'primary_key', 'params_minus_pk', + 'get_dn', )) backend = None methods = None @@ -790,6 +791,12 @@ class Object(plugable.Plugin): if 'Backend' in self.api and self.backend_name in self.api.Backend: self.backend = self.api.Backend[self.backend_name] + def get_dn(self, primary_key): + """ + Construct an LDAP DN from a primary_key. + """ + raise NotImplementedError('%s.get_dn()' % self.name) + def __get_attrs(self, name): if name not in self.api: return -- cgit From 149912d0e7bb75459c52c9404a1af537023729dc Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 14 Oct 2008 00:38:17 -0600 Subject: Added ldap.get_user_dn() method --- ipalib/config.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/config.py b/ipalib/config.py index a0a33b40..c55c47c8 100644 --- a/ipalib/config.py +++ b/ipalib/config.py @@ -25,13 +25,15 @@ DEFAULT_CONF='/etc/ipa/ipa.conf' def generate_env(d={}): default = dict( - server_context = False, + server_context = True, query_dns = True, verbose = False, interactive = True, server = LazyIter(get_servers), realm = LazyProp(get_realm), domain = LazyProp(get_domain), + container_user='cn=users,cn=accounts', + basedn='dc=example,dc=com', ) for key, value in d.iteritems(): if key in default and type(default[key]) in (LazyIter, LazyProp): -- cgit From 20fa90cfb6e954040de47551762dfbb7680dba51 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 14 Oct 2008 00:39:23 -0600 Subject: Some small cleanup on Environment, filled in docstrings --- ipalib/plugable.py | 42 ++++++++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 10 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 87f96876..4a2658a7 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -693,19 +693,37 @@ class Registrar(DictProxy): class Environment(object): + """ + A mapping object used to store the environment variables. + """ + def __init__(self): object.__setattr__(self, '_Environment__map', {}) - def __setattr__(self, name, value): - self[name] = value - def __getattr__(self, name): + """ + Return the attribute named ``name``. + """ return self[name] + def __setattr__(self, name, value): + """ + Set the attribute named ``name`` to ``value``. + """ + self[name] = value + def __delattr__(self, name): - del self[name] + """ + Raise AttributeError (deletion is not allowed). + """ + raise AttributeError('cannot del %s.%s' % + (self.__class__.__name__, name) + ) def __getitem__(self, key): + """ + Return the value corresponding to ``key``. + """ val = self.__map[key] if hasattr(val, 'get_value'): return val.get_value() @@ -713,22 +731,26 @@ class Environment(object): return val def __setitem__(self, key, value): + """ + Set the item at ``key`` to ``value``. + """ if key in self or hasattr(self, key): raise AttributeError('cannot overwrite %s.%s' % (self.__class__.__name__, key) ) self.__map[key] = value - def __delitem__(self, key): - raise AttributeError('read-only: cannot del %s.%s' % - (self.__class__.__name__, key) - ) - def __contains__(self, key): + """ + Return True if instance contains ``key``; otherwise return False. + """ return key in self.__map def __iter__(self): - for key in self.__map: + """ + Iterate through keys in ascending order. + """ + for key in sorted(self.__map): yield key def update(self, new_vals, ignore_errors = False): -- cgit From 1480224724864cb7cf34c9be755b905c61f885b9 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 14 Oct 2008 01:45:30 -0600 Subject: Started roughing out user_add() using api.Backend.ldap; added Command.output_for_cli() to take care of formatting print output --- ipalib/cli.py | 2 +- ipalib/frontend.py | 9 +++++++++ ipalib/plugins/f_user.py | 28 +++++++++++++++++++--------- 3 files changed, 29 insertions(+), 10 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index 5bebc88d..5dd2c44f 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -302,7 +302,7 @@ class CLI(object): break except errors.ValidationError, e: error = e.error - cmd(**kw) + cmd.output_for_cli(cmd(**kw)) def parse(self, cmd, argv): parser = self.build_parser(cmd) diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 65b053e6..da4fd00b 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -486,6 +486,7 @@ class Command(plugable.Plugin): 'params', 'args_to_kw', 'kw_to_args', + 'output_for_cli', )) takes_options = tuple() takes_args = tuple() @@ -741,6 +742,14 @@ class Command(plugable.Plugin): multivalue = True yield arg + def output_for_cli(self, ret): + """ + Output result of this command to command line interface. + """ + assert type(ret) is dict, 'base output_for_cli() only works with dict' + for key in sorted(ret): + print '%s = %r' % (key, ret[key]) + class Object(plugable.Plugin): __public__ = frozenset(( diff --git a/ipalib/plugins/f_user.py b/ipalib/plugins/f_user.py index 22fb8a27..571f6fa8 100644 --- a/ipalib/plugins/f_user.py +++ b/ipalib/plugins/f_user.py @@ -75,12 +75,26 @@ api.register(user) class user_add(crud.Add): 'Add a new user.' - def execute(self, *args, **kw): - """args[0] = uid of the user to add - kw{container} is the location in the DIT to add the user, not - required - kw otherwise contains all the attributes + + def execute(self, uid, **kw): + """ + Execute the user-add operation. + + The dn should not be passed as a keyword argument as it is constructed + by this method. + + Returns the entry as it will be created in LDAP. + + :param uid: The login name of the user being added. + :param kw: Keyword arguments for the other LDAP attributes. """ + assert 'uid' not in kw + assert 'dn' not in kw + kw['uid'] = uid + kw['dn'] = self.api.Backend.ldap.get_user_dn(uid) + + return kw + # FIXME: ug, really? if not kw.get('container'): user_container = servercore.DefaultUserContainer @@ -162,10 +176,6 @@ class user_add(crud.Add): result = servercore.add_entry(entry) return result - def forward(self, *args, **kw): - result = super(crud.Add, self).forward(*args, **kw) - if result: - print "User %s added" % args[0] api.register(user_add) -- cgit From 9788800aa41146551baee6d36314a20203fd9d20 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 14 Oct 2008 02:23:56 -0600 Subject: More work on making user-add use Backend.ldap --- ipalib/plugins/f_user.py | 42 ++++++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 18 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugins/f_user.py b/ipalib/plugins/f_user.py index 571f6fa8..b35a1122 100644 --- a/ipalib/plugins/f_user.py +++ b/ipalib/plugins/f_user.py @@ -55,20 +55,36 @@ class user(frontend.Object): User object. """ takes_params = ( - Param('givenname', cli_name='firstname'), - Param('sn', cli_name='lastname'), + Param('givenname', + cli_name='first', + doc='User first name', + ), + Param('sn', + cli_name='last', + doc='User last name', + ), Param('uid', cli_name='user', primary_key=True, default_from=lambda givenname, sn: givenname[0] + sn, normalize=lambda value: value.lower(), ), - Param('krbprincipalname', - default_from=lambda uid: '%s@EXAMPLE.COM' % uid, + Param('gecos', + doc='GECOS field', + default_from=lambda uid: uid, ), Param('homedirectory', + cli_name='home', + doc='Path of user home directory', default_from=lambda uid: '/home/%s' % uid, - ) + ), + Param('shell', + default=u'/bin/sh', + doc='Login shell', + ), + Param('krbprincipalname?', cli_name='principal', + default_from=lambda uid: '%s@EXAMPLE.COM' % uid, + ), ) api.register(user) @@ -90,21 +106,11 @@ class user_add(crud.Add): """ assert 'uid' not in kw assert 'dn' not in kw + ldap = self.api.Backend.ldap kw['uid'] = uid - kw['dn'] = self.api.Backend.ldap.get_user_dn(uid) - - return kw - - # FIXME: ug, really? - if not kw.get('container'): - user_container = servercore.DefaultUserContainer - else: - user_container = kw['container'] - del kw['container'] - - user = kw + kw['dn'] = ldap.get_user_dn(uid) + return ldap.create(**kw) - user['uid'] = args[0] if servercore.user_exists(user['uid']): raise errors.Duplicate("user already exists") -- cgit From ff88652a405c7fd9236a9b1d80dd8955a9ca056d Mon Sep 17 00:00:00 2001 From: Martin Nagy Date: Tue, 14 Oct 2008 21:22:44 +0200 Subject: Convert string values to boolean when generating environment --- ipalib/config.py | 38 ++++++++++++++++++++++++++++---------- ipalib/plugins/f_user.py | 1 + 2 files changed, 29 insertions(+), 10 deletions(-) (limited to 'ipalib') diff --git a/ipalib/config.py b/ipalib/config.py index c55c47c8..a606a40b 100644 --- a/ipalib/config.py +++ b/ipalib/config.py @@ -25,25 +25,41 @@ DEFAULT_CONF='/etc/ipa/ipa.conf' def generate_env(d={}): default = dict( - server_context = True, - query_dns = True, - verbose = False, + basedn = 'dc=example,dc=com', + container_user = 'cn=users,cn=accounts', + domain = LazyProp(get_domain), interactive = True, - server = LazyIter(get_servers), + query_dns = True, realm = LazyProp(get_realm), - domain = LazyProp(get_domain), - container_user='cn=users,cn=accounts', - basedn='dc=example,dc=com', + server_context = True, + server = LazyIter(get_servers), + verbose = False, ) for key, value in d.iteritems(): - if key in default and type(default[key]) in (LazyIter, LazyProp): - default[key].set_value(value) + if key in default: + if isinstance(default[key], (LazyIter, LazyProp)): + default[key].set_value(value) + else: + default[key] = convert_val(type(default[key]), value) else: - default[key] = value + default[key] = value return default +# TODO: Add a validation function +def convert_val(target_type, value): + bool_true = ('true', 'yes', 'on') + bool_false = ('false', 'no', 'off') + + if target_type == bool and isinstance(value, basestring): + if value.lower() in bool_true: + return True + elif value.lower() in bool_false: + return False + return target_type(value) + + class LazyProp(object): def __init__(self, func, value=None): assert isinstance(func, types.FunctionType) @@ -73,6 +89,8 @@ class LazyIter(LazyProp): yield item +# TODO: Make it possible to use var = 'foo, bar' without +# turning it into ("'foo", "bar'") def read_config(config_file=None): assert config_file == None or isinstance(config_file, (basestring, file)) diff --git a/ipalib/plugins/f_user.py b/ipalib/plugins/f_user.py index b35a1122..b2c191fb 100644 --- a/ipalib/plugins/f_user.py +++ b/ipalib/plugins/f_user.py @@ -47,6 +47,7 @@ class envtest(frontend.Command): print "" else: print " %s: %s" % (var, val) + return {} api.register(envtest) -- cgit From 30664cde88b70f478d75a768426db5f655c5f867 Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Tue, 14 Oct 2008 17:46:36 -0400 Subject: Move some functionality from user-add to the backend ldap create function --- ipalib/plugins/f_user.py | 73 +++++++++++++++--------------------------------- 1 file changed, 23 insertions(+), 50 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugins/f_user.py b/ipalib/plugins/f_user.py index b35a1122..03bc9aa8 100644 --- a/ipalib/plugins/f_user.py +++ b/ipalib/plugins/f_user.py @@ -69,16 +69,17 @@ class user(frontend.Object): default_from=lambda givenname, sn: givenname[0] + sn, normalize=lambda value: value.lower(), ), - Param('gecos', + Param('gecos?', doc='GECOS field', default_from=lambda uid: uid, ), - Param('homedirectory', + Param('homedirectory?', cli_name='home', doc='Path of user home directory', default_from=lambda uid: '/home/%s' % uid, ), - Param('shell', + Param('loginshell?', + cli_name='shell', default=u'/bin/sh', doc='Login shell', ), @@ -109,44 +110,22 @@ class user_add(crud.Add): ldap = self.api.Backend.ldap kw['uid'] = uid kw['dn'] = ldap.get_user_dn(uid) - return ldap.create(**kw) - - if servercore.user_exists(user['uid']): - raise errors.Duplicate("user already exists") - if servercore.uid_too_long(user['uid']): + if servercore.uid_too_long(kw['uid']): raise errors.UsernameTooLong - # dn is set here, not by the user - try: - del user['dn'] - except KeyError: - pass - - # No need to set empty fields, and they can cause issues when they - # get to LDAP, like: - # TypeError: ('expected a string in the list', None) - for k in user.keys(): - if not user[k] or len(user[k]) == 0 or (isinstance(user[k],list) and len(user[k]) == 1 and '' in user[k]): - del user[k] - - dn="uid=%s,%s,%s" % (ldap.dn.escape_dn_chars(user['uid']), - user_container,servercore.basedn) - - entry = ipaldap.Entry(dn) - # Get our configuration config = servercore.get_ipa_config() # Let us add in some missing attributes - if user.get('homedirectory') is None: - user['homedirectory'] = '%s/%s' % (config.get('ipahomesrootdir'), user.get('uid')) - user['homedirectory'] = user['homedirectory'].replace('//', '/') - user['homedirectory'] = user['homedirectory'].rstrip('/') - if user.get('loginshell') is None: - user['loginshell'] = config.get('ipadefaultloginshell') - if user.get('gecos') is None: - user['gecos'] = user['uid'] + if kw.get('homedirectory') is None: + kw['homedirectory'] = '%s/%s' % (config.get('ipahomesrootdir'), kw.get('uid')) + kw['homedirectory'] = kw['homedirectory'].replace('//', '/') + kw['homedirectory'] = kw['homedirectory'].rstrip('/') + if kw.get('loginshell') is None: + kw['loginshell'] = config.get('ipadefaultloginshell') + if kw.get('gecos') is None: + kw['gecos'] = kw['uid'] # If uidnumber is blank the the FDS dna_plugin will automatically # assign the next value. So we don't have to do anything with it. @@ -155,33 +134,27 @@ class user_add(crud.Add): try: default_group = servercore.get_entry_by_dn(group_dn, ['dn','gidNumber']) if default_group: - user['gidnumber'] = default_group.get('gidnumber') + kw['gidnumber'] = default_group.get('gidnumber') except errors.NotFound: - # Fake an LDAP error so we can return something useful to the user - raise errors.NotFound, "The default group for new users, '%s', cannot be found." % config.get('ipadefaultprimarygroup') + # Fake an LDAP error so we can return something useful to the kw + raise errors.NotFound, "The default group for new kws, '%s', cannot be found." % config.get('ipadefaultprimarygroup') except Exception, e: # catch everything else raise e - if user.get('krbprincipalname') is None: - user['krbprincipalname'] = "%s@%s" % (user.get('uid'), servercore.realm) + if kw.get('krbprincipalname') is None: + kw['krbprincipalname'] = "%s@%s" % (kw.get('uid'), servercore.realm) # FIXME. This is a hack so we can request separate First and Last # name in the GUI. - if user.get('cn') is None: - user['cn'] = "%s %s" % (user.get('givenname'), - user.get('sn')) + if kw.get('cn') is None: + kw['cn'] = "%s %s" % (kw.get('givenname'), + kw.get('sn')) # some required objectclasses - entry.setValues('objectClass', (config.get('ipauserobjectclasses'))) - # entry.setValues('objectClass', ['top', 'person', 'organizationalPerson', 'inetOrgPerson', 'inetUser', 'posixAccount', 'krbPrincipalAux']) - - # fill in our new entry with everything sent by the user - for u in user: - entry.setValues(u, user[u]) + kw['objectClass'] = config.get('ipauserobjectclasses') - result = servercore.add_entry(entry) - return result + return ldap.create(**kw) api.register(user_add) -- cgit From 1c3f81852cb8337e2305f968be5bd8165997d27e Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Tue, 14 Oct 2008 17:46:36 -0400 Subject: Move some functionality from user-add to the backend ldap create function --- ipalib/plugins/f_user.py | 73 +++++++++++++++--------------------------------- 1 file changed, 23 insertions(+), 50 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugins/f_user.py b/ipalib/plugins/f_user.py index b2c191fb..e3ecd223 100644 --- a/ipalib/plugins/f_user.py +++ b/ipalib/plugins/f_user.py @@ -70,16 +70,17 @@ class user(frontend.Object): default_from=lambda givenname, sn: givenname[0] + sn, normalize=lambda value: value.lower(), ), - Param('gecos', + Param('gecos?', doc='GECOS field', default_from=lambda uid: uid, ), - Param('homedirectory', + Param('homedirectory?', cli_name='home', doc='Path of user home directory', default_from=lambda uid: '/home/%s' % uid, ), - Param('shell', + Param('loginshell?', + cli_name='shell', default=u'/bin/sh', doc='Login shell', ), @@ -110,44 +111,22 @@ class user_add(crud.Add): ldap = self.api.Backend.ldap kw['uid'] = uid kw['dn'] = ldap.get_user_dn(uid) - return ldap.create(**kw) - - if servercore.user_exists(user['uid']): - raise errors.Duplicate("user already exists") - if servercore.uid_too_long(user['uid']): + if servercore.uid_too_long(kw['uid']): raise errors.UsernameTooLong - # dn is set here, not by the user - try: - del user['dn'] - except KeyError: - pass - - # No need to set empty fields, and they can cause issues when they - # get to LDAP, like: - # TypeError: ('expected a string in the list', None) - for k in user.keys(): - if not user[k] or len(user[k]) == 0 or (isinstance(user[k],list) and len(user[k]) == 1 and '' in user[k]): - del user[k] - - dn="uid=%s,%s,%s" % (ldap.dn.escape_dn_chars(user['uid']), - user_container,servercore.basedn) - - entry = ipaldap.Entry(dn) - # Get our configuration config = servercore.get_ipa_config() # Let us add in some missing attributes - if user.get('homedirectory') is None: - user['homedirectory'] = '%s/%s' % (config.get('ipahomesrootdir'), user.get('uid')) - user['homedirectory'] = user['homedirectory'].replace('//', '/') - user['homedirectory'] = user['homedirectory'].rstrip('/') - if user.get('loginshell') is None: - user['loginshell'] = config.get('ipadefaultloginshell') - if user.get('gecos') is None: - user['gecos'] = user['uid'] + if kw.get('homedirectory') is None: + kw['homedirectory'] = '%s/%s' % (config.get('ipahomesrootdir'), kw.get('uid')) + kw['homedirectory'] = kw['homedirectory'].replace('//', '/') + kw['homedirectory'] = kw['homedirectory'].rstrip('/') + if kw.get('loginshell') is None: + kw['loginshell'] = config.get('ipadefaultloginshell') + if kw.get('gecos') is None: + kw['gecos'] = kw['uid'] # If uidnumber is blank the the FDS dna_plugin will automatically # assign the next value. So we don't have to do anything with it. @@ -156,33 +135,27 @@ class user_add(crud.Add): try: default_group = servercore.get_entry_by_dn(group_dn, ['dn','gidNumber']) if default_group: - user['gidnumber'] = default_group.get('gidnumber') + kw['gidnumber'] = default_group.get('gidnumber') except errors.NotFound: - # Fake an LDAP error so we can return something useful to the user - raise errors.NotFound, "The default group for new users, '%s', cannot be found." % config.get('ipadefaultprimarygroup') + # Fake an LDAP error so we can return something useful to the kw + raise errors.NotFound, "The default group for new kws, '%s', cannot be found." % config.get('ipadefaultprimarygroup') except Exception, e: # catch everything else raise e - if user.get('krbprincipalname') is None: - user['krbprincipalname'] = "%s@%s" % (user.get('uid'), servercore.realm) + if kw.get('krbprincipalname') is None: + kw['krbprincipalname'] = "%s@%s" % (kw.get('uid'), servercore.realm) # FIXME. This is a hack so we can request separate First and Last # name in the GUI. - if user.get('cn') is None: - user['cn'] = "%s %s" % (user.get('givenname'), - user.get('sn')) + if kw.get('cn') is None: + kw['cn'] = "%s %s" % (kw.get('givenname'), + kw.get('sn')) # some required objectclasses - entry.setValues('objectClass', (config.get('ipauserobjectclasses'))) - # entry.setValues('objectClass', ['top', 'person', 'organizationalPerson', 'inetOrgPerson', 'inetUser', 'posixAccount', 'krbPrincipalAux']) - - # fill in our new entry with everything sent by the user - for u in user: - entry.setValues(u, user[u]) + kw['objectClass'] = config.get('ipauserobjectclasses') - result = servercore.add_entry(entry) - return result + return ldap.create(**kw) api.register(user_add) -- cgit From cfc8450efd92dc0fb6648e97b27416c67625adfb Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Tue, 14 Oct 2008 22:22:01 -0400 Subject: Port user-show to new CrudBackend framework --- ipalib/config.py | 3 +++ ipalib/crud.py | 8 +++++--- ipalib/plugins/b_xmlrpc.py | 2 +- ipalib/plugins/f_user.py | 31 ++++++++++++++++++------------- 4 files changed, 27 insertions(+), 17 deletions(-) (limited to 'ipalib') diff --git a/ipalib/config.py b/ipalib/config.py index a606a40b..42bf7787 100644 --- a/ipalib/config.py +++ b/ipalib/config.py @@ -25,8 +25,11 @@ DEFAULT_CONF='/etc/ipa/ipa.conf' def generate_env(d={}): default = dict( + container_accounts = 'cn=accounts', basedn = 'dc=example,dc=com', container_user = 'cn=users,cn=accounts', + container_group = 'cn=groups,cn=accounts', + container_service = 'cn=services,cn=accounts', domain = LazyProp(get_domain), interactive = True, query_dns = True, diff --git a/ipalib/crud.py b/ipalib/crud.py index 5a60ac8c..5cd7b0a4 100644 --- a/ipalib/crud.py +++ b/ipalib/crud.py @@ -87,12 +87,14 @@ class CrudBackend(backend.Backend): """ raise NotImplementedError('%s.create()' % self.name) - def retrieve(self, primary_key): + def retrieve(self, primary_key, attributes): """ Retrieve an existing entry. - This method should take a single argument, the primary_key of the - entry in question. + This method should take a two arguments: the primary_key of the + entry in question and a list of the attributes to be retrieved. + If the list of attributes is None then all non-operational + attributes will be returned. If such an entry exists, this method should return a dict representing that entry. If no such entry exists, this method diff --git a/ipalib/plugins/b_xmlrpc.py b/ipalib/plugins/b_xmlrpc.py index 618f8385..db2af1ab 100644 --- a/ipalib/plugins/b_xmlrpc.py +++ b/ipalib/plugins/b_xmlrpc.py @@ -58,6 +58,6 @@ class xmlrpc(Backend): print "%s: %s" % (code, getattr(err,'__doc__','')) else: raise err - return False + return {} api.register(xmlrpc) diff --git a/ipalib/plugins/f_user.py b/ipalib/plugins/f_user.py index e3ecd223..1e79c4b8 100644 --- a/ipalib/plugins/f_user.py +++ b/ipalib/plugins/f_user.py @@ -110,7 +110,7 @@ class user_add(crud.Add): assert 'dn' not in kw ldap = self.api.Backend.ldap kw['uid'] = uid - kw['dn'] = ldap.get_user_dn(uid) + kw['dn'] = ldap.make_user_dn(uid) if servercore.uid_too_long(kw['uid']): raise errors.UsernameTooLong @@ -244,18 +244,23 @@ api.register(user_find) class user_show(crud.Get): 'Examine an existing user.' - def execute(self, *args, **kw): - uid=args[0] - result = servercore.get_user_by_uid(uid, ["*"]) - return result - def forward(self, *args, **kw): - try: - result = super(crud.Get, self).forward(*args, **kw) - if not result: return - for a in result: - print a, ": ", result[a] - except errors.NotFound: - print "User %s not found" % args[0] + def execute(self, uid, **kw): + """ + Execute the user-show operation. + + The dn should not be passed as a keyword argument as it is constructed + by this method. + + Returns the entry + + :param uid: The login name of the user to retrieve. + :param kw: Not used. + """ + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("uid", uid, ["*"], "posixAccount") + # FIXME: should kw contain the list of attributes? + return ldap.retrieve(dn) + api.register(user_show) class user_lock(frontend.Command): -- cgit From f7c044495ae22d372fb064dbacfe0ff027c437a7 Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Tue, 14 Oct 2008 22:48:57 -0400 Subject: Port user_del to CrudBackend Override output_for_cli() to generate nicer output --- ipalib/plugins/f_user.py | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugins/f_user.py b/ipalib/plugins/f_user.py index 1e79c4b8..79c45735 100644 --- a/ipalib/plugins/f_user.py +++ b/ipalib/plugins/f_user.py @@ -157,23 +157,30 @@ class user_add(crud.Add): return ldap.create(**kw) + def output_for_cli(self, ret): + """ + Output result of this command to command line interface. + """ + if ret: + print "User added" + api.register(user_add) class user_del(crud.Del): 'Delete an existing user.' - def execute(self, *args, **kw): - """args[0] = uid of the user to remove - - Delete a user. Not to be confused with inactivate_user. This + def execute(self, uid, **kw): + """Delete a user. Not to be confused with inactivate_user. This makes the entry go away completely. uid is the uid of the user to delete The memberOf plugin handles removing the user from any other groups. + + :param uid: The login name of the user being added. + :param kw: Not used. """ - uid = args[0] if uid == "admin": # FIXME: do we still want a "special" user? raise SyntaxError("admin required") @@ -183,11 +190,16 @@ class user_del(crud.Del): if not user: raise errors.NotFound - return servercore.delete_entry(user['dn']) - def forward(self, *args, **kw): - result = super(crud.Del, self).forward(*args, **kw) - if result: - print "User %s removed" % args[0] + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("uid", uid, ["*"], "posixAccount") + return ldap.delete(dn) + def output_for_cli(self, ret): + """ + Output result of this command to command line interface. + """ + if ret: + print "User deleted" + api.register(user_del) -- cgit From e7937f294445d53396f7fb87d52eb4d4c9b97110 Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Wed, 15 Oct 2008 09:57:49 -0400 Subject: Add missing * to *kw to make it pass named arguments instead of positional --- ipalib/crud.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'ipalib') diff --git a/ipalib/crud.py b/ipalib/crud.py index 5cd7b0a4..ba4d5718 100644 --- a/ipalib/crud.py +++ b/ipalib/crud.py @@ -70,7 +70,7 @@ class CrudBackend(backend.Backend): Base class defining generic CRUD backend API. """ - def create(self, *kw): + def create(self, **kw): """ Create a new entry. @@ -102,7 +102,7 @@ class CrudBackend(backend.Backend): """ raise NotImplementedError('%s.retrieve()' % self.name) - def update(self, primary_key, *kw): + def update(self, primary_key, **kw): """ Update an existing entry. -- cgit From 789a248daa71d5d1377e0dc9f0cd3afe107d4f2a Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Wed, 15 Oct 2008 09:58:29 -0400 Subject: Port user-mod to use ldap update() method --- ipalib/plugins/f_user.py | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugins/f_user.py b/ipalib/plugins/f_user.py index 79c45735..e95ee3b2 100644 --- a/ipalib/plugins/f_user.py +++ b/ipalib/plugins/f_user.py @@ -205,27 +205,31 @@ api.register(user_del) class user_mod(crud.Mod): 'Edit an existing user.' - def execute(self, *args, **kw): - uid=args[0] + def execute(self, uid, **kw): + """ + Execute the user-mod operation. - # Get the existing user entry - result = servercore.get_sub_entry("cn=accounts," + servercore.basedn, "uid=%s" % uid, ["*"]) + The dn should not be passed as a keyword argument as it is constructed + by this method. - user = kw - dn = result.get('dn') - del result['dn'] - entry = ipaldap.Entry((dn, servercore.convert_scalar_values(result))) + Returns the entry - for u in user: - entry.setValues(u, user[u]) + :param uid: The login name of the user to retrieve. + :param kw: Keyword arguments for the other LDAP attributes. + """ + assert 'uid' not in kw + assert 'dn' not in kw + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("uid", uid, "posixAccount") + return ldap.update(dn, **kw) - result = servercore.update_entry(entry.toDict()) + def output_for_cli(self, ret): + """ + Output result of this command to command line interface. + """ + if ret: + print "User updated" - return result - def forward(self, *args, **kw): - result = super(crud.Mod, self).forward(*args, **kw) - if result: - print "User %s modified" % args[0] api.register(user_mod) @@ -269,7 +273,7 @@ class user_show(crud.Get): :param kw: Not used. """ ldap = self.api.Backend.ldap - dn = ldap.find_entry_dn("uid", uid, ["*"], "posixAccount") + dn = ldap.find_entry_dn("uid", uid, "posixAccount") # FIXME: should kw contain the list of attributes? return ldap.retrieve(dn) -- cgit From 3268b65ae0dfc7ffdeba685e8e2515a437bf092e Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Wed, 15 Oct 2008 16:11:34 -0400 Subject: Initial implementation of a generic search routine. --- ipalib/plugins/f_user.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugins/f_user.py b/ipalib/plugins/f_user.py index a1078fe7..c2bb7b6f 100644 --- a/ipalib/plugins/f_user.py +++ b/ipalib/plugins/f_user.py @@ -237,11 +237,10 @@ api.register(user_mod) class user_find(crud.Find): 'Search the users.' def execute(self, *args, **kw): - uid=args[0] - result = servercore.find_users(uid, ["*"]) - return result - def forward(self, *args, **kw): - users = super(crud.Find, self).forward(*args, **kw) + ldap = self.api.Backend.ldap + kw['uid'] = args[0] + return ldap.search(**kw) + def output_for_cli(self, users): if not users: return counter = users[0] -- cgit From 14a33d461960b4183ac25a83a8ef9f375fd75d49 Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Wed, 15 Oct 2008 16:50:46 -0400 Subject: Fix some remaining merge issues and don't use forward() in user-*lock() --- ipalib/plugins/f_user.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugins/f_user.py b/ipalib/plugins/f_user.py index c2bb7b6f..ed88ef9f 100644 --- a/ipalib/plugins/f_user.py +++ b/ipalib/plugins/f_user.py @@ -154,8 +154,6 @@ class user_add(crud.Add): # some required objectclasses kw['objectClass'] = config.get('ipauserobjectclasses') -<<<<<<< HEAD:ipalib/plugins/f_user.py -======= return ldap.create(**kw) def output_for_cli(self, ret): @@ -288,9 +286,8 @@ class user_lock(frontend.Command): uid = args[0] user = servercore.get_user_by_uid(uid, ['dn', 'uid']) return servercore.mark_entry_inactive(user['dn']) - def forward(self, *args, **kw): - result = super(user_lock, self).forward(*args, **kw) - if result: + def output_for_cli(self, ret): + if ret: print "User locked" api.register(user_lock) @@ -303,8 +300,7 @@ class user_unlock(frontend.Command): uid = args[0] user = servercore.get_user_by_uid(uid, ['dn', 'uid']) return servercore.mark_entry_active(user['dn']) - def forward(self, *args, **kw): - result = super(user_unlock, self).forward(*args, **kw) - if result: + def output_for_cli(self, ret): + if ret: print "User unlocked" api.register(user_unlock) -- cgit From 1a8317ff7471214811d39ab846d402dc22a03779 Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Wed, 15 Oct 2008 17:46:01 -0400 Subject: Port group-add to use LDAP backend Have create and update return the record that was just added/modified --- ipalib/plugins/f_group.py | 57 ++++++++++++++++++++++------------------------- 1 file changed, 27 insertions(+), 30 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugins/f_group.py b/ipalib/plugins/f_group.py index eeb18c5c..a07d314b 100644 --- a/ipalib/plugins/f_group.py +++ b/ipalib/plugins/f_group.py @@ -38,6 +38,7 @@ class group(frontend.Object): takes_params = ( 'description', Param('cn', + cli_name='name', primary_key=True, normalize=lambda value: value.lower(), ) @@ -47,47 +48,43 @@ api.register(group) class group_add(crud.Add): 'Add a new group.' - def execute(self, *args, **kw): - """args[0] = uid of the group to add - kw{container} is the location in the DIT to add the group, not - required - kw otherwise contains all the attributes + + def execute(self, cn, **kw): """ - # FIXME: ug, really? - if not kw.get('container'): - group_container = servercore.DefaultGroupContainer - else: - group_container = kw['container'] - del kw['container'] + Execute the group-add operation. - group = kw + The dn should not be passed as a keyword argument as it is constructed + by this method. - group['cn'] = args[0] + Returns the entry as it will be created in LDAP. - # Get our configuration - config = servercore.get_ipa_config() + No need to explicitly set gidNumber. The dna_plugin will do this + for us if the value isn't provided by the caller. - dn="cn=%s,%s,%s" % (ldap.dn.escape_dn_chars(group['cn']), - group_container,servercore.basedn) + :param cn: The name of the group being added. + :param kw: Keyword arguments for the other LDAP attributes. + """ + assert 'cn' not in kw + assert 'dn' not in kw + ldap = self.api.Backend.ldap + kw['cn'] = cn + kw['dn'] = ldap.make_group_dn(cn) - entry = ipaldap.Entry(dn) + # Get our configuration + config = servercore.get_ipa_config() # some required objectclasses - entry.setValues('objectClass', (config.get('ipagroupobjectclasses'))) + kw['objectClass'] = config.get('ipagroupobjectclasses') - # No need to explicitly set gidNumber. The dna_plugin will do this - # for us if the value isn't provided by the user. + return ldap.create(**kw) - # fill in our new entry with everything sent by the user - for g in group: - entry.setValues(g, group[g]) + def output_for_cli(self, ret): + """ + Output result of this command to command line interface. + """ + if ret: + print "Group added" - result = servercore.add_entry(entry) - return result - def forward(self, *args, **kw): - result = super(crud.Add, self).forward(*args, **kw) - if result: - print "Group %s added" % args[0] api.register(group_add) -- cgit From 12f1e7fdf75b001ed2b73525a242feb15b272d51 Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Thu, 16 Oct 2008 10:32:20 -0400 Subject: Remove all references to ipa_server.* from user plugin --- ipalib/plugins/f_user.py | 64 +++++++++++++++++++++++------------------------- 1 file changed, 30 insertions(+), 34 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugins/f_user.py b/ipalib/plugins/f_user.py index ed88ef9f..9fec1bd4 100644 --- a/ipalib/plugins/f_user.py +++ b/ipalib/plugins/f_user.py @@ -27,9 +27,6 @@ from ipalib.frontend import Param from ipalib import api from ipalib import errors from ipalib import ipa_types -from ipa_server import servercore -from ipa_server import ipaldap -import ldap # Command to get the idea how plugins will interact with api.env class envtest(frontend.Command): @@ -112,11 +109,12 @@ class user_add(crud.Add): kw['uid'] = uid kw['dn'] = ldap.make_user_dn(uid) - if servercore.uid_too_long(kw['uid']): - raise errors.UsernameTooLong + # FIXME: enforce this elsewhere +# if servercore.uid_too_long(kw['uid']): +# raise errors.UsernameTooLong # Get our configuration - config = servercore.get_ipa_config() + config = ldap.get_ipa_config() # Let us add in some missing attributes if kw.get('homedirectory') is None: @@ -131,20 +129,21 @@ class user_add(crud.Add): # If uidnumber is blank the the FDS dna_plugin will automatically # assign the next value. So we don't have to do anything with it. - group_dn="cn=%s,%s,%s" % (config.get('ipadefaultprimarygroup'), servercore.DefaultGroupContainer, servercore.basedn) - try: - default_group = servercore.get_entry_by_dn(group_dn, ['dn','gidNumber']) - if default_group: - kw['gidnumber'] = default_group.get('gidnumber') - except errors.NotFound: - # Fake an LDAP error so we can return something useful to the kw - raise errors.NotFound, "The default group for new kws, '%s', cannot be found." % config.get('ipadefaultprimarygroup') - except Exception, e: - # catch everything else - raise e + if not kw.get('gidnumber'): + try: + group_dn = ldap.find_entry_dn("cn", config.get('ipadefaultprimarygroup')) + default_group = ldap.retrieve(group_dn, ['dn','gidNumber']) + if default_group: + kw['gidnumber'] = default_group.get('gidnumber') + except errors.NotFound: + # Fake an LDAP error so we can return something useful to the kw + raise errors.NotFound, "The default group for new kws, '%s', cannot be found." % config.get('ipadefaultprimarygroup') + except Exception, e: + # catch everything else + raise e if kw.get('krbprincipalname') is None: - kw['krbprincipalname'] = "%s@%s" % (kw.get('uid'), servercore.realm) + kw['krbprincipalname'] = "%s@%s" % (kw.get('uid'), self.api.env.realm) # FIXME. This is a hack so we can request separate First and Last # name in the GUI. @@ -185,12 +184,9 @@ class user_del(crud.Del): raise SyntaxError("admin required") # raise ipaerror.gen_exception(ipaerror.INPUT_ADMIN_REQUIRED) # logging.info("IPA: delete_user '%s'" % uid) - user = servercore.get_user_by_uid(uid, ['dn', 'uid']) - if not user: - raise errors.NotFound ldap = self.api.Backend.ldap - dn = ldap.find_entry_dn("uid", uid, ["*"], "posixAccount") + dn = ldap.find_entry_dn("uid", uid, "posixAccount") return ldap.delete(dn) def output_for_cli(self, ret): """ @@ -234,9 +230,9 @@ api.register(user_mod) class user_find(crud.Find): 'Search the users.' - def execute(self, *args, **kw): + def execute(self, uid, **kw): ldap = self.api.Backend.ldap - kw['uid'] = args[0] + kw['uid'] = uid return ldap.search(**kw) def output_for_cli(self, users): if not users: @@ -244,7 +240,7 @@ class user_find(crud.Find): counter = users[0] users = users[1:] if counter == 0: - print "No entries found for", args[0] + print "No entries found" return elif counter == -1: print "These results are truncated." @@ -272,7 +268,7 @@ class user_show(crud.Get): """ ldap = self.api.Backend.ldap dn = ldap.find_entry_dn("uid", uid, "posixAccount") - # FIXME: should kw contain the list of attributes? + # FIXME: should kw contain the list of attributes to display? return ldap.retrieve(dn) api.register(user_show) @@ -282,10 +278,10 @@ class user_lock(frontend.Command): takes_args = ( Param('uid', primary_key=True), ) - def execute(self, *args, **kw): - uid = args[0] - user = servercore.get_user_by_uid(uid, ['dn', 'uid']) - return servercore.mark_entry_inactive(user['dn']) + def execute(self, uid, **kw): + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("uid", uid, "posixAccount") + return ldap.mark_entry_inactive(dn) def output_for_cli(self, ret): if ret: print "User locked" @@ -296,10 +292,10 @@ class user_unlock(frontend.Command): takes_args = ( Param('uid', primary_key=True), ) - def execute(self, *args, **kw): - uid = args[0] - user = servercore.get_user_by_uid(uid, ['dn', 'uid']) - return servercore.mark_entry_active(user['dn']) + def execute(self, uid, **kw): + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("uid", uid, "posixAccount") + return ldap.mark_entry_active(dn) def output_for_cli(self, ret): if ret: print "User unlocked" -- cgit From 5748fce84ca0c0256183e1da308cb9f7ae4e73de Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Thu, 16 Oct 2008 10:59:03 -0400 Subject: Remove references to ipa_server.* and port group plugin to ldap backend --- ipalib/plugins/f_group.py | 144 +++++++++++++++++++++++++++------------------- 1 file changed, 85 insertions(+), 59 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugins/f_group.py b/ipalib/plugins/f_group.py index a07d314b..c2280a4e 100644 --- a/ipalib/plugins/f_group.py +++ b/ipalib/plugins/f_group.py @@ -25,10 +25,7 @@ from ipalib import frontend from ipalib import crud from ipalib.frontend import Param from ipalib import api -from ipa_server import servercore -from ipa_server import ipaldap from ipa_server import ipautil -import ldap class group(frontend.Object): @@ -71,7 +68,7 @@ class group_add(crud.Add): kw['dn'] = ldap.make_group_dn(cn) # Get our configuration - config = servercore.get_ipa_config() + config = ldap.get_ipa_config() # some required objectclasses kw['objectClass'] = config.get('ipagroupobjectclasses') @@ -90,87 +87,116 @@ api.register(group_add) class group_del(crud.Del): 'Delete an existing group.' - def execute(self, *args, **kw): - """args[0] = dn of the group to remove - - Delete a group - - The memberOf plugin handles removing the group from any other - groups. + def execute(self, cn, **kw): """ - group_dn = args[0] + Delete a group - group = servercore.get_entry_by_dn(group_dn, ['dn', 'cn']) - if group is None: - raise errors.NotFound -# logging.info("IPA: delete_group '%s'" % group_dn) + The memberOf plugin handles removing the group from any other + groups. + :param cn: The name of the group being removed + :param kw: Unused + """ # We have 2 special groups, don't allow them to be removed - # FIXME -# if "admins" in group.get('cn') or "editors" in group.get('cn'): +# if "admins" == cn.lower() or "editors" == cn.lower(): # raise ipaerror.gen_exception(ipaerror.CONFIG_REQUIRED_GROUPS) + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("cn", cn, "posixGroup") +# logging.info("IPA: delete_group '%s'" % dn) + # Don't allow the default user group to be removed - config=servercore.get_ipa_config() - default_group = servercore.get_entry_by_cn(config.get('ipadefaultprimarygroup'), None) - if group_dn == default_group.get('dn'): + config=ldap.get_ipa_config() + default_group = ldap.find_entry_dn("cn", config.get('ipadefaultprimarygroup'), "posixGroup") + if dn == default_group: raise errors.DefaultGroup - return servercore.delete_entry(group_dn) - def forward(self, *args, **kw): - group = self.api.Command['group_show'](ipautil.utf8_encode_value(args[0])) - if not group: - print "nothing found" - return False - a = group.get('dn') - result = super(crud.Del, self).forward(a) + return ldap.delete(dn) + + def output_for_cli(self, ret): + """ + Output result of this command to command line interface. + """ + if ret: + print "Group deleted" + api.register(group_del) class group_mod(crud.Mod): 'Edit an existing group.' - def execute(self, *args, **kw): - group_cn=args[0] - result = servercore.get_entry_by_cn(group_cn, ["*"]) + def execute(self, cn, **kw): + """ + Execute the user-mod operation. - group = kw - dn = result.get('dn') - del result['dn'] - entry = ipaldap.Entry((dn, servercore.convert_scalar_values(result))) + The dn should not be passed as a keyword argument as it is constructed + by this method. - for g in group: - entry.setValues(g, group[g]) + Returns the entry - result = servercore.update_entry(entry.toDict()) + :param cn: The name of the group to update. + :param kw: Keyword arguments for the other LDAP attributes. + """ + assert 'cn' not in kw + assert 'dn' not in kw + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("cn", cn, "posixGroup") + return ldap.update(dn, **kw) + + def output_for_cli(self, ret): + """ + Output result of this command to command line interface. + """ + if ret: + print "Group updated" - return result - def forward(self, *args, **kw): - result = super(crud.Mod, self).forward(*args, **kw) - if result: - print "Group %s modified" % args[0] api.register(group_mod) class group_find(crud.Find): 'Search the groups.' - def execute(self, *args, **kw): - cn=args[0] - result = servercore.get_sub_entry(servercore.basedn, "cn=%s" % cn, ["*"]) - return result - def forward(self, *args, **kw): - result = super(crud.Find, self).forward(*args, **kw) - for a in result: - print a, ": ", result[a] + def execute(self, cn, **kw): + ldap = self.api.Backend.ldap + kw['cn'] = cn + return ldap.search(**kw) + + def output_for_cli(self, groups): + if not groups: + return + + counter = groups[0] + groups = groups[1:] + if counter == 0: + print "No entries found" + return + elif counter == -1: + print "These results are truncated." + print "Please refine your search and try again." + + for g in groups: + for a in g.keys(): + print "%s: %s" % (a, g[a]) + api.register(group_find) class group_show(crud.Get): 'Examine an existing group.' - def execute(self, *args, **kw): - cn=args[0] - result = servercore.get_sub_entry(servercore.basedn, "cn=%s" % cn, ["*"]) - return result - def forward(self, *args, **kw): - result = super(crud.Get, self).forward(*args, **kw) - return result + def execute(self, cn, **kw): + """ + Execute the group-show operation. + + The dn should not be passed as a keyword argument as it is constructed + by this method. + + Returns the entry + + :param cn: The group name to retrieve. + :param kw: Not used. + """ + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("cn", cn, "posixGroup") + # FIXME: should kw contain the list of attributes to display? + return ldap.retrieve(dn) + api.register(group_show) -- cgit From f777f72de6a7c1d3ef29088fbf89722c1148f246 Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Thu, 16 Oct 2008 15:00:30 -0400 Subject: Use the search fields from the configuration when searching Generalize the attribute -> objectclass search helper --- ipalib/plugins/f_group.py | 15 +++++++++++++-- ipalib/plugins/f_user.py | 25 ++++++++++++++++++------- 2 files changed, 31 insertions(+), 9 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugins/f_group.py b/ipalib/plugins/f_group.py index c2280a4e..132e45ef 100644 --- a/ipalib/plugins/f_group.py +++ b/ipalib/plugins/f_group.py @@ -155,9 +155,20 @@ api.register(group_mod) class group_find(crud.Find): 'Search the groups.' - def execute(self, cn, **kw): + def execute(self, term, **kw): ldap = self.api.Backend.ldap - kw['cn'] = cn + + # Pull the list of searchable attributes out of the configuration. + config = ldap.get_ipa_config() + search_fields_conf_str = config.get('ipagroupsearchfields') + search_fields = search_fields_conf_str.split(",") + + for s in search_fields: + kw[s] = term + + object_type = ldap.get_object_type("cn") + if object_type and not kw.get('objectclass'): + kw['objectclass'] = ldap.get_object_type("cn") return ldap.search(**kw) def output_for_cli(self, groups): diff --git a/ipalib/plugins/f_user.py b/ipalib/plugins/f_user.py index 9fec1bd4..da0262b6 100644 --- a/ipalib/plugins/f_user.py +++ b/ipalib/plugins/f_user.py @@ -186,7 +186,7 @@ class user_del(crud.Del): # logging.info("IPA: delete_user '%s'" % uid) ldap = self.api.Backend.ldap - dn = ldap.find_entry_dn("uid", uid, "posixAccount") + dn = ldap.find_entry_dn("uid", uid) return ldap.delete(dn) def output_for_cli(self, ret): """ @@ -215,7 +215,7 @@ class user_mod(crud.Mod): assert 'uid' not in kw assert 'dn' not in kw ldap = self.api.Backend.ldap - dn = ldap.find_entry_dn("uid", uid, "posixAccount") + dn = ldap.find_entry_dn("uid", uid) return ldap.update(dn, **kw) def output_for_cli(self, ret): @@ -230,9 +230,20 @@ api.register(user_mod) class user_find(crud.Find): 'Search the users.' - def execute(self, uid, **kw): + def execute(self, term, **kw): ldap = self.api.Backend.ldap - kw['uid'] = uid + + # Pull the list of searchable attributes out of the configuration. + config = ldap.get_ipa_config() + search_fields_conf_str = config.get('ipausersearchfields') + search_fields = search_fields_conf_str.split(",") + + for s in search_fields: + kw[s] = term + + object_type = ldap.get_object_type("uid") + if object_type and not kw.get('objectclass'): + kw['objectclass'] = ldap.get_object_type("uid") return ldap.search(**kw) def output_for_cli(self, users): if not users: @@ -267,7 +278,7 @@ class user_show(crud.Get): :param kw: Not used. """ ldap = self.api.Backend.ldap - dn = ldap.find_entry_dn("uid", uid, "posixAccount") + dn = ldap.find_entry_dn("uid", uid) # FIXME: should kw contain the list of attributes to display? return ldap.retrieve(dn) @@ -280,7 +291,7 @@ class user_lock(frontend.Command): ) def execute(self, uid, **kw): ldap = self.api.Backend.ldap - dn = ldap.find_entry_dn("uid", uid, "posixAccount") + dn = ldap.find_entry_dn("uid", uid) return ldap.mark_entry_inactive(dn) def output_for_cli(self, ret): if ret: @@ -294,7 +305,7 @@ class user_unlock(frontend.Command): ) def execute(self, uid, **kw): ldap = self.api.Backend.ldap - dn = ldap.find_entry_dn("uid", uid, "posixAccount") + dn = ldap.find_entry_dn("uid", uid) return ldap.mark_entry_active(dn) def output_for_cli(self, ret): if ret: -- cgit From b045f220692e016a105f03af025d49f9a9cddc74 Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Thu, 16 Oct 2008 23:33:44 -0400 Subject: Add mod_python-based XML-RPC server. Use -e kerberos on the command-line to use the mod_python server, otherwise it defaults to use the simple-server URL. --- ipalib/errors.py | 2 ++ ipalib/plugins/b_xmlrpc.py | 47 ++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 45 insertions(+), 4 deletions(-) (limited to 'ipalib') diff --git a/ipalib/errors.py b/ipalib/errors.py index f1c9e26e..36df0690 100644 --- a/ipalib/errors.py +++ b/ipalib/errors.py @@ -23,6 +23,8 @@ All custom errors raised by `ipalib` package. Also includes a few utility functions for raising exceptions. """ +IPA_ERROR_BASE = 1000 + TYPE_FORMAT = '%s: need a %r; got %r' def raise_TypeError(value, type_, name): diff --git a/ipalib/plugins/b_xmlrpc.py b/ipalib/plugins/b_xmlrpc.py index db2af1ab..9fe5b133 100644 --- a/ipalib/plugins/b_xmlrpc.py +++ b/ipalib/plugins/b_xmlrpc.py @@ -1,5 +1,6 @@ # Authors: # Jason Gerard DeRose +# Rob Crittenden # # Copyright (C) 2008 Red Hat # see file 'COPYING' for use and warranty information @@ -26,6 +27,8 @@ This provides a lightwieght XML-RPC client using Python standard library import xmlrpclib import socket +import httplib +import kerberos from ipalib.backend import Backend from ipalib.util import xmlrpc_marshal from ipalib import api @@ -38,7 +41,12 @@ class xmlrpc(Backend): def get_client(self, verbose=False): # FIXME: The server uri should come from self.api.env.server_uri - return xmlrpclib.ServerProxy('http://localhost:8888', verbose=verbose) + if api.env.get('kerberos'): + server = api.env.server.next() + if verbose: print "Connecting to %s" % server + return xmlrpclib.ServerProxy('https://%s/ipa/xml' % server, transport=KerbTransport(), verbose=verbose) + else: + return xmlrpclib.ServerProxy('http://localhost:8888', verbose=verbose) def forward_call(self, name, *args, **kw): """ @@ -54,10 +62,41 @@ class xmlrpc(Backend): except xmlrpclib.Fault, e: err = errors.convertFault(e) code = getattr(err,'faultCode',None) - if code: - print "%s: %s" % (code, getattr(err,'__doc__','')) - else: + faultString = getattr(err,'faultString',None) + if not code: raise err + if code < errors.IPA_ERROR_BASE: + print "%s: %s" % (code, faultString) + else: + print "%s: %s" % (code, getattr(err,'__doc__','')) return {} api.register(xmlrpc) + +class KerbTransport(xmlrpclib.SafeTransport): + """Handles Kerberos Negotiation authentication to an XML-RPC server.""" + + def get_host_info(self, host): + + host, extra_headers, x509 = xmlrpclib.Transport.get_host_info(self, host) + + # Set the remote host principal + h = host + hostinfo = h.split(':') + service = "HTTP@" + hostinfo[0] + + try: + rc, vc = kerberos.authGSSClientInit(service); + except kerberos.GSSError, e: + raise kerberos.GSSError(e) + + try: + kerberos.authGSSClientStep(vc, ""); + except kerberos.GSSError, e: + raise kerberos.GSSError(e) + + extra_headers = [ + ("Authorization", "negotiate %s" % kerberos.authGSSClientResponse(vc) ) + ] + + return host, extra_headers, x509 -- cgit From ae8370be44d95b9f6793ded46ef81126aebef3e0 Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Fri, 17 Oct 2008 19:20:23 -0400 Subject: Port f_service to LDAP backend Add new keyword, 'filter', that can be passed to the search function. This is globbed onto the filter that is auto-created. --- ipalib/plugins/f_group.py | 2 +- ipalib/plugins/f_service.py | 137 +++++++++++++++++++++++++++++--------------- ipalib/plugins/f_user.py | 2 +- 3 files changed, 94 insertions(+), 47 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugins/f_group.py b/ipalib/plugins/f_group.py index 132e45ef..e83c870e 100644 --- a/ipalib/plugins/f_group.py +++ b/ipalib/plugins/f_group.py @@ -168,7 +168,7 @@ class group_find(crud.Find): object_type = ldap.get_object_type("cn") if object_type and not kw.get('objectclass'): - kw['objectclass'] = ldap.get_object_type("cn") + kw['objectclass'] = object_type return ldap.search(**kw) def output_for_cli(self, groups): diff --git a/ipalib/plugins/f_service.py b/ipalib/plugins/f_service.py index baed5233..38c80ad2 100644 --- a/ipalib/plugins/f_service.py +++ b/ipalib/plugins/f_service.py @@ -1,5 +1,6 @@ # Authors: # Jason Gerard DeRose +# Rob Crittenden # # Copyright (C) 2008 Red Hat # see file 'COPYING' for use and warranty information @@ -27,9 +28,6 @@ from ipalib.frontend import Param from ipalib import api from ipalib import errors from ipalib import ipa_types -from ipa_server import servercore -from ipa_server import ipaldap -import ldap class service(frontend.Object): """ @@ -46,13 +44,26 @@ class service_add(crud.Add): takes_options = ( Param('force?', type=ipa_types.Bool(), default=False, doc='Force a service principal name'), ) - def execute(self, *args, **kw): - """args[0] = service principal to add - kw{force} determines whether we continue on errors + def execute(self, principal, **kw): """ - force = kw.get('force', False) + Execute the service-add operation. + + The dn should not be passed as a keyword argument as it is constructed + by this method. - principal = args[0] + Returns the entry as it will be created in LDAP. + + :param principal: The service to be added in the form: service/hostname + :param kw: Keyword arguments for the other LDAP attributes. + """ + assert 'krbprincipalname' not in kw + ldap = self.api.Backend.ldap + + force = kw.get('force', False) + try: + del kw['force'] + except: + pass # Break down the principal into its component parts, which may or # may not include the realm. @@ -64,7 +75,7 @@ class service_add(crud.Add): sr = sp[1].split('@') if len(sr) == 1: hostname = sr[0].lower() - realm = servercore.realm + realm = self.api.env.realm elif len(sr) == 2: hostname = sr[0].lower() realm = sr[1] @@ -83,68 +94,104 @@ class service_add(crud.Add): logging.debug("IPA: found %d records for '%s'" % (len(rs), hostname)) """ - service_container = servercore.DefaultServiceContainer - # At some point we'll support multiple realms - if (realm != servercore.realm): + if (realm != self.api.env.realm): raise errors.RealmMismatch # Put the principal back together again princ_name = service + "/" + hostname + "@" + realm - dn = "krbprincipalname=%s,%s,%s" % (ldap.dn.escape_dn_chars(princ_name), - service_container,servercore.basedn) - entry = ipaldap.Entry(dn) + dn = ldap.make_service_dn(princ_name) - entry.setValues('objectClass', 'krbPrincipal', 'krbPrincipalAux', 'krbTicketPolicyAux') - entry.setValues('krbprincipalname', princ_name) + kw['dn'] = dn + kw['objectClass'] = ['krbPrincipal', 'krbPrincipalAux', 'krbTicketPolicyAux'] + + return ldap.create(**kw) + + def output_to_cli(self, ret): + if ret: + print "Service added" - result = servercore.add_entry(entry) - return result - def forward(self, *args, **kw): - result = super(crud.Add, self).forward(*args, **kw) - if result: - print "Service %s added" % args[0] api.register(service_add) class service_del(crud.Del): 'Delete an existing service.' - def execute(self, *args, **kw): - """args[0] = princial to remove + def execute(self, principal, **kw): + """ + Delete a service principal. - Delete a service principal. + principal is the krbprincipalname of the entry to delete. - principal is the full DN of the entry to delete. + This should be called with much care. - This should be called with much care. + :param principal: The service to be added in the form: service/hostname + :param kw: not used """ - principal = args[0] - return False - def forward(self, *args, **kw): - result = super(crud.Del, self).forward(*args, **kw) - if result: - print "Service %s removed" % args[0] -api.register(service_del) + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("krbprincipalname", principal) + return ldap.delete(dn) + def output_to_cli(self, ret): + if ret: + print "Service removed" -class service_mod(crud.Mod): - 'Edit an existing service.' -api.register(service_mod) +api.register(service_del) +# There is no service-mod. The principal itself contains nothing that +# is user-changeable class service_find(crud.Find): 'Search the existing services.' + def execute(self, principal, **kw): + ldap = self.api.Backend.ldap + + kw['filter'] = "&(objectclass=krbPrincipalAux)(!(objectClass=person))(!(|(krbprincipalname=kadmin/*)(krbprincipalname=K/M@*)(krbprincipalname=krbtgt/*)))" + kw['krbprincipalname'] = principal + + object_type = ldap.get_object_type("krbprincipalname") + if object_type and not kw.get('objectclass'): + kw['objectclass'] = object_type + + return ldap.search(**kw) + + def output_for_cli(self, services): + if not services: + return + + counter = services[0] + services = services[1:] + if counter == 0: + print "No entries found" + return + elif counter == -1: + print "These results are truncated." + print "Please refine your search and try again." + + for s in services: + for a in s.keys(): + print "%s: %s" % (a, s[a]) + api.register(service_find) class service_show(crud.Get): 'Examine an existing service.' - def execute(self, *args, **kw): - filter = "(&(objectclass=krbPrincipalAux)(!(objectClass=person))(!(|(krbprincipalname=kadmin/*)(krbprincipalname=K/M@*)(krbprincipalname=krbtgt/*)))(&(|(krbprincipalname=%s))))" % args[0] - result = servercore.get_sub_entry(servercore.basedn, filter, ["*"]) - return result - def forward(self, *args, **kw): - result = super(crud.Get, self).forward(*args, **kw) - return result + def execute(self, principal, **kw): + """ + Execute the service-show operation. + + The dn should not be passed as a keyword argument as it is constructed + by this method. + + Returns the entry + + :param principal: The service principal to retrieve + :param kw: Not used. + """ + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("krbprincipalname", principal) + # FIXME: should kw contain the list of attributes to display? + return ldap.retrieve(dn) + api.register(service_show) diff --git a/ipalib/plugins/f_user.py b/ipalib/plugins/f_user.py index da0262b6..8b4def80 100644 --- a/ipalib/plugins/f_user.py +++ b/ipalib/plugins/f_user.py @@ -243,7 +243,7 @@ class user_find(crud.Find): object_type = ldap.get_object_type("uid") if object_type and not kw.get('objectclass'): - kw['objectclass'] = ldap.get_object_type("uid") + kw['objectclass'] = object_type return ldap.search(**kw) def output_for_cli(self, users): if not users: -- cgit From 3a80297b04d6fbfd2367ec76c5651d20293adccc Mon Sep 17 00:00:00 2001 From: Martin Nagy Date: Fri, 17 Oct 2008 22:55:03 +0200 Subject: Reworking Environment, moved it to config.py --- ipalib/cli.py | 25 ++++--- ipalib/config.py | 211 +++++++++++++++++++++++++++++++++++++++++------------ ipalib/plugable.py | 72 +----------------- 3 files changed, 179 insertions(+), 129 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index 5dd2c44f..07956e0a 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -25,11 +25,12 @@ import re import sys import code import optparse + import frontend import errors import plugable import ipa_types -import config +from config import set_default_env, read_config def exit_error(error): sys.exit('ipa: ERROR: %s' % error) @@ -207,7 +208,6 @@ class CLI(object): self.__api = api self.__all_interactive = False self.__not_interactive = False - self.__config = None def __get_api(self): return self.__api @@ -256,9 +256,8 @@ class CLI(object): def run(self): self.finalize() - (args, env_dict) = self.parse_globals() - env_dict.update(config.read_config(self.__config)) - self.api.env.update(config.generate_env(env_dict)) + set_default_env(self.api.env) + args = self.parse_globals() if len(args) < 1: self.print_commands() print 'Usage: ipa [global-options] COMMAND' @@ -329,7 +328,6 @@ class CLI(object): return parser def parse_globals(self, argv=sys.argv[1:]): - env_dict = {} parser = optparse.OptionParser() parser.disable_interspersed_args() parser.add_option('-a', dest='interactive', action='store_true', @@ -348,20 +346,23 @@ class CLI(object): self.__all_interactive = True elif options.interactive == False: self.__not_interactive = True - if options.config_file: - self.__config = options.config_file + if options.verbose != None: + self.api.env.verbose = True if options.environment: + env_dict = {} for a in options.environment.split(','): a = a.split('=', 1) if len(a) < 2: parser.error('badly specified environment string,'\ 'use var1=val1[,var2=val2]..') env_dict[a[0].strip()] = a[1].strip() - if options.verbose != None: - env_dict.update(verbose=True) - - return (args, env_dict) + self.api.env.update(env_dict, True) + if options.config_file: + self.api.env.update(read_config(options.config_file), True) + else: + self.api.env.update(read_config(), True) + return args def get_usage(self, cmd): return ' '.join(self.get_usage_iter(cmd)) diff --git a/ipalib/config.py b/ipalib/config.py index 42bf7787..7899d077 100644 --- a/ipalib/config.py +++ b/ipalib/config.py @@ -21,74 +21,193 @@ from ConfigParser import SafeConfigParser, ParsingError import types import os +from errors import check_isinstance, raise_TypeError + DEFAULT_CONF='/etc/ipa/ipa.conf' -def generate_env(d={}): - default = dict( - container_accounts = 'cn=accounts', - basedn = 'dc=example,dc=com', - container_user = 'cn=users,cn=accounts', - container_group = 'cn=groups,cn=accounts', - container_service = 'cn=services,cn=accounts', - domain = LazyProp(get_domain), - interactive = True, - query_dns = True, - realm = LazyProp(get_realm), - server_context = True, - server = LazyIter(get_servers), - verbose = False, - ) - for key, value in d.iteritems(): - if key in default: - if isinstance(default[key], (LazyIter, LazyProp)): - default[key].set_value(value) + +class Environment(object): + """ + A mapping object used to store the environment variables. + """ + + def __init__(self): + object.__setattr__(self, '_Environment__map', {}) + + def __getattr__(self, name): + """ + Return the attribute named ``name``. + """ + return self[name] + + def __setattr__(self, name, value): + """ + Set the attribute named ``name`` to ``value``. + """ + self[name] = value + + def __delattr__(self, name): + """ + Raise AttributeError (deletion is not allowed). + """ + raise AttributeError('cannot del %s.%s' % + (self.__class__.__name__, name) + ) + + def __getitem__(self, key): + """ + Return the value corresponding to ``key``. + """ + val = self.__map[key] + if hasattr(val, 'get_value'): + return val.get_value() + else: + return val + + def __setitem__(self, key, value): + """ + Set the item at ``key`` to ``value``. + """ + if key in self or hasattr(self, key): + if hasattr(self.__map[key], 'set_value'): + self.__map[key].set_value(value) else: - default[key] = convert_val(type(default[key]), value) + raise AttributeError('cannot overwrite %s.%s' % + (self.__class__.__name__, key) + ) else: - default[key] = value + self.__map[key] = value - return default + def __contains__(self, key): + """ + Return True if instance contains ``key``; otherwise return False. + """ + return key in self.__map + def __iter__(self): + """ + Iterate through keys in ascending order. + """ + for key in sorted(self.__map): + yield key -# TODO: Add a validation function -def convert_val(target_type, value): - bool_true = ('true', 'yes', 'on') - bool_false = ('false', 'no', 'off') + def update(self, new_vals, ignore_errors = False): + assert type(new_vals) == dict + for key, value in new_vals.iteritems(): + if ignore_errors: + try: + self[key] = value + except (AttributeError, KeyError): + pass + else: + self[key] = value - if target_type == bool and isinstance(value, basestring): - if value.lower() in bool_true: - return True - elif value.lower() in bool_false: - return False - return target_type(value) + def get(self, name, default=None): + return self.__map.get(name, default) -class LazyProp(object): - def __init__(self, func, value=None): - assert isinstance(func, types.FunctionType) - self._func = func - self._value = value - def set_value(self, value): - self._value = value +def set_default_env(env): + assert isinstance(env, Environment) + + default = dict( + basedn = EnvProp(basestring, 'dc=example,dc=com'), + container_accounts = EnvProp(basestring, 'cn=accounts'), + container_user = EnvProp(basestring, 'cn=users,cn=accounts'), + container_group = EnvProp(basestring, 'cn=groups,cn=accounts'), + container_service = EnvProp(basestring, 'cn=services,cn=accounts'), + domain = LazyProp(basestring, get_domain), + interactive = EnvProp(bool, True), + query_dns = EnvProp(bool, True), + realm = LazyProp(basestring, get_realm), + server_context = EnvProp(bool, True), + server = LazyIter(basestring, get_servers), + verbose = EnvProp(bool, False), + ) + + env.update(default) + + +class EnvProp(object): + def __init__(self, type_, default, multi_value=False): + if multi_value: + if isinstance(default, tuple) and len(default): + check_isinstance(default[0], type_, allow_none=True) + self._type = type_ + self._default = default + self._value = None + self._multi_value = multi_value def get_value(self): - if self._value == None: - return self._func() + if self._get() != None: + return self._get() else: + raise KeyError, 'Value not set' + + def set_value(self, value): + if self._value != None: + raise KeyError, 'Value already set' + self._value = self._validate(value) + + def _get(self): + if self._value != None: return self._value + elif self._default != None: + return self._default + else: + return None + + def _validate(self, value): + if self._multi_value and isinstance(value, tuple): + converted = [] + for val in value: + converted.append(self._validate_value(val)) + return tuple(converted) + else: + return self._validate_value(value) + + def _validate_value(self, value): + bool_true = ('true', 'yes', 'on') + bool_false = ('false', 'no', 'off') + + if self._type == bool and isinstance(value, basestring): + if value.lower() in bool_true: + return True + elif value.lower() in bool_false: + return False + else: + raise raise_TypeError(value, bool, 'value') + check_isinstance(value, self._type, 'value') + return value + + +class LazyProp(EnvProp): + def __init__(self, type_, func, default=None, multi_value=False): + check_isinstance(func, types.FunctionType, 'func') + self._func = func + EnvProp.__init__(self, type_, default, multi_value) + + def get_value(self): + if self._get() != None: + return self._get() + else: + return self._func() class LazyIter(LazyProp): + def __init__(self, type_, func, default=None): + LazyProp.__init__(self, type_, func, default, multi_value=True) + def get_value(self): - if self._value != None: - if type(self._value) == tuple: - for item in self._value: + val = self._get() + if val != None: + if type(val) == tuple: + for item in val: yield item else: - yield self._value + yield val for item in self._func(): - if not self._value or item not in self._value: + if not val or item not in val: yield item diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 4a2658a7..98aa4172 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -29,6 +29,7 @@ import re import inspect import errors from errors import check_type, check_isinstance +from config import Environment class ReadOnly(object): @@ -692,77 +693,6 @@ class Registrar(DictProxy): self.__registered.add(klass) -class Environment(object): - """ - A mapping object used to store the environment variables. - """ - - def __init__(self): - object.__setattr__(self, '_Environment__map', {}) - - def __getattr__(self, name): - """ - Return the attribute named ``name``. - """ - return self[name] - - def __setattr__(self, name, value): - """ - Set the attribute named ``name`` to ``value``. - """ - self[name] = value - - def __delattr__(self, name): - """ - Raise AttributeError (deletion is not allowed). - """ - raise AttributeError('cannot del %s.%s' % - (self.__class__.__name__, name) - ) - - def __getitem__(self, key): - """ - Return the value corresponding to ``key``. - """ - val = self.__map[key] - if hasattr(val, 'get_value'): - return val.get_value() - else: - return val - - def __setitem__(self, key, value): - """ - Set the item at ``key`` to ``value``. - """ - if key in self or hasattr(self, key): - raise AttributeError('cannot overwrite %s.%s' % - (self.__class__.__name__, key) - ) - self.__map[key] = value - - def __contains__(self, key): - """ - Return True if instance contains ``key``; otherwise return False. - """ - return key in self.__map - - def __iter__(self): - """ - Iterate through keys in ascending order. - """ - for key in sorted(self.__map): - yield key - - def update(self, new_vals, ignore_errors = False): - assert type(new_vals) == dict - for key, value in new_vals.iteritems(): - if key in self and ignore_errors: - continue - self[key] = value - - def get(self, name, default=None): - return self.__map.get(name, default) - class API(DictProxy): """ Dynamic API object through which `Plugin` instances are accessed. -- cgit From f7b7fa5553fa216caea67ba8a952ce71f29863db Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 17 Oct 2008 19:11:26 -0600 Subject: Cleaned up ipalib package-level docstring, removed broken cross-referce to --- ipalib/__init__.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) (limited to 'ipalib') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index 46edb822..92544e10 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -22,13 +22,11 @@ Package containing core library. To learn about the ``ipalib`` library, you should read the code in this order: - 1. Learn about the plugin framework in `ipalib.plugable` + 1. Get the big picture from some actual plugins, like `plugins.f_user`. - 2. Learn about the frontend plugins in `ipalib.frontend` + 2. Learn about the base classes for frontend plugins in `frontend`. - 3. Learn about the backend plugins in `ipalib.backend` - - 4. Look at some example plugins in `ipalib.plugins.example` + 3. Learn about the core plugin framework in `plugable`. Here is a short console example on using the plugable API: -- cgit From 8322138f38a4f9c826e4ab148a4fee7df5e93b34 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 17 Oct 2008 19:34:26 -0600 Subject: Added new Param.flags attribute (set with flags=foo kwarg) --- ipalib/frontend.py | 3 +++ 1 file changed, 3 insertions(+) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index da4fd00b..bf3eb7f2 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -207,6 +207,7 @@ class Param(plugable.ReadOnly): normalize callable None default same as type.type None default_from callable None + flags frozenset frozenset() ============ ================= ================== """ __nones = (None, '', tuple(), []) @@ -220,6 +221,7 @@ class Param(plugable.ReadOnly): normalize=None, default=None, default_from=None, + flags=frozenset(), rules=tuple(), ) @@ -249,6 +251,7 @@ class Param(plugable.ReadOnly): self.default_from = check_type(df, DefaultFrom, 'default_from', allow_none=True ) + self.flags = frozenset(kw['flags']) self.__normalize = kw['normalize'] self.rules = self.__check_type(tuple, 'rules') self.all_rules = (self.type.validate,) + self.rules -- cgit From f1eb74e22cadf3a9f4ac991e0f8b922f6fb56d1e Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 17 Oct 2008 20:50:34 -0600 Subject: make-test now runs doctests also; fixed several broken doctests --- ipalib/__init__.py | 24 ------------------- ipalib/frontend.py | 8 +++---- ipalib/plugable.py | 67 +++++++++++++++++++++++++----------------------------- 3 files changed, 35 insertions(+), 64 deletions(-) (limited to 'ipalib') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index 92544e10..4593e581 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -27,30 +27,6 @@ To learn about the ``ipalib`` library, you should read the code in this order: 2. Learn about the base classes for frontend plugins in `frontend`. 3. Learn about the core plugin framework in `plugable`. - -Here is a short console example on using the plugable API: - ->>> from ipalib import api ->>> list(api.register) # Plugins must subclass from one of these base classes: -['Command', 'Method', 'Object', 'Property'] ->>> 'user_add' in api.register.Command # Has 'user_add' been registered? -False ->>> import ipalib.load_plugins # This causes all plugins to be loaded ->>> 'user_add' in api.register.Command # Yes, 'user_add' has been registered: -True ->>> list(api) # API is empty till finalize() is called: -[] ->>> api.finalize() # Instantiates plugins, builds API namespaces: ->>> list(api) # Lists the namespaces in the API: -['Command', 'Method', 'Object', 'Property'] ->>> 'user_add' in api.Command # Yes, the 'user_add' command exists: -True ->>> api['Command'] is api.Command # Access as dict item or as attribute: -True ->>> list(api.Command) # List available commands: -['discover', 'group_add', 'group_del', 'group_find', 'group_mod', 'krbtest', 'service_add', 'service_del', 'service_find', 'service_mod', 'user_add', 'user_del', 'user_find', 'user_mod'] ->>> list(api.Command.user_add) # List public methods for user_add: -['__call__', 'default', 'execute', 'get_doc', 'normalize', 'options', 'validate'] """ import plugable diff --git a/ipalib/frontend.py b/ipalib/frontend.py index bf3eb7f2..2c34b972 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -63,8 +63,8 @@ class DefaultFrom(plugable.ReadOnly): The callback is available through the ``DefaultFrom.callback`` instance attribute, like this: - >>> login.callback - at 0x7fdd225cd7d0> + >>> login.callback # doctest:+ELLIPSIS + at 0x...> >>> login.callback.func_code.co_varnames # The keys ('first', 'last') @@ -473,8 +473,8 @@ class Command(plugable.Plugin): >>> api.finalize() >>> list(api.Command) ['my_command'] - >>> api.Command.my_command - PluginProxy(Command, __main__.my_command()) + >>> api.Command.my_command # doctest:+ELLIPSIS + PluginProxy(Command, ...my_command()) """ __public__ = frozenset(( diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 98aa4172..2a1bdb62 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -208,14 +208,15 @@ class DictProxy(SetProxy): class MagicDict(DictProxy): """ - A read-only mapping container whose values can also be accessed as - attributes. + A mapping container whose values can be accessed as attributes. - For example, assuming ``magic`` is a MagicDict instance that contains the - key ``name``, you could do this: + For example: - >>> magic[name] is getattr(magic, name) - True + >>> magic = MagicDict({'the_key': 'the value'}) + >>> magic['the_key'] + 'the value' + >>> magic.the_key + 'the value' This container acts as a proxy to an actual mapping object (a dict) that is passed to the constructor. To the extent possible in Python, this @@ -270,35 +271,27 @@ class Plugin(ReadOnly): @classmethod def implements(cls, arg): """ - Returns True if this cls.__public__ frozenset contains `arg`; - returns False otherwise. + Return True if class implements ``arg``. - There are three different ways this can be called: + There are three different ways this method can be called: With a argument, e.g.: - >>> class base(ProxyTarget): - >>> __public__ = frozenset(['some_attr', 'another_attr']) - >>> base.implements('some_attr') + >>> class base(Plugin): + ... __public__ = frozenset(['attr1', 'attr2']) + ... + >>> base.implements('attr1') + True + >>> base.implements('attr2') True - >>> base.implements('an_unknown_attribute') + >>> base.implements('attr3') False With a argument, e.g.: - >>> base.implements(frozenset(['some_attr'])) - True - >>> base.implements(frozenset(['some_attr', 'an_unknown_attribute'])) - False - With any object that has a `__public__` attribute that is , e.g.: - >>> class whatever(object): - >>> __public__ = frozenset(['another_attr']) - >>> base.implements(whatever) - True - Unlike ProxyTarget.implemented_by(), this returns an abstract answer because only the __public__ frozenset is checked... a ProxyTarget need not itself have attributes for all names in __public__ @@ -493,34 +486,36 @@ class NameSpace(ReadOnly): 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, assuming ``obj`` is a member - in the NameSpace instance ``namespace``, you could do this: + through a dictionary interface. For example: - >>> obj is getattr(namespace, obj.name) # As attribute + >>> class obj(object): + ... name = 'my_obj' + ... + >>> namespace = NameSpace([obj]) + >>> obj is getattr(namespace, 'my_obj') # As attribute True - >>> obj is namespace[obj.name] # As dictionary item + >>> obj is namespace['my_obj'] # As dictionary item True Here is a more detailed example: >>> class member(object): ... def __init__(self, i): + ... self.i = i ... self.name = 'member_%d' % i + ... def __repr__(self): + ... return 'member(%d)' % self.i ... - >>> def get_members(cnt): - ... for i in xrange(cnt): - ... yield member(i) - ... - >>> namespace = NameSpace(get_members(2)) + >>> namespace = NameSpace(member(i) for i in xrange(3)) >>> namespace.member_0 is namespace['member_0'] True >>> len(namespace) # Returns the number of members in namespace - 2 + 3 >>> list(namespace) # As iterable, iterates through the member names - ['member_0', 'member_1'] + ['member_0', 'member_1', 'member_2'] >>> list(namespace()) # Calling a NameSpace iterates through the members - [<__main__.member object at 0x836710>, <__main__.member object at 0x836750>] - >>> 'member_1' in namespace # NameSpace.__contains__() + [member(0), member(1), member(2)] + >>> 'member_1' in namespace # Does namespace contain 'member_1'? True """ -- cgit From 721982870ed6dd5507a634d09dd06309abc3778a Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 17 Oct 2008 21:05:03 -0600 Subject: Removed generic Command.output_for_cli() method; CLI.run_interactive() now only calls output_for_cli() if it has been implemented --- ipalib/cli.py | 4 +++- ipalib/frontend.py | 9 +-------- 2 files changed, 4 insertions(+), 9 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index 07956e0a..7148afc1 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -301,7 +301,9 @@ class CLI(object): break except errors.ValidationError, e: error = e.error - cmd.output_for_cli(cmd(**kw)) + ret = cmd(**kw) + if callable(cmd.output_for_cli): + cmd.output_for_cli(ret) def parse(self, cmd, argv): parser = self.build_parser(cmd) diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 2c34b972..af640cb5 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -496,6 +496,7 @@ class Command(plugable.Plugin): args = None options = None params = None + output_for_cli = None def __call__(self, *args, **kw): """ @@ -745,14 +746,6 @@ class Command(plugable.Plugin): multivalue = True yield arg - def output_for_cli(self, ret): - """ - Output result of this command to command line interface. - """ - assert type(ret) is dict, 'base output_for_cli() only works with dict' - for key in sorted(ret): - print '%s = %r' % (key, ret[key]) - class Object(plugable.Plugin): __public__ = frozenset(( -- cgit From 80ccf8b1c641fd6c41256fd28ea3b89c590e8d4c Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 17 Oct 2008 22:09:50 -0600 Subject: Reworked load_plugins so it doesn't use imp.load_module() to load from the plugins/ sub-packages, which previously caused them to be loaded multiple times when runnig the doctests --- ipalib/load_plugins.py | 37 +++++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 14 deletions(-) (limited to 'ipalib') diff --git a/ipalib/load_plugins.py b/ipalib/load_plugins.py index 4352ac65..4e02f5ba 100644 --- a/ipalib/load_plugins.py +++ b/ipalib/load_plugins.py @@ -33,9 +33,9 @@ import imp import inspect -def load_plugins(src_dir): +def find_modules_in_dir(src_dir): """ - Import each Python module found in ``src_dir``. + Iterate through module names found in ``src_dir``. """ if not (path.abspath(src_dir) == src_dir and path.isdir(src_dir)): return @@ -51,23 +51,32 @@ def load_plugins(src_dir): module = name[:-len(suffix)] if module == '__init__': continue + yield module + + +def load_plugins_in_dir(src_dir): + """ + Import each Python module found in ``src_dir``. + """ + for module in find_modules_in_dir(src_dir): imp.load_module(module, *imp.find_module(module, [src_dir])) -def load_plugins_subpackage(file_in_package): +def import_plugins(name): """ - Load all Python modules found in a plugins/ subpackage. + Load all plugins found in standard 'plugins' sub-package. """ - package_dir = path.dirname(path.abspath(file_in_package)) - plugins_dir = path.join(package_dir, 'plugins') - load_plugins(plugins_dir) + try: + plugins = __import__(name + '.plugins').plugins + except ImportError: + return + src_dir = path.dirname(path.abspath(plugins.__file__)) + for name in find_modules_in_dir(src_dir): + full_name = '%s.%s' % (plugins.__name__, name) + __import__(full_name) -load_plugins_subpackage(__file__) -try: - import ipa_server - load_plugins_subpackage(ipa_server.__file__) -except ImportError: - pass +for name in ['ipalib', 'ipa_server', 'ipa_not_a_package']: + import_plugins(name) -load_plugins(path.expanduser('~/.freeipa')) +load_plugins_in_dir(path.expanduser('~/.freeipa')) -- cgit From 675fadc641ed6b521943c2c265bce70ac2c7994c Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 17 Oct 2008 23:25:50 -0600 Subject: Some PEP-257 and reStructuredText fixes in ipalib/aci.py, ipa_server/ipaldap.py --- ipalib/aci.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) (limited to 'ipalib') diff --git a/ipalib/aci.py b/ipalib/aci.py index 17956812..9dde767c 100755 --- a/ipalib/aci.py +++ b/ipalib/aci.py @@ -43,7 +43,7 @@ class ACI: __actions = ["allow", "deny"] - __permissions = ["read", "write", "add", "delete", "search", "compare", + __permissions = ["read", "write", "add", "delete", "search", "compare", "selfwrite", "proxy", "all"] def __init__(self,acistr=None): @@ -71,9 +71,14 @@ class ACI: return self.export_to_string() def __getattr__(self, name): - """Backwards compatibility for the old ACI class. - The following extra attributes are available: - source_group, dest_group and attrs. + """ + Backward compatibility for the old ACI class. + + The following extra attributes are available: + + - source_group + - dest_group + - attrs """ if name == 'source_group': group = '' @@ -96,9 +101,13 @@ class ACI: raise AttributeError, "object has no attribute '%s'" % name def __setattr__(self, name, value): - """Backwards compatibility for the old ACI class. - The following extra attributes are available: - source_group, dest_group and attrs. + """ + Backward compatibility for the old ACI class. + + The following extra attributes are available: + - source_group + - dest_group + - attrs """ if name == 'source_group': self.__dict__['bindrule'] = 'groupdn="ldap:///%s"' % value -- cgit From 5c5641e8c2e988dff8b81308775c048fb7178929 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sat, 18 Oct 2008 00:16:22 -0600 Subject: Added some more examples to Param docstrings --- ipalib/frontend.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index af640cb5..d2985fa7 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -200,7 +200,7 @@ class Param(plugable.ReadOnly): ============ ================= ================== cli_name str defaults to name type ipa_type.Type ipa_type.Unicode() - doc str '' + doc str "" required bool True multivalue bool False primary_key bool False @@ -305,6 +305,14 @@ class Param(plugable.ReadOnly): """ Normalize ``value`` using normalize callback. + For example: + + >>> param = Param('telephone', + ... normalize=lambda value: value.replace('.', '-') + ... ) + >>> param.normalize('800.123.4567') + '800-123-4567' + If this `Param` instance does not have a normalize callback, ``value`` is returned unchanged. @@ -340,6 +348,14 @@ class Param(plugable.ReadOnly): """ Convert/coerce ``value`` to Python type for this `Param`. + For example: + + >>> param = Param('an_int', type=ipa_types.Int()) + >>> param.convert(7.2) + 7 + >>> param.convert(" 7 ") + 7 + If ``value`` can not be converted, ConversionError is raised, which is as subclass of ValidationError. @@ -413,6 +429,12 @@ class Param(plugable.ReadOnly): return self.default def get_values(self): + """ + Return a tuple of possible values. + + For enumerable types, a tuple containing the possible values is + returned. For all other types, an empty tuple is returned. + """ if self.type.name in ('Enum', 'CallbackEnum'): return self.type.values return tuple() -- cgit From 77a378bd61edf50b16b48c9ca73a50ecafd94e09 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sat, 18 Oct 2008 01:02:31 -0600 Subject: Some PEP-257 and reStructuredText cleanup in plugable.py --- ipalib/plugable.py | 89 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 52 insertions(+), 37 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 2a1bdb62..0ece9451 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -57,7 +57,7 @@ class ReadOnly(object): >>> ro.message = 'How are you?' Traceback (most recent call last): File "", line 1, in - File "/home/jderose/projects/freeipa2/ipalib/plugable.py", line 93, in __setattr__ + File ".../ipalib/plugable.py", line 93, in __setattr__ (self.__class__.__name__, name) AttributeError: read-only: cannot set ReadOnly.message >>> del ro.name @@ -113,7 +113,7 @@ class ReadOnly(object): def lock(readonly): """ - Locks a `ReadOnly` instance. + Lock a `ReadOnly` instance. This is mostly a convenience function to call `ReadOnly.__lock__()`. It also verifies that the locking worked using `ReadOnly.__islocked__()` @@ -148,23 +148,22 @@ class SetProxy(ReadOnly): def __len__(self): """ - Returns the number of items in this container. + Return the number of items in this container. """ return len(self.__s) def __iter__(self): """ - Iterates (in ascending order) through the items (or keys) in this - container. + Iterate (in ascending order) through keys. """ for key in sorted(self.__s): yield key def __contains__(self, key): """ - Returns True if this container contains ``key``, False otherwise. + Return True if this container contains ``key``. - :param key: The item (or key) to test for membership. + :param key: The key to test for membership. """ return key in self.__s @@ -191,7 +190,7 @@ class DictProxy(SetProxy): def __getitem__(self, key): """ - Returns the value corresponding to ``key``. + Return the value corresponding to ``key``. :param key: The key of the value you wish to retrieve. """ @@ -199,8 +198,7 @@ class DictProxy(SetProxy): def __call__(self): """ - Iterates (in ascending order by key) through the values in this - container. + Iterate (in ascending order by key) through values. """ for key in self: yield self.__d[key] @@ -228,7 +226,7 @@ class MagicDict(DictProxy): def __getattr__(self, name): """ - Returns the value corresponding to ``name``. + Return the value corresponding to ``name``. :param name: The name of the attribute you wish to retrieve. """ @@ -262,8 +260,9 @@ class Plugin(ReadOnly): def __get_api(self): """ - Returns the `API` instance passed to `finalize()`, or - or returns None if `finalize()` has not yet been called. + Return `API` instance passed to `finalize()`. + + If `finalize()` has not yet been called, None is returned. """ return self.__api api = property(__get_api) @@ -271,7 +270,7 @@ class Plugin(ReadOnly): @classmethod def implements(cls, arg): """ - Return True if class implements ``arg``. + Return True if this class implements ``arg``. There are three different ways this method can be called: @@ -311,12 +310,14 @@ class Plugin(ReadOnly): @classmethod def implemented_by(cls, arg): """ - Returns True if: + Return True if ``arg`` implements public interface of this class. + + This classmethod returns True if: - 1. ``arg`` is an instance of or subclass of this class, and + 1. ``arg`` is an instance of or subclass of this class, and - 2. ``arg`` (or ``arg.__class__`` if instance) has an attribute for - each name in this class's ``__public__`` frozenset + 2. ``arg`` (or ``arg.__class__`` if instance) has an attribute for + each name in this class's ``__public__`` frozenset. Otherwise, returns False. @@ -350,8 +351,10 @@ class Plugin(ReadOnly): def __repr__(self): """ - Returns a fully qualified module_name.class_name() representation that - could be used to construct this Plugin instance. + Return 'module_name.class_name()' representation. + + This representation could be used to instantiate this Plugin + instance given the appropriate environment. """ return '%s.%s()' % ( self.__class__.__module__, @@ -361,13 +364,14 @@ class Plugin(ReadOnly): class PluginProxy(SetProxy): """ - Allows access to only certain attributes on a `Plugin`. + Allow access to only certain attributes on a `Plugin`. Think of a proxy as an agreement that "I will have at most these attributes". This is different from (although similar to) an interface, which can be thought of as an agreement that "I will have at least these attributes". """ + __slots__ = ( '__base', '__target', @@ -403,26 +407,33 @@ class PluginProxy(SetProxy): def implements(self, arg): """ - Returns True if this proxy implements `arg`. Calls the corresponding - classmethod on ProxyTarget. + Return True if plugin being proxied implements ``arg``. + + This method simply calls the corresponding `Plugin.implements` + classmethod. - Unlike ProxyTarget.implements(), this is not a classmethod as a Proxy - only implements anything as an instance. + Unlike `Plugin.implements`, this is not a classmethod as a + `PluginProxy` can only implement anything as an instance. """ return self.__base.implements(arg) def __clone__(self, name_attr): """ - Returns a Proxy instance identical to this one except the proxy name - might be derived from a different attribute on the target. The same - base and target will be used. + Return a `PluginProxy` instance similar to this one. + + The new `PluginProxy` returned will be identical to this one except + the proxy name might be derived from a different attribute on the + target `Plugin`. The same base and target will be used. """ return self.__class__(self.__base, self.__target, name_attr) def __getitem__(self, key): """ - If this proxy allows access to an attribute named ``key``, return that - attribute. + Return attribute named ``key`` on target `Plugin`. + + If this proxy allows access to an attribute named ``key``, that + attribute will be returned. If access is not allowed, + KeyError will be raised. """ if key in self.__public__: return getattr(self.__target, key) @@ -430,8 +441,11 @@ class PluginProxy(SetProxy): def __getattr__(self, name): """ - If this proxy allows access to an attribute named ``name``, return - that attribute. + Return attribute named ``name`` on target `Plugin`. + + If this proxy allows access to an attribute named ``name``, that + attribute will be returned. If access is not allowed, + AttributeError will be raised. """ if name in self.__public__: return getattr(self.__target, name) @@ -439,15 +453,16 @@ class PluginProxy(SetProxy): def __call__(self, *args, **kw): """ - Attempts to call target.__call__(); raises KeyError if `__call__` is - not an attribute this proxy allows access to. + Call target `Plugin` and return its return value. + + If `__call__` is not an attribute this proxy allows access to, + KeyError is raised. """ return self['__call__'](*args, **kw) def __repr__(self): """ - Returns a Python expression that could be used to construct this Proxy - instance given the appropriate environment. + Return a Python expression that could create this instance. """ return '%s(%s, %r)' % ( self.__class__.__name__, @@ -458,7 +473,7 @@ class PluginProxy(SetProxy): def check_name(name): """ - Verifies that ``name`` is suitable for a `NameSpace` member name. + Verify that ``name`` is suitable for a `NameSpace` member name. Raises `errors.NameSpaceError` if ``name`` is not a valid Python identifier suitable for use as the name of `NameSpace` member. -- cgit From 18e74643a6979ca4e490d34975a38c83a1722417 Mon Sep 17 00:00:00 2001 From: Martin Nagy Date: Mon, 20 Oct 2008 19:53:07 +0200 Subject: Add comments in config.py and fix Environment.get() --- ipalib/config.py | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 2 deletions(-) (limited to 'ipalib') diff --git a/ipalib/config.py b/ipalib/config.py index 7899d077..e1b12f1e 100644 --- a/ipalib/config.py +++ b/ipalib/config.py @@ -17,6 +17,14 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" +Basic configuration management. + +This module handles the reading and representation of basic local settings. +It will also take care of settings that can be discovered by different +methods, such as DNS. +""" + from ConfigParser import SafeConfigParser, ParsingError import types import os @@ -92,6 +100,12 @@ class Environment(object): yield key def update(self, new_vals, ignore_errors = False): + """ + Update variables using keys and values from ``new_vals``. + + Error will occur if there is an attempt to override variable that was + already set, unless``ignore_errors`` is True. + """ assert type(new_vals) == dict for key, value in new_vals.iteritems(): if ignore_errors: @@ -103,11 +117,19 @@ class Environment(object): self[key] = value def get(self, name, default=None): - return self.__map.get(name, default) - + """ + Return the value corresponding to ``key``. Defaults to ``default``. + """ + if name in self: + return self[name] + else: + return default def set_default_env(env): + """ + Set default values for ``env``. + """ assert isinstance(env, Environment) default = dict( @@ -129,7 +151,15 @@ def set_default_env(env): class EnvProp(object): + """ + Environment set-once property with optional default value. + """ def __init__(self, type_, default, multi_value=False): + """ + :param type_: Type of the property. + :param default: Default value. + :param multi_value: Allow multiple values. + """ if multi_value: if isinstance(default, tuple) and len(default): check_isinstance(default[0], type_, allow_none=True) @@ -139,17 +169,29 @@ class EnvProp(object): self._multi_value = multi_value def get_value(self): + """ + Return the value if it was set. + + If the value is not set return the default. Otherwise raise an + exception. + """ if self._get() != None: return self._get() else: raise KeyError, 'Value not set' def set_value(self, value): + """ + Set the value. + """ if self._value != None: raise KeyError, 'Value already set' self._value = self._validate(value) def _get(self): + """ + Return value, default, or None. + """ if self._value != None: return self._value elif self._default != None: @@ -158,6 +200,11 @@ class EnvProp(object): return None def _validate(self, value): + """ + Make sure ``value`` is of the right type. Do conversions if necessary. + + This will also handle multi value. + """ if self._multi_value and isinstance(value, tuple): converted = [] for val in value: @@ -167,6 +214,9 @@ class EnvProp(object): return self._validate_value(value) def _validate_value(self, value): + """ + Validate and convert a single value. + """ bool_true = ('true', 'yes', 'on') bool_false = ('false', 'no', 'off') -- cgit From d615e4dafb9c4f3d737143f826ed20be918317fe Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Mon, 20 Oct 2008 16:12:19 -0400 Subject: Port pwpolicy plugin to use b_ldap Add basic output_for_cli() function to user-show --- ipalib/plugins/f_pwpolicy.py | 117 +++++++++++++++++++++++++++---------------- ipalib/plugins/f_user.py | 4 ++ 2 files changed, 79 insertions(+), 42 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugins/f_pwpolicy.py b/ipalib/plugins/f_pwpolicy.py index 36e232dc..ce52e467 100644 --- a/ipalib/plugins/f_pwpolicy.py +++ b/ipalib/plugins/f_pwpolicy.py @@ -27,65 +27,92 @@ from ipalib.frontend import Param from ipalib import api from ipalib import errors from ipalib import ipa_types -from ipa_server import servercore -from ipa_server import ipaldap -import ldap class pwpolicy_mod(frontend.Command): 'Edit existing password policy.' - # FIXME, switch to more human-readable names at some point takes_options = ( - Param('krbmaxpwdlife?', type=ipa_types.Int(), doc='Max. Password Lifetime (days)'), - Param('krbminpwdlife?', type=ipa_types.Int(), doc='Min. Password Lifetime (hours)'), - Param('krbpwdhistorylength?', type=ipa_types.Int(), doc='Password History Size'), - Param('krbpwdmindiffchars?', type=ipa_types.Int(), doc='Min. Number of Character Classes'), - Param('krbpwdminlength?', type=ipa_types.Int(), doc='Min. Length of Password'), + Param('krbmaxpwdlife?', + cli_name='maxlife', + type=ipa_types.Int(), + doc='Max. Password Lifetime (days)' + ), + Param('krbminpwdlife?', + cli_name='minlife', + type=ipa_types.Int(), + doc='Min. Password Lifetime (hours)' + ), + Param('krbpwdhistorylength?', + cli_name='history', + type=ipa_types.Int(), + doc='Password History Size' + ), + Param('krbpwdmindiffchars?', + cli_name='minclasses', + type=ipa_types.Int(), + doc='Min. Number of Character Classes' + ), + Param('krbpwdminlength?', + cli_name='minlength', + type=ipa_types.Int(), + doc='Min. Length of Password' + ), ) def execute(self, *args, **kw): - # Get the existing policy entry - oldpolicy = servercore.get_entry_by_cn("accounts", None) + """ + Execute the pwpolicy-mod operation. - # Convert the existing policy into an entry object - dn = oldpolicy.get('dn') - del oldpolicy['dn'] - entry = ipaldap.Entry((dn, servercore.convert_scalar_values(oldpolicy))) + The dn should not be passed as a keyword argument as it is constructed + by this method. - # FIXME: if the user passed no options should we return something - # more than No modifications to be performed? + Returns the entry - policy = kw + :param args: This function takes no positional arguments + :param kw: Keyword arguments for the other LDAP attributes. + """ + assert 'dn' not in kw + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("cn", "accounts", "krbPwdPolicy") # The LDAP routines want strings, not ints, so convert a few # things. Otherwise it sees a string -> int conversion as a change. - for k in policy.iterkeys(): + for k in kw.iterkeys(): if k.startswith("krb", 0, 3): - policy[k] = str(policy[k]) - - # Convert hours and days to seconds - if policy.get('krbmaxpwdlife'): - policy['krbmaxpwdlife'] = str(int(policy.get('krbmaxpwdlife')) * 86400) - if policy.get('krbminpwdlife'): - policy['krbminpwdlife'] = str(int(policy.get('krbminpwdlife')) * 3600) - # Update the values passed-in - for p in policy: - # Values need to be strings, not integers - entry.setValues(p, str(policy[p])) - - result = servercore.update_entry(entry.toDict()) - - return result - def forward(self, *args, **kw): - result = super(pwpolicy_mod, self).forward(*args, **kw) - if result: + kw[k] = str(kw[k]) + + # Convert hours and days to seconds + if kw.get('krbmaxpwdlife'): + kw['krbmaxpwdlife'] = str(int(kw.get('krbmaxpwdlife')) * 86400) + if kw.get('krbminpwdlife'): + kw['krbminpwdlife'] = str(int(kw.get('krbminpwdlife')) * 3600) + + return ldap.update(dn, **kw) + + def output_for_cli(self, ret): + if ret: print "Policy modified" + api.register(pwpolicy_mod) class pwpolicy_show(frontend.Command): 'Retrieve current password policy' def execute(self, *args, **kw): - policy = servercore.get_entry_by_cn("accounts", None) + """ + Execute the pwpolicy-show operation. + + The dn should not be passed as a keyword argument as it is constructed + by this method. + + Returns the entry + + :param args: Not used. + :param kw: Not used. + """ + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("cn", "accounts", "krbPwdPolicy") + + policy = ldap.retrieve(dn) # convert some values for display purposes policy['krbmaxpwdlife'] = str(int(policy.get('krbmaxpwdlife')) / 86400) @@ -93,8 +120,14 @@ class pwpolicy_show(frontend.Command): return policy - def forward(self, *args, **kw): - result = super(pwpolicy_show, self).forward(*args, **kw) - if not result: return - print result + def output_for_cli(self, policy): + if not policy: return + + print "Password Policy" + print "Min. Password Lifetime (hours): %s" % policy.get('krbminpwdlife') + print "Max. Password Lifetime (days): %s" % policy.get('krbmaxpwdlife') + print "Min. Number of Character Classes: %s" % policy.get('krbpwdmindiffchars') + print "Min. Length of Password: %s" % policy.get('krbpwdminlength') + print "Password History Size: %s" % policy.get('krbpwdhistorylength') + api.register(pwpolicy_show) diff --git a/ipalib/plugins/f_user.py b/ipalib/plugins/f_user.py index 8b4def80..6aebddfa 100644 --- a/ipalib/plugins/f_user.py +++ b/ipalib/plugins/f_user.py @@ -281,6 +281,10 @@ class user_show(crud.Get): dn = ldap.find_entry_dn("uid", uid) # FIXME: should kw contain the list of attributes to display? return ldap.retrieve(dn) + def output_for_cli(self, user): + if user: + for a in user.keys(): + print "%s: %s" % (a, user[a]) api.register(user_show) -- cgit From bb978e591b08b3388345c848fb866c22239094ac Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 20 Oct 2008 16:45:32 -0600 Subject: Fixed bug in DefaultFrom where impleied keys were using entire func_code.co_varnames instead of an approprate slice --- ipalib/frontend.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index d2985fa7..d70a725e 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -121,7 +121,8 @@ class DefaultFrom(plugable.ReadOnly): raise TypeError('callback must be callable; got %r' % callback) self.callback = callback if len(keys) == 0: - self.keys = callback.func_code.co_varnames + fc = callback.func_code + self.keys = fc.co_varnames[:fc.co_argcount] else: self.keys = keys for key in self.keys: -- cgit From ac0a019605e951e1177d4f721bd4174f3c4b53a3 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 20 Oct 2008 18:57:03 -0600 Subject: Reworked 'plugins' command to use output_for_cli() --- ipalib/cli.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index 7148afc1..365eea20 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -154,25 +154,28 @@ class plugins(text_ui): """Show all loaded plugins""" def run(self): + plugins = sorted(self.api.plugins, key=lambda o: o.plugin) + return tuple( + (p.plugin, p.bases) for p in plugins + ) + + def output_for_cli(self, result): self.print_name() first = True - for p in sorted(self.api.plugins, key=lambda o: o.plugin): + for (plugin, bases) in result: if first: first = False else: print '' - print ' plugin: %s' % p.plugin - print ' in namespaces: %s' % ', '.join(p.bases) - if len(self.api.plugins) == 1: + print ' Plugin: %s' % plugin + print ' In namespaces: %s' % ', '.join(bases) + if len(result) == 1: s = '1 plugin loaded.' else: - s = '%d plugins loaded.' % len(self.api.plugins) + s = '%d plugins loaded.' % len(result) self.print_dashed(s) - - - cli_application_commands = ( help, console, -- cgit From c818fe1d2d9ab87c5291ead043784a9d68f95448 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 20 Oct 2008 19:57:02 -0600 Subject: Added docstring (with examples) to frontend.Method class --- ipalib/frontend.py | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index d70a725e..dbbb2bc5 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -894,6 +894,68 @@ class Attribute(plugable.Plugin): class Method(Attribute, Command): + """ + A command with an associated object. + + A `Method` plugin must have a corresponding `Object` plugin. The + association between object and method is done through a simple naming + convention: the first part of the method name (up to the first under + score) is the object name, as the examples in this table show: + + ============= =========== ============== + Method name Object name Attribute name + ============= =========== ============== + user_add user add + noun_verb noun verb + door_open_now door open_door + ============= =========== ============== + + There are three different places a method can be accessed. For example, + say you created a `Method` plugin and its corresponding `Object` plugin + like this: + + >>> api = plugable.API(Command, Object, Method, Property) + >>> class user_add(Method): + ... def run(self): + ... return 'Added the user!' + ... + >>> class user(Object): + ... pass + ... + >>> api.register(user_add) + >>> api.register(user) + >>> api.finalize() + + First, the ``user_add`` plugin can be accessed through the ``api.Method`` + namespace: + + >>> list(api.Method) + ['user_add'] + >>> api.Method.user_add() # Will call user_add.run() + 'Added the user!' + + Second, because `Method` is a subclass of `Command`, the ``user_add`` + plugin can also be accessed through the ``api.Command`` namespace: + + >>> list(api.Command) + ['user_add'] + >>> api.Command.user_add() # Will call user_add.run() + 'Added the user!' + + And third, ``user_add`` can be accessed as an attribute on the ``user`` + `Object`: + + >>> list(api.Object) + ['user'] + >>> list(api.Object.user.methods) + ['add'] + >>> api.Object.user.methods.add() # Will call user_add.run() + 'Added the user!' + + The `Attribute` base class implements the naming convention for the + attribute-to-object association. Also see the `Object` and the + `Property` classes. + """ __public__ = Attribute.__public__.union(Command.__public__) def __init__(self): -- cgit From 461f547e6ae29df72534cce65eb490a7898c1f0a Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 20 Oct 2008 20:28:24 -0600 Subject: Added docstring (with example) to frontend.Attribute class --- ipalib/frontend.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index dbbb2bc5..e0d6fa78 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -857,6 +857,40 @@ class Object(plugable.Plugin): class Attribute(plugable.Plugin): + """ + Base class implementing the attribute-to-object association. + + `Attribute` plugins are associated with an `Object` plugin to group + a common set of commands that operate on a common set of parameters. + + The association between attribute and object is done using a simple + naming convention: the first part of the plugin class name (up to the + first underscore) is the object name, and rest is the attribute name, + as this table shows: + + ============= =========== ============== + Class name Object name Attribute name + ============= =========== ============== + user_add user add + noun_verb noun verb + door_open_now door open_door + ============= =========== ============== + + For example: + + >>> class user_add(Attribute): + ... pass + ... + >>> instance = user_add() + >>> instance.obj_name + 'user' + >>> instance.attr_name + 'add' + + In practice the `Attribute` class is not used directly, but rather is + only the base class for the `Method` and `Property` classes. Also see + the `Object` class. + """ __public__ = frozenset(( 'obj', 'obj_name', -- cgit From 8c54f730c0a156543f23ca90b6220ddd89d76dcc Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Mon, 20 Oct 2008 22:41:53 -0400 Subject: Framework for doing password changes Need mechanism to prompt for new password twice and verify they are the same --- ipalib/plugins/f_passwd.py | 82 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 ipalib/plugins/f_passwd.py (limited to 'ipalib') diff --git a/ipalib/plugins/f_passwd.py b/ipalib/plugins/f_passwd.py new file mode 100644 index 00000000..b1f90732 --- /dev/null +++ b/ipalib/plugins/f_passwd.py @@ -0,0 +1,82 @@ +# Authors: +# Rob Crittenden +# +# 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 + +""" +Frontend plugins for password changes. +""" + +from ipalib import frontend +from ipalib.frontend import Param +from ipalib import api +from ipalib import errors +from ipalib import ipa_types +import krbV + +def get_current_principal(): + try: + return krbV.default_context().default_ccache().principal().name + except krbV.Krb5Error: + #TODO: do a kinit + print "Unable to get kerberos principal" + return None + +class passwd(frontend.Command): + 'Edit existing password policy.' + takes_args = ( + Param('principal', + cli_name='user', + primary_key=True, + default_from=get_current_principal, + ), + ) + def execute(self, principal, **kw): + """ + Execute the passwd operation. + + The dn should not be passed as a keyword argument as it is constructed + by this method. + + Returns the entry + + :param param uid: The login name of the user being updated. + :param kw: Not used. + """ + ldap = self.api.Backend.ldap + + if principal.find('@') < 0: + u = principal.split('@') + if len(u) > 2 or len(u) == 0: + print "Invalid user name (%s)" % principal + if len(u) == 1: + principal = principal+"@"+self.api.env.realm + else: + principal = principal + + dn = ldap.find_entry_dn("krbprincipalname", principal, "person") + + # FIXME: we need a way to prompt for passwords using getpass + kw['newpass'] = "password" + + return ldap.modify_password(dn, **kw) + + def output_for_cli(self, ret): + if ret: + print "Password change successful" + +api.register(passwd) -- cgit From 6b998ed479958ec288bafa6075bb7dc03641fa48 Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Tue, 21 Oct 2008 09:31:06 -0400 Subject: Make boolean options work like standard OptionParser booleans --- ipalib/cli.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index 365eea20..62528034 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -325,11 +325,16 @@ class CLI(object): usage=self.get_usage(cmd), ) for option in cmd.options(): - parser.add_option('--%s' % to_cli(option.cli_name), + o = optparse.make_option('--%s' % to_cli(option.cli_name), dest=option.name, metavar=option.type.name.upper(), help=option.doc, ) + if isinstance(option.type, ipa_types.Bool): + o.action = 'store_true' + o.default = option.default + o.type = None + parser.add_option(o) return parser def parse_globals(self, argv=sys.argv[1:]): -- cgit From 475265ed378cd0879d22e2b9bc59f7eab742fee9 Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Tue, 21 Oct 2008 09:31:44 -0400 Subject: Implement --all option to display all attributes. Still need to strip the dn when not doing all. --- ipalib/plugins/f_user.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugins/f_user.py b/ipalib/plugins/f_user.py index 6aebddfa..972ee075 100644 --- a/ipalib/plugins/f_user.py +++ b/ipalib/plugins/f_user.py @@ -265,6 +265,9 @@ api.register(user_find) class user_show(crud.Get): 'Examine an existing user.' + takes_options = ( + Param('all?', type=ipa_types.Bool(), doc='Display all user attributes'), + ) def execute(self, uid, **kw): """ Execute the user-show operation. @@ -275,12 +278,15 @@ class user_show(crud.Get): Returns the entry :param uid: The login name of the user to retrieve. - :param kw: Not used. + :param kw: "all" set to True = return all attributes """ ldap = self.api.Backend.ldap dn = ldap.find_entry_dn("uid", uid) # FIXME: should kw contain the list of attributes to display? - return ldap.retrieve(dn) + if kw.get('all', False): + return ldap.retrieve(dn) + else: + return ldap.retrieve(dn, ['uid','givenname','sn','homeDirectory','loginshell']) def output_for_cli(self, user): if user: for a in user.keys(): -- cgit From 603baf6b1051ea38a969ac59be334ff38d66998c Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 21 Oct 2008 08:42:52 -0600 Subject: Fixed typos in tables in docstrings for Attribute and Method --- ipalib/frontend.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index e0d6fa78..d918dd83 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -868,13 +868,13 @@ class Attribute(plugable.Plugin): first underscore) is the object name, and rest is the attribute name, as this table shows: - ============= =========== ============== - Class name Object name Attribute name - ============= =========== ============== - user_add user add - noun_verb noun verb - door_open_now door open_door - ============= =========== ============== + =============== =========== ============== + Class name Object name Attribute name + =============== =========== ============== + noun_verb noun verb + user_add user add + user_first_name user first_name + =============== =========== ============== For example: @@ -941,7 +941,7 @@ class Method(Attribute, Command): ============= =========== ============== user_add user add noun_verb noun verb - door_open_now door open_door + door_open_now door open_now ============= =========== ============== There are three different places a method can be accessed. For example, -- cgit From 5e0a0fa745433ef11d7c4ce2afbcbef401c96645 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 21 Oct 2008 08:47:08 -0600 Subject: In second example in NameSpace docstring, renamed 'member' class to 'Member' to make the example clearer --- ipalib/plugable.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 0ece9451..fd87586d 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -514,14 +514,14 @@ class NameSpace(ReadOnly): Here is a more detailed example: - >>> class member(object): + >>> class Member(object): ... def __init__(self, i): ... self.i = i ... self.name = 'member_%d' % i ... def __repr__(self): - ... return 'member(%d)' % self.i + ... return 'Member(%d)' % self.i ... - >>> namespace = NameSpace(member(i) for i in xrange(3)) + >>> namespace = NameSpace(Member(i) for i in xrange(3)) >>> namespace.member_0 is namespace['member_0'] True >>> len(namespace) # Returns the number of members in namespace @@ -529,7 +529,7 @@ class NameSpace(ReadOnly): >>> list(namespace) # As iterable, iterates through the member names ['member_0', 'member_1', 'member_2'] >>> list(namespace()) # Calling a NameSpace iterates through the members - [member(0), member(1), member(2)] + [Member(0), Member(1), Member(2)] >>> 'member_1' in namespace # Does namespace contain 'member_1'? True """ -- cgit From bc5edcf8939d5ed2bcd3ee9dcc3bc80979bf563a Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Tue, 21 Oct 2008 14:42:13 -0400 Subject: Gracefully handle keyboard interrupts (^C) --- ipalib/cli.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index 62528034..ab7e3620 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -277,7 +277,10 @@ class CLI(object): def run_cmd(self, cmd, argv): kw = self.parse(cmd, argv) - self.run_interactive(cmd, kw) + try: + self.run_interactive(cmd, kw) + except KeyboardInterrupt: + return def run_interactive(self, cmd, kw): for param in cmd.params(): -- cgit From 8d07faed4df28b8397971d06a2b101078c88ef86 Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Tue, 21 Oct 2008 16:32:30 -0400 Subject: Update the command-line options to more closely match v1 --- ipalib/plugins/f_user.py | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugins/f_user.py b/ipalib/plugins/f_user.py index 972ee075..70952b29 100644 --- a/ipalib/plugins/f_user.py +++ b/ipalib/plugins/f_user.py @@ -55,11 +55,11 @@ class user(frontend.Object): takes_params = ( Param('givenname', cli_name='first', - doc='User first name', + doc='User\'s first name', ), Param('sn', cli_name='last', - doc='User last name', + doc='User\'s last name', ), Param('uid', cli_name='user', @@ -68,22 +68,40 @@ class user(frontend.Object): normalize=lambda value: value.lower(), ), Param('gecos?', - doc='GECOS field', + doc='Set the GECOS field', default_from=lambda uid: uid, ), Param('homedirectory?', cli_name='home', - doc='Path of user home directory', + doc='Set the User\'s home directory', default_from=lambda uid: '/home/%s' % uid, ), Param('loginshell?', cli_name='shell', default=u'/bin/sh', - doc='Login shell', + doc='Set User\'s Login shell', ), Param('krbprincipalname?', cli_name='principal', - default_from=lambda uid: '%s@EXAMPLE.COM' % uid, + doc='Set User\'s Kerberos Principal name', + default_from=lambda uid: '%s@%s' % (uid, api.env.realm), ), + Param('mailaddress?', + cli_name='mail', + doc='Set User\'s e-mail address', + ), + Param('userpassword?', + cli_name='password', + doc='Set User\'s password', + ), + Param('groups?', + doc='Add account to one or more groups (comma-separated)', + ), + Param('uidnumber?', + cli_name='uid', + type=ipa_types.Int(), + doc='The uid to use for this user. If not included one is automatically set.', + ), + ) api.register(user) -- cgit From 3cbb5c6eeb131e931e4489eafd434079442ca3a7 Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Tue, 21 Oct 2008 16:32:45 -0400 Subject: Don't import servercore --- ipalib/plugins/f_delegation.py | 3 --- 1 file changed, 3 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugins/f_delegation.py b/ipalib/plugins/f_delegation.py index 1fb2b4f9..fbf8cfbf 100644 --- a/ipalib/plugins/f_delegation.py +++ b/ipalib/plugins/f_delegation.py @@ -26,9 +26,6 @@ from ipalib import crud from ipalib.frontend import Param from ipalib import api from ipalib import errors -from ipa_server import servercore -from ipa_server import ipaldap -import ldap class delegation(frontend.Object): """ -- cgit From 245969858d8484428db1edbff8d6bd36587fb144 Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Tue, 21 Oct 2008 16:33:34 -0400 Subject: Implement group member add/remove Add gidNumber to the group command-line --- ipalib/plugins/f_group.py | 151 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 149 insertions(+), 2 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugins/f_group.py b/ipalib/plugins/f_group.py index e83c870e..b5f80f93 100644 --- a/ipalib/plugins/f_group.py +++ b/ipalib/plugins/f_group.py @@ -25,7 +25,8 @@ from ipalib import frontend from ipalib import crud from ipalib.frontend import Param from ipalib import api -from ipa_server import ipautil +from ipalib import errors +from ipalib import ipa_types class group(frontend.Object): @@ -33,7 +34,14 @@ class group(frontend.Object): Group object. """ takes_params = ( - 'description', + Param('description', + doc='A description of this group', + ), + Param('gidnumber?', + cli_name='gid', + type=ipa_types.Int(), + doc='The gid to use for this group. If not included one is automatically set.', + ), Param('cn', cli_name='name', primary_key=True, @@ -210,4 +218,143 @@ class group_show(crud.Get): # FIXME: should kw contain the list of attributes to display? return ldap.retrieve(dn) + def output_for_cli(self, group): + if not group: + return + + for a in group.keys(): + print "%s: %s" % (a, group[a]) + api.register(group_show) + + +class group_add_member(frontend.Command): + 'Add a member to a group.' + takes_args = ( + Param('group', primary_key=True), + ) + takes_options = ( + Param('users?', doc='comma-separated list of users to add'), + Param('groups?', doc='comma-separated list of groups to add'), + ) + def execute(self, cn, **kw): + """ + Execute the group-add-member operation. + + Returns the updated group entry + + :param cn: The group name to add new members to. + :param kw: groups is a comma-separated list of groups to add + :parem kw: users is a comma-separated list of users to add + """ + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("cn", cn) + add_failed = [] + to_add = [] + completed = 0 + + members = kw.get('groups', '').split(',') + for m in members: + if not m: continue + try: + member_dn = ldap.find_entry_dn("cn", m) + to_add.append(member_dn) + except errors.NotFound: + add_failed.append(m) + continue + + members = kw.get('users', '').split(',') + for m in members: + if not m: continue + try: + member_dn = ldap.find_entry_dn("uid", m) + to_add.append(member_dn) + except errors.NotFound: + add_failed.append(m) + continue + + for member_dn in to_add: + try: + ldap.add_member_to_group(member_dn, dn) + completed+=1 + except: + add_failed.append(member_dn) + + return add_failed + + def output_for_cli(self, add_failed): + """ + Output result of this command to command line interface. + """ + if add_failed: + print "These entries failed to add to the group:" + for a in add_failed: + print "\t'%s'" % a + + +api.register(group_add_member) + + +class group_remove_member(frontend.Command): + 'Remove a member from a group.' + takes_args = ( + Param('group', primary_key=True), + ) + takes_options = ( + Param('users?', doc='comma-separated list of users to remove'), + Param('groups?', doc='comma-separated list of groups to remove'), + ) + def execute(self, cn, **kw): + """ + Execute the group-remove-member operation. + + Returns the members that could not be added + + :param cn: The group name to add new members to. + :param kw: groups is a comma-separated list of groups to remove + :parem kw: users is a comma-separated list of users to remove + """ + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("cn", cn) + to_remove = [] + remove_failed = [] + completed = 0 + + members = kw.get('groups', '').split(',') + for m in members: + if not m: continue + try: + member_dn = ldap.find_entry_dn("cn", m) + to_remove.append(member_dn) + except errors.NotFound: + remove_failed.append(m) + continue + + members = kw.get('users', '').split(',') + for m in members: + try: + member_dn = ldap.find_entry_dn("uid", m,) + to_remove.append(member_dn) + except errors.NotFound: + remove_failed.append(m) + continue + + for member_dn in to_remove: + try: + ldap.remove_member_from_group(member_dn, dn) + completed+=1 + except: + remove_failed.append(member_dn) + + return remove_failed + + def output_for_cli(self, remove_failed): + """ + Output result of this command to command line interface. + """ + if remove_failed: + print "These entries failed to be removed from the group:" + for a in remove_failed: + print "\t'%s'" % a + +api.register(group_remove_member) -- cgit From f189b02996668e5d600f1abed675cb20cd72290f Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Wed, 22 Oct 2008 17:52:32 -0400 Subject: Return a value to the shell that called ipa --- ipalib/cli.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index ab7e3620..4e5e433e 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -270,7 +270,7 @@ class CLI(object): self.print_commands() print 'ipa: ERROR: unknown command %r' % key sys.exit(2) - self.run_cmd( + return self.run_cmd( self[key], list(s.decode('utf-8') for s in args[1:]) ) @@ -280,7 +280,11 @@ class CLI(object): try: self.run_interactive(cmd, kw) except KeyboardInterrupt: - return + return 0 + except errors.RuleError, e: + print e + return 2 + return 0 def run_interactive(self, cmd, kw): for param in cmd.params(): -- cgit From 1daf319a19f902d7c7bef37af065cac81be9189e Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Wed, 22 Oct 2008 17:54:04 -0400 Subject: Implement the host commands In order for this to work against a v1 database the update host.update needs to be applied --- ipalib/config.py | 1 + ipalib/plugins/b_xmlrpc.py | 2 +- ipalib/plugins/f_host.py | 271 +++++++++++++++++++++++++++++++++++++++++++++ ipalib/plugins/f_passwd.py | 12 +- ipalib/util.py | 9 ++ 5 files changed, 284 insertions(+), 11 deletions(-) create mode 100644 ipalib/plugins/f_host.py (limited to 'ipalib') diff --git a/ipalib/config.py b/ipalib/config.py index e1b12f1e..ebd602b9 100644 --- a/ipalib/config.py +++ b/ipalib/config.py @@ -138,6 +138,7 @@ def set_default_env(env): container_user = EnvProp(basestring, 'cn=users,cn=accounts'), container_group = EnvProp(basestring, 'cn=groups,cn=accounts'), container_service = EnvProp(basestring, 'cn=services,cn=accounts'), + container_host = EnvProp(basestring, 'cn=computers,cn=accounts'), domain = LazyProp(basestring, get_domain), interactive = EnvProp(bool, True), query_dns = EnvProp(bool, True), diff --git a/ipalib/plugins/b_xmlrpc.py b/ipalib/plugins/b_xmlrpc.py index 9fe5b133..572a7511 100644 --- a/ipalib/plugins/b_xmlrpc.py +++ b/ipalib/plugins/b_xmlrpc.py @@ -69,7 +69,7 @@ class xmlrpc(Backend): print "%s: %s" % (code, faultString) else: print "%s: %s" % (code, getattr(err,'__doc__','')) - return {} + return api.register(xmlrpc) diff --git a/ipalib/plugins/f_host.py b/ipalib/plugins/f_host.py new file mode 100644 index 00000000..da281548 --- /dev/null +++ b/ipalib/plugins/f_host.py @@ -0,0 +1,271 @@ +# Authors: +# Rob Crittenden +# +# 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 + +""" +Frontend plugins for host/machine Identity. +""" + +from ipalib import frontend +from ipalib import crud +from ipalib import util +from ipalib.frontend import Param +from ipalib import api +from ipalib import errors +from ipalib import ipa_types + + +def get_host(hostname): + """ + Try to get the hostname as fully-qualified first, then fall back to + just a host name search. + """ + ldap = api.Backend.ldap + + # Strip off trailing dot + if hostname.endswith('.'): + hostname = hostname[:-1] + try: + dn = ldap.find_entry_dn("cn", hostname, "ipaHost") + except errors.NotFound: + dn = ldap.find_entry_dn("serverhostname", hostname, "ipaHost") + return dn + +def validate_host(cn): + """ + Require at least one dot in the hostname (to support localhost.localdomain) + """ + dots = len(cn.split('.')) + if dots < 2: + return 'Fully-qualified hostname required' + return None + + +class host(frontend.Object): + """ + Host object. + """ + takes_params = ( + Param('cn', + cli_name='hostname', + primary_key=True, + normalize=lambda value: value.lower(), + rules=(validate_host,) + ), + Param('description?', + doc='Description of the host', + ), + Param('localityname?', + cli_name='locality', + doc='Locality of this host (Baltimore, MD)', + ), + Param('nshostlocation?', + cli_name='location', + doc='Location of this host (e.g. Lab 2)', + ), + Param('nshardwareplatform?', + cli_name='platform', + doc='Hardware platform of this host (e.g. Lenovo T61)', + ), + Param('nsosversion?', + cli_name='os', + doc='Operating System and version on this host (e.g. Fedora 9)', + ), + Param('userpassword?', + cli_name='password', + doc='Set a password to be used in bulk enrollment', + ), + ) +api.register(host) + + +class host_add(crud.Add): + 'Add a new host.' + def execute(self, hostname, **kw): + """ + Execute the host-add operation. + + The dn should not be passed as a keyword argument as it is constructed + by this method. + + Returns the entry as it will be created in LDAP. + + :param hostname: The name of the host being added. + :param kw: Keyword arguments for the other LDAP attributes. + """ + assert 'cn' not in kw + assert 'dn' not in kw + ldap = self.api.Backend.ldap + + kw['cn'] = hostname + kw['serverhostname'] = hostname.split('.',1)[0] + kw['dn'] = ldap.make_host_dn(hostname) + kw['krbPrincipalName'] = "host/%s@%s" % (hostname, self.api.env.realm) + + # FIXME: do a DNS lookup to ensure host exists + + current = util.get_current_principal() + if not current: + raise errors.NotFound('Unable to determine current user') + kw['enrolledBy'] = ldap.find_entry_dn("krbPrincipalName", current, "person") + + # Get our configuration + config = ldap.get_ipa_config() + + # some required objectclasses + # FIXME: add this attribute to cn=ipaconfig + #kw['objectClass'] = config.get('ipahostobjectclasses') + kw['objectClass'] = ['nsHost', 'krbPrincipalAux', 'ipaHost'] + + return ldap.create(**kw) + def output_for_cli(self, ret): + """ + Output result of this command to command line interface. + """ + if ret: + print "Host added" + +api.register(host_add) + + +class host_del(crud.Del): + 'Delete an existing host.' + def execute(self, hostname, **kw): + """Delete a host. + + hostname is the name of the host to delete + + :param hostname: The name of the host being removed. + :param kw: Not used. + """ + ldap = self.api.Backend.ldap + dn = get_host(hostname) + return ldap.delete(dn) + def output_for_cli(self, ret): + """ + Output result of this command to command line interface. + """ + if ret: + print "Host deleted" + +api.register(host_del) + + +class host_mod(crud.Mod): + 'Edit an existing host.' + def execute(self, hostname, **kw): + """ + Execute the host-mod operation. + + The dn should not be passed as a keyword argument as it is constructed + by this method. + + Returns the entry + + :param hostname: The name of the host to retrieve. + :param kw: Keyword arguments for the other LDAP attributes. + """ + assert 'cn' not in kw + assert 'dn' not in kw + ldap = self.api.Backend.ldap + dn = get_host(hostname) + return ldap.update(dn, **kw) + + def output_for_cli(self, ret): + """ + Output result of this command to command line interface. + """ + if ret: + print "Host updated" + +api.register(host_mod) + + +class host_find(crud.Find): + 'Search the hosts.' + def get_args(self): + """ + Override Find.get_args() so we can exclude the validation rules + """ + yield self.obj.primary_key.__clone__(rules=tuple()) + def execute(self, term, **kw): + ldap = self.api.Backend.ldap + + # Pull the list of searchable attributes out of the configuration. + #config = ldap.get_ipa_config() + # FIXME: add this attribute to cn=ipaconfig + #search_fields_conf_str = config.get('ipahostsearchfields') + #search_fields = search_fields_conf_str.split(",") + search_fields = ['cn','serverhostname','description','localityname','nshostlocation','nshardwareplatform','nsosversion'] + + for s in search_fields: + kw[s] = term + + # Can't use ldap.get_object_type() since cn is also used for group dns + kw['objectclass'] = "ipaHost" + return ldap.search(**kw) + def output_for_cli(self, hosts): + if not hosts: + return + counter = hosts[0] + hosts = hosts[1:] + if counter == 0: + print "No entries found" + return + elif counter == -1: + print "These results are truncated." + print "Please refine your search and try again." + + for h in hosts: + for a in h.keys(): + print "%s: %s" % (a, h[a]) +api.register(host_find) + + +class host_show(crud.Get): + 'Examine an existing host.' + takes_options = ( + Param('all?', type=ipa_types.Bool(), doc='Display all host attributes'), + ) + def execute(self, hostname, **kw): + """ + Execute the host-show operation. + + The dn should not be passed as a keyword argument as it is constructed + by this method. + + Returns the entry + + :param hostname: The login name of the host to retrieve. + :param kw: "all" set to True = return all attributes + """ + ldap = self.api.Backend.ldap + dn = get_host(hostname) + # FIXME: should kw contain the list of attributes to display? + if kw.get('all', False): + return ldap.retrieve(dn) + else: + value = ldap.retrieve(dn, ['cn','description','localityname','nshostlocation','nshardwareplatform','nsosversion']) + del value['dn'] + return value + def output_for_cli(self, host): + if host: + for a in host.keys(): + print "%s: %s" % (a, host[a]) + +api.register(host_show) diff --git a/ipalib/plugins/f_passwd.py b/ipalib/plugins/f_passwd.py index b1f90732..f70eacac 100644 --- a/ipalib/plugins/f_passwd.py +++ b/ipalib/plugins/f_passwd.py @@ -26,15 +26,7 @@ from ipalib.frontend import Param from ipalib import api from ipalib import errors from ipalib import ipa_types -import krbV - -def get_current_principal(): - try: - return krbV.default_context().default_ccache().principal().name - except krbV.Krb5Error: - #TODO: do a kinit - print "Unable to get kerberos principal" - return None +from ipalib import util class passwd(frontend.Command): 'Edit existing password policy.' @@ -42,7 +34,7 @@ class passwd(frontend.Command): Param('principal', cli_name='user', primary_key=True, - default_from=get_current_principal, + default_from=util.get_current_principal, ), ) def execute(self, principal, **kw): diff --git a/ipalib/util.py b/ipalib/util.py index b60bfc8a..184c6d7c 100644 --- a/ipalib/util.py +++ b/ipalib/util.py @@ -20,6 +20,7 @@ """ Various utility functions. """ +import krbV def xmlrpc_marshal(*args, **kw): """ @@ -39,3 +40,11 @@ def xmlrpc_unmarshal(*params): else: kw = {} return (params[1:], kw) + +def get_current_principal(): + try: + return krbV.default_context().default_ccache().principal().name + except krbV.Krb5Error: + #TODO: do a kinit + print "Unable to get kerberos principal" + return None -- cgit From 06a82bf4b646cd077a43841abb5670d9a495b24c Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Thu, 23 Oct 2008 11:00:50 -0400 Subject: Fix ipa command running in server_context=True Make the LDAP host and port environment variables More changes so that commands have a shell return value lite-xmlrpc no longer hardcodes the kerberos credentials cache location --- ipalib/cli.py | 18 +++++++++++++++++- ipalib/config.py | 2 ++ 2 files changed, 19 insertions(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index 4e5e433e..a802f8ef 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -311,9 +311,25 @@ class CLI(object): break except errors.ValidationError, e: error = e.error + if self.api.env.server_context: + try: + import krbV + import ldap + from ipa_server import conn + from ipa_server.servercore import context + krbccache = krbV.default_context().default_ccache().name + context.conn = conn.IPAConn(self.api.env.ldaphost, self.api.env.ldapport, krbccache) + except ImportError: + print >> sys.stderr, "There was a problem importing a Python module: %s" % sys.exc_value + return 2 + except ldap.LDAPError, e: + print >> sys.stderr, "There was a problem connecting to the LDAP server: %s" % e[0].get('desc') + return 2 ret = cmd(**kw) if callable(cmd.output_for_cli): - cmd.output_for_cli(ret) + return cmd.output_for_cli(ret) + else: + return 0 def parse(self, cmd, argv): parser = self.build_parser(cmd) diff --git a/ipalib/config.py b/ipalib/config.py index ebd602b9..b3155490 100644 --- a/ipalib/config.py +++ b/ipalib/config.py @@ -146,6 +146,8 @@ def set_default_env(env): server_context = EnvProp(bool, True), server = LazyIter(basestring, get_servers), verbose = EnvProp(bool, False), + ldaphost = EnvProp(basestring, 'localhost'), + ldapport = EnvProp(int, 389), ) env.update(default) -- cgit From d2b46f176e5dbc40b67ebd90e6953498c5d6249a Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Thu, 23 Oct 2008 14:36:24 -0400 Subject: Use common display function for user-show and user-find. Add --all option to user-find Fix command-line help to make sense on searches as well --- ipalib/crud.py | 2 ++ ipalib/plugins/f_user.py | 48 ++++++++++++++++++++++++++++++++++++------------ 2 files changed, 38 insertions(+), 12 deletions(-) (limited to 'ipalib') diff --git a/ipalib/crud.py b/ipalib/crud.py index ba4d5718..6194b3fa 100644 --- a/ipalib/crud.py +++ b/ipalib/crud.py @@ -63,6 +63,8 @@ class Find(frontend.Method): def get_options(self): for param in self.obj.params_minus_pk(): yield param.__clone__(required=False) + for option in self.takes_options: + yield option class CrudBackend(backend.Backend): diff --git a/ipalib/plugins/f_user.py b/ipalib/plugins/f_user.py index 70952b29..d8bb49e2 100644 --- a/ipalib/plugins/f_user.py +++ b/ipalib/plugins/f_user.py @@ -47,6 +47,24 @@ class envtest(frontend.Command): return {} api.register(envtest) +def display_user(user): + # FIXME: for now delete dn here. In the future pass in the kw to + # output_for_cli() + attr = sorted(user.keys()) + # Always have sn following givenname + try: + l = attr.index('givenname') + attr.remove('sn') + attr.insert(l+1, 'sn') + except ValueError: + pass + + for a in attr: + if a != 'dn': + print "%s: %s" % (a, user[a]) + +default_attributes = ['uid','givenname','sn','homeDirectory','loginshell'] + class user(frontend.Object): """ @@ -68,30 +86,30 @@ class user(frontend.Object): normalize=lambda value: value.lower(), ), Param('gecos?', - doc='Set the GECOS field', + doc='GECOS field', default_from=lambda uid: uid, ), Param('homedirectory?', cli_name='home', - doc='Set the User\'s home directory', + doc='User\'s home directory', default_from=lambda uid: '/home/%s' % uid, ), Param('loginshell?', cli_name='shell', default=u'/bin/sh', - doc='Set User\'s Login shell', + doc='User\'s Login shell', ), Param('krbprincipalname?', cli_name='principal', - doc='Set User\'s Kerberos Principal name', + doc='User\'s Kerberos Principal name', default_from=lambda uid: '%s@%s' % (uid, api.env.realm), ), Param('mailaddress?', cli_name='mail', - doc='Set User\'s e-mail address', + doc='User\'s e-mail address', ), Param('userpassword?', cli_name='password', - doc='Set User\'s password', + doc='User\'s password', ), Param('groups?', doc='Add account to one or more groups (comma-separated)', @@ -248,6 +266,9 @@ api.register(user_mod) class user_find(crud.Find): 'Search the users.' + takes_options = ( + Param('all?', type=ipa_types.Bool(), doc='Retrieve all user attributes'), + ) def execute(self, term, **kw): ldap = self.api.Backend.ldap @@ -262,6 +283,10 @@ class user_find(crud.Find): object_type = ldap.get_object_type("uid") if object_type and not kw.get('objectclass'): kw['objectclass'] = object_type + if kw.get('all', False): + kw['attributes'] = ['*'] + else: + kw['attributes'] = default_attributes return ldap.search(**kw) def output_for_cli(self, users): if not users: @@ -276,15 +301,15 @@ class user_find(crud.Find): print "Please refine your search and try again." for u in users: - for a in u.keys(): - print "%s: %s" % (a, u[a]) + display_user(u) + print "" api.register(user_find) class user_show(crud.Get): 'Examine an existing user.' takes_options = ( - Param('all?', type=ipa_types.Bool(), doc='Display all user attributes'), + Param('all?', type=ipa_types.Bool(), doc='Retrieve all user attributes'), ) def execute(self, uid, **kw): """ @@ -304,11 +329,10 @@ class user_show(crud.Get): if kw.get('all', False): return ldap.retrieve(dn) else: - return ldap.retrieve(dn, ['uid','givenname','sn','homeDirectory','loginshell']) + return ldap.retrieve(dn, default_attributes) def output_for_cli(self, user): if user: - for a in user.keys(): - print "%s: %s" % (a, user[a]) + display_user(user) api.register(user_show) -- cgit From 59a2cffff45499e1898ebbc7b76ede12d848addb Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 23 Oct 2008 21:21:51 -0600 Subject: IPAError now more appropriately subclasses from StandardError instead of Exception --- ipalib/errors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/errors.py b/ipalib/errors.py index 36df0690..9c40981f 100644 --- a/ipalib/errors.py +++ b/ipalib/errors.py @@ -89,7 +89,7 @@ def check_isinstance(value, type_, name, allow_none=False): return value -class IPAError(Exception): +class IPAError(StandardError): """ Base class for all custom IPA errors. -- cgit From 2ec0312eb6d4131fe22fab6f4409b71cac83f98f Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 24 Oct 2008 01:51:36 -0600 Subject: Finished doodle with stricter version of Environment --- ipalib/config.py | 87 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) (limited to 'ipalib') diff --git a/ipalib/config.py b/ipalib/config.py index b3155490..4f7a008d 100644 --- a/ipalib/config.py +++ b/ipalib/config.py @@ -28,6 +28,7 @@ methods, such as DNS. from ConfigParser import SafeConfigParser, ParsingError import types import os +import sys from errors import check_isinstance, raise_TypeError @@ -126,6 +127,92 @@ class Environment(object): return default +class Env(object): + """ + A mapping object used to store the environment variables. + """ + + __locked = False + + def __init__(self): + object.__setattr__(self, '_Env__d', {}) + + def __lock__(self): + """ + Prevent further changes to environment. + """ + if self.__locked is True: + raise StandardError( + '%s.__lock__() already called' % self.__class__.__name__ + ) + object.__setattr__(self, '_Env__locked', True) + + def __getattr__(self, name): + """ + Return the attribute named ``name``. + """ + if name in self.__d: + return self[name] + raise AttributeError('%s.%s' % + (self.__class__.__name__, name) + ) + + def __setattr__(self, name, value): + """ + Set the attribute named ``name`` to ``value``. + """ + self[name] = value + + def __delattr__(self, name): + """ + Raise AttributeError (deletion is not allowed). + """ + raise AttributeError('cannot del %s.%s' % + (self.__class__.__name__, name) + ) + + def __getitem__(self, key): + """ + Return the value corresponding to ``key``. + """ + if key not in self.__d: + raise KeyError(key) + value = self.__d[key] + if callable(value): + return value() + return value + + def __setitem__(self, key, value): + """ + Set ``key`` to ``value``. + """ + if self.__locked: + raise AttributeError('locked: cannot set %s.%s to %r' % + (self.__class__.__name__, key, value) + ) + if key in self.__d or hasattr(self, key): + raise AttributeError('cannot overwrite %s.%s with %r' % + (self.__class__.__name__, key, value) + ) + self.__d[key] = value + if not callable(value): + assert type(value) in (str, int, bool) + object.__setattr__(self, key, value) + + def __contains__(self, key): + """ + Return True if instance contains ``key``; otherwise return False. + """ + return key in self.__d + + def __iter__(self): # Fix + """ + Iterate through keys in ascending order. + """ + for key in sorted(self.__d): + yield key + + def set_default_env(env): """ Set default values for ``env``. -- cgit From 6a8026f974c4ab65313729eb9e61303b5395a0c0 Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Fri, 24 Oct 2008 11:39:47 -0400 Subject: If a password is supplied then this host will be bulk-enrolled A bulk-enrolled host does not get a kerberos service principal until enrollment time. --- ipalib/plugins/f_host.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugins/f_host.py b/ipalib/plugins/f_host.py index da281548..4f4f7204 100644 --- a/ipalib/plugins/f_host.py +++ b/ipalib/plugins/f_host.py @@ -103,6 +103,9 @@ class host_add(crud.Add): The dn should not be passed as a keyword argument as it is constructed by this method. + If password is set then this is considered a 'bulk' host so we + do not create a kerberos service principal. + Returns the entry as it will be created in LDAP. :param hostname: The name of the host being added. @@ -110,27 +113,39 @@ class host_add(crud.Add): """ assert 'cn' not in kw assert 'dn' not in kw + assert 'krbprincipalname' not in kw ldap = self.api.Backend.ldap kw['cn'] = hostname kw['serverhostname'] = hostname.split('.',1)[0] kw['dn'] = ldap.make_host_dn(hostname) - kw['krbPrincipalName'] = "host/%s@%s" % (hostname, self.api.env.realm) # FIXME: do a DNS lookup to ensure host exists current = util.get_current_principal() if not current: raise errors.NotFound('Unable to determine current user') - kw['enrolledBy'] = ldap.find_entry_dn("krbPrincipalName", current, "person") + kw['enrolledby'] = ldap.find_entry_dn("krbPrincipalName", current, "posixAccount") # Get our configuration config = ldap.get_ipa_config() # some required objectclasses # FIXME: add this attribute to cn=ipaconfig - #kw['objectClass'] = config.get('ipahostobjectclasses') - kw['objectClass'] = ['nsHost', 'krbPrincipalAux', 'ipaHost'] + #kw['objectclass'] = config.get('ipahostobjectclasses') + kw['objectclass'] = ['nsHost', 'ipaHost'] + + # Ensure the list of objectclasses is lower-case + kw['objectclass'] = map(lambda z: z.lower(), kw.get('objectclass')) + + if not kw.get('userpassword', False): + kw['krbprincipalname'] = "host/%s@%s" % (hostname, self.api.env.realm) + + if 'krbprincipalaux' not in kw.get('objectclass'): + kw['objectclass'].append('krbprincipalaux') + else: + if 'krbprincipalaux' in kw.get('objectclass'): + kw['objectclass'].remove('krbprincipalaux') return ldap.create(**kw) def output_for_cli(self, ret): -- cgit From 8788afe18403e7585e4fc2b6a52a352a035fee0b Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Fri, 24 Oct 2008 11:40:47 -0400 Subject: Use posixAccount instead of person to identify users Add output_for_cli to service-find --- ipalib/plugins/f_passwd.py | 2 +- ipalib/plugins/f_service.py | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugins/f_passwd.py b/ipalib/plugins/f_passwd.py index f70eacac..7b424a3b 100644 --- a/ipalib/plugins/f_passwd.py +++ b/ipalib/plugins/f_passwd.py @@ -60,7 +60,7 @@ class passwd(frontend.Command): else: principal = principal - dn = ldap.find_entry_dn("krbprincipalname", principal, "person") + dn = ldap.find_entry_dn("krbprincipalname", principal, "posixAccount") # FIXME: we need a way to prompt for passwords using getpass kw['newpass'] = "password" diff --git a/ipalib/plugins/f_service.py b/ipalib/plugins/f_service.py index 38c80ad2..9e9cec53 100644 --- a/ipalib/plugins/f_service.py +++ b/ipalib/plugins/f_service.py @@ -110,7 +110,7 @@ class service_add(crud.Add): def output_to_cli(self, ret): if ret: - print "Service added" + print "Service added" api.register(service_add) @@ -146,7 +146,7 @@ class service_find(crud.Find): def execute(self, principal, **kw): ldap = self.api.Backend.ldap - kw['filter'] = "&(objectclass=krbPrincipalAux)(!(objectClass=person))(!(|(krbprincipalname=kadmin/*)(krbprincipalname=K/M@*)(krbprincipalname=krbtgt/*)))" + kw['filter'] = "&(objectclass=krbPrincipalAux)(!(objectClass=posixAccount))(!(|(krbprincipalname=kadmin/*)(krbprincipalname=K/M@*)(krbprincipalname=krbtgt/*)))" kw['krbprincipalname'] = principal object_type = ldap.get_object_type("krbprincipalname") @@ -193,5 +193,11 @@ class service_show(crud.Get): dn = ldap.find_entry_dn("krbprincipalname", principal) # FIXME: should kw contain the list of attributes to display? return ldap.retrieve(dn) + def output_for_cli(self, service): + if not service: + return + + for a in service.keys(): + print "%s: %s" % (a, service[a]) api.register(service_show) -- cgit From 34520981eeaac5d4f37915509a9e26428e26f5c0 Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Fri, 24 Oct 2008 14:17:20 -0400 Subject: Don't allow service-add to create host/ principals --- ipalib/errors.py | 4 ++++ ipalib/plugins/f_service.py | 3 +++ 2 files changed, 7 insertions(+) (limited to 'ipalib') diff --git a/ipalib/errors.py b/ipalib/errors.py index 9c40981f..c2d83e73 100644 --- a/ipalib/errors.py +++ b/ipalib/errors.py @@ -356,6 +356,10 @@ class DefaultGroup(ConfigurationError): """You cannot remove the default users group""" faultCode = 1025 +class HostService(ConfigurationError): + """You must enroll a host in order to create a host service""" + faultCode = 1026 + class FunctionDeprecated(GenericError): """Raised by a deprecated function""" faultCode = 2000 diff --git a/ipalib/plugins/f_service.py b/ipalib/plugins/f_service.py index 9e9cec53..f02176ff 100644 --- a/ipalib/plugins/f_service.py +++ b/ipalib/plugins/f_service.py @@ -72,6 +72,9 @@ class service_add(crud.Add): raise errors.MalformedServicePrincipal service = sp[0] + if service.lower() == "host": + raise errors.HostService + sr = sp[1].split('@') if len(sr) == 1: hostname = sr[0].lower() -- cgit From f80beb948bb8914df922e85ef20d9152ca47b527 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 24 Oct 2008 15:07:07 -0600 Subject: Added ipalib/constants.py; added Env._load_config() method along with comprehensive unit tests for same --- ipalib/config.py | 81 +++++++++++++++++++++++++++++++++++++++++++++++++++-- ipalib/constants.py | 25 +++++++++++++++++ 2 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 ipalib/constants.py (limited to 'ipalib') diff --git a/ipalib/config.py b/ipalib/config.py index 4f7a008d..86d8f1da 100644 --- a/ipalib/config.py +++ b/ipalib/config.py @@ -25,12 +25,13 @@ It will also take care of settings that can be discovered by different methods, such as DNS. """ -from ConfigParser import SafeConfigParser, ParsingError +from ConfigParser import SafeConfigParser, ParsingError, RawConfigParser import types import os +from os import path import sys - from errors import check_isinstance, raise_TypeError +import constants DEFAULT_CONF='/etc/ipa/ipa.conf' @@ -136,6 +137,71 @@ class Env(object): def __init__(self): object.__setattr__(self, '_Env__d', {}) + self.ipalib = path.dirname(path.abspath(__file__)) + self.site_packages = path.dirname(self.ipalib) + self.script = path.abspath(sys.argv[0]) + self.bin = path.dirname(self.script) + self.home = path.abspath(os.environ['HOME']) + self.dot_ipa = path.join(self.home, '.ipa') + + def _bootstrap(self, **overrides): + """ + Initialize basic environment. + + This method will initialize only enough environment information to + determine whether ipa is running in-tree, what the context is, + and the location of the configuration file. + + This method should be called before any plugins are loaded. + """ + for (key, value) in overrides.items(): + self[key] = value + if 'in_tree' not in self: + if self.bin == self.site_packages and \ + path.isfile(path.join(self.bin, 'setup.py')): + self.in_tree = True + else: + self.in_tree = False + if 'context' not in self: + self.context = 'default' + if 'conf' not in self: + name = '%s.conf' % self.context + if self.in_tree: + self.conf = path.join(self.dot_ipa, name) + else: + self.conf = path.join('/', 'etc', 'ipa', name) + + def _load_config(self, conf_file): + """ + Merge in values from ``conf_file`` into this `Env`. + """ + section = constants.CONFIG_SECTION + if not path.isfile(conf_file): + return + parser = RawConfigParser() + try: + parser.read(conf_file) + except ParsingError: + return + if not parser.has_section(section): + parser.add_section(section) + items = parser.items(section) + if len(items) == 0: + return + i = 0 + for (key, value) in items: + if key not in self: + self[key] = value + i += 1 + return (i, len(items)) + + def _finalize(self, **defaults): + """ + Finalize and lock environment. + + This method should be called after all plugins have bean loaded and + after `plugable.API.finalize()` has been called. + """ def __lock__(self): """ @@ -186,6 +252,7 @@ class Env(object): """ Set ``key`` to ``value``. """ + # FIXME: the key should be checked with check_name() if self.__locked: raise AttributeError('locked: cannot set %s.%s to %r' % (self.__class__.__name__, key, value) @@ -194,10 +261,18 @@ class Env(object): raise AttributeError('cannot overwrite %s.%s with %r' % (self.__class__.__name__, key, value) ) - self.__d[key] = value if not callable(value): + if isinstance(value, basestring): + value = str(value.strip()) + if value.lower() == 'true': + value = True + elif value.lower() == 'false': + value = False + elif value.isdigit(): + value = int(value) assert type(value) in (str, int, bool) object.__setattr__(self, key, value) + self.__d[key] = value def __contains__(self, key): """ diff --git a/ipalib/constants.py b/ipalib/constants.py new file mode 100644 index 00000000..d817fda4 --- /dev/null +++ b/ipalib/constants.py @@ -0,0 +1,25 @@ +# Authors: +# Jason Gerard DeRose +# +# 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 + +""" +Constants centralized in one file. +""" + +# The section read in config files, i.e. [global] +CONFIG_SECTION = 'global' -- cgit From 2a41db33c6d9f6efa826e16d91eba76defe899d2 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 24 Oct 2008 15:35:58 -0600 Subject: Env._bootstrap() now raises StandardError if called more than once --- ipalib/config.py | 9 +++++++++ 1 file changed, 9 insertions(+) (limited to 'ipalib') diff --git a/ipalib/config.py b/ipalib/config.py index 86d8f1da..dd00d713 100644 --- a/ipalib/config.py +++ b/ipalib/config.py @@ -137,6 +137,7 @@ class Env(object): def __init__(self): object.__setattr__(self, '_Env__d', {}) + object.__setattr__(self, '_Env__done', set()) self.ipalib = path.dirname(path.abspath(__file__)) self.site_packages = path.dirname(self.ipalib) self.script = path.abspath(sys.argv[0]) @@ -144,6 +145,13 @@ class Env(object): self.home = path.abspath(os.environ['HOME']) self.dot_ipa = path.join(self.home, '.ipa') + def __do(self, name): + if name in self.__done: + raise StandardError( + '%s.%s() already called' % (self.__class__.__name__, name) + ) + self.__done.add(name) + def _bootstrap(self, **overrides): """ Initialize basic environment. @@ -154,6 +162,7 @@ class Env(object): This method should be called before any plugins are loaded. """ + self.__do('_bootstrap') for (key, value) in overrides.items(): self[key] = value if 'in_tree' not in self: -- cgit From ac4efac3944d180cffd0ad9d63f631dc928e1d28 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 24 Oct 2008 20:02:14 -0600 Subject: Finished Env._finalize_core() and corresponding unit tests --- ipalib/config.py | 62 ++++++++++++++++++++++++++++++++++++++++++----------- ipalib/constants.py | 38 +++++++++++++++++++++++++++++++- 2 files changed, 86 insertions(+), 14 deletions(-) (limited to 'ipalib') diff --git a/ipalib/config.py b/ipalib/config.py index dd00d713..bced62fd 100644 --- a/ipalib/config.py +++ b/ipalib/config.py @@ -145,13 +145,20 @@ class Env(object): self.home = path.abspath(os.environ['HOME']) self.dot_ipa = path.join(self.home, '.ipa') - def __do(self, name): + def __doing(self, name): if name in self.__done: raise StandardError( '%s.%s() already called' % (self.__class__.__name__, name) ) self.__done.add(name) + def __do_if_not_done(self, name): + if name not in self.__done: + getattr(self, name)() + + def _isdone(self, name): + return name in self.__done + def _bootstrap(self, **overrides): """ Initialize basic environment. @@ -159,10 +166,8 @@ class Env(object): This method will initialize only enough environment information to determine whether ipa is running in-tree, what the context is, and the location of the configuration file. - - This method should be called before any plugins are loaded. """ - self.__do('_bootstrap') + self.__doing('_bootstrap') for (key, value) in overrides.items(): self[key] = value if 'in_tree' not in self: @@ -180,7 +185,46 @@ class Env(object): else: self.conf = path.join('/', 'etc', 'ipa', name) - def _load_config(self, conf_file): + def _finalize_core(self, **defaults): + """ + Complete initialization of standard IPA environment. + + After this method is called, the all environment variables + used by all the built-in plugins will be available. + + This method should be called before loading any plugins. It will + automatically call `Env._bootstrap()` if it has not yet been called. + + After this method has finished, the `Env` instance is still writable + so that third + """ + self.__doing('_finalize_core') + self.__do_if_not_done('_bootstrap') + self._merge_config(self.conf) + if 'in_server' not in self: + self.in_server = (self.context == 'server') + if 'log' not in self: + name = '%s.log' % self.context + if self.in_tree or self.context == 'cli': + self.log = path.join(self.dot_ipa, 'log', name) + else: + self.log = path.join('/', 'var', 'log', 'ipa', name) + for (key, value) in defaults.items(): + if key not in self: + self[key] = value + + def _finalize(self): + """ + Finalize and lock environment. + + This method should be called after all plugins have bean loaded and + after `plugable.API.finalize()` has been called. + """ + self.__doing('_finalize') + self.__do_if_not_done('_finalize_core') + self.__lock__() + + def _merge_config(self, conf_file): """ Merge in values from ``conf_file`` into this `Env`. """ @@ -204,14 +248,6 @@ class Env(object): i += 1 return (i, len(items)) - def _finalize(self, **defaults): - """ - Finalize and lock environment. - - This method should be called after all plugins have bean loaded and - after `plugable.API.finalize()` has been called. - """ - def __lock__(self): """ Prevent further changes to environment. diff --git a/ipalib/constants.py b/ipalib/constants.py index d817fda4..a1cc5b4c 100644 --- a/ipalib/constants.py +++ b/ipalib/constants.py @@ -21,5 +21,41 @@ Constants centralized in one file. """ -# The section read in config files, i.e. [global] +# The section to read in the config files, i.e. [global] CONFIG_SECTION = 'global' + + +# The default configuration for api.env +DEFAULT_CONFIG = ( + ('lite_xmlrpc_port', 8888), + ('lite_webui_port', 9999), + ('xmlrpc_uri', 'http://localhost:8888'), + ('ldap_uri', ''), + + ('verbose', False), + ('debug', False), + + # Env.__init__() or Env._bootstrap() or Env._finalize_core() + # will have filled in all the keys below by the time DEFAULT_CONFIG + # is merged in, so the values below are never actually used. They are + # listed both to provide a big picture and so DEFAULT_CONFIG contains + # the keys that should be present after Env._load_standard is called. + + # Set in Env.__init__(): + ('ipalib', None), # The directory containing ipalib/__init__.py + ('site_packages', None), # The directory contaning ipalib + ('script', None), # sys.argv[0] + ('bin', None), # The directory containing script + ('home', None), # The home directory of user underwhich process is running + ('dot_ipa', None), # ~/.ipa directory + + # Set in Env._bootstrap(): + ('in_tree', None), # Whether or not running in-tree (bool) + ('context', None), # Name of context, default is 'default' + ('conf', None), # Path to configuration file + + # Set in Env._finalize_core(): + ('in_server', None), # Whether or not running in-server (bool) + ('log', None), # Path to log file + +) -- cgit From 759734864e72c209d62c970d2e325e96ae02fcb7 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 24 Oct 2008 20:21:27 -0600 Subject: Finished Env._finalize() and corresponding unit tests --- ipalib/config.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) (limited to 'ipalib') diff --git a/ipalib/config.py b/ipalib/config.py index bced62fd..71d3024c 100644 --- a/ipalib/config.py +++ b/ipalib/config.py @@ -168,7 +168,7 @@ class Env(object): and the location of the configuration file. """ self.__doing('_bootstrap') - for (key, value) in overrides.items(): + for (key, value) in overrides.iteritems(): self[key] = value if 'in_tree' not in self: if self.bin == self.site_packages and \ @@ -209,11 +209,11 @@ class Env(object): self.log = path.join(self.dot_ipa, 'log', name) else: self.log = path.join('/', 'var', 'log', 'ipa', name) - for (key, value) in defaults.items(): + for (key, value) in defaults.iteritems(): if key not in self: self[key] = value - def _finalize(self): + def _finalize(self, **lastchance): """ Finalize and lock environment. @@ -222,11 +222,14 @@ class Env(object): """ self.__doing('_finalize') self.__do_if_not_done('_finalize_core') + for (key, value) in lastchance.iteritems(): + if key not in self: + self[key] = value self.__lock__() def _merge_config(self, conf_file): """ - Merge in values from ``conf_file`` into this `Env`. + Merge values from ``conf_file`` into this `Env`. """ section = constants.CONFIG_SECTION if not path.isfile(conf_file): @@ -258,6 +261,9 @@ class Env(object): ) object.__setattr__(self, '_Env__locked', True) + def __islocked__(self): + return self.__locked + def __getattr__(self, name): """ Return the attribute named ``name``. -- cgit From ff5cb4cf6f036db892ebedc018b9740438c7e192 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 24 Oct 2008 20:59:11 -0600 Subject: Added more needed config in DEFAULT_CONFIG --- ipalib/constants.py | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) (limited to 'ipalib') diff --git a/ipalib/constants.py b/ipalib/constants.py index a1cc5b4c..e7b370f1 100644 --- a/ipalib/constants.py +++ b/ipalib/constants.py @@ -1,4 +1,5 @@ # Authors: +# Martin Nagy # Jason Gerard DeRose # # Copyright (C) 2008 Red Hat @@ -18,7 +19,7 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ -Constants centralized in one file. +All constants centralized in one file. """ # The section to read in the config files, i.e. [global] @@ -26,20 +27,46 @@ CONFIG_SECTION = 'global' # The default configuration for api.env +# This is a tuple instead of a dict so that it is immutable. +# To create a dict with this config, just "d = dict(DEFAULT_CONFIG)". DEFAULT_CONFIG = ( + # Domain, realm, basedn: + ('domain', 'example.com'), + ('realm', 'EXAMPLE.COM'), + ('basedn', 'dc=example,dc=com'), + + # LDAP containers: + ('container_accounts', 'cn=accounts'), + ('container_user', 'cn=users,cn=accounts'), + ('container_group', 'cn=groups,cn=accounts'), + ('container_service', 'cn=services,cn=accounts'), + ('container_host', 'cn=computers,cn=accounts'), + + # Ports, hosts, and URIs: ('lite_xmlrpc_port', 8888), ('lite_webui_port', 9999), ('xmlrpc_uri', 'http://localhost:8888'), - ('ldap_uri', ''), + ('ldap_uri', 'ldap://localhost:389'), + ('ldap_host', 'localhost'), + ('ldap_port', 389), + # Debugging: ('verbose', False), ('debug', False), + + # ******************************************************** + # The remaining keys are never set from the values here! + # ******************************************************** + # # Env.__init__() or Env._bootstrap() or Env._finalize_core() # will have filled in all the keys below by the time DEFAULT_CONFIG # is merged in, so the values below are never actually used. They are - # listed both to provide a big picture and so DEFAULT_CONFIG contains - # the keys that should be present after Env._load_standard is called. + # listed both to provide a big picture and also so DEFAULT_CONFIG contains + # the keys that should be present after Env._finalize_core() is called. + # + # The values are all None so if for some reason any of these keys were + # set from the values here, an exception will be raised. # Set in Env.__init__(): ('ipalib', None), # The directory containing ipalib/__init__.py -- cgit From 6b8abb0d78a8d86d7ca52083a267fe226bf74656 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sun, 26 Oct 2008 23:28:06 -0600 Subject: Implemented placeholder API.bootstrap() method; added API __doing(), __do_if_not_done(), isdone() methods borrowed from Env; API.finalize() now cascades call to API.bootstrap() --- ipalib/plugable.py | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index fd87586d..f0121433 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -707,19 +707,40 @@ class API(DictProxy): """ Dynamic API object through which `Plugin` instances are accessed. """ - __finalized = False def __init__(self, *allowed): self.__d = dict() + self.__done = set() self.register = Registrar(*allowed) self.env = Environment() super(API, self).__init__(self.__d) + def __doing(self, name): + if name in self.__done: + raise StandardError( + '%s.%s() already called' % (self.__class__.__name__, name) + ) + self.__done.add(name) + + def __do_if_not_done(self, name): + if name not in self.__done: + getattr(self, name)() + + def isdone(self, name): + return name in self.__done + + def bootstrap(self, **overrides): + """ + Initialize environment variables needed by built-in plugins. + """ + self.__doing('bootstrap') + def finalize(self): """ Finalize the registration, instantiate the plugins. """ - assert not self.__finalized, 'finalize() can only be called once' + self.__doing('finalize') + self.__do_if_not_done('bootstrap') class PluginInstance(object): """ -- cgit From 4fe03f5e17dfe9d4478a75dfada2282535c989fe Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sun, 26 Oct 2008 23:53:44 -0600 Subject: Added API.load_plugins() place-holder, which cascades call to API.bootstrap() --- ipalib/plugable.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index f0121433..4c0b175f 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -735,9 +735,22 @@ class API(DictProxy): """ self.__doing('bootstrap') + def load_plugins(self, dry_run=False): + """ + Load plugins from all standard locations. + + `API.bootstrap` will automatically be called if it hasn't been + already. + """ + self.__doing('load_plugins') + self.__do_if_not_done('bootstrap') + def finalize(self): """ Finalize the registration, instantiate the plugins. + + `API.bootstrap` will automatically be called if it hasn't been + already. """ self.__doing('finalize') self.__do_if_not_done('bootstrap') -- cgit From 03accc5fb382777d9bbdb245f3211d5c06489f6e Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 27 Oct 2008 00:23:43 -0600 Subject: Copied plugin loading function from load_plugins.py to util.py; API.load_plugins() method now calls functions in util --- ipalib/plugable.py | 5 +++++ ipalib/util.py | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 4c0b175f..36721157 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -30,6 +30,7 @@ import inspect import errors from errors import check_type, check_isinstance from config import Environment +import util class ReadOnly(object): @@ -744,6 +745,10 @@ class API(DictProxy): """ self.__doing('load_plugins') self.__do_if_not_done('bootstrap') + if dry_run: + return + util.import_plugins_subpackage('ipalib') + util.import_plugins_subpackage('ipa_server') def finalize(self): """ diff --git a/ipalib/util.py b/ipalib/util.py index 184c6d7c..d7e2c2a4 100644 --- a/ipalib/util.py +++ b/ipalib/util.py @@ -20,7 +20,11 @@ """ Various utility functions. """ + import krbV +import os +from os import path +import imp def xmlrpc_marshal(*args, **kw): """ @@ -41,6 +45,7 @@ def xmlrpc_unmarshal(*params): kw = {} return (params[1:], kw) + def get_current_principal(): try: return krbV.default_context().default_ccache().principal().name @@ -48,3 +53,49 @@ def get_current_principal(): #TODO: do a kinit print "Unable to get kerberos principal" return None + + +# FIXME: This function has no unit test +def find_modules_in_dir(src_dir): + """ + Iterate through module names found in ``src_dir``. + """ + if not (path.abspath(src_dir) == src_dir and path.isdir(src_dir)): + return + if path.islink(src_dir): + return + suffix = '.py' + for name in sorted(os.listdir(src_dir)): + if not name.endswith(suffix): + continue + py_file = path.join(src_dir, name) + if path.islink(py_file) or not path.isfile(py_file): + continue + module = name[:-len(suffix)] + if module == '__init__': + continue + yield module + + +# FIXME: This function has no unit test +def load_plugins_in_dir(src_dir): + """ + Import each Python module found in ``src_dir``. + """ + for module in find_modules_in_dir(src_dir): + imp.load_module(module, *imp.find_module(module, [src_dir])) + + +# FIXME: This function has no unit test +def import_plugins_subpackage(name): + """ + Import everythig in ``plugins`` sub-package of package named ``name``. + """ + try: + plugins = __import__(name + '.plugins').plugins + except ImportError: + return + src_dir = path.dirname(path.abspath(plugins.__file__)) + for name in find_modules_in_dir(src_dir): + full_name = '%s.%s' % (plugins.__name__, name) + __import__(full_name) -- cgit From c8b3f6516513dc3e5948fe8280c3f159ad122684 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 27 Oct 2008 00:41:37 -0600 Subject: Removed depreciated load_plugins.py module; changed all places where load_plugins was imported to now call api.load_plugins() --- ipalib/load_plugins.py | 82 -------------------------------------------------- 1 file changed, 82 deletions(-) delete mode 100644 ipalib/load_plugins.py (limited to 'ipalib') diff --git a/ipalib/load_plugins.py b/ipalib/load_plugins.py deleted file mode 100644 index 4e02f5ba..00000000 --- a/ipalib/load_plugins.py +++ /dev/null @@ -1,82 +0,0 @@ -# Authors: -# Jason Gerard DeRose -# -# 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 - -""" -Importing this module causes the plugins to be loaded. - -This is not in __init__.py so that importing ipalib or its other sub-modules -does not cause unnecessary side effects. - -Eventually this will also load the out-of tree plugins, but for now it just -loads the internal plugins. -""" - -import os -from os import path -import imp -import inspect - - -def find_modules_in_dir(src_dir): - """ - Iterate through module names found in ``src_dir``. - """ - if not (path.abspath(src_dir) == src_dir and path.isdir(src_dir)): - return - if path.islink(src_dir): - return - suffix = '.py' - for name in sorted(os.listdir(src_dir)): - if not name.endswith(suffix): - continue - py_file = path.join(src_dir, name) - if path.islink(py_file) or not path.isfile(py_file): - continue - module = name[:-len(suffix)] - if module == '__init__': - continue - yield module - - -def load_plugins_in_dir(src_dir): - """ - Import each Python module found in ``src_dir``. - """ - for module in find_modules_in_dir(src_dir): - imp.load_module(module, *imp.find_module(module, [src_dir])) - - -def import_plugins(name): - """ - Load all plugins found in standard 'plugins' sub-package. - """ - try: - plugins = __import__(name + '.plugins').plugins - except ImportError: - return - src_dir = path.dirname(path.abspath(plugins.__file__)) - for name in find_modules_in_dir(src_dir): - full_name = '%s.%s' % (plugins.__name__, name) - __import__(full_name) - - -for name in ['ipalib', 'ipa_server', 'ipa_not_a_package']: - import_plugins(name) - -load_plugins_in_dir(path.expanduser('~/.freeipa')) -- cgit From 28dd8e74bdefd62307881f6e086af59db97a21a0 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 27 Oct 2008 00:58:25 -0600 Subject: Env._bootstrap() now also sets Env.conf_default --- ipalib/config.py | 12 +++++++----- ipalib/constants.py | 3 ++- 2 files changed, 9 insertions(+), 6 deletions(-) (limited to 'ipalib') diff --git a/ipalib/config.py b/ipalib/config.py index 71d3024c..7bb3e072 100644 --- a/ipalib/config.py +++ b/ipalib/config.py @@ -178,12 +178,14 @@ class Env(object): self.in_tree = False if 'context' not in self: self.context = 'default' + if self.in_tree: + base = self.dot_ipa + else: + base = path.join('/', 'etc', 'ipa') if 'conf' not in self: - name = '%s.conf' % self.context - if self.in_tree: - self.conf = path.join(self.dot_ipa, name) - else: - self.conf = path.join('/', 'etc', 'ipa', name) + self.conf = path.join(base, '%s.conf' % self.context) + if 'conf_default' not in self: + self.conf_default = path.join(base, 'default.conf') def _finalize_core(self, **defaults): """ diff --git a/ipalib/constants.py b/ipalib/constants.py index e7b370f1..4942cc9b 100644 --- a/ipalib/constants.py +++ b/ipalib/constants.py @@ -79,7 +79,8 @@ DEFAULT_CONFIG = ( # Set in Env._bootstrap(): ('in_tree', None), # Whether or not running in-tree (bool) ('context', None), # Name of context, default is 'default' - ('conf', None), # Path to configuration file + ('conf', None), # Path to config file + ('conf_default', None), # Path to common default config file # Set in Env._finalize_core(): ('in_server', None), # Whether or not running in-server (bool) -- cgit From 25a7df9615058372b81a41df6baa2c4692df0063 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 27 Oct 2008 01:09:53 -0600 Subject: Env._finalize_core() now also loads config from Env.conf_default --- ipalib/config.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'ipalib') diff --git a/ipalib/config.py b/ipalib/config.py index 7bb3e072..538a97a7 100644 --- a/ipalib/config.py +++ b/ipalib/config.py @@ -203,6 +203,8 @@ class Env(object): self.__doing('_finalize_core') self.__do_if_not_done('_bootstrap') self._merge_config(self.conf) + if self.conf_default != self.conf: + self._merge_config(self.conf_default) if 'in_server' not in self: self.in_server = (self.context == 'server') if 'log' not in self: -- cgit From d76202fea37e63fbc660ed2cf2059f455b8e2213 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 27 Oct 2008 01:35:40 -0600 Subject: API.env is now an Env instance rather than an Environment instance --- ipalib/frontend.py | 2 +- ipalib/plugable.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index d918dd83..62a503cc 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -668,7 +668,7 @@ class Command(plugable.Plugin): on the nearest IPA server and the actual work this command performs is executed remotely. """ - if self.api.env.server_context: + if self.api.env.in_server: target = self.execute else: target = self.forward diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 36721157..59484989 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -29,7 +29,8 @@ import re import inspect import errors from errors import check_type, check_isinstance -from config import Environment +from config import Environment, Env +import constants import util @@ -713,7 +714,7 @@ class API(DictProxy): self.__d = dict() self.__done = set() self.register = Registrar(*allowed) - self.env = Environment() + self.env = Env super(API, self).__init__(self.__d) def __doing(self, name): -- cgit From 201a963930b69baff2a31f685cb4cdd38d6da42e Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Mon, 27 Oct 2008 12:23:49 -0400 Subject: Fix comment --- ipalib/plugins/f_group.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/plugins/f_group.py b/ipalib/plugins/f_group.py index b5f80f93..13af14c1 100644 --- a/ipalib/plugins/f_group.py +++ b/ipalib/plugins/f_group.py @@ -135,7 +135,7 @@ class group_mod(crud.Mod): 'Edit an existing group.' def execute(self, cn, **kw): """ - Execute the user-mod operation. + Execute the group-mod operation. The dn should not be passed as a keyword argument as it is constructed by this method. -- cgit From 54f37503d2076b99b3b7479b19fec4fa17bc7c59 Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Mon, 27 Oct 2008 12:24:17 -0400 Subject: Implement host groups --- ipalib/config.py | 1 + ipalib/plugins/f_hostgroup.py | 328 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 329 insertions(+) create mode 100644 ipalib/plugins/f_hostgroup.py (limited to 'ipalib') diff --git a/ipalib/config.py b/ipalib/config.py index b3155490..75e009dc 100644 --- a/ipalib/config.py +++ b/ipalib/config.py @@ -139,6 +139,7 @@ def set_default_env(env): container_group = EnvProp(basestring, 'cn=groups,cn=accounts'), container_service = EnvProp(basestring, 'cn=services,cn=accounts'), container_host = EnvProp(basestring, 'cn=computers,cn=accounts'), + container_hostgroup = EnvProp(basestring, 'cn=hostgroups,cn=accounts'), domain = LazyProp(basestring, get_domain), interactive = EnvProp(bool, True), query_dns = EnvProp(bool, True), diff --git a/ipalib/plugins/f_hostgroup.py b/ipalib/plugins/f_hostgroup.py new file mode 100644 index 00000000..27aea00c --- /dev/null +++ b/ipalib/plugins/f_hostgroup.py @@ -0,0 +1,328 @@ +# Authors: +# Rob Crittenden +# +# 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 + +""" +Frontend plugins for groups of hosts +""" + +from ipalib import frontend +from ipalib import crud +from ipalib.frontend import Param +from ipalib import api +from ipalib import errors +from ipalib import ipa_types + +hostgroup_filter = "groupofnames)(!(objectclass=posixGroup)" + +class hostgroup(frontend.Object): + """ + Host Group object. + """ + takes_params = ( + Param('description', + doc='A description of this group', + ), + Param('cn', + cli_name='name', + primary_key=True, + normalize=lambda value: value.lower(), + ) + ) +api.register(hostgroup) + + +class hostgroup_add(crud.Add): + 'Add a new group of hosts.' + + def execute(self, cn, **kw): + """ + Execute the hostgroup-add operation. + + The dn should not be passed as a keyword argument as it is constructed + by this method. + + Returns the entry as it will be created in LDAP. + + No need to explicitly set gidNumber. The dna_plugin will do this + for us if the value isn't provided by the caller. + + :param cn: The name of the host group being added. + :param kw: Keyword arguments for the other LDAP attributes. + """ + assert 'cn' not in kw + assert 'dn' not in kw + ldap = self.api.Backend.ldap + kw['cn'] = cn + kw['dn'] = ldap.make_hostgroup_dn(cn) + + # Get our configuration + #config = ldap.get_ipa_config() + + # some required objectclasses + # FIXME: get this out of config + kw['objectClass'] = ['groupofnames'] + + return ldap.create(**kw) + + def output_for_cli(self, ret): + """ + Output result of this command to command line interface. + """ + if ret: + print "Group added" + +api.register(hostgroup_add) + + +class hostgroup_del(crud.Del): + 'Delete an existing group of hosts.' + def execute(self, cn, **kw): + """ + Delete a group of hosts + + The memberOf plugin handles removing the group from any other + groups. + + :param cn: The name of the group being removed + :param kw: Unused + """ + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("cn", cn, hostgroup_filter) + + return ldap.delete(dn) + + def output_for_cli(self, ret): + """ + Output result of this command to command line interface. + """ + if ret: + print "Group deleted" + +api.register(hostgroup_del) + + +class hostgroup_mod(crud.Mod): + 'Edit an existing group of hosts.' + def execute(self, cn, **kw): + """ + Execute the hostgroup-mod operation. + + The dn should not be passed as a keyword argument as it is constructed + by this method. + + Returns the entry + + :param cn: The name of the group to update. + :param kw: Keyword arguments for the other LDAP attributes. + """ + assert 'cn' not in kw + assert 'dn' not in kw + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("cn", cn, hostgroup_filter) + return ldap.update(dn, **kw) + + def output_for_cli(self, ret): + """ + Output result of this command to command line interface. + """ + if ret: + print "Group updated" + +api.register(hostgroup_mod) + + +class hostgroup_find(crud.Find): + 'Search the groups of hosts.' + def execute(self, term, **kw): + ldap = self.api.Backend.ldap + + # Pull the list of searchable attributes out of the configuration. + config = ldap.get_ipa_config() + + # FIXME: for now use same search fields as user groups + search_fields_conf_str = config.get('ipagroupsearchfields') + search_fields = search_fields_conf_str.split(",") + + for s in search_fields: + kw[s] = term + + kw['objectclass'] = hostgroup_filter + return ldap.search(**kw) + + def output_for_cli(self, groups): + if not groups: + return + + counter = groups[0] + groups = groups[1:] + if counter == 0: + print "No entries found" + return + elif counter == -1: + print "These results are truncated." + print "Please refine your search and try again." + + for g in groups: + for a in g.keys(): + print "%s: %s" % (a, g[a]) + +api.register(hostgroup_find) + + +class hostgroup_show(crud.Get): + 'Examine an existing group of hosts.' + def execute(self, cn, **kw): + """ + Execute the hostgroup-show operation. + + The dn should not be passed as a keyword argument as it is constructed + by this method. + + Returns the entry + + :param cn: The group name to retrieve. + :param kw: Not used. + """ + ldap = self.api.Backend.ldap + # FIXME: this works for now but the plan is to add a new objectclass + # type. + dn = ldap.find_entry_dn("cn", cn, hostgroup_filter) + # FIXME: should kw contain the list of attributes to display? + return ldap.retrieve(dn) + + def output_for_cli(self, group): + if not group: + return + + for a in group.keys(): + print "%s: %s" % (a, group[a]) + +api.register(hostgroup_show) + + +class hostgroup_add_member(frontend.Command): + 'Add a member to a group.' + takes_args = ( + Param('group', primary_key=True), + ) + takes_options = ( + Param('groups?', doc='comma-separated list of groups to add'), + ) + def execute(self, cn, **kw): + """ + Execute the hostgroup-add-member operation. + + Returns the updated group entry + + :param cn: The group name to add new members to. + :param kw: groups is a comma-separated list of groups to add + """ + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("cn", cn, hostgroup_filter) + add_failed = [] + to_add = [] + completed = 0 + + members = kw.get('groups', '').split(',') + for m in members: + if not m: continue + try: + member_dn = ldap.find_entry_dn("cn", m, hostgroup_filter) + to_add.append(member_dn) + except errors.NotFound: + add_failed.append(m) + continue + + for member_dn in to_add: + try: + ldap.add_member_to_group(member_dn, dn) + completed+=1 + except: + add_failed.append(member_dn) + + return add_failed + + def output_for_cli(self, add_failed): + """ + Output result of this command to command line interface. + """ + if add_failed: + print "These entries failed to add to the group:" + for a in add_failed: + print "\t'%s'" % a + else: + print "Group membership updated." + +api.register(hostgroup_add_member) + + +class hostgroup_remove_member(frontend.Command): + 'Remove a member from a group.' + takes_args = ( + Param('group', primary_key=True), + ) + takes_options = ( + Param('groups?', doc='comma-separated list of groups to remove'), + ) + def execute(self, cn, **kw): + """ + Execute the group-remove-member operation. + + Returns the members that could not be added + + :param cn: The group name to add new members to. + :param kw: groups is a comma-separated list of groups to remove + """ + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("cn", cn, hostgroup_filter) + to_remove = [] + remove_failed = [] + completed = 0 + + members = kw.get('groups', '').split(',') + for m in members: + if not m: continue + try: + member_dn = ldap.find_entry_dn("cn", m, hostgroup_filter) + to_remove.append(member_dn) + except errors.NotFound: + remove_failed.append(m) + continue + + for member_dn in to_remove: + try: + ldap.remove_member_from_group(member_dn, dn) + completed+=1 + except: + remove_failed.append(member_dn) + + return remove_failed + + def output_for_cli(self, remove_failed): + """ + Output result of this command to command line interface. + """ + if remove_failed: + print "These entries failed to be removed from the group:" + for a in remove_failed: + print "\t'%s'" % a + else: + print "Group membership updated." + +api.register(hostgroup_remove_member) -- cgit From 10026284dbf8f1b8a6eedf3b1c6ce05da568b4fa Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 27 Oct 2008 14:48:02 -0600 Subject: Started cleanup work on CLI class, added unit tests for CLI.parse_globals() --- ipalib/cli.py | 117 ++++++++++++++++++++++++++++++++++------------------------ 1 file changed, 69 insertions(+), 48 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index a802f8ef..21b02299 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -84,6 +84,24 @@ class help(frontend.Application): print 'Purpose: %s' % cmd.doc self.application.build_parser(cmd).print_help() + def print_commands(self): + std = set(self.api.Command) - set(self.api.Application) + print '\nStandard IPA commands:' + for key in sorted(std): + cmd = self.api.Command[key] + self.print_cmd(cmd) + print '\nSpecial CLI commands:' + for cmd in self.api.Application(): + self.print_cmd(cmd) + print '\nUse the --help option to see all the global options' + print '' + + def print_cmd(self, cmd): + print ' %s %s' % ( + to_cli(cmd.name).ljust(self.mcl), + cmd.doc, + ) + class console(frontend.Application): 'Start the IPA interactive Python console.' @@ -207,32 +225,24 @@ class CLI(object): __d = None __mcl = None - def __init__(self, api): - self.__api = api - self.__all_interactive = False - self.__not_interactive = False + def __init__(self, api, argv): + self.api = api + self.argv = tuple(argv) + self.__done = set() - def __get_api(self): - return self.__api - api = property(__get_api) + def __doing(self, name): + if name in self.__done: + raise StandardError( + '%s.%s() already called' % (self.__class__.__name__, name) + ) + self.__done.add(name) - def print_commands(self): - std = set(self.api.Command) - set(self.api.Application) - print '\nStandard IPA commands:' - for key in sorted(std): - cmd = self.api.Command[key] - self.print_cmd(cmd) - print '\nSpecial CLI commands:' - for cmd in self.api.Application(): - self.print_cmd(cmd) - print '\nUse the --help option to see all the global options' - print '' + def __do_if_not_done(self, name): + if name not in self.__done: + getattr(self, name)() - def print_cmd(self, cmd): - print ' %s %s' % ( - to_cli(cmd.name).ljust(self.mcl), - cmd.doc, - ) + def isdone(self, name): + return name in self.__done def __contains__(self, key): assert self.__d is not None, 'you must call finalize() first' @@ -360,10 +370,11 @@ class CLI(object): parser.add_option(o) return parser - def parse_globals(self, argv=sys.argv[1:]): + def parse_globals(self): + self.__doing('parse_globals') parser = optparse.OptionParser() parser.disable_interspersed_args() - parser.add_option('-a', dest='interactive', action='store_true', + parser.add_option('-a', dest='prompt_all', action='store_true', help='Prompt for all missing options interactively') parser.add_option('-n', dest='interactive', action='store_false', help='Don\'t prompt for any options interactively') @@ -373,29 +384,39 @@ class CLI(object): help='Specify or override environment variables') parser.add_option('-v', dest='verbose', action='store_true', help='Verbose output') - (options, args) = parser.parse_args(argv) - - if options.interactive == True: - self.__all_interactive = True - elif options.interactive == False: - self.__not_interactive = True - if options.verbose != None: - self.api.env.verbose = True - if options.environment: - env_dict = {} - for a in options.environment.split(','): - a = a.split('=', 1) - if len(a) < 2: - parser.error('badly specified environment string,'\ - 'use var1=val1[,var2=val2]..') - env_dict[a[0].strip()] = a[1].strip() - self.api.env.update(env_dict, True) - if options.config_file: - self.api.env.update(read_config(options.config_file), True) - else: - self.api.env.update(read_config(), True) - - return args + parser.set_defaults( + prompt_all=False, + interactive=True, + verbose=False, + ) + (options, args) = parser.parse_args(list(self.argv)) + self.options = options + self.cmd_argv = tuple(args) + + def bootstrap(self): + pass + +# if options.interactive == True: +# self.__all_interactive = True +# elif options.interactive == False: +# self.__not_interactive = True +# if options.verbose != None: +# self.api.env.verbose = True +# if options.environment: +# env_dict = {} +# for a in options.environment.split(','): +# a = a.split('=', 1) +# if len(a) < 2: +# parser.error('badly specified environment string,'\ +# 'use var1=val1[,var2=val2]..') +# env_dict[a[0].strip()] = a[1].strip() +# self.api.env.update(env_dict, True) +# if options.config_file: +# self.api.env.update(read_config(options.config_file), True) +# else: +# self.api.env.update(read_config(), True) + +# return args def get_usage(self, cmd): return ' '.join(self.get_usage_iter(cmd)) -- cgit From 17fd9cc4315f171a8d9e9d189936eea8ba2af0c0 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 27 Oct 2008 14:49:34 -0600 Subject: Started cleanup work on CLI class, added unit tests for CLI.parse_globals() --- ipalib/cli.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index 21b02299..eb8df591 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -394,7 +394,8 @@ class CLI(object): self.cmd_argv = tuple(args) def bootstrap(self): - pass + self.__doing('bootstrap') + self.parse_globals() # if options.interactive == True: # self.__all_interactive = True -- cgit From e6254026fe73c423d357a2fa1489de35475da46c Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 27 Oct 2008 15:19:49 -0600 Subject: Implemented basic CLI.bootstrap(); added corresponding unit tests --- ipalib/cli.py | 35 +++++++++++++---------------------- ipalib/plugable.py | 3 ++- 2 files changed, 15 insertions(+), 23 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index eb8df591..e15e2ff0 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -396,28 +396,19 @@ class CLI(object): def bootstrap(self): self.__doing('bootstrap') self.parse_globals() - -# if options.interactive == True: -# self.__all_interactive = True -# elif options.interactive == False: -# self.__not_interactive = True -# if options.verbose != None: -# self.api.env.verbose = True -# if options.environment: -# env_dict = {} -# for a in options.environment.split(','): -# a = a.split('=', 1) -# if len(a) < 2: -# parser.error('badly specified environment string,'\ -# 'use var1=val1[,var2=val2]..') -# env_dict[a[0].strip()] = a[1].strip() -# self.api.env.update(env_dict, True) -# if options.config_file: -# self.api.env.update(read_config(options.config_file), True) -# else: -# self.api.env.update(read_config(), True) - -# return args + self.api.env.verbose = self.options.verbose + if self.options.config_file: + self.api.env.conf = self.options.config_file + overrides = {} + if self.options.environment: + for a in self.options.environment.split(','): + a = a.split('=', 1) + if len(a) < 2: + parser.error('badly specified environment string,'\ + 'use var1=val1[,var2=val2]..') + overrides[a[0].strip()] = a[1].strip() + overrides['context'] = 'cli' + self.api.bootstrap(**overrides) def get_usage(self, cmd): return ' '.join(self.get_usage_iter(cmd)) diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 59484989..f704077a 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -714,7 +714,7 @@ class API(DictProxy): self.__d = dict() self.__done = set() self.register = Registrar(*allowed) - self.env = Env + self.env = Env() super(API, self).__init__(self.__d) def __doing(self, name): @@ -736,6 +736,7 @@ class API(DictProxy): Initialize environment variables needed by built-in plugins. """ self.__doing('bootstrap') + self.env._bootstrap(**overrides) def load_plugins(self, dry_run=False): """ -- cgit From bb9691099b7b025fc491279314d8803f4fa3b571 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 27 Oct 2008 15:36:41 -0600 Subject: API.bootstrap() now calls Env._finalize_core(); updated unit tests --- ipalib/plugable.py | 1 + 1 file changed, 1 insertion(+) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index f704077a..6fe22429 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -737,6 +737,7 @@ class API(DictProxy): """ self.__doing('bootstrap') self.env._bootstrap(**overrides) + self.env._finalize_core(**dict(constants.DEFAULT_CONFIG)) def load_plugins(self, dry_run=False): """ -- cgit From 9b1e3f59465c6ba33f4266bc3add469b5e1711eb Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 27 Oct 2008 19:21:49 -0600 Subject: More docstrings, functionality, and unit tests for improved CLI class --- ipalib/cli.py | 246 +++++++++++++++++++++++++++++++++++----------------- ipalib/constants.py | 1 - 2 files changed, 166 insertions(+), 81 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index e15e2ff0..7141ae4b 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -84,23 +84,7 @@ class help(frontend.Application): print 'Purpose: %s' % cmd.doc self.application.build_parser(cmd).print_help() - def print_commands(self): - std = set(self.api.Command) - set(self.api.Application) - print '\nStandard IPA commands:' - for key in sorted(std): - cmd = self.api.Command[key] - self.print_cmd(cmd) - print '\nSpecial CLI commands:' - for cmd in self.api.Application(): - self.print_cmd(cmd) - print '\nUse the --help option to see all the global options' - print '' - def print_cmd(self, cmd): - print ' %s %s' % ( - to_cli(cmd.name).ljust(self.mcl), - cmd.doc, - ) class console(frontend.Application): @@ -222,6 +206,10 @@ class KWCollector(object): class CLI(object): + """ + All logic for dispatching over command line interface. + """ + __d = None __mcl = None @@ -230,6 +218,144 @@ class CLI(object): self.argv = tuple(argv) self.__done = set() + def run(self): + """ + Run a command (or attempt to at least). + + This method requires several initialization steps to be completed + first, all of which all automatically called with a single call to + `CLI.finalize()`. The initialization steps are broken into separate + methods simply to make it easy to write unit tests. + + The initialization involves these steps: + + 1. `CLI.parse_globals` parses the global options, which get stored + in ``CLI.options``, and stores the remaining args in + ``CLI.cmd_argv``. + + 2. `CLI.bootstrap` initializes the environment information in + ``CLI.api.env``. + + 3. `CLI.load_plugins` registers all plugins, including the + CLI-specific plugins. + + 4. `CLI.finalize` instantiates all plugins and performs the + remaining initialization needed to use the `plugable.API` + instance. + """ + self.__doing('run') + self.finalize() + return + if len(self.cmd_argv) < 1: + self.print_commands() + print 'Usage: ipa [global-options] COMMAND' + sys.exit(2) + key = self.cmd_argv[0] + if key not in self: + self.print_commands() + print 'ipa: ERROR: unknown command %r' % key + sys.exit(2) + return self.run_cmd( + self[key], + list(s.decode('utf-8') for s in args[1:]) + ) + + def finalize(self): + """ + Fully initialize ``CLI.api`` `plugable.API` instance. + + This method first calls `CLI.load_plugins` to perform some dependant + initialization steps, after which `plugable.API.finalize` is called. + + Finally, the CLI-specific commands are passed a reference to this + `CLI` instance by calling `frontend.Application.set_application`. + """ + self.__doing('finalize') + self.load_plugins() + self.api.finalize() + for a in self.api.Application(): + a.set_application(self) + assert self.__d is None + self.__d = dict( + (c.name.replace('_', '-'), c) for c in self.api.Command() + ) + + def load_plugins(self): + """ + Load all standard plugins plus the CLI-specific plugins. + + This method first calls `CLI.bootstrap` to preform some dependant + initialization steps, after which `plugable.API.load_plugins` is + called. + + Finally, all the CLI-specific plugins are registered. + """ + self.__doing('load_plugins') + self.bootstrap() + self.api.load_plugins() + for klass in cli_application_commands: + self.api.register(klass) + + def bootstrap(self): + """ + Initialize the ``CLI.api.env`` environment variables. + + This method first calls `CLI.parse_globals` to perform some dependant + initialization steps. Then, using environment variables that may have + been passed in the global options, the ``overrides`` are constructed + and `plugable.API.bootstrap` is called. + """ + self.__doing('bootstrap') + self.parse_globals() + self.api.env.verbose = self.options.verbose + if self.options.config_file: + self.api.env.conf = self.options.config_file + overrides = {} + if self.options.environment: + for a in self.options.environment.split(','): + a = a.split('=', 1) + if len(a) < 2: + parser.error('badly specified environment string,'\ + 'use var1=val1[,var2=val2]..') + overrides[a[0].strip()] = a[1].strip() + overrides['context'] = 'cli' + self.api.bootstrap(**overrides) + + def parse_globals(self): + """ + Parse out the global options. + + This method parses the global options out of the ``CLI.argv`` instance + attribute, after which two new instance attributes are available: + + 1. ``CLI.options`` - an ``optparse.Values`` instance containing + the global options. + + 2. ``CLI.cmd_argv`` - a tuple containing the remainder of + ``CLI.argv`` after the global options have been consumed. + """ + self.__doing('parse_globals') + parser = optparse.OptionParser() + parser.disable_interspersed_args() + parser.add_option('-a', dest='prompt_all', action='store_true', + help='Prompt for all missing options interactively') + parser.add_option('-n', dest='interactive', action='store_false', + help='Don\'t prompt for any options interactively') + parser.add_option('-c', dest='config_file', + help='Specify different configuration file') + parser.add_option('-e', dest='environment', + help='Specify or override environment variables') + parser.add_option('-v', dest='verbose', action='store_true', + help='Verbose output') + parser.set_defaults( + prompt_all=False, + interactive=True, + verbose=False, + ) + (options, args) = parser.parse_args(list(self.argv)) + self.options = options + self.cmd_argv = tuple(args) + def __doing(self, name): if name in self.__done: raise StandardError( @@ -237,11 +363,28 @@ class CLI(object): ) self.__done.add(name) - def __do_if_not_done(self, name): - if name not in self.__done: - getattr(self, name)() + def print_commands(self): + std = set(self.api.Command) - set(self.api.Application) + print '\nStandard IPA commands:' + for key in sorted(std): + cmd = self.api.Command[key] + self.print_cmd(cmd) + print '\nSpecial CLI commands:' + for cmd in self.api.Application(): + self.print_cmd(cmd) + print '\nUse the --help option to see all the global options' + print '' + + def print_cmd(self, cmd): + print ' %s %s' % ( + to_cli(cmd.name).ljust(self.mcl), + cmd.doc, + ) def isdone(self, name): + """ + Return True in method named ``name`` has already been called. + """ return name in self.__done def __contains__(self, key): @@ -252,7 +395,7 @@ class CLI(object): assert self.__d is not None, 'you must call finalize() first' return self.__d[key] - def finalize(self): + def old_finalize(self): api = self.api for klass in cli_application_commands: api.register(klass) @@ -261,29 +404,8 @@ class CLI(object): a.set_application(self) self.build_map() - def build_map(self): - assert self.__d is None - self.__d = dict( - (c.name.replace('_', '-'), c) for c in self.api.Command() - ) - def run(self): - self.finalize() - set_default_env(self.api.env) - args = self.parse_globals() - if len(args) < 1: - self.print_commands() - print 'Usage: ipa [global-options] COMMAND' - sys.exit(2) - key = args[0] - if key not in self: - self.print_commands() - print 'ipa: ERROR: unknown command %r' % key - sys.exit(2) - return self.run_cmd( - self[key], - list(s.decode('utf-8') for s in args[1:]) - ) + def run_cmd(self, cmd, argv): kw = self.parse(cmd, argv) @@ -370,45 +492,9 @@ class CLI(object): parser.add_option(o) return parser - def parse_globals(self): - self.__doing('parse_globals') - parser = optparse.OptionParser() - parser.disable_interspersed_args() - parser.add_option('-a', dest='prompt_all', action='store_true', - help='Prompt for all missing options interactively') - parser.add_option('-n', dest='interactive', action='store_false', - help='Don\'t prompt for any options interactively') - parser.add_option('-c', dest='config_file', - help='Specify different configuration file') - parser.add_option('-e', dest='environment', - help='Specify or override environment variables') - parser.add_option('-v', dest='verbose', action='store_true', - help='Verbose output') - parser.set_defaults( - prompt_all=False, - interactive=True, - verbose=False, - ) - (options, args) = parser.parse_args(list(self.argv)) - self.options = options - self.cmd_argv = tuple(args) - def bootstrap(self): - self.__doing('bootstrap') - self.parse_globals() - self.api.env.verbose = self.options.verbose - if self.options.config_file: - self.api.env.conf = self.options.config_file - overrides = {} - if self.options.environment: - for a in self.options.environment.split(','): - a = a.split('=', 1) - if len(a) < 2: - parser.error('badly specified environment string,'\ - 'use var1=val1[,var2=val2]..') - overrides[a[0].strip()] = a[1].strip() - overrides['context'] = 'cli' - self.api.bootstrap(**overrides) + + def get_usage(self, cmd): return ' '.join(self.get_usage_iter(cmd)) diff --git a/ipalib/constants.py b/ipalib/constants.py index 4942cc9b..9da93e00 100644 --- a/ipalib/constants.py +++ b/ipalib/constants.py @@ -54,7 +54,6 @@ DEFAULT_CONFIG = ( ('verbose', False), ('debug', False), - # ******************************************************** # The remaining keys are never set from the values here! # ******************************************************** -- cgit From 6e456cc7494bc00e905361f3e6d42dff99089c6b Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 27 Oct 2008 23:30:55 -0600 Subject: More CLI cleanup, got all basics working again --- ipalib/cli.py | 140 ++++++++++++++++++++++++++-------------------------- ipalib/constants.py | 1 + 2 files changed, 71 insertions(+), 70 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index 7141ae4b..671d4053 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -218,9 +218,9 @@ class CLI(object): self.argv = tuple(argv) self.__done = set() - def run(self): + def run(self, init_only=False): """ - Run a command (or attempt to at least). + Parse ``argv`` and potentially run a command. This method requires several initialization steps to be completed first, all of which all automatically called with a single call to @@ -245,7 +245,8 @@ class CLI(object): """ self.__doing('run') self.finalize() - return + if self.api.env.mode == 'unit-test': + return if len(self.cmd_argv) < 1: self.print_commands() print 'Usage: ipa [global-options] COMMAND' @@ -255,10 +256,7 @@ class CLI(object): self.print_commands() print 'ipa: ERROR: unknown command %r' % key sys.exit(2) - return self.run_cmd( - self[key], - list(s.decode('utf-8') for s in args[1:]) - ) + return self.run_cmd(self[key]) def finalize(self): """ @@ -381,51 +379,38 @@ class CLI(object): cmd.doc, ) - def isdone(self, name): - """ - Return True in method named ``name`` has already been called. - """ - return name in self.__done - - def __contains__(self, key): - assert self.__d is not None, 'you must call finalize() first' - return key in self.__d - - def __getitem__(self, key): - assert self.__d is not None, 'you must call finalize() first' - return self.__d[key] - - def old_finalize(self): - api = self.api - for klass in cli_application_commands: - api.register(klass) - api.finalize() - for a in api.Application(): - a.set_application(self) - self.build_map() - - - - - def run_cmd(self, cmd, argv): - kw = self.parse(cmd, argv) + def run_cmd(self, cmd): + kw = self.parse(cmd) + # If options.interactive, interactively validate params: + if self.options.interactive: + try: + kw = self.prompt_interactively(cmd, kw) + except KeyboardInterrupt: + return 0 + # Now run the command try: - self.run_interactive(cmd, kw) - except KeyboardInterrupt: + ret = cmd(**kw) + if callable(cmd.output_for_cli): + cmd.output_for_cli(ret) return 0 - except errors.RuleError, e: + except StandardError, e: print e return 2 - return 0 - def run_interactive(self, cmd, kw): + def prompt_interactively(self, cmd, kw): + """ + Interactively prompt for missing or invalid values. + + By default this method will only prompt for *required* Param that + have a missing or invalid value. However, if + ``CLI.options.prompt_all`` is True, this method will prompt for any + params that have a missing or required values, even if the param is + optional. + """ for param in cmd.params(): if param.name not in kw: - if not param.required: - if not self.__all_interactive: - continue - elif self.__not_interactive: - exit_error('Not enough arguments given') + if not (param.required or self.options.prompt_all): + continue default = param.get_default(**kw) if default is None: prompt = '%s: ' % param.cli_name @@ -443,29 +428,34 @@ class CLI(object): break except errors.ValidationError, e: error = e.error - if self.api.env.server_context: - try: - import krbV - import ldap - from ipa_server import conn - from ipa_server.servercore import context - krbccache = krbV.default_context().default_ccache().name - context.conn = conn.IPAConn(self.api.env.ldaphost, self.api.env.ldapport, krbccache) - except ImportError: - print >> sys.stderr, "There was a problem importing a Python module: %s" % sys.exc_value - return 2 - except ldap.LDAPError, e: - print >> sys.stderr, "There was a problem connecting to the LDAP server: %s" % e[0].get('desc') - return 2 - ret = cmd(**kw) - if callable(cmd.output_for_cli): - return cmd.output_for_cli(ret) - else: - return 0 + return kw - def parse(self, cmd, argv): +# FIXME: This should be done as the plugins are loaded +# if self.api.env.server_context: +# try: +# import krbV +# import ldap +# from ipa_server import conn +# from ipa_server.servercore import context +# krbccache = krbV.default_context().default_ccache().name +# context.conn = conn.IPAConn(self.api.env.ldaphost, self.api.env.ldapport, krbccache) +# except ImportError: +# print >> sys.stderr, "There was a problem importing a Python module: %s" % sys.exc_value +# return 2 +# except ldap.LDAPError, e: +# print >> sys.stderr, "There was a problem connecting to the LDAP server: %s" % e[0].get('desc') +# return 2 +# ret = cmd(**kw) +# if callable(cmd.output_for_cli): +# return cmd.output_for_cli(ret) +# else: +# return 0 + + def parse(self, cmd): parser = self.build_parser(cmd) - (kwc, args) = parser.parse_args(argv, KWCollector()) + (kwc, args) = parser.parse_args( + list(self.cmd_argv), KWCollector() + ) kw = kwc.__todict__() try: arg_kw = cmd.args_to_kw(*args) @@ -492,10 +482,6 @@ class CLI(object): parser.add_option(o) return parser - - - - def get_usage(self, cmd): return ' '.join(self.get_usage_iter(cmd)) @@ -520,3 +506,17 @@ class CLI(object): self.__mcl = max(len(k) for k in self.__d) return self.__mcl mcl = property(__get_mcl) + + def isdone(self, name): + """ + Return True in method named ``name`` has already been called. + """ + return name in self.__done + + def __contains__(self, key): + assert self.__d is not None, 'you must call finalize() first' + return key in self.__d + + def __getitem__(self, key): + assert self.__d is not None, 'you must call finalize() first' + return self.__d[key] diff --git a/ipalib/constants.py b/ipalib/constants.py index 9da93e00..25ee6c31 100644 --- a/ipalib/constants.py +++ b/ipalib/constants.py @@ -53,6 +53,7 @@ DEFAULT_CONFIG = ( # Debugging: ('verbose', False), ('debug', False), + ('mode', 'production'), # ******************************************************** # The remaining keys are never set from the values here! -- cgit From 83d6c95e4636049a5bcedb533ad49f6e2cf79dfe Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 27 Oct 2008 23:39:43 -0600 Subject: API.load_plugins() no longer takes dry_run=False kwarg and instead checks in env.mode == 'unit_test' to decide whether to load the plugins; it also only loads ipa_server.plugins in env.in_server is True --- ipalib/cli.py | 2 +- ipalib/plugable.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index 671d4053..3613dfeb 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -245,7 +245,7 @@ class CLI(object): """ self.__doing('run') self.finalize() - if self.api.env.mode == 'unit-test': + if self.api.env.mode == 'unit_test': return if len(self.cmd_argv) < 1: self.print_commands() diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 6fe22429..8d689f7d 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -739,7 +739,7 @@ class API(DictProxy): self.env._bootstrap(**overrides) self.env._finalize_core(**dict(constants.DEFAULT_CONFIG)) - def load_plugins(self, dry_run=False): + def load_plugins(self): """ Load plugins from all standard locations. @@ -748,10 +748,11 @@ class API(DictProxy): """ self.__doing('load_plugins') self.__do_if_not_done('bootstrap') - if dry_run: + if self.env.mode == 'unit_test': return util.import_plugins_subpackage('ipalib') - util.import_plugins_subpackage('ipa_server') + if self.env.in_server: + util.import_plugins_subpackage('ipa_server') def finalize(self): """ -- cgit From 2307d4ddd0409f00511c4d83ad7dab5e9d6d96df Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 27 Oct 2008 23:56:22 -0600 Subject: Fixed use of depreciated env.get() in b_xmlrpc.py module --- ipalib/plugins/b_xmlrpc.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugins/b_xmlrpc.py b/ipalib/plugins/b_xmlrpc.py index 572a7511..2c98fb8a 100644 --- a/ipalib/plugins/b_xmlrpc.py +++ b/ipalib/plugins/b_xmlrpc.py @@ -36,23 +36,26 @@ from ipalib import errors class xmlrpc(Backend): """ - Kerberos backend plugin. + XML-RPC client backend plugin. """ - def get_client(self, verbose=False): - # FIXME: The server uri should come from self.api.env.server_uri - if api.env.get('kerberos'): - server = api.env.server.next() - if verbose: print "Connecting to %s" % server - return xmlrpclib.ServerProxy('https://%s/ipa/xml' % server, transport=KerbTransport(), verbose=verbose) - else: - return xmlrpclib.ServerProxy('http://localhost:8888', verbose=verbose) + def get_client(self): + """ + Return an xmlrpclib.ServerProxy instance (the client). + """ + uri = self.api.env.xmlrpc_uri + if uri.startswith('https://'): + return xmlrpclib.ServerProxy(uri, + transport=KerbTransport(), + verbose=self.api.env.verbose, + ) + return xmlrpclib.ServerProxy(uri, verbose=self.api.env.verbose) def forward_call(self, name, *args, **kw): """ Forward a call over XML-RPC to an IPA server. """ - client = self.get_client(verbose=api.env.get('verbose', False)) + client = self.get_client() command = getattr(client, name) params = xmlrpc_marshal(*args, **kw) try: -- cgit From 316bd855d5720f4babfb79d20c391de3f8958a60 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 28 Oct 2008 01:39:02 -0600 Subject: Added util.configure_logging() function; API.bootstrap() now calls util.configure_logging() --- ipalib/cli.py | 2 +- ipalib/constants.py | 16 ++++++++++++++++ ipalib/plugable.py | 7 +++++++ ipalib/util.py | 36 +++++++++++++++++++++++++++++++++++- 4 files changed, 59 insertions(+), 2 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index 3613dfeb..6407e9e2 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -454,7 +454,7 @@ class CLI(object): def parse(self, cmd): parser = self.build_parser(cmd) (kwc, args) = parser.parse_args( - list(self.cmd_argv), KWCollector() + list(self.cmd_argv[1:]), KWCollector() ) kw = kwc.__todict__() try: diff --git a/ipalib/constants.py b/ipalib/constants.py index 25ee6c31..7220561f 100644 --- a/ipalib/constants.py +++ b/ipalib/constants.py @@ -87,3 +87,19 @@ DEFAULT_CONFIG = ( ('log', None), # Path to log file ) + + +LOGGING_CONSOLE_FORMAT = ' '.join([ + '%(levelname)s', + '%(message)s', +]) + + +# Tab-delimited format designed to be easily opened in a spreadsheet: +LOGGING_FILE_FORMAT = ' '.join([ + '%(created)f', + '%(levelname)s', + '%(message)r', # Using %r for repr() so message is a single line + '%(pathname)s', + '%(lineno)d', +]) diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 8d689f7d..dd74dc08 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -738,6 +738,13 @@ class API(DictProxy): self.__doing('bootstrap') self.env._bootstrap(**overrides) self.env._finalize_core(**dict(constants.DEFAULT_CONFIG)) + if self.env.mode == 'unit_test': + return + logger = util.configure_logging( + self.env.log, + self.env.verbose, + ) + object.__setattr__(self, 'log', 'logger') def load_plugins(self): """ diff --git a/ipalib/util.py b/ipalib/util.py index d7e2c2a4..e65f15ca 100644 --- a/ipalib/util.py +++ b/ipalib/util.py @@ -21,10 +21,13 @@ Various utility functions. """ -import krbV +import logging import os from os import path import imp +import krbV +from constants import LOGGING_CONSOLE_FORMAT, LOGGING_FILE_FORMAT + def xmlrpc_marshal(*args, **kw): """ @@ -99,3 +102,34 @@ def import_plugins_subpackage(name): for name in find_modules_in_dir(src_dir): full_name = '%s.%s' % (plugins.__name__, name) __import__(full_name) + + +def configure_logging(log_file, verbose): + """ + Configure standard logging. + """ + # Check that directory log_file is in exists: + log_dir = path.dirname(log_file) + if not path.isdir(log_dir): + os.makedirs(log_dir) + + # Set logging level: + level = logging.INFO + if verbose: + level -= 10 + + log = logging.getLogger('ipa') + + # Configure console handler + console = logging.StreamHandler() + console.setLevel(level) + console.setFormatter(logging.Formatter(LOGGING_CONSOLE_FORMAT)) + log.addHandler(console) + + # Configure file handler + file_handler = logging.FileHandler(log_file) + file_handler.setLevel(level) + file_handler.setFormatter(logging.Formatter(LOGGING_FILE_FORMAT)) + log.addHandler(file_handler) + + return log -- cgit From a9f1c74a7fb7619cfcdb9f5eaf0f62745b1b551e Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 28 Oct 2008 01:45:02 -0600 Subject: util.configure_logging() now only configures file logging if it can create the log_dir --- ipalib/util.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) (limited to 'ipalib') diff --git a/ipalib/util.py b/ipalib/util.py index e65f15ca..280910dc 100644 --- a/ipalib/util.py +++ b/ipalib/util.py @@ -108,11 +108,6 @@ def configure_logging(log_file, verbose): """ Configure standard logging. """ - # Check that directory log_file is in exists: - log_dir = path.dirname(log_file) - if not path.isdir(log_dir): - os.makedirs(log_dir) - # Set logging level: level = logging.INFO if verbose: @@ -127,6 +122,13 @@ def configure_logging(log_file, verbose): log.addHandler(console) # Configure file handler + log_dir = path.dirname(log_file) + if not path.isdir(log_dir): + try: + os.makedirs(log_dir) + except OSError: + log.warn('Could not create log_dir %r', log_dir) + return log file_handler = logging.FileHandler(log_file) file_handler.setLevel(level) file_handler.setFormatter(logging.Formatter(LOGGING_FILE_FORMAT)) -- cgit From fbcb55bd11d17dbff8ec3c7c99cf7b3bb91d3752 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 28 Oct 2008 02:10:56 -0600 Subject: lite-xmlrpc.py now uses api.bootstrap() property, logs to api.logger --- ipalib/constants.py | 3 ++- ipalib/plugable.py | 2 +- ipalib/util.py | 3 +-- 3 files changed, 4 insertions(+), 4 deletions(-) (limited to 'ipalib') diff --git a/ipalib/constants.py b/ipalib/constants.py index 7220561f..f4a440c6 100644 --- a/ipalib/constants.py +++ b/ipalib/constants.py @@ -89,7 +89,8 @@ DEFAULT_CONFIG = ( ) -LOGGING_CONSOLE_FORMAT = ' '.join([ +LOGGING_CONSOLE_FORMAT = ': '.join([ + '%(name)s', '%(levelname)s', '%(message)s', ]) diff --git a/ipalib/plugable.py b/ipalib/plugable.py index dd74dc08..b0ba32b7 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -744,7 +744,7 @@ class API(DictProxy): self.env.log, self.env.verbose, ) - object.__setattr__(self, 'log', 'logger') + object.__setattr__(self, 'logger', logger) def load_plugins(self): """ diff --git a/ipalib/util.py b/ipalib/util.py index 280910dc..d577524b 100644 --- a/ipalib/util.py +++ b/ipalib/util.py @@ -114,10 +114,10 @@ def configure_logging(log_file, verbose): level -= 10 log = logging.getLogger('ipa') + log.setLevel(level) # Configure console handler console = logging.StreamHandler() - console.setLevel(level) console.setFormatter(logging.Formatter(LOGGING_CONSOLE_FORMAT)) log.addHandler(console) @@ -130,7 +130,6 @@ def configure_logging(log_file, verbose): log.warn('Could not create log_dir %r', log_dir) return log file_handler = logging.FileHandler(log_file) - file_handler.setLevel(level) file_handler.setFormatter(logging.Formatter(LOGGING_FILE_FORMAT)) log.addHandler(file_handler) -- cgit From 138305b365307a30d151ff144d5a1e0d95723293 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 28 Oct 2008 02:23:13 -0600 Subject: Added an example CLI-specific env command --- ipalib/cli.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index 6407e9e2..021e01ad 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -97,6 +97,21 @@ class console(frontend.Application): ) +class env(frontend.Application): + """ + Show environment variables. + """ + + def run(self): + return tuple( + (key, self.api.env[key]) for key in self.api.env + ) + + def output_for_cli(self, ret): + for (key, value) in ret: + print '%s = %r' % (key, value) + + class show_api(text_ui): 'Show attributes on dynamic API object' @@ -183,6 +198,7 @@ cli_application_commands = ( console, show_api, plugins, + env, ) -- cgit From ddb5449c7faabbd4c1b71adfe84c386b943a163f Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 30 Oct 2008 01:11:33 -0600 Subject: Did some initial work for Context plugins --- ipalib/__init__.py | 27 +++++++++++++++------------ ipalib/backend.py | 14 ++++++++++++++ ipalib/cli.py | 2 +- ipalib/plugable.py | 21 +++++++++++++++++++++ 4 files changed, 51 insertions(+), 13 deletions(-) (limited to 'ipalib') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index 4593e581..a6664f73 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -30,15 +30,18 @@ To learn about the ``ipalib`` library, you should read the code in this order: """ import plugable -import frontend -import backend -import config - -api = plugable.API( - frontend.Command, - frontend.Object, - frontend.Method, - frontend.Property, - frontend.Application, - backend.Backend, -) +from backend import Backend, Context +from frontend import Command, Object, Method, Property, Application +from ipa_types import Bool, Int, Unicode, Enum +from frontend import Param, DefaultFrom + +def get_standard_api(unit_test=False): + api = plugable.API( + Command, Object, Method, Property, Application, + Backend, Context, + ) + if unit_test is True: + api.env.mode = 'unit_test' + return api + +api = get_standard_api() diff --git a/ipalib/backend.py b/ipalib/backend.py index 82ed14f3..b1e15f33 100644 --- a/ipalib/backend.py +++ b/ipalib/backend.py @@ -23,9 +23,23 @@ Base classes for all backed-end plugins. import plugable + class Backend(plugable.Plugin): """ Base class for all backend plugins. """ __proxy__ = False # Backend plugins are not wrapped in a PluginProxy + + +class Context(plugable.Plugin): + """ + Base class for plugable context components. + """ + + __proxy__ = False # Backend plugins are not wrapped in a PluginProxy + + def get_value(self): + raise NotImplementedError( + '%s.get_value()' % self.__class__.__name__ + ) diff --git a/ipalib/cli.py b/ipalib/cli.py index 021e01ad..39773d73 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -331,7 +331,7 @@ class CLI(object): if len(a) < 2: parser.error('badly specified environment string,'\ 'use var1=val1[,var2=val2]..') - overrides[a[0].strip()] = a[1].strip() + overrides[str(a[0].strip())] = a[1].strip() overrides['context'] = 'cli' self.api.bootstrap(**overrides) diff --git a/ipalib/plugable.py b/ipalib/plugable.py index b0ba32b7..9ddcb30f 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -27,6 +27,7 @@ http://docs.python.org/ref/sequence-types.html import re import inspect +import threading import errors from errors import check_type, check_isinstance from config import Environment, Env @@ -705,6 +706,25 @@ class Registrar(DictProxy): self.__registered.add(klass) +class LazyContext(object): + """ + On-demand creation of thread-local context attributes. + """ + + def __init__(self, api): + self.__api = api + self.__context = threading.local() + + def __getattr__(self, name): + if name not in self.__context.__dict__: + if name not in self.__api.Context: + raise AttributeError('no Context plugin for %r' % name) + value = self.__api.Context[name].get_value() + self.__context.__dict__[name] = value + return self.__context.__dict__[name] + + + class API(DictProxy): """ Dynamic API object through which `Plugin` instances are accessed. @@ -715,6 +735,7 @@ class API(DictProxy): self.__done = set() self.register = Registrar(*allowed) self.env = Env() + self.context = LazyContext(self) super(API, self).__init__(self.__d) def __doing(self, name): -- cgit From 2fee6a3e20169f12b837647f0f71d6f28937490f Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 30 Oct 2008 01:34:46 -0600 Subject: Added tests.util.get_api() function to create a standard (api, home) tuple for unit testing --- ipalib/__init__.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) (limited to 'ipalib') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index a6664f73..5cc4c121 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -35,13 +35,11 @@ from frontend import Command, Object, Method, Property, Application from ipa_types import Bool, Int, Unicode, Enum from frontend import Param, DefaultFrom -def get_standard_api(unit_test=False): - api = plugable.API( +def get_standard_api(): + return plugable.API( Command, Object, Method, Property, Application, Backend, Context, ) - if unit_test is True: - api.env.mode = 'unit_test' - return api + api = get_standard_api() -- cgit From 6879140db790a23a8782f7200400f2b58a69f6a0 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 30 Oct 2008 02:20:28 -0600 Subject: Added ipalib.plugins.f_misc with new 'context' Command; moved 'env' Command from cli to f_misc --- ipalib/cli.py | 18 ------------ ipalib/plugable.py | 3 ++ ipalib/plugins/f_misc.py | 73 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 18 deletions(-) create mode 100644 ipalib/plugins/f_misc.py (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index 39773d73..161ea1d8 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -97,22 +97,6 @@ class console(frontend.Application): ) -class env(frontend.Application): - """ - Show environment variables. - """ - - def run(self): - return tuple( - (key, self.api.env[key]) for key in self.api.env - ) - - def output_for_cli(self, ret): - for (key, value) in ret: - print '%s = %r' % (key, value) - - - class show_api(text_ui): 'Show attributes on dynamic API object' @@ -198,8 +182,6 @@ cli_application_commands = ( console, show_api, plugins, - env, - ) diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 9ddcb30f..2f86fa22 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -723,6 +723,9 @@ class LazyContext(object): self.__context.__dict__[name] = value return self.__context.__dict__[name] + def __getitem__(self, key): + return self.__getattr__(key) + class API(DictProxy): diff --git a/ipalib/plugins/f_misc.py b/ipalib/plugins/f_misc.py new file mode 100644 index 00000000..ff8569b1 --- /dev/null +++ b/ipalib/plugins/f_misc.py @@ -0,0 +1,73 @@ +# Authors: +# Jason Gerard DeRose +# +# 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 + +""" +Misc frontend plugins. +""" + +from ipalib import api, Command, Param, Bool + + +class env_and_context(Command): + """ + Base class for `env` and `context` commands. + """ + + def run(self, **kw): + if kw.get('server', False) and not self.api.env.in_server: + return self.forward() + return self.execute() + + def output_for_cli(self, ret): + for (key, value) in ret: + print '%s = %r' % (key, value) + + +class env(env_and_context): + """Show environment variables""" + + takes_options = ( + Param('server?', type=Bool(), default=False, + doc='Show environment variables of server', + ), + ) + + def execute(self): + return tuple( + (key, self.api.env[key]) for key in self.api.env + ) + +api.register(env) + + +class context(env_and_context): + """Show request context""" + + takes_options = ( + Param('server?', type=Bool(), default=False, + doc='Show request context in server', + ), + ) + + def execute(self): + return [ + (key, self.api.context[key]) for key in self.api.Context + ] + +api.register(context) -- cgit From 3076cb4d2fa1be023a1c72d70cbdf5024047ff2a Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 30 Oct 2008 14:11:24 -0600 Subject: Plugin.set_api() now sets convience instance attributes from api for env, context, log, and all NameSpace --- ipalib/plugable.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 2f86fa22..d10ff797 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -351,6 +351,15 @@ class Plugin(ReadOnly): assert self.__api is None, 'set_api() can only be called once' assert api is not None, 'set_api() argument cannot be None' self.__api = api + if not isinstance(api, API): + return + for name in api: + assert not hasattr(self, name) + setattr(self, name, api[name]) + for name in ('env', 'context', 'log'): + if hasattr(api, name): + assert not hasattr(self, name) + setattr(self, name, getattr(api, name)) def __repr__(self): """ @@ -768,7 +777,7 @@ class API(DictProxy): self.env.log, self.env.verbose, ) - object.__setattr__(self, 'logger', logger) + object.__setattr__(self, 'log', logger) def load_plugins(self): """ -- cgit From 62876ccee3ba679adda926b88564732552459619 Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Thu, 30 Oct 2008 17:25:45 -0400 Subject: Initial implementation of automount support Add argument handling to crud.Del Make get_list handle LDAP scope --- ipalib/crud.py | 7 + ipalib/plugins/f_automount.py | 472 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 479 insertions(+) create mode 100644 ipalib/plugins/f_automount.py (limited to 'ipalib') diff --git a/ipalib/crud.py b/ipalib/crud.py index 6194b3fa..60c605dd 100644 --- a/ipalib/crud.py +++ b/ipalib/crud.py @@ -46,6 +46,11 @@ class Del(frontend.Method): def get_args(self): yield self.obj.primary_key + def get_options(self): + for param in self.obj.params_minus_pk(): + yield param + for option in self.takes_options: + yield option class Mod(frontend.Method): def get_args(self): @@ -54,6 +59,8 @@ class Mod(frontend.Method): def get_options(self): for param in self.obj.params_minus_pk(): yield param.__clone__(required=False) + for option in self.takes_options: + yield option class Find(frontend.Method): diff --git a/ipalib/plugins/f_automount.py b/ipalib/plugins/f_automount.py new file mode 100644 index 00000000..78e96ccd --- /dev/null +++ b/ipalib/plugins/f_automount.py @@ -0,0 +1,472 @@ +# Authors: +# Rob Crittenden +# +# 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 + +""" +Frontend plugins for automount. + +RFC 2707bis http://www.padl.com/~lukeh/rfc2307bis.txt +""" + +from ipalib import frontend +from ipalib import crud +from ipalib.frontend import Param +from ipalib import api +from ipalib import errors +from ipalib import ipa_types +from ldap import explode_dn + +map_attributes = ['automountMapName', 'description', ] +key_attributes = ['description', 'automountKey', 'automountInformation'] + +def display_entry(entry): + # FIXME: for now delete dn here. In the future pass in the kw to + # output_for_cli() + attr = sorted(entry.keys()) + + for a in attr: + if a != 'dn': + print "%s: %s" % (a, entry[a]) + +def make_automount_dn(mapname): + """ + Construct automount dn from map name. + """ + # FIXME, should this be in b_ldap? + # Experimenting to see what a plugin looks like for a 3rd party who can't + # modify the backend. + import ldap + return 'automountmapname=%s,%s,%s' % ( + ldap.dn.escape_dn_chars(mapname), + api.env.container_accounts, + api.env.basedn, + ) + +class automount(frontend.Object): + """ + Automount object. + """ + takes_params = ( + Param('automountmapname', + cli_name='mapname', + primary_key=True, + doc='A group of related automount objects', + ), + ) +api.register(automount) + + +class automount_addmap(crud.Add): + 'Add a new automount map.' + takes_options = ( + Param('description?', + doc='A description of the automount map'), + ) + + def execute(self, mapname, **kw): + """ + Execute the automount-addmap operation. + + Returns the entry as it will be created in LDAP. + + :param mapname: The map name being added. + :param kw: Keyword arguments for the other LDAP attributes. + """ + assert 'automountmapname' not in kw + assert 'dn' not in kw + ldap = self.api.Backend.ldap + kw['automountmapname'] = mapname + kw['dn'] = make_automount_dn(mapname) + + kw['objectClass'] = ['automountMap'] + + return ldap.create(**kw) + def output_for_cli(self, ret): + """ + Output result of this command to command line interface. + """ + if ret: + print "Automount map added" + +api.register(automount_addmap) + + +class automount_addkey(crud.Add): + 'Add a new automount key.' + takes_options = ( + Param('automountkey', + cli_name='key', + doc='An entry in an automount map'), + Param('automountinformation', + cli_name='info', + doc='Mount information for this key'), + Param('description?', + doc='A description of the mount'), + ) + + def execute(self, mapname, **kw): + """ + Execute the automount-addkey operation. + + Returns the entry as it will be created in LDAP. + + :param mapname: The map name being added to. + :param kw: Keyword arguments for the other LDAP attributes. + """ + assert 'automountmapname' not in kw + assert 'dn' not in kw + ldap = self.api.Backend.ldap + # use find_entry_dn instead of make_automap_dn so we can confirm that + # the map exists + map_dn = ldap.find_entry_dn("automountmapname", mapname, "automountmap") + kw['dn'] = "automountkey=%s,%s" % (kw['automountkey'], map_dn) + + kw['objectClass'] = ['automount'] + + return ldap.create(**kw) + def output_for_cli(self, ret): + """ + Output result of this command to command line interface. + """ + if ret: + print "Automount key added" + +api.register(automount_addkey) + + +class automount_delmap(crud.Del): + 'Delete an automount map.' + def execute(self, mapname, **kw): + """Delete an automount map. This will also remove all of the keys + associated with this map. + + mapname is the automount map to remove + + :param mapname: The map to be removed + :param kw: Not used. + """ + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("automountmapname", mapname, "automountmap") + keys = api.Command['automount_getkeys'](mapname) + if keys: + for k in keys: + ldap.delete(k.get('dn')) + return ldap.delete(dn) + def output_for_cli(self, ret): + """ + Output result of this command to command line interface. + """ + if ret: + print "Automount map and associated keys deleted" + +api.register(automount_delmap) + + +class automount_delkey(crud.Del): + 'Delete an automount key.' + takes_options = ( + Param('key', + doc='The automount key to remove'), + ) + def execute(self, mapname, **kw): + """Delete an automount key. + + key is the automount key to remove + + :param mapname: The automount map containing the key to be removed + :param kw: "key" the key to be removed + """ + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("automountmapname", mapname, "automountmap") + keys = api.Command['automount_getkeys'](mapname) + keydn = None + keyname = kw.get('key').lower() + if keys: + for k in keys: + if k.get('automountkey').lower() == keyname: + keydn = k.get('dn') + break + if not keydn: + raise errors.NotFound + return ldap.delete(keydn) + def output_for_cli(self, ret): + """ + Output result of this command to command line interface. + """ + if ret: + print "Automount key deleted" + +api.register(automount_delkey) + +class automount_modmap(crud.Mod): + 'Edit an existing automount map.' + takes_options = ( + Param('description?', + doc='A description of the automount map'), + ) + def execute(self, mapname, **kw): + """ + Execute the automount-modmap operation. + + The dn should not be passed as a keyword argument as it is constructed + by this method. + + Returns the entry + + :param mapname: The map name to update. + :param kw: Keyword arguments for the other LDAP attributes. + """ + assert 'automountmapname' not in kw + assert 'dn' not in kw + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("automountmapname", mapname, "automountmap") + return ldap.update(dn, **kw) + + def output_for_cli(self, ret): + """ + Output result of this command to command line interface. + """ + if ret: + print "Automount map updated" + +api.register(automount_modmap) + + +class automount_modkey(crud.Mod): + 'Edit an existing automount key.' + takes_options = ( + Param('automountkey', + cli_name='key', + doc='An entry in an automount map'), + Param('automountinformation?', + cli_name='info', + doc='Mount information for this key'), + Param('description?', + doc='A description of the automount map'), + ) + def execute(self, mapname, **kw): + """ + Execute the automount-modkey operation. + + Returns the entry + + :param mapname: The map name to update. + :param kw: Keyword arguments for the other LDAP attributes. + """ + assert 'automountmapname' not in kw + assert 'dn' not in kw + keyname = kw.get('automountkey').lower() + del kw['automountkey'] + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("automountmapname", mapname, "automountmap") + keys = api.Command['automount_getkeys'](mapname) + keydn = None + if keys: + for k in keys: + if k.get('automountkey').lower() == keyname: + keydn = k.get('dn') + break + if not keydn: + raise errors.NotFound + return ldap.update(keydn, **kw) + + def output_for_cli(self, ret): + """ + Output result of this command to command line interface. + """ + if ret: + print "Automount key updated" +api.register(automount_modkey) + + +class automount_findmap(crud.Find): + 'Search automount maps.' + takes_options = ( + Param('all?', type=ipa_types.Bool(), doc='Retrieve all attributes'), + ) + def execute(self, term, **kw): + ldap = self.api.Backend.ldap + + search_fields = map_attributes + + for s in search_fields: + kw[s] = term + + kw['objectclass'] = 'automountMap' + if kw.get('all', False): + kw['attributes'] = ['*'] + else: + kw['attributes'] = map_attributes + return ldap.search(**kw) + def output_for_cli(self, entries): + if not entries: + return + counter = entries[0] + entries = entries[1:] + if counter == 0: + print "No entries found" + return + elif counter == -1: + print "These results are truncated." + print "Please refine your search and try again." + + for e in entries: + display_entry(e) + print "" +api.register(automount_findmap) + + +class automount_findkey(crud.Find): + 'Search automount keys.' + takes_options = ( + Param('all?', type=ipa_types.Bool(), doc='Retrieve all attributes'), + ) + def execute(self, term, **kw): + ldap = self.api.Backend.ldap + + search_fields = key_attributes + + for s in search_fields: + kw[s] = term + + kw['objectclass'] = 'automount' + if kw.get('all', False): + kw['attributes'] = ['*'] + else: + kw['attributes'] = key_attributes + return ldap.search(**kw) + def output_for_cli(self, entries): + if not entries: + return + counter = entries[0] + entries = entries[1:] + if counter == 0: + print "No entries found" + return + elif counter == -1: + print "These results are truncated." + print "Please refine your search and try again." + + for e in entries: + display_entry(e) + print "" +api.register(automount_findkey) + + +class automount_showmap(crud.Get): + 'Examine an existing automount map.' + takes_options = ( + Param('all?', type=ipa_types.Bool(), doc='Retrieve all attributes'), + ) + def execute(self, mapname, **kw): + """ + Execute the automount-showmap operation. + + Returns the entry + + :param mapname: The automount map to retrieve + :param kw: "all" set to True = return all attributes + """ + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("automountmapname", mapname, "automountmap") + # FIXME: should kw contain the list of attributes to display? + if kw.get('all', False): + return ldap.retrieve(dn) + else: + return ldap.retrieve(dn, map_attributes) + def output_for_cli(self, entry): + if entry: + display_entry(entry) + +api.register(automount_showmap) + + +class automount_showkey(crud.Get): + 'Examine an existing automount key.' + takes_options = ( + Param('key', + doc='The automount key to display'), + Param('all?', type=ipa_types.Bool(), doc='Retrieve all attributes'), + ) + def execute(self, mapname, **kw): + """ + Execute the automount-showkey operation. + + Returns the entry + + :param mapname: The mapname to examine + :param kw: "key" the key to retrieve + :param kw: "all" set to True = return all attributes + """ + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("automountmapname", mapname, "automountmap") + keys = api.Command['automount_getkeys'](mapname) + keyname = kw.get('key').lower() + keydn = None + if keys: + for k in keys: + if k.get('automountkey').lower() == keyname: + keydn = k.get('dn') + break + if not keydn: + raise errors.NotFound + # FIXME: should kw contain the list of attributes to display? + if kw.get('all', False): + return ldap.retrieve(keydn) + else: + return ldap.retrieve(keydn, key_attributes) + def output_for_cli(self, entry): + # The automount map name associated with this key is available only + # in the dn. Add it as an attribute to display instead. + if entry and not entry.get('automountmapname'): + elements = explode_dn(entry.get('dn').lower()) + for e in elements: + (attr, value) = e.split('=',1) + if attr == 'automountmapname': + entry['automountmapname'] = value + display_entry(entry) + +api.register(automount_showkey) + + +class automount_getkeys(frontend.Command): + 'Retrieve all keys for an automount map.' + takes_args = ( + Param('automountmapname', + cli_name='mapname', + primary_key=True, + doc='A group of related automount objects', + ), + ) + def execute(self, mapname, **kw): + """ + Execute the automount-getkeys operation. + + Return a list of all automount keys for this mapname + + :param mapname: Retrieve all keys for this mapname + """ + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("automountmapname", mapname, "automountmap") + return ldap.get_one_entry(dn, 'objectclass=*', ['automountkey']) + def output_for_cli(self, keys): + if keys: + for k in keys: + print k.get('automountkey') + +api.register(automount_getkeys) -- cgit From 140458cfc694a1b77100c81a58600365627e7758 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 31 Oct 2008 12:29:59 -0600 Subject: API.finalize() now cascades call to API.load_plugins() --- ipalib/plugable.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index d10ff797..de63e4ca 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -802,7 +802,7 @@ class API(DictProxy): already. """ self.__doing('finalize') - self.__do_if_not_done('bootstrap') + self.__do_if_not_done('load_plugins') class PluginInstance(object): """ -- cgit From cdfb7bfd5ebc1f5e44f4ee60cec14354040a0a72 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 31 Oct 2008 13:27:42 -0600 Subject: Logging is now configured in API.bootstrap(); removed depreciated util.configure_logging() function --- ipalib/plugable.py | 52 +++++++++++++++++++++++++++++++++++++++++++--------- ipalib/util.py | 33 --------------------------------- 2 files changed, 43 insertions(+), 42 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index de63e4ca..9e612d68 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -28,10 +28,13 @@ http://docs.python.org/ref/sequence-types.html import re import inspect import threading +import logging +import os +from os import path import errors from errors import check_type, check_isinstance from config import Environment, Env -import constants +from constants import LOGGING_FILE_FORMAT, LOGGING_CONSOLE_FORMAT, DEFAULT_CONFIG import util @@ -766,18 +769,49 @@ class API(DictProxy): def bootstrap(self, **overrides): """ - Initialize environment variables needed by built-in plugins. + Initialize environment variables and logging. """ self.__doing('bootstrap') self.env._bootstrap(**overrides) - self.env._finalize_core(**dict(constants.DEFAULT_CONFIG)) + self.env._finalize_core(**dict(DEFAULT_CONFIG)) + log = logging.getLogger('ipa') + object.__setattr__(self, 'log', log) + if self.env.debug: + log.setLevel(logging.DEBUG) + else: + log.setLevel(logging.INFO) + + # Add stderr handler: + stderr = logging.StreamHandler() + stderr.setFormatter(logging.Formatter(LOGGING_CONSOLE_FORMAT)) + if self.env.debug: + level = logging.DEBUG + elif self.env.verbose: + level = logging.INFO + else: + level = logging.WARNING + stderr.setLevel(level) + log.addHandler(stderr) + + # Add file handler: if self.env.mode == 'unit_test': - return - logger = util.configure_logging( - self.env.log, - self.env.verbose, - ) - object.__setattr__(self, 'log', logger) + return # But not if in unit-test mode + log_dir = path.dirname(self.env.log) + if not path.isdir(log_dir): + try: + os.makedirs(log_dir) + except OSError: + log.warn('Could not create log_dir %r', log_dir) + return + handler = logging.FileHandler(self.env.log) + handler.setFormatter(logging.Formatter(LOGGING_FILE_FORMAT)) + if self.env.debug: + level = logging.DEBUG + else: + level = logging.INFO + handler.setLevel(level) + log.addHandler(handler) + def load_plugins(self): """ diff --git a/ipalib/util.py b/ipalib/util.py index d577524b..3222c5a7 100644 --- a/ipalib/util.py +++ b/ipalib/util.py @@ -21,7 +21,6 @@ Various utility functions. """ -import logging import os from os import path import imp @@ -102,35 +101,3 @@ def import_plugins_subpackage(name): for name in find_modules_in_dir(src_dir): full_name = '%s.%s' % (plugins.__name__, name) __import__(full_name) - - -def configure_logging(log_file, verbose): - """ - Configure standard logging. - """ - # Set logging level: - level = logging.INFO - if verbose: - level -= 10 - - log = logging.getLogger('ipa') - log.setLevel(level) - - # Configure console handler - console = logging.StreamHandler() - console.setFormatter(logging.Formatter(LOGGING_CONSOLE_FORMAT)) - log.addHandler(console) - - # Configure file handler - log_dir = path.dirname(log_file) - if not path.isdir(log_dir): - try: - os.makedirs(log_dir) - except OSError: - log.warn('Could not create log_dir %r', log_dir) - return log - file_handler = logging.FileHandler(log_file) - file_handler.setFormatter(logging.Formatter(LOGGING_FILE_FORMAT)) - log.addHandler(file_handler) - - return log -- cgit From a23d41a57f43c3a0f298d3918ae1712181fa544e Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 31 Oct 2008 18:17:08 -0600 Subject: Reoganized global option functionality to it is easy for any script to use the environment-related global options; lite-xmlrpc.py now uses same global options --- ipalib/cli.py | 29 +++++++++-------------------- ipalib/plugable.py | 27 +++++++++++++++++++++++++++ ipalib/util.py | 22 ++++++++++++++++++++++ 3 files changed, 58 insertions(+), 20 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index 161ea1d8..c1ad82d5 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -31,6 +31,7 @@ import errors import plugable import ipa_types from config import set_default_env, read_config +import util def exit_error(error): sys.exit('ipa: ERROR: %s' % error) @@ -303,19 +304,7 @@ class CLI(object): """ self.__doing('bootstrap') self.parse_globals() - self.api.env.verbose = self.options.verbose - if self.options.config_file: - self.api.env.conf = self.options.config_file - overrides = {} - if self.options.environment: - for a in self.options.environment.split(','): - a = a.split('=', 1) - if len(a) < 2: - parser.error('badly specified environment string,'\ - 'use var1=val1[,var2=val2]..') - overrides[str(a[0].strip())] = a[1].strip() - overrides['context'] = 'cli' - self.api.bootstrap(**overrides) + self.api.bootstrap_from_options(self.options, context='cli') def parse_globals(self): """ @@ -337,17 +326,17 @@ class CLI(object): help='Prompt for all missing options interactively') parser.add_option('-n', dest='interactive', action='store_false', help='Don\'t prompt for any options interactively') - parser.add_option('-c', dest='config_file', - help='Specify different configuration file') - parser.add_option('-e', dest='environment', - help='Specify or override environment variables') - parser.add_option('-v', dest='verbose', action='store_true', - help='Verbose output') +# parser.add_option('-c', dest='config_file', +# help='Specify different configuration file') +# parser.add_option('-e', dest='environment', +# help='Specify or override environment variables') +# parser.add_option('-v', dest='verbose', action='store_true', +# help='Verbose output') parser.set_defaults( prompt_all=False, interactive=True, - verbose=False, ) + util.add_global_options(parser) (options, args) = parser.parse_args(list(self.argv)) self.options = options self.cmd_argv = tuple(args) diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 9e612d68..f552b61f 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -26,6 +26,7 @@ http://docs.python.org/ref/sequence-types.html """ import re +import sys import inspect import threading import logging @@ -38,6 +39,7 @@ from constants import LOGGING_FILE_FORMAT, LOGGING_CONSOLE_FORMAT, DEFAULT_CONFI import util + class ReadOnly(object): """ Base class for classes with read-only attributes. @@ -812,6 +814,31 @@ class API(DictProxy): handler.setLevel(level) log.addHandler(handler) + def bootstrap_from_options(self, options=None, context=None): + if options is None: + parser = util.add_global_options() + (options, args) = parser.parse_args( + list(s.decode('utf-8') for s in sys.argv[1:]) + ) + overrides = {} + if options.env is not None: + assert type(options.env) is list + for item in options.env: + try: + (key, value) = item.split('=', 1) + except ValueError: + # FIXME: this should raise an IPA exception with an + # error code. + # --Jason, 2008-10-31 + pass + overrides[str(key.strip())] = value.strip() + for key in ('conf', 'debug', 'verbose'): + value = getattr(options, key, None) + if value is not None: + overrides[key] = value + if context is not None: + overrides['context'] = context + self.bootstrap(**overrides) def load_plugins(self): """ diff --git a/ipalib/util.py b/ipalib/util.py index 3222c5a7..48a3a129 100644 --- a/ipalib/util.py +++ b/ipalib/util.py @@ -24,6 +24,7 @@ Various utility functions. import os from os import path import imp +import optparse import krbV from constants import LOGGING_CONSOLE_FORMAT, LOGGING_FILE_FORMAT @@ -101,3 +102,24 @@ def import_plugins_subpackage(name): for name in find_modules_in_dir(src_dir): full_name = '%s.%s' % (plugins.__name__, name) __import__(full_name) + + +def add_global_options(parser=None): + """ + Add global options to an optparse.OptionParser instance. + """ + if parser is None: + parser = optparse.OptionParser() + parser.add_option('-e', dest='env', metavar='KEY=VAL', action='append', + help='Set environment variable KEY to VAL', + ) + parser.add_option('-c', dest='conf', metavar='FILE', + help='Load configuration from FILE', + ) + parser.add_option('-d', '--debug', action='store_true', + help='Produce full debuging output', + ) + parser.add_option('-v', '--verbose', action='store_true', + help='Produce more verbose output', + ) + return parser -- cgit From 5269d1396c2e299d7fc66b55df7a84d482927549 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 31 Oct 2008 18:55:32 -0600 Subject: Logging formats are now env variables; added log_format_stderr_debug format used when env.debug is True --- ipalib/constants.py | 50 +++++++++++++++++++++++++++++++++----------------- ipalib/plugable.py | 20 ++++++++++---------- ipalib/util.py | 1 - 3 files changed, 43 insertions(+), 28 deletions(-) (limited to 'ipalib') diff --git a/ipalib/constants.py b/ipalib/constants.py index f4a440c6..7b0e1661 100644 --- a/ipalib/constants.py +++ b/ipalib/constants.py @@ -22,10 +22,38 @@ All constants centralized in one file. """ + # The section to read in the config files, i.e. [global] CONFIG_SECTION = 'global' +# Log format for console output +LOG_FORMAT_STDERR = ': '.join([ + '%(name)s', + '%(levelname)s', + '%(message)s', +]) + + +# Log format for console output when env.dubug is True: +LOG_FORMAT_STDERR_DEBUG = ' '.join([ + '%(levelname)s', + '%(message)r', + '%(lineno)d', + '%(filename)s', +]) + + +# Tab-delimited log format for file (easy to opened in a spreadsheet): +LOG_FORMAT_FILE = ' '.join([ + '%(created)f', + '%(levelname)s', + '%(message)r', # Using %r for repr() so message is a single line + '%(lineno)d', + '%(pathname)s', +]) + + # The default configuration for api.env # This is a tuple instead of a dict so that it is immutable. # To create a dict with this config, just "d = dict(DEFAULT_CONFIG)". @@ -55,6 +83,11 @@ DEFAULT_CONFIG = ( ('debug', False), ('mode', 'production'), + # Logging: + ('log_format_stderr', LOG_FORMAT_STDERR), + ('log_format_stderr_debug', LOG_FORMAT_STDERR_DEBUG), + ('log_format_file', LOG_FORMAT_FILE), + # ******************************************************** # The remaining keys are never set from the values here! # ******************************************************** @@ -87,20 +120,3 @@ DEFAULT_CONFIG = ( ('log', None), # Path to log file ) - - -LOGGING_CONSOLE_FORMAT = ': '.join([ - '%(name)s', - '%(levelname)s', - '%(message)s', -]) - - -# Tab-delimited format designed to be easily opened in a spreadsheet: -LOGGING_FILE_FORMAT = ' '.join([ - '%(created)f', - '%(levelname)s', - '%(message)r', # Using %r for repr() so message is a single line - '%(pathname)s', - '%(lineno)d', -]) diff --git a/ipalib/plugable.py b/ipalib/plugable.py index f552b61f..ccaf1f15 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -35,7 +35,7 @@ from os import path import errors from errors import check_type, check_isinstance from config import Environment, Env -from constants import LOGGING_FILE_FORMAT, LOGGING_CONSOLE_FORMAT, DEFAULT_CONFIG +from constants import DEFAULT_CONFIG import util @@ -785,14 +785,15 @@ class API(DictProxy): # Add stderr handler: stderr = logging.StreamHandler() - stderr.setFormatter(logging.Formatter(LOGGING_CONSOLE_FORMAT)) + format = self.env.log_format_stderr if self.env.debug: - level = logging.DEBUG + format = self.env.log_format_stderr_debug + stderr.setLevel(logging.DEBUG) elif self.env.verbose: - level = logging.INFO + stderr.setLevel(logging.INFO) else: - level = logging.WARNING - stderr.setLevel(level) + stderr.setLevel(logging.WARNING) + stderr.setFormatter(logging.Formatter(format)) log.addHandler(stderr) # Add file handler: @@ -806,12 +807,11 @@ class API(DictProxy): log.warn('Could not create log_dir %r', log_dir) return handler = logging.FileHandler(self.env.log) - handler.setFormatter(logging.Formatter(LOGGING_FILE_FORMAT)) + handler.setFormatter(logging.Formatter(self.env.log_format_file)) if self.env.debug: - level = logging.DEBUG + handler.setLevel(logging.DEBUG) else: - level = logging.INFO - handler.setLevel(level) + handler.setLevel(logging.INFO) log.addHandler(handler) def bootstrap_from_options(self, options=None, context=None): diff --git a/ipalib/util.py b/ipalib/util.py index 48a3a129..c1652065 100644 --- a/ipalib/util.py +++ b/ipalib/util.py @@ -26,7 +26,6 @@ from os import path import imp import optparse import krbV -from constants import LOGGING_CONSOLE_FORMAT, LOGGING_FILE_FORMAT def xmlrpc_marshal(*args, **kw): -- cgit From 5e5a83e4e84d2e9a5d6d987056199a8ed83978b8 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 31 Oct 2008 19:03:07 -0600 Subject: Renamed API.bootstrap_from_options() to bootstrap_with_global_options() --- ipalib/cli.py | 11 ++++------- ipalib/plugable.py | 2 +- 2 files changed, 5 insertions(+), 8 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index c1ad82d5..732e38bb 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -304,7 +304,7 @@ class CLI(object): """ self.__doing('bootstrap') self.parse_globals() - self.api.bootstrap_from_options(self.options, context='cli') + self.api.bootstrap_with_global_options(self.options, context='cli') def parse_globals(self): """ @@ -318,6 +318,9 @@ class CLI(object): 2. ``CLI.cmd_argv`` - a tuple containing the remainder of ``CLI.argv`` after the global options have been consumed. + + The common global options are added using the + `util.add_global_options` function. """ self.__doing('parse_globals') parser = optparse.OptionParser() @@ -326,12 +329,6 @@ class CLI(object): help='Prompt for all missing options interactively') parser.add_option('-n', dest='interactive', action='store_false', help='Don\'t prompt for any options interactively') -# parser.add_option('-c', dest='config_file', -# help='Specify different configuration file') -# parser.add_option('-e', dest='environment', -# help='Specify or override environment variables') -# parser.add_option('-v', dest='verbose', action='store_true', -# help='Verbose output') parser.set_defaults( prompt_all=False, interactive=True, diff --git a/ipalib/plugable.py b/ipalib/plugable.py index ccaf1f15..64a9d835 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -814,7 +814,7 @@ class API(DictProxy): handler.setLevel(logging.INFO) log.addHandler(handler) - def bootstrap_from_options(self, options=None, context=None): + def bootstrap_with_global_options(self, options=None, context=None): if options is None: parser = util.add_global_options() (options, args) = parser.parse_args( -- cgit From 242a8183a7cc002f496421352e8346db4232648b Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 31 Oct 2008 20:25:33 -0600 Subject: Added custom log formatter util.LogFormatter that makes the human-readable time stamp in UTC --- ipalib/constants.py | 4 ++-- ipalib/plugable.py | 4 ++-- ipalib/util.py | 9 +++++++++ 3 files changed, 13 insertions(+), 4 deletions(-) (limited to 'ipalib') diff --git a/ipalib/constants.py b/ipalib/constants.py index 7b0e1661..99b0ea71 100644 --- a/ipalib/constants.py +++ b/ipalib/constants.py @@ -45,8 +45,8 @@ LOG_FORMAT_STDERR_DEBUG = ' '.join([ # Tab-delimited log format for file (easy to opened in a spreadsheet): -LOG_FORMAT_FILE = ' '.join([ - '%(created)f', +LOG_FORMAT_FILE = '\t'.join([ + '%(asctime)s', '%(levelname)s', '%(message)r', # Using %r for repr() so message is a single line '%(lineno)d', diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 64a9d835..08a978ed 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -793,7 +793,7 @@ class API(DictProxy): stderr.setLevel(logging.INFO) else: stderr.setLevel(logging.WARNING) - stderr.setFormatter(logging.Formatter(format)) + stderr.setFormatter(util.LogFormatter(format)) log.addHandler(stderr) # Add file handler: @@ -807,7 +807,7 @@ class API(DictProxy): log.warn('Could not create log_dir %r', log_dir) return handler = logging.FileHandler(self.env.log) - handler.setFormatter(logging.Formatter(self.env.log_format_file)) + handler.setFormatter(util.LogFormatter(self.env.log_format_file)) if self.env.debug: handler.setLevel(logging.DEBUG) else: diff --git a/ipalib/util.py b/ipalib/util.py index c1652065..12f9c781 100644 --- a/ipalib/util.py +++ b/ipalib/util.py @@ -25,6 +25,8 @@ import os from os import path import imp import optparse +import logging +import time import krbV @@ -122,3 +124,10 @@ def add_global_options(parser=None): help='Produce more verbose output', ) return parser + + +class LogFormatter(logging.Formatter): + """ + Log formatter that uses UTC for all timestamps. + """ + converter = time.gmtime -- cgit From f18c84444d4ed87d79f3cb41156c6b66f49ccac3 Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Fri, 31 Oct 2008 17:02:51 -0400 Subject: Partially revert back change. Del shouldn't provide default options. It can provide custom ones though, if defined with takes_params() in the class. --- ipalib/crud.py | 2 -- 1 file changed, 2 deletions(-) (limited to 'ipalib') diff --git a/ipalib/crud.py b/ipalib/crud.py index 60c605dd..867f9fe1 100644 --- a/ipalib/crud.py +++ b/ipalib/crud.py @@ -47,8 +47,6 @@ class Del(frontend.Method): yield self.obj.primary_key def get_options(self): - for param in self.obj.params_minus_pk(): - yield param for option in self.takes_options: yield option -- cgit From dd9206deb62c1c96344d2280f672353a53a7fd11 Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Fri, 31 Oct 2008 17:03:10 -0400 Subject: Uncomment some logging statements ported over from v1. --- ipalib/plugins/f_group.py | 2 +- ipalib/plugins/f_service.py | 4 ++-- ipalib/plugins/f_user.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugins/f_group.py b/ipalib/plugins/f_group.py index 13af14c1..9df83a29 100644 --- a/ipalib/plugins/f_group.py +++ b/ipalib/plugins/f_group.py @@ -111,7 +111,7 @@ class group_del(crud.Del): ldap = self.api.Backend.ldap dn = ldap.find_entry_dn("cn", cn, "posixGroup") -# logging.info("IPA: delete_group '%s'" % dn) + self.log.info("IPA: group-del '%s'" % dn) # Don't allow the default user group to be removed config=ldap.get_ipa_config() diff --git a/ipalib/plugins/f_service.py b/ipalib/plugins/f_service.py index f02176ff..04187a86 100644 --- a/ipalib/plugins/f_service.py +++ b/ipalib/plugins/f_service.py @@ -91,10 +91,10 @@ class service_add(crud.Add): fqdn = hostname + "." rs = dnsclient.query(fqdn, dnsclient.DNS_C_IN, dnsclient.DNS_T_A) if len(rs) == 0: - logging.debug("IPA: DNS A record lookup failed for '%s'" % hostname) + self.log.debug("IPA: DNS A record lookup failed for '%s'" % hostname) raise ipaerror.gen_exception(ipaerror.INPUT_NOT_DNS_A_RECORD) else: - logging.debug("IPA: found %d records for '%s'" % (len(rs), hostname)) + self.log.debug("IPA: found %d records for '%s'" % (len(rs), hostname)) """ # At some point we'll support multiple realms diff --git a/ipalib/plugins/f_user.py b/ipalib/plugins/f_user.py index d8bb49e2..3adb328c 100644 --- a/ipalib/plugins/f_user.py +++ b/ipalib/plugins/f_user.py @@ -219,7 +219,7 @@ class user_del(crud.Del): # FIXME: do we still want a "special" user? raise SyntaxError("admin required") # raise ipaerror.gen_exception(ipaerror.INPUT_ADMIN_REQUIRED) -# logging.info("IPA: delete_user '%s'" % uid) + self.log.info("IPA: user-del '%s'" % uid) ldap = self.api.Backend.ldap dn = ldap.find_entry_dn("uid", uid) -- cgit From d53218a9321eb4def0bfeb484709323de74eef1a Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Mon, 3 Nov 2008 17:19:29 -0500 Subject: Handle exceptions in the command-line instead of in the XMLRPC client plugin --- ipalib/cli.py | 10 ++++++++++ ipalib/plugins/b_xmlrpc.py | 9 +-------- 2 files changed, 11 insertions(+), 8 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index 732e38bb..8cf8d304 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -377,6 +377,16 @@ class CLI(object): if callable(cmd.output_for_cli): cmd.output_for_cli(ret) return 0 + except errors.GenericError, err: + code = getattr(err,'faultCode',None) + faultString = getattr(err,'faultString',None) + if not code: + raise err + if code < errors.IPA_ERROR_BASE: + print "%s: %s" % (code, faultString) + else: + print "%s: %s" % (code, getattr(err,'__doc__','')) + return 1 except StandardError, e: print e return 2 diff --git a/ipalib/plugins/b_xmlrpc.py b/ipalib/plugins/b_xmlrpc.py index 2c98fb8a..9c6af0a0 100644 --- a/ipalib/plugins/b_xmlrpc.py +++ b/ipalib/plugins/b_xmlrpc.py @@ -64,14 +64,7 @@ class xmlrpc(Backend): print e[1] except xmlrpclib.Fault, e: err = errors.convertFault(e) - code = getattr(err,'faultCode',None) - faultString = getattr(err,'faultString',None) - if not code: - raise err - if code < errors.IPA_ERROR_BASE: - print "%s: %s" % (code, faultString) - else: - print "%s: %s" % (code, getattr(err,'__doc__','')) + raise err return api.register(xmlrpc) -- cgit From f1314806434b9226f8a7722675b060bdf574c455 Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Mon, 3 Nov 2008 17:38:05 -0500 Subject: Move socket errors from the XML-RPC plugin to the client --- ipalib/cli.py | 4 ++++ ipalib/plugins/b_xmlrpc.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index 8cf8d304..d7288154 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -25,6 +25,7 @@ import re import sys import code import optparse +import socket import frontend import errors @@ -377,6 +378,9 @@ class CLI(object): if callable(cmd.output_for_cli): cmd.output_for_cli(ret) return 0 + except socket.error, e: + print e[1] + return 1 except errors.GenericError, err: code = getattr(err,'faultCode',None) faultString = getattr(err,'faultString',None) diff --git a/ipalib/plugins/b_xmlrpc.py b/ipalib/plugins/b_xmlrpc.py index 9c6af0a0..87dc9505 100644 --- a/ipalib/plugins/b_xmlrpc.py +++ b/ipalib/plugins/b_xmlrpc.py @@ -61,7 +61,7 @@ class xmlrpc(Backend): try: return command(*params) except socket.error, e: - print e[1] + raise except xmlrpclib.Fault, e: err = errors.convertFault(e) raise err -- cgit From 49670023591f8eb88dd5860fffc3f8e99764e5a8 Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Tue, 4 Nov 2008 14:02:42 -0500 Subject: Add 'all' option to host-find and pull attributes into a global list --- ipalib/plugins/f_host.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/plugins/f_host.py b/ipalib/plugins/f_host.py index 4f4f7204..e842230f 100644 --- a/ipalib/plugins/f_host.py +++ b/ipalib/plugins/f_host.py @@ -55,6 +55,7 @@ def validate_host(cn): return 'Fully-qualified hostname required' return None +default_attributes = ['cn','description','localityname','nshostlocation','nshardwareplatform','nsosversion'] class host(frontend.Object): """ @@ -213,6 +214,9 @@ api.register(host_mod) class host_find(crud.Find): 'Search the hosts.' + takes_options = ( + Param('all?', type=ipa_types.Bool(), doc='Retrieve all attributes'), + ) def get_args(self): """ Override Find.get_args() so we can exclude the validation rules @@ -233,6 +237,10 @@ class host_find(crud.Find): # Can't use ldap.get_object_type() since cn is also used for group dns kw['objectclass'] = "ipaHost" + if kw.get('all', False): + kw['attributes'] = ['*'] + else: + kw['attributes'] = default_attributes return ldap.search(**kw) def output_for_cli(self, hosts): if not hosts: @@ -275,7 +283,7 @@ class host_show(crud.Get): if kw.get('all', False): return ldap.retrieve(dn) else: - value = ldap.retrieve(dn, ['cn','description','localityname','nshostlocation','nshardwareplatform','nsosversion']) + value = ldap.retrieve(dn, default_attributes) del value['dn'] return value def output_for_cli(self, host): -- cgit From e825bc7ccbc787e65efa7171e9f19793bcd88615 Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Tue, 4 Nov 2008 14:03:43 -0500 Subject: Revive the hostgroup_container and include add/remove hosts in hostgroups plugin --- ipalib/constants.py | 1 + ipalib/plugins/f_hostgroup.py | 28 ++++++++++++++++++++++++++-- 2 files changed, 27 insertions(+), 2 deletions(-) (limited to 'ipalib') diff --git a/ipalib/constants.py b/ipalib/constants.py index 99b0ea71..9bd82a1b 100644 --- a/ipalib/constants.py +++ b/ipalib/constants.py @@ -69,6 +69,7 @@ DEFAULT_CONFIG = ( ('container_group', 'cn=groups,cn=accounts'), ('container_service', 'cn=services,cn=accounts'), ('container_host', 'cn=computers,cn=accounts'), + ('container_hostgroup', 'cn=hostgroups,cn=accounts'), # Ports, hosts, and URIs: ('lite_xmlrpc_port', 8888), diff --git a/ipalib/plugins/f_hostgroup.py b/ipalib/plugins/f_hostgroup.py index 27aea00c..8e4c3740 100644 --- a/ipalib/plugins/f_hostgroup.py +++ b/ipalib/plugins/f_hostgroup.py @@ -222,7 +222,8 @@ class hostgroup_add_member(frontend.Command): Param('group', primary_key=True), ) takes_options = ( - Param('groups?', doc='comma-separated list of groups to add'), + Param('groups?', doc='comma-separated list of host groups to add'), + Param('hosts?', doc='comma-separated list of hosts to add'), ) def execute(self, cn, **kw): """ @@ -231,7 +232,8 @@ class hostgroup_add_member(frontend.Command): Returns the updated group entry :param cn: The group name to add new members to. - :param kw: groups is a comma-separated list of groups to add + :param kw: groups is a comma-separated list of host groups to add + :param kw: hosts is a comma-separated list of hosts to add """ ldap = self.api.Backend.ldap dn = ldap.find_entry_dn("cn", cn, hostgroup_filter) @@ -249,6 +251,16 @@ class hostgroup_add_member(frontend.Command): add_failed.append(m) continue + members = kw.get('hosts', '').split(',') + for m in members: + if not m: continue + try: + member_dn = ldap.find_entry_dn("cn", m, "ipaHost") + to_add.append(member_dn) + except errors.NotFound: + add_failed.append(m) + continue + for member_dn in to_add: try: ldap.add_member_to_group(member_dn, dn) @@ -278,6 +290,7 @@ class hostgroup_remove_member(frontend.Command): Param('group', primary_key=True), ) takes_options = ( + Param('hosts?', doc='comma-separated list of hosts to add'), Param('groups?', doc='comma-separated list of groups to remove'), ) def execute(self, cn, **kw): @@ -288,6 +301,7 @@ class hostgroup_remove_member(frontend.Command): :param cn: The group name to add new members to. :param kw: groups is a comma-separated list of groups to remove + :param kw: hosts is a comma-separated list of hosts to add """ ldap = self.api.Backend.ldap dn = ldap.find_entry_dn("cn", cn, hostgroup_filter) @@ -305,6 +319,16 @@ class hostgroup_remove_member(frontend.Command): remove_failed.append(m) continue + members = kw.get('hosts', '').split(',') + for m in members: + if not m: continue + try: + member_dn = ldap.find_entry_dn("cn", m, "ipaHost") + to_remove.append(member_dn) + except errors.NotFound: + remove_failed.append(m) + continue + for member_dn in to_remove: try: ldap.remove_member_from_group(member_dn, dn) -- cgit From e8adb59fd42eddb9f8911f6d888d9a64e0773df9 Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Tue, 4 Nov 2008 16:21:10 -0500 Subject: Fix some problems uncovered during automation test work --- ipalib/plugins/f_automount.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugins/f_automount.py b/ipalib/plugins/f_automount.py index 78e96ccd..fba9e12c 100644 --- a/ipalib/plugins/f_automount.py +++ b/ipalib/plugins/f_automount.py @@ -180,7 +180,8 @@ api.register(automount_delmap) class automount_delkey(crud.Del): 'Delete an automount key.' takes_options = ( - Param('key', + Param('automountkey', + cli_name='key', doc='The automount key to remove'), ) def execute(self, mapname, **kw): @@ -195,7 +196,7 @@ class automount_delkey(crud.Del): dn = ldap.find_entry_dn("automountmapname", mapname, "automountmap") keys = api.Command['automount_getkeys'](mapname) keydn = None - keyname = kw.get('key').lower() + keyname = kw.get('automountkey').lower() if keys: for k in keys: if k.get('automountkey').lower() == keyname: @@ -336,6 +337,10 @@ class automount_findkey(crud.Find): takes_options = ( Param('all?', type=ipa_types.Bool(), doc='Retrieve all attributes'), ) + def get_args(self): + return (Param('automountkey', + cli_name='key', + doc='An entry in an automount map'),) def execute(self, term, **kw): ldap = self.api.Backend.ldap @@ -399,7 +404,8 @@ api.register(automount_showmap) class automount_showkey(crud.Get): 'Examine an existing automount key.' takes_options = ( - Param('key', + Param('automountkey', + cli_name='key', doc='The automount key to display'), Param('all?', type=ipa_types.Bool(), doc='Retrieve all attributes'), ) @@ -410,13 +416,13 @@ class automount_showkey(crud.Get): Returns the entry :param mapname: The mapname to examine - :param kw: "key" the key to retrieve + :param kw: "automountkey" the key to retrieve :param kw: "all" set to True = return all attributes """ ldap = self.api.Backend.ldap dn = ldap.find_entry_dn("automountmapname", mapname, "automountmap") keys = api.Command['automount_getkeys'](mapname) - keyname = kw.get('key').lower() + keyname = kw.get('automountkey').lower() keydn = None if keys: for k in keys: @@ -463,7 +469,12 @@ class automount_getkeys(frontend.Command): """ ldap = self.api.Backend.ldap dn = ldap.find_entry_dn("automountmapname", mapname, "automountmap") - return ldap.get_one_entry(dn, 'objectclass=*', ['automountkey']) + try: + keys = ldap.get_one_entry(dn, 'objectclass=*', ['automountkey']) + except errors.NotFound: + keys = [] + + return keys def output_for_cli(self, keys): if keys: for k in keys: -- cgit From 5bdf860647c5d5825791d50a94b34fbd9a7a71a9 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 6 Nov 2008 11:57:21 -0700 Subject: Added Plugin.call() method that calls an external executable via subprocess.call() --- ipalib/plugable.py | 11 +++++++++++ 1 file changed, 11 insertions(+) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 08a978ed..5e0611f9 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -32,6 +32,7 @@ import threading import logging import os from os import path +import subprocess import errors from errors import check_type, check_isinstance from config import Environment, Env @@ -366,6 +367,16 @@ class Plugin(ReadOnly): assert not hasattr(self, name) setattr(self, name, getattr(api, name)) + def call(self, *args): + """ + Call an external command via ``subprocess.call``. + + Returns the exit status of the call. + """ + if hasattr(self, 'log'): + self.log.debug('Calling %r', args) + return subprocess.call(args) + def __repr__(self): """ Return 'module_name.class_name()' representation. -- cgit From c26a3c8542472a2d3931c7dc82edfd684354af6b Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 7 Nov 2008 02:26:38 -0700 Subject: Finished fist draft of plugin tutorial in ipalib/__init__.py docstring --- ipalib/__init__.py | 646 ++++++++++++++++++++++++++++++++++++++++++++++++++++- ipalib/config.py | 12 +- ipalib/plugable.py | 4 +- 3 files changed, 648 insertions(+), 14 deletions(-) (limited to 'ipalib') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index 5cc4c121..4db6a04f 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -17,17 +17,622 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -""" + +''' Package containing core library. -To learn about the ``ipalib`` library, you should read the code in this order: +============================= + Tutorial for Plugin Authors +============================= + +This tutorial gives a broad learn-by-doing introduction to writing plugins +for freeIPA v2. As not to overwhelm the reader, it does not cover every +detail, but it does provides enough to get one started and is heavily +cross-referenced with further documentation that (hopefully) fills in the +missing details. + +Where the documentation has left the reader confused, the many built-in +plugins in `ipalib.plugins` and `ipa_server.plugins` provide real-life +examples of how to write good plugins. + +*Note:* + + This tutorial, along with all the Python docstrings in freeIPA v2, + uses the *reStructuredText* markup language. For documentation on + reStructuredText, see: + + http://docutils.sourceforge.net/rst.html + + For documentation on using reStructuredText markup with epydoc, see: + + http://epydoc.sourceforge.net/manual-othermarkup.html + + +------------------------------------ +First steps: A simple command plugin +------------------------------------ + +Our first example will create the most basic command plugin possible. A +command plugin simultaneously adds a new command that can be called through +the command-line ``ipa`` script *and* adds a new XML-RPC method... the two are +one in the same, simply invoked in different ways. + +All plugins must subclass from `plugable.Plugin`, and furthermore, must +subclass from one of the base classes allowed by the `plugable.API` instance +returned by the `get_standard_api()` function. + +To be a command plugin, your plugin must subclass from `frontend.Command`. +Creating a basic plugin involves two steps, defining the class and then +registering the class: + +>>> from ipalib import Command, get_standard_api +>>> api = get_standard_api() +>>> class my_command(Command): # Step 1, define class +... """My example plugin.""" +... +>>> api.register(my_command) # Step 2, register class + +Notice that we are registering the ``my_command`` class itself and not an +instance thereof. + +Until `plugable.API.finalize()` is called, your plugin class has not been +instantiated nor the does the ``Command`` namespace yet exist. For example: + +>>> hasattr(api, 'Command') +False +>>> api.finalize() +>>> hasattr(api.Command, 'my_command') +True +>>> api.Command.my_command.doc +'My example plugin.' + +Notice that your plugin instance in accessed through an attribute named +'my_command', the same name as your plugin class name. + + +------------------------------ +Make your command do something +------------------------------ + +This simplest way to make your example command plugin do something is to +implement a ``run()`` method, like this: + +>>> class my_command(Command): +... """My example plugin with run().""" +... +... def run(self): +... return 'My run() method was called!' +... +>>> api = get_standard_api() +>>> api.register(my_command) +>>> api.finalize() +>>> api.Command.my_command() # Call your plugin +'My run() method was called!' + +When `frontend.Command.__call__()` is called, it first validates any arguments +and options your command plugin takes (if any) and then calls its ``run()`` +method. + + +------------------------ +Forwarding vs. execution +------------------------ + +However, unlike the example above, a typical command plugin will implement an +``execute()`` method instead of a ``run()`` method. Your command plugin can +be loaded in two distinct contexts: + + 1. In a *client* context - Your command plugin is only used to validate + any arguments and options it takes, and then ``self.forward()`` is + called, which forwards the call over XML-RPC to an IPA server where + the actual work is done. + + 2. In a *server* context - Your same command plugin validates any + arguments and options it takes, and then ``self.execute()`` is called, + which you should implement to perform whatever work your plugin does. + +The base `frontend.Command.run()` method simply dispatches the call to +``self.execute()`` if ``self.env.in_server`` is True, or otherwise +dispatches the call to ``self.forward()``. + +For example, say you have a command plugin like this: + +>>> class my_command(Command): +... """Forwarding vs. execution.""" +... +... def forward(self): +... return 'in_server=%r; forward() was called.' % self.env.in_server +... +... def execute(self): +... return 'in_server=%r; execute() was called.' % self.env.in_server +... + +If ``my_command`` is loaded in a *client* context, ``forward()`` will be +called: + +>>> api = get_standard_api() +>>> api.env.in_server = False # run() will dispatch to forward() +>>> api.register(my_command) +>>> api.finalize() +>>> api.Command.my_command() # Call your command plugin +'in_server=False; forward() was called.' + +On the other hand, if ``my_command`` is loaded in a *server* context, +``execute()`` will be called: + +>>> api = get_standard_api() +>>> api.env.in_server = True # run() will dispatch to execute() +>>> api.register(my_command) +>>> api.finalize() +>>> api.Command.my_command() # Call your command plugin +'in_server=True; execute() was called.' + +Normally there should be no reason to override `frontend.Command.forward()`, +but, as above, it can be done for demonstration purposes. In contrast, there +*is* a reason you might want to override `frontend.Command.run()`: if it only +makes sense to execute your command locally, if it should never be forwarded +to the server. In this case, you should implement your *do-stuff* in the +``run()`` method instead of in the ``execute()`` method. + +For example, the ``ipa`` command line script has a ``help`` command +(`ipalib.cli.help`) that is specific to the command-line-interface and should +never be forwarded to the server. + + +--------------- +Backend plugins +--------------- + +There are two types of plugins: + + 1. *Frontend plugins* - These are loaded in both the *client* and *server* + contexts. These need to be installed with any application built atop + the `ipalib` library. The built-in frontend plugins can be found in + `ipalib.plugins`. The ``my_command`` example above is a frontend + plugin. + + 2. *Backend plugins* - These are only loaded in a *server* context and + only need to be installed on the IPA server. The built-in backend + plugins can be found in `ipa_server.plugins`. + +Backend plugins should provide a set of methods that standardize how IPA +interacts with some external system or library. For example, all interaction +with LDAP is done through the ``ldap`` backend plugin defined in +`ipa_server.plugins.b_ldap`. As a good rule of thumb, anytime you need to +import some package that is not part of the Python standard library, you +should probably interact with that package via a corresponding backend +plugin you implement. + +Backend plugins are much more free-form than command plugins. Aside from a +few reserved attribute names, you can define arbitrary public methods on your +backend plugin (in contrast, frontend plugins get wrapped in a +`plugable.PluginProxy`, which allow access to only specific attributes on the +frontend plugin). + +Here is a simple example: + +>>> from ipalib import Backend +>>> class my_backend(Backend): +... """My example backend plugin.""" +... +... def do_stuff(self): +... """Part of your API.""" +... return 'Stuff got done.' +... +>>> api = get_standard_api() +>>> api.register(my_backend) +>>> api.finalize() +>>> api.Backend.my_backend.do_stuff() +'Stuff got done.' + + +------------------------------- +How your command should do work +------------------------------- + +We now return to our ``my_command`` plugin example. + +Plugins are separated into frontend and backend plugins so that there are not +unnecessary dependencies required by an application that only uses `ipalib` and +its built-in frontend plugins (and then forwards over XML-RPC for execution). + +But how do we avoid introducing additional dependencies? For example, the +``user_add`` command needs to talk to LDAP to add the user, yet we want to +somehow load the ``user_add`` plugin on client machines without requiring the +``python-ldap`` package (Python bindings to openldap) to be installed. To +answer that, we consult our golden rule: + + **The golden rule:** A command plugin should implement its ``execute()`` + method strictly via calls to methods on one or more backend plugins. + +So the module containing the ``user_add`` command does not itself import the +Python LDAP bindings, only the module containing the ``ldap`` backend plugin +does that, and the backend plugins are only installed on the server. The +``user_add.execute()`` method, which is only called when in a server context, +is implemented as a series of calls to methods on the ``ldap`` backend plugin. + +When `plugable.Plugin.set_api()` is called, each plugin stores a reference to +the `plugable.API` instance it has been loaded into. So your plugin can +access the ``my_backend`` plugin as ``self.api.Backend.my_backend``. + +Additionally, convenience attributes are set for each namespace, so your +plugin can also access the ``my_backend`` plugin as simply +``self.Backend.my_backend``. + +This next example will tie everything together. First we create our backend +plugin: + +>>> api = get_standard_api() +>>> api.env.in_server = True # We want to execute, not forward +>>> class my_backend(Backend): +... """My example backend plugin.""" +... +... def do_stuff(self): +... """my_command.execute() calls this.""" +... return 'my_backend.do_stuff() indeed did do stuff!' +... +>>> api.register(my_backend) + +Second, we have our frontend plugin, the command: + +>>> class my_command(Command): +... """My example command plugin.""" +... +... def execute(self): +... """Implemented against Backend.my_backend""" +... return self.Backend.my_backend.do_stuff() +... +>>> api.register(my_command) + +Lastly, we call ``api.finalize()`` and see what happens when we call +``my_command()``: + +>>> api.finalize() +>>> api.Command.my_command() +'my_backend.do_stuff() indeed did do stuff!' + +When not in a server context, ``my_command.execute()`` never gets called, so +it never tries to access the non-existent backend plugin at +``self.Backend.my_backend.`` To emphasize this point, here is one last +example: + +>>> api = get_standard_api() +>>> api.env.in_server = False # We want to forward, not execute +>>> class my_command(Command): +... """My example command plugin.""" +... +... def execute(self): +... """Same as above.""" +... return self.Backend.my_backend.do_stuff() +... +... def forward(self): +... return 'Just my_command.forward() getting called here.' +... +>>> api.register(my_command) +>>> api.finalize() + +Notice that the ``my_backend`` plugin has certainly not be registered: + +>>> hasattr(api.Backend, 'my_backend') +False + +And yet we can call ``my_command()``: + +>>> api.Command.my_command() +'Just my_command.forward() getting called here.' + + +---------------------------------------- +Calling other commands from your command +---------------------------------------- + +It can be useful to have your ``execute()`` method call other command plugins. +Among other things, this allows for meta-commands that conveniently call +several other commands in a single operation. For example: + +>>> api = get_standard_api() +>>> api.env.in_server = True # We want to execute, not forward +>>> class meta_command(Command): +... """My meta-command plugin.""" +... +... def execute(self): +... """Calls command_1(), command_2()""" +... return '%s; %s.' % ( +... self.Command.command_1(), +... self.Command.command_2() +... ) +>>> class command_1(Command): +... def execute(self): +... return 'command_1.execute() called' +... +>>> class command_2(Command): +... def execute(self): +... return 'command_2.execute() called' +... +>>> api.register(meta_command) +>>> api.register(command_1) +>>> api.register(command_2) +>>> api.finalize() +>>> api.Command.meta_command() +'command_1.execute() called; command_2.execute() called.' + +Because this is quite useful, we are going to revise our golden rule somewhat: - 1. Get the big picture from some actual plugins, like `plugins.f_user`. + **The revised golden rule:** A command plugin should implement its + ``execute()`` method strictly via what it can access through ``self.api``, + most likely via the backend plugins in ``self.api.Backend`` (which can also + be conveniently accessed as ``self.Backend``). - 2. Learn about the base classes for frontend plugins in `frontend`. - 3. Learn about the core plugin framework in `plugable`. -""" +----------------------------------------------- +Defining arguments and options for your command +----------------------------------------------- + +You can define a command can accept arbitrary arguments and options. +For example: + +>>> from ipalib import Param +>>> class nudge(Command): +... """Takes one argument, one option""" +... +... takes_args = ['programmer'] +... +... takes_options = [Param('stuff', default=u'documentation')] +... +... def execute(self, programmer, **kw): +... return '%s, go write more %s!' % (programmer, kw['stuff']) +... +>>> api = get_standard_api() +>>> api.env.in_server = True +>>> api.register(nudge) +>>> api.finalize() +>>> api.Command.nudge('Jason') +u'Jason, go write more documentation!' +>>> api.Command.nudge('Jason', stuff='unit tests') +u'Jason, go write more unit tests!' + +The ``args`` and ``options`` attributes are `plugable.NameSpace` instances +containing a command's arguments and options, respectively, as you can see: + +>>> list(api.Command.nudge.args) # Iterates through argument names +['programmer'] +>>> api.Command.nudge.args.programmer +Param('programmer', Unicode()) +>>> list(api.Command.nudge.options) # Iterates through option names +['stuff'] +>>> api.Command.nudge.options.stuff +Param('stuff', Unicode()) +>>> api.Command.nudge.options.stuff.default +u'documentation' + +The arguments and options must not contain colliding names. They are both +merged together into the ``params`` attribute, another `plugable.NameSpace` +instance, as you can see: + +>>> api.Command.nudge.params +NameSpace(<2 members>, sort=False) +>>> list(api.Command.nudge.params) # Iterates through the param names +['programmer', 'stuff'] + +When calling a command, its positional arguments can also be provided as +keyword arguments, and in any order. For example: + +>>> api.Command.nudge(stuff='lines of code', programmer='Jason') +u'Jason, go write more lines of code!' + +When a command plugin is called, the values supplied for its parameters are +put through a sophisticated processing pipeline that includes steps for +normalization, type conversion, validation, and dynamically constructing +the defaults for missing values. The details wont be covered here; however, +here is a quick teaser: + +>>> from ipalib import Int +>>> class create_player(Command): +... takes_options = [ +... 'first', +... 'last', +... Param('nick', +... normalize=lambda value: value.lower(), +... default_from=lambda first, last: first[0] + last, +... ), +... Param('points', type=Int(), default=0), +... ] +... +>>> cp = create_player() +>>> cp.finalize() +>>> cp.convert(points=" 1000 ") +{'points': 1000} +>>> cp.normalize(nick=u'NickName') +{'nick': u'nickname'} +>>> cp.get_default(first='Jason', last='DeRose') +{'nick': u'jderose', 'points': 0} + +For the full details on the parameter system, see the +`frontend.parse_param_spec()` function, and the `frontend.Param` and +`frontend.Command` classes. + + +------------------------ +Logging from your plugin +------------------------ + +After `plugable.Plugin.set_api()` is called, your plugin will have a +``self.log`` attribute. Plugins should only log through this attribute. +For example: + +>>> class paint_house(Command): +... +... takes_args = ['color'] +... +... def execute(self, color): +... """Uses self.log.error()""" +... if color not in ('red', 'blue', 'green'): +... self.log.error("I don't have %s paint!", color) # Log error +... return +... return 'I painted the house %s.' % color +... + +Some basic knowledge of the Python ``logging`` module might be helpful. See: + + http://www.python.org/doc/2.5.2/lib/module-logging.html + +The important thing to remember is that your plugin should not configure +logging itself, but should instead simply use the ``self.log`` logger. + +Also see the `plugable.API.bootstrap()` method for details on how the logging +is configured. + + +--------------------- +Environment variables +--------------------- + +Plugins access various environment variables and run-time information through +``self.api.env`` (for convenience, ``self.env`` is equivalent). + +When you create a fresh `plugable.API` instance, its ``env`` attribute is +likewise a freshly created `config.Env` instance, which will already be +populated with certain run-time information. For example: + +>>> api = get_standard_api() +>>> list(api.env) +['bin', 'dot_ipa', 'home', 'ipalib', 'mode', 'script', 'site_packages'] + +Here is a quick overview of the run-time information: + +============= ================================ ======================= +Key Source or example value Description +============= ================================ ======================= +bin /usr/bin Dir. containing script +dot_ipa ~/.ipa User config directory +home os.environ['HOME'] User home dir. +ipalib .../site-packages/ipalib Dir. of ipalib package +mode 'production' or 'unit_test' The mode ipalib is in +script sys.argv[0] Path of script +site_packages /usr/lib/python2.5/site-packages Dir. containing ipalib/ +============= ================================ ======================= + +After `plugable.API.bootstrap()` has been called, the env instance will be +populated with all the environment information used by the built-in plugins. +This will typically be called before any plugins are registered. For example: + +>>> len(api.env) +7 +>>> api.bootstrap(in_server=True) # We want to execute, not forward +>>> len(api.env) +33 + +If your plugin requires new environment variables *and* will be included in +the freeIPA built-in plugins, you should add the defaults for your variables +in `ipalib.constants.DEFAULT_CONFIG`. Also, you should consider whether your +new environment variables should have any auto-magic logic to determine their +values if they haven't already been set by the time `config.Env._bootstrap()`, +`config.Env._finalize_core()`, or `config.Env._finalize()` is called. + +On the other hand, if your plugin requires new environment variables and will +be installed in a 3rd-party package, your plugin should set these variables +in the module it is defined in. + +`config.Env` values work on a first-one-wins basis... after a value has been +set, it can not be overridden with a new value. As any variables can be set +using the command-line ``-e`` global option or set in a configuration file, +your module must check whether a variable has already been set before +setting its default value. For example: + +>>> if 'message_of_the_day' not in api.env: +... api.env.message_of_the_day = 'Hello, world!' +... + +Your plugin can access any environment variables via ``self.env``. +For example: + +>>> class motd(Command): +... """Print message of the day.""" +... +... def execute(self): +... return self.env.message_of_the_day +... +>>> api.register(motd) +>>> api.finalize() +>>> api.Command.motd() +'Hello, world!' + +Also see the `plugable.API.bootstrap_with_global_options()` method. + + +--------------------------------------------- +Indispensable ipa script commands and options +--------------------------------------------- + +The ``console`` command will launch a custom interactive Python interpreter +session. The global environment will have an ``api`` variable, which is the +standard `plugable.API` instance found at ``ipalib.api``. All plugins will +have been loaded (well, except the backend plugins if ``in_server`` is False) +and ``api`` will be fully initialized. To launch the console from within the +top-level directory in the the source tree, just run ``ipa console`` from a +terminal, like this: + + :: + + $ ./ipa console + +By default, ``in_server`` is False. If you want to start the console in a +server context (so that all the backend plugins are loaded), you can use the +``-e`` option to set the ``in_server`` environment variable, like this: + + :: + + $ ./ipa -e in_server=True console + +You can specify multiple environment variables by including the ``-e`` option +multiple times, like this: + + :: + + $ ./ipa -e in_server=True -e mode=dummy console + +The space after the ``-e`` is optional. This is equivalent to the above command: + + :: + + $ ./ipa -ein_server=True -emode=dummy console + +The ``env`` command will print out the full environment in key=value pairs, +like this: + + :: + + $ ./ipa env + +If you use the ``--server`` option, it will forward the call to the server +over XML-RPC and print out what the environment is on the server, like this: + + :: + + $ ./ipa env --server + +The ``plugins`` command will show details of all the plugin that are loaded, +like this: + + :: + + $ ./ipa plugins + + +---------------- +Learning more... +---------------- + +To learn more about writing plugins, you should: + + 1. Look at some of the built-in plugins, like the frontend plugins in + `ipalib.plugins.f_user` and the backend plugins in + `ipa_server.plugins.b_ldap`. + + 2. Learn about the base classes for frontend plugins in `ipalib.frontend`. + + 3. Learn about the core plugin framework in `ipalib.plugable`. +''' import plugable from backend import Backend, Context @@ -35,11 +640,34 @@ from frontend import Command, Object, Method, Property, Application from ipa_types import Bool, Int, Unicode, Enum from frontend import Param, DefaultFrom -def get_standard_api(): - return plugable.API( +def get_standard_api(mode='dummy'): + """ + Return standard `plugable.API` instance. + + This standard instance allows plugins that subclass from the following + base classes: + + - `frontend.Command` + + - `frontend.Object` + + - `frontend.Method` + + - `frontend.Property` + + - `frontend.Application` + + - `backend.Backend` + + - `backend.Context` + """ + api = plugable.API( Command, Object, Method, Property, Application, Backend, Context, ) + if mode is not None: + api.env.mode = mode + return api -api = get_standard_api() +api = get_standard_api(mode=None) diff --git a/ipalib/config.py b/ipalib/config.py index 02a3fadd..aa7d9cdf 100644 --- a/ipalib/config.py +++ b/ipalib/config.py @@ -202,8 +202,8 @@ class Env(object): """ self.__doing('_finalize_core') self.__do_if_not_done('_bootstrap') - self._merge_config(self.conf) - if self.conf_default != self.conf: + if self.__d.get('mode', None) != 'dummy': + self._merge_config(self.conf) self._merge_config(self.conf_default) if 'in_server' not in self: self.in_server = (self.context == 'server') @@ -335,7 +335,13 @@ class Env(object): """ return key in self.__d - def __iter__(self): # Fix + def __len__(self): + """ + Return number of variables currently set. + """ + return len(self.__d) + + def __iter__(self): """ Iterate through keys in ascending order. """ diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 5e0611f9..d65a83e2 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -808,7 +808,7 @@ class API(DictProxy): log.addHandler(stderr) # Add file handler: - if self.env.mode == 'unit_test': + if self.env.mode in ('dummy', 'unit_test'): return # But not if in unit-test mode log_dir = path.dirname(self.env.log) if not path.isdir(log_dir): @@ -860,7 +860,7 @@ class API(DictProxy): """ self.__doing('load_plugins') self.__do_if_not_done('bootstrap') - if self.env.mode == 'unit_test': + if self.env.mode in ('dummy', 'unit_test'): return util.import_plugins_subpackage('ipalib') if self.env.in_server: -- cgit From 174af50f6d895572792d5601f19b499c677b4fdf Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 7 Nov 2008 02:30:19 -0700 Subject: Fixed typo and made sentance clearer in tutorial --- ipalib/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index 4db6a04f..90a76a31 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -368,7 +368,7 @@ Because this is quite useful, we are going to revise our golden rule somewhat: Defining arguments and options for your command ----------------------------------------------- -You can define a command can accept arbitrary arguments and options. +You can define a command that will accept specific arguments and options. For example: >>> from ipalib import Param -- cgit From 9aa14333a46d3a57c1fc9fad6068090eb029070f Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 10 Nov 2008 15:53:10 -0700 Subject: Added 'conf_dir' env variable, which is directory containing config files --- ipalib/__init__.py | 2 +- ipalib/config.py | 2 ++ ipalib/constants.py | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index 90a76a31..d943a7c7 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -520,7 +520,7 @@ This will typically be called before any plugins are registered. For example: 7 >>> api.bootstrap(in_server=True) # We want to execute, not forward >>> len(api.env) -33 +34 If your plugin requires new environment variables *and* will be included in the freeIPA built-in plugins, you should add the defaults for your variables diff --git a/ipalib/config.py b/ipalib/config.py index aa7d9cdf..1bec57e5 100644 --- a/ipalib/config.py +++ b/ipalib/config.py @@ -186,6 +186,8 @@ class Env(object): self.conf = path.join(base, '%s.conf' % self.context) if 'conf_default' not in self: self.conf_default = path.join(base, 'default.conf') + if 'conf_dir' not in self: + self.conf_dir = base def _finalize_core(self, **defaults): """ diff --git a/ipalib/constants.py b/ipalib/constants.py index 9bd82a1b..b8f93d21 100644 --- a/ipalib/constants.py +++ b/ipalib/constants.py @@ -115,6 +115,7 @@ DEFAULT_CONFIG = ( ('context', None), # Name of context, default is 'default' ('conf', None), # Path to config file ('conf_default', None), # Path to common default config file + ('conf_dir', None), # Directory containing config files # Set in Env._finalize_core(): ('in_server', None), # Whether or not running in-server (bool) -- cgit From 2be9f2bba81f844088d0699910698e187ca9d957 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 10 Nov 2008 20:08:22 -0700 Subject: More tutorial work: made introduction more concise; moved note on markup to end; added note about Bazaar --- ipalib/__init__.py | 102 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 64 insertions(+), 38 deletions(-) (limited to 'ipalib') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index d943a7c7..ecb2d633 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -25,45 +25,32 @@ Package containing core library. Tutorial for Plugin Authors ============================= -This tutorial gives a broad learn-by-doing introduction to writing plugins -for freeIPA v2. As not to overwhelm the reader, it does not cover every -detail, but it does provides enough to get one started and is heavily -cross-referenced with further documentation that (hopefully) fills in the -missing details. +This tutorial will introduce you to writing plugins for freeIPA v2. It does +not cover every detail, but it provides enough to get you started and is +heavily cross-referenced with further documentation that (hopefully) fills +in the missing details. -Where the documentation has left the reader confused, the many built-in -plugins in `ipalib.plugins` and `ipa_server.plugins` provide real-life -examples of how to write good plugins. - -*Note:* - - This tutorial, along with all the Python docstrings in freeIPA v2, - uses the *reStructuredText* markup language. For documentation on - reStructuredText, see: - - http://docutils.sourceforge.net/rst.html - - For documentation on using reStructuredText markup with epydoc, see: - - http://epydoc.sourceforge.net/manual-othermarkup.html +In addition to this tutorial, the many built-in plugins in `ipalib.plugins` +and `ipa_server.plugins` provide real-life examples of how to write good +plugins. ------------------------------------ First steps: A simple command plugin ------------------------------------ -Our first example will create the most basic command plugin possible. A -command plugin simultaneously adds a new command that can be called through +Our first example will create the most basic command plugin possible. This +command will be seen in the list of command plugins, but it wont be capable +of actually doing anything yet. + +A command plugin simultaneously adds a new command that can be called through the command-line ``ipa`` script *and* adds a new XML-RPC method... the two are one in the same, simply invoked in different ways. -All plugins must subclass from `plugable.Plugin`, and furthermore, must -subclass from one of the base classes allowed by the `plugable.API` instance -returned by the `get_standard_api()` function. - -To be a command plugin, your plugin must subclass from `frontend.Command`. -Creating a basic plugin involves two steps, defining the class and then -registering the class: +A freeIPA plugin is a Python class, and when you create a plugin, you register +this class itself (instead of an instance of the class). To be a command +plugin, your plugin must subclass from `frontend.Command` (or from a subclass +thereof). Here is our first example: >>> from ipalib import Command, get_standard_api >>> api = get_standard_api() @@ -72,8 +59,8 @@ registering the class: ... >>> api.register(my_command) # Step 2, register class -Notice that we are registering the ``my_command`` class itself and not an -instance thereof. +Notice that we are registering the ``my_command`` class itself, not an +instance of ``my_command``. Until `plugable.API.finalize()` is called, your plugin class has not been instantiated nor the does the ``Command`` namespace yet exist. For example: @@ -86,8 +73,8 @@ True >>> api.Command.my_command.doc 'My example plugin.' -Notice that your plugin instance in accessed through an attribute named -'my_command', the same name as your plugin class name. +Notice that your plugin instance is accessed through an attribute named +``my_command``, the same name as your plugin class name. ------------------------------ @@ -106,7 +93,7 @@ implement a ``run()`` method, like this: >>> api = get_standard_api() >>> api.register(my_command) >>> api.finalize() ->>> api.Command.my_command() # Call your plugin +>>> api.Command.my_command() # Call your command 'My run() method was called!' When `frontend.Command.__call__()` is called, it first validates any arguments @@ -619,11 +606,11 @@ like this: $ ./ipa plugins ----------------- -Learning more... ----------------- +----------------------------------- +Learning more about freeIPA plugins +----------------------------------- -To learn more about writing plugins, you should: +To learn more about writing freeIPA plugins, you should: 1. Look at some of the built-in plugins, like the frontend plugins in `ipalib.plugins.f_user` and the backend plugins in @@ -632,6 +619,45 @@ To learn more about writing plugins, you should: 2. Learn about the base classes for frontend plugins in `ipalib.frontend`. 3. Learn about the core plugin framework in `ipalib.plugable`. + +Furthermore, the freeIPA plugin architecture was inspired by the Bazaar plugin +architecture. Although the two are different enough that learning how to +write plugins for Bazaar will not particularly help you write plugins for +freeIPA, some might be interested in the documentation on writing plugins for +Bazaar, available here: + + http://bazaar-vcs.org/WritingPlugins + +If nothing else, we just want to give credit where credit is deserved! +However, freeIPA does not use any *code* from Bazaar... it merely borrows a +little inspiration. + + +-------------------------- +A note on docstring markup +-------------------------- + +Lastly, a quick note on markup: All the Python docstrings in freeIPA v2 +(including this tutorial) use the *reStructuredText* markup language. For +information on reStructuredText, see: + + http://docutils.sourceforge.net/rst.html + +For information on using reStructuredText markup with epydoc, see: + + http://epydoc.sourceforge.net/manual-othermarkup.html + + +-------------------------------------------------- +Next steps: get involved with freeIPA development! +-------------------------------------------------- + +The freeIPA team is always interested in feedback and contribution from the +community. To get involved with freeIPA, see the *Contribute* page on +freeIPA.org: + + http://freeipa.org/page/Contribute + ''' import plugable -- cgit From 16b86d559a9c80db3634987226cf8766a3a86cd9 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 10 Nov 2008 21:33:15 -0700 Subject: Tutorial: added intro section about Python interactive intepreter --- ipalib/__init__.py | 66 ++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 57 insertions(+), 9 deletions(-) (limited to 'ipalib') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index ecb2d633..86a0c277 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -19,7 +19,7 @@ ''' -Package containing core library. +Package containing the core library. ============================= Tutorial for Plugin Authors @@ -35,6 +35,54 @@ and `ipa_server.plugins` provide real-life examples of how to write good plugins. +---------------------------- +How this tutorial is written +---------------------------- + +The code examples in this tutorial are presented as if entered into a Python +interactive interpreter session. As such, when you create a real plugin in +a source file, a few details will be different (in addition to the fact that +you will never include the ``>>>`` nor ``...`` at the beginning of each line +of code). + +The tutorial examples all have this pattern: + + :: + + >>> from ipalib import Command, get_standard_api + >>> api = get_standard_api() + >>> class my_command(Command): + ... pass + ... + >>> api.register(my_command) + >>> api.finalize() + +We call `get_standard_api()` to get an *example* instance of `plugable.API` +to work with. But a real plugin will use the standard *run-time* instance +of `plugable.API`, which is available at ``ipalib.api``. + +A real plugin will have this pattern: + + :: + + from ipalib import Command, api + + class my_command(Command): + pass + api.register(my_command) + +The differences are that in a real plugin you will use the standard +``ipalib.api`` instance of `plugable.API` and that you will *not* call +`plugable.API.finalize()`. When in doubt, look at some of the built-in +plugins for guidance, like those in `ipalib.plugins`. + +If don't know what the Python *interactive interpreter* is, or are confused +about what this *Python* is in the first place, then you probably should start +with the Python tutorial: + + http://docs.python.org/tutorial/index.html + + ------------------------------------ First steps: A simple command plugin ------------------------------------ @@ -67,7 +115,7 @@ instantiated nor the does the ``Command`` namespace yet exist. For example: >>> hasattr(api, 'Command') False ->>> api.finalize() +>>> api.finalize() # plugable.API.finalize() >>> hasattr(api.Command, 'my_command') True >>> api.Command.my_command.doc @@ -561,7 +609,7 @@ terminal, like this: :: - $ ./ipa console + $ ./ipa console By default, ``in_server`` is False. If you want to start the console in a server context (so that all the backend plugins are loaded), you can use the @@ -569,41 +617,41 @@ server context (so that all the backend plugins are loaded), you can use the :: - $ ./ipa -e in_server=True console + $ ./ipa -e in_server=True console You can specify multiple environment variables by including the ``-e`` option multiple times, like this: :: - $ ./ipa -e in_server=True -e mode=dummy console + $ ./ipa -e in_server=True -e mode=dummy console The space after the ``-e`` is optional. This is equivalent to the above command: :: - $ ./ipa -ein_server=True -emode=dummy console + $ ./ipa -ein_server=True -emode=dummy console The ``env`` command will print out the full environment in key=value pairs, like this: :: - $ ./ipa env + $ ./ipa env If you use the ``--server`` option, it will forward the call to the server over XML-RPC and print out what the environment is on the server, like this: :: - $ ./ipa env --server + $ ./ipa env --server The ``plugins`` command will show details of all the plugin that are loaded, like this: :: - $ ./ipa plugins + $ ./ipa plugins ----------------------------------- -- cgit From 786c965c12dccbf04e1bb2e8e9786decc8163e80 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 10 Nov 2008 21:36:56 -0700 Subject: Tutorial: fixed typo --- ipalib/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'ipalib') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index 86a0c277..da849e7d 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -76,9 +76,9 @@ The differences are that in a real plugin you will use the standard `plugable.API.finalize()`. When in doubt, look at some of the built-in plugins for guidance, like those in `ipalib.plugins`. -If don't know what the Python *interactive interpreter* is, or are confused -about what this *Python* is in the first place, then you probably should start -with the Python tutorial: +If you don't know what the Python *interactive interpreter* is, or are +confused about what this *Python* is in the first place, then you probably +should start with the Python tutorial: http://docs.python.org/tutorial/index.html -- cgit From 18945135747b15a98b64ddcf92d0847099469208 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 11 Nov 2008 10:24:30 -0700 Subject: Tutorial: improved clarity of 'How this tutorial is written' section --- ipalib/__init__.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) (limited to 'ipalib') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index da849e7d..71d78dc4 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -42,8 +42,8 @@ How this tutorial is written The code examples in this tutorial are presented as if entered into a Python interactive interpreter session. As such, when you create a real plugin in a source file, a few details will be different (in addition to the fact that -you will never include the ``>>>`` nor ``...`` at the beginning of each line -of code). +you will never include the ``>>>`` nor ``...`` that the interpreter places at +the beginning of each line of code). The tutorial examples all have this pattern: @@ -57,9 +57,9 @@ The tutorial examples all have this pattern: >>> api.register(my_command) >>> api.finalize() -We call `get_standard_api()` to get an *example* instance of `plugable.API` -to work with. But a real plugin will use the standard *run-time* instance -of `plugable.API`, which is available at ``ipalib.api``. +In the tutorial we call `get_standard_api()` to create an *example* instance +of `plugable.API` to work with. But a real plugin will simply use +``ipalib.api``, the standard run-time instance of `plugable.API`. A real plugin will have this pattern: @@ -71,8 +71,7 @@ A real plugin will have this pattern: pass api.register(my_command) -The differences are that in a real plugin you will use the standard -``ipalib.api`` instance of `plugable.API` and that you will *not* call +As seen above, also note that in a real plugin you will *not* call `plugable.API.finalize()`. When in doubt, look at some of the built-in plugins for guidance, like those in `ipalib.plugins`. -- cgit From f3869d7b24f65ca04494ff756e092d7aedd67a5c Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 11 Nov 2008 15:24:18 -0700 Subject: Renamed ipalib.get_standard_api() to create_api() --- ipalib/__init__.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) (limited to 'ipalib') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index 71d78dc4..4ebb7a1e 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -49,15 +49,15 @@ The tutorial examples all have this pattern: :: - >>> from ipalib import Command, get_standard_api - >>> api = get_standard_api() + >>> from ipalib import Command, create_api + >>> api = create_api() >>> class my_command(Command): ... pass ... >>> api.register(my_command) >>> api.finalize() -In the tutorial we call `get_standard_api()` to create an *example* instance +In the tutorial we call `create_api()` to create an *example* instance of `plugable.API` to work with. But a real plugin will simply use ``ipalib.api``, the standard run-time instance of `plugable.API`. @@ -99,8 +99,8 @@ this class itself (instead of an instance of the class). To be a command plugin, your plugin must subclass from `frontend.Command` (or from a subclass thereof). Here is our first example: ->>> from ipalib import Command, get_standard_api ->>> api = get_standard_api() +>>> from ipalib import Command, create_api +>>> api = create_api() >>> class my_command(Command): # Step 1, define class ... """My example plugin.""" ... @@ -137,7 +137,7 @@ implement a ``run()`` method, like this: ... def run(self): ... return 'My run() method was called!' ... ->>> api = get_standard_api() +>>> api = create_api() >>> api.register(my_command) >>> api.finalize() >>> api.Command.my_command() # Call your command @@ -184,7 +184,7 @@ For example, say you have a command plugin like this: If ``my_command`` is loaded in a *client* context, ``forward()`` will be called: ->>> api = get_standard_api() +>>> api = create_api() >>> api.env.in_server = False # run() will dispatch to forward() >>> api.register(my_command) >>> api.finalize() @@ -194,7 +194,7 @@ called: On the other hand, if ``my_command`` is loaded in a *server* context, ``execute()`` will be called: ->>> api = get_standard_api() +>>> api = create_api() >>> api.env.in_server = True # run() will dispatch to execute() >>> api.register(my_command) >>> api.finalize() @@ -253,7 +253,7 @@ Here is a simple example: ... """Part of your API.""" ... return 'Stuff got done.' ... ->>> api = get_standard_api() +>>> api = create_api() >>> api.register(my_backend) >>> api.finalize() >>> api.Backend.my_backend.do_stuff() @@ -296,7 +296,7 @@ plugin can also access the ``my_backend`` plugin as simply This next example will tie everything together. First we create our backend plugin: ->>> api = get_standard_api() +>>> api = create_api() >>> api.env.in_server = True # We want to execute, not forward >>> class my_backend(Backend): ... """My example backend plugin.""" @@ -330,7 +330,7 @@ it never tries to access the non-existent backend plugin at ``self.Backend.my_backend.`` To emphasize this point, here is one last example: ->>> api = get_standard_api() +>>> api = create_api() >>> api.env.in_server = False # We want to forward, not execute >>> class my_command(Command): ... """My example command plugin.""" @@ -364,7 +364,7 @@ It can be useful to have your ``execute()`` method call other command plugins. Among other things, this allows for meta-commands that conveniently call several other commands in a single operation. For example: ->>> api = get_standard_api() +>>> api = create_api() >>> api.env.in_server = True # We want to execute, not forward >>> class meta_command(Command): ... """My meta-command plugin.""" @@ -416,7 +416,7 @@ For example: ... def execute(self, programmer, **kw): ... return '%s, go write more %s!' % (programmer, kw['stuff']) ... ->>> api = get_standard_api() +>>> api = create_api() >>> api.env.in_server = True >>> api.register(nudge) >>> api.finalize() @@ -528,7 +528,7 @@ When you create a fresh `plugable.API` instance, its ``env`` attribute is likewise a freshly created `config.Env` instance, which will already be populated with certain run-time information. For example: ->>> api = get_standard_api() +>>> api = create_api() >>> list(api.env) ['bin', 'dot_ipa', 'home', 'ipalib', 'mode', 'script', 'site_packages'] @@ -713,7 +713,7 @@ from frontend import Command, Object, Method, Property, Application from ipa_types import Bool, Int, Unicode, Enum from frontend import Param, DefaultFrom -def get_standard_api(mode='dummy'): +def create_api(mode='dummy'): """ Return standard `plugable.API` instance. @@ -743,4 +743,4 @@ def get_standard_api(mode='dummy'): return api -api = get_standard_api(mode=None) +api = create_api(mode=None) -- cgit From 014af24731ff39520a9635694ed99dc9d09669c9 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 12 Nov 2008 00:46:04 -0700 Subject: Changed calling signature of output_for_cli(); started work on 'textui' backend plugin --- ipalib/cli.py | 219 ++++++++++++++++++++++++++++++++++++++++------- ipalib/constants.py | 2 + ipalib/plugins/f_misc.py | 48 ++++------- 3 files changed, 206 insertions(+), 63 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index d7288154..8878c212 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -22,20 +22,20 @@ Functionality for Command Line Interface. """ import re +import textwrap import sys import code import optparse import socket import frontend +import backend import errors import plugable import ipa_types from config import set_default_env, read_config import util - -def exit_error(error): - sys.exit('ipa: ERROR: %s' % error) +from constants import CLI_TAB def to_cli(name): @@ -55,21 +55,189 @@ def from_cli(cli_name): return str(cli_name).replace('-', '_') -class text_ui(frontend.Application): +class textui(backend.Backend): """ - Base class for CLI commands with special output needs. + Backend plugin to nicely format output to stdout. """ - def print_dashed(self, string, top=True, bottom=True): + def get_tty_width(self): + """ + Return the width (in characters) of output tty. + + If stdout is not a tty, this method will return ``None``. + """ + if sys.stdout.isatty(): + return 80 # FIXME: we need to return the actual tty width + + def max_col_width(self, rows, col=None): + """ + Return the max width (in characters) of a specified column. + + For example: + + >>> ui = textui() + >>> rows = [ + ... ('a', 'package'), + ... ('an', 'egg'), + ... ] + >>> ui.max_col_width(rows, col=0) # len('an') + 2 + >>> ui.max_col_width(rows, col=1) # len('package') + 7 + >>> ui.max_col_width(['a', 'cherry', 'py']) # len('cherry') + 6 + """ + if type(rows) not in (list, tuple): + raise TypeError( + 'rows: need %r or %r; got %r' % (list, tuple, rows) + ) + if len(rows) == 0: + return 0 + if col is None: + return max(len(row) for row in rows) + return max(len(row[col]) for row in rows) + + def print_dashed(self, string, above=True, below=True): + """ + Print a string with with a dashed line above and/or below. + + For example: + + >>> ui = textui() + >>> ui.print_dashed('Dashed above and below.') + ----------------------- + Dashed above and below. + ----------------------- + >>> ui.print_dashed('Only dashed below.', above=False) + Only dashed below. + ------------------ + >>> ui.print_dashed('Only dashed above.', below=False) + ------------------ + Only dashed above. + """ dashes = '-' * len(string) - if top: + if above: print dashes print string - if bottom: + if below: print dashes - def print_name(self, **kw): - self.print_dashed('%s:' % self.name, **kw) + def print_line(self, text, width=None): + """ + Force printing on a single line, using ellipsis if needed. + + For example: + + >>> ui = textui() + >>> ui.print_line('This line can fit!', width=18) + This line can fit! + >>> ui.print_line('This line wont quite fit!', width=18) + This line wont ... + + The above example aside, you normally should not specify the + ``width``. When you don't, it is automatically determined by calling + `textui.get_tty_width()`. + """ + if width is None: + width = self.get_tty_width() + if width is not None and width < len(text): + text = text[:width - 3] + '...' + print text + + def print_indented(self, text, indent=1): + """ + Print at specified indentation level. + + For example: + + >>> ui = textui() + >>> ui.print_indented('One indentation level.') + One indentation level. + >>> ui.print_indented('Two indentation levels.', indent=2) + Two indentation levels. + >>> ui.print_indented('No indentation.', indent=0) + No indentation. + """ + print (CLI_TAB * indent + text) + + def print_name(self, name): + """ + Print a command name. + + The typical use for this is to mark the start of output from a + command. For example, a hypothetical ``show_status`` command would + output something like this: + + >>> ui = textui() + >>> ui.print_name('show_status') + ------------ + show-status: + ------------ + """ + self.print_dashed('%s:' % to_cli(name)) + + def print_keyval(self, rows, indent=1): + """ + Print (key = value) pairs, one pair per line. + + For example: + + >>> items = [ + ... ('in_server', True), + ... ('mode', 'production'), + ... ] + >>> ui = textui() + >>> ui.print_keyval(items) + in_server = True + mode = 'production' + >>> ui.print_keyval(items, indent=0) + in_server = True + mode = 'production' + + Also see `textui.print_indented`. + """ + for row in rows: + self.print_indented('%s = %r' % row, indent) + + def print_count(self, count, singular, plural=None): + """ + Print a summary count. + + The typical use for this is to print the number of items returned + by a command, especially when this return count can vary. This + preferably should be used as a summary and should be the final text + a command outputs. + + For example: + + >>> ui = textui() + >>> ui.print_count(1, '%d goose', '%d geese') + ------- + 1 goose + ------- + >>> ui.print_count(['Don', 'Sue'], 'Found %d user', 'Found %d users') + ------------- + Found 2 users + ------------- + + If ``count`` is not an integer, it must be a list or tuple, and then + ``len(count)`` is used as the count. + """ + if type(count) is not int: + assert type(count) in (list, tuple) + count = len(count) + self.print_dashed( + self.choose_number(count, singular, plural) + ) + + def choose_number(self, n, singular, plural=None): + if n == 1 or plural is None: + return singular % n + return plural % n + + +def exit_error(error): + sys.exit('ipa: ERROR: %s' % error) class help(frontend.Application): @@ -87,10 +255,8 @@ class help(frontend.Application): self.application.build_parser(cmd).print_help() - - class console(frontend.Application): - 'Start the IPA interactive Python console.' + """Start the IPA interactive Python console.""" def run(self): code.interact( @@ -99,7 +265,7 @@ class console(frontend.Application): ) -class show_api(text_ui): +class show_api(frontend.Application): 'Show attributes on dynamic API object' takes_args = ('namespaces*',) @@ -153,7 +319,7 @@ class show_api(text_ui): self.__traverse_namespace(n, attr, lines, tab + 2) -class plugins(text_ui): +class plugins(frontend.Application): """Show all loaded plugins""" def run(self): @@ -162,21 +328,13 @@ class plugins(text_ui): (p.plugin, p.bases) for p in plugins ) - def output_for_cli(self, result): - self.print_name() - first = True + def output_for_cli(self, textui, result, **kw): + textui.print_name(self.name) for (plugin, bases) in result: - if first: - first = False - else: - print '' - print ' Plugin: %s' % plugin - print ' In namespaces: %s' % ', '.join(bases) - if len(result) == 1: - s = '1 plugin loaded.' - else: - s = '%d plugins loaded.' % len(result) - self.print_dashed(s) + textui.print_indented( + '%s: %s' % (plugin, ', '.join(bases)) + ) + textui.print_count(result, '%d plugin loaded', '%s plugins loaded') cli_application_commands = ( @@ -293,6 +451,7 @@ class CLI(object): self.api.load_plugins() for klass in cli_application_commands: self.api.register(klass) + self.api.register(textui) def bootstrap(self): """ @@ -376,7 +535,7 @@ class CLI(object): try: ret = cmd(**kw) if callable(cmd.output_for_cli): - cmd.output_for_cli(ret) + cmd.output_for_cli(self.api.Backend.textui, ret, **kw) return 0 except socket.error, e: print e[1] diff --git a/ipalib/constants.py b/ipalib/constants.py index b8f93d21..6210e6c8 100644 --- a/ipalib/constants.py +++ b/ipalib/constants.py @@ -22,6 +22,8 @@ All constants centralized in one file. """ +# Used for a tab (or indentation level) when formatting for CLI: +CLI_TAB = ' ' # Two spaces # The section to read in the config files, i.e. [global] CONFIG_SECTION = 'global' diff --git a/ipalib/plugins/f_misc.py b/ipalib/plugins/f_misc.py index ff8569b1..055e54d7 100644 --- a/ipalib/plugins/f_misc.py +++ b/ipalib/plugins/f_misc.py @@ -24,22 +24,11 @@ Misc frontend plugins. from ipalib import api, Command, Param, Bool -class env_and_context(Command): - """ - Base class for `env` and `context` commands. - """ - - def run(self, **kw): - if kw.get('server', False) and not self.api.env.in_server: - return self.forward() - return self.execute() - - def output_for_cli(self, ret): - for (key, value) in ret: - print '%s = %r' % (key, value) - - -class env(env_and_context): +# FIXME: We should not let env return anything in_server +# when mode == 'production'. This would allow an attacker to see the +# configuration of the server, potentially revealing compromising +# information. However, it's damn handy for testing/debugging. +class env(Command): """Show environment variables""" takes_options = ( @@ -48,26 +37,19 @@ class env(env_and_context): ), ) + def run(self, **kw): + if kw.get('server', False) and not self.api.env.in_server: + return self.forward() + return self.execute() + def execute(self): return tuple( (key, self.api.env[key]) for key in self.api.env ) -api.register(env) - - -class context(env_and_context): - """Show request context""" - - takes_options = ( - Param('server?', type=Bool(), default=False, - doc='Show request context in server', - ), - ) - - def execute(self): - return [ - (key, self.api.context[key]) for key in self.api.Context - ] + def output_for_cli(self, textui, result, **kw): + textui.print_name(self.name) + textui.print_keyval(result) + textui.print_count(result, '%d variable', '%d variables') -api.register(context) +api.register(env) -- cgit From 09161e399a61e2a548e9efb3c3abb2c7b47d5520 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 12 Nov 2008 01:47:37 -0700 Subject: Command.get_default() will now fill-in None for all missing non-required params --- ipalib/frontend.py | 9 ++++++--- ipalib/plugins/b_xmlrpc.py | 4 ++++ ipalib/plugins/f_misc.py | 27 +++++++++++++++++++-------- ipalib/util.py | 6 ++++++ 4 files changed, 35 insertions(+), 11 deletions(-) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 62a503cc..ce4168bc 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -639,8 +639,11 @@ class Command(plugable.Plugin): Generator method used by `Command.get_default`. """ for param in self.params(): - if param.required and kw.get(param.name, None) is None: - yield (param.name, param.get_default(**kw)) + if kw.get(param.name, None) is None: + if param.required: + yield (param.name, param.get_default(**kw)) + else: + yield (param.name, None) def validate(self, **kw): """ @@ -694,7 +697,7 @@ class Command(plugable.Plugin): """ Forward call over XML-RPC to this same command on server. """ - return self.api.Backend.xmlrpc.forward_call(self.name, *args, **kw) + return self.Backend.xmlrpc.forward_call(self.name, *args, **kw) def finalize(self): """ diff --git a/ipalib/plugins/b_xmlrpc.py b/ipalib/plugins/b_xmlrpc.py index 87dc9505..22361b1b 100644 --- a/ipalib/plugins/b_xmlrpc.py +++ b/ipalib/plugins/b_xmlrpc.py @@ -43,6 +43,10 @@ class xmlrpc(Backend): """ Return an xmlrpclib.ServerProxy instance (the client). """ + # FIXME: Rob, is there any reason we can't use allow_none=True here? + # Are there any reasonably common XML-RPC client implementations + # that don't support the extension? + # See: http://docs.python.org/library/xmlrpclib.html uri = self.api.env.xmlrpc_uri if uri.startswith('https://'): return xmlrpclib.ServerProxy(uri, diff --git a/ipalib/plugins/f_misc.py b/ipalib/plugins/f_misc.py index 055e54d7..1acf1c99 100644 --- a/ipalib/plugins/f_misc.py +++ b/ipalib/plugins/f_misc.py @@ -31,23 +31,34 @@ from ipalib import api, Command, Param, Bool class env(Command): """Show environment variables""" + takes_args = ('variables*',) + takes_options = ( Param('server?', type=Bool(), default=False, doc='Show environment variables of server', ), ) - def run(self, **kw): - if kw.get('server', False) and not self.api.env.in_server: - return self.forward() - return self.execute() + def run(self, variables, **kw): + if kw['server'] and not self.env.in_server: + return self.forward(variables) + return self.execute(variables) + + def find_keys(self, variables): + for key in variables: + if key in self.env: + yield (key, self.env[key]) - def execute(self): - return tuple( - (key, self.api.env[key]) for key in self.api.env - ) + def execute(self, variables): + if variables is None: + return tuple( + (key, self.env[key]) for key in self.env + ) + return tuple(self.find_keys(variables)) def output_for_cli(self, textui, result, **kw): + if len(result) == 0: + return textui.print_name(self.name) textui.print_keyval(result) textui.print_count(result, '%d variable', '%d variables') diff --git a/ipalib/util.py b/ipalib/util.py index 12f9c781..9bc43254 100644 --- a/ipalib/util.py +++ b/ipalib/util.py @@ -34,6 +34,12 @@ def xmlrpc_marshal(*args, **kw): """ Marshal (args, kw) into ((kw,) + args). """ + kw = dict( + filter(lambda item: item[1] is not None, kw.iteritems()) + ) + args = tuple( + filter(lambda value: value is not None, args) + ) return ((kw,) + args) -- cgit From f04aaff97c9c8c22b36706f2c6d4de6f23d06b95 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 12 Nov 2008 09:55:11 -0700 Subject: output_for_cli signature is now output_for_cli(textui, result, *args, **options) --- ipalib/cli.py | 3 ++- ipalib/frontend.py | 16 ++++++++++------ ipalib/plugins/f_misc.py | 6 +++--- 3 files changed, 15 insertions(+), 10 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index 8878c212..d86647c6 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -535,7 +535,8 @@ class CLI(object): try: ret = cmd(**kw) if callable(cmd.output_for_cli): - cmd.output_for_cli(self.api.Backend.textui, ret, **kw) + (args, options) = cmd.params_2_args_options(kw) + cmd.output_for_cli(self.api.Backend.textui, ret, *args, **options) return 0 except socket.error, e: print e[1] diff --git a/ipalib/frontend.py b/ipalib/frontend.py index ce4168bc..56c4ea01 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -511,7 +511,7 @@ class Command(plugable.Plugin): 'options', 'params', 'args_to_kw', - 'kw_to_args', + 'params_2_args_options', 'output_for_cli', )) takes_options = tuple() @@ -536,8 +536,8 @@ class Command(plugable.Plugin): kw = self.convert(**kw) kw.update(self.get_default(**kw)) self.validate(**kw) - args = tuple(kw.pop(name) for name in self.args) - return self.run(*args, **kw) + (args, options) = self.params_2_args_options(kw) + return self.run(*args, **options) def args_to_kw(self, *values): """ @@ -569,11 +569,15 @@ class Command(plugable.Plugin): else: break - def kw_to_args(self, **kw): + def params_2_args_options(self, params): """ - Map keyword into positional arguments. + Split params into (args, kw). """ - return tuple(kw.get(name, None) for name in self.args) + args = tuple(params.get(name, None) for name in self.args) + options = dict( + (name, params.get(name, None)) for name in self.options + ) + return (args, options) def normalize(self, **kw): """ diff --git a/ipalib/plugins/f_misc.py b/ipalib/plugins/f_misc.py index 1acf1c99..05fd6d52 100644 --- a/ipalib/plugins/f_misc.py +++ b/ipalib/plugins/f_misc.py @@ -39,8 +39,8 @@ class env(Command): ), ) - def run(self, variables, **kw): - if kw['server'] and not self.env.in_server: + def run(self, variables, **options): + if options['server'] and not self.env.in_server: return self.forward(variables) return self.execute(variables) @@ -56,7 +56,7 @@ class env(Command): ) return tuple(self.find_keys(variables)) - def output_for_cli(self, textui, result, **kw): + def output_for_cli(self, textui, result, variables, **options): if len(result) == 0: return textui.print_name(self.name) -- cgit From 01a7f1f437b72c2c13c6abfb02c6dea3924fa291 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 12 Nov 2008 10:15:24 -0700 Subject: Calling ./ipa with no command now calls Command.help() --- ipalib/cli.py | 51 +++++++++++++++++++++++++++------------------------ 1 file changed, 27 insertions(+), 24 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index d86647c6..febf399d 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -241,12 +241,16 @@ def exit_error(error): class help(frontend.Application): - 'Display help on a command.' + '''Display help on a command.''' - takes_args = ['command'] + takes_args = ['command?'] - def run(self, key): - key = str(key) + def run(self, command): + textui = self.Backend.textui + if command is None: + self.print_commands() + return + key = str(command) if key not in self.application: print 'help: no such command %r' % key sys.exit(2) @@ -254,6 +258,24 @@ class help(frontend.Application): print 'Purpose: %s' % cmd.doc self.application.build_parser(cmd).print_help() + def print_commands(self): + std = set(self.Command) - set(self.Application) + print '\nStandard IPA commands:' + for key in sorted(std): + cmd = self.api.Command[key] + self.print_cmd(cmd) + print '\nSpecial CLI commands:' + for cmd in self.api.Application(): + self.print_cmd(cmd) + print '\nUse the --help option to see all the global options' + print '' + + def print_cmd(self, cmd): + print ' %s %s' % ( + to_cli(cmd.name).ljust(self.application.mcl), + cmd.doc, + ) + class console(frontend.Application): """Start the IPA interactive Python console.""" @@ -406,12 +428,9 @@ class CLI(object): if self.api.env.mode == 'unit_test': return if len(self.cmd_argv) < 1: - self.print_commands() - print 'Usage: ipa [global-options] COMMAND' - sys.exit(2) + sys.exit(self.api.Command.help()) key = self.cmd_argv[0] if key not in self: - self.print_commands() print 'ipa: ERROR: unknown command %r' % key sys.exit(2) return self.run_cmd(self[key]) @@ -505,23 +524,7 @@ class CLI(object): ) self.__done.add(name) - def print_commands(self): - std = set(self.api.Command) - set(self.api.Application) - print '\nStandard IPA commands:' - for key in sorted(std): - cmd = self.api.Command[key] - self.print_cmd(cmd) - print '\nSpecial CLI commands:' - for cmd in self.api.Application(): - self.print_cmd(cmd) - print '\nUse the --help option to see all the global options' - print '' - def print_cmd(self, cmd): - print ' %s %s' % ( - to_cli(cmd.name).ljust(self.mcl), - cmd.doc, - ) def run_cmd(self, cmd): kw = self.parse(cmd) -- cgit From 8ad5502354a364db606b72455c5514cb56e918ba Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 13 Nov 2008 21:07:47 -0700 Subject: Added util.make_repr() function; added corresponding unit tests --- ipalib/frontend.py | 1 + ipalib/util.py | 9 +++++++++ 2 files changed, 10 insertions(+) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 56c4ea01..87126a44 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -227,6 +227,7 @@ class Param(plugable.ReadOnly): ) def __init__(self, name, **override): + self.__override = override if not ('required' in override or 'multivalue' in override): (name, kw_from_spec) = parse_param_spec(name) override.update(kw_from_spec) diff --git a/ipalib/util.py b/ipalib/util.py index 9bc43254..89e2c5a7 100644 --- a/ipalib/util.py +++ b/ipalib/util.py @@ -137,3 +137,12 @@ class LogFormatter(logging.Formatter): Log formatter that uses UTC for all timestamps. """ converter = time.gmtime + + +def make_repr(name, *args, **kw): + """ + Construct a standard representation of a class instance. + """ + args = [repr(a) for a in args] + kw = ['%s=%r' % (k, kw[k]) for k in sorted(kw)] + return '%s(%s)' % (name, ', '.join(args + kw)) -- cgit From 1f635269e8c0253230c3d20b6b41ccd91e02f361 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 13 Nov 2008 21:17:33 -0700 Subject: Param.__repr__() now uses util.make_repr() --- ipalib/frontend.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 87126a44..f3c0f0fa 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -28,6 +28,7 @@ from plugable import lock, check_name import errors from errors import check_type, check_isinstance, raise_TypeError import ipa_types +from util import make_repr RULE_FLAG = 'validation_rule' @@ -450,11 +451,10 @@ class Param(plugable.ReadOnly): return value def __repr__(self): - return '%s(%r, %s())' % ( - self.__class__.__name__, - self.name, - self.type.name, - ) + """ + Return an expresion that could construct this `Param` instance. + """ + return make_repr(self.__class__.__name__, self.name, **self.__override) def create_param(spec): -- cgit From 860d391f3e905e20ba3f409c92d98e68450f3137 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 13 Nov 2008 22:16:04 -0700 Subject: Change Param.__repr__() so it returns the exact expression that could create it; added unit test for Param.__repre__() --- ipalib/__init__.py | 4 ++-- ipalib/frontend.py | 30 +++++++++++++++++------------- 2 files changed, 19 insertions(+), 15 deletions(-) (limited to 'ipalib') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index 4ebb7a1e..59d725f5 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -431,11 +431,11 @@ containing a command's arguments and options, respectively, as you can see: >>> list(api.Command.nudge.args) # Iterates through argument names ['programmer'] >>> api.Command.nudge.args.programmer -Param('programmer', Unicode()) +Param('programmer') >>> list(api.Command.nudge.options) # Iterates through option names ['stuff'] >>> api.Command.nudge.options.stuff -Param('stuff', Unicode()) +Param('stuff', default=u'documentation') >>> api.Command.nudge.options.stuff.default u'documentation' diff --git a/ipalib/frontend.py b/ipalib/frontend.py index f3c0f0fa..3e04db51 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -228,34 +228,34 @@ class Param(plugable.ReadOnly): ) def __init__(self, name, **override): + self.__param_spec = name self.__override = override + self.__kw = dict(self.__defaults) if not ('required' in override or 'multivalue' in override): (name, kw_from_spec) = parse_param_spec(name) - override.update(kw_from_spec) - kw = dict(self.__defaults) - kw['cli_name'] = name - if not set(kw).issuperset(override): - extra = sorted(set(override) - set(kw)) + self.__kw.update(kw_from_spec) + self.__kw['cli_name'] = name + if not set(self.__kw).issuperset(override): + extra = sorted(set(override) - set(self.__kw)) raise TypeError( 'Param.__init__() takes no such kwargs: %s' % ', '.join(extra) ) - kw.update(override) - self.__kw = kw + self.__kw.update(override) self.name = check_name(name) - self.cli_name = check_name(kw.get('cli_name', name)) + self.cli_name = check_name(self.__kw.get('cli_name', name)) self.type = self.__check_isinstance(ipa_types.Type, 'type') self.doc = self.__check_type(str, 'doc') self.required = self.__check_type(bool, 'required') self.multivalue = self.__check_type(bool, 'multivalue') - self.default = kw['default'] - df = kw['default_from'] + self.default = self.__kw['default'] + df = self.__kw['default_from'] if callable(df) and not isinstance(df, DefaultFrom): df = DefaultFrom(df) self.default_from = check_type(df, DefaultFrom, 'default_from', allow_none=True ) - self.flags = frozenset(kw['flags']) - self.__normalize = kw['normalize'] + self.flags = frozenset(self.__kw['flags']) + self.__normalize = self.__kw['normalize'] self.rules = self.__check_type(tuple, 'rules') self.all_rules = (self.type.validate,) + self.rules self.primary_key = self.__check_type(bool, 'primary_key') @@ -454,7 +454,11 @@ class Param(plugable.ReadOnly): """ Return an expresion that could construct this `Param` instance. """ - return make_repr(self.__class__.__name__, self.name, **self.__override) + return make_repr( + self.__class__.__name__, + self.__param_spec, + **self.__override + ) def create_param(spec): -- cgit From f5594dd489317dc406d20f897fc720e0cf89c9d2 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 13 Nov 2008 23:29:35 -0700 Subject: Started work on cleaning up how exceptions are caught and sys.exit() is called in ipalib.cli.CLI --- ipalib/cli.py | 97 +++++++++++++++++++++++++++++++------------------------- ipalib/errors.py | 11 +++++++ 2 files changed, 65 insertions(+), 43 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index febf399d..5659cfc0 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -236,10 +236,6 @@ class textui(backend.Backend): return plural % n -def exit_error(error): - sys.exit('ipa: ERROR: %s' % error) - - class help(frontend.Application): '''Display help on a command.''' @@ -252,8 +248,7 @@ class help(frontend.Application): return key = str(command) if key not in self.application: - print 'help: no such command %r' % key - sys.exit(2) + raise errors.UnknownHelpError(key) cmd = self.application[key] print 'Purpose: %s' % cmd.doc self.application.build_parser(cmd).print_help() @@ -398,7 +393,22 @@ class CLI(object): self.argv = tuple(argv) self.__done = set() - def run(self, init_only=False): + def run(self): + """ + Call `CLI.run_real` in a try/except. + """ + self.bootstrap() + try: + self.run_real() + except KeyboardInterrupt: + print '' + self.api.log.info('operation aborted') + sys.exit() + except errors.IPAError, e: + self.api.log.error(unicode(e)) + sys.exit(e.faultCode) + + def run_real(self): """ Parse ``argv`` and potentially run a command. @@ -423,17 +433,42 @@ class CLI(object): remaining initialization needed to use the `plugable.API` instance. """ - self.__doing('run') + self.__doing('run_real') self.finalize() if self.api.env.mode == 'unit_test': return if len(self.cmd_argv) < 1: - sys.exit(self.api.Command.help()) + self.api.Command.help() + return key = self.cmd_argv[0] if key not in self: - print 'ipa: ERROR: unknown command %r' % key - sys.exit(2) - return self.run_cmd(self[key]) + raise errors.UnknownCommandError(key) + self.run_cmd(self[key]) + + # FIXME: Stuff that might need special handling still: +# # Now run the command +# try: +# ret = cmd(**kw) +# if callable(cmd.output_for_cli): +# (args, options) = cmd.params_2_args_options(kw) +# cmd.output_for_cli(self.api.Backend.textui, ret, *args, **options) +# return 0 +# except socket.error, e: +# print e[1] +# return 1 +# except errors.GenericError, err: +# code = getattr(err,'faultCode',None) +# faultString = getattr(err,'faultString',None) +# if not code: +# raise err +# if code < errors.IPA_ERROR_BASE: +# print "%s: %s" % (code, faultString) +# else: +# print "%s: %s" % (code, getattr(err,'__doc__','')) +# return 1 +# except StandardError, e: +# print e +# return 2 def finalize(self): """ @@ -466,7 +501,8 @@ class CLI(object): Finally, all the CLI-specific plugins are registered. """ self.__doing('load_plugins') - self.bootstrap() + if 'bootstrap' not in self.__done: + self.bootstrap() self.api.load_plugins() for klass in cli_application_commands: self.api.register(klass) @@ -524,39 +560,14 @@ class CLI(object): ) self.__done.add(name) - - def run_cmd(self, cmd): kw = self.parse(cmd) - # If options.interactive, interactively validate params: if self.options.interactive: - try: - kw = self.prompt_interactively(cmd, kw) - except KeyboardInterrupt: - return 0 - # Now run the command - try: - ret = cmd(**kw) - if callable(cmd.output_for_cli): - (args, options) = cmd.params_2_args_options(kw) - cmd.output_for_cli(self.api.Backend.textui, ret, *args, **options) - return 0 - except socket.error, e: - print e[1] - return 1 - except errors.GenericError, err: - code = getattr(err,'faultCode',None) - faultString = getattr(err,'faultString',None) - if not code: - raise err - if code < errors.IPA_ERROR_BASE: - print "%s: %s" % (code, faultString) - else: - print "%s: %s" % (code, getattr(err,'__doc__','')) - return 1 - except StandardError, e: - print e - return 2 + kw = self.prompt_interactively(cmd, kw) + result = cmd(**kw) + if callable(cmd.output_for_cli): + (args, options) = cmd.params_2_args_options(kw) + cmd.output_for_cli(self.api.Backend.textui, result, *args, **options) def prompt_interactively(self, cmd, kw): """ diff --git a/ipalib/errors.py b/ipalib/errors.py index c2d83e73..71a837e9 100644 --- a/ipalib/errors.py +++ b/ipalib/errors.py @@ -98,6 +98,7 @@ class IPAError(StandardError): """ format = None + faultCode = 1 def __init__(self, *args): self.args = args @@ -109,6 +110,16 @@ class IPAError(StandardError): return self.format % self.args +class InvocationError(IPAError): + pass + +class UnknownCommandError(InvocationError): + format = 'unknown command "%s"' + +class UnknownHelpError(InvocationError): + format = 'no command nor topic "%s"' + + class ArgumentError(IPAError): """ Raised when a command is called with wrong number of arguments. -- cgit From 82d3de773b2504145cddbcf8a6e5d1abf58fcb12 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 13 Nov 2008 23:54:34 -0700 Subject: Added textui.prompt() method, which CLI.prompt_interactively() uses --- ipalib/cli.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index 5659cfc0..3b365cdb 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -235,6 +235,16 @@ class textui(backend.Backend): return singular % n return plural % n + def prompt(self, label, default=None, get_values=None): + """ + Prompt user for input. + """ + if default is None: + prompt = '%s: ' % label + else: + prompt = '%s [%s]: ' % (label, default) + return raw_input(prompt) + class help(frontend.Application): '''Display help on a command.''' @@ -489,6 +499,7 @@ class CLI(object): self.__d = dict( (c.name.replace('_', '-'), c) for c in self.api.Command() ) + self.textui = self.api.Backend.textui def load_plugins(self): """ @@ -584,15 +595,11 @@ class CLI(object): if not (param.required or self.options.prompt_all): continue default = param.get_default(**kw) - if default is None: - prompt = '%s: ' % param.cli_name - else: - prompt = '%s [%s]: ' % (param.cli_name, default) error = None while True: if error is not None: print '>>> %s: %s' % (param.cli_name, error) - raw = raw_input(prompt) + raw = self.textui.prompt(param.cli_name, default) try: value = param(raw, **kw) if value is not None: -- cgit From 44171a0bad44a16ab78dabcff2e2e1a84c40ee12 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 14 Nov 2008 01:25:05 -0700 Subject: Tutorial: added section on allowed return values from a command's execute() method --- ipalib/__init__.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) (limited to 'ipalib') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index 59d725f5..0c744414 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -486,6 +486,48 @@ For the full details on the parameter system, see the `frontend.Command` classes. +--------------------------------------- +Allowed return values from your command +--------------------------------------- + +The return values from your command can be rendered by different user +interfaces (CLI, web-UI); furthermore, a call to your command can be +transparently forwarded over the network (XML-RPC, JSON). As such, the return +values from your command must be usable by the least common denominator. + +Your command should return only simple data types and simple data structures, +the kind that can be represented in an XML-RPC request or in the JSON format. +The return values from your command's ``execute()`` method can include only +the following: + + Simple scalar values: + These can be ``str``, ``unicode``, ``int``, and ``float`` instances, + plus the ``True``, ``False``, and ``None`` constants. + + Simple compound values: + These can be ``dict``, ``list``, and ``tuple`` instances. These + compound values must contain only the simple scalar values above or + other simple compound values. These compound values can also be empty. + The ``list`` and ``tuple`` types are equivalent and can be used + interchangeably. + +Also note that your ``execute()`` method should not contain any ``print`` +statements or otherwise cause any output on ``sys.stdout``. Your command can +(and should) produce log messages by using ``self.log`` (see below). + +To learn more about XML-RPC (XML Remote Procedure Call), see: + + http://docs.python.org/library/xmlrpclib.html + + http://en.wikipedia.org/wiki/XML-RPC + +To learn more about JSON (Java Script Object Notation), see: + + http://docs.python.org/library/json.html + + http://www.json.org/ + + ------------------------ Logging from your plugin ------------------------ -- cgit From 0313bb7ec09071a9bb124ea625c9936921b8dde7 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 14 Nov 2008 12:19:18 -0700 Subject: Tutorial: added section on implementing an output_for_cli() method --- ipalib/__init__.py | 109 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 105 insertions(+), 4 deletions(-) (limited to 'ipalib') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index 0c744414..356482dc 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -496,7 +496,7 @@ transparently forwarded over the network (XML-RPC, JSON). As such, the return values from your command must be usable by the least common denominator. Your command should return only simple data types and simple data structures, -the kind that can be represented in an XML-RPC request or in the JSON format. +the kinds that can be represented in an XML-RPC request or in the JSON format. The return values from your command's ``execute()`` method can include only the following: @@ -508,8 +508,8 @@ the following: These can be ``dict``, ``list``, and ``tuple`` instances. These compound values must contain only the simple scalar values above or other simple compound values. These compound values can also be empty. - The ``list`` and ``tuple`` types are equivalent and can be used - interchangeably. + For our purposes here, the ``list`` and ``tuple`` types are equivalent + and can be used interchangeably. Also note that your ``execute()`` method should not contain any ``print`` statements or otherwise cause any output on ``sys.stdout``. Your command can @@ -528,6 +528,107 @@ To learn more about JSON (Java Script Object Notation), see: http://www.json.org/ +----------------------------------------- +How your command should output to the CLI +----------------------------------------- + +As noted above, your command should not print anything while in its +``execute()`` method. So how does your command format its output when +called from the ``ipa`` script? + +After the `cli.CLI.run_cmd()` method calls your command, it will call your +command's ``output_for_cli()`` method (if you have implemented one). + +If you implement an ``output_for_cli()`` method, it must have the following +signature: + + :: + + output_for_cli(textui, result, *args, **options) + + textui + An object implementing methods for outputting to the console. + Currently the `ipalib.cli.textui` plugin is passed, which your method + can also access as ``self.Backend.textui``. However, in case this + changes in the future, your method should use the instance passed to + it in this first argument. + + result + This is the return value from calling your command plugin. Depending + upon how your command is implemented, this is probably the return + value from your ``execute()`` method. + + args + The arguments your command was called with. If your command takes no + arguments, you can omit this. You can also explicitly list your + arguments rather than using the generic ``*args`` form. + + options + The options your command was called with. If your command takes no + options, you can omit this. If your command takes any options, you + must use the ``**options`` form as they will be provided strictly as + keyword arguments. + +For example, say we setup a command like this: + +>>> from ipalib import cli +>>> class show_items(Command): +... takes_options = [Param('reverse', type=Bool(), default=False)] +... +... def execute(self, **options): +... items = [ +... ('apple', 'fruit'), +... ('dog', 'pet'), +... ] +... if options['reverse']: +... items.reverse() +... return items +... +... def output_for_cli(self, textui, result, **options): +... textui.print_name(self.name) +... textui.print_keyval(result) +... format = '%d items' +... if options['reverse']: +... format += ' (in reverse order)' +... textui.print_count(result, format) +... +>>> api = create_api() +>>> api.env.in_server = True # We want to execute, not forward. +>>> api.register(show_items) +>>> api.finalize() +>>> textui = cli.textui() # We'll pass this to output_for_cli() + +Calling it through the ``ipa`` script would basically do the following: + +>>> options = dict(reverse=False) +>>> result = api.Command.show_items(**options) +>>> api.Command.show_items.output_for_cli(textui, result, **options) +----------- +show-items: +----------- + apple = 'fruit' + dog = 'pet' +------- +2 items +------- + +Similarly, calling it with ``reverse=True`` would result in the following: + +>>> options = dict(reverse=True) +>>> result = api.Command.show_items(**options) +>>> api.Command.show_items.output_for_cli(textui, result, **options) +----------- +show-items: +----------- + dog = 'pet' + apple = 'fruit' +-------------------------- +2 items (in reverse order) +-------------------------- + +See the `ipalib.cli.textui` plugin for a description of its methods. + + ------------------------ Logging from your plugin ------------------------ @@ -550,7 +651,7 @@ For example: Some basic knowledge of the Python ``logging`` module might be helpful. See: - http://www.python.org/doc/2.5.2/lib/module-logging.html + http://docs.python.org/library/logging.html The important thing to remember is that your plugin should not configure logging itself, but should instead simply use the ``self.log`` logger. -- cgit From 6d1ec6360cd5a7c2b5ad4a6089a1fe98c585036d Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 14 Nov 2008 12:43:10 -0700 Subject: Tutorial: small improvements to section on using output_for_cli() --- ipalib/__init__.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) (limited to 'ipalib') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index 356482dc..52ae6b53 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -528,9 +528,9 @@ To learn more about JSON (Java Script Object Notation), see: http://www.json.org/ ------------------------------------------ -How your command should output to the CLI ------------------------------------------ +--------------------------------------- +How your command should print to stdout +--------------------------------------- As noted above, your command should not print anything while in its ``execute()`` method. So how does your command format its output when @@ -571,7 +571,6 @@ signature: For example, say we setup a command like this: ->>> from ipalib import cli >>> class show_items(Command): ... takes_options = [Param('reverse', type=Bool(), default=False)] ... @@ -596,9 +595,15 @@ For example, say we setup a command like this: >>> api.env.in_server = True # We want to execute, not forward. >>> api.register(show_items) >>> api.finalize() + +Normally `cli.CLI.load_plugins()` will register the `cli.textui` plugin, but for +the sake of our example, we'll just create an instance here: + +>>> from ipalib import cli >>> textui = cli.textui() # We'll pass this to output_for_cli() -Calling it through the ``ipa`` script would basically do the following: +For what we are concerned with in this example, calling your command through +the ``ipa`` script basically will do the following: >>> options = dict(reverse=False) >>> result = api.Command.show_items(**options) -- cgit From c974451edf7895bcac9531a62c5d49c7c4141978 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 14 Nov 2008 13:33:42 -0700 Subject: Added print_plain() and print_paragraph() methods to textui plugin and cleaned up the order of its methods --- ipalib/cli.py | 119 +++++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 77 insertions(+), 42 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index 3b365cdb..69a3c28f 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -97,30 +97,16 @@ class textui(backend.Backend): return max(len(row) for row in rows) return max(len(row[col]) for row in rows) - def print_dashed(self, string, above=True, below=True): - """ - Print a string with with a dashed line above and/or below. - - For example: + def choose_number(self, n, singular, plural=None): + if n == 1 or plural is None: + return singular % n + return plural % n - >>> ui = textui() - >>> ui.print_dashed('Dashed above and below.') - ----------------------- - Dashed above and below. - ----------------------- - >>> ui.print_dashed('Only dashed below.', above=False) - Only dashed below. - ------------------ - >>> ui.print_dashed('Only dashed above.', below=False) - ------------------ - Only dashed above. + def print_plain(self, string): + """ + Print exactly like ``print`` statement would. """ - dashes = '-' * len(string) - if above: - print dashes print string - if below: - print dashes def print_line(self, text, width=None): """ @@ -144,6 +130,35 @@ class textui(backend.Backend): text = text[:width - 3] + '...' print text + def print_paragraph(self, text, width=None): + """ + Print a paragraph, automatically word-wrapping to tty width. + + For example: + + >>> text = ''' + ... Python is a dynamic object-oriented programming language that can + ... be used for many kinds of software development. + ... ''' + >>> ui = textui() + >>> ui.print_paragraph(text, width=45) + Python is a dynamic object-oriented + programming language that can be used for + many kinds of software development. + + The above example aside, you normally should not specify the + ``width``. When you don't, it is automatically determined by calling + `textui.get_tty_width()`. + + The word-wrapping is done using the Python ``textwrap`` module. See: + + http://docs.python.org/library/textwrap.html + """ + if width is None: + width = self.get_tty_width() + for line in textwrap.wrap(text.strip(), width): + print line + def print_indented(self, text, indent=1): """ Print at specified indentation level. @@ -160,22 +175,6 @@ class textui(backend.Backend): """ print (CLI_TAB * indent + text) - def print_name(self, name): - """ - Print a command name. - - The typical use for this is to mark the start of output from a - command. For example, a hypothetical ``show_status`` command would - output something like this: - - >>> ui = textui() - >>> ui.print_name('show_status') - ------------ - show-status: - ------------ - """ - self.print_dashed('%s:' % to_cli(name)) - def print_keyval(self, rows, indent=1): """ Print (key = value) pairs, one pair per line. @@ -199,6 +198,47 @@ class textui(backend.Backend): for row in rows: self.print_indented('%s = %r' % row, indent) + def print_dashed(self, string, above=True, below=True): + """ + Print a string with a dashed line above and/or below. + + For example: + + >>> ui = textui() + >>> ui.print_dashed('Dashed above and below.') + ----------------------- + Dashed above and below. + ----------------------- + >>> ui.print_dashed('Only dashed below.', above=False) + Only dashed below. + ------------------ + >>> ui.print_dashed('Only dashed above.', below=False) + ------------------ + Only dashed above. + """ + dashes = '-' * len(string) + if above: + print dashes + print string + if below: + print dashes + + def print_name(self, name): + """ + Print a command name. + + The typical use for this is to mark the start of output from a + command. For example, a hypothetical ``show_status`` command would + output something like this: + + >>> ui = textui() + >>> ui.print_name('show_status') + ------------ + show-status: + ------------ + """ + self.print_dashed('%s:' % to_cli(name)) + def print_count(self, count, singular, plural=None): """ Print a summary count. @@ -230,11 +270,6 @@ class textui(backend.Backend): self.choose_number(count, singular, plural) ) - def choose_number(self, n, singular, plural=None): - if n == 1 or plural is None: - return singular % n - return plural % n - def prompt(self, label, default=None, get_values=None): """ Prompt user for input. -- cgit From 0f1ed3e904ace46411db549551753005363c30f9 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 14 Nov 2008 14:27:09 -0700 Subject: Tutorial: command in output_for_cli() example now also takes an argument --- ipalib/__init__.py | 78 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 47 insertions(+), 31 deletions(-) (limited to 'ipalib') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index 52ae6b53..da2e711a 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -572,65 +572,81 @@ signature: For example, say we setup a command like this: >>> class show_items(Command): +... +... takes_args = ['key?'] +... ... takes_options = [Param('reverse', type=Bool(), default=False)] ... -... def execute(self, **options): -... items = [ -... ('apple', 'fruit'), -... ('dog', 'pet'), +... def execute(self, key, **options): +... items = dict( +... fruit='apple', +... pet='dog', +... city='Berlin', +... ) +... if key in items: +... return [(key, items[key])] +... return [ +... (k, items[k]) for k in sorted(items, reverse=options['reverse']) ... ] -... if options['reverse']: -... items.reverse() -... return items -... -... def output_for_cli(self, textui, result, **options): -... textui.print_name(self.name) -... textui.print_keyval(result) -... format = '%d items' -... if options['reverse']: -... format += ' (in reverse order)' -... textui.print_count(result, format) +... +... def output_for_cli(self, textui, result, key, **options): +... if key is not None: +... textui.print_keyval(result) +... else: +... textui.print_name(self.name) +... textui.print_keyval(result) +... format = '%d items' +... if options['reverse']: +... format += ' (in reverse order)' +... textui.print_count(result, format) ... >>> api = create_api() >>> api.env.in_server = True # We want to execute, not forward. >>> api.register(show_items) >>> api.finalize() -Normally `cli.CLI.load_plugins()` will register the `cli.textui` plugin, but for -the sake of our example, we'll just create an instance here: +Normally when you invoke the ``ipa`` script, `cli.CLI.load_plugins()` will +register the `cli.textui` backend plugin, but for the sake of our example, +we just create an instance here: >>> from ipalib import cli >>> textui = cli.textui() # We'll pass this to output_for_cli() -For what we are concerned with in this example, calling your command through -the ``ipa`` script basically will do the following: +Now for what we are concerned with in this example, calling your command +through the ``ipa`` script basically will do the following: ->>> options = dict(reverse=False) ->>> result = api.Command.show_items(**options) ->>> api.Command.show_items.output_for_cli(textui, result, **options) +>>> result = api.Command.show_items() +>>> api.Command.show_items.output_for_cli(textui, result, None, reverse=False) ----------- show-items: ----------- - apple = 'fruit' - dog = 'pet' + city = 'Berlin' + fruit = 'apple' + pet = 'dog' ------- -2 items +3 items ------- Similarly, calling it with ``reverse=True`` would result in the following: ->>> options = dict(reverse=True) ->>> result = api.Command.show_items(**options) ->>> api.Command.show_items.output_for_cli(textui, result, **options) +>>> result = api.Command.show_items(reverse=True) +>>> api.Command.show_items.output_for_cli(textui, result, None, reverse=True) ----------- show-items: ----------- - dog = 'pet' - apple = 'fruit' + pet = 'dog' + fruit = 'apple' + city = 'Berlin' -------------------------- -2 items (in reverse order) +3 items (in reverse order) -------------------------- +Lastly, providing a ``key`` would result in the following: + +>>> result = api.Command.show_items('city') +>>> api.Command.show_items.output_for_cli(textui, result, 'city', reverse=False) + city = 'Berlin' + See the `ipalib.cli.textui` plugin for a description of its methods. -- cgit From 1abe3abb87dee628003301c307f4c0d06fe0aa0d Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 14 Nov 2008 14:35:52 -0700 Subject: Tutorial: another small change to section on using output_for_cli() --- ipalib/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'ipalib') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index da2e711a..3e1dba9d 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -584,14 +584,14 @@ For example, say we setup a command like this: ... city='Berlin', ... ) ... if key in items: -... return [(key, items[key])] +... return items[key] ... return [ ... (k, items[k]) for k in sorted(items, reverse=options['reverse']) ... ] ... ... def output_for_cli(self, textui, result, key, **options): ... if key is not None: -... textui.print_keyval(result) +... textui.print_plain('%s = %r' % (key, result)) ... else: ... textui.print_name(self.name) ... textui.print_keyval(result) @@ -645,7 +645,7 @@ Lastly, providing a ``key`` would result in the following: >>> result = api.Command.show_items('city') >>> api.Command.show_items.output_for_cli(textui, result, 'city', reverse=False) - city = 'Berlin' +city = 'Berlin' See the `ipalib.cli.textui` plugin for a description of its methods. -- cgit From caa98476f0abe72c387ec5809bb77568f39a1c33 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 14 Nov 2008 14:49:48 -0700 Subject: Tutorial: fixed typo in 'First steps: A simple command plugin' section --- ipalib/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index 3e1dba9d..5c1e4c3a 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -110,7 +110,7 @@ Notice that we are registering the ``my_command`` class itself, not an instance of ``my_command``. Until `plugable.API.finalize()` is called, your plugin class has not been -instantiated nor the does the ``Command`` namespace yet exist. For example: +instantiated nor does the ``Command`` namespace yet exist. For example: >>> hasattr(api, 'Command') False -- cgit From f8f4058014fda80f776bc177a5fba22009fb5836 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 14 Nov 2008 14:53:55 -0700 Subject: Tutorial: fixed typo in 'How your command should print to stdout' section --- ipalib/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index 5c1e4c3a..a04ce7f8 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -607,7 +607,7 @@ For example, say we setup a command like this: Normally when you invoke the ``ipa`` script, `cli.CLI.load_plugins()` will register the `cli.textui` backend plugin, but for the sake of our example, -we just create an instance here: +we will just create an instance here: >>> from ipalib import cli >>> textui = cli.textui() # We'll pass this to output_for_cli() -- cgit From c513743e7c9a611d0b3b0abaf13b79d6237ed6da Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Fri, 14 Nov 2008 18:04:57 -0500 Subject: Add autmount-specific location and default entries --- ipalib/constants.py | 1 + ipalib/plugins/f_automount.py | 16 ++++++++-------- 2 files changed, 9 insertions(+), 8 deletions(-) (limited to 'ipalib') diff --git a/ipalib/constants.py b/ipalib/constants.py index 6210e6c8..7e562b53 100644 --- a/ipalib/constants.py +++ b/ipalib/constants.py @@ -72,6 +72,7 @@ DEFAULT_CONFIG = ( ('container_service', 'cn=services,cn=accounts'), ('container_host', 'cn=computers,cn=accounts'), ('container_hostgroup', 'cn=hostgroups,cn=accounts'), + ('container_automount', 'cn=automount'), # Ports, hosts, and URIs: ('lite_xmlrpc_port', 8888), diff --git a/ipalib/plugins/f_automount.py b/ipalib/plugins/f_automount.py index fba9e12c..7a251572 100644 --- a/ipalib/plugins/f_automount.py +++ b/ipalib/plugins/f_automount.py @@ -53,7 +53,7 @@ def make_automount_dn(mapname): import ldap return 'automountmapname=%s,%s,%s' % ( ldap.dn.escape_dn_chars(mapname), - api.env.container_accounts, + api.env.container_automount, api.env.basedn, ) @@ -133,7 +133,7 @@ class automount_addkey(crud.Add): ldap = self.api.Backend.ldap # use find_entry_dn instead of make_automap_dn so we can confirm that # the map exists - map_dn = ldap.find_entry_dn("automountmapname", mapname, "automountmap") + map_dn = ldap.find_entry_dn("automountmapname", mapname, "automountmap", api.env.container_automount) kw['dn'] = "automountkey=%s,%s" % (kw['automountkey'], map_dn) kw['objectClass'] = ['automount'] @@ -193,7 +193,7 @@ class automount_delkey(crud.Del): :param kw: "key" the key to be removed """ ldap = self.api.Backend.ldap - dn = ldap.find_entry_dn("automountmapname", mapname, "automountmap") + dn = ldap.find_entry_dn("automountmapname", mapname, "automountmap", api.env.container_automount) keys = api.Command['automount_getkeys'](mapname) keydn = None keyname = kw.get('automountkey').lower() @@ -235,7 +235,7 @@ class automount_modmap(crud.Mod): assert 'automountmapname' not in kw assert 'dn' not in kw ldap = self.api.Backend.ldap - dn = ldap.find_entry_dn("automountmapname", mapname, "automountmap") + dn = ldap.find_entry_dn("automountmapname", mapname, "automountmap", api.env.container_automount) return ldap.update(dn, **kw) def output_for_cli(self, ret): @@ -274,7 +274,7 @@ class automount_modkey(crud.Mod): keyname = kw.get('automountkey').lower() del kw['automountkey'] ldap = self.api.Backend.ldap - dn = ldap.find_entry_dn("automountmapname", mapname, "automountmap") + dn = ldap.find_entry_dn("automountmapname", mapname, "automountmap", api.env.container_automount) keys = api.Command['automount_getkeys'](mapname) keydn = None if keys: @@ -388,7 +388,7 @@ class automount_showmap(crud.Get): :param kw: "all" set to True = return all attributes """ ldap = self.api.Backend.ldap - dn = ldap.find_entry_dn("automountmapname", mapname, "automountmap") + dn = ldap.find_entry_dn("automountmapname", mapname, "automountmap", api.env.container_automount) # FIXME: should kw contain the list of attributes to display? if kw.get('all', False): return ldap.retrieve(dn) @@ -420,7 +420,7 @@ class automount_showkey(crud.Get): :param kw: "all" set to True = return all attributes """ ldap = self.api.Backend.ldap - dn = ldap.find_entry_dn("automountmapname", mapname, "automountmap") + dn = ldap.find_entry_dn("automountmapname", mapname, "automountmap", api.env.container_automount) keys = api.Command['automount_getkeys'](mapname) keyname = kw.get('automountkey').lower() keydn = None @@ -468,7 +468,7 @@ class automount_getkeys(frontend.Command): :param mapname: Retrieve all keys for this mapname """ ldap = self.api.Backend.ldap - dn = ldap.find_entry_dn("automountmapname", mapname, "automountmap") + dn = ldap.find_entry_dn("automountmapname", mapname, "automountmap", api.env.container_automount) try: keys = ldap.get_one_entry(dn, 'objectclass=*', ['automountkey']) except errors.NotFound: -- cgit From 3433840692d294d8c16bd775cfea225b86b4d9e1 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 14 Nov 2008 19:48:01 -0700 Subject: Fixed doctest in tutorial --- ipalib/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index a04ce7f8..53462cff 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -718,7 +718,7 @@ This will typically be called before any plugins are registered. For example: 7 >>> api.bootstrap(in_server=True) # We want to execute, not forward >>> len(api.env) -34 +35 If your plugin requires new environment variables *and* will be included in the freeIPA built-in plugins, you should add the defaults for your variables -- cgit From 36737c2d913716eb99aece5cc1f6a21234abe46a Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 14 Nov 2008 21:29:46 -0700 Subject: Added frontend.LocalOrRemote command base class for commands like env --- ipalib/__init__.py | 3 ++- ipalib/frontend.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index 53462cff..b9a3c96d 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -873,7 +873,8 @@ freeIPA.org: import plugable from backend import Backend, Context -from frontend import Command, Object, Method, Property, Application +from frontend import Command, LocalOrRemote, Application +from frontend import Object, Method, Property from ipa_types import Bool, Int, Unicode, Enum from frontend import Param, DefaultFrom diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 3e04db51..446384a3 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -782,6 +782,37 @@ class Command(plugable.Plugin): yield arg +class LocalOrRemote(Command): + """ + A command that is explicitly executed locally or remotely. + + This is for commands that makes sense to execute either locally or + remotely to return a perhaps different result. The best example of + this is the `ipalib.plugins.f_misc.env` plugin which returns the + key/value pairs describing the configuration state: it can be + """ + + takes_options = ( + Param('server', type=ipa_types.Bool(), default=False, + doc='Forward to server instead of running locally', + ), + ) + + def run(self, *args, **options): + """ + Dispatch to forward() or execute() based on ``server`` option. + + When running in a client context, this command is executed remotely if + ``options['server']`` is true; otherwise it is executed locally. + + When running in a server context, this command is always executed + locally and the value of ``options['server']`` is ignored. + """ + if options['server'] and not self.env.in_server: + return self.forward(*args, **options) + return self.execute(*args, **options) + + class Object(plugable.Plugin): __public__ = frozenset(( 'backend', -- cgit From 9de56d43f054bc5e509e38bda1a048e5af6d73d7 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 14 Nov 2008 21:58:39 -0700 Subject: env plugin now subclasses from RemoteOrLocal --- ipalib/frontend.py | 2 +- ipalib/plugins/f_misc.py | 24 ++++++++---------------- 2 files changed, 9 insertions(+), 17 deletions(-) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 446384a3..3ae143ef 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -793,7 +793,7 @@ class LocalOrRemote(Command): """ takes_options = ( - Param('server', type=ipa_types.Bool(), default=False, + Param('server?', type=ipa_types.Bool(), default=False, doc='Forward to server instead of running locally', ), ) diff --git a/ipalib/plugins/f_misc.py b/ipalib/plugins/f_misc.py index 05fd6d52..6988d6f8 100644 --- a/ipalib/plugins/f_misc.py +++ b/ipalib/plugins/f_misc.py @@ -21,44 +21,36 @@ Misc frontend plugins. """ -from ipalib import api, Command, Param, Bool +from ipalib import api, LocalOrRemote # FIXME: We should not let env return anything in_server # when mode == 'production'. This would allow an attacker to see the # configuration of the server, potentially revealing compromising # information. However, it's damn handy for testing/debugging. -class env(Command): +class env(LocalOrRemote): """Show environment variables""" takes_args = ('variables*',) - takes_options = ( - Param('server?', type=Bool(), default=False, - doc='Show environment variables of server', - ), - ) - - def run(self, variables, **options): - if options['server'] and not self.env.in_server: - return self.forward(variables) - return self.execute(variables) - - def find_keys(self, variables): + def __find_keys(self, variables): for key in variables: if key in self.env: yield (key, self.env[key]) - def execute(self, variables): + def execute(self, variables, **options): if variables is None: return tuple( (key, self.env[key]) for key in self.env ) - return tuple(self.find_keys(variables)) + return tuple(self.__find_keys(variables)) def output_for_cli(self, textui, result, variables, **options): if len(result) == 0: return + if len(result) == 1: + textui.print_keyval(result) + return textui.print_name(self.name) textui.print_keyval(result) textui.print_count(result, '%d variable', '%d variables') -- cgit From e3fec8f2192019e9b16dcc8ef3e965bd13c1e1d4 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 14 Nov 2008 22:03:31 -0700 Subject: Fixed textui.print_keyval for cases when the row is a list instead of a tuple --- ipalib/cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index 69a3c28f..dd20b365 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -195,8 +195,8 @@ class textui(backend.Backend): Also see `textui.print_indented`. """ - for row in rows: - self.print_indented('%s = %r' % row, indent) + for (key, value) in rows: + self.print_indented('%s = %r' % (key, value), indent) def print_dashed(self, string, above=True, below=True): """ -- cgit From e059591d6b190cbc6c50d0b96e1f63fddb30a934 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 14 Nov 2008 22:21:36 -0700 Subject: env command now supports * wildcard for searching --- ipalib/plugins/f_misc.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugins/f_misc.py b/ipalib/plugins/f_misc.py index 6988d6f8..b2f97c71 100644 --- a/ipalib/plugins/f_misc.py +++ b/ipalib/plugins/f_misc.py @@ -21,9 +21,11 @@ Misc frontend plugins. """ +import re from ipalib import api, LocalOrRemote + # FIXME: We should not let env return anything in_server # when mode == 'production'. This would allow an attacker to see the # configuration of the server, potentially revealing compromising @@ -34,16 +36,25 @@ class env(LocalOrRemote): takes_args = ('variables*',) def __find_keys(self, variables): - for key in variables: - if key in self.env: - yield (key, self.env[key]) + keys = set() + for query in variables: + if '*' in query: + pat = re.compile(query.replace('*', '.*') + '$') + for key in self.env: + if pat.match(key): + keys.add(key) + elif query in self.env: + keys.add(query) + return sorted(keys) def execute(self, variables, **options): if variables is None: - return tuple( - (key, self.env[key]) for key in self.env - ) - return tuple(self.__find_keys(variables)) + keys = self.env + else: + keys = self.__find_keys(variables) + return tuple( + (key, self.env[key]) for key in keys + ) def output_for_cli(self, textui, result, variables, **options): if len(result) == 0: @@ -53,6 +64,6 @@ class env(LocalOrRemote): return textui.print_name(self.name) textui.print_keyval(result) - textui.print_count(result, '%d variable', '%d variables') + textui.print_count(result, '%d variables') api.register(env) -- cgit From e7ec4131589d5d387c4257bca76e91a17ad7e1a3 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sun, 16 Nov 2008 19:50:17 -0700 Subject: Moved plugins command from ipalib.cli to ipalib.plugins.f_misc --- ipalib/cli.py | 19 ------------------- ipalib/plugins/f_misc.py | 20 ++++++++++++++++++++ 2 files changed, 20 insertions(+), 19 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index dd20b365..fc58f2e9 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -381,29 +381,10 @@ class show_api(frontend.Application): self.__traverse_namespace(n, attr, lines, tab + 2) -class plugins(frontend.Application): - """Show all loaded plugins""" - - def run(self): - plugins = sorted(self.api.plugins, key=lambda o: o.plugin) - return tuple( - (p.plugin, p.bases) for p in plugins - ) - - def output_for_cli(self, textui, result, **kw): - textui.print_name(self.name) - for (plugin, bases) in result: - textui.print_indented( - '%s: %s' % (plugin, ', '.join(bases)) - ) - textui.print_count(result, '%d plugin loaded', '%s plugins loaded') - - cli_application_commands = ( help, console, show_api, - plugins, ) diff --git a/ipalib/plugins/f_misc.py b/ipalib/plugins/f_misc.py index b2f97c71..a2f0fa4e 100644 --- a/ipalib/plugins/f_misc.py +++ b/ipalib/plugins/f_misc.py @@ -67,3 +67,23 @@ class env(LocalOrRemote): textui.print_count(result, '%d variables') api.register(env) + + +class plugins(LocalOrRemote): + """Show all loaded plugins""" + + def execute(self, **options): + plugins = sorted(self.api.plugins, key=lambda o: o.plugin) + return tuple( + (p.plugin, p.bases) for p in plugins + ) + + def output_for_cli(self, textui, result, **options): + textui.print_name(self.name) + for (plugin, bases) in result: + textui.print_indented( + '%s: %s' % (plugin, ', '.join(bases)) + ) + textui.print_count(result, '%d plugin loaded', '%s plugins loaded') + +api.register(plugins) -- cgit From 42bf555a3ad1f1777b26a4092b49512b9360c882 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 17 Nov 2008 15:27:08 -0700 Subject: Started updated user_* commands to use textui --- ipalib/cli.py | 22 ++++++++++++++++++++++ ipalib/plugins/f_user.py | 29 +++++++++++++++++------------ 2 files changed, 39 insertions(+), 12 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index fc58f2e9..1c7256a2 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -198,6 +198,28 @@ class textui(backend.Backend): for (key, value) in rows: self.print_indented('%s = %r' % (key, value), indent) + def print_entry(self, entry, indent=1): + """ + Print an ldap entry dict. + + For example: + + >>> entry = dict(sn='Last', givenname='First', uid='flast') + >>> ui = textui() + >>> ui.print_entry(entry) + givenname: 'First' + sn: 'Last' + uid: 'flast' + """ + assert type(entry) is dict + for key in sorted(entry): + value = entry[key] + if type(value) in (list, tuple): + value = ', '.join(repr(v) for v in value) + else: + value = repr(value) + self.print_indented('%s: %s' % (key, value), indent) + def print_dashed(self, string, above=True, below=True): """ Print a string with a dashed line above and/or below. diff --git a/ipalib/plugins/f_user.py b/ipalib/plugins/f_user.py index 3adb328c..e96d787b 100644 --- a/ipalib/plugins/f_user.py +++ b/ipalib/plugins/f_user.py @@ -191,18 +191,21 @@ class user_add(crud.Add): kw['objectClass'] = config.get('ipauserobjectclasses') return ldap.create(**kw) - def output_for_cli(self, ret): + + def output_for_cli(self, textui, result, *args, **options): """ Output result of this command to command line interface. """ - if ret: - print "User added" + textui.print_name(self.name) + textui.print_entry(result) + textui.print_dashed('Added user "%s"' % result['uid']) api.register(user_add) class user_del(crud.Del): 'Delete an existing user.' + def execute(self, uid, **kw): """Delete a user. Not to be confused with inactivate_user. This makes the entry go away completely. @@ -224,12 +227,12 @@ class user_del(crud.Del): ldap = self.api.Backend.ldap dn = ldap.find_entry_dn("uid", uid) return ldap.delete(dn) - def output_for_cli(self, ret): + + def output_for_cli(self, textui, result, uid): """ Output result of this command to command line interface. """ - if ret: - print "User deleted" + textui.print_plain('Deleted user "%s"' % uid) api.register(user_del) @@ -254,12 +257,13 @@ class user_mod(crud.Mod): dn = ldap.find_entry_dn("uid", uid) return ldap.update(dn, **kw) - def output_for_cli(self, ret): + def output_for_cli(self, textui, result, uid, **options): """ Output result of this command to command line interface. """ - if ret: - print "User updated" + textui.print_name(self.name) + textui.print_entry(result) + textui.print_dashed('Updated user "%s"' % result['uid']) api.register(user_mod) @@ -330,9 +334,10 @@ class user_show(crud.Get): return ldap.retrieve(dn) else: return ldap.retrieve(dn, default_attributes) - def output_for_cli(self, user): - if user: - display_user(user) + + def output_for_cli(self, textui, result, uid, **options): + if result: + display_user(result) api.register(user_show) -- cgit From 12dc0a0aa916c9289fe7fb36eddf887e3a53775e Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 17 Nov 2008 16:40:42 -0700 Subject: user-find now works again, uses textui --- ipalib/plugins/f_user.py | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugins/f_user.py b/ipalib/plugins/f_user.py index e96d787b..eed6d8ab 100644 --- a/ipalib/plugins/f_user.py +++ b/ipalib/plugins/f_user.py @@ -292,21 +292,26 @@ class user_find(crud.Find): else: kw['attributes'] = default_attributes return ldap.search(**kw) - def output_for_cli(self, users): - if not users: + + def output_for_cli(self, textui, result, uid, **options): + counter = result[0] + users = result[1:] + if counter == 0 or len(users) == 0: + textui.print_plain("No entries found") return - counter = users[0] - users = users[1:] - if counter == 0: - print "No entries found" + if len(users) == 1: + textui.print_entry(users[0]) return - elif counter == -1: - print "These results are truncated." - print "Please refine your search and try again." - + textui.print_name(self.name) for u in users: - display_user(u) - print "" + textui.print_plain('%(givenname)s %(sn)s:' % u) + textui.print_entry(u) + textui.print_plain('') + if counter == -1: + textui.print_plain('These results are truncated.') + textui.print_plain('Please refine your search and try again.') + textui.print_count(users, '%d users matched') + api.register(user_find) -- cgit From 5c16047092652d2d56c86d83259c56eff883b485 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 17 Nov 2008 18:15:40 -0700 Subject: user-lock and user-unlock commands now use textui, which finishes the user plugins --- ipalib/plugins/f_user.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugins/f_user.py b/ipalib/plugins/f_user.py index eed6d8ab..ad7572c2 100644 --- a/ipalib/plugins/f_user.py +++ b/ipalib/plugins/f_user.py @@ -348,28 +348,37 @@ api.register(user_show) class user_lock(frontend.Command): 'Lock a user account.' + takes_args = ( Param('uid', primary_key=True), ) + def execute(self, uid, **kw): ldap = self.api.Backend.ldap dn = ldap.find_entry_dn("uid", uid) return ldap.mark_entry_inactive(dn) - def output_for_cli(self, ret): - if ret: - print "User locked" + + def output_for_cli(self, textui, result, uid): + if result: + textui.print_plain('Locked user "%s"' % uid) + api.register(user_lock) + class user_unlock(frontend.Command): 'Unlock a user account.' + takes_args = ( Param('uid', primary_key=True), ) + def execute(self, uid, **kw): ldap = self.api.Backend.ldap dn = ldap.find_entry_dn("uid", uid) return ldap.mark_entry_active(dn) - def output_for_cli(self, ret): - if ret: - print "User unlocked" + + def output_for_cli(self, textui, result, uid): + if result: + textui.print_plain('Unlocked user "%s"' % uid) + api.register(user_unlock) -- cgit From 8474bd01da13b9b72ba06e832d4c35ef6ccf5c9e Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 17 Nov 2008 18:50:30 -0700 Subject: Command.get_defaults() now returns param.default if param.type is a Bool --- ipalib/cli.py | 6 ++++-- ipalib/frontend.py | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index 1c7256a2..909e2acb 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -693,8 +693,10 @@ class CLI(object): help=option.doc, ) if isinstance(option.type, ipa_types.Bool): - o.action = 'store_true' - o.default = option.default + if option.default is True: + o.action = 'store_false' + else: + o.action = 'store_true' o.type = None parser.add_option(o) return parser diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 3ae143ef..61cba513 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -651,6 +651,8 @@ class Command(plugable.Plugin): if kw.get(param.name, None) is None: if param.required: yield (param.name, param.get_default(**kw)) + elif isinstance(param.type, ipa_types.Bool): + yield (param.name, param.default) else: yield (param.name, None) -- cgit From 75d1918996dc62a99927d3f7e2ff13404c67ef5b Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 17 Nov 2008 20:41:01 -0700 Subject: Added some experimental textui methods --- ipalib/cli.py | 40 +++++++++++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 5 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index 909e2acb..f1d8eebe 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -220,7 +220,7 @@ class textui(backend.Backend): value = repr(value) self.print_indented('%s: %s' % (key, value), indent) - def print_dashed(self, string, above=True, below=True): + def print_dashed(self, string, above=True, below=True, indent=0, dash='-'): """ Print a string with a dashed line above and/or below. @@ -238,12 +238,42 @@ class textui(backend.Backend): ------------------ Only dashed above. """ - dashes = '-' * len(string) + assert isinstance(dash, basestring) + assert len(dash) == 1 + dashes = dash * len(string) if above: - print dashes - print string + self.print_indented(dashes, indent) + self.print_indented(string, indent) if below: - print dashes + self.print_indented(dashes, indent) + + def print_h1(self, text): + """ + Print a primary header at indentation level 0. + + For example: + + >>> ui = textui() + >>> ui.print_h1('A primary header') + ================ + A primary header + ================ + """ + self.print_dashed(text, indent=0, dash='=') + + def print_h2(self, text): + """ + Print a secondary header at indentation level 1. + + For example: + + >>> ui = textui() + >>> ui.print_h2('A secondary header') + ------------------ + A secondary header + ------------------ + """ + self.print_dashed(text, indent=1, dash='-') def print_name(self, name): """ -- cgit From 0a60a6bcc4c8eaa7d42dc25fa6fc69d837e3e816 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 18 Nov 2008 11:30:16 -0700 Subject: Added textui.prompt_password() method; added logic in cli for dealing with 'password' flag in param.flags --- ipalib/cli.py | 45 ++++++++++++++++++++++++++++++++++----------- ipalib/plugins/f_user.py | 3 ++- 2 files changed, 36 insertions(+), 12 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index f1d8eebe..7cbf6e4b 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -24,6 +24,7 @@ Functionality for Command Line Interface. import re import textwrap import sys +import getpass import code import optparse import socket @@ -332,6 +333,21 @@ class textui(backend.Backend): prompt = '%s [%s]: ' % (label, default) return raw_input(prompt) + def prompt_password(self, label): + """ + Prompt user for a password. + """ + try: + while True: + pw1 = getpass.getpass('%s: ' % label) + pw2 = getpass.getpass('Enter again to verify: ') + if pw1 == pw2: + return pw1 + print ' ** Passwords do not match. Please enter again. **' + except KeyboardInterrupt: + print '' + print ' ** Cancelled. **' + class help(frontend.Application): '''Display help on a command.''' @@ -659,7 +675,14 @@ class CLI(object): optional. """ for param in cmd.params(): - if param.name not in kw: + if 'password' in param.flags: + if kw.get(param.name, False) is True: + kw[param.name] = self.textui.prompt_password( + param.cli_name + ) + else: + kw.pop(param.name, None) + elif param.name not in kw: if not (param.required or self.options.prompt_all): continue default = param.get_default(**kw) @@ -704,10 +727,7 @@ class CLI(object): list(self.cmd_argv[1:]), KWCollector() ) kw = kwc.__todict__() - try: - arg_kw = cmd.args_to_kw(*args) - except errors.ArgumentError, e: - exit_error('%s %s' % (to_cli(cmd.name), e.error)) + arg_kw = cmd.args_to_kw(*args) assert set(arg_kw).intersection(kw) == set() kw.update(arg_kw) return kw @@ -717,17 +737,20 @@ class CLI(object): usage=self.get_usage(cmd), ) for option in cmd.options(): - o = optparse.make_option('--%s' % to_cli(option.cli_name), + kw = dict( dest=option.name, - metavar=option.type.name.upper(), help=option.doc, ) - if isinstance(option.type, ipa_types.Bool): + if 'password' in option.flags: + kw['action'] = 'store_true' + elif isinstance(option.type, ipa_types.Bool): if option.default is True: - o.action = 'store_false' + kw['action'] = 'store_false' else: - o.action = 'store_true' - o.type = None + kw['action'] = 'store_true' + else: + kw['metavar'] = metavar=option.type.name.upper() + o = optparse.make_option('--%s' % to_cli(option.cli_name), **kw) parser.add_option(o) return parser diff --git a/ipalib/plugins/f_user.py b/ipalib/plugins/f_user.py index ad7572c2..e1076242 100644 --- a/ipalib/plugins/f_user.py +++ b/ipalib/plugins/f_user.py @@ -109,7 +109,8 @@ class user(frontend.Object): ), Param('userpassword?', cli_name='password', - doc='User\'s password', + doc="Set user's password", + flags=['password'], ), Param('groups?', doc='Add account to one or more groups (comma-separated)', -- cgit From 4afee15d4b523a641552bee9993882bb1ae6e2cc Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 18 Nov 2008 13:43:43 -0700 Subject: Calling 'passwd' command now prompts for password using textui.prompt_password() --- ipalib/cli.py | 27 ++++++++++++++++++++------- ipalib/frontend.py | 7 +++++++ ipalib/plugins/f_passwd.py | 26 +++++++++++++------------- 3 files changed, 40 insertions(+), 20 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index 7cbf6e4b..b3aa1099 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -658,12 +658,28 @@ class CLI(object): def run_cmd(self, cmd): kw = self.parse(cmd) if self.options.interactive: - kw = self.prompt_interactively(cmd, kw) + self.prompt_interactively(cmd, kw) + self.prompt_for_passwords(cmd, kw) result = cmd(**kw) if callable(cmd.output_for_cli): + for param in cmd.params(): + if param.ispassword(): + del kw[param.name] (args, options) = cmd.params_2_args_options(kw) cmd.output_for_cli(self.api.Backend.textui, result, *args, **options) + def prompt_for_passwords(self, cmd, kw): + for param in cmd.params(): + if 'password' not in param.flags: + continue + if kw.get(param.name, False) is True or param.name in cmd.args: + kw[param.name] = self.textui.prompt_password( + param.cli_name + ) + else: + kw.pop(param.name, None) + return kw + def prompt_interactively(self, cmd, kw): """ Interactively prompt for missing or invalid values. @@ -676,12 +692,7 @@ class CLI(object): """ for param in cmd.params(): if 'password' in param.flags: - if kw.get(param.name, False) is True: - kw[param.name] = self.textui.prompt_password( - param.cli_name - ) - else: - kw.pop(param.name, None) + continue elif param.name not in kw: if not (param.required or self.options.prompt_all): continue @@ -760,6 +771,8 @@ class CLI(object): def get_usage_iter(self, cmd): yield 'Usage: %%prog [global-options] %s' % to_cli(cmd.name) for arg in cmd.args(): + if 'password' in arg.flags: + continue name = to_cli(arg.cli_name).upper() if arg.multivalue: name = '%s...' % name diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 61cba513..db399ba5 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -261,6 +261,13 @@ class Param(plugable.ReadOnly): self.primary_key = self.__check_type(bool, 'primary_key') lock(self) + def ispassword(self): + """ + Return ``True`` is this Param is a password. + """ + # FIXME: add unit test + return 'password' in self.flags + def __clone__(self, **override): """ Return a new `Param` instance similar to this one. diff --git a/ipalib/plugins/f_passwd.py b/ipalib/plugins/f_passwd.py index 7b424a3b..edc13b63 100644 --- a/ipalib/plugins/f_passwd.py +++ b/ipalib/plugins/f_passwd.py @@ -30,14 +30,17 @@ from ipalib import util class passwd(frontend.Command): 'Edit existing password policy.' + takes_args = ( Param('principal', cli_name='user', primary_key=True, default_from=util.get_current_principal, ), + Param('password', flags=['password']), ) - def execute(self, principal, **kw): + + def execute(self, principal, password): """ Execute the passwd operation. @@ -49,8 +52,6 @@ class passwd(frontend.Command): :param param uid: The login name of the user being updated. :param kw: Not used. """ - ldap = self.api.Backend.ldap - if principal.find('@') < 0: u = principal.split('@') if len(u) > 2 or len(u) == 0: @@ -59,16 +60,15 @@ class passwd(frontend.Command): principal = principal+"@"+self.api.env.realm else: principal = principal + dn = self.Backend.ldap.find_entry_dn( + "krbprincipalname", + principal, + "posixAccount" + ) + return self.Backend.ldap.modify_password(dn, newpass=password) - dn = ldap.find_entry_dn("krbprincipalname", principal, "posixAccount") - - # FIXME: we need a way to prompt for passwords using getpass - kw['newpass'] = "password" - - return ldap.modify_password(dn, **kw) - - def output_for_cli(self, ret): - if ret: - print "Password change successful" + def output_for_cli(self, textui, result, principal, password): + assert password is None + textui.print_plain('Changed password for "%s"' % principal) api.register(passwd) -- cgit From 500b8166811e39ac63bdbaee8dcf01eb0643d868 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 18 Nov 2008 16:29:08 -0700 Subject: Added unit test for Param.ispassword() method --- ipalib/frontend.py | 1 - 1 file changed, 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index db399ba5..6e79e539 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -265,7 +265,6 @@ class Param(plugable.ReadOnly): """ Return ``True`` is this Param is a password. """ - # FIXME: add unit test return 'password' in self.flags def __clone__(self, **override): -- cgit From 2478ccd357efa7c38cc4acbfe2bfab2f3a8bf0cd Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 19 Nov 2008 03:31:29 -0700 Subject: Fixed some unicode encoded/decode issues in textui.prompt_password() and textui.prompt() --- ipalib/cli.py | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index b3aa1099..37fdad44 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -98,6 +98,30 @@ class textui(backend.Backend): return max(len(row) for row in rows) return max(len(row[col]) for row in rows) + def __get_encoding(self, stream): + assert stream in (sys.stdin, sys.stdout) + if stream.encoding is None: + if stream.isatty(): + return sys.getdefaultencoding() + return 'UTF-8' + return stream.encoding + + def decode(self, str_buffer): + """ + Decode text from stdin. + """ + assert type(str_buffer) is str + encoding = self.__get_encoding(sys.stdin) + return str_buffer.decode(encoding) + + def encode(self, unicode_text): + """ + Encode text for output to stdout. + """ + assert type(unicode_text) is unicode + encoding = self.__get_encoding(sys.stdout) + return unicode_text.encode(encoding) + def choose_number(self, n, singular, plural=None): if n == 1 or plural is None: return singular % n @@ -327,11 +351,14 @@ class textui(backend.Backend): """ Prompt user for input. """ + # TODO: Add tab completion using readline if default is None: - prompt = '%s: ' % label + prompt = u'%s: ' % label else: - prompt = '%s [%s]: ' % (label, default) - return raw_input(prompt) + prompt = u'%s [%s]: ' % (label, default) + return self.decode( + raw_input(self.encode(prompt)) + ) def prompt_password(self, label): """ @@ -342,7 +369,7 @@ class textui(backend.Backend): pw1 = getpass.getpass('%s: ' % label) pw2 = getpass.getpass('Enter again to verify: ') if pw1 == pw2: - return pw1 + return self.decode(pw1) print ' ** Passwords do not match. Please enter again. **' except KeyboardInterrupt: print '' -- cgit From cfe4ec2175c42f208ae23401991febb8525bdd9b Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 19 Nov 2008 16:11:23 -0700 Subject: Added util.xmlrpc_wrap(), util.xmlrpc_unwrap() functions an corresponding unit tests --- ipalib/util.py | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) (limited to 'ipalib') diff --git a/ipalib/util.py b/ipalib/util.py index 89e2c5a7..60b9409f 100644 --- a/ipalib/util.py +++ b/ipalib/util.py @@ -27,9 +27,12 @@ import imp import optparse import logging import time +from types import NoneType +from xmlrpclib import Binary import krbV + def xmlrpc_marshal(*args, **kw): """ Marshal (args, kw) into ((kw,) + args). @@ -56,6 +59,67 @@ def xmlrpc_unmarshal(*params): return (params[1:], kw) +def xmlrpc_wrap(value): + """ + Wrap all ``str`` in ``xmlrpclib.Binary``. + + Because ``xmlrpclib.dumps()`` will itself convert all ``unicode`` instances + into UTF-8 encoded ``str`` instances, we don't do it here. + + So in total, when encoding data for an XML-RPC request, the following + transformations occur: + + * All ``str`` instances are treated as binary data and are wrapped in + an ``xmlrpclib.Binary()`` instance. + + * Only ``unicode`` instances are treated as character data. They get + converted to UTF-8 encoded ``str`` instances (although as mentioned, + not by this function). + + Also see `xmlrpc_unwrap`. + """ + if type(value) in (list, tuple): + return tuple(xmlrpc_wrap(v) for v in value) + if type(value) is dict: + return dict( + (k, xmlrpc_wrap(v)) for (k, v) in value.iteritems() + ) + if type(value) is str: + return Binary(value) + assert type(value) in (unicode, int, float, bool, NoneType) + return value + + +def xmlrpc_unwrap(value, encoding='UTF-8'): + """ + Unwrap all ``xmlrpc.Binary``, decode all ``str`` into ``unicode``. + + When decoding data from an XML-RPC request, the following transformations + occur: + + * The binary payloads of all ``xmlrpclib.Binary`` instances are + returned as ``str`` instances. + + * All ``str`` instances are treated as UTF-8 encoded character data. + They are decoded and the resulting ``unicode`` instance is returned. + + Also see `xmlrpc_wrap`. + """ + if type(value) in (list, tuple): + return tuple(xmlrpc_unwrap(v, encoding) for v in value) + if type(value) is dict: + return dict( + (k, xmlrpc_unwrap(v, encoding)) for (k, v) in value.iteritems() + ) + if type(value) is str: + return value.decode(encoding) + if isinstance(value, Binary): + assert type(value.data) is str + return value.data + assert type(value) in (int, float, bool, NoneType) + return value + + def get_current_principal(): try: return krbV.default_context().default_ccache().principal().name -- cgit From 75bdea29be8ea53c8e005e9020f3f2d1c7dcf689 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 20 Nov 2008 12:41:06 -0700 Subject: Added test_util.test_round_trip() test that tests use of xmlrpc_wrap() and xmlrpc_unwrap() with dumps(), loads(); fixed a bug in xmlrpc_unwrap() --- ipalib/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/util.py b/ipalib/util.py index 60b9409f..223422fe 100644 --- a/ipalib/util.py +++ b/ipalib/util.py @@ -116,7 +116,7 @@ def xmlrpc_unwrap(value, encoding='UTF-8'): if isinstance(value, Binary): assert type(value.data) is str return value.data - assert type(value) in (int, float, bool, NoneType) + assert type(value) in (unicode, int, float, bool, NoneType) return value -- cgit From 2db738e8996528502293b8cc6861efedcba22c9a Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 24 Nov 2008 10:09:30 -0700 Subject: Some changes to make reading dubugging output easier --- ipalib/frontend.py | 5 ++++- ipalib/plugable.py | 5 +++++ ipalib/plugins/b_xmlrpc.py | 5 +++-- 3 files changed, 12 insertions(+), 3 deletions(-) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 6e79e539..6dcbea69 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -539,6 +539,7 @@ class Command(plugable.Plugin): If not in a server context, the call will be forwarded over XML-RPC and the executed an the nearest IPA server. """ + self.debug(make_repr(self.name, *args, **kw)) if len(args) > 0: arg_kw = self.args_to_kw(*args) assert set(arg_kw).intersection(kw) == set() @@ -548,7 +549,9 @@ class Command(plugable.Plugin): kw.update(self.get_default(**kw)) self.validate(**kw) (args, options) = self.params_2_args_options(kw) - return self.run(*args, **options) + result = self.run(*args, **options) + self.debug('%s result: %r', self.name, result) + return result def args_to_kw(self, *values): """ diff --git a/ipalib/plugable.py b/ipalib/plugable.py index d65a83e2..7dafd440 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -253,6 +253,11 @@ class Plugin(ReadOnly): __proxy__ = True __api = None + def __init__(self): + log = logging.getLogger('ipa') + for name in ('debug', 'info', 'warning', 'error', 'critical'): + setattr(self, name, getattr(log, name)) + def __get_name(self): """ Convenience property to return the class name. diff --git a/ipalib/plugins/b_xmlrpc.py b/ipalib/plugins/b_xmlrpc.py index 22361b1b..b6e113a5 100644 --- a/ipalib/plugins/b_xmlrpc.py +++ b/ipalib/plugins/b_xmlrpc.py @@ -51,14 +51,15 @@ class xmlrpc(Backend): if uri.startswith('https://'): return xmlrpclib.ServerProxy(uri, transport=KerbTransport(), - verbose=self.api.env.verbose, + #verbose=self.api.env.verbose, ) - return xmlrpclib.ServerProxy(uri, verbose=self.api.env.verbose) + return xmlrpclib.ServerProxy(uri) def forward_call(self, name, *args, **kw): """ Forward a call over XML-RPC to an IPA server. """ + self.info('Forwarding %r call to %r' % (name, self.env.xmlrpc_uri)) client = self.get_client() command = getattr(client, name) params = xmlrpc_marshal(*args, **kw) -- cgit From 237c16f0fd3998f4a2e69d9096997d10af2cf8c9 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 24 Nov 2008 12:51:03 -0700 Subject: Started moving xmlrpc-functions from ipalib.util to ipalib.rpc --- ipalib/plugins/b_xmlrpc.py | 3 +- ipalib/rpc.py | 86 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 ipalib/rpc.py (limited to 'ipalib') diff --git a/ipalib/plugins/b_xmlrpc.py b/ipalib/plugins/b_xmlrpc.py index b6e113a5..14f2a9be 100644 --- a/ipalib/plugins/b_xmlrpc.py +++ b/ipalib/plugins/b_xmlrpc.py @@ -47,11 +47,10 @@ class xmlrpc(Backend): # Are there any reasonably common XML-RPC client implementations # that don't support the extension? # See: http://docs.python.org/library/xmlrpclib.html - uri = self.api.env.xmlrpc_uri + uri = self.env.xmlrpc_uri if uri.startswith('https://'): return xmlrpclib.ServerProxy(uri, transport=KerbTransport(), - #verbose=self.api.env.verbose, ) return xmlrpclib.ServerProxy(uri) diff --git a/ipalib/rpc.py b/ipalib/rpc.py new file mode 100644 index 00000000..c4662f84 --- /dev/null +++ b/ipalib/rpc.py @@ -0,0 +1,86 @@ +# Authors: +# Jason Gerard DeRose +# +# 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 + +""" +Core RPC functionality. +""" + +from types import NoneType +from xmlrpclib import Binary + + +def xmlrpc_wrap(value): + """ + Wrap all ``str`` in ``xmlrpclib.Binary``. + + Because ``xmlrpclib.dumps()`` will itself convert all ``unicode`` instances + into UTF-8 encoded ``str`` instances, we don't do it here. + + So in total, when encoding data for an XML-RPC request, the following + transformations occur: + + * All ``str`` instances are treated as binary data and are wrapped in + an ``xmlrpclib.Binary()`` instance. + + * Only ``unicode`` instances are treated as character data. They get + converted to UTF-8 encoded ``str`` instances (although as mentioned, + not by this function). + + Also see `xmlrpc_unwrap`. + """ + if type(value) in (list, tuple): + return tuple(xmlrpc_wrap(v) for v in value) + if type(value) is dict: + return dict( + (k, xmlrpc_wrap(v)) for (k, v) in value.iteritems() + ) + if type(value) is str: + return Binary(value) + assert type(value) in (unicode, int, float, bool, NoneType) + return value + + +def xmlrpc_unwrap(value, encoding='UTF-8'): + """ + Unwrap all ``xmlrpc.Binary``, decode all ``str`` into ``unicode``. + + When decoding data from an XML-RPC request, the following transformations + occur: + + * The binary payloads of all ``xmlrpclib.Binary`` instances are + returned as ``str`` instances. + + * All ``str`` instances are treated as UTF-8 encoded character data. + They are decoded and the resulting ``unicode`` instance is returned. + + Also see `xmlrpc_wrap`. + """ + if type(value) in (list, tuple): + return tuple(xmlrpc_unwrap(v, encoding) for v in value) + if type(value) is dict: + return dict( + (k, xmlrpc_unwrap(v, encoding)) for (k, v) in value.iteritems() + ) + if type(value) is str: + return value.decode(encoding) + if isinstance(value, Binary): + assert type(value.data) is str + return value.data + assert type(value) in (unicode, int, float, bool, NoneType) + return value -- cgit From 2d458a12339fbb7ef006ff7defc1e2f541e2f23f Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 24 Nov 2008 21:34:01 -0700 Subject: Stared some RPC-related error cleanup; started work on ipa_server.rcp.xmlrpc plugin --- ipalib/errors.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) (limited to 'ipalib') diff --git a/ipalib/errors.py b/ipalib/errors.py index 71a837e9..a0108250 100644 --- a/ipalib/errors.py +++ b/ipalib/errors.py @@ -113,9 +113,33 @@ class IPAError(StandardError): class InvocationError(IPAError): pass + class UnknownCommandError(InvocationError): format = 'unknown command "%s"' +def _(text): + return text + + +class HandledError(StandardError): + """ + Base class for errors that can be raised across a remote procecdure call. + """ + def __init__(self, message=None, **kw): + self.kw = kw + if message is None: + message = self.format % kw + StandardError.__init__(self, message) + + +class CommandError(HandledError): + format = _('Unknown command %(name)r') + + +class RemoteCommandError(HandledError): + format = 'Server at %(uri)r has no command %(command)r' + + class UnknownHelpError(InvocationError): format = 'no command nor topic "%s"' -- cgit From 7350ccbffefdf81992b3ccd8aac814f3bb954be8 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 25 Nov 2008 11:54:51 -0700 Subject: Started fleshing out doodles in xmlrpc.execute() --- ipalib/errors.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/errors.py b/ipalib/errors.py index a0108250..8412d1f6 100644 --- a/ipalib/errors.py +++ b/ipalib/errors.py @@ -125,6 +125,9 @@ class HandledError(StandardError): """ Base class for errors that can be raised across a remote procecdure call. """ + + code = 1 + def __init__(self, message=None, **kw): self.kw = kw if message is None: @@ -132,12 +135,22 @@ class HandledError(StandardError): StandardError.__init__(self, message) + +class UnknownError(HandledError): + """ + Raised when the true error is not a handled error. + """ + + format = _('An unknown internal error has occurred') + + class CommandError(HandledError): format = _('Unknown command %(name)r') + class RemoteCommandError(HandledError): - format = 'Server at %(uri)r has no command %(command)r' + format = 'Server at %(uri)r has no command %(name)r' class UnknownHelpError(InvocationError): -- cgit From 29d680b211021fe755522f4453853344233bc78e Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 25 Nov 2008 13:52:40 -0700 Subject: Continued work on xmlrpc.dispatch() unit tests; fixed bug in Command.args_to_kw() --- ipalib/frontend.py | 8 +++++++- ipalib/ipa_types.py | 7 +++++++ 2 files changed, 14 insertions(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 6dcbea69..e4dd7637 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -577,12 +577,18 @@ class Command(plugable.Plugin): if len(values) > i: if arg.multivalue: multivalue = True - yield (arg.name, values[i:]) + if len(values) == i + 1 and type(values[i]) in (list, tuple): + yield (arg.name, values[i]) + else: + yield (arg.name, values[i:]) else: yield (arg.name, values[i]) else: break + def args_options_2_params(self, args, options): + pass + def params_2_args_options(self, params): """ Split params into (args, kw). diff --git a/ipalib/ipa_types.py b/ipalib/ipa_types.py index 2da8e0be..583cceed 100644 --- a/ipalib/ipa_types.py +++ b/ipalib/ipa_types.py @@ -145,6 +145,13 @@ class Unicode(Type): self.regex = re.compile(pattern) super(Unicode, self).__init__(unicode) + def convert(self, value): + assert type(value) not in (list, tuple) + try: + return self.type(value) + except (TypeError, ValueError): + return None + def validate(self, value): if type(value) is not self.type: return 'Must be a string' -- cgit From 4591057203e61a4ab304b8730ffede6560f74d4b Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 8 Dec 2008 15:15:50 -0700 Subject: Removed depreciated rpc code from ipalib.util; removed corresponding unit tests in test_util --- ipalib/util.py | 61 ---------------------------------------------------------- 1 file changed, 61 deletions(-) (limited to 'ipalib') diff --git a/ipalib/util.py b/ipalib/util.py index 223422fe..4a58d7fb 100644 --- a/ipalib/util.py +++ b/ipalib/util.py @@ -59,67 +59,6 @@ def xmlrpc_unmarshal(*params): return (params[1:], kw) -def xmlrpc_wrap(value): - """ - Wrap all ``str`` in ``xmlrpclib.Binary``. - - Because ``xmlrpclib.dumps()`` will itself convert all ``unicode`` instances - into UTF-8 encoded ``str`` instances, we don't do it here. - - So in total, when encoding data for an XML-RPC request, the following - transformations occur: - - * All ``str`` instances are treated as binary data and are wrapped in - an ``xmlrpclib.Binary()`` instance. - - * Only ``unicode`` instances are treated as character data. They get - converted to UTF-8 encoded ``str`` instances (although as mentioned, - not by this function). - - Also see `xmlrpc_unwrap`. - """ - if type(value) in (list, tuple): - return tuple(xmlrpc_wrap(v) for v in value) - if type(value) is dict: - return dict( - (k, xmlrpc_wrap(v)) for (k, v) in value.iteritems() - ) - if type(value) is str: - return Binary(value) - assert type(value) in (unicode, int, float, bool, NoneType) - return value - - -def xmlrpc_unwrap(value, encoding='UTF-8'): - """ - Unwrap all ``xmlrpc.Binary``, decode all ``str`` into ``unicode``. - - When decoding data from an XML-RPC request, the following transformations - occur: - - * The binary payloads of all ``xmlrpclib.Binary`` instances are - returned as ``str`` instances. - - * All ``str`` instances are treated as UTF-8 encoded character data. - They are decoded and the resulting ``unicode`` instance is returned. - - Also see `xmlrpc_wrap`. - """ - if type(value) in (list, tuple): - return tuple(xmlrpc_unwrap(v, encoding) for v in value) - if type(value) is dict: - return dict( - (k, xmlrpc_unwrap(v, encoding)) for (k, v) in value.iteritems() - ) - if type(value) is str: - return value.decode(encoding) - if isinstance(value, Binary): - assert type(value.data) is str - return value.data - assert type(value) in (unicode, int, float, bool, NoneType) - return value - - def get_current_principal(): try: return krbV.default_context().default_ccache().principal().name -- cgit From 7e21ea5ad826e65da10d7a12917fd4d0d4f1874e Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 8 Dec 2008 16:56:24 -0700 Subject: Fixed Warning messages about log dir in unit test --- ipalib/errors.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'ipalib') diff --git a/ipalib/errors.py b/ipalib/errors.py index 8412d1f6..25f594f2 100644 --- a/ipalib/errors.py +++ b/ipalib/errors.py @@ -123,7 +123,7 @@ def _(text): class HandledError(StandardError): """ - Base class for errors that can be raised across a remote procecdure call. + Base class for errors that can be raised across a remote procedure call. """ code = 1 @@ -135,7 +135,6 @@ class HandledError(StandardError): StandardError.__init__(self, message) - class UnknownError(HandledError): """ Raised when the true error is not a handled error. @@ -145,10 +144,12 @@ class UnknownError(HandledError): class CommandError(HandledError): + """ + Raised when an unknown command is called client-side. + """ format = _('Unknown command %(name)r') - class RemoteCommandError(HandledError): format = 'Server at %(uri)r has no command %(name)r' -- cgit From fc8ac693726ec33b5c0924f9b8ff5d663705a5a3 Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Fri, 5 Dec 2008 15:31:18 -0500 Subject: Port plugins to use the new output_for_cli() argument list Fix some errors uncovered by the nosetests --- ipalib/cli.py | 5 ++- ipalib/errors.py | 4 ++ ipalib/plugins/f_automount.py | 95 ++++++++++++++++++++----------------------- ipalib/plugins/f_group.py | 95 +++++++++++++++++++++++++++---------------- ipalib/plugins/f_hostgroup.py | 21 ++++++++-- ipalib/plugins/f_user.py | 4 +- 6 files changed, 134 insertions(+), 90 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index 37fdad44..af3eb6e3 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -691,7 +691,10 @@ class CLI(object): if callable(cmd.output_for_cli): for param in cmd.params(): if param.ispassword(): - del kw[param.name] + try: + del kw[param.name] + except KeyError: + pass (args, options) = cmd.params_2_args_options(kw) cmd.output_for_cli(self.api.Backend.textui, result, *args, **options) diff --git a/ipalib/errors.py b/ipalib/errors.py index 25f594f2..989721be 100644 --- a/ipalib/errors.py +++ b/ipalib/errors.py @@ -409,6 +409,10 @@ class HostService(ConfigurationError): """You must enroll a host in order to create a host service""" faultCode = 1026 +class InsufficientAccess(GenericError): + """You do not have permission to perform this task""" + faultCode = 1027 + class FunctionDeprecated(GenericError): """Raised by a deprecated function""" faultCode = 2000 diff --git a/ipalib/plugins/f_automount.py b/ipalib/plugins/f_automount.py index 7a251572..d2a70784 100644 --- a/ipalib/plugins/f_automount.py +++ b/ipalib/plugins/f_automount.py @@ -34,14 +34,14 @@ from ldap import explode_dn map_attributes = ['automountMapName', 'description', ] key_attributes = ['description', 'automountKey', 'automountInformation'] -def display_entry(entry): +def display_entry(textui, entry): # FIXME: for now delete dn here. In the future pass in the kw to # output_for_cli() attr = sorted(entry.keys()) for a in attr: if a != 'dn': - print "%s: %s" % (a, entry[a]) + textui.print_plain("%s: %s" % (a, entry[a])) def make_automount_dn(mapname): """ @@ -96,12 +96,11 @@ class automount_addmap(crud.Add): kw['objectClass'] = ['automountMap'] return ldap.create(**kw) - def output_for_cli(self, ret): + def output_for_cli(self, textui, result, map, **options): """ Output result of this command to command line interface. """ - if ret: - print "Automount map added" + textui.print_plain("Automount map %s added" % map) api.register(automount_addmap) @@ -139,12 +138,11 @@ class automount_addkey(crud.Add): kw['objectClass'] = ['automount'] return ldap.create(**kw) - def output_for_cli(self, ret): + def output_for_cli(self, textui, result, *args, **options): """ Output result of this command to command line interface. """ - if ret: - print "Automount key added" + textui.print_plain("Automount key added") api.register(automount_addkey) @@ -161,18 +159,17 @@ class automount_delmap(crud.Del): :param kw: Not used. """ ldap = self.api.Backend.ldap - dn = ldap.find_entry_dn("automountmapname", mapname, "automountmap") + dn = ldap.find_entry_dn("automountmapname", mapname, "automountmap", api.env.container_automount) keys = api.Command['automount_getkeys'](mapname) if keys: for k in keys: ldap.delete(k.get('dn')) return ldap.delete(dn) - def output_for_cli(self, ret): + def output_for_cli(self, textui, result, *args, **options): """ Output result of this command to command line interface. """ - if ret: - print "Automount map and associated keys deleted" + print "Automount map and associated keys deleted" api.register(automount_delmap) @@ -205,12 +202,11 @@ class automount_delkey(crud.Del): if not keydn: raise errors.NotFound return ldap.delete(keydn) - def output_for_cli(self, ret): + def output_for_cli(self, textui, result, *args, **options): """ Output result of this command to command line interface. """ - if ret: - print "Automount key deleted" + print "Automount key deleted" api.register(automount_delkey) @@ -238,12 +234,11 @@ class automount_modmap(crud.Mod): dn = ldap.find_entry_dn("automountmapname", mapname, "automountmap", api.env.container_automount) return ldap.update(dn, **kw) - def output_for_cli(self, ret): + def output_for_cli(self, textui, result, *args, **options): """ Output result of this command to command line interface. """ - if ret: - print "Automount map updated" + print "Automount map updated" api.register(automount_modmap) @@ -286,12 +281,12 @@ class automount_modkey(crud.Mod): raise errors.NotFound return ldap.update(keydn, **kw) - def output_for_cli(self, ret): + def output_for_cli(self, textui, result, *args, **options): """ Output result of this command to command line interface. """ - if ret: - print "Automount key updated" + print "Automount key updated" + api.register(automount_modkey) @@ -309,26 +304,27 @@ class automount_findmap(crud.Find): kw[s] = term kw['objectclass'] = 'automountMap' + kw['base'] = api.env.container_automount if kw.get('all', False): kw['attributes'] = ['*'] else: kw['attributes'] = map_attributes return ldap.search(**kw) - def output_for_cli(self, entries): - if not entries: - return - counter = entries[0] - entries = entries[1:] + + def output_for_cli(self, textui, result, *args, **options): + counter = result[0] + entries = result[1:] if counter == 0: - print "No entries found" + textui.print_plain("No entries found") return elif counter == -1: - print "These results are truncated." - print "Please refine your search and try again." + textui.print_plain("These results are truncated.") + textui.print_plain("Please refine your search and try again.") for e in entries: - display_entry(e) - print "" + display_entry(textui, e) + textui.print_plain("") + api.register(automount_findmap) @@ -350,26 +346,26 @@ class automount_findkey(crud.Find): kw[s] = term kw['objectclass'] = 'automount' + kw['base'] = api.env.container_automount if kw.get('all', False): kw['attributes'] = ['*'] else: kw['attributes'] = key_attributes return ldap.search(**kw) - def output_for_cli(self, entries): - if not entries: - return - counter = entries[0] - entries = entries[1:] + def output_for_cli(self, textui, result, *args, **options): + counter = result[0] + entries = result[1:] if counter == 0: - print "No entries found" + textui.print_plain("No entries found") return elif counter == -1: - print "These results are truncated." - print "Please refine your search and try again." + textui.print_plain("These results are truncated.") + textui.print_plain("Please refine your search and try again.") for e in entries: - display_entry(e) - print "" + display_entry(textui, e) + textui.print_plain("") + api.register(automount_findkey) @@ -394,9 +390,9 @@ class automount_showmap(crud.Get): return ldap.retrieve(dn) else: return ldap.retrieve(dn, map_attributes) - def output_for_cli(self, entry): - if entry: - display_entry(entry) + def output_for_cli(self, textui, result, *args, **options): + if result: + display_entry(textui, result) api.register(automount_showmap) @@ -436,7 +432,7 @@ class automount_showkey(crud.Get): return ldap.retrieve(keydn) else: return ldap.retrieve(keydn, key_attributes) - def output_for_cli(self, entry): + def output_for_cli(self, textui, result, *args, **options): # The automount map name associated with this key is available only # in the dn. Add it as an attribute to display instead. if entry and not entry.get('automountmapname'): @@ -445,7 +441,7 @@ class automount_showkey(crud.Get): (attr, value) = e.split('=',1) if attr == 'automountmapname': entry['automountmapname'] = value - display_entry(entry) + display_entry(textui, entry) api.register(automount_showkey) @@ -475,9 +471,8 @@ class automount_getkeys(frontend.Command): keys = [] return keys - def output_for_cli(self, keys): - if keys: - for k in keys: - print k.get('automountkey') + def output_for_cli(self, textui, result, *args, **options): + for k in result: + textui.print_plain('%s' % k.get('automountkey')) api.register(automount_getkeys) diff --git a/ipalib/plugins/f_group.py b/ipalib/plugins/f_group.py index 9df83a29..6fe95006 100644 --- a/ipalib/plugins/f_group.py +++ b/ipalib/plugins/f_group.py @@ -29,6 +29,19 @@ from ipalib import errors from ipalib import ipa_types +def get_members(members): + """ + Return a list of members. + + It is possible that the value passed in is None. + """ + if members: + members = members.split(',') + else: + members = [] + + return members + class group(frontend.Object): """ Group object. @@ -83,12 +96,13 @@ class group_add(crud.Add): return ldap.create(**kw) - def output_for_cli(self, ret): + def output_for_cli(self, textui, result, *args, **options): """ Output result of this command to command line interface. """ - if ret: - print "Group added" + textui.print_name(self.name) + textui.print_entry(result) + textui.print_dashed('Added group "%s"' % result['cn']) api.register(group_add) @@ -121,12 +135,11 @@ class group_del(crud.Del): return ldap.delete(dn) - def output_for_cli(self, ret): + def output_for_cli(self, textui, result, cn): """ Output result of this command to command line interface. """ - if ret: - print "Group deleted" + textui.print_plain("Deleted group %s" % cn) api.register(group_del) @@ -151,12 +164,12 @@ class group_mod(crud.Mod): dn = ldap.find_entry_dn("cn", cn, "posixGroup") return ldap.update(dn, **kw) - def output_for_cli(self, ret): + def output_for_cli(self, textui, result, cn, **options): """ Output result of this command to command line interface. """ - if ret: - print "Group updated" + if result: + textui.print_plain("Group updated") api.register(group_mod) @@ -179,22 +192,24 @@ class group_find(crud.Find): kw['objectclass'] = object_type return ldap.search(**kw) - def output_for_cli(self, groups): - if not groups: + def output_for_cli(self, textui, result, uid, **options): + counter = result[0] + groups = result[1:] + if counter == 0 or len(groups) == 0: + textui.print_plain("No entries found") return - - counter = groups[0] - groups = groups[1:] - if counter == 0: - print "No entries found" + if len(groups) == 1: + textui.print_entry(groups[0]) return - elif counter == -1: - print "These results are truncated." - print "Please refine your search and try again." + textui.print_name(self.name) for g in groups: - for a in g.keys(): - print "%s: %s" % (a, g[a]) + textui.print_entry(g) + textui.print_plain('') + if counter == -1: + textui.print_plain("These results are truncated.") + textui.print_plain("Please refine your search and try again.") + textui.print_count(groups, '%d groups matched') api.register(group_find) @@ -218,12 +233,24 @@ class group_show(crud.Get): # FIXME: should kw contain the list of attributes to display? return ldap.retrieve(dn) - def output_for_cli(self, group): - if not group: + def output_for_cli(self, textui, result, *args, **options): + counter = result[0] + groups = result[1:] + if counter == 0 or len(groups) == 0: + textui.print_plain("No entries found") return - - for a in group.keys(): - print "%s: %s" % (a, group[a]) + if len(groups) == 1: + textui.print_entry(groups[0]) + return + textui.print_name(self.name) + for u in groups: + textui.print_plain('%(givenname)s %(sn)s:' % u) + textui.print_entry(u) + textui.print_plain('') + if counter == -1: + textui.print_plain('These results are truncated.') + textui.print_plain('Please refine your search and try again.') + textui.print_count(groups, '%d groups matched') api.register(group_show) @@ -253,7 +280,7 @@ class group_add_member(frontend.Command): to_add = [] completed = 0 - members = kw.get('groups', '').split(',') + members = get_members(kw.get('groups', '')) for m in members: if not m: continue try: @@ -263,7 +290,7 @@ class group_add_member(frontend.Command): add_failed.append(m) continue - members = kw.get('users', '').split(',') + members = get_members(kw.get('users', '')) for m in members: if not m: continue try: @@ -282,11 +309,11 @@ class group_add_member(frontend.Command): return add_failed - def output_for_cli(self, add_failed): + def output_for_cli(self, textui, result, *args, **options): """ Output result of this command to command line interface. """ - if add_failed: + if result: print "These entries failed to add to the group:" for a in add_failed: print "\t'%s'" % a @@ -320,7 +347,7 @@ class group_remove_member(frontend.Command): remove_failed = [] completed = 0 - members = kw.get('groups', '').split(',') + members = get_members(kw.get('groups', '')) for m in members: if not m: continue try: @@ -330,7 +357,7 @@ class group_remove_member(frontend.Command): remove_failed.append(m) continue - members = kw.get('users', '').split(',') + members = get_members(kw.get('users', '')) for m in members: try: member_dn = ldap.find_entry_dn("uid", m,) @@ -348,11 +375,11 @@ class group_remove_member(frontend.Command): return remove_failed - def output_for_cli(self, remove_failed): + def output_for_cli(self, textui, result, *args, **options): """ Output result of this command to command line interface. """ - if remove_failed: + if result: print "These entries failed to be removed from the group:" for a in remove_failed: print "\t'%s'" % a diff --git a/ipalib/plugins/f_hostgroup.py b/ipalib/plugins/f_hostgroup.py index 8e4c3740..6cbf4d51 100644 --- a/ipalib/plugins/f_hostgroup.py +++ b/ipalib/plugins/f_hostgroup.py @@ -30,6 +30,19 @@ from ipalib import ipa_types hostgroup_filter = "groupofnames)(!(objectclass=posixGroup)" +def get_members(members): + """ + Return a list of members. + + It is possible that the value passed in is None. + """ + if members: + members = members.split(',') + else: + members = [] + + return members + class hostgroup(frontend.Object): """ Host Group object. @@ -241,7 +254,7 @@ class hostgroup_add_member(frontend.Command): to_add = [] completed = 0 - members = kw.get('groups', '').split(',') + members = get_members(kw.get('groups', '')) for m in members: if not m: continue try: @@ -251,7 +264,7 @@ class hostgroup_add_member(frontend.Command): add_failed.append(m) continue - members = kw.get('hosts', '').split(',') + members = get_members(kw.get('hosts', '')) for m in members: if not m: continue try: @@ -309,7 +322,7 @@ class hostgroup_remove_member(frontend.Command): remove_failed = [] completed = 0 - members = kw.get('groups', '').split(',') + members = get_members(kw.get('groups', '')) for m in members: if not m: continue try: @@ -319,7 +332,7 @@ class hostgroup_remove_member(frontend.Command): remove_failed.append(m) continue - members = kw.get('hosts', '').split(',') + members = get_members(kw.get('hosts', '')) for m in members: if not m: continue try: diff --git a/ipalib/plugins/f_user.py b/ipalib/plugins/f_user.py index e1076242..c8b819dd 100644 --- a/ipalib/plugins/f_user.py +++ b/ipalib/plugins/f_user.py @@ -305,7 +305,9 @@ class user_find(crud.Find): return textui.print_name(self.name) for u in users: - textui.print_plain('%(givenname)s %(sn)s:' % u) + gn = u.get('givenname', '') + sn= u.get('sn', '') + textui.print_plain('%s %s:' % (gn, sn)) textui.print_entry(u) textui.print_plain('') if counter == -1: -- cgit From 039ee0fd56bbca60a79c99cdd489a1590f6f2b78 Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Mon, 8 Dec 2008 15:37:36 -0500 Subject: Add a function to show all the maps under a given mapname, def. is auto.master --- ipalib/plugins/f_automount.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) (limited to 'ipalib') diff --git a/ipalib/plugins/f_automount.py b/ipalib/plugins/f_automount.py index d2a70784..8de6c5ab 100644 --- a/ipalib/plugins/f_automount.py +++ b/ipalib/plugins/f_automount.py @@ -476,3 +476,35 @@ class automount_getkeys(frontend.Command): textui.print_plain('%s' % k.get('automountkey')) api.register(automount_getkeys) + + +class automount_getmaps(frontend.Command): + 'Retrieve all automount maps' + takes_args = ( + Param('automountmapname?', + cli_name='mapname', + primary_key=True, + doc='A group of related automount objects', + ), + ) + def execute(self, mapname, **kw): + """ + Execute the automount-getmaps operation. + + Return a list of all automount maps. + """ + + ldap = self.api.Backend.ldap + base = api.env.container_automount + "," + api.env.basedn + + if not mapname: + mapname = "auto.master" + search_base = "automountmapname=%s,%s" % (mapname, base) + maps = ldap.get_one_entry(search_base, "objectClass=*", ["*"]) + + return maps + def output_for_cli(self, textui, result, *args, **options): + for k in result: + textui.print_plain('%s: %s' % (k.get('automountinformation'), k.get('automountkey'))) + +api.register(automount_getmaps) -- cgit From 3583735c60515d604b02ddb0c62e3da9c47807cf Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Wed, 10 Dec 2008 13:49:59 -0500 Subject: Set defaults even for optional arguments. --- ipalib/cli.py | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index af3eb6e3..ca3364ae 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -687,6 +687,7 @@ class CLI(object): if self.options.interactive: self.prompt_interactively(cmd, kw) self.prompt_for_passwords(cmd, kw) + self.set_defaults(cmd, kw) result = cmd(**kw) if callable(cmd.output_for_cli): for param in cmd.params(): @@ -698,6 +699,13 @@ class CLI(object): (args, options) = cmd.params_2_args_options(kw) cmd.output_for_cli(self.api.Backend.textui, result, *args, **options) + def set_defaults(self, cmd, kw): + for param in cmd.params(): + if not kw.get(param.name): + value = param.get_default(**kw) + if value: + kw[param.name] = value + def prompt_for_passwords(self, cmd, kw): for param in cmd.params(): if 'password' not in param.flags: -- cgit From c34d2b8923ba0c8dc9a8aa1779a507a64c7c77db Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Wed, 10 Dec 2008 13:53:33 -0500 Subject: Add helper for adding Indirect maps. This creates the map and the key pointing to the map. By default the key is associated with the auto.master map but it can be overriden. --- ipalib/plugins/f_automount.py | 57 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 53 insertions(+), 4 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugins/f_automount.py b/ipalib/plugins/f_automount.py index 8de6c5ab..4c392438 100644 --- a/ipalib/plugins/f_automount.py +++ b/ipalib/plugins/f_automount.py @@ -435,13 +435,13 @@ class automount_showkey(crud.Get): def output_for_cli(self, textui, result, *args, **options): # The automount map name associated with this key is available only # in the dn. Add it as an attribute to display instead. - if entry and not entry.get('automountmapname'): - elements = explode_dn(entry.get('dn').lower()) + if result and not result.get('automountmapname'): + elements = explode_dn(result.get('dn').lower()) for e in elements: (attr, value) = e.split('=',1) if attr == 'automountmapname': - entry['automountmapname'] = value - display_entry(textui, entry) + result['automountmapname'] = value + display_entry(textui, result) api.register(automount_showkey) @@ -508,3 +508,52 @@ class automount_getmaps(frontend.Command): textui.print_plain('%s: %s' % (k.get('automountinformation'), k.get('automountkey'))) api.register(automount_getmaps) + +class automount_addindirectmap(crud.Add): + 'Add a new automap indirect mount point.' + takes_options = ( + Param('parentmap?', + cli_name='parentmap', + default='auto.master', + doc='The parent map to connect this to. Default: auto.master'), + Param('automountkey', + cli_name='key', + doc='An entry in an automount map'), + Param('description?', + doc='A description of the automount map'), + ) + + def execute(self, mapname, **kw): + """ + Execute the automount-addindirectmap operation. + + Returns the key entry as it will be created in LDAP. + + This function creates 2 LDAP entries. It creates an + automountmapname entry and an automountkey entry. + + :param mapname: The map name being added. + :param kw['parentmap'] is the top-level map to add this to. + defaulting to auto.master + :param kw['automountkey'] is the mount point + :param kw['description'] is a textual description of this map + """ + mapkw = {} + if kw.get('description'): + mapkw['description'] = kw.get('description') + newmap = api.Command['automount_addmap'](mapname, **mapkw) + + keykw = {'automountkey': kw['automountkey'], 'automountinformation': mapname} + if kw.get('description'): + keykw['description'] = kw.get('description') + newkey = api.Command['automount_addkey'](kw['parentmap'], **keykw) + + return newkey + def output_for_cli(self, textui, result, map, **options): + """ + Output result of this command to command line interface. + """ + textui.print_plain("Indirect automount map %s added" % map) + +api.register(automount_addindirectmap) + -- cgit From af7b5645af001352aff626f46ec39031b2e9b10a Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Wed, 10 Dec 2008 16:42:45 -0500 Subject: Convert to new output_for_cli() function --- ipalib/plugins/f_host.py | 40 +++++++++++---------------- ipalib/plugins/f_hostgroup.py | 64 ++++++++++++++++++------------------------- ipalib/plugins/f_pwpolicy.py | 21 ++++++-------- ipalib/plugins/f_service.py | 30 ++++++++------------ 4 files changed, 64 insertions(+), 91 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugins/f_host.py b/ipalib/plugins/f_host.py index e842230f..020231e5 100644 --- a/ipalib/plugins/f_host.py +++ b/ipalib/plugins/f_host.py @@ -149,12 +149,11 @@ class host_add(crud.Add): kw['objectclass'].remove('krbprincipalaux') return ldap.create(**kw) - def output_for_cli(self, ret): + def output_for_cli(self, textui, result, *args, **options): """ Output result of this command to command line interface. """ - if ret: - print "Host added" + textui.print_plain("Host added") api.register(host_add) @@ -172,12 +171,11 @@ class host_del(crud.Del): ldap = self.api.Backend.ldap dn = get_host(hostname) return ldap.delete(dn) - def output_for_cli(self, ret): + def output_for_cli(self, textui, result, *args, **options): """ Output result of this command to command line interface. """ - if ret: - print "Host deleted" + textui.print_plain("Host deleted") api.register(host_del) @@ -202,12 +200,11 @@ class host_mod(crud.Mod): dn = get_host(hostname) return ldap.update(dn, **kw) - def output_for_cli(self, ret): + def output_for_cli(self, textui, result, *args, **options): """ Output result of this command to command line interface. """ - if ret: - print "Host updated" + textui.print_plain("Host updated") api.register(host_mod) @@ -242,21 +239,18 @@ class host_find(crud.Find): else: kw['attributes'] = default_attributes return ldap.search(**kw) - def output_for_cli(self, hosts): - if not hosts: - return - counter = hosts[0] - hosts = hosts[1:] + def output_for_cli(self, textui, result, *args, **options): + counter = result[0] + hosts = result[1:] if counter == 0: - print "No entries found" + textui.print_plain("No entries found") return - elif counter == -1: - print "These results are truncated." - print "Please refine your search and try again." for h in hosts: - for a in h.keys(): - print "%s: %s" % (a, h[a]) + textui.print_entry(h) + if counter == -1: + textui.print_plain("These results are truncated.") + textui.print_plain("Please refine your search and try again.") api.register(host_find) @@ -286,9 +280,7 @@ class host_show(crud.Get): value = ldap.retrieve(dn, default_attributes) del value['dn'] return value - def output_for_cli(self, host): - if host: - for a in host.keys(): - print "%s: %s" % (a, host[a]) + def output_for_cli(self, textui, result, *args, **options): + textui.print_entry(result) api.register(host_show) diff --git a/ipalib/plugins/f_hostgroup.py b/ipalib/plugins/f_hostgroup.py index 6cbf4d51..bde257f9 100644 --- a/ipalib/plugins/f_hostgroup.py +++ b/ipalib/plugins/f_hostgroup.py @@ -93,12 +93,11 @@ class hostgroup_add(crud.Add): return ldap.create(**kw) - def output_for_cli(self, ret): + def output_for_cli(self, textui, result, *args, **options): """ Output result of this command to command line interface. """ - if ret: - print "Group added" + textui.print_plain("Group added") api.register(hostgroup_add) @@ -120,12 +119,11 @@ class hostgroup_del(crud.Del): return ldap.delete(dn) - def output_for_cli(self, ret): + def output_for_cli(self, textui, result, *args, **options): """ Output result of this command to command line interface. """ - if ret: - print "Group deleted" + textui.print_plain("Group deleted") api.register(hostgroup_del) @@ -150,12 +148,11 @@ class hostgroup_mod(crud.Mod): dn = ldap.find_entry_dn("cn", cn, hostgroup_filter) return ldap.update(dn, **kw) - def output_for_cli(self, ret): + def output_for_cli(self, textui, result, *args, **options): """ Output result of this command to command line interface. """ - if ret: - print "Group updated" + texui.print_plain("Group updated") api.register(hostgroup_mod) @@ -178,22 +175,19 @@ class hostgroup_find(crud.Find): kw['objectclass'] = hostgroup_filter return ldap.search(**kw) - def output_for_cli(self, groups): - if not groups: - return - - counter = groups[0] - groups = groups[1:] + def output_for_cli(self, textui, result, *args, **options): + counter = result[0] + groups = result[1:] if counter == 0: - print "No entries found" + textui.print_plain("No entries found") return - elif counter == -1: - print "These results are truncated." - print "Please refine your search and try again." for g in groups: - for a in g.keys(): - print "%s: %s" % (a, g[a]) + textui.print_entry(g) + + if counter == -1: + textui.print_plain("These results are truncated.") + textui.print_plain("Please refine your search and try again.") api.register(hostgroup_find) @@ -219,12 +213,8 @@ class hostgroup_show(crud.Get): # FIXME: should kw contain the list of attributes to display? return ldap.retrieve(dn) - def output_for_cli(self, group): - if not group: - return - - for a in group.keys(): - print "%s: %s" % (a, group[a]) + def output_for_cli(self, textui, result, *args, **options): + textui.print_entry(result) api.register(hostgroup_show) @@ -283,16 +273,16 @@ class hostgroup_add_member(frontend.Command): return add_failed - def output_for_cli(self, add_failed): + def output_for_cli(self, textui, result, *args, **options): """ Output result of this command to command line interface. """ - if add_failed: - print "These entries failed to add to the group:" - for a in add_failed: + if result: + textui.print_plain("These entries failed to add to the group:") + for a in result: print "\t'%s'" % a else: - print "Group membership updated." + textui.print_entry("Group membership updated.") api.register(hostgroup_add_member) @@ -351,15 +341,15 @@ class hostgroup_remove_member(frontend.Command): return remove_failed - def output_for_cli(self, remove_failed): + def output_for_cli(self, textui, result, *args, **options): """ Output result of this command to command line interface. """ - if remove_failed: - print "These entries failed to be removed from the group:" - for a in remove_failed: + if result: + textui.print_plain("These entries failed to be removed from the group:") + for a in result: print "\t'%s'" % a else: - print "Group membership updated." + textui.print_plain("Group membership updated.") api.register(hostgroup_remove_member) diff --git a/ipalib/plugins/f_pwpolicy.py b/ipalib/plugins/f_pwpolicy.py index ce52e467..87a7d8fa 100644 --- a/ipalib/plugins/f_pwpolicy.py +++ b/ipalib/plugins/f_pwpolicy.py @@ -88,9 +88,8 @@ class pwpolicy_mod(frontend.Command): return ldap.update(dn, **kw) - def output_for_cli(self, ret): - if ret: - print "Policy modified" + def output_for_cli(self, textui, result, *args, **options): + textui.print_plain("Policy modified") api.register(pwpolicy_mod) @@ -120,14 +119,12 @@ class pwpolicy_show(frontend.Command): return policy - def output_for_cli(self, policy): - if not policy: return - - print "Password Policy" - print "Min. Password Lifetime (hours): %s" % policy.get('krbminpwdlife') - print "Max. Password Lifetime (days): %s" % policy.get('krbmaxpwdlife') - print "Min. Number of Character Classes: %s" % policy.get('krbpwdmindiffchars') - print "Min. Length of Password: %s" % policy.get('krbpwdminlength') - print "Password History Size: %s" % policy.get('krbpwdhistorylength') + def output_for_cli(self, textui, result, *args, **options): + textui.print_plain("Password Policy") + textui.print_plain("Min. Password Lifetime (hours): %s" % result.get('krbminpwdlife')) + textui.print_plain("Max. Password Lifetime (days): %s" % result.get('krbmaxpwdlife')) + textui.print_plain("Min. Number of Character Classes: %s" % result.get('krbpwdmindiffchars')) + textui.print_plain("Min. Length of Password: %s" % result.get('krbpwdminlength')) + textui.print_plain("Password History Size: %s" % result.get('krbpwdhistorylength')) api.register(pwpolicy_show) diff --git a/ipalib/plugins/f_service.py b/ipalib/plugins/f_service.py index 04187a86..fc0ae65e 100644 --- a/ipalib/plugins/f_service.py +++ b/ipalib/plugins/f_service.py @@ -158,22 +158,20 @@ class service_find(crud.Find): return ldap.search(**kw) - def output_for_cli(self, services): - if not services: - return - - counter = services[0] - services = services[1:] + def output_for_cli(self, textui, result, *args, **options): + counter = result[0] + services = result[1:] if counter == 0: - print "No entries found" + textui.print_plain("No entries found") return - elif counter == -1: - print "These results are truncated." - print "Please refine your search and try again." for s in services: - for a in s.keys(): - print "%s: %s" % (a, s[a]) + textui.print_entry(s) + + if counter == -1: + textui.print_plain("These results are truncated.") + textui.print_plain("Please refine your search and try again.") + textui.print_count(services, '%d services matched') api.register(service_find) @@ -196,11 +194,7 @@ class service_show(crud.Get): dn = ldap.find_entry_dn("krbprincipalname", principal) # FIXME: should kw contain the list of attributes to display? return ldap.retrieve(dn) - def output_for_cli(self, service): - if not service: - return - - for a in service.keys(): - print "%s: %s" % (a, service[a]) + def output_for_cli(self, textui, result, *args, **options): + textui.print_entry(result) api.register(service_show) -- cgit From 46bd3974af5ce312cb1dd3ca12e6184d78dc470e Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Wed, 10 Dec 2008 16:45:07 -0500 Subject: Don't pass along the kw dictionary we were passed by XML-RPC. We generally want to just search indexed attributes. We get this list of attributes from the configuration, use it. --- ipalib/plugins/f_group.py | 7 ++++--- ipalib/plugins/f_host.py | 11 ++++++----- ipalib/plugins/f_hostgroup.py | 7 ++++--- ipalib/plugins/f_service.py | 9 +++++---- ipalib/plugins/f_user.py | 11 ++++++----- 5 files changed, 25 insertions(+), 20 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugins/f_group.py b/ipalib/plugins/f_group.py index 6fe95006..803e5d00 100644 --- a/ipalib/plugins/f_group.py +++ b/ipalib/plugins/f_group.py @@ -184,13 +184,14 @@ class group_find(crud.Find): search_fields_conf_str = config.get('ipagroupsearchfields') search_fields = search_fields_conf_str.split(",") + search_kw = {} for s in search_fields: - kw[s] = term + search_kw[s] = term object_type = ldap.get_object_type("cn") if object_type and not kw.get('objectclass'): - kw['objectclass'] = object_type - return ldap.search(**kw) + search_kw['objectclass'] = object_type + return ldap.search(**search_kw) def output_for_cli(self, textui, result, uid, **options): counter = result[0] diff --git a/ipalib/plugins/f_host.py b/ipalib/plugins/f_host.py index 020231e5..7903ff90 100644 --- a/ipalib/plugins/f_host.py +++ b/ipalib/plugins/f_host.py @@ -229,16 +229,17 @@ class host_find(crud.Find): #search_fields = search_fields_conf_str.split(",") search_fields = ['cn','serverhostname','description','localityname','nshostlocation','nshardwareplatform','nsosversion'] + search_kw = {} for s in search_fields: - kw[s] = term + search_kw[s] = term # Can't use ldap.get_object_type() since cn is also used for group dns - kw['objectclass'] = "ipaHost" + search_kw['objectclass'] = "ipaHost" if kw.get('all', False): - kw['attributes'] = ['*'] + search_kw['attributes'] = ['*'] else: - kw['attributes'] = default_attributes - return ldap.search(**kw) + search_kw['attributes'] = default_attributes + return ldap.search(**search_kw) def output_for_cli(self, textui, result, *args, **options): counter = result[0] hosts = result[1:] diff --git a/ipalib/plugins/f_hostgroup.py b/ipalib/plugins/f_hostgroup.py index bde257f9..3e14b09a 100644 --- a/ipalib/plugins/f_hostgroup.py +++ b/ipalib/plugins/f_hostgroup.py @@ -169,11 +169,12 @@ class hostgroup_find(crud.Find): search_fields_conf_str = config.get('ipagroupsearchfields') search_fields = search_fields_conf_str.split(",") + search_kw = {} for s in search_fields: - kw[s] = term + search_kw[s] = term - kw['objectclass'] = hostgroup_filter - return ldap.search(**kw) + search_kw['objectclass'] = hostgroup_filter + return ldap.search(**search_kw) def output_for_cli(self, textui, result, *args, **options): counter = result[0] diff --git a/ipalib/plugins/f_service.py b/ipalib/plugins/f_service.py index fc0ae65e..a353d52e 100644 --- a/ipalib/plugins/f_service.py +++ b/ipalib/plugins/f_service.py @@ -149,14 +149,15 @@ class service_find(crud.Find): def execute(self, principal, **kw): ldap = self.api.Backend.ldap - kw['filter'] = "&(objectclass=krbPrincipalAux)(!(objectClass=posixAccount))(!(|(krbprincipalname=kadmin/*)(krbprincipalname=K/M@*)(krbprincipalname=krbtgt/*)))" - kw['krbprincipalname'] = principal + search_kw = {} + search_kw['filter'] = "&(objectclass=krbPrincipalAux)(!(objectClass=posixAccount))(!(|(krbprincipalname=kadmin/*)(krbprincipalname=K/M@*)(krbprincipalname=krbtgt/*)))" + search_kw['krbprincipalname'] = principal object_type = ldap.get_object_type("krbprincipalname") if object_type and not kw.get('objectclass'): - kw['objectclass'] = object_type + search_kw['objectclass'] = object_type - return ldap.search(**kw) + return ldap.search(**search_kw) def output_for_cli(self, textui, result, *args, **options): counter = result[0] diff --git a/ipalib/plugins/f_user.py b/ipalib/plugins/f_user.py index c8b819dd..8cd3a592 100644 --- a/ipalib/plugins/f_user.py +++ b/ipalib/plugins/f_user.py @@ -282,17 +282,18 @@ class user_find(crud.Find): search_fields_conf_str = config.get('ipausersearchfields') search_fields = search_fields_conf_str.split(",") + search_kw = {} for s in search_fields: - kw[s] = term + search_kw[s] = term object_type = ldap.get_object_type("uid") if object_type and not kw.get('objectclass'): - kw['objectclass'] = object_type + search_kw['objectclass'] = object_type if kw.get('all', False): - kw['attributes'] = ['*'] + search_kw['attributes'] = ['*'] else: - kw['attributes'] = default_attributes - return ldap.search(**kw) + search_kw['attributes'] = default_attributes + return ldap.search(**search_kw) def output_for_cli(self, textui, result, uid, **options): counter = result[0] -- cgit From 22209a0f0333526fe953a66f6ea4dd1be18dddc6 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 10 Dec 2008 21:14:05 -0700 Subject: Started roughing out the consolidated type/parameter system in parameters.py; started corresponding unit tests --- ipalib/constants.py | 6 +- ipalib/parameter.py | 188 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 ipalib/parameter.py (limited to 'ipalib') diff --git a/ipalib/constants.py b/ipalib/constants.py index 7e562b53..06ff99d5 100644 --- a/ipalib/constants.py +++ b/ipalib/constants.py @@ -19,9 +19,13 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ -All constants centralized in one file. +All constants centralised in one file. """ +# The parameter system treats all these values as None: +NULLS = (None, '', u'', tuple(), []) + + # Used for a tab (or indentation level) when formatting for CLI: CLI_TAB = ' ' # Two spaces diff --git a/ipalib/parameter.py b/ipalib/parameter.py new file mode 100644 index 00000000..b75b8fbe --- /dev/null +++ b/ipalib/parameter.py @@ -0,0 +1,188 @@ +# Authors: +# Jason Gerard DeRose +# +# 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 + +""" +Parameter system for command plugins. +""" + +from plugable import ReadOnly, lock, check_name +from constants import NULLS + + +def parse_param_spec(spec): + """ + Parse a param spec into to (name, kw). + + The ``spec`` string determines the param name, whether the param is + required, and whether the param is multivalue according the following + syntax: + + ====== ===== ======== ========== + Spec Name Required Multivalue + ====== ===== ======== ========== + 'var' 'var' True False + 'var?' 'var' False False + 'var*' 'var' False True + 'var+' 'var' True True + ====== ===== ======== ========== + + For example, + + >>> parse_param_spec('login') + ('login', {'required': True, 'multivalue': False}) + >>> parse_param_spec('gecos?') + ('gecos', {'required': False, 'multivalue': False}) + >>> parse_param_spec('telephone_numbers*') + ('telephone_numbers', {'required': False, 'multivalue': True}) + >>> parse_param_spec('group+') + ('group', {'required': True, 'multivalue': True}) + + :param spec: A spec string. + """ + if type(spec) is not str: + raise_TypeError(spec, str, 'spec') + if len(spec) < 2: + raise ValueError( + 'param spec must be at least 2 characters; got %r' % spec + ) + _map = { + '?': dict(required=False, multivalue=False), + '*': dict(required=False, multivalue=True), + '+': dict(required=True, multivalue=True), + } + end = spec[-1] + if end in _map: + return (spec[:-1], _map[end]) + return (spec, dict(required=True, multivalue=False)) + + +class Param(ReadOnly): + """ + Base class for all IPA types. + """ + + __kwargs = dict( + cli_name=(str, None), + doc=(str, ''), + required=(bool, True), + multivalue=(bool, False), + primary_key=(bool, False), + normalize=(callable, None), + default=(None, None), + default_from=(callable, None), + flags=(frozenset, frozenset()), + ) + + def __init__(self, name, **overrides): + self.param_spec = name + self.name = check_name(name) + lock(self) + + def normalize(self, value): + """ + """ + if self.__normalize is None: + return value + if self.multivalue: + if type(value) in (tuple, list): + return tuple( + self.__normalize_scalar(v) for v in value + ) + return (self.__normalize_scalar(value),) # Return a tuple + return self.__normalize_scalar(value) + + def __normalize_scalar(self, value): + """ + Normalize a scalar value. + + This method is called once for each value in multivalue. + """ + if type(value) is not unicode: + return value + try: + return self.__normalize(value) + except StandardError: + return value + + + def convert(self, value): + if value in NULLS: + return + if self.multivalue: + if type(value) in (tuple, list): + values = filter( + lambda val: val not in NULLS, + (self._convert_scalar(v, i) for (i, v) in enumerate(value)) + ) + if len(values) == 0: + return + return tuple(values) + return (scalar(value, 0),) # Return a tuple + return scalar(value) + + def _convert_scalar(self, value, index=None): + """ + Implement in subclass. + """ + raise NotImplementedError( + '%s.%s()' % (self.__class__.__name__, '_convert_scalar') + ) + + + + +class Bool(Param): + """ + + """ + + +class Int(Param): + """ + + """ + + +class Float(Param): + """ + + """ + + +class Bytes(Param): + """ + + """ + + +class Str(Param): + """ + + """ + + def __init__(self, name, **overrides): + self.type = unicode + super(Str, self).__init__(name, **overrides) + + def _convert_scalar(self, value, index=None): + if type(value) in (self.type, int, float, bool): + return self.type(value) + raise TypeError( + 'Can only implicitly convert int, float, or bool; got %r' % value + ) -- cgit From e41fcf19fe82c41fe024b261d94814e092e6abaf Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Thu, 11 Dec 2008 10:31:27 -0500 Subject: Raise an error on bad principals instead of printing one when changing passwords Fix logic in determining what to do with an incoming principal --- ipalib/errors.py | 4 ++++ ipalib/plugins/f_passwd.py | 14 +++++++------- 2 files changed, 11 insertions(+), 7 deletions(-) (limited to 'ipalib') diff --git a/ipalib/errors.py b/ipalib/errors.py index 989721be..724654ff 100644 --- a/ipalib/errors.py +++ b/ipalib/errors.py @@ -413,6 +413,10 @@ class InsufficientAccess(GenericError): """You do not have permission to perform this task""" faultCode = 1027 +class InvalidUserPrincipal(GenericError): + """Invalid user principal""" + faultCode = 1028 + class FunctionDeprecated(GenericError): """Raised by a deprecated function""" faultCode = 2000 diff --git a/ipalib/plugins/f_passwd.py b/ipalib/plugins/f_passwd.py index edc13b63..c82cd455 100644 --- a/ipalib/plugins/f_passwd.py +++ b/ipalib/plugins/f_passwd.py @@ -52,14 +52,14 @@ class passwd(frontend.Command): :param param uid: The login name of the user being updated. :param kw: Not used. """ - if principal.find('@') < 0: + import pdb + pdb.set_trace() + if principal.find('@') > 0: u = principal.split('@') - if len(u) > 2 or len(u) == 0: - print "Invalid user name (%s)" % principal - if len(u) == 1: - principal = principal+"@"+self.api.env.realm - else: - principal = principal + if len(u) > 2: + raise errors.InvalidUserPrincipal, principal + else: + principal = principal+"@"+self.api.env.realm dn = self.Backend.ldap.find_entry_dn( "krbprincipalname", principal, -- cgit From c025ed6404e147f19b71b398e920fd1b3a05452a Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Thu, 11 Dec 2008 16:06:26 -0500 Subject: Remove some debugging statements --- ipalib/plugins/f_passwd.py | 2 -- 1 file changed, 2 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugins/f_passwd.py b/ipalib/plugins/f_passwd.py index c82cd455..1e0dfc1c 100644 --- a/ipalib/plugins/f_passwd.py +++ b/ipalib/plugins/f_passwd.py @@ -52,8 +52,6 @@ class passwd(frontend.Command): :param param uid: The login name of the user being updated. :param kw: Not used. """ - import pdb - pdb.set_trace() if principal.find('@') > 0: u = principal.split('@') if len(u) > 2: -- cgit From 5c47b56d14d56b825cbfe6a06e056bb98fbb2378 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 11 Dec 2008 18:07:54 -0700 Subject: Finished kwarg validation and extension mechanism in parameter.Param --- ipalib/constants.py | 6 ++++++ ipalib/parameter.py | 36 +++++++++++++++++++++++++++++++----- 2 files changed, 37 insertions(+), 5 deletions(-) (limited to 'ipalib') diff --git a/ipalib/constants.py b/ipalib/constants.py index 06ff99d5..d028a001 100644 --- a/ipalib/constants.py +++ b/ipalib/constants.py @@ -26,6 +26,12 @@ All constants centralised in one file. NULLS = (None, '', u'', tuple(), []) +TYPE_ERROR = '%s: need a %r; got %r (a %r)' + + +CALLABLE_ERROR = '%s: need a callable; got %r (a %r)' + + # Used for a tab (or indentation level) when formatting for CLI: CLI_TAB = ' ' # Two spaces diff --git a/ipalib/parameter.py b/ipalib/parameter.py index b75b8fbe..d67eb595 100644 --- a/ipalib/parameter.py +++ b/ipalib/parameter.py @@ -22,7 +22,7 @@ Parameter system for command plugins. """ from plugable import ReadOnly, lock, check_name -from constants import NULLS +from constants import NULLS, TYPE_ERROR, CALLABLE_ERROR def parse_param_spec(spec): @@ -83,15 +83,42 @@ class Param(ReadOnly): required=(bool, True), multivalue=(bool, False), primary_key=(bool, False), - normalize=(callable, None), + normalizer=(callable, None), default=(None, None), default_from=(callable, None), flags=(frozenset, frozenset()), ) - def __init__(self, name, **overrides): + def __init__(self, name, kwargs, **overrides): self.param_spec = name self.name = check_name(name) + kwargs = dict(kwargs) + assert set(self.__kwargs).intersection(kwargs) == set() + kwargs.update(self.__kwargs) + for (key, (kind, default)) in kwargs.iteritems(): + value = overrides.get(key, default) + if value is None: + if kind is bool: + raise TypeError( + TYPE_ERROR % (key, bool, value, type(value)) + ) + else: + if ( + type(kind) is type and type(value) is not kind or + type(kind) is tuple and not isinstance(value, kind) + ): + raise TypeError( + TYPE_ERROR % (key, kind, value, type(value)) + ) + elif kind is callable and not callable(value): + raise TypeError( + CALLABLE_ERROR % (key, value, type(value)) + ) + if hasattr(self, key): + raise ValueError('kwarg %r conflicts with attribute on %s' % ( + key, self.__class__.__name__) + ) + setattr(self, key, value) lock(self) def normalize(self, value): @@ -120,7 +147,6 @@ class Param(ReadOnly): except StandardError: return value - def convert(self, value): if value in NULLS: return @@ -178,7 +204,7 @@ class Str(Param): def __init__(self, name, **overrides): self.type = unicode - super(Str, self).__init__(name, **overrides) + super(Str, self).__init__(name, {}, **overrides) def _convert_scalar(self, value, index=None): if type(value) in (self.type, int, float, bool): -- cgit From 64ae4bc986176383f8aed1c1272edf0031e99d71 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 11 Dec 2008 20:30:59 -0700 Subject: Copied DefaultFrom into parameter.py; added docstring to new Param.normalize() method; more work and unit tests in new Param class --- ipalib/parameter.py | 147 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 142 insertions(+), 5 deletions(-) (limited to 'ipalib') diff --git a/ipalib/parameter.py b/ipalib/parameter.py index d67eb595..b071e9d5 100644 --- a/ipalib/parameter.py +++ b/ipalib/parameter.py @@ -72,6 +72,110 @@ def parse_param_spec(spec): return (spec, dict(required=True, multivalue=False)) +class DefaultFrom(ReadOnly): + """ + Derive a default value from other supplied values. + + For example, say you wanted to create a default for the user's login from + the user's first and last names. It could be implemented like this: + + >>> login = DefaultFrom(lambda first, last: first[0] + last) + >>> login(first='John', last='Doe') + 'JDoe' + + If you do not explicitly provide keys when you create a DefaultFrom + instance, the keys are implicitly derived from your callback by + inspecting ``callback.func_code.co_varnames``. The keys are available + through the ``DefaultFrom.keys`` instance attribute, like this: + + >>> login.keys + ('first', 'last') + + The callback is available through the ``DefaultFrom.callback`` instance + attribute, like this: + + >>> login.callback # doctest:+ELLIPSIS + at 0x...> + >>> login.callback.func_code.co_varnames # The keys + ('first', 'last') + + The keys can be explicitly provided as optional positional arguments after + the callback. For example, this is equivalent to the ``login`` instance + above: + + >>> login2 = DefaultFrom(lambda a, b: a[0] + b, 'first', 'last') + >>> login2.keys + ('first', 'last') + >>> login2.callback.func_code.co_varnames # Not the keys + ('a', 'b') + >>> login2(first='John', last='Doe') + 'JDoe' + + If any keys are missing when calling your DefaultFrom instance, your + callback is not called and None is returned. For example: + + >>> login(first='John', lastname='Doe') is None + True + >>> login() is None + True + + Any additional keys are simply ignored, like this: + + >>> login(last='Doe', first='John', middle='Whatever') + 'JDoe' + + As above, because `DefaultFrom.__call__` takes only pure keyword + arguments, they can be supplied in any order. + + Of course, the callback need not be a lambda expression. This third + example is equivalent to both the ``login`` and ``login2`` instances + above: + + >>> def get_login(first, last): + ... return first[0] + last + ... + >>> login3 = DefaultFrom(get_login) + >>> login3.keys + ('first', 'last') + >>> login3.callback.func_code.co_varnames + ('first', 'last') + >>> login3(first='John', last='Doe') + 'JDoe' + """ + + def __init__(self, callback, *keys): + """ + :param callback: The callable to call when all keys are present. + :param keys: Optional keys used for source values. + """ + if not callable(callback): + raise TypeError('callback must be callable; got %r' % callback) + self.callback = callback + if len(keys) == 0: + fc = callback.func_code + self.keys = fc.co_varnames[:fc.co_argcount] + else: + self.keys = keys + for key in self.keys: + if type(key) is not str: + raise_TypeError(key, str, 'keys') + lock(self) + + def __call__(self, **kw): + """ + If all keys are present, calls the callback; otherwise returns None. + + :param kw: The keyword arguments. + """ + vals = tuple(kw.get(k, None) for k in self.keys) + if None in vals: + return + try: + return self.callback(*vals) + except StandardError: + pass + + class Param(ReadOnly): """ Base class for all IPA types. @@ -89,14 +193,23 @@ class Param(ReadOnly): flags=(frozenset, frozenset()), ) - def __init__(self, name, kwargs, **overrides): + def __init__(self, name, kwargs, **override): self.param_spec = name + self.__override = dict(override) + if not ('required' in override or 'multivalue' in override): + (name, kw_from_spec) = parse_param_spec(name) + override.update(kw_from_spec) self.name = check_name(name) + if 'cli_name' not in override: + override['cli_name'] = self.name + df = override.get('default_from', None) + if callable(df) and not isinstance(df, DefaultFrom): + override['default_from'] = DefaultFrom(df) kwargs = dict(kwargs) assert set(self.__kwargs).intersection(kwargs) == set() kwargs.update(self.__kwargs) for (key, (kind, default)) in kwargs.iteritems(): - value = overrides.get(key, default) + value = override.get(key, default) if value is None: if kind is bool: raise TypeError( @@ -104,7 +217,8 @@ class Param(ReadOnly): ) else: if ( - type(kind) is type and type(value) is not kind or + type(kind) is type and type(value) is not kind + or type(kind) is tuple and not isinstance(value, kind) ): raise TypeError( @@ -119,12 +233,35 @@ class Param(ReadOnly): key, self.__class__.__name__) ) setattr(self, key, value) + check_name(self.cli_name) lock(self) def normalize(self, value): """ + Normalize ``value`` using normalizer callback. + + For example: + + >>> param = Str('telephone', + ... normalizer=lambda value: value.replace('.', '-') + ... ) + >>> param.normalize(u'800.123.4567') + u'800-123-4567' + + (Note that `Str` is a subclass of `Param`.) + + If this `Param` instance was created with a normalizer callback and + ``value`` is a unicode instance, the normalizer callback is called and + *its* return value is returned. + + On the other hand, if this `Param` instance was *not* created with a + normalizer callback, if ``value`` is *not* a unicode instance, or if an + exception is caught when calling the normalizer callback, ``value`` is + returned unchanged. + + :param value: A proposed value for this parameter. """ - if self.__normalize is None: + if self.normalizer is None: return value if self.multivalue: if type(value) in (tuple, list): @@ -143,7 +280,7 @@ class Param(ReadOnly): if type(value) is not unicode: return value try: - return self.__normalize(value) + return self.normalizer(value) except StandardError: return value -- cgit From 079721da2cfdd9bc040f7fe6e2842a4775e0e964 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 11 Dec 2008 22:39:50 -0700 Subject: New Param: changed kwargs class attribute to a tuple so the subclass interface is simpler --- ipalib/parameter.py | 85 ++++++++++++++++++++++++++++++----------------------- 1 file changed, 49 insertions(+), 36 deletions(-) (limited to 'ipalib') diff --git a/ipalib/parameter.py b/ipalib/parameter.py index b071e9d5..6acf0483 100644 --- a/ipalib/parameter.py +++ b/ipalib/parameter.py @@ -21,6 +21,7 @@ Parameter system for command plugins. """ +from types import NoneType from plugable import ReadOnly, lock, check_name from constants import NULLS, TYPE_ERROR, CALLABLE_ERROR @@ -178,44 +179,46 @@ class DefaultFrom(ReadOnly): class Param(ReadOnly): """ - Base class for all IPA types. + Base class for all parameters. """ - __kwargs = dict( - cli_name=(str, None), - doc=(str, ''), - required=(bool, True), - multivalue=(bool, False), - primary_key=(bool, False), - normalizer=(callable, None), - default=(None, None), - default_from=(callable, None), - flags=(frozenset, frozenset()), + kwargs = ( + ('cli_name', str, None), + ('doc', str, ''), + ('required', bool, True), + ('multivalue', bool, False), + ('primary_key', bool, False), + ('normalizer', callable, None), + ('default_from', callable, None), + ('flags', frozenset, frozenset()), + + # The 'default' kwarg gets appended in Param.__init__(): + # ('default', self.type, None), ) - def __init__(self, name, kwargs, **override): + # This is a dummy type so that most of the functionality of Param can be + # unit tested directly without always creating a subclass; however, real + # (direct) subclasses should *always* override this class attribute: + type = NoneType # This isn't very useful in the real world! + + def __init__(self, name, **kw): + assert type(self.type) is type self.param_spec = name - self.__override = dict(override) - if not ('required' in override or 'multivalue' in override): + self.__kw = dict(kw) + if not ('required' in kw or 'multivalue' in kw): (name, kw_from_spec) = parse_param_spec(name) - override.update(kw_from_spec) + kw.update(kw_from_spec) self.name = check_name(name) - if 'cli_name' not in override: - override['cli_name'] = self.name - df = override.get('default_from', None) + if kw.get('cli_name', None) is None: + kw['cli_name'] = self.name + df = kw.get('default_from', None) if callable(df) and not isinstance(df, DefaultFrom): - override['default_from'] = DefaultFrom(df) - kwargs = dict(kwargs) - assert set(self.__kwargs).intersection(kwargs) == set() - kwargs.update(self.__kwargs) - for (key, (kind, default)) in kwargs.iteritems(): - value = override.get(key, default) - if value is None: - if kind is bool: - raise TypeError( - TYPE_ERROR % (key, bool, value, type(value)) - ) - else: + kw['default_from'] = DefaultFrom(df) + self.__clonekw = kw + self.kwargs += (('default', self.type, None),) + for (key, kind, default) in self.kwargs: + value = kw.get(key, default) + if value is not None: if ( type(kind) is type and type(value) is not kind or @@ -275,7 +278,7 @@ class Param(ReadOnly): """ Normalize a scalar value. - This method is called once for each value in multivalue. + This method is called once for each value in a multivalue. """ if type(value) is not unicode: return value @@ -308,8 +311,6 @@ class Param(ReadOnly): ) - - class Bool(Param): """ @@ -333,15 +334,27 @@ class Bytes(Param): """ + type = str + + def __init__(self, name, **kw): + kwargs = dict( + minlength=(int, None), + maxlength=(int, None), + length=(int, None), + pattern=(str, None), + ) + + class Str(Param): """ """ - def __init__(self, name, **overrides): - self.type = unicode - super(Str, self).__init__(name, {}, **overrides) + type = unicode + + def __init__(self, name, **kw): + super(Str, self).__init__(name, **kw) def _convert_scalar(self, value, index=None): if type(value) in (self.type, int, float, bool): -- cgit From 5272949bfd01b9648902fd9dea77762d06d52832 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 12 Dec 2008 03:13:58 -0700 Subject: New Param: added all logic for minlength, maxlength, and length in Bytes class (which Str inherits) --- ipalib/parameter.py | 70 ++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 56 insertions(+), 14 deletions(-) (limited to 'ipalib') diff --git a/ipalib/parameter.py b/ipalib/parameter.py index 6acf0483..82be8fee 100644 --- a/ipalib/parameter.py +++ b/ipalib/parameter.py @@ -203,19 +203,27 @@ class Param(ReadOnly): def __init__(self, name, **kw): assert type(self.type) is type + self.kwargs += (('default', self.type, None),) self.param_spec = name self.__kw = dict(kw) if not ('required' in kw or 'multivalue' in kw): (name, kw_from_spec) = parse_param_spec(name) kw.update(kw_from_spec) self.name = check_name(name) + self.nice = '%s(%r)' % (self.__class__.__name__, self.name) + if not set(t[0] for t in self.kwargs).issuperset(self.__kw): + extra = set(kw) - set(t[0] for t in self.kwargs) + raise TypeError( + '%s: no such kwargs: %s' % (self.nice, + ', '.join(repr(k) for k in sorted(extra)) + ) + ) if kw.get('cli_name', None) is None: kw['cli_name'] = self.name df = kw.get('default_from', None) if callable(df) and not isinstance(df, DefaultFrom): kw['default_from'] = DefaultFrom(df) self.__clonekw = kw - self.kwargs += (('default', self.type, None),) for (key, kind, default) in self.kwargs: value = kw.get(key, default) if value is not None: @@ -245,14 +253,12 @@ class Param(ReadOnly): For example: - >>> param = Str('telephone', + >>> param = Param('telephone', ... normalizer=lambda value: value.replace('.', '-') ... ) >>> param.normalize(u'800.123.4567') u'800-123-4567' - (Note that `Str` is a subclass of `Param`.) - If this `Param` instance was created with a normalizer callback and ``value`` is a unicode instance, the normalizer callback is called and *its* return value is returned. @@ -293,8 +299,8 @@ class Param(ReadOnly): if self.multivalue: if type(value) in (tuple, list): values = filter( - lambda val: val not in NULLS, - (self._convert_scalar(v, i) for (i, v) in enumerate(value)) + lambda val: val not in NULLS, + (self._convert_scalar(v, i) for (i, v) in enumerate(value)) ) if len(values) == 0: return @@ -336,23 +342,59 @@ class Bytes(Param): type = str - def __init__(self, name, **kw): - kwargs = dict( - minlength=(int, None), - maxlength=(int, None), - length=(int, None), - pattern=(str, None), - ) + kwargs = Param.kwargs + ( + ('minlength', int, None), + ('maxlength', int, None), + ('length', int, None), + ('pattern', str, None), + ) + def __init__(self, name, **kw): + super(Bytes, self).__init__(name, **kw) + + if not ( + self.length is None or + (self.minlength is None and self.maxlength is None) + ): + raise ValueError( + '%s: cannot mix length with minlength or maxlength' % self.nice + ) + + if self.minlength is not None and self.minlength < 1: + raise ValueError( + '%s: minlength must be >= 1; got %r' % (self.nice, self.minlength) + ) + + if self.maxlength is not None and self.maxlength < 1: + raise ValueError( + '%s: maxlength must be >= 1; got %r' % (self.nice, self.maxlength) + ) + + if None not in (self.minlength, self.maxlength): + if self.minlength > self.maxlength: + raise ValueError( + '%s: minlength > maxlength (minlength=%r, maxlength=%r)' % ( + self.nice, self.minlength, self.maxlength) + ) + elif self.minlength == self.maxlength: + raise ValueError( + '%s: minlength == maxlength; use length=%d instead' % ( + self.nice, self.minlength) + ) -class Str(Param): + +class Str(Bytes): """ """ type = unicode + kwargs = Bytes.kwargs[:-1] + ( + ('pattern', unicode, None), + ) + def __init__(self, name, **kw): super(Str, self).__init__(name, **kw) -- cgit From 66faffdfb09ee2bcee1b405c78b37e340bc043aa Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 12 Dec 2008 03:38:02 -0700 Subject: New Param: cleanup up readability of Param.__init__(); added unit tests for unknown kwargs --- ipalib/parameter.py | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) (limited to 'ipalib') diff --git a/ipalib/parameter.py b/ipalib/parameter.py index 82be8fee..2d94138c 100644 --- a/ipalib/parameter.py +++ b/ipalib/parameter.py @@ -182,6 +182,11 @@ class Param(ReadOnly): Base class for all parameters. """ + # This is a dummy type so that most of the functionality of Param can be + # unit tested directly without always creating a subclass; however, real + # (direct) subclasses should *always* override this class attribute: + type = NoneType # This isn't very useful in the real world! + kwargs = ( ('cli_name', str, None), ('doc', str, ''), @@ -196,37 +201,50 @@ class Param(ReadOnly): # ('default', self.type, None), ) - # This is a dummy type so that most of the functionality of Param can be - # unit tested directly without always creating a subclass; however, real - # (direct) subclasses should *always* override this class attribute: - type = NoneType # This isn't very useful in the real world! - def __init__(self, name, **kw): - assert type(self.type) is type - self.kwargs += (('default', self.type, None),) + # We keep these values to use in __repr__(): self.param_spec = name self.__kw = dict(kw) + + # Merge in kw from parse_param_spec(): if not ('required' in kw or 'multivalue' in kw): (name, kw_from_spec) = parse_param_spec(name) kw.update(kw_from_spec) self.name = check_name(name) self.nice = '%s(%r)' % (self.__class__.__name__, self.name) + + # Add 'default' to self.kwargs and makes sure no unknown kw were given: + assert type(self.type) is type + self.kwargs += (('default', self.type, None),) if not set(t[0] for t in self.kwargs).issuperset(self.__kw): extra = set(kw) - set(t[0] for t in self.kwargs) raise TypeError( - '%s: no such kwargs: %s' % (self.nice, + '%s: takes no such kwargs: %s' % (self.nice, ', '.join(repr(k) for k in sorted(extra)) ) ) + + # Merge in default for 'cli_name' if not given: if kw.get('cli_name', None) is None: kw['cli_name'] = self.name + + # Wrap 'default_from' in a DefaultFrom if not already: df = kw.get('default_from', None) if callable(df) and not isinstance(df, DefaultFrom): kw['default_from'] = DefaultFrom(df) + + # Keep the copy with merged values also to use when cloning: self.__clonekw = kw + + # Perform type validation on kw: for (key, kind, default) in self.kwargs: value = kw.get(key, default) if value is not None: + if kind is frozenset: + if type(value) in (list, tuple): + value = frozenset(value) + elif type(value) is str: + value = frozenset([value]) if ( type(kind) is type and type(value) is not kind or -- cgit From e05fd7ab03eb05c26e880ae04fbf095198b58027 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 12 Dec 2008 04:48:25 -0700 Subject: New Param: added basic rule logic --- ipalib/parameter.py | 55 +++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 49 insertions(+), 6 deletions(-) (limited to 'ipalib') diff --git a/ipalib/parameter.py b/ipalib/parameter.py index 2d94138c..e4268825 100644 --- a/ipalib/parameter.py +++ b/ipalib/parameter.py @@ -183,9 +183,9 @@ class Param(ReadOnly): """ # This is a dummy type so that most of the functionality of Param can be - # unit tested directly without always creating a subclass; however, real - # (direct) subclasses should *always* override this class attribute: - type = NoneType # This isn't very useful in the real world! + # unit tested directly without always creating a subclass; however, a real + # (direct) subclass must *always* override this class attribute: + type = NoneType # Ouch, this wont be very useful in the real world! kwargs = ( ('cli_name', str, None), @@ -201,7 +201,7 @@ class Param(ReadOnly): # ('default', self.type, None), ) - def __init__(self, name, **kw): + def __init__(self, name, *rules, **kw): # We keep these values to use in __repr__(): self.param_spec = name self.__kw = dict(kw) @@ -233,10 +233,11 @@ class Param(ReadOnly): if callable(df) and not isinstance(df, DefaultFrom): kw['default_from'] = DefaultFrom(df) - # Keep the copy with merged values also to use when cloning: + # We keep this copy with merged values also to use when cloning: self.__clonekw = kw - # Perform type validation on kw: + # Perform type validation on kw, add in class rules: + class_rules = [] for (key, kind, default) in self.kwargs: value = kw.get(key, default) if value is not None: @@ -262,7 +263,20 @@ class Param(ReadOnly): key, self.__class__.__name__) ) setattr(self, key, value) + rule_name = 'rule_%s' % key + if value is not None and hasattr(self, rule_name): + class_rules.append(getattr(self, rule_name)) check_name(self.cli_name) + + # Check that all the rules are callable + self.rules = tuple(class_rules) + rules + for rule in self.rules: + if not callable(rule): + raise TypeError( + '%s: rules must be callable; got %r' % (self.nice, rule) + ) + + # And we're done. lock(self) def normalize(self, value): @@ -401,6 +415,35 @@ class Bytes(Param): self.nice, self.minlength) ) + def rule_minlength(self, value): + """ + Check minlength constraint. + """ + if len(value) < self.minlength: + return 'Must be at least %(minlength)d bytes long.' % dict( + minlength=self.minlength, + ) + + def rule_maxlength(self, value): + """ + Check maxlength constraint. + """ + if len(value) > self.maxlength: + return 'Can be at most %(maxlength)d bytes long.' % dict( + maxlength=self.maxlength, + ) + + def rule_length(self, value): + """ + Check length constraint. + """ + if len(value) != self.length: + return 'Must be exactly %(length)d bytes long.' % dict( + length=self.length, + ) + + + class Str(Bytes): """ -- cgit From 67b688c7b26845dedeca378d4ba8df88e6b44c0c Mon Sep 17 00:00:00 2001 From: Jakub Hrozek Date: Tue, 16 Dec 2008 19:00:39 -0700 Subject: Jakub Hrozek's patch to make textui.get_tty_width() actually work --- ipalib/cli.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index 37fdad44..a0fe63db 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -28,6 +28,9 @@ import getpass import code import optparse import socket +import fcntl +import termios +import struct import frontend import backend @@ -67,8 +70,15 @@ class textui(backend.Backend): If stdout is not a tty, this method will return ``None``. """ + # /usr/include/asm/termios.h says that struct winsize has four + # unsigned shorts, hence the HHHH if sys.stdout.isatty(): - return 80 # FIXME: we need to return the actual tty width + try: + winsize = fcntl.ioctl(sys.stdout, termios.TIOCGWINSZ, + struct.pack('HHHH', 0, 0, 0, 0)) + return struct.unpack('HHHH', winsize)[1] + except IOError: + return None def max_col_width(self, rows, col=None): """ -- cgit From f0bbe1b5a09e1e2fc33d662c4775203f594af416 Mon Sep 17 00:00:00 2001 From: Jakub Hrozek Date: Wed, 17 Dec 2008 17:17:02 -0700 Subject: Add body for the NameSpaceError exception --- ipalib/errors.py | 11 +++++++++++ 1 file changed, 11 insertions(+) (limited to 'ipalib') diff --git a/ipalib/errors.py b/ipalib/errors.py index 25f594f2..d8591304 100644 --- a/ipalib/errors.py +++ b/ipalib/errors.py @@ -231,8 +231,19 @@ class RegistrationError(IPAError): class NameSpaceError(RegistrationError): + """ + Raised when name is not a valid Python identifier for use for use as + the name of NameSpace member. + """ msg = 'name %r does not re.match %r' + def __init__(self, name, regex): + self.name = name + self.regex = regex + + def __str__(self): + return self.msg % (self.name, self.regex) + class SubclassError(RegistrationError): """ -- cgit From 360f95341a78e2fd601a38ffa103a5f5cbe8c424 Mon Sep 17 00:00:00 2001 From: Jakub Hrozek Date: Wed, 17 Dec 2008 17:21:25 -0700 Subject: Fix show_api command --- ipalib/cli.py | 6 +++--- ipalib/errors.py | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index a0fe63db..0bab952b 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -443,11 +443,11 @@ class show_api(frontend.Application): else: for name in namespaces: if name not in self.api: - exit_error('api has no such namespace: %s' % name) + raise errors.NoSuchNamespaceError(name) names = namespaces lines = self.__traverse(names) ml = max(len(l[1]) for l in lines) - self.print_name() + self.Backend.textui.print_name('run') first = True for line in lines: if line[0] == 0 and not first: @@ -463,7 +463,7 @@ class show_api(frontend.Application): s = '1 attribute shown.' else: s = '%d attributes show.' % len(lines) - self.print_dashed(s) + self.Backend.textui.print_dashed(s) def __traverse(self, names): diff --git a/ipalib/errors.py b/ipalib/errors.py index d8591304..bc4074d2 100644 --- a/ipalib/errors.py +++ b/ipalib/errors.py @@ -117,6 +117,9 @@ class InvocationError(IPAError): class UnknownCommandError(InvocationError): format = 'unknown command "%s"' +class NoSuchNamespaceError(InvocationError): + format = 'api has no such namespace: %s' + def _(text): return text -- cgit From ba481e7712b9d92694a38399936fd0eceef93cb6 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 17 Dec 2008 18:32:46 -0700 Subject: New Param: split class_rules and *rules into separate attributes --- ipalib/parameter.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) (limited to 'ipalib') diff --git a/ipalib/parameter.py b/ipalib/parameter.py index e4268825..9653e168 100644 --- a/ipalib/parameter.py +++ b/ipalib/parameter.py @@ -211,7 +211,7 @@ class Param(ReadOnly): (name, kw_from_spec) = parse_param_spec(name) kw.update(kw_from_spec) self.name = check_name(name) - self.nice = '%s(%r)' % (self.__class__.__name__, self.name) + self.nice = '%s(%r)' % (self.__class__.__name__, self.param_spec) # Add 'default' to self.kwargs and makes sure no unknown kw were given: assert type(self.type) is type @@ -269,8 +269,10 @@ class Param(ReadOnly): check_name(self.cli_name) # Check that all the rules are callable - self.rules = tuple(class_rules) + rules - for rule in self.rules: + self.class_rules = tuple(class_rules) + self.rules = rules + self.all_rules = self.class_rules + self.rules + for rule in self.all_rules: if not callable(rule): raise TypeError( '%s: rules must be callable; got %r' % (self.nice, rule) -- cgit From 69041c3b1b2494d89097e490048c23292c8cbc52 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 17 Dec 2008 21:47:43 -0700 Subject: Removed Plugin.name property and replaced with instance attribute created in Plugin.__init__() --- ipalib/frontend.py | 5 +++-- ipalib/plugable.py | 11 ++++------- 2 files changed, 7 insertions(+), 9 deletions(-) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index e4dd7637..4ff77c59 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -965,6 +965,7 @@ class Attribute(plugable.Plugin): assert m self.__obj_name = m.group(1) self.__attr_name = m.group(2) + super(Attribute, self).__init__() def __get_obj_name(self): return self.__obj_name @@ -1053,8 +1054,7 @@ class Method(Attribute, Command): __public__ = Attribute.__public__.union(Command.__public__) def __init__(self): - Attribute.__init__(self) - Command.__init__(self) + super(Method, self).__init__() class Property(Attribute): @@ -1087,6 +1087,7 @@ class Property(Attribute): rules=self.rules, normalize=self.normalize, ) + super(Property, self).__init__() def __rules_iter(self): """ diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 7dafd440..2bed992d 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -254,17 +254,14 @@ class Plugin(ReadOnly): __api = None def __init__(self): + cls = self.__class__ + self.name = cls.__name__ + self.module = cls.__module__ + self.fullname = '%s.%s' % (self.module, self.name) log = logging.getLogger('ipa') for name in ('debug', 'info', 'warning', 'error', 'critical'): setattr(self, name, getattr(log, name)) - def __get_name(self): - """ - Convenience property to return the class name. - """ - return self.__class__.__name__ - name = property(__get_name) - def __get_doc(self): """ Convenience property to return the class docstring. -- cgit From 171ed58367e58c59f9f67ef831f08ce80ba8508b Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 17 Dec 2008 21:57:58 -0700 Subject: Removed Plugin.doc property and replaced with instance attribute created in Plugin.__init__() --- ipalib/plugable.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 2bed992d..5363a51e 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -258,17 +258,11 @@ class Plugin(ReadOnly): self.name = cls.__name__ self.module = cls.__module__ self.fullname = '%s.%s' % (self.module, self.name) + self.doc = cls.__doc__ log = logging.getLogger('ipa') for name in ('debug', 'info', 'warning', 'error', 'critical'): setattr(self, name, getattr(log, name)) - def __get_doc(self): - """ - Convenience property to return the class docstring. - """ - return self.__class__.__doc__ - doc = property(__get_doc) - def __get_api(self): """ Return `API` instance passed to `finalize()`. -- cgit From 4f24f0fd8837383f4a2abc54946f6f84810807b8 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 17 Dec 2008 23:08:52 -0700 Subject: Plugin.doc instance attribute is now parsed out using inspect.getdoc(); added Plugin.summary instance attribute, created in Plugin.__init__() --- ipalib/plugable.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 5363a51e..e6b5c1ac 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -258,7 +258,11 @@ class Plugin(ReadOnly): self.name = cls.__name__ self.module = cls.__module__ self.fullname = '%s.%s' % (self.module, self.name) - self.doc = cls.__doc__ + self.doc = inspect.getdoc(cls) + if self.doc is None: + self.summary = '<%s>' % self.fullname + else: + self.summary = self.doc.split('\n\n', 1)[0] log = logging.getLogger('ipa') for name in ('debug', 'info', 'warning', 'error', 'critical'): setattr(self, name, getattr(log, name)) -- cgit From 285fa3d33077b336784a8ea7633b0e011646adaa Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 17 Dec 2008 23:18:14 -0700 Subject: Removed depreciated envtest command from f_user.py --- ipalib/plugins/f_user.py | 18 ------------------ 1 file changed, 18 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugins/f_user.py b/ipalib/plugins/f_user.py index e1076242..45ee59f4 100644 --- a/ipalib/plugins/f_user.py +++ b/ipalib/plugins/f_user.py @@ -28,24 +28,6 @@ from ipalib import api from ipalib import errors from ipalib import ipa_types -# Command to get the idea how plugins will interact with api.env -class envtest(frontend.Command): - 'Show current environment.' - def run(self, *args, **kw): - print "" - print "Environment variables:" - for var in api.env: - val = api.env[var] - if var is 'server': - print "" - print " Servers:" - for item in api.env.server: - print " %s" % item - print "" - else: - print " %s: %s" % (var, val) - return {} -api.register(envtest) def display_user(user): # FIXME: for now delete dn here. In the future pass in the kw to -- cgit From b26894c166a6bdde38c627b5e1f71c8218009e7e Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 17 Dec 2008 23:30:29 -0700 Subject: New Param: changed naming convention for class rules to '_rule_%s' as these methods aren't part of the public interface --- ipalib/parameter.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'ipalib') diff --git a/ipalib/parameter.py b/ipalib/parameter.py index 9653e168..5439b9ff 100644 --- a/ipalib/parameter.py +++ b/ipalib/parameter.py @@ -263,7 +263,7 @@ class Param(ReadOnly): key, self.__class__.__name__) ) setattr(self, key, value) - rule_name = 'rule_%s' % key + rule_name = '_rule_%s' % key if value is not None and hasattr(self, rule_name): class_rules.append(getattr(self, rule_name)) check_name(self.cli_name) @@ -417,7 +417,7 @@ class Bytes(Param): self.nice, self.minlength) ) - def rule_minlength(self, value): + def _rule_minlength(self, value): """ Check minlength constraint. """ @@ -426,7 +426,7 @@ class Bytes(Param): minlength=self.minlength, ) - def rule_maxlength(self, value): + def _rule_maxlength(self, value): """ Check maxlength constraint. """ @@ -435,7 +435,7 @@ class Bytes(Param): maxlength=self.maxlength, ) - def rule_length(self, value): + def _rule_length(self, value): """ Check length constraint. """ -- cgit From 8ef6819059dbfaa8e2f97c312e0b932546b0b417 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 17 Dec 2008 23:32:58 -0700 Subject: New Param: renamed Param.__normalize_scalar() to Param._normalize_scalar() as it might make sense for subclasses to override --- ipalib/parameter.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'ipalib') diff --git a/ipalib/parameter.py b/ipalib/parameter.py index 5439b9ff..96255385 100644 --- a/ipalib/parameter.py +++ b/ipalib/parameter.py @@ -309,12 +309,12 @@ class Param(ReadOnly): if self.multivalue: if type(value) in (tuple, list): return tuple( - self.__normalize_scalar(v) for v in value + self._normalize_scalarS(v) for v in value ) - return (self.__normalize_scalar(value),) # Return a tuple - return self.__normalize_scalar(value) + return (self._normalize_scalar(value),) # Return a tuple + return self._normalize_scalar(value) - def __normalize_scalar(self, value): + def _normalize_scalar(self, value): """ Normalize a scalar value. -- cgit From bf8154fa5017d12c7377175f85f60b670dc294f9 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 18 Dec 2008 00:02:38 -0700 Subject: New Param: fixed a few things in Param.convert() and added corresponding unit tests --- ipalib/parameter.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) (limited to 'ipalib') diff --git a/ipalib/parameter.py b/ipalib/parameter.py index 96255385..aaca8d9d 100644 --- a/ipalib/parameter.py +++ b/ipalib/parameter.py @@ -309,7 +309,7 @@ class Param(ReadOnly): if self.multivalue: if type(value) in (tuple, list): return tuple( - self._normalize_scalarS(v) for v in value + self._normalize_scalar(v) for v in value ) return (self._normalize_scalar(value),) # Return a tuple return self._normalize_scalar(value) @@ -331,16 +331,16 @@ class Param(ReadOnly): if value in NULLS: return if self.multivalue: - if type(value) in (tuple, list): - values = filter( - lambda val: val not in NULLS, - (self._convert_scalar(v, i) for (i, v) in enumerate(value)) - ) - if len(values) == 0: - return - return tuple(values) - return (scalar(value, 0),) # Return a tuple - return scalar(value) + if type(value) not in (tuple, list): + value = (value,) + values = filter( + lambda val: val not in NULLS, + (self._convert_scalar(v, i) for (i, v) in enumerate(value)) + ) + if len(values) == 0: + return + return tuple(values) + return self._convert_scalar(value) def _convert_scalar(self, value, index=None): """ -- cgit From ac335bc7ea5ca290a25b9ac27e17f1b68bd8b4a9 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 18 Dec 2008 01:18:17 -0700 Subject: New Param: fixed small bug in Param.convert() and added detailed docstring --- ipalib/parameter.py | 53 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 3 deletions(-) (limited to 'ipalib') diff --git a/ipalib/parameter.py b/ipalib/parameter.py index aaca8d9d..6a5695a7 100644 --- a/ipalib/parameter.py +++ b/ipalib/parameter.py @@ -328,14 +328,61 @@ class Param(ReadOnly): return value def convert(self, value): + """ + Convert ``value`` to the Python type required by this parameter. + + For example: + + >>> scalar = Str('my_scalar') + >>> scalar.type + + >>> scalar.convert(43.2) + u'43.2' + + (Note that `Str` is a subclass of `Param`.) + + All values in `constants.NULLS` will be converted to None. For + example: + + >>> scalar.convert(u'') is None # An empty string + True + >>> scalar.convert([]) is None # An empty list + True + + Likewise, values in `constants.NULLS` will be filtered out of a + multivalue parameter. For example: + + >>> multi = Str('my_multi', multivalue=True) + >>> multi.convert([True, '', 17, None, False]) + (u'True', u'17', u'False') + >>> multi.convert([None, u'']) is None # Filters to an empty list + True + + Lastly, multivalue parameters will always return a tuple (well, + assuming they don't return None as in the last example above). + For example: + + >>> multi.convert(42) # Called with a scalar value + (u'42',) + >>> multi.convert([True, False]) # Called with a list value + (u'True', u'False') + + Note that how values are converted (and from what types they will be + converted) completely depends upon how a subclass implements its + `Param._convert_scalar()` method. For example, see + `Str._convert_scalar()`. + + :param value: A proposed value for this parameter. + """ if value in NULLS: return if self.multivalue: if type(value) not in (tuple, list): value = (value,) - values = filter( - lambda val: val not in NULLS, - (self._convert_scalar(v, i) for (i, v) in enumerate(value)) + values = tuple( + self._convert_scalar(v, i) for (i, v) in filter( + lambda tup: tup[1] not in NULLS, enumerate(value) + ) ) if len(values) == 0: return -- cgit From a632c2935aad26666a579204505ec0770bbe953d Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 18 Dec 2008 01:27:03 -0700 Subject: New Param: removed unneeded conversion to tuple() in Param.convert() --- ipalib/parameter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/parameter.py b/ipalib/parameter.py index 6a5695a7..76b69609 100644 --- a/ipalib/parameter.py +++ b/ipalib/parameter.py @@ -386,7 +386,7 @@ class Param(ReadOnly): ) if len(values) == 0: return - return tuple(values) + return values return self._convert_scalar(value) def _convert_scalar(self, value, index=None): -- cgit From 99363131df63f3b1d22bf325282eee5671eff924 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 18 Dec 2008 01:45:13 -0700 Subject: New Param: swapped location of DefaultFrom class and parse_param_spec() function --- ipalib/parameter.py | 94 ++++++++++++++++++++++++++--------------------------- 1 file changed, 47 insertions(+), 47 deletions(-) (limited to 'ipalib') diff --git a/ipalib/parameter.py b/ipalib/parameter.py index 76b69609..a80c43c4 100644 --- a/ipalib/parameter.py +++ b/ipalib/parameter.py @@ -26,53 +26,6 @@ from plugable import ReadOnly, lock, check_name from constants import NULLS, TYPE_ERROR, CALLABLE_ERROR -def parse_param_spec(spec): - """ - Parse a param spec into to (name, kw). - - The ``spec`` string determines the param name, whether the param is - required, and whether the param is multivalue according the following - syntax: - - ====== ===== ======== ========== - Spec Name Required Multivalue - ====== ===== ======== ========== - 'var' 'var' True False - 'var?' 'var' False False - 'var*' 'var' False True - 'var+' 'var' True True - ====== ===== ======== ========== - - For example, - - >>> parse_param_spec('login') - ('login', {'required': True, 'multivalue': False}) - >>> parse_param_spec('gecos?') - ('gecos', {'required': False, 'multivalue': False}) - >>> parse_param_spec('telephone_numbers*') - ('telephone_numbers', {'required': False, 'multivalue': True}) - >>> parse_param_spec('group+') - ('group', {'required': True, 'multivalue': True}) - - :param spec: A spec string. - """ - if type(spec) is not str: - raise_TypeError(spec, str, 'spec') - if len(spec) < 2: - raise ValueError( - 'param spec must be at least 2 characters; got %r' % spec - ) - _map = { - '?': dict(required=False, multivalue=False), - '*': dict(required=False, multivalue=True), - '+': dict(required=True, multivalue=True), - } - end = spec[-1] - if end in _map: - return (spec[:-1], _map[end]) - return (spec, dict(required=True, multivalue=False)) - - class DefaultFrom(ReadOnly): """ Derive a default value from other supplied values. @@ -177,6 +130,53 @@ class DefaultFrom(ReadOnly): pass +def parse_param_spec(spec): + """ + Parse a param spec into to (name, kw). + + The ``spec`` string determines the param name, whether the param is + required, and whether the param is multivalue according the following + syntax: + + ====== ===== ======== ========== + Spec Name Required Multivalue + ====== ===== ======== ========== + 'var' 'var' True False + 'var?' 'var' False False + 'var*' 'var' False True + 'var+' 'var' True True + ====== ===== ======== ========== + + For example, + + >>> parse_param_spec('login') + ('login', {'required': True, 'multivalue': False}) + >>> parse_param_spec('gecos?') + ('gecos', {'required': False, 'multivalue': False}) + >>> parse_param_spec('telephone_numbers*') + ('telephone_numbers', {'required': False, 'multivalue': True}) + >>> parse_param_spec('group+') + ('group', {'required': True, 'multivalue': True}) + + :param spec: A spec string. + """ + if type(spec) is not str: + raise_TypeError(spec, str, 'spec') + if len(spec) < 2: + raise ValueError( + 'param spec must be at least 2 characters; got %r' % spec + ) + _map = { + '?': dict(required=False, multivalue=False), + '*': dict(required=False, multivalue=True), + '+': dict(required=True, multivalue=True), + } + end = spec[-1] + if end in _map: + return (spec[:-1], _map[end]) + return (spec, dict(required=True, multivalue=False)) + + class Param(ReadOnly): """ Base class for all parameters. -- cgit From cb2f294cfef9b47e03b82c85cf1db7e7bc3574ef Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 18 Dec 2008 01:57:39 -0700 Subject: New Param: added missing unit tests for TypeError and ValueError cases in parse_param_spec() --- ipalib/constants.py | 2 +- ipalib/parameter.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) (limited to 'ipalib') diff --git a/ipalib/constants.py b/ipalib/constants.py index d028a001..ad1e3f7c 100644 --- a/ipalib/constants.py +++ b/ipalib/constants.py @@ -26,7 +26,7 @@ All constants centralised in one file. NULLS = (None, '', u'', tuple(), []) -TYPE_ERROR = '%s: need a %r; got %r (a %r)' +TYPE_ERROR = '%s: need a %r; got %r (which is a %r)' CALLABLE_ERROR = '%s: need a callable; got %r (a %r)' diff --git a/ipalib/parameter.py b/ipalib/parameter.py index a80c43c4..ca578cd9 100644 --- a/ipalib/parameter.py +++ b/ipalib/parameter.py @@ -161,10 +161,12 @@ def parse_param_spec(spec): :param spec: A spec string. """ if type(spec) is not str: - raise_TypeError(spec, str, 'spec') + raise TypeError( + TYPE_ERROR % ('spec', str, spec, type(spec)) + ) if len(spec) < 2: raise ValueError( - 'param spec must be at least 2 characters; got %r' % spec + 'spec must be at least 2 characters; got %r' % spec ) _map = { '?': dict(required=False, multivalue=False), -- cgit From 4d1681176afc45c57fb4316892f939bda1bacf1d Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 18 Dec 2008 02:08:41 -0700 Subject: New Param: added unit tests for TypeError cases in DefaultFrom.__init__() --- ipalib/constants.py | 2 +- ipalib/parameter.py | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) (limited to 'ipalib') diff --git a/ipalib/constants.py b/ipalib/constants.py index ad1e3f7c..ef2aef72 100644 --- a/ipalib/constants.py +++ b/ipalib/constants.py @@ -29,7 +29,7 @@ NULLS = (None, '', u'', tuple(), []) TYPE_ERROR = '%s: need a %r; got %r (which is a %r)' -CALLABLE_ERROR = '%s: need a callable; got %r (a %r)' +CALLABLE_ERROR = '%s: need a callable; got %r (which is a %r)' # Used for a tab (or indentation level) when formatting for CLI: diff --git a/ipalib/parameter.py b/ipalib/parameter.py index ca578cd9..fca95b0e 100644 --- a/ipalib/parameter.py +++ b/ipalib/parameter.py @@ -103,7 +103,9 @@ class DefaultFrom(ReadOnly): :param keys: Optional keys used for source values. """ if not callable(callback): - raise TypeError('callback must be callable; got %r' % callback) + raise TypeError( + CALLABLE_ERROR % ('callback', callback, type(callback)) + ) self.callback = callback if len(keys) == 0: fc = callback.func_code @@ -112,7 +114,9 @@ class DefaultFrom(ReadOnly): self.keys = keys for key in self.keys: if type(key) is not str: - raise_TypeError(key, str, 'keys') + raise TypeError( + TYPE_ERROR % ('keys', str, key, type(key)) + ) lock(self) def __call__(self, **kw): -- cgit From 46e37ab14491db06ffa46b682c079c397e644014 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 18 Dec 2008 11:21:12 -0700 Subject: New Param: ported Param.__repr__() and corresponding unit test --- ipalib/parameter.py | 11 +++++++++++ 1 file changed, 11 insertions(+) (limited to 'ipalib') diff --git a/ipalib/parameter.py b/ipalib/parameter.py index fca95b0e..76a9cd50 100644 --- a/ipalib/parameter.py +++ b/ipalib/parameter.py @@ -24,6 +24,7 @@ Parameter system for command plugins. from types import NoneType from plugable import ReadOnly, lock, check_name from constants import NULLS, TYPE_ERROR, CALLABLE_ERROR +from util import make_repr class DefaultFrom(ReadOnly): @@ -287,6 +288,16 @@ class Param(ReadOnly): # And we're done. lock(self) + def __repr__(self): + """ + Return an expresion that could construct this `Param` instance. + """ + return make_repr( + self.__class__.__name__, + self.param_spec, + **self.__kw + ) + def normalize(self, value): """ Normalize ``value`` using normalizer callback. -- cgit From dc54dee622bf9ff95a59530423ac5caa01868373 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 18 Dec 2008 14:01:59 -0700 Subject: Started work on per-request gettext setup --- ipalib/constants.py | 6 ++++-- ipalib/request.py | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 ipalib/request.py (limited to 'ipalib') diff --git a/ipalib/constants.py b/ipalib/constants.py index ef2aef72..45c9f278 100644 --- a/ipalib/constants.py +++ b/ipalib/constants.py @@ -25,12 +25,14 @@ All constants centralised in one file. # The parameter system treats all these values as None: NULLS = (None, '', u'', tuple(), []) - +# Standard format for TypeError message: TYPE_ERROR = '%s: need a %r; got %r (which is a %r)' - +# Stardard format for TypeError message when a callable is expected: CALLABLE_ERROR = '%s: need a callable; got %r (which is a %r)' +# Standard format for StandardError message when overriding an attribute: +OVERRIDE_ERROR = 'cannot override %s existing value %r with %r' # Used for a tab (or indentation level) when formatting for CLI: CLI_TAB = ' ' # Two spaces diff --git a/ipalib/request.py b/ipalib/request.py new file mode 100644 index 00000000..3e4b2798 --- /dev/null +++ b/ipalib/request.py @@ -0,0 +1,48 @@ +# Authors: +# Rob Crittenden +# Jason Gerard DeRose +# +# Copyright (C) 2008 Red Hat +# see file 'COPYING' for use and warranty contextrmation +# +# 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 + +""" +Per-request thread-local data. +""" + +import threading +import locale +import gettext +from constants import OVERRIDE_ERROR + + +# Thread-local storage of most per-request contextrmation +context = threading.local() + + +# Thread-local storage of gettext.Translations instances (one per gettext +# domain): +translations = threading.local() + + +def set_languages(*languages): + if hasattr(context, 'languages'): + raise StandardError( + OVERRIDE_ERROR % ('context.languages', context.languages, languages) + ) + if len(languages) == 0: + languages = locale.getdefaultlocale()[:1] + context.languages = languages + assert type(context.languages) is tuple -- cgit From 9a69adeef001ddd0c55513271cf02eedc0a9aef8 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 18 Dec 2008 16:58:48 -0700 Subject: Added request.create_translation() function and corresponding unit tests --- ipalib/request.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) (limited to 'ipalib') diff --git a/ipalib/request.py b/ipalib/request.py index 3e4b2798..545ebc54 100644 --- a/ipalib/request.py +++ b/ipalib/request.py @@ -28,15 +28,10 @@ import gettext from constants import OVERRIDE_ERROR -# Thread-local storage of most per-request contextrmation +# Thread-local storage of most per-request information context = threading.local() -# Thread-local storage of gettext.Translations instances (one per gettext -# domain): -translations = threading.local() - - def set_languages(*languages): if hasattr(context, 'languages'): raise StandardError( @@ -46,3 +41,17 @@ def set_languages(*languages): languages = locale.getdefaultlocale()[:1] context.languages = languages assert type(context.languages) is tuple + + +def create_translation(domain, localedir, *languages): + if hasattr(context, 'gettext') or hasattr(context, 'ngettext'): + raise StandardError( + 'create_translation() already called in thread %r' % + threading.currentThread().getName() + ) + set_languages(*languages) + translation = gettext.translation(domain, + localedir=localedir, languages=context.languages, fallback=True + ) + context.gettext = translation.ugettext + context.ngettext = translation.ungettext -- cgit From b3f95b17417342c7ede2e4b21cc917d84f8e2b8c Mon Sep 17 00:00:00 2001 From: Andrew Wnuk Date: Sun, 21 Dec 2008 14:15:53 -0700 Subject: Merged in Andrew's RA plugin --- ipalib/plugins/f_ra.py | 119 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 ipalib/plugins/f_ra.py (limited to 'ipalib') diff --git a/ipalib/plugins/f_ra.py b/ipalib/plugins/f_ra.py new file mode 100644 index 00000000..724cbf5e --- /dev/null +++ b/ipalib/plugins/f_ra.py @@ -0,0 +1,119 @@ +# Authors: +# Andrew Wnuk +# Jason Gerard DeRose +# +# Copyright (C) 2009 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 + +""" +Frontend plugins for IPA-RA PKI operations. +""" + +from ipalib import api, Command, Param +from ipalib import cli + + +class request_certificate(Command): + """ Submit a certificate request. """ + + takes_args = ['csr'] + + takes_options = [Param('request_type?', default='pkcs10')] + + def execute(self, csr, **options): + return self.Backend.ra.request_certificate(csr, **options) + + def output_for_cli(self, textui, result, *args, **options): + if isinstance(result, dict) and len(result) > 0: + textui.print_entry(result, 0) + else: + textui.print_plain('Failed to submit a certificate request.') + +api.register(request_certificate) + + +class get_certificate(Command): + """ Retrieve an existing certificate. """ + + takes_args = ['serial_number'] + + def execute(self, serial_number, **options): + return self.Backend.ra.get_certificate(serial_number) + + def output_for_cli(self, textui, result, *args, **options): + if isinstance(result, dict) and len(result) > 0: + textui.print_entry(result, 0) + else: + textui.print_plain('Failed to obtain a certificate.') + +api.register(get_certificate) + + +class check_request_status(Command): + """ Check a request status. """ + + takes_args = ['request_id'] + + + def execute(self, request_id, **options): + return self.Backend.ra.check_request_status(request_id) + + def output_for_cli(self, textui, result, *args, **options): + if isinstance(result, dict) and len(result) > 0: + textui.print_entry(result, 0) + else: + textui.print_plain('Failed to retrieve a request status.') + +api.register(check_request_status) + + +class revoke_certificate(Command): + """ Revoke a certificate. """ + + takes_args = ['serial_number'] + + takes_options = [Param('revocation_reason?', default=0)] + + + def execute(self, serial_number, **options): + return self.Backend.ra.revoke_certificate(serial_number, **options) + + def output_for_cli(self, textui, result, *args, **options): + if isinstance(result, dict) and len(result) > 0: + textui.print_entry(result, 0) + else: + textui.print_plain('Failed to revoke a certificate.') + +api.register(revoke_certificate) + + +class take_certificate_off_hold(Command): + """ Take a revoked certificate off hold. """ + + takes_args = ['serial_number'] + + def execute(self, serial_number, **options): + return self.Backend.ra.take_certificate_off_hold(serial_number) + + def output_for_cli(self, textui, result, *args, **options): + if isinstance(result, dict) and len(result) > 0: + textui.print_entry(result, 0) + else: + textui.print_plain('Failed to take a revoked certificate off hold.') + +api.register(take_certificate_off_hold) + + -- cgit From 4390523b7f854cefcb91843e1df3ca7575d43fea Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sun, 21 Dec 2008 17:12:00 -0700 Subject: Improved Plugin.call() method and added its unit test --- ipalib/errors.py | 8 ++++++++ ipalib/plugable.py | 19 +++++++++++++------ 2 files changed, 21 insertions(+), 6 deletions(-) (limited to 'ipalib') diff --git a/ipalib/errors.py b/ipalib/errors.py index 6dd6eb01..7191ff40 100644 --- a/ipalib/errors.py +++ b/ipalib/errors.py @@ -124,6 +124,14 @@ def _(text): return text +class SubprocessError(StandardError): + def __init__(self, returncode, argv): + self.returncode = returncode + self.argv = argv + StandardError.__init__(self, + 'return code %d from %r' % (returncode, argv) + ) + class HandledError(StandardError): """ Base class for errors that can be raised across a remote procedure call. diff --git a/ipalib/plugable.py b/ipalib/plugable.py index e6b5c1ac..f3b35d30 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -367,15 +367,22 @@ class Plugin(ReadOnly): assert not hasattr(self, name) setattr(self, name, getattr(api, name)) - def call(self, *args): + def call(self, executable, *args): """ - Call an external command via ``subprocess.call``. + Call ``executable`` with ``args`` using subprocess.call(). - Returns the exit status of the call. + If the call exits with a non-zero exit status, + `ipalib.errors.SubprocessError` is raised, from which you can retrieve + the exit code by checking the SubprocessError.returncode attribute. + + This method does *not* return what ``executable`` sent to stdout... for + that, use `Plugin.callread()`. """ - if hasattr(self, 'log'): - self.log.debug('Calling %r', args) - return subprocess.call(args) + argv = (executable,) + args + self.debug('Calling %r', argv) + returncode = subprocess.call(argv) + if returncode != 0: + raise errors.SubprocessError(returncode, argv) def __repr__(self): """ -- cgit From 9d091c98f1f1bf7bacf49e9eaaa18ba8bb1bfd70 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sun, 21 Dec 2008 19:34:32 -0700 Subject: Plugin.__init__() now checks that subclass hasn't defined attributes that conflict with the logger methods; added corresponding unit test --- ipalib/frontend.py | 1 - ipalib/plugable.py | 9 +++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 4ff77c59..c614e547 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -1087,7 +1087,6 @@ class Property(Attribute): rules=self.rules, normalize=self.normalize, ) - super(Property, self).__init__() def __rules_iter(self): """ diff --git a/ipalib/plugable.py b/ipalib/plugable.py index f3b35d30..019386c3 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -263,8 +263,13 @@ class Plugin(ReadOnly): self.summary = '<%s>' % self.fullname else: self.summary = self.doc.split('\n\n', 1)[0] - log = logging.getLogger('ipa') - for name in ('debug', 'info', 'warning', 'error', 'critical'): + log = logging.getLogger(self.fullname) + for name in ('debug', 'info', 'warning', 'error', 'critical', 'exception'): + if hasattr(self, name): + raise StandardError( + '%s.%s attribute (%r) conflicts with Plugin logger' % ( + self.name, name, getattr(self, name)) + ) setattr(self, name, getattr(log, name)) def __get_api(self): -- cgit From f82c48f775d1a8440a19d2040dbc8da51cec04b1 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sun, 21 Dec 2008 19:58:48 -0700 Subject: Added note in Plugin.set_api() about Plugin.log attribute being depreciated --- ipalib/plugable.py | 1 + 1 file changed, 1 insertion(+) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 019386c3..01b9b33c 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -367,6 +367,7 @@ class Plugin(ReadOnly): for name in api: assert not hasattr(self, name) setattr(self, name, api[name]) + # FIXME: the 'log' attribute is depreciated. See Plugin.__init__() for name in ('env', 'context', 'log'): if hasattr(api, name): assert not hasattr(self, name) -- cgit From 5b637f6a18a647a0ff084b2932faa1a4a887a5c2 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 22 Dec 2008 15:41:24 -0700 Subject: Removed depreciated code from config.py; removed corresponding unit tests --- ipalib/cli.py | 1 - ipalib/config.py | 281 +---------------------------------------------------- ipalib/plugable.py | 2 +- 3 files changed, 2 insertions(+), 282 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index 518b7129..f4cdbbf5 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -37,7 +37,6 @@ import backend import errors import plugable import ipa_types -from config import set_default_env, read_config import util from constants import CLI_TAB diff --git a/ipalib/config.py b/ipalib/config.py index 1bec57e5..06ecb13f 100644 --- a/ipalib/config.py +++ b/ipalib/config.py @@ -25,107 +25,13 @@ It will also take care of settings that can be discovered by different methods, such as DNS. """ -from ConfigParser import SafeConfigParser, ParsingError, RawConfigParser +from ConfigParser import RawConfigParser, ParsingError import types import os from os import path import sys -from errors import check_isinstance, raise_TypeError import constants -DEFAULT_CONF='/etc/ipa/ipa.conf' - - -class Environment(object): - """ - A mapping object used to store the environment variables. - """ - - def __init__(self): - object.__setattr__(self, '_Environment__map', {}) - - def __getattr__(self, name): - """ - Return the attribute named ``name``. - """ - return self[name] - - def __setattr__(self, name, value): - """ - Set the attribute named ``name`` to ``value``. - """ - self[name] = value - - def __delattr__(self, name): - """ - Raise AttributeError (deletion is not allowed). - """ - raise AttributeError('cannot del %s.%s' % - (self.__class__.__name__, name) - ) - - def __getitem__(self, key): - """ - Return the value corresponding to ``key``. - """ - val = self.__map[key] - if hasattr(val, 'get_value'): - return val.get_value() - else: - return val - - def __setitem__(self, key, value): - """ - Set the item at ``key`` to ``value``. - """ - if key in self or hasattr(self, key): - if hasattr(self.__map[key], 'set_value'): - self.__map[key].set_value(value) - else: - raise AttributeError('cannot overwrite %s.%s' % - (self.__class__.__name__, key) - ) - else: - self.__map[key] = value - - def __contains__(self, key): - """ - Return True if instance contains ``key``; otherwise return False. - """ - return key in self.__map - - def __iter__(self): - """ - Iterate through keys in ascending order. - """ - for key in sorted(self.__map): - yield key - - def update(self, new_vals, ignore_errors = False): - """ - Update variables using keys and values from ``new_vals``. - - Error will occur if there is an attempt to override variable that was - already set, unless``ignore_errors`` is True. - """ - assert type(new_vals) == dict - for key, value in new_vals.iteritems(): - if ignore_errors: - try: - self[key] = value - except (AttributeError, KeyError): - pass - else: - self[key] = value - - def get(self, name, default=None): - """ - Return the value corresponding to ``key``. Defaults to ``default``. - """ - if name in self: - return self[name] - else: - return default class Env(object): @@ -349,188 +255,3 @@ class Env(object): """ for key in sorted(self.__d): yield key - - -def set_default_env(env): - """ - Set default values for ``env``. - """ - assert isinstance(env, Environment) - - default = dict( - basedn = EnvProp(basestring, 'dc=example,dc=com'), - container_accounts = EnvProp(basestring, 'cn=accounts'), - container_user = EnvProp(basestring, 'cn=users,cn=accounts'), - container_group = EnvProp(basestring, 'cn=groups,cn=accounts'), - container_service = EnvProp(basestring, 'cn=services,cn=accounts'), - container_host = EnvProp(basestring, 'cn=computers,cn=accounts'), - container_hostgroup = EnvProp(basestring, 'cn=hostgroups,cn=accounts'), - domain = LazyProp(basestring, get_domain), - interactive = EnvProp(bool, True), - query_dns = EnvProp(bool, True), - realm = LazyProp(basestring, get_realm), - server_context = EnvProp(bool, True), - server = LazyIter(basestring, get_servers), - verbose = EnvProp(bool, False), - ldaphost = EnvProp(basestring, 'localhost'), - ldapport = EnvProp(int, 389), - ) - - env.update(default) - - -class EnvProp(object): - """ - Environment set-once property with optional default value. - """ - def __init__(self, type_, default, multi_value=False): - """ - :param type_: Type of the property. - :param default: Default value. - :param multi_value: Allow multiple values. - """ - if multi_value: - if isinstance(default, tuple) and len(default): - check_isinstance(default[0], type_, allow_none=True) - self._type = type_ - self._default = default - self._value = None - self._multi_value = multi_value - - def get_value(self): - """ - Return the value if it was set. - - If the value is not set return the default. Otherwise raise an - exception. - """ - if self._get() != None: - return self._get() - else: - raise KeyError, 'Value not set' - - def set_value(self, value): - """ - Set the value. - """ - if self._value != None: - raise KeyError, 'Value already set' - self._value = self._validate(value) - - def _get(self): - """ - Return value, default, or None. - """ - if self._value != None: - return self._value - elif self._default != None: - return self._default - else: - return None - - def _validate(self, value): - """ - Make sure ``value`` is of the right type. Do conversions if necessary. - - This will also handle multi value. - """ - if self._multi_value and isinstance(value, tuple): - converted = [] - for val in value: - converted.append(self._validate_value(val)) - return tuple(converted) - else: - return self._validate_value(value) - - def _validate_value(self, value): - """ - Validate and convert a single value. - """ - bool_true = ('true', 'yes', 'on') - bool_false = ('false', 'no', 'off') - - if self._type == bool and isinstance(value, basestring): - if value.lower() in bool_true: - return True - elif value.lower() in bool_false: - return False - else: - raise raise_TypeError(value, bool, 'value') - check_isinstance(value, self._type, 'value') - return value - - -class LazyProp(EnvProp): - def __init__(self, type_, func, default=None, multi_value=False): - check_isinstance(func, types.FunctionType, 'func') - self._func = func - EnvProp.__init__(self, type_, default, multi_value) - - def get_value(self): - if self._get() != None: - return self._get() - else: - return self._func() - - -class LazyIter(LazyProp): - def __init__(self, type_, func, default=None): - LazyProp.__init__(self, type_, func, default, multi_value=True) - - def get_value(self): - val = self._get() - if val != None: - if type(val) == tuple: - for item in val: - yield item - else: - yield val - for item in self._func(): - if not val or item not in val: - yield item - - -# TODO: Make it possible to use var = 'foo, bar' without -# turning it into ("'foo", "bar'") -def read_config(config_file=None): - assert config_file == None or isinstance(config_file, (basestring, file)) - - parser = SafeConfigParser() - if config_file == None: - files = [DEFAULT_CONF, os.path.expanduser('~/.ipa.conf')] - else: - files = [config_file] - - for f in files: - try: - if isinstance(f, file): - parser.readfp(f) - else: - parser.read(f) - except ParsingError: - print "Can't read %s" % f - - ret = {} - if parser.has_section('defaults'): - for name, value in parser.items('defaults'): - value = tuple(elem.strip() for elem in value.split(',')) - if len(value) == 1: - value = value[0] - ret[name] = value - - return ret - - -# these functions are here just to "emulate" dns resolving for now -def get_domain(): - return "ipatest.com" - - -def get_realm(): - return "IPATEST.COM" - - -def get_servers(): - yield "server.ipatest.com" - yield "backup.ipatest.com" - yield "fake.ipatest.com" diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 01b9b33c..0120f972 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -35,7 +35,7 @@ from os import path import subprocess import errors from errors import check_type, check_isinstance -from config import Environment, Env +from config import Env from constants import DEFAULT_CONFIG import util -- cgit From c070d390e92df0c9cc6b6070e6c94bd3a130ff65 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 22 Dec 2008 15:51:54 -0700 Subject: Removed Env.__getattr__(); Env no longer accepts callables for values (no more dynamic/lazy values) --- ipalib/config.py | 38 +++++++++++--------------------------- 1 file changed, 11 insertions(+), 27 deletions(-) (limited to 'ipalib') diff --git a/ipalib/config.py b/ipalib/config.py index 06ecb13f..8a23cde6 100644 --- a/ipalib/config.py +++ b/ipalib/config.py @@ -176,16 +176,6 @@ class Env(object): def __islocked__(self): return self.__locked - def __getattr__(self, name): - """ - Return the attribute named ``name``. - """ - if name in self.__d: - return self[name] - raise AttributeError('%s.%s' % - (self.__class__.__name__, name) - ) - def __setattr__(self, name, value): """ Set the attribute named ``name`` to ``value``. @@ -204,12 +194,7 @@ class Env(object): """ Return the value corresponding to ``key``. """ - if key not in self.__d: - raise KeyError(key) - value = self.__d[key] - if callable(value): - return value() - return value + return self.__d[key] def __setitem__(self, key, value): """ @@ -224,17 +209,16 @@ class Env(object): raise AttributeError('cannot overwrite %s.%s with %r' % (self.__class__.__name__, key, value) ) - if not callable(value): - if isinstance(value, basestring): - value = str(value.strip()) - if value.lower() == 'true': - value = True - elif value.lower() == 'false': - value = False - elif value.isdigit(): - value = int(value) - assert type(value) in (str, int, bool) - object.__setattr__(self, key, value) + if isinstance(value, basestring): + value = str(value.strip()) + if value.lower() == 'true': + value = True + elif value.lower() == 'false': + value = False + elif value.isdigit(): + value = int(value) + assert type(value) in (str, int, bool) + object.__setattr__(self, key, value) self.__d[key] = value def __contains__(self, key): -- cgit From 014cca57ad31f0ff9230923c8b7fdb1b59157dae Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 22 Dec 2008 16:16:57 -0700 Subject: The Env.__setitem__() implied conversion is now case sensitive; Env.__setitem__() now also accepts None as a value --- ipalib/config.py | 24 +++++++++++++----------- ipalib/constants.py | 37 ++++++++++++++++++++----------------- 2 files changed, 33 insertions(+), 28 deletions(-) (limited to 'ipalib') diff --git a/ipalib/config.py b/ipalib/config.py index 8a23cde6..8ff45dd9 100644 --- a/ipalib/config.py +++ b/ipalib/config.py @@ -26,11 +26,11 @@ methods, such as DNS. """ from ConfigParser import RawConfigParser, ParsingError -import types +from types import NoneType import os from os import path import sys -import constants +from constants import CONFIG_SECTION, TYPE_ERROR, OVERRIDE_ERROR @@ -143,7 +143,6 @@ class Env(object): """ Merge values from ``conf_file`` into this `Env`. """ - section = constants.CONFIG_SECTION if not path.isfile(conf_file): return parser = RawConfigParser() @@ -151,9 +150,9 @@ class Env(object): parser.read(conf_file) except ParsingError: return - if not parser.has_section(section): - parser.add_section(section) - items = parser.items(section) + if not parser.has_section(CONFIG_SECTION): + parser.add_section(CONFIG_SECTION) + items = parser.items(CONFIG_SECTION) if len(items) == 0: return i = 0 @@ -211,13 +210,16 @@ class Env(object): ) if isinstance(value, basestring): value = str(value.strip()) - if value.lower() == 'true': - value = True - elif value.lower() == 'false': - value = False + m = { + 'True': True, + 'False': False, + 'None': None, + } + if value in m: + value = m[value] elif value.isdigit(): value = int(value) - assert type(value) in (str, int, bool) + assert type(value) in (str, int, bool, type(NoneType)) object.__setattr__(self, key, value) self.__d[key] = value diff --git a/ipalib/constants.py b/ipalib/constants.py index 45c9f278..ef7de44c 100644 --- a/ipalib/constants.py +++ b/ipalib/constants.py @@ -32,7 +32,7 @@ TYPE_ERROR = '%s: need a %r; got %r (which is a %r)' CALLABLE_ERROR = '%s: need a callable; got %r (which is a %r)' # Standard format for StandardError message when overriding an attribute: -OVERRIDE_ERROR = 'cannot override %s existing value %r with %r' +OVERRIDE_ERROR = 'cannot override %s value %r with %r' # Used for a tab (or indentation level) when formatting for CLI: CLI_TAB = ' ' # Two spaces @@ -112,28 +112,31 @@ DEFAULT_CONFIG = ( # will have filled in all the keys below by the time DEFAULT_CONFIG # is merged in, so the values below are never actually used. They are # listed both to provide a big picture and also so DEFAULT_CONFIG contains - # the keys that should be present after Env._finalize_core() is called. + # at least all the keys that should be present after Env._finalize_core() + # is called. # - # The values are all None so if for some reason any of these keys were - # set from the values here, an exception will be raised. + # Each environment variable below is sent to ``object``, which just happens + # to be an invalid value for an environment variable, so if for some reason + # any of these keys were set from the values here, an exception will be + # raised. # Set in Env.__init__(): - ('ipalib', None), # The directory containing ipalib/__init__.py - ('site_packages', None), # The directory contaning ipalib - ('script', None), # sys.argv[0] - ('bin', None), # The directory containing script - ('home', None), # The home directory of user underwhich process is running - ('dot_ipa', None), # ~/.ipa directory + ('ipalib', object), # The directory containing ipalib/__init__.py + ('site_packages', object), # The directory contaning ipalib + ('script', object), # sys.argv[0] + ('bin', object), # The directory containing script + ('home', object), # The home directory of user underwhich process is running + ('dot_ipa', object), # ~/.ipa directory # Set in Env._bootstrap(): - ('in_tree', None), # Whether or not running in-tree (bool) - ('context', None), # Name of context, default is 'default' - ('conf', None), # Path to config file - ('conf_default', None), # Path to common default config file - ('conf_dir', None), # Directory containing config files + ('in_tree', object), # Whether or not running in-tree (bool) + ('context', object), # Name of context, default is 'default' + ('conf', object), # Path to config file + ('conf_default', object), # Path to common default config file + ('conf_dir', object), # Directory containing config files # Set in Env._finalize_core(): - ('in_server', None), # Whether or not running in-server (bool) - ('log', None), # Path to log file + ('in_server', object), # Whether or not running in-server (bool) + ('log', object), # Path to log file ) -- cgit From 6b055b435f93bf9b63ee9b3b2fdd6f082dacc07b Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 22 Dec 2008 17:29:11 -0700 Subject: Cleaned up Env.__setattr__() and Env.__setitem__() a bit updated their unit tests --- ipalib/config.py | 73 +++++++++++++++++++++++++++++------------------------ ipalib/constants.py | 7 ++++- ipalib/request.py | 4 +-- 3 files changed, 48 insertions(+), 36 deletions(-) (limited to 'ipalib') diff --git a/ipalib/config.py b/ipalib/config.py index 8ff45dd9..7f12b425 100644 --- a/ipalib/config.py +++ b/ipalib/config.py @@ -30,7 +30,8 @@ from types import NoneType import os from os import path import sys -from constants import CONFIG_SECTION, TYPE_ERROR, OVERRIDE_ERROR +from constants import CONFIG_SECTION +from constants import TYPE_ERROR, OVERRIDE_ERROR, LOCK_ERROR @@ -51,6 +52,42 @@ class Env(object): self.home = path.abspath(os.environ['HOME']) self.dot_ipa = path.join(self.home, '.ipa') + def __setattr__(self, name, value): + """ + Set the attribute named ``name`` to ``value``. + + This just calls `Env.__setitem__()`. + """ + self[name] = value + + def __setitem__(self, key, value): + """ + Set ``key`` to ``value``. + """ + # FIXME: the key should be checked with check_name() + if self.__locked: + raise AttributeError( + LOCK_ERROR % (self.__class__.__name__, key, value) + ) + if key in self.__d: + raise AttributeError(OVERRIDE_ERROR % + (self.__class__.__name__, key, self.__d[key], value) + ) + if isinstance(value, basestring): + value = str(value.strip()) + m = { + 'True': True, + 'False': False, + 'None': None, + } + if value in m: + value = m[value] + elif value.isdigit(): + value = int(value) + assert type(value) in (str, int, bool, NoneType) + object.__setattr__(self, key, value) + self.__d[key] = value + def __doing(self, name): if name in self.__done: raise StandardError( @@ -175,11 +212,7 @@ class Env(object): def __islocked__(self): return self.__locked - def __setattr__(self, name, value): - """ - Set the attribute named ``name`` to ``value``. - """ - self[name] = value + def __delattr__(self, name): """ @@ -195,33 +228,7 @@ class Env(object): """ return self.__d[key] - def __setitem__(self, key, value): - """ - Set ``key`` to ``value``. - """ - # FIXME: the key should be checked with check_name() - if self.__locked: - raise AttributeError('locked: cannot set %s.%s to %r' % - (self.__class__.__name__, key, value) - ) - if key in self.__d or hasattr(self, key): - raise AttributeError('cannot overwrite %s.%s with %r' % - (self.__class__.__name__, key, value) - ) - if isinstance(value, basestring): - value = str(value.strip()) - m = { - 'True': True, - 'False': False, - 'None': None, - } - if value in m: - value = m[value] - elif value.isdigit(): - value = int(value) - assert type(value) in (str, int, bool, type(NoneType)) - object.__setattr__(self, key, value) - self.__d[key] = value + def __contains__(self, key): """ diff --git a/ipalib/constants.py b/ipalib/constants.py index ef7de44c..c74808d6 100644 --- a/ipalib/constants.py +++ b/ipalib/constants.py @@ -32,7 +32,12 @@ TYPE_ERROR = '%s: need a %r; got %r (which is a %r)' CALLABLE_ERROR = '%s: need a callable; got %r (which is a %r)' # Standard format for StandardError message when overriding an attribute: -OVERRIDE_ERROR = 'cannot override %s value %r with %r' +OVERRIDE_ERROR = 'cannot override %s.%s value %r with %r' + +# Standard format for AttributeError message when a read-only attribute is +# already locked: +LOCK_ERROR = 'locked: cannot set %s.%s to %r' +DEL_ERROR = 'locked: cannot set %s.%s to %r' # Used for a tab (or indentation level) when formatting for CLI: CLI_TAB = ' ' # Two spaces diff --git a/ipalib/request.py b/ipalib/request.py index 545ebc54..ea028239 100644 --- a/ipalib/request.py +++ b/ipalib/request.py @@ -34,8 +34,8 @@ context = threading.local() def set_languages(*languages): if hasattr(context, 'languages'): - raise StandardError( - OVERRIDE_ERROR % ('context.languages', context.languages, languages) + raise StandardError(OVERRIDE_ERROR % + ('context', 'languages', context.languages, languages) ) if len(languages) == 0: languages = locale.getdefaultlocale()[:1] -- cgit From 01cae56e0a19876cf6a614469c0c5e6fb73170e6 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 22 Dec 2008 21:02:43 -0700 Subject: Some more reorganization in Env and added class docstring to Env with lots of examples --- ipalib/config.py | 120 +++++++++++++++++++++++++++++++++++++++++++++------- ipalib/constants.py | 4 +- 2 files changed, 106 insertions(+), 18 deletions(-) (limited to 'ipalib') diff --git a/ipalib/config.py b/ipalib/config.py index 7f12b425..4631d899 100644 --- a/ipalib/config.py +++ b/ipalib/config.py @@ -31,13 +31,95 @@ import os from os import path import sys from constants import CONFIG_SECTION -from constants import TYPE_ERROR, OVERRIDE_ERROR, LOCK_ERROR - +from constants import TYPE_ERROR, OVERRIDE_ERROR, SET_ERROR, DEL_ERROR class Env(object): """ - A mapping object used to store the environment variables. + Store and retrieve environment variables. + + First an foremost, the `Env` class provides a handy container for + environment variables. These variables can be both set and retrieved as + either attributes or as dictionary items. + + For example, we can set a variable as an attribute: + + >>> env = Env() + >>> env.attr = 'I was set as an attribute.' + >>> env.attr # Retrieve as an attribute + 'I was set as an attribute.' + >>> env['attr'] # Also retrieve as a dictionary item + 'I was set as an attribute.' + + Or we can set a variable as a dictionary item: + + >>> env['item'] = 'I was set as a dictionary item.' + >>> env['item'] # Retrieve as a dictionary item + 'I was set as a dictionary item.' + >>> env.item # Also retrieve as an attribute + 'I was set as a dictionary item.' + + The variable values can be ``str`` or ``int`` instances, or the ``True``, + ``False``, or ``None`` constants. When the value provided is an ``str`` + instance, some limited automatic type conversion is performed, which allows + values of specific types to be set easily from configuration files and from + command-line options. + + The ``True``, ``False``, and ``None`` constants can be specified with a + string that matches what ``repr()`` would return. For example: + + >>> env.true = 'True' + >>> env.true + True + + Note that the automatic type conversion is case sensitive. For example: + + >>> env.false = 'false' # Doesn't match repr(False) + >>> env.false + 'false' + + If an ``str`` value looks like an integer, it's automatically converted to + the ``int`` type. For example: + + >>> env.lucky = '7' + >>> env.lucky + 7 + + Also, leading and trailing white-space is automatically stripped from + ``str`` values. For example: + + >>> env.message = ' Hello! ' # Surrounded by double spaces + >>> env.message + 'Hello!' + >>> env.number = '42 ' # Still converted to an int + >>> env.number + 42 + >>> env.actually_false = ' False' # Still matches repr(False) + >>> env.actually_false + False + + `Env` is set-once, first-one-wins. Once a variable has been set, trying to + override it will raise an ``AttributeError``. For example: + + >>> env.my_var = 'first' + >>> env.my_var = 'second' + Traceback (most recent call last): + ... + AttributeError: cannot override Env.my_var value 'first' with 'second' + + An `Env` instance can also be *locked*, after which no further variables can + be set. Trying to set variables on a locked `Env` instance will also raise + an ``AttributeError``. For example: + + >>> env = Env() + >>> env.var1 = 'This will work.' + >>> env.__lock__() + >>> env.var2 = 'This wont work!' + Traceback (most recent call last): + ... + AttributeError: locked: cannot set Env.var2 to 'This wont work!' + + Finish me! """ __locked = False @@ -67,12 +149,16 @@ class Env(object): # FIXME: the key should be checked with check_name() if self.__locked: raise AttributeError( - LOCK_ERROR % (self.__class__.__name__, key, value) + SET_ERROR % (self.__class__.__name__, key, value) ) if key in self.__d: raise AttributeError(OVERRIDE_ERROR % (self.__class__.__name__, key, self.__d[key], value) ) + if hasattr(self, key): + raise AttributeError(OVERRIDE_ERROR % + (self.__class__.__name__, key, getattr(self, key), value) + ) if isinstance(value, basestring): value = str(value.strip()) m = { @@ -88,6 +174,20 @@ class Env(object): object.__setattr__(self, key, value) self.__d[key] = value + def __getitem__(self, key): + """ + Return the value corresponding to ``key``. + """ + return self.__d[key] + + def __delattr__(self, name): + """ + Raise AttributeError (deletion is never allowed). + """ + raise AttributeError( + DEL_ERROR % (self.__class__.__name__, name) + ) + def __doing(self, name): if name in self.__done: raise StandardError( @@ -214,19 +314,7 @@ class Env(object): - def __delattr__(self, name): - """ - Raise AttributeError (deletion is not allowed). - """ - raise AttributeError('cannot del %s.%s' % - (self.__class__.__name__, name) - ) - def __getitem__(self, key): - """ - Return the value corresponding to ``key``. - """ - return self.__d[key] diff --git a/ipalib/constants.py b/ipalib/constants.py index c74808d6..dc23b109 100644 --- a/ipalib/constants.py +++ b/ipalib/constants.py @@ -36,8 +36,8 @@ OVERRIDE_ERROR = 'cannot override %s.%s value %r with %r' # Standard format for AttributeError message when a read-only attribute is # already locked: -LOCK_ERROR = 'locked: cannot set %s.%s to %r' -DEL_ERROR = 'locked: cannot set %s.%s to %r' +SET_ERROR = 'locked: cannot set %s.%s to %r' +DEL_ERROR = 'locked: cannot del %s.%s' # Used for a tab (or indentation level) when formatting for CLI: CLI_TAB = ' ' # Two spaces -- cgit From fd43b39145382b96cd2e0d0da3d5dcbe0d3a4a2a Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 22 Dec 2008 23:09:35 -0700 Subject: Moved setting of run-time variables from Env.__init__() to Env._bootstrap() --- ipalib/__init__.py | 53 ++++++++++++++++++++++++++--------------------------- ipalib/config.py | 32 +++++++++++++++++++------------- 2 files changed, 45 insertions(+), 40 deletions(-) (limited to 'ipalib') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index b9a3c96d..e30b7fed 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -685,41 +685,40 @@ is configured. Environment variables --------------------- -Plugins access various environment variables and run-time information through -``self.api.env`` (for convenience, ``self.env`` is equivalent). +Plugins access configuration variables and run-time information through +``self.api.env`` (or for convenience, ``self.env`` is equivalent). This +attribute is a refences to the `ipalib.config.Env` instance created in +`plugable.API.__init__()`. -When you create a fresh `plugable.API` instance, its ``env`` attribute is -likewise a freshly created `config.Env` instance, which will already be -populated with certain run-time information. For example: +After `API.bootstrap()` has been called, the `Env` instance will be populated +with all the environment information used by the built-in plugins. +This will be called before any plugins are registered, so plugin authors can +assume these variables will all exist by the time the module containing their +plugin (or plugins) is imported. For example: >>> api = create_api() ->>> list(api.env) -['bin', 'dot_ipa', 'home', 'ipalib', 'mode', 'script', 'site_packages'] - -Here is a quick overview of the run-time information: - -============= ================================ ======================= -Key Source or example value Description -============= ================================ ======================= -bin /usr/bin Dir. containing script -dot_ipa ~/.ipa User config directory -home os.environ['HOME'] User home dir. -ipalib .../site-packages/ipalib Dir. of ipalib package -mode 'production' or 'unit_test' The mode ipalib is in -script sys.argv[0] Path of script -site_packages /usr/lib/python2.5/site-packages Dir. containing ipalib/ -============= ================================ ======================= - -After `plugable.API.bootstrap()` has been called, the env instance will be -populated with all the environment information used by the built-in plugins. -This will typically be called before any plugins are registered. For example: - >>> len(api.env) -7 +1 >>> api.bootstrap(in_server=True) # We want to execute, not forward >>> len(api.env) 35 +`Env._bootstrap()`, which is called by `API.bootstrap()`, will create several +run-time variables that connot be overriden in configuration files or through +command-line options. Here is an overview of this run-time information: + +============= ============================= ======================= +Key Example value Description +============= ============================= ======================= +bin '/usr/bin' Dir. containing script +dot_ipa '/home/jderose/.ipa' User config directory +home os.environ['HOME'] User home dir. +ipalib '.../site-packages/ipalib' Dir. of ipalib package +mode 'unit_test' The mode ipalib is in +script sys.argv[0] Path of script +site_packages '.../python2.5/site-packages' Dir. containing ipalib/ +============= ============================= ======================= + If your plugin requires new environment variables *and* will be included in the freeIPA built-in plugins, you should add the defaults for your variables in `ipalib.constants.DEFAULT_CONFIG`. Also, you should consider whether your diff --git a/ipalib/config.py b/ipalib/config.py index 4631d899..9fe02cb3 100644 --- a/ipalib/config.py +++ b/ipalib/config.py @@ -62,15 +62,18 @@ class Env(object): The variable values can be ``str`` or ``int`` instances, or the ``True``, ``False``, or ``None`` constants. When the value provided is an ``str`` instance, some limited automatic type conversion is performed, which allows - values of specific types to be set easily from configuration files and from + values of specific types to be set easily from configuration files or command-line options. The ``True``, ``False``, and ``None`` constants can be specified with a string that matches what ``repr()`` would return. For example: - >>> env.true = 'True' + >>> env.true = True + >>> env.also_true = 'True' >>> env.true True + >>> env.also_true + True Note that the automatic type conversion is case sensitive. For example: @@ -98,14 +101,14 @@ class Env(object): >>> env.actually_false False - `Env` is set-once, first-one-wins. Once a variable has been set, trying to - override it will raise an ``AttributeError``. For example: + `Env` variables are all set-once (first-one-wins). Once a variable has been + set, trying to override it will raise an ``AttributeError``. For example: - >>> env.my_var = 'first' - >>> env.my_var = 'second' + >>> env.date = 'First' + >>> env.date = 'Second' Traceback (most recent call last): ... - AttributeError: cannot override Env.my_var value 'first' with 'second' + AttributeError: cannot override Env.date value 'First' with 'Second' An `Env` instance can also be *locked*, after which no further variables can be set. Trying to set variables on a locked `Env` instance will also raise @@ -127,12 +130,6 @@ class Env(object): def __init__(self): object.__setattr__(self, '_Env__d', {}) object.__setattr__(self, '_Env__done', set()) - self.ipalib = path.dirname(path.abspath(__file__)) - self.site_packages = path.dirname(self.ipalib) - self.script = path.abspath(sys.argv[0]) - self.bin = path.dirname(self.script) - self.home = path.abspath(os.environ['HOME']) - self.dot_ipa = path.join(self.home, '.ipa') def __setattr__(self, name, value): """ @@ -211,6 +208,15 @@ class Env(object): and the location of the configuration file. """ self.__doing('_bootstrap') + + # Set run-time variables: + self.ipalib = path.dirname(path.abspath(__file__)) + self.site_packages = path.dirname(self.ipalib) + self.script = path.abspath(sys.argv[0]) + self.bin = path.dirname(self.script) + self.home = path.abspath(os.environ['HOME']) + self.dot_ipa = path.join(self.home, '.ipa') + for (key, value) in overrides.iteritems(): self[key] = value if 'in_tree' not in self: -- cgit From 16526142f36e81f4d8a767f339c559188485f756 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 23 Dec 2008 01:11:03 -0700 Subject: Finished Env class docstring; more organizational cleanup in Env and its unit tests --- ipalib/config.py | 159 ++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 99 insertions(+), 60 deletions(-) (limited to 'ipalib') diff --git a/ipalib/config.py b/ipalib/config.py index 9fe02cb3..c39d99a9 100644 --- a/ipalib/config.py +++ b/ipalib/config.py @@ -39,14 +39,14 @@ class Env(object): Store and retrieve environment variables. First an foremost, the `Env` class provides a handy container for - environment variables. These variables can be both set and retrieved as - either attributes or as dictionary items. + environment variables. These variables can be both set *and* retrieved + either as attributes *or* as dictionary items. For example, we can set a variable as an attribute: >>> env = Env() >>> env.attr = 'I was set as an attribute.' - >>> env.attr # Retrieve as an attribute + >>> env.attr 'I was set as an attribute.' >>> env['attr'] # Also retrieve as a dictionary item 'I was set as an attribute.' @@ -54,7 +54,7 @@ class Env(object): Or we can set a variable as a dictionary item: >>> env['item'] = 'I was set as a dictionary item.' - >>> env['item'] # Retrieve as a dictionary item + >>> env['item'] 'I was set as a dictionary item.' >>> env.item # Also retrieve as an attribute 'I was set as a dictionary item.' @@ -65,11 +65,12 @@ class Env(object): values of specific types to be set easily from configuration files or command-line options. - The ``True``, ``False``, and ``None`` constants can be specified with a - string that matches what ``repr()`` would return. For example: + So in addition to their actual values, the ``True``, ``False``, and ``None`` + constants can be specified with an ``str`` equal to what ``repr()`` would + return. For example: >>> env.true = True - >>> env.also_true = 'True' + >>> env.also_true = 'True' # Equal to repr(True) >>> env.true True >>> env.also_true @@ -77,7 +78,7 @@ class Env(object): Note that the automatic type conversion is case sensitive. For example: - >>> env.false = 'false' # Doesn't match repr(False) + >>> env.false = 'false' # Not equal to repr(False)! >>> env.false 'false' @@ -88,19 +89,25 @@ class Env(object): >>> env.lucky 7 - Also, leading and trailing white-space is automatically stripped from - ``str`` values. For example: + Leading and trailing white-space is automatically stripped from ``str`` + values. For example: >>> env.message = ' Hello! ' # Surrounded by double spaces >>> env.message 'Hello!' - >>> env.number = '42 ' # Still converted to an int + >>> env.number = ' 42 ' # Still converted to an int >>> env.number 42 - >>> env.actually_false = ' False' # Still matches repr(False) + >>> env.actually_false = ' False ' # Still equal to repr(False) >>> env.actually_false False + Also, empty ``str`` instances are converted to ``None``. For example: + + >>> env.empty = '' + >>> env.empty is None + True + `Env` variables are all set-once (first-one-wins). Once a variable has been set, trying to override it will raise an ``AttributeError``. For example: @@ -110,19 +117,56 @@ class Env(object): ... AttributeError: cannot override Env.date value 'First' with 'Second' - An `Env` instance can also be *locked*, after which no further variables can - be set. Trying to set variables on a locked `Env` instance will also raise + An `Env` instance can be *locked*, after which no further variables can be + set. Trying to set variables on a locked `Env` instance will also raise an ``AttributeError``. For example: >>> env = Env() - >>> env.var1 = 'This will work.' + >>> env.okay = 'This will work.' >>> env.__lock__() - >>> env.var2 = 'This wont work!' + >>> env.nope = 'This wont work!' Traceback (most recent call last): ... - AttributeError: locked: cannot set Env.var2 to 'This wont work!' + AttributeError: locked: cannot set Env.nope to 'This wont work!' - Finish me! + `Env` instances also provide standard container emulation for membership + testing, counting, and iteration. For example: + + >>> env = Env() + >>> 'key1' in env # Has key1 been set? + False + >>> env.key1 = 'value 1' + >>> 'key1' in env + True + >>> env.key2 = 'value 2' + >>> len(env) # How many variables have been set? + 2 + >>> list(env) # What variables have been set? + ['key1', 'key2'] + + Lastly, in addition to all the handy container functionality, the `Env` + class provides high-level methods for bootstraping a fresh `Env` instance + into one containing all the run-time and configuration information needed + by the built-in freeIPA plugins. + + These are the `Env` bootstraping methods, in the order they must be called: + + 1. `Env._bootstrap()` - initialize the run-time variables and then + merge-in variables specified on the command-line. + + 2. `Env._finalize_core()` - merge-in variables from the configuration + files and then merge-in variables from the internal defaults, after + which at least all the standard variables will be set. After this + method is called, the plugins will be loaded, during which 3rd-party + plugins can set additional variables they may need. + + 3. `Env._finalize()` - one last chance to merge-in variables and then + the instance is locked. After this method is called, no more + environment variables can be set during the remaining life of the + process. + + However, normally none of the above methods are called directly and only + `ipalib.plugable.API.bootstrap()` is called instead. """ __locked = False @@ -131,6 +175,22 @@ class Env(object): object.__setattr__(self, '_Env__d', {}) object.__setattr__(self, '_Env__done', set()) + def __lock__(self): + """ + Prevent further changes to environment. + """ + if self.__locked is True: + raise StandardError( + '%s.__lock__() already called' % self.__class__.__name__ + ) + object.__setattr__(self, '_Env__locked', True) + + def __islocked__(self): + """ + Return ``True`` if locked. + """ + return self.__locked + def __setattr__(self, name, value): """ Set the attribute named ``name`` to ``value``. @@ -152,16 +212,14 @@ class Env(object): raise AttributeError(OVERRIDE_ERROR % (self.__class__.__name__, key, self.__d[key], value) ) - if hasattr(self, key): - raise AttributeError(OVERRIDE_ERROR % - (self.__class__.__name__, key, getattr(self, key), value) - ) + assert not hasattr(self, key) if isinstance(value, basestring): value = str(value.strip()) m = { 'True': True, 'False': False, 'None': None, + '': None, } if value in m: value = m[value] @@ -185,6 +243,25 @@ class Env(object): DEL_ERROR % (self.__class__.__name__, name) ) + def __contains__(self, key): + """ + Return True if instance contains ``key``; otherwise return False. + """ + return key in self.__d + + def __len__(self): + """ + Return number of variables currently set. + """ + return len(self.__d) + + def __iter__(self): + """ + Iterate through keys in ascending order. + """ + for key in sorted(self.__d): + yield key + def __doing(self, name): if name in self.__done: raise StandardError( @@ -304,41 +381,3 @@ class Env(object): self[key] = value i += 1 return (i, len(items)) - - def __lock__(self): - """ - Prevent further changes to environment. - """ - if self.__locked is True: - raise StandardError( - '%s.__lock__() already called' % self.__class__.__name__ - ) - object.__setattr__(self, '_Env__locked', True) - - def __islocked__(self): - return self.__locked - - - - - - - - def __contains__(self, key): - """ - Return True if instance contains ``key``; otherwise return False. - """ - return key in self.__d - - def __len__(self): - """ - Return number of variables currently set. - """ - return len(self.__d) - - def __iter__(self): - """ - Iterate through keys in ascending order. - """ - for key in sorted(self.__d): - yield key -- cgit From f7cae9a27cde3346783809cbf949762b0c0708f1 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 23 Dec 2008 01:28:00 -0700 Subject: More docstring cleanup in Env and its methods --- ipalib/config.py | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) (limited to 'ipalib') diff --git a/ipalib/config.py b/ipalib/config.py index c39d99a9..8c602cdc 100644 --- a/ipalib/config.py +++ b/ipalib/config.py @@ -165,8 +165,8 @@ class Env(object): environment variables can be set during the remaining life of the process. - However, normally none of the above methods are called directly and only - `ipalib.plugable.API.bootstrap()` is called instead. + However, normally none of the above methods are called directly and instead + only `ipalib.plugable.API.bootstrap()` is called. """ __locked = False @@ -280,9 +280,10 @@ class Env(object): """ Initialize basic environment. - This method will initialize only enough environment information to - determine whether ipa is running in-tree, what the context is, - and the location of the configuration file. + In addition to certain run-time information, this method will + initialize only enough environment information to determine whether + IPA is running in-tree, what the context is, and the location of the + configuration file. """ self.__doing('_bootstrap') @@ -322,11 +323,12 @@ class Env(object): After this method is called, the all environment variables used by all the built-in plugins will be available. - This method should be called before loading any plugins. It will + This method should be called before loading any plugins. It will automatically call `Env._bootstrap()` if it has not yet been called. After this method has finished, the `Env` instance is still writable - so that third + so that 3rd-party plugins can set variables they may require as the + plugins are registered. """ self.__doing('_finalize_core') self.__do_if_not_done('_bootstrap') @@ -349,8 +351,17 @@ class Env(object): """ Finalize and lock environment. - This method should be called after all plugins have bean loaded and - after `plugable.API.finalize()` has been called. + This method should be called after all plugins have been loaded and + after `plugable.API.finalize()` has been called. This method will + automatically call `Env._finalize_core()` if it hasn't been called + already, but in normal operation this would result in an exception + being raised because the internal default values will not have been + merged-in. + + After this method finishes, the `Env` instance will be locked and no + more environment variables can be set. Aside from unit-tests and + example code, normally only one `Env` instance is created, meaning + no more variables can be set during the remaining life of the process. """ self.__doing('_finalize') self.__do_if_not_done('_finalize_core') -- cgit From 7766f0be6189c06c3cbbdef59bdbf4eb2a65e2a1 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 23 Dec 2008 01:59:31 -0700 Subject: Yet more small docstring cleanup in Env --- ipalib/config.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) (limited to 'ipalib') diff --git a/ipalib/config.py b/ipalib/config.py index 8c602cdc..dc5d35f8 100644 --- a/ipalib/config.py +++ b/ipalib/config.py @@ -42,7 +42,7 @@ class Env(object): environment variables. These variables can be both set *and* retrieved either as attributes *or* as dictionary items. - For example, we can set a variable as an attribute: + For example, you can set a variable as an attribute: >>> env = Env() >>> env.attr = 'I was set as an attribute.' @@ -51,7 +51,7 @@ class Env(object): >>> env['attr'] # Also retrieve as a dictionary item 'I was set as an attribute.' - Or we can set a variable as a dictionary item: + Or you can set a variable as a dictionary item: >>> env['item'] = 'I was set as a dictionary item.' >>> env['item'] @@ -166,7 +166,8 @@ class Env(object): process. However, normally none of the above methods are called directly and instead - only `ipalib.plugable.API.bootstrap()` is called. + only `plugable.API.bootstrap()` is called, which itself takes care of + correctly calling the `Env` bootstrapping methods. """ __locked = False @@ -360,7 +361,7 @@ class Env(object): After this method finishes, the `Env` instance will be locked and no more environment variables can be set. Aside from unit-tests and - example code, normally only one `Env` instance is created, meaning + example code, normally only one `Env` instance is created, which means no more variables can be set during the remaining life of the process. """ self.__doing('_finalize') -- cgit From e14fc84dfccbb06f775bbd5d3de864c7b879453f Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 29 Dec 2008 21:23:34 -0700 Subject: Renamed Env._merge_config() to Env._merge_from_file() --- ipalib/config.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'ipalib') diff --git a/ipalib/config.py b/ipalib/config.py index dc5d35f8..6b016541 100644 --- a/ipalib/config.py +++ b/ipalib/config.py @@ -334,8 +334,8 @@ class Env(object): self.__doing('_finalize_core') self.__do_if_not_done('_bootstrap') if self.__d.get('mode', None) != 'dummy': - self._merge_config(self.conf) - self._merge_config(self.conf_default) + self._merge_from_file(self.conf) + self._merge_from_file(self.conf_default) if 'in_server' not in self: self.in_server = (self.context == 'server') if 'log' not in self: @@ -371,7 +371,7 @@ class Env(object): self[key] = value self.__lock__() - def _merge_config(self, conf_file): + def _merge_from_file(self, conf_file): """ Merge values from ``conf_file`` into this `Env`. """ -- cgit From 447c88a2bb9dd364f9c67a73bfce5000ac81d375 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 30 Dec 2008 00:45:48 -0700 Subject: Started moving some core classes and functions from plugable.py to new base.py module --- ipalib/base.py | 172 ++++++++++++++++++++++++++++++++++++++++++++++++++++ ipalib/config.py | 81 ++++++++++++++++--------- ipalib/constants.py | 10 ++- 3 files changed, 234 insertions(+), 29 deletions(-) create mode 100644 ipalib/base.py (limited to 'ipalib') diff --git a/ipalib/base.py b/ipalib/base.py new file mode 100644 index 00000000..2e8ae066 --- /dev/null +++ b/ipalib/base.py @@ -0,0 +1,172 @@ +# Authors: +# Jason Gerard DeRose +# +# 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 + +""" +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 + + +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 simply telling our programmers something like, + "Don't set any attributes on this ``FooBar`` instance because doing so wont + be thread-safe", this class gives us a way to enforce it. + + For example, before a `ReadOnly` instance is locked, you can set and delete + its attributes as normal: + + >>> class Person(ReadOnly): + ... pass + ... + >>> p = Person() + >>> p.__islocked__() # Initially unlocked + False + >>> p.name = 'John Doe' + >>> p.phone = '123-456-7890' + >>> del p.phone + + But after an instance is locked, you cannot set its attributes: + + >>> 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 above, 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 as a mere accident. + """ + + __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 check_name(name): + """ + Verify that ``name`` is suitable for a `NameSpace` member name. + + 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'name') + Traceback (most recent call last): + ... + TypeError: name: need a ; got u'name' (a ) + + 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('name') + >>> n + '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 diff --git a/ipalib/config.py b/ipalib/config.py index 6b016541..7317e4f0 100644 --- a/ipalib/config.py +++ b/ipalib/config.py @@ -30,6 +30,8 @@ from types import NoneType import os from os import path import sys + +from base import check_name from constants import CONFIG_SECTION from constants import TYPE_ERROR, OVERRIDE_ERROR, SET_ERROR, DEL_ERROR @@ -204,11 +206,11 @@ class Env(object): """ Set ``key`` to ``value``. """ - # FIXME: the key should be checked with check_name() if self.__locked: raise AttributeError( SET_ERROR % (self.__class__.__name__, key, value) ) + check_name(key) if key in self.__d: raise AttributeError(OVERRIDE_ERROR % (self.__class__.__name__, key, self.__d[key], value) @@ -263,6 +265,56 @@ class Env(object): for key in sorted(self.__d): yield key + def _merge(self, **kw): + """ + Merge variables in ``kw`` into environment. + + Any variables in ``kw`` that have already been set will be skipped + (which means this method will not try to override them). + + This method returns a (set, total) tuple contained the number of + variables actually set and the number of variables requested to be set. + + For example: + + >>> env = Env() + >>> env._merge(first=1, second=2) + (2, 2) + >>> env._merge(first=1, third=3) + (1, 2) + >>> env._merge(first=1, second=2, third=3) + (0, 3) + """ + i = 0 + for (key, value) in kw.iteritems(): + if key not in self: + self[key] = value + i += 1 + return (i, len(kw)) + + def _merge_from_file(self, conf_file): + """ + Merge values from ``conf_file`` into this `Env`. + """ + if not path.isfile(conf_file): + return + parser = RawConfigParser() + try: + parser.read(conf_file) + except ParsingError: + return + if not parser.has_section(CONFIG_SECTION): + parser.add_section(CONFIG_SECTION) + items = parser.items(CONFIG_SECTION) + if len(items) == 0: + return + i = 0 + for (key, value) in items: + if key not in self: + self[key] = value + i += 1 + return (i, len(items)) + def __doing(self, name): if name in self.__done: raise StandardError( @@ -344,9 +396,7 @@ class Env(object): self.log = path.join(self.dot_ipa, 'log', name) else: self.log = path.join('/', 'var', 'log', 'ipa', name) - for (key, value) in defaults.iteritems(): - if key not in self: - self[key] = value + self._merge(**defaults) def _finalize(self, **lastchance): """ @@ -370,26 +420,3 @@ class Env(object): if key not in self: self[key] = value self.__lock__() - - def _merge_from_file(self, conf_file): - """ - Merge values from ``conf_file`` into this `Env`. - """ - if not path.isfile(conf_file): - return - parser = RawConfigParser() - try: - parser.read(conf_file) - except ParsingError: - return - if not parser.has_section(CONFIG_SECTION): - parser.add_section(CONFIG_SECTION) - items = parser.items(CONFIG_SECTION) - if len(items) == 0: - return - i = 0 - for (key, value) in items: - if key not in self: - self[key] = value - i += 1 - return (i, len(items)) diff --git a/ipalib/constants.py b/ipalib/constants.py index dc23b109..5687c53e 100644 --- a/ipalib/constants.py +++ b/ipalib/constants.py @@ -25,8 +25,14 @@ All constants centralised in one file. # The parameter system treats all these values as None: NULLS = (None, '', u'', tuple(), []) +# regular expression NameSpace member names must match: +NAME_REGEX = r'^[a-z][_a-z0-9]*[a-z0-9]$' + +# Format for ValueError raised when name does not match above regex: +NAME_ERROR = 'name must match %r; got %r' + # Standard format for TypeError message: -TYPE_ERROR = '%s: need a %r; got %r (which is a %r)' +TYPE_ERROR = '%s: need a %r; got %r (a %r)' # Stardard format for TypeError message when a callable is expected: CALLABLE_ERROR = '%s: need a callable; got %r (which is a %r)' @@ -37,7 +43,7 @@ OVERRIDE_ERROR = 'cannot override %s.%s value %r with %r' # Standard format for AttributeError message when a read-only attribute is # already locked: SET_ERROR = 'locked: cannot set %s.%s to %r' -DEL_ERROR = 'locked: cannot del %s.%s' +DEL_ERROR = 'locked: cannot delete %s.%s' # Used for a tab (or indentation level) when formatting for CLI: CLI_TAB = ' ' # Two spaces -- cgit From 8decf4d8c3f5a6290d4b7605d0162a46d29c1edc Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 30 Dec 2008 00:57:56 -0700 Subject: Decided against indenting the example code in the base.ReadOnly docstring --- ipalib/base.py | 79 +++++++++++++++++++++++++++++----------------------------- 1 file changed, 40 insertions(+), 39 deletions(-) (limited to 'ipalib') diff --git a/ipalib/base.py b/ipalib/base.py index 2e8ae066..0d7c646b 100644 --- a/ipalib/base.py +++ b/ipalib/base.py @@ -43,42 +43,43 @@ class ReadOnly(object): For example, before a `ReadOnly` instance is locked, you can set and delete its attributes as normal: - >>> class Person(ReadOnly): - ... pass - ... - >>> p = Person() - >>> p.__islocked__() # Initially unlocked - False - >>> p.name = 'John Doe' - >>> p.phone = '123-456-7890' - >>> del p.phone + >>> class Person(ReadOnly): + ... pass + ... + >>> p = Person() + >>> p.__islocked__() # Initially unlocked + False + >>> p.name = 'John Doe' + >>> p.phone = '123-456-7890' + >>> del p.phone But after an instance is locked, you cannot set its attributes: - >>> 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' + >>> 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 + >>> del p.name + Traceback (most recent call last): + ... + AttributeError: locked: cannot delete Person.name - However, as noted above, there are still obscure ways in which attributes - can be set or deleted on a locked `ReadOnly` instance. For example: + 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 + >>> 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 as a mere accident. @@ -139,25 +140,25 @@ def check_name(name): 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' + >>> 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'name') - Traceback (most recent call last): - ... - TypeError: name: need a ; got u'name' (a ) + >>> check_name(u'my_name') + Traceback (most recent call last): + ... + TypeError: name: need a ; got u'my_name' (a ) 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('name') - >>> n - 'name' + >>> n = check_name('my_name') + >>> n + 'my_name' :param name: Identifier to test. """ -- cgit From 7012bed29949dacc26459ab2b49b51a494faf42f Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 30 Dec 2008 01:08:04 -0700 Subject: Small changes to base.ReadOnly docstring --- ipalib/base.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) (limited to 'ipalib') diff --git a/ipalib/base.py b/ipalib/base.py index 0d7c646b..2d80a077 100644 --- a/ipalib/base.py +++ b/ipalib/base.py @@ -47,8 +47,6 @@ class ReadOnly(object): ... pass ... >>> p = Person() - >>> p.__islocked__() # Initially unlocked - False >>> p.name = 'John Doe' >>> p.phone = '123-456-7890' >>> del p.phone @@ -56,18 +54,16 @@ class ReadOnly(object): But after an instance is locked, you cannot set its attributes: >>> 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 @@ -82,7 +78,7 @@ class ReadOnly(object): False But again, the point is that a programmer would never employ the above - techniques as a mere accident. + techniques accidentally. """ __locked = False @@ -142,7 +138,7 @@ def check_name(name): >>> 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 @@ -150,7 +146,7 @@ def check_name(name): >>> check_name(u'my_name') Traceback (most recent call last): - ... + ... TypeError: name: need a ; got u'my_name' (a ) So that `check_name()` can be easily used within an assignment, ``name`` -- cgit From 11e165073eb3bd21d764fa1c4d71f1e6a52eae1b Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 30 Dec 2008 03:11:45 -0700 Subject: Docstring cleanup in the Env bootstraping methods --- ipalib/config.py | 123 ++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 86 insertions(+), 37 deletions(-) (limited to 'ipalib') diff --git a/ipalib/config.py b/ipalib/config.py index 7317e4f0..c3c3a95c 100644 --- a/ipalib/config.py +++ b/ipalib/config.py @@ -267,23 +267,27 @@ class Env(object): def _merge(self, **kw): """ - Merge variables in ``kw`` into environment. + Merge variables from ``kw`` into the environment. - Any variables in ``kw`` that have already been set will be skipped - (which means this method will not try to override them). + Any variables in ``kw`` that have already been set will be ignored + (meaning this method will *not* try to override them, which would raise + an exception). - This method returns a (set, total) tuple contained the number of - variables actually set and the number of variables requested to be set. + This method returns a ``(num_set, num_total)`` tuple containing first + the number of variables that were actually set, and second the total + number of variables that were provided. For example: >>> env = Env() - >>> env._merge(first=1, second=2) + >>> env._merge(one=1, two=2) (2, 2) - >>> env._merge(first=1, third=3) + >>> env._merge(one=1, three=3) (1, 2) - >>> env._merge(first=1, second=2, third=3) + >>> env._merge(one=1, two=2, three=3) (0, 3) + + Also see `Env._merge_from_file()`. """ i = 0 for (key, value) in kw.iteritems(): @@ -292,22 +296,35 @@ class Env(object): i += 1 return (i, len(kw)) - def _merge_from_file(self, conf_file): + def _merge_from_file(self, config_file): """ - Merge values from ``conf_file`` into this `Env`. + Merge variables from ``config_file`` into the environment. + + Any variables in ``config_file`` that have already been set will be + ignored (meaning this method will *not* try to override them, which + would raise an exception). + + If ``config_file`` does not exist or is not a regular file, or if there + is an error parsing ``config_file``, ``None`` is returned. + + Otherwise this method returns a ``(num_set, num_total)`` tuple + containing first the number of variables that were actually set, and + second the total number of variables found in ``config_file``. + + Also see `Env._merge()`. """ - if not path.isfile(conf_file): + if not path.isfile(config_file): return parser = RawConfigParser() try: - parser.read(conf_file) + parser.read(config_file) except ParsingError: return if not parser.has_section(CONFIG_SECTION): parser.add_section(CONFIG_SECTION) items = parser.items(CONFIG_SECTION) if len(items) == 0: - return + return (0, 0) i = 0 for (key, value) in items: if key not in self: @@ -333,10 +350,24 @@ class Env(object): """ Initialize basic environment. - In addition to certain run-time information, this method will - initialize only enough environment information to determine whether - IPA is running in-tree, what the context is, and the location of the - configuration file. + This method will perform the following steps: + + 1. Initialize certain run-time variables. These run-time variables + are strictly determined by the external environment the process + is running in; they cannot be specified on the command-line nor + in the configuration files. + + 2. Merge-in the variables in ``overrides`` by calling + `Env._merge()`. The intended use of ``overrides`` is to merge-in + variables specified on the command-line. + + 3. Intelligently fill-in the ``in_tree``, ``context``, ``conf``, + and ``conf_default`` variables if they haven`t been set already. + + Also see `Env._finalize_core()`, the next method in the bootstrap + sequence. + + :param overrides: Variables specified via command-line options. """ self.__doing('_bootstrap') @@ -347,9 +378,7 @@ class Env(object): self.bin = path.dirname(self.script) self.home = path.abspath(os.environ['HOME']) self.dot_ipa = path.join(self.home, '.ipa') - - for (key, value) in overrides.iteritems(): - self[key] = value + self._merge(**overrides) if 'in_tree' not in self: if self.bin == self.site_packages and \ path.isfile(path.join(self.bin, 'setup.py')): @@ -373,15 +402,33 @@ class Env(object): """ Complete initialization of standard IPA environment. - After this method is called, the all environment variables - used by all the built-in plugins will be available. + This method will perform the following steps: - This method should be called before loading any plugins. It will - automatically call `Env._bootstrap()` if it has not yet been called. + 1. Call `Env._bootstrap()` if it hasn't already been called. + + 2. Merge-in variables from the configuration file ``self.conf`` + (if it exists) by calling `Env._merge_from_file()`. + + 3. Merge-in variables from the defaults configuration file + ``self.conf_default`` (if it exists) by calling + `Env._merge_from_file()`. + + 4. Intelligently fill-in the ``in_server`` and ``log`` variables + if they haven't already been set. + + 5. Merge in the internal defaults by calling `Env._merge()`. In + normal circumstances, these internal defaults will simply be + those specified in `constants.DEFAULT_CONFIG`. + + After this method is called, the all environment variables + used by all the built-in plugins will be available. As such, this + method should be called *before* any plugins are loaded. After this method has finished, the `Env` instance is still writable so that 3rd-party plugins can set variables they may require as the plugins are registered. + + Also see `Env._finalize()`, the final method in the bootstrap sequence. """ self.__doing('_finalize_core') self.__do_if_not_done('_bootstrap') @@ -402,21 +449,23 @@ class Env(object): """ Finalize and lock environment. + This method will perform the following steps: + + 1. Call `Env._finalize_core()` if it hasn't already been called. + + 2. Merge-in the variables in ``lastchance`` by calling + `Env._merge()`. + + 3. Lock this `Env` instance, after which no more environment + variables can be set on this instance. Aside from unit-tests + and example code, normally only one `Env` instance is created, + which means that after this step, no more variables can be set + during the remaining life of the process. + This method should be called after all plugins have been loaded and - after `plugable.API.finalize()` has been called. This method will - automatically call `Env._finalize_core()` if it hasn't been called - already, but in normal operation this would result in an exception - being raised because the internal default values will not have been - merged-in. - - After this method finishes, the `Env` instance will be locked and no - more environment variables can be set. Aside from unit-tests and - example code, normally only one `Env` instance is created, which means - no more variables can be set during the remaining life of the process. + after `plugable.API.finalize()` has been called. """ self.__doing('_finalize') self.__do_if_not_done('_finalize_core') - for (key, value) in lastchance.iteritems(): - if key not in self: - self[key] = value + self._merge(**lastchance) self.__lock__() -- cgit From 03c9114958e428c5fe6b286df9eda3bd932dc9dc Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 30 Dec 2008 13:52:36 -0700 Subject: More docstring cleanup in ipalib.config --- ipalib/base.py | 7 ++++--- ipalib/config.py | 50 +++++++++++++++++++++++++++++++++++++------------- 2 files changed, 41 insertions(+), 16 deletions(-) (limited to 'ipalib') diff --git a/ipalib/base.py b/ipalib/base.py index 2d80a077..1651b01d 100644 --- a/ipalib/base.py +++ b/ipalib/base.py @@ -36,9 +36,10 @@ class ReadOnly(object): 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 simply telling our programmers something like, - "Don't set any attributes on this ``FooBar`` instance because doing so wont - be thread-safe", this class gives us a way to enforce it. + *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: diff --git a/ipalib/config.py b/ipalib/config.py index c3c3a95c..84c0aab4 100644 --- a/ipalib/config.py +++ b/ipalib/config.py @@ -18,11 +18,14 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ -Basic configuration management. +Process-wide static configuration and environment. -This module handles the reading and representation of basic local settings. -It will also take care of settings that can be discovered by different -methods, such as DNS. +The standard run-time instance of the `Env` class is initialized early in the +`ipalib` process and is then locked into a read-only state, after which no +further changes can be made to the environment throughout the remaining life +of the process. + +For the per-request thread-local information, see `ipalib.request`. """ from ConfigParser import RawConfigParser, ParsingError @@ -288,6 +291,8 @@ class Env(object): (0, 3) Also see `Env._merge_from_file()`. + + :param kw: Variables provides as keyword arguments. """ i = 0 for (key, value) in kw.iteritems(): @@ -311,8 +316,23 @@ class Env(object): containing first the number of variables that were actually set, and second the total number of variables found in ``config_file``. + This method will raise a ``ValueError`` if ``config_file`` is not an + absolute path. For example: + + >>> env = Env() + >>> env._merge_from_file('my/config.conf') + Traceback (most recent call last): + ... + ValueError: config_file must be an absolute path; got 'my/config.conf' + Also see `Env._merge()`. + + :param config_file: Absolute path of the configuration file to load. """ + if path.abspath(config_file) != config_file: + raise ValueError( + 'config_file must be an absolute path; got %r' % config_file + ) if not path.isfile(config_file): return parser = RawConfigParser() @@ -361,8 +381,8 @@ class Env(object): `Env._merge()`. The intended use of ``overrides`` is to merge-in variables specified on the command-line. - 3. Intelligently fill-in the ``in_tree``, ``context``, ``conf``, - and ``conf_default`` variables if they haven`t been set already. + 3. Intelligently fill-in the *in_tree*, *context*, *conf*, and + *conf_default* variables if they haven`t been set already. Also see `Env._finalize_core()`, the next method in the bootstrap sequence. @@ -413,22 +433,24 @@ class Env(object): ``self.conf_default`` (if it exists) by calling `Env._merge_from_file()`. - 4. Intelligently fill-in the ``in_server`` and ``log`` variables + 4. Intelligently fill-in the *in_server* and *log* variables if they haven't already been set. - 5. Merge in the internal defaults by calling `Env._merge()`. In - normal circumstances, these internal defaults will simply be - those specified in `constants.DEFAULT_CONFIG`. + 5. Merge-in the variables in ``defaults`` by calling `Env._merge()`. + In normal circumstances ``defaults`` will simply be those + specified in `constants.DEFAULT_CONFIG`. - After this method is called, the all environment variables - used by all the built-in plugins will be available. As such, this - method should be called *before* any plugins are loaded. + After this method is called, all the environment variables used by all + the built-in plugins will be available. As such, this method should be + called *before* any plugins are loaded. After this method has finished, the `Env` instance is still writable so that 3rd-party plugins can set variables they may require as the plugins are registered. Also see `Env._finalize()`, the final method in the bootstrap sequence. + + :param defaults: Internal defaults for all built-in variables. """ self.__doing('_finalize_core') self.__do_if_not_done('_bootstrap') @@ -464,6 +486,8 @@ class Env(object): This method should be called after all plugins have been loaded and after `plugable.API.finalize()` has been called. + + :param lastchance: Any final variables to merge-in before locking. """ self.__doing('_finalize') self.__do_if_not_done('_finalize_core') -- cgit From ecccc5c236a1a11f899e9776991dbc1e719b9490 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 30 Dec 2008 14:05:08 -0700 Subject: Added my name to Athors of config.py --- ipalib/config.py | 1 + 1 file changed, 1 insertion(+) (limited to 'ipalib') diff --git a/ipalib/config.py b/ipalib/config.py index 84c0aab4..59e531e9 100644 --- a/ipalib/config.py +++ b/ipalib/config.py @@ -1,5 +1,6 @@ # Authors: # Martin Nagy +# Jason Gerard DeRose # # Copyright (C) 2008 Red Hat # see file 'COPYING' for use and warranty information -- cgit From 379c549fc16fbb2eed6685f5e189da26f021abe9 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 30 Dec 2008 15:02:15 -0700 Subject: Env now supports float values --- ipalib/config.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) (limited to 'ipalib') diff --git a/ipalib/config.py b/ipalib/config.py index 59e531e9..c1947433 100644 --- a/ipalib/config.py +++ b/ipalib/config.py @@ -65,10 +65,10 @@ class Env(object): >>> env.item # Also retrieve as an attribute 'I was set as a dictionary item.' - The variable values can be ``str`` or ``int`` instances, or the ``True``, - ``False``, or ``None`` constants. When the value provided is an ``str`` - instance, some limited automatic type conversion is performed, which allows - values of specific types to be set easily from configuration files or + The variable values can be ``str``, ``int``, or ``float`` instances, or the + ``True``, ``False``, or ``None`` constants. When the value provided is an + ``str`` instance, some limited automatic type conversion is performed, which + allows values of specific types to be set easily from configuration files or command-line options. So in addition to their actual values, the ``True``, ``False``, and ``None`` @@ -89,11 +89,15 @@ class Env(object): 'false' If an ``str`` value looks like an integer, it's automatically converted to - the ``int`` type. For example: + the ``int`` type. Likewise, if an ``str`` value looks like a floating-point + number, it's automatically converted to the ``float`` type. For example: >>> env.lucky = '7' >>> env.lucky 7 + >>> env.three_halves = '1.5' + >>> env.three_halves + 1.5 Leading and trailing white-space is automatically stripped from ``str`` values. For example: @@ -232,7 +236,12 @@ class Env(object): value = m[value] elif value.isdigit(): value = int(value) - assert type(value) in (str, int, bool, NoneType) + else: + try: + value = float(value) + except (TypeError, ValueError): + pass + assert type(value) in (str, int, float, bool, NoneType) object.__setattr__(self, key, value) self.__d[key] = value -- cgit From e9be796950070790543ca0cfaf5182958aee5dd6 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 30 Dec 2008 15:14:33 -0700 Subject: Fixed Env._bootstrap() docstring typo --- ipalib/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/config.py b/ipalib/config.py index c1947433..6feae573 100644 --- a/ipalib/config.py +++ b/ipalib/config.py @@ -392,7 +392,7 @@ class Env(object): variables specified on the command-line. 3. Intelligently fill-in the *in_tree*, *context*, *conf*, and - *conf_default* variables if they haven`t been set already. + *conf_default* variables if they haven't been set already. Also see `Env._finalize_core()`, the next method in the bootstrap sequence. -- cgit From 57dae28d9c4eb90d49f98cd528f85d203c8cbc94 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 30 Dec 2008 21:14:51 -0700 Subject: Added base.lock() and base.islocked() functions; added corresponding unit tests --- ipalib/base.py | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) (limited to 'ipalib') diff --git a/ipalib/base.py b/ipalib/base.py index 1651b01d..e427b747 100644 --- a/ipalib/base.py +++ b/ipalib/base.py @@ -130,6 +130,61 @@ class ReadOnly(object): 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. -- cgit From 86325bf4ebd8a9d40e81e4fd835635dfaa4139cd Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 31 Dec 2008 02:28:49 -0700 Subject: Copied plugable.NameSpace to base.NameSpace and made many docstring and unit test improvements --- ipalib/base.py | 240 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 239 insertions(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/base.py b/ipalib/base.py index e427b747..e3d08208 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) -- cgit From b3063dbb8a652c9eb4eea940fff3032e2082dce3 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 31 Dec 2008 15:47:28 -0700 Subject: A few base.NameSpace docstring tweaks --- ipalib/base.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) (limited to 'ipalib') diff --git a/ipalib/base.py b/ipalib/base.py index e3d08208..6fcd248f 100644 --- a/ipalib/base.py +++ b/ipalib/base.py @@ -18,7 +18,7 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ -Low-level functions and abstract base classes. +Core functions and classes. """ import re @@ -255,6 +255,10 @@ class NameSpace(ReadOnly): >>> 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 @@ -315,19 +319,19 @@ class NameSpace(ReadOnly): >>> 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: + As another convenience, the `NameSpace.__todict__()` method 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)} - - `NameSpace.__init__()` locks the instance, so `NameSpace` instances are - read-only from the get-go. For example: + 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 -- cgit From ea7f9594dfd7e781e9ce06aabb17388071749855 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 2 Jan 2009 00:35:42 -0700 Subject: A few docstring edits in base.NameSpace --- ipalib/base.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) (limited to 'ipalib') diff --git a/ipalib/base.py b/ipalib/base.py index 6fcd248f..d8394874 100644 --- a/ipalib/base.py +++ b/ipalib/base.py @@ -311,16 +311,18 @@ class NameSpace(ReadOnly): ['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: + 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, less verbose way to do it + >>> list(ns()) # A more efficient, simpler way to do it [Member(0), Member(1), Member(2)] - As another convenience, the `NameSpace.__todict__()` method will return a - copy of the ``dict`` mapping the member names to the members. For example: + 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)} -- cgit From b4dc333ee2a010f3629002932d06a8b8a10df1d3 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 2 Jan 2009 00:46:45 -0700 Subject: Removed depreciated code in ipalib.plugable that has been moving into ipalib.base --- ipalib/plugable.py | 251 +---------------------------------------------------- 1 file changed, 2 insertions(+), 249 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 0120f972..923b72ad 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -36,104 +36,9 @@ import subprocess import errors from errors import check_type, check_isinstance from config import Env -from constants import DEFAULT_CONFIG import util - - - -class ReadOnly(object): - """ - Base class for classes with read-only attributes. - - Be forewarned that Python does not offer true read-only user defined - classes. In particular, 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 delete - attributes, but to make it impossible to accidentally do so. The plugins - are not thread-safe: in the server, they are loaded once and the same - instances will be used to process many requests. Therefore, it is - imperative that they not set any instance attributes after they have - been initialized. This base class enforces that policy. - - For example: - - >>> ro = ReadOnly() # Initially unlocked, can setattr, delattr - >>> ro.name = 'John Doe' - >>> ro.message = 'Hello, world!' - >>> del ro.message - >>> ro.__lock__() # Now locked, cannot setattr, delattr - >>> ro.message = 'How are you?' - Traceback (most recent call last): - File "", line 1, in - File ".../ipalib/plugable.py", line 93, in __setattr__ - (self.__class__.__name__, name) - AttributeError: read-only: cannot set ReadOnly.message - >>> del ro.name - Traceback (most recent call last): - File "", line 1, in - File "/home/jderose/projects/freeipa2/ipalib/plugable.py", line 104, in __delattr__ - (self.__class__.__name__, name) - AttributeError: read-only: cannot del ReadOnly.name - """ - - __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 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, AttributeError will be raised. - """ - if self.__locked: - raise AttributeError('read-only: cannot set %s.%s' % - (self.__class__.__name__, name) - ) - return object.__setattr__(self, name, value) - - def __delattr__(self, name): - """ - If unlocked, delete attribute named ``name``. - - If this instance is locked, AttributeError will be raised. - """ - if self.__locked: - raise AttributeError('read-only: cannot del %s.%s' % - (self.__class__.__name__, name) - ) - return object.__delattr__(self, name) - - -def lock(readonly): - """ - Lock a `ReadOnly` instance. - - This is mostly a convenience function to call `ReadOnly.__lock__()`. It - also verifies that the locking worked using `ReadOnly.__islocked__()` - - :param readonly: An instance of the `ReadOnly` class. - """ - if not isinstance(readonly, ReadOnly): - raise ValueError('not a ReadOnly instance: %r' % readonly) - readonly.__lock__() - assert readonly.__islocked__(), 'Ouch! The locking failed?' - return readonly +from base import ReadOnly, NameSpace, lock, islocked, check_name +from constants import DEFAULT_CONFIG class SetProxy(ReadOnly): @@ -512,158 +417,6 @@ class PluginProxy(SetProxy): ) -def check_name(name): - """ - Verify that ``name`` is suitable for a `NameSpace` member name. - - Raises `errors.NameSpaceError` if ``name`` is not a valid Python - identifier suitable for use as the name of `NameSpace` member. - - :param name: Identifier to test. - """ - check_type(name, str, 'name') - regex = r'^[a-z][_a-z0-9]*[a-z0-9]$' - if re.match(regex, name) is None: - raise errors.NameSpaceError(name, regex) - return name - - -class NameSpace(ReadOnly): - """ - A read-only namespace with handy container behaviours. - - Each member of a NameSpace instance must have 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: - - >>> class obj(object): - ... name = 'my_obj' - ... - >>> namespace = NameSpace([obj]) - >>> obj is getattr(namespace, 'my_obj') # As attribute - True - >>> obj is namespace['my_obj'] # As dictionary item - True - - Here is a more detailed example: - - >>> class Member(object): - ... def __init__(self, i): - ... self.i = i - ... self.name = 'member_%d' % i - ... def __repr__(self): - ... return 'Member(%d)' % self.i - ... - >>> namespace = NameSpace(Member(i) for i in xrange(3)) - >>> namespace.member_0 is namespace['member_0'] - True - >>> len(namespace) # Returns the number of members in namespace - 3 - >>> list(namespace) # As iterable, iterates through the member names - ['member_0', 'member_1', 'member_2'] - >>> list(namespace()) # Calling a NameSpace iterates through the members - [Member(0), Member(1), Member(2)] - >>> 'member_1' in namespace # Does namespace contain 'member_1'? - True - """ - - def __init__(self, members, sort=True): - """ - :param members: An iterable providing the members. - :param sort: Whether to sort the members by member name. - """ - self.__sort = check_type(sort, bool, 'sort') - if self.__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) - assert name not in self.__map, 'already has key %r' % name - self.__map[name] = member - assert not hasattr(self, name), 'already has attribute %r' % name - 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=True``, the names will be in - alphabetical order; otherwise the names will be in the same order as - the members were passed to the constructor. - - 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=True``, the members will be - in alphabetical order by name; otherwise the members will be in the - same order as they were passed to the constructor. - - 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, spec): - """ - Return a member by name or index, or returns a slice of members. - - :param spec: The name or index of a member, or a slice object. - """ - if type(spec) is str: - return self.__map[spec] - if type(spec) in (int, slice): - return self.__members[spec] - raise TypeError( - 'spec: must be %r, %r, or %r; got %r' % (str, int, slice, spec) - ) - - def __repr__(self): - """ - Return a pseudo-valid expression that could create this instance. - """ - return '%s(<%d members>, sort=%r)' % ( - self.__class__.__name__, - len(self), - self.__sort, - ) - - def __todict__(self): - """ - Return a copy of the private dict mapping name to member. - """ - return dict(self.__map) - - class Registrar(DictProxy): """ Collects plugin classes as they are registered. -- cgit From 7be459af0b52753ccde74e6833d664341935973d Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 2 Jan 2009 01:14:37 -0700 Subject: Added a bit to config.Env docstring about that variable names must pass check_name() function --- ipalib/config.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) (limited to 'ipalib') diff --git a/ipalib/config.py b/ipalib/config.py index 6feae573..04f1442f 100644 --- a/ipalib/config.py +++ b/ipalib/config.py @@ -65,6 +65,20 @@ class Env(object): >>> env.item # Also retrieve as an attribute 'I was set as a dictionary item.' + The variable names must be valid lower-case Python identifiers that neither + start nor end with an underscore. If your variable name doesn't meet these + criteria, a ``ValueError`` will be raised when you try to set the variable + (compliments of the `base.check_name()` function). For example: + + >>> env.BadName = 'Wont work as an attribute' + Traceback (most recent call last): + ... + ValueError: name must match '^[a-z][_a-z0-9]*[a-z0-9]$'; got 'BadName' + >>> env['BadName'] = 'Also wont work as a dictionary item' + Traceback (most recent call last): + ... + ValueError: name must match '^[a-z][_a-z0-9]*[a-z0-9]$'; got 'BadName' + The variable values can be ``str``, ``int``, or ``float`` instances, or the ``True``, ``False``, or ``None`` constants. When the value provided is an ``str`` instance, some limited automatic type conversion is performed, which -- cgit From 72340a594d558796d2ff447cd612311825033128 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 2 Jan 2009 01:16:17 -0700 Subject: Removed unneeded import of check_type, check_instance in plugable.py --- ipalib/plugable.py | 1 - 1 file changed, 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 923b72ad..4ef2135b 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -34,7 +34,6 @@ import os from os import path import subprocess import errors -from errors import check_type, check_isinstance from config import Env import util from base import ReadOnly, NameSpace, lock, islocked, check_name -- cgit From dae08b3ee6bfa3d9d4108b839255fd152e543f36 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 2 Jan 2009 02:22:48 -0700 Subject: Small docstring cleanup in parameters.py --- ipalib/parameter.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) (limited to 'ipalib') diff --git a/ipalib/parameter.py b/ipalib/parameter.py index 76a9cd50..1d629f38 100644 --- a/ipalib/parameter.py +++ b/ipalib/parameter.py @@ -22,9 +22,9 @@ Parameter system for command plugins. """ from types import NoneType +from util import make_repr from plugable import ReadOnly, lock, check_name from constants import NULLS, TYPE_ERROR, CALLABLE_ERROR -from util import make_repr class DefaultFrom(ReadOnly): @@ -38,7 +38,7 @@ class DefaultFrom(ReadOnly): >>> login(first='John', last='Doe') 'JDoe' - If you do not explicitly provide keys when you create a DefaultFrom + If you do not explicitly provide keys when you create a `DefaultFrom` instance, the keys are implicitly derived from your callback by inspecting ``callback.func_code.co_varnames``. The keys are available through the ``DefaultFrom.keys`` instance attribute, like this: @@ -49,9 +49,9 @@ class DefaultFrom(ReadOnly): The callback is available through the ``DefaultFrom.callback`` instance attribute, like this: - >>> login.callback # doctest:+ELLIPSIS + >>> login.callback # doctest:+ELLIPSIS at 0x...> - >>> login.callback.func_code.co_varnames # The keys + >>> login.callback.func_code.co_varnames # The keys ('first', 'last') The keys can be explicitly provided as optional positional arguments after @@ -61,13 +61,13 @@ class DefaultFrom(ReadOnly): >>> login2 = DefaultFrom(lambda a, b: a[0] + b, 'first', 'last') >>> login2.keys ('first', 'last') - >>> login2.callback.func_code.co_varnames # Not the keys + >>> login2.callback.func_code.co_varnames # Not the keys ('a', 'b') >>> login2(first='John', last='Doe') 'JDoe' - If any keys are missing when calling your DefaultFrom instance, your - callback is not called and None is returned. For example: + If any keys are missing when calling your `DefaultFrom` instance, your + callback is not called and ``None`` is returned. For example: >>> login(first='John', lastname='Doe') is None True @@ -82,7 +82,7 @@ class DefaultFrom(ReadOnly): As above, because `DefaultFrom.__call__` takes only pure keyword arguments, they can be supplied in any order. - Of course, the callback need not be a lambda expression. This third + Of course, the callback need not be a ``lambda`` expression. This third example is equivalent to both the ``login`` and ``login2`` instances above: @@ -358,7 +358,7 @@ class Param(ReadOnly): (Note that `Str` is a subclass of `Param`.) - All values in `constants.NULLS` will be converted to None. For + All values in `constants.NULLS` will be converted to ``None``. For example: >>> scalar.convert(u'') is None # An empty string @@ -375,9 +375,8 @@ class Param(ReadOnly): >>> multi.convert([None, u'']) is None # Filters to an empty list True - Lastly, multivalue parameters will always return a tuple (well, - assuming they don't return None as in the last example above). - For example: + Lastly, multivalue parameters will always return a ``tuple`` (assuming + they don't return ``None`` as in the last example above). For example: >>> multi.convert(42) # Called with a scalar value (u'42',) -- cgit From b32965dffe1dbd4404dd4afb63994c807e80e25c Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 2 Jan 2009 17:27:44 -0700 Subject: Clarifed base.DefaultFrom.__call__() docstring --- ipalib/parameter.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/parameter.py b/ipalib/parameter.py index 1d629f38..7605339d 100644 --- a/ipalib/parameter.py +++ b/ipalib/parameter.py @@ -122,7 +122,10 @@ class DefaultFrom(ReadOnly): def __call__(self, **kw): """ - If all keys are present, calls the callback; otherwise returns None. + Call the callback if all keys are present. + + If all keys are present, the callback is called and its return value is + returned. If any keys are missing, ``None`` is returned. :param kw: The keyword arguments. """ -- cgit From 0d3ddef93b0a72b824297e5504e435a4427f14bd Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sat, 3 Jan 2009 02:35:36 -0700 Subject: Started fleshing out reoganization of errors in errors.py (with gettext support) --- ipalib/errors.py | 15 ---- ipalib/errors2.py | 213 ++++++++++++++++++++++++++++++++++++++++++++++++++++ ipalib/parameter.py | 7 ++ ipalib/request.py | 6 ++ 4 files changed, 226 insertions(+), 15 deletions(-) create mode 100644 ipalib/errors2.py (limited to 'ipalib') diff --git a/ipalib/errors.py b/ipalib/errors.py index 7191ff40..beb6342d 100644 --- a/ipalib/errors.py +++ b/ipalib/errors.py @@ -241,21 +241,6 @@ class RegistrationError(IPAError): """ -class NameSpaceError(RegistrationError): - """ - Raised when name is not a valid Python identifier for use for use as - the name of NameSpace member. - """ - msg = 'name %r does not re.match %r' - - def __init__(self, name, regex): - self.name = name - self.regex = regex - - def __str__(self): - return self.msg % (self.name, self.regex) - - class SubclassError(RegistrationError): """ Raised when registering a plugin that is not a subclass of one of the diff --git a/ipalib/errors2.py b/ipalib/errors2.py new file mode 100644 index 00000000..f49c10ef --- /dev/null +++ b/ipalib/errors2.py @@ -0,0 +1,213 @@ +# Authors: +# Jason Gerard DeRose +# +# Copyright (C) 2008 Red Hat +# see file 'COPYING' for use and warranty inmsgion +# +# 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 + +""" +Custom exception classes. + +Certain errors can be returned in RPC response to relay some error condition +to the caller. + + ============= ======================================== + Error codes Exceptions + ============= ======================================== + 900 `PublicError` + 901 `InternalError` + 902 - 999 *Reserved for future use* + 1000 - 1999 `AuthenticationError` and its subclasses + 2000 - 2999 `AuthorizationError` and its subclasses + 3000 - 3999 `InvocationError` and its subclasses + 4000 - 4999 `ExecutionError` and its subclasses + 5000 - 5999 `GenericError` and its subclasses + ============= ======================================== +""" + +from inspect import isclass +import request + + +class PrivateError(StandardError): + """ + Base class for exceptions that are *never* returned in an RPC response. + """ + + +class PublicError(StandardError): + """ + **900** Base class for exceptions that can be returned in an RPC response. + """ + + code = 900 + + def __init__(self, message=None, **kw): + self.kw = kw + if message is None: + message = self.get_format(request._) % kw + StandardError.__init__(self, message) + + def get_format(self, _): + return _('') + + + + + +class InternalError(PublicError): + """ + **901** Used to conceal a non-public exception. + """ + + code = 901 + + + +############################################################################## +# 1000 - 1999: Authentication Errors +class AuthenticationError(PublicError): + """ + **1000** Base class for authentication errors (*1000 - 1999*). + """ + + code = 1000 + + + +############################################################################## +# 2000 - 2999: Authorization Errors +class AuthorizationError(PublicError): + """ + **2000** Base class for authorization errors (*2000 - 2999*). + """ + + code = 2000 + + + +############################################################################## +# 3000 - 3999: Invocation Errors + +class InvocationError(PublicError): + """ + **3000** Base class for command invocation errors (*3000 - 3999*). + """ + + code = 3000 + + +class CommandError(InvocationError): + """ + **3001** Raised when an unknown command is called. + """ + + code = 3001 + + def get_format(self, _): + return _('Unknown command %(name)r') + + +class RemoteCommandError(InvocationError): + """ + **3002** Raised when client receives a `CommandError` from server. + """ + + code = 3002 + + +class ArgumentError(InvocationError): + """ + **3003** Raised when a command is called with wrong number of arguments. + """ + + code = 3003 + + +class OptionError(InvocationError): + """ + **3004** Raised when a command is called with unknown options. + """ + + code = 3004 + + +class RequirementError(InvocationError): + """ + **3005** Raised when a required parameter is not provided. + """ + + code = 3005 + + +class ConversionError(InvocationError): + """ + **3006** Raised when a parameter value is the wrong type. + """ + + code = 3006 + + +class ValidationError(InvocationError): + """ + **3007** Raised when a parameter value fails a validation rule. + """ + + code = 3007 + + + +############################################################################## +# 4000 - 4999: Execution Errors + +class ExecutionError(PublicError): + """ + **4000** Base class for execution/operation errors (*4000 - 4999*). + """ + + code = 4000 + + + +############################################################################## +# 5000 - 5999: Generic Errors + +class GenericError(PublicError): + """ + **5000** Errors inappropriate for other categories (*5000 - 5999*). + """ + + code = 5000 + + + +def __errors_iter(): + """ + Iterate through all the `PublicError` subclasses. + """ + for (key, value) in globals().items(): + if key.startswith('_') or not isclass(value): + continue + if issubclass(value, PublicError): + yield value + +public_errors = tuple( + sorted(__errors_iter(), key=lambda E: E.code) +) + +if __name__ == '__main__': + for klass in public_errors: + print '%d\t%s' % (klass.code, klass.__name__) + print '(%d public errors)' % len(public_errors) diff --git a/ipalib/parameter.py b/ipalib/parameter.py index 7605339d..55a9bc6d 100644 --- a/ipalib/parameter.py +++ b/ipalib/parameter.py @@ -416,6 +416,13 @@ class Param(ReadOnly): '%s.%s()' % (self.__class__.__name__, '_convert_scalar') ) + def validate(self, value): + """ + Check validity of ``value``. + + :param value: A proposed value for this parameter. + """ + class Bool(Param): """ diff --git a/ipalib/request.py b/ipalib/request.py index ea028239..5a07d4db 100644 --- a/ipalib/request.py +++ b/ipalib/request.py @@ -32,6 +32,12 @@ from constants import OVERRIDE_ERROR context = threading.local() +def _(message): + if hasattr(context, 'gettext'): + return context.gettext(message) + return message.decode('UTF-8') + + def set_languages(*languages): if hasattr(context, 'languages'): raise StandardError(OVERRIDE_ERROR % -- cgit From d1517b95ca14773773647434fb589c8224307328 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sat, 3 Jan 2009 15:35:54 -0700 Subject: Ported errors.SubprocessError to errors2 --- ipalib/errors2.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) (limited to 'ipalib') diff --git a/ipalib/errors2.py b/ipalib/errors2.py index f49c10ef..676dd79f 100644 --- a/ipalib/errors2.py +++ b/ipalib/errors2.py @@ -46,6 +46,37 @@ class PrivateError(StandardError): Base class for exceptions that are *never* returned in an RPC response. """ + format = '' + + def __init__(self, **kw): + self.message = self.format % kw + for (key, value) in kw.iteritems(): + assert not hasattr(self, key), 'conflicting kwarg %s.%s = %r' % ( + self.__class__.__name__, key, value, + ) + setattr(self, key, value) + StandardError.__init__(self, self.message) + + +class SubprocessError(PrivateError): + """ + Raised when ``subprocess.call()`` returns a non-zero exit status. + + This custom exception is needed because Python 2.4 doesn't have the + ``subprocess.CalledProcessError`` exception (which was added in Python 2.5). + + For example: + + >>> e = SubprocessError(returncode=1, argv=('/bin/false',)) + >>> e.returncode + 1 + >>> e.argv + ('/bin/false',) + >>> str(e) + "return code 1 from ('/bin/false',)" + """ + format = 'return code %(returncode)d from %(argv)r' + class PublicError(StandardError): """ -- cgit From 6b6e6b1cab7a633faf16631a565ecb6988dadb48 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sat, 3 Jan 2009 17:27:53 -0700 Subject: Ported plugin registration errors into errors2.py; plugable.Registrar now raises new errors2 exceptions --- ipalib/errors2.py | 83 ++++++++++++++++++++++++++++++++++++++++++++++++------ ipalib/plugable.py | 21 ++++++++++---- 2 files changed, 90 insertions(+), 14 deletions(-) (limited to 'ipalib') diff --git a/ipalib/errors2.py b/ipalib/errors2.py index 676dd79f..7793a914 100644 --- a/ipalib/errors2.py +++ b/ipalib/errors2.py @@ -43,7 +43,7 @@ import request class PrivateError(StandardError): """ - Base class for exceptions that are *never* returned in an RPC response. + Base class for exceptions that are *never* forwarded in an RPC response. """ format = '' @@ -72,15 +72,82 @@ class SubprocessError(PrivateError): 1 >>> e.argv ('/bin/false',) + >>> e.message + "return code 1 from ('/bin/false',)" >>> str(e) "return code 1 from ('/bin/false',)" """ + format = 'return code %(returncode)d from %(argv)r' +class PluginSubclassError(PrivateError): + """ + Raised when a plugin doesn't subclass from an allowed base. + + For example: + + >>> raise PluginSubclassError(plugin='bad', bases=('base1', 'base2')) + Traceback (most recent call last): + ... + PluginSubclassError: 'bad' not subclass of any base in ('base1', 'base2') + + """ + + format = '%(plugin)r not subclass of any base in %(bases)r' + + +class PluginDuplicateError(PrivateError): + """ + Raised when the same plugin class is registered more than once. + + For example: + + >>> raise PluginDuplicateError(plugin='my_plugin') + Traceback (most recent call last): + ... + PluginDuplicateError: 'my_plugin' was already registered + """ + + format = '%(plugin)r was already registered' + + +class PluginOverrideError(PrivateError): + """ + Raised when a plugin overrides another without using ``override=True``. + + For example: + + >>> raise PluginOverrideError(base='Command', name='env', plugin='my_env') + Traceback (most recent call last): + ... + PluginOverrideError: unexpected override of Command.env with 'my_env' + """ + + format = 'unexpected override of %(base)s.%(name)s with %(plugin)r' + + +class PluginMissingOverrideError(PrivateError): + """ + Raised when a plugin overrides another that has not been registered. + + For example: + + >>> raise PluginMissingOverrideError(base='Command', name='env', plugin='my_env') + Traceback (most recent call last): + ... + PluginMissingOverrideError: Command.env not registered, cannot override with 'my_env' + """ + + format = '%(base)s.%(name)s not registered, cannot override with %(plugin)r' + + + +############################################################################## +# Public errors: class PublicError(StandardError): """ - **900** Base class for exceptions that can be returned in an RPC response. + **900** Base class for exceptions that can be forwarded in an RPC response. """ code = 900 @@ -96,8 +163,6 @@ class PublicError(StandardError): - - class InternalError(PublicError): """ **901** Used to conceal a non-public exception. @@ -108,7 +173,7 @@ class InternalError(PublicError): ############################################################################## -# 1000 - 1999: Authentication Errors +# 1000 - 1999: Authentication errors class AuthenticationError(PublicError): """ **1000** Base class for authentication errors (*1000 - 1999*). @@ -119,7 +184,7 @@ class AuthenticationError(PublicError): ############################################################################## -# 2000 - 2999: Authorization Errors +# 2000 - 2999: Authorization errors class AuthorizationError(PublicError): """ **2000** Base class for authorization errors (*2000 - 2999*). @@ -130,7 +195,7 @@ class AuthorizationError(PublicError): ############################################################################## -# 3000 - 3999: Invocation Errors +# 3000 - 3999: Invocation errors class InvocationError(PublicError): """ @@ -201,7 +266,7 @@ class ValidationError(InvocationError): ############################################################################## -# 4000 - 4999: Execution Errors +# 4000 - 4999: Execution errors class ExecutionError(PublicError): """ @@ -213,7 +278,7 @@ class ExecutionError(PublicError): ############################################################################## -# 5000 - 5999: Generic Errors +# 5000 - 5999: Generic errors class GenericError(PublicError): """ diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 4ef2135b..094634d3 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -34,6 +34,7 @@ import os from os import path import subprocess import errors +import errors2 from config import Env import util from base import ReadOnly, NameSpace, lock, islocked, check_name @@ -461,7 +462,9 @@ class Registrar(DictProxy): found = True yield (base, sub_d) if not found: - raise errors.SubclassError(klass, self.__allowed.keys()) + raise errors2.PluginSubclassError( + plugin=klass, bases=self.__allowed.keys() + ) def __call__(self, klass, override=False): """ @@ -471,11 +474,11 @@ class Registrar(DictProxy): :param override: If true, override an already registered plugin. """ if not inspect.isclass(klass): - raise TypeError('plugin must be a class: %r' % klass) + raise TypeError('plugin must be a class; got %r' % klass) # Raise DuplicateError if this exact class was already registered: if klass in self.__registered: - raise errors.DuplicateError(klass) + raise errors2.PluginDuplicateError(plugin=klass) # Find the base class or raise SubclassError: for (base, sub_d) in self.__findbases(klass): @@ -483,11 +486,19 @@ class Registrar(DictProxy): if klass.__name__ in sub_d: if not override: # Must use override=True to override: - raise errors.OverrideError(base, klass) + raise errors2.PluginOverrideError( + base=base.__name__, + name=klass.__name__, + plugin=klass, + ) else: if override: # There was nothing already registered to override: - raise errors.MissingOverrideError(base, klass) + raise errors2.PluginMissingOverrideError( + base=base.__name__, + name=klass.__name__, + plugin=klass, + ) # The plugin is okay, add to sub_d: sub_d[klass.__name__] = klass -- cgit From bb6e9cfe9ff25f3a018b23785f71302911eab435 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sat, 3 Jan 2009 18:02:58 -0700 Subject: Plugin.call() now uses errors2 version of SubprocessError --- ipalib/errors2.py | 14 +++++++++----- ipalib/plugable.py | 6 +++--- 2 files changed, 12 insertions(+), 8 deletions(-) (limited to 'ipalib') diff --git a/ipalib/errors2.py b/ipalib/errors2.py index 7793a914..7fd4b9c9 100644 --- a/ipalib/errors2.py +++ b/ipalib/errors2.py @@ -67,15 +67,19 @@ class SubprocessError(PrivateError): For example: + >>> raise SubprocessError(returncode=2, argv=('ls', '-lh', '/no-foo/')) + Traceback (most recent call last): + ... + SubprocessError: return code 2 from ('ls', '-lh', '/no-foo/') + + The exit code of the sub-process is available via the ``returncode`` + instance attribute. For example: + >>> e = SubprocessError(returncode=1, argv=('/bin/false',)) >>> e.returncode 1 - >>> e.argv + >>> e.argv # argv is also available ('/bin/false',) - >>> e.message - "return code 1 from ('/bin/false',)" - >>> str(e) - "return code 1 from ('/bin/false',)" """ format = 'return code %(returncode)d from %(argv)r' diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 094634d3..f7baa2a1 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -291,9 +291,9 @@ class Plugin(ReadOnly): """ argv = (executable,) + args self.debug('Calling %r', argv) - returncode = subprocess.call(argv) - if returncode != 0: - raise errors.SubprocessError(returncode, argv) + code = subprocess.call(argv) + if code != 0: + raise errors2.SubprocessError(returncode=code, argv=argv) def __repr__(self): """ -- cgit From 912ab9e68b6030d4d115d3683893477abb4f6451 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sat, 3 Jan 2009 18:08:39 -0700 Subject: Removed unneeded import of errors from plugable.py --- ipalib/plugable.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugable.py b/ipalib/plugable.py index f7baa2a1..d4ec8749 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -33,7 +33,6 @@ import logging import os from os import path import subprocess -import errors import errors2 from config import Env import util @@ -283,7 +282,7 @@ class Plugin(ReadOnly): Call ``executable`` with ``args`` using subprocess.call(). If the call exits with a non-zero exit status, - `ipalib.errors.SubprocessError` is raised, from which you can retrieve + `ipalib.errors2.SubprocessError` is raised, from which you can retrieve the exit code by checking the SubprocessError.returncode attribute. This method does *not* return what ``executable`` sent to stdout... for @@ -450,10 +449,10 @@ class Registrar(DictProxy): """ Iterates through allowed bases that ``klass`` is a subclass of. - Raises `errors.SubclassError` if ``klass`` is not a subclass of any - allowed base. + Raises `errors2.PluginSubclassError` if ``klass`` is not a subclass of + any allowed base. - :param klass: The class to find bases for. + :param klass: The plugin class to find bases for. """ assert inspect.isclass(klass) found = False -- cgit From c081ce5460018634fb30249ead2168ebf3a79044 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sat, 3 Jan 2009 22:03:37 -0700 Subject: request.create_translation() now sets context.ugettext and context.ungettext --- ipalib/request.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'ipalib') diff --git a/ipalib/request.py b/ipalib/request.py index 5a07d4db..f5400b75 100644 --- a/ipalib/request.py +++ b/ipalib/request.py @@ -50,7 +50,7 @@ def set_languages(*languages): def create_translation(domain, localedir, *languages): - if hasattr(context, 'gettext') or hasattr(context, 'ngettext'): + if hasattr(context, 'ugettext') or hasattr(context, 'ungettext'): raise StandardError( 'create_translation() already called in thread %r' % threading.currentThread().getName() @@ -59,5 +59,5 @@ def create_translation(domain, localedir, *languages): translation = gettext.translation(domain, localedir=localedir, languages=context.languages, fallback=True ) - context.gettext = translation.ugettext - context.ngettext = translation.ungettext + context.ugettext = translation.ugettext + context.ungettext = translation.ungettext -- cgit From c161784973fdedb146a4087d8692b157214c4db0 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sun, 4 Jan 2009 00:46:21 -0700 Subject: Added request.ugettext() and request.ungettext() functions; added corresponding unit tests --- ipalib/errors2.py | 4 ++-- ipalib/request.py | 14 +++++++++++--- 2 files changed, 13 insertions(+), 5 deletions(-) (limited to 'ipalib') diff --git a/ipalib/errors2.py b/ipalib/errors2.py index 7fd4b9c9..4cb84870 100644 --- a/ipalib/errors2.py +++ b/ipalib/errors2.py @@ -38,7 +38,7 @@ to the caller. """ from inspect import isclass -import request +from request import ugettext, ungettext class PrivateError(StandardError): @@ -159,7 +159,7 @@ class PublicError(StandardError): def __init__(self, message=None, **kw): self.kw = kw if message is None: - message = self.get_format(request._) % kw + message = self.get_format() % kw StandardError.__init__(self, message) def get_format(self, _): diff --git a/ipalib/request.py b/ipalib/request.py index f5400b75..6ad7ad35 100644 --- a/ipalib/request.py +++ b/ipalib/request.py @@ -32,12 +32,20 @@ from constants import OVERRIDE_ERROR context = threading.local() -def _(message): - if hasattr(context, 'gettext'): - return context.gettext(message) +def ugettext(message): + if hasattr(context, 'ugettext'): + return context.ugettext(message) return message.decode('UTF-8') +def ungettext(singular, plural, n): + if hasattr(context, 'ungettext'): + return context.ungettext(singular, plural, n) + if n == 1: + return singular.decode('UTF-8') + return plural.decode('UTF-8') + + def set_languages(*languages): if hasattr(context, 'languages'): raise StandardError(OVERRIDE_ERROR % -- cgit From 2608838ef1f96b0c8d3ff3ed4310eaa63ba73031 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sun, 4 Jan 2009 03:52:08 -0700 Subject: Quite a bit of work on new public errors and their unit tests --- ipalib/errors2.py | 103 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 95 insertions(+), 8 deletions(-) (limited to 'ipalib') diff --git a/ipalib/errors2.py b/ipalib/errors2.py index 4cb84870..5ec97fbe 100644 --- a/ipalib/errors2.py +++ b/ipalib/errors2.py @@ -28,7 +28,9 @@ to the caller. ============= ======================================== 900 `PublicError` 901 `InternalError` - 902 - 999 *Reserved for future use* + 902 `RemoteInternalError` + 903 `VersionError` + 904 - 999 *Reserved for future use* 1000 - 1999 `AuthenticationError` and its subclasses 2000 - 2999 `AuthorizationError` and its subclasses 3000 - 3999 `InvocationError` and its subclasses @@ -39,6 +41,7 @@ to the caller. from inspect import isclass from request import ugettext, ungettext +from constants import TYPE_ERROR class PrivateError(StandardError): @@ -157,23 +160,90 @@ class PublicError(StandardError): code = 900 def __init__(self, message=None, **kw): - self.kw = kw if message is None: - message = self.get_format() % kw + message = self.get_format(ugettext) % kw + assert type(message) is unicode + elif type(message) is not unicode: + raise TypeError( + TYPE_ERROR % ('message', unicode, message, type(message)) + ) + self.message = message + for (key, value) in kw.iteritems(): + assert not hasattr(self, key), 'conflicting kwarg %s.%s = %r' % ( + self.__class__.__name__, key, value, + ) + setattr(self, key, value) StandardError.__init__(self, message) def get_format(self, _): return _('') - class InternalError(PublicError): """ **901** Used to conceal a non-public exception. + + For example: + + >>> raise InternalError() + Traceback (most recent call last): + ... + InternalError: an internal error has occured """ code = 901 + def __init__(self, message=None): + """ + Security issue: ignore any information given to constructor. + """ + PublicError.__init__(self, self.get_format(ugettext)) + + def get_format(self, _): + return _('an internal error has occured') + + +class RemoteInternalError(PublicError): + """ + **902** Raised when client catches an `InternalError` from server. + + For example: + + >>> raise RemoteInternalError(uri='http://localhost:8888') + Traceback (most recent call last): + ... + RemoteInternalError: an internal error has occured on server 'http://localhost:8888' + """ + + code = 902 + + def get_format(self, _): + return _('an internal error has occured on server %(uri)r') + + +class VersionError(PublicError): + """ + **903** Raised when client and server versions are incompatible. + + For example: + + >>> raise VersionError(client='2.0', server='2.1', uri='http://localhost:8888') + Traceback (most recent call last): + ... + VersionError: 2.0 client incompatible with 2.1 server at 'http://localhost:8888' + + """ + + code = 903 + + def get_format(self, _): + return _( + '%(client)s client incompatible with %(server)s server at %(uri)r' + ) + + + + ############################################################################## @@ -212,21 +282,38 @@ class InvocationError(PublicError): class CommandError(InvocationError): """ **3001** Raised when an unknown command is called. + + For example: + + >>> raise CommandError(name='foobar') + Traceback (most recent call last): + ... + CommandError: unknown command 'foobar' """ code = 3001 def get_format(self, _): - return _('Unknown command %(name)r') + return _('unknown command %(name)r') class RemoteCommandError(InvocationError): """ - **3002** Raised when client receives a `CommandError` from server. + **3002** Raised when client catches a `CommandError` from server. + + For example: + + >>> raise RemoteCommandError(name='foobar', uri='http://localhost:8888') + Traceback (most recent call last): + ... + RemoteCommandError: command 'foobar' unknown on server 'http://localhost:8888' """ code = 3002 + def get_format(self, _): + return _('command %(name)r unknown on server %(uri)r') + class ArgumentError(InvocationError): """ @@ -254,7 +341,7 @@ class RequirementError(InvocationError): class ConversionError(InvocationError): """ - **3006** Raised when a parameter value is the wrong type. + **3006** Raised when parameter value can't be converted to correct type. """ code = 3006 @@ -286,7 +373,7 @@ class ExecutionError(PublicError): class GenericError(PublicError): """ - **5000** Errors inappropriate for other categories (*5000 - 5999*). + **5000** Base class for errors that don't fit elsewhere (*5000 - 5999*). """ code = 5000 -- cgit From ff66c7ece65a042b645b2cdaef700f143727cc2f Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sun, 4 Jan 2009 18:20:39 -0700 Subject: Added more public exceptions and did some other cleanup in errors2 --- ipalib/errors2.py | 142 +++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 98 insertions(+), 44 deletions(-) (limited to 'ipalib') diff --git a/ipalib/errors2.py b/ipalib/errors2.py index 5ec97fbe..51b9a02f 100644 --- a/ipalib/errors2.py +++ b/ipalib/errors2.py @@ -27,10 +27,14 @@ to the caller. Error codes Exceptions ============= ======================================== 900 `PublicError` - 901 `InternalError` - 902 `RemoteInternalError` - 903 `VersionError` - 904 - 999 *Reserved for future use* + 901 `VersionError` + 902 `InternalError` + 903 `ServerInternalError` + 904 `CommandError` + 905 `ServerCommandError` + 906 `NetworkError` + 907 `ServerNetworkError` + 908 - 999 *Reserved for future use* 1000 - 1999 `AuthenticationError` and its subclasses 2000 - 2999 `AuthorizationError` and its subclasses 3000 - 3999 `InvocationError` and its subclasses @@ -179,9 +183,30 @@ class PublicError(StandardError): return _('') +class VersionError(PublicError): + """ + **901** Raised when client and server versions are incompatible. + + For example: + + >>> raise VersionError(cver='2.0', sver='2.1', server='https://localhost') + Traceback (most recent call last): + ... + VersionError: 2.0 client incompatible with 2.1 server at 'https://localhost' + + """ + + code = 901 + + def get_format(self, _): + return _( + '%(cver)s client incompatible with %(sver)s server at %(server)r' + ) + + class InternalError(PublicError): """ - **901** Used to conceal a non-public exception. + **902** Raised to conceal a non-public exception. For example: @@ -191,7 +216,7 @@ class InternalError(PublicError): InternalError: an internal error has occured """ - code = 901 + code = 902 def __init__(self, message=None): """ @@ -203,47 +228,96 @@ class InternalError(PublicError): return _('an internal error has occured') -class RemoteInternalError(PublicError): +class ServerInternalError(PublicError): """ - **902** Raised when client catches an `InternalError` from server. + **903** Raised when client catches an `InternalError` from server. For example: - >>> raise RemoteInternalError(uri='http://localhost:8888') + >>> raise ServerInternalError(server='https://localhost') Traceback (most recent call last): ... - RemoteInternalError: an internal error has occured on server 'http://localhost:8888' + ServerInternalError: an internal error has occured on server at 'https://localhost' """ - code = 902 + code = 903 def get_format(self, _): - return _('an internal error has occured on server %(uri)r') + return _('an internal error has occured on server at %(server)r') -class VersionError(PublicError): +class CommandError(PublicError): """ - **903** Raised when client and server versions are incompatible. + **904** Raised when an unknown command is called. For example: - >>> raise VersionError(client='2.0', server='2.1', uri='http://localhost:8888') + >>> raise CommandError(name='foobar') Traceback (most recent call last): ... - VersionError: 2.0 client incompatible with 2.1 server at 'http://localhost:8888' + CommandError: unknown command 'foobar' + """ + + code = 904 + + def get_format(self, _): + return _('unknown command %(name)r') + +class ServerCommandError(PublicError): """ + **905** Raised when client catches a `CommandError` from server. - code = 903 + For example: + + >>> e = CommandError(name='foobar') + >>> raise ServerCommandError(error=e.message, server='https://localhost') + Traceback (most recent call last): + ... + ServerCommandError: error on server 'https://localhost': unknown command 'foobar' + """ + + code = 905 def get_format(self, _): - return _( - '%(client)s client incompatible with %(server)s server at %(uri)r' - ) + return _('error on server %(server)r: %(error)s') + + +class NetworkError(PublicError): + """ + **906** Raised when a network connection cannot be created. + + For example: + + >>> raise NetworkError(uri='ldap://localhost:389') + Traceback (most recent call last): + ... + NetworkError: cannot connect to 'ldap://localhost:389' + """ + code = 906 + def get_format(self, _): + return _('cannot connect to %(uri)r') + + +class ServerNetworkError(PublicError): + """ + **907** Raised when client catches a `NetworkError` from server. + + For example: + + >>> e = NetworkError(uri='ldap://localhost:389') + >>> raise ServerNetworkError(error=e.message, server='https://localhost') + Traceback (most recent call last): + ... + ServerNetworkError: error on server 'https://localhost': cannot connect to 'ldap://localhost:389' + """ + code = 907 + def get_format(self, _): + return _('error on server %(server)r: %(error)s') ############################################################################## @@ -279,41 +353,21 @@ class InvocationError(PublicError): code = 3000 -class CommandError(InvocationError): +class EncodingError(InvocationError): """ - **3001** Raised when an unknown command is called. - - For example: - - >>> raise CommandError(name='foobar') - Traceback (most recent call last): - ... - CommandError: unknown command 'foobar' + **3001** Raised when received text is incorrectly encoded. """ code = 3001 - def get_format(self, _): - return _('unknown command %(name)r') - -class RemoteCommandError(InvocationError): +class BinaryEncodingError(InvocationError): """ - **3002** Raised when client catches a `CommandError` from server. - - For example: - - >>> raise RemoteCommandError(name='foobar', uri='http://localhost:8888') - Traceback (most recent call last): - ... - RemoteCommandError: command 'foobar' unknown on server 'http://localhost:8888' + **3002** Raised when received binary data is incorrectly encoded. """ code = 3002 - def get_format(self, _): - return _('command %(name)r unknown on server %(uri)r') - class ArgumentError(InvocationError): """ -- cgit From 6fe78a4944f11d430b724103f7d8d49c92af9b63 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sun, 4 Jan 2009 18:39:39 -0700 Subject: Renamed all references to 'ipa_server' to 'ipaserver' --- ipalib/__init__.py | 8 ++++---- ipalib/cli.py | 4 ++-- ipalib/plugable.py | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) (limited to 'ipalib') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index e30b7fed..e1ef09c1 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -31,7 +31,7 @@ heavily cross-referenced with further documentation that (hopefully) fills in the missing details. In addition to this tutorial, the many built-in plugins in `ipalib.plugins` -and `ipa_server.plugins` provide real-life examples of how to write good +and `ipaserver.plugins` provide real-life examples of how to write good plugins. @@ -227,12 +227,12 @@ There are two types of plugins: 2. *Backend plugins* - These are only loaded in a *server* context and only need to be installed on the IPA server. The built-in backend - plugins can be found in `ipa_server.plugins`. + plugins can be found in `ipaserver.plugins`. Backend plugins should provide a set of methods that standardize how IPA interacts with some external system or library. For example, all interaction with LDAP is done through the ``ldap`` backend plugin defined in -`ipa_server.plugins.b_ldap`. As a good rule of thumb, anytime you need to +`ipaserver.plugins.b_ldap`. As a good rule of thumb, anytime you need to import some package that is not part of the Python standard library, you should probably interact with that package via a corresponding backend plugin you implement. @@ -824,7 +824,7 @@ To learn more about writing freeIPA plugins, you should: 1. Look at some of the built-in plugins, like the frontend plugins in `ipalib.plugins.f_user` and the backend plugins in - `ipa_server.plugins.b_ldap`. + `ipaserver.plugins.b_ldap`. 2. Learn about the base classes for frontend plugins in `ipalib.frontend`. diff --git a/ipalib/cli.py b/ipalib/cli.py index f4cdbbf5..442e5061 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -763,8 +763,8 @@ class CLI(object): # try: # import krbV # import ldap -# from ipa_server import conn -# from ipa_server.servercore import context +# from ipaserver import conn +# from ipaserver.servercore import context # krbccache = krbV.default_context().default_ccache().name # context.conn = conn.IPAConn(self.api.env.ldaphost, self.api.env.ldapport, krbccache) # except ImportError: diff --git a/ipalib/plugable.py b/ipalib/plugable.py index d4ec8749..b52db900 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -639,7 +639,7 @@ class API(DictProxy): return util.import_plugins_subpackage('ipalib') if self.env.in_server: - util.import_plugins_subpackage('ipa_server') + util.import_plugins_subpackage('ipaserver') def finalize(self): """ -- cgit From c121d0064bb7a7bd1a289ae29ceb2dee314c2d2f Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 5 Jan 2009 01:20:14 -0700 Subject: New Param: Added Param.get_label() method for a way to retrieve translated message at request time --- ipalib/parameter.py | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'ipalib') diff --git a/ipalib/parameter.py b/ipalib/parameter.py index 55a9bc6d..93a9a693 100644 --- a/ipalib/parameter.py +++ b/ipalib/parameter.py @@ -23,6 +23,7 @@ Parameter system for command plugins. from types import NoneType from util import make_repr +from request import ugettext from plugable import ReadOnly, lock, check_name from constants import NULLS, TYPE_ERROR, CALLABLE_ERROR @@ -199,6 +200,7 @@ class Param(ReadOnly): kwargs = ( ('cli_name', str, None), + ('label', callable, None), ('doc', str, ''), ('required', bool, True), ('multivalue', bool, False), @@ -301,6 +303,14 @@ class Param(ReadOnly): **self.__kw ) + def get_label(self): + """ + Return translated label using `request.ugettext`. + """ + if self.label is None: + return self.cli_name.decode('UTF-8') + return self.label(ugettext) + def normalize(self, value): """ Normalize ``value`` using normalizer callback. -- cgit From 6d6c0d81ddbfc56672f0595a5f631c5e846d8b2b Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 5 Jan 2009 02:20:09 -0700 Subject: New Param: decided on calling signature for rules; added unit tests for Bytes._rule_minlength, _rule_maxlength, and _rule_length --- ipalib/parameter.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) (limited to 'ipalib') diff --git a/ipalib/parameter.py b/ipalib/parameter.py index 93a9a693..357e3441 100644 --- a/ipalib/parameter.py +++ b/ipalib/parameter.py @@ -500,30 +500,36 @@ class Bytes(Param): self.nice, self.minlength) ) - def _rule_minlength(self, value): + def _rule_minlength(self, _, name, value): """ Check minlength constraint. """ + assert type(value) is str if len(value) < self.minlength: - return 'Must be at least %(minlength)d bytes long.' % dict( + return _('%(name)s must be at least %(minlength)d bytes') % dict( + name=name, minlength=self.minlength, ) - def _rule_maxlength(self, value): + def _rule_maxlength(self, _, name, value): """ Check maxlength constraint. """ + assert type(value) is str if len(value) > self.maxlength: - return 'Can be at most %(maxlength)d bytes long.' % dict( + return _('%(name)s can be at most %(maxlength)d bytes') % dict( + name=name, maxlength=self.maxlength, ) - def _rule_length(self, value): + def _rule_length(self, _, name, value): """ Check length constraint. """ + assert type(value) is str if len(value) != self.length: - return 'Must be exactly %(length)d bytes long.' % dict( + return _('%(name)s must be exactly %(length)d bytes') % dict( + name=name, length=self.length, ) -- cgit From 690ad4766d9265026d46fcc50118f341776f81b0 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 5 Jan 2009 02:45:07 -0700 Subject: New Param: added Str length rule methods; added corresponding unit tests --- ipalib/parameter.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) (limited to 'ipalib') diff --git a/ipalib/parameter.py b/ipalib/parameter.py index 357e3441..204fda66 100644 --- a/ipalib/parameter.py +++ b/ipalib/parameter.py @@ -556,3 +556,36 @@ class Str(Bytes): raise TypeError( 'Can only implicitly convert int, float, or bool; got %r' % value ) + + def _rule_minlength(self, _, name, value): + """ + Check minlength constraint. + """ + assert type(value) is unicode + if len(value) < self.minlength: + return _('%(name)s must be at least %(minlength)d characters') % dict( + name=name, + minlength=self.minlength, + ) + + def _rule_maxlength(self, _, name, value): + """ + Check maxlength constraint. + """ + assert type(value) is unicode + if len(value) > self.maxlength: + return _('%(name)s can be at most %(maxlength)d characters') % dict( + name=name, + maxlength=self.maxlength, + ) + + def _rule_length(self, _, name, value): + """ + Check length constraint. + """ + assert type(value) is unicode + if len(value) != self.length: + return _('%(name)s must be exactly %(length)d characters') % dict( + name=name, + length=self.length, + ) -- cgit From 4a24b49d5d1df0558242c4a8e7e3f9333fc007db Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 5 Jan 2009 03:28:27 -0700 Subject: A few docstring improvements in Env --- ipalib/config.py | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) (limited to 'ipalib') diff --git a/ipalib/config.py b/ipalib/config.py index 04f1442f..3544331d 100644 --- a/ipalib/config.py +++ b/ipalib/config.py @@ -98,8 +98,8 @@ class Env(object): Note that the automatic type conversion is case sensitive. For example: - >>> env.false = 'false' # Not equal to repr(False)! - >>> env.false + >>> env.not_false = 'false' # Not equal to repr(False)! + >>> env.not_false 'false' If an ``str`` value looks like an integer, it's automatically converted to @@ -122,8 +122,8 @@ class Env(object): >>> env.number = ' 42 ' # Still converted to an int >>> env.number 42 - >>> env.actually_false = ' False ' # Still equal to repr(False) - >>> env.actually_false + >>> env.false = ' False ' # Still equal to repr(False) + >>> env.false False Also, empty ``str`` instances are converted to ``None``. For example: @@ -181,17 +181,18 @@ class Env(object): 2. `Env._finalize_core()` - merge-in variables from the configuration files and then merge-in variables from the internal defaults, after which at least all the standard variables will be set. After this - method is called, the plugins will be loaded, during which 3rd-party - plugins can set additional variables they may need. + method is called, the plugins will be loaded, during which + third-party plugins can merge-in defaults for additional variables + they use (likely using the `Env._merge()` method). 3. `Env._finalize()` - one last chance to merge-in variables and then the instance is locked. After this method is called, no more environment variables can be set during the remaining life of the process. - However, normally none of the above methods are called directly and instead - only `plugable.API.bootstrap()` is called, which itself takes care of - correctly calling the `Env` bootstrapping methods. + However, normally none of these three bootstraping methods are called + directly and instead only `plugable.API.bootstrap()` is called, which itself + takes care of correctly calling the `Env` bootstrapping methods. """ __locked = False @@ -267,7 +268,16 @@ class Env(object): def __delattr__(self, name): """ - Raise AttributeError (deletion is never allowed). + Raise an ``AttributeError`` (deletion is never allowed). + + For example: + + >>> env = Env() + >>> env.name = 'A value' + >>> del env.name + Traceback (most recent call last): + ... + AttributeError: locked: cannot delete Env.name """ raise AttributeError( DEL_ERROR % (self.__class__.__name__, name) -- cgit From 2462135da0f230b9795755fbf7e9bd917d13acf3 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 5 Jan 2009 12:41:02 -0700 Subject: Added a few missing things to base.ReadOnly docstrings --- ipalib/base.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) (limited to 'ipalib') diff --git a/ipalib/base.py b/ipalib/base.py index d8394874..bff8f195 100644 --- a/ipalib/base.py +++ b/ipalib/base.py @@ -18,7 +18,7 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ -Core functions and classes. +Foundational classes and functions. """ import re @@ -54,7 +54,11 @@ class ReadOnly(object): 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): ... @@ -79,7 +83,20 @@ class ReadOnly(object): False But again, the point is that a programmer would never employ the above - techniques accidentally. + 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 -- cgit From f130da56c38bafb3c05c2273fbf01e148ddb2d4a Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 6 Jan 2009 11:15:41 -0700 Subject: Additional work on the new error code tree in errors2.py --- ipalib/errors2.py | 101 +++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 89 insertions(+), 12 deletions(-) (limited to 'ipalib') diff --git a/ipalib/errors2.py b/ipalib/errors2.py index 51b9a02f..8ae2d440 100644 --- a/ipalib/errors2.py +++ b/ipalib/errors2.py @@ -18,29 +18,82 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ -Custom exception classes. +Custom exception classes (some which are RPC transparent). -Certain errors can be returned in RPC response to relay some error condition -to the caller. +`PrivateError` and its subclasses are custom IPA excetions that will *never* be +forwarded in a Remote Procedure Call (RPC) response. + +On the other hand, `PublicError` and its subclasses can be forwarded in an RPC +response. These public errors each carry a unique integer error code as well as +a gettext translated error message (translated a the time the exception is +raised). The purpose of the public errors is to relay information about +*expected* user errors, service availability errors, and so on. They should +*never* be used for *unexpected* programmatic or run-time errors. + +For security reasons it is *extremely* important that arbitrary exceptions *not* +be forwarded in an RPC response. Unexpected exceptions can easily contain +compromising information in their error messages. Any time the server catches +any exception that isn't a `PublicError` subclass, it should raise an +`InternalError`, which itself always has the same, static error message (and +therefore cannot be populated with information about the true exception). + +The public errors are arranging into five main blocks of error code ranges: ============= ======================================== Error codes Exceptions ============= ======================================== - 900 `PublicError` - 901 `VersionError` - 902 `InternalError` - 903 `ServerInternalError` - 904 `CommandError` - 905 `ServerCommandError` - 906 `NetworkError` - 907 `ServerNetworkError` - 908 - 999 *Reserved for future use* 1000 - 1999 `AuthenticationError` and its subclasses 2000 - 2999 `AuthorizationError` and its subclasses 3000 - 3999 `InvocationError` and its subclasses 4000 - 4999 `ExecutionError` and its subclasses 5000 - 5999 `GenericError` and its subclasses ============= ======================================== + +Within these five blocks some sub-ranges are already allocated for certain types +of error messages, while others are reserved for future use. Here are the +current block assignments: + + - **900-5999** `PublicError` and its subclasses + + - **901 - 907** Assigned to special top-level public errors + + - **908 - 999** *Reserved for future use* + + - **1000 - 1999** `AuthenticationError` and its subclasses + + - **1001 - 1099** Open for general authentication errors + + - **1100 - 1199** `KerberosError` and its subclasses + + - **1200 - 1999** *Reserved for future use* + + - **2000 - 2999** `AuthorizationError` and its subclasses + + - **2001 - 2099** Open for general authorization errors + + - **2100 - 2199** `ACIError` and its subclasses + + - **2200 - 2999** *Reserved for future use* + + - **3000 - 3999** `InvocationError` and its subclasses + + - **3001 - 3099** Open for general invocation errors + + - **3100 - 3199** *Reserved for future use* + + - **4000 - 4999** `ExecutionError` and its subclasses + + - **4001 - 4099** Open for general execution errors + + - **4100 - 4299** `LDAPError` and its subclasses + + - **4300 - 4999** *Reserved for future use* + + - **5000 - 5999** `GenericError` and its subclasses + + - **5001 - 5099** Open for generic errors + + - **5100 - 5999** *Reserved for future use* """ from inspect import isclass @@ -330,6 +383,14 @@ class AuthenticationError(PublicError): code = 1000 +class KerberosError(AuthenticationError): + """ + **1100** Base class for Kerberos authorization errors (*1100 - 1199*). + """ + + code = 1100 + + ############################################################################## # 2000 - 2999: Authorization errors @@ -341,6 +402,14 @@ class AuthorizationError(PublicError): code = 2000 +class ACIError(AuthorizationError): + """ + **2100** Base class for ACI authorization errors (*2100 - 2199*). + """ + + code = 2100 + + ############################################################################## # 3000 - 3999: Invocation errors @@ -421,6 +490,14 @@ class ExecutionError(PublicError): code = 4000 +class LDAPError(ExecutionError): + """ + **4100** Base class for LDAP execution errors (*4100 - 4299*). + """ + + code = 4100 + + ############################################################################## # 5000 - 5999: Generic errors -- cgit From ac89267c2f701e56798de0160abd8f81b190c8ef Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 6 Jan 2009 11:54:58 -0700 Subject: Fixed type in KerberosError, droped LDAPError range to just 100 codes for now --- ipalib/errors2.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'ipalib') diff --git a/ipalib/errors2.py b/ipalib/errors2.py index 8ae2d440..58568244 100644 --- a/ipalib/errors2.py +++ b/ipalib/errors2.py @@ -85,7 +85,7 @@ current block assignments: - **4001 - 4099** Open for general execution errors - - **4100 - 4299** `LDAPError` and its subclasses + - **4100 - 4199** `LDAPError` and its subclasses - **4300 - 4999** *Reserved for future use* @@ -385,7 +385,7 @@ class AuthenticationError(PublicError): class KerberosError(AuthenticationError): """ - **1100** Base class for Kerberos authorization errors (*1100 - 1199*). + **1100** Base class for Kerberos authentication errors (*1100 - 1199*). """ code = 1100 @@ -484,7 +484,7 @@ class ValidationError(InvocationError): class ExecutionError(PublicError): """ - **4000** Base class for execution/operation errors (*4000 - 4999*). + **4000** Base class for execution errors (*4000 - 4999*). """ code = 4000 @@ -492,7 +492,7 @@ class ExecutionError(PublicError): class LDAPError(ExecutionError): """ - **4100** Base class for LDAP execution errors (*4100 - 4299*). + **4100** Base class for LDAP execution errors (*4100 - 4199*). """ code = 4100 -- cgit From 9e430755a5cbcd27e2312d5bee1061704a7215bf Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 6 Jan 2009 13:33:22 -0700 Subject: Renamed PublicError.code attribute to PublicError.errno --- ipalib/errors2.py | 50 +++++++++++++++++++++++++------------------------- 1 file changed, 25 insertions(+), 25 deletions(-) (limited to 'ipalib') diff --git a/ipalib/errors2.py b/ipalib/errors2.py index 58568244..660bb1bd 100644 --- a/ipalib/errors2.py +++ b/ipalib/errors2.py @@ -25,7 +25,7 @@ forwarded in a Remote Procedure Call (RPC) response. On the other hand, `PublicError` and its subclasses can be forwarded in an RPC response. These public errors each carry a unique integer error code as well as -a gettext translated error message (translated a the time the exception is +a gettext translated error message (translated at the time the exception is raised). The purpose of the public errors is to relay information about *expected* user errors, service availability errors, and so on. They should *never* be used for *unexpected* programmatic or run-time errors. @@ -214,7 +214,7 @@ class PublicError(StandardError): **900** Base class for exceptions that can be forwarded in an RPC response. """ - code = 900 + errno = 900 def __init__(self, message=None, **kw): if message is None: @@ -249,7 +249,7 @@ class VersionError(PublicError): """ - code = 901 + errno = 901 def get_format(self, _): return _( @@ -269,7 +269,7 @@ class InternalError(PublicError): InternalError: an internal error has occured """ - code = 902 + errno = 902 def __init__(self, message=None): """ @@ -293,7 +293,7 @@ class ServerInternalError(PublicError): ServerInternalError: an internal error has occured on server at 'https://localhost' """ - code = 903 + errno = 903 def get_format(self, _): return _('an internal error has occured on server at %(server)r') @@ -311,7 +311,7 @@ class CommandError(PublicError): CommandError: unknown command 'foobar' """ - code = 904 + errno = 904 def get_format(self, _): return _('unknown command %(name)r') @@ -330,7 +330,7 @@ class ServerCommandError(PublicError): ServerCommandError: error on server 'https://localhost': unknown command 'foobar' """ - code = 905 + errno = 905 def get_format(self, _): return _('error on server %(server)r: %(error)s') @@ -348,7 +348,7 @@ class NetworkError(PublicError): NetworkError: cannot connect to 'ldap://localhost:389' """ - code = 906 + errno = 906 def get_format(self, _): return _('cannot connect to %(uri)r') @@ -367,7 +367,7 @@ class ServerNetworkError(PublicError): ServerNetworkError: error on server 'https://localhost': cannot connect to 'ldap://localhost:389' """ - code = 907 + errno = 907 def get_format(self, _): return _('error on server %(server)r: %(error)s') @@ -380,7 +380,7 @@ class AuthenticationError(PublicError): **1000** Base class for authentication errors (*1000 - 1999*). """ - code = 1000 + errno = 1000 class KerberosError(AuthenticationError): @@ -388,7 +388,7 @@ class KerberosError(AuthenticationError): **1100** Base class for Kerberos authentication errors (*1100 - 1199*). """ - code = 1100 + errno = 1100 @@ -399,7 +399,7 @@ class AuthorizationError(PublicError): **2000** Base class for authorization errors (*2000 - 2999*). """ - code = 2000 + errno = 2000 class ACIError(AuthorizationError): @@ -407,7 +407,7 @@ class ACIError(AuthorizationError): **2100** Base class for ACI authorization errors (*2100 - 2199*). """ - code = 2100 + errno = 2100 @@ -419,7 +419,7 @@ class InvocationError(PublicError): **3000** Base class for command invocation errors (*3000 - 3999*). """ - code = 3000 + errno = 3000 class EncodingError(InvocationError): @@ -427,7 +427,7 @@ class EncodingError(InvocationError): **3001** Raised when received text is incorrectly encoded. """ - code = 3001 + errno = 3001 class BinaryEncodingError(InvocationError): @@ -435,7 +435,7 @@ class BinaryEncodingError(InvocationError): **3002** Raised when received binary data is incorrectly encoded. """ - code = 3002 + errno = 3002 class ArgumentError(InvocationError): @@ -443,7 +443,7 @@ class ArgumentError(InvocationError): **3003** Raised when a command is called with wrong number of arguments. """ - code = 3003 + errno = 3003 class OptionError(InvocationError): @@ -451,7 +451,7 @@ class OptionError(InvocationError): **3004** Raised when a command is called with unknown options. """ - code = 3004 + errno = 3004 class RequirementError(InvocationError): @@ -459,7 +459,7 @@ class RequirementError(InvocationError): **3005** Raised when a required parameter is not provided. """ - code = 3005 + errno = 3005 class ConversionError(InvocationError): @@ -467,7 +467,7 @@ class ConversionError(InvocationError): **3006** Raised when parameter value can't be converted to correct type. """ - code = 3006 + errno = 3006 class ValidationError(InvocationError): @@ -475,7 +475,7 @@ class ValidationError(InvocationError): **3007** Raised when a parameter value fails a validation rule. """ - code = 3007 + errno = 3007 @@ -487,7 +487,7 @@ class ExecutionError(PublicError): **4000** Base class for execution errors (*4000 - 4999*). """ - code = 4000 + errno = 4000 class LDAPError(ExecutionError): @@ -495,7 +495,7 @@ class LDAPError(ExecutionError): **4100** Base class for LDAP execution errors (*4100 - 4199*). """ - code = 4100 + errno = 4100 @@ -507,7 +507,7 @@ class GenericError(PublicError): **5000** Base class for errors that don't fit elsewhere (*5000 - 5999*). """ - code = 5000 + errno = 5000 @@ -522,7 +522,7 @@ def __errors_iter(): yield value public_errors = tuple( - sorted(__errors_iter(), key=lambda E: E.code) + sorted(__errors_iter(), key=lambda E: E.errno) ) if __name__ == '__main__': -- cgit From 3e9eb0bda000ef138ff04c677aa9014186f547d3 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 8 Jan 2009 00:07:18 -0700 Subject: Changed PublicError so str(e) is untranslated (for logging) and added format=None kwarg for generic use --- ipalib/errors2.py | 80 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 44 insertions(+), 36 deletions(-) (limited to 'ipalib') diff --git a/ipalib/errors2.py b/ipalib/errors2.py index 660bb1bd..b052882d 100644 --- a/ipalib/errors2.py +++ b/ipalib/errors2.py @@ -209,31 +209,53 @@ class PluginMissingOverrideError(PrivateError): ############################################################################## # Public errors: + +__messages = [] + +def _(message): + __messages.append(message) + return message + + class PublicError(StandardError): """ **900** Base class for exceptions that can be forwarded in an RPC response. """ errno = 900 - - def __init__(self, message=None, **kw): - if message is None: - message = self.get_format(ugettext) % kw - assert type(message) is unicode - elif type(message) is not unicode: - raise TypeError( - TYPE_ERROR % ('message', unicode, message, type(message)) + format = None + + def __init__(self, format=None, message=None, **kw): + name = self.__class__.__name__ + if self.format is not None and format is not None: + raise ValueError( + 'non-generic %r needs format=None; got format=%r' % ( + name, format) ) - self.message = message + if message is None: + if self.format is None: + if format is None: + raise ValueError( + '%s.format is None yet format=None, message=None' % name + ) + self.format = format + self.forwarded = False + self.message = self.format % kw + self.strerror = ugettext(self.format) % kw + else: + if type(message) is not unicode: + raise TypeError( + TYPE_ERROR % ('message', unicode, message, type(message)) + ) + self.forwarded = True + self.message = message + self.strerror = message for (key, value) in kw.iteritems(): assert not hasattr(self, key), 'conflicting kwarg %s.%s = %r' % ( - self.__class__.__name__, key, value, + name, key, value, ) setattr(self, key, value) - StandardError.__init__(self, message) - - def get_format(self, _): - return _('') + StandardError.__init__(self, self.message) class VersionError(PublicError): @@ -250,11 +272,8 @@ class VersionError(PublicError): """ errno = 901 + format = _('%(cver)s client incompatible with %(sver)s server at %(server)r') - def get_format(self, _): - return _( - '%(cver)s client incompatible with %(sver)s server at %(server)r' - ) class InternalError(PublicError): @@ -270,15 +289,13 @@ class InternalError(PublicError): """ errno = 902 + format = _('an internal error has occured') def __init__(self, message=None): """ Security issue: ignore any information given to constructor. """ - PublicError.__init__(self, self.get_format(ugettext)) - - def get_format(self, _): - return _('an internal error has occured') + PublicError.__init__(self) class ServerInternalError(PublicError): @@ -294,9 +311,7 @@ class ServerInternalError(PublicError): """ errno = 903 - - def get_format(self, _): - return _('an internal error has occured on server at %(server)r') + format = _('an internal error has occured on server at %(server)r') class CommandError(PublicError): @@ -312,9 +327,7 @@ class CommandError(PublicError): """ errno = 904 - - def get_format(self, _): - return _('unknown command %(name)r') + format = _('unknown command %(name)r') class ServerCommandError(PublicError): @@ -331,9 +344,7 @@ class ServerCommandError(PublicError): """ errno = 905 - - def get_format(self, _): - return _('error on server %(server)r: %(error)s') + format = _('error on server %(server)r: %(error)s') class NetworkError(PublicError): @@ -349,9 +360,7 @@ class NetworkError(PublicError): """ errno = 906 - - def get_format(self, _): - return _('cannot connect to %(uri)r') + format = _('cannot connect to %(uri)r') class ServerNetworkError(PublicError): @@ -368,9 +377,8 @@ class ServerNetworkError(PublicError): """ errno = 907 + format = _('error on server %(server)r: %(error)s') - def get_format(self, _): - return _('error on server %(server)r: %(error)s') ############################################################################## -- cgit From 5c7c0b35bb2484efad2a8776b42fbf4066618706 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 12 Jan 2009 16:14:46 -0700 Subject: New Param: added Param.validate() and Param._validate_scalar() methods; added corresponding unit tests --- ipalib/errors2.py | 16 ++++++++++++++++ ipalib/parameter.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) (limited to 'ipalib') diff --git a/ipalib/errors2.py b/ipalib/errors2.py index b052882d..81b1fb2e 100644 --- a/ipalib/errors2.py +++ b/ipalib/errors2.py @@ -465,9 +465,17 @@ class OptionError(InvocationError): class RequirementError(InvocationError): """ **3005** Raised when a required parameter is not provided. + + For example: + + >>> raise RequirementError(name='givenname') + Traceback (most recent call last): + ... + RequirementError: 'givenname' is required """ errno = 3005 + format = _('%(name)r is required') class ConversionError(InvocationError): @@ -481,9 +489,17 @@ class ConversionError(InvocationError): class ValidationError(InvocationError): """ **3007** Raised when a parameter value fails a validation rule. + + For example: + + >>> raise ValidationError(name='sn', error='can be at most 128 characters') + Traceback (most recent call last): + ... + ValidationError: invalid 'sn': can be at most 128 characters """ errno = 3007 + format = _('invalid %(name)r: %(error)s') diff --git a/ipalib/parameter.py b/ipalib/parameter.py index 204fda66..0890160a 100644 --- a/ipalib/parameter.py +++ b/ipalib/parameter.py @@ -25,6 +25,7 @@ from types import NoneType from util import make_repr from request import ugettext from plugable import ReadOnly, lock, check_name +from errors2 import RequirementError, ValidationError from constants import NULLS, TYPE_ERROR, CALLABLE_ERROR @@ -207,6 +208,7 @@ class Param(ReadOnly): ('primary_key', bool, False), ('normalizer', callable, None), ('default_from', callable, None), + ('create_default', callable, None), ('flags', frozenset, frozenset()), # The 'default' kwarg gets appended in Param.__init__(): @@ -432,6 +434,39 @@ class Param(ReadOnly): :param value: A proposed value for this parameter. """ + if value is None: + if self.required: + raise RequirementError(name=self.name) + return + if self.multivalue: + if type(value) is not tuple: + raise TypeError( + TYPE_ERROR % ('value', tuple, value, type(value)) + ) + if len(value) < 1: + raise ValueError('value: empty tuple must be converted to None') + for (i, v) in enumerate(value): + self._validate_scalar(v, i) + else: + self._validate_scalar(value) + + def _validate_scalar(self, value, index=None): + if type(value) is not self.type: + if index is None: + name = 'value' + else: + name = 'value[%d]' % index + raise TypeError( + TYPE_ERROR % (name, self.type, value, type(value)) + ) + if index is not None and type(index) is not int: + raise TypeError( + TYPE_ERROR % ('index', int, index, type(index)) + ) + for rule in self.all_rules: + error = rule(ugettext, value) + if error is not None: + raise ValidationError(name=self.name, error=error, index=index) class Bool(Param): -- cgit From 11dce19225b2ebcf41adb9d06b402610226ab047 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 12 Jan 2009 22:48:04 -0700 Subject: New Param: added Param.get_default() method and detailed docstring; added corresponding unit tests --- ipalib/parameter.py | 136 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 132 insertions(+), 4 deletions(-) (limited to 'ipalib') diff --git a/ipalib/parameter.py b/ipalib/parameter.py index 0890160a..b5adf58f 100644 --- a/ipalib/parameter.py +++ b/ipalib/parameter.py @@ -207,7 +207,7 @@ class Param(ReadOnly): ('multivalue', bool, False), ('primary_key', bool, False), ('normalizer', callable, None), - ('default_from', callable, None), + ('default_from', DefaultFrom, None), ('create_default', callable, None), ('flags', frozenset, frozenset()), @@ -282,6 +282,20 @@ class Param(ReadOnly): class_rules.append(getattr(self, rule_name)) check_name(self.cli_name) + # Check that only default_from or create_default was provided: + assert not hasattr(self, '_get_default'), self.nice + if callable(self.default_from): + if callable(self.create_default): + raise ValueError( + '%s: cannot have both %r and %r' % ( + self.nice, 'default_from', 'create_default') + ) + self._get_default = self.default_from + elif callable(self.create_default): + self._get_default = self.create_default + else: + self._get_default = None + # Check that all the rules are callable self.class_rules = tuple(class_rules) self.rules = rules @@ -468,6 +482,117 @@ class Param(ReadOnly): if error is not None: raise ValidationError(name=self.name, error=error, index=index) + def get_default(self, **kw): + """ + Return the static default or construct and return a dynamic default. + + (In these examples, we will use the `Str` and `Bytes` classes, which + both subclass from `Param`.) + + The *default* static default is ``None``. For example: + + >>> s = Str('my_str') + >>> s.default is None + True + >>> s.get_default() is None + True + + However, you can provide your own static default via the ``default`` + keyword argument when you create your `Param` instance. For example: + + >>> s = Str('my_str', default=u'My Static Default') + >>> s.default + u'My Static Default' + >>> s.get_default() + u'My Static Default' + + If you need to generate a dynamic default from other supplied parameter + values, provide a callback via the ``default_from`` keyword argument. + This callback will be automatically wrapped in a `DefaultFrom` instance + if it isn't one already (see the `DefaultFrom` class for all the gory + details). For example: + + >>> login = Str('login', default=u'my-static-login-default', + ... default_from=lambda first, last: (first[0] + last).lower(), + ... ) + >>> isinstance(login.default_from, DefaultFrom) + True + >>> login.default_from.keys + ('first', 'last') + + Then when all the keys needed by the `DefaultFrom` instance are present, + the dynamic default is constructed and returned. For example: + + >>> kw = dict(last=u'Doe', first=u'John') + >>> login.get_default(**kw) + u'jdoe' + + Or if any keys are missing, your *static* default is returned. + For example: + + >>> kw = dict(first=u'John', department=u'Engineering') + >>> login.get_default(**kw) + u'my-static-login-default' + + The second, less common way to construct a dynamic default is to provide + a callback via the ``create_default`` keyword argument. Unlike a + ``default_from`` callback, your ``create_default`` callback will not get + wrapped in any dispatcher. Instead, it will be called directly, which + means your callback must accept arbitrary keyword arguments, although + whether your callback utilises these values is up to your + implementation. For example: + + >>> def make_csr(**kw): + ... print ' make_csr(%r)' % (kw,) # Note output below + ... return 'Certificate Signing Request' + ... + >>> csr = Bytes('csr', create_default=make_csr) + + Your ``create_default`` callback will be called with whatever keyword + arguments are passed to `Param.get_default()`. For example: + + >>> kw = dict(arbitrary='Keyword', arguments='Here') + >>> csr.get_default(**kw) + make_csr({'arguments': 'Here', 'arbitrary': 'Keyword'}) + 'Certificate Signing Request' + + And your ``create_default`` callback is called even if + `Param.get_default()` is called with *zero* keyword arguments. + For example: + + >>> csr.get_default() + make_csr({}) + 'Certificate Signing Request' + + The ``create_default`` callback will most likely be used as a + pre-execute hook to perform some special client-side operation. For + example, the ``csr`` parameter above might make a call to + ``/usr/bin/openssl``. However, often a ``create_default`` callback + could also be implemented as a ``default_from`` callback. When this is + the case, a ``default_from`` callback should be used as they are more + structured and therefore less error-prone. + + The ``default_from`` and ``create_default`` keyword arguments are + mutually exclusive. If you provide both, a ``ValueError`` will be + raised. For example: + + >>> homedir = Str('home', + ... default_from=lambda login: '/home/%s' % login, + ... create_default=lambda **kw: '/lets/use/this', + ... ) + Traceback (most recent call last): + ... + ValueError: Str('home'): cannot have both 'default_from' and 'create_default' + """ + if self._get_default is not None: + default = self._get_default(**kw) + if default is not None: + try: + return self.convert(self.normalize(default)) + except StandardError: + pass + return self.default + class Bool(Param): """ @@ -535,6 +660,12 @@ class Bytes(Param): self.nice, self.minlength) ) + def _convert_scalar(self, value, index=None): + """ + Implement in subclass. + """ + return value + def _rule_minlength(self, _, name, value): """ Check minlength constraint. @@ -582,9 +713,6 @@ class Str(Bytes): ('pattern', unicode, None), ) - def __init__(self, name, **kw): - super(Str, self).__init__(name, **kw) - def _convert_scalar(self, value, index=None): if type(value) in (self.type, int, float, bool): return self.type(value) -- cgit From 33db9fee6017c0777f4fc5da8b020aefd714e387 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 13 Jan 2009 00:27:06 -0700 Subject: New Param: ported create_param() function and unit tests --- ipalib/parameter.py | 52 +++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 3 deletions(-) (limited to 'ipalib') diff --git a/ipalib/parameter.py b/ipalib/parameter.py index b5adf58f..1c88d286 100644 --- a/ipalib/parameter.py +++ b/ipalib/parameter.py @@ -142,10 +142,10 @@ class DefaultFrom(ReadOnly): def parse_param_spec(spec): """ - Parse a param spec into to (name, kw). + Parse shorthand ``spec`` into to ``(name, kw)``. - The ``spec`` string determines the param name, whether the param is - required, and whether the param is multivalue according the following + The ``spec`` string determines the parameter name, whether the parameter is + required, and whether the parameter is multivalue according the following syntax: ====== ===== ======== ========== @@ -605,12 +605,16 @@ class Int(Param): """ + type = int + class Float(Param): """ """ + type = float + class Bytes(Param): """ @@ -752,3 +756,45 @@ class Str(Bytes): name=name, length=self.length, ) + + +def create_param(spec): + """ + Create an `Str` instance from the shorthand ``spec``. + + This function allows you to create `Str` parameters (the most common) from + a convenient shorthand that defines the parameter name, whether it is + required, and whether it is multivalue. (For a definition shorthand + syntax, see the `parse_param_spec()` function.) + + If ``spec`` is an ``str`` instance, it will be used to create a new `Str` + parameter, which will be returned. For example: + + >>> s = create_param('hometown?') + >>> s + Str('hometown?') + >>> (s.name, s.required, s.multivalue) + ('hometown', False, False) + + On the other hand, if ``spec`` is already a `Param` instance, it is + returned unchanged. For example: + + >>> b = Bytes('cert') + >>> create_param(b) is b + True + + As a plugin author, you will not call this function directly (which would + be no more convenient than simply creating the `Str` instance). Instead, + `frontend.Command` will call it for you when it evaluates the + ``takes_args`` and ``takes_options`` attributes, and `frontend.Object` + will call it for you when it evaluates the ``takes_params`` attribute. + + :param spec: A spec string or a `Param` instance. + """ + if isinstance(spec, Param): + return spec + if type(spec) is not str: + raise TypeError( + TYPE_ERROR % ('spec', (str, Param), spec, type(spec)) + ) + return Str(spec) -- cgit From c2b0d03f82f16debcc55d34ac44197e0bc97e0e8 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 13 Jan 2009 01:07:33 -0700 Subject: New Param: updated Bytes and Str length rules to use new rule(_, value) calling signature; updated corresponding unit tests --- ipalib/parameter.py | 36 ++++++++++++++---------------------- 1 file changed, 14 insertions(+), 22 deletions(-) (limited to 'ipalib') diff --git a/ipalib/parameter.py b/ipalib/parameter.py index 1c88d286..dbf75298 100644 --- a/ipalib/parameter.py +++ b/ipalib/parameter.py @@ -618,7 +618,7 @@ class Float(Param): class Bytes(Param): """ - + A parameter for binary data. """ type = str @@ -670,45 +670,40 @@ class Bytes(Param): """ return value - def _rule_minlength(self, _, name, value): + def _rule_minlength(self, _, value): """ Check minlength constraint. """ assert type(value) is str if len(value) < self.minlength: - return _('%(name)s must be at least %(minlength)d bytes') % dict( - name=name, + return _('must be at least %(minlength)d bytes') % dict( minlength=self.minlength, ) - def _rule_maxlength(self, _, name, value): + def _rule_maxlength(self, _, value): """ Check maxlength constraint. """ assert type(value) is str if len(value) > self.maxlength: - return _('%(name)s can be at most %(maxlength)d bytes') % dict( - name=name, + return _('can be at most %(maxlength)d bytes') % dict( maxlength=self.maxlength, ) - def _rule_length(self, _, name, value): + def _rule_length(self, _, value): """ Check length constraint. """ assert type(value) is str if len(value) != self.length: - return _('%(name)s must be exactly %(length)d bytes') % dict( - name=name, + return _('must be exactly %(length)d bytes') % dict( length=self.length, ) - - class Str(Bytes): """ - + A parameter for character (textual) data. """ type = unicode @@ -724,36 +719,33 @@ class Str(Bytes): 'Can only implicitly convert int, float, or bool; got %r' % value ) - def _rule_minlength(self, _, name, value): + def _rule_minlength(self, _, value): """ Check minlength constraint. """ assert type(value) is unicode if len(value) < self.minlength: - return _('%(name)s must be at least %(minlength)d characters') % dict( - name=name, + return _('must be at least %(minlength)d characters') % dict( minlength=self.minlength, ) - def _rule_maxlength(self, _, name, value): + def _rule_maxlength(self, _, value): """ Check maxlength constraint. """ assert type(value) is unicode if len(value) > self.maxlength: - return _('%(name)s can be at most %(maxlength)d characters') % dict( - name=name, + return _('can be at most %(maxlength)d characters') % dict( maxlength=self.maxlength, ) - def _rule_length(self, _, name, value): + def _rule_length(self, _, value): """ Check length constraint. """ assert type(value) is unicode if len(value) != self.length: - return _('%(name)s must be exactly %(length)d characters') % dict( - name=name, + return _('must be exactly %(length)d characters') % dict( length=self.length, ) -- cgit From 10747103fa3748677e6e1948977de1313fe25bc9 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 13 Jan 2009 02:17:16 -0700 Subject: New Param: implemented a base Param._convert_scalar() method; added Param.type_error attribute for ConversionError message --- ipalib/errors2.py | 8 ++++++++ ipalib/parameter.py | 37 ++++++++++++++++++++++--------------- 2 files changed, 30 insertions(+), 15 deletions(-) (limited to 'ipalib') diff --git a/ipalib/errors2.py b/ipalib/errors2.py index 81b1fb2e..4c8acd5d 100644 --- a/ipalib/errors2.py +++ b/ipalib/errors2.py @@ -481,9 +481,17 @@ class RequirementError(InvocationError): class ConversionError(InvocationError): """ **3006** Raised when parameter value can't be converted to correct type. + + For example: + + >>> raise ConversionError(name='age', error='must be an integer') + Traceback (most recent call last): + ... + ConversionError: invalid 'age': must be an integer """ errno = 3006 + format = _('invalid %(name)r: %(error)s') class ValidationError(InvocationError): diff --git a/ipalib/parameter.py b/ipalib/parameter.py index dbf75298..3ec7579a 100644 --- a/ipalib/parameter.py +++ b/ipalib/parameter.py @@ -25,7 +25,7 @@ from types import NoneType from util import make_repr from request import ugettext from plugable import ReadOnly, lock, check_name -from errors2 import RequirementError, ValidationError +from errors2 import ConversionError, RequirementError, ValidationError from constants import NULLS, TYPE_ERROR, CALLABLE_ERROR @@ -189,6 +189,13 @@ def parse_param_spec(spec): return (spec, dict(required=True, multivalue=False)) +__messages = set() + +def _(message): + __messages.add(message) + return message + + class Param(ReadOnly): """ Base class for all parameters. @@ -199,6 +206,9 @@ class Param(ReadOnly): # (direct) subclass must *always* override this class attribute: type = NoneType # Ouch, this wont be very useful in the real world! + # Subclasses should override this with something more specific: + type_error = _('incorrect type') + kwargs = ( ('cli_name', str, None), ('label', callable, None), @@ -436,10 +446,12 @@ class Param(ReadOnly): def _convert_scalar(self, value, index=None): """ - Implement in subclass. + Convert a single scalar value. """ - raise NotImplementedError( - '%s.%s()' % (self.__class__.__name__, '_convert_scalar') + if type(value) is self.type: + return value + raise ConversionError(name=self.name, index=index, + error=ugettext(self.type_error), ) def validate(self, value): @@ -602,10 +614,11 @@ class Bool(Param): class Int(Param): """ - + A parameter for integer values (stored in Python ``int`` type). """ type = int + type_error = _('must be an integer') class Float(Param): @@ -618,7 +631,7 @@ class Float(Param): class Bytes(Param): """ - A parameter for binary data. + A parameter for binary data (stored in Python ``str`` type). """ type = str @@ -664,12 +677,6 @@ class Bytes(Param): self.nice, self.minlength) ) - def _convert_scalar(self, value, index=None): - """ - Implement in subclass. - """ - return value - def _rule_minlength(self, _, value): """ Check minlength constraint. @@ -703,7 +710,7 @@ class Bytes(Param): class Str(Bytes): """ - A parameter for character (textual) data. + A parameter for character data (stored in Python ``unicode`` type). """ type = unicode @@ -756,8 +763,8 @@ def create_param(spec): This function allows you to create `Str` parameters (the most common) from a convenient shorthand that defines the parameter name, whether it is - required, and whether it is multivalue. (For a definition shorthand - syntax, see the `parse_param_spec()` function.) + required, and whether it is multivalue. (For the definition of the + shorthand syntax, see the `parse_param_spec()` function.) If ``spec`` is an ``str`` instance, it will be used to create a new `Str` parameter, which will be returned. For example: -- cgit From a0fb215a2c5f2dfaf26b06f93c3a651f2420083d Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 13 Jan 2009 18:29:45 -0700 Subject: New Param: updated Str._convert_scalar() so it raises a ConversionError --- ipalib/parameter.py | 42 +++++++++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 13 deletions(-) (limited to 'ipalib') diff --git a/ipalib/parameter.py b/ipalib/parameter.py index 3ec7579a..6c942cab 100644 --- a/ipalib/parameter.py +++ b/ipalib/parameter.py @@ -409,8 +409,8 @@ class Param(ReadOnly): multivalue parameter. For example: >>> multi = Str('my_multi', multivalue=True) - >>> multi.convert([True, '', 17, None, False]) - (u'True', u'17', u'False') + >>> multi.convert([1.5, '', 17, None, u'Hello']) + (u'1.5', u'17', u'Hello') >>> multi.convert([None, u'']) is None # Filters to an empty list True @@ -419,8 +419,8 @@ class Param(ReadOnly): >>> multi.convert(42) # Called with a scalar value (u'42',) - >>> multi.convert([True, False]) # Called with a list value - (u'True', u'False') + >>> multi.convert([0, 1]) # Called with a list value + (u'0', u'1') Note that how values are converted (and from what types they will be converted) completely depends upon how a subclass implements its @@ -436,7 +436,7 @@ class Param(ReadOnly): value = (value,) values = tuple( self._convert_scalar(v, i) for (i, v) in filter( - lambda tup: tup[1] not in NULLS, enumerate(value) + lambda iv: iv[1] not in NULLS, enumerate(value) ) ) if len(values) == 0: @@ -608,13 +608,13 @@ class Param(ReadOnly): class Bool(Param): """ - + A parameter for boolean values (stored in the ``bool`` type). """ class Int(Param): """ - A parameter for integer values (stored in Python ``int`` type). + A parameter for integer values (stored in the ``int`` type). """ type = int @@ -623,7 +623,7 @@ class Int(Param): class Float(Param): """ - + A parameter for floating-point values (stored in the ``float`` type). """ type = float @@ -631,7 +631,12 @@ class Float(Param): class Bytes(Param): """ - A parameter for binary data (stored in Python ``str`` type). + A parameter for binary data (stored in the ``str`` type). + + This class is named *Bytes* instead of *Str* so it's aligned with the + Python v3 ``(str, unicode) => (bytes, str)`` clean-up. See: + + http://docs.python.org/3.0/whatsnew/3.0.html """ type = str @@ -710,20 +715,31 @@ class Bytes(Param): class Str(Bytes): """ - A parameter for character data (stored in Python ``unicode`` type). + A parameter for Unicode text (stored in the``unicode`` type). + + This class is named *Str* instead of *Unicode* so it's aligned with the + Python v3 ``(str, unicode) => (bytes, str)`` clean-up. See: + + http://docs.python.org/3.0/whatsnew/3.0.html """ type = unicode + type_error = _('must be Unicode text') kwargs = Bytes.kwargs[:-1] + ( ('pattern', unicode, None), ) def _convert_scalar(self, value, index=None): - if type(value) in (self.type, int, float, bool): + """ + Convert a single scalar value. + """ + if type(value) is self.type: + return value + if type(value) in (int, float): return self.type(value) - raise TypeError( - 'Can only implicitly convert int, float, or bool; got %r' % value + raise ConversionError(name=self.name, index=index, + error=ugettext(self.type_error), ) def _rule_minlength(self, _, value): -- cgit From 659bb4c142ee9a987babd38fad93b539e51309f3 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 13 Jan 2009 19:49:23 -0700 Subject: New Param: added Param.clone() method and corresponding unit test --- ipalib/parameter.py | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'ipalib') diff --git a/ipalib/parameter.py b/ipalib/parameter.py index 6c942cab..7d70f40a 100644 --- a/ipalib/parameter.py +++ b/ipalib/parameter.py @@ -329,6 +329,14 @@ class Param(ReadOnly): **self.__kw ) + def clone(self, **overrides): + """ + Return a new `Param` instance similar to this one. + """ + kw = dict(self.__clonekw) + kw.update(overrides) + return self.__class__(self.name, **kw) + def get_label(self): """ Return translated label using `request.ugettext`. -- cgit From 8cc38e681f9caca838540511664337f964302f56 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 13 Jan 2009 20:27:19 -0700 Subject: New Param: added new Flag param class and its unit test --- ipalib/parameter.py | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) (limited to 'ipalib') diff --git a/ipalib/parameter.py b/ipalib/parameter.py index 7d70f40a..31bd9db2 100644 --- a/ipalib/parameter.py +++ b/ipalib/parameter.py @@ -219,6 +219,7 @@ class Param(ReadOnly): ('normalizer', callable, None), ('default_from', DefaultFrom, None), ('create_default', callable, None), + ('autofill', bool, False), ('flags', frozenset, frozenset()), # The 'default' kwarg gets appended in Param.__init__(): @@ -329,6 +330,17 @@ class Param(ReadOnly): **self.__kw ) + def __call__(self, value, **kw): + """ + One stop shopping. + """ + if value in NULLS: + value = self.get_default(**kw) + else: + value = self.convert(self.normalize(value)) + self.validate(value) + return value + def clone(self, **overrides): """ Return a new `Param` instance similar to this one. @@ -619,6 +631,20 @@ class Bool(Param): A parameter for boolean values (stored in the ``bool`` type). """ + type = bool + + +class Flag(Bool): + """ + A boolean parameter that always gets filled in with a default value. + + This `Bool` subclass forces ``autofill=True`` in `Flag.__init__()`. + """ + + def __init__(self, name, *rules, **kw): + kw['autofill'] = True + super(Flag, self).__init__(name, *rules, **kw) + class Int(Param): """ @@ -635,6 +661,7 @@ class Float(Param): """ type = float + type_error = _('must be a decimal number') class Bytes(Param): @@ -648,6 +675,7 @@ class Bytes(Param): """ type = str + type_error = _('must be binary data') kwargs = Param.kwargs + ( ('minlength', int, None), @@ -657,8 +685,8 @@ class Bytes(Param): ) - def __init__(self, name, **kw): - super(Bytes, self).__init__(name, **kw) + def __init__(self, name, *rules, **kw): + super(Bytes, self).__init__(name, *rules, **kw) if not ( self.length is None or -- cgit From 05514292dcf2448f0340e09cd0da88f815c43c5b Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 14 Jan 2009 09:56:10 -0700 Subject: New Param: Flag now fill-in default=False and also forces default to be a bool --- ipalib/parameter.py | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) (limited to 'ipalib') diff --git a/ipalib/parameter.py b/ipalib/parameter.py index 31bd9db2..0617c84d 100644 --- a/ipalib/parameter.py +++ b/ipalib/parameter.py @@ -638,11 +638,34 @@ class Flag(Bool): """ A boolean parameter that always gets filled in with a default value. - This `Bool` subclass forces ``autofill=True`` in `Flag.__init__()`. + This `Bool` subclass forces ``autofill=True`` in `Flag.__init__()`. If no + default is provided, it also fills in a default value of ``False``. + Lastly, unlike the `Bool` class, the default must be either ``True`` or + ``False`` and cannot be ``None``. + + For example: + + >>> Flag('my_flag') + Flag('my_flag', autofill=True, default=False) + >>> Flag('my_flag', default=True) # Do this for default of True + Flag('my_flag', autofill=True, default=True) + + Also note that creating a `Flag` instance with ``autofill=False`` will have + no effect. For example: + + >>> Flag('my_flag', autofill=False) # autofill will still be True + Flag('my_flag', autofill=True, default=False) """ def __init__(self, name, *rules, **kw): kw['autofill'] = True + if 'default' not in kw: + kw['default'] = False + if type(kw['default']) is not bool: + default = kw['default'] + raise TypeError( + TYPE_ERROR % ('default', bool, default, type(default)) + ) super(Flag, self).__init__(name, *rules, **kw) @@ -751,7 +774,7 @@ class Bytes(Param): class Str(Bytes): """ - A parameter for Unicode text (stored in the``unicode`` type). + A parameter for Unicode text (stored in the ``unicode`` type). This class is named *Str* instead of *Unicode* so it's aligned with the Python v3 ``(str, unicode) => (bytes, str)`` clean-up. See: -- cgit From cc5d7e8adb5882573a47883208e6ac8e60f35002 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 14 Jan 2009 10:17:39 -0700 Subject: New Param: Small docstring change in Flag --- ipalib/parameter.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) (limited to 'ipalib') diff --git a/ipalib/parameter.py b/ipalib/parameter.py index 0617c84d..bfe601a9 100644 --- a/ipalib/parameter.py +++ b/ipalib/parameter.py @@ -632,6 +632,7 @@ class Bool(Param): """ type = bool + type_error = _('must be True or False') class Flag(Bool): @@ -645,16 +646,23 @@ class Flag(Bool): For example: - >>> Flag('my_flag') - Flag('my_flag', autofill=True, default=False) - >>> Flag('my_flag', default=True) # Do this for default of True - Flag('my_flag', autofill=True, default=True) + >>> flag = Flag('my_flag') + >>> (flag.autofill, flag.default) + (True, False) + + To have a default value of ``True``, create your `Flag` intance with + ``default=True``. For example: + + >>> flag = Flag('my_flag', default=True) + >>> (flag.autofill, flag.default) + (True, True) Also note that creating a `Flag` instance with ``autofill=False`` will have no effect. For example: - >>> Flag('my_flag', autofill=False) # autofill will still be True - Flag('my_flag', autofill=True, default=False) + >>> flag = Flag('my_flag', autofill=False) + >>> flag.autofill + True """ def __init__(self, name, *rules, **kw): -- cgit From 5d1e5a0aa9f79e179a5538dcf1bcec5426369951 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 14 Jan 2009 10:25:33 -0700 Subject: New Param: renamed parameter.py to parameters.py --- ipalib/parameter.py | 882 --------------------------------------------------- ipalib/parameters.py | 882 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 882 insertions(+), 882 deletions(-) delete mode 100644 ipalib/parameter.py create mode 100644 ipalib/parameters.py (limited to 'ipalib') diff --git a/ipalib/parameter.py b/ipalib/parameter.py deleted file mode 100644 index bfe601a9..00000000 --- a/ipalib/parameter.py +++ /dev/null @@ -1,882 +0,0 @@ -# Authors: -# Jason Gerard DeRose -# -# 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 - -""" -Parameter system for command plugins. -""" - -from types import NoneType -from util import make_repr -from request import ugettext -from plugable import ReadOnly, lock, check_name -from errors2 import ConversionError, RequirementError, ValidationError -from constants import NULLS, TYPE_ERROR, CALLABLE_ERROR - - -class DefaultFrom(ReadOnly): - """ - Derive a default value from other supplied values. - - For example, say you wanted to create a default for the user's login from - the user's first and last names. It could be implemented like this: - - >>> login = DefaultFrom(lambda first, last: first[0] + last) - >>> login(first='John', last='Doe') - 'JDoe' - - If you do not explicitly provide keys when you create a `DefaultFrom` - instance, the keys are implicitly derived from your callback by - inspecting ``callback.func_code.co_varnames``. The keys are available - through the ``DefaultFrom.keys`` instance attribute, like this: - - >>> login.keys - ('first', 'last') - - The callback is available through the ``DefaultFrom.callback`` instance - attribute, like this: - - >>> login.callback # doctest:+ELLIPSIS - at 0x...> - >>> login.callback.func_code.co_varnames # The keys - ('first', 'last') - - The keys can be explicitly provided as optional positional arguments after - the callback. For example, this is equivalent to the ``login`` instance - above: - - >>> login2 = DefaultFrom(lambda a, b: a[0] + b, 'first', 'last') - >>> login2.keys - ('first', 'last') - >>> login2.callback.func_code.co_varnames # Not the keys - ('a', 'b') - >>> login2(first='John', last='Doe') - 'JDoe' - - If any keys are missing when calling your `DefaultFrom` instance, your - callback is not called and ``None`` is returned. For example: - - >>> login(first='John', lastname='Doe') is None - True - >>> login() is None - True - - Any additional keys are simply ignored, like this: - - >>> login(last='Doe', first='John', middle='Whatever') - 'JDoe' - - As above, because `DefaultFrom.__call__` takes only pure keyword - arguments, they can be supplied in any order. - - Of course, the callback need not be a ``lambda`` expression. This third - example is equivalent to both the ``login`` and ``login2`` instances - above: - - >>> def get_login(first, last): - ... return first[0] + last - ... - >>> login3 = DefaultFrom(get_login) - >>> login3.keys - ('first', 'last') - >>> login3.callback.func_code.co_varnames - ('first', 'last') - >>> login3(first='John', last='Doe') - 'JDoe' - """ - - def __init__(self, callback, *keys): - """ - :param callback: The callable to call when all keys are present. - :param keys: Optional keys used for source values. - """ - if not callable(callback): - raise TypeError( - CALLABLE_ERROR % ('callback', callback, type(callback)) - ) - self.callback = callback - if len(keys) == 0: - fc = callback.func_code - self.keys = fc.co_varnames[:fc.co_argcount] - else: - self.keys = keys - for key in self.keys: - if type(key) is not str: - raise TypeError( - TYPE_ERROR % ('keys', str, key, type(key)) - ) - lock(self) - - def __call__(self, **kw): - """ - Call the callback if all keys are present. - - If all keys are present, the callback is called and its return value is - returned. If any keys are missing, ``None`` is returned. - - :param kw: The keyword arguments. - """ - vals = tuple(kw.get(k, None) for k in self.keys) - if None in vals: - return - try: - return self.callback(*vals) - except StandardError: - pass - - -def parse_param_spec(spec): - """ - Parse shorthand ``spec`` into to ``(name, kw)``. - - The ``spec`` string determines the parameter name, whether the parameter is - required, and whether the parameter is multivalue according the following - syntax: - - ====== ===== ======== ========== - Spec Name Required Multivalue - ====== ===== ======== ========== - 'var' 'var' True False - 'var?' 'var' False False - 'var*' 'var' False True - 'var+' 'var' True True - ====== ===== ======== ========== - - For example, - - >>> parse_param_spec('login') - ('login', {'required': True, 'multivalue': False}) - >>> parse_param_spec('gecos?') - ('gecos', {'required': False, 'multivalue': False}) - >>> parse_param_spec('telephone_numbers*') - ('telephone_numbers', {'required': False, 'multivalue': True}) - >>> parse_param_spec('group+') - ('group', {'required': True, 'multivalue': True}) - - :param spec: A spec string. - """ - if type(spec) is not str: - raise TypeError( - TYPE_ERROR % ('spec', str, spec, type(spec)) - ) - if len(spec) < 2: - raise ValueError( - 'spec must be at least 2 characters; got %r' % spec - ) - _map = { - '?': dict(required=False, multivalue=False), - '*': dict(required=False, multivalue=True), - '+': dict(required=True, multivalue=True), - } - end = spec[-1] - if end in _map: - return (spec[:-1], _map[end]) - return (spec, dict(required=True, multivalue=False)) - - -__messages = set() - -def _(message): - __messages.add(message) - return message - - -class Param(ReadOnly): - """ - Base class for all parameters. - """ - - # This is a dummy type so that most of the functionality of Param can be - # unit tested directly without always creating a subclass; however, a real - # (direct) subclass must *always* override this class attribute: - type = NoneType # Ouch, this wont be very useful in the real world! - - # Subclasses should override this with something more specific: - type_error = _('incorrect type') - - kwargs = ( - ('cli_name', str, None), - ('label', callable, None), - ('doc', str, ''), - ('required', bool, True), - ('multivalue', bool, False), - ('primary_key', bool, False), - ('normalizer', callable, None), - ('default_from', DefaultFrom, None), - ('create_default', callable, None), - ('autofill', bool, False), - ('flags', frozenset, frozenset()), - - # The 'default' kwarg gets appended in Param.__init__(): - # ('default', self.type, None), - ) - - def __init__(self, name, *rules, **kw): - # We keep these values to use in __repr__(): - self.param_spec = name - self.__kw = dict(kw) - - # Merge in kw from parse_param_spec(): - if not ('required' in kw or 'multivalue' in kw): - (name, kw_from_spec) = parse_param_spec(name) - kw.update(kw_from_spec) - self.name = check_name(name) - self.nice = '%s(%r)' % (self.__class__.__name__, self.param_spec) - - # Add 'default' to self.kwargs and makes sure no unknown kw were given: - assert type(self.type) is type - self.kwargs += (('default', self.type, None),) - if not set(t[0] for t in self.kwargs).issuperset(self.__kw): - extra = set(kw) - set(t[0] for t in self.kwargs) - raise TypeError( - '%s: takes no such kwargs: %s' % (self.nice, - ', '.join(repr(k) for k in sorted(extra)) - ) - ) - - # Merge in default for 'cli_name' if not given: - if kw.get('cli_name', None) is None: - kw['cli_name'] = self.name - - # Wrap 'default_from' in a DefaultFrom if not already: - df = kw.get('default_from', None) - if callable(df) and not isinstance(df, DefaultFrom): - kw['default_from'] = DefaultFrom(df) - - # We keep this copy with merged values also to use when cloning: - self.__clonekw = kw - - # Perform type validation on kw, add in class rules: - class_rules = [] - for (key, kind, default) in self.kwargs: - value = kw.get(key, default) - if value is not None: - if kind is frozenset: - if type(value) in (list, tuple): - value = frozenset(value) - elif type(value) is str: - value = frozenset([value]) - if ( - type(kind) is type and type(value) is not kind - or - type(kind) is tuple and not isinstance(value, kind) - ): - raise TypeError( - TYPE_ERROR % (key, kind, value, type(value)) - ) - elif kind is callable and not callable(value): - raise TypeError( - CALLABLE_ERROR % (key, value, type(value)) - ) - if hasattr(self, key): - raise ValueError('kwarg %r conflicts with attribute on %s' % ( - key, self.__class__.__name__) - ) - setattr(self, key, value) - rule_name = '_rule_%s' % key - if value is not None and hasattr(self, rule_name): - class_rules.append(getattr(self, rule_name)) - check_name(self.cli_name) - - # Check that only default_from or create_default was provided: - assert not hasattr(self, '_get_default'), self.nice - if callable(self.default_from): - if callable(self.create_default): - raise ValueError( - '%s: cannot have both %r and %r' % ( - self.nice, 'default_from', 'create_default') - ) - self._get_default = self.default_from - elif callable(self.create_default): - self._get_default = self.create_default - else: - self._get_default = None - - # Check that all the rules are callable - self.class_rules = tuple(class_rules) - self.rules = rules - self.all_rules = self.class_rules + self.rules - for rule in self.all_rules: - if not callable(rule): - raise TypeError( - '%s: rules must be callable; got %r' % (self.nice, rule) - ) - - # And we're done. - lock(self) - - def __repr__(self): - """ - Return an expresion that could construct this `Param` instance. - """ - return make_repr( - self.__class__.__name__, - self.param_spec, - **self.__kw - ) - - def __call__(self, value, **kw): - """ - One stop shopping. - """ - if value in NULLS: - value = self.get_default(**kw) - else: - value = self.convert(self.normalize(value)) - self.validate(value) - return value - - def clone(self, **overrides): - """ - Return a new `Param` instance similar to this one. - """ - kw = dict(self.__clonekw) - kw.update(overrides) - return self.__class__(self.name, **kw) - - def get_label(self): - """ - Return translated label using `request.ugettext`. - """ - if self.label is None: - return self.cli_name.decode('UTF-8') - return self.label(ugettext) - - def normalize(self, value): - """ - Normalize ``value`` using normalizer callback. - - For example: - - >>> param = Param('telephone', - ... normalizer=lambda value: value.replace('.', '-') - ... ) - >>> param.normalize(u'800.123.4567') - u'800-123-4567' - - If this `Param` instance was created with a normalizer callback and - ``value`` is a unicode instance, the normalizer callback is called and - *its* return value is returned. - - On the other hand, if this `Param` instance was *not* created with a - normalizer callback, if ``value`` is *not* a unicode instance, or if an - exception is caught when calling the normalizer callback, ``value`` is - returned unchanged. - - :param value: A proposed value for this parameter. - """ - if self.normalizer is None: - return value - if self.multivalue: - if type(value) in (tuple, list): - return tuple( - self._normalize_scalar(v) for v in value - ) - return (self._normalize_scalar(value),) # Return a tuple - return self._normalize_scalar(value) - - def _normalize_scalar(self, value): - """ - Normalize a scalar value. - - This method is called once for each value in a multivalue. - """ - if type(value) is not unicode: - return value - try: - return self.normalizer(value) - except StandardError: - return value - - def convert(self, value): - """ - Convert ``value`` to the Python type required by this parameter. - - For example: - - >>> scalar = Str('my_scalar') - >>> scalar.type - - >>> scalar.convert(43.2) - u'43.2' - - (Note that `Str` is a subclass of `Param`.) - - All values in `constants.NULLS` will be converted to ``None``. For - example: - - >>> scalar.convert(u'') is None # An empty string - True - >>> scalar.convert([]) is None # An empty list - True - - Likewise, values in `constants.NULLS` will be filtered out of a - multivalue parameter. For example: - - >>> multi = Str('my_multi', multivalue=True) - >>> multi.convert([1.5, '', 17, None, u'Hello']) - (u'1.5', u'17', u'Hello') - >>> multi.convert([None, u'']) is None # Filters to an empty list - True - - Lastly, multivalue parameters will always return a ``tuple`` (assuming - they don't return ``None`` as in the last example above). For example: - - >>> multi.convert(42) # Called with a scalar value - (u'42',) - >>> multi.convert([0, 1]) # Called with a list value - (u'0', u'1') - - Note that how values are converted (and from what types they will be - converted) completely depends upon how a subclass implements its - `Param._convert_scalar()` method. For example, see - `Str._convert_scalar()`. - - :param value: A proposed value for this parameter. - """ - if value in NULLS: - return - if self.multivalue: - if type(value) not in (tuple, list): - value = (value,) - values = tuple( - self._convert_scalar(v, i) for (i, v) in filter( - lambda iv: iv[1] not in NULLS, enumerate(value) - ) - ) - if len(values) == 0: - return - return values - return self._convert_scalar(value) - - def _convert_scalar(self, value, index=None): - """ - Convert a single scalar value. - """ - if type(value) is self.type: - return value - raise ConversionError(name=self.name, index=index, - error=ugettext(self.type_error), - ) - - def validate(self, value): - """ - Check validity of ``value``. - - :param value: A proposed value for this parameter. - """ - if value is None: - if self.required: - raise RequirementError(name=self.name) - return - if self.multivalue: - if type(value) is not tuple: - raise TypeError( - TYPE_ERROR % ('value', tuple, value, type(value)) - ) - if len(value) < 1: - raise ValueError('value: empty tuple must be converted to None') - for (i, v) in enumerate(value): - self._validate_scalar(v, i) - else: - self._validate_scalar(value) - - def _validate_scalar(self, value, index=None): - if type(value) is not self.type: - if index is None: - name = 'value' - else: - name = 'value[%d]' % index - raise TypeError( - TYPE_ERROR % (name, self.type, value, type(value)) - ) - if index is not None and type(index) is not int: - raise TypeError( - TYPE_ERROR % ('index', int, index, type(index)) - ) - for rule in self.all_rules: - error = rule(ugettext, value) - if error is not None: - raise ValidationError(name=self.name, error=error, index=index) - - def get_default(self, **kw): - """ - Return the static default or construct and return a dynamic default. - - (In these examples, we will use the `Str` and `Bytes` classes, which - both subclass from `Param`.) - - The *default* static default is ``None``. For example: - - >>> s = Str('my_str') - >>> s.default is None - True - >>> s.get_default() is None - True - - However, you can provide your own static default via the ``default`` - keyword argument when you create your `Param` instance. For example: - - >>> s = Str('my_str', default=u'My Static Default') - >>> s.default - u'My Static Default' - >>> s.get_default() - u'My Static Default' - - If you need to generate a dynamic default from other supplied parameter - values, provide a callback via the ``default_from`` keyword argument. - This callback will be automatically wrapped in a `DefaultFrom` instance - if it isn't one already (see the `DefaultFrom` class for all the gory - details). For example: - - >>> login = Str('login', default=u'my-static-login-default', - ... default_from=lambda first, last: (first[0] + last).lower(), - ... ) - >>> isinstance(login.default_from, DefaultFrom) - True - >>> login.default_from.keys - ('first', 'last') - - Then when all the keys needed by the `DefaultFrom` instance are present, - the dynamic default is constructed and returned. For example: - - >>> kw = dict(last=u'Doe', first=u'John') - >>> login.get_default(**kw) - u'jdoe' - - Or if any keys are missing, your *static* default is returned. - For example: - - >>> kw = dict(first=u'John', department=u'Engineering') - >>> login.get_default(**kw) - u'my-static-login-default' - - The second, less common way to construct a dynamic default is to provide - a callback via the ``create_default`` keyword argument. Unlike a - ``default_from`` callback, your ``create_default`` callback will not get - wrapped in any dispatcher. Instead, it will be called directly, which - means your callback must accept arbitrary keyword arguments, although - whether your callback utilises these values is up to your - implementation. For example: - - >>> def make_csr(**kw): - ... print ' make_csr(%r)' % (kw,) # Note output below - ... return 'Certificate Signing Request' - ... - >>> csr = Bytes('csr', create_default=make_csr) - - Your ``create_default`` callback will be called with whatever keyword - arguments are passed to `Param.get_default()`. For example: - - >>> kw = dict(arbitrary='Keyword', arguments='Here') - >>> csr.get_default(**kw) - make_csr({'arguments': 'Here', 'arbitrary': 'Keyword'}) - 'Certificate Signing Request' - - And your ``create_default`` callback is called even if - `Param.get_default()` is called with *zero* keyword arguments. - For example: - - >>> csr.get_default() - make_csr({}) - 'Certificate Signing Request' - - The ``create_default`` callback will most likely be used as a - pre-execute hook to perform some special client-side operation. For - example, the ``csr`` parameter above might make a call to - ``/usr/bin/openssl``. However, often a ``create_default`` callback - could also be implemented as a ``default_from`` callback. When this is - the case, a ``default_from`` callback should be used as they are more - structured and therefore less error-prone. - - The ``default_from`` and ``create_default`` keyword arguments are - mutually exclusive. If you provide both, a ``ValueError`` will be - raised. For example: - - >>> homedir = Str('home', - ... default_from=lambda login: '/home/%s' % login, - ... create_default=lambda **kw: '/lets/use/this', - ... ) - Traceback (most recent call last): - ... - ValueError: Str('home'): cannot have both 'default_from' and 'create_default' - """ - if self._get_default is not None: - default = self._get_default(**kw) - if default is not None: - try: - return self.convert(self.normalize(default)) - except StandardError: - pass - return self.default - - -class Bool(Param): - """ - A parameter for boolean values (stored in the ``bool`` type). - """ - - type = bool - type_error = _('must be True or False') - - -class Flag(Bool): - """ - A boolean parameter that always gets filled in with a default value. - - This `Bool` subclass forces ``autofill=True`` in `Flag.__init__()`. If no - default is provided, it also fills in a default value of ``False``. - Lastly, unlike the `Bool` class, the default must be either ``True`` or - ``False`` and cannot be ``None``. - - For example: - - >>> flag = Flag('my_flag') - >>> (flag.autofill, flag.default) - (True, False) - - To have a default value of ``True``, create your `Flag` intance with - ``default=True``. For example: - - >>> flag = Flag('my_flag', default=True) - >>> (flag.autofill, flag.default) - (True, True) - - Also note that creating a `Flag` instance with ``autofill=False`` will have - no effect. For example: - - >>> flag = Flag('my_flag', autofill=False) - >>> flag.autofill - True - """ - - def __init__(self, name, *rules, **kw): - kw['autofill'] = True - if 'default' not in kw: - kw['default'] = False - if type(kw['default']) is not bool: - default = kw['default'] - raise TypeError( - TYPE_ERROR % ('default', bool, default, type(default)) - ) - super(Flag, self).__init__(name, *rules, **kw) - - -class Int(Param): - """ - A parameter for integer values (stored in the ``int`` type). - """ - - type = int - type_error = _('must be an integer') - - -class Float(Param): - """ - A parameter for floating-point values (stored in the ``float`` type). - """ - - type = float - type_error = _('must be a decimal number') - - -class Bytes(Param): - """ - A parameter for binary data (stored in the ``str`` type). - - This class is named *Bytes* instead of *Str* so it's aligned with the - Python v3 ``(str, unicode) => (bytes, str)`` clean-up. See: - - http://docs.python.org/3.0/whatsnew/3.0.html - """ - - type = str - type_error = _('must be binary data') - - kwargs = Param.kwargs + ( - ('minlength', int, None), - ('maxlength', int, None), - ('length', int, None), - ('pattern', str, None), - - ) - - def __init__(self, name, *rules, **kw): - super(Bytes, self).__init__(name, *rules, **kw) - - if not ( - self.length is None or - (self.minlength is None and self.maxlength is None) - ): - raise ValueError( - '%s: cannot mix length with minlength or maxlength' % self.nice - ) - - if self.minlength is not None and self.minlength < 1: - raise ValueError( - '%s: minlength must be >= 1; got %r' % (self.nice, self.minlength) - ) - - if self.maxlength is not None and self.maxlength < 1: - raise ValueError( - '%s: maxlength must be >= 1; got %r' % (self.nice, self.maxlength) - ) - - if None not in (self.minlength, self.maxlength): - if self.minlength > self.maxlength: - raise ValueError( - '%s: minlength > maxlength (minlength=%r, maxlength=%r)' % ( - self.nice, self.minlength, self.maxlength) - ) - elif self.minlength == self.maxlength: - raise ValueError( - '%s: minlength == maxlength; use length=%d instead' % ( - self.nice, self.minlength) - ) - - def _rule_minlength(self, _, value): - """ - Check minlength constraint. - """ - assert type(value) is str - if len(value) < self.minlength: - return _('must be at least %(minlength)d bytes') % dict( - minlength=self.minlength, - ) - - def _rule_maxlength(self, _, value): - """ - Check maxlength constraint. - """ - assert type(value) is str - if len(value) > self.maxlength: - return _('can be at most %(maxlength)d bytes') % dict( - maxlength=self.maxlength, - ) - - def _rule_length(self, _, value): - """ - Check length constraint. - """ - assert type(value) is str - if len(value) != self.length: - return _('must be exactly %(length)d bytes') % dict( - length=self.length, - ) - - -class Str(Bytes): - """ - A parameter for Unicode text (stored in the ``unicode`` type). - - This class is named *Str* instead of *Unicode* so it's aligned with the - Python v3 ``(str, unicode) => (bytes, str)`` clean-up. See: - - http://docs.python.org/3.0/whatsnew/3.0.html - """ - - type = unicode - type_error = _('must be Unicode text') - - kwargs = Bytes.kwargs[:-1] + ( - ('pattern', unicode, None), - ) - - def _convert_scalar(self, value, index=None): - """ - Convert a single scalar value. - """ - if type(value) is self.type: - return value - if type(value) in (int, float): - return self.type(value) - raise ConversionError(name=self.name, index=index, - error=ugettext(self.type_error), - ) - - def _rule_minlength(self, _, value): - """ - Check minlength constraint. - """ - assert type(value) is unicode - if len(value) < self.minlength: - return _('must be at least %(minlength)d characters') % dict( - minlength=self.minlength, - ) - - def _rule_maxlength(self, _, value): - """ - Check maxlength constraint. - """ - assert type(value) is unicode - if len(value) > self.maxlength: - return _('can be at most %(maxlength)d characters') % dict( - maxlength=self.maxlength, - ) - - def _rule_length(self, _, value): - """ - Check length constraint. - """ - assert type(value) is unicode - if len(value) != self.length: - return _('must be exactly %(length)d characters') % dict( - length=self.length, - ) - - -def create_param(spec): - """ - Create an `Str` instance from the shorthand ``spec``. - - This function allows you to create `Str` parameters (the most common) from - a convenient shorthand that defines the parameter name, whether it is - required, and whether it is multivalue. (For the definition of the - shorthand syntax, see the `parse_param_spec()` function.) - - If ``spec`` is an ``str`` instance, it will be used to create a new `Str` - parameter, which will be returned. For example: - - >>> s = create_param('hometown?') - >>> s - Str('hometown?') - >>> (s.name, s.required, s.multivalue) - ('hometown', False, False) - - On the other hand, if ``spec`` is already a `Param` instance, it is - returned unchanged. For example: - - >>> b = Bytes('cert') - >>> create_param(b) is b - True - - As a plugin author, you will not call this function directly (which would - be no more convenient than simply creating the `Str` instance). Instead, - `frontend.Command` will call it for you when it evaluates the - ``takes_args`` and ``takes_options`` attributes, and `frontend.Object` - will call it for you when it evaluates the ``takes_params`` attribute. - - :param spec: A spec string or a `Param` instance. - """ - if isinstance(spec, Param): - return spec - if type(spec) is not str: - raise TypeError( - TYPE_ERROR % ('spec', (str, Param), spec, type(spec)) - ) - return Str(spec) diff --git a/ipalib/parameters.py b/ipalib/parameters.py new file mode 100644 index 00000000..bfe601a9 --- /dev/null +++ b/ipalib/parameters.py @@ -0,0 +1,882 @@ +# Authors: +# Jason Gerard DeRose +# +# 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 + +""" +Parameter system for command plugins. +""" + +from types import NoneType +from util import make_repr +from request import ugettext +from plugable import ReadOnly, lock, check_name +from errors2 import ConversionError, RequirementError, ValidationError +from constants import NULLS, TYPE_ERROR, CALLABLE_ERROR + + +class DefaultFrom(ReadOnly): + """ + Derive a default value from other supplied values. + + For example, say you wanted to create a default for the user's login from + the user's first and last names. It could be implemented like this: + + >>> login = DefaultFrom(lambda first, last: first[0] + last) + >>> login(first='John', last='Doe') + 'JDoe' + + If you do not explicitly provide keys when you create a `DefaultFrom` + instance, the keys are implicitly derived from your callback by + inspecting ``callback.func_code.co_varnames``. The keys are available + through the ``DefaultFrom.keys`` instance attribute, like this: + + >>> login.keys + ('first', 'last') + + The callback is available through the ``DefaultFrom.callback`` instance + attribute, like this: + + >>> login.callback # doctest:+ELLIPSIS + at 0x...> + >>> login.callback.func_code.co_varnames # The keys + ('first', 'last') + + The keys can be explicitly provided as optional positional arguments after + the callback. For example, this is equivalent to the ``login`` instance + above: + + >>> login2 = DefaultFrom(lambda a, b: a[0] + b, 'first', 'last') + >>> login2.keys + ('first', 'last') + >>> login2.callback.func_code.co_varnames # Not the keys + ('a', 'b') + >>> login2(first='John', last='Doe') + 'JDoe' + + If any keys are missing when calling your `DefaultFrom` instance, your + callback is not called and ``None`` is returned. For example: + + >>> login(first='John', lastname='Doe') is None + True + >>> login() is None + True + + Any additional keys are simply ignored, like this: + + >>> login(last='Doe', first='John', middle='Whatever') + 'JDoe' + + As above, because `DefaultFrom.__call__` takes only pure keyword + arguments, they can be supplied in any order. + + Of course, the callback need not be a ``lambda`` expression. This third + example is equivalent to both the ``login`` and ``login2`` instances + above: + + >>> def get_login(first, last): + ... return first[0] + last + ... + >>> login3 = DefaultFrom(get_login) + >>> login3.keys + ('first', 'last') + >>> login3.callback.func_code.co_varnames + ('first', 'last') + >>> login3(first='John', last='Doe') + 'JDoe' + """ + + def __init__(self, callback, *keys): + """ + :param callback: The callable to call when all keys are present. + :param keys: Optional keys used for source values. + """ + if not callable(callback): + raise TypeError( + CALLABLE_ERROR % ('callback', callback, type(callback)) + ) + self.callback = callback + if len(keys) == 0: + fc = callback.func_code + self.keys = fc.co_varnames[:fc.co_argcount] + else: + self.keys = keys + for key in self.keys: + if type(key) is not str: + raise TypeError( + TYPE_ERROR % ('keys', str, key, type(key)) + ) + lock(self) + + def __call__(self, **kw): + """ + Call the callback if all keys are present. + + If all keys are present, the callback is called and its return value is + returned. If any keys are missing, ``None`` is returned. + + :param kw: The keyword arguments. + """ + vals = tuple(kw.get(k, None) for k in self.keys) + if None in vals: + return + try: + return self.callback(*vals) + except StandardError: + pass + + +def parse_param_spec(spec): + """ + Parse shorthand ``spec`` into to ``(name, kw)``. + + The ``spec`` string determines the parameter name, whether the parameter is + required, and whether the parameter is multivalue according the following + syntax: + + ====== ===== ======== ========== + Spec Name Required Multivalue + ====== ===== ======== ========== + 'var' 'var' True False + 'var?' 'var' False False + 'var*' 'var' False True + 'var+' 'var' True True + ====== ===== ======== ========== + + For example, + + >>> parse_param_spec('login') + ('login', {'required': True, 'multivalue': False}) + >>> parse_param_spec('gecos?') + ('gecos', {'required': False, 'multivalue': False}) + >>> parse_param_spec('telephone_numbers*') + ('telephone_numbers', {'required': False, 'multivalue': True}) + >>> parse_param_spec('group+') + ('group', {'required': True, 'multivalue': True}) + + :param spec: A spec string. + """ + if type(spec) is not str: + raise TypeError( + TYPE_ERROR % ('spec', str, spec, type(spec)) + ) + if len(spec) < 2: + raise ValueError( + 'spec must be at least 2 characters; got %r' % spec + ) + _map = { + '?': dict(required=False, multivalue=False), + '*': dict(required=False, multivalue=True), + '+': dict(required=True, multivalue=True), + } + end = spec[-1] + if end in _map: + return (spec[:-1], _map[end]) + return (spec, dict(required=True, multivalue=False)) + + +__messages = set() + +def _(message): + __messages.add(message) + return message + + +class Param(ReadOnly): + """ + Base class for all parameters. + """ + + # This is a dummy type so that most of the functionality of Param can be + # unit tested directly without always creating a subclass; however, a real + # (direct) subclass must *always* override this class attribute: + type = NoneType # Ouch, this wont be very useful in the real world! + + # Subclasses should override this with something more specific: + type_error = _('incorrect type') + + kwargs = ( + ('cli_name', str, None), + ('label', callable, None), + ('doc', str, ''), + ('required', bool, True), + ('multivalue', bool, False), + ('primary_key', bool, False), + ('normalizer', callable, None), + ('default_from', DefaultFrom, None), + ('create_default', callable, None), + ('autofill', bool, False), + ('flags', frozenset, frozenset()), + + # The 'default' kwarg gets appended in Param.__init__(): + # ('default', self.type, None), + ) + + def __init__(self, name, *rules, **kw): + # We keep these values to use in __repr__(): + self.param_spec = name + self.__kw = dict(kw) + + # Merge in kw from parse_param_spec(): + if not ('required' in kw or 'multivalue' in kw): + (name, kw_from_spec) = parse_param_spec(name) + kw.update(kw_from_spec) + self.name = check_name(name) + self.nice = '%s(%r)' % (self.__class__.__name__, self.param_spec) + + # Add 'default' to self.kwargs and makes sure no unknown kw were given: + assert type(self.type) is type + self.kwargs += (('default', self.type, None),) + if not set(t[0] for t in self.kwargs).issuperset(self.__kw): + extra = set(kw) - set(t[0] for t in self.kwargs) + raise TypeError( + '%s: takes no such kwargs: %s' % (self.nice, + ', '.join(repr(k) for k in sorted(extra)) + ) + ) + + # Merge in default for 'cli_name' if not given: + if kw.get('cli_name', None) is None: + kw['cli_name'] = self.name + + # Wrap 'default_from' in a DefaultFrom if not already: + df = kw.get('default_from', None) + if callable(df) and not isinstance(df, DefaultFrom): + kw['default_from'] = DefaultFrom(df) + + # We keep this copy with merged values also to use when cloning: + self.__clonekw = kw + + # Perform type validation on kw, add in class rules: + class_rules = [] + for (key, kind, default) in self.kwargs: + value = kw.get(key, default) + if value is not None: + if kind is frozenset: + if type(value) in (list, tuple): + value = frozenset(value) + elif type(value) is str: + value = frozenset([value]) + if ( + type(kind) is type and type(value) is not kind + or + type(kind) is tuple and not isinstance(value, kind) + ): + raise TypeError( + TYPE_ERROR % (key, kind, value, type(value)) + ) + elif kind is callable and not callable(value): + raise TypeError( + CALLABLE_ERROR % (key, value, type(value)) + ) + if hasattr(self, key): + raise ValueError('kwarg %r conflicts with attribute on %s' % ( + key, self.__class__.__name__) + ) + setattr(self, key, value) + rule_name = '_rule_%s' % key + if value is not None and hasattr(self, rule_name): + class_rules.append(getattr(self, rule_name)) + check_name(self.cli_name) + + # Check that only default_from or create_default was provided: + assert not hasattr(self, '_get_default'), self.nice + if callable(self.default_from): + if callable(self.create_default): + raise ValueError( + '%s: cannot have both %r and %r' % ( + self.nice, 'default_from', 'create_default') + ) + self._get_default = self.default_from + elif callable(self.create_default): + self._get_default = self.create_default + else: + self._get_default = None + + # Check that all the rules are callable + self.class_rules = tuple(class_rules) + self.rules = rules + self.all_rules = self.class_rules + self.rules + for rule in self.all_rules: + if not callable(rule): + raise TypeError( + '%s: rules must be callable; got %r' % (self.nice, rule) + ) + + # And we're done. + lock(self) + + def __repr__(self): + """ + Return an expresion that could construct this `Param` instance. + """ + return make_repr( + self.__class__.__name__, + self.param_spec, + **self.__kw + ) + + def __call__(self, value, **kw): + """ + One stop shopping. + """ + if value in NULLS: + value = self.get_default(**kw) + else: + value = self.convert(self.normalize(value)) + self.validate(value) + return value + + def clone(self, **overrides): + """ + Return a new `Param` instance similar to this one. + """ + kw = dict(self.__clonekw) + kw.update(overrides) + return self.__class__(self.name, **kw) + + def get_label(self): + """ + Return translated label using `request.ugettext`. + """ + if self.label is None: + return self.cli_name.decode('UTF-8') + return self.label(ugettext) + + def normalize(self, value): + """ + Normalize ``value`` using normalizer callback. + + For example: + + >>> param = Param('telephone', + ... normalizer=lambda value: value.replace('.', '-') + ... ) + >>> param.normalize(u'800.123.4567') + u'800-123-4567' + + If this `Param` instance was created with a normalizer callback and + ``value`` is a unicode instance, the normalizer callback is called and + *its* return value is returned. + + On the other hand, if this `Param` instance was *not* created with a + normalizer callback, if ``value`` is *not* a unicode instance, or if an + exception is caught when calling the normalizer callback, ``value`` is + returned unchanged. + + :param value: A proposed value for this parameter. + """ + if self.normalizer is None: + return value + if self.multivalue: + if type(value) in (tuple, list): + return tuple( + self._normalize_scalar(v) for v in value + ) + return (self._normalize_scalar(value),) # Return a tuple + return self._normalize_scalar(value) + + def _normalize_scalar(self, value): + """ + Normalize a scalar value. + + This method is called once for each value in a multivalue. + """ + if type(value) is not unicode: + return value + try: + return self.normalizer(value) + except StandardError: + return value + + def convert(self, value): + """ + Convert ``value`` to the Python type required by this parameter. + + For example: + + >>> scalar = Str('my_scalar') + >>> scalar.type + + >>> scalar.convert(43.2) + u'43.2' + + (Note that `Str` is a subclass of `Param`.) + + All values in `constants.NULLS` will be converted to ``None``. For + example: + + >>> scalar.convert(u'') is None # An empty string + True + >>> scalar.convert([]) is None # An empty list + True + + Likewise, values in `constants.NULLS` will be filtered out of a + multivalue parameter. For example: + + >>> multi = Str('my_multi', multivalue=True) + >>> multi.convert([1.5, '', 17, None, u'Hello']) + (u'1.5', u'17', u'Hello') + >>> multi.convert([None, u'']) is None # Filters to an empty list + True + + Lastly, multivalue parameters will always return a ``tuple`` (assuming + they don't return ``None`` as in the last example above). For example: + + >>> multi.convert(42) # Called with a scalar value + (u'42',) + >>> multi.convert([0, 1]) # Called with a list value + (u'0', u'1') + + Note that how values are converted (and from what types they will be + converted) completely depends upon how a subclass implements its + `Param._convert_scalar()` method. For example, see + `Str._convert_scalar()`. + + :param value: A proposed value for this parameter. + """ + if value in NULLS: + return + if self.multivalue: + if type(value) not in (tuple, list): + value = (value,) + values = tuple( + self._convert_scalar(v, i) for (i, v) in filter( + lambda iv: iv[1] not in NULLS, enumerate(value) + ) + ) + if len(values) == 0: + return + return values + return self._convert_scalar(value) + + def _convert_scalar(self, value, index=None): + """ + Convert a single scalar value. + """ + if type(value) is self.type: + return value + raise ConversionError(name=self.name, index=index, + error=ugettext(self.type_error), + ) + + def validate(self, value): + """ + Check validity of ``value``. + + :param value: A proposed value for this parameter. + """ + if value is None: + if self.required: + raise RequirementError(name=self.name) + return + if self.multivalue: + if type(value) is not tuple: + raise TypeError( + TYPE_ERROR % ('value', tuple, value, type(value)) + ) + if len(value) < 1: + raise ValueError('value: empty tuple must be converted to None') + for (i, v) in enumerate(value): + self._validate_scalar(v, i) + else: + self._validate_scalar(value) + + def _validate_scalar(self, value, index=None): + if type(value) is not self.type: + if index is None: + name = 'value' + else: + name = 'value[%d]' % index + raise TypeError( + TYPE_ERROR % (name, self.type, value, type(value)) + ) + if index is not None and type(index) is not int: + raise TypeError( + TYPE_ERROR % ('index', int, index, type(index)) + ) + for rule in self.all_rules: + error = rule(ugettext, value) + if error is not None: + raise ValidationError(name=self.name, error=error, index=index) + + def get_default(self, **kw): + """ + Return the static default or construct and return a dynamic default. + + (In these examples, we will use the `Str` and `Bytes` classes, which + both subclass from `Param`.) + + The *default* static default is ``None``. For example: + + >>> s = Str('my_str') + >>> s.default is None + True + >>> s.get_default() is None + True + + However, you can provide your own static default via the ``default`` + keyword argument when you create your `Param` instance. For example: + + >>> s = Str('my_str', default=u'My Static Default') + >>> s.default + u'My Static Default' + >>> s.get_default() + u'My Static Default' + + If you need to generate a dynamic default from other supplied parameter + values, provide a callback via the ``default_from`` keyword argument. + This callback will be automatically wrapped in a `DefaultFrom` instance + if it isn't one already (see the `DefaultFrom` class for all the gory + details). For example: + + >>> login = Str('login', default=u'my-static-login-default', + ... default_from=lambda first, last: (first[0] + last).lower(), + ... ) + >>> isinstance(login.default_from, DefaultFrom) + True + >>> login.default_from.keys + ('first', 'last') + + Then when all the keys needed by the `DefaultFrom` instance are present, + the dynamic default is constructed and returned. For example: + + >>> kw = dict(last=u'Doe', first=u'John') + >>> login.get_default(**kw) + u'jdoe' + + Or if any keys are missing, your *static* default is returned. + For example: + + >>> kw = dict(first=u'John', department=u'Engineering') + >>> login.get_default(**kw) + u'my-static-login-default' + + The second, less common way to construct a dynamic default is to provide + a callback via the ``create_default`` keyword argument. Unlike a + ``default_from`` callback, your ``create_default`` callback will not get + wrapped in any dispatcher. Instead, it will be called directly, which + means your callback must accept arbitrary keyword arguments, although + whether your callback utilises these values is up to your + implementation. For example: + + >>> def make_csr(**kw): + ... print ' make_csr(%r)' % (kw,) # Note output below + ... return 'Certificate Signing Request' + ... + >>> csr = Bytes('csr', create_default=make_csr) + + Your ``create_default`` callback will be called with whatever keyword + arguments are passed to `Param.get_default()`. For example: + + >>> kw = dict(arbitrary='Keyword', arguments='Here') + >>> csr.get_default(**kw) + make_csr({'arguments': 'Here', 'arbitrary': 'Keyword'}) + 'Certificate Signing Request' + + And your ``create_default`` callback is called even if + `Param.get_default()` is called with *zero* keyword arguments. + For example: + + >>> csr.get_default() + make_csr({}) + 'Certificate Signing Request' + + The ``create_default`` callback will most likely be used as a + pre-execute hook to perform some special client-side operation. For + example, the ``csr`` parameter above might make a call to + ``/usr/bin/openssl``. However, often a ``create_default`` callback + could also be implemented as a ``default_from`` callback. When this is + the case, a ``default_from`` callback should be used as they are more + structured and therefore less error-prone. + + The ``default_from`` and ``create_default`` keyword arguments are + mutually exclusive. If you provide both, a ``ValueError`` will be + raised. For example: + + >>> homedir = Str('home', + ... default_from=lambda login: '/home/%s' % login, + ... create_default=lambda **kw: '/lets/use/this', + ... ) + Traceback (most recent call last): + ... + ValueError: Str('home'): cannot have both 'default_from' and 'create_default' + """ + if self._get_default is not None: + default = self._get_default(**kw) + if default is not None: + try: + return self.convert(self.normalize(default)) + except StandardError: + pass + return self.default + + +class Bool(Param): + """ + A parameter for boolean values (stored in the ``bool`` type). + """ + + type = bool + type_error = _('must be True or False') + + +class Flag(Bool): + """ + A boolean parameter that always gets filled in with a default value. + + This `Bool` subclass forces ``autofill=True`` in `Flag.__init__()`. If no + default is provided, it also fills in a default value of ``False``. + Lastly, unlike the `Bool` class, the default must be either ``True`` or + ``False`` and cannot be ``None``. + + For example: + + >>> flag = Flag('my_flag') + >>> (flag.autofill, flag.default) + (True, False) + + To have a default value of ``True``, create your `Flag` intance with + ``default=True``. For example: + + >>> flag = Flag('my_flag', default=True) + >>> (flag.autofill, flag.default) + (True, True) + + Also note that creating a `Flag` instance with ``autofill=False`` will have + no effect. For example: + + >>> flag = Flag('my_flag', autofill=False) + >>> flag.autofill + True + """ + + def __init__(self, name, *rules, **kw): + kw['autofill'] = True + if 'default' not in kw: + kw['default'] = False + if type(kw['default']) is not bool: + default = kw['default'] + raise TypeError( + TYPE_ERROR % ('default', bool, default, type(default)) + ) + super(Flag, self).__init__(name, *rules, **kw) + + +class Int(Param): + """ + A parameter for integer values (stored in the ``int`` type). + """ + + type = int + type_error = _('must be an integer') + + +class Float(Param): + """ + A parameter for floating-point values (stored in the ``float`` type). + """ + + type = float + type_error = _('must be a decimal number') + + +class Bytes(Param): + """ + A parameter for binary data (stored in the ``str`` type). + + This class is named *Bytes* instead of *Str* so it's aligned with the + Python v3 ``(str, unicode) => (bytes, str)`` clean-up. See: + + http://docs.python.org/3.0/whatsnew/3.0.html + """ + + type = str + type_error = _('must be binary data') + + kwargs = Param.kwargs + ( + ('minlength', int, None), + ('maxlength', int, None), + ('length', int, None), + ('pattern', str, None), + + ) + + def __init__(self, name, *rules, **kw): + super(Bytes, self).__init__(name, *rules, **kw) + + if not ( + self.length is None or + (self.minlength is None and self.maxlength is None) + ): + raise ValueError( + '%s: cannot mix length with minlength or maxlength' % self.nice + ) + + if self.minlength is not None and self.minlength < 1: + raise ValueError( + '%s: minlength must be >= 1; got %r' % (self.nice, self.minlength) + ) + + if self.maxlength is not None and self.maxlength < 1: + raise ValueError( + '%s: maxlength must be >= 1; got %r' % (self.nice, self.maxlength) + ) + + if None not in (self.minlength, self.maxlength): + if self.minlength > self.maxlength: + raise ValueError( + '%s: minlength > maxlength (minlength=%r, maxlength=%r)' % ( + self.nice, self.minlength, self.maxlength) + ) + elif self.minlength == self.maxlength: + raise ValueError( + '%s: minlength == maxlength; use length=%d instead' % ( + self.nice, self.minlength) + ) + + def _rule_minlength(self, _, value): + """ + Check minlength constraint. + """ + assert type(value) is str + if len(value) < self.minlength: + return _('must be at least %(minlength)d bytes') % dict( + minlength=self.minlength, + ) + + def _rule_maxlength(self, _, value): + """ + Check maxlength constraint. + """ + assert type(value) is str + if len(value) > self.maxlength: + return _('can be at most %(maxlength)d bytes') % dict( + maxlength=self.maxlength, + ) + + def _rule_length(self, _, value): + """ + Check length constraint. + """ + assert type(value) is str + if len(value) != self.length: + return _('must be exactly %(length)d bytes') % dict( + length=self.length, + ) + + +class Str(Bytes): + """ + A parameter for Unicode text (stored in the ``unicode`` type). + + This class is named *Str* instead of *Unicode* so it's aligned with the + Python v3 ``(str, unicode) => (bytes, str)`` clean-up. See: + + http://docs.python.org/3.0/whatsnew/3.0.html + """ + + type = unicode + type_error = _('must be Unicode text') + + kwargs = Bytes.kwargs[:-1] + ( + ('pattern', unicode, None), + ) + + def _convert_scalar(self, value, index=None): + """ + Convert a single scalar value. + """ + if type(value) is self.type: + return value + if type(value) in (int, float): + return self.type(value) + raise ConversionError(name=self.name, index=index, + error=ugettext(self.type_error), + ) + + def _rule_minlength(self, _, value): + """ + Check minlength constraint. + """ + assert type(value) is unicode + if len(value) < self.minlength: + return _('must be at least %(minlength)d characters') % dict( + minlength=self.minlength, + ) + + def _rule_maxlength(self, _, value): + """ + Check maxlength constraint. + """ + assert type(value) is unicode + if len(value) > self.maxlength: + return _('can be at most %(maxlength)d characters') % dict( + maxlength=self.maxlength, + ) + + def _rule_length(self, _, value): + """ + Check length constraint. + """ + assert type(value) is unicode + if len(value) != self.length: + return _('must be exactly %(length)d characters') % dict( + length=self.length, + ) + + +def create_param(spec): + """ + Create an `Str` instance from the shorthand ``spec``. + + This function allows you to create `Str` parameters (the most common) from + a convenient shorthand that defines the parameter name, whether it is + required, and whether it is multivalue. (For the definition of the + shorthand syntax, see the `parse_param_spec()` function.) + + If ``spec`` is an ``str`` instance, it will be used to create a new `Str` + parameter, which will be returned. For example: + + >>> s = create_param('hometown?') + >>> s + Str('hometown?') + >>> (s.name, s.required, s.multivalue) + ('hometown', False, False) + + On the other hand, if ``spec`` is already a `Param` instance, it is + returned unchanged. For example: + + >>> b = Bytes('cert') + >>> create_param(b) is b + True + + As a plugin author, you will not call this function directly (which would + be no more convenient than simply creating the `Str` instance). Instead, + `frontend.Command` will call it for you when it evaluates the + ``takes_args`` and ``takes_options`` attributes, and `frontend.Object` + will call it for you when it evaluates the ``takes_params`` attribute. + + :param spec: A spec string or a `Param` instance. + """ + if isinstance(spec, Param): + return spec + if type(spec) is not str: + raise TypeError( + TYPE_ERROR % ('spec', (str, Param), spec, type(spec)) + ) + return Str(spec) -- cgit From 3e201dfff6d96f415a7f7d7f6010e97877b5a5c0 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 14 Jan 2009 10:58:05 -0700 Subject: New Param: split common Bytes/Str functionality into new Data base class; Str no longer subclasses from Bytes --- ipalib/parameters.py | 51 ++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 36 insertions(+), 15 deletions(-) (limited to 'ipalib') diff --git a/ipalib/parameters.py b/ipalib/parameters.py index bfe601a9..da01bfcf 100644 --- a/ipalib/parameters.py +++ b/ipalib/parameters.py @@ -19,6 +19,16 @@ """ Parameter system for command plugins. + +TODO: + + * Change rule call signature to rule(_, value, **kw) so that rules can also + validate relative to other parameter values (e.g., login name as it relates + to first name and last name) + + * Add the _rule_pattern() methods to `Bytes` and `Str` + + * Add maxvalue, minvalue kwargs and rules to `Int` and `Float` """ from types import NoneType @@ -28,7 +38,6 @@ from plugable import ReadOnly, lock, check_name from errors2 import ConversionError, RequirementError, ValidationError from constants import NULLS, TYPE_ERROR, CALLABLE_ERROR - class DefaultFrom(ReadOnly): """ Derive a default value from other supplied values. @@ -695,29 +704,23 @@ class Float(Param): type_error = _('must be a decimal number') -class Bytes(Param): +class Data(Param): """ - A parameter for binary data (stored in the ``str`` type). - - This class is named *Bytes* instead of *Str* so it's aligned with the - Python v3 ``(str, unicode) => (bytes, str)`` clean-up. See: + Base class for the `Bytes` and `Str` parameters. - http://docs.python.org/3.0/whatsnew/3.0.html + Previously `Str` was as subclass of `Bytes`. Now the common functionality + has been split into this base class so that ``isinstance(foo, Bytes)`` wont + be ``True`` when ``foo`` is actually an `Str` instance (which is confusing). """ - type = str - type_error = _('must be binary data') - kwargs = Param.kwargs + ( ('minlength', int, None), ('maxlength', int, None), ('length', int, None), - ('pattern', str, None), - ) def __init__(self, name, *rules, **kw): - super(Bytes, self).__init__(name, *rules, **kw) + super(Data, self).__init__(name, *rules, **kw) if not ( self.length is None or @@ -749,6 +752,24 @@ class Bytes(Param): self.nice, self.minlength) ) + +class Bytes(Data): + """ + A parameter for binary data (stored in the ``str`` type). + + This class is named *Bytes* instead of *Str* so it's aligned with the + Python v3 ``(str, unicode) => (bytes, str)`` clean-up. See: + + http://docs.python.org/3.0/whatsnew/3.0.html + """ + + type = str + type_error = _('must be binary data') + + kwargs = Data.kwargs + ( + ('pattern', str, None), + ) + def _rule_minlength(self, _, value): """ Check minlength constraint. @@ -780,7 +801,7 @@ class Bytes(Param): ) -class Str(Bytes): +class Str(Data): """ A parameter for Unicode text (stored in the ``unicode`` type). @@ -793,7 +814,7 @@ class Str(Bytes): type = unicode type_error = _('must be Unicode text') - kwargs = Bytes.kwargs[:-1] + ( + kwargs = Data.kwargs + ( ('pattern', unicode, None), ) -- cgit From 47e8b1c0b75b19765d8682e52dcdcfcd66b7760b Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 14 Jan 2009 11:25:26 -0700 Subject: Removed deprecited ipa_types.py and test_ipa_types.py --- ipalib/ipa_types.py | 189 ---------------------------------------------------- 1 file changed, 189 deletions(-) delete mode 100644 ipalib/ipa_types.py (limited to 'ipalib') diff --git a/ipalib/ipa_types.py b/ipalib/ipa_types.py deleted file mode 100644 index 583cceed..00000000 --- a/ipalib/ipa_types.py +++ /dev/null @@ -1,189 +0,0 @@ -# Authors: -# Jason Gerard DeRose -# -# 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 - -""" -Type system for coercing and normalizing input values. -""" - -import re -from plugable import ReadOnly, lock -import errors - - -def check_min_max(min_value, max_value, min_name, max_name): - assert type(min_name) is str, 'min_name must be an str' - assert type(max_name) is str, 'max_name must be an str' - for (name, value) in [(min_name, min_value), (max_name, max_value)]: - if not (value is None or type(value) is int): - raise TypeError( - '%s must be an int or None, got: %r' % (name, value) - ) - if None not in (min_value, max_value) and min_value > max_value: - d = dict( - k0=min_name, - v0=min_value, - k1=max_name, - v1=max_value, - ) - raise ValueError( - '%(k0)s > %(k1)s: %(k0)s=%(v0)r, %(k1)s=%(v1)r' % d - ) - - -class Type(ReadOnly): - """ - Base class for all IPA types. - """ - - def __init__(self, type_): - if type(type_) is not type: - raise TypeError('%r is not %r' % (type(type_), type)) - allowed = (bool, int, float, unicode) - if type_ not in allowed: - raise ValueError('not an allowed type: %r' % type_) - self.type = type_ - # FIXME: This should be replaced with a more user friendly message - # as this is what is returned to the user. - self.conversion_error = 'Must be a %r' % self.type - lock(self) - - def __get_name(self): - """ - Convenience property to return the class name. - """ - return self.__class__.__name__ - name = property(__get_name) - - def convert(self, value): - try: - return self.type(value) - except (TypeError, ValueError): - return None - - def validate(self, value): - pass - - def __call__(self, value): - if value is None: - raise TypeError('value cannot be None') - if type(value) is self.type: - return value - return self.convert(value) - - -class Bool(Type): - def __init__(self, true='Yes', false='No'): - if true is None: - raise TypeError('`true` cannot be None') - if false is None: - raise TypeError('`false` cannot be None') - if true == false: - raise ValueError( - 'cannot be equal: true=%r, false=%r' % (true, false) - ) - self.true = true - self.false = false - super(Bool, self).__init__(bool) - - def convert(self, value): - if value == self.true: - return True - if value == self.false: - return False - return None - - -class Int(Type): - def __init__(self, min_value=None, max_value=None): - check_min_max(min_value, max_value, 'min_value', 'max_value') - self.min_value = min_value - self.max_value = max_value - super(Int, self).__init__(int) - - def validate(self, value): - if type(value) is not self.type: - return 'Must be an integer' - if self.min_value is not None and value < self.min_value: - return 'Cannot be smaller than %d' % self.min_value - if self.max_value is not None and value > self.max_value: - return 'Cannot be larger than %d' % self.max_value - - -class Unicode(Type): - def __init__(self, min_length=None, max_length=None, pattern=None): - check_min_max(min_length, max_length, 'min_length', 'max_length') - if min_length is not None and min_length < 0: - raise ValueError('min_length must be >= 0, got: %r' % min_length) - if max_length is not None and max_length < 1: - raise ValueError('max_length must be >= 1, got: %r' % max_length) - if not (pattern is None or isinstance(pattern, basestring)): - raise TypeError( - 'pattern must be a basestring or None, got: %r' % pattern - ) - self.min_length = min_length - self.max_length = max_length - self.pattern = pattern - if pattern is None: - self.regex = None - else: - self.regex = re.compile(pattern) - super(Unicode, self).__init__(unicode) - - def convert(self, value): - assert type(value) not in (list, tuple) - try: - return self.type(value) - except (TypeError, ValueError): - return None - - def validate(self, value): - if type(value) is not self.type: - return 'Must be a string' - - if self.regex and self.regex.match(value) is None: - return 'Must match %r' % self.pattern - - if self.min_length is not None and len(value) < self.min_length: - return 'Must be at least %d characters long' % self.min_length - - if self.max_length is not None and len(value) > self.max_length: - return 'Can be at most %d characters long' % self.max_length - - -class Enum(Type): - def __init__(self, *values): - if len(values) < 1: - raise ValueError('%s requires at least one value' % self.name) - type_ = type(values[0]) - if type_ not in (unicode, int, float): - raise TypeError( - '%r: %r not unicode, int, nor float' % (values[0], type_) - ) - for val in values[1:]: - if type(val) is not type_: - raise TypeError('%r: %r is not %r' % (val, type(val), type_)) - self.values = values - self.frozenset = frozenset(values) - super(Enum, self).__init__(type_) - - def validate(self, value): - if type(value) is not self.type: - return 'Incorrect type' - if value not in self.frozenset: - return 'Invalid value' -- cgit From f3e0900ebc01d8fae8ce4068b0fae8d14c8069bb Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 14 Jan 2009 11:32:32 -0700 Subject: New Param: ipalib.__init__ no longer import ipa_types and instead imports appropriate classes from parameters --- ipalib/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'ipalib') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index e1ef09c1..cf2e5a6a 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -874,8 +874,8 @@ import plugable from backend import Backend, Context from frontend import Command, LocalOrRemote, Application from frontend import Object, Method, Property -from ipa_types import Bool, Int, Unicode, Enum -from frontend import Param, DefaultFrom +from parameters import DefaultFrom, Bool, Flag, Int, Float, Bytes, Str + def create_api(mode='dummy'): """ -- cgit From 2b2e73e7df90d38175e035d6ada4d752120dc0ec Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 14 Jan 2009 11:39:29 -0700 Subject: Removed depreciated code from frontend.py; frontend.py no longer imports ipa_types --- ipalib/frontend.py | 453 +---------------------------------------------------- 1 file changed, 3 insertions(+), 450 deletions(-) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index c614e547..baa37b17 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -27,7 +27,7 @@ import plugable from plugable import lock, check_name import errors from errors import check_type, check_isinstance, raise_TypeError -import ipa_types +import parameters from util import make_repr @@ -42,453 +42,6 @@ def is_rule(obj): return callable(obj) and getattr(obj, RULE_FLAG, False) is True -class DefaultFrom(plugable.ReadOnly): - """ - Derive a default value from other supplied values. - - For example, say you wanted to create a default for the user's login from - the user's first and last names. It could be implemented like this: - - >>> login = DefaultFrom(lambda first, last: first[0] + last) - >>> login(first='John', last='Doe') - 'JDoe' - - If you do not explicitly provide keys when you create a DefaultFrom - instance, the keys are implicitly derived from your callback by - inspecting ``callback.func_code.co_varnames``. The keys are available - through the ``DefaultFrom.keys`` instance attribute, like this: - - >>> login.keys - ('first', 'last') - - The callback is available through the ``DefaultFrom.callback`` instance - attribute, like this: - - >>> login.callback # doctest:+ELLIPSIS - at 0x...> - >>> login.callback.func_code.co_varnames # The keys - ('first', 'last') - - The keys can be explicitly provided as optional positional arguments after - the callback. For example, this is equivalent to the ``login`` instance - above: - - >>> login2 = DefaultFrom(lambda a, b: a[0] + b, 'first', 'last') - >>> login2.keys - ('first', 'last') - >>> login2.callback.func_code.co_varnames # Not the keys - ('a', 'b') - >>> login2(first='John', last='Doe') - 'JDoe' - - If any keys are missing when calling your DefaultFrom instance, your - callback is not called and None is returned. For example: - - >>> login(first='John', lastname='Doe') is None - True - >>> login() is None - True - - Any additional keys are simply ignored, like this: - - >>> login(last='Doe', first='John', middle='Whatever') - 'JDoe' - - As above, because `DefaultFrom.__call__` takes only pure keyword - arguments, they can be supplied in any order. - - Of course, the callback need not be a lambda expression. This third - example is equivalent to both the ``login`` and ``login2`` instances - above: - - >>> def get_login(first, last): - ... return first[0] + last - ... - >>> login3 = DefaultFrom(get_login) - >>> login3.keys - ('first', 'last') - >>> login3.callback.func_code.co_varnames - ('first', 'last') - >>> login3(first='John', last='Doe') - 'JDoe' - """ - - def __init__(self, callback, *keys): - """ - :param callback: The callable to call when all keys are present. - :param keys: Optional keys used for source values. - """ - if not callable(callback): - raise TypeError('callback must be callable; got %r' % callback) - self.callback = callback - if len(keys) == 0: - fc = callback.func_code - self.keys = fc.co_varnames[:fc.co_argcount] - else: - self.keys = keys - for key in self.keys: - if type(key) is not str: - raise_TypeError(key, str, 'keys') - lock(self) - - def __call__(self, **kw): - """ - If all keys are present, calls the callback; otherwise returns None. - - :param kw: The keyword arguments. - """ - vals = tuple(kw.get(k, None) for k in self.keys) - if None in vals: - return - try: - return self.callback(*vals) - except StandardError: - pass - - -def parse_param_spec(spec): - """ - Parse a param spec into to (name, kw). - - The ``spec`` string determines the param name, whether the param is - required, and whether the param is multivalue according the following - syntax: - - ====== ===== ======== ========== - Spec Name Required Multivalue - ====== ===== ======== ========== - 'var' 'var' True False - 'var?' 'var' False False - 'var*' 'var' False True - 'var+' 'var' True True - ====== ===== ======== ========== - - For example, - - >>> parse_param_spec('login') - ('login', {'required': True, 'multivalue': False}) - >>> parse_param_spec('gecos?') - ('gecos', {'required': False, 'multivalue': False}) - >>> parse_param_spec('telephone_numbers*') - ('telephone_numbers', {'required': False, 'multivalue': True}) - >>> parse_param_spec('group+') - ('group', {'required': True, 'multivalue': True}) - - :param spec: A spec string. - """ - if type(spec) is not str: - raise_TypeError(spec, str, 'spec') - if len(spec) < 2: - raise ValueError( - 'param spec must be at least 2 characters; got %r' % spec - ) - _map = { - '?': dict(required=False, multivalue=False), - '*': dict(required=False, multivalue=True), - '+': dict(required=True, multivalue=True), - } - end = spec[-1] - if end in _map: - return (spec[:-1], _map[end]) - return (spec, dict(required=True, multivalue=False)) - - -class Param(plugable.ReadOnly): - """ - A parameter accepted by a `Command`. - - ============ ================= ================== - Keyword Type Default - ============ ================= ================== - cli_name str defaults to name - type ipa_type.Type ipa_type.Unicode() - doc str "" - required bool True - multivalue bool False - primary_key bool False - normalize callable None - default same as type.type None - default_from callable None - flags frozenset frozenset() - ============ ================= ================== - """ - __nones = (None, '', tuple(), []) - __defaults = dict( - cli_name=None, - type=ipa_types.Unicode(), - doc='', - required=True, - multivalue=False, - primary_key=False, - normalize=None, - default=None, - default_from=None, - flags=frozenset(), - rules=tuple(), - ) - - def __init__(self, name, **override): - self.__param_spec = name - self.__override = override - self.__kw = dict(self.__defaults) - if not ('required' in override or 'multivalue' in override): - (name, kw_from_spec) = parse_param_spec(name) - self.__kw.update(kw_from_spec) - self.__kw['cli_name'] = name - if not set(self.__kw).issuperset(override): - extra = sorted(set(override) - set(self.__kw)) - raise TypeError( - 'Param.__init__() takes no such kwargs: %s' % ', '.join(extra) - ) - self.__kw.update(override) - self.name = check_name(name) - self.cli_name = check_name(self.__kw.get('cli_name', name)) - self.type = self.__check_isinstance(ipa_types.Type, 'type') - self.doc = self.__check_type(str, 'doc') - self.required = self.__check_type(bool, 'required') - self.multivalue = self.__check_type(bool, 'multivalue') - self.default = self.__kw['default'] - df = self.__kw['default_from'] - if callable(df) and not isinstance(df, DefaultFrom): - df = DefaultFrom(df) - self.default_from = check_type(df, DefaultFrom, 'default_from', - allow_none=True - ) - self.flags = frozenset(self.__kw['flags']) - self.__normalize = self.__kw['normalize'] - self.rules = self.__check_type(tuple, 'rules') - self.all_rules = (self.type.validate,) + self.rules - self.primary_key = self.__check_type(bool, 'primary_key') - lock(self) - - def ispassword(self): - """ - Return ``True`` is this Param is a password. - """ - return 'password' in self.flags - - def __clone__(self, **override): - """ - Return a new `Param` instance similar to this one. - """ - kw = dict(self.__kw) - kw.update(override) - return self.__class__(self.name, **kw) - - def __check_type(self, type_, name, allow_none=False): - value = self.__kw[name] - return check_type(value, type_, name, allow_none) - - def __check_isinstance(self, type_, name, allow_none=False): - value = self.__kw[name] - return check_isinstance(value, type_, name, allow_none) - - def __dispatch(self, value, scalar): - """ - Helper method used by `normalize` and `convert`. - """ - if value in self.__nones: - return - if self.multivalue: - if type(value) in (tuple, list): - return tuple( - scalar(v, i) for (i, v) in enumerate(value) - ) - return (scalar(value, 0),) # tuple - return scalar(value) - - def __normalize_scalar(self, value, index=None): - """ - Normalize a scalar value. - - This method is called once with each value in multivalue. - """ - if not isinstance(value, basestring): - return value - try: - return self.__normalize(value) - except StandardError: - return value - - def normalize(self, value): - """ - Normalize ``value`` using normalize callback. - - For example: - - >>> param = Param('telephone', - ... normalize=lambda value: value.replace('.', '-') - ... ) - >>> param.normalize('800.123.4567') - '800-123-4567' - - If this `Param` instance does not have a normalize callback, - ``value`` is returned unchanged. - - If this `Param` instance has a normalize callback and ``value`` is - a basestring, the normalize callback is called and its return value - is returned. - - If ``value`` is not a basestring, or if an exception is caught - when calling the normalize callback, ``value`` is returned unchanged. - - :param value: A proposed value for this parameter. - """ - if self.__normalize is None: - return value - return self.__dispatch(value, self.__normalize_scalar) - - def __convert_scalar(self, value, index=None): - """ - Convert a scalar value. - - This method is called once with each value in multivalue. - """ - if value in self.__nones: - return - converted = self.type(value) - if converted is None: - raise errors.ConversionError( - self.name, value, self.type, index=index - ) - return converted - - def convert(self, value): - """ - Convert/coerce ``value`` to Python type for this `Param`. - - For example: - - >>> param = Param('an_int', type=ipa_types.Int()) - >>> param.convert(7.2) - 7 - >>> param.convert(" 7 ") - 7 - - If ``value`` can not be converted, ConversionError is raised, which - is as subclass of ValidationError. - - If ``value`` is None, conversion is not attempted and None is - returned. - - :param value: A proposed value for this parameter. - """ - return self.__dispatch(value, self.__convert_scalar) - - def __validate_scalar(self, value, index=None): - """ - Validate a scalar value. - - This method is called once with each value in multivalue. - """ - if type(value) is not self.type.type: - raise_TypeError(value, self.type.type, 'value') - for rule in self.rules: - error = rule(value) - if error is not None: - raise errors.RuleError( - self.name, value, error, rule, index=index - ) - - def validate(self, value): - """ - Check validity of a value. - - Each validation rule is called in turn and if any returns and error, - RuleError is raised, which is a subclass of ValidationError. - - :param value: A proposed value for this parameter. - """ - if value is None: - if self.required: - raise errors.RequirementError(self.name) - return - if self.multivalue: - if type(value) is not tuple: - raise_TypeError(value, tuple, 'value') - for (i, v) in enumerate(value): - self.__validate_scalar(v, i) - else: - self.__validate_scalar(value) - - def get_default(self, **kw): - """ - Return a default value for this parameter. - - If this `Param` instance does not have a default_from() callback, this - method always returns the static Param.default instance attribute. - - On the other hand, if this `Param` instance has a default_from() - callback, the callback is called and its return value is returned - (assuming that value is not None). - - If the default_from() callback returns None, or if an exception is - caught when calling the default_from() callback, the static - Param.default instance attribute is returned. - - :param kw: Optional keyword arguments to pass to default_from(). - """ - if self.default_from is not None: - default = self.default_from(**kw) - if default is not None: - try: - return self.convert(self.normalize(default)) - except errors.ValidationError: - return None - return self.default - - def get_values(self): - """ - Return a tuple of possible values. - - For enumerable types, a tuple containing the possible values is - returned. For all other types, an empty tuple is returned. - """ - if self.type.name in ('Enum', 'CallbackEnum'): - return self.type.values - return tuple() - - def __call__(self, value, **kw): - if value in self.__nones: - value = self.get_default(**kw) - else: - value = self.convert(self.normalize(value)) - self.validate(value) - return value - - def __repr__(self): - """ - Return an expresion that could construct this `Param` instance. - """ - return make_repr( - self.__class__.__name__, - self.__param_spec, - **self.__override - ) - - -def create_param(spec): - """ - Create a `Param` instance from a param spec. - - If ``spec`` is a `Param` instance, ``spec`` is returned unchanged. - - If ``spec`` is an str instance, then ``spec`` is parsed and an - appropriate `Param` instance is created and returned. - - See `parse_param_spec` for the definition of the spec syntax. - - :param spec: A spec string or a `Param` instance. - """ - if type(spec) is Param: - return spec - if type(spec) is not str: - raise TypeError( - 'create_param() takes %r or %r; got %r' % (str, Param, spec) - ) - return Param(spec) - - class Command(plugable.Plugin): """ A public IPA atomic operation. @@ -810,7 +363,7 @@ class LocalOrRemote(Command): """ takes_options = ( - Param('server?', type=ipa_types.Bool(), default=False, + parameters.Flag('server?', doc='Forward to server instead of running locally', ), ) @@ -1064,7 +617,7 @@ class Property(Attribute): 'type', )).union(Attribute.__public__) - type = ipa_types.Unicode() + type = parameters.Str required = False multivalue = False default = None -- cgit From 69acff450c043bdd7d70da473c3adafdd9d3fe03 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 14 Jan 2009 12:00:47 -0700 Subject: New Param: removed more depreciated 'import ipa_types' --- ipalib/cli.py | 3 +-- ipalib/frontend.py | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index 442e5061..f6c187b3 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -36,7 +36,6 @@ import frontend import backend import errors import plugable -import ipa_types import util from constants import CLI_TAB @@ -801,7 +800,7 @@ class CLI(object): ) if 'password' in option.flags: kw['action'] = 'store_true' - elif isinstance(option.type, ipa_types.Bool): + elif option.type is bool: if option.default is True: kw['action'] = 'store_false' else: diff --git a/ipalib/frontend.py b/ipalib/frontend.py index baa37b17..64323d0a 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -28,6 +28,7 @@ from plugable import lock, check_name import errors from errors import check_type, check_isinstance, raise_TypeError import parameters +from parameters import create_param from util import make_repr -- cgit From 09e2f5d615a17943ba572fd02a2e0d9b15ca1076 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 14 Jan 2009 13:17:30 -0700 Subject: New Param: got most of unit tests ported (still have 6 errors); haven't ported doctests yet --- ipalib/frontend.py | 48 ++++++++++++++++++++++-------------------------- 1 file changed, 22 insertions(+), 26 deletions(-) (limited to 'ipalib') diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 64323d0a..98ecc46b 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -28,7 +28,7 @@ from plugable import lock, check_name import errors from errors import check_type, check_isinstance, raise_TypeError import parameters -from parameters import create_param +from parameters import create_param, Param from util import make_repr @@ -217,13 +217,12 @@ class Command(plugable.Plugin): Generator method used by `Command.get_default`. """ for param in self.params(): - if kw.get(param.name, None) is None: - if param.required: - yield (param.name, param.get_default(**kw)) - elif isinstance(param.type, ipa_types.Bool): - yield (param.name, param.default) - else: - yield (param.name, None) + if param.name in kw: + continue + if param.required or param.autofill: + default = param.get_default(**kw) + if default is not None: + yield (param.name, default) def validate(self, **kw): """ @@ -454,7 +453,7 @@ class Object(plugable.Plugin): if type(spec) is str: key = spec.rstrip('?*+') else: - assert type(spec) is Param + assert isinstance(spec, Param) key = spec.name if key in props: yield props.pop(key).param @@ -618,29 +617,26 @@ class Property(Attribute): 'type', )).union(Attribute.__public__) - type = parameters.Str - required = False - multivalue = False + klass = parameters.Str default = None default_from = None - normalize = None + normalizer = None def __init__(self): super(Property, self).__init__() - self.rules = tuple(sorted( - self.__rules_iter(), - key=lambda f: getattr(f, '__name__'), - )) - self.param = Param(self.attr_name, - type=self.type, - doc=self.doc, - required=self.required, - multivalue=self.multivalue, - default=self.default, - default_from=self.default_from, - rules=self.rules, - normalize=self.normalize, + self.rules = tuple( + sorted(self.__rules_iter(), key=lambda f: getattr(f, '__name__')) ) + self.kwargs = tuple( + sorted(self.__kw_iter(), key=lambda keyvalue: keyvalue[0]) + ) + kw = dict(self.kwargs) + self.param = self.klass(self.attr_name, *self.rules, **kw) + + def __kw_iter(self): + for (key, kind, default) in self.klass.kwargs: + if getattr(self, key, None) is not None: + yield (key, getattr(self, key)) def __rules_iter(self): """ -- cgit From 79422d048959a7f6a5fff981caf91de924788e85 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 14 Jan 2009 13:51:37 -0700 Subject: All unit tests now working (except for doctests and Rob's xmlrpc tests) --- ipalib/crud.py | 4 ++-- ipalib/parameters.py | 8 +++++++- 2 files changed, 9 insertions(+), 3 deletions(-) (limited to 'ipalib') diff --git a/ipalib/crud.py b/ipalib/crud.py index 867f9fe1..d34a7c57 100644 --- a/ipalib/crud.py +++ b/ipalib/crud.py @@ -56,7 +56,7 @@ class Mod(frontend.Method): def get_options(self): for param in self.obj.params_minus_pk(): - yield param.__clone__(required=False) + yield param.clone(required=False) for option in self.takes_options: yield option @@ -67,7 +67,7 @@ class Find(frontend.Method): def get_options(self): for param in self.obj.params_minus_pk(): - yield param.__clone__(required=False) + yield param.clone(required=False) for option in self.takes_options: yield option diff --git a/ipalib/parameters.py b/ipalib/parameters.py index da01bfcf..4d46297c 100644 --- a/ipalib/parameters.py +++ b/ipalib/parameters.py @@ -521,7 +521,13 @@ class Param(ReadOnly): for rule in self.all_rules: error = rule(ugettext, value) if error is not None: - raise ValidationError(name=self.name, error=error, index=index) + raise ValidationError( + name=self.name, + value=value, + index=index, + error=error, + rule=rule, + ) def get_default(self, **kw): """ -- cgit From cd3508bacee20c01640964470b0c623691b3c216 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 14 Jan 2009 14:04:05 -0700 Subject: New Param: added Param.query kwarg for crud operations like Retrieve and Search where criteria should not be validated --- ipalib/crud.py | 5 +++-- ipalib/parameters.py | 3 +++ 2 files changed, 6 insertions(+), 2 deletions(-) (limited to 'ipalib') diff --git a/ipalib/crud.py b/ipalib/crud.py index d34a7c57..345fc270 100644 --- a/ipalib/crud.py +++ b/ipalib/crud.py @@ -50,13 +50,14 @@ class Del(frontend.Method): for option in self.takes_options: yield option + class Mod(frontend.Method): def get_args(self): yield self.obj.primary_key def get_options(self): for param in self.obj.params_minus_pk(): - yield param.clone(required=False) + yield param.clone(required=False, query=True) for option in self.takes_options: yield option @@ -67,7 +68,7 @@ class Find(frontend.Method): def get_options(self): for param in self.obj.params_minus_pk(): - yield param.clone(required=False) + yield param.clone(required=False, query=True) for option in self.takes_options: yield option diff --git a/ipalib/parameters.py b/ipalib/parameters.py index 4d46297c..ff088ff8 100644 --- a/ipalib/parameters.py +++ b/ipalib/parameters.py @@ -229,6 +229,7 @@ class Param(ReadOnly): ('default_from', DefaultFrom, None), ('create_default', callable, None), ('autofill', bool, False), + ('query', bool, False), ('flags', frozenset, frozenset()), # The 'default' kwarg gets appended in Param.__init__(): @@ -489,6 +490,8 @@ class Param(ReadOnly): :param value: A proposed value for this parameter. """ + if self.query: + return if value is None: if self.required: raise RequirementError(name=self.name) -- cgit From 0327b83899389e38aebde9de4219f64a716e611d Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 14 Jan 2009 20:36:17 -0700 Subject: New Param: all docstring examples now pass under doctests --- ipalib/__init__.py | 28 ++++++++++++++-------------- ipalib/frontend.py | 30 ++++++++++++++++-------------- ipalib/parameters.py | 26 ++++++++++++++++++++++++-- 3 files changed, 54 insertions(+), 30 deletions(-) (limited to 'ipalib') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index cf2e5a6a..40d3a744 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -405,13 +405,13 @@ Defining arguments and options for your command You can define a command that will accept specific arguments and options. For example: ->>> from ipalib import Param +>>> from ipalib import Str >>> class nudge(Command): ... """Takes one argument, one option""" ... ... takes_args = ['programmer'] ... -... takes_options = [Param('stuff', default=u'documentation')] +... takes_options = [Str('stuff', default=u'documentation')] ... ... def execute(self, programmer, **kw): ... return '%s, go write more %s!' % (programmer, kw['stuff']) @@ -420,9 +420,9 @@ For example: >>> api.env.in_server = True >>> api.register(nudge) >>> api.finalize() ->>> api.Command.nudge('Jason') +>>> api.Command.nudge(u'Jason') u'Jason, go write more documentation!' ->>> api.Command.nudge('Jason', stuff='unit tests') +>>> api.Command.nudge(u'Jason', stuff=u'unit tests') u'Jason, go write more unit tests!' The ``args`` and ``options`` attributes are `plugable.NameSpace` instances @@ -431,11 +431,11 @@ containing a command's arguments and options, respectively, as you can see: >>> list(api.Command.nudge.args) # Iterates through argument names ['programmer'] >>> api.Command.nudge.args.programmer -Param('programmer') +Str('programmer') >>> list(api.Command.nudge.options) # Iterates through option names ['stuff'] >>> api.Command.nudge.options.stuff -Param('stuff', default=u'documentation') +Str('stuff', default=u'documentation') >>> api.Command.nudge.options.stuff.default u'documentation' @@ -451,7 +451,7 @@ NameSpace(<2 members>, sort=False) When calling a command, its positional arguments can also be provided as keyword arguments, and in any order. For example: ->>> api.Command.nudge(stuff='lines of code', programmer='Jason') +>>> api.Command.nudge(stuff=u'lines of code', programmer=u'Jason') u'Jason, go write more lines of code!' When a command plugin is called, the values supplied for its parameters are @@ -465,20 +465,20 @@ here is a quick teaser: ... takes_options = [ ... 'first', ... 'last', -... Param('nick', -... normalize=lambda value: value.lower(), +... Str('nick', +... normalizer=lambda value: value.lower(), ... default_from=lambda first, last: first[0] + last, ... ), -... Param('points', type=Int(), default=0), +... Int('points', default=0), ... ] ... >>> cp = create_player() >>> cp.finalize() ->>> cp.convert(points=" 1000 ") +>>> cp.convert(points=u' 1000 ') {'points': 1000} >>> cp.normalize(nick=u'NickName') {'nick': u'nickname'} ->>> cp.get_default(first='Jason', last='DeRose') +>>> cp.get_default(first=u'Jason', last=u'DeRose') {'nick': u'jderose', 'points': 0} For the full details on the parameter system, see the @@ -575,7 +575,7 @@ For example, say we setup a command like this: ... ... takes_args = ['key?'] ... -... takes_options = [Param('reverse', type=Bool(), default=False)] +... takes_options = [Flag('reverse')] ... ... def execute(self, key, **options): ... items = dict( @@ -643,7 +643,7 @@ show-items: Lastly, providing a ``key`` would result in the following: ->>> result = api.Command.show_items('city') +>>> result = api.Command.show_items(u'city') >>> api.Command.show_items.output_for_cli(textui, result, 'city', reverse=False) city = 'Berlin' diff --git a/ipalib/frontend.py b/ipalib/frontend.py index 98ecc46b..b30205fe 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -27,8 +27,7 @@ import plugable from plugable import lock, check_name import errors from errors import check_type, check_isinstance, raise_TypeError -import parameters -from parameters import create_param, Param +from parameters import create_param, Param, Str, Flag from util import make_repr @@ -53,7 +52,8 @@ class Command(plugable.Plugin): Plugins that subclass from Command are registered in the ``api.Command`` namespace. For example: - >>> api = plugable.API(Command) + >>> from ipalib import create_api + >>> api = create_api() >>> class my_command(Command): ... pass ... @@ -161,14 +161,14 @@ class Command(plugable.Plugin): >>> class my_command(Command): ... takes_options = ( - ... Param('first', normalize=lambda value: value.lower()), + ... Param('first', normalizer=lambda value: value.lower()), ... Param('last'), ... ) ... >>> c = my_command() >>> c.finalize() - >>> c.normalize(first='JOHN', last='DOE') - {'last': 'DOE', 'first': 'john'} + >>> c.normalize(first=u'JOHN', last=u'DOE') + {'last': u'DOE', 'first': u'john'} """ return dict( (k, self.params[k].normalize(v)) for (k, v) in kw.iteritems() @@ -178,10 +178,10 @@ class Command(plugable.Plugin): """ Return a dictionary of values converted to correct type. - >>> from ipalib import ipa_types + >>> from ipalib import Int >>> class my_command(Command): ... takes_args = ( - ... Param('one', type=ipa_types.Int()), + ... Int('one'), ... 'two', ... ) ... @@ -200,14 +200,15 @@ class Command(plugable.Plugin): For example: + >>> from ipalib import Str >>> class my_command(Command): - ... takes_args = [Param('color', default='Red')] + ... takes_args = [Str('color', default=u'Red')] ... >>> c = my_command() >>> c.finalize() >>> c.get_default() - {'color': 'Red'} - >>> c.get_default(color='Yellow') + {'color': u'Red'} + >>> c.get_default(color=u'Yellow') {} """ return dict(self.__get_default_iter(kw)) @@ -363,7 +364,7 @@ class LocalOrRemote(Command): """ takes_options = ( - parameters.Flag('server?', + Flag('server?', doc='Forward to server instead of running locally', ), ) @@ -562,7 +563,8 @@ class Method(Attribute, Command): say you created a `Method` plugin and its corresponding `Object` plugin like this: - >>> api = plugable.API(Command, Object, Method, Property) + >>> from ipalib import create_api + >>> api = create_api() >>> class user_add(Method): ... def run(self): ... return 'Added the user!' @@ -617,7 +619,7 @@ class Property(Attribute): 'type', )).union(Attribute.__public__) - klass = parameters.Str + klass = Str default = None default_from = None normalizer = None diff --git a/ipalib/parameters.py b/ipalib/parameters.py index ff088ff8..cf658a41 100644 --- a/ipalib/parameters.py +++ b/ipalib/parameters.py @@ -490,6 +490,7 @@ class Param(ReadOnly): :param value: A proposed value for this parameter. """ + # FIXME: this should be after 'if value is None:' if self.query: return if value is None: @@ -695,7 +696,28 @@ class Flag(Bool): super(Flag, self).__init__(name, *rules, **kw) -class Int(Param): +class Number(Param): + """ + Base class for the `Int` and `Float` parameters. + """ + + def _convert_scalar(self, value, index=None): + """ + Convert a single scalar value. + """ + if type(value) is self.type: + return value + if type(value) in (unicode, int, float): + try: + return self.type(value) + except ValueError: + pass + raise ConversionError(name=self.name, index=index, + error=ugettext(self.type_error), + ) + + +class Int(Number): """ A parameter for integer values (stored in the ``int`` type). """ @@ -704,7 +726,7 @@ class Int(Param): type_error = _('must be an integer') -class Float(Param): +class Float(Number): """ A parameter for floating-point values (stored in the ``float`` type). """ -- cgit From 39068ab7ca4cb2346c3c7079a435fb82ebd29591 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 14 Jan 2009 21:11:14 -0700 Subject: Fixed automount plugins module to where it can at least be imported --- ipalib/plugins/f_automount.py | 76 +++++++++++++++++++++++-------------------- 1 file changed, 40 insertions(+), 36 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugins/f_automount.py b/ipalib/plugins/f_automount.py index 4c392438..2365ce22 100644 --- a/ipalib/plugins/f_automount.py +++ b/ipalib/plugins/f_automount.py @@ -23,13 +23,9 @@ Frontend plugins for automount. RFC 2707bis http://www.padl.com/~lukeh/rfc2307bis.txt """ -from ipalib import frontend -from ipalib import crud -from ipalib.frontend import Param -from ipalib import api -from ipalib import errors -from ipalib import ipa_types from ldap import explode_dn +from ipalib import crud, errors +from ipalib import api, Str, Flag, Object, Command map_attributes = ['automountMapName', 'description', ] key_attributes = ['description', 'automountKey', 'automountInformation'] @@ -57,12 +53,12 @@ def make_automount_dn(mapname): api.env.basedn, ) -class automount(frontend.Object): +class automount(Object): """ Automount object. """ takes_params = ( - Param('automountmapname', + Str('automountmapname', cli_name='mapname', primary_key=True, doc='A group of related automount objects', @@ -73,8 +69,9 @@ api.register(automount) class automount_addmap(crud.Add): 'Add a new automount map.' + takes_options = ( - Param('description?', + Str('description?', doc='A description of the automount map'), ) @@ -96,6 +93,7 @@ class automount_addmap(crud.Add): kw['objectClass'] = ['automountMap'] return ldap.create(**kw) + def output_for_cli(self, textui, result, map, **options): """ Output result of this command to command line interface. @@ -108,13 +106,13 @@ api.register(automount_addmap) class automount_addkey(crud.Add): 'Add a new automount key.' takes_options = ( - Param('automountkey', + Str('automountkey', cli_name='key', doc='An entry in an automount map'), - Param('automountinformation', + Str('automountinformation', cli_name='info', doc='Mount information for this key'), - Param('description?', + Str('description?', doc='A description of the mount'), ) @@ -138,6 +136,7 @@ class automount_addkey(crud.Add): kw['objectClass'] = ['automount'] return ldap.create(**kw) + def output_for_cli(self, textui, result, *args, **options): """ Output result of this command to command line interface. @@ -177,7 +176,7 @@ api.register(automount_delmap) class automount_delkey(crud.Del): 'Delete an automount key.' takes_options = ( - Param('automountkey', + Str('automountkey', cli_name='key', doc='The automount key to remove'), ) @@ -213,7 +212,7 @@ api.register(automount_delkey) class automount_modmap(crud.Mod): 'Edit an existing automount map.' takes_options = ( - Param('description?', + Str('description?', doc='A description of the automount map'), ) def execute(self, mapname, **kw): @@ -246,13 +245,13 @@ api.register(automount_modmap) class automount_modkey(crud.Mod): 'Edit an existing automount key.' takes_options = ( - Param('automountkey', + Str('automountkey', cli_name='key', doc='An entry in an automount map'), - Param('automountinformation?', + Str('automountinformation?', cli_name='info', doc='Mount information for this key'), - Param('description?', + Str('description?', doc='A description of the automount map'), ) def execute(self, mapname, **kw): @@ -293,7 +292,7 @@ api.register(automount_modkey) class automount_findmap(crud.Find): 'Search automount maps.' takes_options = ( - Param('all?', type=ipa_types.Bool(), doc='Retrieve all attributes'), + Flag('all', doc='Retrieve all attributes'), ) def execute(self, term, **kw): ldap = self.api.Backend.ldap @@ -331,10 +330,10 @@ api.register(automount_findmap) class automount_findkey(crud.Find): 'Search automount keys.' takes_options = ( - Param('all?', type=ipa_types.Bool(), doc='Retrieve all attributes'), + Flag('all?', doc='Retrieve all attributes'), ) def get_args(self): - return (Param('automountkey', + return (Str('automountkey', cli_name='key', doc='An entry in an automount map'),) def execute(self, term, **kw): @@ -372,7 +371,7 @@ api.register(automount_findkey) class automount_showmap(crud.Get): 'Examine an existing automount map.' takes_options = ( - Param('all?', type=ipa_types.Bool(), doc='Retrieve all attributes'), + Flag('all?', doc='Retrieve all attributes'), ) def execute(self, mapname, **kw): """ @@ -400,10 +399,10 @@ api.register(automount_showmap) class automount_showkey(crud.Get): 'Examine an existing automount key.' takes_options = ( - Param('automountkey', + Str('automountkey', cli_name='key', doc='The automount key to display'), - Param('all?', type=ipa_types.Bool(), doc='Retrieve all attributes'), + Flag('all?', doc='Retrieve all attributes'), ) def execute(self, mapname, **kw): """ @@ -446,10 +445,10 @@ class automount_showkey(crud.Get): api.register(automount_showkey) -class automount_getkeys(frontend.Command): +class automount_getkeys(Command): 'Retrieve all keys for an automount map.' takes_args = ( - Param('automountmapname', + Str('automountmapname', cli_name='mapname', primary_key=True, doc='A group of related automount objects', @@ -478,10 +477,10 @@ class automount_getkeys(frontend.Command): api.register(automount_getkeys) -class automount_getmaps(frontend.Command): +class automount_getmaps(Command): 'Retrieve all automount maps' takes_args = ( - Param('automountmapname?', + Str('automountmapname?', cli_name='mapname', primary_key=True, doc='A group of related automount objects', @@ -510,17 +509,23 @@ class automount_getmaps(frontend.Command): api.register(automount_getmaps) class automount_addindirectmap(crud.Add): - 'Add a new automap indirect mount point.' + """ + Add a new automap indirect mount point. + """ + takes_options = ( - Param('parentmap?', + Str('parentmap?', cli_name='parentmap', - default='auto.master', - doc='The parent map to connect this to. Default: auto.master'), - Param('automountkey', + default=u'auto.master', + doc='The parent map to connect this to.', + ), + Str('automountkey', cli_name='key', - doc='An entry in an automount map'), - Param('description?', - doc='A description of the automount map'), + doc='An entry in an automount map', + ), + Str('description?', + doc='A description of the automount map', + ), ) def execute(self, mapname, **kw): @@ -556,4 +561,3 @@ class automount_addindirectmap(crud.Add): textui.print_plain("Indirect automount map %s added" % map) api.register(automount_addindirectmap) - -- cgit From 29e5a58795da7283eb5976d14f8e5344d4db0e28 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 14 Jan 2009 21:23:20 -0700 Subject: Updated group plugins module to where it can at least be imported --- ipalib/plugins/f_group.py | 38 +++++++++++++++++--------------------- 1 file changed, 17 insertions(+), 21 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugins/f_group.py b/ipalib/plugins/f_group.py index 803e5d00..c9d7b86b 100644 --- a/ipalib/plugins/f_group.py +++ b/ipalib/plugins/f_group.py @@ -21,12 +21,9 @@ Frontend plugins for group (Identity). """ -from ipalib import frontend -from ipalib import crud -from ipalib.frontend import Param -from ipalib import api -from ipalib import errors -from ipalib import ipa_types +from ipalib import api, crud, errors +from ipalib import Object, Command # Plugin base classes +from ipalib import Str, Int # Parameter types def get_members(members): @@ -42,24 +39,23 @@ def get_members(members): return members -class group(frontend.Object): +class group(Object): """ Group object. """ takes_params = ( - Param('description', + Str('description', doc='A description of this group', ), - Param('gidnumber?', + Int('gidnumber?', cli_name='gid', - type=ipa_types.Int(), doc='The gid to use for this group. If not included one is automatically set.', ), - Param('cn', + Str('cn', cli_name='name', primary_key=True, - normalize=lambda value: value.lower(), - ) + normalizer=lambda value: value.lower(), + ), ) api.register(group) @@ -256,14 +252,14 @@ class group_show(crud.Get): api.register(group_show) -class group_add_member(frontend.Command): +class group_add_member(Command): 'Add a member to a group.' takes_args = ( - Param('group', primary_key=True), + Str('group', primary_key=True), ) takes_options = ( - Param('users?', doc='comma-separated list of users to add'), - Param('groups?', doc='comma-separated list of groups to add'), + Str('users?', doc='comma-separated list of users to add'), + Str('groups?', doc='comma-separated list of groups to add'), ) def execute(self, cn, **kw): """ @@ -323,14 +319,14 @@ class group_add_member(frontend.Command): api.register(group_add_member) -class group_remove_member(frontend.Command): +class group_remove_member(Command): 'Remove a member from a group.' takes_args = ( - Param('group', primary_key=True), + Str('group', primary_key=True), ) takes_options = ( - Param('users?', doc='comma-separated list of users to remove'), - Param('groups?', doc='comma-separated list of groups to remove'), + Str('users?', doc='comma-separated list of users to remove'), + Str('groups?', doc='comma-separated list of groups to remove'), ) def execute(self, cn, **kw): """ -- cgit From ec14fbfbc5f822490ed5266f1b0a8eb110a13a98 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 14 Jan 2009 21:55:04 -0700 Subject: Updated host plugins module to where it can at least be imported --- ipalib/plugins/f_host.py | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugins/f_host.py b/ipalib/plugins/f_host.py index 7903ff90..bb800b50 100644 --- a/ipalib/plugins/f_host.py +++ b/ipalib/plugins/f_host.py @@ -21,13 +21,9 @@ Frontend plugins for host/machine Identity. """ -from ipalib import frontend -from ipalib import crud -from ipalib import util -from ipalib.frontend import Param -from ipalib import api -from ipalib import errors -from ipalib import ipa_types +from ipalib import api, crud, errors +from ipalib import Object # Plugin base class +from ipalib import Str, Flag # Parameter types def get_host(hostname): @@ -57,37 +53,36 @@ def validate_host(cn): default_attributes = ['cn','description','localityname','nshostlocation','nshardwareplatform','nsosversion'] -class host(frontend.Object): +class host(Object): """ Host object. """ takes_params = ( - Param('cn', + Str('cn', validate_host, cli_name='hostname', primary_key=True, - normalize=lambda value: value.lower(), - rules=(validate_host,) + normalizer=lambda value: value.lower(), ), - Param('description?', + Str('description?', doc='Description of the host', ), - Param('localityname?', + Str('localityname?', cli_name='locality', doc='Locality of this host (Baltimore, MD)', ), - Param('nshostlocation?', + Str('nshostlocation?', cli_name='location', doc='Location of this host (e.g. Lab 2)', ), - Param('nshardwareplatform?', + Str('nshardwareplatform?', cli_name='platform', doc='Hardware platform of this host (e.g. Lenovo T61)', ), - Param('nsosversion?', + Str('nsosversion?', cli_name='os', doc='Operating System and version on this host (e.g. Fedora 9)', ), - Param('userpassword?', + Str('userpassword?', cli_name='password', doc='Set a password to be used in bulk enrollment', ), @@ -212,7 +207,7 @@ api.register(host_mod) class host_find(crud.Find): 'Search the hosts.' takes_options = ( - Param('all?', type=ipa_types.Bool(), doc='Retrieve all attributes'), + Flag('all', doc='Retrieve all attributes'), ) def get_args(self): """ @@ -258,7 +253,7 @@ api.register(host_find) class host_show(crud.Get): 'Examine an existing host.' takes_options = ( - Param('all?', type=ipa_types.Bool(), doc='Display all host attributes'), + Flag('all', doc='Display all host attributes'), ) def execute(self, hostname, **kw): """ -- cgit From 64c072b7b3deb1800c242b365e277591f056093d Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 14 Jan 2009 22:10:09 -0700 Subject: Updated hostgroup plugins module to where it can at least be imported --- ipalib/plugins/f_hostgroup.py | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugins/f_hostgroup.py b/ipalib/plugins/f_hostgroup.py index 3e14b09a..c365c918 100644 --- a/ipalib/plugins/f_hostgroup.py +++ b/ipalib/plugins/f_hostgroup.py @@ -21,12 +21,10 @@ Frontend plugins for groups of hosts """ -from ipalib import frontend -from ipalib import crud -from ipalib.frontend import Param -from ipalib import api -from ipalib import errors -from ipalib import ipa_types +from ipalib import api, crud, errors +from ipalib import Object, Command # Plugin base classes +from ipalib import Str # Parameter types + hostgroup_filter = "groupofnames)(!(objectclass=posixGroup)" @@ -43,18 +41,18 @@ def get_members(members): return members -class hostgroup(frontend.Object): +class hostgroup(Object): """ Host Group object. """ takes_params = ( - Param('description', + Str('description', doc='A description of this group', ), - Param('cn', + Str('cn', cli_name='name', primary_key=True, - normalize=lambda value: value.lower(), + normalizer=lambda value: value.lower(), ) ) api.register(hostgroup) @@ -220,14 +218,14 @@ class hostgroup_show(crud.Get): api.register(hostgroup_show) -class hostgroup_add_member(frontend.Command): +class hostgroup_add_member(Command): 'Add a member to a group.' takes_args = ( - Param('group', primary_key=True), + Str('group', primary_key=True), ) takes_options = ( - Param('groups?', doc='comma-separated list of host groups to add'), - Param('hosts?', doc='comma-separated list of hosts to add'), + Str('groups?', doc='comma-separated list of host groups to add'), + Str('hosts?', doc='comma-separated list of hosts to add'), ) def execute(self, cn, **kw): """ @@ -288,14 +286,14 @@ class hostgroup_add_member(frontend.Command): api.register(hostgroup_add_member) -class hostgroup_remove_member(frontend.Command): +class hostgroup_remove_member(Command): 'Remove a member from a group.' takes_args = ( - Param('group', primary_key=True), + Str('group', primary_key=True), ) takes_options = ( - Param('hosts?', doc='comma-separated list of hosts to add'), - Param('groups?', doc='comma-separated list of groups to remove'), + Str('hosts?', doc='comma-separated list of hosts to add'), + Str('groups?', doc='comma-separated list of groups to remove'), ) def execute(self, cn, **kw): """ -- cgit From ec86208a9007ec9febca620c777b80b20e9c360d Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 14 Jan 2009 22:19:31 -0700 Subject: Updated passwd plugins module to where it can at least be imported --- ipalib/__init__.py | 2 +- ipalib/parameters.py | 6 ++++++ ipalib/plugins/f_group.py | 2 +- ipalib/plugins/f_passwd.py | 16 +++++++--------- 4 files changed, 15 insertions(+), 11 deletions(-) (limited to 'ipalib') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index 40d3a744..e5aa65d6 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -874,7 +874,7 @@ import plugable from backend import Backend, Context from frontend import Command, LocalOrRemote, Application from frontend import Object, Method, Property -from parameters import DefaultFrom, Bool, Flag, Int, Float, Bytes, Str +from parameters import DefaultFrom, Bool, Flag, Int, Float, Bytes, Str, Password def create_api(mode='dummy'): diff --git a/ipalib/parameters.py b/ipalib/parameters.py index cf658a41..fd693e71 100644 --- a/ipalib/parameters.py +++ b/ipalib/parameters.py @@ -892,6 +892,12 @@ class Str(Data): ) +class Password(Str): + """ + A parameter for passwords (stored in the ``unicode`` type). + """ + + def create_param(spec): """ Create an `Str` instance from the shorthand ``spec``. diff --git a/ipalib/plugins/f_group.py b/ipalib/plugins/f_group.py index c9d7b86b..740b32f8 100644 --- a/ipalib/plugins/f_group.py +++ b/ipalib/plugins/f_group.py @@ -23,7 +23,7 @@ Frontend plugins for group (Identity). from ipalib import api, crud, errors from ipalib import Object, Command # Plugin base classes -from ipalib import Str, Int # Parameter types +from ipalib import Str, Int # Parameter types def get_members(members): diff --git a/ipalib/plugins/f_passwd.py b/ipalib/plugins/f_passwd.py index 1e0dfc1c..ea78c4c1 100644 --- a/ipalib/plugins/f_passwd.py +++ b/ipalib/plugins/f_passwd.py @@ -21,23 +21,21 @@ Frontend plugins for password changes. """ -from ipalib import frontend -from ipalib.frontend import Param -from ipalib import api -from ipalib import errors -from ipalib import ipa_types -from ipalib import util +from ipalib import api, errors, util +from ipalib import Command # Plugin base classes +from ipalib import Str, Password # Parameter types -class passwd(frontend.Command): + +class passwd(Command): 'Edit existing password policy.' takes_args = ( - Param('principal', + Str('principal', cli_name='user', primary_key=True, default_from=util.get_current_principal, ), - Param('password', flags=['password']), + Password('password'), ) def execute(self, principal, password): -- cgit From a41a7f406f5e5192ed7a8c1b05c76de1826f0d7b Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 14 Jan 2009 22:25:45 -0700 Subject: Updated pwpolicy plugins module to where it can at least be imported --- ipalib/plugins/f_pwpolicy.py | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugins/f_pwpolicy.py b/ipalib/plugins/f_pwpolicy.py index 87a7d8fa..d914ce72 100644 --- a/ipalib/plugins/f_pwpolicy.py +++ b/ipalib/plugins/f_pwpolicy.py @@ -21,40 +21,32 @@ Frontend plugins for password policy. """ -from ipalib import frontend -from ipalib import crud -from ipalib.frontend import Param from ipalib import api -from ipalib import errors -from ipalib import ipa_types +from ipalib import Command # Plugin base classes +from ipalib import Int # Parameter types -class pwpolicy_mod(frontend.Command): +class pwpolicy_mod(Command): 'Edit existing password policy.' takes_options = ( - Param('krbmaxpwdlife?', + Int('krbmaxpwdlife?', cli_name='maxlife', - type=ipa_types.Int(), doc='Max. Password Lifetime (days)' ), - Param('krbminpwdlife?', + Int('krbminpwdlife?', cli_name='minlife', - type=ipa_types.Int(), doc='Min. Password Lifetime (hours)' ), - Param('krbpwdhistorylength?', + Int('krbpwdhistorylength?', cli_name='history', - type=ipa_types.Int(), doc='Password History Size' ), - Param('krbpwdmindiffchars?', + Int('krbpwdmindiffchars?', cli_name='minclasses', - type=ipa_types.Int(), doc='Min. Number of Character Classes' ), - Param('krbpwdminlength?', + Int('krbpwdminlength?', cli_name='minlength', - type=ipa_types.Int(), doc='Min. Length of Password' ), ) @@ -94,7 +86,7 @@ class pwpolicy_mod(frontend.Command): api.register(pwpolicy_mod) -class pwpolicy_show(frontend.Command): +class pwpolicy_show(Command): 'Retrieve current password policy' def execute(self, *args, **kw): """ -- cgit From 86b7ebf717a99276a135a5888246cee99798a94b Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 14 Jan 2009 22:29:59 -0700 Subject: Updated ra plugins module to where it can at least be imported --- ipalib/plugins/f_ra.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugins/f_ra.py b/ipalib/plugins/f_ra.py index 724cbf5e..7ac84e65 100644 --- a/ipalib/plugins/f_ra.py +++ b/ipalib/plugins/f_ra.py @@ -22,8 +22,7 @@ Frontend plugins for IPA-RA PKI operations. """ -from ipalib import api, Command, Param -from ipalib import cli +from ipalib import api, Command, Str, Int class request_certificate(Command): @@ -31,7 +30,7 @@ class request_certificate(Command): takes_args = ['csr'] - takes_options = [Param('request_type?', default='pkcs10')] + takes_options = [Str('request_type?', default=u'pkcs10')] def execute(self, csr, **options): return self.Backend.ra.request_certificate(csr, **options) @@ -85,7 +84,8 @@ class revoke_certificate(Command): takes_args = ['serial_number'] - takes_options = [Param('revocation_reason?', default=0)] + # FIXME: The default is 0. Is this really an Int param? + takes_options = [Int('revocation_reason?', default=0)] def execute(self, serial_number, **options): @@ -115,5 +115,3 @@ class take_certificate_off_hold(Command): textui.print_plain('Failed to take a revoked certificate off hold.') api.register(take_certificate_off_hold) - - -- cgit From a10144be247d109e0bcfb4d5b7812bef508ab8d6 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 14 Jan 2009 22:35:13 -0700 Subject: Updated service plugins module to where it can at least be imported --- ipalib/plugins/f_service.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugins/f_service.py b/ipalib/plugins/f_service.py index a353d52e..06d6a5d0 100644 --- a/ipalib/plugins/f_service.py +++ b/ipalib/plugins/f_service.py @@ -22,27 +22,30 @@ Frontend plugins for service (Identity). """ -from ipalib import frontend -from ipalib import crud -from ipalib.frontend import Param -from ipalib import api -from ipalib import errors -from ipalib import ipa_types - -class service(frontend.Object): +from ipalib import api, crud, errors +from ipalib import Object # Plugin base classes +from ipalib import Str, Flag # Parameter types + + +class service(Object): """ Service object. """ takes_params = ( - Param('principal', primary_key=True), + Str('principal', primary_key=True), ) api.register(service) class service_add(crud.Add): - 'Add a new service.' + """ + Add a new service. + """ + takes_options = ( - Param('force?', type=ipa_types.Bool(), default=False, doc='Force a service principal name'), + Flag('force', + doc='Force a service principal name', + ), ) def execute(self, principal, **kw): """ -- cgit From fdda31c50bcf79ef4e4017dc8075d55b3e5df466 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 14 Jan 2009 22:59:44 -0700 Subject: Fixed a problem in the host plugin module; added not in TODO about using Param.query --- ipalib/plugins/f_host.py | 14 ++++++---- ipalib/plugins/f_user.py | 67 +++++++++++++++++++++++------------------------- 2 files changed, 41 insertions(+), 40 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugins/f_host.py b/ipalib/plugins/f_host.py index bb800b50..3fcda77c 100644 --- a/ipalib/plugins/f_host.py +++ b/ipalib/plugins/f_host.py @@ -206,14 +206,18 @@ api.register(host_mod) class host_find(crud.Find): 'Search the hosts.' + takes_options = ( Flag('all', doc='Retrieve all attributes'), ) - def get_args(self): - """ - Override Find.get_args() so we can exclude the validation rules - """ - yield self.obj.primary_key.__clone__(rules=tuple()) + + # FIXME: This should no longer be needed with the Param.query kwarg. +# def get_args(self): +# """ +# Override Find.get_args() so we can exclude the validation rules +# """ +# yield self.obj.primary_key.__clone__(rules=tuple()) + def execute(self, term, **kw): ldap = self.api.Backend.ldap diff --git a/ipalib/plugins/f_user.py b/ipalib/plugins/f_user.py index 04d7c930..506ad14d 100644 --- a/ipalib/plugins/f_user.py +++ b/ipalib/plugins/f_user.py @@ -21,12 +21,9 @@ Frontend plugins for user (Identity). """ -from ipalib import frontend -from ipalib import crud -from ipalib.frontend import Param -from ipalib import api -from ipalib import errors -from ipalib import ipa_types +from ipalib import api, crud, errors +from ipalib import Object, Command # Plugin base classes +from ipalib import Str, Password, Flag, Int # Parameter types def display_user(user): @@ -48,62 +45,62 @@ def display_user(user): default_attributes = ['uid','givenname','sn','homeDirectory','loginshell'] -class user(frontend.Object): +class user(Object): """ User object. """ + takes_params = ( - Param('givenname', + Str('givenname', cli_name='first', - doc='User\'s first name', + doc="User's first name", ), - Param('sn', + Str('sn', cli_name='last', - doc='User\'s last name', + doc="User's last name", ), - Param('uid', + Str('uid', cli_name='user', primary_key=True, default_from=lambda givenname, sn: givenname[0] + sn, - normalize=lambda value: value.lower(), + normalizer=lambda value: value.lower(), ), - Param('gecos?', + Str('gecos?', doc='GECOS field', default_from=lambda uid: uid, ), - Param('homedirectory?', + Str('homedirectory?', cli_name='home', - doc='User\'s home directory', + doc="User's home directory", default_from=lambda uid: '/home/%s' % uid, ), - Param('loginshell?', + Str('loginshell?', cli_name='shell', default=u'/bin/sh', - doc='User\'s Login shell', + doc="User's Login shell", ), - Param('krbprincipalname?', cli_name='principal', - doc='User\'s Kerberos Principal name', + Str('krbprincipalname?', + cli_name='principal', + doc="User's Kerberos Principal name", default_from=lambda uid: '%s@%s' % (uid, api.env.realm), ), - Param('mailaddress?', - cli_name='mail', - doc='User\'s e-mail address', + Str('mailaddress?', + cli_name='email', + doc="User's e-mail address", ), - Param('userpassword?', + Password('userpassword?', cli_name='password', doc="Set user's password", - flags=['password'], ), - Param('groups?', + Str('groups?', doc='Add account to one or more groups (comma-separated)', ), - Param('uidnumber?', + Int('uidnumber?', cli_name='uid', - type=ipa_types.Int(), doc='The uid to use for this user. If not included one is automatically set.', ), - ) + api.register(user) @@ -254,7 +251,7 @@ api.register(user_mod) class user_find(crud.Find): 'Search the users.' takes_options = ( - Param('all?', type=ipa_types.Bool(), doc='Retrieve all user attributes'), + Flag('all', doc='Retrieve all user attributes'), ) def execute(self, term, **kw): ldap = self.api.Backend.ldap @@ -304,7 +301,7 @@ api.register(user_find) class user_show(crud.Get): 'Examine an existing user.' takes_options = ( - Param('all?', type=ipa_types.Bool(), doc='Retrieve all user attributes'), + Flag('all', doc='Retrieve all user attributes'), ) def execute(self, uid, **kw): """ @@ -332,11 +329,11 @@ class user_show(crud.Get): api.register(user_show) -class user_lock(frontend.Command): +class user_lock(Command): 'Lock a user account.' takes_args = ( - Param('uid', primary_key=True), + Str('uid', primary_key=True), ) def execute(self, uid, **kw): @@ -351,11 +348,11 @@ class user_lock(frontend.Command): api.register(user_lock) -class user_unlock(frontend.Command): +class user_unlock(Command): 'Unlock a user account.' takes_args = ( - Param('uid', primary_key=True), + Str('uid', primary_key=True), ) def execute(self, uid, **kw): -- cgit From 4d4fa694ee2b84975a0e7596b51e21109f58a76d Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 14 Jan 2009 23:15:46 -0700 Subject: Small change in ipalib.cli to check if param is a Password instance instead of calling depreciated ispasswd() method --- ipalib/cli.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index f6c187b3..5cf68852 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -38,6 +38,7 @@ import errors import plugable import util from constants import CLI_TAB +from parameters import Password def to_cli(name): @@ -699,7 +700,7 @@ class CLI(object): result = cmd(**kw) if callable(cmd.output_for_cli): for param in cmd.params(): - if param.ispassword(): + if isinstance(param, Password): try: del kw[param.name] except KeyError: -- cgit From 6be5e4a0a55c1ba048444430afc0e01b3048d8b9 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 15 Jan 2009 23:52:50 -0700 Subject: ipalib.rpc: now using allow_none=True after conversation with Rob; added xml_dumps() and xml_loads() functions; some name cleanup --- ipalib/rpc.py | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) (limited to 'ipalib') diff --git a/ipalib/rpc.py b/ipalib/rpc.py index c4662f84..df31669d 100644 --- a/ipalib/rpc.py +++ b/ipalib/rpc.py @@ -22,10 +22,10 @@ Core RPC functionality. """ from types import NoneType -from xmlrpclib import Binary +from xmlrpclib import Binary, Fault, dumps, loads -def xmlrpc_wrap(value): +def xml_wrap(value): """ Wrap all ``str`` in ``xmlrpclib.Binary``. @@ -42,13 +42,13 @@ def xmlrpc_wrap(value): converted to UTF-8 encoded ``str`` instances (although as mentioned, not by this function). - Also see `xmlrpc_unwrap`. + Also see `xml_unwrap`. """ if type(value) in (list, tuple): - return tuple(xmlrpc_wrap(v) for v in value) + return tuple(xml_wrap(v) for v in value) if type(value) is dict: return dict( - (k, xmlrpc_wrap(v)) for (k, v) in value.iteritems() + (k, xml_wrap(v)) for (k, v) in value.iteritems() ) if type(value) is str: return Binary(value) @@ -56,7 +56,7 @@ def xmlrpc_wrap(value): return value -def xmlrpc_unwrap(value, encoding='UTF-8'): +def xml_unwrap(value, encoding='UTF-8'): """ Unwrap all ``xmlrpc.Binary``, decode all ``str`` into ``unicode``. @@ -69,13 +69,13 @@ def xmlrpc_unwrap(value, encoding='UTF-8'): * All ``str`` instances are treated as UTF-8 encoded character data. They are decoded and the resulting ``unicode`` instance is returned. - Also see `xmlrpc_wrap`. + Also see `xml_wrap`. """ if type(value) in (list, tuple): - return tuple(xmlrpc_unwrap(v, encoding) for v in value) + return tuple(xml_unwrap(v, encoding) for v in value) if type(value) is dict: return dict( - (k, xmlrpc_unwrap(v, encoding)) for (k, v) in value.iteritems() + (k, xml_unwrap(v, encoding)) for (k, v) in value.iteritems() ) if type(value) is str: return value.decode(encoding) @@ -84,3 +84,21 @@ def xmlrpc_unwrap(value, encoding='UTF-8'): return value.data assert type(value) in (unicode, int, float, bool, NoneType) return value + + +def xml_dumps(params, methodname=None, methodresponse=False, encoding='UTF-8'): + if type(params) is tuple: + params = xml_wrap(params) + else: + assert isinstance(params, Fault) + return dumps(params, + methodname=methodname, + methodresponse=methodresponse, + encoding=encoding, + allow_none=True, + ) + + +def xml_loads(data): + (params, method) = loads(data) + return (xml_unwrap(params), method) -- cgit From 0227a129496acf4be48f0d5821fb9f602643feac Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 16 Jan 2009 01:23:55 -0700 Subject: Added docstrings to the new rpc.xml_dumps() and rcp.xml_loads() functions --- ipalib/rpc.py | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 57 insertions(+), 6 deletions(-) (limited to 'ipalib') diff --git a/ipalib/rpc.py b/ipalib/rpc.py index df31669d..486aff8a 100644 --- a/ipalib/rpc.py +++ b/ipalib/rpc.py @@ -18,7 +18,13 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ -Core RPC functionality. +Shared RPC client/server functionality. + +This module adds some additional functionality on top of the ``xmlrpclib`` +module in the Python standard library. For documentation on the +``xmlrpclib`` module, see: + + http://docs.python.org/library/xmlrpclib.html """ from types import NoneType @@ -32,7 +38,7 @@ def xml_wrap(value): Because ``xmlrpclib.dumps()`` will itself convert all ``unicode`` instances into UTF-8 encoded ``str`` instances, we don't do it here. - So in total, when encoding data for an XML-RPC request, the following + So in total, when encoding data for an XML-RPC packet, the following transformations occur: * All ``str`` instances are treated as binary data and are wrapped in @@ -42,7 +48,9 @@ def xml_wrap(value): converted to UTF-8 encoded ``str`` instances (although as mentioned, not by this function). - Also see `xml_unwrap`. + Also see `xml_unwrap()`. + + :param value: The simple scalar or simple compound value to wrap. """ if type(value) in (list, tuple): return tuple(xml_wrap(v) for v in value) @@ -60,16 +68,19 @@ def xml_unwrap(value, encoding='UTF-8'): """ Unwrap all ``xmlrpc.Binary``, decode all ``str`` into ``unicode``. - When decoding data from an XML-RPC request, the following transformations + When decoding data from an XML-RPC packet, the following transformations occur: * The binary payloads of all ``xmlrpclib.Binary`` instances are returned as ``str`` instances. - * All ``str`` instances are treated as UTF-8 encoded character data. + * All ``str`` instances are treated as UTF-8 encoded Unicode strings. They are decoded and the resulting ``unicode`` instance is returned. - Also see `xml_wrap`. + Also see `xml_wrap()`. + + :param value: The value to unwrap. + :param encoding: The Unicode encoding to use (defaults to ``'UTF-8'``). """ if type(value) in (list, tuple): return tuple(xml_unwrap(v, encoding) for v in value) @@ -87,6 +98,24 @@ def xml_unwrap(value, encoding='UTF-8'): def xml_dumps(params, methodname=None, methodresponse=False, encoding='UTF-8'): + """ + Encode an XML-RPC data packet, transparently wraping ``params``. + + This function will wrap ``params`` using `xml_wrap()` and will + then encode the XML-RPC data packet using ``xmlrpclib.dumps()`` (from the + Python standard library). + + For documentation on the ``xmlrpclib.dumps()`` function, see: + + http://docs.python.org/library/xmlrpclib.html#convenience-functions + + Also see `xml_loads()`. + + :param params: A ``tuple`` or an ``xmlrpclib.Fault`` instance. + :param methodname: The name of the method to call if this is a request. + :param methodresponse: Set this to ``True`` if this is a response. + :param encoding: The Unicode encoding to use (defaults to ``'UTF-8'``). + """ if type(params) is tuple: params = xml_wrap(params) else: @@ -100,5 +129,27 @@ def xml_dumps(params, methodname=None, methodresponse=False, encoding='UTF-8'): def xml_loads(data): + """ + Decode the XML-RPC packet in ``data``, transparently unwrapped its params. + + This function will decode the XML-RPC packet in ``data`` using + ``xmlrpclib.loads()`` (from the Python standard library). If ``data`` + contains a fault, ``xmlrpclib.loads()`` will itself raise an + ``xmlrpclib.Fault`` exception. + + Assuming an exception is not raised, this function will then unwrap the + params in ``data`` using `xml_unwrap()`. Finally, a + ``(params, methodname)`` tuple is returned containing the unwrapped params + and the name of the method being called. If the packet contains no method + name, ``methodname`` will be ``None``. + + For documentation on the ``xmlrpclib.loads()`` function, see: + + http://docs.python.org/library/xmlrpclib.html#convenience-functions + + Also see `xml_dumps()`. + + :param data: The XML-RPC packet to decode. + """ (params, method) = loads(data) return (xml_unwrap(params), method) -- cgit From 462bac3c13de92511085b080ee5b9999f275a1e3 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 16 Jan 2009 01:56:39 -0700 Subject: Added docstring cross-references between rpc and rpcserver modules --- ipalib/rpc.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'ipalib') diff --git a/ipalib/rpc.py b/ipalib/rpc.py index 486aff8a..acfdae95 100644 --- a/ipalib/rpc.py +++ b/ipalib/rpc.py @@ -18,13 +18,15 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ -Shared RPC client/server functionality. +RPC client and shared RPC client/server functionality. This module adds some additional functionality on top of the ``xmlrpclib`` module in the Python standard library. For documentation on the ``xmlrpclib`` module, see: http://docs.python.org/library/xmlrpclib.html + +Also see the `ipaserver.rpcserver` module. """ from types import NoneType @@ -130,7 +132,7 @@ def xml_dumps(params, methodname=None, methodresponse=False, encoding='UTF-8'): def xml_loads(data): """ - Decode the XML-RPC packet in ``data``, transparently unwrapped its params. + Decode the XML-RPC packet in ``data``, transparently unwrapping its params. This function will decode the XML-RPC packet in ``data`` using ``xmlrpclib.loads()`` (from the Python standard library). If ``data`` -- cgit From 7514f96173575c42c2f5592034211d19ff711a02 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 16 Jan 2009 11:07:21 -0700 Subject: New Param: fixed metavar bug in cli.py --- ipalib/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/cli.py b/ipalib/cli.py index 5cf68852..fb2fd95f 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -807,7 +807,7 @@ class CLI(object): else: kw['action'] = 'store_true' else: - kw['metavar'] = metavar=option.type.name.upper() + kw['metavar'] = metavar=option.__class__.__name__.upper() o = optparse.make_option('--%s' % to_cli(option.cli_name), **kw) parser.add_option(o) return parser -- cgit From 364e05def194b80714a5ea2a3e89598db9fb4892 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sun, 18 Jan 2009 15:55:56 -0700 Subject: Added missing enumerable parameters --- ipalib/__init__.py | 2 +- ipalib/parameters.py | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index e5aa65d6..8abb9029 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -875,7 +875,7 @@ from backend import Backend, Context from frontend import Command, LocalOrRemote, Application from frontend import Object, Method, Property from parameters import DefaultFrom, Bool, Flag, Int, Float, Bytes, Str, Password - +from parameters import BytesEnum, StrEnum def create_api(mode='dummy'): """ diff --git a/ipalib/parameters.py b/ipalib/parameters.py index fd693e71..0d764d60 100644 --- a/ipalib/parameters.py +++ b/ipalib/parameters.py @@ -898,6 +898,47 @@ class Password(Str): """ +class Enum(Param): + """ + Base class for parameters with enumerable values. + """ + + kwargs = Param.kwargs + ( + ('values', tuple, tuple()), + ) + + def __init__(self, name, *rules, **kw): + super(Enum, self).__init__(name, *rules, **kw) + for (i, v) in enumerate(self.values): + if type(v) is not self.type: + n = '%s values[%d]' % (self.nice, i) + raise TypeError( + TYPE_ERROR % (n, self.type, v, type(v)) + ) + + def _rule_values(self, _, value, **kw): + if value not in self.values: + return _('must be one of %(values)r') % dict( + values=self.values, + ) + + +class BytesEnum(Enum): + """ + Enumerable for binary data (stored in the ``str`` type). + """ + + type = unicode + + +class StrEnum(Enum): + """ + Enumerable for Unicode text (stored in the ``unicode`` type). + """ + + type = unicode + + def create_param(spec): """ Create an `Str` instance from the shorthand ``spec``. -- cgit From bae9dd7c073a8a23f71b1df0fa4cb6d90b00a337 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sun, 18 Jan 2009 16:03:02 -0700 Subject: Added example to StrEnum docstring --- ipalib/parameters.py | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'ipalib') diff --git a/ipalib/parameters.py b/ipalib/parameters.py index 0d764d60..7cc93e90 100644 --- a/ipalib/parameters.py +++ b/ipalib/parameters.py @@ -934,6 +934,16 @@ class BytesEnum(Enum): class StrEnum(Enum): """ Enumerable for Unicode text (stored in the ``unicode`` type). + + For example: + + >>> enum = StrEnum('my_enum', values=(u'One', u'Two', u'Three')) + >>> enum.validate(u'Two') is None + True + >>> enum.validate(u'Four') + Traceback (most recent call last): + ... + ValidationError: invalid 'my_enum': must be one of (u'One', u'Two', u'Three') """ type = unicode -- cgit From e708765d61b73e1f8ccba266d7bc934f6f4c1277 Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Fri, 16 Jan 2009 09:50:34 -0500 Subject: Include local copy of UUID generator for Python 2.4. Python 2.5+ has a built-in RFC 4122-compliant UUID generator. Include a copy of this file in our library and import it in a way that it will work with Python 2.4. --- ipalib/__init__.py | 5 + ipalib/ipauuid.py | 541 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 546 insertions(+) create mode 100644 ipalib/ipauuid.py (limited to 'ipalib') diff --git a/ipalib/__init__.py b/ipalib/__init__.py index 8abb9029..29344e18 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -877,6 +877,11 @@ from frontend import Object, Method, Property from parameters import DefaultFrom, Bool, Flag, Int, Float, Bytes, Str, Password from parameters import BytesEnum, StrEnum +try: + import uuid +except ImportError: + import ipauuid as uuid + def create_api(mode='dummy'): """ Return standard `plugable.API` instance. diff --git a/ipalib/ipauuid.py b/ipalib/ipauuid.py new file mode 100644 index 00000000..ae3da25c --- /dev/null +++ b/ipalib/ipauuid.py @@ -0,0 +1,541 @@ +r"""UUID objects (universally unique identifiers) according to RFC 4122. + +This module provides immutable UUID objects (class UUID) and the functions +uuid1(), uuid3(), uuid4(), uuid5() for generating version 1, 3, 4, and 5 +UUIDs as specified in RFC 4122. + +If all you want is a unique ID, you should probably call uuid1() or uuid4(). +Note that uuid1() may compromise privacy since it creates a UUID containing +the computer's network address. uuid4() creates a random UUID. + +Typical usage: + + >>> import uuid + + # make a UUID based on the host ID and current time + >>> uuid.uuid1() + UUID('a8098c1a-f86e-11da-bd1a-00112444be1e') + + # make a UUID using an MD5 hash of a namespace UUID and a name + >>> uuid.uuid3(uuid.NAMESPACE_DNS, 'python.org') + UUID('6fa459ea-ee8a-3ca4-894e-db77e160355e') + + # make a random UUID + >>> uuid.uuid4() + UUID('16fd2706-8baf-433b-82eb-8c7fada847da') + + # make a UUID using a SHA-1 hash of a namespace UUID and a name + >>> uuid.uuid5(uuid.NAMESPACE_DNS, 'python.org') + UUID('886313e1-3b8a-5372-9b90-0c9aee199e5d') + + # make a UUID from a string of hex digits (braces and hyphens ignored) + >>> x = uuid.UUID('{00010203-0405-0607-0809-0a0b0c0d0e0f}') + + # convert a UUID to a string of hex digits in standard form + >>> str(x) + '00010203-0405-0607-0809-0a0b0c0d0e0f' + + # get the raw 16 bytes of the UUID + >>> x.bytes + '\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f' + + # make a UUID from a 16-byte string + >>> uuid.UUID(bytes=x.bytes) + UUID('00010203-0405-0607-0809-0a0b0c0d0e0f') +""" + +__author__ = 'Ka-Ping Yee ' + +RESERVED_NCS, RFC_4122, RESERVED_MICROSOFT, RESERVED_FUTURE = [ + 'reserved for NCS compatibility', 'specified in RFC 4122', + 'reserved for Microsoft compatibility', 'reserved for future definition'] + +class UUID(object): + """Instances of the UUID class represent UUIDs as specified in RFC 4122. + UUID objects are immutable, hashable, and usable as dictionary keys. + Converting a UUID to a string with str() yields something in the form + '12345678-1234-1234-1234-123456789abc'. The UUID constructor accepts + five possible forms: a similar string of hexadecimal digits, or a tuple + of six integer fields (with 32-bit, 16-bit, 16-bit, 8-bit, 8-bit, and + 48-bit values respectively) as an argument named 'fields', or a string + of 16 bytes (with all the integer fields in big-endian order) as an + argument named 'bytes', or a string of 16 bytes (with the first three + fields in little-endian order) as an argument named 'bytes_le', or a + single 128-bit integer as an argument named 'int'. + + UUIDs have these read-only attributes: + + bytes the UUID as a 16-byte string (containing the six + integer fields in big-endian byte order) + + bytes_le the UUID as a 16-byte string (with time_low, time_mid, + and time_hi_version in little-endian byte order) + + fields a tuple of the six integer fields of the UUID, + which are also available as six individual attributes + and two derived attributes: + + time_low the first 32 bits of the UUID + time_mid the next 16 bits of the UUID + time_hi_version the next 16 bits of the UUID + clock_seq_hi_variant the next 8 bits of the UUID + clock_seq_low the next 8 bits of the UUID + node the last 48 bits of the UUID + + time the 60-bit timestamp + clock_seq the 14-bit sequence number + + hex the UUID as a 32-character hexadecimal string + + int the UUID as a 128-bit integer + + urn the UUID as a URN as specified in RFC 4122 + + variant the UUID variant (one of the constants RESERVED_NCS, + RFC_4122, RESERVED_MICROSOFT, or RESERVED_FUTURE) + + version the UUID version number (1 through 5, meaningful only + when the variant is RFC_4122) + """ + + def __init__(self, hex=None, bytes=None, bytes_le=None, fields=None, + int=None, version=None): + r"""Create a UUID from either a string of 32 hexadecimal digits, + a string of 16 bytes as the 'bytes' argument, a string of 16 bytes + in little-endian order as the 'bytes_le' argument, a tuple of six + integers (32-bit time_low, 16-bit time_mid, 16-bit time_hi_version, + 8-bit clock_seq_hi_variant, 8-bit clock_seq_low, 48-bit node) as + the 'fields' argument, or a single 128-bit integer as the 'int' + argument. When a string of hex digits is given, curly braces, + hyphens, and a URN prefix are all optional. For example, these + expressions all yield the same UUID: + + UUID('{12345678-1234-5678-1234-567812345678}') + UUID('12345678123456781234567812345678') + UUID('urn:uuid:12345678-1234-5678-1234-567812345678') + UUID(bytes='\x12\x34\x56\x78'*4) + UUID(bytes_le='\x78\x56\x34\x12\x34\x12\x78\x56' + + '\x12\x34\x56\x78\x12\x34\x56\x78') + UUID(fields=(0x12345678, 0x1234, 0x5678, 0x12, 0x34, 0x567812345678)) + UUID(int=0x12345678123456781234567812345678) + + Exactly one of 'hex', 'bytes', 'bytes_le', 'fields', or 'int' must + be given. The 'version' argument is optional; if given, the resulting + UUID will have its variant and version set according to RFC 4122, + overriding the given 'hex', 'bytes', 'bytes_le', 'fields', or 'int'. + """ + + if [hex, bytes, bytes_le, fields, int].count(None) != 4: + raise TypeError('need one of hex, bytes, bytes_le, fields, or int') + if hex is not None: + hex = hex.replace('urn:', '').replace('uuid:', '') + hex = hex.strip('{}').replace('-', '') + if len(hex) != 32: + raise ValueError('badly formed hexadecimal UUID string') + int = long(hex, 16) + if bytes_le is not None: + if len(bytes_le) != 16: + raise ValueError('bytes_le is not a 16-char string') + bytes = (bytes_le[3] + bytes_le[2] + bytes_le[1] + bytes_le[0] + + bytes_le[5] + bytes_le[4] + bytes_le[7] + bytes_le[6] + + bytes_le[8:]) + if bytes is not None: + if len(bytes) != 16: + raise ValueError('bytes is not a 16-char string') + int = long(('%02x'*16) % tuple(map(ord, bytes)), 16) + if fields is not None: + if len(fields) != 6: + raise ValueError('fields is not a 6-tuple') + (time_low, time_mid, time_hi_version, + clock_seq_hi_variant, clock_seq_low, node) = fields + if not 0 <= time_low < 1<<32L: + raise ValueError('field 1 out of range (need a 32-bit value)') + if not 0 <= time_mid < 1<<16L: + raise ValueError('field 2 out of range (need a 16-bit value)') + if not 0 <= time_hi_version < 1<<16L: + raise ValueError('field 3 out of range (need a 16-bit value)') + if not 0 <= clock_seq_hi_variant < 1<<8L: + raise ValueError('field 4 out of range (need an 8-bit value)') + if not 0 <= clock_seq_low < 1<<8L: + raise ValueError('field 5 out of range (need an 8-bit value)') + if not 0 <= node < 1<<48L: + raise ValueError('field 6 out of range (need a 48-bit value)') + clock_seq = (clock_seq_hi_variant << 8L) | clock_seq_low + int = ((time_low << 96L) | (time_mid << 80L) | + (time_hi_version << 64L) | (clock_seq << 48L) | node) + if int is not None: + if not 0 <= int < 1<<128L: + raise ValueError('int is out of range (need a 128-bit value)') + if version is not None: + if not 1 <= version <= 5: + raise ValueError('illegal version number') + # Set the variant to RFC 4122. + int &= ~(0xc000 << 48L) + int |= 0x8000 << 48L + # Set the version number. + int &= ~(0xf000 << 64L) + int |= version << 76L + self.__dict__['int'] = int + + def __cmp__(self, other): + if isinstance(other, UUID): + return cmp(self.int, other.int) + return NotImplemented + + def __hash__(self): + return hash(self.int) + + def __int__(self): + return self.int + + def __repr__(self): + return 'UUID(%r)' % str(self) + + def __setattr__(self, name, value): + raise TypeError('UUID objects are immutable') + + def __str__(self): + hex = '%032x' % self.int + return '%s-%s-%s-%s-%s' % ( + hex[:8], hex[8:12], hex[12:16], hex[16:20], hex[20:]) + + def get_bytes(self): + bytes = '' + for shift in range(0, 128, 8): + bytes = chr((self.int >> shift) & 0xff) + bytes + return bytes + + bytes = property(get_bytes) + + def get_bytes_le(self): + bytes = self.bytes + return (bytes[3] + bytes[2] + bytes[1] + bytes[0] + + bytes[5] + bytes[4] + bytes[7] + bytes[6] + bytes[8:]) + + bytes_le = property(get_bytes_le) + + def get_fields(self): + return (self.time_low, self.time_mid, self.time_hi_version, + self.clock_seq_hi_variant, self.clock_seq_low, self.node) + + fields = property(get_fields) + + def get_time_low(self): + return self.int >> 96L + + time_low = property(get_time_low) + + def get_time_mid(self): + return (self.int >> 80L) & 0xffff + + time_mid = property(get_time_mid) + + def get_time_hi_version(self): + return (self.int >> 64L) & 0xffff + + time_hi_version = property(get_time_hi_version) + + def get_clock_seq_hi_variant(self): + return (self.int >> 56L) & 0xff + + clock_seq_hi_variant = property(get_clock_seq_hi_variant) + + def get_clock_seq_low(self): + return (self.int >> 48L) & 0xff + + clock_seq_low = property(get_clock_seq_low) + + def get_time(self): + return (((self.time_hi_version & 0x0fffL) << 48L) | + (self.time_mid << 32L) | self.time_low) + + time = property(get_time) + + def get_clock_seq(self): + return (((self.clock_seq_hi_variant & 0x3fL) << 8L) | + self.clock_seq_low) + + clock_seq = property(get_clock_seq) + + def get_node(self): + return self.int & 0xffffffffffff + + node = property(get_node) + + def get_hex(self): + return '%032x' % self.int + + hex = property(get_hex) + + def get_urn(self): + return 'urn:uuid:' + str(self) + + urn = property(get_urn) + + def get_variant(self): + if not self.int & (0x8000 << 48L): + return RESERVED_NCS + elif not self.int & (0x4000 << 48L): + return RFC_4122 + elif not self.int & (0x2000 << 48L): + return RESERVED_MICROSOFT + else: + return RESERVED_FUTURE + + variant = property(get_variant) + + def get_version(self): + # The version bits are only meaningful for RFC 4122 UUIDs. + if self.variant == RFC_4122: + return int((self.int >> 76L) & 0xf) + + version = property(get_version) + +def _find_mac(command, args, hw_identifiers, get_index): + import os + for dir in ['', '/sbin/', '/usr/sbin']: + executable = os.path.join(dir, command) + if not os.path.exists(executable): + continue + + try: + # LC_ALL to get English output, 2>/dev/null to + # prevent output on stderr + cmd = 'LC_ALL=C %s %s 2>/dev/null' % (executable, args) + pipe = os.popen(cmd) + except IOError: + continue + + for line in pipe: + words = line.lower().split() + for i in range(len(words)): + if words[i] in hw_identifiers: + return int(words[get_index(i)].replace(':', ''), 16) + return None + +def _ifconfig_getnode(): + """Get the hardware address on Unix by running ifconfig.""" + + # This works on Linux ('' or '-a'), Tru64 ('-av'), but not all Unixes. + for args in ('', '-a', '-av'): + mac = _find_mac('ifconfig', args, ['hwaddr', 'ether'], lambda i: i+1) + if mac: + return mac + + import socket + ip_addr = socket.gethostbyname(socket.gethostname()) + + # Try getting the MAC addr from arp based on our IP address (Solaris). + mac = _find_mac('arp', '-an', [ip_addr], lambda i: -1) + if mac: + return mac + + # This might work on HP-UX. + mac = _find_mac('lanscan', '-ai', ['lan0'], lambda i: 0) + if mac: + return mac + + return None + +def _ipconfig_getnode(): + """Get the hardware address on Windows by running ipconfig.exe.""" + import os, re + dirs = ['', r'c:\windows\system32', r'c:\winnt\system32'] + try: + import ctypes + buffer = ctypes.create_string_buffer(300) + ctypes.windll.kernel32.GetSystemDirectoryA(buffer, 300) + dirs.insert(0, buffer.value.decode('mbcs')) + except: + pass + for dir in dirs: + try: + pipe = os.popen(os.path.join(dir, 'ipconfig') + ' /all') + except IOError: + continue + for line in pipe: + value = line.split(':')[-1].strip().lower() + if re.match('([0-9a-f][0-9a-f]-){5}[0-9a-f][0-9a-f]', value): + return int(value.replace('-', ''), 16) + +def _netbios_getnode(): + """Get the hardware address on Windows using NetBIOS calls. + See http://support.microsoft.com/kb/118623 for details.""" + import win32wnet, netbios + ncb = netbios.NCB() + ncb.Command = netbios.NCBENUM + ncb.Buffer = adapters = netbios.LANA_ENUM() + adapters._pack() + if win32wnet.Netbios(ncb) != 0: + return + adapters._unpack() + for i in range(adapters.length): + ncb.Reset() + ncb.Command = netbios.NCBRESET + ncb.Lana_num = ord(adapters.lana[i]) + if win32wnet.Netbios(ncb) != 0: + continue + ncb.Reset() + ncb.Command = netbios.NCBASTAT + ncb.Lana_num = ord(adapters.lana[i]) + ncb.Callname = '*'.ljust(16) + ncb.Buffer = status = netbios.ADAPTER_STATUS() + if win32wnet.Netbios(ncb) != 0: + continue + status._unpack() + bytes = map(ord, status.adapter_address) + return ((bytes[0]<<40L) + (bytes[1]<<32L) + (bytes[2]<<24L) + + (bytes[3]<<16L) + (bytes[4]<<8L) + bytes[5]) + +# Thanks to Thomas Heller for ctypes and for his help with its use here. + +# If ctypes is available, use it to find system routines for UUID generation. +_uuid_generate_random = _uuid_generate_time = _UuidCreate = None +try: + import ctypes, ctypes.util + _buffer = ctypes.create_string_buffer(16) + + # The uuid_generate_* routines are provided by libuuid on at least + # Linux and FreeBSD, and provided by libc on Mac OS X. + for libname in ['uuid', 'c']: + try: + lib = ctypes.CDLL(ctypes.util.find_library(libname)) + except: + continue + if hasattr(lib, 'uuid_generate_random'): + _uuid_generate_random = lib.uuid_generate_random + if hasattr(lib, 'uuid_generate_time'): + _uuid_generate_time = lib.uuid_generate_time + + # On Windows prior to 2000, UuidCreate gives a UUID containing the + # hardware address. On Windows 2000 and later, UuidCreate makes a + # random UUID and UuidCreateSequential gives a UUID containing the + # hardware address. These routines are provided by the RPC runtime. + # NOTE: at least on Tim's WinXP Pro SP2 desktop box, while the last + # 6 bytes returned by UuidCreateSequential are fixed, they don't appear + # to bear any relationship to the MAC address of any network device + # on the box. + try: + lib = ctypes.windll.rpcrt4 + except: + lib = None + _UuidCreate = getattr(lib, 'UuidCreateSequential', + getattr(lib, 'UuidCreate', None)) +except: + pass + +def _unixdll_getnode(): + """Get the hardware address on Unix using ctypes.""" + _uuid_generate_time(_buffer) + return UUID(bytes=_buffer.raw).node + +def _windll_getnode(): + """Get the hardware address on Windows using ctypes.""" + if _UuidCreate(_buffer) == 0: + return UUID(bytes=_buffer.raw).node + +def _random_getnode(): + """Get a random node ID, with eighth bit set as suggested by RFC 4122.""" + import random + return random.randrange(0, 1<<48L) | 0x010000000000L + +_node = None + +def getnode(): + """Get the hardware address as a 48-bit positive integer. + + The first time this runs, it may launch a separate program, which could + be quite slow. If all attempts to obtain the hardware address fail, we + choose a random 48-bit number with its eighth bit set to 1 as recommended + in RFC 4122. + """ + + global _node + if _node is not None: + return _node + + import sys + if sys.platform == 'win32': + getters = [_windll_getnode, _netbios_getnode, _ipconfig_getnode] + else: + getters = [_unixdll_getnode, _ifconfig_getnode] + + for getter in getters + [_random_getnode]: + try: + _node = getter() + except: + continue + if _node is not None: + return _node + +_last_timestamp = None + +def uuid1(node=None, clock_seq=None): + """Generate a UUID from a host ID, sequence number, and the current time. + If 'node' is not given, getnode() is used to obtain the hardware + address. If 'clock_seq' is given, it is used as the sequence number; + otherwise a random 14-bit sequence number is chosen.""" + + # When the system provides a version-1 UUID generator, use it (but don't + # use UuidCreate here because its UUIDs don't conform to RFC 4122). + if _uuid_generate_time and node is clock_seq is None: + _uuid_generate_time(_buffer) + return UUID(bytes=_buffer.raw) + + global _last_timestamp + import time + nanoseconds = int(time.time() * 1e9) + # 0x01b21dd213814000 is the number of 100-ns intervals between the + # UUID epoch 1582-10-15 00:00:00 and the Unix epoch 1970-01-01 00:00:00. + timestamp = int(nanoseconds/100) + 0x01b21dd213814000L + if timestamp <= _last_timestamp: + timestamp = _last_timestamp + 1 + _last_timestamp = timestamp + if clock_seq is None: + import random + clock_seq = random.randrange(1<<14L) # instead of stable storage + time_low = timestamp & 0xffffffffL + time_mid = (timestamp >> 32L) & 0xffffL + time_hi_version = (timestamp >> 48L) & 0x0fffL + clock_seq_low = clock_seq & 0xffL + clock_seq_hi_variant = (clock_seq >> 8L) & 0x3fL + if node is None: + node = getnode() + return UUID(fields=(time_low, time_mid, time_hi_version, + clock_seq_hi_variant, clock_seq_low, node), version=1) + +def uuid3(namespace, name): + """Generate a UUID from the MD5 hash of a namespace UUID and a name.""" + import md5 + hash = md5.md5(namespace.bytes + name).digest() + return UUID(bytes=hash[:16], version=3) + +def uuid4(): + """Generate a random UUID.""" + + # When the system provides a version-4 UUID generator, use it. + if _uuid_generate_random: + _uuid_generate_random(_buffer) + return UUID(bytes=_buffer.raw) + + # Otherwise, get randomness from urandom or the 'random' module. + try: + import os + return UUID(bytes=os.urandom(16), version=4) + except: + import random + bytes = [chr(random.randrange(256)) for i in range(16)] + return UUID(bytes=bytes, version=4) + +def uuid5(namespace, name): + """Generate a UUID from the SHA-1 hash of a namespace UUID and a name.""" + import sha + hash = sha.sha(namespace.bytes + name).digest() + return UUID(bytes=hash[:16], version=5) + +# The following standard UUIDs are for use with uuid3() or uuid5(). + +NAMESPACE_DNS = UUID('6ba7b810-9dad-11d1-80b4-00c04fd430c8') +NAMESPACE_URL = UUID('6ba7b811-9dad-11d1-80b4-00c04fd430c8') +NAMESPACE_OID = UUID('6ba7b812-9dad-11d1-80b4-00c04fd430c8') +NAMESPACE_X500 = UUID('6ba7b814-9dad-11d1-80b4-00c04fd430c8') -- cgit From bc40686b7fd8929f83a9594380a02d4104ae0cdf Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Fri, 16 Jan 2009 10:19:08 -0500 Subject: Fix rule definition to match new API --- ipalib/plugins/f_host.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'ipalib') diff --git a/ipalib/plugins/f_host.py b/ipalib/plugins/f_host.py index 3fcda77c..ea819a77 100644 --- a/ipalib/plugins/f_host.py +++ b/ipalib/plugins/f_host.py @@ -21,7 +21,7 @@ Frontend plugins for host/machine Identity. """ -from ipalib import api, crud, errors +from ipalib import api, crud, errors, util from ipalib import Object # Plugin base class from ipalib import Str, Flag # Parameter types @@ -42,7 +42,7 @@ def get_host(hostname): dn = ldap.find_entry_dn("serverhostname", hostname, "ipaHost") return dn -def validate_host(cn): +def validate_host(ugettext, cn): """ Require at least one dot in the hostname (to support localhost.localdomain) """ @@ -129,7 +129,7 @@ class host_add(crud.Add): # some required objectclasses # FIXME: add this attribute to cn=ipaconfig #kw['objectclass'] = config.get('ipahostobjectclasses') - kw['objectclass'] = ['nsHost', 'ipaHost'] + kw['objectclass'] = ['nsHost', 'ipaHost', 'pkiUser'] # Ensure the list of objectclasses is lower-case kw['objectclass'] = map(lambda z: z.lower(), kw.get('objectclass')) -- cgit From 8154131ce1975c9e2109e408a0a25631ea797a8c Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Fri, 16 Jan 2009 10:19:29 -0500 Subject: Use correct function for outputing a string --- ipalib/plugins/f_hostgroup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ipalib') diff --git a/ipalib/plugins/f_hostgroup.py b/ipalib/plugins/f_hostgroup.py index c365c918..706712c9 100644 --- a/ipalib/plugins/f_hostgroup.py +++ b/ipalib/plugins/f_hostgroup.py @@ -281,7 +281,7 @@ class hostgroup_add_member(Command): for a in result: print "\t'%s'" % a else: - textui.print_entry("Group membership updated.") + textui.print_plain("Group membership updated.") api.register(hostgroup_add_member) -- cgit From 98ab09fafc7e24fb32b44e691eb6d6c9464197d5 Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Fri, 16 Jan 2009 10:34:13 -0500 Subject: Initial implementation of netgroups --- ipalib/plugins/f_netgroup.py | 461 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 461 insertions(+) create mode 100644 ipalib/plugins/f_netgroup.py (limited to 'ipalib') diff --git a/ipalib/plugins/f_netgroup.py b/ipalib/plugins/f_netgroup.py new file mode 100644 index 00000000..6ee55b0d --- /dev/null +++ b/ipalib/plugins/f_netgroup.py @@ -0,0 +1,461 @@ +# Authors: +# Rob Crittenden +# +# Copyright (C) 2009 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 + +""" +Frontend plugin for netgroups. +""" + +from ipalib import api, crud, errors +from ipalib import Object, Command # Plugin base classes +from ipalib import Str # Parameter types +from ipalib import uuid + +netgroup_base = "cn=ng, cn=alt" +netgroup_filter = "ipaNISNetgroup" +hostgroup_filter = "groupofnames)(!(objectclass=posixGroup)" + +def get_members(members): + """ + Return a list of members. + + It is possible that the value passed in is None. + """ + if members: + members = members.split(',') + else: + members = [] + + return members + +def find_members(ldap, failed, members, attribute, filter=None): + """ + Return 2 lists: one a list of DNs found, one a list of errors + """ + found = [] + for m in members: + if not m: continue + try: + member_dn = ldap.find_entry_dn(attribute, m, filter) + found.append(member_dn) + except errors.NotFound: + failed.append(m) + continue + + return found, failed + +def add_members(ldap, completed, members, dn, memberattr): + add_failed = [] + for member_dn in members: + try: + ldap.add_member_to_group(member_dn, dn, memberattr) + completed+=1 + except: + add_failed.append(member_dn) + + return completed, add_failed + +def add_external(ldap, completed, members, cn): + failed = [] + netgroup = api.Command['netgroup_show'](cn) + external = netgroup.get('externalhost', []) + if not isinstance(external, list): + external = [external] + external_len = len(external) + for m in members: + if not m in external: + external.append(m) + completed+=1 + else: + failed.append(m) + if len(external) > external_len: + kw = {'externalhost': external} + ldap.update(netgroup['dn'], **kw) + + return completed, failed + +def remove_members(ldap, completed, members, dn, memberattr): + remove_failed = [] + for member_dn in members: + try: + ldap.remove_member_from_group(member_dn, dn, memberattr) + completed+=1 + except: + remove_failed.append(member_dn) + + return completed, remove_failed + +def remove_external(ldap, completed, members, cn): + failed = [] + netgroup = api.Command['netgroup_show'](cn) + external = netgroup.get('externalhost', []) + if not isinstance(external, list): + external = [external] + external_len = len(external) + for m in members: + try: + external.remove(m) + completed+=1 + except ValueError: + failed.append(m) + if len(external) < external_len: + kw = {'externalhost': external} + ldap.update(netgroup['dn'], **kw) + + return completed, failed + +class netgroup(Object): + """ + netgroups object. + """ + takes_params = ( + Str('cn', + cli_name='name', + primary_key=True + ), + Str('description', + doc='Description', + ), + Str('nisdomainname?', + cli_name='domainname', + doc='Domain name', + ), + ) +api.register(netgroup) + + +class netgroup_add(crud.Add): + 'Add a new netgroup.' + + def execute(self, cn, **kw): + """ + Execute the netgroup-add operation. + + The dn should not be passed as a keyword argument as it is constructed + by this method. + + Returns the entry as it will be created in LDAP. + + :param cn: The name of the netgroup + :param kw: Keyword arguments for the other LDAP attributes. + """ + self.log.info("IPA: netgroup-add '%s'" % cn) + assert 'cn' not in kw + assert 'dn' not in kw + ldap = self.api.Backend.ldap + kw['cn'] = cn +# kw['dn'] = ldap.make_netgroup_dn() + kw['ipauniqueid'] = str(uuid.uuid1()) + kw['dn'] = "ipauniqueid=%s,%s,%s" % (kw['ipauniqueid'], netgroup_base, api.env.basedn) + + if not kw.get('nisdomainname', False): + kw['nisdomainname'] = api.env.domain + + # some required objectclasses + kw['objectClass'] = ['top', 'ipaAssociation', 'ipaNISNetgroup'] + + return ldap.create(**kw) + + def output_for_cli(self, textui, result, *args, **options): + """ + Output result of this command to command line interface. + """ + textui.print_name(self.name) + textui.print_entry(result) + textui.print_dashed('Added netgroup "%s"' % result.get('cn')) + +api.register(netgroup_add) + + +class netgroup_del(crud.Del): + 'Delete an existing netgroup.' + + def execute(self, cn, **kw): + """Delete a netgroup. + + cn is the cn of the netgroup to delete + + The memberOf plugin handles removing the netgroup from any other + groups. + + :param cn: The name of the netgroup being removed. + :param kw: Not used. + """ + self.log.info("IPA: netgroup-del '%s'" % cn) + + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("cn", cn, netgroup_filter, netgroup_base) + return ldap.delete(dn) + + def output_for_cli(self, textui, result, cn): + """ + Output result of this command to command line interface. + """ + textui.print_plain('Deleted net group "%s"' % cn) + +api.register(netgroup_del) + + +class netgroup_mod(crud.Mod): + 'Edit an existing netgroup.' + def execute(self, cn, **kw): + """ + Execute the netgroup-mod operation. + + The dn should not be passed as a keyword argument as it is constructed + by this method. + + Returns the entry + + :param cn: The name of the netgroup to retrieve. + :param kw: Keyword arguments for the other LDAP attributes. + """ + self.log.info("IPA: netgroup-mod '%s'" % cn) + assert 'cn' not in kw + assert 'dn' not in kw + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("cn", cn, netgroup_filter, netgroup_base) + return ldap.update(dn, **kw) + + def output_for_cli(self, textui, result, cn, **options): + """ + Output result of this command to command line interface. + """ + textui.print_name(self.name) + textui.print_entry(result) + textui.print_dashed('Updated netgroup "%s"' % result['cn']) + +api.register(netgroup_mod) + + +class netgroup_find(crud.Find): + 'Search the netgroups.' + def execute(self, term, **kw): + ldap = self.api.Backend.ldap + + search_fields = ['ipauniqueid','description','nisdomainname','cn'] + + search_kw = {} + for s in search_fields: + search_kw[s] = term + + search_kw['objectclass'] = netgroup_filter + search_kw['base'] = netgroup_base + return ldap.search(**search_kw) + + def output_for_cli(self, textui, result, *args, **options): + counter = result[0] + groups = result[1:] + if counter == 0 or len(groups) == 0: + textui.print_plain("No entries found") + return + if len(groups) == 1: + textui.print_entry(groups[0]) + return + textui.print_name(self.name) + for g in groups: + textui.print_entry(g) + textui.print_plain('') + if counter == -1: + textui.print_plain('These results are truncated.') + textui.print_plain('Please refine your search and try again.') + textui.print_count(groups, '%d netgroups matched') + +api.register(netgroup_find) + + +class netgroup_show(crud.Get): + 'Examine an existing netgroup.' + def execute(self, cn, **kw): + """ + Execute the netgroup-show operation. + + The dn should not be passed as a keyword argument as it is constructed + by this method. + + Returns the entry + + :param cn: The name of the netgroup to retrieve. + :param kw: Unused + """ + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("cn", cn, netgroup_filter, netgroup_base) + return ldap.retrieve(dn) + + def output_for_cli(self, textui, result, *args, **options): + textui.print_entry(result) + +api.register(netgroup_show) + +class netgroup_add_member(Command): + 'Add a member to a group.' + takes_args = ( + Str('cn', + cli_name='name', + primary_key=True + ), + ) + takes_options = ( + Str('hosts?', doc='comma-separated list of hosts to add'), + Str('hostgroups?', doc='comma-separated list of host groups to add'), + Str('users?', doc='comma-separated list of users to add'), + Str('groups?', doc='comma-separated list of groups to add'), + ) + + def execute(self, cn, **kw): + """ + Execute the netgroup-add-member operation. + + Returns the updated group entry + + :param cn: The netgroup name to add new members to. + :param kw: hosts is a comma-separated list of hosts to add + :param kw: hostgroups is a comma-separated list of host groups to add + :param kw: users is a comma-separated list of users to add + :param kw: groups is a comma-separated list of host to add + """ + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("cn", cn, netgroup_filter, netgroup_base) + add_failed = [] + to_add = [] + completed = 0 + + # Hosts + members = get_members(kw.get('hosts', '')) + (to_add, add_failed) = find_members(ldap, add_failed, members, "cn", "ipaHost") + + # If a host is not found we'll consider it an externalHost. It will + # be up to the user to handle typos + if add_failed: + (completed, failed) = add_external(ldap, completed, add_failed, cn) + add_failed = failed + + (completed, failed) = add_members(ldap, completed, to_add, dn, 'memberhost') + add_failed+=failed + + # Host groups + members = get_members(kw.get('hostgroups', '')) + (to_add, add_failed) = find_members(ldap, add_failed, members, "cn", hostgroup_filter) + (completed, failed) = add_members(ldap, completed, to_add, dn, 'memberhost') + add_failed+=failed + + # User + members = get_members(kw.get('users', '')) + (to_add, add_failed) = find_members(ldap, add_failed, members, "uid") + (completed, failed) = add_members(ldap, completed, to_add, dn, 'memberuser') + add_failed+=failed + + # Groups + members = get_members(kw.get('groups', '')) + (to_add, add_failed) = find_members(ldap, add_failed, members, "cn", "posixGroup") + (completed, failed) = add_members(ldap, completed, to_add, dn, 'memberuser') + add_failed+=failed + + return add_failed + + def output_for_cli(self, textui, result, *args, **options): + """ + Output result of this command to command line interface. + """ + if result: + textui.print_plain("These entries failed to add to the group:") + for a in result: + print "\t'%s'" % a + else: + textui.print_plain("netgroup membership updated.") + +api.register(netgroup_add_member) + + +class netgroup_remove_member(Command): + 'Remove a member from a group.' + takes_args = ( + Str('cn', + cli_name='name', + primary_key=True + ), + ) + takes_options = ( + Str('hosts?', doc='comma-separated list of hosts to remove'), + Str('hostgroups?', doc='comma-separated list of groups to remove'), + Str('users?', doc='comma-separated list of users to remove'), + Str('groups?', doc='comma-separated list of groups to remove'), + ) + def execute(self, cn, **kw): + """ + Execute the group-remove-member operation. + + Returns the members that could not be added + + :param cn: The group name to add new members to. + :param kw: hosts is a comma-separated list of hosts to remove + :param kw: hostgroups is a comma-separated list of host groups to remove + :param kw: users is a comma-separated list of users to remove + :param kw: groups is a comma-separated list of host to remove + """ + ldap = self.api.Backend.ldap + dn = ldap.find_entry_dn("cn", cn, netgroup_filter, netgroup_base) + remove_failed = [] + to_remove = [] + completed = 0 + + # Hosts + members = get_members(kw.get('hosts', '')) + (to_remove, remove_failed) = find_members(ldap, remove_failed, members, "cn", "ipaHost") + + # If a host is not found we'll consider it an externalHost. It will + # be up to the user to handle typos + if remove_failed: + (completed, failed) = remove_external(ldap, completed, remove_failed, cn) + remove_failed = failed + + (completed, failed) = remove_members(ldap, completed, to_remove, dn, 'memberhost') + remove_failed+=failed + + # Host groups + members = get_members(kw.get('hostgroups', '')) + (to_remove, remove_failed) = find_members(ldap, remove_failed, members, "cn", hostgroup_filter) + (completed, failed) = remove_members(ldap, completed, to_remove, dn, 'memberhost') + remove_failed+=failed + + # User + members = get_members(kw.get('users', '')) + (to_remove, remove_failed) = find_members(ldap, remove_failed, members, "uid") + (completed, failed) = remove_members(ldap, completed, to_remove, dn, 'memberuser') + remove_failed+=failed + + # Groups + members = get_members(kw.get('groups', '')) + (to_remove, remove_failed) = find_members(ldap, remove_failed, members, "cn", "posixGroup") + (completed, failed) = remove_members(ldap, completed, to_remove, dn, 'memberuser') + remove_failed+=failed + + return remove_failed + + def output_for_cli(self, textui, result, *args, **options): + """ + Output result of this command to command line interface. + """ + if result: + textui.print_plain("These entries failed to be removed from the group:") + for a in result: + print "\t'%s'" % a + else: + textui.print_plain("netgroup membership updated.") + +api.register(netgroup_remove_member) -- cgit From aba16941ee6826b0d65d11e73efb8a62f4a35da1 Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Mon, 19 Jan 2009 10:39:08 -0500 Subject: Skip doc testing of the uuid module since by nature it changes every time --- ipalib/ipauuid.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) (limited to 'ipalib') diff --git a/ipalib/ipauuid.py b/ipalib/ipauuid.py index ae3da25c..457639d8 100644 --- a/ipalib/ipauuid.py +++ b/ipalib/ipauuid.py @@ -13,34 +13,34 @@ Typical usage: >>> import uuid # make a UUID based on the host ID and current time - >>> uuid.uuid1() + >>> uuid.uuid1() #doctest: +SKIP UUID('a8098c1a-f86e-11da-bd1a-00112444be1e') # make a UUID using an MD5 hash of a namespace UUID and a name - >>> uuid.uuid3(uuid.NAMESPACE_DNS, 'python.org') + >>> uuid.uuid3(uuid.NAMESPACE_DNS, 'python.org') #doctest: +SKIP UUID('6fa459ea-ee8a-3ca4-894e-db77e160355e') # make a random UUID - >>> uuid.uuid4() + >>> uuid.uuid4() #doctest: +SKIP UUID('16fd2706-8baf-433b-82eb-8c7fada847da') # make a UUID using a SHA-1 hash of a namespace UUID and a name - >>> uuid.uuid5(uuid.NAMESPACE_DNS, 'python.org') + >>> uuid.uuid5(uuid.NAMESPACE_DNS, 'python.org') #doctest: +SKIP UUID('886313e1-3b8a-5372-9b90-0c9aee199e5d') # make a UUID from a string of hex digits (braces and hyphens ignored) - >>> x = uuid.UUID('{00010203-0405-0607-0809-0a0b0c0d0e0f}') + >>> x = uuid.UUID('{00010203-0405-0607-0809-0a0b0c0d0e0f}') #doctest: +SKIP # convert a UUID to a string of hex digits in standard form - >>> str(x) + >>> str(x) #doctest: +SKIP '00010203-0405-0607-0809-0a0b0c0d0e0f' # get the raw 16 bytes of the UUID - >>> x.bytes + >>> x.bytes #doctest: +SKIP '\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f' # make a UUID from a 16-byte string - >>> uuid.UUID(bytes=x.bytes) + >>> uuid.UUID(bytes=x.bytes) #doctest: +SKIP UUID('00010203-0405-0607-0809-0a0b0c0d0e0f') """ -- cgit From 55fba5420d8ea57931937728102094492ca73d86 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 19 Jan 2009 21:10:42 -0700 Subject: Added rpc.xmlclient backend plugin for forwarding; added corresponding unit tests --- ipalib/errors2.py | 41 +++++++++++++++++++++++++++++------------ ipalib/rpc.py | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 12 deletions(-) (limited to 'ipalib') diff --git a/ipalib/errors2.py b/ipalib/errors2.py index 4c8acd5d..7e2eea05 100644 --- a/ipalib/errors2.py +++ b/ipalib/errors2.py @@ -275,10 +275,27 @@ class VersionError(PublicError): format = _('%(cver)s client incompatible with %(sver)s server at %(server)r') +class UnknownError(PublicError): + """ + **902** Raised when client does not know error it caught from server. + + For example: + + >>> raise UnknownError(code=57, server='localhost', error=u'a new error') + ... + Traceback (most recent call last): + ... + UnknownError: unknown error 57 from localhost: a new error + + """ + + errno = 902 + format = _('unknown error %(code)d from %(server)s: %(error)s') + class InternalError(PublicError): """ - **902** Raised to conceal a non-public exception. + **903** Raised to conceal a non-public exception. For example: @@ -288,7 +305,7 @@ class InternalError(PublicError): InternalError: an internal error has occured """ - errno = 902 + errno = 903 format = _('an internal error has occured') def __init__(self, message=None): @@ -300,7 +317,7 @@ class InternalError(PublicError): class ServerInternalError(PublicError): """ - **903** Raised when client catches an `InternalError` from server. + **904** Raised when client catches an `InternalError` from server. For example: @@ -310,13 +327,13 @@ class ServerInternalError(PublicError): ServerInternalError: an internal error has occured on server at 'https://localhost' """ - errno = 903 + errno = 904 format = _('an internal error has occured on server at %(server)r') class CommandError(PublicError): """ - **904** Raised when an unknown command is called. + **905** Raised when an unknown command is called. For example: @@ -326,13 +343,13 @@ class CommandError(PublicError): CommandError: unknown command 'foobar' """ - errno = 904 + errno = 905 format = _('unknown command %(name)r') class ServerCommandError(PublicError): """ - **905** Raised when client catches a `CommandError` from server. + **906** Raised when client catches a `CommandError` from server. For example: @@ -343,13 +360,13 @@ class ServerCommandError(PublicError): ServerCommandError: error on server 'https://localhost': unknown command 'foobar' """ - errno = 905 + errno = 906 format = _('error on server %(server)r: %(error)s') class NetworkError(PublicError): """ - **906** Raised when a network connection cannot be created. + **907** Raised when a network connection cannot be created. For example: @@ -359,13 +376,13 @@ class NetworkError(PublicError): NetworkError: cannot connect to 'ldap://localhost:389' """ - errno = 906 + errno = 907 format = _('cannot connect to %(uri)r') class ServerNetworkError(PublicError): """ - **907** Raised when client catches a `NetworkError` from server. + **908** Raised when client catches a `NetworkError` from server. For example: @@ -376,7 +393,7 @@ class ServerNetworkError(PublicError): ServerNetworkError: error on server 'https://localhost': cannot connect to 'ldap://localhost:389' """ - errno = 907 + errno = 908 format = _('error on server %(server)r: %(error)s') diff --git a/ipalib/rpc.py b/ipalib/rpc.py index acfdae95..e7823ef9 100644 --- a/ipalib/rpc.py +++ b/ipalib/rpc.py @@ -30,7 +30,11 @@ Also see the `ipaserver.rpcserver` module. """ from types import NoneType +import threading from xmlrpclib import Binary, Fault, dumps, loads +from ipalib.backend import Backend +from ipalib.errors2 import public_errors, PublicError, UnknownError +from ipalib.request import context def xml_wrap(value): @@ -155,3 +159,49 @@ def xml_loads(data): """ (params, method) = loads(data) return (xml_unwrap(params), method) + + +class xmlclient(Backend): + """ + Forwarding backend for XML-RPC client. + """ + + def __init__(self): + super(xmlclient, self).__init__() + self.__errors = dict((e.errno, e) for e in public_errors) + + def forward(self, name, *args, **kw): + """ + Forward call to command named ``name`` over XML-RPC. + + This method will encode and forward an XML-RPC request, and will then + decode and return the corresponding XML-RPC response. + + :param command: The name of the command being forwarded. + :param args: Positional arguments to pass to remote command. + :param kw: Keyword arguments to pass to remote command. + """ + if name not in self.Command: + raise ValueError( + '%s.forward(): %r not in api.Command' % (self.name, name) + ) + if not hasattr(context, 'xmlconn'): + raise StandardError( + '%s.forward(%r): need context.xmlconn in thread %r' % ( + self.name, name, threading.currentThread().getName() + ) + ) + command = getattr(context.xmlconn, name) + params = args + (kw,) + try: + response = command(xml_wrap(params)) + return xml_unwrap(response) + except Fault, e: + if e.faultCode in self.__errors: + error = self.__errors[e.faultCode] + raise error(message=e.faultString) + raise UnknownError( + code=e.faultCode, + error=e.faultString, + server=self.env.xmlrpc_uri, + ) -- cgit From f22f3eabb20e6c46f78ba5c40d75bb52b7872060 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Tue, 20 Jan 2009 09:38:39 -0700 Subject: Fixed Param.validate() so that self.query is checked after self.required --- ipalib/parameters.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'ipalib') diff --git a/ipalib/parameters.py b/ipalib/parameters.py index 7cc93e90..76d88347 100644 --- a/ipalib/parameters.py +++ b/ipalib/parameters.py @@ -490,13 +490,12 @@ class Param(ReadOnly): :param value: A proposed value for this parameter. """ - # FIXME: this should be after 'if value is None:' - if self.query: - return if value is None: if self.required: raise RequirementError(name=self.name) return + if self.query: + return if self.multivalue: if type(value) is not tuple: raise TypeError( -- cgit From c4702f3a1e2e8f225ed3a53288adaf9167169772 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 21 Jan 2009 13:08:30 -0700 Subject: Fixed some Python2.4 issues in ipauuid.py doctests --- ipalib/ipauuid.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) (limited to 'ipalib') diff --git a/ipalib/ipauuid.py b/ipalib/ipauuid.py index 457639d8..0c364e76 100644 --- a/ipalib/ipauuid.py +++ b/ipalib/ipauuid.py @@ -10,37 +10,37 @@ the computer's network address. uuid4() creates a random UUID. Typical usage: - >>> import uuid + >>> from ipalib import uuid # make a UUID based on the host ID and current time - >>> uuid.uuid1() #doctest: +SKIP - UUID('a8098c1a-f86e-11da-bd1a-00112444be1e') + >>> uuid.uuid1() #doctest: +ELLIPSIS + UUID('...') # make a UUID using an MD5 hash of a namespace UUID and a name - >>> uuid.uuid3(uuid.NAMESPACE_DNS, 'python.org') #doctest: +SKIP - UUID('6fa459ea-ee8a-3ca4-894e-db77e160355e') + >>> uuid.uuid3(uuid.NAMESPACE_DNS, 'python.org') #doctest: +ELLIPSIS + UUID('...') # make a random UUID - >>> uuid.uuid4() #doctest: +SKIP - UUID('16fd2706-8baf-433b-82eb-8c7fada847da') + >>> uuid.uuid4() #doctest: +ELLIPSIS + UUID('...') # make a UUID using a SHA-1 hash of a namespace UUID and a name - >>> uuid.uuid5(uuid.NAMESPACE_DNS, 'python.org') #doctest: +SKIP - UUID('886313e1-3b8a-5372-9b90-0c9aee199e5d') + >>> uuid.uuid5(uuid.NAMESPACE_DNS, 'python.org') #doctest: +ELLIPSIS + UUID('...') # make a UUID from a string of hex digits (braces and hyphens ignored) - >>> x = uuid.UUID('{00010203-0405-0607-0809-0a0b0c0d0e0f}') #doctest: +SKIP + >>> x = uuid.UUID('{00010203-0405-0607-0809-0a0b0c0d0e0f}') # convert a UUID to a string of hex digits in standard form - >>> str(x) #doctest: +SKIP + >>> str(x) '00010203-0405-0607-0809-0a0b0c0d0e0f' # get the raw 16 bytes of the UUID - >>> x.bytes #doctest: +SKIP + >>> x.bytes '\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f' # make a UUID from a 16-byte string - >>> uuid.UUID(bytes=x.bytes) #doctest: +SKIP + >>> uuid.UUID(bytes=x.bytes) UUID('00010203-0405-0607-0809-0a0b0c0d0e0f') """ -- cgit From 0c95e86cf328a443cbe8559c175f022fd93ce212 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 21 Jan 2009 13:22:22 -0700 Subject: Removed doctest +ELLIPSIS directive from some examples in ipauuid.py that didn't need it --- ipalib/ipauuid.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'ipalib') diff --git a/ipalib/ipauuid.py b/ipalib/ipauuid.py index 0c364e76..dde7f8ae 100644 --- a/ipalib/ipauuid.py +++ b/ipalib/ipauuid.py @@ -17,16 +17,16 @@ Typical usage: UUID('...') # make a UUID using an MD5 hash of a namespace UUID and a name - >>> uuid.uuid3(uuid.NAMESPACE_DNS, 'python.org') #doctest: +ELLIPSIS - UUID('...') + >>> uuid.uuid3(uuid.NAMESPACE_DNS, 'python.org') + UUID('6fa459ea-ee8a-3ca4-894e-db77e160355e') # make a random UUID >>> uuid.uuid4() #doctest: +ELLIPSIS UUID('...') # make a UUID using a SHA-1 hash of a namespace UUID and a name - >>> uuid.uuid5(uuid.NAMESPACE_DNS, 'python.org') #doctest: +ELLIPSIS - UUID('...') + >>> uuid.uuid5(uuid.NAMESPACE_DNS, 'python.org') + UUID('886313e1-3b8a-5372-9b90-0c9aee199e5d') # make a UUID from a string of hex digits (braces and hyphens ignored) >>> x = uuid.UUID('{00010203-0405-0607-0809-0a0b0c0d0e0f}') -- cgit From 5d82e3b35a8fb2d4c25f282cddad557a7650197c Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Wed, 21 Jan 2009 13:36:53 -0700 Subject: Changed ipauuid.py docstring slightly so epydoc formats it correctly --- ipalib/ipauuid.py | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) (limited to 'ipalib') diff --git a/ipalib/ipauuid.py b/ipalib/ipauuid.py index dde7f8ae..9923dc7a 100644 --- a/ipalib/ipauuid.py +++ b/ipalib/ipauuid.py @@ -1,3 +1,5 @@ +# This is a backport of the Python2.5 uuid module. + r"""UUID objects (universally unique identifiers) according to RFC 4122. This module provides immutable UUID objects (class UUID) and the functions @@ -10,36 +12,49 @@ the computer's network address. uuid4() creates a random UUID. Typical usage: + **Important:** So that the freeIPA Python 2.4 ``uuid`` backport can be + automatically loaded when needed, import the ``uuid`` module like this: + >>> from ipalib import uuid - # make a UUID based on the host ID and current time + Make a UUID based on the host ID and current time: + >>> uuid.uuid1() #doctest: +ELLIPSIS UUID('...') - # make a UUID using an MD5 hash of a namespace UUID and a name + Make a UUID using an MD5 hash of a namespace UUID and a name: + >>> uuid.uuid3(uuid.NAMESPACE_DNS, 'python.org') UUID('6fa459ea-ee8a-3ca4-894e-db77e160355e') - # make a random UUID + Make a random UUID: + >>> uuid.uuid4() #doctest: +ELLIPSIS UUID('...') - # make a UUID using a SHA-1 hash of a namespace UUID and a name + Make a UUID using a SHA-1 hash of a namespace UUID and a name: + >>> uuid.uuid5(uuid.NAMESPACE_DNS, 'python.org') UUID('886313e1-3b8a-5372-9b90-0c9aee199e5d') - # make a UUID from a string of hex digits (braces and hyphens ignored) + Make a UUID from a string of hex digits (braces and hyphens ignored): + >>> x = uuid.UUID('{00010203-0405-0607-0809-0a0b0c0d0e0f}') + >>> x + UUID('00010203-0405-0607-0809-0a0b0c0d0e0f') + + Convert a UUID to a string of hex digits in standard form: - # convert a UUID to a string of hex digits in standard form >>> str(x) '00010203-0405-0607-0809-0a0b0c0d0e0f' - # get the raw 16 bytes of the UUID + Get the raw 16 bytes of the UUID: + >>> x.bytes '\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f' - # make a UUID from a 16-byte string + Make a UUID from a 16-byte string: + >>> uuid.UUID(bytes=x.bytes) UUID('00010203-0405-0607-0809-0a0b0c0d0e0f') """ -- cgit