import importlib
import inspect
import pkgutil
import types

import six
import unittest

import chainer
from chainer import testing


def walk_modules():
    root = chainer.__path__
    for loader, modname, ispkg in pkgutil.walk_packages(root, 'chainer.'):
        # Skip modules generated by protobuf.
        if '_pb2' in modname:
            continue

        try:
            mod = importlib.import_module(modname)
        except ImportError:
            continue

        yield mod


def get_classes(module):
    # Enumerate classes from a module
    for name, o in module.__dict__.items():
        if (inspect.isclass(o)
                and o.__module__.startswith('chainer.')):
            yield o


def get_functions(module):
    # Enumerate functions from a module

    # Normal functions
    for k, o in module.__dict__.items():
        if (isinstance(o, types.FunctionType)
                and o.__module__.startswith('chainer.')):
            yield o

    # Methods defined in a class
    for cls in get_classes(module):
        if cls.__module__.startswith('chainer.'):
            for k, o in cls.__dict__.items():
                if inspect.isfunction(o):
                    yield o


def get_default_arguments(func):
    # Retrieves the defaults arguments (names and values) of a function.
    if six.PY2:
        # Python 2
        spec = inspect.getargspec(func)
        if spec.defaults is not None:
            n = len(spec.defaults)
            for name, default_value in zip(spec.args[-n:], spec.defaults):
                yield name, default_value
    else:
        # Python 3
        signature = inspect.signature(func)
        for name, param in signature.parameters.items():
            if param.default is not inspect.Parameter.empty:
                yield name, param.default


class TestFunctions(unittest.TestCase):

    def test_no_mutable_default_args(self):
        type_blacklist = (list, dict)
        badlist = []
        # Collect mutable default arguments
        for mod in walk_modules():
            for func in get_functions(mod):
                for arg_name, value in get_default_arguments(func):
                    if isinstance(value, type_blacklist):
                        badlist.append((func, arg_name, type(value)))

        if len(badlist) > 0:
            # Report the error
            s = six.StringIO()
            s.write(
                'Some functions have mutable values as default values:\n\n')
            for func, arg_name, value_type in badlist:
                s.write('{}.{}: arg=\'{}\' type={}\n'.format(
                    func.__module__, func.__name__, arg_name, value_type))
            assert False, s.getvalue()


testing.run_module(__name__, __file__)

Add test to ensure no mutable default arguments