Skip to content

Commit

Permalink
extensions: add function to create Extensions from pyproject.toml
Browse files Browse the repository at this point in the history
The function is named create_extensions()

Integration tests are updated
  • Loading branch information
lbertho-gpsw committed Nov 26, 2024
1 parent 41f59d9 commit 60dbe4d
Show file tree
Hide file tree
Showing 8 changed files with 168 additions and 7 deletions.
5 changes: 3 additions & 2 deletions cython_setuptools/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
from ._version import __version__
from .vendor import setup
from ._version import __version__ # noqa
from .extentions import create_extensions # noqa
from .vendor import setup # noqa
2 changes: 0 additions & 2 deletions cython_setuptools/common.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import os.path as op

# FIXME
# distutils is deprecated starting from python3.10
# but the migration to setuptools is not completed
Expand Down
98 changes: 98 additions & 0 deletions cython_setuptools/extentions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import os
from pathlib import Path

import Cython
import Cython.Build
import Cython.Distutils
# Distutils is deprecated but for the moment this is the only way the default compiler is exposed when using setuptools
from setuptools._distutils.ccompiler import get_default_compiler

from .pyproject import CythonSetuptoolsOptions, read_cython_setuptools_option
from .pkgconfig_wrapper import get_flags
from .common import C_EXT, CPP_EXT, CYTHON_EXT, convert_to_bool, get_cpp_std_flag


def create_extensions(original_setup_file: str, cythonize: bool = True) -> list:
"""
Create a list of extentions to be used as argument ``ext_modules`` of ``setuptools.setup()`` by reading the ``pyproject.toml``
To compile pyx into .c/.cpp set ``CYTHONIZE`` env variable to True or if it is not set use the cythonize of this function
To get debug symboles ``DEBUG`` env variable (does not work with msvc)
To enable profiling use ``PROFILE_CYTHON`` env variable
Example of a what can be added to a ``pyproject.toml`` to have an extension named ``lol``:
```
[cython_extensions.lol]
# The list of Cython and C/C++ source files that are compiled to build the module.
sources = ["a.pyx"]
# A list of libraries to link with the module.
libraries = ["a", "b"]
# A list of directories to find include files.
include_dirs = ["toto/include"]
# A list of directories to find libraries.
library_dirs = ["toto/lib", "/usr/lib"]
# Extra arguments passed to the compiler.
extra_compile_args = ["-g"]
# Extra arguments passed to the linker.
extra_link_args = ["--strip-debug"]
# Typically "c" or "c++".
langage = "c++"
# Typically "11", "14", "17" or "20".
cpp_std = 23
# A list of `pkg-config` package names to link with the module.
pkg_config_packages = ["super_lib"]
# A list of directories to add to the pkg-config search paths (extends the `PKG_CONFIG_PATH` environment variable).
pkg_config_dirs = ["toto/lib/pkgconfig"]
```
Args:
original_setup_file:
Location of the ``setup.py`` calling this file.
It is used to retrieve the location of the ``pyproject.toml`` (that should be in the same directory)
It is recommanded to just use ``__file__``
cythonize:
If True ``Cython.Build.cythonize`` will be called
It is overrided by the env variable ``CYTHONIZE``
Returns:
A list Extentions, It can be safely used for ``ext_modules`` argument of ``setuptools.setup()``
"""
extensions_options = read_cython_setuptools_option(Path(original_setup_file).parent / "pyproject.toml")
extensions = []
cythonize = convert_to_bool(os.environ.get("CYTHONIZE", cythonize))
profile_cython = convert_to_bool(os.environ.get("PROFILE_CYTHON", False))
debug = convert_to_bool(os.environ.get("DEBUG", False))
for name, options in extensions_options.items():
_complete_cython_options(options, debug, cythonize)
extensions.append(_create_extension(name, options, profile_cython))
if cythonize:
extensions = Cython.Build.cythonize(extensions, force=True)
return extensions


def _complete_cython_options(options: CythonSetuptoolsOptions, debug: bool, cythonize: bool):
if debug and get_default_compiler() != "msvc":
options.extra_compile_args.append("-g")
if options.langage == "c++":
options.extra_compile_args.append(get_cpp_std_flag(options.cpp_std))

# Get flags from pkg-config dependencies
build_flags = get_flags(options.pkg_config_packages, options.pkg_config_dirs)
options.extra_compile_args += build_flags.compile_flags
options.extra_link_args += build_flags.link_flags

# Force to use already generated .c/.cpp files if cythonize is False
if not cythonize:
new_ext = CPP_EXT if options.langage == "c++" else C_EXT
new_sources = []
for source in options.sources:
source_path = Path(source)
if source_path.suffix == CYTHON_EXT:
source = str(source_path.with_suffix(new_ext))
new_sources.append(source)
options.sources = new_sources


