diff options
author | Jason Gerard DeRose <jderose@redhat.com> | 2009-01-23 15:49:16 -0700 |
---|---|---|
committer | Rob Crittenden <rcritten@redhat.com> | 2009-02-03 15:29:03 -0500 |
commit | f7375bb6090f32c3feb3d71e196ed01ee19fecc8 (patch) | |
tree | f80054b751d6bcace55e90f60a8ffb72c6afc6fe | |
parent | e0b00d598147f294c88b3cfb1b2218fe4381792a (diff) | |
download | freeipa-f7375bb6090f32c3feb3d71e196ed01ee19fecc8.tar.gz freeipa-f7375bb6090f32c3feb3d71e196ed01ee19fecc8.tar.xz freeipa-f7375bb6090f32c3feb3d71e196ed01ee19fecc8.zip |
Added stuff for managing connections and new Executioner backend base class
-rw-r--r-- | ipalib/backend.py | 68 | ||||
-rw-r--r-- | ipalib/request.py | 33 | ||||
-rw-r--r-- | tests/test_ipalib/test_backend.py | 179 | ||||
-rw-r--r-- | tests/util.py | 6 |
4 files changed, 280 insertions, 6 deletions
diff --git a/ipalib/backend.py b/ipalib/backend.py index b1e15f33..827067f4 100644 --- a/ipalib/backend.py +++ b/ipalib/backend.py @@ -21,7 +21,10 @@ Base classes for all backed-end plugins. """ +import threading import plugable +from errors2 import PublicError, InternalError, CommandError +from request import context, Connection, destroy_context class Backend(plugable.Plugin): @@ -29,7 +32,70 @@ class Backend(plugable.Plugin): Base class for all backend plugins. """ - __proxy__ = False # Backend plugins are not wrapped in a PluginProxy + __proxy__ = False # Backend plugins are not wrapped in a PluginProxy + + +class Connectible(Backend): + # Override in subclass: + connection_klass = None + + def connect(self, *args, **kw): + """ + Create thread-local connection. + """ + if hasattr(context, self.name): + raise StandardError( + "connection 'context.%s' already exists in thread %r" % ( + self.name, threading.currentThread().getName() + ) + ) + if not issubclass(self.connection_klass, Connection): + raise ValueError( + '%s.connection_klass must be a request.Connection subclass' % self.name + ) + conn = self.connection_klass(*args, **kw) + setattr(context, self.name, conn) + assert self.conn is conn.conn + + def isconnected(self): + """ + Return ``True`` if thread-local connection on `request.context` exists. + """ + return hasattr(context, self.name) + + def __get_conn(self): + """ + Return thread-local connection. + """ + if not hasattr(context, self.name): + raise AttributeError('no context.%s in thread %r' % ( + self.name, threading.currentThread().getName()) + ) + return getattr(context, self.name).conn + conn = property(__get_conn) + + +class Executioner(Backend): + + def execute(self, name, *args, **options): + error = None + try: + if name not in self.Command: + raise CommandError(name=name) + result = self.Command[name](*args, **options) + except PublicError, e: + error = e + except StandardError, e: + self.exception( + 'non-public: %s: %s', e.__class__.__name__, str(e) + ) + error = InternalError() + destroy_context() + if error is None: + return result + assert isinstance(error, PublicError) + raise error + class Context(plugable.Plugin): diff --git a/ipalib/request.py b/ipalib/request.py index 6ad7ad35..812e526d 100644 --- a/ipalib/request.py +++ b/ipalib/request.py @@ -25,6 +25,7 @@ Per-request thread-local data. import threading import locale import gettext +from base import ReadOnly, lock from constants import OVERRIDE_ERROR @@ -32,6 +33,38 @@ from constants import OVERRIDE_ERROR context = threading.local() +class Connection(ReadOnly): + """ + Base class for connection objects stored on `request.context`. + """ + + def __init__(self, *args, **kw): + self.conn = self.create(*args, **kw) + lock(self) + + def create(self, *args, **kw): + """ + Create and return the connection (implement in subclass). + """ + raise NotImplementedError('%s.create()' % self.__class__.__name__) + + def close(self): + """ + Close the connection (implement in subclass). + """ + raise NotImplementedError('%s.close()' % self.__class__.__name__) + + +def destroy_context(): + """ + Delete all attributes on thread-local `request.context`. + """ + for (name, value) in context.__dict__.items(): + if isinstance(value, Connection): + value.close() + delattr(context, name) + + def ugettext(message): if hasattr(context, 'ugettext'): return context.ugettext(message) diff --git a/tests/test_ipalib/test_backend.py b/tests/test_ipalib/test_backend.py index 88bd2da4..e9e17b92 100644 --- a/tests/test_ipalib/test_backend.py +++ b/tests/test_ipalib/test_backend.py @@ -21,8 +21,13 @@ Test the `ipalib.backend` module. """ -from ipalib import backend, plugable, errors -from tests.util import ClassChecker, raises +import threading +from tests.util import ClassChecker, raises, create_test_api +from tests.data import unicode_str +from ipalib.request import context, Connection +from ipalib.frontend import Command +from ipalib import backend, plugable, errors2, base + class test_Backend(ClassChecker): @@ -37,6 +42,176 @@ class test_Backend(ClassChecker): assert self.cls.__proxy__ is False +class DummyConnection(Connection): + + def create(self, *args, **kw): + self.args = args + self.kw = kw + self.closed = False + return 'The connection' + + def close(self): + assert self.closed is False + object.__setattr__(self, 'closed', True) + + +class test_Connectible(ClassChecker): + """ + Test the `ipalib.backend.Connectible` class. + """ + + _cls = backend.Connectible + + def test_connect(self): + """ + Test the `ipalib.backend.Connectible.connect` method. + """ + # Test that TypeError is raised when connection_klass isn't a + # Connection subclass: + class bad(self.cls): + connection_klass = base.ReadOnly + o = bad() + m = '%s.connection_klass must be a request.Connection subclass' + e = raises(ValueError, o.connect) + assert str(e) == m % 'bad' + + # Test that connection is created: + class example(self.cls): + connection_klass = DummyConnection + o = example() + args = ('Arg1', 'Arg2', 'Arg3') + kw = dict(key1='Val1', key2='Val2', key3='Val3') + assert not hasattr(context, 'example') + assert o.connect(*args, **kw) is None + conn = context.example + assert type(conn) is DummyConnection + assert conn.args == args + assert conn.kw == kw + assert conn.conn == 'The connection' + + # Test that StandardError is raised if already connected: + m = "connection 'context.%s' already exists in thread %r" + e = raises(StandardError, o.connect, *args, **kw) + assert str(e) == m % ('example', threading.currentThread().getName()) + + # Double check that it works after deleting context.example: + del context.example + assert o.connect(*args, **kw) is None + + def test_isconnected(self): + """ + Test the `ipalib.backend.Connectible.isconnected` method. + """ + class example(self.cls): + pass + for klass in (self.cls, example): + o = klass() + assert o.isconnected() is False + conn = DummyConnection() + setattr(context, klass.__name__, conn) + assert o.isconnected() is True + delattr(context, klass.__name__) + + def test_conn(self): + """ + Test the `ipalib.backend.Connectible.conn` property. + """ + msg = 'no context.%s in thread %r' + class example(self.cls): + pass + for klass in (self.cls, example): + o = klass() + e = raises(AttributeError, getattr, o, 'conn') + assert str(e) == msg % ( + klass.__name__, threading.currentThread().getName() + ) + conn = DummyConnection() + setattr(context, klass.__name__, conn) + assert o.conn is conn.conn + delattr(context, klass.__name__) + + +class test_Executioner(ClassChecker): + """ + Test the `ipalib.backend.Executioner` class. + """ + _cls = backend.Executioner + + def test_execute(self): + """ + Test the `ipalib.backend.Executioner.execute` method. + """ + (api, home) = create_test_api(in_server=True) + + class echo(Command): + takes_args = ['arg1', 'arg2+'] + takes_options = ['option1?', 'option2?'] + def execute(self, *args, **options): + assert type(args[1]) is tuple + return args + (options,) + api.register(echo) + + class good(Command): + def execute(self): + raise errors2.ValidationError( + name='nurse', + error=u'Not naughty!', + ) + api.register(good) + + class bad(Command): + def execute(self): + raise ValueError('This is private.') + api.register(bad) + + api.finalize() + o = self.cls() + o.set_api(api) + o.finalize() + + # Test that CommandError is raised: + conn = DummyConnection() + context.someconn = conn + e = raises(errors2.CommandError, o.execute, 'nope') + assert e.name == 'nope' + assert conn.closed is True # Make sure destroy_context() was called + assert context.__dict__.keys() == [] + + # Test with echo command: + arg1 = unicode_str + arg2 = (u'Hello', unicode_str, u'world!') + args = (arg1,) + arg2 + options = dict(option1=u'How are you?', option2=unicode_str) + + conn = DummyConnection() + context.someconn = conn + assert o.execute('echo', arg1, arg2, **options) == (arg1, arg2, options) + assert conn.closed is True # Make sure destroy_context() was called + assert context.__dict__.keys() == [] + + conn = DummyConnection() + context.someconn = conn + assert o.execute('echo', *args, **options) == (arg1, arg2, options) + assert conn.closed is True # Make sure destroy_context() was called + assert context.__dict__.keys() == [] + + # Test with good command: + conn = DummyConnection() + context.someconn = conn + e = raises(errors2.ValidationError, o.execute, 'good') + assert e.name == 'nurse' + assert e.error == u'Not naughty!' + assert conn.closed is True # Make sure destroy_context() was called + assert context.__dict__.keys() == [] + + # Test with bad command: + conn = DummyConnection() + context.someconn = conn + e = raises(errors2.InternalError, o.execute, 'bad') + assert conn.closed is True # Make sure destroy_context() was called + assert context.__dict__.keys() == [] + + class test_Context(ClassChecker): """ Test the `ipalib.backend.Context` class. diff --git a/tests/util.py b/tests/util.py index 631d4a05..cd7400ba 100644 --- a/tests/util.py +++ b/tests/util.py @@ -207,9 +207,9 @@ class ClassChecker(object): """ nose tear-down fixture. """ - for name in ('ugettext', 'ungettext'): - if hasattr(context, name): - delattr(context, name) + for name in context.__dict__.keys(): + delattr(context, name) + |