aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorFriedemann Kleint <Friedemann.Kleint@qt.io>2019-12-20 13:08:25 +0100
committerFriedemann Kleint <Friedemann.Kleint@qt.io>2020-03-05 11:20:18 +0100
commite88f08c180bc30aa52887bd5cc27fe0fe1a33b2b (patch)
treef7677ee1ac84afbb7ad8556f2de69522a03a2c16
parent4f1739e062623d3ab8aebe9540e945464734a75b (diff)
Add Python to Qt/C++ conversion tool
Change-Id: I275e776248bd55c3c38f5fedd83088bf475a1cf9 Reviewed-by: Christian Tismer <tismer@stackless.com>
-rw-r--r--tools/qtpy2cpp.py99
-rw-r--r--tools/qtpy2cpp.pyproject6
-rw-r--r--tools/qtpy2cpp_lib/astdump.py149
-rw-r--r--tools/qtpy2cpp_lib/formatter.py264
-rw-r--r--tools/qtpy2cpp_lib/nodedump.py86
-rw-r--r--tools/qtpy2cpp_lib/test_baseline/basic_test.py38
-rw-r--r--tools/qtpy2cpp_lib/test_baseline/uic.py208
-rw-r--r--tools/qtpy2cpp_lib/tokenizer.py91
-rw-r--r--tools/qtpy2cpp_lib/visitor.py260
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)