diff options
Diffstat (limited to 'tools/snippets_translate')
-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 |
8 files changed, 2059 insertions, 0 deletions
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 |