aboutsummaryrefslogtreecommitdiffstats
path: root/sources/pyside-tools/qtpy2cpp_lib
diff options
context:
space:
mode:
Diffstat (limited to 'sources/pyside-tools/qtpy2cpp_lib')
-rw-r--r--sources/pyside-tools/qtpy2cpp_lib/astdump.py111
-rw-r--r--sources/pyside-tools/qtpy2cpp_lib/formatter.py265
-rw-r--r--sources/pyside-tools/qtpy2cpp_lib/nodedump.py50
-rw-r--r--sources/pyside-tools/qtpy2cpp_lib/qt.py56
-rw-r--r--sources/pyside-tools/qtpy2cpp_lib/tests/baseline/basic_test.cpp62
-rw-r--r--sources/pyside-tools/qtpy2cpp_lib/tests/baseline/basic_test.py44
-rw-r--r--sources/pyside-tools/qtpy2cpp_lib/tests/test_qtpy2cpp.py54
-rw-r--r--sources/pyside-tools/qtpy2cpp_lib/tokenizer.py55
-rw-r--r--sources/pyside-tools/qtpy2cpp_lib/visitor.py442
9 files changed, 1139 insertions, 0 deletions
diff --git a/sources/pyside-tools/qtpy2cpp_lib/astdump.py b/sources/pyside-tools/qtpy2cpp_lib/astdump.py
new file mode 100644
index 000000000..d92fb7589
--- /dev/null
+++ b/sources/pyside-tools/qtpy2cpp_lib/astdump.py
@@ -0,0 +1,111 @@
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+"""Tool to dump a Python AST"""
+
+
+import ast
+import tokenize
+from argparse import ArgumentParser, RawTextHelpFormatter
+from enum import Enum
+
+from nodedump import debug_format_node
+
+DESCRIPTION = "Tool to dump a Python AST"
+
+
+_source_lines = []
+_opt_verbose = False
+
+
+def first_non_space(s):
+ for i, c in enumerate(s):
+ if c != ' ':
+ return i
+ return 0
+
+
+class NodeType(Enum):
+ IGNORE = 1
+ PRINT_ONE_LINE = 2 # Print as a one liner, do not visit children
+ PRINT = 3 # Print with opening closing tag, visit children
+ PRINT_WITH_SOURCE = 4 # Like PRINT, but print source line above
+
+
+def get_node_type(node):
+ if isinstance(node, (ast.Load, ast.Store, ast.Delete)):
+ return NodeType.IGNORE
+ if isinstance(node, (ast.Add, ast.alias, ast.arg, ast.Eq, ast.Gt, ast.Lt,
+ ast.Mult, ast.Name, ast.NotEq, ast.NameConstant, ast.Not,
+ ast.Num, ast.Str)):
+ return NodeType.PRINT_ONE_LINE
+ if not hasattr(node, 'lineno'):
+ return NodeType.PRINT
+ if isinstance(node, (ast.Attribute)):
+ return NodeType.PRINT_ONE_LINE if isinstance(node.value, ast.Name) else NodeType.PRINT
+ return NodeType.PRINT_WITH_SOURCE
+
+
+class DumpVisitor(ast.NodeVisitor):
+ def __init__(self):
+ ast.NodeVisitor.__init__(self)
+ self._indent = 0
+ self._printed_source_lines = {-1}
+
+ def generic_visit(self, node):
+ node_type = get_node_type(node)
+ if _opt_verbose and node_type in (NodeType.IGNORE, NodeType.PRINT_ONE_LINE):
+ node_type = NodeType.PRINT
+ if node_type == NodeType.IGNORE:
+ return
+ self._indent = self._indent + 1
+ indent = ' ' * self._indent
+
+ if node_type == NodeType.PRINT_WITH_SOURCE:
+ line_number = node.lineno - 1
+ if line_number not in self._printed_source_lines:
+ self._printed_source_lines.add(line_number)
+ line = _source_lines[line_number]
+ non_space = first_non_space(line)
+ print('{:04d} {}{}'.format(line_number, '_' * non_space,
+ line[non_space:]))
+
+ if node_type == NodeType.PRINT_ONE_LINE:
+ print(indent, debug_format_node(node))
+ else:
+ print(indent, '>', debug_format_node(node))
+ ast.NodeVisitor.generic_visit(self, node)
+ print(indent, '<', type(node).__name__)
+
+ self._indent = self._indent - 1
+
+
+def parse_ast(filename):
+ node = None
+ with tokenize.open(filename) as f:
+ global _source_lines
+ source = f.read()
+ _source_lines = source.split('\n')
+ node = ast.parse(source, mode="exec")
+ return node
+
+
+def create_arg_parser(desc):
+ parser = ArgumentParser(description=desc,
+ formatter_class=RawTextHelpFormatter)
+ parser.add_argument('--verbose', '-v', action='store_true',
+ help='Verbose')
+ parser.add_argument('source', type=str, help='Python source')
+ return parser
+
+
+if __name__ == '__main__':
+ arg_parser = create_arg_parser(DESCRIPTION)
+ options = arg_parser.parse_args()
+ _opt_verbose = options.verbose
+ title = f'AST tree for {options.source}'
+ print('=' * len(title))
+ print(title)
+ print('=' * len(title))
+ tree = parse_ast(options.source)
+ DumpVisitor().visit(tree)
diff --git a/sources/pyside-tools/qtpy2cpp_lib/formatter.py b/sources/pyside-tools/qtpy2cpp_lib/formatter.py
new file mode 100644
index 000000000..9a38e803d
--- /dev/null
+++ b/sources/pyside-tools/qtpy2cpp_lib/formatter.py
@@ -0,0 +1,265 @@
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+"""C++ formatting helper functions and formatter class"""
+
+
+import ast
+
+from .qt import ClassFlag, qt_class_flags
+
+CLOSING = {"{": "}", "(": ")", "[": "]"} # Closing parenthesis for C++
+
+
+def _fix_function_argument_type(type, for_return):
+ """Fix function argument/return qualifiers using some heuristics for Qt."""
+ if type == "float":
+ return "double"
+ if type == "str":
+ type = "QString"
+ if not type.startswith("Q"):
+ return type
+ flags = qt_class_flags(type)
+ if flags & ClassFlag.PASS_BY_VALUE:
+ return type
+ if flags & ClassFlag.PASS_BY_CONSTREF:
+ return type if for_return else f"const {type} &"
+ if flags & ClassFlag.PASS_BY_REF:
+ return type if for_return else f"{type} &"
+ return type + " *" # Assume pointer by default
+
+
+def to_string(node):
+ """Helper to retrieve a string from the (Lists of)Name/Attribute
+ aggregated into some nodes"""
+ if isinstance(node, ast.Name):
+ return node.id
+ if isinstance(node, ast.Attribute):
+ return node.attr
+ return ''
+
+
+def format_inheritance(class_def_node):
+ """Returns inheritance specification of a class"""
+ result = ''
+ for base in class_def_node.bases:
+ name = to_string(base)
+ if name != 'object':
+ result += ', public ' if result else ' : public '
+ result += name
+ return result
+
+
+def format_for_target(target_node):
+ if isinstance(target_node, ast.Tuple): # for i,e in enumerate()
+ result = ''
+ for i, el in enumerate(target_node.elts):
+ if i > 0:
+ result += ', '
+ result += format_reference(el)
+ return result
+ return format_reference(target_node)
+
+
+def format_for_loop(f_node):
+ """Format a for loop
+ This applies some heuristics to detect:
+ 1) "for a in [1,2])" -> "for (f: {1, 2}) {"
+ 2) "for i in range(5)" -> "for (i = 0; i < 5; ++i) {"
+ 3) "for i in range(2,5)" -> "for (i = 2; i < 5; ++i) {"
+
+ TODO: Detect other cases, maybe including enumerate().
+ """
+ loop_vars = format_for_target(f_node.target)
+ result = 'for (' + loop_vars
+ if isinstance(f_node.iter, ast.Call):
+ f = format_reference(f_node.iter.func)
+ if f == 'range':
+ start = 0
+ end = -1
+ if len(f_node.iter.args) == 2:
+ start = format_literal(f_node.iter.args[0])
+ end = format_literal(f_node.iter.args[1])
+ elif len(f_node.iter.args) == 1:
+ end = format_literal(f_node.iter.args[0])
+ result += f' = {start}; {loop_vars} < {end}; ++{loop_vars}'
+ elif isinstance(f_node.iter, ast.List):
+ # Range based for over list
+ result += ': ' + format_literal_list(f_node.iter)
+ elif isinstance(f_node.iter, ast.Name):
+ # Range based for over variable
+ result += ': ' + f_node.iter.id
+ result += ') {'
+ return result
+
+
+def format_name_constant(node):
+ """Format a ast.NameConstant."""
+ if node.value is None:
+ return "nullptr"
+ return "true" if node.value else "false"
+
+
+def format_literal(node):
+ """Returns the value of number/string literals"""
+ if isinstance(node, ast.NameConstant):
+ return format_name_constant(node)
+ if isinstance(node, ast.Num):
+ return str(node.n)
+ if isinstance(node, ast.Str):
+ # Fixme: escaping
+ return f'"{node.s}"'
+ return ''
+
+
+def format_literal_list(l_node, enclosing='{'):
+ """Formats a list/tuple of number/string literals as C++ initializer list"""
+ result = enclosing
+ for i, el in enumerate(l_node.elts):
+ if i > 0:
+ result += ', '
+ result += format_literal(el)
+ result += CLOSING[enclosing]
+ return result
+
+
+def format_member(attrib_node, qualifier_in='auto'):
+ """Member access foo->member() is expressed as an attribute with
+ further nested Attributes/Names as value"""
+ n = attrib_node
+ result = ''
+ # Black magic: Guess '::' if name appears to be a class name
+ qualifier = qualifier_in
+ if qualifier_in == 'auto':
+ qualifier = '::' if n.attr[0:1].isupper() else '->'
+ while isinstance(n, ast.Attribute):
+ result = n.attr if not result else n.attr + qualifier + result
+ n = n.value
+ if isinstance(n, ast.Name) and n.id != 'self':
+ if qualifier_in == 'auto' and n.id == "Qt": # Qt namespace
+ qualifier = "::"
+ result = n.id + qualifier + result
+ return result
+
+
+def format_reference(node, qualifier='auto'):
+ """Format member reference or free item"""
+ return node.id if isinstance(node, ast.Name) else format_member(node, qualifier)
+
+
+def format_function_def_arguments(function_def_node):
+ """Formats arguments of a function definition"""
+ # Default values is a list of the last default values, expand
+ # so that indexes match
+ argument_count = len(function_def_node.args.args)
+ default_values = function_def_node.args.defaults
+ while len(default_values) < argument_count:
+ default_values.insert(0, None)
+ result = ''
+ for i, a in enumerate(function_def_node.args.args):
+ if result:
+ result += ', '
+ if a.arg != 'self':
+ if a.annotation and isinstance(a.annotation, ast.Name):
+ result += _fix_function_argument_type(a.annotation.id, False) + ' '
+ result += a.arg
+ if default_values[i]:
+ result += ' = '
+ default_value = default_values[i]
+ if isinstance(default_value, ast.Attribute):
+ result += format_reference(default_value)
+ else:
+ result += format_literal(default_value)
+ return result
+
+
+def format_start_function_call(call_node):
+ """Format a call of a free or member function"""
+ return format_reference(call_node.func) + '('
+
+
+def write_import(file, i_node):
+ """Print an import of a Qt class as #include"""
+ for alias in i_node.names:
+ if alias.name.startswith('Q'):
+ file.write(f'#include <{alias.name}>\n')
+
+
+def write_import_from(file, i_node):
+ """Print an import from Qt classes as #include sequence"""
+ # "from PySide6.QtGui import QGuiApplication" or
+ # "from PySide6 import QtGui"
+ mod = i_node.module
+ if not mod.startswith('PySide') and not mod.startswith('PyQt'):
+ return
+ dot = mod.find('.')
+ qt_module = mod[dot + 1:] + '/' if dot >= 0 else ''
+ for i in i_node.names:
+ if i.name.startswith('Q'):
+ file.write(f'#include <{qt_module}{i.name}>\n')
+
+
+class Indenter:
+ """Helper for Indentation"""
+
+ def __init__(self, output_file):
+ self._indent_level = 0
+ self._indentation = ''
+ self._output_file = output_file
+
+ def indent_string(self, string):
+ """Start a new line by a string"""
+ self._output_file.write(self._indentation)
+ self._output_file.write(string)
+
+ def indent_line(self, line):
+ """Write an indented line"""
+ self._output_file.write(self._indentation)
+ self._output_file.write(line)
+ self._output_file.write('\n')
+
+ def INDENT(self):
+ """Write indentation"""
+ self._output_file.write(self._indentation)
+
+ def indent(self):
+ """Increase indentation level"""
+ self._indent_level = self._indent_level + 1
+ self._indentation = ' ' * self._indent_level
+
+ def dedent(self):
+ """Decrease indentation level"""
+ self._indent_level = self._indent_level - 1
+ self._indentation = ' ' * self._indent_level
+
+
+class CppFormatter(Indenter):
+ """Provides helpers for formatting multi-line C++ constructs"""
+
+ def __init__(self, output_file):
+ Indenter.__init__(self, output_file)
+
+ def write_class_def(self, class_node):
+ """Print a class definition with inheritance"""
+ self._output_file.write('\n')
+ inherits = format_inheritance(class_node)
+ self.indent_line(f'class {class_node.name}{inherits}')
+ self.indent_line('{')
+ self.indent_line('public:')
+
+ def write_function_def(self, f_node, class_context):
+ """Print a function definition with arguments"""
+ self._output_file.write('\n')
+ arguments = format_function_def_arguments(f_node)
+ if f_node.name == '__init__' and class_context: # Constructor
+ name = class_context
+ elif f_node.name == '__del__' and class_context: # Destructor
+ name = '~' + class_context
+ else:
+ return_type = "void"
+ if f_node.returns and isinstance(f_node.returns, ast.Name):
+ return_type = _fix_function_argument_type(f_node.returns.id, True)
+ name = return_type + " " + f_node.name
+ self.indent_string(f'{name}({arguments})')
+ self._output_file.write('\n')
+ self.indent_line('{')
diff --git a/sources/pyside-tools/qtpy2cpp_lib/nodedump.py b/sources/pyside-tools/qtpy2cpp_lib/nodedump.py
new file mode 100644
index 000000000..de62e9700
--- /dev/null
+++ b/sources/pyside-tools/qtpy2cpp_lib/nodedump.py
@@ -0,0 +1,50 @@
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+"""Helper to dump AST nodes for debugging"""
+
+
+import ast
+
+
+def to_string(node):
+ """Helper to retrieve a string from the (Lists of )Name/Attribute
+ aggregated into some nodes"""
+ if isinstance(node, ast.Name):
+ return node.id
+ if isinstance(node, ast.Attribute):
+ return node.attr
+ return ''
+
+
+def debug_format_node(node):
+ """Format AST node for debugging"""
+ if isinstance(node, ast.alias):
+ return f'alias("{node.name}")'
+ if isinstance(node, ast.arg):
+ return f'arg({node.arg})'
+ if isinstance(node, ast.Attribute):
+ if isinstance(node.value, ast.Name):
+ nested_name = debug_format_node(node.value)
+ return f'Attribute("{node.attr}", {nested_name})'
+ return f'Attribute("{node.attr}")'
+ if isinstance(node, ast.Call):
+ return 'Call({}({}))'.format(to_string(node.func), len(node.args))
+ if isinstance(node, ast.ClassDef):
+ base_names = [to_string(base) for base in node.bases]
+ bases = ': ' + ','.join(base_names) if base_names else ''
+ return f'ClassDef({node.name}{bases})'
+ if isinstance(node, ast.ImportFrom):
+ return f'ImportFrom("{node.module}")'
+ if isinstance(node, ast.FunctionDef):
+ arg_names = [a.arg for a in node.args.args]
+ return 'FunctionDef({}({}))'.format(node.name, ', '.join(arg_names))
+ if isinstance(node, ast.Name):
+ return 'Name("{}", Ctx={})'.format(node.id, type(node.ctx).__name__)
+ if isinstance(node, ast.NameConstant):
+ return f'NameConstant({node.value})'
+ if isinstance(node, ast.Num):
+ return f'Num({node.n})'
+ if isinstance(node, ast.Str):
+ return f'Str("{node.s}")'
+ return type(node).__name__
diff --git a/sources/pyside-tools/qtpy2cpp_lib/qt.py b/sources/pyside-tools/qtpy2cpp_lib/qt.py
new file mode 100644
index 000000000..69bd54aeb
--- /dev/null
+++ b/sources/pyside-tools/qtpy2cpp_lib/qt.py
@@ -0,0 +1,56 @@
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+"""Provides some type information on Qt classes"""
+
+
+from enum import Flag
+
+
+class ClassFlag(Flag):
+ PASS_BY_CONSTREF = 1
+ PASS_BY_REF = 2
+ PASS_BY_VALUE = 4
+ PASS_ON_STACK_MASK = PASS_BY_CONSTREF | PASS_BY_REF | PASS_BY_VALUE
+ INSTANTIATE_ON_STACK = 8
+
+
+_QT_CLASS_FLAGS = {
+ "QBrush": ClassFlag.PASS_BY_CONSTREF | ClassFlag.INSTANTIATE_ON_STACK,
+ "QGradient": ClassFlag.PASS_BY_CONSTREF | ClassFlag.INSTANTIATE_ON_STACK,
+ "QIcon": ClassFlag.PASS_BY_CONSTREF | ClassFlag.INSTANTIATE_ON_STACK,
+ "QLine": ClassFlag.PASS_BY_CONSTREF | ClassFlag.INSTANTIATE_ON_STACK,
+ "QLineF": ClassFlag.PASS_BY_CONSTREF | ClassFlag.INSTANTIATE_ON_STACK,
+ "QPixmap": ClassFlag.PASS_BY_CONSTREF | ClassFlag.INSTANTIATE_ON_STACK,
+ "QPointF": ClassFlag.PASS_BY_CONSTREF | ClassFlag.INSTANTIATE_ON_STACK,
+ "QRect": ClassFlag.PASS_BY_CONSTREF | ClassFlag.INSTANTIATE_ON_STACK,
+ "QRectF": ClassFlag.PASS_BY_CONSTREF | ClassFlag.INSTANTIATE_ON_STACK,
+ "QSizeF": ClassFlag.PASS_BY_CONSTREF | ClassFlag.INSTANTIATE_ON_STACK,
+ "QString": ClassFlag.PASS_BY_CONSTREF | ClassFlag.INSTANTIATE_ON_STACK,
+ "QFile": ClassFlag.PASS_BY_REF | ClassFlag.INSTANTIATE_ON_STACK,
+ "QSettings": ClassFlag.PASS_BY_REF | ClassFlag.INSTANTIATE_ON_STACK,
+ "QTextStream": ClassFlag.PASS_BY_REF | ClassFlag.INSTANTIATE_ON_STACK,
+ "QColor": ClassFlag.PASS_BY_VALUE | ClassFlag.INSTANTIATE_ON_STACK,
+ "QPoint": ClassFlag.PASS_BY_VALUE | ClassFlag.INSTANTIATE_ON_STACK,
+ "QSize": ClassFlag.PASS_BY_VALUE | ClassFlag.INSTANTIATE_ON_STACK,
+ "QApplication": ClassFlag.INSTANTIATE_ON_STACK,
+ "QColorDialog": ClassFlag.INSTANTIATE_ON_STACK,
+ "QCoreApplication": ClassFlag.INSTANTIATE_ON_STACK,
+ "QFileDialog": ClassFlag.INSTANTIATE_ON_STACK,
+ "QFileInfo": ClassFlag.INSTANTIATE_ON_STACK,
+ "QFontDialog": ClassFlag.INSTANTIATE_ON_STACK,
+ "QGuiApplication": ClassFlag.INSTANTIATE_ON_STACK,
+ "QMessageBox": ClassFlag.INSTANTIATE_ON_STACK,
+ "QPainter": ClassFlag.INSTANTIATE_ON_STACK,
+ "QPen": ClassFlag.INSTANTIATE_ON_STACK,
+ "QQmlApplicationEngine": ClassFlag.INSTANTIATE_ON_STACK,
+ "QQmlComponent": ClassFlag.INSTANTIATE_ON_STACK,
+ "QQmlEngine": ClassFlag.INSTANTIATE_ON_STACK,
+ "QQuickView": ClassFlag.INSTANTIATE_ON_STACK,
+ "QSaveFile": ClassFlag.INSTANTIATE_ON_STACK
+}
+
+
+def qt_class_flags(type):
+ f = _QT_CLASS_FLAGS.get(type)
+ return f if f else ClassFlag(0)
diff --git a/sources/pyside-tools/qtpy2cpp_lib/tests/baseline/basic_test.cpp b/sources/pyside-tools/qtpy2cpp_lib/tests/baseline/basic_test.cpp
new file mode 100644
index 000000000..8ee7be31e
--- /dev/null
+++ b/sources/pyside-tools/qtpy2cpp_lib/tests/baseline/basic_test.cpp
@@ -0,0 +1,62 @@
+// Copyright (C) 2022 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+
+// Converted from basic_test.py
+#include <QtCore/Qt>
+#include <QtGui/QColor>
+#include <QtGui/QPainter>
+#include <QtGui/QPaintEvent>
+#include <QtGui/QShortcut>
+#include <QtWidgets/QApplication>
+#include <QtWidgets/QWidget>
+
+class Window : public QWidget
+{
+public:
+
+ Window(QWidget * parent = nullptr)
+ {
+ super()->__init__(parent);
+ }
+
+ void paintEvent(QPaintEvent * e)
+ {
+ paint("bla");
+ }
+
+ void paint(const QString & what, color = Qt::blue)
+ {
+ { // Converted from context manager
+ p = QPainter();
+ p->setPen(QColor(color));
+ rect = rect();
+ w = rect->width();
+ h = rect->height();
+ p->drawLine(0, 0, w, h);
+ p->drawLine(0, h, w, 0);
+ p->drawText(rect->center(), what);
+ }
+ }
+
+ void sum()
+ {
+ values = {1, 2, 3};
+ result = 0;
+ for (v: values) {
+ result += v
+ }
+ return result;
+ }
+};
+
+int main(int argc, char *argv[])
+{
+ QApplication app(sys->argv);
+ window = Window();
+ auto *sc = new QShortcut((Qt::CTRL | Qt::Key_Q), window);
+ sc->activated->connect(window->close);
+ window->setWindowTitle("Test");
+ window->show();
+ sys->exit(app.exec());
+ return 0;
+}
diff --git a/sources/pyside-tools/qtpy2cpp_lib/tests/baseline/basic_test.py b/sources/pyside-tools/qtpy2cpp_lib/tests/baseline/basic_test.py
new file mode 100644
index 000000000..1466ac6b1
--- /dev/null
+++ b/sources/pyside-tools/qtpy2cpp_lib/tests/baseline/basic_test.py
@@ -0,0 +1,44 @@
+#!/usr/bin/env python
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+
+import sys
+
+from PySide6.QtCore import Qt
+from PySide6.QtGui import QColor, QPainter, QPaintEvent, QShortcut
+from PySide6.QtWidgets import QApplication, QWidget
+
+
+class Window(QWidget):
+ def __init__(self, parent: QWidget = None):
+ super().__init__(parent)
+
+ def paintEvent(self, e: QPaintEvent):
+ self.paint("bla")
+
+ def paint(self, what: str, color: Qt.GlobalColor = Qt.blue):
+ with QPainter(self) as p:
+ p.setPen(QColor(color))
+ rect = self.rect()
+ w = rect.width()
+ h = rect.height()
+ p.drawLine(0, 0, w, h)
+ p.drawLine(0, h, w, 0)
+ p.drawText(rect.center(), what)
+
+ def sum(self):
+ values = [1, 2, 3]
+ result = 0
+ for v in values:
+ result += v
+ return result
+
+
+if __name__ == '__main__':
+ app = QApplication(sys.argv)
+ window = Window()
+ sc = QShortcut(Qt.CTRL | Qt.Key_Q, window)
+ sc.activated.connect(window.close)
+ window.setWindowTitle("Test")
+ window.show()
+ sys.exit(app.exec())
diff --git a/sources/pyside-tools/qtpy2cpp_lib/tests/test_qtpy2cpp.py b/sources/pyside-tools/qtpy2cpp_lib/tests/test_qtpy2cpp.py
new file mode 100644
index 000000000..894b2a958
--- /dev/null
+++ b/sources/pyside-tools/qtpy2cpp_lib/tests/test_qtpy2cpp.py
@@ -0,0 +1,54 @@
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+import subprocess
+import tempfile
+import sys
+from pathlib import Path
+
+# run pytest-3
+
+
+def diff_code(actual_code, expected_file):
+ """Helper to run diff if something fails (Linux only)."""
+ with tempfile.NamedTemporaryFile(suffix=".cpp") as tf:
+ tf.write(actual_code.encode('utf-8'))
+ tf.flush()
+ diff_cmd = ["diff", "-u", expected_file, tf.name]
+ subprocess.run(diff_cmd)
+
+
+def run_converter(tool, file):
+ """Run the converter and return C++ code generated from file."""
+ cmd = [sys.executable, tool, "--stdout", file]
+ output = ""
+ with subprocess.Popen(cmd, stdout=subprocess.PIPE) as proc:
+ output_b, errors_b = proc.communicate()
+ output = output_b.decode('utf-8')
+ if errors_b:
+ print(errors_b.decode('utf-8'), file=sys.stderr)
+ return output
+
+
+def test_examples():
+ dir = Path(__file__).resolve().parent
+ tool = dir.parents[1] / "qtpy2cpp.py"
+ assert tool.is_file
+ for test_file in (dir / "baseline").glob("*.py"):
+ assert test_file.is_file
+ expected_file = test_file.parent / (test_file.stem + ".cpp")
+ if expected_file.is_file():
+ actual_code = run_converter(tool, test_file)
+ assert actual_code
+ expected_code = expected_file.read_text()
+ # Strip the license
+ code_start = expected_code.find("// Converted from")
+ assert code_start != -1
+ expected_code = expected_code[code_start:]
+
+ if actual_code != expected_code:
+ diff_code(actual_code, expected_file)
+ assert actual_code == expected_code
+ else:
+ print(f"Warning, {test_file} is missing a .cpp file.",
+ file=sys.stderr)
diff --git a/sources/pyside-tools/qtpy2cpp_lib/tokenizer.py b/sources/pyside-tools/qtpy2cpp_lib/tokenizer.py
new file mode 100644
index 000000000..d5e26c2a8
--- /dev/null
+++ b/sources/pyside-tools/qtpy2cpp_lib/tokenizer.py
@@ -0,0 +1,55 @@
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+"""Tool to dump Python Tokens"""
+
+
+import sys
+import tokenize
+
+
+def format_token(t):
+ r = repr(t)
+ if r.startswith('TokenInfo('):
+ r = r[10:]
+ pos = r.find("), line='")
+ if pos < 0:
+ pos = r.find('), line="')
+ if pos > 0:
+ r = r[:pos + 1]
+ return r
+
+
+def first_non_space(s):
+ for i, c in enumerate(s):
+ if c != ' ':
+ return i
+ return 0
+
+
+if __name__ == '__main__':
+ if len(sys.argv) < 2:
+ print("Specify file Name")
+ sys.exit(1)
+ filename = sys.argv[1]
+ indent_level = 0
+ indent = ''
+ last_line_number = -1
+ with tokenize.open(filename) as f:
+ generator = tokenize.generate_tokens(f.readline)
+ for t in generator:
+ line_number = t.start[0]
+ if line_number != last_line_number:
+ code_line = t.line.rstrip()
+ non_space = first_non_space(code_line)
+ print('{:04d} {}{}'.format(line_number, '_' * non_space,
+ code_line[non_space:]))
+ last_line_number = line_number
+ if t.type == tokenize.INDENT:
+ indent_level = indent_level + 1
+ indent = ' ' * indent_level
+ elif t.type == tokenize.DEDENT:
+ indent_level = indent_level - 1
+ indent = ' ' * indent_level
+ else:
+ print(' ', indent, format_token(t))
diff --git a/sources/pyside-tools/qtpy2cpp_lib/visitor.py b/sources/pyside-tools/qtpy2cpp_lib/visitor.py
new file mode 100644
index 000000000..2056951ae
--- /dev/null
+++ b/sources/pyside-tools/qtpy2cpp_lib/visitor.py
@@ -0,0 +1,442 @@
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+"""AST visitor printing out C++"""
+
+import ast
+import sys
+import tokenize
+import warnings
+
+from .formatter import (CppFormatter, format_for_loop, format_literal,
+ format_name_constant,
+ format_reference, write_import, write_import_from)
+from .nodedump import debug_format_node
+from .qt import ClassFlag, qt_class_flags
+
+
+def _is_qt_constructor(assign_node):
+ """Is this assignment node a plain construction of a Qt class?
+ 'f = QFile(name)'. Returns the class_name."""
+ call = assign_node.value
+ if (isinstance(call, ast.Call) and isinstance(call.func, ast.Name)):
+ func = call.func.id
+ if func.startswith("Q"):
+ return func
+ return None
+
+
+def _is_if_main(if_node):
+ """Return whether an if statement is: if __name__ == '__main__' """
+ test = if_node.test
+ return (isinstance(test, ast.Compare)
+ and len(test.ops) == 1
+ and isinstance(test.ops[0], ast.Eq)
+ and isinstance(test.left, ast.Name)
+ and test.left.id == "__name__"
+ and len(test.comparators) == 1
+ and isinstance(test.comparators[0], ast.Constant)
+ and test.comparators[0].value == "__main__")
+
+
+class ConvertVisitor(ast.NodeVisitor, CppFormatter):
+ """AST visitor printing out C++
+ Note on implementation:
+ - Any visit_XXX() overridden function should call self.generic_visit(node)
+ to continue visiting
+ - When controlling the visiting manually (cf visit_Call()),
+ self.visit(child) needs to be called since that dispatches to
+ visit_XXX(). This is usually done to prevent undesired output
+ for example from references of calls, etc.
+ """
+
+ debug = False
+
+ def __init__(self, file_name, output_file):
+ ast.NodeVisitor.__init__(self)
+ CppFormatter.__init__(self, output_file)
+ self._file_name = file_name
+ self._class_scope = [] # List of class names
+ self._stack = [] # nodes
+ self._stack_variables = [] # variables instantiated on stack
+ self._debug_indent = 0
+
+ @staticmethod
+ def create_ast(filename):
+ """Create an Abstract Syntax Tree on which a visitor can be run"""
+ node = None
+ with tokenize.open(filename) as file:
+ node = ast.parse(file.read(), mode="exec")
+ return node
+
+ def generic_visit(self, node):
+ parent = self._stack[-1] if self._stack else None
+ if self.debug:
+ self._debug_enter(node, parent)
+ self._stack.append(node)
+ try:
+ super().generic_visit(node)
+ except Exception as e:
+ line_no = node.lineno if hasattr(node, 'lineno') else -1
+ error_message = str(e)
+ message = f'{self._file_name}:{line_no}: Error "{error_message}"'
+ warnings.warn(message)
+ self._output_file.write(f'\n// {error_message}\n')
+ del self._stack[-1]
+ if self.debug:
+ self._debug_leave(node)
+
+ def visit_Add(self, node):
+ self._handle_bin_op(node, "+")
+
+ def _is_augmented_assign(self):
+ """Is it 'Augmented_assign' (operators +=/-=, etc)?"""
+ return self._stack and isinstance(self._stack[-1], ast.AugAssign)
+
+ def visit_AugAssign(self, node):
+ """'Augmented_assign', Operators +=/-=, etc."""
+ self.INDENT()
+ self.generic_visit(node)
+ self._output_file.write("\n")
+
+ def visit_Assign(self, node):
+ self.INDENT()
+
+ qt_class = _is_qt_constructor(node)
+ on_stack = qt_class and qt_class_flags(qt_class) & ClassFlag.INSTANTIATE_ON_STACK
+
+ # Is this a free variable and not a member assignment? Instantiate
+ # on stack or give a type
+ if len(node.targets) == 1 and isinstance(node.targets[0], ast.Name):
+ if qt_class:
+ if on_stack:
+ # "QFile f(args)"
+ var = node.targets[0].id
+ self._stack_variables.append(var)
+ self._output_file.write(f"{qt_class} {var}(")
+ self._write_function_args(node.value.args)
+ self._output_file.write(");\n")
+ return
+ self._output_file.write("auto *")
+
+ line_no = node.lineno if hasattr(node, 'lineno') else -1
+ for target in node.targets:
+ if isinstance(target, ast.Tuple):
+ w = f"{self._file_name}:{line_no}: List assignment not handled."
+ warnings.warn(w)
+ elif isinstance(target, ast.Subscript):
+ w = f"{self._file_name}:{line_no}: Subscript assignment not handled."
+ warnings.warn(w)
+ else:
+ self._output_file.write(format_reference(target))
+ self._output_file.write(' = ')
+ if qt_class and not on_stack:
+ self._output_file.write("new ")
+ self.visit(node.value)
+ self._output_file.write(';\n')
+
+ def visit_Attribute(self, node):
+ """Format a variable reference (cf visit_Name)"""
+ # Default parameter (like Qt::black)?
+ if self._ignore_function_def_node(node):
+ return
+ self._output_file.write(format_reference(node))
+
+ def visit_BinOp(self, node):
+ # Parentheses are not exposed, so, every binary operation needs to
+ # be enclosed by ().
+ self._output_file.write('(')
+ self.generic_visit(node)
+ self._output_file.write(')')
+
+ def _handle_bin_op(self, node, op):
+ """Handle a binary operator which can appear as 'Augmented Assign'."""
+ self.generic_visit(node)
+ full_op = f" {op}= " if self._is_augmented_assign() else f" {op} "
+ self._output_file.write(full_op)
+
+ def visit_BitAnd(self, node):
+ self._handle_bin_op(node, "&")
+
+ def visit_BitOr(self, node):
+ self._handle_bin_op(node, "|")
+
+ def _format_call(self, node):
+ # Decorator list?
+ if self._ignore_function_def_node(node):
+ return
+ f = node.func
+ if isinstance(f, ast.Name):
+ self._output_file.write(f.id)
+ else:
+ # Attributes denoting chained calls "a->b()->c()". Walk along in
+ # reverse order, recursing for other calls.
+ names = []
+ n = f
+ while isinstance(n, ast.Attribute):
+ names.insert(0, n.attr)
+ n = n.value
+
+ if isinstance(n, ast.Name): # Member or variable reference
+ if n.id != "self":
+ sep = "->"
+ if n.id in self._stack_variables:
+ sep = "."
+ elif n.id[0:1].isupper(): # Heuristics for static
+ sep = "::"
+ self._output_file.write(n.id)
+ self._output_file.write(sep)
+ elif isinstance(n, ast.Call): # A preceding call
+ self._format_call(n)
+ self._output_file.write("->")
+
+ self._output_file.write("->".join(names))
+
+ self._output_file.write('(')
+ self._write_function_args(node.args)
+ self._output_file.write(')')
+
+ def visit_Call(self, node):
+ self._format_call(node)
+ # Context manager expression?
+ if self._within_context_manager():
+ self._output_file.write(";\n")
+
+ def _write_function_args(self, args_node):
+ # Manually do visit(), skip the children of func
+ for i, arg in enumerate(args_node):
+ if i > 0:
+ self._output_file.write(', ')
+ self.visit(arg)
+
+ def visit_ClassDef(self, node):
+ # Manually do visit() to skip over base classes
+ # and annotations
+ self._class_scope.append(node.name)
+ self.write_class_def(node)
+ self.indent()
+ for b in node.body:
+ self.visit(b)
+ self.dedent()
+ self.indent_line('};')
+ del self._class_scope[-1]
+
+ def visit_Div(self, node):
+ self._handle_bin_op(node, "/")
+
+ def visit_Eq(self, node):
+ self.generic_visit(node)
+ self._output_file.write(" == ")
+
+ def visit_Expr(self, node):
+ self.INDENT()
+ self.generic_visit(node)
+ self._output_file.write(';\n')
+
+ def visit_Gt(self, node):
+ self.generic_visit(node)
+ self._output_file.write(" > ")
+
+ def visit_GtE(self, node):
+ self.generic_visit(node)
+ self._output_file.write(" >= ")
+
+ def visit_For(self, node):
+ # Manually do visit() to get the indentation right.
+ # TODO: what about orelse?
+ self.indent_line(format_for_loop(node))
+ self.indent()
+ for b in node.body:
+ self.visit(b)
+ self.dedent()
+ self.indent_line('}')
+
+ def visit_FunctionDef(self, node):
+ class_context = self._class_scope[-1] if self._class_scope else None
+ for decorator in node.decorator_list:
+ func = decorator.func # (Call)
+ if isinstance(func, ast.Name) and func.id == "Slot":
+ self._output_file.write("\npublic slots:")
+ self.write_function_def(node, class_context)
+ # Find stack variables
+ for arg in node.args.args:
+ if arg.annotation and isinstance(arg.annotation, ast.Name):
+ type_name = arg.annotation.id
+ flags = qt_class_flags(type_name)
+ if flags & ClassFlag.PASS_ON_STACK_MASK:
+ self._stack_variables.append(arg.arg)
+ self.indent()
+ self.generic_visit(node)
+ self.dedent()
+ self.indent_line('}')
+ self._stack_variables.clear()
+
+ def visit_If(self, node):
+ # Manually do visit() to get the indentation right. Note:
+ # elsif() is modelled as nested if.
+
+ # Check for the main function
+ if _is_if_main(node):
+ self._output_file.write("\nint main(int argc, char *argv[])\n{\n")
+ self.indent()
+ for b in node.body:
+ self.visit(b)
+ self.indent_string("return 0;\n")
+ self.dedent()
+ self._output_file.write("}\n")
+ return
+
+ self.indent_string('if (')
+ self.visit(node.test)
+ self._output_file.write(') {\n')
+ self.indent()
+ for b in node.body:
+ self.visit(b)
+ self.dedent()
+ self.indent_string('}')
+ if node.orelse:
+ self._output_file.write(' else {\n')
+ self.indent()
+ for b in node.orelse:
+ self.visit(b)
+ self.dedent()
+ self.indent_string('}')
+ self._output_file.write('\n')
+
+ def visit_Import(self, node):
+ write_import(self._output_file, node)
+
+ def visit_ImportFrom(self, node):
+ write_import_from(self._output_file, node)
+
+ def visit_List(self, node):
+ # Manually do visit() to get separators right
+ self._output_file.write('{')
+ for i, el in enumerate(node.elts):
+ if i > 0:
+ self._output_file.write(', ')
+ self.visit(el)
+ self._output_file.write('}')
+
+ def visit_LShift(self, node):
+ self.generic_visit(node)
+ self._output_file.write(" << ")
+
+ def visit_Lt(self, node):
+ self.generic_visit(node)
+ self._output_file.write(" < ")
+
+ def visit_LtE(self, node):
+ self.generic_visit(node)
+ self._output_file.write(" <= ")
+
+ def visit_Mult(self, node):
+ self._handle_bin_op(node, "*")
+
+ def _within_context_manager(self):
+ """Return whether we are within a context manager (with)."""
+ parent = self._stack[-1] if self._stack else None
+ return parent and isinstance(parent, ast.withitem)
+
+ def _ignore_function_def_node(self, node):
+ """Should this node be ignored within a FunctionDef."""
+ if not self._stack:
+ return False
+ parent = self._stack[-1]
+ # A type annotation or default value of an argument?
+ if isinstance(parent, (ast.arguments, ast.arg)):
+ return True
+ if not isinstance(parent, ast.FunctionDef):
+ return False
+ # Return type annotation or decorator call
+ return node == parent.returns or node in parent.decorator_list
+
+ def visit_Index(self, node):
+ self._output_file.write("[")
+ self.generic_visit(node)
+ self._output_file.write("]")
+
+ def visit_Name(self, node):
+ """Format a variable reference (cf visit_Attribute)"""
+ # Skip Context manager variables, return or argument type annotation
+ if self._within_context_manager() or self._ignore_function_def_node(node):
+ return
+ self._output_file.write(format_reference(node))
+
+ def visit_NameConstant(self, node):
+ # Default parameter?
+ if self._ignore_function_def_node(node):
+ return
+ self.generic_visit(node)
+ self._output_file.write(format_name_constant(node))
+
+ def visit_Not(self, node):
+ self.generic_visit(node)
+ self._output_file.write("!")
+
+ def visit_NotEq(self, node):
+ self.generic_visit(node)
+ self._output_file.write(" != ")
+
+ def visit_Num(self, node):
+ self.generic_visit(node)
+ self._output_file.write(format_literal(node))
+
+ def visit_RShift(self, node):
+ self.generic_visit(node)
+ self._output_file.write(" >> ")
+
+ def visit_Return(self, node):
+ self.indent_string("return")
+ if node.value:
+ self._output_file.write(" ")
+ self.generic_visit(node)
+ self._output_file.write(";\n")
+
+ def visit_Slice(self, node):
+ self._output_file.write("[")
+ if node.lower:
+ self.visit(node.lower)
+ self._output_file.write(":")
+ if node.upper:
+ self.visit(node.upper)
+ self._output_file.write("]")
+
+ def visit_Str(self, node):
+ self.generic_visit(node)
+ self._output_file.write(format_literal(node))
+
+ def visit_Sub(self, node):
+ self._handle_bin_op(node, "-")
+
+ def visit_UnOp(self, node):
+ self.generic_visit(node)
+
+ def visit_With(self, node):
+ self.INDENT()
+ self._output_file.write("{ // Converted from context manager\n")
+ self.indent()
+ for item in node.items:
+ self.INDENT()
+ if item.optional_vars:
+ self._output_file.write(format_reference(item.optional_vars))
+ self._output_file.write(" = ")
+ self.generic_visit(node)
+ self.dedent()
+ self.INDENT()
+ self._output_file.write("}\n")
+
+ def _debug_enter(self, node, parent=None):
+ message = '{}>generic_visit({})'.format(' ' * self ._debug_indent,
+ debug_format_node(node))
+ if parent:
+ message += ', parent={}'.format(debug_format_node(parent))
+ message += '\n'
+ sys.stderr.write(message)
+ self._debug_indent += 1
+
+ def _debug_leave(self, node):
+ self._debug_indent -= 1
+ message = '{}<generic_visit({})\n'.format(' ' * self ._debug_indent,
+ type(node).__name__)
+ sys.stderr.write(message)