diff options
author | Friedemann Kleint <Friedemann.Kleint@qt.io> | 2019-12-20 13:08:25 +0100 |
---|---|---|
committer | Friedemann Kleint <Friedemann.Kleint@qt.io> | 2020-03-05 11:20:18 +0100 |
commit | e88f08c180bc30aa52887bd5cc27fe0fe1a33b2b (patch) | |
tree | f7677ee1ac84afbb7ad8556f2de69522a03a2c16 /tools | |
parent | 4f1739e062623d3ab8aebe9540e945464734a75b (diff) |
Add Python to Qt/C++ conversion tool
Change-Id: I275e776248bd55c3c38f5fedd83088bf475a1cf9
Reviewed-by: Christian Tismer <tismer@stackless.com>
Diffstat (limited to 'tools')
-rw-r--r-- | tools/qtpy2cpp.py | 99 | ||||
-rw-r--r-- | tools/qtpy2cpp.pyproject | 6 | ||||
-rw-r--r-- | tools/qtpy2cpp_lib/astdump.py | 149 | ||||
-rw-r--r-- | tools/qtpy2cpp_lib/formatter.py | 264 | ||||
-rw-r--r-- | tools/qtpy2cpp_lib/nodedump.py | 86 | ||||
-rw-r--r-- | tools/qtpy2cpp_lib/test_baseline/basic_test.py | 38 | ||||
-rw-r--r-- | tools/qtpy2cpp_lib/test_baseline/uic.py | 208 | ||||
-rw-r--r-- | tools/qtpy2cpp_lib/tokenizer.py | 91 | ||||
-rw-r--r-- | tools/qtpy2cpp_lib/visitor.py | 260 |
9 files changed, 1201 insertions, 0 deletions
diff --git a/tools/qtpy2cpp.py b/tools/qtpy2cpp.py new file mode 100644 index 000000000..52bff787d --- /dev/null +++ b/tools/qtpy2cpp.py @@ -0,0 +1,99 @@ +############################################################################# +## +## Copyright (C) 2020 The Qt Company Ltd. +## Contact: https://www.qt.io/licensing/ +## +## This file is part of the Qt for Python project. +## +## $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 argparse import ArgumentParser, RawTextHelpFormatter +import logging +import os +import sys +from qtpy2cpp_lib.visitor import ConvertVisitor + + +DESCRIPTION = "Tool to convert Python to C++" + + +def create_arg_parser(desc): + parser = ArgumentParser(description=desc, + formatter_class=RawTextHelpFormatter) + parser.add_argument('--debug', '-d', action='store_true', + help='Debug') + parser.add_argument('--stdout', '-s', action='store_true', + help='Write to stdout') + parser.add_argument('--force', '-f', action='store_true', + help='Force overwrite of existing files') + parser.add_argument('file', type=str, help='Python source file') + return parser + + +if __name__ == '__main__': + if sys.version_info < (3, 6, 0): + raise Exception("This script requires Python 3.6") + logging.basicConfig(level=logging.INFO) + logger = logging.getLogger(__name__) + arg_parser = create_arg_parser(DESCRIPTION) + args = arg_parser.parse_args() + ConvertVisitor.debug = args.debug + + input_file = args.file + if not os.path.isfile(input_file): + logger.error(f'{input_file} does not exist or is not a file.') + sys.exit(-1) + file_root, ext = os.path.splitext(input_file) + if ext != '.py': + logger.error(f'{input_file} does not appear to be a Python file.') + sys.exit(-1) + + ast_tree = ConvertVisitor.create_ast(input_file) + if args.stdout: + sys.stdout.write(f'// Converted from {input_file}\n') + ConvertVisitor(sys.stdout).visit(ast_tree) + sys.exit(0) + + target_file = file_root + '.cpp' + if os.path.exists(target_file): + if not os.path.isfile(target_file): + logger.error(f'{target_file} exists and is not a file.') + sys.exit(-1) + if not args.force: + logger.error(f'{target_file} exists. Use -f to overwrite.') + sys.exit(-1) + + with open(target_file, "w") as file: + file.write(f'// Converted from {input_file}\n') + ConvertVisitor(file).visit(ast_tree) + logger.info(f"Wrote {target_file} ...") diff --git a/tools/qtpy2cpp.pyproject b/tools/qtpy2cpp.pyproject new file mode 100644 index 000000000..a9d223a4d --- /dev/null +++ b/tools/qtpy2cpp.pyproject @@ -0,0 +1,6 @@ +{ + "files": ["qtpy2cpp.py", + "qtpy2cpp_lib/formatter.py", "qtpy2cpp_lib/visitor.py", "qtpy2cpp_lib/nodedump.py", + "qtpy2cpp_lib/astdump.py", "qtpy2cpp_lib/tokenizer.py", + "qtpy2cpp_lib/test_baseline/basic_test.py", "qtpy2cpp_lib/test_baseline/uic.py"] +} diff --git a/tools/qtpy2cpp_lib/astdump.py b/tools/qtpy2cpp_lib/astdump.py new file mode 100644 index 000000000..ea37590c2 --- /dev/null +++ b/tools/qtpy2cpp_lib/astdump.py @@ -0,0 +1,149 @@ +############################################################################# +## +## Copyright (C) 2020 The Qt Company Ltd. +## Contact: https://www.qt.io/licensing/ +## +## This file is part of the Qt for Python project. +## +## $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$ +## +############################################################################# + +"""Tool to dump a Python AST""" + + +from argparse import ArgumentParser, RawTextHelpFormatter +import ast +from enum import Enum +import sys +import tokenize + + +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/tools/qtpy2cpp_lib/formatter.py b/tools/qtpy2cpp_lib/formatter.py new file mode 100644 index 000000000..81a920bbc --- /dev/null +++ b/tools/qtpy2cpp_lib/formatter.py @@ -0,0 +1,264 @@ +############################################################################# +## +## Copyright (C) 2020 The Qt Company Ltd. +## Contact: https://www.qt.io/licensing/ +## +## This file is part of the Qt for Python project. +## +## $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$ +## +############################################################################# + +"""C++ formatting helper functions and formatter class""" + + +import ast +import sys + + +CLOSING = {"{": "}", "(": ")", "[": "]"} # Closing parenthesis for C++ + + +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): + 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) + result += ') {' + return result + + +def format_literal(node): + """Returns the value of number/string literals""" + 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='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 + if qualifier == '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': + 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': + result += a.arg + if default_values[i]: + result += ' = ' + result += format_literal(default_values[i]) + 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 PySide2.QtGui import QGuiApplication" or + # "from PySide2 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) + warn = True + if f_node.name == '__init__' and class_context: # Constructor + name = class_context + warn = len(arguments) > 0 + elif f_node.name == '__del__' and class_context: # Destructor + name = '~' + class_context + warn = False + else: + name = 'void ' + f_node.name + self.indent_string(f'{name}({arguments})') + if warn: + self._output_file.write(' /* FIXME: types */') + self._output_file.write('\n') + self.indent_line('{') diff --git a/tools/qtpy2cpp_lib/nodedump.py b/tools/qtpy2cpp_lib/nodedump.py new file mode 100644 index 000000000..5cb7c3f2d --- /dev/null +++ b/tools/qtpy2cpp_lib/nodedump.py @@ -0,0 +1,86 @@ +############################################################################# +## +## Copyright (C) 2020 The Qt Company Ltd. +## Contact: https://www.qt.io/licensing/ +## +## This file is part of the Qt for Python project. +## +## $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$ +## +############################################################################# + +"""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/tools/qtpy2cpp_lib/test_baseline/basic_test.py b/tools/qtpy2cpp_lib/test_baseline/basic_test.py new file mode 100644 index 000000000..e5dc92f9f --- /dev/null +++ b/tools/qtpy2cpp_lib/test_baseline/basic_test.py @@ -0,0 +1,38 @@ +############################################################################# +## +## Copyright (C) 2020 The Qt Company Ltd. +## Contact: https://www.qt.io/licensing/ +## +## This file is part of the test suite of Qt for Python. +## +## $QT_BEGIN_LICENSE:GPL-EXCEPT$ +## 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 General Public License Usage +## Alternatively, this file may be used under the terms of the GNU +## General Public License version 3 as published by the Free Software +## Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +## 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-3.0.html. +## +## $QT_END_LICENSE$ +## +############################################################################# + +a = 7 + +if a > 5: + for f in [1, 2]: + print(f) +else: + for i in range(5): + print(i) + for i in range(2, 5): + print(i) diff --git a/tools/qtpy2cpp_lib/test_baseline/uic.py b/tools/qtpy2cpp_lib/test_baseline/uic.py new file mode 100644 index 000000000..fe97c7825 --- /dev/null +++ b/tools/qtpy2cpp_lib/test_baseline/uic.py @@ -0,0 +1,208 @@ +# -*- coding: utf-8 -*- +############################################################################# +## +## Copyright (C) 2020 The Qt Company Ltd. +## Contact: https://www.qt.io/licensing/ +## +## This file is part of the test suite of Qt for Python. +## +## $QT_BEGIN_LICENSE:GPL-EXCEPT$ +## 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 General Public License Usage +## Alternatively, this file may be used under the terms of the GNU +## General Public License version 3 as published by the Free Software +## Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +## 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-3.0.html. +## +## $QT_END_LICENSE$ +## +############################################################################# + +from PySide2.QtCore import (QCoreApplication, QMetaObject, QObject, QPoint, + QRect, QSize, QUrl, Qt) +from PySide2.QtGui import (QBrush, QColor, QConicalGradient, QFont, + QFontDatabase, QIcon, QLinearGradient, QPalette, QPainter, QPixmap, + QRadialGradient) +from PySide2.QtWidgets import * + +class Ui_ImageDialog(object): + def setupUi(self, dialog): + if dialog.objectName(): + dialog.setObjectName(u"dialog") + dialog.setObjectName(u"ImageDialog") + dialog.resize(320, 180) + self.vboxLayout = QVBoxLayout(dialog) +#ifndef Q_OS_MAC + self.vboxLayout.setSpacing(6) +#endif +#ifndef Q_OS_MAC + self.vboxLayout.setContentsMargins(9, 9, 9, 9) +#endif + self.vboxLayout.setObjectName(u"vboxLayout") + self.vboxLayout.setObjectName(u"") + self.gridLayout = QGridLayout() +#ifndef Q_OS_MAC + self.gridLayout.setSpacing(6) +#endif + self.gridLayout.setContentsMargins(1, 1, 1, 1) + self.gridLayout.setObjectName(u"gridLayout") + self.gridLayout.setObjectName(u"") + self.widthLabel = QLabel(dialog) + self.widthLabel.setObjectName(u"widthLabel") + self.widthLabel.setObjectName(u"widthLabel") + self.widthLabel.setGeometry(QRect(1, 27, 67, 22)) + self.widthLabel.setFrameShape(QFrame.NoFrame) + self.widthLabel.setFrameShadow(QFrame.Plain) + self.widthLabel.setTextFormat(Qt.AutoText) + + self.gridLayout.addWidget(self.widthLabel, 1, 0, 1, 1) + + self.heightLabel = QLabel(dialog) + self.heightLabel.setObjectName(u"heightLabel") + self.heightLabel.setObjectName(u"heightLabel") + self.heightLabel.setGeometry(QRect(1, 55, 67, 22)) + self.heightLabel.setFrameShape(QFrame.NoFrame) + self.heightLabel.setFrameShadow(QFrame.Plain) + self.heightLabel.setTextFormat(Qt.AutoText) + + self.gridLayout.addWidget(self.heightLabel, 2, 0, 1, 1) + + self.colorDepthCombo = QComboBox(dialog) + self.colorDepthCombo.setObjectName(u"colorDepthCombo") + self.colorDepthCombo.setObjectName(u"colorDepthCombo") + self.colorDepthCombo.setGeometry(QRect(74, 83, 227, 22)) + sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.colorDepthCombo.sizePolicy().hasHeightForWidth()) + self.colorDepthCombo.setSizePolicy(sizePolicy) + self.colorDepthCombo.setInsertPolicy(QComboBox.InsertAtBottom) + + self.gridLayout.addWidget(self.colorDepthCombo, 3, 1, 1, 1) + + self.nameLineEdit = QLineEdit(dialog) + self.nameLineEdit.setObjectName(u"nameLineEdit") + self.nameLineEdit.setObjectName(u"nameLineEdit") + self.nameLineEdit.setGeometry(QRect(74, 83, 227, 22)) + sizePolicy1 = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed) + sizePolicy1.setHorizontalStretch(1) + sizePolicy1.setVerticalStretch(0) + sizePolicy1.setHeightForWidth(self.nameLineEdit.sizePolicy().hasHeightForWidth()) + self.nameLineEdit.setSizePolicy(sizePolicy1) + self.nameLineEdit.setEchoMode(QLineEdit.Normal) + + self.gridLayout.addWidget(self.nameLineEdit, 0, 1, 1, 1) + + self.spinBox = QSpinBox(dialog) + self.spinBox.setObjectName(u"spinBox") + self.spinBox.setObjectName(u"spinBox") + self.spinBox.setGeometry(QRect(74, 1, 227, 20)) + sizePolicy.setHeightForWidth(self.spinBox.sizePolicy().hasHeightForWidth()) + self.spinBox.setSizePolicy(sizePolicy) + self.spinBox.setButtonSymbols(QAbstractSpinBox.UpDownArrows) + self.spinBox.setValue(32) + self.spinBox.setMaximum(1024) + self.spinBox.setMinimum(1) + + self.gridLayout.addWidget(self.spinBox, 1, 1, 1, 1) + + self.spinBox_2 = QSpinBox(dialog) + self.spinBox_2.setObjectName(u"spinBox_2") + self.spinBox_2.setObjectName(u"spinBox_2") + self.spinBox_2.setGeometry(QRect(74, 27, 227, 22)) + sizePolicy.setHeightForWidth(self.spinBox_2.sizePolicy().hasHeightForWidth()) + self.spinBox_2.setSizePolicy(sizePolicy) + self.spinBox_2.setButtonSymbols(QAbstractSpinBox.UpDownArrows) + self.spinBox_2.setValue(32) + self.spinBox_2.setMaximum(1024) + self.spinBox_2.setMinimum(1) + + self.gridLayout.addWidget(self.spinBox_2, 2, 1, 1, 1) + + self.nameLabel = QLabel(dialog) + self.nameLabel.setObjectName(u"nameLabel") + self.nameLabel.setObjectName(u"nameLabel") + self.nameLabel.setGeometry(QRect(1, 1, 67, 20)) + self.nameLabel.setFrameShape(QFrame.NoFrame) + self.nameLabel.setFrameShadow(QFrame.Plain) + self.nameLabel.setTextFormat(Qt.AutoText) + + self.gridLayout.addWidget(self.nameLabel, 0, 0, 1, 1) + + self.colorDepthLabel = QLabel(dialog) + self.colorDepthLabel.setObjectName(u"colorDepthLabel") + self.colorDepthLabel.setObjectName(u"colorDepthLabel") + self.colorDepthLabel.setGeometry(QRect(1, 83, 67, 22)) + self.colorDepthLabel.setFrameShape(QFrame.NoFrame) + self.colorDepthLabel.setFrameShadow(QFrame.Plain) + self.colorDepthLabel.setTextFormat(Qt.AutoText) + + self.gridLayout.addWidget(self.colorDepthLabel, 3, 0, 1, 1) + + + self.vboxLayout.addLayout(self.gridLayout) + + self.spacerItem = QSpacerItem(0, 0, QSizePolicy.Minimum, QSizePolicy.Expanding) + + self.vboxLayout.addItem(self.spacerItem) + + self.hboxLayout = QHBoxLayout() +#ifndef Q_OS_MAC + self.hboxLayout.setSpacing(6) +#endif + self.hboxLayout.setContentsMargins(1, 1, 1, 1) + self.hboxLayout.setObjectName(u"hboxLayout") + self.hboxLayout.setObjectName(u"") + self.spacerItem1 = QSpacerItem(0, 0, QSizePolicy.Expanding, QSizePolicy.Minimum) + + self.hboxLayout.addItem(self.spacerItem1) + + self.okButton = QPushButton(dialog) + self.okButton.setObjectName(u"okButton") + self.okButton.setObjectName(u"okButton") + self.okButton.setGeometry(QRect(135, 1, 80, 24)) + + self.hboxLayout.addWidget(self.okButton) + + self.cancelButton = QPushButton(dialog) + self.cancelButton.setObjectName(u"cancelButton") + self.cancelButton.setObjectName(u"cancelButton") + self.cancelButton.setGeometry(QRect(221, 1, 80, 24)) + + self.hboxLayout.addWidget(self.cancelButton) + + + self.vboxLayout.addLayout(self.hboxLayout) + + QWidget.setTabOrder(self.nameLineEdit, self.spinBox) + QWidget.setTabOrder(self.spinBox, self.spinBox_2) + QWidget.setTabOrder(self.spinBox_2, self.colorDepthCombo) + QWidget.setTabOrder(self.colorDepthCombo, self.okButton) + QWidget.setTabOrder(self.okButton, self.cancelButton) + + self.retranslateUi(dialog) + self.nameLineEdit.returnPressed.connect(self.okButton.animateClick) + + QMetaObject.connectSlotsByName(dialog) + # setupUi + + def retranslateUi(self, dialog): + dialog.setWindowTitle(QCoreApplication.translate("ImageDialog", u"Create Image", None)) + self.widthLabel.setText(QCoreApplication.translate("ImageDialog", u"Width:", None)) + self.heightLabel.setText(QCoreApplication.translate("ImageDialog", u"Height:", None)) + self.nameLineEdit.setText(QCoreApplication.translate("ImageDialog", u"Untitled image", None)) + self.nameLabel.setText(QCoreApplication.translate("ImageDialog", u"Name:", None)) + self.colorDepthLabel.setText(QCoreApplication.translate("ImageDialog", u"Color depth:", None)) + self.okButton.setText(QCoreApplication.translate("ImageDialog", u"OK", None)) + self.cancelButton.setText(QCoreApplication.translate("ImageDialog", u"Cancel", None)) + # retranslateUi + diff --git a/tools/qtpy2cpp_lib/tokenizer.py b/tools/qtpy2cpp_lib/tokenizer.py new file mode 100644 index 000000000..dee63c177 --- /dev/null +++ b/tools/qtpy2cpp_lib/tokenizer.py @@ -0,0 +1,91 @@ +############################################################################# +## +## Copyright (C) 2019 The Qt Company Ltd. +## Contact: https://www.qt.io/licensing/ +## +## This file is part of the Qt for Python project. +## +## $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$ +## +############################################################################# + +"""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/tools/qtpy2cpp_lib/visitor.py b/tools/qtpy2cpp_lib/visitor.py new file mode 100644 index 000000000..d17d5f53c --- /dev/null +++ b/tools/qtpy2cpp_lib/visitor.py @@ -0,0 +1,260 @@ +############################################################################# +## +## Copyright (C) 2020 The Qt Company Ltd. +## Contact: https://www.qt.io/licensing/ +## +## This file is part of the Qt for Python project. +## +## $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$ +## +############################################################################# + +"""AST visitor printing out C++""" + +import ast +import sys +import tokenize +import warnings + +from .formatter import (CppFormatter, format_for_loop, + format_function_def_arguments, format_inheritance, + format_literal, format_reference, + format_start_function_call, + write_import, write_import_from) + +from .nodedump import debug_format_node + + +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, output_file): + ast.NodeVisitor.__init__(self) + CppFormatter.__init__(self, output_file) + self._class_scope = [] # List of class names + self._stack = [] # nodes + 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 + message = 'Error "{}" at line {}'.format(str(e), line_no) + warnings.warn(message) + self._output_file.write(f'\n// {message}\n') + del self._stack[-1] + if self.debug: + self._debug_leave(node) + + def visit_Add(self, node): + self.generic_visit(node) + self._output_file.write(' + ') + + def visit_Assign(self, node): + self._output_file.write('\n') + self.INDENT() + for target in node.targets: + if isinstance(target, ast.Tuple): + warnings.warn('List assignment not handled (line {}).'. + format(node.lineno)) + elif isinstance(target, ast.Subscript): + warnings.warn('Subscript assignment not handled (line {}).'. + format(node.lineno)) + else: + self._output_file.write(format_reference(target)) + self._output_file.write(' = ') + self.visit(node.value) + self._output_file.write(';\n') + + def visit_Attribute(self, node): + """Format a variable reference (cf visit_Name)""" + 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 visit_Call(self, node): + self._output_file.write(format_start_function_call(node)) + # Manually do visit(), skip the children of func + for i, arg in enumerate(node.args): + if i > 0: + self._output_file.write(', ') + self.visit(arg) + self._output_file.write(')') + + 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_Expr(self, node): + self._output_file.write('\n') + 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_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 + self.write_function_def(node, class_context) + self.indent() + self.generic_visit(node) + self.dedent() + self.indent_line('}') + + def visit_If(self, node): + # Manually do visit() to get the indentation right. Note: + # elsif() is modelled as nested if. + 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_Lt(self, node): + self.generic_visit(node) + self._output_file.write('<') + + def visit_Mult(self, node): + self.generic_visit(node) + self._output_file.write(' * ') + + def visit_Name(self, node): + """Format a variable reference (cf visit_Attribute)""" + self._output_file.write(format_reference(node)) + + def visit_NameConstant(self, node): + self.generic_visit(node) + if node.value is None: + self._output_file.write('nullptr') + elif not node.value: + self._output_file.write('false') + else: + self._output_file.write('true') + + def visit_Num(self, node): + self.generic_visit(node) + self._output_file.write(format_literal(node)) + + def visit_Str(self, node): + self.generic_visit(node) + self._output_file.write(format_literal(node)) + + def visit_UnOp(self, node): + self.generic_visit(node) + + 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) |