From 8a2ad82232251a7e91dc01bcefcf21d79ba38b79 Mon Sep 17 00:00:00 2001 From: Martin Sivak Date: Fri, 23 Nov 2012 16:02:06 +0100 Subject: Add setuptools and Fedora infrastructure structure and files --- di.py | 219 ------------------------------------------------------- di/__init__.py | 1 + di/core.py | 226 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ python-di.spec | 41 +++++++++++ setup.py | 29 ++++++++ 5 files changed, 297 insertions(+), 219 deletions(-) delete mode 100644 di.py create mode 100644 di/__init__.py create mode 100644 di/core.py create mode 100644 python-di.spec create mode 100644 setup.py diff --git a/di.py b/di.py deleted file mode 100644 index 8ffce38..0000000 --- a/di.py +++ /dev/null @@ -1,219 +0,0 @@ -"""This module implements dependency injection mechanisms.""" - -__author__ = "Martin Sivak " -__all__ = ["DI_ENABLE", "di_enable", "inject", "usesclassinject"] - -from functools import wraps, partial -from types import FunctionType - -DI_ENABLE = True - -def di_enable(method): - """This decorator enables DI mechanisms in an environment - where DI is disabled by default. - - Can be used only on methods or simple functions. - """ - @wraps(method) - def caller(*args, **kwargs): - """The replacement method doing the DI enablement. - """ - global DI_ENABLE - old = DI_ENABLE - DI_ENABLE = True - ret = method(*args, **kwargs) - DI_ENABLE = old - return ret - - return caller - -class DiRegistry(object): - """This class is the internal core of the DI engine. - It records the injected objects, handles the execution - and cleanup tasks associated with the DI mechanisms. - """ - - def __init__(self, obj): - self._obj = obj - self._used_objects = {} - self._obj._di_ = self._used_objects - - def __get__(self, obj, objtype): - """Support instance methods.""" - return partial(self.__call__, obj) - - def register(self, *args, **kwargs): - """Add registered injections to the instance of DiRegistry - """ - self._used_objects.update(kwargs) - for used_object in args: - if hasattr(used_object, "__name__"): - self._used_objects[used_object.__name__] = used_object - elif isinstance(used_object, basestring): - pass # it is already global, so this is just an annotation - else: - raise ValueError("%s is not a string or object with __name__" % used_object) - - def __call__(self, *args, **kwargs): - if not issubclass(type(self._obj), FunctionType): - # call constructor or callable class - # (which use @usesclassinject if needed) - return self._obj(*args, **kwargs) - else: - return di_call(self._used_objects, self._obj, - *args, **kwargs) - -def func_globals(func): - """Helper method that allows access to globals - depending on the Python version. - """ - if hasattr(func, "func_globals"): - return func.func_globals # Python 2 - else: - return func.__globals__ # Python 3 - -def di_call(di_dict, method, *args, **kwargs): - """This method is the core of dependency injection framework. - It modifies methods global namespace to define all the injected - variables, executed the method under test and then restores - the global namespace back. - - This variant is used on plain functions. - - The modified global namespace is discarded after the method finishes - so all new global variables and changes to scalars will be lost. - """ - # modify the globals - new_globals = func_globals(method).copy() - new_globals.update(di_dict) - - # create new func with modified globals - new_method = FunctionType(method.func_code, - new_globals, method.func_name, - method.func_defaults, method.func_closure) - - # execute the method and return it's ret value - return new_method(*args, **kwargs) - - -def inject(*args, **kwargs): - """Decorator that registers all the injections we want to pass into - a unit possibly under test. - - It can be used to decorate class, method or simple function, but - if it is a decorated class, it's methods has to be decorated with - @usesinject to use the DI mechanism. - """ - def inject_decorate(obj): - """The actual decorator generated by @inject.""" - if not DI_ENABLE: - return obj - - if not isinstance(obj, DiRegistry): - obj = DiRegistry(obj) - - obj.register(*args, **kwargs) - return obj - - return inject_decorate - -def usesclassinject(method): - """This decorator marks a method inside of @inject decorated - class as a method that should use the dependency injection - mechanisms. - """ - if not DI_ENABLE: - return method - - @wraps(method) - def call(*args, **kwargs): - """The replacement method acting as a proxy to @inject - decorated class and it's DI mechanisms.""" - self = args[0] - return di_call(self._di_, method, *args, **kwargs) - - return call - -### Unittests are defined below this point -import unittest - -class BareFuncTestCase(unittest.TestCase): - @inject(injected_func = str.lower) - def method(self, arg): - return injected_func(arg) - - @inject(injected_func = method) - def method2(self, arg): - return injected_func(self, arg) - - def test_bare_inject(self): - """Tests the injection to plain methods.""" - self.assertEqual("a", self.method("A")) - - def test_double_inject(self): - """Tests the injection to two plain methods.""" - self.assertEqual("a", self.method2("A")) - - def test_inject_global_tainting(self): - """Tests whether the global namespace is clean - after the injection is done.""" - global injected_func - injected_func = None - self.method("A") - self.assertEqual(None, injected_func) - - -@inject(injected_func = str.lower) -class Test(object): - """Test fixture for class injection.""" - @usesclassinject - def method(self, arg): - return injected_func(arg) - - -@inject(injected_func = str.lower) -class TestInit(object): - """Test fixture for injection to __init__.""" - @usesclassinject - def __init__(self, arg): - self.value = injected_func(arg) - - -@inject(injected_func = str.lower) -class TestCallable(object): - """Test fixture for callable classes.""" - @usesclassinject - def __call__(self, arg): - return injected_func(arg) - -class TestCallableSingle(object): - """Test fixture for callable classes with - simple method injection.""" - @inject(injected_func = str.lower) - def __call__(self, arg): - return injected_func(arg) - -class ClassDITestCase(unittest.TestCase): - - def test_class_inject(self): - """Test injection to instance method.""" - obj = Test() - self.assertEqual("a", obj.method("A")) - - def test_class_init_inject(self): - """Test injection to class constructor.""" - obj = TestInit("A") - self.assertEqual("a", obj.value) - - def test_callable_class(self): - """Test class injection to callable class.""" - obj = TestCallable() - self.assertEqual("a", obj("A")) - - def test_callable_class_single(self): - """Test method injection to callable class.""" - obj = TestCallableSingle() - self.assertEqual("a", obj("A")) - -if __name__ == "__main__": - unittest.main() diff --git a/di/__init__.py b/di/__init__.py new file mode 100644 index 0000000..bb67a43 --- /dev/null +++ b/di/__init__.py @@ -0,0 +1 @@ +from .core import * diff --git a/di/core.py b/di/core.py new file mode 100644 index 0000000..0962750 --- /dev/null +++ b/di/core.py @@ -0,0 +1,226 @@ +"""This module implements dependency injection mechanisms.""" + +__author__ = "Martin Sivak " +__all__ = ["DI_ENABLE", "di_enable", "inject", "usesclassinject"] + +from functools import wraps, partial +from types import FunctionType + +DI_ENABLE = True + +def di_enable(method): + """This decorator enables DI mechanisms in an environment + where DI is disabled by default. Must be the outermost + decorator. + + Can be used only on methods or simple functions. + """ + @wraps(method) + def caller(*args, **kwargs): + """The replacement method doing the DI enablement. + """ + global DI_ENABLE + old = DI_ENABLE + DI_ENABLE = True + ret = method(*args, **kwargs) + DI_ENABLE = old + return ret + + return caller + +class DiRegistry(object): + """This class is the internal core of the DI engine. + It records the injected objects, handles the execution + and cleanup tasks associated with the DI mechanisms. + """ + + def __init__(self, obj): + self._obj = obj + self._used_objects = {} + self._obj._di_ = self._used_objects + + def __get__(self, obj, objtype): + """Support instance methods.""" + return partial(self.__call__, obj) + + def register(self, *args, **kwargs): + """Add registered injections to the instance of DiRegistry + """ + self._used_objects.update(kwargs) + for used_object in args: + if hasattr(used_object, "__name__"): + self._used_objects[used_object.__name__] = used_object + elif isinstance(used_object, basestring): + pass # it is already global, so this is just an annotation + else: + raise ValueError("%s is not a string or object with __name__" % used_object) + + def __call__(self, *args, **kwargs): + if not issubclass(type(self._obj), FunctionType): + # call constructor or callable class + # (which use @usesclassinject if needed) + return self._obj(*args, **kwargs) + else: + return di_call(self._used_objects, self._obj, + *args, **kwargs) + +def func_globals(func): + """Helper method that allows access to globals + depending on the Python version. + """ + if hasattr(func, "func_globals"): + return func.func_globals # Python 2 + else: + return func.__globals__ # Python 3 + +def di_call(di_dict, method, *args, **kwargs): + """This method is the core of dependency injection framework. + It modifies methods global namespace to define all the injected + variables, executed the method under test and then restores + the global namespace back. + + This variant is used on plain functions. + + The modified global namespace is discarded after the method finishes + so all new global variables and changes to scalars will be lost. + """ + # modify the globals + new_globals = func_globals(method).copy() + new_globals.update(di_dict) + + # create new func with modified globals + new_method = FunctionType(method.func_code, + new_globals, method.func_name, + method.func_defaults, method.func_closure) + + # execute the method and return it's ret value + return new_method(*args, **kwargs) + + +def inject(*args, **kwargs): + """Decorator that registers all the injections we want to pass into + a unit possibly under test. + + It can be used to decorate class, method or simple function, but + if it is a decorated class, it's methods has to be decorated with + @usesinject to use the DI mechanism. + """ + def inject_decorate(obj): + """The actual decorator generated by @inject.""" + if not DI_ENABLE: + return obj + + if not isinstance(obj, DiRegistry): + obj = DiRegistry(obj) + + obj.register(*args, **kwargs) + return obj + + return inject_decorate + +def usesclassinject(method): + """This decorator marks a method inside of @inject decorated + class as a method that should use the dependency injection + mechanisms. + """ + if not DI_ENABLE: + return method + + @wraps(method) + def call(*args, **kwargs): + """The replacement method acting as a proxy to @inject + decorated class and it's DI mechanisms.""" + self = args[0] + return di_call(self._di_, method, *args, **kwargs) + + return call + +### Unittests are defined below this point +import unittest + + +class BareFuncEnableTestCase(unittest.TestCase): + @inject(injected_func = str.lower) + def method(self, arg): + return injected_func(arg) + +class BareFuncTestCase(unittest.TestCase): + @inject(injected_func = str.lower) + def method(self, arg): + return injected_func(arg) + + @inject(injected_func = method) + def method2(self, arg): + return injected_func(self, arg) + + def test_bare_inject(self): + """Tests the injection to plain methods.""" + self.assertEqual("a", self.method("A")) + + def test_double_inject(self): + """Tests the injection to two plain methods.""" + self.assertEqual("a", self.method2("A")) + + def test_inject_global_tainting(self): + """Tests whether the global namespace is clean + after the injection is done.""" + global injected_func + injected_func = None + self.method("A") + self.assertEqual(None, injected_func) + + +@inject(injected_func = str.lower) +class Test(object): + """Test fixture for class injection.""" + @usesclassinject + def method(self, arg): + return injected_func(arg) + + +@inject(injected_func = str.lower) +class TestInit(object): + """Test fixture for injection to __init__.""" + @usesclassinject + def __init__(self, arg): + self.value = injected_func(arg) + + +@inject(injected_func = str.lower) +class TestCallable(object): + """Test fixture for callable classes.""" + @usesclassinject + def __call__(self, arg): + return injected_func(arg) + +class TestCallableSingle(object): + """Test fixture for callable classes with + simple method injection.""" + @inject(injected_func = str.lower) + def __call__(self, arg): + return injected_func(arg) + +class ClassDITestCase(unittest.TestCase): + + def test_class_inject(self): + """Test injection to instance method.""" + obj = Test() + self.assertEqual("a", obj.method("A")) + + def test_class_init_inject(self): + """Test injection to class constructor.""" + obj = TestInit("A") + self.assertEqual("a", obj.value) + + def test_callable_class(self): + """Test class injection to callable class.""" + obj = TestCallable() + self.assertEqual("a", obj("A")) + + def test_callable_class_single(self): + """Test method injection to callable class.""" + obj = TestCallableSingle() + self.assertEqual("a", obj("A")) + +if __name__ == "__main__": + unittest.main() diff --git a/python-di.spec b/python-di.spec new file mode 100644 index 0000000..9efd750 --- /dev/null +++ b/python-di.spec @@ -0,0 +1,41 @@ +%if 0%{?rhel} && 0%{?rhel} <= 5 +%{!?python_sitelib: %global python_sitelib %(%{__python} -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())")} +%{!?python_sitearch: %global python_sitearch %(%{__python} -c "from distutils.sysconfig import get_python_lib; print(get_python_lib(1))")} +%endif + +Name: python-di +Version: 0.1 +Release: 1%{?dist} +Summary: Python library for dependency injection support + +License: GPLv2+ +URL: http://fedorapeople.org/cgit/msivak/public_git/python-di.git/ +Source0: python-di-%{version}-%{release}.tar.gz + +#BuildRequires: +#Requires: + +%description +This python package provides a "di" module. That module contains couple of +decorators which try to implement Dependency Injection pattern without +requiring the user to change local variables in his methods. + +It is intended to be used in unit testing environments. + +%prep +%setup -q + +%build +%{__python} setup.py make + +%install +%{__python} setup.py install + +%files +%{python_sitelib}/di + +%doc + + + +%changelog diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..2f1a99c --- /dev/null +++ b/setup.py @@ -0,0 +1,29 @@ +import os +from setuptools import setup + +# Utility function to read the README file. +# Used for the long_description. It's nice, because now 1) we have a top level +# README file and 2) it's easier to type in the README file than to put a raw +# string in below ... +def read(fname): + return open(os.path.join(os.path.dirname(__file__), fname)).read() + +setup( + name = "di", + version = "0.1", + author = "Martin Sivak", + author_email = "msivak@redhat.com", + description = ("Python module which provides decorators to make " + "dependency injection easy to use."), + license = "GPLv2+", + keywords = "testing dependency injection IoC", + url = "http://fedorapeople.org/cgit/msivak/public_git/python-di.git/", + packages = ['di'], + long_description=read('README'), + classifiers=[ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "Topic :: Software Development :: Testing", + "License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)", + ], +) -- cgit