diff options
Diffstat (limited to 'sources/pyside-tools/qtpy2cpp_lib')
-rw-r--r-- | sources/pyside-tools/qtpy2cpp_lib/astdump.py | 111 | ||||
-rw-r--r-- | sources/pyside-tools/qtpy2cpp_lib/formatter.py | 265 | ||||
-rw-r--r-- | sources/pyside-tools/qtpy2cpp_lib/nodedump.py | 50 | ||||
-rw-r--r-- | sources/pyside-tools/qtpy2cpp_lib/qt.py | 56 | ||||
-rw-r--r-- | sources/pyside-tools/qtpy2cpp_lib/tests/baseline/basic_test.cpp | 62 | ||||
-rw-r--r-- | sources/pyside-tools/qtpy2cpp_lib/tests/baseline/basic_test.py | 44 | ||||
-rw-r--r-- | sources/pyside-tools/qtpy2cpp_lib/tests/test_qtpy2cpp.py | 54 | ||||
-rw-r--r-- | sources/pyside-tools/qtpy2cpp_lib/tokenizer.py | 55 | ||||
-rw-r--r-- | sources/pyside-tools/qtpy2cpp_lib/visitor.py | 442 |
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) |