Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 52 additions & 1 deletion .github/workflows/buildwheel.yml
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,57 @@ jobs:
- run: spin run -- pytest --doctest-glob='*.rst' doc/source
- run: spin docs

test_cython_c_api:
name: 'Test C API with Cython consumers'
runs-on: ubuntu-26.04
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.13'
- run: sudo apt-get update
- run: sudo apt-get install libflint-dev
- run: pip install "cython==3.3.0" meson-python ninja
- run: pip wheel --no-build-isolation --no-deps --wheel-dir=/tmp/python-flint-wheel .
- run: pip install /tmp/python-flint-wheel/python_flint-*.whl
- name: Build examples with Cython 3.1 through 3.3
run: |
for version in 3.1.0 3.2.9 3.3.0; do
pip install "cython==$version"
pip install --no-build-isolation --no-deps --force-reinstall ./examples/cython/capi
pip install --no-build-isolation --no-deps --force-reinstall ./examples/cython/flint
(cd /tmp && python "$GITHUB_WORKSPACE/examples/cython/capi/test_capi.py")
(cd /tmp && python "$GITHUB_WORKSPACE/examples/cython/flint/test_flint.py")
done
- name: Examples have the expected FLINT dependencies
run: |
python - <<'PY'
import _flint_capi_example
import _flint_direct_example
import subprocess
capi_dependencies = subprocess.check_output(
["ldd", _flint_capi_example.__file__], text=True)
direct_dependencies = subprocess.check_output(
["ldd", _flint_direct_example.__file__], text=True)
print(capi_dependencies)
print(direct_dependencies)
assert "libflint" not in capi_dependencies
assert "libflint" in direct_dependencies
PY
- name: Installed API files are present
run: |
cd /tmp
python - <<'PY'
import flint
from pathlib import Path
package = Path(flint.__file__).parent
assert (package / "__init__.pxd").is_file()
assert (package / "__init__.cython-31.pxd").is_file()
assert (Path(flint.get_include()) / "python_flint" / "fmpz.h").is_file()
PY

# Test build with minimum Cython and meson-python versions.
test_old_build_requires:
name: 'Test old Cython/meson-python'
Expand All @@ -349,7 +400,7 @@ jobs:
# We don't need to specify ninja as a requirement in pyproject.toml
# because without --no-build-isolation meson-python handles it
# automatically in get_requirements_for_build_wheel().
- run: 'pip install "cython==3.0.11" "meson-python==0.18" "ninja<1.11"'
- run: 'pip install "cython==3.1.0" "meson-python==0.18" "ninja<1.11"'
- run: pip install --no-build-isolation .
- run: python -m flint.test --verbose

Expand Down
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,9 +150,8 @@ Compatible versions:
| `0.7.0` | 16th Mar 2025 | `3.11-3.13` | `3.0-3.2` | `3.0.11-3.1.0a1` |
| `0.6.0` | 1st Feb 2024 | `3.9-3.12` | `3.0` only | `3.0` only |

The requirement for Cython 3.1 is only for CPython's free-threaded build.
Otherwise Cython 3.0 is fine. Cython 3.2 is required for a stable ABI build of
python-flint.
Cython 3.1 or newer is required. Cython 3.2 is required for a stable ABI build
of python-flint.

