File: conftest.py

package info (click to toggle)
python-beartype 0.20.2-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 8,280 kB
  • sloc: python: 76,306; sh: 299; makefile: 30
file content (167 lines) | stat: -rw-r--r-- 7,962 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
#!/usr/bin/env python3
# --------------------( LICENSE                            )--------------------
# Copyright (c) 2014-2025 Beartype authors.
# See "LICENSE" for further details.

'''
:mod:`pytest` **global test configuration** (i.e., early-time configuration
guaranteed to be run by :mod:`pytest` *after* passed command-line arguments are
parsed).

:mod:`pytest` implicitly imports *all* functionality defined by this module
into *all* submodules of this subpackage.

See Also
----------
https://github.com/pytest-dev/pytest-asyncio/blob/master/pytest_asyncio/plugin.py
    :mod:`pytest` plugin strongly inspiring this implementation. Despite its
    popularity,, pytest-asyncio is mostly unmaintained, poorly commented and
    documented, overly obfuscatory, has an extreme number of unresolved issues
    and unmerged pull requests, and just generally exhibits code smells.
'''

# ....................{ TODO                               }....................
#FIXME: Consider refactoring the pytest_pyfunc_call() hook defined below into:
#* A pull request against pytest itself. Pytest absolutely requires support for
#  asynchronous test functions. This is 2021, people.
#* A new competing "pytest-async" plugin. This is substantially easier but less
#  ideal, as pytest *REALLY* both wants and needs this functionality.

# ....................{ IMPORTS                            }....................
from asyncio import (
    get_event_loop_policy,
    new_event_loop,
    set_event_loop,
)
from functools import wraps
from inspect import iscoroutinefunction
from pytest import hookimpl
from warnings import (
    catch_warnings,
    simplefilter,
)

# ....................{ HOOKS ~ configure                  }....................
@hookimpl(hookwrapper=True, tryfirst=True)
def pytest_pyfunc_call(pyfuncitem: 'Function') -> None:
    '''
    Hook wrapper called immediately *before* calling the passed test function.

    Specifically, this hook wrapper:

    * If this test function is synchronous (i.e., declared with ``def``),
      preserves this test function as is.
    * If this test function is asynchronous (i.e., declared with ``async
      def``), wraps this test function in a synchronous wrapper function
      synchronously running this test function under an event loop uniquely
      isolated to this test function. For safety, each asynchronous test
      function is run under a new event loop.

    This wrapper wraps all non-wrapper ``pytest_pyfunc_call()`` hooks and is
    hopefully called *before* all wrapper ``pytest_pyfunc_call()`` hooks. See
    also `the official pytest hook wrapper documentation <hook wrapper_>`__.

    Parameters
    ----------
    pyfuncitem : Function
        :mod:`pytest` object encapsulating the test function to be run.

    .. _hook wrapper:
       https://docs.pytest.org/en/6.2.x/writing_plugins.html#hookwrapper-executing-around-other-hooks
    '''

    # Test function to be called by this hook.
    test_func = pyfuncitem.obj

    # If this test function is an asynchronous coroutine function (i.e.,
    # callable declared with "async def" containing *NO* "yield"
    # expressions)...
    #
    # Note that we intentionally prefer calling this well-tested tester of the
    # well-tested "inspect" module rather than our comparable
    # beartype._util.func.utilfunctest.is_func_coro() tester, which
    # is hopefully but *NOT* necessarily known to be working here.
    if iscoroutinefunction(test_func):
        @wraps(test_func)
        def test_func_synchronous(*args, **kwargs):
            '''
            Closure synchronously calling the current asynchronous test
            coroutine function under a new event loop uniquely isolated to this
            coroutine.
            '''

            # With a warning context manager...
            with catch_warnings():
                # Ignore *ALL* deprecating warnings emitted by the
                # get_event_loop() function called below. For unknown reasons,
                # CPython 3.11 devs thought that emitting a "There is no current
                # event loop" warning (erroneously classified as a
                # "deprecation") was a wonderful idea. "asyncio" is arduous
                # enough to portably support as it is. Work with me here, guys!
                simplefilter('ignore', DeprecationWarning)

                # Current event loop for the current threading context if any
                # *OR* create a new event loop otherwise. Note that the
                # higher-level asyncio.get_event_loop() getter is intentionally
                # *NOT* called here, as Python 3.10 broke backward compatibility
                # by refactoring that getter to be an alias for the wildly
                # different asyncio.get_running_loop() getter, which *MUST* be
                # called only from within either an asynchronous callable or
                # running event loop. In either case, asyncio.get_running_loop()
                # and thus asyncio.get_event_loop() is useless in this context.
                # Instead, we call the lower-level
                # get_event_loop_policy().get_event_loop() getter -- which
                # asyncio.get_event_loop() used to wrap. *facepalm*
                #
                # This getter should ideally return "None" rather than creating
                # a new event loop without our permission if no loop has been
                # set. This getter instead does the latter, implying that this
                # closure will typically instantiate two event loops per
                # asynchronous coroutine test function:
                # * The first useless event loop implicitly created by this
                #   get_event_loop() call.
                # * The second useful event loop explicitly created by the
                #   subsequent new_event_loop() call.
                #
                # Since there exists *NO* other means of querying the current
                # event loop, we reluctantly bite the bullet and pay the piper.
                event_loop_old = get_event_loop_policy().get_event_loop()

                # Close this loop, regardless of whether the prior
                # get_event_loop() call just implicitly created this loop,
                # because the "asyncio" API offers *NO* means of differentiating
                # these two common edge cases. *double facepalm*
                event_loop_old.close()

            # New event loop isolated to this coroutine.
            #
            # Note that this event loop has yet to be set as the current event
            # loop for the current threading context. Explicit is better than
            # implicit.
            event_loop = new_event_loop()

            # Set this as the current event loop for this threading context.
            set_event_loop(event_loop)

            # Coroutine object produced by this asynchronous coroutine test
            # function. Technically, coroutine functions are *NOT* actually
            # coroutines; they're just syntactic sugar implemented as standard
            # synchronous functions dynamically creating and returning
            # asynchronous coroutine objects on each call.
            test_func_coroutine = test_func(*args, **kwargs)

            # Synchronously run a new asynchronous task implicitly scheduled to
            # run this coroutine, ignoring the value returned by this coroutine
            # (if any) while reraising any exception raised by this coroutine
            # up the call stack to pytest.
            event_loop.run_until_complete(test_func_coroutine)

            # Close this event loop.
            event_loop.close()

        # Replace this asynchronous coroutine test function with this
        # synchronous closure wrapping this test function.
        pyfuncitem.obj = test_func_synchronous

    # Perform this test by calling this test function.
    yield