From 4ce8b42140d52bf681488e01c1a772629186fe3c Mon Sep 17 00:00:00 2001
From: Jan Rusnacko <jrusnack@redhat.com>
Date: Wed, 30 Oct 2013 16:00:14 +0100
Subject: [PATCH 1/4] Enhancing DSModuleProxy to work with classes.

Until now, DSModuleProxy served as proxy for functions from module.
This patch enhances it to serve as proxy for instances of class
defined within a module. The proxy can hold multiple instances of
class and all instances have access to DS and their name.
---
 lib389/module_proxy.py     |  128 ++++++++++++++++++++++++++++++++++++++++++++
 tests/conftest.py          |   86 +++++++++++++++++++++++++-----
 tests/test_module_proxy.py |  106 ++++++++++++++++++++++--------------
 3 files changed, 264 insertions(+), 56 deletions(-)
 create mode 100644 lib389/module_proxy.py

diff --git a/lib389/module_proxy.py b/lib389/module_proxy.py
new file mode 100644
index 0000000..5b524df
--- /dev/null
+++ b/lib389/module_proxy.py
@@ -0,0 +1,128 @@
+
+import os, re, sys
+from inspect import isclass
+
+class DSFuncDecorator(object):
+    """ Implements decorator that can be instantiated during runtime.
+
+        Accepts function and instance of DSInstance. When called,
+        it will call passed function with instance of DSInstance 
+        passed as the first argument.
+    """
+    def __init__(self, fun, ds_instance):
+        self.fun = fun
+        self.ds_instance = ds_instance
+
+    def __call__(self, *args, **kwargs):
+        if args is None and kwargs is None:
+            return self.fun(self.ds_instance)
+        elif args is not None and kwargs is None:
+            return self.fun(self.ds_instance, *args)
+        elif args is None and kwargs is not None:
+            return self.fun(self.ds_instance, **kwargs)
+        else:
+            return self.fun(self.ds_instance, *args, **kwargs)
+
+class DSClassProxy(dict):
+
+    def __init__(self, ds, proxied_class):
+        super(DSClassProxy, self).__init__()
+        self.proxied_class = proxied_class
+        self.ds = ds
+
+    def new(self, name, *args, **kwargs):
+        if args is None and kwargs is None:
+            self[name] = self.proxied_class(self.ds, name)
+        elif args is not None and kwargs is None:
+            self[name] = self.proxied_class(self.ds, name, *args)
+        elif args is None and kwargs is not None:
+            self[name] = self.proxied_class(self.ds, name, **kwargs)
+        else:
+            self[name] = self.proxied_class(self.ds, name, *args, **kwargs)
+
+    def remove(self, name):
+        self[name].__remove__()
+        del self[name]
+
+
+class DSModuleProxy(object):
+    """ Proxy with DS-decorated functions from modules in directory.
+
+        DSModuleProxy object acts as a proxy to functions defined in modules
+        stored in directory. Proxy itself can have other proxies as 
+        attributes, so returned object follows the structure of passed 
+        directory. All funcions from all modules are decorated with 
+        DSDecorator - DSInstance object will be passed to them as first 
+        argument.
+
+        Kudos to Milan Falesnik <mfalesni@redhat.com>
+    """
+
+    def __init__(self, module_name):
+        self.name = module_name
+
+    @classmethod
+    def populate_with_proxies(cls, ds, obj, directory):
+        """
+            Returns a proxy with decorated functions from modules in directory.
+        """
+        if not os.path.isdir(directory):
+            raise RuntimeError("Last argument %s was not directory" % directory)
+        for item in os.listdir(directory):
+
+            # case when item is directory
+            if os.path.isdir(directory + "/" + item):
+                # Recursively call self on all subdirectories
+                proxy = cls(item)
+                cls.populate_with_proxies(ds, proxy, directory + "/" + item)
+                setattr(obj, item, proxy)
+
+            # case when item is python module
+            elif (os.path.isfile(directory + "/" + item) and
+                    re.match(r'(.*\/)?[^_/].*[^_/]\.py$', item)):
+            
+                # get the module object corresponding to processed file
+                item = item.replace('.py', '')
+                to_import = (directory + "/" + item).replace('/','.')
+                __import__(to_import)
+                module = sys.modules[to_import]
+
+                # Module cannot contain both class definition and functions
+                #if cls.module_contains_class(module) and cls.module_contains_func(module):
+                #    raise RuntimeError("Module %s contains both class and functions." % module.__name__)
+
+                if cls.module_contains_class(module):
+                    proxy = DSClassProxy(ds, cls.get_class_from_module(module))
+                    setattr(obj, item, proxy)
+                else:
+                    proxy = cls(item)
+                    cls.populate_proxy_from_module(proxy, module, ds)
+                    setattr(obj, item, proxy)
+
+            else:
+                # skip anything that is not directory or python module
+                pass
+
+    @classmethod
+    def module_contains_class(cls, module):
+        for item in dir(module):
+            val = getattr(module, item)
+            if isclass(val):
+                return True
+        return False
+
+    @classmethod
+    def populate_proxy_from_module(cls, proxy, module, ds):
+        # for each function from module create decorated one and keep it
+        for attr in dir(module):
+            fun = getattr(module, attr)
+            if callable(fun):
+                decorated = DSFuncDecorator(fun, ds)
+                setattr(proxy, attr, decorated)
+
+    @classmethod
+    def get_class_from_module(cls, module):
+        for item in dir(module):
+            val = getattr(module, item)
+            if isclass(val):
+                return val
diff --git a/tests/conftest.py b/tests/conftest.py
index 763774c..8e5960d 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -5,26 +5,20 @@ import sys
 import pytest
 from tempfile import mkdtemp
 from shutil import rmtree
