extension-helpers
extension-helpers provides a suite of utilities designed to simplify the process of building and installing Python packages that include compiled extensions (e.g., C, C++, Fortran). It helps manage compiler flags, OpenMP support, and the Python Limited API (PEP 384) for ABI compatibility. The current version is 1.4.0, and it follows a stable release cadence with minor versions released every few months, often coinciding with Astropy-related ecosystem updates.
Warnings
- gotcha ABI Compatibility with Python Limited API (PEP 384) requires careful code review. While `EXTENSION_HELPERS_PY_LIMITED_API=true` (v1.4.0+) enables compiler flags for PEP 384, developers must ensure their C/C++ code adheres to the Limited API restrictions (e.g., avoiding private Python C API functions). Incorrect usage can lead to runtime errors or subtle bugs when the extension is used with different Python versions than it was built with.
- gotcha extension-helpers relies on a correctly configured C/C++ compiler toolchain (e.g., GCC, Clang, MSVC) and potentially additional libraries like OpenMP. Build failures often stem from missing development tools, incorrect environment variables (e.g., `CC`, `CXX`, `LDFLAGS`), or incompatible compiler versions.
- gotcha The library supports configuration via `[tool.extension-helpers]` in `pyproject.toml` (since v1.1.0) or by directly calling its functions within a `setup.py`. Mixing these approaches or misunderstanding their precedence can lead to unexpected build behavior, where extensions are not discovered or configured correctly.
Install
-
pip install extension-helpers
Imports
- get_extensions
from extension_helpers import get_extensions
- add_extension_helpers_build_options
from extension_helpers import add_extension_helpers_build_options
Quickstart
import os
import shutil
from setuptools import Extension # setuptools must be installed
from extension_helpers import add_extension_helpers_build_options
# Ensure 'src' directory exists for the dummy C file
os.makedirs("src", exist_ok=True)
# For demonstration, create a dummy C file
dummy_c_file_path = os.path.join("src", "_dummy_ext.c")
dummy_c_file_content = """
#include <Python.h>
static PyObject *
dummy_func(PyObject *self, PyObject *args)
{
Py_RETURN_NONE;
}
static PyMethodDef DummyMethods[] = {
{"dummy_func", dummy_func, METH_NOARGS, "A dummy function."},
{NULL, NULL, 0, NULL}
};
static struct PyModuleDef dummymodule = {
PyModuleDef_HEAD_INIT,
"_dummy_ext", /* name of module */
NULL, /* module documentation, may be NULL */
-1, /* size of per-interpreter state of the module,
or -1 if the module keeps state in global variables. */
DummyMethods
};
PyMODINIT_FUNC
PyInit__dummy_ext(void)
{
return PyModule_Create(&dummymodule);
}
"""
with open(dummy_c_file_path, "w") as f:
f.write(dummy_c_file_content)
# Define a single extension
ext = Extension(
'my_package._dummy_ext',
sources=[dummy_c_file_path],
include_dirs=[],
extra_compile_args=[],
extra_link_args=[]
)
print(f"Initial Extension: {ext.name}")
print(f" Sources: {ext.sources}")
print(f" Initial extra_compile_args: {ext.extra_compile_args}")
# Apply extension-helpers build options (e.g., for OpenMP, limited API)
# This modifies the 'ext' object in-place
add_extension_helpers_build_options(ext)
print(f"\nExtension after add_extension_helpers_build_options:")
print(f" Updated extra_compile_args: {ext.extra_compile_args}")
print(f" Updated extra_link_args: {ext.extra_link_args}")
# Demonstrate the Limited API env var effect
os.environ['EXTENSION_HELPERS_PY_LIMITED_API'] = 'true'
print("\n--- Rerunning with EXTENSION_HELPERS_PY_LIMITED_API=true ---")
ext_limited = Extension(
'my_package._dummy_ext_limited',
sources=[dummy_c_file_path],
include_dirs=[],
extra_compile_args=[],
extra_link_args=[]
)
add_extension_helpers_build_options(ext_limited)
print(f"Extension (Limited API enabled): {ext_limited.name}")
print(f" Updated extra_compile_args (Limited API): {ext_limited.extra_compile_args}")
print(f" Updated extra_link_args (Limited API): {ext_limited.extra_link_args}")
del os.environ['EXTENSION_HELPERS_PY_LIMITED_API']
# Clean up the dummy C file and directory
if os.path.exists(dummy_c_file_path):
os.remove(dummy_c_file_path)
if os.path.exists("src") and not os.listdir("src"):
os.rmdir("src")
elif os.path.exists("src"):
print("\nNote: 'src' directory not removed as it contains other files.")