# -*- coding: UTF-8 -*-
"""Tests for the @export decorator."""

import importlib
import sys
import textwrap
import types
from importlib.machinery import ModuleSpec
from importlib.util import module_from_spec

import pytest

from pydecor.decorators import export


@pytest.fixture()
def reset_modules():
    """Ensure sys.modules is reset at the end of a test."""
    modules = dict(sys.modules)
    yield
    sys.modules = modules


class TestExport:
    """Test the @export decorator."""

    def test_bad_type(self):
        """Passing something with no __module__ attribute is a TypeError."""
        with pytest.raises(TypeError):
            export("foo")  # type: ignore

    def test_not_in_modules(self):
        """Calling in a non-imported context is an error."""
        module = types.ModuleType("my_module")  # creates a new module

        module_code = textwrap.dedent(
            """
            from pydecor.decorators import export

            @export
            def exported():
                pass
            """
        )
        with pytest.raises(ValueError):
            exec(module_code, module.__dict__)

    @pytest.mark.usefixtures("reset_modules")
    def test_imported_module_dynamic(self):
        """The __all__ attr is created on the imported module if needed."""
        # In actual import machinery, the module is added to sys.modules
        # before the contained code is executed, so we mimic that here.
        module = module_from_spec(ModuleSpec("my_module", None))
        sys.modules["my_module"] = module

        module_code = textwrap.dedent(
            """
            from pydecor.decorators import export

            @export
            def exported():
                pass
            """
        )
        exec(module_code, module.__dict__)

        imported = importlib.import_module("my_module")
        assert imported.__all__ == ["exported"]  # type: ignore

    @pytest.mark.usefixtures("reset_modules")
    def test_imported_module_dynamic_append(self):
        """The __all__ attr is appended to if it already exists."""
        # In actual import machinery, the module is added to sys.modules
        # before the contained code is executed, so we mimic that here.
        module = module_from_spec(ModuleSpec("my_module", None))
        sys.modules["my_module"] = module

        module_code = textwrap.dedent(
            """
            __all__ = ["first"]

            from pydecor.decorators import export

            first = "some other thing that is already exported"

            @export
            def exported():
                pass
            """
        )
        exec(module_code, module.__dict__)

        imported = importlib.import_module("my_module")
        assert imported.__all__ == ["first", "exported"]  # type: ignore

    @pytest.mark.usefixtures("reset_modules")
    def test_imported_module_dynamic_append_tuple(self):
        """If __all__ is a tuple, the generated one is still a tuple."""
        # In actual import machinery, the module is added to sys.modules
        # before the contained code is executed, so we mimic that here.
        module = module_from_spec(ModuleSpec("my_module", None))
        sys.modules["my_module"] = module

        module_code = textwrap.dedent(
            """
            __all__ = ("first",)

            from pydecor.decorators import export

            first = "some other thing that is already exported"

            @export
            def exported():
                pass
            """
        )
        exec(module_code, module.__dict__)

        imported = importlib.import_module("my_module")
        assert imported.__all__ == ("first", "exported")  # type: ignore

    @pytest.mark.usefixtures("reset_modules")
    def test_export_idempotent_already_present(self):
        """The module is not added if already present."""
        # In actual import machinery, the module is added to sys.modules
        # before the contained code is executed, so we mimic that here.
        module = module_from_spec(ModuleSpec("my_module", None))
        sys.modules["my_module"] = module

        module_code = textwrap.dedent(
            """
            __all__ = ["exported"]

            from pydecor.decorators import export

            @export
            def exported():
                pass
            """
        )
        exec(module_code, module.__dict__)

        imported = importlib.import_module("my_module")
        assert imported.__all__ == ["exported"]  # type: ignore

    @pytest.mark.usefixtures("reset_modules")
    def test_export_idempotent_multiple_calls(self):
        """Multiple calls don't hurt."""
        # In actual import machinery, the module is added to sys.modules
        # before the contained code is executed, so we mimic that here.
        module = module_from_spec(ModuleSpec("my_module", None))
        sys.modules["my_module"] = module

        module_code = textwrap.dedent(
            """
            from pydecor.decorators import export

            @export
            @export
            def exported():
                pass
            """
        )
        exec(module_code, module.__dict__)

        imported = importlib.import_module("my_module")
        assert imported.__all__ == ["exported"]  # type: ignore

    @pytest.mark.usefixtures("reset_modules")
    def test_imported_module_static_no_all(self):
        """A module present in imports is manipulated correctly."""
        from .exports import no_all

        # pylint: disable=no-member
        assert no_all.__all__ == ["exported"]  # type: ignore
        # pylint: enable=no-member

    @pytest.mark.usefixtures("reset_modules")
    def test_imported_module_static_list_all(self):
        """A list __all__ is appended to."""
        from .exports import list_all

        assert list_all.__all__ == ["first", "exported"]

    @pytest.mark.usefixtures("reset_modules")
    def test_imported_module_static_tuple_all(self):
        """A tuple __all__ is replaced with a new tuple."""
        from .exports import tuple_all

        assert tuple_all.__all__ == ("first", "exported")

    @pytest.mark.usefixtures("reset_modules")
    def test_imported_module_static_class_export(self):
        """Classes may also be exported."""
        from .exports import class_export

        # pylint: disable=no-member
        assert class_export.__all__ == ["Exported"]  # type: ignore

    @pytest.mark.usefixtures("reset_modules")
    def test_imported_module_static_multi_export(self):
        """Multiple items may be exported."""
        from .exports import multi_export

        # pylint: disable=no-member
        assert multi_export.__all__ == ["Exported", "exported"]  # type: ignore

    @pytest.mark.usefixtures("reset_modules")
    def test_export_class_dynamic(self):
        """Classes may also be exported."""
        # In actual import machinery, the module is added to sys.modules
        # before the contained code is executed, so we mimic that here.
        module = module_from_spec(ModuleSpec("my_module", None))
        sys.modules["my_module"] = module

        module_code = textwrap.dedent(
            """
            from pydecor.decorators import export

            @export
            class Exported:
                pass

            """
        )
        exec(module_code, module.__dict__)

        imported = importlib.import_module("my_module")
        assert imported.__all__ == ["Exported"]  # type: ignore

    @pytest.mark.usefixtures("reset_modules")
    def test_export_instancemethod_fails(self):
        """Instance methods may not be exported directly."""
        # In actual import machinery, the module is added to sys.modules
        # before the contained code is executed, so we mimic that here.
        module = module_from_spec(ModuleSpec("my_module", None))
        sys.modules["my_module"] = module

        module_code = textwrap.dedent(
            """
            from pydecor.decorators import export

            class BadClass:
                @export
                def foo(self):
                    pass

            """
        )
        with pytest.raises(TypeError):
            exec(module_code, module.__dict__)

    @pytest.mark.usefixtures("reset_modules")
    def test_export_classmethod_fails(self):
        """Classmethods may not be exported directly."""
        # In actual import machinery, the module is added to sys.modules
        # before the contained code is executed, so we mimic that here.
        module = module_from_spec(ModuleSpec("my_module", None))
        sys.modules["my_module"] = module

        module_code = textwrap.dedent(
            """
            from pydecor.decorators import export

            class BadClass:
                @export
                @classmethod
                def foo(self):
                    pass

            """
        )
        with pytest.raises(TypeError):
            exec(module_code, module.__dict__)

    @pytest.mark.usefixtures("reset_modules")
    def test_export_staticmethod_fails(self):
        """Staticmethods may not be exported directly."""
        # In actual import machinery, the module is added to sys.modules
        # before the contained code is executed, so we mimic that here.
        module = module_from_spec(ModuleSpec("my_module", None))
        sys.modules["my_module"] = module

        module_code = textwrap.dedent(
            """
            from pydecor.decorators import export

            class BadClass:
                @export
                @staticmethod
                def foo(self):
                    pass

            """
        )
        with pytest.raises(TypeError):
            exec(module_code, module.__dict__)

    @pytest.mark.usefixtures("reset_modules")
    @pytest.mark.parametrize(
        "value",
        (
            "lambda: None",
            "'foo'",
            "12",
            "{}",
            "[]",
            "()",
            "set()",
            "None",
            "False",
            "range(5)",
            "iter(())",
        ),
    )
    def test_export_failure_inline_expression(self, value):
        """Inline expressions are not valid input."""
        # In actual import machinery, the module is added to sys.modules
        # before the contained code is executed, so we mimic that here.
        module = module_from_spec(ModuleSpec("my_module", None))
        sys.modules["my_module"] = module

        module_code = textwrap.dedent(
            """
            from pydecor.decorators import export
            export(lambda: None)
            """
        )
        with pytest.raises(TypeError):
            exec(module_code, module.__dict__)

    @pytest.mark.usefixtures("reset_modules")
    def test_export_failure_local_function(self):
        """Interior functions may not be exported."""
        # In actual import machinery, the module is added to sys.modules
        # before the contained code is executed, so we mimic that here.
        module = module_from_spec(ModuleSpec("my_module", None))
        sys.modules["my_module"] = module

        module_code = textwrap.dedent(
            """
            from pydecor.decorators import export

            def foo():
                @export
                def inner():
                    pass

            foo()
            """
        )
        with pytest.raises(TypeError):
            exec(module_code, module.__dict__)

    @pytest.mark.usefixtures("reset_modules")
    def test_export_failure_class_instance(self):
        """Class instances may not be exported."""
        # In actual import machinery, the module is added to sys.modules
        # before the contained code is executed, so we mimic that here.
        module = module_from_spec(ModuleSpec("my_module", None))
        sys.modules["my_module"] = module

        module_code = textwrap.dedent(
            """
            from pydecor.decorators import export

            class Foo: pass

            foo = Foo()

            export(foo)
            """
        )
        with pytest.raises(TypeError):
            exec(module_code, module.__dict__)

