aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-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