diff --git a/CMakeLists.txt b/CMakeLists.txt index 919969e..46e20e6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,14 +3,22 @@ project(python_qt_binding) find_package(ament_cmake REQUIRED) find_package(ament_cmake_python REQUIRED) +find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets) -ament_python_install_package(${PROJECT_NAME} - PACKAGE_DIR src/${PROJECT_NAME}) +if (${QT_VERSION_MAJOR} GREATER "5") + ament_python_install_package(python_qt_binding + PACKAGE_DIR src/python_qt6_binding) +else() + ament_python_install_package(python_qt_binding + PACKAGE_DIR src/python_qt_binding + ) +endif() install(FILES cmake/shiboken_helper.cmake cmake/sip_configure.py cmake/sip_helper.cmake + cmake/pyside_config.py DESTINATION share/${PROJECT_NAME}/cmake) if(BUILD_TESTING) diff --git a/cmake/pyside_config.py b/cmake/pyside_config.py new file mode 100755 index 0000000..2282e43 --- /dev/null +++ b/cmake/pyside_config.py @@ -0,0 +1,341 @@ +#!/usr/bin/python3 +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +from enum import Enum +from glob import glob +import os +import re +import sys +import sysconfig + + +PYSIDE = 'pyside6' +PYSIDE_MODULE = 'PySide6' +SHIBOKEN = 'shiboken6' + + +class Package(Enum): + SHIBOKEN_MODULE = 1 + SHIBOKEN_GENERATOR = 2 + PYSIDE_MODULE = 3 + + +generic_error = ('Did you forget to activate your virtualenv? Or perhaps' + f' you forgot to build / install {PYSIDE_MODULE} into your currently' + ' active Python environment?') +pyside_error = f'Unable to locate {PYSIDE_MODULE}. {generic_error}' +shiboken_module_error = f'Unable to locate {SHIBOKEN}-module. {generic_error}' +shiboken_generator_error = f'Unable to locate shiboken-generator. {generic_error}' +pyside_libs_error = f'Unable to locate the PySide shared libraries. {generic_error}' +python_link_error = 'Unable to locate the Python library for linking.' +python_include_error = 'Unable to locate the Python include headers directory.' + +options = [] + +# option, function, error, description +options.append(('--shiboken-module-path', + lambda: find_shiboken_module(), + shiboken_module_error, + 'Print shiboken module location')) +options.append(('--shiboken-generator-path', + lambda: find_shiboken_generator(), + shiboken_generator_error, + 'Print shiboken generator location')) +options.append(('--pyside-path', lambda: find_pyside(), pyside_error, + f'Print {PYSIDE_MODULE} location')) + +options.append(('--python-include-path', + lambda: get_python_include_path(), + python_include_error, + 'Print Python include path')) +options.append(('--shiboken-generator-include-path', + lambda: get_package_include_path(Package.SHIBOKEN_GENERATOR), + pyside_error, + 'Print shiboken generator include paths')) +options.append(('--pyside-include-path', + lambda: get_package_include_path(Package.PYSIDE_MODULE), + pyside_error, + 'Print PySide6 include paths')) + +options.append(('--python-link-flags-qmake', lambda: python_link_flags_qmake(), python_link_error, + 'Print python link flags for qmake')) +options.append(('--python-link-flags-cmake', lambda: python_link_flags_cmake(), python_link_error, + 'Print python link flags for cmake')) + +options.append(('--shiboken-module-qmake-lflags', + lambda: get_package_qmake_lflags(Package.SHIBOKEN_MODULE), pyside_error, + 'Print shiboken6 shared library link flags for qmake')) +options.append(('--pyside-qmake-lflags', + lambda: get_package_qmake_lflags(Package.PYSIDE_MODULE), pyside_error, + 'Print PySide6 shared library link flags for qmake')) + +options.append(('--shiboken-module-shared-libraries-qmake', + lambda: get_shared_libraries_qmake(Package.SHIBOKEN_MODULE), pyside_libs_error, + "Print paths of shiboken shared libraries (.so's, .dylib's, .dll's) for qmake")) +options.append(('--shiboken-module-shared-libraries-cmake', + lambda: get_shared_libraries_cmake(Package.SHIBOKEN_MODULE), pyside_libs_error, + "Print paths of shiboken shared libraries (.so's, .dylib's, .dll's) for cmake")) + +options.append(('--pyside-shared-libraries-qmake', + lambda: get_shared_libraries_qmake(Package.PYSIDE_MODULE), pyside_libs_error, + "Print paths of f{PYSIDE_MODULE} shared libraries (.so's, .dylib's, .dll's) " + 'for qmake')) +options.append(('--pyside-shared-libraries-cmake', + lambda: get_shared_libraries_cmake(Package.PYSIDE_MODULE), pyside_libs_error, + f"Print paths of {PYSIDE_MODULE} shared libraries (.so's, .dylib's, .dll's) " + 'for cmake')) + +options_usage = '' +for i, (flag, _, _, description) in enumerate(options): + options_usage += f' {flag:<45} {description}' + if i < len(options) - 1: + options_usage += '\n' + +usage = f""" +Utility to determine include/link options of shiboken/PySide and Python for qmake/CMake projects +that would like to embed or build custom shiboken/PySide bindings. + +Usage: pyside_config.py [option] +Options: +{options_usage} + -a Print all options and their values + --help/-h Print this help +""" + +option = sys.argv[1] if len(sys.argv) == 2 else '-a' +if option == '-h' or option == '--help': + print(usage) + sys.exit(0) + + +def clean_path(path): + return path if sys.platform != 'win32' else path.replace('\\', '/') + + +def shared_library_suffix(): + if sys.platform == 'win32': + return 'lib' + elif sys.platform == 'darwin': + return 'dylib' + # Linux + else: + return 'so.*' + + +def import_suffixes(): + import importlib.machinery + return importlib.machinery.EXTENSION_SUFFIXES + + +def is_debug(): + debug_suffix = '_d.pyd' if sys.platform == 'win32' else '_d.so' + return any(s.endswith(debug_suffix) for s in import_suffixes()) + + +def shared_library_glob_pattern(): + glob = '*.' + shared_library_suffix() + return glob if sys.platform == 'win32' else 'lib' + glob + + +def filter_shared_libraries(libs_list): + def predicate(lib_name): + basename = os.path.basename(lib_name) + if 'shiboken' in basename or 'pyside6' in basename: + return True + return False + result = [lib for lib in libs_list if predicate(lib)] + return result + + +# Return qmake link option for a library file name +def link_option(lib): + # On Linux: + # Since we cannot include symlinks with wheel packages + # we are using an absolute path for the libpyside and libshiboken + # libraries when compiling the project + baseName = os.path.basename(lib) + link = ' -l' + if sys.platform in ['linux', 'linux2']: # Linux: 'libfoo.so' -> '/absolute/path/libfoo.so' + link = lib + elif sys.platform in ['darwin']: # Darwin: 'libfoo.so' -> '-lfoo' + link += os.path.splitext(baseName[3:])[0] + else: # Windows: 'libfoo.dll' -> 'libfoo.dll' + link += os.path.splitext(baseName)[0] + return link + + +# Locate PySide6 via sys.path package path. +def find_pyside(): + return find_package_path(PYSIDE_MODULE) + + +def find_shiboken_module(): + return find_package_path(SHIBOKEN) + + +def find_shiboken_generator(): + return find_package_path(f'{SHIBOKEN}_generator') + + +def find_package(which_package): + if which_package == Package.SHIBOKEN_MODULE: + return find_shiboken_module() + if which_package == Package.SHIBOKEN_GENERATOR: + return find_shiboken_generator() + if which_package == Package.PYSIDE_MODULE: + return find_pyside() + return None + + +def find_package_path(dir_name): + for p in sys.path: + if 'site-' in p or 'dist-' in p: + package = os.path.join(p, dir_name) + if os.path.exists(package): + return clean_path(os.path.realpath(package)) + return None + + +# Return version as 'x.y' (e.g. 3.9, 3.12, etc) +def python_version(): + return str(sys.version_info[0]) + '.' + str(sys.version_info[1]) + + +def get_python_include_path(): + return sysconfig.get_path('include') + + +def python_link_flags_qmake(): + flags = python_link_data() + if sys.platform == 'win32': + libdir = flags['libdir'] + # This will add the '~1' shortcut for directories that + # contain white spaces + # e.g.: 'Program Files' to 'Progra~1' + for d in libdir.split('\\'): + if ' ' in d: + libdir = libdir.replace(d, d.split(' ')[0][:-1] + '~1') + lib_flags = flags['lib'] + return f'-L{libdir} -l{lib_flags}' + elif sys.platform == 'darwin': + libdir = flags['libdir'] + lib_flags = flags['lib'] + return f'-L{libdir} -l{lib_flags}' + else: + # Linux and anything else + libdir = flags['libdir'] + lib_flags = flags['lib'] + return f'-L{libdir} -l{lib_flags}' + + +def python_link_flags_cmake(): + flags = python_link_data() + libdir = flags['libdir'] + lib = re.sub(r'.dll$', '.lib', flags['lib']) + return f'{libdir};{lib}' + + +def python_link_data(): + # @TODO Fix to work with static builds of Python + libdir = sysconfig.get_config_var('LIBDIR') + if libdir is None: + libdir = os.path.abspath(os.path.join( + sysconfig.get_config_var('LIBDEST'), '..', 'libs')) + version = python_version() + version_no_dots = version.replace('.', '') + + flags = {} + flags['libdir'] = libdir + if sys.platform == 'win32': + suffix = '_d' if is_debug() else '' + flags['lib'] = f'python{version_no_dots}{suffix}' + + elif sys.platform == 'darwin': + flags['lib'] = f'python{version}' + + # Linux and anything else + else: + flags['lib'] = f'python{version}{sys.abiflags}' + + return flags + + +def get_package_include_path(which_package): + package_path = find_package(which_package) + if package_path is None: + return None + + includes = f'{package_path}/include' + + return includes + + +def get_package_qmake_lflags(which_package): + package_path = find_package(which_package) + if package_path is None: + return None + + link = f'-L{package_path}' + glob_result = glob(os.path.join(package_path, shared_library_glob_pattern())) + for lib in filter_shared_libraries(glob_result): + link += ' ' + link += link_option(lib) + return link + + +def get_shared_libraries_data(which_package): + package_path = find_package(which_package) + if package_path is None: + return None + + glob_result = glob(os.path.join(package_path, shared_library_glob_pattern())) + filtered_libs = filter_shared_libraries(glob_result) + libs = [] + if sys.platform == 'win32': + for lib in filtered_libs: + libs.append(os.path.realpath(lib)) + else: + for lib in filtered_libs: + libs.append(lib) + return libs + + +def get_shared_libraries_qmake(which_package): + libs = get_shared_libraries_data(which_package) + if libs is None: + return None + + if sys.platform == 'win32': + if not libs: + return '' + dlls = '' + for lib in libs: + dll = os.path.splitext(lib)[0] + '.dll' + dlls += dll + ' ' + + return dlls + else: + libs_string = '' + for lib in libs: + libs_string += lib + ' ' + return libs_string + + +def get_shared_libraries_cmake(which_package): + libs = get_shared_libraries_data(which_package) + result = ';'.join(libs) + return result + + +print_all = option == '-a' +for argument, handler, error, _ in options: + if option == argument or print_all: + handler_result = handler() + if handler_result is None: + sys.exit(error) + + line = handler_result + if print_all: + line = f'{argument:<40}: {line}' + print(line) diff --git a/cmake/shiboken_helper.cmake b/cmake/shiboken_helper.cmake index 7624157..008ec9f 100644 --- a/cmake/shiboken_helper.cmake +++ b/cmake/shiboken_helper.cmake @@ -162,4 +162,4 @@ function(shiboken_target_link_libraries PROJECT_NAME QT_COMPONENTS) endforeach() target_link_libraries(${PROJECT_NAME} ${shiboken_LINK_LIBRARIES}) -endfunction() +endfunction() \ No newline at end of file diff --git a/cmake/sip_configure.py b/cmake/sip_configure.py index 5210ee5..a25c832 100644 --- a/cmake/sip_configure.py +++ b/cmake/sip_configure.py @@ -228,4 +228,4 @@ def split_paths(paths): makefile.LIBS.set(libs) # Generate the Makefile itself -makefile.generate() +makefile.generate() \ No newline at end of file diff --git a/cmake/sip_helper.cmake b/cmake/sip_helper.cmake index a5ac3c2..7cf6d68 100644 --- a/cmake/sip_helper.cmake +++ b/cmake/sip_helper.cmake @@ -31,9 +31,11 @@ execute_process( if(PYTHON_SIP_EXECUTABLE) string(STRIP ${PYTHON_SIP_EXECUTABLE} SIP_EXECUTABLE) else() - find_program(SIP_EXECUTABLE sip) + find_program(SIP_EXECUTABLE NAMES sip-build) endif() +set(SIP_PROJECT_INCLUDE_DIRS "$ENV{SIP_PROJECT_INCLUDE_DIRS}") + if(SIP_EXECUTABLE) message(STATUS "SIP binding generator available at: ${SIP_EXECUTABLE}") set(sip_helper_FOUND TRUE) @@ -42,6 +44,138 @@ else() set(sip_helper_NOTFOUND TRUE) endif() +if(sip_helper_FOUND) + execute_process( + COMMAND ${SIP_EXECUTABLE} -V + OUTPUT_VARIABLE SIP_VERSION + ERROR_QUIET) + string(STRIP ${SIP_VERSION} SIP_VERSION) + message(STATUS "SIP binding generator version: ${SIP_VERSION}") +endif() + +execute_process( + COMMAND ${Python3_EXECUTABLE} -c "import sysconfig as c; print(c.get_config_var('EXT_SUFFIX'), end='')" + OUTPUT_VARIABLE PYTHON_EXTENSION_MODULE_SUFFIX + ERROR_QUIET) + +# +# Run the SIP generator and compile the generated code into a library. +# +# .. note:: The target lib${PROJECT_NAME} is created. +# +# :param PROJECT_NAME: The name of the sip project +# :type PROJECT_NAME: string +# :param SIP_FILE: the SIP file to be processed +# :type SIP_FILE: string +# +# The following options can be used to override the default behavior: +# SIP_CONFIGURE: the used configure script for SIP +# (default: sip_configure.py in the same folder as this file) +# SOURCE_DIR: the source dir (default: ${PROJECT_SOURCE_DIR}/src) +# LIBRARY_DIR: the library dir (default: ${PROJECT_SOURCE_DIR}/src) +# BINARY_DIR: the binary dir (default: ${PROJECT_BINARY_DIR}) +# +# The following keywords arguments can be used to specify: +# DEPENDS: depends for the custom command +# (should list all sip and header files) +# DEPENDENCIES: target dependencies +# (should list the library for which SIP generates the bindings) +# +function(build_sip_6_binding PROJECT_NAME SIP_FILE) + cmake_parse_arguments(sip "" "SIP_CONFIGURE;SOURCE_DIR;LIBRARY_DIR;BINARY_DIR" "DEPENDS;DEPENDENCIES" ${ARGN}) + if(sip_UNPARSED_ARGUMENTS) + message(WARNING "build_sip_binding(${PROJECT_NAME}) called with unused arguments: ${sip_UNPARSED_ARGUMENTS}") + endif() + + # set default values for optional arguments + if(NOT sip_SIP_CONFIGURE) + # default to sip_configure.py in this directory + set(sip_SIP_CONFIGURE ${__PYTHON_QT_BINDING_SIP_HELPER_DIR}/sip_configure.py) + endif() + if(NOT sip_SOURCE_DIR) + set(sip_SOURCE_DIR ${PROJECT_SOURCE_DIR}/src) + endif() + if(NOT sip_LIBRARY_DIR) + set(sip_LIBRARY_DIR ${PROJECT_SOURCE_DIR}/lib) + endif() + if(NOT sip_BINARY_DIR) + set(sip_BINARY_DIR ${PROJECT_BINARY_DIR}) + endif() + + set(SIP_BUILD_DIR ${sip_BINARY_DIR}/sip/${PROJECT_NAME}) + + set(INCLUDE_DIRS ${${PROJECT_NAME}_INCLUDE_DIRS} ${Python3_INCLUDE_DIRS}) + set(LIBRARIES ${${PROJECT_NAME}_LIBRARIES}) + set(LIBRARY_DIRS ${${PROJECT_NAME}_LIBRARY_DIRS}) + set(LDFLAGS_OTHER ${${PROJECT_NAME}_LDFLAGS_OTHER}) + + find_program(QMAKE_EXECUTABLE NAMES qmake REQUIRED) + + file(REMOVE_RECURSE ${SIP_BUILD_DIR}) + file(MAKE_DIRECTORY ${sip_LIBRARY_DIR}) + set(SIP_FILES_DIR ${sip_SOURCE_DIR}) + + set(SIP_INCLUDE_DIRS "") + foreach(_x ${INCLUDE_DIRS}) + set(SIP_INCLUDE_DIRS "${SIP_INCLUDE_DIRS},\"${_x}\"") + endforeach() + string(REGEX REPLACE "^," "" SIP_INCLUDE_DIRS "${SIP_INCLUDE_DIRS}") + + # pyproject.toml expects libraries listed as such to be added to the linker command + # via `-l`, but this does not work for libraries with absolute paths + # instead we have to pass them to the linker via a different parameter + set(_SIP_ABS_LIBRARIES "") + foreach(_x ${LIBRARIES} ${Python3_LIBRARIES}) + cmake_path(IS_ABSOLUTE _x is_abs) + if(is_abs) + list(APPEND _SIP_ABS_LIBRARIES "${_x}") + endif() + endforeach() + + if(APPLE) + set(LIBQT_GUI_CPP_SIP_SUFFIX .so) + elseif(WIN32) + set(LIBQT_GUI_CPP_SIP_SUFFIX .pyd) + else() + set(LIBQT_GUI_CPP_SIP_SUFFIX ${CMAKE_SHARED_LIBRARY_SUFFIX}) + endif() + + list(APPEND _SIP_ABS_LIBRARIES ${sip_BINARY_DIR}/${sip_DEPENDENCIES}${LIBQT_GUI_CPP_SIP_SUFFIX}) + list(JOIN _SIP_ABS_LIBRARIES " " SIP_ABS_LIBRARIES) + + set(SIP_LIBRARY_DIRS "") + foreach(_x ${LIBRARY_DIRS}) + set(SIP_LIBRARY_DIRS "${SIP_LIBRARY_DIRS},\"${_x}\"") + endforeach() + string(REGEX REPLACE "^," "" SIP_LIBRARY_DIRS "${SIP_LIBRARY_DIRS}") + + set(SIP_EXTRA_DEFINES "") + foreach(_x ${EXTRA_DEFINES}) + set(SIP_EXTRA_DEFINES "${SIP_EXTRA_DEFINES},\"${_x}\"") + endforeach() + string(REGEX REPLACE "^," "" SIP_EXTRA_DEFINES "${SIP_EXTRA_DEFINES}") + + set(SIP_PROJECT_INCLUDE_DIRS /home/ahcorde/ros2_rolling/qt6-env/lib/python3.12/site-packages/PyQt6/bindings) + + configure_file( + ${sip_SOURCE_DIR}/pyproject.toml.in + ${sip_BINARY_DIR}/sip/pyproject.toml + ) + add_custom_command( + OUTPUT ${sip_LIBRARY_DIR}/lib${PROJECT_NAME}${PYTHON_EXTENSION_MODULE_SUFFIX} + COMMAND ${Python3_EXECUTABLE} -m pip install . --target ${sip_LIBRARY_DIR} --no-deps --no-build-isolation + DEPENDS ${sip_SIP_CONFIGURE} ${SIP_FILE} ${sip_DEPENDS} + WORKING_DIRECTORY ${sip_BINARY_DIR}/sip + COMMENT "Running SIP-build generator for ${PROJECT_NAME} Python bindings..." + ) + + add_custom_target(lib${PROJECT_NAME} ALL + DEPENDS ${sip_LIBRARY_DIR}/lib${PROJECT_NAME}${PYTHON_EXTENSION_MODULE_SUFFIX} + COMMENT "Meta target for ${PROJECT_NAME} Python bindings..." + ) + add_dependencies(lib${PROJECT_NAME} ${sip_DEPENDENCIES}) +endfunction() + # # Run the SIP generator and compile the generated code into a library. # @@ -95,8 +229,7 @@ function(build_sip_binding PROJECT_NAME SIP_FILE) add_custom_command( OUTPUT ${SIP_BUILD_DIR}/Makefile - COMMAND ${Python3_EXECUTABLE} ${sip_SIP_CONFIGURE} ${SIP_BUILD_DIR} ${SIP_FILE} ${sip_LIBRARY_DIR} - \"${INCLUDE_DIRS}\" \"${LIBRARIES}\" \"${LIBRARY_DIRS}\" \"${LDFLAGS_OTHER}\" + COMMAND ${Python3_EXECUTABLE} ${sip_SIP_CONFIGURE} ${SIP_BUILD_DIR} ${SIP_FILE} ${sip_LIBRARY_DIR} \"${INCLUDE_DIRS}\" \"${LIBRARIES}\" \"${LIBRARY_DIRS}\" \"${LDFLAGS_OTHER}\" \"${EXTRA_DEFINES}\" DEPENDS ${sip_SIP_CONFIGURE} ${SIP_FILE} ${sip_DEPENDS} WORKING_DIRECTORY ${sip_SOURCE_DIR} COMMENT "Running SIP generator for ${PROJECT_NAME} Python bindings..." diff --git a/package.xml b/package.xml index 34703d6..7a454d2 100644 --- a/package.xml +++ b/package.xml @@ -28,7 +28,7 @@ ament_cmake - qtbase5-dev + qt6-base-dev python3-qt5-bindings python3-qt5-bindings @@ -36,7 +36,7 @@ ament_cmake_pytest ament_lint_auto ament_lint_common - + ament_cmake diff --git a/src/python_qt6_binding/__init__.py b/src/python_qt6_binding/__init__.py new file mode 100644 index 0000000..59a8769 --- /dev/null +++ b/src/python_qt6_binding/__init__.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python + +# Copyright (c) 2011, Dirk Thomas, Dorian Scholz, TU Darmstadt +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided +# with the distribution. +# * Neither the name of the TU Darmstadt nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +""" +Abstraction for different Python Qt bindings. + +Supported Python Qt 5 bindings are PyQt and PySide. +The Qt modules can be imported like this: + +from python_qt_binding.QtCore import QObject +from python_qt_binding import QtGui, loadUi + +The name of the selected binding is available in QT_BINDING. +The version of the selected binding is available in QT_BINDING_VERSION. +All available Qt modules are listed in QT_BINDING_MODULES. + +The default binding order ('pyqt', 'pyside') can be overridden with a +SELECT_QT_BINDING_ORDER attribute on sys: + setattr(sys, 'SELECT_QT_BINDING_ORDER', [FIRST_NAME, NEXT_NAME, ..]) + +A specific binding can be selected with a SELECT_QT_BINDING attribute on sys: + setattr(sys, 'SELECT_QT_BINDING', MY_BINDING_NAME) +""" + +import sys + +from python_qt_binding.binding_helper import loadUi # noqa: F401 +from python_qt_binding.binding_helper import QT_BINDING # noqa: F401 +from python_qt_binding.binding_helper import QT_BINDING_MODULES +from python_qt_binding.binding_helper import QT_BINDING_VERSION # noqa: F401 + +print('QT_BINDING', QT_BINDING) +for module, value in QT_BINDING_MODULES.items(): + print('QT_BINDING_MODULES', module, value) +print('QT_BINDING_VERSION', QT_BINDING_VERSION) + +# register binding modules as sub modules of this package (python_qt_binding) for easy importing +for module_name, module in QT_BINDING_MODULES.items(): + sys.modules[__name__ + '.' + module_name] = module + setattr(sys.modules[__name__], module_name, module) + del module_name + del module + +del sys diff --git a/src/python_qt6_binding/binding_helper.py b/src/python_qt6_binding/binding_helper.py new file mode 100644 index 0000000..bd5bca5 --- /dev/null +++ b/src/python_qt6_binding/binding_helper.py @@ -0,0 +1,287 @@ +#!/usr/bin/env python + +# Copyright (c) 2011, Dirk Thomas, Dorian Scholz, TU Darmstadt +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided +# with the distribution. +# * Neither the name of the TU Darmstadt nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +try: + import __builtin__ as builtins +except ImportError: + # since the 'future' package provides a 'builtins' module in Python 2 + # this must not be checked second + import builtins +import os +import platform +import sys +import traceback + + +QT_BINDING = None +QT_BINDING_MODULES = {} +QT_BINDING_VERSION = None + + +def _select_qt_binding(binding_name=None, binding_order=None): + global QT_BINDING, QT_BINDING_VERSION + + # order of default bindings can be changed here + DEFAULT_BINDING_ORDER = ['pyqt'] + # if platform.system() == 'Darwin': + # DEFAULT_BINDING_ORDER = ['pyside'] + # else: + # DEFAULT_BINDING_ORDER = ['pyside', 'pyqt'] + + binding_order = binding_order or DEFAULT_BINDING_ORDER + + # determine binding preference + if binding_name: + if binding_name not in binding_order: + raise ImportError("Qt binding '%s' is unknown" % binding_name) + binding_order = [binding_name] + + required_modules = [ + 'QtCore', + 'QtGui', + 'QtWidgets', + ] + optional_modules = [ + 'QtBluetooth', + 'QtDBus', + 'QtDesigner', + 'QtHelp', + 'QtLocation', + 'QtMultimedia', + 'QtMultimediaWidgets', + 'QtNetwork', + 'QNetworkAuth', + 'QtNfc', + 'QtOpenGL', + 'QtPositioning', + 'QtPrintSupport', + 'QtQml', + 'QtQuick', + 'QtQuickWidgets', + 'QtScript', + 'QtScriptTools', + 'QtSensors', + 'QtSerialPort', + 'QtSql', + 'QtSvg', + 'QtTest', + 'QtWebChannel', + 'QtWebEngine', # Qt 5.6 and higher + 'QtWebEngineCore', + 'QtWebEngineWidgets', + 'QtWebKitWidgets', # Qt 5.0 - 5.5 + 'QtWebSockets', + 'QtX11Extras', + 'QtXml', + 'QtXmlPatterns', + ] + + # try to load preferred bindings + error_msgs = [] + for binding_name in binding_order: + try: + binding_loader = getattr(sys.modules[__name__], '_load_%s' % binding_name, None) + if binding_loader: + QT_BINDING_VERSION = binding_loader(required_modules, optional_modules) + QT_BINDING = binding_name + break + else: + error_msgs.append(" Binding loader '_load_%s' not found." % binding_name) + except ImportError as e: + error_msgs.append(" ImportError for '%s': %s\n%s" % + (binding_name, e, traceback.format_exc())) + + if not QT_BINDING: + raise ImportError( + 'Could not find Qt binding (looked for: %s):\n%s' % + (', '.join(["'%s'" % b for b in binding_order]), '\n'.join(error_msgs))) + + +def _register_binding_module(module_name, module): + # register module using only its own name (TODO: legacy compatibility, remove when possible) + sys.modules[module_name] = module + # add module to the binding modules + QT_BINDING_MODULES[module_name] = module + + +def _named_import(name): + parts = name.split('.') + assert len(parts) >= 2 + module = builtins.__import__(name) + for m in parts[1:]: + module = module.__dict__[m] + module_name = parts[-1] + _register_binding_module(module_name, module) + + +def _named_optional_import(name): + try: + _named_import(name) + except ImportError: + pass + + +def _load_pyqt(required_modules, optional_modules): + # set environment variable QT_API for matplotlib + os.environ['QT_API'] = 'pyqt' + + # register required and optional PyQt modules + for module_name in required_modules: + print('PyQt6.%s' % module_name) + _named_import('PyQt6.%s' % module_name) + for module_name in optional_modules: + _named_optional_import('PyQt6.%s' % module_name) + + # set some names for compatibility with PySide + sys.modules['QtCore'].Signal = sys.modules['QtCore'].pyqtSignal + sys.modules['QtCore'].Slot = sys.modules['QtCore'].pyqtSlot + sys.modules['QtCore'].Property = sys.modules['QtCore'].pyqtProperty + + # try to register Qwt module + try: + import PyQt6.Qwt6 + _register_binding_module('Qwt', PyQt6.Qwt6) + except ImportError: + pass + + global _loadUi + + def _loadUi(uifile, baseinstance=None, custom_widgets_=None): + from PyQt6 import uic + return uic.loadUi(uifile, baseinstance=baseinstance) + + import PyQt6.QtCore + return PyQt6.QtCore.PYQT_VERSION_STR + + +def _load_pyside(required_modules, optional_modules): + # set environment variable QT_API for matplotlib + os.environ['QT_API'] = 'pyside' + + # register required and optional PySide modules + for module_name in required_modules: + _named_import('PySide6.%s' % module_name) + for module_name in optional_modules: + _named_optional_import('PySide6.%s' % module_name) + + # set some names for compatibility with PyQt + sys.modules['QtCore'].pyqtSignal = sys.modules['QtCore'].Signal + sys.modules['QtCore'].pyqtSlot = sys.modules['QtCore'].Slot + sys.modules['QtCore'].pyqtProperty = sys.modules['QtCore'].Property + + # try to register PySideQwt module + try: + import PySideQwt + _register_binding_module('Qwt', PySideQwt) + except ImportError: + pass + + global _loadUi + + def _loadUi(uifile, baseinstance=None, custom_widgets=None): + from PySide6.QtUiTools import QUiLoader + from PySide6.QtCore import QMetaObject + + class CustomUiLoader(QUiLoader): + class_aliases = { + 'Line': 'QFrame', + } + + def __init__(self, baseinstance=None, custom_widgets=None): + super(CustomUiLoader, self).__init__(baseinstance) + self._base_instance = baseinstance + self._custom_widgets = custom_widgets or {} + + def createWidget(self, class_name, parent=None, name=''): + # don't create the top-level widget, if a base instance is set + if self._base_instance and not parent: + return self._base_instance + + if class_name in self._custom_widgets: + widget = self._custom_widgets[class_name](parent) + else: + widget = QUiLoader.createWidget(self, class_name, parent, name) + + if str(type(widget)).find(self.class_aliases.get(class_name, class_name)) < 0: + sys.modules['QtCore'].qDebug( + 'PySide.loadUi(): could not find widget class "%s", defaulting to "%s"' % + (class_name, type(widget))) + + if self._base_instance: + setattr(self._base_instance, name, widget) + + return widget + + loader = CustomUiLoader(baseinstance, custom_widgets) + + # instead of passing the custom widgets, they should be registered using + # QUiLoader.registerCustomWidget(), + # but this does not work in PySide 1.0.6: it simply segfaults... + # loader = CustomUiLoader(baseinstance) + # custom_widgets = custom_widgets or {} + # for custom_widget in custom_widgets.values(): + # loader.registerCustomWidget(custom_widget) + + ui = loader.load(uifile) + QMetaObject.connectSlotsByName(ui) + return ui + + import PySide6 + return PySide6.__version__ + + +def loadUi(uifile, baseinstance=None, custom_widgets=None): + """ + Load a provided UI file chosen Python Qt 5 binding. + + @type uifile: str + @param uifile: Absolute path of .ui file + @type baseinstance: QWidget + @param baseinstance: the optional instance of the Qt base class. + If specified then the user interface is created in + it. Otherwise a new instance of the base class is + automatically created. + @type custom_widgets: dict of {str:QWidget} + @param custom_widgets: Class name and type of the custom classes used + in uifile if any. This can be None if no custom + class is in use. (Note: this is only necessary + for PySide, see + http://answers.ros.org/question/56382/what-does-python_qt_bindingloaduis-3rd-arg-do-in-pyqt-binding/ + for more information) + """ + return _loadUi(uifile, baseinstance, custom_widgets) + + +_select_qt_binding( + getattr(sys, 'SELECT_QT_BINDING', None), + getattr(sys, 'SELECT_QT_BINDING_ORDER', None), +) diff --git a/src/python_qt_binding/__init__.py b/src/python_qt_binding/__init__.py index 1e209de..59a8769 100644 --- a/src/python_qt_binding/__init__.py +++ b/src/python_qt_binding/__init__.py @@ -58,6 +58,11 @@ from python_qt_binding.binding_helper import QT_BINDING_MODULES from python_qt_binding.binding_helper import QT_BINDING_VERSION # noqa: F401 +print('QT_BINDING', QT_BINDING) +for module, value in QT_BINDING_MODULES.items(): + print('QT_BINDING_MODULES', module, value) +print('QT_BINDING_VERSION', QT_BINDING_VERSION) + # register binding modules as sub modules of this package (python_qt_binding) for easy importing for module_name, module in QT_BINDING_MODULES.items(): sys.modules[__name__ + '.' + module_name] = module diff --git a/src/python_qt_binding/binding_helper.py b/src/python_qt_binding/binding_helper.py index 27c3237..63ccf92 100644 --- a/src/python_qt_binding/binding_helper.py +++ b/src/python_qt_binding/binding_helper.py @@ -282,4 +282,4 @@ class is in use. (Note: this is only necessary _select_qt_binding( getattr(sys, 'SELECT_QT_BINDING', None), getattr(sys, 'SELECT_QT_BINDING_ORDER', None), -) +) \ No newline at end of file