# Copyright (c) 2010-2012 extras developers. See LICENSE for details.

import sys
import types

from testtools import TestCase
from testtools.matchers import (
    Equals,
    Is,
    Not,
    )

from extras import (
    safe_hasattr,
    try_import,
    try_imports,
    )

def check_error_callback(test, function, arg, expected_error_count,
    expect_result):
    """General test template for error_callback argument.

    :param test: Test case instance.
    :param function: Either try_import or try_imports.
    :param arg: Name or names to import.
    :param expected_error_count: Expected number of calls to the callback.
    :param expect_result: Boolean for whether a module should
        ultimately be returned or not.
    """
    cb_calls = []
    def cb(e):
        test.assertIsInstance(e, ImportError)
        cb_calls.append(e)
    try:
        result = function(arg, error_callback=cb)
    except ImportError:
        test.assertFalse(expect_result)
    else:
        if expect_result:
            test.assertThat(result, Not(Is(None)))
        else:
            test.assertThat(result, Is(None))
    test.assertEquals(len(cb_calls), expected_error_count)


class TestSafeHasattr(TestCase):

    def test_attribute_not_there(self):
        class Foo(object):
            pass
        self.assertEqual(False, safe_hasattr(Foo(), 'anything'))

    def test_attribute_there(self):
        class Foo(object):
            pass
        foo = Foo()
        foo.attribute = None
        self.assertEqual(True, safe_hasattr(foo, 'attribute'))

    def test_property_there(self):
        class Foo(object):
            @property
            def attribute(self):
                return None
        foo = Foo()
        self.assertEqual(True, safe_hasattr(foo, 'attribute'))

    def test_property_raises(self):
        class Foo(object):
            @property
            def attribute(self):
                1/0
        foo = Foo()
        self.assertRaises(ZeroDivisionError, safe_hasattr, foo, 'attribute')


class TestTryImport(TestCase):

    def test_doesnt_exist(self):
        # try_import('thing', foo) returns foo if 'thing' doesn't exist.
        marker = object()
        result = try_import('doesntexist', marker)
        self.assertThat(result, Is(marker))

    def test_None_is_default_alternative(self):
        # try_import('thing') returns None if 'thing' doesn't exist.
        result = try_import('doesntexist')
        self.assertThat(result, Is(None))

    def test_existing_module(self):
        # try_import('thing', foo) imports 'thing' and returns it if it's a
        # module that exists.
        result = try_import('os', object())
        import os
        self.assertThat(result, Is(os))

    def test_existing_submodule(self):
        # try_import('thing.another', foo) imports 'thing' and returns it if
        # it's a module that exists.
        result = try_import('os.path', object())
        import os
        self.assertThat(result, Is(os.path))

    def test_nonexistent_submodule(self):
        # try_import('thing.another', foo) imports 'thing' and returns foo if
        # 'another' doesn't exist.
        marker = object()
        result = try_import('os.doesntexist', marker)
        self.assertThat(result, Is(marker))

    def test_object_from_module(self):
        # try_import('thing.object') imports 'thing' and returns
        # 'thing.object' if 'thing' is a module and 'object' is not.
        result = try_import('os.path.join')
        import os
        self.assertThat(result, Is(os.path.join))

    def test_error_callback(self):
        # the error callback is called on failures.
        check_error_callback(self, try_import, 'doesntexist', 1, False)

    def test_error_callback_missing_module_member(self):
        # the error callback is called on failures to find an object
        # inside an existing module.
        check_error_callback(self, try_import, 'os.nonexistent', 1, False)

    def test_error_callback_not_on_success(self):
        # the error callback is not called on success.
        check_error_callback(self, try_import, 'os.path', 0, True)

    def test_handle_partly_imported_name(self):
        # try_import('thing.other') when thing.other is mid-import
        # used to fail because thing.other is not assigned until thing.other
        # finishes its import - but thing.other is accessible via sys.modules.
        outer = types.ModuleType("extras.outer")
        inner = types.ModuleType("extras.outer.inner")
        inner.attribute = object()
        self.addCleanup(sys.modules.pop, "extras.outer", None)
        self.addCleanup(sys.modules.pop, "extras.outer.inner", None)
        sys.modules["extras.outer"] = outer
        sys.modules["extras.outer.inner"] = inner
        result = try_import("extras.outer.inner.attribute")
        self.expectThat(result, Is(inner.attribute))
        result = try_import("extras.outer.inner")
        self.expectThat(result, Is(inner))


class TestTryImports(TestCase):

    def test_doesnt_exist(self):
        # try_imports('thing', foo) returns foo if 'thing' doesn't exist.
        marker = object()
        result = try_imports(['doesntexist'], marker)
        self.assertThat(result, Is(marker))

    def test_fallback(self):
        result = try_imports(['doesntexist', 'os'])
        import os
        self.assertThat(result, Is(os))

    def test_None_is_default_alternative(self):
        # try_imports('thing') returns None if 'thing' doesn't exist.
        e = self.assertRaises(
            ImportError, try_imports, ['doesntexist', 'noreally'])
        self.assertThat(
            str(e),
            Equals("Could not import any of: doesntexist, noreally"))

    def test_existing_module(self):
        # try_imports('thing', foo) imports 'thing' and returns it if it's a
        # module that exists.
        result = try_imports(['os'], object())
        import os
        self.assertThat(result, Is(os))

    def test_existing_submodule(self):
        # try_imports('thing.another', foo) imports 'thing' and returns it if
        # it's a module that exists.
        result = try_imports(['os.path'], object())
        import os
        self.assertThat(result, Is(os.path))

    def test_nonexistent_submodule(self):
        # try_imports('thing.another', foo) imports 'thing' and returns foo if
        # 'another' doesn't exist.
        marker = object()
        result = try_imports(['os.doesntexist'], marker)
        self.assertThat(result, Is(marker))

    def test_fallback_submodule(self):
        result = try_imports(['os.doesntexist', 'os.path'])
        import os
        self.assertThat(result, Is(os.path))

    def test_error_callback(self):
        # One error for every class that doesn't exist.
        check_error_callback(self, try_imports,
            ['os.doesntexist', 'os.notthiseither'],
            2, False)
        check_error_callback(self, try_imports,
            ['os.doesntexist', 'os.notthiseither', 'os'],
            2, True)
        check_error_callback(self, try_imports,
            ['os.path'],
            0, True)


