Commits

Nam Nguyen committed 6059292

Initial commit.

Comments (0)

Files changed (5)

+Copyright (c) 2012, Nam T. Nguyen
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+Intfprgm helps separate interface from implementation.
+
+Intfprgm offers a set of decorators to clearly mark and enforce a class to be
+an interface, an abstract class, or a concrete class, and a function to be
+abstract.
+
+To define an abstract function, we can use ``abstract`` decorator::
+
+    @abstract
+    def func():
+        pass
+
+Or raise ``NotImplementedError``::
+
+    def func():
+        raise NotImplementedError()
+
+An interface can be defined with ``interface`` decorator::
+
+    @interface
+    class Interface(object):
+
+        def method_1(self):
+            raise NotImplementedError()
+
+        @abstract
+        def method_2(self):
+            pass
+
+An abstract is defined with the same ``abstract`` decorator::
+
+    @abstract
+    class BaseClass(Interface):
+
+        def method_1(self):
+            pass
+
+And lastly, a concrete class is defined with ``concrete`` decorator::
+
+    @concrete
+    class ConcreteClass(BaseClass):
+
+        def method_2(self):
+            pass

intfprgm/__init__.py

+'''Set of (class) decorators to help with interface programming.
+
+Examples::
+
+    @concrete
+    class subclass(parent):
+        ...
+
+Released to the public domain.
+
+Nam T. Nguyen, 2012
+
+'''
+
+import dis
+import types
+
+
+def is_abstract(func):
+    '''Check if a function is un-implemented.
+
+    An un-implemented method is defined similarly to::
+
+        def method(...):
+            raise NotImplementedError()
+
+    This, when translated to bytecode, looks like::
+
+        LOAD_GLOBAL       0 (NotImplementedError)
+        CALL_FUNCTION     1
+        RAISE_VARARGS     1
+        ...
+
+    Or when ``raise NotImplementedError``::
+
+        LOAD_GLOBAL       0 (NotImplementedError)
+        RAISE_VARARGS     1
+        ...
+
+    The check here is for such patterns.
+
+    Args:
+
+        func (function): A function object
+
+    Return:
+
+        ``True`` if this function has the mentioned pattern,
+        ``False`` otherwise
+
+    '''
+
+    # check if first name is NotImplementedError
+    if len(func.func_code.co_names) < 1 or \
+            func.func_code.co_names[0] != 'NotImplementedError':
+        return False
+    # and RAISE_VARARGS somewhere after that
+    for position in (3, 6):
+        if len(func.func_code.co_code) < position:
+            continue
+        opcode = ord(func.func_code.co_code[position])
+        if dis.opname[opcode] == 'RAISE_VARARGS':
+            return True
+    return False
+
+
+def get_functions(clz, filter_=lambda x: True):
+    '''Return all functions and methods of ``clz`` that satisfy ``filter_``.
+
+    Args:
+
+        clz (class): A class object
+        filter_: A filter function. It accepts a function object and return
+            a boolean.
+
+    Returns:
+
+        List of function and method objects
+
+    '''
+
+    funcs = []
+
+    for name in dir(clz):
+        func = getattr(clz, name)
+        if type(func) in (types.FunctionType, types.MethodType) and \
+                filter_(func):
+            funcs.append(func)
+
+    return funcs
+
+
+def get_implemented_functions(clz):
+    '''Return all implemented (not abstract) functions and methods of ``clz``.
+
+    See func:`get_functions` and func:`is_abstract`.
+
+    Args:
+
+        clz (class): A class object
+
+    Returns:
+
+        List of implemented function and method objects in ``clz``
+
+    '''
+
+    return get_functions(clz, lambda f: not is_abstract(f))
+
+
+def get_unimplemented_functions(clz):
+    '''Return all unimplemented (abstract) functions and methods of ``clz``.
+
+    See func:`get_functions` and func:`is_abstract`.
+
+    Args:
+
+        clz (class): A class object
+
+    Returns:
+
+        List of implemented function and method objects in ``clz``
+
+    '''
+
+    return get_functions(clz, lambda f: is_abstract(f))
+
+
+def __raise_constructor(s, *args, **kw_args):
+    raise SyntaxError(s.__class__.__name__ + ' cannot be instantiated.')
+
+
+def __raise(*args, **kw_args):
+    raise NotImplementedError()
+
+
+def abstract(orig_class):
+    '''A decorator to ensure that a class cannot be instantiated and at least
+    one of its methods is unimplemented.
+
+    If this decorator is applied on a function, it will replace that function
+    with a stub that raises ``NotImplementedError``.
+
+    '''
+
+    if type(orig_class) is not types.ClassType:
+        return __raise
+
+    funcs = get_unimplemented_functions(orig_class)
+    if not funcs:
+        raise SyntaxError('%s does not have any unimplemented method.' %
+            orig_class.__name__)
+
+    orig_class.__init__ = __raise_constructor
+    return orig_class
+
+
+def interface(orig_class):
+    '''A decorator to ensure that an interface cannot be instantiated and all
+    its methods are abstract.
+
+    '''
+
+    funcs = get_implemented_functions(orig_class)
+    if funcs:
+        raise SyntaxError('Interface %s cannot implement %s.' % (
+                orig_class.__name__, ', '.join(f.func_name for f in funcs)))
+
+    orig_class.__init__ = __raise_constructor
+    return orig_class
+
+
+def concrete(orig_class):
+    '''A decorator to ensure that a concrete class has no un-implemented
+    methods.
+
+    '''
+
+    funcs = get_unimplemented_functions(orig_class)
+    if funcs:
+        raise SyntaxError('Concrete class %s must implement %s.' % (
+                orig_class.__name__, ', '.join(f.func_name for f in funcs)))
+
+    return orig_class

