summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJason Gerard DeRose <jderose@redhat.com>2009-01-23 15:49:16 -0700
committerRob Crittenden <rcritten@redhat.com>2009-02-03 15:29:03 -0500
commitf7375bb6090f32c3feb3d71e196ed01ee19fecc8 (patch)
treef80054b751d6bcace55e90f60a8ffb72c6afc6fe
parente0b00d598147f294c88b3cfb1b2218fe4381792a (diff)
downloadfreeipa-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.py68
-rw-r--r--ipalib/request.py33
-rw-r--r--tests/test_ipalib/test_backend.py179
-rw-r--r--tests/util.py6
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)
+