Skip to content

Commit

Permalink
Add ability to create Python wheels (#313)
Browse files Browse the repository at this point in the history
* Rename Python bindings from pywraps2 to s2geometry and set SWIG CMake policies to remove warnings about using deprecated default binding name
* Fix broken unit tests
* Pass -DCMAKE_POSITION_INDEPENDENT_CODE=ON to CMake via setup.py for building Python wheel
* Use a PEP 440-compliant pre-release version since the code in master does not correspond to already released version 0.10.0
  • Loading branch information
selimnairb authored Apr 28, 2023
1 parent d184302 commit 7773d51
Show file tree
Hide file tree
Showing 6 changed files with 131 additions and 21 deletions.
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,12 @@ Disable building of shared libraries with `-DBUILD_SHARED_LIBS=OFF`.

Enable the python interface with `-DWITH_PYTHON=ON`.

If OpenSSL is installed in a non-standard location set `OPENSSL_ROOT_DIR`
before running configure, for example on macOS:
```
OPENSSL_ROOT_DIR=/opt/homebrew/Cellar/openssl@3/3.1.0 cmake -DCMAKE_PREFIX_PATH=/opt/homebrew -DCMAKE_CXX_STANDARD=17
```

## Installing

From `build` subdirectory:
Expand Down Expand Up @@ -174,6 +180,25 @@ even 2.0.

Python 3 is required.

### Creating wheels
First, make a virtual environment and install `cmake_build_extension` and `wheel`
into it:
```
python3 -m venv venv
source venv/bin/activate
pip install cmake_build_extension wheel
```

Then build the wheel:
```
python setup.py bdist_wheel
```

The resulting wheel will be in the `dist` directory.

> If OpenSSL is in a non-standard location make sure to set `OPENSSL_ROOT_DIR`
> when calling `setup.py`, see above for more information.
## Other S2 implementations

* [Go](https://github.com/golang/geo) (Approximately 40% complete.)
Expand Down
8 changes: 8 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[build-system]
requires = [
"wheel",
"setuptools",
"setuptools_scm[toml]",
"cmake_build_extension",
]
build-backend = "setuptools.build_meta"
22 changes: 22 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[metadata]
name = s2geometry
version = 0.11.0.dev1
description = Python packaging of s2geometry
author = Brian Miles
author_email = selimnairb@gmail.com
license= Apache 2
project_urls =
Source = https://github.com/google/s2geometry
classifiers =
Programming Language :: Python :: 3
Operating System :: POSIX
License :: OSI Approved :: Apache Software License

[options]
zip_safe = False
packages = find:
package_dir = =src
python_requres = >=3.7

[options.packages.find]
where = src
33 changes: 33 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import sys
from pathlib import Path

import cmake_build_extension
import setuptools


setuptools.setup(
ext_modules=[
cmake_build_extension.CMakeExtension(
# This could be anything you like, it is used to create build folders
name="SwigBindings",
# Name of the resulting package name (import s2geometry)
install_prefix="s2geometry",
# Selects the folder where the main CMakeLists.txt is stored
# (it could be a subfolder)
source_dir=str(Path(__file__).parent.absolute()),
cmake_configure_options=[
# This option points CMake to the right Python interpreter, and helps
# the logic of FindPython3.cmake to find the active version
f"-DPython3_ROOT_DIR={Path(sys.prefix)}",
'-DCALL_FROM_SETUP_PY:BOOL=ON',
'-DBUILD_SHARED_LIBS:BOOL=OFF',
'-DCMAKE_POSITION_INDEPENDENT_CODE=ON',
'-DWITH_PYTHON=ON'
]
)
],
cmdclass=dict(
# Enable the CMakeExtension entries defined above
build_ext=cmake_build_extension.BuildExtension,
),
)
40 changes: 31 additions & 9 deletions src/python/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,21 +1,43 @@
# Generate standard target names.
cmake_policy(SET CMP0078 NEW)
# Honor SWIG_MODULE_NAME via -module flag.
cmake_policy(SET CMP0086 NEW)

# Handle where to install the resulting Python package
if (CALL_FROM_SETUP_PY)
# The CMakeExtension will set CMAKE_INSTALL_PREFIX to the root
# of the resulting wheel archive
set(S2GEOMETRY_INSTALL_PREFIX ${CMAKE_INSTALL_PREFIX})
else()
# The Python package is installed directly in the folder of the
# detected interpreter (system, user, or virtualenv)
set(S2GEOMETRY_INSTALL_PREFIX ${Python3_SITELIB})
endif()

include(${SWIG_USE_FILE})
include_directories(${Python3_INCLUDE_DIRS})

set(CMAKE_SWIG_FLAGS "")
set_property(SOURCE s2.i PROPERTY SWIG_FLAGS "-module" "pywraps2")
set_property(SOURCE s2.i PROPERTY SWIG_FLAGS "-module" "s2geometry")
set_property(SOURCE s2.i PROPERTY CPLUSPLUS ON)

swig_add_library(pywraps2 LANGUAGE python SOURCES s2.i)
swig_add_library(s2geometry LANGUAGE python SOURCES s2.i)

swig_link_libraries(pywraps2 ${Python3_LIBRARIES} s2)
swig_link_libraries(s2geometry ${Python3_LIBRARIES} s2)
enable_testing()
add_test(NAME pywraps2_test COMMAND
add_test(NAME s2geometry_test COMMAND
${Python3_EXECUTABLE}
"${PROJECT_SOURCE_DIR}/src/python/pywraps2_test.py")
set_property(TEST pywraps2_test PROPERTY ENVIRONMENT
"${PROJECT_SOURCE_DIR}/src/python/s2geometry_test.py")
set_property(TEST s2geometry_test PROPERTY ENVIRONMENT
"PYTHONPATH=$ENV{PYTHONPATH}:${PROJECT_BINARY_DIR}/python")

# Install the wrapper.
install(TARGETS _pywraps2 DESTINATION ${Python3_SITELIB})
install(FILES "${PROJECT_BINARY_DIR}/python/pywraps2.py"
DESTINATION ${Python3_SITELIB})
install(TARGETS s2geometry DESTINATION ${S2GEOMETRY_INSTALL_PREFIX})

# Install swig-generated Python file (we rename it to __init__.py as it will
# ultimately end up in a directory called s2geometry in site-packages, which will
# serve as the module directory.
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/s2geometry.py"
DESTINATION ${S2GEOMETRY_INSTALL_PREFIX}
RENAME __init__.py
COMPONENT s2geometry)
24 changes: 12 additions & 12 deletions src/python/pywraps2_test.py → src/python/s2geometry_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import unittest
from collections import defaultdict

import pywraps2 as s2
import s2geometry as s2


class PyWrapS2TestCase(unittest.TestCase):
Expand Down Expand Up @@ -1039,38 +1039,38 @@ def testInitToUnionDistinct(self):

class S2ChordAngleTest(unittest.TestCase):
def testBasic(self):
ca = s2.S1ChordAngle(s2.S1Angle_Degrees(100))
ca = s2.S1ChordAngle(s2.S1Angle.Degrees(100))
self.assertAlmostEqual(100, ca.degrees())

def testArithmetic(self):
ca1 = s2.S1ChordAngle(s2.S1Angle_Degrees(10))
ca2 = s2.S1ChordAngle(s2.S1Angle_Degrees(20))
ca1 = s2.S1ChordAngle(s2.S1Angle.Degrees(10))
ca2 = s2.S1ChordAngle(s2.S1Angle.Degrees(20))
ca3 = ca1 + ca2
self.assertAlmostEqual(30, ca3.degrees())
ca4 = ca2 - ca1
self.assertAlmostEqual(10, ca4.degrees())

def testComparison(self):
ca1 = s2.S1ChordAngle(s2.S1Angle_Degrees(10))
ca2 = s2.S1ChordAngle(s2.S1Angle_Degrees(20))
ca1 = s2.S1ChordAngle(s2.S1Angle.Degrees(10))
ca2 = s2.S1ChordAngle(s2.S1Angle.Degrees(20))
self.assertTrue(ca1 < ca2)
self.assertTrue(ca2 > ca1)
self.assertFalse(ca1 > ca2)
self.assertFalse(ca2 < ca1)

ca3 = s2.S1ChordAngle(s2.S1Angle_Degrees(10))
ca3 = s2.S1ChordAngle(s2.S1Angle.Degrees(10))
self.assertTrue(ca1 == ca3)
self.assertFalse(ca1 == ca2)
self.assertFalse(ca1 != ca3)
self.assertTrue(ca1 != ca2)

def testInfinity(self):
ca1 = s2.S1ChordAngle(s2.S1Angle_Degrees(179))
ca1 = s2.S1ChordAngle(s2.S1Angle.Degrees(179))
ca2 = s2.S1ChordAngle.Infinity()
self.assertTrue(ca2 > ca1)

def testCopy(self):
ca1 = s2.S1ChordAngle(s2.S1Angle_Degrees(100))
ca1 = s2.S1ChordAngle(s2.S1Angle.Degrees(100))
ca2 = s2.S1ChordAngle(ca1)
self.assertAlmostEqual(100, ca2.degrees())

Expand All @@ -1092,7 +1092,7 @@ def testDefaults(self):
self.assertEqual(4, loop.num_vertices())

def testRadius(self):
self.opts.set_buffer_radius(s2.S1Angle_Degrees(0.001))
self.opts.set_buffer_radius(s2.S1Angle.Degrees(0.001))
op = s2.S2BufferOperation(self.layer, self.opts)

cell1 = s2.S2Cell(s2.S2CellId(s2.S2LatLng.FromDegrees(3.0, 4.0)).parent(8))
Expand All @@ -1104,7 +1104,7 @@ def testRadius(self):
self.assertEqual(20, loop.num_vertices())

def testRadiusAndError(self):
self.opts.set_buffer_radius(s2.S1Angle_Degrees(0.001))
self.opts.set_buffer_radius(s2.S1Angle.Degrees(0.001))
self.opts.set_error_fraction(0.1)
op = s2.S2BufferOperation(self.layer, self.opts)

Expand All @@ -1117,7 +1117,7 @@ def testRadiusAndError(self):
self.assertEqual(12, loop.num_vertices())

def testPoint(self):
self.opts.set_buffer_radius(s2.S1Angle_Degrees(0.001))
self.opts.set_buffer_radius(s2.S1Angle.Degrees(0.001))
op = s2.S2BufferOperation(self.layer, self.opts)

op.AddPoint(s2.S2LatLng.FromDegrees(14.0, 15.0).ToPoint())
Expand Down

0 comments on commit 7773d51

Please sign in to comment.