intfprgm/tests.py

+#!/usr/bin/env python
+
+import unittest
+
+from intfprgm import interface, abstract, concrete
+
+class InterfaceTest(unittest.TestCase):
+
+    def test_init(self):
+        @interface
+        class test:
+            pass
+        self.assertRaises(SyntaxError, test)
+
+    def test_method_raise(self):
+        class test:
+            def __init__(self):
+                pass
+        self.assertRaises(SyntaxError, interface, test)
+
+    def test_method_okay(self):
+        class test:
+            def __init__(self):
+                raise NotImplementedError()
+        interface(test)
+
+    def test_abstract(self):
+        class test1:
+            def method(self):
+                pass
+        class test2:
+            @abstract
+            def method(self):
+                pass
+        self.assertRaises(SyntaxError, interface, test1)
+        interface(test2)
+
+class AbstractTest(unittest.TestCase):
+
+    def test_init(self):
+        @abstract
+        class test:
+            def method(self):
+                raise NotImplementedError()
+        self.assertRaises(SyntaxError, test)
+
+    def test_method(self):
+        class test:
+            pass
+        self.assertRaises(SyntaxError, abstract, test)
+
+    def test_okay(self):
+        class test:
+            def method(self):
+                raise NotImplementedError
+        abstract(test)
+
+    def test_function(self):
+        @abstract
+        def raises():
+            pass
+        self.assertRaises(NotImplementedError, raises)
+
+
+class ConcreteTest(unittest.TestCase):
+
+    def test_raise(self):
+        class test1:
+            def method(self):
+                raise NotImplementedError
+        self.assertRaises(SyntaxError, concrete, test1)
+
+        class test2:
+            def method(self):
+                raise NotImplementedError()
+        self.assertRaises(SyntaxError, concrete, test2)
+
+    def test_not_raise(self):
+        class test:
+            def method(self):
+                return NotImplementedError()
+        concrete(test)
+
+    def test_subclass(self):
+        class parent(object):
+            def override_me(self):
+                raise NotImplementedError()
+        class test(parent):
+            def leave_it(self):
+                pass
+        self.assertRaises(SyntaxError, concrete, test)
+
+    def test_abstract(self):
+        class test1:
+            def method(self):
+                pass
+        class test2:
+            @abstract
+            def method(self):
+                pass
+        concrete(test1)
+        self.assertRaises(SyntaxError, concrete, test2)
+
+
+if __name__ == '__main__':
+    unittest.main()
+from setuptools import setup
+name = 'intfprgm'
+desc = 'Intfprgm is a pure Python library of decorators to help with interface programming.'
+setup(
+    name=name,
+    version='1.0.0',
+    author='Nam T. Nguyen',
+    author_email='namn@bluemoon.com.vn',
+    url='https://bitbucket.org/namn/intfprgm/overview',
+    description=desc,
+    long_description=desc,
+    platforms='Any',
+    package_dir={'':'.'},
+    packages=['intfprgm'],
+    package_data={'': ['README', 'LICENSE']},
+    license='MIT',
+    classifiers=[
+        'Development Status :: 5 - Production/Stable',
+        'Intended Audience :: Developers',
+        'License :: OSI Approved :: MIT License',
+        'Operating System :: OS Independent',
+        'Programming Language :: Python',
+        'Topic :: Software Development',
+        'Topic :: Software Development :: Libraries :: Python Modules',
+        'Topic :: Software Development :: Pre-processors',
+    ]
+)