+from lib389.ds_instance import DSInstance
 
-def ldrop(numchar, inputstr):
-    """
-        Drop numchar characters from the beginning of each line in inputstring.
-    """
-    return "\n".join([x[numchar:] for x in string.split(inputstr, "\n")])
+### Fixtures ###
 
 @pytest.fixture
-def fake_ds_class():
+def mock_ds():
     """
-        Returns fake DSInstance-like class
+        Fixture returns mock DS object with real repl module attached to it
     """
-    class FakeDSInstance(object):
-        def return_one(self):
-            return 1
-
-    return FakeDSInstance
+    ds_inst = MockDS()
+    return ds_inst
 
 @pytest.fixture(scope="module")
-def fake_ds_modules(request):
+def fake_ds_modules_dir(request):
     """
         Returns path to directory containing fake modules.
     """
@@ -101,6 +95,70 @@ def fake_ds_modules(request):
         # Drop 12 chars from left and write to file
         automem_file.write(ldrop(12, automember_module))
 
+
+    # create module module_dir/repl.py with few functions
+    with open(module_dir + "/suffix.py", "w") as suffix_file:
+        suffix_module = """
+
+            class Suffix(object):
+                def __init__(self, ds, suffix_dn, backend):
+                    self.ds = ds
+                    self.ds.suffix_exists = True
+                    self.dn = suffix_dn
+                    self.backend = backend
+
+                def set_attribute(self, name, value):
+                    setattr(self, name, value)
+
+                def call_ds_repl_method_returns_one(self):
+                    return self.ds.repl.return_argument_given(1)
+
+                def __remove__(self):
+                    self.ds.suffix_exists = False
+
+            """
+        # Drop 12 chars from left and write to file
+        suffix_file.write(ldrop(12, suffix_module))
+
     os.chdir(os.path.dirname(module_dir))
     sys.path.append(os.path.dirname(module_dir))
-    return module_dir
+    return os.path.basename(module_dir)
+
+### Classes, helper functions ###
+
+def ldrop(numchar, inputstr):
+    """
+        Drop numchar characters from the beginning of each line in inputstring.
+    """
+    return "\n".join([x[numchar:] for x in string.split(inputstr, "\n")])
+
+class MockDS(DSInstance):
+    """
+        DS mock class for testing purposes
+
+        Inherits everything from DSInstance, but overrides few methods,
+        especially ldap query methods (ldapadd, ldapmodify ..)
+    """
+
+    def __init__(self):
+        super(MockDS, self).__init__()
+        self.dit = {}
+
+    def ldapadd_r(self, input):
+        """Simulates adding entry to DS
+
+        The entry can be found in self.dit attribute, which is 
+        a dict with structure {'dn of entry' : entry}. Entry is 
+        also converted to dict with structure {attr_name : attr_value}
+        """
+        input = input.strip()
+        entry = dict(e.strip().split(': ') for e in input.split('\n'))
+        self.dit[entry['dn']] = entry
+
+    def ldapdelete_r(self, dn):
+        """
+            Simulates deletion of entry from DS
+        """
+        del self.dit[dn]
+
+
diff --git a/tests/test_module_proxy.py b/tests/test_module_proxy.py
index e21271e..18c920d 100644
--- a/tests/test_module_proxy.py
+++ b/tests/test_module_proxy.py
@@ -1,45 +1,67 @@
 
 import os
