diff options
Diffstat (limited to 'tools')
| -rw-r--r-- | tools/checklibs.py | 386 | ||||
| -rw-r--r-- | tools/debug_windows.py | 360 | ||||
| -rw-r--r-- | tools/missing_bindings.py | 448 |
3 files changed, 1194 insertions, 0 deletions
diff --git a/tools/checklibs.py b/tools/checklibs.py new file mode 100644 index 000000000..18aa11e93 --- /dev/null +++ b/tools/checklibs.py @@ -0,0 +1,386 @@ +############################################################################# +## +## Copyright (C) 2017 The Qt Company Ltd. +## Contact: https://www.qt.io/licensing/ +## +## This file is part of Qt for Python. +## +## $QT_BEGIN_LICENSE:LGPL$ +## Commercial License Usage +## Licensees holding valid commercial Qt licenses may use this file in +## accordance with the commercial license agreement provided with the +## Software or, alternatively, in accordance with the terms contained in +## a written agreement between you and The Qt Company. For licensing terms +## and conditions see https://www.qt.io/terms-conditions. For further +## information use the contact form at https://www.qt.io/contact-us. +## +## GNU Lesser General Public License Usage +## Alternatively, this file may be used under the terms of the GNU Lesser +## General Public License version 3 as published by the Free Software +## Foundation and appearing in the file LICENSE.LGPL3 included in the +## packaging of this file. Please review the following information to +## ensure the GNU Lesser General Public License version 3 requirements +## will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +## +## GNU General Public License Usage +## Alternatively, this file may be used under the terms of the GNU +## General Public License version 2.0 or (at your option) the GNU General +## Public license version 3 or any later version approved by the KDE Free +## Qt Foundation. The licenses are as published by the Free Software +## Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +## included in the packaging of this file. Please review the following +## information to ensure the GNU General Public License requirements will +## be met: https://www.gnu.org/licenses/gpl-2.0.html and +## https://www.gnu.org/licenses/gpl-3.0.html. +## +## $QT_END_LICENSE$ +## +############################################################################# + +#!/usr/bin/env python +# +# checklibs.py +# +# Check Mach-O dependencies. +# +# See http://www.entropy.ch/blog/Developer/2011/03/05/2011-Update-to-checklibs-Script-for-dynamic-library-dependencies.html +# +# Written by Marc Liyanage <http://www.entropy.ch> +# +# + +import subprocess, sys, re, os.path, optparse, collections +from pprint import pprint + + +class MachOFile: + + def __init__(self, image_path, arch, parent = None): + self.image_path = image_path + self._dependencies = [] + self._cache = dict(paths = {}, order = []) + self.arch = arch + self.parent = parent + self.header_info = {} + self.load_info() + self.add_to_cache() + + def load_info(self): + if not self.image_path.exists(): + return + self.load_header() + self.load_rpaths() + + def load_header(self): + # Get the mach-o header info, we're interested in the file type + # (executable, dylib) + cmd = 'otool -arch {0} -h "{1}"' + output = self.shell(cmd, [self.arch, self.image_path.resolved_path], + fatal = True) + if not output: + print("Unable to load mach header for {} ({}), architecture " + "mismatch? Use --arch option to pick architecture".format( + self.image_path.resolved_path, self.arch), file=sys.stderr) + exit() + (keys, values) = output.splitlines()[2:] + self.header_info = dict(zip(keys.split(), values.split())) + + def load_rpaths(self): + output = self.shell('otool -arch {0} -l "{1}"', + [self.arch, self.image_path.resolved_path], fatal = True) + # skip file name on first line + load_commands = re.split('Load command (\d+)', output)[1:] + self._rpaths = [] + load_commands = collections.deque(load_commands) + while load_commands: + load_commands.popleft() # command index + command = load_commands.popleft().strip().splitlines() + if command[0].find('LC_RPATH') == -1: + continue + + path = re.findall('path (.+) \(offset \d+\)$', command[2])[0] + image_path = self.image_path_for_recorded_path(path) + image_path.rpath_source = self + self._rpaths.append(image_path) + + def ancestors(self): + ancestors = [] + parent = self.parent + while parent: + ancestors.append(parent) + parent = parent.parent + + return ancestors + + def self_and_ancestors(self): + return [self] + self.ancestors() + + def rpaths(self): + return self._rpaths + + def all_rpaths(self): + rpaths = [] + for image in self.self_and_ancestors(): + rpaths.extend(image.rpaths()) + return rpaths + + def root(self): + if not self.parent: + return self + return self.ancestors()[-1] + + def executable_path(self): + root = self.root() + if root.is_executable(): + return root.image_path + return None + + def filetype(self): + return long(self.header_info.get('filetype', 0)) + + def is_dylib(self): + return self.filetype() == MachOFile.MH_DYLIB + + def is_executable(self): + return self.filetype() == MachOFile.MH_EXECUTE + + def all_dependencies(self): + self.walk_dependencies() + return self.cache()['order'] + + def walk_dependencies(self, known = {}): + if known.get(self.image_path.resolved_path): + return + + known[self.image_path.resolved_path] = self + + for item in self.dependencies(): + item.walk_dependencies(known) + + def dependencies(self): + if not self.image_path.exists(): + return [] + + if self._dependencies: + return self._dependencies + + output = self.shell('otool -arch {0} -L "{1}"', + [self.arch, self.image_path.resolved_path], fatal = True) + output = [line.strip() for line in output.splitlines()] + del(output[0]) + if self.is_dylib(): + # In the case of dylibs, the first line is the id line + del(output[0]) + + self._dependencies = [] + for line in output: + match = re.match('^(.+)\s+(\(.+)\)$', line) + if not match: + continue + recorded_path = match.group(1) + image_path = self.image_path_for_recorded_path(recorded_path) + image = self.lookup_or_make_item(image_path) + self._dependencies.append(image) + + return self._dependencies + + # The root item holds the cache, all lower-level requests bubble up + # the parent chain + def cache(self): + if self.parent: + return self.parent.cache() + return self._cache + + def add_to_cache(self): + cache = self.cache() + cache['paths'][self.image_path.resolved_path] = self + cache['order'].append(self) + + def cached_item_for_path(self, path): + if not path: + return None + return self.cache()['paths'].get(path) + + def lookup_or_make_item(self, image_path): + image = self.cached_item_for_path(image_path.resolved_path) + if not image: # cache miss + image = MachOFile(image_path, self.arch, parent = self) + return image + + def image_path_for_recorded_path(self, recorded_path): + path = ImagePath(None, recorded_path) + + # handle @executable_path + if recorded_path.startswith(ImagePath.EXECUTABLE_PATH_TOKEN): + executable_image_path = self.executable_path() + if executable_image_path: + path.resolved_path = os.path.normpath( + recorded_path.replace( + ImagePath.EXECUTABLE_PATH_TOKEN, + os.path.dirname(executable_image_path.resolved_path))) + + # handle @loader_path + elif recorded_path.startswith(ImagePath.LOADER_PATH_TOKEN): + path.resolved_path = os.path.normpath(recorded_path.replace( + ImagePath.LOADER_PATH_TOKEN, + os.path.dirname(self.image_path.resolved_path))) + + # handle @rpath + elif recorded_path.startswith(ImagePath.RPATH_TOKEN): + for rpath in self.all_rpaths(): + resolved_path = os.path.normpath(recorded_path.replace( + ImagePath.RPATH_TOKEN, rpath.resolved_path)) + if os.path.exists(resolved_path): + path.resolved_path = resolved_path + path.rpath_source = rpath.rpath_source + break + + # handle absolute path + elif recorded_path.startswith('/'): + path.resolved_path = recorded_path + + return path + + def __repr__(self): + return str(self.image_path) + + def dump(self): + print(self.image_path) + for dependency in self.dependencies(): + print('\t{0}'.format(dependency)) + + @staticmethod + def shell(cmd_format, args, fatal = False): + cmd = cmd_format.format(*args) + popen = subprocess.Popen(cmd, shell = True, stdout = subprocess.PIPE) + output = popen.communicate()[0] + if popen.returncode and fatal: + print("Nonzero exit status for shell command '{}'".format(cmd), + file=sys.stderr) + sys.exit(1) + + return output + + @classmethod + def architectures_for_image_at_path(cls, path): + output = cls.shell('file "{}"', [path]) + file_architectures = re.findall(r' executable (\w+)', output) + ordering = 'x86_64 i386'.split() + file_architectures = sorted(file_architectures, lambda a, b: cmp( + ordering.index(a), ordering.index(b))) + return file_architectures + + MH_EXECUTE = 0x2 + MH_DYLIB = 0x6 + MH_BUNDLE = 0x8 + + +# ANSI terminal coloring sequences +class Color: + HEADER = '\033[95m' + BLUE = '\033[94m' + GREEN = '\033[92m' + RED = '\033[91m' + ENDC = '\033[0m' + + @staticmethod + def red(string): + return Color.wrap(string, Color.RED) + + @staticmethod + def blue(string): + return Color.wrap(string, Color.BLUE) + + @staticmethod + def wrap(string, color): + return Color.HEADER + color + string + Color.ENDC + + +# This class holds path information for a mach-0 image file. +# It holds the path as it was recorded in the loading binary as well as +# the effective, resolved file system path. +# The former can contain @-replacement tokens. +# In the case where the recorded path contains an @rpath token that was +# resolved successfully, we also capture the path of the binary that +# supplied the rpath value that was used. +# That path itself can contain replacement tokens such as @loader_path. +class ImagePath: + + def __init__(self, resolved_path, recorded_path = None): + self.recorded_path = recorded_path + self.resolved_path = resolved_path + self.rpath_source = None + + def __repr__(self): + description = None + + if self.resolved_equals_recorded() or self.recorded_path == None: + description = self.resolved_path + else: + description = '{0} ({1})'.format(self.resolved_path, + self.recorded_path) + + if (not self.is_system_location()) and (not self.uses_dyld_token()): + description = Color.blue(description) + + if self.rpath_source: + description += ' (rpath source: {0})'.format( + self.rpath_source.image_path.resolved_path) + + if not self.exists(): + description += Color.red(' (missing)') + + return description + + def exists(self): + return self.resolved_path and os.path.exists(self.resolved_path) + + def resolved_equals_recorded(self): + return (self.resolved_path and self.recorded_path and + self.resolved_path == self.recorded_path) + + def uses_dyld_token(self): + return self.recorded_path and self.recorded_path.startswith('@') + + def is_system_location(self): + system_prefixes = ['/System/Library', '/usr/lib'] + for prefix in system_prefixes: + if self.resolved_path and self.resolved_path.startswith(prefix): + return True + + EXECUTABLE_PATH_TOKEN = '@executable_path' + LOADER_PATH_TOKEN = '@loader_path' + RPATH_TOKEN = '@rpath' + + +# Command line driver +parser = optparse.OptionParser( + usage = "Usage: %prog [options] path_to_mach_o_file") +parser.add_option( + "--arch", dest = "arch", help = "architecture", metavar = "ARCH") +parser.add_option( + "--all", dest = "include_system_libraries", + help = "Include system frameworks and libraries", action="store_true") +(options, args) = parser.parse_args() + +if len(args) < 1: + parser.print_help() + sys.exit(1) + +archs = MachOFile.architectures_for_image_at_path(args[0]) +if archs and not options.arch: + print('Analyzing architecture {}, override with --arch if needed'.format( + archs[0]), file=sys.stderr) + options.arch = archs[0] + +toplevel_image = MachOFile(ImagePath(args[0]), options.arch) + +for dependency in toplevel_image.all_dependencies(): + if (dependency.image_path.exists() and + (not options.include_system_libraries) and + dependency.image_path.is_system_location()): + continue + + dependency.dump() + print("\n") + diff --git a/tools/debug_windows.py b/tools/debug_windows.py new file mode 100644 index 000000000..ab1c03aba --- /dev/null +++ b/tools/debug_windows.py @@ -0,0 +1,360 @@ +############################################################################# +## +## Copyright (C) 2018 The Qt Company Ltd. +## Contact: https://www.qt.io/licensing/ +## +## This file is part of Qt for Python. +## +## $QT_BEGIN_LICENSE:LGPL$ +## Commercial License Usage +## Licensees holding valid commercial Qt licenses may use this file in +## accordance with the commercial license agreement provided with the +## Software or, alternatively, in accordance with the terms contained in +## a written agreement between you and The Qt Company. For licensing terms +## and conditions see https://www.qt.io/terms-conditions. For further +## information use the contact form at https://www.qt.io/contact-us. +## +## GNU Lesser General Public License Usage +## Alternatively, this file may be used under the terms of the GNU Lesser +## General Public License version 3 as published by the Free Software +## Foundation and appearing in the file LICENSE.LGPL3 included in the +## packaging of this file. Please review the following information to +## ensure the GNU Lesser General Public License version 3 requirements +## will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +## +## GNU General Public License Usage +## Alternatively, this file may be used under the terms of the GNU +## General Public License version 2.0 or (at your option) the GNU General +## Public license version 3 or any later version approved by the KDE Free +## Qt Foundation. The licenses are as published by the Free Software +## Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +## included in the packaging of this file. Please review the following +## information to ensure the GNU General Public License requirements will +## be met: https://www.gnu.org/licenses/gpl-2.0.html and +## https://www.gnu.org/licenses/gpl-3.0.html. +## +## $QT_END_LICENSE$ +## +############### + +""" +This is a troubleshooting script that assists finding out which DLLs or +which symbols in a DLL are missing when executing a PySide2 python +script. +It can also be used with any other non Python executable. + +Usage: python debug_windows.py + When no arguments are given the script will try to import + PySide2.QtCore. + +Usage: python debug_windows.py python -c "import PySide2.QtWebEngine" + python debug_windows.py my_executable.exe arg1 arg2 --arg3=4 + Any arguments given after the script name will be considered + as the target executable and the arguments passed to that + executable. + +The script requires administrator privileges. + +The script uses cdb.exe and gflags.exe under the hood, which are +installed together with the Windows Kit found at: +https://developer.microsoft.com/en-us/windows/downloads/windows-10-sdk + +""" + +from __future__ import print_function + +import sys +import re +import subprocess +import ctypes +import logging +import argparse +from os import path +from textwrap import dedent + +is_win = sys.platform == "win32" +is_py_3 = sys.version_info[0] == 3 +if is_win: + if is_py_3: + import winreg + else: + import _winreg as winreg + import exceptions + + +def get_parser_args(): + desc_msg = "Run an executable under cdb with loader snaps set." + help_msg = "Pass the executable and the arguments passed to it as a list." + parser = argparse.ArgumentParser(description=desc_msg) + parser.add_argument('args', nargs='*', help=help_msg) + # Prepend -- so that python options like '-c' are ignored by + # argparse. + massaged_args = ['--'] + sys.argv[1:] + return parser.parse_args(massaged_args) + + +parser_args = get_parser_args() +verbose_log_file_name = path.join(path.dirname(path.abspath(__file__)), + 'log_debug_windows.txt') + + +def is_admin(): + try: + return ctypes.windll.shell32.IsUserAnAdmin() + except Exception as e: + log.error("is_admin: Exception error: {}".format(e)) + return False + + +def get_verbose_logger(): + handler = logging.FileHandler(verbose_log_file_name, mode='w') + main_logger = logging.getLogger('main') + main_logger.setLevel(logging.INFO) + main_logger.addHandler(handler) + return main_logger + + +def get_non_verbose_logger(): + handler = logging.StreamHandler() + main_logger = logging.getLogger('main.non_verbose') + main_logger.setLevel(logging.INFO) + main_logger.addHandler(handler) + return main_logger + + +big_log = get_verbose_logger() +log = get_non_verbose_logger() + + +def sub_keys(key): + i = 0 + while True: + try: + sub_key = winreg.EnumKey(key, i) + yield sub_key + i += 1 + except WindowsError as e: + log.error(e) + break + + +def sub_values(key): + i = 0 + while True: + try: + v = winreg.EnumValue(key, i) + yield v + i += 1 + except WindowsError as e: + log.error(e) + break + + +def get_installed_windows_kits(): + roots_key = r"SOFTWARE\Microsoft\Windows Kits\Installed Roots" + log.info("Searching for Windows kits in registry path: " + "{}".format(roots_key)) + roots = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, roots_key, 0, + winreg.KEY_READ) + kits = [] + pattern = re.compile(r'KitsRoot(\d+)') + + for (name, value, value_type) in sub_values(roots): + if value_type == winreg.REG_SZ and name.startswith('KitsRoot'): + match = pattern.search(name) + if match: + version = match.group(1) + kits.append({'version': version, 'value': value}) + + if not kits: + log.error(dedent(""" + No windows kits found in the registry. + Consider downloading and installing the latest kit, either from + https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/debugger-download-tools + or from + https://developer.microsoft.com/en-us/windows/downloads/windows-10-sdk + """)) + exit(1) + return kits + + +def get_appropriate_kit(kits): + # Fixme, figure out if there is a more special way to choose a kit + # and not just latest version. + log.info("Found Windows kits are: {}".format(kits)) + chosen_kit = {'version': "0", 'value': None} + for kit in kits: + if (kit['version'] > chosen_kit['version'] and + # version 8.1 is actually '81', so consider everything + # above version 20, as '2.0', etc. + kit['version'] < "20"): + chosen_kit = kit + first_kit = kits[0] + return first_kit + + +def get_cdb_and_gflags_path(kits): + first_kit = get_appropriate_kit(kits) + first_path_path = first_kit['value'] + log.info('Using kit found at {}'.format(first_path_path)) + bits = 'x64' if (sys.maxsize > 2 ** 32) else 'x32' + debuggers_path = path.join(first_path_path, 'Debuggers', bits) + cdb_path = path.join(debuggers_path, 'cdb.exe') + if not path.exists(cdb_path): # Try for older "Debugging Tools" packages + debuggers_path = "C:\\Program Files\\Debugging Tools for Windows (x64)" + cdb_path = path.join(debuggers_path, 'cdb.exe') + + if not path.exists(cdb_path): + log.error("Couldn't find cdb.exe at: {}.".format(cdb_path)) + exit(1) + else: + log.info("Found cdb.exe at: {}.".format(cdb_path)) + + gflags_path = path.join(debuggers_path, 'gflags.exe') + + if not path.exists(gflags_path): + log.error('Couldn\'t find gflags.exe at: {}.'.format(gflags_path)) + exit(1) + else: + log.info('Found gflags.exe at: {}.'.format(cdb_path)) + + return cdb_path, gflags_path + + +def toggle_loader_snaps(executable_name, gflags_path, enable=True): + arg = '+sls' if enable else '-sls' + gflags_args = [gflags_path, '-i', executable_name, arg] + try: + log.info('Invoking gflags: {}'.format(gflags_args)) + output = subprocess.check_output(gflags_args, stderr=subprocess.STDOUT, + universal_newlines=True) + log.info(output) + except exceptions.WindowsError as e: + log.error("\nRunning {} exited with exception: " + "\n{}".format(gflags_args, e)) + exit(1) + except subprocess.CalledProcessError as e: + log.error("\nRunning {} exited with: {} and stdout was: " + "{}".format(gflags_args, e.returncode, e.output)) + exit(1) + + +def find_error_like_snippets(content): + snippets = [] + lines = content.splitlines() + context_lines = 4 + + def error_predicate(l): + # A list of mostly false positives are filtered out. + # For deeper inspection, the full log exists. + errors = {'errorhandling', + 'windowserrorreporting', + 'core-winrt-error', + 'RtlSetLastWin32Error', + 'RaiseInvalid16BitExeError', + 'BaseWriteErrorElevationRequiredEvent', + 'for DLL "Unknown"', + 'LdrpGetProcedureAddress', + 'X509_STORE_CTX_get_error', + 'ERR_clear_error', + 'ERR_peek_last_error', + 'ERR_error_string', + 'ERR_get_error', + ('ERROR: Module load completed but symbols could ' + 'not be loaded')} + return (re.search('error', l, re.IGNORECASE) + and all(e not in errors for e in errors)) + + for i in range(1, len(lines)): + line = lines[i] + if error_predicate(line): + snippets.append(lines[i - context_lines:i + context_lines + 1]) + + return snippets + + +def print_error_snippets(snippets): + if len(snippets) > 0: + log.info("\nThe following possible errors were found:\n") + + for i in range(1, len(snippets)): + log.info("Snippet {}:".format(i)) + for line in snippets[i]: + log.info(line) + log.info("") + + +def call_command_under_cdb_with_gflags(executable_path, args): + executable_name = path.basename(executable_path) + invocation = [executable_path] + args + + kits = get_installed_windows_kits() + cdb_path, gflags_path = get_cdb_and_gflags_path(kits) + + toggle_loader_snaps(executable_name, gflags_path, enable=True) + + log.info("Debugging the following command invocation: " + "{}".format(invocation)) + + cdb_args = [cdb_path] + invocation + + log.info('Invoking cdb: {}'.format(cdb_args)) + + p = subprocess.Popen(cdb_args, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + stdin=subprocess.PIPE, + shell=False) + + # Symbol fix, start process, print all thread stack traces, exit. + cdb_commands = ['.symfix', 'g', '!uniqstack', 'q'] + cdb_commands_text = '\n'.join(cdb_commands) + out, err = p.communicate(input=cdb_commands_text.encode('utf-8')) + + out_decoded = out.decode('utf-8') + big_log.info('stdout: {}'.format(out_decoded)) + if err: + big_log.info('stderr: {}'.format(err.decode('utf-8'))) + + log.info('Finished execution of process under cdb.') + + toggle_loader_snaps(executable_name, gflags_path, enable=False) + + snippets = find_error_like_snippets(out_decoded) + print_error_snippets(snippets) + + log.info("Finished processing.\n !!! Full log can be found at:\n" + "{}".format(verbose_log_file_name)) + + +def test_run_import_qt_core_under_cdb_with_gflags(): + # The weird characters are there for faster grepping of the output + # because there is a lot of content in the full log. + # The 2+2 is just ensure that Python itself works. + python_code = """ +print(">>>>>>>>>>>>>>>>>>>>>>> Test computation of 2+2 is: {}".format(2+2)) +import PySide2.QtCore +print(">>>>>>>>>>>>>>>>>>>>>>> QtCore object instance: {}".format(PySide2.QtCore)) +""" + call_command_under_cdb_with_gflags(sys.executable, ["-c", python_code]) + + +def handle_args(): + if not parser_args.args: + test_run_import_qt_core_under_cdb_with_gflags() + else: + call_command_under_cdb_with_gflags(parser_args.args[0], + parser_args.args[1:]) + + +if __name__ == '__main__': + if not is_win: + log.error("This script only works on Windows.") + exit(1) + + if is_admin(): + handle_args() + else: + log.error("Please rerun the script with administrator privileges. " + "It is required for gflags.exe to work. ") + exit(1) diff --git a/tools/missing_bindings.py b/tools/missing_bindings.py new file mode 100644 index 000000000..8f3c0b808 --- /dev/null +++ b/tools/missing_bindings.py @@ -0,0 +1,448 @@ +############################################################################# +## +## Copyright (C) 2017 The Qt Company Ltd. +## Contact: https://www.qt.io/licensing/ +## +## This file is part of Qt for Python. +## +## $QT_BEGIN_LICENSE:LGPL$ +## Commercial License Usage +## Licensees holding valid commercial Qt licenses may use this file in +## accordance with the commercial license agreement provided with the +## Software or, alternatively, in accordance with the terms contained in +## a written agreement between you and The Qt Company. For licensing terms +## and conditions see https://www.qt.io/terms-conditions. For further +## information use the contact form at https://www.qt.io/contact-us. +## +## GNU Lesser General Public License Usage +## Alternatively, this file may be used under the terms of the GNU Lesser +## General Public License version 3 as published by the Free Software +## Foundation and appearing in the file LICENSE.LGPL3 included in the +## packaging of this file. Please review the following information to +## ensure the GNU Lesser General Public License version 3 requirements +## will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +## +## GNU General Public License Usage +## Alternatively, this file may be used under the terms of the GNU +## General Public License version 2.0 or (at your option) the GNU General +## Public license version 3 or any later version approved by the KDE Free +## Qt Foundation. The licenses are as published by the Free Software +## Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +## included in the packaging of this file. Please review the following +## information to ensure the GNU General Public License requirements will +## be met: https://www.gnu.org/licenses/gpl-2.0.html and +## https://www.gnu.org/licenses/gpl-3.0.html. +## +## $QT_END_LICENSE$ +## +############################################################################# + +# This script is used to generate a summary of missing types / classes +# which are present in C++ Qt5, but are missing in PySide2. +# +# Required packages: bs4 +# Installed via: pip install bs4 +# +# The script uses beautiful soup 4 to parse out the class names from +# the online Qt documentation. It then tries to import the types from +# PySide2. +# +# Example invocation of script: +# python missing_bindings.py --qt-version 5.9 -w all +# --qt-version - specify which version of qt documentation to load. +# -w - if PyQt5 is an installed package, check if the tested +# class also exists there. + +from __future__ import print_function + +try: + import urllib.request as urllib2 +except ImportError: + import urllib2 + +import argparse +from bs4 import BeautifulSoup +from collections import OrderedDict +from time import gmtime, strftime +import sys +import os.path + +modules_to_test = OrderedDict() + +# Essentials +modules_to_test['QtCore'] = 'qtcore-module.html' +modules_to_test['QtGui'] = 'qtgui-module.html' +modules_to_test['QtMultimedia'] = 'qtmultimedia-module.html' +modules_to_test['QtMultimediaWidgets'] = 'qtmultimediawidgets-module.html' +modules_to_test['QtNetwork'] = 'qtnetwork-module.html' +modules_to_test['QtQml'] = 'qtqml-module.html' +modules_to_test['QtQuick'] = 'qtquick-module.html' +modules_to_test['QtSql'] = 'qtsql-module.html' +modules_to_test['QtTest'] = 'qttest-module.html' +modules_to_test['QtWidgets'] = 'qtwidgets-module.html' + +# Addons +modules_to_test['Qt3DCore'] = 'qt3dcore-module.html' +modules_to_test['Qt3DInput'] = 'qt3dinput-module.html' +modules_to_test['Qt3DLogic'] = 'qt3dlogic-module.html' +modules_to_test['Qt3DRender'] = 'qt3drender-module.html' +modules_to_test['Qt3DAnimation'] = 'qt3danimation-module.html' +modules_to_test['Qt3DExtras'] = 'qt3dextras-module.html' +modules_to_test['QtConcurrent'] = 'qtconcurrent-module.html' +modules_to_test['QtNetworkAuth'] = 'qtnetworkauth-module.html' +modules_to_test['QtHelp'] = 'qthelp-module.html' +modules_to_test['QtLocation'] = 'qtlocation-module.html' +modules_to_test['QtPrintSupport'] = 'qtprintsupport-module.html' +modules_to_test['QtSCXML'] = 'qtscxml-module.html' +modules_to_test['QtSpeech'] = 'qtspeech-module.html' +modules_to_test['QtSvg'] = 'qtsvg-module.html' +modules_to_test['QtUiTools'] = 'qtuitools-module.html' +modules_to_test['QtWebChannel'] = 'qtwebchannel-module.html' +modules_to_test['QtWebEngine'] = 'qtwebengine-module.html' +modules_to_test['QtWebEngineWidgets'] = 'qtwebenginewidgets-module.html' +modules_to_test['QtWebSockets'] = 'qtwebsockets-module.html' +modules_to_test['QtMacExtras'] = 'qtmacextras-module.html' +modules_to_test['QtX11Extras'] = 'qtx11extras-module.html' +modules_to_test['QtWinExtras'] = 'qtwinextras-module.html' +modules_to_test['QtXml'] = 'qtxml-module.html' +modules_to_test['QtXmlPatterns'] = 'qtxmlpatterns-module.html' +modules_to_test['QtCharts'] = 'qtcharts-module.html' +modules_to_test['QtDataVisualization'] = 'qtdatavisualization-module.html' + +types_to_ignore = set() +# QtCore +types_to_ignore.add('QFlag') +types_to_ignore.add('QFlags') +types_to_ignore.add('QGlobalStatic') +types_to_ignore.add('QDebug') +types_to_ignore.add('QDebugStateSaver') +types_to_ignore.add('QMetaObject.Connection') +types_to_ignore.add('QPointer') +types_to_ignore.add('QAssociativeIterable') +types_to_ignore.add('QSequentialIterable') +types_to_ignore.add('QStaticPlugin') +types_to_ignore.add('QChar') +types_to_ignore.add('QLatin1Char') +types_to_ignore.add('QHash') +types_to_ignore.add('QMultiHash') +types_to_ignore.add('QLinkedList') +types_to_ignore.add('QList') +types_to_ignore.add('QMap') +types_to_ignore.add('QMultiMap') +types_to_ignore.add('QMap.key_iterator') +types_to_ignore.add('QPair') +types_to_ignore.add('QQueue') +types_to_ignore.add('QScopedArrayPointer') +types_to_ignore.add('QScopedPointer') +types_to_ignore.add('QScopedValueRollback') +types_to_ignore.add('QMutableSetIterator') +types_to_ignore.add('QSet') +types_to_ignore.add('QSet.const_iterator') +types_to_ignore.add('QSet.iterator') +types_to_ignore.add('QExplicitlySharedDataPointer') +types_to_ignore.add('QSharedData') +types_to_ignore.add('QSharedDataPointer') +types_to_ignore.add('QEnableSharedFromThis') +types_to_ignore.add('QSharedPointer') +types_to_ignore.add('QWeakPointer') +types_to_ignore.add('QStack') +types_to_ignore.add('QLatin1String') +types_to_ignore.add('QString') +types_to_ignore.add('QStringRef') +types_to_ignore.add('QStringList') +types_to_ignore.add('QStringMatcher') +types_to_ignore.add('QVarLengthArray') +types_to_ignore.add('QVector') +types_to_ignore.add('QFutureIterator') +types_to_ignore.add('QHashIterator') +types_to_ignore.add('QMutableHashIterator') +types_to_ignore.add('QLinkedListIterator') +types_to_ignore.add('QMutableLinkedListIterator') +types_to_ignore.add('QListIterator') +types_to_ignore.add('QMutableListIterator') +types_to_ignore.add('QMapIterator') +types_to_ignore.add('QMutableMapIterator') +types_to_ignore.add('QSetIterator') +types_to_ignore.add('QMutableVectorIterator') +types_to_ignore.add('QVectorIterator') + +# QtGui +types_to_ignore.add('QIconEnginePlugin') +types_to_ignore.add('QImageIOPlugin') +types_to_ignore.add('QGenericPlugin') +types_to_ignore.add('QGenericPluginFactory') +types_to_ignore.add('QGenericMatrix') +types_to_ignore.add('QOpenGLExtraFunctions') +types_to_ignore.add('QOpenGLFunctions') +types_to_ignore.add('QOpenGLFunctions_1_0') +types_to_ignore.add('QOpenGLFunctions_1_1') +types_to_ignore.add('QOpenGLFunctions_1_2') +types_to_ignore.add('QOpenGLFunctions_1_3') +types_to_ignore.add('QOpenGLFunctions_1_4') +types_to_ignore.add('QOpenGLFunctions_1_5') +types_to_ignore.add('QOpenGLFunctions_2_0') +types_to_ignore.add('QOpenGLFunctions_2_1') +types_to_ignore.add('QOpenGLFunctions_3_0') +types_to_ignore.add('QOpenGLFunctions_3_1') +types_to_ignore.add('QOpenGLFunctions_3_2_Compatibility') +types_to_ignore.add('QOpenGLFunctions_3_2_Core') +types_to_ignore.add('QOpenGLFunctions_3_3_Compatibility') +types_to_ignore.add('QOpenGLFunctions_3_3_Core') +types_to_ignore.add('QOpenGLFunctions_4_0_Compatibility') +types_to_ignore.add('QOpenGLFunctions_4_0_Core') +types_to_ignore.add('QOpenGLFunctions_4_1_Compatibility') +types_to_ignore.add('QOpenGLFunctions_4_1_Core') +types_to_ignore.add('QOpenGLFunctions_4_2_Compatibility') +types_to_ignore.add('QOpenGLFunctions_4_2_Core') +types_to_ignore.add('QOpenGLFunctions_4_3_Compatibility') +types_to_ignore.add('QOpenGLFunctions_4_3_Core') +types_to_ignore.add('QOpenGLFunctions_4_4_Compatibility') +types_to_ignore.add('QOpenGLFunctions_4_4_Core') +types_to_ignore.add('QOpenGLFunctions_4_5_Compatibility') +types_to_ignore.add('QOpenGLFunctions_4_5_Core') +types_to_ignore.add('QOpenGLFunctions_ES2') + +# QtWidgets +types_to_ignore.add('QItemEditorCreator') +types_to_ignore.add('QStandardItemEditorCreator') +types_to_ignore.add('QStylePlugin') + +# QtSql +types_to_ignore.add('QSqlDriverCreator') +types_to_ignore.add('QSqlDriverPlugin') + +qt_documentation_website_prefixes = OrderedDict() +qt_documentation_website_prefixes['5.6'] = 'http://doc.qt.io/qt-5.6/' +qt_documentation_website_prefixes['5.8'] = 'http://doc.qt.io/qt-5.8/' +qt_documentation_website_prefixes['5.9'] = 'http://doc.qt.io/qt-5.9/' +qt_documentation_website_prefixes['5.10'] = 'http://doc.qt.io/qt-5.10/' +qt_documentation_website_prefixes['5.11'] = 'http://doc.qt.io/qt-5.11/' +qt_documentation_website_prefixes['5.11'] = 'http://doc.qt.io/qt-5.11/' +qt_documentation_website_prefixes['5.12'] = 'http://doc.qt.io/qt-5.12/' +qt_documentation_website_prefixes['dev'] = 'http://doc-snapshots.qt.io/qt5-dev/' + + +def qt_version_to_doc_prefix(version): + if version in qt_documentation_website_prefixes: + return qt_documentation_website_prefixes[version] + else: + raise RuntimeError("The specified qt version is not supported") + + +def create_doc_url(module_doc_page_url, version): + return qt_version_to_doc_prefix(version) + module_doc_page_url + +parser = argparse.ArgumentParser() +parser.add_argument("module", + default='all', + choices=list(modules_to_test.keys()).append('all'), + nargs='?', + type=str, + help="the Qt module for which to get the missing types") +parser.add_argument("--qt-version", + "-v", + default='5.12', + choices=['5.6', '5.9', '5.11', '5.12', 'dev'], + type=str, + dest='version', + help="the Qt version to use to check for types") +parser.add_argument("--which-missing", + "-w", + default='all', + choices=['all', 'in-pyqt', 'not-in-pyqt'], + type=str, + dest='which_missing', + help="Which missing types to show (all, or just those " + "that are not present in PyQt)") + +args = parser.parse_args() + +if hasattr(args, "module") and args.module != 'all': + saved_value = modules_to_test[args.module] + modules_to_test.clear() + modules_to_test[args.module] = saved_value + +pyside_package_name = "PySide2" +pyqt_package_name = "PyQt5" + +total_missing_types_count = 0 +total_missing_types_count_compared_to_pyqt = 0 +total_missing_modules_count = 0 + +wiki_file = open('missing_bindings_for_wiki_qt_io.txt', 'w') +wiki_file.truncate() + + +def log(*pargs, **kw): + print(*pargs) + + computed_str = '' + for arg in pargs: + computed_str += str(arg) + + style = 'text' + if 'style' in kw: + style = kw['style'] + + if style == 'heading1': + computed_str = '= ' + computed_str + ' =' + elif style == 'heading5': + computed_str = '===== ' + computed_str + ' =====' + elif style == 'with_newline': + computed_str += '\n' + elif style == 'bold_colon': + computed_str = computed_str.replace(':', ":'''") + computed_str += "'''" + computed_str += '\n' + elif style == 'error': + computed_str = "''" + computed_str.strip('\n') + "''\n" + elif style == 'text_with_link': + computed_str = computed_str + elif style == 'code': + computed_str = ' ' + computed_str + elif style == 'end': + return + + print(computed_str, file=wiki_file) + +log('PySide2 bindings for Qt {}'.format(args.version), style='heading1') + +log("""Using Qt version {} documentation to find public API Qt types and test +if the types are present in the PySide2 package.""".format(args.version)) + +log("""Results are usually stored at +https://wiki.qt.io/PySide2_Missing_Bindings +so consider taking the contents of the generated +missing_bindings_for_wiki_qt_io.txt file and updating the linked wiki page.""", +style='end') + +log("""Similar report: +https://gist.github.com/ethanhs/6c626ca4e291f3682589699296377d3a""", +style='text_with_link') + +python_executable = os.path.basename(sys.executable or '') +command_line_arguments = ' '.join(sys.argv) +report_date = strftime("%Y-%m-%d %H:%M:%S %Z", gmtime()) + +log(""" +This report was generated by running the following command: + {} {} +on the following date: + {} +""".format(python_executable, command_line_arguments, report_date)) + +for module_name in modules_to_test.keys(): + log(module_name, style='heading5') + + url = create_doc_url(modules_to_test[module_name], args.version) + log('Documentation link: {}\n'.format(url), style='text_with_link') + + # Import the tested module + try: + pyside_tested_module = getattr(__import__(pyside_package_name, + fromlist=[module_name]), module_name) + except Exception as e: + log('\nCould not load {}.{}. Received error: {}. Skipping.\n'.format( + pyside_package_name, module_name, str(e).replace("'", '')), + style='error') + total_missing_modules_count += 1 + continue + + try: + pyqt_module_name = module_name + if module_name == "QtCharts": + pyqt_module_name = module_name[:-1] + + pyqt_tested_module = getattr(__import__(pyqt_package_name, + fromlist=[pyqt_module_name]), pyqt_module_name) + except Exception as e: + log("\nCould not load {}.{} for comparison. " + "Received error: {}.\n".format(pyqt_package_name, module_name, + str(e).replace("'", '')), style='error') + + # Get C++ class list from documentation page. + page = urllib2.urlopen(url) + soup = BeautifulSoup(page, 'html.parser') + + # Extract the Qt type names from the documentation classes table + links = soup.body.select('.annotated a') + types_on_html_page = [] + + for link in links: + link_text = link.text + link_text = link_text.replace('::', '.') + if link_text not in types_to_ignore: + types_on_html_page.append(link_text) + + log('Number of types in {}: {}'.format(module_name, + len(types_on_html_page)), style='bold_colon') + + missing_types_count = 0 + missing_types_compared_to_pyqt = 0 + missing_types = [] + for qt_type in types_on_html_page: + try: + pyside_qualified_type = 'pyside_tested_module.' + + if "QtCharts" == module_name: + pyside_qualified_type += 'QtCharts.' + elif "DataVisualization" in module_name: + pyside_qualified_type += 'QtDataVisualization.' + + pyside_qualified_type += qt_type + eval(pyside_qualified_type) + except: + missing_type = qt_type + missing_types_count += 1 + total_missing_types_count += 1 + + is_present_in_pyqt = False + try: + pyqt_qualified_type = 'pyqt_tested_module.' + + if "Charts" in module_name: + pyqt_qualified_type += 'QtCharts.' + elif "DataVisualization" in module_name: + pyqt_qualified_type += 'QtDataVisualization.' + + pyqt_qualified_type += qt_type + eval(pyqt_qualified_type) + missing_type += " (is present in PyQt5)" + missing_types_compared_to_pyqt += 1 + total_missing_types_count_compared_to_pyqt += 1 + is_present_in_pyqt = True + except: + pass + + if args.which_missing == 'all': + missing_types.append(missing_type) + elif args.which_missing == 'in-pyqt' and is_present_in_pyqt: + missing_types.append(missing_type) + elif (args.which_missing == 'not-in-pyqt' and + not is_present_in_pyqt): + missing_types.append(missing_type) + + if len(missing_types) > 0: + log('Missing types in {}:'.format(module_name), style='with_newline') + missing_types.sort() + for missing_type in missing_types: + log(missing_type, style='code') + log('') + + log('Number of missing types: {}'.format(missing_types_count), + style='bold_colon') + if len(missing_types) > 0: + log('Number of missing types that are present in PyQt5: {}' + .format(missing_types_compared_to_pyqt), style='bold_colon') + log('End of missing types for {}\n'.format(module_name), style='end') + else: + log('', style='end') + +log('Summary', style='heading5') +log('Total number of missing types: {}'.format(total_missing_types_count), + style='bold_colon') +log('Total number of missing types that are present in PyQt5: {}' + .format(total_missing_types_count_compared_to_pyqt), style='bold_colon') +log('Total number of missing modules: {}' + .format(total_missing_modules_count), style='bold_colon') +wiki_file.close() |
