├── .gitignore ├── CHANGELOG.rst ├── CMakeLists.txt ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── cmake ├── shiboken_helper.cmake ├── sip_configure.py └── sip_helper.cmake ├── package.xml ├── pytest.ini ├── setup.py ├── src └── python_qt_binding │ ├── __init__.py │ └── binding_helper.py └── test └── test_imports.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *~ 3 | src/python_qt_binding.egg-info/ 4 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 2 | Changelog for package python_qt_binding 3 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 4 | 5 | 2.4.0 (2025-04-28) 6 | ------------------ 7 | * Remove the mirror-rolling-to-main workflow (`#145 `_) 8 | * Remove CODEOWNERS (`#144 `_) 9 | * Contributors: Alejandro Hernández Cordero, Chris Lalancette 10 | 11 | 2.3.1 (2024-06-25) 12 | ------------------ 13 | * Skip running the tests on Windows Debug. (`#142 `_) 14 | * Contributors: Chris Lalancette 15 | 16 | 2.3.0 (2024-04-26) 17 | ------------------ 18 | * Only suppress Python warnings on new enough CMake (`#139 `_) 19 | * Older CMake doesn't have the policy, so skip it there. 20 | * Suppress warning from Shiboken2. (`#137 `_) 21 | The comment has more information on why we are doing this. 22 | * Contributors: Alejandro Hernández Cordero, Chris Lalancette 23 | 24 | 2.2.0 (2024-03-28) 25 | ------------------ 26 | * Switch to C++17 for SIP and Shiboken (`#135 `_) 27 | * Set hints to find the python version we actually want. (`#134 `_) 28 | * Contributors: Chris Lalancette, Christophe Bedard 29 | 30 | 2.1.1 (2024-02-07) 31 | ------------------ 32 | * Remove unnecessary parentheses around assert. (`#133 `_) 33 | * Contributors: Chris Lalancette 34 | 35 | 2.1.0 (2024-01-24) 36 | ------------------ 37 | * Switch to FindPython3 in the shiboken_helper.cmake. (`#132 `_) 38 | * Contributors: Chris Lalancette 39 | 40 | 2.0.0 (2023-12-26) 41 | ------------------ 42 | * Cleanup of the sip_configure.py file. (`#131 `_) 43 | * Update the SIP support so we can deal with a broken RHEL-9. (`#129 `_) 44 | * Contributors: Chris Lalancette 45 | 46 | 1.3.0 (2023-04-28) 47 | ------------------ 48 | 49 | 1.2.3 (2023-04-11) 50 | ------------------ 51 | * Fix to allow ninja to use make for generators (`#123 `_) 52 | * Fix flake8 linter regression (`#125 `_) 53 | * Remove pyqt from default binding order for macOS (`#118 `_) 54 | * Contributors: Christoph Hellmann Santos, Cristóbal Arroyo, Michael Carroll, Rhys Mainwaring 55 | 56 | 1.2.2 (2023-02-24) 57 | ------------------ 58 | * Demote missing SIP message from WARNING to STATUS (`#122 `_) 59 | * Contributors: Scott K Logan 60 | 61 | 1.2.1 (2023-02-14) 62 | ------------------ 63 | * [rolling] Update maintainers - 2022-11-07 (`#120 `_) 64 | * Contributors: Audrow Nash 65 | 66 | 1.2.0 (2022-05-10) 67 | ------------------ 68 | 69 | 1.1.1 (2021-12-06) 70 | ------------------ 71 | * Replace PythonInterp to Python3 COMPONENTS (`#108 `_) 72 | * Use PyQt5 module path to find SIP bindings (`#106 `_) 73 | * Contributors: Ben Wolsieffer, Homalozoa X 74 | 75 | 1.1.0 (2021-11-02) 76 | ------------------ 77 | * Make FindPythonInterp dependency explicit (`#107 `_) 78 | * Add note about galactic branch (`#104 `_) 79 | * fuerte-devel is too new for ROS Electric (`#101 `_) 80 | * Contributors: Shane Loretz 81 | 82 | 1.0.7 (2021-03-18) 83 | ------------------ 84 | * Add repo README 85 | * Shorten some long lines of CMake (`#99 `_) 86 | * Contributors: Scott K Logan, Shane Loretz 87 | 88 | 1.0.6 (2021-01-25) 89 | ------------------ 90 | * Update maintainers (`#96 `_) (`#98 `_) 91 | * Add pytest.ini so local tests don't display warning (`#93 `_) 92 | * Contributors: Chris Lalancette, Shane Loretz 93 | 94 | 1.0.5 (2020-05-26) 95 | ------------------ 96 | * allow a list of INCLUDE_PATH (`#92 `_) 97 | * Use magic $(MAKE) variable to suppress build warning (`#91 `_) 98 | * Fix linking with non framework builds of qt (e.g. from conda-forge) (`#84 `_) 99 | * Contributors: Anton Matosov, Dirk Thomas, Robert Haschke 100 | 101 | 1.0.4 (2020-05-05) 102 | ------------------ 103 | * remove obsolete function used for backward compatibility (`#88 `_) 104 | * disable Shiboken with CMake < 3.14 (`#87 `_) 105 | * fix case of CMake function (`#86 `_) 106 | * restore QUIET which was reverted in `#79 `_ 107 | * use PySide2 and Shiboken2 targets for variables (`#79 `_) 108 | * Contributors: Dirk Thomas, Hermann von Kleist 109 | 110 | 1.0.3 (2019-11-12) 111 | ------------------ 112 | * check if Shiboken2Config.cmake defines a target instead of a variable (`#77 `_) 113 | 114 | 1.0.2 (2019-09-30) 115 | ------------------ 116 | * replace Qt variable in generated Makefile (`#64 `_) 117 | * don't add -l prefix if it already exists (`#59 `_) 118 | * if present, use the sipconfig suggested sip program (`#70 `_) 119 | * replace Qt variable in generated Makefile (`#64 `_) (`#67 `_) 120 | * fixing trivial accidental string concatenation (`#66 `_) 121 | 122 | 1.0.1 (2018-12-11) 123 | ------------------ 124 | * no warnings for unavailable PySide/Shiboken (`#58 `_) 125 | 126 | 1.0.0 (2018-12-10) 127 | ------------------ 128 | * check for Homebrew's PyQt5 install path (`#57 `_) 129 | * port to Windows (`#56 `_) 130 | * fix lint tests (`#55 `_) 131 | * update sip_configure to handle improper lib names (`#54 `_) 132 | * port to ROS 2 (`#52 `_) 133 | * autopep8 (`#51 `_) 134 | * remove :: from shiboken include path (`#48 `_) 135 | 136 | 0.3.4 (2018-08-03) 137 | ------------------ 138 | * add support for additional Qt5 modules (`#45 `_) 139 | 140 | 0.3.3 (2017-10-25) 141 | ------------------ 142 | * Prefer qmake-qt5 over qmake when available (`#43 `_) 143 | 144 | 0.3.2 (2017-01-23) 145 | ------------------ 146 | * Fix problems on OS X (`#40 `_) 147 | 148 | 0.3.1 (2016-04-21) 149 | ------------------ 150 | * support for the Qt 5 modules QtWebEngine and QtWebKitWidgets (`#37 `_) 151 | 152 | 0.3.0 (2016-04-01) 153 | ------------------ 154 | * switch to Qt5 (`#30 `_) 155 | * print full stacktrace 156 | 157 | 0.2.18 (2016-03-17) 158 | ------------------- 159 | * remove LGPL and GPL from licenses, all code is BSD (`#27 `_) 160 | 161 | 0.2.17 (2015-09-19) 162 | ------------------- 163 | * change import order of builtins to work when the 'future' package is installed in Python 2 (`#24 `_) 164 | 165 | 0.2.16 (2015-05-04) 166 | ------------------- 167 | * use qmake with QT_SELECT since qmake-qt4 is not available on all platforms (`#22 `_) 168 | 169 | 0.2.15 (2015-04-23) 170 | ------------------- 171 | * support PyQt4.11 and higher when built with configure-ng.py (`#13 `_) 172 | * __builtin__ became builtins in Python 3 (`#16 `_) 173 | 174 | 0.2.14 (2014-07-10) 175 | ------------------- 176 | * add Python_ADDITIONAL_VERSIONS and ask for specific version of PythonInterp 177 | * fix finding specific version of PythonLibs with CMake 3 (`#11 `_) 178 | * fix sip_helper to use python header dirs on OS X (`#12 `_) 179 | 180 | 0.2.13 (2014-05-07) 181 | ------------------- 182 | * fix sip arguments when path contains spaces 183 | 184 | 0.2.12 (2014-01-08) 185 | ------------------- 186 | * python 3 compatibility 187 | * fix sip bindings when paths contain spaces (`#9 `_) 188 | 189 | 0.2.11 (2013-08-21) 190 | ------------------- 191 | * allow overriding binding order 192 | * allow to release python_qt_binding as a standalone package to PyPI (`#5 `_) 193 | 194 | 0.2.10 (2013-06-06) 195 | ------------------- 196 | * refactor loadUi function to be documentable (`#2 `_) 197 | 198 | 0.2.9 (2013-04-19) 199 | ------------------ 200 | 201 | 0.2.8 (2013-01-13) 202 | ------------------ 203 | 204 | 0.2.7 (2012-12-21) 205 | ------------------ 206 | * first public release for Groovy 207 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5) 2 | project(python_qt_binding) 3 | 4 | find_package(ament_cmake REQUIRED) 5 | find_package(ament_cmake_python REQUIRED) 6 | 7 | ament_python_install_package(${PROJECT_NAME} 8 | PACKAGE_DIR src/${PROJECT_NAME}) 9 | 10 | install(FILES 11 | cmake/shiboken_helper.cmake 12 | cmake/sip_configure.py 13 | cmake/sip_helper.cmake 14 | DESTINATION share/${PROJECT_NAME}/cmake) 15 | 16 | if(BUILD_TESTING) 17 | find_package(ament_cmake_pytest REQUIRED) 18 | find_package(ament_lint_auto REQUIRED) 19 | 20 | # Disabling copyright test. The copyright used in this package does not conform to 21 | # ament's copyright tests 22 | set(ament_cmake_copyright_FOUND TRUE) 23 | ament_lint_auto_find_test_dependencies() 24 | 25 | ament_add_pytest_test(python_qt_binding test 26 | APPEND_ENV PYTHONPATH=${CMAKE_CURRENT_BINARY_DIR} 27 | TIMEOUT 90) 28 | endif() 29 | 30 | ament_package() 31 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Any contribution that you make to this repository will 2 | be under the BSD license. 3 | [license](https://opensource.org/licenses/BSD-3-Clause): 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011, Dirk Thomas, Dorian Scholz, TU Darmstadt 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include package.xml 2 | recursive-include cmake *.cmake *.py 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all setup clean_dist distro clean install upload push 2 | 3 | NAME=python_qt_binding 4 | VERSION=`./setup.py --version` 5 | 6 | all: 7 | echo "noop for debbuild" 8 | 9 | setup: 10 | echo "building version ${VERSION}" 11 | 12 | clean_dist: 13 | -rm -f MANIFEST 14 | -rm -rf dist 15 | 16 | distro: setup clean_dist 17 | python setup.py sdist 18 | 19 | push: distro 20 | python setup.py sdist register upload 21 | scp dist/${NAME}-${VERSION}.tar.gz ros@ftp-osl.osuosl.org:/home/ros/data/download.ros.org/downloads/${NAME} 22 | 23 | clean: clean_dist 24 | echo "clean" 25 | 26 | install: distro 27 | sudo checkinstall python setup.py install 28 | 29 | testsetup: 30 | echo "running ${NAME} tests" 31 | 32 | test: testsetup 33 | cd test && nosetests 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python_qt_binding 2 | 3 | This stack provides Python bindings for Qt. 4 | There are two providers: pyside and pyqt. 5 | PySide2 is available under the GPL, LGPL and a commercial license. 6 | PyQt is released under the GPL. 7 | 8 | Both the bindings and tools to build bindings are included from each available provider. 9 | For PySide, it is called "Shiboken". 10 | For PyQt, this is called "SIP". 11 | 12 | Also provided is adapter code to make the user's Python code independent of which binding provider was actually used. 13 | 14 | # Contributing 15 | 16 | For ROS 2 changes, please open pull requests on the branch `main`. 17 | For ROS 1 changes, please open pull requests on the `melodic-devel` branch. 18 | If you would like changes made to older ROS distro branches, backporting will be considered once the changes are made to the latest branch. 19 | Changes will only be made to [active ROS Distros](https://dlu.github.io/ros_clock/index.html). 20 | 21 | # Branches 22 | 23 | The branches on this repo are for different ROS distros. 24 | 25 | * `main` - ROS Rolling 26 | * `galactic_devel` - ROS Galactic 27 | * `crystal-devel` - ROS Foxy, ROS Eloquent, ROS Dashing, ROS Crystal 28 | * `melodic-devel` - ROS Noetic, ROS Melodic 29 | * `kinetic-devel` - ROS Lunar, ROS Kinetic 30 | * `groovy-devel` - ROS Jade, ROS Indigo, ROS Hydro, ROS Groovy 31 | * `fuerte-devel` - ROS Fuerte 32 | -------------------------------------------------------------------------------- /cmake/shiboken_helper.cmake: -------------------------------------------------------------------------------- 1 | # By default, without the settings below, find_package(Python3) will attempt 2 | # to find the newest python version it can, and additionally will find the 3 | # most specific version. For instance, on a system that has 4 | # /usr/bin/python3.10, /usr/bin/python3.11, and /usr/bin/python3, it will find 5 | # /usr/bin/python3.11, even if /usr/bin/python3 points to /usr/bin/python3.10. 6 | # The behavior we want is to prefer the "system" installed version unless the 7 | # user specifically tells us othewise through the Python3_EXECUTABLE hint. 8 | # Setting CMP0094 to NEW means that the search will stop after the first 9 | # python version is found. Setting Python3_FIND_UNVERSIONED_NAMES means that 10 | # the search will prefer /usr/bin/python3 over /usr/bin/python3.11. And that 11 | # latter functionality is only available in CMake 3.20 or later, so we need 12 | # at least that version. 13 | cmake_minimum_required(VERSION 3.20) 14 | cmake_policy(SET CMP0094 NEW) 15 | set(Python3_FIND_UNVERSIONED_NAMES FIRST) 16 | 17 | find_package(Python3 REQUIRED COMPONENTS Interpreter Development) 18 | 19 | if(__PYTHON_QT_BINDING_SHIBOKEN_HELPER_INCLUDED) 20 | return() 21 | endif() 22 | set(__PYTHON_QT_BINDING_SHIBOKEN_HELPER_INCLUDED TRUE) 23 | 24 | # In CMake 3.27 and later, FindPythonInterp and FindPythonLibs are deprecated. 25 | # However, Shiboken2 as packaged in Ubuntu 24.04 still use them, so set CMP0148 to 26 | # "OLD" to silence this warning. 27 | if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.27.0") 28 | cmake_policy(SET CMP0148 OLD) 29 | endif() 30 | find_package(Shiboken2 QUIET) 31 | if(Shiboken2_FOUND) 32 | message(STATUS "Found Shiboken2 version ${Shiboken2_VERSION}") 33 | if(NOT ${Shiboken2_VERSION} VERSION_LESS "5.13") 34 | get_property(SHIBOKEN_INCLUDE_DIR TARGET Shiboken2::libshiboken PROPERTY INTERFACE_INCLUDE_DIRECTORIES) 35 | get_property(SHIBOKEN_LIBRARY TARGET Shiboken2::libshiboken PROPERTY LOCATION) 36 | set(SHIBOKEN_BINARY Shiboken2::shiboken2) 37 | endif() 38 | message(STATUS "Using SHIBOKEN_INCLUDE_DIR: ${SHIBOKEN_INCLUDE_DIR}") 39 | message(STATUS "Using SHIBOKEN_LIBRARY: ${SHIBOKEN_LIBRARY}") 40 | message(STATUS "Using SHIBOKEN_BINARY: ${SHIBOKEN_BINARY}") 41 | endif() 42 | 43 | find_package(PySide2 QUIET) 44 | if(PySide2_FOUND) 45 | message(STATUS "Found PySide2 version ${PySide2_VERSION}") 46 | if(NOT ${PySide2_VERSION} VERSION_LESS "5.13") 47 | get_property(PYSIDE_INCLUDE_DIR TARGET PySide2::pyside2 PROPERTY INTERFACE_INCLUDE_DIRECTORIES) 48 | get_property(PYSIDE_LIBRARY TARGET PySide2::pyside2 PROPERTY LOCATION) 49 | endif() 50 | message(STATUS "Using PYSIDE_INCLUDE_DIR: ${PYSIDE_INCLUDE_DIR}") 51 | message(STATUS "Using PYSIDE_LIBRARY: ${PYSIDE_LIBRARY}") 52 | endif() 53 | 54 | if(Shiboken2_FOUND AND PySide2_FOUND) 55 | message(STATUS "Shiboken binding generator available.") 56 | set(shiboken_helper_FOUND TRUE) 57 | else() 58 | message(STATUS "Shiboken binding generator NOT available.") 59 | set(shiboken_helper_NOTFOUND TRUE) 60 | endif() 61 | 62 | 63 | macro(_shiboken_generator_command VAR GLOBAL TYPESYSTEM INCLUDE_PATH BUILD_DIR) 64 | # Add includes from current directory, Qt, PySide and compiler specific dirs 65 | get_directory_property(SHIBOKEN_HELPER_INCLUDE_DIRS INCLUDE_DIRECTORIES) 66 | list(APPEND SHIBOKEN_HELPER_INCLUDE_DIRS 67 | ${QT_INCLUDE_DIR} 68 | ${PYSIDE_INCLUDE_DIR} 69 | ${CMAKE_CXX_IMPLICIT_INCLUDE_DIRECTORIES}) 70 | # See ticket https://code.ros.org/trac/ros-pkg/ticket/5219 71 | set(SHIBOKEN_HELPER_INCLUDE_DIRS_WITH_COLONS "") 72 | foreach(dir ${SHIBOKEN_HELPER_INCLUDE_DIRS}) 73 | set(SHIBOKEN_HELPER_INCLUDE_DIRS_WITH_COLONS "${SHIBOKEN_HELPER_INCLUDE_DIRS_WITH_COLONS}:${dir}") 74 | endforeach() 75 | string(REPLACE ";" ":" INCLUDE_PATH_WITH_COLONS "${INCLUDE_PATH}") 76 | set(${VAR} ${SHIBOKEN_BINARY} 77 | --generatorSet=shiboken 78 | --enable-pyside-extensions 79 | -std=c++17 80 | --include-paths=${INCLUDE_PATH_WITH_COLONS}${SHIBOKEN_HELPER_INCLUDE_DIRS_WITH_COLONS} 81 | --typesystem-paths=${PYSIDE_TYPESYSTEMS} 82 | --output-directory=${BUILD_DIR} ${GLOBAL} ${TYPESYSTEM}) 83 | endmacro() 84 | 85 | 86 | # 87 | # Run the Shiboken generator. 88 | # 89 | # :param PROJECT_NAME: The name of the shiboken project is only use for 90 | # the custom command comment 91 | # :type PROJECT_NAME: string 92 | # :param GLOBAL: the SIP file 93 | # :type GLOBAL: string 94 | # :param TYPESYSTEM: the typesystem file 95 | # :type TYPESYSTEM: string 96 | # :param WORKING_DIR: the working directory 97 | # :type WORKING_DIR: string 98 | # :param GENERATED_SRCS: the generated source files 99 | # :type GENERATED_SRCS: list of strings 100 | # :param HDRS: the processed header files 101 | # :type HDRS: list of strings 102 | # :param INCLUDE_PATH: the include path 103 | # :type INCLUDE_PATH: list of strings 104 | # :param BUILD_DIR: the build directory 105 | # :type BUILD_DIR: string 106 | # 107 | function(shiboken_generator PROJECT_NAME GLOBAL TYPESYSTEM WORKING_DIR GENERATED_SRCS HDRS INCLUDE_PATH BUILD_DIR) 108 | _shiboken_generator_command(COMMAND "${GLOBAL}" "${TYPESYSTEM}" "${INCLUDE_PATH}" "${BUILD_DIR}") 109 | add_custom_command( 110 | OUTPUT ${GENERATED_SRCS} 111 | COMMAND ${COMMAND} 112 | DEPENDS ${GLOBAL} ${TYPESYSTEM} ${HDRS} 113 | WORKING_DIRECTORY ${WORKING_DIR} 114 | COMMENT "Running Shiboken generator for ${PROJECT_NAME} Python bindings..." 115 | ) 116 | endfunction() 117 | 118 | 119 | # 120 | # Add the Shiboken/PySide specific include directories. 121 | # 122 | # :param PROJECT_NAME: The namespace of the binding 123 | # :type PROJECT_NAME: string 124 | # :param QT_COMPONENTS: the Qt components 125 | # :type QT_COMPONENTS: list of strings 126 | # 127 | function(shiboken_include_directories PROJECT_NAME QT_COMPONENTS) 128 | set(shiboken_INCLUDE_DIRECTORIES 129 | ${Python3_INCLUDE_DIRS} 130 | ${SHIBOKEN_INCLUDE_DIR} 131 | ${PYSIDE_INCLUDE_DIR} 132 | ${PYSIDE_INCLUDE_DIR}/QtCore 133 | ${PYSIDE_INCLUDE_DIR}/QtGui 134 | ) 135 | 136 | foreach(component ${QT_COMPONENTS}) 137 | set(shiboken_INCLUDE_DIRECTORIES ${shiboken_INCLUDE_DIRECTORIES} ${PYSIDE_INCLUDE_DIR}/${component}) 138 | endforeach() 139 | 140 | include_directories(${PROJECT_NAME} ${shiboken_INCLUDE_DIRECTORIES}) 141 | endfunction() 142 | 143 | 144 | # 145 | # Add the Shiboken/PySide specific link libraries. 146 | # 147 | # :param PROJECT_NAME: The target name of the binding library 148 | # :type PROJECT_NAME: string 149 | # :param QT_COMPONENTS: the Qt components 150 | # :type QT_COMPONENTS: list of strings 151 | # 152 | function(shiboken_target_link_libraries PROJECT_NAME QT_COMPONENTS) 153 | set(shiboken_LINK_LIBRARIES 154 | ${SHIBOKEN_PYTHON_LIBRARIES} 155 | ${SHIBOKEN_LIBRARY} 156 | ${PYSIDE_LIBRARY} 157 | ) 158 | 159 | foreach(component ${QT_COMPONENTS}) 160 | string(TOUPPER ${component} component) 161 | set(shiboken_LINK_LIBRARIES ${shiboken_LINK_LIBRARIES} ${QT_${component}_LIBRARY}) 162 | endforeach() 163 | 164 | target_link_libraries(${PROJECT_NAME} ${shiboken_LINK_LIBRARIES}) 165 | endfunction() 166 | -------------------------------------------------------------------------------- /cmake/sip_configure.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import os 3 | import re 4 | import shutil 5 | import subprocess 6 | import sys 7 | import tempfile 8 | 9 | import PyQt5 10 | from PyQt5 import QtCore 11 | import sipconfig 12 | 13 | libqt5_rename = False 14 | 15 | 16 | class Configuration(sipconfig.Configuration): 17 | 18 | def __init__(self): 19 | env = copy.copy(os.environ) 20 | env['QT_SELECT'] = '5' 21 | qmake_exe = 'qmake-qt5' if shutil.which('qmake-qt5') else 'qmake' 22 | qtconfig = subprocess.check_output( 23 | [qmake_exe, '-query'], env=env, universal_newlines=True) 24 | qtconfig = dict(line.split(':', 1) for line in qtconfig.splitlines()) 25 | pyqtconfig = { 26 | 'qt_archdata_dir': qtconfig['QT_INSTALL_DATA'], 27 | 'qt_data_dir': qtconfig['QT_INSTALL_DATA'], 28 | 'qt_dir': qtconfig['QT_INSTALL_PREFIX'], 29 | 'qt_inc_dir': qtconfig['QT_INSTALL_HEADERS'], 30 | 'qt_lib_dir': qtconfig['QT_INSTALL_LIBS'], 31 | 'qt_threaded': 1, 32 | 'qt_version': QtCore.QT_VERSION, 33 | 'qt_winconfig': 'shared exceptions', 34 | } 35 | if sys.platform == 'darwin': 36 | if os.path.exists(os.path.join(qtconfig['QT_INSTALL_LIBS'], 'QtCore.framework')): 37 | pyqtconfig['qt_framework'] = 1 38 | else: 39 | global libqt5_rename 40 | libqt5_rename = True 41 | 42 | sipconfig.Configuration.__init__(self, [pyqtconfig]) 43 | 44 | macros = sipconfig._default_macros.copy() 45 | macros['INCDIR_QT'] = qtconfig['QT_INSTALL_HEADERS'] 46 | macros['LIBDIR_QT'] = qtconfig['QT_INSTALL_LIBS'] 47 | macros['MOC'] = 'moc-qt5' if shutil.which('moc-qt5') else 'moc' 48 | self.set_build_macros(macros) 49 | 50 | 51 | def get_sip_dir_flags(config): 52 | """ 53 | Get the extra SIP flags needed by the imported qt module, and locate PyQt5 sip install files. 54 | 55 | Note that this normally only includes those flags (-x and -t) that relate to SIP's versioning 56 | system. 57 | """ 58 | try: 59 | sip_dir = config.pyqt_sip_dir 60 | sip_flags = config.pyqt_sip_flags 61 | return sip_dir, sip_flags 62 | except AttributeError: 63 | pass 64 | 65 | # We didn't find the sip_dir and sip_flags from the config, continue looking 66 | 67 | # sipconfig.Configuration does not have a pyqt_sip_dir or pyqt_sip_flags AttributeError 68 | sip_flags = QtCore.PYQT_CONFIGURATION['sip_flags'] 69 | 70 | candidate_sip_dirs = [] 71 | 72 | # Archlinux installs sip files here by default 73 | candidate_sip_dirs.append(os.path.join(PyQt5.__path__[0], 'bindings')) 74 | 75 | # sip4 installs here by default 76 | candidate_sip_dirs.append(os.path.join(sipconfig._pkg_config['default_sip_dir'], 'PyQt5')) 77 | 78 | # Homebrew installs sip files here by default 79 | candidate_sip_dirs.append(os.path.join(sipconfig._pkg_config['default_sip_dir'], 'Qt5')) 80 | 81 | for sip_dir in candidate_sip_dirs: 82 | if os.path.exists(sip_dir): 83 | return sip_dir, sip_flags 84 | 85 | raise FileNotFoundError('The sip directory for PyQt5 could not be located. Please ensure' + 86 | ' that PyQt5 is installed') 87 | 88 | 89 | if len(sys.argv) != 8: 90 | print('usage: %s build-dir sip-file output_dir include_dirs libs lib_dirs ldflags' % 91 | sys.argv[0]) 92 | sys.exit(1) 93 | 94 | # The SIP build folder, the SIP file, the output directory, the include 95 | # directories, the libraries, the library directories and the linker 96 | # flags. 97 | build_dir, sip_file, output_dir, include_dirs, libs, lib_dirs, ldflags = sys.argv[1:] 98 | 99 | # The name of the SIP build file generated by SIP and used by the build system. 100 | build_file = 'pyqtscripting.sbf' 101 | 102 | # Get the PyQt configuration information. 103 | config = Configuration() 104 | 105 | sip_dir, sip_flags = get_sip_dir_flags(config) 106 | 107 | try: 108 | os.makedirs(build_dir) 109 | except OSError: 110 | pass 111 | 112 | # Run SIP to generate the code. Note that we tell SIP where to find the qt 113 | # module's specification files using the -I flag. 114 | 115 | sip_bin = config.sip_bin 116 | # Without the .exe, this might actually be a directory in Windows 117 | if sys.platform == 'win32' and os.path.isdir(sip_bin): 118 | sip_bin += '.exe' 119 | 120 | # SIP4 has an incompatibility with Qt 5.15.6. In particular, Qt 5.15.6 uses a new SIP directive 121 | # called py_ssize_t_clean in QtCoremod.sip that SIP4 does not understand. 122 | # 123 | # Unfortunately, the combination of SIP4 and Qt 5.15.6 is common. Archlinux, Ubuntu 22.04 124 | # and RHEL-9 all have this combination. On Ubuntu 22.04, there is a custom patch to SIP4 125 | # to make it understand the py_ssize_t_clean tag, so the combination works. But on most 126 | # other platforms, it fails. 127 | # 128 | # To workaround this, copy all of the SIP files into a temporary directory, remove the offending 129 | # line, and then use that temporary directory as the include path. This is unnecessary on 130 | # Ubuntu 22.04, but shouldn't hurt anything there. 131 | with tempfile.TemporaryDirectory() as tmpdirname: 132 | shutil.copytree(sip_dir, tmpdirname, dirs_exist_ok=True) 133 | 134 | output = '' 135 | with open(os.path.join(tmpdirname, 'QtCore', 'QtCoremod.sip'), 'r') as infp: 136 | for line in infp: 137 | if line.startswith('%Module(name='): 138 | result = re.sub(r', py_ssize_t_clean=True', '', line) 139 | output += result 140 | else: 141 | output += line 142 | 143 | with open(os.path.join(tmpdirname, 'QtCore', 'QtCoremod.sip'), 'w') as outfp: 144 | outfp.write(output) 145 | 146 | cmd = [ 147 | sip_bin, 148 | '-c', build_dir, 149 | '-b', os.path.join(build_dir, build_file), 150 | '-I', tmpdirname, 151 | '-w' 152 | ] 153 | cmd += sip_flags.split(' ') 154 | cmd.append(sip_file) 155 | 156 | subprocess.check_call(cmd) 157 | 158 | # Create the Makefile. The QtModuleMakefile class provided by the 159 | # pyqtconfig module takes care of all the extra preprocessor, compiler and 160 | # linker flags needed by the Qt library. 161 | makefile = sipconfig.SIPModuleMakefile( 162 | dir=build_dir, 163 | configuration=config, 164 | build_file=build_file, 165 | qt=['QtCore', 'QtGui'] 166 | ) 167 | 168 | # hack to override makefile behavior which always prepend -l to libraries 169 | # which is wrong for absolute paths 170 | default_platform_lib_function = sipconfig.SIPModuleMakefile.platform_lib 171 | 172 | 173 | def custom_platform_lib_function(self, clib, framework=0): 174 | if not clib or clib.isspace(): 175 | return None 176 | # Only add '-l' if a library doesn't already start with '-l' and is not an absolute path 177 | if os.path.isabs(clib) or clib.startswith('-l'): 178 | return clib 179 | 180 | global libqt5_rename 181 | # sip renames libs to Qt5 automatically on Linux, but not on macOS 182 | if libqt5_rename and not framework and clib.startswith('Qt') and not clib.startswith('Qt5'): 183 | return '-lQt5' + clib[2:] 184 | 185 | return default_platform_lib_function(self, clib, framework) 186 | 187 | 188 | sipconfig.SIPModuleMakefile.platform_lib = custom_platform_lib_function 189 | 190 | 191 | # split paths on whitespace 192 | # while dealing with whitespaces within the paths if they are escaped with backslashes 193 | def split_paths(paths): 194 | paths = re.split('(?<=[^\\\\]) ', paths) 195 | return paths 196 | 197 | 198 | for include_dir in split_paths(include_dirs): 199 | include_dir = include_dir.replace('\\', '') 200 | makefile.extra_include_dirs.append(include_dir) 201 | for lib in split_paths(libs): 202 | makefile.extra_libs.append(lib) 203 | for lib_dir in split_paths(lib_dirs): 204 | lib_dir = lib_dir.replace('\\', '') 205 | makefile.extra_lib_dirs.append(lib_dir) 206 | for ldflag in ldflags.split('\\ '): 207 | makefile.LFLAGS.append(ldflag) 208 | 209 | # redirect location of generated library 210 | makefile._target = '"%s"' % os.path.join(output_dir, makefile._target) 211 | 212 | # Force c++17 213 | if sys.platform == 'win32': 214 | makefile.extra_cxxflags.append('/std:c++17') 215 | # The __cplusplus flag is not properly set on Windows for backwards 216 | # compatibilty. This flag sets it correctly 217 | makefile.CXXFLAGS.append('/Zc:__cplusplus') 218 | else: 219 | makefile.extra_cxxflags.append('-std=c++17') 220 | 221 | # Finalise the Makefile, preparing it to be saved to disk 222 | makefile.finalise() 223 | 224 | # Replace Qt variables from libraries 225 | libs = makefile.LIBS.as_list() 226 | for i in range(len(libs)): 227 | libs[i] = libs[i].replace('$$[QT_INSTALL_LIBS]', config.build_macros()['LIBDIR_QT']) 228 | makefile.LIBS.set(libs) 229 | 230 | # Generate the Makefile itself 231 | makefile.generate() 232 | -------------------------------------------------------------------------------- /cmake/sip_helper.cmake: -------------------------------------------------------------------------------- 1 | if(__PYTHON_QT_BINDING_SIP_HELPER_INCLUDED) 2 | return() 3 | endif() 4 | set(__PYTHON_QT_BINDING_SIP_HELPER_INCLUDED TRUE) 5 | 6 | set(__PYTHON_QT_BINDING_SIP_HELPER_DIR ${CMAKE_CURRENT_LIST_DIR}) 7 | 8 | # By default, without the settings below, find_package(Python3) will attempt 9 | # to find the newest python version it can, and additionally will find the 10 | # most specific version. For instance, on a system that has 11 | # /usr/bin/python3.10, /usr/bin/python3.11, and /usr/bin/python3, it will find 12 | # /usr/bin/python3.11, even if /usr/bin/python3 points to /usr/bin/python3.10. 13 | # The behavior we want is to prefer the "system" installed version unless the 14 | # user specifically tells us othewise through the Python3_EXECUTABLE hint. 15 | # Setting CMP0094 to NEW means that the search will stop after the first 16 | # python version is found. Setting Python3_FIND_UNVERSIONED_NAMES means that 17 | # the search will prefer /usr/bin/python3 over /usr/bin/python3.11. And that 18 | # latter functionality is only available in CMake 3.20 or later, so we need 19 | # at least that version. 20 | cmake_minimum_required(VERSION 3.20) 21 | cmake_policy(SET CMP0094 NEW) 22 | set(Python3_FIND_UNVERSIONED_NAMES FIRST) 23 | 24 | find_package(Python3 ${Python3_VERSION} REQUIRED COMPONENTS Interpreter Development) 25 | 26 | execute_process( 27 | COMMAND ${Python3_EXECUTABLE} -c "import sipconfig; print(sipconfig.Configuration().sip_bin)" 28 | OUTPUT_VARIABLE PYTHON_SIP_EXECUTABLE 29 | ERROR_QUIET) 30 | 31 | if(PYTHON_SIP_EXECUTABLE) 32 | string(STRIP ${PYTHON_SIP_EXECUTABLE} SIP_EXECUTABLE) 33 | else() 34 | find_program(SIP_EXECUTABLE sip) 35 | endif() 36 | 37 | if(SIP_EXECUTABLE) 38 | message(STATUS "SIP binding generator available at: ${SIP_EXECUTABLE}") 39 | set(sip_helper_FOUND TRUE) 40 | else() 41 | message(STATUS "SIP binding generator NOT available.") 42 | set(sip_helper_NOTFOUND TRUE) 43 | endif() 44 | 45 | # 46 | # Run the SIP generator and compile the generated code into a library. 47 | # 48 | # .. note:: The target lib${PROJECT_NAME} is created. 49 | # 50 | # :param PROJECT_NAME: The name of the sip project 51 | # :type PROJECT_NAME: string 52 | # :param SIP_FILE: the SIP file to be processed 53 | # :type SIP_FILE: string 54 | # 55 | # The following options can be used to override the default behavior: 56 | # SIP_CONFIGURE: the used configure script for SIP 57 | # (default: sip_configure.py in the same folder as this file) 58 | # SOURCE_DIR: the source dir (default: ${PROJECT_SOURCE_DIR}/src) 59 | # LIBRARY_DIR: the library dir (default: ${PROJECT_SOURCE_DIR}/src) 60 | # BINARY_DIR: the binary dir (default: ${PROJECT_BINARY_DIR}) 61 | # 62 | # The following keywords arguments can be used to specify: 63 | # DEPENDS: depends for the custom command 64 | # (should list all sip and header files) 65 | # DEPENDENCIES: target dependencies 66 | # (should list the library for which SIP generates the bindings) 67 | # 68 | function(build_sip_binding PROJECT_NAME SIP_FILE) 69 | cmake_parse_arguments(sip "" "SIP_CONFIGURE;SOURCE_DIR;LIBRARY_DIR;BINARY_DIR" "DEPENDS;DEPENDENCIES" ${ARGN}) 70 | if(sip_UNPARSED_ARGUMENTS) 71 | message(WARNING "build_sip_binding(${PROJECT_NAME}) called with unused arguments: ${sip_UNPARSED_ARGUMENTS}") 72 | endif() 73 | 74 | # set default values for optional arguments 75 | if(NOT sip_SIP_CONFIGURE) 76 | # default to sip_configure.py in this directory 77 | set(sip_SIP_CONFIGURE ${__PYTHON_QT_BINDING_SIP_HELPER_DIR}/sip_configure.py) 78 | endif() 79 | if(NOT sip_SOURCE_DIR) 80 | set(sip_SOURCE_DIR ${PROJECT_SOURCE_DIR}/src) 81 | endif() 82 | if(NOT sip_LIBRARY_DIR) 83 | set(sip_LIBRARY_DIR ${PROJECT_SOURCE_DIR}/lib) 84 | endif() 85 | if(NOT sip_BINARY_DIR) 86 | set(sip_BINARY_DIR ${PROJECT_BINARY_DIR}) 87 | endif() 88 | 89 | set(SIP_BUILD_DIR ${sip_BINARY_DIR}/sip/${PROJECT_NAME}) 90 | 91 | set(INCLUDE_DIRS ${${PROJECT_NAME}_INCLUDE_DIRS} ${Python3_INCLUDE_DIRS}) 92 | set(LIBRARIES ${${PROJECT_NAME}_LIBRARIES}) 93 | set(LIBRARY_DIRS ${${PROJECT_NAME}_LIBRARY_DIRS}) 94 | set(LDFLAGS_OTHER ${${PROJECT_NAME}_LDFLAGS_OTHER}) 95 | 96 | add_custom_command( 97 | OUTPUT ${SIP_BUILD_DIR}/Makefile 98 | COMMAND ${Python3_EXECUTABLE} ${sip_SIP_CONFIGURE} ${SIP_BUILD_DIR} ${SIP_FILE} ${sip_LIBRARY_DIR} 99 | \"${INCLUDE_DIRS}\" \"${LIBRARIES}\" \"${LIBRARY_DIRS}\" \"${LDFLAGS_OTHER}\" 100 | DEPENDS ${sip_SIP_CONFIGURE} ${SIP_FILE} ${sip_DEPENDS} 101 | WORKING_DIRECTORY ${sip_SOURCE_DIR} 102 | COMMENT "Running SIP generator for ${PROJECT_NAME} Python bindings..." 103 | ) 104 | 105 | if(NOT EXISTS "${sip_LIBRARY_DIR}") 106 | file(MAKE_DIRECTORY ${sip_LIBRARY_DIR}) 107 | endif() 108 | 109 | if(WIN32) 110 | set(MAKE_EXECUTABLE NMake.exe) 111 | else() 112 | find_program(MAKE_PROGRAM NAMES make) 113 | message(STATUS "Found required make: ${MAKE_PROGRAM}") 114 | set(MAKE_EXECUTABLE ${MAKE_PROGRAM}) 115 | endif() 116 | 117 | add_custom_command( 118 | OUTPUT ${sip_LIBRARY_DIR}/lib${PROJECT_NAME}${CMAKE_SHARED_LIBRARY_SUFFIX} 119 | COMMAND ${MAKE_EXECUTABLE} 120 | DEPENDS ${SIP_BUILD_DIR}/Makefile 121 | WORKING_DIRECTORY ${SIP_BUILD_DIR} 122 | COMMENT "Compiling generated code for ${PROJECT_NAME} Python bindings..." 123 | ) 124 | 125 | add_custom_target(lib${PROJECT_NAME} ALL 126 | DEPENDS ${sip_LIBRARY_DIR}/lib${PROJECT_NAME}${CMAKE_SHARED_LIBRARY_SUFFIX} 127 | COMMENT "Meta target for ${PROJECT_NAME} Python bindings..." 128 | ) 129 | add_dependencies(lib${PROJECT_NAME} ${sip_DEPENDENCIES}) 130 | endfunction() 131 | -------------------------------------------------------------------------------- /package.xml: -------------------------------------------------------------------------------- 1 | 2 | python_qt_binding 3 | 2.4.0 4 | 5 | This stack provides Python bindings for Qt. 6 | There are two providers: pyside and pyqt. PySide2 is available under 7 | the GPL, LGPL and a commercial license. PyQt is released under the GPL. 8 | 9 | Both the bindings and tools to build bindings are included from each 10 | available provider. For PySide, it is called "Shiboken". For PyQt, 11 | this is called "SIP". 12 | 13 | Also provided is adapter code to make the user's Python code 14 | independent of which binding provider was actually used which makes 15 | it very easy to switch between these. 16 | 17 | 18 | Shane Loretz 19 | 20 | BSD 21 | 22 | http://ros.org/wiki/python_qt_binding 23 | 24 | Dave Hershberger 25 | Dirk Thomas 26 | Dorian Scholz 27 | Stephen Brawner 28 | 29 | ament_cmake 30 | 31 | qtbase5-dev 32 | python3-qt5-bindings 33 | 34 | python3-qt5-bindings 35 | 36 | ament_cmake_pytest 37 | ament_lint_auto 38 | ament_lint_common 39 | 40 | 41 | ament_cmake 42 | 43 | 44 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | junit_family=xunit2 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | try: 4 | from setuptools import setup 5 | except ImportError: 6 | from distutils.core import setup 7 | 8 | try: 9 | from catkin_pkg.python_setup import generate_distutils_setup 10 | d = generate_distutils_setup() 11 | except ImportError: 12 | # extract information from package.xml manually when catkin_pkg is unavailable 13 | from xml.etree import ElementTree 14 | tree = ElementTree.parse('package.xml') 15 | root = tree.getroot() 16 | d = { 17 | 'name': root.find('./name').text, 18 | 'version': root.find('./version').text, 19 | 'maintainer': root.findall('./maintainer')[0].text, 20 | 'maintainer_email': root.findall('./maintainer')[0].attrib['email'], 21 | 'license': ', '.join([x.text for x in root.findall('./license')]), 22 | 'url': root.findall('./url')[0].text, 23 | 'author': ', '.join([x.text for x in root.findall('./author')]), 24 | } 25 | description = root.find('./description').text.strip() 26 | if len(description) <= 200: 27 | d['description'] = description 28 | else: 29 | d['description'] = description[:197] + '...' 30 | d['long_description'] = description 31 | 32 | d.update({ 33 | 'packages': [d['name']], 34 | 'package_dir': {'': 'src'}, 35 | 'classifiers': [ 36 | 'Development Status :: 5 - Production/Stable', 37 | 'Intended Audience :: Developers', 38 | 'Operating System :: OS Independent', 39 | 'Programming Language :: Python', 40 | 'Topic :: Software Development :: Libraries :: Python Modules', 41 | 'License :: OSI Approved :: BSD License', 42 | 'License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)', 43 | 'License :: OSI Approved :: GNU General Public License (GPL)', 44 | ], 45 | }) 46 | 47 | setup(**d) 48 | -------------------------------------------------------------------------------- /src/python_qt_binding/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (c) 2011, Dirk Thomas, Dorian Scholz, TU Darmstadt 4 | # All rights reserved. 5 | # 6 | # Redistribution and use in source and binary forms, with or without 7 | # modification, are permitted provided that the following conditions 8 | # are met: 9 | # 10 | # * Redistributions of source code must retain the above copyright 11 | # notice, this list of conditions and the following disclaimer. 12 | # * Redistributions in binary form must reproduce the above 13 | # copyright notice, this list of conditions and the following 14 | # disclaimer in the documentation and/or other materials provided 15 | # with the distribution. 16 | # * Neither the name of the TU Darmstadt nor the names of its 17 | # contributors may be used to endorse or promote products derived 18 | # from this software without specific prior written permission. 19 | # 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 23 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 24 | # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 25 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 26 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 29 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 30 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 31 | # POSSIBILITY OF SUCH DAMAGE. 32 | 33 | """ 34 | Abstraction for different Python Qt bindings. 35 | 36 | Supported Python Qt 5 bindings are PyQt and PySide. 37 | The Qt modules can be imported like this: 38 | 39 | from python_qt_binding.QtCore import QObject 40 | from python_qt_binding import QtGui, loadUi 41 | 42 | The name of the selected binding is available in QT_BINDING. 43 | The version of the selected binding is available in QT_BINDING_VERSION. 44 | All available Qt modules are listed in QT_BINDING_MODULES. 45 | 46 | The default binding order ('pyqt', 'pyside') can be overridden with a 47 | SELECT_QT_BINDING_ORDER attribute on sys: 48 | setattr(sys, 'SELECT_QT_BINDING_ORDER', [FIRST_NAME, NEXT_NAME, ..]) 49 | 50 | A specific binding can be selected with a SELECT_QT_BINDING attribute on sys: 51 | setattr(sys, 'SELECT_QT_BINDING', MY_BINDING_NAME) 52 | """ 53 | 54 | import sys 55 | 56 | from python_qt_binding.binding_helper import loadUi # noqa: F401 57 | from python_qt_binding.binding_helper import QT_BINDING # noqa: F401 58 | from python_qt_binding.binding_helper import QT_BINDING_MODULES 59 | from python_qt_binding.binding_helper import QT_BINDING_VERSION # noqa: F401 60 | 61 | # register binding modules as sub modules of this package (python_qt_binding) for easy importing 62 | for module_name, module in QT_BINDING_MODULES.items(): 63 | sys.modules[__name__ + '.' + module_name] = module 64 | setattr(sys.modules[__name__], module_name, module) 65 | del module_name 66 | del module 67 | 68 | del sys 69 | -------------------------------------------------------------------------------- /src/python_qt_binding/binding_helper.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (c) 2011, Dirk Thomas, Dorian Scholz, TU Darmstadt 4 | # All rights reserved. 5 | # 6 | # Redistribution and use in source and binary forms, with or without 7 | # modification, are permitted provided that the following conditions 8 | # are met: 9 | # 10 | # * Redistributions of source code must retain the above copyright 11 | # notice, this list of conditions and the following disclaimer. 12 | # * Redistributions in binary form must reproduce the above 13 | # copyright notice, this list of conditions and the following 14 | # disclaimer in the documentation and/or other materials provided 15 | # with the distribution. 16 | # * Neither the name of the TU Darmstadt nor the names of its 17 | # contributors may be used to endorse or promote products derived 18 | # from this software without specific prior written permission. 19 | # 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 23 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 24 | # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 25 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 26 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 29 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 30 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 31 | # POSSIBILITY OF SUCH DAMAGE. 32 | 33 | try: 34 | import __builtin__ as builtins 35 | except ImportError: 36 | # since the 'future' package provides a 'builtins' module in Python 2 37 | # this must not be checked second 38 | import builtins 39 | import os 40 | import platform 41 | import sys 42 | import traceback 43 | 44 | 45 | QT_BINDING = None 46 | QT_BINDING_MODULES = {} 47 | QT_BINDING_VERSION = None 48 | 49 | 50 | def _select_qt_binding(binding_name=None, binding_order=None): 51 | global QT_BINDING, QT_BINDING_VERSION 52 | 53 | # order of default bindings can be changed here 54 | if platform.system() == 'Darwin': 55 | DEFAULT_BINDING_ORDER = ['pyside'] 56 | else: 57 | DEFAULT_BINDING_ORDER = ['pyqt', 'pyside'] 58 | 59 | binding_order = binding_order or DEFAULT_BINDING_ORDER 60 | 61 | # determine binding preference 62 | if binding_name: 63 | if binding_name not in binding_order: 64 | raise ImportError("Qt binding '%s' is unknown" % binding_name) 65 | binding_order = [binding_name] 66 | 67 | required_modules = [ 68 | 'QtCore', 69 | 'QtGui', 70 | 'QtWidgets', 71 | ] 72 | optional_modules = [ 73 | 'QtBluetooth', 74 | 'QtDBus', 75 | 'QtDesigner', 76 | 'QtHelp', 77 | 'QtLocation', 78 | 'QtMultimedia', 79 | 'QtMultimediaWidgets', 80 | 'QtNetwork', 81 | 'QNetworkAuth', 82 | 'QtNfc', 83 | 'QtOpenGL', 84 | 'QtPositioning', 85 | 'QtPrintSupport', 86 | 'QtQml', 87 | 'QtQuick', 88 | 'QtQuickWidgets', 89 | 'QtScript', 90 | 'QtScriptTools', 91 | 'QtSensors', 92 | 'QtSerialPort', 93 | 'QtSql', 94 | 'QtSvg', 95 | 'QtTest', 96 | 'QtWebChannel', 97 | 'QtWebEngine', # Qt 5.6 and higher 98 | 'QtWebEngineCore', 99 | 'QtWebEngineWidgets', 100 | 'QtWebKitWidgets', # Qt 5.0 - 5.5 101 | 'QtWebSockets', 102 | 'QtX11Extras', 103 | 'QtXml', 104 | 'QtXmlPatterns', 105 | ] 106 | 107 | # try to load preferred bindings 108 | error_msgs = [] 109 | for binding_name in binding_order: 110 | try: 111 | binding_loader = getattr(sys.modules[__name__], '_load_%s' % binding_name, None) 112 | if binding_loader: 113 | QT_BINDING_VERSION = binding_loader(required_modules, optional_modules) 114 | QT_BINDING = binding_name 115 | break 116 | else: 117 | error_msgs.append(" Binding loader '_load_%s' not found." % binding_name) 118 | except ImportError as e: 119 | error_msgs.append(" ImportError for '%s': %s\n%s" % 120 | (binding_name, e, traceback.format_exc())) 121 | 122 | if not QT_BINDING: 123 | raise ImportError( 124 | 'Could not find Qt binding (looked for: %s):\n%s' % 125 | (', '.join(["'%s'" % b for b in binding_order]), '\n'.join(error_msgs))) 126 | 127 | 128 | def _register_binding_module(module_name, module): 129 | # register module using only its own name (TODO: legacy compatibility, remove when possible) 130 | sys.modules[module_name] = module 131 | # add module to the binding modules 132 | QT_BINDING_MODULES[module_name] = module 133 | 134 | 135 | def _named_import(name): 136 | parts = name.split('.') 137 | assert len(parts) >= 2 138 | module = builtins.__import__(name) 139 | for m in parts[1:]: 140 | module = module.__dict__[m] 141 | module_name = parts[-1] 142 | _register_binding_module(module_name, module) 143 | 144 | 145 | def _named_optional_import(name): 146 | try: 147 | _named_import(name) 148 | except ImportError: 149 | pass 150 | 151 | 152 | def _load_pyqt(required_modules, optional_modules): 153 | # set environment variable QT_API for matplotlib 154 | os.environ['QT_API'] = 'pyqt' 155 | 156 | # register required and optional PyQt modules 157 | for module_name in required_modules: 158 | _named_import('PyQt5.%s' % module_name) 159 | for module_name in optional_modules: 160 | _named_optional_import('PyQt5.%s' % module_name) 161 | 162 | # set some names for compatibility with PySide 163 | sys.modules['QtCore'].Signal = sys.modules['QtCore'].pyqtSignal 164 | sys.modules['QtCore'].Slot = sys.modules['QtCore'].pyqtSlot 165 | sys.modules['QtCore'].Property = sys.modules['QtCore'].pyqtProperty 166 | 167 | # try to register Qwt module 168 | try: 169 | import PyQt5.Qwt5 170 | _register_binding_module('Qwt', PyQt5.Qwt5) 171 | except ImportError: 172 | pass 173 | 174 | global _loadUi 175 | 176 | def _loadUi(uifile, baseinstance=None, custom_widgets_=None): 177 | from PyQt5 import uic 178 | return uic.loadUi(uifile, baseinstance=baseinstance) 179 | 180 | import PyQt5.QtCore 181 | return PyQt5.QtCore.PYQT_VERSION_STR 182 | 183 | 184 | def _load_pyside(required_modules, optional_modules): 185 | # set environment variable QT_API for matplotlib 186 | os.environ['QT_API'] = 'pyside' 187 | 188 | # register required and optional PySide modules 189 | for module_name in required_modules: 190 | _named_import('PySide2.%s' % module_name) 191 | for module_name in optional_modules: 192 | _named_optional_import('PySide2.%s' % module_name) 193 | 194 | # set some names for compatibility with PyQt 195 | sys.modules['QtCore'].pyqtSignal = sys.modules['QtCore'].Signal 196 | sys.modules['QtCore'].pyqtSlot = sys.modules['QtCore'].Slot 197 | sys.modules['QtCore'].pyqtProperty = sys.modules['QtCore'].Property 198 | 199 | # try to register PySideQwt module 200 | try: 201 | import PySideQwt 202 | _register_binding_module('Qwt', PySideQwt) 203 | except ImportError: 204 | pass 205 | 206 | global _loadUi 207 | 208 | def _loadUi(uifile, baseinstance=None, custom_widgets=None): 209 | from PySide2.QtUiTools import QUiLoader 210 | from PySide2.QtCore import QMetaObject 211 | 212 | class CustomUiLoader(QUiLoader): 213 | class_aliases = { 214 | 'Line': 'QFrame', 215 | } 216 | 217 | def __init__(self, baseinstance=None, custom_widgets=None): 218 | super(CustomUiLoader, self).__init__(baseinstance) 219 | self._base_instance = baseinstance 220 | self._custom_widgets = custom_widgets or {} 221 | 222 | def createWidget(self, class_name, parent=None, name=''): 223 | # don't create the top-level widget, if a base instance is set 224 | if self._base_instance and not parent: 225 | return self._base_instance 226 | 227 | if class_name in self._custom_widgets: 228 | widget = self._custom_widgets[class_name](parent) 229 | else: 230 | widget = QUiLoader.createWidget(self, class_name, parent, name) 231 | 232 | if str(type(widget)).find(self.class_aliases.get(class_name, class_name)) < 0: 233 | sys.modules['QtCore'].qDebug( 234 | 'PySide.loadUi(): could not find widget class "%s", defaulting to "%s"' % 235 | (class_name, type(widget))) 236 | 237 | if self._base_instance: 238 | setattr(self._base_instance, name, widget) 239 | 240 | return widget 241 | 242 | loader = CustomUiLoader(baseinstance, custom_widgets) 243 | 244 | # instead of passing the custom widgets, they should be registered using 245 | # QUiLoader.registerCustomWidget(), 246 | # but this does not work in PySide 1.0.6: it simply segfaults... 247 | # loader = CustomUiLoader(baseinstance) 248 | # custom_widgets = custom_widgets or {} 249 | # for custom_widget in custom_widgets.values(): 250 | # loader.registerCustomWidget(custom_widget) 251 | 252 | ui = loader.load(uifile) 253 | QMetaObject.connectSlotsByName(ui) 254 | return ui 255 | 256 | import PySide2 257 | return PySide2.__version__ 258 | 259 | 260 | def loadUi(uifile, baseinstance=None, custom_widgets=None): 261 | """ 262 | Load a provided UI file chosen Python Qt 5 binding. 263 | 264 | @type uifile: str 265 | @param uifile: Absolute path of .ui file 266 | @type baseinstance: QWidget 267 | @param baseinstance: the optional instance of the Qt base class. 268 | If specified then the user interface is created in 269 | it. Otherwise a new instance of the base class is 270 | automatically created. 271 | @type custom_widgets: dict of {str:QWidget} 272 | @param custom_widgets: Class name and type of the custom classes used 273 | in uifile if any. This can be None if no custom 274 | class is in use. (Note: this is only necessary 275 | for PySide, see 276 | http://answers.ros.org/question/56382/what-does-python_qt_bindingloaduis-3rd-arg-do-in-pyqt-binding/ 277 | for more information) 278 | """ 279 | return _loadUi(uifile, baseinstance, custom_widgets) 280 | 281 | 282 | _select_qt_binding( 283 | getattr(sys, 'SELECT_QT_BINDING', None), 284 | getattr(sys, 'SELECT_QT_BINDING_ORDER', None), 285 | ) 286 | -------------------------------------------------------------------------------- /test/test_imports.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018, PickNik Consulting 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import importlib.machinery 16 | import sys 17 | 18 | import pytest 19 | 20 | 21 | # If this is running on a Python Windows interpreter built in debug mode, skip running tests 22 | # because we do not have the debug libraries available for PyQt. It is surprisingly tricky to 23 | # discover whether the current interpreter was built in debug mode (note that this is different 24 | # than running the interpreter in debug mode, i.e. PYTHONDEBUG=1). The only non-deprecated way 25 | # we've found is to look for _d.pyd in the extension suffixes, so that is what we do here. 26 | is_windows_debug = sys.platform == 'win32' and '_d.pyd' in importlib.machinery.EXTENSION_SUFFIXES 27 | 28 | 29 | @pytest.mark.skipif(is_windows_debug, reason='Skipping test on Windows Debug') 30 | def test_import_qtcore(): 31 | from python_qt_binding import QtCore 32 | assert QtCore is not None 33 | 34 | 35 | @pytest.mark.skipif(is_windows_debug, reason='Skipping test on Windows Debug') 36 | def test_import_qtgui(): 37 | from python_qt_binding import QtGui 38 | assert QtGui is not None 39 | 40 | 41 | @pytest.mark.skipif(is_windows_debug, reason='Skipping test on Windows Debug') 42 | def test_import_qtwidgets(): 43 | from python_qt_binding import QtWidgets 44 | assert QtWidgets is not None 45 | 46 | 47 | @pytest.mark.skipif(is_windows_debug, reason='Skipping test on Windows Debug') 48 | def test_import_qtobject(): 49 | from python_qt_binding.QtCore import QObject 50 | assert QObject is not None 51 | --------------------------------------------------------------------------------