intfprgm / 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 collections
import dis
import inspect
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


def check_override(clz, func):
    '''Walk the base classes of ``clz`` and check if ``func`` is declared
    with the same signature.

    Raise ``RuntimeError`` if ``func`` is not found in any of the base class.

    Args:

        clz (class object): A class object where ``func`` should be defined.
        func (function object): A function object to check for. This function
            must have been defined somewhere up in the class hierachy with
            the exact signature.

    Raises:

        ``RuntimeError`` if ``func`` is not found in any of the base class.

    '''

    queue = collections.deque()
    queue.extend(clz.__bases__)
    orig_argspec = inspect.getargspec(func)
    while queue:
        base = queue.popleft()
        # get the function
        f = getattr(base, func.func_name, None)
        try:
            argspec = inspect.getargspec(f)
            # same signature? good match
            if orig_argspec == argspec:
                return
            # not? continue up the chain
            else:
                queue.extend(base.__bases__)
        except Exception:
            # f may not be a function, that's okay, carry on
            queue.extend(base.__bases__)
    else:
        raise RuntimeError('%r is not found in any base class of %r.' % (
            func.func_name, func.im_class.__name__))


def overrides(orig):
    '''A decorator for both class and function that marks and checks if a
    function is defined in any of the base classes.

    For example::

        @overrides
        class Derived(Base)

            @overrides
            def method(signature):
                pass

    This decorator MUST be applied on both the class and the function. The
    reason is that during definition, the function is not assigned to a class
    yet. We apply ``overrides`` to the function so that it can mark that
    function to be checked later. And we apply ``overrides`` to the class to
    walk its members after the class has been fully defined.
    
    '''

    if type(orig) in (types.TypeType, types.ClassType):
        for f in dir(orig):
            f = getattr(orig, f)
            if type(f) in (types.FunctionType, types.MethodType):
                if not hasattr(f, '__intfprgm_overrides__'):
                    continue
                check_override(orig, f)
    else:
        orig.__intfprgm_overrides__ = True
    return orig
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.