aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorCristian Maureira-Fredes <Cristian.Maureira-Fredes@qt.io>2021-01-05 01:10:55 +0100
committerQt Cherry-pick Bot <cherrypick_bot@qt-project.org>2021-03-18 12:54:29 +0000
commitc9788f47faa25f1f0151092a3cfda166b27b6d2e (patch)
tree4555e90b6893db502d54c4530b8049f50d6972f8
parentdc3e4a064ee7e88e0950b860f8d0aa7557fb6f5c (diff)
Long live snippets_translate!
This is not a C++ -> Python translator, but a line-by-line conversion tool. This scripts requires two arguments to identify a Qt and PySide directory including the sources. There is a set of file extensions that are currently omitted from the process, and for the ones that will be copied, there will be messages related if the file already exists or if it's new. If you use the '-v' option, you will see the C++ code and the converted Python code, so it's easy to check for issues and missing features. Also, two command line options were added to have a different behavior '--filter' to include a word to filter the full paths of all the snippets found (for example the name of a directory), and '-s/--single' to translate only a specific C++ file to be translated. Including test cases for transformations related to the C++ snippets. Fixes: PYSIDE-691 Change-Id: I208e3a9139c7e84fe369a7c2ea93af240d83fa83 Reviewed-by: Christian Tismer <tismer@stackless.com> (cherry picked from commit 1c65d71c468f2166ab20a867011a6d217a5f3ec1) Reviewed-by: Qt Cherry-pick Bot <cherrypick_bot@qt-project.org>
-rw-r--r--tools/snippets_translate/README.md151
-rw-r--r--tools/snippets_translate/converter.py334
-rw-r--r--tools/snippets_translate/handlers.py519
-rw-r--r--tools/snippets_translate/main.py438
-rw-r--r--tools/snippets_translate/parse_utils.py145
-rw-r--r--tools/snippets_translate/requirements.txt2
-rw-r--r--tools/snippets_translate/snippets_translate.pyproject3
-rw-r--r--tools/snippets_translate/tests/test_converter.py439
8 files changed, 2031 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..0e4ce233c
--- /dev/null
+++ b/tools/snippets_translate/main.py
@@ -0,0 +1,438 @@
+#############################################################################
+##
+## 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")
+
+# Filter and paths configuration
+SKIP_END = (".pro", ".pri", ".cmake", ".qdoc", ".yaml", ".frag", ".qsb", ".vert", "CMakeLists.txt")
+SKIP_BEGIN = ("changes-", ".")
+OUT_SNIPPETS = Path("sources/pyside6/doc/codesnippets/doc/src/snippets/")
+OUT_EXAMPLES = Path("sources/pyside6/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(
+ "-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:
+ 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]"
+ 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:
+ print(line, translated_line)
+
+ if verbose and have_rich:
+ 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")))
+ log.info(f"Written: {written_file}")
+ else:
+ 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:
+ log.info(f"From {file_path} to")
+ log.info(f"==> {final_path}")
+
+ 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():
+ 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
+
+ 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
+ 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
+
+ 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()
+
+ 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