diff options
Diffstat (limited to 'tools')
-rw-r--r-- | tools/create_changelog.py | 2 | ||||
-rw-r--r-- | tools/example_gallery/main.py | 225 | ||||
-rw-r--r-- | tools/missing_bindings.py | 456 | ||||
-rw-r--r-- | tools/missing_bindings/config.py | 173 | ||||
-rw-r--r-- | tools/missing_bindings/main.py | 330 | ||||
-rw-r--r-- | tools/missing_bindings/requirements.txt | 8 | ||||
-rw-r--r-- | tools/snippets_translate/README.md | 151 | ||||
-rw-r--r-- | tools/snippets_translate/converter.py | 334 | ||||
-rw-r--r-- | tools/snippets_translate/handlers.py | 519 | ||||
-rw-r--r-- | tools/snippets_translate/main.py | 466 | ||||
-rw-r--r-- | tools/snippets_translate/parse_utils.py | 145 | ||||
-rw-r--r-- | tools/snippets_translate/requirements.txt | 2 | ||||
-rw-r--r-- | tools/snippets_translate/snippets_translate.pyproject | 3 | ||||
-rw-r--r-- | tools/snippets_translate/tests/test_converter.py | 439 |
14 files changed, 2718 insertions, 535 deletions
diff --git a/tools/create_changelog.py b/tools/create_changelog.py index 914b87cc1..539df3fe2 100644 --- a/tools/create_changelog.py +++ b/tools/create_changelog.py @@ -64,7 +64,7 @@ information about a particular change. """ shiboken_header = """**************************************************************************** -* Shiboken2 * +* Shiboken6 * **************************************************************************** """ diff --git a/tools/example_gallery/main.py b/tools/example_gallery/main.py index afacab628..332f2a2f5 100644 --- a/tools/example_gallery/main.py +++ b/tools/example_gallery/main.py @@ -38,7 +38,7 @@ ############### -DESCRIPTION = """ +""" This tool reads all the examples from the main repository that have a '.pyproject' file, and generates a special table/gallery in the documentation page. @@ -51,21 +51,30 @@ since there is no special requirements. from argparse import ArgumentParser, RawTextHelpFormatter import json import math +import shutil from pathlib import Path from textwrap import dedent opt_quiet = False +suffixes = { + ".py": "py", + ".qml": "js", + ".conf": "ini", + ".qrc": "xml", + ".ui": "xml", + ".xbel": "xml", +} def ind(x): return " " * 4 * x -def get_colgroup(columns, indent=2): - width = 80 # percentage - width_column = width // columns - return f'{ind(indent)}<col style="width: {width_column}%" />\n' * columns +def get_lexer(suffix): + if suffix in suffixes: + return suffixes[suffix] + return "text" def add_indent(s, level): @@ -84,43 +93,39 @@ def get_module_gallery(examples): information, from one specific module. """ - gallery = dedent(f"""\ - <table class="special"> - <colgroup> -{get_colgroup(columns, indent=3)} - </colgroup> - """ + gallery = ( + ".. panels::\n" + f"{ind(1)}:container: container-lg pb-3\n" + f"{ind(1)}:column: col-lg-4 col-md-4 col-sm-6 col-xs-12 p-2\n\n" ) # Iteration per rows - for i in range(math.ceil(len(examples) / columns)): - gallery += f"{ind(1)}<tr>\n" - # Iteration per columns - for j in range(columns): - # We use a 'try-except' to handle when the examples are - # not an exact 'rows x columns', meaning that some cells - # will be empty. - try: - e = examples[i * columns + j] - url = e["rst"].replace(".rst", ".html") - name = e["example"] - underline = f'{e["module"]}' - if e["extra"]: - underline += f'/{e["extra"]}' - gallery += ( - f'{ind(2)}<td><a href="{url}"><p><strong>{name}</strong><br/>' - f"({underline})</p></a></td>\n" - ) - except IndexError: - # We use display:none to hide the cell - gallery += f'{ind(2)}<td style="display: none;"></td>\n' - gallery += f"{ind(1)}</tr>\n" - - gallery += dedent("""\ - </table> - """ - ) - return gallery + for i in range(math.ceil(len(examples))): + e = examples[i] + url = e["rst"].replace(".rst", ".html") + name = e["example"] + underline = f'{e["module"]}' + + + if e["extra"]: + underline += f'/{e["extra"]}' + + if i > 0: + gallery += f"{ind(1)}---\n" + elif e["img_doc"]: + gallery += f"{ind(1)}---\n" + + if e["img_doc"]: + gallery += f"{ind(1)}:img-top: {e['img_doc'].name}\n\n" + else: + gallery += "\n" + + + gallery += f"{ind(1)}`{name} <{url}>`_\n" + gallery += f"{ind(1)}+++\n" + gallery += f"{ind(1)}{underline}\n" + + return f"{gallery}\n" def remove_licenses(s): @@ -132,18 +137,54 @@ def remove_licenses(s): return "\n".join(new_s) +def get_code_tabs(files, project_file): + content = "\n" + + for i, project_file in enumerate(files): + pfile = Path(project_file) + if pfile.suffix in (".png", ".pyc"): + continue + + content += f".. tabbed:: {project_file}\n\n" + + lexer = get_lexer(pfile.suffix) + content += add_indent(f".. code-block:: {lexer}", 1) + content += "\n" + + _path = f_path.resolve().parents[0] / project_file + _file_content = "" + with open(_path, "r") as _f: + _file_content = remove_licenses(_f.read()) + + content += add_indent(_file_content, 2) + content += "\n\n" + return content + + +def get_header_title(f_path): + _title = f_path.stem + url_name = "/".join(f_path.parts[f_path.parts.index("examples")+1:-1]) + url = f"{BASE_URL}/{url_name}" + return ( + "..\n This file was auto-generated by the 'examples_gallery' " + "script.\n Any change will be lost!\n\n" + f"{_title}\n" + f"{'=' * len(_title)}\n\n" + f"(You can also check this code `in the repository <{url}>`_)\n\n" + ) + + if __name__ == "__main__": # Only examples with a '.pyproject' file will be listed. DIR = Path(__file__).parent - EXAMPLES_DOC = f"{DIR}/../../sources/pyside6/doc/examples" + EXAMPLES_DOC = Path(f"{DIR}/../../sources/pyside6/doc/examples") EXAMPLES_DIR = Path(f"{DIR}/../../examples/") + BASE_URL = "https://code.qt.io/cgit/pyside/pyside-setup.git/tree/examples" columns = 5 gallery = "" - parser = ArgumentParser(description=DESCRIPTION, - formatter_class=RawTextHelpFormatter) - parser.add_argument('--quiet', '-q', action='store_true', - help='Quiet') + parser = ArgumentParser(description=__doc__, formatter_class=RawTextHelpFormatter) + parser.add_argument("--quiet", "-q", action="store_true", help="Quiet") options = parser.parse_args() opt_quiet = options.quiet @@ -153,11 +194,16 @@ if __name__ == "__main__": # * Read the .pyproject file to output the content of each file # on the final .rst file for that specific example. examples = {} + + # Create the 'examples' directory if it doesn't exist + if not EXAMPLES_DOC.is_dir(): + EXAMPLES_DOC.mkdir() + for f_path in EXAMPLES_DIR.glob("**/*.pyproject"): if str(f_path).endswith("examples.pyproject"): continue - parts = f_path.parts[len(EXAMPLES_DIR.parts) : -1] + parts = f_path.parts[len(EXAMPLES_DIR.parts):-1] module_name = parts[0] example_name = parts[-1] @@ -166,6 +212,27 @@ if __name__ == "__main__": rst_file = f"example_{module_name}_{extra_names}_{example_name}.rst" + def check_img_ext(i): + EXT = (".png", ".jpg", ".jpeg") + if i.suffix in EXT: + return True + return False + + # Check for a 'doc' directory inside the example + has_doc = False + img_doc = None + original_doc_dir = Path(f_path.parent / "doc") + if original_doc_dir.is_dir(): + has_doc = True + images = [i for i in original_doc_dir.glob("*") if i.is_file() and check_img_ext(i)] + if len(images) > 0: + # We look for an image with the same example_name first, if not, we select the first + image_path = [i for i in images if example_name in str(i)] + if not image_path: + image_path = images[0] + else: + img_doc = image_path[0] + if module_name not in examples: examples[module_name] = [] @@ -176,6 +243,8 @@ if __name__ == "__main__": "extra": extra_names, "rst": rst_file, "abs_path": str(f_path), + "has_doc": has_doc, + "img_doc": img_doc, } ) @@ -184,32 +253,41 @@ if __name__ == "__main__": pyproject = json.load(pyf) if pyproject: - with open(f"{EXAMPLES_DOC}/{rst_file}", "w") as out_f: - content_f = ( - "..\n This file was auto-generated by the 'examples_gallery' " - "script.\n Any change will be lost!\n\n" - ) - for project_file in pyproject["files"]: - if project_file.split(".")[-1] in ("png", "pyc"): - continue - length = len(project_file) - content_f += f"{project_file}\n{'=' * length}\n\n::\n\n" - - _path = f_path.resolve().parents[0] / project_file - _content = "" - with open(_path, "r") as _f: - _content = remove_licenses(_f.read()) - - content_f += add_indent(_content, 1) - content_f += "\n\n" + rst_file_full = EXAMPLES_DOC / rst_file + + with open(rst_file_full, "w") as out_f: + if has_doc: + doc_path = Path(f_path.parent) / "doc" + doc_rst = doc_path / f"{example_name}.rst" + + with open(doc_rst) as doc_f: + content_f = doc_f.read() + + # Copy other files in the 'doc' directory, but + # excluding the main '.rst' file and all the + # directories. + for _f in doc_path.glob("*"): + if _f == doc_rst or _f.is_dir(): + continue + src = _f + dst = EXAMPLES_DOC / _f.name + + resource_written = shutil.copy(src, dst) + if not opt_quiet: + print("Written resource:", resource_written) + else: + content_f = get_header_title(f_path) + content_f += get_code_tabs(pyproject["files"], out_f) out_f.write(content_f) + if not opt_quiet: print(f"Written: {EXAMPLES_DOC}/{rst_file}") else: if not opt_quiet: print("Empty '.pyproject' file, skipping") - base_content = dedent("""\ + base_content = dedent( + """\ .. This file was auto-generated from the 'pyside-setup/tools/example_gallery' All editions in this file will be lost. @@ -220,21 +298,10 @@ if __name__ == "__main__": A collection of examples are provided with |project| to help new users to understand different use cases of the module. - .. toctree:: - :maxdepth: 1 - - tabbedbrowser.rst - ../pyside-examples/all-pyside-examples.rst - - Gallery - ------- - You can find all these examples inside the ``pyside-setup`` on the ``examples`` directory, or you can access them after installing |pymodname| from ``pip`` inside the ``site-packages/PySide6/examples`` directory. - .. raw:: html - """ ) @@ -243,7 +310,8 @@ if __name__ == "__main__": # for them will be able to, since they are indexed. # Notice that :hidden: will not add the list of files by the end of the # main examples HTML page. - footer_index = dedent("""\ + footer_index = dedent( + """\ .. toctree:: :hidden: :maxdepth: 1 @@ -258,8 +326,9 @@ if __name__ == "__main__": for module_name, e in sorted(examples.items()): for i in e: index_files.append(i["rst"]) - f.write(f"{ind(1)}<h3>{module_name.title()}</h3>\n") - f.write(add_indent(get_module_gallery(e), 1)) + f.write(f"{module_name.title()}\n") + f.write(f"{'*' * len(module_name.title())}\n") + f.write(get_module_gallery(e)) f.write("\n\n") f.write(footer_index) for i in index_files: diff --git a/tools/missing_bindings.py b/tools/missing_bindings.py deleted file mode 100644 index 63314c1ab..000000000 --- a/tools/missing_bindings.py +++ /dev/null @@ -1,456 +0,0 @@ -############################################################################# -## -## Copyright (C) 2017 The Qt Company Ltd. -## Contact: https://www.qt.io/licensing/ -## -## This file is part of Qt for Python. -## -## $QT_BEGIN_LICENSE:LGPL$ -## Commercial License Usage -## Licensees holding valid commercial Qt licenses may use this file in -## accordance with the commercial license agreement provided with the -## Software or, alternatively, in accordance with the terms contained in -## a written agreement between you and The Qt Company. For licensing terms -## and conditions see https://www.qt.io/terms-conditions. For further -## information use the contact form at https://www.qt.io/contact-us. -## -## GNU Lesser General Public License Usage -## Alternatively, this file may be used under the terms of the GNU Lesser -## General Public License version 3 as published by the Free Software -## Foundation and appearing in the file LICENSE.LGPL3 included in the -## packaging of this file. Please review the following information to -## ensure the GNU Lesser General Public License version 3 requirements -## will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -## -## GNU General Public License Usage -## Alternatively, this file may be used under the terms of the GNU -## General Public License version 2.0 or (at your option) the GNU General -## Public license version 3 or any later version approved by the KDE Free -## Qt Foundation. The licenses are as published by the Free Software -## Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -## included in the packaging of this file. Please review the following -## information to ensure the GNU General Public License requirements will -## be met: https://www.gnu.org/licenses/gpl-2.0.html and -## https://www.gnu.org/licenses/gpl-3.0.html. -## -## $QT_END_LICENSE$ -## -############################################################################# - -# This script is used to generate a summary of missing types / classes -# which are present in C++ Qt5, 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 5.9 -w all -# --qt-version - specify which version of qt documentation to load. -# -w - if PyQt5 is an installed package, check if the tested -# class also exists there. - -try: - import urllib.request as urllib2 -except ImportError: - import urllib2 - -import argparse -from bs4 import BeautifulSoup -from collections import OrderedDict -from time import gmtime, strftime -import sys -import os.path - -modules_to_test = OrderedDict() - -# Essentials -modules_to_test['QtCore'] = 'qtcore-module.html' -modules_to_test['QtGui'] = 'qtgui-module.html' -modules_to_test['QtMultimedia'] = 'qtmultimedia-module.html' -modules_to_test['QtMultimediaWidgets'] = 'qtmultimediawidgets-module.html' -modules_to_test['QtNetwork'] = 'qtnetwork-module.html' -modules_to_test['QtQml'] = 'qtqml-module.html' -modules_to_test['QtQuick'] = 'qtquick-module.html' -modules_to_test['QtQuickWidgets'] = 'qtquickwidgets-module.html' -modules_to_test['QtSql'] = 'qtsql-module.html' -modules_to_test['QtTest'] = 'qttest-module.html' -modules_to_test['QtWidgets'] = 'qtwidgets-module.html' - -# Addons -modules_to_test['Qt3DCore'] = 'qt3dcore-module.html' -modules_to_test['Qt3DInput'] = 'qt3dinput-module.html' -modules_to_test['Qt3DLogic'] = 'qt3dlogic-module.html' -modules_to_test['Qt3DRender'] = 'qt3drender-module.html' -modules_to_test['Qt3DAnimation'] = 'qt3danimation-module.html' -modules_to_test['Qt3DExtras'] = 'qt3dextras-module.html' -modules_to_test['QtConcurrent'] = 'qtconcurrent-module.html' -#modules_to_test['QtNetworkAuth'] = 'qtnetworkauth-module.html' -modules_to_test['QtHelp'] = 'qthelp-module.html' -modules_to_test['QtLocation'] = 'qtlocation-module.html' -modules_to_test['QtPrintSupport'] = 'qtprintsupport-module.html' -modules_to_test['QtScxml'] = 'qtscxml-module.html' -#modules_to_test['QtSpeech'] = 'qtspeech-module.html' -modules_to_test['QtSvg'] = 'qtsvg-module.html' -modules_to_test['QtUiTools'] = 'qtuitools-module.html' -modules_to_test['QtWebChannel'] = 'qtwebchannel-module.html' -modules_to_test['QtWebEngine'] = 'qtwebengine-module.html' -modules_to_test['QtWebEngineCore'] = 'qtwebenginecore-module.html' -modules_to_test['QtWebEngineWidgets'] = 'qtwebenginewidgets-module.html' -modules_to_test['QtWebSockets'] = 'qtwebsockets-module.html' -modules_to_test['QtMacExtras'] = 'qtmacextras-module.html' -modules_to_test['QtX11Extras'] = 'qtx11extras-module.html' -modules_to_test['QtWinExtras'] = 'qtwinextras-module.html' -modules_to_test['QtXml'] = 'qtxml-module.html' -modules_to_test['QtXmlPatterns'] = 'qtxmlpatterns-module.html' -modules_to_test['QtCharts'] = 'qtcharts-module.html' -modules_to_test['QtDataVisualization'] = 'qtdatavisualization-module.html' -modules_to_test['QtOpenGL'] = 'qtopengl-module.html' -modules_to_test['QtPositioning'] = 'qtpositioning-module.html' -modules_to_test['QtRemoteObjects'] = 'qtremoteobjects-module.html' -modules_to_test['QtScriptTools'] = 'qtscripttools-module.html' -modules_to_test['QtSensors'] = 'qtsensors-module.html' -modules_to_test['QtSerialPort'] = 'qtserialport-module.html' -types_to_ignore = set() -# QtCore -types_to_ignore.add('QFlag') -types_to_ignore.add('QFlags') -types_to_ignore.add('QGlobalStatic') -types_to_ignore.add('QDebug') -types_to_ignore.add('QDebugStateSaver') -types_to_ignore.add('QMetaObject.Connection') -types_to_ignore.add('QPointer') -types_to_ignore.add('QAssociativeIterable') -types_to_ignore.add('QSequentialIterable') -types_to_ignore.add('QStaticPlugin') -types_to_ignore.add('QChar') -types_to_ignore.add('QLatin1Char') -types_to_ignore.add('QHash') -types_to_ignore.add('QMultiHash') -types_to_ignore.add('QLinkedList') -types_to_ignore.add('QList') -types_to_ignore.add('QMap') -types_to_ignore.add('QMultiMap') -types_to_ignore.add('QMap.key_iterator') -types_to_ignore.add('QPair') -types_to_ignore.add('QQueue') -types_to_ignore.add('QScopedArrayPointer') -types_to_ignore.add('QScopedPointer') -types_to_ignore.add('QScopedValueRollback') -types_to_ignore.add('QMutableSetIterator') -types_to_ignore.add('QSet') -types_to_ignore.add('QSet.const_iterator') -types_to_ignore.add('QSet.iterator') -types_to_ignore.add('QExplicitlySharedDataPointer') -types_to_ignore.add('QSharedData') -types_to_ignore.add('QSharedDataPointer') -types_to_ignore.add('QEnableSharedFromThis') -types_to_ignore.add('QSharedPointer') -types_to_ignore.add('QWeakPointer') -types_to_ignore.add('QStack') -types_to_ignore.add('QLatin1String') -types_to_ignore.add('QString') -types_to_ignore.add('QStringRef') -types_to_ignore.add('QStringList') -types_to_ignore.add('QStringMatcher') -types_to_ignore.add('QVarLengthArray') -types_to_ignore.add('QVector') -types_to_ignore.add('QFutureIterator') -types_to_ignore.add('QHashIterator') -types_to_ignore.add('QMutableHashIterator') -types_to_ignore.add('QLinkedListIterator') -types_to_ignore.add('QMutableLinkedListIterator') -types_to_ignore.add('QListIterator') -types_to_ignore.add('QMutableListIterator') -types_to_ignore.add('QMapIterator') -types_to_ignore.add('QMutableMapIterator') -types_to_ignore.add('QSetIterator') -types_to_ignore.add('QMutableVectorIterator') -types_to_ignore.add('QVectorIterator') - -# QtGui -types_to_ignore.add('QIconEnginePlugin') -types_to_ignore.add('QImageIOPlugin') -types_to_ignore.add('QGenericPlugin') -types_to_ignore.add('QGenericPluginFactory') -types_to_ignore.add('QGenericMatrix') -types_to_ignore.add('QOpenGLExtraFunctions') -types_to_ignore.add('QOpenGLFunctions') -types_to_ignore.add('QOpenGLFunctions_1_0') -types_to_ignore.add('QOpenGLFunctions_1_1') -types_to_ignore.add('QOpenGLFunctions_1_2') -types_to_ignore.add('QOpenGLFunctions_1_3') -types_to_ignore.add('QOpenGLFunctions_1_4') -types_to_ignore.add('QOpenGLFunctions_1_5') -types_to_ignore.add('QOpenGLFunctions_2_0') -types_to_ignore.add('QOpenGLFunctions_2_1') -types_to_ignore.add('QOpenGLFunctions_3_0') -types_to_ignore.add('QOpenGLFunctions_3_1') -types_to_ignore.add('QOpenGLFunctions_3_2_Compatibility') -types_to_ignore.add('QOpenGLFunctions_3_2_Core') -types_to_ignore.add('QOpenGLFunctions_3_3_Compatibility') -types_to_ignore.add('QOpenGLFunctions_3_3_Core') -types_to_ignore.add('QOpenGLFunctions_4_0_Compatibility') -types_to_ignore.add('QOpenGLFunctions_4_0_Core') -types_to_ignore.add('QOpenGLFunctions_4_1_Compatibility') -types_to_ignore.add('QOpenGLFunctions_4_1_Core') -types_to_ignore.add('QOpenGLFunctions_4_2_Compatibility') -types_to_ignore.add('QOpenGLFunctions_4_2_Core') -types_to_ignore.add('QOpenGLFunctions_4_3_Compatibility') -types_to_ignore.add('QOpenGLFunctions_4_3_Core') -types_to_ignore.add('QOpenGLFunctions_4_4_Compatibility') -types_to_ignore.add('QOpenGLFunctions_4_4_Core') -types_to_ignore.add('QOpenGLFunctions_4_5_Compatibility') -types_to_ignore.add('QOpenGLFunctions_4_5_Core') -types_to_ignore.add('QOpenGLFunctions_ES2') - -# QtWidgets -types_to_ignore.add('QItemEditorCreator') -types_to_ignore.add('QStandardItemEditorCreator') -types_to_ignore.add('QStylePlugin') - -# QtSql -types_to_ignore.add('QSqlDriverCreator') -types_to_ignore.add('QSqlDriverPlugin') - -qt_documentation_website_prefixes = OrderedDict() -qt_documentation_website_prefixes['5.6'] = 'http://doc.qt.io/qt-5.6/' -qt_documentation_website_prefixes['5.8'] = 'http://doc.qt.io/qt-5.8/' -qt_documentation_website_prefixes['5.9'] = 'http://doc.qt.io/qt-5.9/' -qt_documentation_website_prefixes['5.10'] = 'http://doc.qt.io/qt-5.10/' -qt_documentation_website_prefixes['5.11'] = 'http://doc.qt.io/qt-5.11/' -qt_documentation_website_prefixes['5.11'] = 'http://doc.qt.io/qt-5.11/' -qt_documentation_website_prefixes['5.12'] = 'http://doc.qt.io/qt-5.12/' -qt_documentation_website_prefixes['5.13'] = 'http://doc.qt.io/qt-5.13/' -qt_documentation_website_prefixes['5.14'] = 'http://doc.qt.io/qt-5.14/' -qt_documentation_website_prefixes['5.15'] = 'http://doc.qt.io/qt-5/' -qt_documentation_website_prefixes['dev'] = 'http://doc-snapshots.qt.io/qt5-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 - -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='5.15', - choices=['5.6', '5.9', '5.11', '5.12', '5.13', '5.14', '5.15', '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'], - type=str, - dest='which_missing', - help="Which missing types to show (all, or just those " - "that are not present in PyQt)") - -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 = "PyQt5" - -total_missing_types_count = 0 -total_missing_types_count_compared_to_pyqt = 0 -total_missing_modules_count = 0 - -wiki_file = open('missing_bindings_for_wiki_qt_io.txt', 'w') -wiki_file.truncate() - - -def log(*pargs, **kw): - print(*pargs) - - computed_str = '' - for arg in pargs: - computed_str += str(arg) - - style = 'text' - if 'style' in kw: - style = kw['style'] - - if style == 'heading1': - computed_str = '= ' + computed_str + ' =' - elif style == 'heading5': - computed_str = '===== ' + computed_str + ' =====' - elif style == 'with_newline': - computed_str += '\n' - elif style == 'bold_colon': - computed_str = computed_str.replace(':', ":'''") - computed_str += "'''" - computed_str += '\n' - elif style == 'error': - computed_str = "''" + computed_str.strip('\n') + "''\n" - elif style == 'text_with_link': - computed_str = computed_str - elif style == 'code': - computed_str = ' ' + computed_str - elif style == 'end': - return - - print(computed_str, file=wiki_file) - -log('PySide6 bindings for Qt {}'.format(args.version), style='heading1') - -log("""Using Qt version {} documentation to find public API Qt types and test -if the types are present in the PySide6 package.""".format(args.version)) - -log("""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') - -log("""Similar report: -https://gist.github.com/ethanhs/6c626ca4e291f3682589699296377d3a""", -style='text_with_link') - -python_executable = os.path.basename(sys.executable or '') -command_line_arguments = ' '.join(sys.argv) -report_date = strftime("%Y-%m-%d %H:%M:%S %Z", gmtime()) - -log(""" -This report was generated by running the following command: - {} {} -on the following date: - {} -""".format(python_executable, command_line_arguments, report_date)) - -for module_name in modules_to_test.keys(): - log(module_name, style='heading5') - - url = create_doc_url(modules_to_test[module_name], args.version) - log('Documentation link: {}\n'.format(url), 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: - log('\nCould not load {}.{}. Received error: {}. Skipping.\n'.format( - pyside_package_name, module_name, str(e).replace("'", '')), - style='error') - total_missing_modules_count += 1 - continue - - try: - pyqt_module_name = module_name - if module_name == "QtCharts": - pyqt_module_name = module_name[:-1] - - pyqt_tested_module = getattr(__import__(pyqt_package_name, - fromlist=[pyqt_module_name]), pyqt_module_name) - except Exception as e: - log("\nCould not load {}.{} for comparison. " - "Received error: {}.\n".format(pyqt_package_name, module_name, - str(e).replace("'", '')), style='error') - - # Get C++ class list from documentation page. - page = urllib2.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 - link_text = link_text.replace('::', '.') - if link_text not in types_to_ignore: - types_on_html_page.append(link_text) - - log('Number of types in {}: {}'.format(module_name, - len(types_on_html_page)), style='bold_colon') - - missing_types_count = 0 - missing_types_compared_to_pyqt = 0 - missing_types = [] - for qt_type in types_on_html_page: - try: - pyside_qualified_type = 'pyside_tested_module.' - - if "QtCharts" == module_name: - pyside_qualified_type += 'QtCharts.' - elif "DataVisualization" in module_name: - pyside_qualified_type += 'QtDataVisualization.' - - pyside_qualified_type += qt_type - eval(pyside_qualified_type) - except: - missing_type = qt_type - missing_types_count += 1 - total_missing_types_count += 1 - - is_present_in_pyqt = False - try: - pyqt_qualified_type = 'pyqt_tested_module.' - - if "Charts" in module_name: - pyqt_qualified_type += 'QtCharts.' - elif "DataVisualization" in module_name: - pyqt_qualified_type += 'QtDataVisualization.' - - pyqt_qualified_type += qt_type - eval(pyqt_qualified_type) - missing_type += " (is present in PyQt5)" - missing_types_compared_to_pyqt += 1 - total_missing_types_count_compared_to_pyqt += 1 - is_present_in_pyqt = True - except: - pass - - if args.which_missing == 'all': - missing_types.append(missing_type) - elif args.which_missing == 'in-pyqt' and is_present_in_pyqt: - missing_types.append(missing_type) - elif (args.which_missing == 'not-in-pyqt' and - not is_present_in_pyqt): - missing_types.append(missing_type) - - if len(missing_types) > 0: - log('Missing types in {}:'.format(module_name), style='with_newline') - missing_types.sort() - for missing_type in missing_types: - log(missing_type, style='code') - log('') - - log('Number of missing types: {}'.format(missing_types_count), - style='bold_colon') - if len(missing_types) > 0: - log('Number of missing types that are present in PyQt5: {}' - .format(missing_types_compared_to_pyqt), style='bold_colon') - log('End of missing types for {}\n'.format(module_name), style='end') - else: - log('', style='end') - -log('Summary', style='heading5') -log('Total number of missing types: {}'.format(total_missing_types_count), - style='bold_colon') -log('Total number of missing types that are present in PyQt5: {}' - .format(total_missing_types_count_compared_to_pyqt), style='bold_colon') -log('Total number of missing modules: {}' - .format(total_missing_modules_count), style='bold_colon') -wiki_file.close() diff --git a/tools/missing_bindings/config.py b/tools/missing_bindings/config.py new file mode 100644 index 000000000..23a733463 --- /dev/null +++ b/tools/missing_bindings/config.py @@ -0,0 +1,173 @@ +############################################################################# +## +## Copyright (C) 2021 The Qt Company Ltd. +## Contact: https://www.qt.io/licensing/ +## +## This file is part of Qt for Python. +## +## $QT_BEGIN_LICENSE:LGPL$ +## Commercial License Usage +## Licensees holding valid commercial Qt licenses may use this file in +## accordance with the commercial license agreement provided with the +## Software or, alternatively, in accordance with the terms contained in +## a written agreement between you and The Qt Company. For licensing terms +## and conditions see https://www.qt.io/terms-conditions. For further +## information use the contact form at https://www.qt.io/contact-us. +## +## GNU Lesser General Public License Usage +## Alternatively, this file may be used under the terms of the GNU Lesser +## General Public License version 3 as published by the Free Software +## Foundation and appearing in the file LICENSE.LGPL3 included in the +## packaging of this file. Please review the following information to +## ensure the GNU Lesser General Public License version 3 requirements +## will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +## +## GNU General Public License Usage +## Alternatively, this file may be used under the terms of the GNU +## General Public License version 2.0 or (at your option) the GNU General +## Public license version 3 or any later version approved by the KDE Free +## Qt Foundation. The licenses are as published by the Free Software +## Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +## included in the packaging of this file. Please review the following +## information to ensure the GNU General Public License requirements will +## be met: https://www.gnu.org/licenses/gpl-2.0.html and +## https://www.gnu.org/licenses/gpl-3.0.html. +## +## $QT_END_LICENSE$ +## +############################################################################# + + +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', + 'QtQuickControls2': 'qtquickcontrols2-module.html', + #QtQuick3D - no python bindings + 'QtSql': 'qtsql-module.html', + 'QtWidgets': 'qtwidgets-module.html', + 'QtConcurrent': 'qtconcurrent-module.html', + #QtDBUS - no python bindings + 'QtHelp': 'qthelp-module.html', + 'QtOpenGL': 'qtopengl-module.html', + 'QtPrintSupport': 'qtprintsupport-module.html', + 'QtSvg': 'qtsvg-module.html', + 'QtUiTools': 'qtuitools-module.html', + 'QtXml': 'qtxml-module.html', + 'QtTest': 'qttest-module.html', + #'QtXmlPatterns': 'qtxmlpatterns-module.html', # in Qt5 compat + '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', # no python bindings + #'QtCoAp' -- TODO + #'QtMqtt' -- TODO + #'QtOpcUA' -- TODO + + # 6.1 + #'QtScxml': 'qtscxml-module.html', + #'QtCharts': 'qtcharts-module.html', + #'QtDataVisualization': 'qtdatavisualization-module.html', + + # 6.2 + #'QtPositioning': 'qtpositioning-module.html', + #'QtMultimedia': 'qtmultimedia-module.html', + #'QtRemoteObjects': 'qtremoteobjects-module.html', + #'QtSensors': 'qtsensors-module.html', + #'QtSerialPort': 'qtserialport-module.html', + #'QtWebChannel': 'qtwebchannel-module.html', + #'QtWebEngine': 'qtwebengine-module.html', + #'QtWebEngineCore': 'qtwebenginecore-module.html', + #'QtWebEngineWidgets': 'qtwebenginewidgets-module.html', + #'QtWebSockets': 'qtwebsockets-module.html', + + # 6.x + #'QtSpeech': 'qtspeech-module.html', + #'QtMultimediaWidgets': 'qtmultimediawidgets-module.html', + #'QtLocation': 'qtlocation-module.html', + + # Not in 6 + #'QtScriptTools': 'qtscripttools-module.html', + #'QtMacExtras': 'qtmacextras-module.html', + #'QtX11Extras': 'qtx11extras-module.html', + #'QtWinExtras': 'qtwinextras-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..7390687ff --- /dev/null +++ b/tools/missing_bindings/main.py @@ -0,0 +1,330 @@ +############################################################################# +## +## Copyright (C) 2021 The Qt Company Ltd. +## Contact: https://www.qt.io/licensing/ +## +## This file is part of Qt for Python. +## +## $QT_BEGIN_LICENSE:LGPL$ +## Commercial License Usage +## Licensees holding valid commercial Qt licenses may use this file in +## accordance with the commercial license agreement provided with the +## Software or, alternatively, in accordance with the terms contained in +## a written agreement between you and The Qt Company. For licensing terms +## and conditions see https://www.qt.io/terms-conditions. For further +## information use the contact form at https://www.qt.io/contact-us. +## +## GNU Lesser General Public License Usage +## Alternatively, this file may be used under the terms of the GNU Lesser +## General Public License version 3 as published by the Free Software +## Foundation and appearing in the file LICENSE.LGPL3 included in the +## packaging of this file. Please review the following information to +## ensure the GNU Lesser General Public License version 3 requirements +## will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +## +## GNU General Public License Usage +## Alternatively, this file may be used under the terms of the GNU +## General Public License version 2.0 or (at your option) the GNU General +## Public license version 3 or any later version approved by the KDE Free +## Qt Foundation. The licenses are as published by the Free Software +## Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +## included in the packaging of this file. Please review the following +## information to ensure the GNU General Public License requirements will +## be met: https://www.gnu.org/licenses/gpl-2.0.html and +## https://www.gnu.org/licenses/gpl-3.0.html. +## +## $QT_END_LICENSE$ +## +############################################################################# + +# 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.0 -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 os.path +import sys +from textwrap import dedent +from time import gmtime, strftime +from urllib import request + +from bs4 import BeautifulSoup + +from config import modules_to_test, types_to_ignore + +qt_documentation_website_prefixes = { + "6.0": "http://doc.qt.io/qt-6/", + "dev": "http://doc-snapshots.qt.io/qt5-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.0", + choices=["6.0", "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"], + type=str, + dest="which_missing", + help="Which missing types to show (all, or just those " "that are not present in PyQt)", + ) + return parser + + +def wikilog(*pargs, **kw): + print(*pargs) + + computed_str = "" + for arg in pargs: + computed_str += str(arg) + + style = "text" + if "style" in kw: + style = kw["style"] + + if style == "heading1": + computed_str = "= " + computed_str + " =" + elif style == "heading5": + computed_str = "===== " + computed_str + " =====" + elif style == "with_newline": + computed_str += "\n" + elif style == "bold_colon": + computed_str = computed_str.replace(":", ":'''") + computed_str += "'''" + computed_str += "\n" + elif style == "error": + computed_str = "''" + computed_str.strip("\n") + "''\n" + elif style == "text_with_link": + computed_str = computed_str + elif style == "code": + computed_str = " " + 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" + + total_missing_types_count = 0 + total_missing_types_count_compared_to_pyqt = 0 + total_missing_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 = os.path.basename(sys.executable 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 + if module_name == "QtCharts": + pyqt_module_name = module_name[:-1] + + 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", + ) + + # 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 + link_text = link_text.replace("::", ".") + if link_text not in types_to_ignore: + types_on_html_page.append(link_text) + + wikilog(f"Number of types in {module_name}: {len(types_on_html_page)}", style="bold_colon") + + missing_types_count = 0 + missing_types_compared_to_pyqt = 0 + missing_types = [] + for qt_type in types_on_html_page: + try: + pyside_qualified_type = "pyside_tested_module." + + if "QtCharts" == module_name: + pyside_qualified_type += "QtCharts." + elif "DataVisualization" in module_name: + pyside_qualified_type += "QtDataVisualization." + + pyside_qualified_type += qt_type + eval(pyside_qualified_type) + except: + missing_type = qt_type + missing_types_count += 1 + total_missing_types_count += 1 + + is_present_in_pyqt = False + try: + pyqt_qualified_type = "pyqt_tested_module." + + if "Charts" in module_name: + pyqt_qualified_type += "QtCharts." + elif "DataVisualization" in module_name: + pyqt_qualified_type += "QtDataVisualization." + + pyqt_qualified_type += qt_type + eval(pyqt_qualified_type) + missing_type += " (is present in PyQt6)" + missing_types_compared_to_pyqt += 1 + total_missing_types_count_compared_to_pyqt += 1 + is_present_in_pyqt = True + except: + pass + + if args.which_missing == "all": + missing_types.append(missing_type) + elif args.which_missing == "in-pyqt" and is_present_in_pyqt: + missing_types.append(missing_type) + elif args.which_missing == "not-in-pyqt" and not is_present_in_pyqt: + missing_types.append(missing_type) + + if len(missing_types) > 0: + wikilog(f"Missing types in {module_name}:", style="with_newline") + missing_types.sort() + for missing_type in missing_types: + wikilog(missing_type, style="code") + wikilog("") + + wikilog(f"Number of missing types: {missing_types_count}", style="bold_colon") + if len(missing_types) > 0: + 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") + + wikilog("Summary", style="heading5") + 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") + wiki_file.close() diff --git a/tools/missing_bindings/requirements.txt b/tools/missing_bindings/requirements.txt new file mode 100644 index 000000000..732522d26 --- /dev/null +++ b/tools/missing_bindings/requirements.txt @@ -0,0 +1,8 @@ +beautifulsoup4 + +# PySide +PySide6 + +# PyQt +PyQt6 +PyQt6-3D diff --git a/tools/snippets_translate/README.md b/tools/snippets_translate/README.md new file mode 100644 index 000000000..8b24b6b7f --- /dev/null +++ b/tools/snippets_translate/README.md @@ -0,0 +1,151 @@ +# Snippets Translate + +To install dependencies on an activated virtual environment run +`pip install -r requirements.txt`. + +To run the tests, execute `python -m pytest`. It's important not to +run `pytest` alone to include the PYTHONPATH so the imports work. + +Here's an explanation for each file: + +* `main.py`, main file that handle the arguments, the general process + of copying/writing files into the pyside-setup/ repository. +* `converter.py`, main function that translate each line depending + of the decision making process that use different handlers. +* `handlers.py`, functions that handle the different translation cases. +* `parse_utils.py`, some useful function that help the translation process. +* `tests/test_converter.py`, tests cases for the converter function. + +## Usage + +``` +% python main.py -h +usage: sync_snippets [-h] --qt QT_DIR --pyside PYSIDE_DIR [-w] [-v] + +optional arguments: + -h, --help show this help message and exit + --qt QT_DIR Path to the Qt directory (QT_SRC_DIR) + --pyside PYSIDE_DIR Path to the pyside-setup directory + -w, --write Actually copy over the files to the pyside-setup directory + -v, --verbose Generate more output +``` + +For example: + +``` +python main.py --qt /home/cmaureir/dev/qt6/ --pyside /home/cmaureir/dev/pyside-setup -w +``` + +which will create all the snippet files in the pyside repository. The `-w` +option is in charge of actually writing the files. + + +## Pending cases + +As described at the end of the `converter.py` and `tests/test_converter.py` +files there are a couple of corner cases that are not covered like: + +* handler `std::` types and functions +* handler for `operator...` +* handler for `tr("... %1").arg(a)` +* support for lambda expressions +* there are also strange cases that cannot be properly handle with + a line-by-line approach, for example, `for ( ; it != end; ++it) {` +* interpretation of `typedef ...` (including function pointers) +* interpretation of `extern "C" ...` + +Additionally, +one could add more test cases for each handler, because at the moment +only the general converter function (which uses handlers) is being +tested as a whole. + +## Patterns for directories + +### Snippets + +Everything that has .../snippets/*, for example: + +``` + qtbase/src/corelib/doc/snippets/ + ./qtdoc/doc/src/snippets/ + +``` + +goes to: + +``` + pyside-setup/sources/pyside6/doc/codesnippets/doc/src/snippets/* +``` + +### Examples + +Everything that has .../examples/*/*, for example: + +``` + ./qtbase/examples/widgets/dialogs/licensewizard + ./qtbase/examples/widgets/itemviews/pixelator +``` + +goes to + +``` + pyside-setup/sources/pyside6/doc/codesnippets/examples/ + dialogs/licensewizard + itemviews/pixelator + +``` + +## Patterns for files + +Files to skip: + +``` + *.pro + *.pri + *.cmake + *.qdoc + CMakeLists.txt +``` + +which means we will be copying: + +``` + *.png + *.cpp + *.h + *.ui + *.qrc + *.xml + *.qml + *.svg + *.js + *.ts + *.xq + *.txt + etc +``` +## Files examples + +``` +[repo] qt5 + + ./qtbase/src/corelib/doc/snippets/code/src_corelib_thread_qmutexpool.cpp + ./qtbase/src/widgets/doc/snippets/code/src_gui_styles_qstyle.cpp + ./qtbase/src/network/doc/snippets/code/src_network_kernel_qhostinfo.cpp + ./qtbase/examples/sql/relationaltablemodel/relationaltablemodel.cpp + ./qtbase/src/printsupport/doc/snippets/code/src_gui_dialogs_qabstractprintdialog.cpp + ./qtdoc/doc/src/snippets/qlistview-using + ./qtbase/src/widgets/doc/snippets/layouts/layouts.cpp +``` + +``` +[repo] pyside-setup + + ./sources/pyside6/doc/codesnippets/doc/src/snippets/code/src_corelib_thread_qmutexpool.cpp + ./sources/pyside6/doc/codesnippets/doc/src/snippets/code/src_gui_styles_qstyle.cpp + ./sources/pyside6/doc/codesnippets/doc/src/snippets/code/src_network_kernel_qhostinfo.cpp + ./sources/pyside6/doc/codesnippets/examples/relationaltablemodel/relationaltablemodel.cpp + ./sources/pyside6/doc/codesnippets/doc/src/snippets/code/src_gui_dialogs_qabstractprintdialog.cpp + ./sources/pyside6/doc/codesnippets/doc/src/snippets/qlistview-using + ./sources/pyside6/doc/codesnippets/doc/src/snippets/layouts +``` diff --git a/tools/snippets_translate/converter.py b/tools/snippets_translate/converter.py new file mode 100644 index 000000000..e5193f598 --- /dev/null +++ b/tools/snippets_translate/converter.py @@ -0,0 +1,334 @@ +############################################################################# +## +## Copyright (C) 2021 The Qt Company Ltd. +## Contact: https://www.qt.io/licensing/ +## +## This file is part of Qt for Python. +## +## $QT_BEGIN_LICENSE:LGPL$ +## Commercial License Usage +## Licensees holding valid commercial Qt licenses may use this file in +## accordance with the commercial license agreement provided with the +## Software or, alternatively, in accordance with the terms contained in +## a written agreement between you and The Qt Company. For licensing terms +## and conditions see https://www.qt.io/terms-conditions. For further +## information use the contact form at https://www.qt.io/contact-us. +## +## GNU Lesser General Public License Usage +## Alternatively, this file may be used under the terms of the GNU Lesser +## General Public License version 3 as published by the Free Software +## Foundation and appearing in the file LICENSE.LGPL3 included in the +## packaging of this file. Please review the following information to +## ensure the GNU Lesser General Public License version 3 requirements +## will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +## +## GNU General Public License Usage +## Alternatively, this file may be used under the terms of the GNU +## General Public License version 2.0 or (at your option) the GNU General +## Public license version 3 or any later version approved by the KDE Free +## Qt Foundation. The licenses are as published by the Free Software +## Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +## included in the packaging of this file. Please review the following +## information to ensure the GNU General Public License requirements will +## be met: https://www.gnu.org/licenses/gpl-2.0.html and +## https://www.gnu.org/licenses/gpl-3.0.html. +## +## $QT_END_LICENSE$ +## +############################################################################# + +import re + + +from handlers import (handle_casts, handle_class, handle_condition, + handle_conditions, handle_constructor_default_values, + handle_constructors, handle_cout_endl, handle_emit, + handle_for, handle_foreach, handle_inc_dec, + handle_include, handle_keywords, handle_negate, + handle_type_var_declaration, handle_void_functions, + handle_methods_return_type, handle_functions, + handle_array_declarations, handle_useless_qt_classes,) + +from parse_utils import get_indent, dstrip, remove_ref + + +def snippet_translate(x): + + ## Cases which are not C++ + ## TODO: Maybe expand this with lines that doesn't need to be translated + if x.strip().startswith("content-type: text/html"): + return x + + ## General Rules + + # Remove ';' at the end of the lines + if x.endswith(";"): + x = x[:-1] + + # Remove lines with only '{' or '}' + if x.strip() == "{" or x.strip() == "}": + return "" + + # Skip lines with the snippet related identifier '//!' + if x.strip().startswith("//!"): + return x + + # handle lines with only comments using '//' + if x.lstrip().startswith("//"): + x = x.replace("//", "#", 1) + return x + + # Handle "->" + if "->" in x: + x = x.replace("->", ".") + + # handle '&&' and '||' + if "&&" in x: + x = x.replace("&&", "and") + if "||" in x: + x = x.replace("||", "or") + + # Handle lines that have comments after the ';' + if ";" in x and "//" in x: + if x.index(";") < x.index("//"): + left, right = x.split("//", 1) + left = left.replace(";", "", 1) + x = f"{left}#{right}" + + # Handle 'new ' + # This contains an extra whitespace because of some variables + # that include the string 'new' + if "new " in x: + x = x.replace("new ", "") + + # Handle 'const' + # Some variables/functions have the word 'const' so we explicitly + # consider the cases with a whitespace before and after. + if " const" in x: + x = x.replace(" const", "") + if "const " in x: + x = x.replace("const ", "") + + # Handle 'static' + if "static " in x: + x = x.replace("static ", "") + + # Handle 'inline' + if "inline " in x: + x = x.replace("inline ", "") + + # Handle 'double' + if "double " in x: + x = x.replace("double ", "float ") + + # Handle increment/decrement operators + if "++" in x: + x = handle_inc_dec(x, "++") + if "--" in x: + x = handle_inc_dec(x, "--") + + # handle negate '!' + if "!" in x: + x = handle_negate(x) + + # Handle "this", "true", "false" but before "#" symbols + if "this" in x: + x = handle_keywords(x, "this", "self") + if "true" in x: + x = handle_keywords(x, "true", "True") + if "false" in x: + x = handle_keywords(x, "false", "False") + if "throw" in x: + x = handle_keywords(x, "throw", "raise") + + # handle 'void Class::method(...)' and 'void method(...)' + if re.search(r"^ *void *[\w\_]+(::)?[\w\d\_]+\(", x): + x = handle_void_functions(x) + + # 'Q*::' -> 'Q*.' + # FIXME: This will break iterators, but it's a small price. + if re.search(r"Q[\w]+::", x): + x = x.replace("::", ".") + + # handle 'nullptr' + if "nullptr" in x: + x = x.replace("nullptr", "None") + + ## Special Cases Rules + + # Special case for 'main' + if x.strip().startswith("int main("): + return f'{get_indent(x)}if __name__ == "__main__":' + + if x.strip().startswith("QApplication app(argc, argv)"): + return f"{get_indent(x)}app = QApplication([])" + + # Special case for 'return app.exec()' + if x.strip().startswith("return app.exec"): + return x.replace("return app.exec()", "sys.exit(app.exec_())") + + # Handle includes -> import + if x.strip().startswith("#include"): + x = handle_include(x) + return dstrip(x) + + if x.strip().startswith("emit "): + x = handle_emit(x) + return dstrip(x) + + # *_cast + if "_cast<" in x: + x = handle_casts(x) + + # Handle Qt classes that needs to be removed + x = handle_useless_qt_classes(x) + + # Handling ternary operator + if re.search(r"^.* \? .+ : .+$", x.strip()): + x = x.replace(" ? ", " if ") + x = x.replace(" : ", " else ") + + # Handle 'while', 'if', and 'else if' + # line might end in ')' or ") {" + if x.strip().startswith(("while", "if", "else if", "} else if")): + x = handle_conditions(x) + return dstrip(x) + elif re.search("^ *}? *else *{?", x): + x = re.sub(r"}? *else *{?", "else:", x) + return dstrip(x) + + # 'cout' and 'endl' + if re.search("^ *(std::)?cout", x) or ("endl" in x) or x.lstrip().startswith("qDebug()"): + x = handle_cout_endl(x) + return dstrip(x) + + # 'for' loops + if re.search(r"^ *for *\(", x.strip()): + return dstrip(handle_for(x)) + + # 'foreach' loops + if re.search(r"^ *foreach *\(", x.strip()): + return dstrip(handle_foreach(x)) + + # 'class' and 'structs' + if re.search(r"^ *class ", x) or re.search(r"^ *struct ", x): + if "struct " in x: + x = x.replace("struct ", "class ") + return handle_class(x) + + # 'delete' + if re.search(r"^ *delete ", x): + return x.replace("delete", "del") + + # 'public:' + if re.search(r"^public:$", x.strip()): + return x.replace("public:", "# public") + + # 'private:' + if re.search(r"^private:$", x.strip()): + return x.replace("private:", "# private") + + # For expressions like: `Type var` + # which does not contain a `= something` on the right side + # should match + # Some thing + # QSome<var> thing + # QSome thing(...) + # should not match + # QSome thing = a + # QSome thing = a(...) + # def something(a, b, c) + # At the end we skip methods with the form: + # QStringView Message::body() + # to threat them as methods. + if (re.search(r"^[a-zA-Z0-9]+(<.*?>)? [\w\*\&]+(\(.*?\))? ?(?!.*=|:).*$", x.strip()) + and x.strip().split()[0] not in ("def", "return", "and", "or") + and not re.search(r"^[a-zA-Z0-9]+(<.*?>)? [\w]+::[\w\*\&]+\(.*\)$", x.strip()) + and ("{" not in x and "}" not in x)): + + # FIXME: this 'if' is a hack for a function declaration with this form: + # QString myDecoderFunc(QByteArray &localFileName) + # No idea how to check for different for variables like + # QString notAFunction(Something something) + # Maybe checking the structure of the arguments? + if "Func" not in x: + return dstrip(handle_type_var_declaration(x)) + + # For expressions like: `Type var = value`, + # considering complex right-side expressions. + # QSome thing = b + # QSome thing = b(...) + # float v = 0.1 + # QSome *thing = ... + if (re.search(r"^[a-zA-Z0-9]+(<.*?>)? [\w\*]+ *= *[\w\.\"\']*(\(.*?\))?", x.strip()) and + ("{" not in x and "}" not in x)): + left, right = x.split("=", 1) + var_name = " ".join(left.strip().split()[1:]) + x = f"{get_indent(x)}{remove_ref(var_name)} = {right.strip()}" + # Special case: When having this: + # QVBoxLayout *layout = new QVBoxLayout; + # we end up like this: + # layout = QVBoxLayout + # so we need to add '()' at the end if it's just a word + # with only alpha numeric content + if re.search(r"\w+ = [A-Z]{1}\w+", x.strip()) and not x.strip().endswith(")"): + x = f"{x.rstrip()}()" + return dstrip(x) + + # For constructors, that we now the shape is: + # ClassName::ClassName(...) + if re.search(r"^ *\w+::\w+\(.*?\)", x.strip()): + x = handle_constructors(x) + return dstrip(x) + + # For base object constructor: + # : QWidget(parent) + if ( + x.strip().startswith(": ") + and ("<<" not in x) + and ("::" not in x) + and not x.strip().endswith(";") + ): + + return handle_constructor_default_values(x) + + # Arrays declarations with the form: + # type var_name[] = {... + # type var_name {... + #if re.search(r"^[a-zA-Z0-9]+(<.*?>)? [\w\*]+\[\] * = *\{", x.strip()): + if re.search(r"^[a-zA-Z0-9]+(<.*?>)? [\w\*]+\[?\]? * =? *\{", x.strip()): + x = handle_array_declarations(x) + + # Methods with return type + # int Class::method(...) + # QStringView Message::body() + if re.search(r"^[a-zA-Z0-9]+(<.*?>)? [\w]+::[\w\*\&]+\(.*\)$", x.strip()): + # We just need to capture the 'method name' and 'arguments' + x = handle_methods_return_type(x) + + # Handling functions + # By this section of the function, we cover all the other cases + # So we can safely assume it's not a variable declaration + if re.search(r"^[a-zA-Z0-9]+(<.*?>)? [\w\*\&]+\(.*\)$", x.strip()): + x = handle_functions(x) + + # General return for no special cases + return dstrip(x) + + # TODO: + # * Lambda expressions + + # * operator overload + # void operator()(int newState) { state = newState; } + # const QDBusArgument &operator>>(const QDBusArgument &argument, MyDictionary &myDict) + # inline bool operator==(const Employee &e1, const Employee &e2) + # void *operator new[](size_t size) + + # * extern "C" ... + # extern "C" MY_EXPORT int avg(int a, int b) + + # * typedef ... + # typedef int (*AvgFunction)(int, int); + + # * function pointers + # typedef void (*MyPrototype)(); diff --git a/tools/snippets_translate/handlers.py b/tools/snippets_translate/handlers.py new file mode 100644 index 000000000..b8ee9f219 --- /dev/null +++ b/tools/snippets_translate/handlers.py @@ -0,0 +1,519 @@ +############################################################################# +## +## Copyright (C) 2021 The Qt Company Ltd. +## Contact: https://www.qt.io/licensing/ +## +## This file is part of Qt for Python. +## +## $QT_BEGIN_LICENSE:LGPL$ +## Commercial License Usage +## Licensees holding valid commercial Qt licenses may use this file in +## accordance with the commercial license agreement provided with the +## Software or, alternatively, in accordance with the terms contained in +## a written agreement between you and The Qt Company. For licensing terms +## and conditions see https://www.qt.io/terms-conditions. For further +## information use the contact form at https://www.qt.io/contact-us. +## +## GNU Lesser General Public License Usage +## Alternatively, this file may be used under the terms of the GNU Lesser +## General Public License version 3 as published by the Free Software +## Foundation and appearing in the file LICENSE.LGPL3 included in the +## packaging of this file. Please review the following information to +## ensure the GNU Lesser General Public License version 3 requirements +## will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +## +## GNU General Public License Usage +## Alternatively, this file may be used under the terms of the GNU +## General Public License version 2.0 or (at your option) the GNU General +## Public license version 3 or any later version approved by the KDE Free +## Qt Foundation. The licenses are as published by the Free Software +## Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +## included in the packaging of this file. Please review the following +## information to ensure the GNU General Public License requirements will +## be met: https://www.gnu.org/licenses/gpl-2.0.html and +## https://www.gnu.org/licenses/gpl-3.0.html. +## +## $QT_END_LICENSE$ +## +############################################################################# + +import re + +from parse_utils import get_indent, dstrip, remove_ref, parse_arguments, replace_main_commas, get_qt_module_class + +def handle_condition(x, name): + # Make sure it's not a multi line condition + x = x.replace("}", "") + if x.count("(") == x.count(")"): + comment = "" + # This handles the lines that have no ';' at the end but + # have a comment after the end of the line, like: + # while (true) // something + # { ... } + if "//" in x: + comment_content = x.split("//", 1) + comment = f" #{comment_content[-1]}" + x = x.replace(f"//{comment_content[-1]}", "") + + re_par = re.compile(r"\((.+)\)") + condition = re_par.search(x).group(1) + return f"{get_indent(x)}{name} {condition.strip()}:{comment}" + return x + + +def handle_keywords(x, word, pyword): + if word in x: + if "#" in x: + if x.index(word) < x.index("#"): + x = x.replace(word, pyword) + else: + x = x.replace(word, pyword) + return x + + +def handle_inc_dec(x, operator): + # Alone on a line + clean_x = x.strip() + if clean_x.startswith(operator) or clean_x.endswith(operator): + x = x.replace(operator, "") + x = f"{x} = {clean_x.replace(operator, '')} {operator[0]} 1" + return x + + +def handle_casts(x): + cast = None + re_type = re.compile(r"<(.*)>") + re_data = re.compile(r"_cast<.*>\((.*)\)") + type_name = re_type.search(x) + data_name = re_data.search(x) + + if type_name and data_name: + type_name = type_name.group(1).replace("*", "") + data_name = data_name.group(1) + new_value = f"{type_name}({data_name})" + + if "static_cast" in x: + x = re.sub(r"static_cast<.*>\(.*\)", new_value, x) + elif "dynamic_cast" in x: + x = re.sub(r"dynamic_cast<.*>\(.*\)", new_value, x) + elif "const_cast" in x: + x = re.sub(r"const_cast<.*>\(.*\)", new_value, x) + elif "reinterpret_cast" in x: + x = re.sub(r"reinterpret_cast<.*>\(.*\)", new_value, x) + elif "qobject_cast" in x: + x = re.sub(r"qobject_cast<.*>\(.*\)", new_value, x) + + return x + + +def handle_include(x): + if '"' in x: + re_par = re.compile(r'"(.*)"') + header = re_par.search(x) + if header: + header_name = header.group(1).replace(".h", "") + module_name = header_name.replace('/', '.') + x = f"from {module_name} import *" + else: + # We discard completely if there is something else + # besides '"something.h"' + x = "" + elif "<" in x and ">" in x: + re_par = re.compile(r"<(.*)>") + name = re_par.search(x).group(1) + t = get_qt_module_class(name) + # if it's not a Qt module or class, we discard it. + if t is None: + x = "" + else: + # is a module + if t[0]: + x = f"from PySide6 import {t[1]}" + # is a class + else: + x = f"from PySide6.{t[1]} import {name}" + return x + + +def handle_conditions(x): + x_strip = x.strip() + if x_strip.startswith("while") and "(" in x: + x = handle_condition(x, "while") + elif x_strip.startswith("if") and "(" in x: + x = handle_condition(x, "if") + elif x_strip.startswith(("else if", "} else if")): + x = handle_condition(x, "else if") + x = x.replace("else if", "elif") + x = x.replace("::", ".") + return x + + +def handle_for(x): + re_content = re.compile(r"\((.*)\)") + content = re_content.search(x) + + new_x = x + if content: + # parenthesis content + content = content.group(1) + + # for (int i = 1; i < argc; ++i) + if x.count(";") == 2: + + # for (start; middle; end) + start, middle, end = content.split(";") + + # iterators + if "begin(" in x.lower() and "end(" in x.lower(): + name = re.search(r"= *(.*)egin\(", start) + iterable = None + iterator = None + if name: + name = name.group(1) + # remove initial '=', and split the '.' + # because '->' was already transformed, + # and we keep the first word. + iterable = name.replace("=", "", 1).split(".")[0] + + iterator = remove_ref(start.split("=")[0].split()[-1]) + if iterator and iterable: + return f"{get_indent(x)}for {iterator} in {iterable}:" + + if ("++" in end or "--" in end) or ("+=" in end or "-=" in end): + if "," in start: + raw_var, value = start.split(",")[0].split("=") + else: + # Malformed for-loop: + # for (; pixel1 > start; pixel1 -= stride) + # We return the same line + if not start.strip(): + return f"{get_indent(x)}{dstrip(x)}" + raw_var, value = start.split("=") + raw_var = raw_var.strip() + value = value.strip() + var = raw_var.split()[-1] + + end_value = None + if "+=" in end: + end_value = end.split("+=")[-1] + elif "-=" in end: + end_value = end.split("-=")[-1] + if end_value: + try: + end_value = int(end_value) + except ValueError: + end_value = None + + if "<" in middle: + limit = middle.split("<")[-1] + + if "<=" in middle: + limit = middle.split("<=")[-1] + try: + limit = int(limit) + limit += 1 + except ValueError: + limit = f"{limit} + 1" + + if end_value: + new_x = f"for {var} in range({value}, {limit}, {end_value}):" + else: + new_x = f"for {var} in range({value}, {limit}):" + elif ">" in middle: + limit = middle.split(">")[-1] + + if ">=" in middle: + limit = middle.split(">=")[-1] + try: + limit = int(limit) + limit -= 1 + except ValueError: + limit = f"{limit} - 1" + if end_value: + new_x = f"for {var} in range({limit}, {value}, -{end_value}):" + else: + new_x = f"for {var} in range({limit}, {value}, -1):" + else: + # TODO: No support if '<' or '>' is not used. + pass + + # for (const QByteArray &ext : qAsConst(extensionList)) + elif x.count(":") > 0: + iterator, iterable = content.split(":", 1) + var = iterator.split()[-1].replace("&", "").strip() + new_x = f"for {remove_ref(var)} in {iterable.strip()}:" + return f"{get_indent(x)}{dstrip(new_x)}" + + +def handle_foreach(x): + re_content = re.compile(r"\((.*)\)") + content = re_content.search(x) + if content: + parenthesis = content.group(1) + iterator, iterable = parenthesis.split(",", 1) + # remove iterator type + it = dstrip(iterator.split()[-1]) + # remove <...> from iterable + value = re.sub("<.*>", "", iterable) + return f"{get_indent(x)}for {it} in {value}:" + + +def handle_type_var_declaration(x): + # remove content between <...> + if "<" in x and ">" in x: + x = " ".join(re.sub("<.*>", "", i) for i in x.split()) + content = re.search(r"\((.*)\)", x) + if content: + # this means we have something like: + # QSome thing(...) + type_name, var_name = x.split()[:2] + var_name = var_name.split("(")[0] + x = f"{get_indent(x)}{var_name} = {type_name}({content.group(1)})" + else: + # this means we have something like: + # QSome thing + type_name, var_name = x.split()[:2] + x = f"{get_indent(x)}{var_name} = {type_name}()" + return x + + +def handle_constructors(x): + re_content = re.compile(r"\((.*)\)") + arguments = re_content.search(x).group(1) + class_method = x.split("(")[0].split("::") + if len(class_method) == 2: + # Equal 'class name' and 'method name' + if len(set(class_method)) == 1: + arguments = ", ".join(remove_ref(i.split()[-1]) for i in arguments.split(",") if i) + if arguments: + return f"{get_indent(x)}def __init__(self, {arguments}):" + else: + return f"{get_indent(x)}def __init__(self):" + return dstrip(x) + + +def handle_constructor_default_values(x): + # if somehow we have a ' { } ' by the end of the line, + # we discard that section completely, since even with a single + # value, we don't need to take care of it, for example: + # ' : a(1) { } -> self.a = 1 + if re.search(".*{ *}.*", x): + x = re.sub("{ *}", "", x) + + values = "".join(x.split(":", 1)) + # Check the commas that are not inside round parenthesis + # For example: + # : QWidget(parent), Something(else, and, other), value(1) + # so we can find only the one after '(parent),' and 'other),' + # and replace them by '@' + # : QWidget(parent)@ Something(else, and, other)@ value(1) + # to be able to split the line. + values = replace_main_commas(values) + # if we have more than one expression + if "@" in values: + return_values = "" + for arg in values.split("@"): + arg = re.sub("^ *: *", "", arg).strip() + if arg.startswith("Q"): + class_name = arg.split("(")[0] + content = arg.replace(class_name, "")[1:-1] + return_values += f" {class_name}.__init__(self, {content})\n" + elif arg: + var_name = arg.split("(")[0] + re_par = re.compile(r"\((.+)\)") + content = re_par.search(arg).group(1) + return_values += f" self.{var_name} = {content}\n" + else: + arg = re.sub("^ *: *", "", values).strip() + if arg.startswith("Q"): + class_name = arg.split("(")[0] + content = arg.replace(class_name, "")[1:-1] + return f" {class_name}.__init__(self, {content})" + elif arg: + var_name = arg.split("(")[0] + re_par = re.compile(r"\((.+)\)") + content = re_par.search(arg).group(1) + return f" self.{var_name} = {content}" + + return return_values.rstrip() + + +def handle_cout_endl(x): + # if comment at the end + comment = "" + if re.search(r" *# *[\w\ ]+$", x): + comment = f' # {re.search(" *# *(.*)$", x).group(1)}' + x = x.split("#")[0] + + if "qDebug()" in x: + x = x.replace("qDebug()", "cout") + + if "cout" in x and "endl" in x: + re_cout_endl = re.compile(r"cout *<<(.*)<< *.*endl") + data = re_cout_endl.search(x) + if data: + data = data.group(1) + data = re.sub(" *<< *", ", ", data) + x = f"{get_indent(x)}print({data}){comment}" + elif "cout" in x: + data = re.sub(".*cout *<<", "", x) + data = re.sub(" *<< *", ", ", data) + x = f"{get_indent(x)}print({data}){comment}" + elif "endl" in x: + data = re.sub("<< +endl", "", x) + data = re.sub(" *<< *", ", ", data) + x = f"{get_indent(x)}print({data}){comment}" + + x = x.replace("( ", "(").replace(" )", ")").replace(" ,", ",").replace("(, ", "(") + x = x.replace("Qt.endl", "").replace(", )", ")") + return x + + +def handle_negate(x): + # Skip if it's part of a comment: + if "#" in x: + if x.index("#") < x.index("!"): + return x + elif "/*" in x: + if x.index("/*") < x.index("!"): + return x + re_negate = re.compile(r"!(.)") + next_char = re_negate.search(x).group(1) + if next_char not in ("=", '"'): + x = x.replace("!", "not ") + return x + + +def handle_emit(x): + function_call = x.replace("emit ", "").strip() + re_content = re.compile(r"\((.*)\)") + arguments = re_content.search(function_call).group(1) + method_name = function_call.split("(")[0].strip() + return f"{get_indent(x)}{method_name}.emit({arguments})" + + +def handle_void_functions(x): + class_method = x.replace("void ", "").split("(")[0] + first_param = "" + if "::" in class_method: + first_param = "self, " + method_name = class_method.split("::")[1] + else: + method_name = class_method.strip() + + # if the arguments are in the same line: + if ")" in x: + re_content = re.compile(r"\((.*)\)") + parenthesis = re_content.search(x).group(1) + arguments = dstrip(parse_arguments(parenthesis)) + elif "," in x: + arguments = dstrip(parse_arguments(x.split("(")[-1])) + + # check if includes a '{ ... }' after the method signature + after_signature = x.split(")")[-1] + re_decl = re.compile(r"\{(.*)\}").search(after_signature) + extra = "" + if re_decl: + extra = re_decl.group(1) + if not extra: + extra = " pass" + + if arguments: + x = f"{get_indent(x)}def {method_name}({first_param}{dstrip(arguments)}):{extra}" + else: + x = f"{get_indent(x)}def {method_name}({first_param.replace(', ', '')}):{extra}" + return x + + +def handle_class(x): + # Check if there is a comment at the end of the line + comment = "" + if "//" in x: + parts = x.split("//") + x = "".join(parts[:-1]) + comment = parts[-1] + + # If the line ends with '{' + if x.rstrip().endswith("{"): + x = x[:-1] + + # Get inheritance + decl_parts = x.split(":") + class_name = decl_parts[0].rstrip() + if len(decl_parts) > 1: + bases = decl_parts[1] + bases_name = ", ".join(i.split()[-1] for i in bases.split(",") if i) + else: + bases_name = "" + + # Check if the class_name is templated, then remove it + if re.search(r".*<.*>", class_name): + class_name = class_name.split("<")[0] + + # Special case: invalid notation for an example: + # class B() {...} -> clas B(): pass + if re.search(r".*{.*}", class_name): + class_name = re.sub(r"{.*}", "", class_name).rstrip() + return f"{class_name}(): pass" + + # Special case: check if the line ends in ',' + if x.endswith(","): + x = f"{class_name}({bases_name}," + else: + x = f"{class_name}({bases_name}):" + + if comment: + return f"{x} #{comment}" + else: + return x + +def handle_array_declarations(x): + re_varname = re.compile(r"^[a-zA-Z0-9\<\>]+ ([\w\*]+) *\[?\]?") + content = re_varname.search(x.strip()) + if content: + var_name = content.group(1) + rest_line = "".join(x.split("{")[1:]) + x = f"{get_indent(x)}{var_name} = {{{rest_line}" + return x + +def handle_methods_return_type(x): + re_capture = re.compile(r"^ *[a-zA-Z0-9]+ [\w]+::([\w\*\&]+\(.*\)$)") + capture = re_capture.search(x) + if capture: + content = capture.group(1) + method_name = content.split("(")[0] + re_par = re.compile(r"\((.+)\)") + par_capture = re_par.search(x) + arguments = "(self)" + if par_capture: + arguments = f"(self, {par_capture.group(1)})" + x = f"{get_indent(x)}def {method_name}{arguments}:" + return x + + +def handle_functions(x): + re_capture = re.compile(r"^ *[a-zA-Z0-9]+ ([\w\*\&]+\(.*\)$)") + capture = re_capture.search(x) + if capture: + content = capture.group(1) + function_name = content.split("(")[0] + re_par = re.compile(r"\((.+)\)") + par_capture = re_par.search(x) + arguments = "" + if par_capture: + for arg in par_capture.group(1).split(","): + arguments += f"{arg.split()[-1]}," + # remove last comma + if arguments.endswith(","): + arguments = arguments[:-1] + x = f"{get_indent(x)}def {function_name}({dstrip(arguments)}):" + return x + +def handle_useless_qt_classes(x): + _classes = ("QLatin1String", "QLatin1Char") + for i in _classes: + re_content = re.compile(fr"{i}\((.*)\)") + content = re_content.search(x) + if content: + x = x.replace(content.group(0), content.group(1)) + return x diff --git a/tools/snippets_translate/main.py b/tools/snippets_translate/main.py new file mode 100644 index 000000000..c1267d22c --- /dev/null +++ b/tools/snippets_translate/main.py @@ -0,0 +1,466 @@ +############################################################################# +## +## Copyright (C) 2021 The Qt Company Ltd. +## Contact: https://www.qt.io/licensing/ +## +## This file is part of Qt for Python. +## +## $QT_BEGIN_LICENSE:LGPL$ +## Commercial License Usage +## Licensees holding valid commercial Qt licenses may use this file in +## accordance with the commercial license agreement provided with the +## Software or, alternatively, in accordance with the terms contained in +## a written agreement between you and The Qt Company. For licensing terms +## and conditions see https://www.qt.io/terms-conditions. For further +## information use the contact form at https://www.qt.io/contact-us. +## +## GNU Lesser General Public License Usage +## Alternatively, this file may be used under the terms of the GNU Lesser +## General Public License version 3 as published by the Free Software +## Foundation and appearing in the file LICENSE.LGPL3 included in the +## packaging of this file. Please review the following information to +## ensure the GNU Lesser General Public License version 3 requirements +## will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +## +## GNU General Public License Usage +## Alternatively, this file may be used under the terms of the GNU +## General Public License version 2.0 or (at your option) the GNU General +## Public license version 3 or any later version approved by the KDE Free +## Qt Foundation. The licenses are as published by the Free Software +## Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +## included in the packaging of this file. Please review the following +## information to ensure the GNU General Public License requirements will +## be met: https://www.gnu.org/licenses/gpl-2.0.html and +## https://www.gnu.org/licenses/gpl-3.0.html. +## +## $QT_END_LICENSE$ +## +############################################################################# + +import argparse +import logging +import os +import re +import shutil +import sys +from enum import Enum +from pathlib import Path +from textwrap import dedent + +from converter import snippet_translate + +# Logger configuration +try: + from rich.logging import RichHandler + + logging.basicConfig( + level="NOTSET", format="%(message)s", datefmt="[%X]", handlers=[RichHandler()] + ) + have_rich = True + extra = {"markup": True} + + from rich.console import Console + from rich.table import Table + +except ModuleNotFoundError: + print("-- 'rich' not found, falling back to default logger") + logging.basicConfig(level=logging.INFO) + have_rich = False + extra = {} + +log = logging.getLogger("snippets_translate") +opt_quiet = False + +# Filter and paths configuration +SKIP_END = (".pro", ".pri", ".cmake", ".qdoc", ".yaml", ".frag", ".qsb", ".vert", "CMakeLists.txt") +SKIP_BEGIN = ("changes-", ".") +OUT_MAIN = Path("sources/pyside6/doc/codesnippets/") +OUT_SNIPPETS = OUT_MAIN / "doc/src/snippets/" +OUT_EXAMPLES = OUT_MAIN / "doc/codesnippets/examples/" + + +class FileStatus(Enum): + Exists = 0 + New = 1 + + +def get_parser(): + parser = argparse.ArgumentParser(prog="snippets_translate") + # List pyproject files + parser.add_argument( + "--qt", + action="store", + dest="qt_dir", + required=True, + help="Path to the Qt directory (QT_SRC_DIR)", + ) + + parser.add_argument( + "--pyside", + action="store", + dest="pyside_dir", + required=True, + help="Path to the pyside-setup directory", + ) + + parser.add_argument( + "-w", + "--write", + action="store_true", + dest="write_files", + help="Actually copy over the files to the pyside-setup directory", + ) + + parser.add_argument( + "-q", + "--quiet", + action="store_true", + help="Quiet" + ) + + parser.add_argument( + "-v", + "--verbose", + action="store_true", + dest="verbose", + help="Generate more output", + ) + + parser.add_argument( + "-s", + "--single", + action="store", + dest="single_snippet", + help="Path to a single file to be translated", + ) + + parser.add_argument( + "--filter", + action="store", + dest="filter_snippet", + help="String to filter the snippets to be translated", + ) + return parser + + +def is_directory(directory): + if not os.path.isdir(directory): + log.error(f"Path '{directory}' is not a directory") + return False + return True + + +def check_arguments(options): + + # Notify 'write' option + if options.write_files: + if not opt_quiet: + log.warning( + f"Files will be copied from '{options.qt_dir}':\n" f"\tto '{options.pyside_dir}'" + ) + else: + msg = "This is a listing only, files are not being copied" + if have_rich: + msg = f"[green]{msg}[/green]" + if not opt_quiet: + log.info(msg, extra=extra) + + # Check 'qt_dir' and 'pyside_dir' + if is_directory(options.qt_dir) and is_directory(options.pyside_dir): + return True + + return False + + +def is_valid_file(x): + file_name = x.name + # Check END + for ext in SKIP_END: + if file_name.endswith(ext): + return False + + # Check BEGIN + for ext in SKIP_BEGIN: + if file_name.startswith(ext): + return False + + # Contains 'snippets' or 'examples' as subdirectory + if not ("snippets" in x.parts or "examples" in x.parts): + return False + + return True + + +def get_snippets(data): + snippet_lines = "" + is_snippet = False + snippets = [] + for line in data: + if not is_snippet and line.startswith("//! ["): + snippet_lines = line + is_snippet = True + elif is_snippet: + snippet_lines = f"{snippet_lines}\n{line}" + if line.startswith("//! ["): + is_snippet = False + snippets.append(snippet_lines) + # Special case when a snippet line is: + # //! [1] //! [2] + if line.count("//!") > 1: + snippet_lines = "" + is_snippet = True + return snippets + + +def get_license_from_file(filename): + lines = [] + with open(filename, "r") as f: + line = True + while line: + line = f.readline().rstrip() + + if line.startswith("/*") or line.startswith("**"): + lines.append(line) + # End of the comment + if line.endswith("*/"): + break + if lines: + # We know we have the whole block, so we can + # perform replacements to translate the comment + lines[0] = lines[0].replace("/*", "**").replace("*", "#") + lines[-1] = lines[-1].replace("*/", "**").replace("*", "#") + + for i in range(1, len(lines) - 1): + lines[i] = re.sub(r"^\*\*", "##", lines[i]) + + return "\n".join(lines) + else: + return "" + +def translate_file(file_path, final_path, verbose, write): + with open(str(file_path)) as f: + snippets = get_snippets(f.read().splitlines()) + if snippets: + # TODO: Get license header first + license_header = get_license_from_file(str(file_path)) + if verbose: + if have_rich: + console = Console() + table = Table(show_header=True, header_style="bold magenta") + table.add_column("C++") + table.add_column("Python") + + file_snippets = [] + for snippet in snippets: + lines = snippet.split("\n") + translated_lines = [] + for line in lines: + if not line: + continue + translated_line = snippet_translate(line) + translated_lines.append(translated_line) + + # logging + if verbose: + if have_rich: + table.add_row(line, translated_line) + else: + if not opt_quiet: + print(line, translated_line) + + if verbose and have_rich: + if not opt_quiet: + console.print(table) + + file_snippets.append("\n".join(translated_lines)) + + if write: + # Open the final file + with open(str(final_path), "w") as out_f: + out_f.write(license_header) + out_f.write("\n") + + for s in file_snippets: + out_f.write(s) + out_f.write("\n\n") + + # Rename to .py + written_file = shutil.move(str(final_path), str(final_path.with_suffix(".py"))) + if not opt_quiet: + log.info(f"Written: {written_file}") + else: + if not opt_quiet: + log.warning("No snippets were found") + + + +def copy_file(file_path, py_path, category, category_path, write=False, verbose=False): + + if not category: + translate_file(file_path, Path("_translated.py"), verbose, write) + return + # Get path after the directory "snippets" or "examples" + # and we add +1 to avoid the same directory + idx = file_path.parts.index(category) + 1 + rel_path = Path().joinpath(*file_path.parts[idx:]) + + final_path = py_path / category_path / rel_path + + # Check if file exists. + if final_path.exists(): + status_msg = " [yellow][Exists][/yellow]" if have_rich else "[Exists]" + status = FileStatus.Exists + elif final_path.with_suffix(".py").exists(): + status_msg = "[cyan][ExistsPy][/cyan]" if have_rich else "[Exists]" + status = FileStatus.Exists + else: + status_msg = " [green][New][/green]" if have_rich else "[New]" + status = FileStatus.New + + if verbose: + if not opt_quiet: + log.info(f"From {file_path} to") + log.info(f"==> {final_path}") + + if not opt_quiet: + if have_rich: + log.info(f"{status_msg} {final_path}", extra={"markup": True}) + else: + log.info(f"{status_msg:10s} {final_path}") + + # Directory where the file will be placed, if it does not exists + # we create it. The option 'parents=True' will create the parents + # directories if they don't exist, and if some of them exists, + # the option 'exist_ok=True' will ignore them. + if write and not final_path.parent.is_dir(): + if not opt_quiet: + log.info(f"Creating directories for {final_path.parent}") + final_path.parent.mkdir(parents=True, exist_ok=True) + + # Change .cpp to .py + # TODO: + # - What do we do with .h in case both .cpp and .h exists with + # the same name? + + # Translate C++ code into Python code + if final_path.name.endswith(".cpp"): + translate_file(file_path, final_path, verbose, write) + + return status + + +def process(options): + qt_path = Path(options.qt_dir) + py_path = Path(options.pyside_dir) + + # (new, exists) + valid_new, valid_exists = 0, 0 + + # Creating directories in case they don't exist + if not OUT_SNIPPETS.is_dir(): + OUT_SNIPPETS.mkdir(parents=True) + + if not OUT_EXAMPLES.is_dir(): + OUT_EXAMPLES.mkdir(parents=True) + + if options.single_snippet: + f = Path(options.single_snippet) + if is_valid_file(f): + if "snippets" in f.parts: + status = copy_file( + f, + py_path, + "snippets", + OUT_SNIPPETS, + write=options.write_files, + verbose=options.verbose, + ) + elif "examples" in f.parts: + status = copy_file( + f, + py_path, + "examples", + OUT_EXAMPLES, + write=options.write_files, + verbose=options.verbose, + ) + else: + log.warning("Path did not contain 'snippets' nor 'examples'." + "File will not be copied over, just generated locally.") + status = copy_file( + f, + py_path, + None, + None, + write=options.write_files, + verbose=options.verbose, + ) + + else: + for i in qt_path.iterdir(): + module_name = i.name + # FIXME: remove this, since it's just for testing. + if i.name != "qtbase": + continue + + # Filter only Qt modules + if not module_name.startswith("qt"): + continue + if not opt_quiet: + log.info(f"Module {module_name}") + + # Iterating everything + for f in i.glob("**/*.*"): + if is_valid_file(f): + if options.filter_snippet: + # Proceed only if the full path contain the filter string + if options.filter_snippet not in str(f.absolute()): + continue + if "snippets" in f.parts: + status = copy_file( + f, + py_path, + "snippets", + OUT_SNIPPETS, + write=options.write_files, + verbose=options.verbose, + ) + elif "examples" in f.parts: + status = copy_file( + f, + py_path, + "examples", + OUT_EXAMPLES, + write=options.write_files, + verbose=options.verbose, + ) + + # Stats + if status == FileStatus.New: + valid_new += 1 + elif status == FileStatus.Exists: + valid_exists += 1 + + if not opt_quiet: + log.info( + dedent( + f"""\ + Summary: + Total valid files: {valid_new + valid_exists} + New files: {valid_new} + Existing files: {valid_exists} + """ + ) + ) + + +if __name__ == "__main__": + parser = get_parser() + options = parser.parse_args() + opt_quiet = options.quiet + + if not check_arguments(options): + parser.print_help() + sys.exit(0) + + process(options) diff --git a/tools/snippets_translate/parse_utils.py b/tools/snippets_translate/parse_utils.py new file mode 100644 index 000000000..c4ba91409 --- /dev/null +++ b/tools/snippets_translate/parse_utils.py @@ -0,0 +1,145 @@ +############################################################################# +## +## Copyright (C) 2021 The Qt Company Ltd. +## Contact: https://www.qt.io/licensing/ +## +## This file is part of Qt for Python. +## +## $QT_BEGIN_LICENSE:LGPL$ +## Commercial License Usage +## Licensees holding valid commercial Qt licenses may use this file in +## accordance with the commercial license agreement provided with the +## Software or, alternatively, in accordance with the terms contained in +## a written agreement between you and The Qt Company. For licensing terms +## and conditions see https://www.qt.io/terms-conditions. For further +## information use the contact form at https://www.qt.io/contact-us. +## +## GNU Lesser General Public License Usage +## Alternatively, this file may be used under the terms of the GNU Lesser +## General Public License version 3 as published by the Free Software +## Foundation and appearing in the file LICENSE.LGPL3 included in the +## packaging of this file. Please review the following information to +## ensure the GNU Lesser General Public License version 3 requirements +## will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +## +## GNU General Public License Usage +## Alternatively, this file may be used under the terms of the GNU +## General Public License version 2.0 or (at your option) the GNU General +## Public license version 3 or any later version approved by the KDE Free +## Qt Foundation. The licenses are as published by the Free Software +## Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +## included in the packaging of this file. Please review the following +## information to ensure the GNU General Public License requirements will +## be met: https://www.gnu.org/licenses/gpl-2.0.html and +## https://www.gnu.org/licenses/gpl-3.0.html. +## +## $QT_END_LICENSE$ +## +############################################################################# + +import re + +# Bring all the PySide modules to find classes for the imports +import PySide6 +from PySide6 import * + + +def get_qt_module_class(x): + """ + Receives the name of an include: + 'QSomething' from '#include <QSomething>' + + Returns a tuple '(bool, str)' where the 'bool' is True if the name is + a module by itself, like QtCore or QtWidgets, and False if it's a class + from one of those modules. The 'str' returns the name of the module + where the class belongs, or the same module. + + In case it doesn't find the class or the module, it will return None. + """ + for imodule in (m for m in dir(PySide6) if m.startswith("Qt")): + if imodule == x: + return True, x + # we use eval() to transform 'QtModule' into QtModule + for iclass in (c for c in dir(eval(f"PySide6.{imodule}")) if c.startswith("Q")): + if iclass == x: + return False, imodule + return None + + +def get_indent(x): + return " " * (len(x) - len(x.lstrip())) + +# Remove more than one whitespace from the code, but not considering +# the indentation. Also removes '&', '*', and ';' from arguments. +def dstrip(x): + right = x + if re.search(r"\s+", x): + right = re.sub(" +", " ", x).strip() + if "&" in right: + right = right.replace("&", "") + + if "*" in right: + re_pointer = re.compile(r"\*(.)") + next_char = re_pointer.search(x) + if next_char: + if next_char.group(1).isalpha(): + right = right.replace("*", "") + + if right.endswith(";"): + right = right.replace(";", "") + x = f"{get_indent(x)}{right}" + + return x + + +def remove_ref(var_name): + var = var_name.strip() + while var.startswith("*") or var.startswith("&"): + var = var[1:] + return var.lstrip() + + +def parse_arguments(p): + unnamed_var = 0 + if "," in p: + v = "" + for i, arg in enumerate(p.split(",")): + if i != 0: + v += ", " + if arg: + new_value = arg.split()[-1] + # handle no variable name + if new_value.strip() == "*": + v += f"arg__{unnamed_var}" + unnamed_var += 1 + else: + v += arg.split()[-1] + elif p.strip(): + new_value = p.split()[-1] + if new_value.strip() == "*": + v = f"arg__{unnamed_var}" + else: + v = new_value + else: + v = p + + return v + + +def replace_main_commas(v): + # : QWidget(parent), Something(else, and, other), value(1) + new_v = "" + parenthesis = 0 + for c in v: + if c == "(": + parenthesis += 1 + elif c == ")": + parenthesis -= 1 + + if c == "," and parenthesis == 0: + c = "@" + + new_v += c + + return new_v + diff --git a/tools/snippets_translate/requirements.txt b/tools/snippets_translate/requirements.txt new file mode 100644 index 000000000..1fb678867 --- /dev/null +++ b/tools/snippets_translate/requirements.txt @@ -0,0 +1,2 @@ +rich +pytest diff --git a/tools/snippets_translate/snippets_translate.pyproject b/tools/snippets_translate/snippets_translate.pyproject new file mode 100644 index 000000000..8016eb637 --- /dev/null +++ b/tools/snippets_translate/snippets_translate.pyproject @@ -0,0 +1,3 @@ +{ + "files": ["main.py", "converter.py", "handlers.py", "tests/test_converter.py"] +} diff --git a/tools/snippets_translate/tests/test_converter.py b/tools/snippets_translate/tests/test_converter.py new file mode 100644 index 000000000..5656ff5e8 --- /dev/null +++ b/tools/snippets_translate/tests/test_converter.py @@ -0,0 +1,439 @@ +############################################################################# +## +## Copyright (C) 2021 The Qt Company Ltd. +## Contact: https://www.qt.io/licensing/ +## +## This file is part of Qt for Python. +## +## $QT_BEGIN_LICENSE:LGPL$ +## Commercial License Usage +## Licensees holding valid commercial Qt licenses may use this file in +## accordance with the commercial license agreement provided with the +## Software or, alternatively, in accordance with the terms contained in +## a written agreement between you and The Qt Company. For licensing terms +## and conditions see https://www.qt.io/terms-conditions. For further +## information use the contact form at https://www.qt.io/contact-us. +## +## GNU Lesser General Public License Usage +## Alternatively, this file may be used under the terms of the GNU Lesser +## General Public License version 3 as published by the Free Software +## Foundation and appearing in the file LICENSE.LGPL3 included in the +## packaging of this file. Please review the following information to +## ensure the GNU Lesser General Public License version 3 requirements +## will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +## +## GNU General Public License Usage +## Alternatively, this file may be used under the terms of the GNU +## General Public License version 2.0 or (at your option) the GNU General +## Public license version 3 or any later version approved by the KDE Free +## Qt Foundation. The licenses are as published by the Free Software +## Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +## included in the packaging of this file. Please review the following +## information to ensure the GNU General Public License requirements will +## be met: https://www.gnu.org/licenses/gpl-2.0.html and +## https://www.gnu.org/licenses/gpl-3.0.html. +## +## $QT_END_LICENSE$ +## +############################################################################# + +from converter import snippet_translate as st + + +def test_comments(): + assert st("// This is a comment") == "# This is a comment" + assert st("// double slash // inside") == "# double slash // inside" + + +def test_comments_eol(): + assert st("a = 1; // comment") == "a = 1 # comment" + assert st("while ( 1 != 1 ) { // comment") == "while 1 != 1: # comment" + + +def test_qdoc_snippets(): + assert st("//! [0]") == "//! [0]" + + +def test_arrow(): + assert st("label->setText('something')") == "label.setText('something')" + + +def test_curly_braces(): + assert st(" {") == "" + assert st("}") == "" + assert st("while (true){") == "while True:" + assert st("while (true) { ") == "while True:" + + +def test_inc_dec(): + assert st("++i;") == "i = i + 1" + assert st("i--;") == "i = i - 1" + + +def test_and_or(): + assert st("while (a && b)") == "while a and b:" + assert st("else if (a || b && c)") == "elif a or b and c:" + + +def test_while_if_elseif(): + assert st("while(a)") == "while a:" + assert st("if (condition){") == "if condition:" + assert st("} else if (a) {") == " elif a:" + assert ( + st("if (!m_vbo.isCreated()) // init() failed,") + == "if not m_vbo.isCreated(): # init() failed," + ) + # Special case, second line from a condition + assert ( + st("&& event->answerRect().intersects(dropFrame->geometry()))") + == "and event.answerRect().intersects(dropFrame.geometry()))" + ) + + +def test_else(): + assert st("else") == "else:" + assert st("} else {") == "else:" + assert st("}else") == "else:" + assert st("else {") == "else:" + + +def test_new(): + assert st("a = new Something(...);") == "a = Something(...)" + assert st("a = new Something") == "a = Something" + + +def test_semicolon(): + assert st("a = 1;") == "a = 1" + assert st("};") == "" + + +def test_include(): + assert st('#include "something.h"') == "from something import *" + assert st("#include <QtCore>") == "from PySide6 import QtCore" + assert st("#include <QLabel>") == "from PySide6.QtWidgets import QLabel" + assert st("#include <NotQt>") == "" + assert st('#include strange"') == "" + + +def test_main(): + assert st("int main(int argc, char *argv[])") == 'if __name__ == "__main__":' + + +def test_cast(): + assert st("a = reinterpret_cast<type>(data);") == "a = type(data)" + assert st("a = reinterpret_cast<type*>(data) * 9;") == "a = type(data) * 9" + assert ( + st("elapsed = (elapsed + qobject_cast<QTimer*>(sender())->interval()) % 1000;") + == "elapsed = (elapsed + QTimer(sender()).interval()) % 1000" + ) + + +def test_double_colon(): + assert st("Qt::Align") == "Qt.Align" + assert st('QSound::play("mysounds/bells.wav");') == 'QSound.play("mysounds/bells.wav")' + # FIXME + assert st("Widget::method") == "Widget::method" + + +def test_cout_endl(): + assert st("cout << 'hello' << 'world' << endl") == "print('hello', 'world')" + assert st(" cout << 'hallo' << 'welt' << endl") == " print('hallo', 'welt')" + assert st("cout << 'hi'") == "print('hi')" + assert st("'world' << endl") == "print('world')" + + assert st("cout << circ.at(i) << endl;") == "print(circ.at(i))" + assert ( + st('cout << "Element name: " << qPrintable(e.tagName()) << "\n";') + == 'print("Element name: ", qPrintable(e.tagName()), "\n")' + ) + assert ( + st('cout << "First occurrence of Harumi is at position " << i << Qt::endl;') + == 'print("First occurrence of Harumi is at position ", i)' + ) + assert st('cout << "Found Jeanette" << endl;') == 'print("Found Jeanette")' + assert st('cout << "The key: " << it.key() << Qt::endl') == 'print("The key: ", it.key())' + assert ( + st("cout << (*constIterator).toLocal8Bit().constData() << Qt::endl;") + == "print((constIterator).toLocal8Bit().constData())" + ) + assert st("cout << ba[0]; // prints H") == "print(ba[0]) # prints H" + assert ( + st('cout << "Also the value: " << (*it) << Qt::endl;') == 'print("Also the value: ", (it))' + ) + assert st('cout << "[" << *data << "]" << Qt::endl;') == 'print("[", data, "]")' + + assert st('out << "Qt rocks!" << Qt::endl;') == 'print(out, "Qt rocks!")' + assert st(' std::cout << "MyObject::MyObject()\n";') == ' print("MyObject::MyObject()\n")' + assert st('qDebug() << "Retrieved:" << retrieved;') == 'print("Retrieved:", retrieved)' + + +def test_variable_declaration(): + assert st("QLabel label;") == "label = QLabel()" + assert st('QLabel label("Hello")') == 'label = QLabel("Hello")' + assert st("Widget w;") == "w = Widget()" + assert st('QLabel *label = new QLabel("Hello");') == 'label = QLabel("Hello")' + assert st('QLabel label = a_function("Hello");') == 'label = a_function("Hello")' + assert st('QString a = "something";') == 'a = "something"' + assert st("int var;") == "var = int()" + assert st("float v = 0.1;") == "v = 0.1" + assert st("QSome<thing> var") == "var = QSome()" + assert st("QQueue<int> queue;") == "queue = QQueue()" + assert st("QVBoxLayout *layout = new QVBoxLayout;") == "layout = QVBoxLayout()" + assert st("QPointer<QLabel> label = new QLabel;") == "label = QLabel()" + assert st("QMatrix4x4 matrix;") == "matrix = QMatrix4x4()" + assert st("QList<QImage> collage =") == "collage =" + + +def test_for(): + assert st("for (int i = 0; i < 10; i++)") == "for i in range(0, 10):" + assert st(" for (int i = 0; i < 10; i+=2)") == " for i in range(0, 10, 2):" + assert st("for (int i = 10; i >= 0; i-=2)") == "for i in range(-1, 10, -2):" + assert st("for (int i = 0; i < 10; ++i)") == "for i in range(0, 10):" + assert ( + st("for (int c = 0;" "c < model.columnCount();" "++c) {") + == "for c in range(0, model.columnCount()):" + ) + assert ( + st("for (int c = 0;" "c < table->columns();" "++c) {") + == "for c in range(0, table.columns()):" + ) + assert st("for (int i = 0; i <= 10; i++)") == "for i in range(0, 11):" + assert st("for (int i = 10; i >= 0; i--)") == "for i in range(-1, 10, -1):" + + ## if contains "begin()" and "end()", do a 'for it in var' + assert ( + st( + "for (QHash<int, QString>::const_iterator it = hash.cbegin()," + "end = hash.cend(); it != end; ++it)" + ) + == "for it in hash:" + ) + assert ( + st("for (QTextBlock it = doc->begin();" "it != doc->end(); it = it.next())") + == "for it in doc:" + ) + assert st("for (auto it = map.begin(); it != map.end(); ++it) {") == "for it in map:" + assert st("for (i = future.constBegin(); i != future.constEnd(); ++i)") == "for i in future:" + assert st("for (it = block.begin(); !(it.atEnd()); ++it) {") == "for it in block:" + assert ( + st(" for (it = snippetPaths.constBegin();" "it != snippetPaths.constEnd(); ++it)") + == " for it in snippetPaths:" + ) + + assert st("for (QChar ch : s)") == "for ch in s:" + assert ( + st("for (const QByteArray &ext : " "qAsConst(extensionList))") + == "for ext in qAsConst(extensionList):" + ) + assert st("for (QTreeWidgetItem *item : found) {") == "for item in found:" + + # TODO: Strange cases + # for ( ; it != end; ++it) { + # for (; !elt.isNull(); elt = elt.nextSiblingElement("entry")) { + # for (int i = 0; ids[i]; ++i) + # for (int i = 0; i < (1>>20); ++i) + # for(QDomNode n = element.firstChild(); !n.isNull(); n = n.nextSibling()) + + +def test_emit(): + assert st("emit sliderPressed();") == "sliderPressed.emit()" + assert st("emit actionTriggered(action);") == "actionTriggered.emit(action)" + assert st("emit activeChanged(d->m_active);") == "activeChanged.emit(d.m_active)" + assert st("emit dataChanged(index, index);") == "dataChanged.emit(index, index)" + assert st("emit dataChanged(index, index, {role});") == "dataChanged.emit(index, index, {role})" + assert ( + st('emit dragResult(tr("The data was copied here."));') + == 'dragResult.emit(tr("The data was copied here."))' + ) + assert ( + st("emit mimeTypes(event->mimeData()->formats());") + == "mimeTypes.emit(event.mimeData().formats())" + ) + assert ( + st("emit q_ptr->averageFrequencyChanged(m_averageFrequency);") + == "q_ptr.averageFrequencyChanged.emit(m_averageFrequency)" + ) + assert st("emit q_ptr->frequencyChanged();") == "q_ptr.frequencyChanged.emit()" + assert ( + st("emit rangeChanged(d->minimum, d->maximum);") + == "rangeChanged.emit(d.minimum, d.maximum)" + ) + assert ( + st("emit sliderMoved((d->position = value));") == "sliderMoved.emit((d.position = value))" + ) + assert ( + st("emit stateChanged(QContactAction::FinishedState);") + == "stateChanged.emit(QContactAction.FinishedState)" + ) + assert st("emit textCompleted(lineEdit->text());") == "textCompleted.emit(lineEdit.text())" + assert ( + st("emit updateProgress(newstat, m_watcher->progressMaximum());") + == "updateProgress.emit(newstat, m_watcher.progressMaximum())" + ) + + +def test_void_functions(): + assert st("void Something::Method(int a, char *b) {") == "def Method(self, a, b):" + assert ( + st("void MainWindow::updateMenus(QListWidgetItem *current)") + == "def updateMenus(self, current):" + ) + assert ( + st("void MyScrollArea::scrollContentsBy(int dx, int dy)") + == "def scrollContentsBy(self, dx, dy):" + ) + assert st("void Wrapper::wrapper6() {") == "def wrapper6(self):" + assert st("void MyClass::setPriority(Priority) {}") == "def setPriority(self, Priority): pass" + assert st("void MyException::raise() const { throw *this; }") == "def raise(self): raise self" + assert st("void tst_Skip::test_data()") == "def test_data(self):" + assert st("void util_function_does_nothing()") == "def util_function_does_nothing():" + assert st("static inline void cleanup(MyCustomClass *pointer)") == "def cleanup(pointer):" + # TODO: Which name? + assert st("void RenderWindow::exposeEvent(QExposeEvent *)") == "def exposeEvent(self, arg__0):" + + +def test_classes(): + assert st("class MyWidget //: public QWidget") == "class MyWidget(): #: public QWidget" + assert st("class MyMfcView : public CView") == "class MyMfcView(CView):" + assert st("class MyGame : public QObject {") == "class MyGame(QObject):" + assert st("class tst_Skip") == "class tst_Skip():" + assert st("class A : public B, protected C") == "class A(B, C):" + assert st("class A : public B, public C") == "class A(B, C):" + assert st("class SomeTemplate<int> : public QFrame") == "class SomeTemplate(QFrame):" + # This is a tricky situation because it has a multi line dependency: + # class MyMemberSheetExtension : public QObject, + # public QDesignerMemberSheetExtension + # { + # we will use the leading comma to trust it's the previously situation. + assert st("class A : public QObject,") == "class A(QObject," + assert st("class B {...};") == "class B(): pass" + + +def test_constuctors(): + assert st("MyWidget::MyWidget(QWidget *parent)") == "def __init__(self, parent):" + assert st("Window::Window()") == "def __init__(self):" + + +def test_inheritance_init(): + assert ( + st(": QClass(fun(re, 1, 2), parent), a(1)") + == " QClass.__init__(self, fun(re, 1, 2), parent)\n self.a = 1" + ) + assert ( + st(": QQmlNdefRecord(copyFooRecord(record), parent)") + == " QQmlNdefRecord.__init__(self, copyFooRecord(record), parent)" + ) + assert ( + st(" : QWidget(parent), helper(helper)") + == " QWidget.__init__(self, parent)\n self.helper = helper" + ) + assert st(" : QWidget(parent)") == " QWidget.__init__(self, parent)" + assert ( + st(": a(0), bB(99), cC2(1), p_S(10),") + == " self.a = 0\n self.bB = 99\n self.cC2 = 1\n self.p_S = 10" + ) + assert ( + st(": QAbstractFileEngineIterator(nameFilters, filters), index(0) ") + == " QAbstractFileEngineIterator.__init__(self, nameFilters, filters)\n self.index = 0" + ) + assert ( + st(": m_document(doc), m_text(text)") == " self.m_document = doc\n self.m_text = text" + ) + assert st(": m_size(size) { }") == " self.m_size = size" + assert ( + st(": option->palette.color(QPalette::Mid);") + == " self.option.palette.color = QPalette.Mid" + ) + assert st(": QSqlResult(driver) {}") == " QSqlResult.__init__(self, driver)" + + +def test_arrays(): + assert st("static const GLfloat vertices[] = {") == "vertices = {" + assert st("static const char *greeting_strings[] = {") == "greeting_strings = {" + assert st("uchar arrow_bits[] = {0x3f, 0x1f, 0x0f}") == "arrow_bits = {0x3f, 0x1f, 0x0f}" + assert st("QList<int> vector { 1, 2, 3, 4 };") == "vector = { 1, 2, 3, 4 }" + + +def test_functions(): + assert st("int Class::method(a, b, c)") == "def method(self, a, b, c):" + assert st("QStringView Message::body() const") == "def body(self):" + assert st("void Ren::exEvent(QExp *)") == "def exEvent(self, arg__0):" + assert ( + st("QString myDecoderFunc(const QByteArray &localFileName);") + == "def myDecoderFunc(localFileName):" + ) + + +def test_foreach(): + assert st("foreach (item, selected) {") == "for item in selected:" + assert st("foreach (const QVariant &v, iterable) {") == "for v in iterable:" + assert st("foreach (QObject *obj, list)") == "for obj in list:" + assert ( + st("foreach (const QContactTag& tag, contact.details<QContactTag>()) {") + == "for tag in contact.details():" + ) + + +def test_structs(): + assert st("struct ScopedPointerCustomDeleter") == "class ScopedPointerCustomDeleter():" + assert st("struct Wrapper : public QWidget {") == "class Wrapper(QWidget):" + assert st("struct Window {") == "class Window():" + + +def test_ternary_operator(): + assert st("int a = 1 ? b > 0 : 3") == "a = 1 if b > 0 else 3" + assert ( + st("if (!game.saveGame(json ? Game::Json : Game::Binary))") + == "if not game.saveGame(json if Game.Json else Game.Binary):" + ) + +def test_useless_qt_classes(): + assert st('result += QLatin1String("; ");') == 'result += "; "' + assert st("<< QLatin1Char('\0') << endl;") == "print('\0')" + +def test_special_cases(): + assert ( + st('http->setProxy("proxy.example.com", 3128);') + == 'http.setProxy("proxy.example.com", 3128)' + ) + assert st("delete something;") == "del something" + assert ( + st("m_program->setUniformValue(m_matrixUniform, matrix);") + == "m_program.setUniformValue(m_matrixUniform, matrix)" + ) + assert ( + st("QObject::connect(&window1, &Window::messageSent,") + == "QObject.connect(window1, Window.messageSent," + ) + assert st("double num;") == "num = float()" + + # Leave a comment to remember it comes from C++ + assert st("public:") == "# public" + assert st("private:") == "# private" + + + # TODO: Handle the existing ones with Python equivalents + # assert st("std::...") + + # FIXME: Maybe a better interpretation? + # assert st("QDebug operator<<(QDebug dbg, const Message &message)") == "def __str__(self):" + + # TODO: Maybe play with the format? + # assert st('m_o.append(tr("version: %1.%2").arg(a).arg(b))') == 'm_o.append(tr("version: {1}.{2}".format(a, b)' + + +def test_lambdas(): + # QtConcurrent::blockingMap(vector, [](int &x) { x *= 2; }); + + # QList<QImage> collage = QtConcurrent::mappedReduced(images, + # [&size](const QImage &image) { + # return image.scaled(size, size); + # }, + # addToCollage + # ).results(); + pass + + +def test_std_function(): + # std::function<QImage(const QImage &)> scale = [](const QImage &img) { + pass |