File: decorators.py

package info (click to toggle)
exhale 0.3.7-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 1,616 kB
  • sloc: python: 9,057; cpp: 1,260; javascript: 915; f90: 29; ansic: 18; makefile: 16
file content (257 lines) | stat: -rw-r--r-- 9,176 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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
# -*- coding: utf8 -*-
########################################################################################
# This file is part of exhale.  Copyright (c) 2017-2024, Stephen McDowell.             #
# Full BSD 3-Clause license available here:                                            #
#                                                                                      #
#                https://github.com/svenevs/exhale/blob/master/LICENSE                 #
########################################################################################
"""
The decorators module defines useful class / function decorators for test cases.
"""

from __future__ import unicode_literals
from copy import deepcopy
from inspect import isclass

import pytest

from .utils import deep_update


__all__ = ["default_confoverrides", "confoverrides", "no_run"]


def _apply_confoverride_to_class(cls, config, priority):
    """
    Apply a configuration override ``config`` to class ``cls`` with a given priority.

    We need the priority trick as the default configuration is applied before a possible
    class-wide decorator, which should supersede the default configuration.  We use
    ``pytest.mark.exhale`` as a store of ``kwargs`` to apply to ``pytest.mark.sphinx``,
    and we use the priority to combine these kwargs with respect to priorities.

    This method performs the ultimate ``pytest.mark.sphinx``.

    **Parameters**
        ``cls`` (Subclass of :class:`testing.base.ExhaleTestCase`)
            The class to apply the ``confoverride`` to.

        ``config`` (:class:`python:dict`)
            The dictionary of ``confoverrides`` as would be supplied to
            ``pytest.mark.sphinx``.  These are the overrides to ``conf.py``.

        ``priority`` (:class:`python:int`)
            The positive integer indicating the priority of the new ``config`` updates,
            higher values take precedence over lower values.

            For example, when :func:`testing.decorators.default_confoverrides` calls
            this function, the priority is ``1``.  When the decorator
            :func:`testing.decorators.confoverrides` is applied to a class, the priority
            is ``2``.  Finally, when :func:`testing.decorators.confoverrides` is applied
            to a test function, its priority is ``3``.  Thus the function-level override
            has the highest priority, meaning any conflicting values with lower level
            priorities will lose out to the function-level override.

    **Return**
        Subclass of :class:`testing.base.ExhaleTestCase`
            The input ``cls`` is returned.
    """
    # look for test methods in the class
    for name, meth in cls.__dict__.items():
        if not callable(meth) or not name.startswith("test_"):
            continue

        # meth is a test method, let's go and mark it!

        # create marker kwargs
        kwargs = dict(confoverrides=deepcopy(config))

        # apply the exhale marker to the method, so that priority is saved
        meth = pytest.mark.exhale(priority, **kwargs)(meth)

        # create a list of exhale markers kwargs
        markers_kwargs = []
        if getattr(meth, "pytestmark", False):
            for mark in meth.pytestmark:
                if mark.name == "exhale":
                    # mark.args[0]: the priority from pytest.mark.exhale
                    # mark.kwargs: the kwargs to override with said priority
                    markers_kwargs.append((mark.args[0], mark.kwargs))

        # sort that list according to priority
        markers_kwargs.sort(key=lambda m: m[0])

        # now we can generate the sphinx fixture kwargs by combining the above list of
        # kwargs depending on priority
        sphinx_kwargs = {}
        for __, kw in markers_kwargs:
            deep_update(sphinx_kwargs, kw)

        # and finally we set the sphinx markers with the combined kwargs, that override
        # the previous ones
        setattr(cls, name, pytest.mark.sphinx(**sphinx_kwargs)(meth))

    return cls


def default_confoverrides(cls, config):
    """
    Apply the default configuration config to test class ``cls``.

    This configuration is set with a priority of ``1`` so that it may be overridden by
    the :func:`testing.decorators.confoverrides` decorator applied to test classes and
    functions.

    **Parameters**
        ``cls`` (Subclass of :class:`testing.base.ExhaleTestCase`)
            The class to apply the ``config`` dictionary as ``confoverrides`` to.

        ``config`` (:class:`python:dict`)
            The dictionary of ``confoverrides`` as would be supplied to
            ``pytest.mark.sphinx``.  These are the overrides to ``conf.py``.

    **Return**
        Subclass of :class:`testing.base.ExhaleTestCase`
            The input ``cls`` is returned.
    """
    return _apply_confoverride_to_class(cls, config, 1)


