conftest.py revision 11986
1"""pytest configuration
2
3Extends output capture as needed by pybind11: ignore constructors, optional unordered lines.
4Adds docstring and exceptions message sanitizers: ignore Python 2 vs 3 differences.
5"""
6
7import pytest
8import textwrap
9import difflib
10import re
11import sys
12import contextlib
13
14_unicode_marker = re.compile(r'u(\'[^\']*\')')
15_long_marker = re.compile(r'([0-9])L')
16_hexadecimal = re.compile(r'0x[0-9a-fA-F]+')
17
18
19def _strip_and_dedent(s):
20    """For triple-quote strings"""
21    return textwrap.dedent(s.lstrip('\n').rstrip())
22
23
24def _split_and_sort(s):
25    """For output which does not require specific line order"""
26    return sorted(_strip_and_dedent(s).splitlines())
27
28
29def _make_explanation(a, b):
30    """Explanation for a failed assert -- the a and b arguments are List[str]"""
31    return ["--- actual / +++ expected"] + [line.strip('\n') for line in difflib.ndiff(a, b)]
32
33
34class Output(object):
35    """Basic output post-processing and comparison"""
36    def __init__(self, string):
37        self.string = string
38        self.explanation = []
39
40    def __str__(self):
41        return self.string
42
43    def __eq__(self, other):
44        # Ignore constructor/destructor output which is prefixed with "###"
45        a = [line for line in self.string.strip().splitlines() if not line.startswith("###")]
46        b = _strip_and_dedent(other).splitlines()
47        if a == b:
48            return True
49        else:
50            self.explanation = _make_explanation(a, b)
51            return False
52
53
54class Unordered(Output):
55    """Custom comparison for output without strict line ordering"""
56    def __eq__(self, other):
57        a = _split_and_sort(self.string)
58        b = _split_and_sort(other)
59        if a == b:
60            return True
61        else:
62            self.explanation = _make_explanation(a, b)
63            return False
64
65
66class Capture(object):
67    def __init__(self, capfd):
68        self.capfd = capfd
69        self.out = ""
70        self.err = ""
71
72    def __enter__(self):
73        self.capfd.readouterr()
74        return self
75
76    def __exit__(self, *_):
77        self.out, self.err = self.capfd.readouterr()
78
79    def __eq__(self, other):
80        a = Output(self.out)
81        b = other
82        if a == b:
83            return True
84        else:
85            self.explanation = a.explanation
86            return False
87
88    def __str__(self):
89        return self.out
90
91    def __contains__(self, item):
92        return item in self.out
93
94    @property
95    def unordered(self):
96        return Unordered(self.out)
97
98    @property
99    def stderr(self):
100        return Output(self.err)
101
102
103@pytest.fixture
104def capture(capfd):
105    """Extended `capfd` with context manager and custom equality operators"""
106    return Capture(capfd)
107
108
109class SanitizedString(object):
110    def __init__(self, sanitizer):
111        self.sanitizer = sanitizer
112        self.string = ""
113        self.explanation = []
114
115    def __call__(self, thing):
116        self.string = self.sanitizer(thing)
117        return self
118
119    def __eq__(self, other):
120        a = self.string
121        b = _strip_and_dedent(other)
122        if a == b:
123            return True
124        else:
125            self.explanation = _make_explanation(a.splitlines(), b.splitlines())
126            return False
127
128
129def _sanitize_general(s):
130    s = s.strip()
131    s = s.replace("pybind11_tests.", "m.")
132    s = s.replace("unicode", "str")
133    s = _long_marker.sub(r"\1", s)
134    s = _unicode_marker.sub(r"\1", s)
135    return s
136
137
138def _sanitize_docstring(thing):
139    s = thing.__doc__
140    s = _sanitize_general(s)
141    return s
142
143
144@pytest.fixture
145def doc():
146    """Sanitize docstrings and add custom failure explanation"""
147    return SanitizedString(_sanitize_docstring)
148
149
150def _sanitize_message(thing):
151    s = str(thing)
152    s = _sanitize_general(s)
153    s = _hexadecimal.sub("0", s)
154    return s
155
156
157@pytest.fixture
158def msg():
159    """Sanitize messages and add custom failure explanation"""
160    return SanitizedString(_sanitize_message)
161
162
163# noinspection PyUnusedLocal
164def pytest_assertrepr_compare(op, left, right):
165    """Hook to insert custom failure explanation"""
166    if hasattr(left, 'explanation'):
167        return left.explanation
168
169
170@contextlib.contextmanager
171def suppress(exception):
172    """Suppress the desired exception"""
173    try:
174        yield
175    except exception:
176        pass
177
178
179def pytest_namespace():
180    """Add import suppression and test requirements to `pytest` namespace"""
181    try:
182        import numpy as np
183    except ImportError:
184        np = None
185    try:
186        import scipy
187    except ImportError:
188        scipy = None
189    try:
190        from pybind11_tests import have_eigen
191    except ImportError:
192        have_eigen = False
193
194    skipif = pytest.mark.skipif
195    return {
196        'suppress': suppress,
197        'requires_numpy': skipif(not np, reason="numpy is not installed"),
198        'requires_scipy': skipif(not np, reason="scipy is not installed"),
199        'requires_eigen_and_numpy': skipif(not have_eigen or not np,
200                                           reason="eigen and/or numpy are not installed"),
201        'requires_eigen_and_scipy': skipif(not have_eigen or not scipy,
202                                           reason="eigen and/or scipy are not installed"),
203    }
204
205
206def _test_import_pybind11():
207    """Early diagnostic for test module initialization errors
208
209    When there is an error during initialization, the first import will report the
210    real error while all subsequent imports will report nonsense. This import test
211    is done early (in the pytest configuration file, before any tests) in order to
212    avoid the noise of having all tests fail with identical error messages.
213
214    Any possible exception is caught here and reported manually *without* the stack
215    trace. This further reduces noise since the trace would only show pytest internals
216    which are not useful for debugging pybind11 module issues.
217    """
218    # noinspection PyBroadException
219    try:
220        import pybind11_tests  # noqa: F401 imported but unused
221    except Exception as e:
222        print("Failed to import pybind11_tests from pytest:")
223        print("  {}: {}".format(type(e).__name__, e))
224        sys.exit(1)
225
226
227_test_import_pybind11()
228