# -*- coding: UTF-8 -*-
"""Tests for the @export decorator."""

import importlib
import sys
import textwrap
import types
from importlib.machinery import ModuleSpec
from importlib.util import module_from_spec

import pytest

from pydecor.decorators import export


@pytest.fixture()
def reset_modules():
    """Ensure sys.modules is reset at the end of a test."""
    modules = dict(sys.modules)
    yield
    sys.modules = modules


class TestExport:
    """Test the @export decorator."""

    def test_bad_type(self):
        """Passing something with no __module__ attribute is a TypeError."""
        with pytest.raises(TypeError):
            export("foo")  # type: ignore

    def test_not_in_modules(self):
        """Calling in a non-imported context is an error."""
        module = types.ModuleType("my_module")  # creates a new module

        module_code = textwrap.dedent(
            """
            from pydecor.decorators import export

            @export
            def exported():
                pass
            """
        )
        with pytest.raises(ValueError):
            exec(module_code, module.__dict__)

    @pytest.mark.usefixtures("reset_modules")
    def test_imported_module_dynamic(self):
        """The __all__ attr is created on the imported module if needed."""
        # In actual import machinery, the module is added to sys.modules
        # before the contained code is executed, so we mimic that here.
        module = module_from_spec(ModuleSpec("my_module", None))
        sys.modules["my_module"] = module

        module_code = textwrap.dedent(
            """
            from pydecor.decorators import export

            @export
            def exported():
                pass
            """
        )
        exec(module_code, module.__dict__)

        imported = importlib.import_module("my_module")
        assert imported.__all__ == ["exported"]  # type: ignore

    @pytest.mark.usefixtures("reset_modules")
    def test_imported_module_dynamic_append(self):
        """The __all__ attr is appended to if it already exists."""
        # In actual import machinery, the module is added to sys.modules
        # before the contained code is executed, so we mimic that here.
        module = module_from_spec(ModuleSpec("my_module", None))
        sys.modules["my_module"] = module

        module_code = textwrap.dedent(
            """
            __all__ = ["first"]

            from pydecor.decorators import export

            first = "some other thing that is already exported"

            @export
            def exported():
                pass
            """
        )
        exec(module_code, module.__dict__)

        imported = importlib.import_module("my_module")
        assert imported.__all__ == ["first", "exported"]  # type: ignore

    @pytest.mark.usefixtures("reset_modules")
    def test_imported_module_dynamic_append_tuple(self):
        """If __all__ is a tuple, the generated one is still a tuple."""
        # In actual import machinery, the module is added to sys.modules
        # before the contained code is executed, so we mimic that here.
        module = module_from_spec(ModuleSpec("my_module", None))
        sys.modules["my_module"] = module

        module_code = textwrap.dedent(
            """
            __all__ = ("first",)

            from pydecor.decorators import export

            first = "some other thing that is already exported"

            @export
            def exported():
                pass
            """
        )
        exec(module_code, module.__dict__)

        imported = importlib.import_module("my_module")
        assert imported.__all__ == ("first", "exported")  # type: ignore

    @pytest.mark.usefixtures("reset_modules")
    def test_export_idempotent_already_present(self):
        """The module is not added if already present."""
        # In actual import machinery, the module is added to sys.modules
        # before the contained code is executed, so we mimic that here.
        module = module_from_spec(ModuleSpec("my_module", None))
        sys.modules["my_module"] = module

        module_code = textwrap.dedent(
            """
            __all__ = ["exported"]

            from pydecor.decorators import export

            @export
            def exported():
                pass
            """
        )
        exec(module_code, module.__dict__)

        imported = importlib.import_module("my_module")
        assert imported.__all__ == ["exported"]  # type: ignore

    @pytest.mark.usefixtures("reset_modules")
    def test_export_idempotent_multiple_calls(self):
        """Multiple calls don't hurt."""
        # In actual import machinery, the module is added to sys.modules
        # before the contained code is executed, so we mimic that here.
        module = module_from_spec(ModuleSpec("my_module", None))
        sys.modules["my_module"] = module

        module_code = textwrap.dedent(
            """
            from pydecor.decorators import export

            @export
            @export
            def exported():
                pass
            """
        )
        exec(module_code, module.__dict__)

        imported = importlib.import_module("my_module")
        assert imported.__all__ == ["exported"]  # type: ignore

    @pytest.mark.usefixtures("reset_modules")
    def test_imported_module_static_no_all(self):
        """A module present in imports is manipulated correctly."""
        from .exports import no_all

        # pylint: disable=no-member
        assert no_all.__all__ == ["exported"]  # type: ignore
        # pylint: enable=no-member

    @pytest.mark.usefixtures("reset_modules")
    def test_imported_module_static_list_all(self):
        """A list __all__ is appended to."""
        from .exports import list_all

        assert list_all.__all__ == ["first", "exported"]

    @pytest.mark.usefixtures("reset_modules")
    def test_imported_module_static_tuple_all(self):
        """A tuple __all__ is replaced with a new tuple."""
        from .exports import tuple_all

        assert tuple_all.__all__ == ("first", "exported")

    @pytest.mark.usefixtures("reset_modules")
    def test_imported_module_static_class_export(self):
        """Classes may also be exported."""
        from .exports import class_export

        # pylint: disable=no-member
        assert class_export.__all__ == ["Exported"]  # type: ignore

    @pytest.mark.usefixtures("reset_modules")
    def test_imported_module_static_multi_export(self):
        """Multiple items may be exported."""
        from .exports import multi_export

        # pylint: disable=no-member
        assert multi_export.__all__ == ["Exported", "exported"]  # type: ignore

    @pytest.mark.usefixtures("reset_modules")
    def test_export_class_dynamic(self):
        """Classes may also be exported."""
        # In actual import machinery, the module is added to sys.modules
        # before the contained code is executed, so we mimic that here.
        module = module_from_spec(ModuleSpec("my_module", None))
        sys.modules["my_module"] = module

        module_code = textwrap.dedent(
            """
            from pydecor.decorators import export

            @export
            class Exported:
                pass

            """
        )
        exec(module_code, module.__dict__)

        imported = importlib.import_module("my_module")
        assert imported.__all__ == ["Exported"]  # type: ignore

    @pytest.mark.usefixtures("reset_modules")
    def test_export_instancemethod_fails(self):
        """Instance methods may not be exported directly."""
        # In actual import machinery, the module is added to sys.modules
        # before the contained code is executed, so we mimic that here.
        module = module_from_spec(ModuleSpec("my_module", None))
        sys.modules["my_module"] = module

        module_code = textwrap.dedent(
            """
            from pydecor.decorators import export

            class BadClass:
                @export
                def foo(self):
                    pass

            """
        )
        with pytest.raises(TypeError):
            exec(module_code, module.__dict__)

    @pytest.mark.usefixtures("reset_modules")
    def test_export_classmethod_fails(self):
        """Classmethods may not be exported directly."""
        # In actual import machinery, the module is added to sys.modules
        # before the contained code is executed, so we mimic that here.
        module = module_from_spec(ModuleSpec("my_module", None))
        sys.modules["my_module"] = module

        module_code = textwrap.dedent(
            """
            from pydecor.decorators import export

            class BadClass:
                @export
                @classmethod
                def foo(self):
                    pass

            """
        )
        with pytest.raises(TypeError):
            exec(module_code, module.__dict__)

    @pytest.mark.usefixtures("reset_modules")
    def test_export_staticmethod_fails(self):
        """Staticmethods may not be exported directly."""
        # In actual import machinery, the module is added to sys.modules
        # before the contained code is executed, so we mimic that here.
        module = module_from_spec(ModuleSpec("my_module", None))
        sys.modules["my_module"] = module

        module_code = textwrap.dedent(
            """
            from pydecor.decorators import export

            class BadClass:
                @export
                @staticmethod
                def foo(self):
                    pass

            """
        )
        with pytest.raises(TypeError):
            exec(module_code, module.__dict__)

    @pytest.mark.usefixtures("reset_modules")
    @pytest.mark.parametrize(
        "value",
        (
            "lambda: None",
            "'foo'",
            "12",
            "{}",
            "[]",
            "()",
            "set()",
            "None",
            "False",
            "range(5)",
            "iter(())",
        ),
    )
    def test_export_failure_inline_expression(self, value):
        """Inline expressions are not valid input."""
        # In actual import machinery, the module is added to sys.modules
        # before the contained code is executed, so we mimic that here.
        module = module_from_spec(ModuleSpec("my_module", None))
        sys.modules["my_module"] = module

        module_code = textwrap.dedent(
            """
            from pydecor.decorators import export
            export(lambda: None)
            """
        )
        with pytest.raises(TypeError):
            exec(module_code, module.__dict__)

    @pytest.mark.usefixtures("reset_modules")
    def test_export_failure_local_function(self):
        """Interior functions may not be exported."""
        # In actual import machinery, the module is added to sys.modules
        # before the contained code is executed, so we mimic that here.
        module = module_from_spec(ModuleSpec("my_module", None))
        sys.modules["my_module"] = module

        module_code = textwrap.dedent(
            """
            from pydecor.decorators import export

            def foo():
                @export
                def inner():
                    pass

            foo()
            """
        )
        with pytest.raises(TypeError):
            exec(module_code, module.__dict__)

    @pytest.mark.usefixtures("reset_modules")
    def test_export_failure_class_instance(self):
        """Class instances may not be exported."""
        # In actual import machinery, the module is added to sys.modules
        # before the contained code is executed, so we mimic that here.
        module = module_from_spec(ModuleSpec("my_module", None))
        sys.modules["my_module"] = module

        module_code = textwrap.dedent(
            """
            from pydecor.decorators import export

            class Foo: pass

            foo = Foo()

            export(foo)
            """
        )
        with pytest.raises(TypeError):
            exec(module_code, module.__dict__)

