Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 3 additions & 9 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,9 @@ find_package(ament_cmake_python REQUIRED)
# Copy python package into binary directory
file(COPY src/${PROJECT_NAME} DESTINATION ${CMAKE_CURRENT_BINARY_DIR})

# Avoid find_package(QT NAMES Qt6 Qt5 ...) due to CMake's default ascending path resolution
find_package(Qt6 QUIET COMPONENTS Widgets Core)
if(Qt6_FOUND)
set(QT_VERSION_MAJOR 6)
else()
find_package(Qt5 REQUIRED COMPONENTS Widgets Core)
set(QT_VERSION_MAJOR 5)
endif()
set(python_qt_binding_QT_MAJOR_VERSION "${QT_VERSION_MAJOR}" CACHE STRING "The major version of Qt to use (5 or 6)")
find_package(Qt6 REQUIRED COMPONENTS Widgets Core)
# Kept for downstream packages that still read this variable.
set(python_qt_binding_QT_MAJOR_VERSION "6" CACHE STRING "The major version of Qt to use")

# Generate a version.py file inside the copied package directory
file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}/version.py
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

This stack provides Python bindings for Qt.
There are two providers: pyside and pyqt.
PySide2 is available under the GPL, LGPL and a commercial license.
PySide6 is available under the GPL, LGPL and a commercial license.
PyQt is released under the GPL.

Both the bindings and tools to build bindings are included from each available provider.
Expand Down
14 changes: 14 additions & 0 deletions cmake/generate_pyproject_toml.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Called via cmake -P from build_sip_binding() in sip_helper.cmake.
# Performs @VAR@ substitution on pyproject.toml.in and writes the result.
#
# Required variables (passed via -DVAR=value on the cmake command line):
# PROJECT_NAME
# python_qt_binding_QMAKE_EXECUTABLE
# SIP_BUILD_DIR
# sip_SOURCE_DIR
# SIP_FILE_NAME
# __PYQT_BINDINGS_DIR
# __QT_SIP_ABI_VERSION
# TEMPLATE_FILE
# OUTPUT_FILE
configure_file("${TEMPLATE_FILE}" "${OUTPUT_FILE}" @ONLY)
43 changes: 18 additions & 25 deletions cmake/shiboken_helper.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -21,37 +21,27 @@ if(__PYTHON_QT_BINDING_SHIBOKEN_HELPER_INCLUDED)
endif()
set(__PYTHON_QT_BINDING_SHIBOKEN_HELPER_INCLUDED TRUE)

# In CMake 3.27 and later, FindPythonInterp and FindPythonLibs are deprecated.
# However, Shiboken2 as packaged in Ubuntu 24.04 still use them, so set CMP0148 to
# "OLD" to silence this warning.
if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.27.0")
cmake_policy(SET CMP0148 OLD)
endif()
find_package(Shiboken2 QUIET)
if(Shiboken2_FOUND)
message(STATUS "Found Shiboken2 version ${Shiboken2_VERSION}")
if(NOT ${Shiboken2_VERSION} VERSION_LESS "5.13")
get_property(SHIBOKEN_INCLUDE_DIR TARGET Shiboken2::libshiboken PROPERTY INTERFACE_INCLUDE_DIRECTORIES)
get_property(SHIBOKEN_LIBRARY TARGET Shiboken2::libshiboken PROPERTY LOCATION)
set(SHIBOKEN_BINARY Shiboken2::shiboken2)
endif()
find_package(Shiboken6 QUIET)
if(Shiboken6_FOUND)
message(STATUS "Found Shiboken6 version ${Shiboken6_VERSION}")
get_property(SHIBOKEN_INCLUDE_DIR TARGET Shiboken6::libshiboken PROPERTY INTERFACE_INCLUDE_DIRECTORIES)
get_property(SHIBOKEN_LIBRARY TARGET Shiboken6::libshiboken PROPERTY LOCATION)
set(SHIBOKEN_BINARY Shiboken6::shiboken6)
message(STATUS "Using SHIBOKEN_INCLUDE_DIR: ${SHIBOKEN_INCLUDE_DIR}")
message(STATUS "Using SHIBOKEN_LIBRARY: ${SHIBOKEN_LIBRARY}")
message(STATUS "Using SHIBOKEN_BINARY: ${SHIBOKEN_BINARY}")
endif()

