mirror of
https://github.com/pybind/pybind11.git
synced 2024-11-24 22:25:10 +00:00
c4a05f9344
* Initial support for GraalPy * Mark tests that currently fail on GraalPy with xfail * Add graalpy to CI * Limit test deps on graalpy to available binary wheels * Skip cmake test installed_function on GraalPy CMake won't find libpython on GraalPy, it either fails or silently picks CPython's libpython. * Factor out setting function docstrings into a macro * Try to narrow down skipped tests
532 lines
17 KiB
Python
532 lines
17 KiB
Python
from __future__ import annotations
|
|
|
|
import re
|
|
|
|
import pytest
|
|
|
|
import env # noqa: F401
|
|
from pybind11_tests import ConstructorStats
|
|
from pybind11_tests import factory_constructors as m
|
|
from pybind11_tests.factory_constructors import tag
|
|
|
|
|
|
@pytest.mark.skipif("env.GRAALPY", reason="Cannot reliably trigger GC")
|
|
def test_init_factory_basic():
|
|
"""Tests py::init_factory() wrapper around various ways of returning the object"""
|
|
|
|
cstats = [
|
|
ConstructorStats.get(c)
|
|
for c in [m.TestFactory1, m.TestFactory2, m.TestFactory3]
|
|
]
|
|
cstats[0].alive() # force gc
|
|
n_inst = ConstructorStats.detail_reg_inst()
|
|
|
|
x1 = m.TestFactory1(tag.unique_ptr, 3)
|
|
assert x1.value == "3"
|
|
y1 = m.TestFactory1(tag.pointer)
|
|
assert y1.value == "(empty)"
|
|
z1 = m.TestFactory1("hi!")
|
|
assert z1.value == "hi!"
|
|
|
|
assert ConstructorStats.detail_reg_inst() == n_inst + 3
|
|
|
|
x2 = m.TestFactory2(tag.move)
|
|
assert x2.value == "(empty2)"
|
|
y2 = m.TestFactory2(tag.pointer, 7)
|
|
assert y2.value == "7"
|
|
z2 = m.TestFactory2(tag.unique_ptr, "hi again")
|
|
assert z2.value == "hi again"
|
|
|
|
assert ConstructorStats.detail_reg_inst() == n_inst + 6
|
|
|
|
x3 = m.TestFactory3(tag.shared_ptr)
|
|
assert x3.value == "(empty3)"
|
|
y3 = m.TestFactory3(tag.pointer, 42)
|
|
assert y3.value == "42"
|
|
z3 = m.TestFactory3("bye")
|
|
assert z3.value == "bye"
|
|
|
|
for null_ptr_kind in [tag.null_ptr, tag.null_unique_ptr, tag.null_shared_ptr]:
|
|
with pytest.raises(TypeError) as excinfo:
|
|
m.TestFactory3(null_ptr_kind)
|
|
assert (
|
|
str(excinfo.value) == "pybind11::init(): factory function returned nullptr"
|
|
)
|
|
|
|
assert [i.alive() for i in cstats] == [3, 3, 3]
|
|
assert ConstructorStats.detail_reg_inst() == n_inst + 9
|
|
|
|
del x1, y2, y3, z3
|
|
assert [i.alive() for i in cstats] == [2, 2, 1]
|
|
assert ConstructorStats.detail_reg_inst() == n_inst + 5
|
|
del x2, x3, y1, z1, z2
|
|
assert [i.alive() for i in cstats] == [0, 0, 0]
|
|
assert ConstructorStats.detail_reg_inst() == n_inst
|
|
|
|
assert [i.values() for i in cstats] == [
|
|
["3", "hi!"],
|
|
["7", "hi again"],
|
|
["42", "bye"],
|
|
]
|
|
assert [i.default_constructions for i in cstats] == [1, 1, 1]
|
|
|
|
|
|
def test_init_factory_signature(msg):
|
|
with pytest.raises(TypeError) as excinfo:
|
|
m.TestFactory1("invalid", "constructor", "arguments")
|
|
assert (
|
|
msg(excinfo.value)
|
|
== """
|
|
__init__(): incompatible constructor arguments. The following argument types are supported:
|
|
1. m.factory_constructors.TestFactory1(arg0: m.factory_constructors.tag.unique_ptr_tag, arg1: int)
|
|
2. m.factory_constructors.TestFactory1(arg0: str)
|
|
3. m.factory_constructors.TestFactory1(arg0: m.factory_constructors.tag.pointer_tag)
|
|
4. m.factory_constructors.TestFactory1(arg0: object, arg1: int, arg2: object)
|
|
|
|
Invoked with: 'invalid', 'constructor', 'arguments'
|
|
"""
|
|
)
|
|
|
|
assert (
|
|
msg(m.TestFactory1.__init__.__doc__)
|
|
== """
|
|
__init__(*args, **kwargs)
|
|
Overloaded function.
|
|
|
|
1. __init__(self: m.factory_constructors.TestFactory1, arg0: m.factory_constructors.tag.unique_ptr_tag, arg1: int) -> None
|
|
|
|
2. __init__(self: m.factory_constructors.TestFactory1, arg0: str) -> None
|
|
|
|
3. __init__(self: m.factory_constructors.TestFactory1, arg0: m.factory_constructors.tag.pointer_tag) -> None
|
|
|
|
4. __init__(self: m.factory_constructors.TestFactory1, arg0: object, arg1: int, arg2: object) -> None
|
|
"""
|
|
)
|
|
|
|
|
|
@pytest.mark.skipif("env.GRAALPY", reason="Cannot reliably trigger GC")
|
|
def test_init_factory_casting():
|
|
"""Tests py::init_factory() wrapper with various upcasting and downcasting returns"""
|
|
|
|
cstats = [
|
|
ConstructorStats.get(c)
|
|
for c in [m.TestFactory3, m.TestFactory4, m.TestFactory5]
|
|
]
|
|
cstats[0].alive() # force gc
|
|
n_inst = ConstructorStats.detail_reg_inst()
|
|
|
|
# Construction from derived references:
|
|
a = m.TestFactory3(tag.pointer, tag.TF4, 4)
|
|
assert a.value == "4"
|
|
b = m.TestFactory3(tag.shared_ptr, tag.TF4, 5)
|
|
assert b.value == "5"
|
|
c = m.TestFactory3(tag.pointer, tag.TF5, 6)
|
|
assert c.value == "6"
|
|
d = m.TestFactory3(tag.shared_ptr, tag.TF5, 7)
|
|
assert d.value == "7"
|
|
|
|
assert ConstructorStats.detail_reg_inst() == n_inst + 4
|
|
|
|
# Shared a lambda with TF3:
|
|
e = m.TestFactory4(tag.pointer, tag.TF4, 8)
|
|
assert e.value == "8"
|
|
|
|
assert ConstructorStats.detail_reg_inst() == n_inst + 5
|
|
assert [i.alive() for i in cstats] == [5, 3, 2]
|
|
|
|
del a
|
|
assert [i.alive() for i in cstats] == [4, 2, 2]
|
|
assert ConstructorStats.detail_reg_inst() == n_inst + 4
|
|
|
|
del b, c, e
|
|
assert [i.alive() for i in cstats] == [1, 0, 1]
|
|
assert ConstructorStats.detail_reg_inst() == n_inst + 1
|
|
|
|
del d
|
|
assert [i.alive() for i in cstats] == [0, 0, 0]
|
|
assert ConstructorStats.detail_reg_inst() == n_inst
|
|
|
|
assert [i.values() for i in cstats] == [
|
|
["4", "5", "6", "7", "8"],
|
|
["4", "5", "8"],
|
|
["6", "7"],
|
|
]
|
|
|
|
|
|
@pytest.mark.skipif("env.GRAALPY", reason="Cannot reliably trigger GC")
|
|
def test_init_factory_alias():
|
|
"""Tests py::init_factory() wrapper with value conversions and alias types"""
|
|
|
|
cstats = [m.TestFactory6.get_cstats(), m.TestFactory6.get_alias_cstats()]
|
|
cstats[0].alive() # force gc
|
|
n_inst = ConstructorStats.detail_reg_inst()
|
|
|
|
a = m.TestFactory6(tag.base, 1)
|
|
assert a.get() == 1
|
|
assert not a.has_alias()
|
|
b = m.TestFactory6(tag.alias, "hi there")
|
|
assert b.get() == 8
|
|
assert b.has_alias()
|
|
c = m.TestFactory6(tag.alias, 3)
|
|
assert c.get() == 3
|
|
assert c.has_alias()
|
|
d = m.TestFactory6(tag.alias, tag.pointer, 4)
|
|
assert d.get() == 4
|
|
assert d.has_alias()
|
|
e = m.TestFactory6(tag.base, tag.pointer, 5)
|
|
assert e.get() == 5
|
|
assert not e.has_alias()
|
|
f = m.TestFactory6(tag.base, tag.alias, tag.pointer, 6)
|
|
assert f.get() == 6
|
|
assert f.has_alias()
|
|
|
|
assert ConstructorStats.detail_reg_inst() == n_inst + 6
|
|
assert [i.alive() for i in cstats] == [6, 4]
|
|
|
|
del a, b, e
|
|
assert [i.alive() for i in cstats] == [3, 3]
|
|
assert ConstructorStats.detail_reg_inst() == n_inst + 3
|
|
del f, c, d
|
|
assert [i.alive() for i in cstats] == [0, 0]
|
|
assert ConstructorStats.detail_reg_inst() == n_inst
|
|
|
|
class MyTest(m.TestFactory6):
|
|
def __init__(self, *args):
|
|
m.TestFactory6.__init__(self, *args)
|
|
|
|
def get(self):
|
|
return -5 + m.TestFactory6.get(self)
|
|
|
|
# Return Class by value, moved into new alias:
|
|
z = MyTest(tag.base, 123)
|
|
assert z.get() == 118
|
|
assert z.has_alias()
|
|
|
|
# Return alias by value, moved into new alias:
|
|
y = MyTest(tag.alias, "why hello!")
|
|
assert y.get() == 5
|
|
assert y.has_alias()
|
|
|
|
# Return Class by pointer, moved into new alias then original destroyed:
|
|
x = MyTest(tag.base, tag.pointer, 47)
|
|
assert x.get() == 42
|
|
assert x.has_alias()
|
|
|
|
assert ConstructorStats.detail_reg_inst() == n_inst + 3
|
|
assert [i.alive() for i in cstats] == [3, 3]
|
|
del x, y, z
|
|
assert [i.alive() for i in cstats] == [0, 0]
|
|
assert ConstructorStats.detail_reg_inst() == n_inst
|
|
|
|
assert [i.values() for i in cstats] == [
|
|
["1", "8", "3", "4", "5", "6", "123", "10", "47"],
|
|
["hi there", "3", "4", "6", "move", "123", "why hello!", "move", "47"],
|
|
]
|
|
|
|
|
|
@pytest.mark.skipif("env.GRAALPY", reason="Cannot reliably trigger GC")
|
|
def test_init_factory_dual():
|
|
"""Tests init factory functions with dual main/alias factory functions"""
|
|
from pybind11_tests.factory_constructors import TestFactory7
|
|
|
|
cstats = [TestFactory7.get_cstats(), TestFactory7.get_alias_cstats()]
|
|
cstats[0].alive() # force gc
|
|
n_inst = ConstructorStats.detail_reg_inst()
|
|
|
|
class PythFactory7(TestFactory7):
|
|
def get(self):
|
|
return 100 + TestFactory7.get(self)
|
|
|
|
a1 = TestFactory7(1)
|
|
a2 = PythFactory7(2)
|
|
assert a1.get() == 1
|
|
assert a2.get() == 102
|
|
assert not a1.has_alias()
|
|
assert a2.has_alias()
|
|
|
|
b1 = TestFactory7(tag.pointer, 3)
|
|
b2 = PythFactory7(tag.pointer, 4)
|
|
assert b1.get() == 3
|
|
assert b2.get() == 104
|
|
assert not b1.has_alias()
|
|
assert b2.has_alias()
|
|
|
|
c1 = TestFactory7(tag.mixed, 5)
|
|
c2 = PythFactory7(tag.mixed, 6)
|
|
assert c1.get() == 5
|
|
assert c2.get() == 106
|
|
assert not c1.has_alias()
|
|
assert c2.has_alias()
|
|
|
|
d1 = TestFactory7(tag.base, tag.pointer, 7)
|
|
d2 = PythFactory7(tag.base, tag.pointer, 8)
|
|
assert d1.get() == 7
|
|
assert d2.get() == 108
|
|
assert not d1.has_alias()
|
|
assert d2.has_alias()
|
|
|
|
# Both return an alias; the second multiplies the value by 10:
|
|
e1 = TestFactory7(tag.alias, tag.pointer, 9)
|
|
e2 = PythFactory7(tag.alias, tag.pointer, 10)
|
|
assert e1.get() == 9
|
|
assert e2.get() == 200
|
|
assert e1.has_alias()
|
|
assert e2.has_alias()
|
|
|
|
f1 = TestFactory7(tag.shared_ptr, tag.base, 11)
|
|
f2 = PythFactory7(tag.shared_ptr, tag.base, 12)
|
|
assert f1.get() == 11
|
|
assert f2.get() == 112
|
|
assert not f1.has_alias()
|
|
assert f2.has_alias()
|
|
|
|
g1 = TestFactory7(tag.shared_ptr, tag.invalid_base, 13)
|
|
assert g1.get() == 13
|
|
assert not g1.has_alias()
|
|
with pytest.raises(TypeError) as excinfo:
|
|
PythFactory7(tag.shared_ptr, tag.invalid_base, 14)
|
|
assert (
|
|
str(excinfo.value)
|
|
== "pybind11::init(): construction failed: returned holder-wrapped instance is not an "
|
|
"alias instance"
|
|
)
|
|
|
|
assert [i.alive() for i in cstats] == [13, 7]
|
|
assert ConstructorStats.detail_reg_inst() == n_inst + 13
|
|
|
|
del a1, a2, b1, d1, e1, e2
|
|
assert [i.alive() for i in cstats] == [7, 4]
|
|
assert ConstructorStats.detail_reg_inst() == n_inst + 7
|
|
del b2, c1, c2, d2, f1, f2, g1
|
|
assert [i.alive() for i in cstats] == [0, 0]
|
|
assert ConstructorStats.detail_reg_inst() == n_inst
|
|
|
|
assert [i.values() for i in cstats] == [
|
|
["1", "2", "3", "4", "5", "6", "7", "8", "9", "100", "11", "12", "13", "14"],
|
|
["2", "4", "6", "8", "9", "100", "12"],
|
|
]
|
|
|
|
|
|
@pytest.mark.skipif("env.GRAALPY", reason="Cannot reliably trigger GC")
|
|
def test_no_placement_new(capture):
|
|
"""Prior to 2.2, `py::init<...>` relied on the type supporting placement
|
|
new; this tests a class without placement new support."""
|
|
with capture:
|
|
a = m.NoPlacementNew(123)
|
|
|
|
found = re.search(r"^operator new called, returning (\d+)\n$", str(capture))
|
|
assert found
|
|
assert a.i == 123
|
|
with capture:
|
|
del a
|
|
pytest.gc_collect()
|
|
assert capture == "operator delete called on " + found.group(1)
|
|
|
|
with capture:
|
|
b = m.NoPlacementNew()
|
|
|
|
found = re.search(r"^operator new called, returning (\d+)\n$", str(capture))
|
|
assert found
|
|
assert b.i == 100
|
|
with capture:
|
|
del b
|
|
pytest.gc_collect()
|
|
assert capture == "operator delete called on " + found.group(1)
|
|
|
|
|
|
def test_multiple_inheritance():
|
|
class MITest(m.TestFactory1, m.TestFactory2):
|
|
def __init__(self):
|
|
m.TestFactory1.__init__(self, tag.unique_ptr, 33)
|
|
m.TestFactory2.__init__(self, tag.move)
|
|
|
|
a = MITest()
|
|
assert m.TestFactory1.value.fget(a) == "33"
|
|
assert m.TestFactory2.value.fget(a) == "(empty2)"
|
|
|
|
|
|
def create_and_destroy(*args):
|
|
a = m.NoisyAlloc(*args)
|
|
print("---")
|
|
del a
|
|
pytest.gc_collect()
|
|
|
|
|
|
def strip_comments(s):
|
|
return re.sub(r"\s+#.*", "", s)
|
|
|
|
|
|
@pytest.mark.skipif("env.GRAALPY", reason="Cannot reliably trigger GC")
|
|
def test_reallocation_a(capture, msg):
|
|
"""When the constructor is overloaded, previous overloads can require a preallocated value.
|
|
This test makes sure that such preallocated values only happen when they might be necessary,
|
|
and that they are deallocated properly."""
|
|
|
|
pytest.gc_collect()
|
|
|
|
with capture:
|
|
create_and_destroy(1)
|
|
assert (
|
|
msg(capture)
|
|
== """
|
|
noisy new
|
|
noisy placement new
|
|
NoisyAlloc(int 1)
|
|
---
|
|
~NoisyAlloc()
|
|
noisy delete
|
|
"""
|
|
)
|
|
|
|
|
|
@pytest.mark.skipif("env.GRAALPY", reason="Cannot reliably trigger GC")
|
|
def test_reallocation_b(capture, msg):
|
|
with capture:
|
|
create_and_destroy(1.5)
|
|
assert msg(capture) == strip_comments(
|
|
"""
|
|
noisy new # allocation required to attempt first overload
|
|
noisy delete # have to dealloc before considering factory init overload
|
|
noisy new # pointer factory calling "new", part 1: allocation
|
|
NoisyAlloc(double 1.5) # ... part two, invoking constructor
|
|
---
|
|
~NoisyAlloc() # Destructor
|
|
noisy delete # operator delete
|
|
"""
|
|
)
|
|
|
|
|
|
@pytest.mark.skipif("env.GRAALPY", reason="Cannot reliably trigger GC")
|
|
def test_reallocation_c(capture, msg):
|
|
with capture:
|
|
create_and_destroy(2, 3)
|
|
assert msg(capture) == strip_comments(
|
|
"""
|
|
noisy new # pointer factory calling "new", allocation
|
|
NoisyAlloc(int 2) # constructor
|
|
---
|
|
~NoisyAlloc() # Destructor
|
|
noisy delete # operator delete
|
|
"""
|
|
)
|
|
|
|
|
|
@pytest.mark.skipif("env.GRAALPY", reason="Cannot reliably trigger GC")
|
|
def test_reallocation_d(capture, msg):
|
|
with capture:
|
|
create_and_destroy(2.5, 3)
|
|
assert msg(capture) == strip_comments(
|
|
"""
|
|
NoisyAlloc(double 2.5) # construction (local func variable: operator_new not called)
|
|
noisy new # return-by-value "new" part 1: allocation
|
|
~NoisyAlloc() # moved-away local func variable destruction
|
|
---
|
|
~NoisyAlloc() # Destructor
|
|
noisy delete # operator delete
|
|
"""
|
|
)
|
|
|
|
|
|
@pytest.mark.skipif("env.GRAALPY", reason="Cannot reliably trigger GC")
|
|
def test_reallocation_e(capture, msg):
|
|
with capture:
|
|
create_and_destroy(3.5, 4.5)
|
|
assert msg(capture) == strip_comments(
|
|
"""
|
|
noisy new # preallocation needed before invoking placement-new overload
|
|
noisy placement new # Placement new
|
|
NoisyAlloc(double 3.5) # construction
|
|
---
|
|
~NoisyAlloc() # Destructor
|
|
noisy delete # operator delete
|
|
"""
|
|
)
|
|
|
|
|
|
@pytest.mark.skipif("env.GRAALPY", reason="Cannot reliably trigger GC")
|
|
def test_reallocation_f(capture, msg):
|
|
with capture:
|
|
create_and_destroy(4, 0.5)
|
|
assert msg(capture) == strip_comments(
|
|
"""
|
|
noisy new # preallocation needed before invoking placement-new overload
|
|
noisy delete # deallocation of preallocated storage
|
|
noisy new # Factory pointer allocation
|
|
NoisyAlloc(int 4) # factory pointer construction
|
|
---
|
|
~NoisyAlloc() # Destructor
|
|
noisy delete # operator delete
|
|
"""
|
|
)
|
|
|
|
|
|
@pytest.mark.skipif("env.GRAALPY", reason="Cannot reliably trigger GC")
|
|
def test_reallocation_g(capture, msg):
|
|
with capture:
|
|
create_and_destroy(5, "hi")
|
|
assert msg(capture) == strip_comments(
|
|
"""
|
|
noisy new # preallocation needed before invoking first placement new
|
|
noisy delete # delete before considering new-style constructor
|
|
noisy new # preallocation for second placement new
|
|
noisy placement new # Placement new in the second placement new overload
|
|
NoisyAlloc(int 5) # construction
|
|
---
|
|
~NoisyAlloc() # Destructor
|
|
noisy delete # operator delete
|
|
"""
|
|
)
|
|
|
|
|
|
def test_invalid_self():
|
|
"""Tests invocation of the pybind-registered base class with an invalid `self` argument."""
|
|
|
|
class NotPybindDerived:
|
|
pass
|
|
|
|
# Attempts to initialize with an invalid type passed as `self`:
|
|
class BrokenTF1(m.TestFactory1):
|
|
def __init__(self, bad):
|
|
if bad == 1:
|
|
a = m.TestFactory2(tag.pointer, 1)
|
|
m.TestFactory1.__init__(a, tag.pointer)
|
|
elif bad == 2:
|
|
a = NotPybindDerived()
|
|
m.TestFactory1.__init__(a, tag.pointer)
|
|
|
|
# Same as above, but for a class with an alias:
|
|
class BrokenTF6(m.TestFactory6):
|
|
def __init__(self, bad):
|
|
if bad == 0:
|
|
m.TestFactory6.__init__()
|
|
elif bad == 1:
|
|
a = m.TestFactory2(tag.pointer, 1)
|
|
m.TestFactory6.__init__(a, tag.base, 1)
|
|
elif bad == 2:
|
|
a = m.TestFactory2(tag.pointer, 1)
|
|
m.TestFactory6.__init__(a, tag.alias, 1)
|
|
elif bad == 3:
|
|
m.TestFactory6.__init__(
|
|
NotPybindDerived.__new__(NotPybindDerived), tag.base, 1
|
|
)
|
|
elif bad == 4:
|
|
m.TestFactory6.__init__(
|
|
NotPybindDerived.__new__(NotPybindDerived), tag.alias, 1
|
|
)
|
|
|
|
for arg in (1, 2):
|
|
with pytest.raises(TypeError) as excinfo:
|
|
BrokenTF1(arg)
|
|
assert (
|
|
str(excinfo.value)
|
|
== "__init__(self, ...) called with invalid or missing `self` argument"
|
|
)
|
|
|
|
for arg in (0, 1, 2, 3, 4):
|
|
with pytest.raises(TypeError) as excinfo:
|
|
BrokenTF6(arg)
|
|
assert (
|
|
str(excinfo.value)
|
|
== "__init__(self, ...) called with invalid or missing `self` argument"
|
|
)
|