# -*- coding: UTF-8 -*- Tests for the @export decorator. Ensure sys.modules is reset at the end of a test. Test the @export decorator. Passing something with no __module__ attribute is a TypeError. # type: ignore Calling in a non-imported context is an error. # creates a new module from pydecor.decorators import export              @export             def exported():                 pass The __all__ attr is created on the imported module if needed. # In actual import machinery, the module is added to sys.modules # before the contained code is executed, so we mimic that here. from pydecor.decorators import export              @export             def exported():                 pass # type: ignore The __all__ attr is appended to if it already exists. # In actual import machinery, the module is added to sys.modules # before the contained code is executed, so we mimic that here. __all__ = ["first"]              from pydecor.decorators import export              first = "some other thing that is already exported"              @export             def exported():                 pass # type: ignore If __all__ is a tuple, the generated one is still a tuple. # In actual import machinery, the module is added to sys.modules # before the contained code is executed, so we mimic that here. __all__ = ("first",)              from pydecor.decorators import export              first = "some other thing that is already exported"              @export             def exported():                 pass # type: ignore The module is not added if already present. # In actual import machinery, the module is added to sys.modules # before the contained code is executed, so we mimic that here. __all__ = ["exported"]              from pydecor.decorators import export              @export             def exported():                 pass # type: ignore Multiple calls don't hurt. # In actual import machinery, the module is added to sys.modules # before the contained code is executed, so we mimic that here. from pydecor.decorators import export              @export             @export             def exported():                 pass # type: ignore A module present in imports is manipulated correctly. # pylint: disable=no-member # type: ignore # pylint: enable=no-member A list __all__ is appended to. A tuple __all__ is replaced with a new tuple. Classes may also be exported. # pylint: disable=no-member # type: ignore Multiple items may be exported. # pylint: disable=no-member # type: ignore Classes may also be exported. # In actual import machinery, the module is added to sys.modules # before the contained code is executed, so we mimic that here. from pydecor.decorators import export              @export             class Exported:                 pass # type: ignore Instance methods may not be exported directly. # In actual import machinery, the module is added to sys.modules # before the contained code is executed, so we mimic that here. from pydecor.decorators import export              class BadClass:                 @export                 def foo(self):                     pass Classmethods may not be exported directly. # In actual import machinery, the module is added to sys.modules # before the contained code is executed, so we mimic that here. from pydecor.decorators import export              class BadClass:                 @export                 @classmethod                 def foo(self):                     pass Staticmethods may not be exported directly. # In actual import machinery, the module is added to sys.modules # before the contained code is executed, so we mimic that here. from pydecor.decorators import export              class BadClass:                 @export                 @staticmethod                 def foo(self):                     pass Inline expressions are not valid input. # In actual import machinery, the module is added to sys.modules # before the contained code is executed, so we mimic that here. from pydecor.decorators import export             export(lambda: None) Interior functions may not be exported. # In actual import machinery, the module is added to sys.modules # before the contained code is executed, so we mimic that here. from pydecor.decorators import export              def foo():                 @export                 def inner():                     pass              foo() Class instances may not be exported. # In actual import machinery, the module is added to sys.modules # before the contained code is executed, so we mimic that here. from pydecor.decorators import export              class Foo: pass              foo = Foo()              export(foo)