Skip to content
Open
133 changes: 33 additions & 100 deletions CMake/ITKSetPython3Vars.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -61,22 +61,48 @@ if(NOT PYTHON_DEVELOPMENT_REQUIRED)
)
set(ITK_WRAP_PYTHON_VERSION "ITK_WRAP_PYTHON=OFF")
else()
# Wrapping requires this version and every module is abi3, so the wrapping
# floor and the Limited API floor are the same number. Cached so
# itk_end_wrap_module.cmake can pin USE_SABI to it.
set(
_ITK_MINIMUM_SUPPORTED_LIMITED_API_VERSION
${ITK_WRAP_PYTHON_MINIMUM_VERSION}
CACHE INTERNAL
"Minimum Python minor version for the abi3 / Limited API floor"
)

if(CMAKE_VERSION VERSION_LESS "3.26")
message(
FATAL_ERROR
"CMake version ${CMAKE_VERSION} is too old for Python wrapping: "
"the FindPython Development.SABIModule component requires CMake >= 3.26. "
"Either upgrade CMake or set ITK_WRAP_PYTHON=OFF."
)
endif()

set(
_python_find_components
Interpreter
Development.SABIModule
)
if(BUILD_TESTING)
# NumPy Required for testing ITK PythonTests, prefer to fail early if not installed
list(APPEND _python_find_components NumPy)
endif()
set(_missing_required_component FALSE)
set(_missing_python_components "")
# set(Python3_FIND_REGISTRY LAST) # default is FIRST. Do we need/want this?
find_package(
Python3
${PYTHON_VERSION_MIN}...${PYTHON_VERSION_MAX}
COMPONENTS
Interpreter
# NOTE: dockcross build environments do not supply
# `Development` python-dev resources
Development.Module
Development.SABIModule
NumPy
${_python_find_components}
)
set(ITK_WRAP_PYTHON_VERSION "${Python3_VERSION}")

# An empty Python3_VERSION means the range found nothing; otherwise the second
# find_package below runs unversioned and silently accepts a below-floor Python.
# An empty Python3_VERSION means the range found nothing.
if(
NOT
Python3_VERSION
Expand All @@ -93,89 +119,6 @@ else()
"Provide a Python >= ${ITK_WRAP_PYTHON_MINIMUM_VERSION} interpreter, or set ITK_WRAP_PYTHON=OFF."
)
endif()

# start section to define package components based on LIMITED_API support and choices
# Cached so the abi3 floor is a single source of truth shared with the module
# wrapping macros (itk_end_wrap_module.cmake USE_SABI).
set(
_ITK_MINIMUM_SUPPORTED_LIMITED_API_VERSION
3.11
CACHE INTERNAL
"Minimum Python minor version for the abi3 / Limited API floor"
)

# Force ITK_WRAP_PYTHON_VERSION if SKBUILD_SABI_COMPONENT requests it
string(
FIND
"${SKBUILD_SABI_COMPONENT}"
"SABIModule"
_SKBUILD_SABI_COMPONENT_REQUIRED
)
if(NOT DEFINED ITK_USE_PYTHON_LIMITED_API)
if(
(
_SKBUILD_SABI_COMPONENT_REQUIRED
GREATER
-1
)
OR
ITK_WRAP_PYTHON_VERSION
VERSION_GREATER_EQUAL
${_ITK_MINIMUM_SUPPORTED_LIMITED_API_VERSION}
)
set(
ITK_USE_PYTHON_LIMITED_API
1
CACHE BOOL
"Configure Python's limited API for Python minor version compatibility."
)
else()
set(
ITK_USE_PYTHON_LIMITED_API
0
CACHE BOOL
"Configure Python's limited API for Python minor version compatibility."
)
endif()
mark_as_advanced(ITK_USE_PYTHON_LIMITED_API)
endif()
unset(_SKBUILD_SABI_COMPONENT_REQUIRED)
if(ITK_USE_PYTHON_LIMITED_API)
if(CMAKE_VERSION VERSION_LESS "3.26")
message(
FATAL_ERROR
"CMake version ${CMAKE_VERSION} is too old for Python limited API wrapping: "
"the FindPython Development.SABIModule component requires CMake >= 3.26. "
"Either upgrade CMake or disable ITK_USE_PYTHON_LIMITED_API."
)
endif()

