File: _testsupport.py

package info (click to toggle)
napari-plugin-engine 0.2.0-4
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 432 kB
  • sloc: python: 3,052; makefile: 21
file content (138 lines) | stat: -rw-r--r-- 4,696 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
from contextlib import contextmanager

import pytest

from napari_plugin_engine import (
    HookCaller,
    HookImplementation,
    HookImplementationMarker,
    HookSpecification,
    HookSpecificationMarker,
    PluginManager,
)


@pytest.fixture
def test_plugin_manager() -> PluginManager:
    """A plugin manager fixture with the project name 'test'."""
    return PluginManager(project_name='test')


@pytest.fixture
def add_specification(test_plugin_manager):
    """Return a decorator that adds a HookSpecification to test_plugin_manager."""

    def addspec(function=None, *, firstresult=False, historic=False):
        def wrap(func):
            project = test_plugin_manager.project_name
            test_hookspec = HookSpecificationMarker(project)
            test_hookspec(firstresult=firstresult, historic=historic)(func)
            name = func.__name__
            namespace = type("Hook", (), {name: func})
            assert not hasattr(
                test_plugin_manager.hook, name
            ), f"Hook already exists with name: {name}"
            opts = getattr(func, HookSpecification.format_tag(project))
            hook_caller = HookCaller(
                name, test_plugin_manager._hookexec, namespace, opts
            )
            setattr(test_plugin_manager.hook, name, hook_caller)

        return wrap(function) if function is not None else wrap

    return addspec


@pytest.fixture
def add_implementation(test_plugin_manager):
    """Return a decorator that adds a HookImplementation to test_plugin_manager."""

    def addimpl(
        function=None,
        *,
        specname=None,
        tryfirst=False,
        trylast=False,
        hookwrapper=False,
    ):
        def wrap(func):
            project = test_plugin_manager.project_name
            HookImplementationMarker(project)(
                tryfirst=tryfirst,
                trylast=trylast,
                hookwrapper=hookwrapper,
                specname=specname,
            )(func)
            _specname = specname or func.__name__
            hook_caller = getattr(test_plugin_manager.hook, _specname, None)
            assert hook_caller, f"No hook with with name: {_specname}"
            opts = getattr(func, HookImplementation.format_tag(project))
            hook_caller._add_hookimpl(HookImplementation(func, **opts))
            return func

        return wrap(function) if function is not None else wrap

    return addimpl


@pytest.fixture
def caller_from_implementation(
    test_plugin_manager, add_specification, add_implementation
):
    """Return hook caller with implementation as its own spec definition.

    Adds a specification and implementation to the test_plugin_manager based on
    a single function definition (e.g. assumes that the implementation has the
    correct signature).  Returns the hook caller instance prepopulated with the
    hook implementation.
    """

    def wrap(func, spec_kwargs={}, impl_kwargs={}):
        add_specification(func, **spec_kwargs)
        add_implementation(func, **impl_kwargs)
        name = spec_kwargs.get('specname') or func.__name__
        return getattr(test_plugin_manager.hook, name)

    return wrap


@pytest.fixture
def temporary_hookimpl(test_plugin_manager):
    """A fixture that can be used to insert a HookImplementation in the hook call loop.

    Use as a context manager, which will return the hook_caller for the
    corresponding hook specification.

    Example
    -------
    .. code-block: python

        def my_hook_implementation(arg):
            raise ValueError("shoot!")

        with temporary_hookimpl(my_hook_implementation) as hook_caller:
            with pytest.raises(PluginCallError):
                hook_caller(arg=42)
    """

    @contextmanager
    def wrap(func, specname=None, *, tryfirst=True, trylast=None):
        project = test_plugin_manager.project_name
        marker = HookImplementationMarker(project)
        marker(tryfirst=tryfirst, trylast=trylast, specname=specname)(func)
        _specname = specname or func.__name__
        hook_caller = getattr(test_plugin_manager.hook, _specname, None)
        assert hook_caller, f"No hook with with name: {_specname}"
        opts = getattr(func, HookImplementation.format_tag(project))
        impl = HookImplementation(func, **opts)
        hook_caller._add_hookimpl(impl)
        try:
            yield hook_caller
        finally:
            if impl in hook_caller._nonwrappers:
                hook_caller._nonwrappers.remove(impl)
            if impl in hook_caller._wrappers:
                hook_caller._wrappers.remove(impl)
            assert impl not in hook_caller.get_hookimpls()

    return wrap