Skip to content

Commit 2227f91

Browse files
axxelDoekinstumpylog
committed
python: pybind11 -> nanobind / setuptools -> scikit_build_core
* replace pybind11 with nanobind * automatic pyi generation * replace setuptools with scikit_build_core * enable stable ABI builds for python 12+ (only one wheel for those) * drop win32 wheels * drop Python 3.9 wheels * move cibuildwheel config to pyproject.toml This is mainly based on the work of @Doekin from #948 with additions from @raymondqin and some cherry picked improvemtns from @stumpylog's #1109. Co-authored-by: Doekin <Leetimemp@gmail.com> Co-authored-by: Trenton H <797416+stumpylog@users.noreply.github.com>
1 parent 1681c2e commit 2227f91

12 files changed

Lines changed: 410 additions & 321 deletions

File tree

.github/workflows/ci.yml

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ jobs:
4747
run: |
4848
echo "${GITHUB_WORKSPACE}/build/core/${BUILD_TYPE}" >> $GITHUB_PATH
4949
echo "${GITHUB_WORKSPACE}/build/bin/${BUILD_TYPE}" >> $GITHUB_PATH
50-
cp "${GITHUB_WORKSPACE}/build/core/Release/ZXing.dll" "${GITHUB_WORKSPACE}/build/wrappers/python/Release/"
5150
5251
- name: Test
5352
run: ctest --test-dir build -V -C ${{env.BUILD_TYPE}}
@@ -88,7 +87,6 @@ jobs:
8887
run: |
8988
echo "${GITHUB_WORKSPACE}/build/core/${BUILD_TYPE}" >> $GITHUB_PATH
9089
echo "${GITHUB_WORKSPACE}/build/bin/${BUILD_TYPE}" >> $GITHUB_PATH
91-
cp "${GITHUB_WORKSPACE}/build/core/Release/ZXing.dll" "${GITHUB_WORKSPACE}/build/wrappers/python/Release/"
9290
9391
- name: Test
9492
run: ctest --test-dir build -V -C ${{env.BUILD_TYPE}}
@@ -298,8 +296,8 @@ jobs:
298296

299297
- name: Install dependencies
300298
run: |
301-
python -m pip install --upgrade pip setuptools
302-
python -m pip install numpy pillow
299+
python -m pip install --upgrade pip
300+
python -m pip install numpy pillow build
303301
304302
- name: Build module
305303
working-directory: wrappers/python
@@ -309,10 +307,14 @@ jobs:
309307
working-directory: wrappers/python
310308
run: python -m unittest -v
311309