find_package(PySide2 QUIET)
if(PySide2_FOUND)
message(STATUS "Found PySide2 version ${PySide2_VERSION}")
if(NOT ${PySide2_VERSION} VERSION_LESS "5.13")
get_property(PYSIDE_INCLUDE_DIR TARGET PySide2::pyside2 PROPERTY INTERFACE_INCLUDE_DIRECTORIES)
get_property(PYSIDE_LIBRARY TARGET PySide2::pyside2 PROPERTY LOCATION)
endif()
find_package(PySide6 QUIET)
if(PySide6_FOUND)
message(STATUS "Found PySide6 version ${PySide6_VERSION}")
get_property(PYSIDE_INCLUDE_DIR TARGET PySide6::pyside6 PROPERTY INTERFACE_INCLUDE_DIRECTORIES)
get_property(PYSIDE_LIBRARY TARGET PySide6::pyside6 PROPERTY LOCATION)
message(STATUS "Using PYSIDE_INCLUDE_DIR: ${PYSIDE_INCLUDE_DIR}")
message(STATUS "Using PYSIDE_LIBRARY: ${PYSIDE_LIBRARY}")
endif()

if(Shiboken2_FOUND AND PySide2_FOUND)
if(Shiboken6_FOUND AND PySide6_FOUND)
message(STATUS "Shiboken binding generator available.")
set(shiboken_helper_FOUND TRUE)
else()
Expand All @@ -64,7 +54,9 @@ macro(_shiboken_generator_command VAR GLOBAL TYPESYSTEM INCLUDE_PATH BUILD_DIR)
# Add includes from current directory, Qt, PySide and compiler specific dirs
get_directory_property(SHIBOKEN_HELPER_INCLUDE_DIRS INCLUDE_DIRECTORIES)
list(APPEND SHIBOKEN_HELPER_INCLUDE_DIRS
${QT_INCLUDE_DIR}
${Qt6Core_INCLUDE_DIRS}
${Qt6Gui_INCLUDE_DIRS}
${Qt6Widgets_INCLUDE_DIRS}
${PYSIDE_INCLUDE_DIR}
${CMAKE_CXX_IMPLICIT_INCLUDE_DIRECTORIES})
# See ticket https://code.ros.org/trac/ros-pkg/ticket/5219
Expand Down Expand Up @@ -157,8 +149,9 @@ function(shiboken_target_link_libraries PROJECT_NAME QT_COMPONENTS)
)

foreach(component ${QT_COMPONENTS})
string(TOUPPER ${component} component)
set(shiboken_LINK_LIBRARIES ${shiboken_LINK_LIBRARIES} ${QT_${component}_LIBRARY})
# QT_COMPONENTS are given as QtCore/QtGui/QtWidgets; Qt6 targets drop the prefix.
string(REGEX REPLACE "^Qt" "" component "${component}")
set(shiboken_LINK_LIBRARIES ${shiboken_LINK_LIBRARIES} Qt6::${component})
endforeach()

target_link_libraries(${PROJECT_NAME} ${shiboken_LINK_LIBRARIES})
Expand Down
25 changes: 16 additions & 9 deletions cmake/sip_helper.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,13 @@ set(Python3_FIND_UNVERSIONED_NAMES FIRST)

find_package(Python3 ${Python3_VERSION} REQUIRED COMPONENTS Interpreter Development)

# Find the directory containing the SIP bindings shipped by PyQt.
#
# :param python_qt_binding_QT_MAJOR_VERSION: The major version of Qt (e.g., 5 or 6).
# Find the directory containing the SIP bindings shipped by PyQt6.
#
# :out __PYQT_BINDINGS_DIR: Path to the directory containing QT*.sip files.
# :out __PYQT_BINDINGS_FOUND: Boolean indicating if the bindings were located.
#
function(__find_qt_sip_files python_qt_binding_QT_MAJOR_VERSION)
set(MODULE_NAME "PyQt${python_qt_binding_QT_MAJOR_VERSION}")
function(__find_qt_sip_files)
set(MODULE_NAME "PyQt6")