-from lib389.ds_instance import DSModuleProxy
-
-def test_module_proxy_fun_with_no_arg(fake_ds_class, fake_ds_modules):
-    ds_inst = fake_ds_class()
-    DSModuleProxy.populate_with_proxies(ds_inst, ds_inst, os.path.basename(fake_ds_modules))
-    assert ds_inst.repl.with_no_arg() == ds_inst
-
-def test_module_proxy_fun_with_one_arg(fake_ds_class, fake_ds_modules):
-    ds_inst = fake_ds_class()
-    DSModuleProxy.populate_with_proxies(ds_inst, ds_inst, os.path.basename(fake_ds_modules))
-    assert ds_inst.repl.with_one_arg(1) == (ds_inst, 1)
-
-def test_module_proxy_plugin_fun_with_no_arg(fake_ds_class, fake_ds_modules):
-    ds_inst = fake_ds_class()
-    DSModuleProxy.populate_with_proxies(ds_inst, ds_inst, os.path.basename(fake_ds_modules))
-    assert ds_inst.plugin.automember.with_no_arg() == ds_inst
-
-def test_module_proxy_plugin_fun_with_one_arg(fake_ds_class, fake_ds_modules):
-    ds_inst = fake_ds_class()
-    DSModuleProxy.populate_with_proxies(ds_inst, ds_inst, os.path.basename(fake_ds_modules))
-    assert ds_inst.plugin.automember.with_one_arg(2) == (ds_inst, 2)
-
-def test_module_proxy_plugin_call_ds_method(fake_ds_class, fake_ds_modules):
-    ds_inst = fake_ds_class()
-    DSModuleProxy.populate_with_proxies(ds_inst, ds_inst, os.path.basename(fake_ds_modules))
-    assert ds_inst.plugin.automember.calling_ds_method(2) == (ds_inst, 1, 2)
-
-def test_module_proxy_plugin_call_another_module(fake_ds_class, fake_ds_modules):
-    ds_inst = fake_ds_class()
-    DSModuleProxy.populate_with_proxies(ds_inst, ds_inst, os.path.basename(fake_ds_modules))
-    assert ds_inst.plugin.automember.calling_repl(2, 3) == 6
-
-def test_module_proxy_plugin_call_another_module_kwarg(fake_ds_class, fake_ds_modules):
-    ds_inst = fake_ds_class()
-    DSModuleProxy.populate_with_proxies(ds_inst, ds_inst, os.path.basename(fake_ds_modules))
-    assert ds_inst.plugin.automember.calling_repl_with_kwarg1() == 1
-    assert ds_inst.plugin.automember.calling_repl_with_kwarg2() == 2
-
-def test_module_proxy_plugin_call_with_all(fake_ds_class, fake_ds_modules):
-    ds_inst = fake_ds_class()
-    DSModuleProxy.populate_with_proxies(ds_inst, ds_inst, os.path.basename(fake_ds_modules))
-    assert ds_inst.plugin.automember.with_all(0, 3, 2, 1) == (1, 7, 5, 3)
+import pytest
+from lib389.module_proxy import DSModuleProxy
 
+@pytest.fixture
+def fake_ds(fake_ds_modules_dir):
+    """
+        Returns fake DSInstance-like object
+    """
+    class FakeDSInstance(object):
+        def return_one(self):
+            return 1
+
+    ds = FakeDSInstance()
+    DSModuleProxy.populate_with_proxies(ds, ds, fake_ds_modules_dir)
+    return ds
+
+
+def test_module_proxy_fun_with_no_arg(fake_ds):
+    assert fake_ds.repl.with_no_arg() == fake_ds
+
+def test_module_proxy_fun_with_one_arg(fake_ds):
+    assert fake_ds.repl.with_one_arg(1) == (fake_ds, 1)
+
+def test_module_proxy_plugin_fun_with_no_arg(fake_ds):
+    assert fake_ds.plugin.automember.with_no_arg() == fake_ds
+
+def test_module_proxy_plugin_fun_with_one_arg(fake_ds):
+    assert fake_ds.plugin.automember.with_one_arg(2) == (fake_ds, 2)
+
+def test_module_proxy_plugin_call_ds_method(fake_ds):
+    assert fake_ds.plugin.automember.calling_ds_method(2) == (fake_ds, 1, 2)
+
+def test_module_proxy_plugin_call_another_module(fake_ds):
+    assert fake_ds.plugin.automember.calling_repl(2, 3) == 6
+
+def test_module_proxy_plugin_call_another_module_kwarg(fake_ds):
+    assert fake_ds.plugin.automember.calling_repl_with_kwarg1() == 1
+    assert fake_ds.plugin.automember.calling_repl_with_kwarg2() == 2
+
+def test_module_proxy_plugin_call_with_all(fake_ds):
+    assert fake_ds.plugin.automember.with_all(0, 3, 2, 1) == (1, 7, 5, 3)
+
+def test_module_proxy_class_new(fake_ds):
+    fake_ds.suffix.new("dc=example,dc=com", "userRoot")
+    assert fake_ds.suffix["dc=example,dc=com"].ds == fake_ds
+    assert fake_ds.suffix["dc=example,dc=com"].backend == "userRoot"
+    assert fake_ds.suffix["dc=example,dc=com"].dn == "dc=example,dc=com"
+
+def test_module_proxy_class_call(fake_ds):
+    fake_ds.suffix.new("dc=example,dc=com", "userRoot")
+    fake_ds.suffix["dc=example,dc=com"].set_attribute("foo", "bar")
+    assert fake_ds.suffix["dc=example,dc=com"].foo == "bar"
+
+def test_module_proxy_class_call_ds_method(fake_ds):
+    fake_ds.suffix.new("dc=example,dc=com", "userRoot")
+    assert fake_ds.suffix["dc=example,dc=com"].call_ds_repl_method_returns_one() == 1
+
+def test_module_proxy_class_remove(fake_ds):
+    fake_ds.suffix.new("dc=example,dc=com", "userRoot")
+    assert fake_ds.suffix_exists == True
+
+    fake_ds.suffix.remove("dc=example,dc=com")
+    assert fake_ds.suffix_exists == False
+    with pytest.raises(KeyError):
+        fake_ds.suffix["dc=example,dc=com"]
\ No newline at end of file
-- 
1.7.1

