# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of logilab-common.
#
# logilab-common is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 2.1 of the License, or (at your option) any
# later version.
#
# logilab-common is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with logilab-common.  If not, see <http://www.gnu.org/licenses/>.
"""unit tests for the decorators module
"""
import types

from logilab.common.testlib import TestCase, unittest_main
from logilab.common.decorators import monkeypatch, cached, clear_cache, copy_cache, cachedproperty


class DecoratorsTC(TestCase):
    def test_monkeypatch_instance_method(self):
        class MyClass:
            pass

        @monkeypatch(MyClass)
        def meth1(self):
            return 12

        class XXX:
            @monkeypatch(MyClass)
            def meth2(self):
                return 12

        # with python3, unbound method are functions
        self.assertIsInstance(MyClass.meth1, types.FunctionType)
        self.assertIsInstance(MyClass.meth2, types.FunctionType)
        self.assertEqual(MyClass().meth1(), 12)
        self.assertEqual(MyClass().meth2(), 12)

    def test_monkeypatch_property(self):
        class MyClass:
            pass

        @monkeypatch(MyClass, methodname="prop1")
        @property
        def meth1(self):
            return 12

        self.assertIsInstance(MyClass.prop1, property)
        self.assertEqual(MyClass().prop1, 12)

    def test_monkeypatch_arbitrary_callable(self):
        class MyClass:
            pass

        class ArbitraryCallable:
            def __call__(self):
                return 12

        # ensure it complains about missing __name__
        with self.assertRaises(AttributeError) as cm:
            monkeypatch(MyClass)(ArbitraryCallable())
        self.assertTrue(
            str(cm.exception).endswith(
                "has no __name__ attribute: you should provide an explicit `methodname`"
            )
        )
        # ensure no black magic under the hood
        monkeypatch(MyClass, "foo")(ArbitraryCallable())
        self.assertTrue(callable(MyClass.foo))
        self.assertEqual(MyClass().foo(), 12)

    def test_monkeypatch_with_same_name(self):
        class MyClass:
            pass

        @monkeypatch(MyClass)
        def meth1(self):
            return 12

        self.assertEqual([attr for attr in dir(MyClass) if attr[:2] != "__"], ["meth1"])
        inst = MyClass()
        self.assertEqual(inst.meth1(), 12)

    def test_monkeypatch_with_custom_name(self):
        class MyClass:
            pass

        @monkeypatch(MyClass, "foo")
        def meth2(self, param):
            return param + 12

        self.assertEqual([attr for attr in dir(MyClass) if attr[:2] != "__"], ["foo"])
        inst = MyClass()
        self.assertEqual(inst.foo(4), 16)

    def test_cannot_cache_generator(self):
        def foo():
            yield 42

        self.assertRaises(AssertionError, cached, foo)

    def test_cached_preserves_docstrings_and_name(self):
        class Foo:
            @cached
            def foo(self):
                """what's up doc ?"""

            def bar(self, zogzog):
                """what's up doc ?"""

            bar = cached(bar, 1)

            @cached
            def quux(self, zogzog):
                """what's up doc ?"""

        self.assertEqual(Foo.foo.__doc__, """what's up doc ?""")
        self.assertEqual(Foo.foo.__name__, "foo")
        self.assertEqual(Foo.bar.__doc__, """what's up doc ?""")
        self.assertEqual(Foo.bar.__name__, "bar")
        self.assertEqual(Foo.quux.__doc__, """what's up doc ?""")
        self.assertEqual(Foo.quux.__name__, "quux")

    def test_cached_single_cache(self):
        class Foo:
            @cached(cacheattr="_foo")
            def foo(self):
                """what's up doc ?"""

        foo = Foo()
        foo.foo()
        self.assertTrue(hasattr(foo, "_foo"))
        clear_cache(foo, "foo")
        self.assertFalse(hasattr(foo, "_foo"))

    def test_cached_multi_cache(self):
        class Foo:
            @cached(cacheattr="_foo")
            def foo(self, args):
                """what's up doc ?"""

        foo = Foo()
        foo.foo(1)
        self.assertEqual(foo._foo, {(1,): None})
        clear_cache(foo, "foo")
        self.assertFalse(hasattr(foo, "_foo"))

    def test_cached_keyarg_cache(self):
        class Foo:
            @cached(cacheattr="_foo", keyarg=1)
            def foo(self, other, args):
                """what's up doc ?"""

        foo = Foo()
        foo.foo(2, 1)
        self.assertEqual(foo._foo, {2: None})
        clear_cache(foo, "foo")
        self.assertFalse(hasattr(foo, "_foo"))

    def test_cached_property(self):
        class Foo:
            @property
            @cached(cacheattr="_foo")
            def foo(self):
                """what's up doc ?"""

        foo = Foo()
        foo.foo
        self.assertEqual(foo._foo, None)
        clear_cache(foo, "foo")
        self.assertFalse(hasattr(foo, "_foo"))

    def test_copy_cache(self):
        class Foo:
            @cached(cacheattr="_foo")
            def foo(self, args):
                """what's up doc ?"""

        foo = Foo()
        foo.foo(1)
        self.assertEqual(foo._foo, {(1,): None})
        foo2 = Foo()
        self.assertFalse(hasattr(foo2, "_foo"))
        copy_cache(foo2, "foo", foo)
        self.assertEqual(foo2._foo, {(1,): None})

    def test_cachedproperty(self):
        class Foo:
            x = 0

            @cachedproperty
            def bar(self):
                self.__class__.x += 1
                return self.__class__.x

            @cachedproperty
            def quux(self):
                """some prop"""
                return 42

        foo = Foo()
        self.assertEqual(Foo.x, 0)
        self.assertFalse("bar" in foo.__dict__)
        self.assertEqual(foo.bar, 1)
        self.assertTrue("bar" in foo.__dict__)
        self.assertEqual(foo.bar, 1)
        self.assertEqual(foo.quux, 42)
        self.assertEqual(Foo.bar.__doc__, "<wrapped by the cachedproperty decorator>")
        self.assertEqual(Foo.quux.__doc__, "<wrapped by the cachedproperty decorator>\nsome prop")

        foo2 = Foo()
        self.assertEqual(foo2.bar, 2)
        # make sure foo.foo is cached
        self.assertEqual(foo.bar, 1)

        class Kallable:
            def __call__(self):
                return 42

        self.assertRaises(TypeError, cachedproperty, Kallable())


if __name__ == "__main__":
    unittest_main()
