WIP: PyPy support (#527)
This commit includes modifications that are needed to get pybind11 to work with PyPy. The full test suite compiles and runs except for a last few functions that are commented out (due to problems in PyPy that were reported on the PyPy bugtracker).
Two somewhat intrusive changes were needed to make it possible: two new tags ``py::buffer_protocol()`` and ``py::metaclass()`` must now be specified to the ``class_`` constructor if the class uses the buffer protocol and/or requires a metaclass (e.g. for static properties).
Note that this is only for the PyPy version based on Python 2.7 for now. When the PyPy 3.x has caught up in terms of cpyext compliance, a PyPy 3.x patch will follow.
diff --git a/tests/conftest.py b/tests/conftest.py
index d4335fc..b69fd6c 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -10,6 +10,8 @@
import re
import sys
import contextlib
+import platform
+import gc
_unicode_marker = re.compile(r'u(\'[^\']*\')')
_long_marker = re.compile(r'([0-9])L')
@@ -176,6 +178,13 @@
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:
@@ -190,6 +199,7 @@
from pybind11_tests import have_eigen
except ImportError:
have_eigen = False
+ pypy = platform.python_implementation() == "PyPy"
skipif = pytest.mark.skipif
return {
@@ -200,6 +210,8 @@
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"),
+ 'gc_collect': gc_collect
}
diff --git a/tests/constructor_stats.h b/tests/constructor_stats.h
index eb3e49c..de5c133 100644
--- a/tests/constructor_stats.h
+++ b/tests/constructor_stats.h
@@ -85,27 +85,51 @@
created(inst);
copy_constructions++;
}
+
void move_created(void *inst) {
created(inst);
move_constructions++;
}
+
void default_created(void *inst) {
created(inst);
default_constructions++;
}
+
void created(void *inst) {
++_instances[inst];
- };
+ }
+
void destroyed(void *inst) {
if (--_instances[inst] < 0)
- throw std::runtime_error("cstats.destroyed() called with unknown instance; potential double-destruction or a missing cstats.created()");
+ throw std::runtime_error("cstats.destroyed() called with unknown "
+ "instance; potential double-destruction "
+ "or a missing cstats.created()");
+ }
+
+ static void gc() {
+ // Force garbage collection to ensure any pending destructors are invoked:
+#if defined(PYPY_VERSION)
+ PyObject *globals = PyEval_GetGlobals();
+ PyObject *result = PyRun_String(
+ "import gc\n"
+ "for i in range(2):"
+ " gc.collect()\n",
+ Py_file_input, globals, globals);
+ if (result == nullptr)
+ throw py::error_already_set();
+ Py_DECREF(result);
+#else
+ py::module::import("gc").attr("collect")();
+#endif
}
int alive() {
- // Force garbage collection to ensure any pending destructors are invoked:
- py::module::import("gc").attr("collect")();
+ gc();
int total = 0;
- for (const auto &p : _instances) if (p.second > 0) total += p.second;
+ for (const auto &p : _instances)
+ if (p.second > 0)
+ total += p.second;
return total;
}
@@ -134,6 +158,9 @@
// Gets constructor stats from a C++ type
template <typename T> static ConstructorStats& get() {
+#if defined(PYPY_VERSION)
+ gc();
+#endif
return get(typeid(T));
}
diff --git a/tests/test_alias_initialization.py b/tests/test_alias_initialization.py
index 0ed9d2f..fb90cfc 100644
--- a/tests/test_alias_initialization.py
+++ b/tests/test_alias_initialization.py
@@ -1,10 +1,11 @@
-import gc
+import pytest
def test_alias_delay_initialization1(capture):
- """A only initializes its trampoline class when we inherit from it; if we just
- create and use an A instance directly, the trampoline initialization is bypassed
- and we only initialize an A() instead (for performance reasons).
+ """
+ A only initializes its trampoline class when we inherit from it; if we just
+ create and use an A instance directly, the trampoline initialization is
+ bypassed and we only initialize an A() instead (for performance reasons).
"""
from pybind11_tests import A, call_f
@@ -20,7 +21,7 @@
a = A()
call_f(a)
del a
- gc.collect()
+ pytest.gc_collect()
assert capture == "A.f()"
# Python version
@@ -28,7 +29,7 @@
b = B()
call_f(b)
del b
- gc.collect()
+ pytest.gc_collect()
assert capture == """
PyA.PyA()
PyA.f()
@@ -57,7 +58,7 @@
a2 = A2()
call_f(a2)
del a2
- gc.collect()
+ pytest.gc_collect()
assert capture == """
PyA2.PyA2()
PyA2.f()
@@ -70,7 +71,7 @@
b2 = B2()
call_f(b2)
del b2
- gc.collect()
+ pytest.gc_collect()
assert capture == """
PyA2.PyA2()
PyA2.f()
diff --git a/tests/test_buffers.cpp b/tests/test_buffers.cpp
index c3a7a9e..057250d 100644
--- a/tests/test_buffers.cpp
+++ b/tests/test_buffers.cpp
@@ -75,7 +75,7 @@
};
test_initializer buffers([](py::module &m) {
- py::class_<Matrix> mtx(m, "Matrix");
+ py::class_<Matrix> mtx(m, "Matrix", py::buffer_protocol());
mtx.def(py::init<size_t, size_t>())
/// Construct from a buffer
diff --git a/tests/test_buffers.py b/tests/test_buffers.py
index f0ea964..956839c 100644
--- a/tests/test_buffers.py
+++ b/tests/test_buffers.py
@@ -6,34 +6,6 @@
@pytest.requires_numpy
-def test_to_python():
- m = Matrix(5, 5)
-
- assert m[2, 3] == 0
- m[2, 3] = 4
- assert m[2, 3] == 4
-
- m2 = np.array(m, copy=False)
- assert m2.shape == (5, 5)
- assert abs(m2).sum() == 4
- assert m2[2, 3] == 4
- m2[2, 3] = 5
- assert m2[2, 3] == 5
-
- cstats = ConstructorStats.get(Matrix)
- assert cstats.alive() == 1
- del m
- assert cstats.alive() == 1
- del m2 # holds an m reference
- assert cstats.alive() == 0
- assert cstats.values() == ["5x5 matrix"]
- assert cstats.copy_constructions == 0
- # assert cstats.move_constructions >= 0 # Don't invoke any
- assert cstats.copy_assignments == 0
- assert cstats.move_assignments == 0
-
-
-@pytest.requires_numpy
def test_from_python():
with pytest.raises(RuntimeError) as excinfo:
Matrix(np.array([1, 2, 3])) # trying to assign a 1D array
@@ -55,3 +27,36 @@
# assert cstats.move_constructions >= 0 # Don't invoke any
assert cstats.copy_assignments == 0
assert cstats.move_assignments == 0
+
+
+# PyPy: Memory leak in the "np.array(m, copy=False)" call
+# https://bitbucket.org/pypy/pypy/issues/2444
+@pytest.unsupported_on_pypy
+@pytest.requires_numpy
+def test_to_python():
+ m = Matrix(5, 5)
+
+ assert m[2, 3] == 0
+ m[2, 3] = 4
+ assert m[2, 3] == 4
+
+ m2 = np.array(m, copy=False)
+ assert m2.shape == (5, 5)
+ assert abs(m2).sum() == 4
+ assert m2[2, 3] == 4
+ m2[2, 3] = 5
+ assert m2[2, 3] == 5
+
+ cstats = ConstructorStats.get(Matrix)
+ assert cstats.alive() == 1
+ del m
+ pytest.gc_collect()
+ assert cstats.alive() == 1
+ del m2 # holds an m reference
+ pytest.gc_collect()
+ assert cstats.alive() == 0
+ assert cstats.values() == ["5x5 matrix"]
+ assert cstats.copy_constructions == 0
+ # assert cstats.move_constructions >= 0 # Don't invoke any
+ assert cstats.copy_assignments == 0
+ assert cstats.move_assignments == 0
diff --git a/tests/test_issues.py b/tests/test_issues.py
index 2098ff8..e60b5ca 100644
--- a/tests/test_issues.py
+++ b/tests/test_issues.py
@@ -1,5 +1,4 @@
import pytest
-import gc
from pybind11_tests import ConstructorStats
@@ -55,7 +54,7 @@
el = ElementList()
for i in range(10):
el.add(ElementA(i))
- gc.collect()
+ pytest.gc_collect()
for i, v in enumerate(el.get()):
assert i == v.value()
@@ -130,13 +129,13 @@
assert c.b.a.as_base().value == 42
del c
- gc.collect()
+ pytest.gc_collect()
del a # Should't delete while abase is still alive
- gc.collect()
+ pytest.gc_collect()
assert abase.value == 42
del abase, b
- gc.collect()
+ pytest.gc_collect()
def test_move_fallback():
diff --git a/tests/test_keep_alive.py b/tests/test_keep_alive.py
index 0cef346..bfd7d40 100644
--- a/tests/test_keep_alive.py
+++ b/tests/test_keep_alive.py
@@ -1,4 +1,4 @@
-import gc
+import pytest
def test_keep_alive_argument(capture):
@@ -9,14 +9,14 @@
assert capture == "Allocating parent."
with capture:
p.addChild(Child())
- gc.collect()
+ pytest.gc_collect()
assert capture == """
Allocating child.
Releasing child.
"""
with capture:
del p
- gc.collect()
+ pytest.gc_collect()
assert capture == "Releasing parent."
with capture:
@@ -24,11 +24,11 @@
assert capture == "Allocating parent."
with capture:
p.addChildKeepAlive(Child())
- gc.collect()
+ pytest.gc_collect()
assert capture == "Allocating child."
with capture:
del p
- gc.collect()
+ pytest.gc_collect()
assert capture == """
Releasing parent.
Releasing child.
@@ -43,14 +43,14 @@
assert capture == "Allocating parent."
with capture:
p.returnChild()
- gc.collect()
+ pytest.gc_collect()
assert capture == """
Allocating child.
Releasing child.
"""
with capture:
del p
- gc.collect()
+ pytest.gc_collect()
assert capture == "Releasing parent."
with capture:
@@ -58,11 +58,11 @@
assert capture == "Allocating parent."
with capture:
p.returnChildKeepAlive()
- gc.collect()
+ pytest.gc_collect()
assert capture == "Allocating child."
with capture:
del p
- gc.collect()
+ pytest.gc_collect()
assert capture == """
Releasing parent.
Releasing child.
@@ -77,11 +77,11 @@
assert capture == "Allocating parent."
with capture:
p.returnNullChildKeepAliveChild()
- gc.collect()
+ pytest.gc_collect()
assert capture == ""
with capture:
del p
- gc.collect()
+ pytest.gc_collect()
assert capture == "Releasing parent."
with capture:
@@ -89,9 +89,9 @@
assert capture == "Allocating parent."
with capture:
p.returnNullChildKeepAliveParent()
- gc.collect()
+ pytest.gc_collect()
assert capture == ""
with capture:
del p
- gc.collect()
+ pytest.gc_collect()
assert capture == "Releasing parent."
diff --git a/tests/test_methods_and_attributes.cpp b/tests/test_methods_and_attributes.cpp
index 11fc900..824e743 100644
--- a/tests/test_methods_and_attributes.cpp
+++ b/tests/test_methods_and_attributes.cpp
@@ -134,10 +134,9 @@
.def("overloaded_const", static_cast<py::str (ExampleMandA::*)(float, int) const>(&ExampleMandA::overloaded))
#endif
.def("__str__", &ExampleMandA::toString)
- .def_readwrite("value", &ExampleMandA::value)
- ;
+ .def_readwrite("value", &ExampleMandA::value);
- py::class_<TestProperties>(m, "TestProperties")
+ py::class_<TestProperties>(m, "TestProperties", py::metaclass())
.def(py::init<>())
.def_readonly("def_readonly", &TestProperties::value)
.def_readwrite("def_readwrite", &TestProperties::value)
@@ -160,7 +159,7 @@
auto static_set2 = [](py::object, int v) { TestPropRVP::sv2.value = v; };
auto rvp_copy = py::return_value_policy::copy;
- py::class_<TestPropRVP>(m, "TestPropRVP")
+ py::class_<TestPropRVP>(m, "TestPropRVP", py::metaclass())
.def(py::init<>())
.def_property_readonly("ro_ref", &TestPropRVP::get1)
.def_property_readonly("ro_copy", &TestPropRVP::get2, rvp_copy)
diff --git a/tests/test_methods_and_attributes.py b/tests/test_methods_and_attributes.py
index 2b0f8d5..1502f77 100644
--- a/tests/test_methods_and_attributes.py
+++ b/tests/test_methods_and_attributes.py
@@ -125,10 +125,19 @@
instance = TestPropRVP()
o = instance.rvalue
assert o.value == 1
+
+
+def test_property_rvalue_policy_static():
+ """When returning an rvalue, the return value policy is automatically changed from
+ `reference(_internal)` to `move`. The following would not work otherwise.
+ """
+ from pybind11_tests import TestPropRVP
o = TestPropRVP.static_rvalue
assert o.value == 1
+# https://bitbucket.org/pypy/pypy/issues/2447
+@pytest.unsupported_on_pypy
def test_dynamic_attributes():
from pybind11_tests import DynamicClass, CppDerivedDynamicClass
diff --git a/tests/test_multiple_inheritance.cpp b/tests/test_multiple_inheritance.cpp
index 3cb12b6..c57cb85 100644
--- a/tests/test_multiple_inheritance.cpp
+++ b/tests/test_multiple_inheritance.cpp
@@ -10,7 +10,6 @@
#include "pybind11_tests.h"
-
struct Base1 {
Base1(int i) : i(i) { }
int foo() { return i; }
diff --git a/tests/test_multiple_inheritance.py b/tests/test_multiple_inheritance.py
index 581cf56..c10298d 100644
--- a/tests/test_multiple_inheritance.py
+++ b/tests/test_multiple_inheritance.py
@@ -1,5 +1,3 @@
-
-
def test_multiple_inheritance_cpp():
from pybind11_tests import MIType
diff --git a/tests/test_numpy_array.py b/tests/test_numpy_array.py
index 1c218a1..b96790c 100644
--- a/tests/test_numpy_array.py
+++ b/tests/test_numpy_array.py
@@ -1,5 +1,4 @@
import pytest
-import gc
with pytest.suppress(ImportError):
import numpy as np
@@ -220,7 +219,7 @@
ac_view_2 = ac.numpy_view()
assert np.all(ac_view_1 == np.array([1, 2], dtype=np.int32))
del ac
- gc.collect()
+ pytest.gc_collect()
assert capture == """
ArrayClass()
ArrayClass::numpy_view()
@@ -233,12 +232,14 @@
with capture:
del ac_view_1
del ac_view_2
- gc.collect()
+ pytest.gc_collect()
+ pytest.gc_collect()
assert capture == """
~ArrayClass()
"""
+@pytest.unsupported_on_pypy
@pytest.requires_numpy
def test_cast_numpy_int64_to_uint64():
from pybind11_tests.array import function_taking_uint64
diff --git a/tests/test_operator_overloading.py b/tests/test_operator_overloading.py
index e0d4239..02ccb96 100644
--- a/tests/test_operator_overloading.py
+++ b/tests/test_operator_overloading.py
@@ -1,4 +1,3 @@
-
def test_operator_overloading():
from pybind11_tests import Vector2, Vector, ConstructorStats
diff --git a/tests/test_pickling.py b/tests/test_pickling.py
index 5e62e1f..548c618 100644
--- a/tests/test_pickling.py
+++ b/tests/test_pickling.py
@@ -1,3 +1,5 @@
+import pytest
+
try:
import cPickle as pickle # Use cPickle on Python 2.7
except ImportError:
@@ -18,6 +20,7 @@
assert p2.extra2() == p.extra2()
+@pytest.unsupported_on_pypy
def test_roundtrip_with_dict():
from pybind11_tests import PickleableWithDict
diff --git a/tests/test_python_types.cpp b/tests/test_python_types.cpp
index ae77f82..e1598e9 100644
--- a/tests/test_python_types.cpp
+++ b/tests/test_python_types.cpp
@@ -185,7 +185,7 @@
test_initializer python_types([](py::module &m) {
/* No constructor is explicitly defined below. An exception is raised when
trying to construct it directly from Python */
- py::class_<ExamplePythonTypes>(m, "ExamplePythonTypes", "Example 2 documentation")
+ py::class_<ExamplePythonTypes>(m, "ExamplePythonTypes", "Example 2 documentation", py::metaclass())
.def("get_dict", &ExamplePythonTypes::get_dict, "Return a Python dictionary")
.def("get_dict_2", &ExamplePythonTypes::get_dict_2, "Return a C++ dictionary")
.def("get_list", &ExamplePythonTypes::get_list, "Return a Python list")
@@ -212,8 +212,7 @@
.def("test_print", &ExamplePythonTypes::test_print, "test the print function")
.def_static("new_instance", &ExamplePythonTypes::new_instance, "Return an instance")
.def_readwrite_static("value", &ExamplePythonTypes::value, "Static value member")
- .def_readonly_static("value2", &ExamplePythonTypes::value2, "Static value member (readonly)")
- ;
+ .def_readonly_static("value2", &ExamplePythonTypes::value2, "Static value member (readonly)");
m.def("test_print_function", []() {
py::print("Hello, World!");
diff --git a/tests/test_python_types.py b/tests/test_python_types.py
index 9fe1ef7..347abaa 100644
--- a/tests/test_python_types.py
+++ b/tests/test_python_types.py
@@ -132,8 +132,12 @@
assert cstats.alive() == 0
-def test_docs(doc):
+# PyPy does not seem to propagate the tp_docs field at the moment
+def test_class_docs(doc):
assert doc(ExamplePythonTypes) == "Example 2 documentation"
+
+
+def test_method_docs(doc):
assert doc(ExamplePythonTypes.get_dict) == """
get_dict(self: m.ExamplePythonTypes) -> dict
diff --git a/tests/test_virtual_functions.py b/tests/test_virtual_functions.py
index a9aecd6..b11c699 100644
--- a/tests/test_virtual_functions.py
+++ b/tests/test_virtual_functions.py
@@ -206,6 +206,9 @@
assert obj.say_everything() == "BT -7"
+# PyPy: Reference count > 1 causes call with noncopyable instance
+# to fail in ncv1.print_nc()
+@pytest.unsupported_on_pypy
@pytest.mark.skipif(not hasattr(pybind11_tests, 'NCVirt'),
reason="NCVirt test broken on ICPC")
def test_move_support():