mirror of
https://github.com/pybind/pybind11.git
synced 2024-11-14 17:43:53 +00:00
464d98962d
This allows you to use: cls.def(py::init(&factory_function)); where `factory_function` returns a pointer, holder, or value of the class type (or a derived type). Various compile-time checks (static_asserts) are performed to ensure the function is valid, and various run-time type checks where necessary. Some other details of this feature: - The `py::init` name doesn't conflict with the templated no-argument `py::init<...>()`, but keeps the naming consistent: the existing templated, no-argument one wraps constructors, the no-template, function-argument one wraps factory functions. - If returning a CppClass (whether by value or pointer) when an CppAlias is required (i.e. python-side inheritance and a declared alias), a dynamic_cast to the alias is attempted (for the pointer version); if it fails, or if returned by value, an Alias(Class &&) constructor is invoked. If this constructor doesn't exist, a runtime error occurs. - for holder returns when an alias is required, we try a dynamic_cast of the wrapped pointer to the alias to see if it is already an alias instance; if it isn't, we raise an error. - `py::init(class_factory, alias_factory)` is also available that takes two factories: the first is called when an alias is not needed, the second when it is. - Reimplement factory instance clearing. The previous implementation failed under python-side multiple inheritance: *each* inherited type's factory init would clear the instance instead of only setting its own type value. The new implementation here clears just the relevant value pointer. - dealloc is updated to explicitly set the leftover value pointer to nullptr and the `holder_constructed` flag to false so that it can be used to clear preallocated value without needing to rebuild the instance internals data. - Added various tests to test out new allocation/deallocation code. - With preallocation now done lazily, init factory holders can completely avoid the extra overhead of needing an extra allocation/deallocation. - Updated documentation to make factory constructors the default advanced constructor style. - If an `__init__` is called a second time, we have two choices: we can throw away the first instance, replacing it with the second; or we can ignore the second call. The latter is slightly easier, so do that.
242 lines
6.5 KiB
Python
242 lines
6.5 KiB
Python
"""pytest configuration
|
|
|
|
Extends output capture as needed by pybind11: ignore constructors, optional unordered lines.
|
|
Adds docstring and exceptions message sanitizers: ignore Python 2 vs 3 differences.
|
|
"""
|
|
|
|
import pytest
|
|
import textwrap
|
|
import difflib
|
|
import re
|
|
import sys
|
|
import contextlib
|
|
import platform
|
|
import gc
|
|
|
|
_unicode_marker = re.compile(r'u(\'[^\']*\')')
|
|
_long_marker = re.compile(r'([0-9])L')
|
|
_hexadecimal = re.compile(r'0x[0-9a-fA-F]+')
|
|
|
|
|
|
def _strip_and_dedent(s):
|
|
"""For triple-quote strings"""
|
|
return textwrap.dedent(s.lstrip('\n').rstrip())
|
|
|
|
|
|
def _split_and_sort(s):
|
|
"""For output which does not require specific line order"""
|
|
return sorted(_strip_and_dedent(s).splitlines())
|
|
|
|
|
|
def _make_explanation(a, b):
|
|
"""Explanation for a failed assert -- the a and b arguments are List[str]"""
|
|
return ["--- actual / +++ expected"] + [line.strip('\n') for line in difflib.ndiff(a, b)]
|
|
|
|
|
|
class Output(object):
|
|
"""Basic output post-processing and comparison"""
|
|
def __init__(self, string):
|
|
self.string = string
|
|
self.explanation = []
|
|
|
|
def __str__(self):
|
|
return self.string
|
|
|
|
def __eq__(self, other):
|
|
# Ignore constructor/destructor output which is prefixed with "###"
|
|
a = [line for line in self.string.strip().splitlines() if not line.startswith("###")]
|
|
b = _strip_and_dedent(other).splitlines()
|
|
if a == b:
|
|
return True
|
|
else:
|
|
self.explanation = _make_explanation(a, b)
|
|
return False
|
|
|
|
|
|
class Unordered(Output):
|
|
"""Custom comparison for output without strict line ordering"""
|
|
def __eq__(self, other):
|
|
a = _split_and_sort(self.string)
|
|
b = _split_and_sort(other)
|
|
if a == b:
|
|
return True
|
|
else:
|
|
self.explanation = _make_explanation(a, b)
|
|
return False
|
|
|
|
|
|
class Capture(object):
|
|
def __init__(self, capfd):
|
|
self.capfd = capfd
|
|
self.out = ""
|
|
self.err = ""
|
|
|
|
def __enter__(self):
|
|
self.capfd.readouterr()
|
|
return self
|
|
|
|
def __exit__(self, *_):
|
|
self.out, self.err = self.capfd.readouterr()
|
|
|
|
def __eq__(self, other):
|
|
a = Output(self.out)
|
|
b = other
|
|
if a == b:
|
|
return True
|
|
else:
|
|
self.explanation = a.explanation
|
|
return False
|
|
|
|
def __str__(self):
|
|
return self.out
|
|
|
|
def __contains__(self, item):
|
|
return item in self.out
|
|
|
|
@property
|
|
def unordered(self):
|
|
return Unordered(self.out)
|
|
|
|
@property
|
|
def stderr(self):
|
|
return Output(self.err)
|
|
|
|
|
|
@pytest.fixture
|
|
def capture(capsys):
|
|
"""Extended `capsys` with context manager and custom equality operators"""
|
|
return Capture(capsys)
|
|
|
|
|
|
class SanitizedString(object):
|
|
def __init__(self, sanitizer):
|
|
self.sanitizer = sanitizer
|
|
self.string = ""
|
|
self.explanation = []
|
|
|
|
def __call__(self, thing):
|
|
self.string = self.sanitizer(thing)
|
|
return self
|
|
|
|
def __eq__(self, other):
|
|
a = self.string
|
|
b = _strip_and_dedent(other)
|
|
if a == b:
|
|
return True
|
|
else:
|
|
self.explanation = _make_explanation(a.splitlines(), b.splitlines())
|
|
return False
|
|
|
|
|
|
def _sanitize_general(s):
|
|
s = s.strip()
|
|
s = s.replace("pybind11_tests.", "m.")
|
|
s = s.replace("unicode", "str")
|
|
s = _long_marker.sub(r"\1", s)
|
|
s = _unicode_marker.sub(r"\1", s)
|
|
return s
|
|
|
|
|
|
def _sanitize_docstring(thing):
|
|
s = thing.__doc__
|
|
s = _sanitize_general(s)
|
|
return s
|
|
|
|
|
|
@pytest.fixture
|
|
def doc():
|
|
"""Sanitize docstrings and add custom failure explanation"""
|
|
return SanitizedString(_sanitize_docstring)
|
|
|
|
|
|
def _sanitize_message(thing):
|
|
s = str(thing)
|
|
s = _sanitize_general(s)
|
|
s = _hexadecimal.sub("0", s)
|
|
return s
|
|
|
|
|
|
@pytest.fixture
|
|
def msg():
|
|
"""Sanitize messages and add custom failure explanation"""
|
|
return SanitizedString(_sanitize_message)
|
|
|
|
|
|
# noinspection PyUnusedLocal
|
|
def pytest_assertrepr_compare(op, left, right):
|
|
"""Hook to insert custom failure explanation"""
|
|
if hasattr(left, 'explanation'):
|
|
return left.explanation
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def suppress(exception):
|
|
"""Suppress the desired exception"""
|
|
try:
|
|
yield
|
|
except exception:
|
|
pass
|
|
|
|
|
|
def gc_collect():
|
|
''' Run the garbage collector twice (needed when running
|
|
reference counting tests with PyPy) '''
|
|
gc.collect()
|
|
gc.collect()
|
|
|
|
|
|
def pytest_namespace():
|
|
"""Add import suppression and test requirements to `pytest` namespace"""
|
|
try:
|
|
import numpy as np
|
|
except ImportError:
|
|
np = None
|
|
try:
|
|
import scipy
|
|
except ImportError:
|
|
scipy = None
|
|
try:
|
|
from pybind11_tests.eigen import have_eigen
|
|
except ImportError:
|
|
have_eigen = False
|
|
pypy = platform.python_implementation() == "PyPy"
|
|
|
|
skipif = pytest.mark.skipif
|
|
return {
|
|
'suppress': suppress,
|
|
'requires_numpy': skipif(not np, reason="numpy is not installed"),
|
|
'requires_scipy': skipif(not np, reason="scipy is not installed"),
|
|
'requires_eigen_and_numpy': skipif(not have_eigen or not np,
|
|
reason="eigen and/or numpy are not installed"),
|
|
'requires_eigen_and_scipy': skipif(not have_eigen or not scipy,
|
|
reason="eigen and/or scipy are not installed"),
|
|
'unsupported_on_pypy': skipif(pypy, reason="unsupported on PyPy"),
|
|
'unsupported_on_py2': skipif(sys.version_info.major < 3,
|
|
reason="unsupported on Python 2.x"),
|
|
'gc_collect': gc_collect
|
|
}
|
|
|
|
|
|
def _test_import_pybind11():
|
|
"""Early diagnostic for test module initialization errors
|
|
|
|
When there is an error during initialization, the first import will report the
|
|
real error while all subsequent imports will report nonsense. This import test
|
|
is done early (in the pytest configuration file, before any tests) in order to
|
|
avoid the noise of having all tests fail with identical error messages.
|
|
|
|
Any possible exception is caught here and reported manually *without* the stack
|
|
trace. This further reduces noise since the trace would only show pytest internals
|
|
which are not useful for debugging pybind11 module issues.
|
|
"""
|
|
# noinspection PyBroadException
|
|
try:
|
|
import pybind11_tests # noqa: F401 imported but unused
|
|
except Exception as e:
|
|
print("Failed to import pybind11_tests from pytest:")
|
|
print(" {}: {}".format(type(e).__name__, e))
|
|
sys.exit(1)
|
|
|
|
|
|
_test_import_pybind11()
|