def _create_extension(name: str, options: CythonSetuptoolsOptions, profile_cython: bool) -> Cython.Distutils.Extension:
cython_directives = {"profile": True} if profile_cython else {}
return Cython.Distutils.Extension(name=name, cython_directives=cython_directives, **options.to_extension_kwargs())
18 changes: 18 additions & 0 deletions cython_setuptools/pyproject.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"""

import os
from typing import Any

from serde import field, serde
from serde.toml import from_toml
Expand Down Expand Up @@ -38,6 +39,23 @@ class CythonSetuptoolsOptions:
pkg_config_packages: list[str] = field(default_factory=list)
pkg_config_dirs: list[str] = field(default_factory=list)

def to_extension_kwargs(self) -> dict[str, Any]:
"""
Helper function to return a dict that can be used to create an Extension
Note that before using this function, fields like ``cpp_std`` or ``pkg_config_packages``
should be used to fill extra elements in ``extra_compile_args`` and ``extra_link_args``
Returns:
A dict with the fields ``sources``, ``libraries``, ``include_dirs``, ``extra_compile_args``, ``extra_link_args``
"""
return {
"sources": self.sources,
"libraries": self.libraries,
"include_dirs": self.include_dirs,
"extra_compile_args": self.extra_compile_args,
"extra_link_args": self.extra_link_args,
}


@serde
class _PyProject:
Expand Down
22 changes: 22 additions & 0 deletions tests/pypkg/pyproject-mypkg.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[build-system]
requires = ["setuptools", "wheel", "cython"]
build-backend = "setuptools.build_meta"

[project]
name = "pypkg"
version = "1.0.0"
description = "pypkg is the tool to pypkg some pypkg using pypkg to use theses pypkg in pypkg"
authors = []
maintainers = []
dependencies = []


[option]
packages = "find:"
include_package_data = "true"
zip_safe = "false"

# The part that we need in our tests
[cython_extensions.foo]
sources = ["foo.pyx", "../src/foo.c"]
include_dirs = ["../src"]
4 changes: 4 additions & 0 deletions tests/pypkg/setup-cythonize-pyproject.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from setuptools import setup
from cython_setuptools import create_extensions

setup(ext_modules=create_extensions(__file__, cythonize=True))
4 changes: 4 additions & 0 deletions tests/pypkg/setup-no-cythonize-pyproject.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from setuptools import setup
from cython_setuptools import create_extensions

setup(ext_modules=create_extensions(__file__, cythonize=False))
22 changes: 19 additions & 3 deletions tests/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,41 @@
cython_setuptools_path = Path(cython_setuptools.__file__, "..", "..").absolute()


def _setup_source(setup_name: str, tmp_path: Path):
def _setup_source(setup_name: str, pyproject_name: str | None, tmp_path: Path):
pypkg_dir = tmp_path / 'pypkg'
src_dir = tmp_path / 'src'
setup_path = pypkg_dir / 'setup.py'
shutil.copytree(this_dir / 'pypkg', pypkg_dir)
shutil.copytree(this_dir / 'src', src_dir)
os.symlink(pypkg_dir / setup_name, setup_path)
if pyproject_name:
os.symlink(pypkg_dir / pyproject_name, pypkg_dir / "pyproject.toml")
return setup_path, pypkg_dir


def test_compile_and_run_no_cythonize_mode(virtualenv, tmp_path: Path):
setup_path, pypkg_dir = _setup_source("setup-no-cythonize.py", tmp_path)
setup_path, pypkg_dir = _setup_source("setup-no-cythonize.py", None, tmp_path)
virtualenv.run(f"pip install {cython_setuptools_path}")
virtualenv.run(f"pip install -e {pypkg_dir}")
assert int(virtualenv.run("python -m bar", capture=True)) == 2


def test_compile_and_run_cythonize_mode(virtualenv, tmp_path: Path):
setup_path, pypkg_dir = _setup_source("setup-cythonize.py", tmp_path)
setup_path, pypkg_dir = _setup_source("setup-cythonize.py", None, tmp_path)
virtualenv.run(f"pip install {cython_setuptools_path}")
virtualenv.run(f"pip install -e {pypkg_dir}")
assert int(virtualenv.run("python -m bar", capture=True)) == 2


def test_compile_and_run_cythonize_mode_pyproject(virtualenv, tmp_path: Path):
setup_path, pypkg_dir = _setup_source("setup-cythonize-pyproject.py", "pyproject-mypkg.toml", tmp_path)
virtualenv.run(f"pip install {cython_setuptools_path}")
virtualenv.run(f"pip install -e {pypkg_dir} --no-build-isolation")
assert int(virtualenv.run("python -m bar", capture=True)) == 2


def test_compile_and_run_no_cythonize_mode_pyproject(virtualenv, tmp_path: Path):
setup_path, pypkg_dir = _setup_source("setup-no-cythonize-pyproject.py", "pyproject-mypkg.toml", tmp_path)
virtualenv.run(f"pip install {cython_setuptools_path}")
virtualenv.run(f"pip install -e {pypkg_dir} --no-build-isolation")
assert int(virtualenv.run("python -m bar", capture=True)) == 2

0 comments on commit 60dbe4d

Please sign in to comment.