Source

intfprgm / intfprgm / __init__.py

Full commit
'''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, check_argspec=True):
    '''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.
        check_argspec (boolean): ``True`` if a function must be matched
            exactly. Default to ``False``.

    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 (not check_argspec) or (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__))


class overrides_impl(object):
    '''Actual implementation of ``overrides``.

    We need this to be a class so that we can create instances from it, with
    different ``check_argspec`` values.

    '''

    def __init__(self, check_argspec=True):
        self.check_argspec = check_argspec

    def __call__(self, orig):
        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, f.__intfprgm_overrides__)
        else:
            orig.__intfprgm_overrides__ = self.check_argspec
        return orig


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 a function must be matched only by its name, we can set
    ``check_argspec`` flag to ``False``::

        @overrides(False)
        def method(signature):
            pass
    
    By the fault, argspec check is set to ``True``.

    (For those who care about the code, this function is basically two
    overloaded functions::

        def overrides(function_or_class):
            return overrides_impl(False)(function_or_class)

        def overrides(boolean_value):
            return overrides_impl(boolean_value)

    When we use ``@overrides``, the ``overrides`` after ``@`` is evaluated  to
    the ``overrides`` function, and that function is invoked on the passed-in
    function or class.

    When we use ``@overrides(True)`` (or ``False``), the part after ``@`` is
    evalulated as a function invocation on ``overrides`` with a boolean
    argument. The returned value of that invocation is then used to decorate
    the original function or class.)

    '''

    if type(orig) is types.BooleanType:
        return overrides_impl(orig)
    else:
        return overrides_impl(True)(orig)