Skip to content

Commit 1f29b75

Browse files
committed
Add free-threaded support
1 parent 610260a commit 1f29b75

9 files changed

Lines changed: 790 additions & 36 deletions

File tree

CMakeLists.txt

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,28 @@
11
cmake_minimum_required(VERSION 3.15...3.29)
22
project(mapbox_earcut LANGUAGES CXX)
33

4+
if (NOT SKBUILD)
5+
message(WARNING "\
6+
This CMake file is meant to be executed using 'scikit-build-core'.
7+
Running it directly will almost certainly not produce the desired
8+
result. If you are a user trying to install this package, use the
9+
command below, which will install all necessary build dependencies,
10+
compile the package in an isolated environment, and then install it.
11+
=====================================================================
12+
$ uv sync
13+
=====================================================================
14+
If you are a software developer, and this is your own package, then
15+
it is usually much more efficient to install the build dependencies
16+
in your environment once and use the following command that avoids
17+
a costly creation of a new virtual environment at every compilation:
18+
=====================================================================
19+
$ uv build
20+
=====================================================================
21+
You may optionally add -Ceditable.rebuild=true to auto-rebuild when
22+
the package is imported. Otherwise, you need to rerun the above
23+
after editing C++ files.")
24+
endif()
25+
426
find_package(Python COMPONENTS Interpreter Development
527
REQUIRED)
628
execute_process(
@@ -9,8 +31,28 @@ execute_process(
931
find_package(nanobind CONFIG REQUIRED)
1032

1133
nanobind_add_module(
12-
mapbox_earcut
34+
_core
35+
# Target the stable ABI for Python 3.12+, which reduces
36+
# the number of binary wheels that must be built. This
37+
# does nothing on older Python versions
38+
STABLE_ABI
39+
FREE_THREADED
40+
NB_DOMAIN mapbox_earcut
1341
src/main.cpp
1442
)
15-
target_include_directories(mapbox_earcut PRIVATE include)
16-
install(TARGETS mapbox_earcut DESTINATION .)
43+
target_include_directories(_core PRIVATE include)
44+
45+
nanobind_add_stub(
46+
mapbox_earcut_stubs
47+
MODULE _core
48+
OUTPUT _core.pyi
49+
PYTHON_PATH $<TARGET_FILE_DIR:_core>
50+
DEPENDS _core
51+
MARKER_FILE py.typed
52+
)
53+
54+
install(TARGETS _core LIBRARY DESTINATION mapbox_earcut)
55+
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/_core.pyi DESTINATION mapbox_earcut)
56+
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/py.typed DESTINATION mapbox_earcut)
57+
install(FILES mapbox_earcut/__init__.py DESTINATION mapbox_earcut)
58+
install(FILES mapbox_earcut/__init__.pyi DESTINATION mapbox_earcut)

mapbox_earcut/__init__.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
"""
2+
Python bindings for the mapbox earcut C++ polygon triangulation library.
3+
4+
This module provides fast triangulation of 2D polygons using the Mapbox Earcut algorithm.
5+
"""
6+
7+
# Import all functions from the compiled extension module
8+
from ._core import (
9+
triangulate_float32,
10+
triangulate_float64,
11+
triangulate_int32,
12+
triangulate_int64,
13+
__version__,
14+
)
15+
16+
__all__ = [
17+
"triangulate_float32",
18+
"triangulate_float64",
19+
"triangulate_int32",
20+
"triangulate_int64",
21+
"__version__",
22+
]

mapbox_earcut/__init__.pyi

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
"""Type stubs for mapbox_earcut package."""
2+
3+
from mapbox_earcut._core import (
4+
__version__ as __version__,
5+
triangulate_float32 as triangulate_float32,
6+
triangulate_float64 as triangulate_float64,
7+
triangulate_int32 as triangulate_int32,
8+
triangulate_int64 as triangulate_int64,
9+
)
10+
11+
__all__ = [
12+
"__version__",
13+
"triangulate_float32",
14+
"triangulate_float64",
15+
"triangulate_int32",
16+
"triangulate_int64",
17+
]

mapbox_earcut/py.typed

Whitespace-only changes.

pyproject.toml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,20 @@ classifiers = [
1717
"Programming Language :: Python :: 3.12",
1818
"Programming Language :: Python :: 3.13",
1919
"Programming Language :: Python :: 3.14",
20+
"Programming Language :: Python :: Free Threading :: 1 - Unstable",
2021
]
2122
dependencies = ["numpy"]
2223

2324
[project.urls]
2425
Source = "https://github.com/skogler/mapbox_earcut_python"
2526

27+
[project.optional-dependencies]
28+
dev = ["pytest>=8.0", "mypy"]
29+
30+
[tool.scikit-build]
31+
minimum-version = "build-system.requires"
32+
build-dir = "build/{wheel_tag}"
33+
2634
[tool.scikit-build.metadata.version]
2735
provider = "scikit_build_core.metadata.regex"
2836
input = "include/version.hpp"
@@ -34,7 +42,7 @@ regex = '''(?sx)
3442
result = "{major}.{minor}.{patch}"
3543

3644
[build-system]
37-
requires = ["nanobind>=2.9.2", "scikit-build-core"]
45+
requires = ["nanobind>=2.9.2", "scikit-build-core>=0.11.6"]
3846
build-backend = "scikit_build_core.build"
3947

4048
[tool.cibuildwheel]

src/main.cpp

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,20 +72,18 @@ auto triangulate(const VertexArray<CoordT>& vertices, const IndexArray<IndexT>&
7272
).cast();
7373
}
7474

75-
NB_MODULE(mapbox_earcut, m)
75+
NB_MODULE(_core, m)
7676
{
7777
m.attr("__version__") = MACRO_TO_STR(VERSION_MAJOR) "." MACRO_TO_STR(VERSION_MINOR) "." MACRO_TO_STR(VERSION_PATCH);
7878
m.doc() = R"pbdoc(
7979
Python bindings to mapbox/earcut.hpp
8080
-----------------------
8181
82-
.. currentmodule:: mapbox_earcut
82+
.. currentmodule:: mapbox_earcut._core
8383
8484
.. autosummary::
8585
:toctree: _generate
8686
87-
add
88-
subtract
8987
)pbdoc";
9088

9189
m.def("triangulate_int32", &triangulate<int32_t, uint32_t>);

tests/test_earcut.py

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -10,64 +10,64 @@ def test_valid_triangulation_float32():
1010
result = earcut.triangulate_float32(verts, rings)
1111

1212
assert result.dtype == np.uint32
13-
assert result.shape == (3, )
14-
assert np.all(result == np.array([1, 2, 0]))
13+
assert result.shape == (3,)
14+
assert np.array_equal(result, np.array([1, 2, 0]))
1515

1616

1717
def test_valid_triangulation_float64():
1818
verts = np.array([[0, 0], [1, 0], [1, 1]], dtype=np.float64).reshape(-1, 2)
1919
rings = np.array([3])
2020

21-
result = earcut.triangulate_float32(verts, rings)
21+
result = earcut.triangulate_float64(verts, rings)
2222

2323
assert result.dtype == np.uint32
24-
assert result.shape == (3, )
25-
assert np.all(result == np.array([1, 2, 0]))
24+
assert result.shape == (3,)
25+
assert np.array_equal(result, np.array([1, 2, 0]))
2626

2727

2828
def test_valid_triangulation_int32():
2929
verts = np.array([[0, 0], [1, 0], [1, 1]], dtype=np.int32).reshape(-1, 2)
3030
rings = np.array([3])
3131

32-
result = earcut.triangulate_float32(verts, rings)
32+
result = earcut.triangulate_int32(verts, rings)
3333

3434
assert result.dtype == np.uint32
35-
assert result.shape == (3, )
36-
assert np.all(result == np.array([1, 2, 0]))
35+
assert result.shape == (3,)
36+
assert np.array_equal(result, np.array([1, 2, 0]))
3737

3838

39-
def test_inverted_vertex_order():
40-
verts = np.array(
41-
list(reversed([[0, 0], [1, 0], [1, 1]])), dtype=np.int32).reshape(
42-
-1, 2)
39+
def test_valid_triangulation_int64():
40+
verts = np.array([[0, 0], [1, 0], [1, 1]], dtype=np.int64).reshape(-1, 2)
4341
rings = np.array([3])
4442

45-
result = earcut.triangulate_float32(verts, rings)
43+
result = earcut.triangulate_int64(verts, rings)
4644

4745
assert result.dtype == np.uint32
48-
assert result.shape == (3, )
49-
assert np.all(result == np.array([1, 0, 2]))
46+
assert result.shape == (3,)
47+
assert np.array_equal(result, np.array([1, 2, 0]))
5048

5149

52-
def test_no_triangles():
53-
verts = np.array([[0, 0], [1, 0], [1, 1]], dtype=np.int32).reshape(-1, 2)
54-
rings = np.array([2, 3])
50+
def test_inverted_vertex_order():
51+
verts = np.array(list(reversed([[0, 0], [1, 0], [1, 1]])), dtype=np.int32).reshape(
52+
-1, 2
53+
)
54+
rings = np.array([3])
5555

56-
result = earcut.triangulate_float32(verts, rings)
56+
result = earcut.triangulate_int32(verts, rings)
5757

5858
assert result.dtype == np.uint32
59-
assert result.shape == (0, )
59+
assert result.shape == (3,)
60+
assert np.array_equal(result, np.array([1, 0, 2]))
6061

6162

62-
def test_valid_triangulation_int64():
63-
verts = np.array([[0, 0], [1, 0], [1, 1]], dtype=np.int64).reshape(-1, 2)
64-
rings = np.array([3])
63+
def test_no_triangles():
64+
verts = np.array([[0, 0], [1, 0], [1, 1]], dtype=np.int32).reshape(-1, 2)
65+
rings = np.array([2, 3])
6566

66-
result = earcut.triangulate_float32(verts, rings)
67+
result = earcut.triangulate_int32(verts, rings)
6768

6869
assert result.dtype == np.uint32
69-
assert result.shape == (3, )
70-
assert np.all(result == np.array([1, 2, 0]))
70+
assert result.shape == (0,)
7171

7272

7373
def test_end_index_too_large():
@@ -124,4 +124,4 @@ def test_empty_data():
124124

125125
result = earcut.triangulate_float32(verts, rings)
126126

127-
assert result.shape == (0, )
127+
assert result.shape == (0,)

0 commit comments

Comments
 (0)