diff options
Diffstat (limited to 'tools/missing_bindings')
-rw-r--r-- | tools/missing_bindings/config.py | 145 | ||||
-rw-r--r-- | tools/missing_bindings/main.py | 350 | ||||
-rw-r--r-- | tools/missing_bindings/requirements.txt | 14 |
3 files changed, 509 insertions, 0 deletions
diff --git a/tools/missing_bindings/config.py b/tools/missing_bindings/config.py new file mode 100644 index 000000000..66d843821 --- /dev/null +++ b/tools/missing_bindings/config.py @@ -0,0 +1,145 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +from __future__ import annotations + +modules_to_test = { + # 6.0 + 'QtCore': 'qtcore-module.html', + 'QtGui': 'qtgui-module.html', + 'QtNetwork': 'qtnetwork-module.html', + 'QtQml': 'qtqml-module.html', + 'QtQuick': 'qtquick-module.html', + 'QtQuickWidgets': 'qtquickwidgets-module.html', + # Broken in 6.5.0 + #'QtQuickControls2': 'qtquickcontrols-module.html', + 'QtSql': 'qtsql-module.html', + 'QtWidgets': 'qtwidgets-module.html', + 'QtConcurrent': 'qtconcurrent-module.html', + 'QtDBus': 'qtdbus-module.html', + 'QtHelp': 'qthelp-module.html', + 'QtOpenGL': 'qtopengl-module.html', + 'QtPrintSupport': 'qtprintsupport-module.html', + 'QtSvg': 'qtsvg-module.html', + 'QtSvgWidgets': 'qtsvgwidgets-module.html', + 'QtUiTools': 'qtuitools-module.html', + 'QtXml': 'qtxml-module.html', + 'QtTest': 'qttest-module.html', + 'Qt3DCore': 'qt3dcore-module.html', + 'Qt3DInput': 'qt3dinput-module.html', + 'Qt3DLogic': 'qt3dlogic-module.html', + 'Qt3DRender': 'qt3drender-module.html', + 'Qt3DAnimation': 'qt3danimation-module.html', + 'Qt3DExtras': 'qt3dextras-module.html', + 'QtNetworkAuth': 'qtnetworkauth-module.html', + 'QtStateMachine': 'qtstatemachine-module.html', + # 'QtCoAp' -- TODO + # 'QtMqtt' -- TODO + # 'QtOpcUA' -- TODO + + # 6.1 + 'QtScxml': 'qtscxml-module.html', + 'QtCharts': 'qtcharts-module.html', + 'QtDataVisualization': 'qtdatavisualization-module.html', + + # 6.2 + 'QtBluetooth': 'qtbluetooth-module.html', + 'QtPositioning': 'qtpositioning-module.html', + 'QtMultimedia': 'qtmultimedia-module.html', + 'QtRemoteObjects': 'qtremoteobjects-module.html', + 'QtSensors': 'qtsensors-module.html', + 'QtSerialPort': 'qtserialport-module.html', + 'QtWebChannel': 'qtwebchannel-module.html', + 'QtWebEngineCore': 'qtwebenginecore-module.html', + 'QtWebEngineQuick': 'qtwebenginequick-module.html', + 'QtWebEngineWidgets': 'qtwebenginewidgets-module.html', + 'QtWebSockets': 'qtwebsockets-module.html', + 'QtHttpServer': 'qthttpserver-module.html', + + # 6.3 + #'QtSpeech': 'qtspeech-module.html', + 'QtMultimediaWidgets': 'qtmultimediawidgets-module.html', + 'QtNfc': 'qtnfc-module.html', + 'QtQuick3D': 'qtquick3d-module.html', + + # 6.4 + 'QtPdf': 'qtpdf-module.html', # this include qtpdfwidgets + 'QtSpatialAudio': 'qtspatialaudio-module.html', + + # 6.5 + 'QtSerialBus': 'qtserialbus-module.html', + 'QtTextToSpeech': 'qttexttospeech-module.html', + 'QtLocation': 'qtlocation-module.html', + +} + +types_to_ignore = { + # QtCore + 'QFlag', + 'QFlags', + 'QGlobalStatic', + 'QDebug', + 'QDebugStateSaver', + 'QMetaObject.Connection', + 'QPointer', + 'QAssociativeIterable', + 'QSequentialIterable', + 'QStaticPlugin', + 'QChar', + 'QLatin1Char', + 'QHash', + 'QMultiHash', + 'QLinkedList', + 'QList', + 'QMap', + 'QMultiMap', + 'QMap.key_iterator', + 'QPair', + 'QQueue', + 'QScopedArrayPointer', + 'QScopedPointer', + 'QScopedValueRollback', + 'QMutableSetIterator', + 'QSet', + 'QSet.const_iterator', + 'QSet.iterator', + 'QExplicitlySharedDataPointer', + 'QSharedData', + 'QSharedDataPointer', + 'QEnableSharedFromThis', + 'QSharedPointer', + 'QWeakPointer', + 'QStack', + 'QLatin1String', + 'QString', + 'QStringRef', + 'QStringList', + 'QStringMatcher', + 'QVarLengthArray', + 'QVector', + 'QFutureIterator', + 'QHashIterator', + 'QMutableHashIterator', + 'QLinkedListIterator', + 'QMutableLinkedListIterator', + 'QListIterator', + 'QMutableListIterator', + 'QMapIterator', + 'QMutableMapIterator', + 'QSetIterator', + 'QMutableVectorIterator', + 'QVectorIterator', + # QtGui + 'QIconEnginePlugin', + 'QImageIOPlugin', + 'QGenericPlugin', + 'QGenericPluginFactory', + 'QGenericMatrix', + 'QOpenGLExtraFunctions', + # QtWidgets + 'QItemEditorCreator', + 'QStandardItemEditorCreator', + 'QStylePlugin', + # QtSql + 'QSqlDriverCreator', + 'QSqlDriverPlugin', +} diff --git a/tools/missing_bindings/main.py b/tools/missing_bindings/main.py new file mode 100644 index 000000000..fe637809e --- /dev/null +++ b/tools/missing_bindings/main.py @@ -0,0 +1,350 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +from __future__ import annotations + +# This script is used to generate a summary of missing types / classes +# which are present in C++ Qt6, but are missing in PySide6. +# +# Required packages: bs4 +# Installed via: pip install bs4 +# +# The script uses beautiful soup 4 to parse out the class names from +# the online Qt documentation. It then tries to import the types from +# PySide6. +# +# Example invocation of script: +# python missing_bindings.py --qt-version 6.3 -w all +# --qt-version - specify which version of qt documentation to load. +# -w - if PyQt6 is an installed package, check if the tested +# class also exists there. + +import argparse +import sys +from textwrap import dedent +from time import gmtime, strftime +from urllib import request +from pathlib import Path + +from bs4 import BeautifulSoup +from config import modules_to_test, types_to_ignore +import pandas as pd +import matplotlib.pyplot as plt + +qt_documentation_website_prefixes = { + "6.5": "https://doc.qt.io/qt-6/", + "dev": "https://doc-snapshots.qt.io/qt6-dev/", +} + + +def qt_version_to_doc_prefix(version): + if version in qt_documentation_website_prefixes: + return qt_documentation_website_prefixes[version] + else: + raise RuntimeError("The specified qt version is not supported") + + +def create_doc_url(module_doc_page_url, version): + return qt_version_to_doc_prefix(version) + module_doc_page_url + + +def get_parser(): + parser = argparse.ArgumentParser() + parser.add_argument( + "module", + default="all", + choices=list(modules_to_test.keys()).append("all"), + nargs="?", + type=str, + help="the Qt module for which to get the missing types", + ) + parser.add_argument( + "--qt-version", + "-v", + default="6.5", + choices=["6.5", "dev"], + type=str, + dest="version", + help="the Qt version to use to check for types", + ) + parser.add_argument( + "--which-missing", + "-w", + default="all", + choices=["all", "in-pyqt", "not-in-pyqt", "in-pyside-not-in-pyqt"], + type=str, + dest="which_missing", + help="Which missing types to show (all, or just those that are not present in PyQt)", + ) + parser.add_argument( + "--plot", + action="store_true", + help="Create module-wise bar plot comparisons for the missing bindings comparisons" + " between Qt, PySide6 and PyQt6", + ) + return parser + + +def wikilog(*pargs, **kw): + print(*pargs) + + computed_str = "".join(str(arg) for arg in pargs) + + style = "text" + if "style" in kw: + style = kw["style"] + + if style == "heading1": + computed_str = f"= {computed_str} =" + elif style == "heading5": + computed_str = f"===== {computed_str} =====" + elif style == "with_newline": + computed_str = f"{computed_str}\n" + elif style == "bold_colon": + computed_str = computed_str.replace(":", ":'''") + computed_str = f"{computed_str}'''\n" + elif style == "error": + computed_str = computed_str.strip("\n") + computed_str = f"''{computed_str}''\n" + elif style == "text_with_link": + computed_str = computed_str + elif style == "code": + computed_str = f" {computed_str}" + elif style == "end": + return + + print(computed_str, file=wiki_file) + + +if __name__ == "__main__": + parser = get_parser() + args = parser.parse_args() + + if hasattr(args, "module") and args.module != "all": + saved_value = modules_to_test[args.module] + modules_to_test.clear() + modules_to_test[args.module] = saved_value + + pyside_package_name = "PySide6" + pyqt_package_name = "PyQt6" + + data = {"module": [], "qt": [], "pyside": [], "pyqt": []} + total_missing_types_count = 0 + total_missing_types_count_compared_to_pyqt = 0 + total_missing_modules_count = 0 + total_missing_pyqt_types_count = 0 + total_missing_pyqt_modules_count = 0 + + wiki_file = open("missing_bindings_for_wiki_qt_io.txt", "w") + wiki_file.truncate() + + wikilog(f"PySide6 bindings for Qt {args.version}", style="heading1") + + wikilog( + f"Using Qt version {args.version} documentation to find public " + "API Qt types and test if the types are present in the PySide6 " + "package." + ) + + wikilog( + dedent( + """\ + Results are usually stored at + https://wiki.qt.io/PySide6_Missing_Bindings + so consider taking the contents of the generated + missing_bindings_for_wiki_qt_io.txt + file and updating the linked wiki page.""" + ), + style="end", + ) + + wikilog( + "Similar report:\n https://gist.github.com/ethanhs/6c626ca4e291f3682589699296377d3a", + style="text_with_link", + ) + + python_executable = Path(sys.executable).name or "" + command_line_arguments = " ".join(sys.argv) + report_date = strftime("%Y-%m-%d %H:%M:%S %Z", gmtime()) + + wikilog( + dedent( + f""" + This report was generated by running the following command: + {python_executable} {command_line_arguments} + on the following date: + {report_date} + """ + ) + ) + + for module_name in modules_to_test.keys(): + wikilog(module_name, style="heading5") + + url = create_doc_url(modules_to_test[module_name], args.version) + wikilog(f"Documentation link: {url}\n", style="text_with_link") + + # Import the tested module + try: + pyside_tested_module = getattr( + __import__(pyside_package_name, fromlist=[module_name]), module_name + ) + except Exception as e: + e_str = str(e).replace('"', "") + wikilog( + f"\nCould not load {pyside_package_name}.{module_name}. " + f"Received error: {e_str}. Skipping.\n", + style="error", + ) + total_missing_modules_count += 1 + continue + + try: + pyqt_module_name = module_name + + pyqt_tested_module = getattr( + __import__(pyqt_package_name, fromlist=[pyqt_module_name]), pyqt_module_name + ) + except Exception as e: + e_str = str(e).replace("'", "") + wikilog( + f"\nCould not load {pyqt_package_name}.{module_name} for comparison. " + f"Received error: {e_str}.\n", + style="error", + ) + total_missing_pyqt_modules_count += 1 + + # Get C++ class list from documentation page. + page = request.urlopen(url) + soup = BeautifulSoup(page, "html.parser") + + # Extract the Qt type names from the documentation classes table + links = soup.body.select(".annotated a") + types_on_html_page = [] + + for link in links: + link_text = link.text.replace("::", ".") + if link_text not in types_to_ignore: + types_on_html_page.append(link_text) + + total_qt_types = len(types_on_html_page) + wikilog(f"Number of types in {module_name}: {total_qt_types}", style="bold_colon") + + missing_pyside_types_count = 0 + missing_pyqt_types_count = 0 + missing_types_compared_to_pyqt = 0 + missing_types = [] + for qt_type in types_on_html_page: + is_present_in_pyqt = False + is_present_in_pyside = False + missing_type = None + + try: + pyqt_qualified_type = f"pyqt_tested_module.{qt_type}" + eval(pyqt_qualified_type) + is_present_in_pyqt = True + except Exception as e: + print(f"{type(e).__name__}: {e}") + missing_pyqt_types_count += 1 + total_missing_pyqt_types_count += 1 + + try: + pyside_qualified_type = f"pyside_tested_module.{qt_type}" + eval(pyside_qualified_type) + is_present_in_pyside = True + except Exception as e: + print("Failed eval-in pyside qualified types") + print(f"{type(e).__name__}: {e}") + missing_type = qt_type + missing_pyside_types_count += 1 + total_missing_types_count += 1 + + if is_present_in_pyqt: + missing_type = f"{missing_type} (is present in PyQt6)" + missing_types_compared_to_pyqt += 1 + total_missing_types_count_compared_to_pyqt += 1 + + # missing in PySide + if not is_present_in_pyside: + if args.which_missing == "all": + missing_types.append(missing_type) + message = f"Missing types in PySide (all) {module_name}:" + # missing in PySide and present in pyqt + elif args.which_missing == "in-pyqt" and is_present_in_pyqt: + missing_types.append(missing_type) + message = f"Missing types in PySide6 (but present in PyQt6) {module_name}:" + # missing in both PyQt and PySide + elif args.which_missing == "not-in-pyqt" and not is_present_in_pyqt: + missing_types.append(missing_type) + message = f"Missing types in PySide6 (also missing in PyQt6) {module_name}:" + elif ( + args.which_missing == "in-pyside-not-in-pyqt" + and not is_present_in_pyqt + ): + missing_types.append(qt_type) + message = f"Missing types in PyQt6 (but present in PySide6) {module_name}:" + + if len(missing_types) > 0: + wikilog(message, style="with_newline") + missing_types.sort() + for missing_type in missing_types: + wikilog(missing_type, style="code") + wikilog("") + + if args.which_missing != "in-pyside-not-in-pyqt": + missing_types_count = missing_pyside_types_count + else: + missing_types_count = missing_pyqt_types_count + + if args.plot: + total_pyside_types = total_qt_types - missing_pyside_types_count + total_pyqt_types = total_qt_types - missing_pyqt_types_count + data["module"].append(module_name) + data["qt"].append(total_qt_types) + data["pyside"].append(total_pyside_types) + data["pyqt"].append(total_pyqt_types) + + wikilog(f"Number of missing types: {missing_types_count}", style="bold_colon") + if len(missing_types) > 0 and args.which_missing != "in-pyside-not-in-pyqt": + wikilog( + "Number of missing types that are present in PyQt6: " + f"{missing_types_compared_to_pyqt}", + style="bold_colon", + ) + wikilog(f"End of missing types for {module_name}\n", style="end") + else: + wikilog("", style="end") + + if args.plot: + df = pd.DataFrame(data=data, columns=["module", "qt", "pyside", "pyqt"]) + df.set_index("module", inplace=True) + df.plot(kind="bar", title="Qt API Coverage plot") + plt.legend() + plt.xticks(rotation=45) + plt.ylabel("Types Count") + figure = plt.gcf() + figure.set_size_inches(32, 18) # set to full_screen + plt.savefig("missing_bindings_comparison_plot.png", bbox_inches='tight') + print(f"Plot saved in {Path.cwd() / 'missing_bindings_comparison_plot.png'}\n") + + wikilog("Summary", style="heading5") + + if args.which_missing != "in-pyside-not-in-pyqt": + wikilog(f"Total number of missing types: {total_missing_types_count}", style="bold_colon") + wikilog( + "Total number of missing types that are present in PyQt6: " + f"{total_missing_types_count_compared_to_pyqt}", + style="bold_colon", + ) + wikilog( + f"Total number of missing modules: {total_missing_modules_count}", style="bold_colon" + ) + else: + wikilog( + f"Total number of missing types in PyQt6: {total_missing_pyqt_types_count}", + style="bold_colon", + ) + wikilog( + f"Total number of missing modules in PyQt6: {total_missing_pyqt_modules_count}", + style="bold_colon", + ) + wiki_file.close() diff --git a/tools/missing_bindings/requirements.txt b/tools/missing_bindings/requirements.txt new file mode 100644 index 000000000..08aa0a024 --- /dev/null +++ b/tools/missing_bindings/requirements.txt @@ -0,0 +1,14 @@ +beautifulsoup4 +pandas +matplotlib + +# PySide +PySide6 + +# PyQt +PyQt6 +PyQt6-3D +PyQt6-Charts +PyQt6-DataVisualization +PyQt6-NetworkAuth +PyQt6-WebEngine |