diff --git a/src/mobase/mobase.cpp b/src/mobase/mobase.cpp index cf9d252..4c38c48 100644 --- a/src/mobase/mobase.cpp +++ b/src/mobase/mobase.cpp @@ -114,53 +114,6 @@ PYBIND11_MODULE(mobase, m) } }); - py::detail::type_caster t1; - py::detail::type_caster t2; - - m.def("testQStringList", [](QStringList const& list) { - QStringList res = list; - for (QString& value : res) { - value = value + "_CPP"; - } - return res; - }); - - m.def("testGuessedString", [](GuessedValue const& value) { - return std::make_tuple(value.operator const QString&(), value.variants()); - }); - - m.def("testQStringList", [](QStringList const& list) { - QStringList res = list; - for (QString& value : res) { - value = value + "_CPP"; - } - return res; - }); - - m.def("testQMap1", [](QMap const& map) { - QMap res; - for (auto it = map.begin(); it != map.end(); ++it) { - res[it.key()] = it.value().size(); - } - return res; - }); - - m.def("testQMap2", [](QMap const& map) { - QMap res; - for (auto it = map.begin(); it != map.end(); ++it) { - res[it.key()] = QString::number(it.value()); - } - return res; - }); - - m.def("testDateTime1", []() { - return QDateTime::fromString("2022-02-15T12:33:45", Qt::ISODate); - }); - - m.def("testDateTime2", [](QDateTime const& datetime) { - return datetime.toString(); - }); - m.def("testEnum0", []() { return Qt::GlobalColor::darkRed; }); diff --git a/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_sip.h b/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_sip.h index d1a29c0..6e5d265 100644 --- a/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_sip.h +++ b/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_sip.h @@ -174,8 +174,7 @@ namespace pybind11::detail::qt { return Py_None; } - if (policy == return_value_policy::take_ownership && - std::is_copy_constructible_v) { + if (policy == return_value_policy::take_ownership) { // ensure Python deletes the C++ component qt::sipAPI()->api_transfer_back(sipObj); } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 0d3ae26..c8bb5f8 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -8,7 +8,7 @@ else() set(UIBASE_PATH "${MO2_INSTALL_PATH}/bin/uibase.dll") endif() -add_custom_target(pytest EXCLUDE_FROM_ALL +add_custom_target(pytest ${CMAKE_COMMAND} -E env PYTHONPATH="${PYLIB_DIR}\\;$" "UIBASE_PATH=${UIBASE_PATH}" "QT_ROOT=${QT_ROOT}" @@ -22,7 +22,7 @@ mo2_python_pip_install(pytest add_dependencies(pytest mobase) -file(GLOB test_files "test_*.cpp") +file(GLOB test_files CONFIGURE_DEPENDS "test_*.cpp") foreach (test_file ${test_files}) get_filename_component(target ${test_file} NAME_WLE) string(REPLACE "test_" "" pymodule ${target}) diff --git a/tests/conftest.py b/tests/conftest.py index a630475..8af3abf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,16 @@ +# -*- encoding: utf-8 -*- + import os +import sys from pathlib import Path def pytest_configure(): + global app + os.add_dll_directory(Path(os.getenv("QT_ROOT")).joinpath("bin")) os.add_dll_directory(Path(os.getenv("UIBASE_PATH")).parent) + + from PyQt6.QtWidgets import QApplication + + app = QApplication(sys.argv) diff --git a/tests/test_qt.cpp b/tests/test_qt.cpp index afcf45d..564e147 100644 --- a/tests/test_qt.cpp +++ b/tests/test_qt.cpp @@ -2,6 +2,8 @@ #include +using namespace pybind11::literals; + PYBIND11_MODULE(qt, m) { // QString @@ -29,4 +31,29 @@ PYBIND11_MODULE(qt, m) m.def("qstringlist_at", [](QStringList const& values, int index) { return values.at(index); }); + + // QMap + m.def("qmap_to_length", [](QMap const& map) { + QMap res; + for (auto it = map.begin(); it != map.end(); ++it) { + res[it.key()] = it.value().size(); + } + return res; + }); + + // QDateTime + + m.def( + "datetime_from_string", + [](QString const& date, Qt::DateFormat format) { + return QDateTime::fromString(date, format); + }, + "string"_a, "format"_a = Qt::DateFormat::ISODate); + + m.def( + "datetime_to_string", + [](QDateTime const& datetime, Qt::DateFormat format) { + return datetime.toString(format); + }, + "datetime"_a, "format"_a = Qt::DateFormat::ISODate); } diff --git a/tests/test_qt.py b/tests/test_qt.py index a66cb9e..71b0771 100644 --- a/tests/test_qt.py +++ b/tests/test_qt.py @@ -1,4 +1,6 @@ import pytest +from PyQt6.QtCore import QDateTime, Qt +from PyQt6.QtWidgets import QWidget m = pytest.importorskip("mobase_tests.qt") @@ -21,3 +23,28 @@ def test_qstringlist(): assert m.qstringlist_at(["x", "y"], 0) == "x" assert m.qstringlist_at(["x", "y"], 1) == "y" + + +def test_qmap(): + assert m.qmap_to_length({"t1": "abc", "t2": "o", "t3": ""}) == { + "t1": 3, + "t2": 1, + "t3": 0, + } + + +def test_qdatetime(): + assert m.datetime_from_string("2022-03-01") == QDateTime(2022, 3, 1, 0, 0) + + date = QDateTime(1995, 5, 20, 0, 0) + assert ( + m.datetime_from_string( + date.toString(Qt.DateFormat.TextDate), Qt.DateFormat.TextDate + ) + == date + ) + + assert m.datetime_to_string(date) == "1995-05-20T00:00:00" + assert m.datetime_to_string(date, Qt.DateFormat.TextDate) == date.toString( + Qt.DateFormat.TextDate + ) diff --git a/tests/test_qt_widgets.cpp b/tests/test_qt_widgets.cpp new file mode 100644 index 0000000..245b3f2 --- /dev/null +++ b/tests/test_qt_widgets.cpp @@ -0,0 +1,87 @@ +#include "pybind11_qt/pybind11_qt.h" + +#include + +#include +#include + +#include + +namespace py = pybind11; +using namespace pybind11::literals; + +QMap s_Widgets; +QWidget* s_Parent; + +class CustomWidget : public QWidget { +public: + CustomWidget(QString const& name, QWidget* parent = nullptr) : QWidget(parent) + { + s_Widgets[name] = this; + setProperty("name", name); + } + + ~CustomWidget() { s_Widgets.remove(property("name").toString()); } +}; + +class PyCustomWidget : public CustomWidget { +public: + using CustomWidget::CustomWidget; + int heightForWidth(int value) const + { + PYBIND11_OVERRIDE(int, CustomWidget, heightForWidth, value); + } +}; + +PYBIND11_MODULE(qt_widgets, m) +{ + s_Parent = new QWidget(); + + py::class_> + pyCustomWidget(m, "CustomWidget"); + pyCustomWidget + .def(py::init(), "name"_a, "parent"_a = (QWidget*)nullptr) + .def("set_parent_cpp", [](CustomWidget* w) { + w->setParent(s_Parent); + }); + py::qt::add_qt_delegate(pyCustomWidget, "_widget"); + + m.def("is_alive", [](QString const& name) { + return s_Widgets.contains(name); + }); + + m.def("get", [](QString const& name) { + return s_Widgets.contains(name) ? s_Widgets[name] : nullptr; + }); + + m.def("set_parent", [](QWidget* widget) { + widget->setParent(s_Parent); + }); + + m.def("is_owned_cpp", [](QString const& name) { + return s_Widgets.contains(name) && s_Widgets[name]->parent() == s_Parent; + }); + + m.def("make_widget_own_cpp", [](QString const& name) -> QWidget* { + return new CustomWidget(name, s_Parent); + }); + + m.def( + "make_widget_own_py", + [](QString const& name) -> QWidget* { + return new CustomWidget(name); + }, + py::return_value_policy::take_ownership); + + // simply passing the widget gives the ownership of the Python object to C++ + m.def("send_to_cpp", [](QString const& name, QWidget* widget) { + widget->setProperty("name", name); + widget->setParent(s_Parent); + s_Widgets[name] = widget; + }); + + m.def("heightForWidth", [](QString const& name, int value) { + return s_Widgets.contains(name) ? s_Widgets[name]->heightForWidth(value) + : -1024; + }); +} diff --git a/tests/test_qt_widgets.py b/tests/test_qt_widgets.py new file mode 100644 index 0000000..110c438 --- /dev/null +++ b/tests/test_qt_widgets.py @@ -0,0 +1,63 @@ +import pytest +from PyQt6.QtWidgets import QWidget + +m = pytest.importorskip("mobase_tests.qt_widgets") + + +class PyWidget(QWidget): + def heightForWidth(self, value: int) -> int: + return value * 3 + 4 + + +class PyCustomWidget(m.CustomWidget): + def __init__(self, name: str): + super().__init__(name) + + def heightForWidth(self, value: int) -> int: + return value * 6 - 5 + + +def test_qt_widget(): + # own cpp + w = m.make_widget_own_cpp("w1") + assert m.is_alive("w1") + assert m.is_owned_cpp("w1") + + del w + assert m.is_alive("w1") + + # own py + w = m.make_widget_own_py("w2") + assert m.is_alive("w2") + assert not m.is_owned_cpp("w2") + + del w + assert not m.is_alive("w2") + + # transfer to C++ + w = PyWidget() + m.send_to_cpp("w3", w) + + # delete the reference w - this should NOT delete the underlying object since it + # was transferred to C++ + del w + assert m.is_alive("w3") + assert m.is_owned_cpp("w3") + + # if the Python object is dead (BAD!), this will crash horrible + assert m.heightForWidth("w3", 4) == 4 * 3 + 4 + + # CustomWidget as a qholder, so the construction itself transfers the ownership + # to C++ + w = PyCustomWidget("w4") + w.set_parent_cpp() + assert m.is_alive("w4") + assert m.heightForWidth("w4", 7) == 6 * 7 - 5 + assert w.heightForWidth(7) == 6 * 7 - 5 + + # can call function not defined and not bound through the delegate + assert not w.hasHeightForWidth() + + del w + assert m.is_alive("w4") + assert m.heightForWidth("w4", 7) == 6 * 7 - 5