310+
- name: Build wheel
311+
working-directory: wrappers/python
312+
run: python -m build --wheel
313+
312314
- uses: actions/upload-artifact@v6
313315
with:
314316
name: ${{matrix.os}}-python-artifacts
315-
path: wrappers/python/zxingcpp.*
317+
path: wrappers/python/dist/*
316318

317319
build-rust:
318320
runs-on: ubuntu-latest

.github/workflows/publish-python.yml

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -39,17 +39,6 @@ jobs:
3939

4040
- name: Build wheels
4141
run: python3 -m cibuildwheel --output-dir wheelhouse wrappers/python
42-
env:
43-
# TODO: setup a "BEFORE" cmake build and link the python module to the prebuild libZXing.a
44-
# see https://github.com/YannickJadoul/Parselmouth/blob/523c117aa780184345121f6ff8315670bc7d4d94/.github/workflows/wheels.yml#L120
45-
CIBW_BUILD: cp310-* cp311-* cp312-* cp313-* cp314-*
46-
CIBW_SKIP: "*musllinux*"
47-
#CIBW_ARCHS_MACOS: universal2
48-
#CIBW_ENVIRONMENT_MACOS: CMAKE_OSX_ARCHITECTURES="arm64;x86_64"
49-
#MACOSX_DEPLOYMENT_TARGET: "10.15"
50-
CIBW_BUILD_VERBOSITY: 1
51-
CIBW_TEST_COMMAND: python {package}/test.py
52-
CIBW_TEST_SOURCES: wrappers/python/test.py
5342

5443
- name: Upload wheels
5544
uses: actions/upload-artifact@v6

wrappers/python/CMakeLists.txt

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,17 @@ project(ZXingPython)
44
# check if we are called from the top-level ZXing project
55
get_directory_property(hasParent PARENT_DIRECTORY)
66
if (NOT hasParent)
7+
# TODO: replace with symlinks once https://github.com/scikit-build/scikit-build-core/issues/801 is resolved
8+
message(STATUS "Copying files from parent folder needed for sdist")
9+
foreach(NAME zxing.cmake LICENSE)
10+
if (NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/${NAME}")
11+
execute_process(COMMAND ${CMAKE_COMMAND} -E copy_if_different
12+
"${CMAKE_CURRENT_SOURCE_DIR}/../../${NAME}"
13+
"${CMAKE_CURRENT_SOURCE_DIR}/${NAME}"
14+
)
15+
endif()
16+
endforeach()
17+
718
set(CMAKE_CXX_STANDARD 20) # required here for the module itself
819

920
option (BUILD_SHARED_LIBS "Link python module to shared lib" OFF)
@@ -27,30 +38,56 @@ if (NOT hasParent)
2738
endif()
2839
endif()
2940

30-
find_package(Python 3.10 COMPONENTS Interpreter Development.Module REQUIRED) # see https://github.com/pybind/pybind11/issues/4785
31-
#set(PYBIND11_FINDPYTHON ON) # see https://github.com/pybind/pybind11/issues/4785
32-
zxing_add_package(pybind11 pybind11 https://github.com/pybind/pybind11.git v3.0.1)
41+
# Try to import all Python components potentially needed by nanobind
42+
find_package(Python 3.10
43+
REQUIRED COMPONENTS Interpreter Development.Module
44+
OPTIONAL_COMPONENTS Development.SABIModule)
45+
zxing_add_package(nanobind nanobind https://github.com/wjakob/nanobind.git v2.11.0)
3346

34-
# build the python module
35-
pybind11_add_module(zxingcpp zxing.cpp)
47+
nanobind_add_module(zxingcpp STABLE_ABI zxing.cpp)
3648
target_link_libraries(zxingcpp PRIVATE ZXing::ZXing)
3749

50+
# Copy ZXing.dll alongside zxingcpp.pyd to solve ImportError during stub generation
51+
if (WIN32 AND BUILD_SHARED_LIBS)
52+
add_custom_command(TARGET zxingcpp POST_BUILD
53+
COMMAND ${CMAKE_COMMAND} -E copy_if_different
54+
$<TARGET_FILE:ZXing::ZXing>
55+
$<TARGET_FILE_DIR:zxingcpp>
56+
VERBATIM
57+
)
58+
endif()
59+
60+
nanobind_add_stub(
61+
zxingcpp_stub
62+
MODULE zxingcpp
63+
OUTPUT zxingcpp.pyi
64+
PYTHON_PATH $<TARGET_FILE_DIR:zxingcpp>
65+
DEPENDS zxingcpp
66+
)
67+
3868
if (ZXING_READERS AND ZXING_WRITERS)
3969
add_test(NAME PythonTest COMMAND ${Python_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test.py -v)
4070
set_property(TEST PythonTest PROPERTY ENVIRONMENT PYTHONPATH=$<TARGET_FILE_DIR:zxingcpp>)
4171
endif()
4272

43-
if (NOT DEFINED ZXING_PYTHON_INSTALL_BINDIR)
44-
set(ZXING_PYTHON_INSTALL_BINDIR "${CMAKE_INSTALL_BINDIR}")
45-
endif()
46-
73+
# Default package install directory ("zxingcpp"), enables 'import zxingcpp'
4774
if (NOT DEFINED ZXING_PYTHON_INSTALL_LIBDIR)
48-
set(ZXING_PYTHON_INSTALL_LIBDIR "${CMAKE_INSTALL_LIBDIR}")
75+
set(ZXING_PYTHON_INSTALL_LIBDIR "zxingcpp")
4976
endif()
5077

78+
# Create py.typed marker file for type checking
79+
file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/py.typed" "")
80+
5181
install(TARGETS zxingcpp
5282
COMPONENT python
53-
RUNTIME DESTINATION "${ZXING_PYTHON_INSTALL_BINDIR}"
54-
LIBRARY DESTINATION "${ZXING_PYTHON_INSTALL_LIBDIR}"
55-
ARCHIVE DESTINATION "${ZXING_PYTHON_INSTALL_LIBDIR}")
83+
LIBRARY DESTINATION "${ZXING_PYTHON_INSTALL_LIBDIR}")
84+
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/init.py
85+
COMPONENT python
86+
RENAME __init__.py
87+
DESTINATION "${ZXING_PYTHON_INSTALL_LIBDIR}")
88+
install(FILES
89+
${CMAKE_CURRENT_BINARY_DIR}/zxingcpp.pyi
90+
${CMAKE_CURRENT_BINARY_DIR}/py.typed
91+
COMPONENT python
92+
DESTINATION "${ZXING_PYTHON_INSTALL_LIBDIR}")
5693

wrappers/python/LICENSE

Lines changed: 0 additions & 1 deletion
This file was deleted.

wrappers/python/MANIFEST.in

Lines changed: 0 additions & 4 deletions
This file was deleted.

wrappers/python/init.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Re-export the native module for 'import zxingcpp' compatibility.
2+
# Named init.py instead of __init__.py to avoid import conflicts when running from this directory.
3+
from .zxingcpp import *

wrappers/python/pyproject.toml

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
[build-system]
22
requires = [
3-
"setuptools>=77",
4-
# "setuptools-scm>=8",
3+
"scikit-build-core >=0.12",
54
"wheel",
6-
"cmake>=3.18",
7-
"pybind11[global]",
5+
"nanobind >=2.11.0",
6+
# Required for nanobind stub generation on Python < 3.11
7+
'typing_extensions >=4.12;python_version<"3.11"',
88
]
9-
build-backend = "setuptools.build_meta"
9+
build-backend = "scikit_build_core.build"
1010

1111
[project]
1212
name = "zxing-cpp"
@@ -34,13 +34,42 @@ Homepage = "https://github.com/zxing-cpp/zxing-cpp"
3434
Repository = "https://github.com/zxing-cpp/zxing-cpp.git"
3535
Issues = "https://github.com/zxing-cpp/zxing-cpp/issues"
3636

37-
[tool.setuptools]
38-
include-package-data = false
37+
[tool.scikit-build]
38+
# Protect the configuration against future changes in scikit-build-core
39+
minimum-version = "build-system.requires"
3940

40-
# [tool.setuptools_scm]
41-
# root = "../.."
42-
# version_scheme = "guess-next-dev"
43-
# local_scheme = "no-local-version"
44-
# tag_regex = "^v?(\\d+\\.\\d+\\.\\d+)"
45-
# fallback_version = "0.0.0"
46-
# write_to = "zxingcpp/_version.py"
41+
cmake.build-type = "Release"
42+
cmake.version = ">=3.18"
43+
44+
# This triggers the logic in CMakeLists.txt that copies zxing.cmake + LICENSE from parent,
45+
# ensuring the sdist is self-contained.
46+
sdist.cmake = true
47+
48+
sdist.exclude = ["zint/backend/tests", "zint/backend/tools"]
49+
50+
# Build stable ABI wheels for CPython 3.12+ (set via cibuildwheel override for cp312-* only)
51+
#wheel.py-api = "cp312"
52+
53+
[tool.cibuildwheel]
54+
build = ["cp310-*", "cp311-*", "cp312-*"]
55+
skip = ["*musllinux*", "*-win32"]
56+
# TODO: setup a "BEFORE" cmake build and link the python module to the prebuild libZXing.a
57+
# see https://github.com/YannickJadoul/Parselmouth/blob/523c117aa780184345121f6ff8315670bc7d4d94/.github/workflows/wheels.yml#L120
58+
before-test = "rm -f wrappers/python/zxingcpp.*"
59+
build-verbosity = 1
60+
test-command = "python {package}/test.py"
61+
test-sources = ["wrappers/python/test.py"]
62+
63+
[tool.cibuildwheel.macos]
64+
#archs = "universal2"
65+
#environment = {CMAKE_OSX_ARCHITECTURES = "arm64;x86_64"}
66+
67+
[[tool.cibuildwheel.overrides]]
68+
select = "*-macosx_x86_64"
69+
# the default macOS target version for intel is 10.9; c++20 requires at least 10.15
70+
environment = {MACOSX_DEPLOYMENT_TARGET = "10.15"}
71+
72+
[[tool.cibuildwheel.overrides]]
73+
select = "cp312-*"
74+
# required for the stable abi wheel tagging
75+
environment = {SKBUILD_WHEEL_PY_API = "cp312"}

wrappers/python/setup.py

Lines changed: 0 additions & 41 deletions
This file was deleted.

wrappers/python/test.py

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,15 @@ def test_formats(self):
2626
self.assertEqual(zxingcpp.BarcodeFormats(BF.EAN13), BF.EAN13)
2727
self.assertIn(BF.QRCode, zxingcpp.BarcodeFormats(BF.EAN13 | BF.QRCode))
2828
self.assertIn(BF.QRCode, zxingcpp.barcode_formats_list(BF.AllMatrix))
29+
self.assertIn(BF.QRCode, zxingcpp.barcode_formats_list((BF.QRCode, BF.Aztec)))
30+
self.assertIn(BF.QRCode, zxingcpp.barcode_formats_list([BF.QRCode, BF.Aztec]))
31+
32+
def test_module_scope_aliases(self):
33+
self.assertIs(zxingcpp.QRCode, zxingcpp.BarcodeFormat.QRCode)
34+
self.assertIs(zxingcpp.LocalAverage, zxingcpp.Binarizer.LocalAverage)
35+
self.assertIs(zxingcpp.Text, zxingcpp.ContentType.Text)
36+
self.assertIs(zxingcpp.HRI, zxingcpp.TextMode.HRI)
37+
self.assertIs(zxingcpp.Lum, zxingcpp.ImageFormat.Lum)
2938

3039
class TestReadWrite(unittest.TestCase):
3140

@@ -126,10 +135,14 @@ def test_write_read_cycle_numpy(self):
126135
format = BF.QRCode
127136
text = "I have the best words."
128137
img = zxingcpp.create_barcode(text, format).to_image()
129-
img = np.array(img)
138+
npa = np.array(img, copy=False)
130139

131-
self.check_res(zxingcpp.read_barcode(img), format, text)
132-
self.check_res(zxingcpp.read_barcode(img[4:40,4:40]), format, text)
140+
self.assertEqual(npa.ndim, 2)
141+
self.assertEqual(npa.dtype, np.uint8)
142+
self.assertEqual(npa.shape, (img.shape[0], img.shape[1]))
143+
144+
self.check_res(zxingcpp.read_barcode(npa), format, text)
145+
self.check_res(zxingcpp.read_barcode(npa[4:40,4:40]), format, text)
133146

134147
@unittest.skipIf(not has_pil, "need PIL for read/write tests")
135148
def test_write_read_cycle_pil(self):
@@ -175,6 +188,23 @@ def test_read_invalid_numpy_array_channels_numpy(self):
175188
np.zeros((100, 100, 4), np.uint8)
176189
)
177190

191+
def test_image_buffer_protocol(self):
192+
"""Test that Image objects support the buffer protocol"""
193+
format = BF.QRCode
194+
text = "Buffer protocol test"
195+
img = zxingcpp.create_barcode(text, format).to_image()
196+
197+
# Test memoryview
198+
mv = memoryview(img)
199+
self.assertEqual(mv.ndim, 2)
200+
self.assertEqual(mv.format, 'B')
201+
self.assertEqual(mv.readonly, True)
202+
self.assertEqual(mv.shape, (img.shape[0], img.shape[1]))
203+
204+
# Test that we can read the barcode back using memoryview
205+
res = zxingcpp.read_barcode(mv, format)
206+
self.check_res(res, format, text)
207+
178208

179209
if __name__ == '__main__':
180210
unittest.main()

wrappers/python/zint/backend

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../../zint/backend

0 commit comments

Comments
 (0)