As of python-flint 0.7.0, CPython 3.13 [PEP
703](https://peps.python.org/pep-0703/) free-threaded (no-GIL) builds of
Expand Down
2 changes: 1 addition & 1 deletion doc/source/build.rst
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ active Python environment before running ``pip install``.
To build without build isolation with ``python-flint >= 0.7.0`` the
dependencies should first be installed in the active Python environment::

pip install Cython==3.0 meson meson-python ninja
pip install Cython==3.1 meson meson-python ninja
pip install --no-build-isolation .

The ``meson`` build system will detect the versions of ``FLINT`` and Cython
Expand Down
49 changes: 49 additions & 0 deletions doc/source/c_api.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
Experimental C API
==================

python-flint provides an experimental version 1 C API for ``fmpz``. The API
may change between releases and is not yet a long-term ABI stability promise.

Cython consumers
----------------

Cython 3.1 or newer can use the installed declarations directly::

from flint cimport fmpz, fmpz_add, fmpz_get_value

``fmpz_add(a, b)`` returns a new ``fmpz`` and operates entirely through the
python-flint capsule. Extensions using only this operation do not need FLINT
headers or a direct FLINT link. See ``examples/cython/capi``.

``fmpz_get_value(value)`` returns a read-only, borrowed pointer to FLINT
storage. It remains valid only while ``value`` is alive. Never clear or mutate
it, retain it independently of its owner, or use it after the owner is
released. Direct consumers must compile and link against the same compatible
FLINT ABI/build as python-flint. In particular, this is not safe with wheels
containing a private, renamed FLINT. See ``examples/cython/flint``.

C consumers
-----------

Include the installed header and import the capsule before using it::

#include <python_flint/fmpz.h>

if (PyFlint_Import() < 0)
return NULL;
result = PyFlint_FMPZ_Add(left, right); /* new reference */

``PyFlint_FMPZ_GetValue`` has the same borrowed, read-only ownership rules as
the Cython wrapper. Both operations return ``NULL`` with a Python exception set
on failure, and reject objects that are not ``flint.fmpz`` instances.

Header location
---------------

Build systems can locate ``python_flint/fmpz.h`` using::

import flint
print(flint.get_include())

The public header and Cython declarations contain no details of python-flint's
private extension-object layout.
1 change: 1 addition & 0 deletions doc/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Introduction
:maxdepth: 2

install.rst
c_api.rst
general.rst
build.rst
workflow.rst
Expand Down
9 changes: 9 additions & 0 deletions examples/cython/capi/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Capsule-only Cython example

This extension uses `fmpz_add` through python-flint's capsule API. It neither
includes FLINT headers nor links to FLINT.

```console
python -m pip install .
python test_capi.py
```
6 changes: 6 additions & 0 deletions examples/cython/capi/_flint_capi_example.pyx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from flint cimport fmpz, fmpz_add


def add(fmpz a, fmpz b):
"""Add through python-flint's capsule; no FLINT linkage is needed."""
return fmpz_add(a, b)
14 changes: 14 additions & 0 deletions examples/cython/capi/meson.build
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
project('python-flint-capi-example', 'cython', 'c', meson_version: '>=1.3')
py = import('python').find_installation(pure: false)
flint_include = run_command(
py, '-c', 'import flint; print(flint.get_include())', check: true,
).stdout().strip()
py.extension_module(
'_flint_capi_example', '_flint_capi_example.pyx',
include_directories: include_directories(flint_include),
install: true,
)
test_env = environment()
test_env.prepend('PYTHONPATH', meson.current_build_dir())
test('capsule API example', py, args: files('test_capi.py'),
env: test_env)
8 changes: 8 additions & 0 deletions examples/cython/capi/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[build-system]
requires = ["meson-python", "cython>=3.1", "python-flint"]
build-backend = "mesonpy"

[project]
name = "python-flint-capi-example"
version = "0.1.0"
dependencies = ["python-flint"]
12 changes: 12 additions & 0 deletions examples/cython/capi/test_capi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from flint import fmpz
from _flint_capi_example import add

a = fmpz(2) ** 300
b = fmpz(3) ** 200
assert add(a, b) == a + b
try:
add(1, b)
except TypeError:
pass
else:
raise AssertionError("wrong argument type did not raise TypeError")
10 changes: 10 additions & 0 deletions examples/cython/flint/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Direct-FLINT Cython example

This extension borrows python-flint's `fmpz` storage and calls the read-only
FLINT function `fmpz_bits`. A compatible system FLINT is an explicit build and
runtime dependency.

```console
python -m pip install .
python test_flint.py
```
10 changes: 10 additions & 0 deletions examples/cython/flint/_flint_direct_example.pyx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from flint cimport fmpz, fmpz_get_value

cdef extern from "flint/fmpz.h":
ctypedef long flint_fmpz "fmpz"
unsigned long fmpz_bits(const flint_fmpz *)


def bit_length(fmpz value):
"""Call a read-only FLINT operation on borrowed fmpz storage."""
return fmpz_bits(<const flint_fmpz *>fmpz_get_value(value))
14 changes: 14 additions & 0 deletions examples/cython/flint/meson.build
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
project('python-flint-direct-example', 'cython', 'c', meson_version: '>=1.3')
py = import('python').find_installation(pure: false)
flint_dep = dependency('flint')
flint_include = run_command(
py, '-c', 'import flint; print(flint.get_include())', check: true,
).stdout().strip()
py.extension_module('_flint_direct_example', '_flint_direct_example.pyx',
dependencies: flint_dep,
include_directories: include_directories(flint_include),
install: true)
test_env = environment()
test_env.prepend('PYTHONPATH', meson.current_build_dir())
test('direct FLINT example', py, args: files('test_flint.py'),
env: test_env)
8 changes: 8 additions & 0 deletions examples/cython/flint/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[build-system]
requires = ["meson-python", "cython>=3.1", "python-flint"]
build-backend = "mesonpy"

[project]
name = "python-flint-direct-example"
version = "0.1.0"
dependencies = ["python-flint"]
11 changes: 11 additions & 0 deletions examples/cython/flint/test_flint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from flint import fmpz
from _flint_direct_example import bit_length

value = fmpz(2) ** 300 + 1
assert bit_length(value) == 301
try:
bit_length(1)
except TypeError:
pass
else:
raise AssertionError("wrong argument type did not raise TypeError")
2 changes: 1 addition & 1 deletion meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ project(
#
flint_lower = '>=3.0'
flint_upper = '<3.7'
cython_lower = '>=3.0.11'
cython_lower = '>=3.1'
cython_upper = '<3.4'

py = import('python').find_installation(pure: false)
Expand Down
4 changes: 0 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,6 @@ content-type = "text/markdown"
# Cython might still work but typically a Cython release breaks something in
# the build of python-flint so we include an upper bound here.
#
# Apart from the freethreading build any Cython version from 3.0.11 onwards is
# fine. It is not possible to have a separate version constraint here for the
# freethreading build only though.
#
requires = ["meson-python >= 0.18", "cython >=3.1,<3.4"]
build-backend = "mesonpy"

Expand Down
10 changes: 9 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@
os.environ['OPT'] = " ".join(flag for flag in opt.split() if flag != '-Wstrict-prototypes')


# Public C API headers live below src so that the same include spelling works
# both in source builds and after installation.
default_include_dirs.append(os.path.join(os.path.dirname(__file__), 'src'))


define_macros = []
compiler_directives = {
'language_level': 3,
Expand Down Expand Up @@ -135,7 +140,10 @@

ext_modules = []
for mod_name, src_files in ext_files:
ext = Extension(mod_name, src_files, **ext_options)
mod_options = ext_options.copy()
if mod_name == "flint.types.fmpz":
mod_options["define_macros"] = define_macros + [("PYFLINT_FMPZ_MODULE", 1)]
ext = Extension(mod_name, src_files, **mod_options)
ext_modules.append(ext)

for e in ext_modules:
Expand Down
20 changes: 20 additions & 0 deletions src/flint/__init__.cython-31.pxd
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from cpython.ref cimport PyObject, Py_DECREF

cdef extern from "python_flint/fmpz.h":
int PyFlint_Import() except -1
PyObject *PyFlint_FMPZ_Add(PyObject *, PyObject *) except NULL
const void *PyFlint_FMPZ_GetValue(PyObject *) except NULL

cdef extern class flint.types.fmpz.fmpz [object PyFlint_FMPZ_Object, check_size ignore]:
pass

cdef inline fmpz fmpz_add(fmpz a, fmpz b):
cdef PyObject *result_ptr = PyFlint_FMPZ_Add(<PyObject *>a, <PyObject *>b)
cdef fmpz result = <fmpz>result_ptr
# Assigning the pointer to an extension-type variable adds a reference.
# Consume the new reference returned by the C API before returning it.
Py_DECREF(<object>result_ptr)
return result

cdef inline const void *fmpz_get_value(fmpz value) except NULL:
return PyFlint_FMPZ_GetValue(<PyObject *>value)
4 changes: 4 additions & 0 deletions src/flint/__init__.pxd
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
cdef extern from *:
"""
#error "python-flint's Cython API requires Cython 3.1 or newer"
"""
8 changes: 8 additions & 0 deletions src/flint/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from os.path import dirname, join

from .pyflint import ctx

from .types.fmpz import fmpz
Expand Down Expand Up @@ -51,6 +53,11 @@
__version__ = "0.9.0"


def get_include():
"""Return the directory containing python-flint's public C headers."""
return join(dirname(__file__), "include")


__all__ = [
"ctx",
"fmpz",
Expand Down Expand Up @@ -105,4 +112,5 @@
"__FLINT_VERSION__",
"__FLINT_RELEASE__",
"__version__",
"get_include",
]
8 changes: 8 additions & 0 deletions src/flint/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ thisdir = 'flint'

pyfiles = [
'__init__.py',
'__init__.pxd',
'__init__.cython-31.pxd',
'typing.py',
'py.typed',
'pyflint.pyi',
Expand All @@ -25,6 +27,12 @@ py.install_sources(
subdir: thisdir,
)

py.install_sources(
'../python_flint/fmpz.h',
pure: false,
subdir: 'flint/include/python_flint',
)

foreach ext : exts
py.extension_module(
ext,
Expand Down
Loading
Loading