From 878101707a43cdf412169f340363ee24c69155c6 Mon Sep 17 00:00:00 2001
From: Jan Rusnacko <jrusnack@redhat.com>
Date: Thu, 10 Oct 2013 18:19:15 +0200
Subject: [PATCH] Initial commit with DSModuleProxy.

In order to keep implementation of class DSInstance organized, we
implement all functionality as functions in separate modules. All
functions accept DSInstance object as their first argument by rule.
Even though it`s beneficial to define everythin as function in
separate module, in runtime it`s nicer to have methods on DSInstance.

ModuleProxy serves two purposes:
 - takes modules and all functions within and adds them to DSInstance
   as methods
 - creates separate namespaces for all modules within DSInstance
---
 lib389/ds_instance.py      |   82 ++++++++++++++++++++++++++++++++++
 tests/conftest.py          |  106 ++++++++++++++++++++++++++++++++++++++++++++
 tests/test_module_proxy.py |   45 +++++++++++++++++++
 3 files changed, 233 insertions(+), 0 deletions(-)
 create mode 100644 lib389/__init__.py
 create mode 100644 lib389/ds_instance.py
 create mode 100644 tests/__init__.py
 create mode 100644 tests/conftest.py
 create mode 100644 tests/test_module_proxy.py

diff --git a/lib389/__init__.py b/lib389/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/lib389/ds_instance.py b/lib389/ds_instance.py
new file mode 100644
index 0000000..7545eee
--- /dev/null
+++ b/lib389/ds_instance.py
@@ -0,0 +1,82 @@
+import os, re, sys
+
+class DSDecorator(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 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]
+                proxy = cls(item)
+
+                # for each function from module create decorated one and keep it
+                for attr in dir(module):
+                    fun = getattr(module, attr)
+                    if callable(fun):
+                        decorated = DSDecorator(fun, ds)
+                        setattr(proxy, attr, decorated)
+                setattr(obj, item, proxy)
+
+            else:
+                # skip anything that is not directory or python module
+                pass
+
+class DSInstance(object):
+
+    def __init__(self):
+        DSModuleProxy.populate_with_proxies(self, self, 'dsmodules')
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..763774c
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,106 @@
+
+import string
+import os
+import sys
+import pytest
+from tempfile import mkdtemp
+from shutil import rmtree
+
+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")])
+
+@pytest.fixture
+def fake_ds_class():
+    """
+        Returns fake DSInstance-like class
+    """
+    class FakeDSInstance(object):
+        def return_one(self):
+            return 1
+
+    return FakeDSInstance
+
+@pytest.fixture(scope="module")
+def fake_ds_modules(request):
+    """
+        Returns path to directory containing fake modules.
+    """
+
+    # module_dir contains fake ds modules, that can be used for testing
+    module_dir = mkdtemp()
+    old_cwd = os.getcwd()
+
+    # when fixture is destroyed, we change back to old cwd 
+    # and delete fake module dir
+    def fin():
+        os.chdir(old_cwd)
+        rmtree(module_dir)
+    request.addfinalizer(fin)
+
+    # make module_dir a python package
+    with open(module_dir + "/__init__.py", 'a'):
+        os.utime(module_dir + "/__init__.py", None)
+
+    # create module module_dir/repl.py with few functions
+    with open(module_dir + "/repl.py", "w") as repl_file:
+        repl_module = """
+            def with_no_arg(ds):
+                return ds
+
+            def with_one_arg(ds, arg):
+                return ds, arg
+
+            def return_argument_given(ds, arg):
+                return arg
+
+            def with_kwarg(ds, kwarg=2):
+                return kwarg
+
+            def with_all(ds, arg1, arg2, kwarg1=1, kwarg2=2):
+                return arg1, arg2, kwarg1, kwarg2
+
+            """
+        # Drop 12 chars from left and write to file
+        repl_file.write(ldrop(12, repl_module))
+
+    os.mkdir(module_dir + "/plugin")
+
+    # make module_dir/plugin a python package
+    with open(module_dir + "/plugin/__init__.py", 'a'):
+        os.utime(module_dir + "/plugin/__init__.py", None)
+
+    # create module module_dir/plugin/automember.py with few functions
+    with open(module_dir + "/plugin/automember.py", "w") as automem_file:
+        automember_module = """
+            def with_no_arg(ds):
+                return ds
+
+            def with_one_arg(ds, arg):
+                return ds, arg
+
+            def calling_ds_method(ds, arg):
+                return (ds, ds.return_one(), arg)
+
+            def calling_repl(ds, arg1, arg2):
+                return ds.repl.return_argument_given(1) + arg1 + arg2
+
+            def calling_repl_with_kwarg2(ds):
+                return ds.repl.with_kwarg()
+
+            def calling_repl_with_kwarg1(ds):
+                return ds.repl.with_kwarg(kwarg=1)
+
+            def with_all(ds, arg1, arg2, kwarg1=1, kwarg2=2):
+                a, b, c, d = ds.repl.with_all(1, 4, kwarg1=3)
+                return a + arg1, b + arg2, c + kwarg1, d + kwarg2
+
+            """
+        # Drop 12 chars from left and write to file
+        automem_file.write(ldrop(12, automember_module))
+
+    os.chdir(os.path.dirname(module_dir))
+    sys.path.append(os.path.dirname(module_dir))
+    return module_dir
diff --git a/tests/test_module_proxy.py b/tests/test_module_proxy.py
new file mode 100644
index 0000000..e21271e
--- /dev/null
+++ b/tests/test_module_proxy.py
@@ -0,0 +1,45 @@
+
+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)
+
-- 
1.7.1

