diff options
author | Friedemann Kleint <Friedemann.Kleint@qt.io> | 2025-04-30 11:55:44 +0200 |
---|---|---|
committer | Friedemann Kleint <Friedemann.Kleint@qt.io> | 2025-08-14 13:46:46 +0200 |
commit | a62699da1428df57584f82720055585782a63d49 (patch) | |
tree | 61b876ffedfaf0b7c021dc4c7092f3e46d2d0994 /sources | |
parent | 00515141c15d1f8368c8b8fb7c1696a32421c2e1 (diff) |
Task-number: PYSIDE-3011
Change-Id: I64048d9263c529ccb41ee70eb6766f5e02507011
Reviewed-by: Cristian Maureira-Fredes <cristian.maureira-fredes@qt.io>
Diffstat (limited to 'sources')
-rw-r--r-- | sources/pyside6/PySide6/QtCore/CMakeLists.txt | 1 | ||||
-rw-r--r-- | sources/pyside6/PySide6/QtCore/typesystem_core_common.xml | 27 | ||||
-rw-r--r-- | sources/pyside6/PySide6/doc/qtcore.rst | 9 | ||||
-rw-r--r-- | sources/pyside6/PySide6/glue/qtcore.cpp | 249 | ||||
-rw-r--r-- | sources/pyside6/tests/QtCore/CMakeLists.txt | 1 | ||||
-rw-r--r-- | sources/pyside6/tests/QtCore/qrangemodel_test.py | 56 |
6 files changed, 343 insertions, 0 deletions
diff --git a/sources/pyside6/PySide6/QtCore/CMakeLists.txt b/sources/pyside6/PySide6/QtCore/CMakeLists.txt index 1a8ad2e5f..2f49b610e 100644 --- a/sources/pyside6/PySide6/QtCore/CMakeLists.txt +++ b/sources/pyside6/PySide6/QtCore/CMakeLists.txt @@ -79,6 +79,7 @@ ${QtCore_GEN_DIR}/qfileselector_wrapper.cpp ${QtCore_GEN_DIR}/qfilesystemwatcher_wrapper.cpp ${QtCore_GEN_DIR}/qfutureinterfacebase_wrapper.cpp ${QtCore_GEN_DIR}/qgenericargument_wrapper.cpp +${QtCore_GEN_DIR}/qrangemodel_wrapper.cpp ${QtCore_GEN_DIR}/qgenericreturnargument_wrapper.cpp ${QtCore_GEN_DIR}/qhashseed_wrapper.cpp ${QtCore_GEN_DIR}/qidentityproxymodel_wrapper.cpp diff --git a/sources/pyside6/PySide6/QtCore/typesystem_core_common.xml b/sources/pyside6/PySide6/QtCore/typesystem_core_common.xml index 54e63cb8f..935668c0d 100644 --- a/sources/pyside6/PySide6/QtCore/typesystem_core_common.xml +++ b/sources/pyside6/PySide6/QtCore/typesystem_core_common.xml @@ -1738,6 +1738,33 @@ <modify-function signature="endResetModel()" allow-thread="yes"/> </object-type> + <object-type name="QRangeModel" since="6.10"> + <extra-includes> + <include file-name="QtCore/qspan.h" location="global"/> + <include file-name="sbknumpycheck.h" location="global"/> + <include file-name="sbknumpyview.h" location="global"/> + <include file-name="pysidevariantutils.h" location="global"/> + <include file-name="vector" location="global"/> + </extra-includes> + <enum-type name="RowCategory"/> + <inject-code class="native" position="wrapper-declaration" + file="../glue/qtcore.cpp" snippet="qrangemodel-wrapper"/> + <inject-code class="native" position="beginning" + file="../glue/qtcore.cpp" snippet="qrangemodel-helper-functions"/> + <add-function signature="QRangeModel(PyArrayObject *@data@, QObject *@parent@ = nullptr)" + overload-number="0"> + <inject-documentation format="target" mode="append" file="../doc/qtcore.rst" + snippet="qrangemodel-numpy-constructor"/> + <inject-code file="../glue/qtcore.cpp" snippet="qrangemodel-numpy-constructor"/> + </add-function> + <add-function signature="QRangeModel(PySequence@list@, QObject *@parent@ = nullptr)" + overload-number="1"> + <inject-documentation format="target" mode="append" file="../doc/qtcore.rst" + snippet="qrangemodel-sequence-constructor"/> + <inject-code file="../glue/qtcore.cpp" snippet="qrangemodel-sequence-constructor"/> + </add-function> + </object-type> + <value-type name="QItemSelection"> <include file-name="QList" location="global"/> <!-- Expose operator==, != inherited from QList, which the parser does diff --git a/sources/pyside6/PySide6/doc/qtcore.rst b/sources/pyside6/PySide6/doc/qtcore.rst index b8d551e70..eb369ee7c 100644 --- a/sources/pyside6/PySide6/doc/qtcore.rst +++ b/sources/pyside6/PySide6/doc/qtcore.rst @@ -116,3 +116,12 @@ Example:: logging.debug("Test debug message") // @snippet qmessagelogger + +// @snippet qrangemodel-numpy-constructor +The function takes one-dimensional or two-dimensional numpy arrays of various +integer or float types to populate an editable QRangeModel. +// @snippet qrangemodel-numpy-constructor + +// @snippet qrangemodel-sequence-constructor +The function takes a sequence of of data to populate a read-only QRangeModel. +// @snippet qrangemodel-sequence-constructor diff --git a/sources/pyside6/PySide6/glue/qtcore.cpp b/sources/pyside6/PySide6/glue/qtcore.cpp index 95eff4b41..1e89dfe43 100644 --- a/sources/pyside6/PySide6/glue/qtcore.cpp +++ b/sources/pyside6/PySide6/glue/qtcore.cpp @@ -2247,3 +2247,252 @@ if (PySequence_Check(%PYARG_0) != 0 && PySequence_Size(%PYARG_0) == 2) { PyTuple_SetItem(%PYARG_0, 0, %CONVERTTOPYTHON[%RETURN_TYPE](%0)); PyTuple_SetItem(%PYARG_0, 1, %CONVERTTOPYTHON[qintptr](*result_out)); // @snippet return-native-eventfilter + + +// @snippet qrangemodel-wrapper +// Import the template constructors +using QRangeModel::QRangeModel; +// @snippet qrangemodel-wrapper + +// @snippet qrangemodel-helper-functions +template <class T> +static inline QSpan<T> createSpan(void *vData, Py_ssize_t size) +{ + auto *data = reinterpret_cast<T *>(vData); + return QSpan<T>{data, data + size}; +} + +// Simple 2d table range for creating a QRangeModel +// (potentially replaceable by a std::mdspan in C++ 23). +template <class T> +class TableRange +{ + struct TableData + { + T *data = nullptr; + qsizetype rowCount = -1; + qsizetype columCount = -1; + }; + +public: + explicit TableRange(void *data, qsizetype rowCount, qsizetype columCount) : + m_data{reinterpret_cast<T *>(data), rowCount, columCount} {} + + class Iterator + { + public: + using value_type = QSpan<T>; + using size_type = qsizetype; + using reference = value_type; + using pointer = value_type; + using difference_type = std::ptrdiff_t; + using iterator_category = std::random_access_iterator_tag; + + explicit Iterator(const TableData &data, size_type row) noexcept: + m_data(data), m_row(row) {} + + Iterator() = default; + + constexpr Iterator &operator++() noexcept + { + Q_ASSERT(m_row < m_data.rowCount); + ++m_row; + return *this; + } + + constexpr Iterator operator++(int) noexcept + { + Q_ASSERT(m_row < m_data.rowCount); + auto copy = *this; + ++m_row; + return copy; + } + + constexpr Iterator &operator--() noexcept + { + Q_ASSERT(m_row > 0); + --m_row; + return *this; + } + + constexpr Iterator operator--(int) noexcept + { + Q_ASSERT(m_row > 0); + auto copy = *this; + --m_row; + return copy; + } + + Iterator &operator+=(difference_type i) + { + const auto row = m_row + i; + Q_ASSERT(row >= 0 && row <= m_data.rowCount); + m_row = row; + return *this; + } + + Iterator &operator-=(difference_type i) + { + const auto row = m_row - i; + Q_ASSERT(row >= 0 && row <= m_data.rowCount); + m_row = row; + return *this; + } + + Iterator operator+(difference_type i) const + { + const auto row = m_row + i; + Q_ASSERT(row >= 0 && row <= m_data.rowCount); + return {m_data, row}; + } + + Iterator operator-(difference_type i) const + { + const auto row = m_row - i; + Q_ASSERT(row >= 0 && row <= m_data.rowCount); + return {m_data, row}; + } + + difference_type operator-(const Iterator &it) const { return m_row - it.m_row; } // std::distance + + reference operator*() const noexcept + { + auto *rowStart = m_data.data + m_row * m_data.columCount; + return {rowStart, rowStart + m_data.columCount}; + } + + [[nodiscard]] value_type operator[](difference_type i) const + { + auto *rowStart = m_data.data + (m_row + i) * m_data.columCount; + return {rowStart, rowStart + m_data.columCount}; + } + + private: + friend bool comparesEqual(const Iterator &lhs, const Iterator &rhs) noexcept + { + Q_ASSERT(lhs.m_data.data != nullptr); + Q_ASSERT(lhs.m_data.data == rhs.m_data.data); + return lhs.m_row == rhs.m_row; + } + + friend Qt::strong_ordering compareThreeWay(const Iterator &lhs, + const Iterator &rhs) noexcept + { + Q_ASSERT(lhs.m_data.data != nullptr); + Q_ASSERT(lhs.m_data.data == rhs.m_data.data); + return Qt::compareThreeWay(lhs.m_row, rhs.m_row); + } + + Q_DECLARE_STRONGLY_ORDERED(Iterator) + + TableData m_data; + size_type m_row = 0; + }; + + [[nodiscard]] Iterator begin() const { return Iterator(m_data, 0); } + [[nodiscard]] Iterator end() const { return Iterator(m_data, m_data.rowCount); } + +private: + TableData m_data; +}; + +template <class RangeModel> // QRangeModelWrapper +static RangeModel *createRangeModel(PyObject *in, QObject *parent) +{ + auto view = Shiboken::Numpy::View::fromPyObject(in); + if (!view) { + PyErr_SetString(PyExc_TypeError, "Invalid parameter or missing numpy support."); + return nullptr; + } + switch (view.ndim) { + case 1: { + const auto size = view.dimensions[0]; + switch (view.type) { + case Shiboken::Numpy::View::Int16: + return new RangeModel(createSpan<short>(view.data, size), parent); + case Shiboken::Numpy::View::Unsigned16: + return new RangeModel(createSpan<unsigned short>(view.data, size), parent); + case Shiboken::Numpy::View::Int: + return new RangeModel(createSpan<int>(view.data, size), parent); + case Shiboken::Numpy::View::Unsigned: + return new RangeModel(createSpan<unsigned>(view.data, size), parent); + case Shiboken::Numpy::View::Int64: + return new RangeModel(createSpan<int64_t>(view.data, size), parent); + case Shiboken::Numpy::View::Unsigned64: + return new RangeModel(createSpan<uint64_t>(view.data, size), parent); + case Shiboken::Numpy::View::Float: + return new RangeModel(createSpan<float>(view.data, size), parent); + case Shiboken::Numpy::View::Double: + return new RangeModel(createSpan<double>(view.data, size), parent); + default: + PyErr_SetString(PyExc_TypeError, "Unsupported data type for one-dimensional arrays."); + return nullptr; + } + } + break; + + case 2: { + const auto rows = view.dimensions[0]; + const auto columns = view.dimensions[1]; + switch (view.type) { + case Shiboken::Numpy::View::Int16: + return new RangeModel(TableRange<short>(view.data, rows, columns), parent); + case Shiboken::Numpy::View::Unsigned16: + return new RangeModel(TableRange<unsigned short>(view.data, rows, columns), parent); + case Shiboken::Numpy::View::Int: + return new RangeModel(TableRange<int>(view.data, rows, columns), parent); + case Shiboken::Numpy::View::Unsigned: + return new RangeModel(TableRange<unsigned>(view.data, rows, columns), parent); + case Shiboken::Numpy::View::Int64: + return new RangeModel(TableRange<int64_t>(view.data, rows, columns), parent); + case Shiboken::Numpy::View::Unsigned64: + return new RangeModel(TableRange<uint64_t>(view.data, rows, columns), parent); + case Shiboken::Numpy::View::Float: + return new RangeModel(TableRange<float>(view.data, rows, columns), parent); + case Shiboken::Numpy::View::Double: + return new RangeModel(TableRange<double>(view.data, rows, columns), parent); + default: + PyErr_SetString(PyExc_TypeError, "Unsupported data type for two-dimensional arrays."); + return nullptr; + } + } + break; + default: + PyErr_SetString(PyExc_TypeError, "Only one and two-dimensional arrays are supported."); + return nullptr; + } + return nullptr; +} + +static bool isVariantList(const QVariant &v) +{ + return v.typeId() == QMetaType::QVariantList; +}; +// @snippet qrangemodel-helper-functions + +// @snippet qrangemodel-numpy-constructor +auto *model = createRangeModel<%TYPE>(%PYARG_1, %2); +if (model == nullptr) + return -1; +%0 = model; +// @snippet qrangemodel-numpy-constructor + +// @snippet qrangemodel-sequence-constructor +const auto vlOptional = PySide::Variant::pyListToVariantList(%PYARG_1); +if (!vlOptional.has_value()) { + PyErr_SetString(PyExc_TypeError, "Unable convert input sequence."); + return -1; +} + +const QVariantList &vList = vlOptional.value(); +if (!vList.isEmpty() && std::all_of(vList.cbegin(), vList.cend(), isVariantList)) { + // Empirical: Transform QVariantList<QVariant(List)> -> QList<QVariantList> for a table + QList<QVariantList> variantTable; + variantTable.reserve(vList.size()); + for (const auto &rowV : vList) + variantTable.append(rowV.value<QVariantList>()); + %0 = new %TYPE(variantTable, %2); +} else { + %0 = new %TYPE(vList, %2); +} +// @snippet qrangemodel-sequence-constructor diff --git a/sources/pyside6/tests/QtCore/CMakeLists.txt b/sources/pyside6/tests/QtCore/CMakeLists.txt index 5ae2130bf..28db96160 100644 --- a/sources/pyside6/tests/QtCore/CMakeLists.txt +++ b/sources/pyside6/tests/QtCore/CMakeLists.txt @@ -79,6 +79,7 @@ PYSIDE_TEST(qfileinfo_test.py) PYSIDE_TEST(qfile_test.py) PYSIDE_TEST(qfileread_test.py) PYSIDE_TEST(qflags_test.py) +PYSIDE_TEST(qrangemodel_test.py) PYSIDE_TEST(qinstallmsghandler_test.py) PYSIDE_TEST(qiodevice_buffered_read_test.py) PYSIDE_TEST(qiopipe_test.py) diff --git a/sources/pyside6/tests/QtCore/qrangemodel_test.py b/sources/pyside6/tests/QtCore/qrangemodel_test.py new file mode 100644 index 000000000..b9319bd8f --- /dev/null +++ b/sources/pyside6/tests/QtCore/qrangemodel_test.py @@ -0,0 +1,56 @@ +# Copyright (C) 2025 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 +from __future__ import annotations + +import os +import sys +import unittest + +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths +init_test_paths(False) + +from PySide6.QtCore import QRangeModel + + +try: + import numpy as np + HAVE_NUMPY = True +except ModuleNotFoundError: + HAVE_NUMPY = False + + +class QRangeModelTest(unittest.TestCase): + + def test_pylist(self): + test_list = [1, 2, 3] + model = QRangeModel(test_list) + self.assertEqual(model.rowCount(), 3) + self.assertEqual(model.data(model.createIndex(2, 0)), 3) + + def test_pytable(self): + test_table = [[1, 2], [3, 4]] + model = QRangeModel(test_table) + self.assertEqual(model.rowCount(), 2) + self.assertEqual(model.columnCount(), 2) + self.assertEqual(model.data(model.createIndex(1, 1)), 4) + + @unittest.skipUnless(HAVE_NUMPY, "requires numpy") + def test_numpy_list(self): + test_array = np.array([1, 2, 3]) + model = QRangeModel(test_array) + self.assertEqual(model.rowCount(), 3) + self.assertEqual(model.data(model.createIndex(2, 0)), 3) + + @unittest.skipUnless(HAVE_NUMPY, "requires numpy") + def test_numpy_table(self): + test_table = np.array([[1, 2], [3, 4]]) + model = QRangeModel(test_table) + self.assertEqual(model.rowCount(), 2) + self.assertEqual(model.columnCount(), 2) + self.assertEqual(model.data(model.createIndex(1, 1)), 4) + + +if __name__ == '__main__': + unittest.main() |