From a314014c949d8b6b63859fdd45cc526541c09fd0 Mon Sep 17 00:00:00 2001 From: Duncan Ogilvie Date: Tue, 16 Dec 2025 13:33:28 +0100 Subject: [PATCH 1/6] Generate type stubs --- .gitignore | 1 + CMakeLists.txt | 34 ++++++++++++++++++++++++++++++---- src/nanobind_example/py.typed | 0 3 files changed, 31 insertions(+), 4 deletions(-) create mode 100644 src/nanobind_example/py.typed diff --git a/.gitignore b/.gitignore index 29f2c4e..6ab71bc 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ build *.egg-info .vscode .vs +.venv diff --git a/CMakeLists.txt b/CMakeLists.txt index af8ce44..4fe9cfe 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -26,12 +26,25 @@ if (NOT SKBUILD) endif() # Try to import all Python components potentially needed by nanobind -find_package(Python 3.8 +find_package(Python 3.12 REQUIRED COMPONENTS Interpreter Development.Module - OPTIONAL_COMPONENTS Development.SABIModule) + OPTIONAL_COMPONENTS Development.SABIModule +) + +include(FetchContent) -# Import nanobind through CMake's find_package mechanism -find_package(nanobind CONFIG REQUIRED) +# Fix warnings about DOWNLOAD_EXTRACT_TIMESTAMP +if(POLICY CMP0135) + cmake_policy(SET CMP0135 NEW) +endif() +message(STATUS "Fetching nanobind (v2.10.2)...") +FetchContent_Declare(nanobind + GIT_REPOSITORY + "https://github.com/wjakob/nanobind" + GIT_TAG + v2.10.2 +) +FetchContent_MakeAvailable(nanobind) # We are now ready to compile the actual extension module nanobind_add_module( @@ -55,5 +68,18 @@ nanobind_add_module( src/nanobind_example_ext.cpp ) +# Generate type stubs for the extension module +nanobind_add_stub( + nanobind_example_ext_stub + MODULE nanobind_example_ext + OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/nanobind_example_ext.pyi + PYTHON_PATH $ + DEPENDS nanobind_example_ext +) + # Install directive for scikit-build-core install(TARGETS nanobind_example_ext LIBRARY DESTINATION nanobind_example) + +# Install the generated type stubs +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/nanobind_example_ext.pyi DESTINATION nanobind_example) +install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/src/nanobind_example/py.typed DESTINATION nanobind_example) diff --git a/src/nanobind_example/py.typed b/src/nanobind_example/py.typed new file mode 100644 index 0000000..e69de29 From 62f20ffb50aafc1509d80442bc9a3fd221b4c7c4 Mon Sep 17 00:00:00 2001 From: Duncan Ogilvie Date: Tue, 16 Dec 2025 13:39:58 +0100 Subject: [PATCH 2/6] Move to uv --- .python_version | 1 + CMakeLists.txt | 41 +++++++++++++------------ pyproject.toml | 20 +++++++++---- uv.lock | 79 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 117 insertions(+), 24 deletions(-) create mode 100644 .python_version create mode 100644 uv.lock diff --git a/.python_version b/.python_version new file mode 100644 index 0000000..fdcfcfd --- /dev/null +++ b/.python_version @@ -0,0 +1 @@ +3.12 \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 4fe9cfe..20ece07 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,6 +2,12 @@ cmake_minimum_required(VERSION 3.15...3.26) project(nanobind_example LANGUAGES CXX) +# Try to import all Python components potentially needed by nanobind +find_package(Python 3.12 + REQUIRED COMPONENTS Interpreter Development.Module + OPTIONAL_COMPONENTS Development.SABIModule +) + if (NOT SKBUILD) message(WARNING "\ This CMake file is meant to be executed using 'scikit-build'. Running @@ -23,28 +29,25 @@ if (NOT SKBUILD) You may optionally add -Ceditable.rebuild=true to auto-rebuild when the package is imported. Otherwise, you need to re-run the above after editing C++ files.") -endif() -# Try to import all Python components potentially needed by nanobind -find_package(Python 3.12 - REQUIRED COMPONENTS Interpreter Development.Module - OPTIONAL_COMPONENTS Development.SABIModule -) - -include(FetchContent) + include(FetchContent) -# Fix warnings about DOWNLOAD_EXTRACT_TIMESTAMP -if(POLICY CMP0135) - cmake_policy(SET CMP0135 NEW) + # Fix warnings about DOWNLOAD_EXTRACT_TIMESTAMP + if(POLICY CMP0135) + cmake_policy(SET CMP0135 NEW) + endif() + message(STATUS "Fetching nanobind (v2.10.2)...") + FetchContent_Declare(nanobind + GIT_REPOSITORY + "https://github.com/wjakob/nanobind" + GIT_TAG + v2.10.2 + ) + FetchContent_MakeAvailable(nanobind) +else() + # Use the nanobind provided by scikit-build-core + find_package(nanobind REQUIRED) endif() -message(STATUS "Fetching nanobind (v2.10.2)...") -FetchContent_Declare(nanobind - GIT_REPOSITORY - "https://github.com/wjakob/nanobind" - GIT_TAG - v2.10.2 -) -FetchContent_MakeAvailable(nanobind) # We are now ready to compile the actual extension module nanobind_add_module( diff --git a/pyproject.toml b/pyproject.toml index e49c78d..5a17b49 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["scikit-build-core >=0.10", "nanobind >=1.3.2"] +requires = ["scikit-build-core >=0.11.6", "nanobind >=2.10.2"] build-backend = "scikit_build_core.build" [project] @@ -7,7 +7,8 @@ name = "nanobind-example" version = "0.0.1" description = "An example minimal project that compiles bindings using nanobind and scikit-build" readme = "README.md" -requires-python = ">=3.8" +requires-python = ">=3.12" +dependencies = [] authors = [ { name = "Wenzel Jakob", email = "wenzel.jakob@epfl.ch" }, ] @@ -18,6 +19,10 @@ classifiers = [ [project.urls] Homepage = "https://github.com/wjakob/nanobind_example" +[dependency-groups] +dev = [ + "pytest", +] [tool.scikit-build] # Protect the configuration against future changes in scikit-build-core @@ -29,6 +34,13 @@ build-dir = "build/{wheel_tag}" # Build stable ABI wheels for CPython 3.12+ wheel.py-api = "cp312" +[tool.uv] +cache-keys = [ + { file = "CMakeLists.txt" }, + { file = "src/**/*.cpp" }, + { file = "src/**/*.h" }, +] + [tool.cibuildwheel] # Necessary to see build output from the actual compilation build-verbosity = 1 @@ -37,9 +49,7 @@ build-verbosity = 1 test-command = "pytest {project}/tests" test-requires = "pytest" -# Don't test Python 3.8 wheels on macOS/arm64 -test-skip="cp38-macosx_*:arm64" - # Needed for full C++17 support +# TODO: this needs to be fixed [tool.cibuildwheel.macos.environment] MACOSX_DEPLOYMENT_TARGET = "10.14" diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..29ed645 --- /dev/null +++ b/uv.lock @@ -0,0 +1,79 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "nanobind-example" +version = "0.0.1" +source = { editable = "." } + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [{ name = "pytest" }] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] From 3e423f1c0b556a04daaa1738871d8f7cca791cb7 Mon Sep 17 00:00:00 2001 From: Duncan Ogilvie Date: Tue, 16 Dec 2025 15:26:26 +0100 Subject: [PATCH 3/6] Add proper type hint support --- .python_version => .python-version | 0 CMakeLists.txt | 14 ++++++++++---- pyproject.toml | 3 +++ src/nanobind_example/__init__.py | 2 +- src/nanobind_example/py.typed | 0 src/nanobind_example_ext.cpp | 6 ++++-- 6 files changed, 18 insertions(+), 7 deletions(-) rename .python_version => .python-version (100%) delete mode 100644 src/nanobind_example/py.typed diff --git a/.python_version b/.python-version similarity index 100% rename from .python_version rename to .python-version diff --git a/CMakeLists.txt b/CMakeLists.txt index 20ece07..13ae289 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,9 @@ cmake_minimum_required(VERSION 3.15...3.26) +if(CMAKE_SOURCE_DIR STREQUAL CMAKE_BINARY_DIR) + message(FATAL_ERROR "In-tree builds are not supported. Run CMake from a separate directory: cmake -B build") +endif() + project(nanobind_example LANGUAGES CXX) # Try to import all Python components potentially needed by nanobind @@ -80,9 +84,11 @@ nanobind_add_stub( DEPENDS nanobind_example_ext ) -# Install directive for scikit-build-core +# Install the extension module and type stubs install(TARGETS nanobind_example_ext LIBRARY DESTINATION nanobind_example) - -# Install the generated type stubs +file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/py.typed" "") +install(FILES "${CMAKE_CURRENT_BINARY_DIR}/py.typed" DESTINATION nanobind_example) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/nanobind_example_ext.pyi DESTINATION nanobind_example) -install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/src/nanobind_example/py.typed DESTINATION nanobind_example) + +# Install the Python package files +install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/src/nanobind_example/__init__.py DESTINATION nanobind_example) diff --git a/pyproject.toml b/pyproject.toml index 5a17b49..bb6b779 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,9 @@ build-dir = "build/{wheel_tag}" # Build stable ABI wheels for CPython 3.12+ wheel.py-api = "cp312" +# This setup only works properly in redirect mode +editable.mode = "redirect" + [tool.uv] cache-keys = [ { file = "CMakeLists.txt" }, diff --git a/src/nanobind_example/__init__.py b/src/nanobind_example/__init__.py index fe8a74c..a053ccd 100644 --- a/src/nanobind_example/__init__.py +++ b/src/nanobind_example/__init__.py @@ -1 +1 @@ -from .nanobind_example_ext import add, __doc__ +from nanobind_example.nanobind_example_ext import add, __doc__ diff --git a/src/nanobind_example/py.typed b/src/nanobind_example/py.typed deleted file mode 100644 index e69de29..0000000 diff --git a/src/nanobind_example_ext.cpp b/src/nanobind_example_ext.cpp index 6d30dbf..01358d6 100644 --- a/src/nanobind_example_ext.cpp +++ b/src/nanobind_example_ext.cpp @@ -4,7 +4,9 @@ namespace nb = nanobind; using namespace nb::literals; +static int add(int a, int b) { return a + b; } + NB_MODULE(nanobind_example_ext, m) { - m.doc() = "This is a \"hello world\" example with nanobind"; - m.def("add", [](int a, int b) { return a + b; }, "a"_a, "b"_a); + m.doc() = "This is a \"hello world\" example with nanobind"; + m.def("add", &add, "a"_a, "b"_a, R"(Add two integers together.)"); } From 14c6c1debe27248a2dc32534dd7431c475db4212 Mon Sep 17 00:00:00 2001 From: Duncan Ogilvie Date: Tue, 16 Dec 2025 16:48:19 +0100 Subject: [PATCH 4/6] Support type checking with ty/pyright/mypy sqush! type checking --- .github/workflows/pip.yml | 54 +++++++++++++++++++++++++++++- README.md | 56 ++++++++++++++++++++++++++++++++ pyproject.toml | 27 ++++++++++++++- src/nanobind_example/__init__.py | 7 +++- tests/test_basic.py | 1 + typecheck_smoke.py | 10 ++++++ 6 files changed, 152 insertions(+), 3 deletions(-) create mode 100644 typecheck_smoke.py diff --git a/.github/workflows/pip.yml b/.github/workflows/pip.yml index 18d8edd..8db578d 100644 --- a/.github/workflows/pip.yml +++ b/.github/workflows/pip.yml @@ -15,7 +15,7 @@ jobs: fail-fast: false matrix: platform: [windows-latest, macos-latest, ubuntu-latest] - python-version: ["3.8", "3.12"] + python-version: ["3.12"] steps: - uses: actions/checkout@v5 @@ -36,3 +36,55 @@ jobs: - name: Test run: python -m pytest + + typecheck: + name: Type checking + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v5 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + + - name: Build package + run: uv sync + + - name: Type check with ty + run: uvx ty check + + - name: Type check with pyright + run: uvx pyright + + - name: Type check with mypy + run: uv run --with mypy mypy . + + - name: Verify type stubs work (ty) + run: | + # This should fail - if it doesn't, stubs aren't working + if uvx ty check typecheck_smoke.py 2>&1 | grep -q "Found 2 diagnostics"; then + echo "✓ ty correctly detected type errors in smoke test" + else + echo "✗ ty did not detect expected type errors" + exit 1 + fi + + - name: Verify type stubs work (pyright) + run: | + # This should fail - if it doesn't, stubs aren't working + if uvx pyright typecheck_smoke.py 2>&1 | grep -q "2 errors"; then + echo "✓ pyright correctly detected type errors in smoke test" + else + echo "✗ pyright did not detect expected type errors" + exit 1 + fi + + - name: Verify type stubs work (mypy) + run: | + # This should fail - if it doesn't, stubs aren't working + if uv run --with mypy mypy typecheck_smoke.py 2>&1 | grep -q 'Argument 1 to "add" has incompatible type'; then + echo "✓ mypy correctly detected type errors in smoke test" + else + echo "✗ mypy did not detect expected type errors" + exit 1 + fi diff --git a/README.md b/README.md index fc81f0a..aacc29c 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,62 @@ interactive Python session): 3 ``` +Development +----------- + +This project uses [uv](https://docs.astral.sh/uv/) for development. + +### Building + +```bash +# Build native project (installs in editable mode) +uv sync + +# Rebuild after C++ changes +uv sync --reinstall + +# Build and run tests +uv run pytest +``` + +### Local C++ Development + +Generate `compile_commands.json` for IDE/LSP support (clangd, etc.): + +```bash +cmake -B build -G Ninja -DCMAKE_EXPORT_COMPILE_COMMANDS=ON +``` + +### Type Checking + +The project includes type stubs (`.pyi` files) generated automatically by +nanobind. A `py.typed` marker file is installed for +[PEP 561](https://peps.python.org/pep-0561/) compliance, so language servers +like Pylance, Pyright, and ty will provide completions and type checking. + +To run type checking: + +```bash +uvx ty check +uvx pyright +uv run --with mypy mypy . +``` + +### Package Structure + +The extension module is built as `nanobind_example_ext` and re-exported through +`nanobind_example/__init__.py`. When adding new bindings: + +1. Add the binding in `src/nanobind_example_ext.cpp` +2. Update `src/nanobind_example/__init__.py` to re-export the new symbols + +Imports must be absolute: + +```python +from nanobind_example import add # Correct +from nanobind_example_ext import add # Won't work +``` + CI Examples ----------- diff --git a/pyproject.toml b/pyproject.toml index bb6b779..eb163d9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,6 +44,31 @@ cache-keys = [ { file = "src/**/*.h" }, ] +[tool.ty.environment] +# Don't include src/ in first-party search path so ty finds the installed +# package (with .pyi stubs) in .venv/lib/python3.12/site-packages/ instead +root = ["."] + +[tool.ty.src] +exclude = ["typecheck_smoke.py"] + +[tool.pyright] +include = ["tests"] +exclude = [ + "build/**", + ".venv/**", + "**/__pycache__", +] +extraPaths = [] +venvPath = "." +venv = ".venv" + +[tool.mypy] +exclude = [ + "^build/", + "^typecheck_smoke\\.py$", +] + [tool.cibuildwheel] # Necessary to see build output from the actual compilation build-verbosity = 1 @@ -53,6 +78,6 @@ test-command = "pytest {project}/tests" test-requires = "pytest" # Needed for full C++17 support -# TODO: this needs to be fixed +# TODO: this needs to be handled in the workflow itself [tool.cibuildwheel.macos.environment] MACOSX_DEPLOYMENT_TARGET = "10.14" diff --git a/src/nanobind_example/__init__.py b/src/nanobind_example/__init__.py index a053ccd..34941c7 100644 --- a/src/nanobind_example/__init__.py +++ b/src/nanobind_example/__init__.py @@ -1 +1,6 @@ -from nanobind_example.nanobind_example_ext import add, __doc__ +# NOTE: we use an absolute import here to get the type checking to work properly. +# Additionally we re-export the imported symbols to make ruff happy. +from nanobind_example.nanobind_example_ext import ( + add as add, + __doc__ as __doc__, +) diff --git a/tests/test_basic.py b/tests/test_basic.py index 9c670fc..61aabff 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -1,4 +1,5 @@ import nanobind_example as m + def test_add(): assert m.add(1, 2) == 3 diff --git a/typecheck_smoke.py b/typecheck_smoke.py new file mode 100644 index 0000000..e9abcab --- /dev/null +++ b/typecheck_smoke.py @@ -0,0 +1,10 @@ +# This file contains intentional type errors to verify that type stubs work. +# CI runs type checkers on this file and expects them to fail. +# Do not import this file in actual tests. + +import nanobind_example as m + + +def bad_add_call(a: str, b: str) -> int: + """Intentionally passing str to int parameters - should fail type checking.""" + return m.add(a, b) From f51e030971d5ced94ad0a4e491eeb02c7eb8d48b Mon Sep 17 00:00:00 2001 From: Duncan Ogilvie Date: Tue, 16 Dec 2025 17:00:55 +0100 Subject: [PATCH 5/6] Make the template easier to reuse and update developer warning --- CMakeLists.txt | 48 ++++++++++++++++++++++++++++-------------------- README.md | 5 ++++- 2 files changed, 32 insertions(+), 21 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 13ae289..1816188 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,25 +27,27 @@ if (NOT SKBUILD) in your environment once and use the following command that avoids a costly creation of a new virtual environment at every compilation: ===================================================================== - $ pip install nanobind scikit-build-core[pyproject] - $ pip install --no-build-isolation -ve . + $ uv sync ===================================================================== - You may optionally add -Ceditable.rebuild=true to auto-rebuild when - the package is imported. Otherwise, you need to re-run the above - after editing C++ files.") - - include(FetchContent) + When using uv run, the package is already rebuilt automatically if + it detects sources listed in cache-keys are updated.") + # NOTE: We allow fetching nanobind directly here for development this allows + # `cmake -B build -G Ninja -DCMAKE_EXPORT_COMPILE_COMMANDS=ON` to work as + # expected and get proper language server support in editors like VSCode. + set(NANOBIND_TAG "v2.10.2") + # Fix warnings about DOWNLOAD_EXTRACT_TIMESTAMP if(POLICY CMP0135) cmake_policy(SET CMP0135 NEW) endif() - message(STATUS "Fetching nanobind (v2.10.2)...") + include(FetchContent) + message(STATUS "Fetching nanobind (${NANOBIND_TAG})...") FetchContent_Declare(nanobind GIT_REPOSITORY "https://github.com/wjakob/nanobind" GIT_TAG - v2.10.2 + ${NANOBIND_TAG} ) FetchContent_MakeAvailable(nanobind) else() @@ -53,10 +55,16 @@ else() find_package(nanobind REQUIRED) endif() +set(PYTHON_MODULE nanobind_example) +set(EXTENSION_MODULE nanobind_example_ext) +set(EXTENSION_SOURCES + src/nanobind_example_ext.cpp +) + # We are now ready to compile the actual extension module nanobind_add_module( # Name of the extension - nanobind_example_ext + ${EXTENSION_MODULE} # Target the stable ABI for Python 3.12+, which reduces # the number of binary wheels that must be built. This @@ -72,23 +80,23 @@ nanobind_add_module( NB_STATIC # Source code goes here - src/nanobind_example_ext.cpp + ${EXTENSION_SOURCES} ) # Generate type stubs for the extension module nanobind_add_stub( - nanobind_example_ext_stub - MODULE nanobind_example_ext - OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/nanobind_example_ext.pyi - PYTHON_PATH $ - DEPENDS nanobind_example_ext + ${EXTENSION_MODULE}_stub + MODULE ${EXTENSION_MODULE} + OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${EXTENSION_MODULE}.pyi + PYTHON_PATH $ + DEPENDS ${EXTENSION_MODULE} ) # Install the extension module and type stubs -install(TARGETS nanobind_example_ext LIBRARY DESTINATION nanobind_example) +install(TARGETS ${EXTENSION_MODULE} LIBRARY DESTINATION ${PYTHON_MODULE}) file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/py.typed" "") -install(FILES "${CMAKE_CURRENT_BINARY_DIR}/py.typed" DESTINATION nanobind_example) -install(FILES ${CMAKE_CURRENT_BINARY_DIR}/nanobind_example_ext.pyi DESTINATION nanobind_example) +install(FILES "${CMAKE_CURRENT_BINARY_DIR}/py.typed" DESTINATION ${PYTHON_MODULE}) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${EXTENSION_MODULE}.pyi DESTINATION ${PYTHON_MODULE}) # Install the Python package files -install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/src/nanobind_example/__init__.py DESTINATION nanobind_example) +install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/src/${PYTHON_MODULE}/__init__.py DESTINATION ${PYTHON_MODULE}) diff --git a/README.md b/README.md index aacc29c..d92b758 100644 --- a/README.md +++ b/README.md @@ -48,13 +48,16 @@ This project uses [uv](https://docs.astral.sh/uv/) for development. # Build native project (installs in editable mode) uv sync -# Rebuild after C++ changes +# Rebuild native project from scratch (usually not necessary) uv sync --reinstall # Build and run tests uv run pytest ``` +When using `uv run` the native code will automatically be rebuilt if changes +are detected in the `cache-keys`. + ### Local C++ Development Generate `compile_commands.json` for IDE/LSP support (clangd, etc.): From 54acadaeb79f7f9b4aeb00f5bb1c4af461464235 Mon Sep 17 00:00:00 2001 From: Duncan Ogilvie Date: Tue, 16 Dec 2025 17:20:17 +0100 Subject: [PATCH 6/6] Address review comments --- CMakeLists.txt | 9 ++++++++- pyproject.toml | 1 - 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1816188..7965a75 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,7 +35,14 @@ if (NOT SKBUILD) # NOTE: We allow fetching nanobind directly here for development this allows # `cmake -B build -G Ninja -DCMAKE_EXPORT_COMPILE_COMMANDS=ON` to work as # expected and get proper language server support in editors like VSCode. - set(NANOBIND_TAG "v2.10.2") + + # Extract the minimum nanobind version from pyproject.toml + file(READ "${CMAKE_CURRENT_SOURCE_DIR}/pyproject.toml" PYPROJECT_TOML_CONTENT) + if(PYPROJECT_TOML_CONTENT MATCHES "\"nanobind *[>=]= *([^\"]+)\"") + set(NANOBIND_TAG "v${CMAKE_MATCH_1}") + else() + message(FATAL_ERROR "Could not find nanobind version in pyproject.toml") + endif() # Fix warnings about DOWNLOAD_EXTRACT_TIMESTAMP if(POLICY CMP0135) diff --git a/pyproject.toml b/pyproject.toml index eb163d9..2b4221c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -78,6 +78,5 @@ test-command = "pytest {project}/tests" test-requires = "pytest" # Needed for full C++17 support -# TODO: this needs to be handled in the workflow itself [tool.cibuildwheel.macos.environment] MACOSX_DEPLOYMENT_TARGET = "10.14"