diff options
Diffstat (limited to 'sources')
41 files changed, 431 insertions, 176 deletions
diff --git a/sources/pyside-tools/project.py b/sources/pyside-tools/project.py index 762e76f31..fbb740c32 100644 --- a/sources/pyside-tools/project.py +++ b/sources/pyside-tools/project.py @@ -104,13 +104,19 @@ class Project: """Return path and command for a file's artifact""" if file.suffix == ".ui": # Qt form files py_file = f"{file.parent}/ui_{file.stem}.py" - return [Path(py_file)], [UIC_CMD, os.fspath(file), "--rc-prefix", "-o", py_file] + cmd = [UIC_CMD] + cmd.extend(self.project.uic_options) + cmd.extend([os.fspath(file), "--rc-prefix", "-o", py_file]) + return [Path(py_file)], cmd if file.suffix == ".qrc": # Qt resources if not output_path: py_file = f"{file.parent}/rc_{file.stem}.py" else: py_file = str(output_path.resolve()) - return [Path(py_file)], [RCC_CMD, os.fspath(file), "-o", py_file] + cmd = [RCC_CMD] + cmd.extend(self.project.rcc_options) + cmd.extend([os.fspath(file), "-o", py_file]) + return [Path(py_file)], cmd # generate .qmltypes from sources with Qml decorators if file.suffix == ".py" and file in self._qml_module_sources: assert self._qml_module_dir diff --git a/sources/pyside-tools/project_lib/project_data.py b/sources/pyside-tools/project_lib/project_data.py index 9a219c957..928e79c7b 100644 --- a/sources/pyside-tools/project_lib/project_data.py +++ b/sources/pyside-tools/project_lib/project_data.py @@ -34,8 +34,10 @@ class ProjectData: self._python_files: list[Path] = [] # ui files self._ui_files: list[Path] = [] + self._uic_options: list[str] = [] # qrc files self._qrc_files: list[Path] = [] + self._rcc_options: list[str] = [] # ts files self._ts_files: list[Path] = [] @@ -53,6 +55,9 @@ class ProjectData: print(f"{error}", file=sys.stderr) sys.exit(1) + self._rcc_options = project_file_data.rcc_options + self._uic_options = project_file_data.uic_options + for f in project_file_data.files: file = Path(project_file.parent / f) if any(file.match(pattern) for pattern in PYPROJECT_FILE_PATTERNS): @@ -101,10 +106,18 @@ class ProjectData: return self._ui_files @property + def uic_options(self): + return self._uic_options + + @property def qrc_files(self): return self._qrc_files @property + def rcc_options(self): + return self._rcc_options + + @property def qml_files(self): return self._qml_files diff --git a/sources/pyside-tools/project_lib/pyproject_parse_result.py b/sources/pyside-tools/project_lib/pyproject_parse_result.py index 6a04bf5ce..4c3264b52 100644 --- a/sources/pyside-tools/project_lib/pyproject_parse_result.py +++ b/sources/pyside-tools/project_lib/pyproject_parse_result.py @@ -8,3 +8,5 @@ from pathlib import Path class PyProjectParseResult: errors: list[str] = field(default_factory=list) files: list[Path] = field(default_factory=list) + rcc_options: list[str] = field(default_factory=list) + uic_options: list[str] = field(default_factory=list) diff --git a/sources/pyside-tools/project_lib/pyproject_toml.py b/sources/pyside-tools/project_lib/pyproject_toml.py index 6da7b455e..bc5a0f69d 100644 --- a/sources/pyside-tools/project_lib/pyproject_toml.py +++ b/sources/pyside-tools/project_lib/pyproject_toml.py @@ -91,11 +91,18 @@ def parse_pyproject_toml(pyproject_toml_file: Path) -> PyProjectParseResult: result.errors.append(str(e)) return result - pyside_table = root_table.get("tool", {}).get("pyside6-project", {}) + tool_entry = root_table.get("tool", {}) + pyside_table = tool_entry.get("pyside6-project", {}) if not pyside_table: result.errors.append("Missing [tool.pyside6-project] table") return result + if rcc_table := tool_entry.get("pyside6-rcc", {}): + result.rcc_options = rcc_table.get("options", []) + + if uic_table := tool_entry.get("pyside6-uic", {}): + result.uic_options = uic_table.get("options", []) + files = pyside_table.get("files", []) if not isinstance(files, list): result.errors.append("Missing or invalid files list") diff --git a/sources/pyside6/PySide6/QtCore/typesystem_core_common.xml b/sources/pyside6/PySide6/QtCore/typesystem_core_common.xml index f001178cc..bf04e04ed 100644 --- a/sources/pyside6/PySide6/QtCore/typesystem_core_common.xml +++ b/sources/pyside6/PySide6/QtCore/typesystem_core_common.xml @@ -336,8 +336,6 @@ <add-conversion type="SbkObject" file="../glue/qtcore.cpp" snippet="conversion-sbkobject"/> <add-conversion type="PyDict" check="PyDict_CheckExact(%in)" file="../glue/qtcore.cpp" snippet="conversion-pydict"/> <add-conversion type="PyList" check="PyList_Check(%in)" file="../glue/qtcore.cpp" snippet="conversion-pylist"/> - <add-conversion type="PyTuple" check="PyTuple_CheckExact(%in)" - file="../glue/qtcore.cpp" snippet="conversion-pylist"/> <add-conversion type="PyObject" file="../glue/qtcore.cpp" snippet="conversion-pyobject"/> </target-to-native> </conversion-rule> diff --git a/sources/pyside6/doc/tools/pyside-project.rst b/sources/pyside6/doc/tools/pyside-project.rst index c6913f363..41b5bc9af 100644 --- a/sources/pyside6/doc/tools/pyside-project.rst +++ b/sources/pyside6/doc/tools/pyside-project.rst @@ -34,6 +34,17 @@ files are listed in the ``tool.pyside6-project`` table. For example: [tool.pyside6-project] files = ["main.py", "main_window.py"] +It is also possible to specify options for the :ref:`pyside6-rcc` and +:ref:`pyside6-uic` tools: + +.. code-block:: toml + + [tool.pyside6-rcc] + options = ["--compress-algo", "zlib"] + + [tool.pyside6-uic] + options = [" --star-imports"] + More information about the ``pyproject.toml`` file format can be found in `Python Packaging User Guide specification: "Writing your pyproject.toml"`_. diff --git a/sources/pyside6/doc/tutorials/datavisualize/add_chart.rst b/sources/pyside6/doc/tutorials/datavisualize/add_chart.rst index acffce40d..87e012021 100644 --- a/sources/pyside6/doc/tutorials/datavisualize/add_chart.rst +++ b/sources/pyside6/doc/tutorials/datavisualize/add_chart.rst @@ -5,17 +5,17 @@ Chapter 5 - Add a chart view ============================= A table is nice to present data, but a chart is even better. For this, you -need the QtCharts module that provides many types of plots and options to +need the QtGraphs module that provides many types of plots and options to graphically represent data. -The placeholder for a plot is a QChartView, and inside that Widget you can -place a QChart. As a first step, try including only this without any data to -plot. +The relevant class for a plot is the GraphsView QML type, to which axes and +data series can be added. As a first step, try including then without any data +to plot. Make the following highlighted changes to :code:`main_widget.py` from the -previous chapter to add a QChartView: +previous chapter to add a chart: .. literalinclude:: datavisualize5/main_widget.py :linenos: - :lines: 3- - :emphasize-lines: 2-3,6,22-36,47-49 + :lines: 5- + :emphasize-lines: 4,27-39,53 diff --git a/sources/pyside6/doc/tutorials/datavisualize/add_mainwindow.rst b/sources/pyside6/doc/tutorials/datavisualize/add_mainwindow.rst index ab5468cd6..9073e679a 100644 --- a/sources/pyside6/doc/tutorials/datavisualize/add_mainwindow.rst +++ b/sources/pyside6/doc/tutorials/datavisualize/add_mainwindow.rst @@ -4,7 +4,8 @@ Chapter 3 - Create an empty QMainWindow ========================================== -You can now think of presenting your data in a UI. A QMainWindow provides a +You can now think of presenting your data in a UI. A +class:`~PySide6.QtWidgets.QMainWindow` provides a convenient structure for GUI applications, such as a menu bar and status bar. The following image shows the layout that QMainWindow offers out-of-the box: @@ -24,12 +25,13 @@ the resolution you currently have. In the following snippet, you will see how window size is defined based on available screen width (80%) and height (70%). .. note:: You can achieve a similar structure using other Qt elements like - QMenuBar, QWidget, and QStatusBar. Refer the QMainWindow layout for + class:`~PySide6.QtWidgets.QMenuBar`, class:`~PySide6.QtWidgets.QWidget`, + and class:`~PySide6.QtWidgets.QStatusBar`. Refer the QMainWindow layout for guidance. .. literalinclude:: datavisualize3/main_window.py :language: python :linenos: - :lines: 4- + :lines: 5- Try running the script to see what output you get with it. diff --git a/sources/pyside6/doc/tutorials/datavisualize/add_tableview.rst b/sources/pyside6/doc/tutorials/datavisualize/add_tableview.rst index 5b7e5e735..b3041349c 100644 --- a/sources/pyside6/doc/tutorials/datavisualize/add_tableview.rst +++ b/sources/pyside6/doc/tutorials/datavisualize/add_tableview.rst @@ -8,21 +8,22 @@ Now that you have a QMainWindow, you can include a centralWidget to your interface. Usually, a QWidget is used to display data in most data-driven applications. Use a table view to display your data. -The first step is to add a horizontal layout with just a QTableView. You -can create a QTableView object and place it inside a QHBoxLayout. Once the +The first step is to add a horizontal layout with just a +class:`~PySide6.QtWidgets.QTableView`. You can create a QTableView object +and place it inside a class:`~PySide6.QtWidgets.QHBoxLayout`. Once the QWidget is properly built, pass the object to the QMainWindow as its central widget. Remember that a QTableView needs a model to display information. In this case, -you can use a QAbstractTableModel instance. +you can use a class:`~PySide6.QtCore.QAbstractTableModel` instance. .. note:: You could also use the default item model that comes with a - QTableWidget instead. QTableWidget is a convenience class that reduces - your codebase considerably as you don't need to implement a data model. - However, it's less flexible than a QTableView, as QTableWidget cannot be - used with just any data. For more insight about Qt's model-view framework, - refer to the - `Model View Programming <https://doc.qt.io/qt-5/model-view-programming.html>` + class:`~PySide6.QtWidgets.QTableWidget` instead. QTableWidget is a + convenience class that reduces your codebase considerably as you don't need + to implement a data model. However, it's less flexible than a QTableView, + as QTableWidget cannot be used with just any data. For more insight about + Qt's model-view framework, refer to the + `Model View Programming <https://doc.qt.io/qt-6/model-view-programming.html>` documentation. Implementing the model for your QTableView, allows you to: @@ -42,7 +43,7 @@ Here is a script that implements the CustomTableModel: .. literalinclude:: datavisualize4/table_model.py :language: python :linenos: - :lines: 3- + :lines: 5- Now, create a QWidget that has a QTableView, and connect it to your CustomTableModel. @@ -50,8 +51,8 @@ CustomTableModel. .. literalinclude:: datavisualize4/main_widget.py :language: python :linenos: - :emphasize-lines: 12-17 - :lines: 3- + :emphasize-lines: 12-12 + :lines: 5- You also need minor changes to the :code:`main_window.py` and :code:`main.py` from chapter 3 to include the Widget inside the @@ -62,11 +63,11 @@ In the following snippets you'll see those changes highlighted: .. literalinclude:: datavisualize4/main_window.py :language: python :linenos: - :lines: 3- - :emphasize-lines: 8,11 + :lines: 5- + :emphasize-lines: 9 .. literalinclude:: datavisualize4/main.py :language: python :linenos: - :lines: 3- - :emphasize-lines: 46-47 + :lines: 5- + :emphasize-lines: 45-46 diff --git a/sources/pyside6/doc/tutorials/datavisualize/datavisualize3/datavisualize3.pyproject b/sources/pyside6/doc/tutorials/datavisualize/datavisualize3/datavisualize3.pyproject new file mode 100644 index 000000000..1bd31f959 --- /dev/null +++ b/sources/pyside6/doc/tutorials/datavisualize/datavisualize3/datavisualize3.pyproject @@ -0,0 +1,3 @@ +{ + "files": ["main.py", "main_window.py"] +} diff --git a/sources/pyside6/doc/tutorials/datavisualize/datavisualize3/main_window.py b/sources/pyside6/doc/tutorials/datavisualize/datavisualize3/main_window.py index 6ee8fa61b..79a4afd36 100644 --- a/sources/pyside6/doc/tutorials/datavisualize/datavisualize3/main_window.py +++ b/sources/pyside6/doc/tutorials/datavisualize/datavisualize3/main_window.py @@ -2,25 +2,22 @@ # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause from __future__ import annotations -from PySide6.QtGui import QAction, QKeySequence +from PySide6.QtGui import QIcon, QKeySequence from PySide6.QtWidgets import QMainWindow class MainWindow(QMainWindow): def __init__(self): - QMainWindow.__init__(self) + super().__init__() self.setWindowTitle("Eartquakes information") # Menu self.menu = self.menuBar() - self.file_menu = self.menu.addMenu("File") + file_menu = self.menu.addMenu("File") # Exit QAction - exit_action = QAction("Exit", self) - exit_action.setShortcut(QKeySequence.Quit) - exit_action.triggered.connect(self.close) - - self.file_menu.addAction(exit_action) + file_menu.addAction(QIcon.fromTheme(QIcon.ThemeIcon.ApplicationExit), + "Exit", QKeySequence.StandardKey.Quit, self.close) # Status Bar self.status = self.statusBar() diff --git a/sources/pyside6/doc/tutorials/datavisualize/datavisualize4/datavisualize4.pyproject b/sources/pyside6/doc/tutorials/datavisualize/datavisualize4/datavisualize4.pyproject new file mode 100644 index 000000000..f54969728 --- /dev/null +++ b/sources/pyside6/doc/tutorials/datavisualize/datavisualize4/datavisualize4.pyproject @@ -0,0 +1,3 @@ +{ + "files": ["main.py", "main_widget.py", "main_window.py", "table_model.py"] +} diff --git a/sources/pyside6/doc/tutorials/datavisualize/datavisualize4/main_widget.py b/sources/pyside6/doc/tutorials/datavisualize/datavisualize4/main_widget.py index 85e24833f..5d8e6ade3 100644 --- a/sources/pyside6/doc/tutorials/datavisualize/datavisualize4/main_widget.py +++ b/sources/pyside6/doc/tutorials/datavisualize/datavisualize4/main_widget.py @@ -10,7 +10,7 @@ from table_model import CustomTableModel class Widget(QWidget): def __init__(self, data): - QWidget.__init__(self) + super().__init__() # Getting the Model self.model = CustomTableModel(data) @@ -22,13 +22,13 @@ class Widget(QWidget): # QTableView Headers self.horizontal_header = self.table_view.horizontalHeader() self.vertical_header = self.table_view.verticalHeader() - self.horizontal_header.setSectionResizeMode(QHeaderView.ResizeToContents) - self.vertical_header.setSectionResizeMode(QHeaderView.ResizeToContents) + self.horizontal_header.setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents) + self.vertical_header.setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents) self.horizontal_header.setStretchLastSection(True) # QWidget Layout self.main_layout = QHBoxLayout() - size = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) + size = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Preferred) # Left layout size.setHorizontalStretch(1) diff --git a/sources/pyside6/doc/tutorials/datavisualize/datavisualize4/main_window.py b/sources/pyside6/doc/tutorials/datavisualize/datavisualize4/main_window.py index ded7fdf5c..600af6503 100644 --- a/sources/pyside6/doc/tutorials/datavisualize/datavisualize4/main_window.py +++ b/sources/pyside6/doc/tutorials/datavisualize/datavisualize4/main_window.py @@ -2,25 +2,22 @@ # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause from __future__ import annotations -from PySide6.QtGui import QAction, QKeySequence +from PySide6.QtGui import QIcon, QKeySequence from PySide6.QtWidgets import QMainWindow class MainWindow(QMainWindow): def __init__(self, widget): - QMainWindow.__init__(self) + super().__init__() self.setWindowTitle("Eartquakes information") self.setCentralWidget(widget) # Menu self.menu = self.menuBar() - self.file_menu = self.menu.addMenu("File") + file_menu = self.menu.addMenu("File") # Exit QAction - exit_action = QAction("Exit", self) - exit_action.setShortcut(QKeySequence.Quit) - exit_action.triggered.connect(self.close) - - self.file_menu.addAction(exit_action) + file_menu.addAction(QIcon.fromTheme(QIcon.ThemeIcon.ApplicationExit), + "Exit", QKeySequence.StandardKey.Quit, self.close) # Status Bar self.status = self.statusBar() diff --git a/sources/pyside6/doc/tutorials/datavisualize/datavisualize4/table_model.py b/sources/pyside6/doc/tutorials/datavisualize/datavisualize4/table_model.py index cc2ac12ab..9a2871c22 100644 --- a/sources/pyside6/doc/tutorials/datavisualize/datavisualize4/table_model.py +++ b/sources/pyside6/doc/tutorials/datavisualize/datavisualize4/table_model.py @@ -25,27 +25,27 @@ class CustomTableModel(QAbstractTableModel): return self.column_count def headerData(self, section, orientation, role): - if role != Qt.DisplayRole: + if role != Qt.ItemDataRole.DisplayRole: return None - if orientation == Qt.Horizontal: + if orientation == Qt.Orientation.Horizontal: return ("Date", "Magnitude")[section] else: return f"{section}" - def data(self, index, role=Qt.DisplayRole): + def data(self, index, role=Qt.ItemDataRole.DisplayRole): column = index.column() row = index.row() - if role == Qt.DisplayRole: + if role == Qt.ItemDataRole.DisplayRole: if column == 0: date = self.input_dates[row].toPython() return str(date)[:-3] elif column == 1: magnitude = self.input_magnitudes[row] return f"{magnitude:.2f}" - elif role == Qt.BackgroundRole: - return QColor(Qt.white) - elif role == Qt.TextAlignmentRole: - return Qt.AlignRight + elif role == Qt.ItemDataRole.BackgroundRole: + return QColor(Qt.GlobalColor.white) + elif role == Qt.ItemDataRole.TextAlignmentRole: + return Qt.AlignmentFlag.AlignRight return None diff --git a/sources/pyside6/doc/tutorials/datavisualize/datavisualize5/datavisualize5.pyproject b/sources/pyside6/doc/tutorials/datavisualize/datavisualize5/datavisualize5.pyproject new file mode 100644 index 000000000..f54969728 --- /dev/null +++ b/sources/pyside6/doc/tutorials/datavisualize/datavisualize5/datavisualize5.pyproject @@ -0,0 +1,3 @@ +{ + "files": ["main.py", "main_widget.py", "main_window.py", "table_model.py"] +} diff --git a/sources/pyside6/doc/tutorials/datavisualize/datavisualize5/main_widget.py b/sources/pyside6/doc/tutorials/datavisualize/datavisualize5/main_widget.py index 77ea4e776..fca09b059 100644 --- a/sources/pyside6/doc/tutorials/datavisualize/datavisualize5/main_widget.py +++ b/sources/pyside6/doc/tutorials/datavisualize/datavisualize5/main_widget.py @@ -2,17 +2,17 @@ # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause from __future__ import annotations -from PySide6.QtGui import QPainter from PySide6.QtWidgets import (QWidget, QHeaderView, QHBoxLayout, QTableView, QSizePolicy) -from PySide6.QtCharts import QChart, QChartView +from PySide6.QtQuickWidgets import QQuickWidget +from PySide6.QtGraphs import QLineSeries, QDateTimeAxis, QValueAxis, QGraphsTheme from table_model import CustomTableModel class Widget(QWidget): def __init__(self, data): - QWidget.__init__(self) + super().__init__() # Getting the Model self.model = CustomTableModel(data) @@ -24,21 +24,27 @@ class Widget(QWidget): # QTableView Headers self.horizontal_header = self.table_view.horizontalHeader() self.vertical_header = self.table_view.verticalHeader() - self.horizontal_header.setSectionResizeMode(QHeaderView.ResizeToContents) - self.vertical_header.setSectionResizeMode(QHeaderView.ResizeToContents) + self.horizontal_header.setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents) + self.vertical_header.setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents) self.horizontal_header.setStretchLastSection(True) - # Creating QChart - self.chart = QChart() - self.chart.setAnimationOptions(QChart.AllAnimations) - - # Creating QChartView - self.chart_view = QChartView(self.chart) - self.chart_view.setRenderHint(QPainter.Antialiasing) + # Create QGraphView via QML + self.series = QLineSeries() + self.axis_x = QDateTimeAxis() + self.axis_y = QValueAxis() + self.quick_widget = QQuickWidget(self) + self.quick_widget.setResizeMode(QQuickWidget.ResizeMode.SizeRootObjectToView) + self.theme = QGraphsTheme() + initial_properties = {"theme": self.theme, + "axisX": self.axis_x, + "axisY": self.axis_y, + "seriesList": self.series} + self.quick_widget.setInitialProperties(initial_properties) + self.quick_widget.loadFromModule("QtGraphs", "GraphsView") # QWidget Layout - self.main_layout = QHBoxLayout() - size = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) + self.main_layout = QHBoxLayout(self) + size = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Preferred) # Left layout size.setHorizontalStretch(1) @@ -47,8 +53,5 @@ class Widget(QWidget): # Right Layout size.setHorizontalStretch(4) - self.chart_view.setSizePolicy(size) - self.main_layout.addWidget(self.chart_view) - - # Set the layout to the QWidget - self.setLayout(self.main_layout) + self.quick_widget.setSizePolicy(size) + self.main_layout.addWidget(self.quick_widget) diff --git a/sources/pyside6/doc/tutorials/datavisualize/datavisualize5/main_window.py b/sources/pyside6/doc/tutorials/datavisualize/datavisualize5/main_window.py index ded7fdf5c..600af6503 100644 --- a/sources/pyside6/doc/tutorials/datavisualize/datavisualize5/main_window.py +++ b/sources/pyside6/doc/tutorials/datavisualize/datavisualize5/main_window.py @@ -2,25 +2,22 @@ # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause from __future__ import annotations -from PySide6.QtGui import QAction, QKeySequence +from PySide6.QtGui import QIcon, QKeySequence from PySide6.QtWidgets import QMainWindow class MainWindow(QMainWindow): def __init__(self, widget): - QMainWindow.__init__(self) + super().__init__() self.setWindowTitle("Eartquakes information") self.setCentralWidget(widget) # Menu self.menu = self.menuBar() - self.file_menu = self.menu.addMenu("File") + file_menu = self.menu.addMenu("File") # Exit QAction - exit_action = QAction("Exit", self) - exit_action.setShortcut(QKeySequence.Quit) - exit_action.triggered.connect(self.close) - - self.file_menu.addAction(exit_action) + file_menu.addAction(QIcon.fromTheme(QIcon.ThemeIcon.ApplicationExit), + "Exit", QKeySequence.StandardKey.Quit, self.close) # Status Bar self.status = self.statusBar() diff --git a/sources/pyside6/doc/tutorials/datavisualize/datavisualize5/table_model.py b/sources/pyside6/doc/tutorials/datavisualize/datavisualize5/table_model.py index cc2ac12ab..9a2871c22 100644 --- a/sources/pyside6/doc/tutorials/datavisualize/datavisualize5/table_model.py +++ b/sources/pyside6/doc/tutorials/datavisualize/datavisualize5/table_model.py @@ -25,27 +25,27 @@ class CustomTableModel(QAbstractTableModel): return self.column_count def headerData(self, section, orientation, role): - if role != Qt.DisplayRole: + if role != Qt.ItemDataRole.DisplayRole: return None - if orientation == Qt.Horizontal: + if orientation == Qt.Orientation.Horizontal: return ("Date", "Magnitude")[section] else: return f"{section}" - def data(self, index, role=Qt.DisplayRole): + def data(self, index, role=Qt.ItemDataRole.DisplayRole): column = index.column() row = index.row() - if role == Qt.DisplayRole: + if role == Qt.ItemDataRole.DisplayRole: if column == 0: date = self.input_dates[row].toPython() return str(date)[:-3] elif column == 1: magnitude = self.input_magnitudes[row] return f"{magnitude:.2f}" - elif role == Qt.BackgroundRole: - return QColor(Qt.white) - elif role == Qt.TextAlignmentRole: - return Qt.AlignRight + elif role == Qt.ItemDataRole.BackgroundRole: + return QColor(Qt.GlobalColor.white) + elif role == Qt.ItemDataRole.TextAlignmentRole: + return Qt.AlignmentFlag.AlignRight return None diff --git a/sources/pyside6/doc/tutorials/datavisualize/datavisualize6/datavisualize6.pyproject b/sources/pyside6/doc/tutorials/datavisualize/datavisualize6/datavisualize6.pyproject new file mode 100644 index 000000000..f54969728 --- /dev/null +++ b/sources/pyside6/doc/tutorials/datavisualize/datavisualize6/datavisualize6.pyproject @@ -0,0 +1,3 @@ +{ + "files": ["main.py", "main_widget.py", "main_window.py", "table_model.py"] +} diff --git a/sources/pyside6/doc/tutorials/datavisualize/datavisualize6/main_widget.py b/sources/pyside6/doc/tutorials/datavisualize/datavisualize6/main_widget.py index f987689ea..336afacd8 100644 --- a/sources/pyside6/doc/tutorials/datavisualize/datavisualize6/main_widget.py +++ b/sources/pyside6/doc/tutorials/datavisualize/datavisualize6/main_widget.py @@ -2,18 +2,20 @@ # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause from __future__ import annotations -from PySide6.QtCore import QDateTime, Qt -from PySide6.QtGui import QPainter +from math import floor, ceil + +from PySide6.QtCore import QDateTime, QTime, QTimeZone from PySide6.QtWidgets import (QWidget, QHeaderView, QHBoxLayout, QTableView, QSizePolicy) -from PySide6.QtCharts import QChart, QChartView, QLineSeries, QDateTimeAxis, QValueAxis +from PySide6.QtQuickWidgets import QQuickWidget +from PySide6.QtGraphs import QLineSeries, QDateTimeAxis, QValueAxis, QGraphsTheme from table_model import CustomTableModel class Widget(QWidget): def __init__(self, data): - QWidget.__init__(self) + super().__init__() # Getting the Model self.model = CustomTableModel(data) @@ -23,25 +25,29 @@ class Widget(QWidget): self.table_view.setModel(self.model) # QTableView Headers - resize = QHeaderView.ResizeToContents + resize = QHeaderView.ResizeMode.ResizeToContents self.horizontal_header = self.table_view.horizontalHeader() self.vertical_header = self.table_view.verticalHeader() self.horizontal_header.setSectionResizeMode(resize) self.vertical_header.setSectionResizeMode(resize) self.horizontal_header.setStretchLastSection(True) - # Creating QChart - self.chart = QChart() - self.chart.setAnimationOptions(QChart.AllAnimations) - self.add_series("Magnitude (Column 1)", [0, 1]) - - # Creating QChartView - self.chart_view = QChartView(self.chart) - self.chart_view.setRenderHint(QPainter.Antialiasing) + # Create QGraphView via QML + self.populate_series() + self.quick_widget = QQuickWidget(self) + self.quick_widget.setResizeMode(QQuickWidget.ResizeMode.SizeRootObjectToView) + self.theme = QGraphsTheme() + self.theme.setTheme(QGraphsTheme.Theme.BlueSeries) + initial_properties = {"theme": self.theme, + "axisX": self.axis_x, + "axisY": self.axis_y, + "seriesList": self.series} + self.quick_widget.setInitialProperties(initial_properties) + self.quick_widget.loadFromModule("QtGraphs", "GraphsView") # QWidget Layout - self.main_layout = QHBoxLayout() - size = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) + self.main_layout = QHBoxLayout(self) + size = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Preferred) # Left layout size.setHorizontalStretch(1) @@ -50,46 +56,51 @@ class Widget(QWidget): # Right Layout size.setHorizontalStretch(4) - self.chart_view.setSizePolicy(size) - self.main_layout.addWidget(self.chart_view) + self.quick_widget.setSizePolicy(size) + self.main_layout.addWidget(self.quick_widget) - # Set the layout to the QWidget - self.setLayout(self.main_layout) + def populate_series(self): + def seconds(qtime: QTime): + return qtime.minute() * 60 + qtime.second() - def add_series(self, name, columns): - # Create QLineSeries self.series = QLineSeries() - self.series.setName(name) + self.series.setName("Magnitude (Column 1)") # Filling QLineSeries + time_min = QDateTime(2100, 1, 1, 0, 0, 0) + time_max = QDateTime(1970, 1, 1, 0, 0, 0) + time_zone = QTimeZone(QTimeZone.Initialization.UTC) + y_min = 1e37 + y_max = -1e37 + date_fmt = "yyyy-MM-dd HH:mm:ss.zzz" for i in range(self.model.rowCount()): - # Getting the data t = self.model.index(i, 0).data() - date_fmt = "yyyy-MM-dd HH:mm:ss.zzz" - - x = QDateTime().fromString(t, date_fmt).toSecsSinceEpoch() + time = QDateTime.fromString(t, date_fmt) + time.setTimeZone(time_zone) y = float(self.model.index(i, 1).data()) - - if x > 0 and y > 0: - self.series.append(x, y) - - self.chart.addSeries(self.series) + if time.isValid() and y > 0: + if time > time_max: + time_max = time + if time < time_min: + time_min = time + if y > y_max: + y_max = y + if y < y_min: + y_min = y + self.series.append(time.toMSecsSinceEpoch(), y) # Setting X-axis self.axis_x = QDateTimeAxis() - self.axis_x.setTickCount(10) - self.axis_x.setFormat("dd.MM (h:mm)") + self.axis_x.setLabelFormat("dd.MM (h:mm)") self.axis_x.setTitleText("Date") - self.chart.addAxis(self.axis_x, Qt.AlignBottom) - self.series.attachAxis(self.axis_x) + self.axis_x.setMin(time_min.addSecs(-seconds(time_min.time()))) + self.axis_x.setMax(time_max.addSecs(3600 - seconds(time_max.time()))) + self.series.setAxisX(self.axis_x) + # Setting Y-axis self.axis_y = QValueAxis() - self.axis_y.setTickCount(10) self.axis_y.setLabelFormat("%.2f") self.axis_y.setTitleText("Magnitude") - self.chart.addAxis(self.axis_y, Qt.AlignLeft) - self.series.attachAxis(self.axis_y) - - # Getting the color from the QChart to use it on the QTableView - color_name = self.series.pen().color().name() - self.model.color = f"{color_name}" + self.axis_y.setMin(floor(y_min)) + self.axis_y.setMax(ceil(y_max)) + self.series.setAxisY(self.axis_y) diff --git a/sources/pyside6/doc/tutorials/datavisualize/datavisualize6/main_window.py b/sources/pyside6/doc/tutorials/datavisualize/datavisualize6/main_window.py index f37268df8..6a9eaea8e 100644 --- a/sources/pyside6/doc/tutorials/datavisualize/datavisualize6/main_window.py +++ b/sources/pyside6/doc/tutorials/datavisualize/datavisualize6/main_window.py @@ -2,26 +2,22 @@ # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause from __future__ import annotations -from PySide6.QtGui import QAction, QKeySequence +from PySide6.QtGui import QIcon, QKeySequence from PySide6.QtWidgets import QMainWindow class MainWindow(QMainWindow): def __init__(self, widget): - QMainWindow.__init__(self) + super().__init__() self.setWindowTitle("Eartquakes information") # Menu self.menu = self.menuBar() - self.file_menu = self.menu.addMenu("File") + file_menu = self.menu.addMenu("File") # Exit QAction - exit_action = QAction("Exit", self) - exit_action.setShortcut(QKeySequence.Quit) - exit_action.triggered.connect(self.close) - - self.file_menu.addAction(exit_action) - + file_menu.addAction(QIcon.fromTheme(QIcon.ThemeIcon.ApplicationExit), + "Exit", QKeySequence.StandardKey.Quit, self.close) # Status Bar self.status = self.statusBar() self.status.showMessage("Data loaded and plotted") diff --git a/sources/pyside6/doc/tutorials/datavisualize/datavisualize6/table_model.py b/sources/pyside6/doc/tutorials/datavisualize/datavisualize6/table_model.py index 3201e5887..9a2871c22 100644 --- a/sources/pyside6/doc/tutorials/datavisualize/datavisualize6/table_model.py +++ b/sources/pyside6/doc/tutorials/datavisualize/datavisualize6/table_model.py @@ -9,7 +9,6 @@ from PySide6.QtGui import QColor class CustomTableModel(QAbstractTableModel): def __init__(self, data=None): QAbstractTableModel.__init__(self) - self.color = None self.load_data(data) def load_data(self, data): @@ -26,27 +25,27 @@ class CustomTableModel(QAbstractTableModel): return self.column_count def headerData(self, section, orientation, role): - if role != Qt.DisplayRole: + if role != Qt.ItemDataRole.DisplayRole: return None - if orientation == Qt.Horizontal: + if orientation == Qt.Orientation.Horizontal: return ("Date", "Magnitude")[section] else: return f"{section}" - def data(self, index, role=Qt.DisplayRole): + def data(self, index, role=Qt.ItemDataRole.DisplayRole): column = index.column() row = index.row() - if role == Qt.DisplayRole: + if role == Qt.ItemDataRole.DisplayRole: if column == 0: date = self.input_dates[row].toPython() return str(date)[:-3] elif column == 1: magnitude = self.input_magnitudes[row] return f"{magnitude:.2f}" - elif role == Qt.BackgroundRole: - return (QColor(Qt.white), QColor(self.color))[column] - elif role == Qt.TextAlignmentRole: - return Qt.AlignRight + elif role == Qt.ItemDataRole.BackgroundRole: + return QColor(Qt.GlobalColor.white) + elif role == Qt.ItemDataRole.TextAlignmentRole: + return Qt.AlignmentFlag.AlignRight return None diff --git a/sources/pyside6/doc/tutorials/datavisualize/filter_data.rst b/sources/pyside6/doc/tutorials/datavisualize/filter_data.rst index bef134e5b..4edde69c1 100644 --- a/sources/pyside6/doc/tutorials/datavisualize/filter_data.rst +++ b/sources/pyside6/doc/tutorials/datavisualize/filter_data.rst @@ -17,7 +17,8 @@ be done by filtering the data that follows the condition, "magnitude > 0", to avoid faulty data or unexpected behavior. The Date column provides data in UTC format (for example, -2018-12-11T21:14:44.682Z), so you could easily map it to a QDateTime object +2018-12-11T21:14:44.682Z), so you could easily map it to a +class:`~PySide6.QtCore.QDateTime` object defining the structure of the string. Additionally, you can adapt the time based on the timezone you are in, using QTimeZone. @@ -26,7 +27,7 @@ The following script filters and formats the CSV data as described earlier: .. literalinclude:: datavisualize2/main.py :language: python :linenos: - :lines: 3- + :lines: 5- -Now that you have a tuple of QDateTime and float data, try improving the +Now that you have a tuple of ``QDateTime`` and float data, try improving the output further. That's what you'll learn in the following chapters. diff --git a/sources/pyside6/doc/tutorials/datavisualize/plot_datapoints.rst b/sources/pyside6/doc/tutorials/datavisualize/plot_datapoints.rst index e4374e861..47d12a7c4 100644 --- a/sources/pyside6/doc/tutorials/datavisualize/plot_datapoints.rst +++ b/sources/pyside6/doc/tutorials/datavisualize/plot_datapoints.rst @@ -1,23 +1,23 @@ .. _tutorial_plot_datapoints: -Chapter 6 - Plot the data in the ChartView +Chapter 6 - Plot the data in the GraphsView =========================================== -The last step of this tutorial is to plot the CSV data inside our QChart. For -this, you need to go over our data and include the data on a QLineSeries. +The last step of this tutorial is to plot the CSV data inside our GraphsView. +For this, you need to go over our data and include the data on a QLineSeries. After adding the data to the series, you can modify the axis to properly display the QDateTime on the X-axis, and the magnitude values on the Y-axis. Here is the updated :code:`main_widget.py` that includes an additional -function to plot data using a QLineSeries: +function to plot data using a :class:`~PySide6.QtGraphs.QLineSeries`: .. literalinclude:: datavisualize6/main_widget.py :language: python :linenos: - :lines: 3- - :emphasize-lines: 33,56-91 + :lines: 5- + :emphasize-lines: 31-42, 68-102 Now, run the application to visualize the earthquake magnitudes data at different times. diff --git a/sources/pyside6/doc/tutorials/datavisualize/read_data.rst b/sources/pyside6/doc/tutorials/datavisualize/read_data.rst index 8be0e1c2f..d083a05ee 100644 --- a/sources/pyside6/doc/tutorials/datavisualize/read_data.rst +++ b/sources/pyside6/doc/tutorials/datavisualize/read_data.rst @@ -21,7 +21,7 @@ The following python script, :code:`main.py`, demonstrates how to do it: .. literalinclude:: datavisualize1/main.py :language: python :linenos: - :lines: 3- + :lines: 5- The Python script uses the :code:`argparse` module to accept and parse input from the command line. It then uses the input, which in this case is the filename, diff --git a/sources/pyside6/tests/QtCore/qobject_property_test.py b/sources/pyside6/tests/QtCore/qobject_property_test.py index 9d2bd2c56..e0a8044fe 100644 --- a/sources/pyside6/tests/QtCore/qobject_property_test.py +++ b/sources/pyside6/tests/QtCore/qobject_property_test.py @@ -106,9 +106,10 @@ class QObjectWithOtherClassPropertyTest(unittest.TestCase): class VariantPropertyTest(unittest.TestCase): - """Test QVariant conversion in properties and signals (PYSIDE-3206, PYSIDE-3244). - It uses a property of list type that is passed a QVariantList - with various element types when using QObject.setProperty().""" + """Test QVariant conversion in properties and signals (PYSIDE-3256, + PYSIDE-3244, PYSIDE-3206 [open]). It uses a property of list type + that is passed a QVariantList with various element types when + using QObject.setProperty().""" def testIt(self): to = TestVariantPropertyObject() @@ -123,11 +124,11 @@ class VariantPropertyTest(unittest.TestCase): to.setProperty("testProperty", [{"key": 42}]) self.assertEqual(type(to.get_property()[0]), dict) - # PYSIDE-3206 (DBus): Convert a tuple to a list + # Tuple (PYSIDE-3256) to.setProperty("testProperty", [(1, 2)]) - self.assertEqual(type(to.get_property()[0]), list) + self.assertEqual(type(to.get_property()[0]), tuple) - # PYSIDE-324: The tuple conversion must not occur for named tuples + # Named Tuple (PYSIDE-3244) to.setProperty("testProperty", [Point(1, 2)]) self.assertEqual(type(to.get_property()[0]), Point) diff --git a/sources/shiboken6/doc/typesystem_specifying_types.rst b/sources/shiboken6/doc/typesystem_specifying_types.rst index 7fe4df392..7c3c1ae63 100644 --- a/sources/shiboken6/doc/typesystem_specifying_types.rst +++ b/sources/shiboken6/doc/typesystem_specifying_types.rst @@ -770,6 +770,7 @@ will be generated into specific modules. reset-method="..." instantiations="..." excluded-instantiations="..."/> + to-python="default" </typesystem> @@ -797,6 +798,24 @@ comma-separated list of types to be excluded from instantiating. Typically, this is used to exclude instantiations present in an underlying base module to prevent symbol clashes. +The *optional* **to-python** attribute specifies how a smart pointer +instance is converted to Python: + +.. list-table:: + :header-rows: 1 + + * - Value + + - Meaning + + * - ``default`` + + - A smart pointer instance is returned in all cases + + * - ``null-as-none`` + + - ``None`` is returned if the smart pointer is null. + The *optional* attribute **type** specifies the type: *shared* diff --git a/sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/lib/enum_sig.py b/sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/lib/enum_sig.py index 6f86df8c8..b31b161a3 100644 --- a/sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/lib/enum_sig.py +++ b/sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/lib/enum_sig.py @@ -53,6 +53,12 @@ def is_inconsistent_overload(signatures): return count != 0 and count != len(signatures) +def is_relevant_type(thing): + t = str(type(thing)) + return (("PySide" in t or "getset_descriptor" in t) + and "QMetaObject" not in t) + + class ExactEnumerator: """ ExactEnumerator enumerates all signatures in a module as they are. @@ -178,7 +184,9 @@ class ExactEnumerator: # Support attributes that have PySide types as values, # but we skip the 'staticMetaObject' that needs # to be defined at a QObject level. - elif "PySide" in str(type(thing)) and "QMetaObject" not in str(type(thing)): + # PYSIDE-3034: added public variables, extracted helper function to + # avoid repetitive calls of str(type(thing)) + elif is_relevant_type(thing): if class_name not in attributes: attributes[class_name] = {} attributes[class_name][thing_name] = thing diff --git a/sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/lib/pyi_generator.py b/sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/lib/pyi_generator.py index cdb4d9575..a84eb38dd 100644 --- a/sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/lib/pyi_generator.py +++ b/sources/shiboken6/shibokenmodule/files.dir/shibokensupport/signature/lib/pyi_generator.py @@ -32,6 +32,38 @@ from shibokensupport.signature.lib.tool import build_brace_pattern indent = " " * 4 +TYPE_MAP = { + # Qt integer types + "qint64": "int", + "qint32": "int", + "qint16": "int", + "qsizetype": "int", + "quint32": "int", + "quint64": "int", + "size_t": "int", + "uint": "int", + "ushort": "int", + "ulong": "int", + "unsigned char": "int", + "unsigned int": "int", + + # Qt floating types + "qreal": "float", + + # Qt string-like + "QString": "str", + "QStringList": "typing.List[str]", + "QChar": "str", + + # Qt containers (minimal) + "QList": "typing.List", + "QVariant": "typing.Any", + + # C strings + "char*": "str", + "const char*": "str", +} + class Writer: def __init__(self, outfile, *args): @@ -86,6 +118,29 @@ class Formatter(Writer): inspect.formatannotation = cls.backup return stringized + @classmethod + def normalize_type(cls, type_repr: str) -> str: + if not type_repr: + return "typing.Any" + if type_repr in {"void", "void*"}: + return "typing.Any" + if any(x in type_repr for x in ("QRhi", ".ComponentType", ".Semantic")): + return "int" + if ( " " in type_repr and + not any(x in type_repr for x in ("*", "::", "<", ">", "[", "]"))): + return "typing.Any" + if type_repr.startswith("QList["): + inner = type_repr[len("QList["):-1] + inner = cls.normalize_type(inner) + return f"typing.List[{inner}]" + if type_repr.startswith("QMap[") or type_repr.startswith("QHash["): + inner = type_repr[type_repr.find("[") + 1:-1] + key, value = map(str.strip, inner.split(",", 1)) + key = cls.normalize_type(key) + value = cls.normalize_type(value) + return f"typing.Dict[{key}, {value}]" + return TYPE_MAP.get(type_repr, type_repr) + # Adding a pattern to substitute "Union[T, NoneType]" by "Optional[T]" # I tried hard to replace typing.Optional by a simple override, but # this became _way_ too much. @@ -221,7 +276,12 @@ class Formatter(Writer): spaces = indent * self.level # PYSIDE-2903: Use a fully qualified name in the type comment. full_name = f"{type(attr_value).__module__}.{type(attr_value).__qualname__}" - self.print(f"{spaces}{attr_name:25} = ... # type: {full_name}") + if full_name == "builtins.getset_descriptor": + # PYSIDE-3034: Public variable types added to __doc__ + type_repr = self.normalize_type(attr_value.__doc__) + else: + type_repr = full_name + self.print(f"{spaces}{attr_name:25} = ... # type: {type_repr}") yield @contextmanager diff --git a/sources/shiboken6/tests/libsmart/smart.cpp b/sources/shiboken6/tests/libsmart/smart.cpp index 2273040f9..6dd4c3c6b 100644 --- a/sources/shiboken6/tests/libsmart/smart.cpp +++ b/sources/shiboken6/tests/libsmart/smart.cpp @@ -147,6 +147,18 @@ SharedPtr<const Integer> Obj::createSharedPtrConstInteger() return co; } +SharedPtr2<Integer> Obj::createNullSharedPtr2Integer() +{ + return {}; +} + +SharedPtr2<Integer> Obj::createSharedPtr2Integer(int value) +{ + auto *i = new Integer; + i->setValue(value); + return SharedPtr2<Integer>(i); +} + int Obj::takeSharedPtrToConstInteger(SharedPtr<const Integer> pInt) { return pInt->m_int; diff --git a/sources/shiboken6/tests/libsmart/smart_obj.h b/sources/shiboken6/tests/libsmart/smart_obj.h index 9f4f8425d..fceca0b6d 100644 --- a/sources/shiboken6/tests/libsmart/smart_obj.h +++ b/sources/shiboken6/tests/libsmart/smart_obj.h @@ -38,6 +38,9 @@ public: static SharedPtr<Integer> createSharedPtrInteger(int value); static SharedPtr<Integer> createNullSharedPtrInteger(); + static SharedPtr2<Integer> createNullSharedPtr2Integer(); + static SharedPtr2<Integer> createSharedPtr2Integer(int value); + int m_integer; // public for testing member field access. Integer *m_internalInteger; }; diff --git a/sources/shiboken6/tests/libsmart/smart_sharedptr.h b/sources/shiboken6/tests/libsmart/smart_sharedptr.h index dc665810a..7a77b3d6c 100644 --- a/sources/shiboken6/tests/libsmart/smart_sharedptr.h +++ b/sources/shiboken6/tests/libsmart/smart_sharedptr.h @@ -91,4 +91,10 @@ public: std::shared_ptr<T> mPtr; }; +template <class T> +class SharedPtr2 : public SharedPtr<T> { +public: + using SharedPtr<T>::SharedPtr; +}; + #endif // SMART_SHARED_PTR_H diff --git a/sources/shiboken6/tests/smartbinding/CMakeLists.txt b/sources/shiboken6/tests/smartbinding/CMakeLists.txt index 02c4e6596..5b3f4feda 100644 --- a/sources/shiboken6/tests/smartbinding/CMakeLists.txt +++ b/sources/shiboken6/tests/smartbinding/CMakeLists.txt @@ -13,6 +13,7 @@ ${CMAKE_CURRENT_BINARY_DIR}/smart/obj_wrapper.cpp ${CMAKE_CURRENT_BINARY_DIR}/smart/integer_wrapper.cpp ${CMAKE_CURRENT_BINARY_DIR}/smart/sharedptr_obj_wrapper.cpp ${CMAKE_CURRENT_BINARY_DIR}/smart/sharedptr_integer_wrapper.cpp +${CMAKE_CURRENT_BINARY_DIR}/smart/sharedptr2_integer_wrapper.cpp ${CMAKE_CURRENT_BINARY_DIR}/smart/registry_wrapper.cpp ${CMAKE_CURRENT_BINARY_DIR}/smart/smart_integer2_wrapper.cpp ${CMAKE_CURRENT_BINARY_DIR}/smart/sharedptr_integer2_wrapper.cpp diff --git a/sources/shiboken6/tests/smartbinding/smart_pointer_test.py b/sources/shiboken6/tests/smartbinding/smart_pointer_test.py index 64267fba7..761478df0 100644 --- a/sources/shiboken6/tests/smartbinding/smart_pointer_test.py +++ b/sources/shiboken6/tests/smartbinding/smart_pointer_test.py @@ -288,6 +288,14 @@ class SmartPointerTests(unittest.TestCase): o.takeSharedPtrToInteger(None) o.takeSharedPtrToIntegerByConstRef(None) + def testNoneConversion(self): + """PYSIDE-3253: SharedPtr2 is configured to convert to None.""" + valid_ptr = Obj.createSharedPtr2Integer(42) + null_ptr = Obj.createNullSharedPtr2Integer() + self.assertEqual(valid_ptr.value(), 42) + self.assertFalse(valid_ptr is None) + self.assertTrue(null_ptr is None) + def testConstruction(self): p1 = SharedPtr_Integer(integerFromValue(42)) self.assertEqual(p1.value(), 42) diff --git a/sources/shiboken6/tests/smartbinding/typesystem_smart.xml b/sources/shiboken6/tests/smartbinding/typesystem_smart.xml index 4024036fa..69c653286 100644 --- a/sources/shiboken6/tests/smartbinding/typesystem_smart.xml +++ b/sources/shiboken6/tests/smartbinding/typesystem_smart.xml @@ -13,6 +13,9 @@ <smart-pointer-type name="SharedPtr" type="shared" getter="data" ref-count-method="useCount" null-check-method="isNull" instantiations="Integer,Smart::Integer2=Test::SmartInteger2Ptr,Obj"/> + <smart-pointer-type name="SharedPtr2" type="shared" getter="data" ref-count-method="useCount" + null-check-method="isNull" to-python="null-as-none" + instantiations="Integer"/> <object-type name="Obj" /> <value-type name="Integer" /> diff --git a/sources/shiboken6_generator/ApiExtractor/smartpointertypeentry.h b/sources/shiboken6_generator/ApiExtractor/smartpointertypeentry.h index 7b67647b9..f3c4eef4e 100644 --- a/sources/shiboken6_generator/ApiExtractor/smartpointertypeentry.h +++ b/sources/shiboken6_generator/ApiExtractor/smartpointertypeentry.h @@ -50,6 +50,9 @@ public: void setExcludedInstantiations(const TypeEntryCList &ex); const TypeEntryCList &excludedInstantiations() const; + TypeSystem::SmartPointerToPythonConversion toPythonConversion() const; + void setToPythonConversion(TypeSystem::SmartPointerToPythonConversion c); + QString getTargetName(const AbstractMetaType &metaType) const; bool hasCustomConversion() const; diff --git a/sources/shiboken6_generator/ApiExtractor/typesystem.cpp b/sources/shiboken6_generator/ApiExtractor/typesystem.cpp index c02ec0c93..7938ce64a 100644 --- a/sources/shiboken6_generator/ApiExtractor/typesystem.cpp +++ b/sources/shiboken6_generator/ApiExtractor/typesystem.cpp @@ -2182,6 +2182,8 @@ public: TypeEntryCList m_excludedInstantiations; CustomConversionPtr m_customConversion; TypeSystem::SmartPointerType m_smartPointerType; + TypeSystem::SmartPointerToPythonConversion m_toPythonConversion = + TypeSystem::SmartPointerToPythonConversion::Default; }; qsizetype SmartPointerTypeEntryPrivate::instantiationIndex(const TypeEntryCPtr &t) const @@ -2293,6 +2295,18 @@ const TypeEntryCList &SmartPointerTypeEntry::excludedInstantiations() const return d->m_excludedInstantiations; } +TypeSystem::SmartPointerToPythonConversion SmartPointerTypeEntry::toPythonConversion() const +{ + S_D(const SmartPointerTypeEntry); + return d->m_toPythonConversion; +} + +void SmartPointerTypeEntry::setToPythonConversion(TypeSystem::SmartPointerToPythonConversion c) +{ + S_D(SmartPointerTypeEntry); + d->m_toPythonConversion = c; +} + SmartPointerTypeEntry::SmartPointerTypeEntry(SmartPointerTypeEntryPrivate *d) : ComplexTypeEntry(d) { diff --git a/sources/shiboken6_generator/ApiExtractor/typesystem_enums.h b/sources/shiboken6_generator/ApiExtractor/typesystem_enums.h index bb2b5cd6f..4237e1593 100644 --- a/sources/shiboken6_generator/ApiExtractor/typesystem_enums.h +++ b/sources/shiboken6_generator/ApiExtractor/typesystem_enums.h @@ -106,6 +106,11 @@ enum class SmartPointerType : std::uint8_t { ValueHandle }; +enum class SmartPointerToPythonConversion : std::uint8_t { + Default, + NullAsNone, +}; + enum class PythonEnumType : std::uint8_t { Unspecified, Enum, diff --git a/sources/shiboken6_generator/ApiExtractor/typesystemparser.cpp b/sources/shiboken6_generator/ApiExtractor/typesystemparser.cpp index a4dfe5e6c..fc591e6bb 100644 --- a/sources/shiboken6_generator/ApiExtractor/typesystemparser.cpp +++ b/sources/shiboken6_generator/ApiExtractor/typesystemparser.cpp @@ -50,6 +50,7 @@ constexpr auto allowThreadAttribute = "allow-thread"_L1; constexpr auto checkFunctionAttribute = "check-function"_L1; constexpr auto defaultConstructibleAttribute = "default-constructible"_L1; constexpr auto copyableAttribute = "copyable"_L1; +constexpr auto smartPointerToPythonConversionAttribute = "to-python"_L1; constexpr auto movableAttribute = "movable"_L1; constexpr auto accessAttribute = "access"_L1; constexpr auto actionAttribute = "action"_L1; @@ -425,6 +426,14 @@ ENUM_LOOKUP_BEGIN(TypeSystem::SmartPointerType, Qt::CaseSensitive, }; ENUM_LOOKUP_LINEAR_SEARCH +ENUM_LOOKUP_BEGIN(TypeSystem::SmartPointerToPythonConversion, Qt::CaseSensitive, + smartPointerToPythonConversionFromAttribute) +{ + {u"default", TypeSystem::SmartPointerToPythonConversion::Default}, + {u"null-as-none", TypeSystem::SmartPointerToPythonConversion::NullAsNone} +}; +ENUM_LOOKUP_LINEAR_SEARCH + template <class EnumType> static std::optional<EnumType> lookupHashElement(const QHash<QStringView, EnumType> &hash, @@ -1430,6 +1439,8 @@ SmartPointerTypeEntryPtr TypeDatabaseParserContext::SmartPointerEntry entry; QString instantiations; QString excludedInstantiations; + TypeSystem::SmartPointerToPythonConversion conversion = + TypeSystem::SmartPointerToPythonConversion::Default; for (auto i = attributes->size() - 1; i >= 0; --i) { const auto name = attributes->at(i).qualifiedName(); if (name == u"type") { @@ -1454,6 +1465,14 @@ SmartPointerTypeEntryPtr nullCheckMethod = attributes->takeAt(i).value().toString(); } else if (name == u"reset-method") { resetMethod = attributes->takeAt(i).value().toString(); + } else if (name == smartPointerToPythonConversionAttribute) { + const auto attribute = attributes->takeAt(i); + const auto convOpt = smartPointerToPythonConversionFromAttribute(attribute.value()); + if (!convOpt.has_value()) { + m_error = msgInvalidAttributeValue(attribute); + return nullptr; + } + conversion = convOpt.value(); } } @@ -1486,6 +1505,7 @@ SmartPointerTypeEntryPtr currentParentTypeEntry()); if (!applyComplexTypeAttributes(reader, type, attributes)) return nullptr; + type->setToPythonConversion(conversion); type->setNullCheckMethod(nullCheckMethod); type->setValueCheckMethod(valueCheckMethod); type->setResetMethod(resetMethod); diff --git a/sources/shiboken6_generator/generator/shiboken/cppgenerator.cpp b/sources/shiboken6_generator/generator/shiboken/cppgenerator.cpp index 4854ebf79..138ec8963 100644 --- a/sources/shiboken6_generator/generator/shiboken/cppgenerator.cpp +++ b/sources/shiboken6_generator/generator/shiboken/cppgenerator.cpp @@ -480,10 +480,17 @@ static QString BuildEnumFlagInfo(const AbstractMetaEnum &cppEnum) } static void writePyGetSetDefEntry(TextStream &s, const QString &name, - const QString &getFunc, const QString &setFunc) + const QString &getFunc, const QString &setFunc, const QString &doc={}) { - s << "{const_cast<char *>(\"" << mangleName(name) << "\"), " << getFunc << ", " - << (setFunc.isEmpty() ? NULL_PTR : setFunc) << ", nullptr, nullptr},\n"; + s << "{\"" << mangleName(name) << "\", " << getFunc << ", " + << (setFunc.isEmpty() ? NULL_PTR : setFunc) << ", "; + + if (doc.isEmpty()) + s << "nullptr"; + else + s << "\"" << doc << "\""; + + s << ", nullptr},\n"; } static bool generateRichComparison(const GeneratorContext &c) @@ -922,8 +929,9 @@ void CppGenerator::generateClass(TextStream &s, const QString setter = canGenerateSetter ? cpythonSetterFunctionName(metaField) : QString(); const auto names = metaField.definitionNames(); + const QString doc = metaField.type().pythonSignature(); for (const auto &name : names) - writePyGetSetDefEntry(s, name, getter, setter); + writePyGetSetDefEntry(s, name, getter, setter, doc); } } @@ -1769,6 +1777,19 @@ void CppGenerator::writeEnumConverterFunctions(TextStream &s, const AbstractMeta s << '\n'; } +static void writeSmartPointerNoneReturnCheck(TextStream &c, QAnyStringView varName, + const SmartPointerTypeEntryCPtr &ste) +{ + c << "if ("; + if (!ste->nullCheckMethod().isEmpty()) + c << varName << "->" << ste->nullCheckMethod() << "()"; + else if (!ste->valueCheckMethod().isEmpty()) + c << '!' << varName << "->" << ste->valueCheckMethod() << "()"; + else + c << "!*" << varName; + c << ")\n" << indent << "Py_RETURN_NONE;\n" << outdent; +} + void CppGenerator::writePointerToPythonConverter(TextStream &c, const GeneratorContext &context, const QString &cpythonType) @@ -1782,6 +1803,16 @@ void CppGenerator::writePointerToPythonConverter(TextStream &c, QString instanceCast = "auto *tCppIn = reinterpret_cast<const "_L1 + getFullTypeName(context) + " *>(cppIn);\n"_L1; + if (context.forSmartPointer()) { + auto ste = std::static_pointer_cast<const SmartPointerTypeEntry>(context.metaClass()->typeEntry()); + const auto toPythonConversion = ste->toPythonConversion(); + if (toPythonConversion == TypeSystem::SmartPointerToPythonConversion::NullAsNone) { + c << instanceCast; + writeSmartPointerNoneReturnCheck(c, "tCppIn", ste); + instanceCast.clear(); + } + } + const QString nameFunc = metaClass->typeEntry()->polymorphicNameFunction(); if (nameFunc.isEmpty() && !metaClass->hasVirtualDestructor()) { c << "return Shiboken::Object::newObjectWithHeuristics(" @@ -1871,6 +1902,14 @@ void CppGenerator::writeConverterFunctions(TextStream &s, const AbstractMetaClas } else { c << "auto *source = reinterpret_cast<const " << typeName << " *>(cppIn);\n"; } + + if (classContext.forSmartPointer()) { + auto ste = std::static_pointer_cast<const SmartPointerTypeEntry>(classContext.metaClass()->typeEntry()); + const auto toPythonConversion = ste->toPythonConversion(); + if (toPythonConversion == TypeSystem::SmartPointerToPythonConversion::NullAsNone) + writeSmartPointerNoneReturnCheck(c, "source", ste); + } + c << "return Shiboken::Object::newObject(" << cpythonType << ", new " << globalScopePrefix(classContext) << classContext.effectiveClassName() << '(' << (needsMove ? "std::move(*source)" : "*source") |