set(
_python_find_components
Interpreter
Development.Module
Development.SABIModule
)
else()
set(
_python_find_components
Interpreter
Development.Module
)
endif()
if(BUILD_TESTING)
# NumPy Required for testing ITK PythonTests, prefer to fail early if not installed
list(APPEND _python_find_components NumPy)
endif()
set(_missing_required_component FALSE)
set(_missing_python_components "")
find_package(
Python3
${ITK_WRAP_PYTHON_VERSION}
COMPONENTS
${_python_find_components}
)
set(ITK_WRAP_PYTHON_VERSION "${Python3_VERSION}")
message(STATUS "Python3_FOUND=${Python3_FOUND}")
foreach(_required_component ${_python_find_components})
if(NOT Python3_${_required_component}_FOUND)
Expand Down Expand Up @@ -203,15 +146,7 @@ else()
NumPy: ${Python3_EXECUTABLE} -m pip install numpy"
)
endif()
if(
Development.Module
IN_LIST
_missing_python_components
OR
Development.SABIModule
IN_LIST
_missing_python_components
)
if(Development.SABIModule IN_LIST _missing_python_components)
string(
APPEND
_missing_component_remedy
Expand All @@ -236,7 +171,6 @@ else()
Python3_Interpreter_FOUND=${Python3_Interpreter_FOUND}
Python3_Compiler_FOUND=${Python3_Compiler_FOUND}
Python3_Development_FOUND=${Python3_Development_FOUND}
Python3_Development.Module_FOUND=${Python3_Development.Module_FOUND}
Python3_Development.SABIModule_FOUND=${Python3_Development.SABIModule_FOUND}
Python3_Development.Embed_FOUND=${Python3_Development.Embed_FOUND}
Python3_NumPy_FOUND=${Python3_NumPy_FOUND}
Expand All @@ -253,7 +187,6 @@ else()
unset(_python_find_components)
# _ITK_MINIMUM_SUPPORTED_LIMITED_API_VERSION is cached (INTERNAL) and intentionally
# left set so itk_end_wrap_module.cmake can pin USE_SABI to the same floor.
# end section to define package components based on LIMITED_API support and choices
if(DEFINED _specified_Python3_EXECUTABLE)
set(
Python3_EXECUTABLE
Expand Down
6 changes: 5 additions & 1 deletion Documentation/docs/contributing/python_packaging.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,15 @@ This section describes how to build Python packages for the main ITK libraries u
## Prerequisites

Building ITK Python wheels requires the following:
- CMake >= 3.16
- CMake >= 3.26, for the `FindPython` `Development.SABIModule` component
- Git
- C++ Compiler (see [scikit-build platform specific requirements](https://scikit-build.readthedocs.io/en/latest/generators.html))
- Python >= 3.11

Wrapped modules are always built as stable-ABI (`abi3`) extensions, so a single
set of wheels serves every supported Python version. Builds that do not set
`ITK_WRAP_PYTHON=ON` are unaffected and still accept older CMake.

## Automated Platform Scripts

The following sections outline how to use the ITKPythonPackage project to build wheels on Linux, macOS, and Windows. Each script will fetch tagged ITK sources, build ITK with Python wrappings, and package binaries into the Python wheel archive format for distribution.
Expand Down
1 change: 0 additions & 1 deletion Modules/Bridge/VtkGlue/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,6 @@ set(_required_vtk_libraries
)
if(ITK_WRAP_PYTHON)
list(APPEND _required_vtk_libraries
VTK::WrappingPythonCore
VTK::CommonCore
VTK::CommonDataModel
VTK::CommonExecutionModel)
Expand Down
1 change: 0 additions & 1 deletion Modules/Bridge/VtkGlue/itk-module-init.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ if(ITK_WRAP_PYTHON)
list(
APPEND
_required_vtk_libraries
VTK::WrappingPythonCore
VTK::CommonCore
VTK::CommonDataModel
VTK::CommonExecutionModel
Expand Down
16 changes: 2 additions & 14 deletions Modules/Bridge/VtkGlue/wrapping/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,15 +1,3 @@
itk_wrap_module(ITKVtkGlue)
if(ITK_USE_PYTHON_LIMITED_API)
message(
FATAL_ERROR
"The ITKVtkGlue module can only built without Python limited API due to VTK limitations."
"Please set `ITK_USE_PYTHON_LIMITED_API` to `FALSE`."
)
else()
list(
APPEND
WRAPPER_SWIG_LIBRARY_FILES
"${CMAKE_CURRENT_SOURCE_DIR}/VtkGlue.i"
)
itk_auto_load_and_end_wrap_submodules()
endif()
list(APPEND WRAPPER_SWIG_LIBRARY_FILES "${CMAKE_CURRENT_SOURCE_DIR}/VtkGlue.i")
itk_auto_load_and_end_wrap_submodules()
137 changes: 117 additions & 20 deletions Modules/Bridge/VtkGlue/wrapping/VtkGlue.i
Original file line number Diff line number Diff line change
Expand Up @@ -51,44 +51,141 @@
%module(package="itk",threads="1") VtkGluePython

%{
#include "vtkPythonUtil.h"
#include "vtkVersion.h"
#if (VTK_MAJOR_VERSION > 5 ||((VTK_MAJOR_VERSION == 5)&&(VTK_MINOR_VERSION > 6)))
Comment thread
hjmjohnson marked this conversation as resolved.
#define vtkPythonGetObjectFromPointer vtkPythonUtil::GetObjectFromPointer
#define vtkPythonGetPointerFromObject vtkPythonUtil::GetPointerFromObject
#endif
#include <cinttypes>
#include <cstdio>
#include <cstring>

// Pointer exchange with VTK's Python layer using only the Limited API, so this
// module stays abi3 and needs no link against VTK::WrappingPythonCore.
namespace itkVtkGlueABI3
{

inline PyObject *
ImportClass(const char * moduleName, const char * className)
{
PyObject * mod = PyImport_ImportModule(moduleName);
if (!mod)
{
return nullptr;
}
PyObject * cls = PyObject_GetAttrString(mod, className);
Py_DECREF(mod);
return cls;
}

// Parses the `_<hex>_p_<ClassName>` encoding VTK publishes as `__this__`. The
// isinstance() gate is what makes trusting that string safe: without it any
// object exposing a forged `__this__` would be cast to a native pointer.
inline void *
GetPointerFromObject(PyObject * obj, const char * moduleName, const char * className)
{
PyObject * cls = ImportClass(moduleName, className);
if (!cls)
{
return nullptr;
}
const int isInstance = PyObject_IsInstance(obj, cls);
Py_DECREF(cls);
if (isInstance < 0)
{
return nullptr;
}
if (isInstance == 0)
{
PyErr_Format(PyExc_TypeError, "expected a VTK %s instance", className);
return nullptr;
}

PyObject * thisStr = PyObject_GetAttrString(obj, "__this__");
if (!thisStr)
{
PyErr_Clear();
PyErr_Format(PyExc_TypeError, "expected a VTK %s instance", className);
return nullptr;
}

void * ptr = nullptr;
Py_ssize_t len = 0;
const char * s = PyUnicode_AsUTF8AndSize(thisStr, &len);
if (s && len > 4 && s[0] == '_' && std::strlen(s) == static_cast<size_t>(len))
{
const char * sep = std::strstr(s + 1, "_p_");
if (sep && std::strcmp(sep + 3, className) == 0)
{
std::uintptr_t addr = 0;
// '_' is not a hex digit, so the conversion stops at the separator.
if (std::sscanf(s + 1, "%" SCNxPTR, &addr) == 1 && addr != 0)
{
ptr = reinterpret_cast<void *>(addr);
}
Comment thread
hjmjohnson marked this conversation as resolved.
}
}
Py_DECREF(thisStr);

if (!ptr)
{
PyErr_Format(PyExc_TypeError, "expected a VTK %s instance", className);
}
return ptr;
}

// Reconstructs through VTK's own `Addr=0x...` path so the IsA() check, the
// object map, and reference counting all stay VTK's responsibility.
// `__new__` is called explicitly rather than `cls(addr)`: vtkmodules.util.data_model
// registers keyword-only `override` subclasses for the data-model classes, whose
// __init__ would reject the positional address string.
inline PyObject *
GetObjectFromPointer(void * ptr, const char * moduleName, const char * className)
{
if (!ptr)
{
Py_RETURN_NONE;
}

PyObject * cls = ImportClass(moduleName, className);
if (!cls)
{
return nullptr;
}

char addr[64];
std::snprintf(addr, sizeof(addr), "Addr=0x%" PRIxPTR, reinterpret_cast<std::uintptr_t>(ptr));
PyObject * obj = PyObject_CallMethod(cls, "__new__", "Os", cls, addr);
Py_DECREF(cls);
return obj;
}

} // namespace itkVtkGlueABI3
%}

%typemap(out) vtkImageExport* {
PyImport_ImportModule("vtk");
$result = vtkPythonGetObjectFromPointer ( (vtkImageExport*)$1 );
$result = itkVtkGlueABI3::GetObjectFromPointer($1, "vtkmodules.vtkIOImage", "vtkImageExport");
if (!$result) { SWIG_fail; }
}

%typemap(out) vtkImageImport* {
PyImport_ImportModule("vtk");
$result = vtkPythonGetObjectFromPointer ( (vtkImageImport*)$1 );
$result = itkVtkGlueABI3::GetObjectFromPointer($1, "vtkmodules.vtkIOImage", "vtkImageImport");
if (!$result) { SWIG_fail; }
}

%typemap(out) vtkImageData* {
PyImport_ImportModule("vtk");
$result = vtkPythonGetObjectFromPointer ( (vtkImageData*)$1 );
$result = itkVtkGlueABI3::GetObjectFromPointer($1, "vtkmodules.vtkCommonDataModel", "vtkImageData");
if (!$result) { SWIG_fail; }
}

%typemap(in) vtkImageData* {
$1 = NULL;
$1 = (vtkImageData*) vtkPythonGetPointerFromObject ( $input, "vtkImageData" );
if ( $1 == NULL ) { SWIG_fail; }
$1 = static_cast<vtkImageData *>(itkVtkGlueABI3::GetPointerFromObject($input, "vtkmodules.vtkCommonDataModel", "vtkImageData"));
if (!$1) { SWIG_fail; }
}

%typemap(out) vtkPolyData* {
PyImport_ImportModule("vtk");
$result = vtkPythonGetObjectFromPointer ( (vtkPolyData*)$1 );
$result = itkVtkGlueABI3::GetObjectFromPointer($1, "vtkmodules.vtkCommonDataModel", "vtkPolyData");
if (!$result) { SWIG_fail; }
}

%typemap(in) vtkPolyData* {
$1 = NULL;
$1 = (vtkPolyData*) vtkPythonGetPointerFromObject ( $input, "vtkPolyData" );
if ( $1 == NULL ) { SWIG_fail; }
$1 = static_cast<vtkPolyData *>(itkVtkGlueABI3::GetPointerFromObject($input, "vtkmodules.vtkCommonDataModel", "vtkPolyData"));
if (!$1) { SWIG_fail; }
}
#endif

Expand Down
Loading
Loading