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/base.py | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 ipalib/base.py (limited to 'ipalib/base.py') 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 -- 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/base.py | 47 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 3 deletions(-) (limited to 'ipalib/base.py') 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 + + + + -- 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 ++++++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 71 insertions(+), 7 deletions(-) (limited to 'ipalib/base.py') 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): -- 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/base.py | 70 ++++++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 51 insertions(+), 19 deletions(-) (limited to 'ipalib/base.py') 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 -- 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 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) (limited to 'ipalib/base.py') 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 -- 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 +++++++++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 20 deletions(-) (limited to 'ipalib/base.py') 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) -- 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 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) (limited to 'ipalib/base.py') 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) -- 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 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) (limited to 'ipalib/base.py') 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): -- 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 +++++++++++++++++++++++++++++++++------------------------ 1 file changed, 65 insertions(+), 48 deletions(-) (limited to 'ipalib/base.py') 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 -- 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/base.py | 67 ++++++++++++++++++++++++++-------------------------------- 1 file changed, 30 insertions(+), 37 deletions(-) (limited to 'ipalib/base.py') 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 -- 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 + 1 file changed, 1 insertion(+) (limited to 'ipalib/base.py') 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 -- 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/base.py') 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 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'ipalib/base.py') 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) -- 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 +++++++++++++++++++++++++++++++++------------------------ 1 file changed, 99 insertions(+), 70 deletions(-) (limited to 'ipalib/base.py') 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 -- 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 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'ipalib/base.py') 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 -- 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/base.py') 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 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 74 insertions(+), 2 deletions(-) (limited to 'ipalib/base.py') 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 = {} -- 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 ++++++++++++++++++++++++++------------------------------- 1 file changed, 69 insertions(+), 84 deletions(-) (limited to 'ipalib/base.py') 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): -- 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 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) (limited to 'ipalib/base.py') 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 -- 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/base.py') 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 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) (limited to 'ipalib/base.py') 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 -- 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 +++++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 86 insertions(+), 8 deletions(-) (limited to 'ipalib/base.py') 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) -- 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 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) (limited to 'ipalib/base.py') 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, ) -- 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 --------------------------------------------------------- 1 file changed, 499 deletions(-) delete mode 100644 ipalib/base.py (limited to 'ipalib/base.py') 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) -- 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 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 ipalib/base.py (limited to 'ipalib/base.py') 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 -- 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/base.py') 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/base.py') 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 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 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'ipalib/base.py') 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: -- 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/base.py') 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/base.py') 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/base.py') 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/base.py') 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 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/base.py') 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