execute_process(
COMMAND ${Python3_EXECUTABLE} -c "import ${MODULE_NAME}.bindings as pb; print(pb.__path__[0])"
Expand Down Expand Up @@ -76,9 +74,9 @@ else()
endif()

# Find Qt's installed SIP files
__find_qt_sip_files(${python_qt_binding_QT_MAJOR_VERSION})
__find_qt_sip_files()
if(NOT __PYQT_BINDINGS_FOUND)
message(FATAL_ERROR "PyQt${python_qt_binding_QT_MAJOR_VERSION} SIP bindings are required but were not found.")
message(FATAL_ERROR "PyQt6 SIP bindings are required but were not found.")
endif()

# Extract the sip-abi-version from PyQt
Expand All @@ -87,8 +85,8 @@ __find_qt_sip_abi("${__PYQT_BINDINGS_DIR}")
# Find qmake
find_program(python_qt_binding_QMAKE_EXECUTABLE
NAMES
qmake${python_qt_binding_QT_MAJOR_VERSION}
qmake-qt${python_qt_binding_QT_MAJOR_VERSION}
qmake6
qmake-qt6
qmake
REQUIRED)

Expand Down Expand Up @@ -162,8 +160,17 @@ function(build_sip_binding PROJECT_NAME SIP_FILE)
COMMENT "Generating C++ code for ${PROJECT_NAME} Python bindings using sip-build..."
)

# Drive the generator from a target of its own. Generating the code only
# needs the sip files and the headers, not the libraries the extension is
# later linked against. Keeping it out of lib${PROJECT_NAME} means it is
# not ordered after those libraries have been built and can run
# concurrently with them.
add_custom_target(${PROJECT_NAME}_codegen DEPENDS ${GENERATED_CPP})
set_source_files_properties(${GENERATED_CPP} PROPERTIES GENERATED TRUE)

# Build the cPython extension natively using CMake
python3_add_library(lib${PROJECT_NAME} MODULE ${GENERATED_CPP})
add_dependencies(lib${PROJECT_NAME} ${PROJECT_NAME}_codegen)

# Link project dependencies against this target
target_include_directories(lib${PROJECT_NAME} PRIVATE ${${PROJECT_NAME}_INCLUDE_DIRS} ${SIP_BUILD_DIR})
Expand Down
11 changes: 9 additions & 2 deletions package.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<version>2.6.1</version>
<description>
This stack provides Python bindings for Qt.
There are two providers: pyside and pyqt. PySide2 is available under
There are two providers: pyside and pyqt. PySide6 is available under
the GPL, LGPL and a commercial license. PyQt is released under the GPL.

Both the bindings and tools to build bindings are included from each
Expand All @@ -28,11 +28,18 @@

<buildtool_depend>ament_cmake</buildtool_depend>

<!-- TODO: python3-qt-bindings still resolves to the Qt5 stack (pyqt5-dev,
libpyside2-dev, qtbase5-dev), but it is the only key that also provides
the SIP toolchain this package builds with (python3-sipbuild,
python3-pyqtbuild, pyqt6-dev). Swap it once rosdep gains keys for
those; see the Qt6-only migration notes. -->
<build_depend>python3-qt-bindings</build_depend>
<build_depend>qt6-base-dev</build_depend>
<build_depend>qt6-qmake</build_depend>
<build_export_depend>python3-qt-bindings</build_export_depend>
<build_export_depend>python3-dev</build_export_depend>

<exec_depend>python3-qt-bindings</exec_depend>
<exec_depend>python3-pyqt6</exec_depend>