def confoverrides(**config):
    """
    Override the defaults of |make_default_config| to supply to ``pytest.mark.sphinx``.

    .. |make_default_config| replace:: :func:`testing.base.make_default_config`

    It can be applied to a test method or a test class, which is equivalent to
    decorating every method in that class.

    Usage:

    .. code-block:: py

       @confoverrides(var1=value1, var2=value2)
       def test_something(self):
           ...

    or:

    .. code-block:: py

       @confoverrides(var1=value1, var2=value2)
       class MyTestCase(ExhaleTestCase):
           test_project = 'my_project'
           ...

    Typical usage of this decorator is to modify a value in ``exhale_args``, such as

    .. code-block:: py

       @confoverrides(exhale_args={"containmentFolder": "./alt_api"})
       def test_alt_out(self):
          ...

    However, this decorator can be used to change or set any value you would typically
    find in a ``conf.py`` file.

    **Parameters**
        ``**config`` (:class:`python:dict`)
            The dictionary of ``confoverrides`` as would be supplied to
            ``pytest.mark.sphinx``.  These are the overrides to ``conf.py``.

    **Return**
        (``class`` or :data:`python:types.FunctionType`)
            The decorated class or function.
    """
    def actual_decorator(meth_or_cls):
        if not config:
            return meth_or_cls

        if isclass(meth_or_cls):
            return _apply_confoverride_to_class(meth_or_cls, config, 2)
        else:
            return pytest.mark.exhale(3, confoverrides=config)(meth_or_cls)

    return actual_decorator


def no_cleanup(method):
    """
    Prevent the "docs" directory and generated Doxygen / API from being deleted.

    Usage:

    .. code-block:: py

       class CMathsTests(ExhaleTestCase):
           # docs dir, generated API, and Doxygen will not be deleted so that you can
           # inspect what may be causing your test to fail
           @no_cleanup
           def test_being_developed(self):
               pass

    .. danger::

       This decorator performs ``self.testroot = [self.testroot]`` as an internal bypass
       to the fixtures created in ``__new__`` for the metaclass.  Specifically, the
       fixtures generated check ``if isinstance(self.testroot, six.string_types)``.

       As such, since ``self.testroot`` may be desired in the given ``@no_cleanup``
       function, you must acquire it with ``testroot = self.testroot[0]``.  This is a
       hacky solution, but should be sufficient.  You have been warned.

    **Parameters**
        ``method`` (:data:`python:types.FunctionType`)
            **Must** be an instance-level testing function of a derived type of
            :class:`testing.base.ExhaleTestCase`.  The function should have only a
            single parameter ``self``.

    **Return**
        (:data:`python:types.FunctionType`)
            The decorated function, which simply calls the provided function and sets
            ``self.testroot = None``.  Click on ``[source]`` link for
            :func:`ExhaleTestCaseMetaclass.__new__ <testing.base.ExhaleTestCaseMetaclass.__new__>`
            and search for ``@no_cleanup`` to see how this prevents cleanup.
    """
    def actual_no_cleanup(self):
        self.testroot = [self.testroot]
        method(self)
    return actual_no_cleanup


def no_run(obj):
    """
    Disable the generation of ``*.rst`` files in a specific test method.

    It can be applied to a test class, which will be equivalent to decorating every
    method in that class.

    Usage:

    .. code-block:: py

       @no_run
       def test_something(self):
           ...

    or:

    .. code-block:: py

       @no_run
       class MyTestCase(ExhaleTestCase):
           test_project = 'my_project'
           ...

    Internally this will use the :func:`testing.fixtures.no_run` fixture.

    **Parameters**
        ``obj`` (``class`` or :data:`python:types.FunctionType`)
            The class or function to disable exhale from generating reStructuredText
            documents for.

    **Return**
        ``class`` or :data:`python:types.FunctionType`
            The decorated ``obj``.
    """
    return pytest.mark.usefixtures("no_run")(obj)