<test_depend>ament_cmake_pytest</test_depend>
<test_depend>ament_lint_auto</test_depend>
Expand Down
2 changes: 1 addition & 1 deletion python_qt_binding-extras.cmake.in
Original file line number Diff line number Diff line change
@@ -1 +1 @@
set(python_qt_binding_QT_MAJOR_VERSION "@python_qt_binding_QT_MAJOR_VERSION@" CACHE STRING "The major version of Qt to use (5 or 6)")
set(python_qt_binding_QT_MAJOR_VERSION "@python_qt_binding_QT_MAJOR_VERSION@" CACHE STRING "The major version of Qt to use")
2 changes: 1 addition & 1 deletion src/python_qt_binding/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"""
Abstraction for different Python Qt bindings.

Supported Python Qt 5 bindings are PyQt and PySide.
Supported Python Qt 6 bindings are PyQt6 and PySide6.
The Qt modules can be imported like this:

from python_qt_binding.QtCore import QObject
Expand Down
41 changes: 7 additions & 34 deletions src/python_qt_binding/binding_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,6 @@
import sys
import traceback

from .version import QT_MAJOR_VERSION


QT_BINDING = None
QT_BINDING_MODULES = {}
Expand Down Expand Up @@ -75,30 +73,25 @@ def _select_qt_binding(binding_name=None, binding_order=None):
'QtMultimedia',
'QtMultimediaWidgets',
'QtNetwork',
'QNetworkAuth',
'QtNetworkAuth',
'QtNfc',
'QtOpenGL',
'QtPositioning',
'QtPrintSupport',
'QtQml',
'QtQuick',
'QtQuickWidgets',
'QtScript',
'QtScriptTools',
'QtSensors',
'QtSerialPort',
'QtSql',
'QtSvg',
'QtTest',
'QtWebChannel',
'QtWebEngine', # Qt 5.6 and higher
'QtWebEngine',
'QtWebEngineCore',
'QtWebEngineWidgets',
'QtWebKitWidgets', # Qt 5.0 - 5.5
'QtWebSockets',
'QtX11Extras',
'QtXml',
'QtXmlPatterns',
]

# try to load preferred bindings
Expand Down Expand Up @@ -152,43 +145,30 @@ def _load_pyqt(required_modules, optional_modules):

# register required and optional PyQt modules
for module_name in required_modules:
_named_import(f'PyQt{QT_MAJOR_VERSION}.{module_name}')
_named_import(f'PyQt6.{module_name}')
for module_name in optional_modules:
_named_optional_import(f'PyQt{QT_MAJOR_VERSION}.{module_name}')
_named_optional_import(f'PyQt6.{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:
Qwt = builtins.__import__(f'PyQt{QT_MAJOR_VERSION}.Qwt{QT_MAJOR_VERSION}', fromlist=['*'])
_register_binding_module('Qwt', Qwt)
except ImportError:
pass

global _loadUi

def _loadUi(uifile, baseinstance=None, custom_widgets_=None):
uic = builtins.__import__(f'PyQt{QT_MAJOR_VERSION}.uic', fromlist=['*'])
uic = builtins.__import__('PyQt6.uic', fromlist=['*'])
return uic.loadUi(uifile, baseinstance=baseinstance)

QtCore = builtins.__import__(f'PyQt{QT_MAJOR_VERSION}.QtCore', fromlist=['*'])
QtCore = builtins.__import__('PyQt6.QtCore', fromlist=['*'])
return QtCore.PYQT_VERSION_STR


def _load_pyside(required_modules, optional_modules):
# set environment variable QT_API for matplotlib
os.environ['QT_API'] = 'pyside'

# Determine the PySide module based on the Qt version
if str(QT_MAJOR_VERSION) == '6':
pyside_module = 'PySide6'
elif str(QT_MAJOR_VERSION) == '5':
pyside_module = 'PySide2'
else:
raise RuntimeError('Only Qt5 and Qt6 are supported')
pyside_module = 'PySide6'

# register required and optional PySide modules
for module_name in required_modules:
Expand All @@ -201,13 +181,6 @@ def _load_pyside(required_modules, optional_modules):
sys.modules['QtCore'].pyqtSlot = sys.modules['QtCore'].Slot
sys.modules['QtCore'].pyqtProperty = sys.modules['QtCore'].Property

# try to register PySideQwt module
try:
PySideQwt = builtins.__import__(f'{pyside_module}Qwt', fromlist=['*'])
_register_binding_module('Qwt', PySideQwt)
except ImportError:
pass

global _loadUi

def _loadUi(uifile, baseinstance=None, custom_widgets=None):
Expand Down