From a2cc30d1a0f242a2d1ccf24fce1219dedbaa16b5 Mon Sep 17 00:00:00 2001 From: Lisandro Dalcin Date: Thu, 4 Dec 2025 21:03:56 +0300 Subject: [PATCH 1/3] python: Modernize packaging using sckit-build-core --- .github/workflows/test_python.yml | 27 +++++--- .github/workflows/wheels.yml | 85 ++++++++++++------------- .gitignore | 3 +- CMakeLists.txt | 2 +- interface/python/.gitignore | 1 + interface/python/CMakeLists.txt | 16 ++--- interface/python/DESCRIPTION.md | 3 + interface/python/mutationpp/__init__.py | 2 + pyproject.toml | 47 +++++++++++++- setup.py | 56 ---------------- 10 files changed, 114 insertions(+), 128 deletions(-) create mode 100644 interface/python/DESCRIPTION.md delete mode 100644 setup.py diff --git a/.github/workflows/test_python.yml b/.github/workflows/test_python.yml index 9a2b10d3..a970397f 100644 --- a/.github/workflows/test_python.yml +++ b/.github/workflows/test_python.yml @@ -1,23 +1,30 @@ -name: Test Python +name: Test Python on: [push, pull_request] jobs: - test_linux: - runs-on: ubuntu-latest + test: + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + runner: + - ubuntu-latest + - macos-latest + python-version: + - "3.10" + - "3.x" steps: - - uses: actions/checkout@v2 + + - uses: actions/checkout@v6 with: submodules: recursive - - uses: actions/setup-python@v2 + - uses: actions/setup-python@v6 with: - python-version: "3.x" - - - name: Upgrade pip - run: pip install -U setuptools wheel pip + python-version: ${{ matrix.python-version }} - name: Install package for testing - run: pip install ".[test]" + run: python -m pip install ".[test]" - name: Run tests run: > diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index e7ecda1b..ae1d3b34 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -8,19 +8,16 @@ jobs: test_linux: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v6 with: submodules: recursive - - uses: actions/setup-python@v2 + - uses: actions/setup-python@v6 with: python-version: "3.x" - - name: Upgrade pip - run: pip install -U setuptools wheel pip - - name: Install package for testing - run: pip install ".[test]" + run: python -m pip install ".[test]" - name: Run tests run: > @@ -31,19 +28,16 @@ jobs: test_mac: runs-on: macos-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v6 with: submodules: recursive - - uses: actions/setup-python@v2 + - uses: actions/setup-python@v6 with: python-version: "3.x" - - name: Upgrade pip - run: pip install -U setuptools wheel pip - - name: Install package for testing - run: pip install ".[test]" + run: python -m pip install ".[test]" - name: Run tests run: > @@ -54,30 +48,28 @@ jobs: linux_wheels: strategy: matrix: - python-version: - - cp36-cp36m - - cp37-cp37m - - cp38-cp38 - - cp39-cp39 + python-version: + - cp310-cp310 + - cp311-cp311 + - cp312-cp312 + - cp313-cp313 + - cp314-cp314 needs: test_linux runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v6 with: submodules: recursive - - uses: actions/setup-python@v2 + - uses: actions/setup-python@v6 with: - python-version: 3.9 - - - name: Upgrade pip - run: pip install -U setuptools wheel pip + python-version: 3.14 - name: Build manylinux Python wheels uses: RalfG/python-wheels-manylinux-build@v0.3.4-manylinux2010_x86_64 with: - python-versions: "${{ matrix.python-version }}" + python-versions: ${{ matrix.python-version }} - uses: actions/upload-artifact@v2 with: @@ -88,27 +80,28 @@ jobs: mac_wheels: strategy: matrix: - python-version: - - 3.6 - - 3.7 - - 3.8 - - 3.9 + python-version: + - "3.10" + - "3.11" + - "3.12" + - "3.13" + - "3.14" needs: test_mac runs-on: macos-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v6 with: submodules: recursive - - uses: actions/setup-python@v2 + - uses: actions/setup-python@v6 with: - python-version: "${{ matrix.python-version }}" + python-version: ${{ matrix.python-version }} - - name: Upgrade pip - run: pip install -U setuptools wheel pip + - name: Install build + run: python -m pip install build - - name: Build wheels - run: pip wheel . -w dist/ + - name: Build wheel + run: python -m build --whell - uses: actions/upload-artifact@v2 with: @@ -120,19 +113,19 @@ jobs: needs: test_linux runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v6 with: submodules: recursive - - uses: actions/setup-python@v2 + - uses: actions/setup-python@v6 with: - python-version: 3.9 + python-version: "3.x" - - name: Upgrade pip - run: pip install -U setuptools wheel pip scikit-build + - name: Install build + run: python -m pip install build - name: Build sdist - run: python setup.py sdist + run: python -m build --sdist - uses: actions/upload-artifact@v2 with: @@ -159,12 +152,12 @@ jobs: name: sdist path: dist - - name: Upgrade pip - run: pip install -U setuptools wheel pip + - uses: actions/setup-python@v6 + with: + python-version: "3.x" - name: Install twine - run: pip install twine + run: python -m pip install twine - name: Upload wheels run: twine upload -u __token__ -p "${{ secrets.TESTPYPI_TOKEN }}" --repository testpypi dist/* - diff --git a/.gitignore b/.gitignore index 7e372b97..30f732ee 100644 --- a/.gitignore +++ b/.gitignore @@ -15,8 +15,9 @@ docs/html/ .vscode/ # Python +dist/ +**/__pycache__/ *.pyc -_skbuild/ *.whl # OS specifics diff --git a/CMakeLists.txt b/CMakeLists.txt index e7c9bc32..d4327cc7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -47,7 +47,7 @@ if (BUILD_FORTRAN_WRAPPER) endif() ####################################################################### -# Python bindings built with pip + scikit-build # +# Python bindings built with scikit-build-core + pybind11 # ####################################################################### if (SKBUILD) diff --git a/interface/python/.gitignore b/interface/python/.gitignore index c6facb83..3eb8b6e0 100644 --- a/interface/python/.gitignore +++ b/interface/python/.gitignore @@ -1,6 +1,7 @@ ###Build### build/ dist/ +**/__pycache__/ mutationpp.egg-info/ *.so diff --git a/interface/python/CMakeLists.txt b/interface/python/CMakeLists.txt index 595b53ac..ccc0e787 100644 --- a/interface/python/CMakeLists.txt +++ b/interface/python/CMakeLists.txt @@ -1,14 +1,4 @@ -if (CMAKE_VERSION VERSION_LESS 3.18) - set(PY_DEV_MODULE Development) -else() - set(PY_DEV_MODULE Development.Module) -endif() - -find_package(Python COMPONENTS Interpreter ${PY_DEV_MODULE} REQUIRED) - -execute_process( - COMMAND "${Python_EXECUTABLE}" -m pybind11 --cmakedir - OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_VARIABLE pybind11_ROOT) +find_package(Python COMPONENTS Interpreter Development.Module REQUIRED) find_package(pybind11 CONFIG REQUIRED) @@ -20,4 +10,6 @@ pybind11_add_module(_mutationpp target_link_libraries(_mutationpp PRIVATE mutation++) -install(TARGETS _mutationpp DESTINATION .) +install(DIRECTORY mutationpp DESTINATION .) + +install(TARGETS _mutationpp DESTINATION mutationpp) diff --git a/interface/python/DESCRIPTION.md b/interface/python/DESCRIPTION.md new file mode 100644 index 00000000..33249638 --- /dev/null +++ b/interface/python/DESCRIPTION.md @@ -0,0 +1,3 @@ +Mutation++ + +*Mutation++* is an open-source library providing thermodynamic, transport, chemistry, and energy transfer properties associated with subsonic to hypersonic flows. diff --git a/interface/python/mutationpp/__init__.py b/interface/python/mutationpp/__init__.py index 2db0c2d5..63130b12 100644 --- a/interface/python/mutationpp/__init__.py +++ b/interface/python/mutationpp/__init__.py @@ -1 +1,3 @@ +"""Mutation++ Python bindings""" + from ._mutationpp import * diff --git a/pyproject.toml b/pyproject.toml index 5df5edcd..9f0141e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,49 @@ +[project] +name = "mutationpp" +dynamic = ["version"] +description = "MUlticomponent Thermodynamic And Transport properties for IONized gases in C++" +readme = "interface/python/DESCRIPTION.md" +license = "LGPL-3.0-only" +license-files = ["COPYING", "COPYING.LESSER"] +authors = [ + {name = "James B. Scoggins", email = "james.scoggins@vki.ac.be"}, +] +maintainers = [ + {name = "Ruben Di Battista", email = "rubendibattista@gmail.com"}, +] +classifiers = [ + "Development Status :: 6 - Mature", + "Intended Audience :: Science/Research", + "Operating System :: MacOS", + "Operating System :: POSIX :: Linux", + "Programming Language :: C++", + "Programming Language :: Python :: 3", + "Topic :: Scientific/Engineering :: Chemistry", + "Topic :: Software Development :: Libraries :: Python Modules", +] +requires-python = ">=3.10" +dependencies = ["numpy"] +[project.urls] +Homepage = "https://github.com/mutationpp/Mutationpp" +Source = "https://github.com/mutationpp/Mutationpp.git" +Issues = "https://github.com/mutationpp/Mutationpp/issues" +Discussions = "https://github.com/mutationpp/Mutationpp/discussions" +Downloads = "https://github.com/mutationpp/Mutationpp/releases" +[project.optional-dependencies] +test = ["pytest"] + [build-system] -requires = ["setuptools", "wheel", "pybind11", "scikit-build", "cmake", "ninja"] -build-backend = "setuptools.build_meta" +requires = ["scikit-build-core>=0.11", "pybind11"] +build-backend = "scikit_build_core.build" + +[tool.scikit-build] +experimental = true +sdist.cmake = false +sdist.exclude = [".*"] +[tool.scikit-build.metadata.version] +provider = "scikit_build_core.metadata.regex" +regex = '^project\(mutation\+\+\s*\n^\s*VERSION\s*(?P\d+\.\d+\.\d+)\s*$' +input = "CMakeLists.txt" [tool.pytest.ini_options] testpaths = [ diff --git a/setup.py b/setup.py deleted file mode 100644 index 3197aef9..00000000 --- a/setup.py +++ /dev/null @@ -1,56 +0,0 @@ -import re -import sys - -from pathlib import Path - -try: - from skbuild import setup -except ImportError: - print( - "Please update pip, you need pip 10 or greater,\n" - " or you need to install the PEP 518 requirements in pyproject.toml yourself", - file=sys.stderr, - ) - raise - -ROOT_CMAKELISTS = "./CMakeLists.txt" - -DESCRIPTION = """An open-source library providing thermodynamic, transport,\ -chemistry, and energy transfer properties associated with subsonic to\ -hypersonic flows.""" - - -def get_version_from_cmake(root_cmakelists): - """A helper function to parse the root CMakeLists.txt and retrieve the - version from there in order to have a single point holding the version - info, easier to update""" - - pattern = re.compile(r" *VERSION *(\d+\.\d+\.\d+)") - - root_file = Path(root_cmakelists) - - with open(root_file, "r") as f: - for line in f: - match = pattern.search(line) - if match is not None: - return match.group(1) - - raise RuntimeError( - "Couldn't parse CMakeLists.txt file to find a version statement" - ) - - -setup( - name="mutationpp", - version=get_version_from_cmake(ROOT_CMAKELISTS), - description=DESCRIPTION, - long_description=DESCRIPTION, - author="James B. Scoggins", - license="LGPL3", - package_dir={"": "interface/python"}, - packages=["mutationpp"], - extras_require={ - "test": ["numpy", "pytest"], - }, - cmake_install_dir="interface/python/mutationpp", -) From 6bc38ff51bde188d48ccb61b41a7acc868562b30 Mon Sep 17 00:00:00 2001 From: Lisandro Dalcin Date: Fri, 5 Dec 2025 11:47:27 +0300 Subject: [PATCH 2/3] python: Add GlobalOptions wrapper --- interface/python/CMakeLists.txt | 1 + interface/python/src/mutationpp_python.cpp | 2 + interface/python/src/pyGlobalOptions.cpp | 36 ++++++++++++ src/general/GlobalOptions.h | 3 + tests/python/test_options.py | 67 ++++++++++++++++++++++ 5 files changed, 109 insertions(+) create mode 100644 interface/python/src/pyGlobalOptions.cpp create mode 100644 tests/python/test_options.py diff --git a/interface/python/CMakeLists.txt b/interface/python/CMakeLists.txt index ccc0e787..52a7011b 100644 --- a/interface/python/CMakeLists.txt +++ b/interface/python/CMakeLists.txt @@ -4,6 +4,7 @@ find_package(pybind11 CONFIG REQUIRED) pybind11_add_module(_mutationpp src/mutationpp_python.cpp + src/pyGlobalOptions.cpp src/pyMixtureOptions.cpp src/pyMixture.cpp ) diff --git a/interface/python/src/mutationpp_python.cpp b/interface/python/src/mutationpp_python.cpp index 05fba317..b1f76fc0 100644 --- a/interface/python/src/mutationpp_python.cpp +++ b/interface/python/src/mutationpp_python.cpp @@ -8,11 +8,13 @@ namespace py = pybind11; +void py_export_GlobalOptions(py::module &); void py_export_MixtureOptions(py::module &); void py_export_Mixture(py::module &); PYBIND11_MODULE(_mutationpp, m) { m.doc() = "Mutation++ Python bindings"; + py_export_GlobalOptions(m); py_export_MixtureOptions(m); py_export_Mixture(m); } diff --git a/interface/python/src/pyGlobalOptions.cpp b/interface/python/src/pyGlobalOptions.cpp new file mode 100644 index 00000000..2cd9fdac --- /dev/null +++ b/interface/python/src/pyGlobalOptions.cpp @@ -0,0 +1,36 @@ +#include +#include + +namespace py = pybind11; + +/** + * Python wrapper definition for the GlobalOptions class. + */ + +void py_export_GlobalOptions(py::module &m) { + + /** + * Overloaded member functions wrappers + */ + + /** + * Python class definition + */ + py::class_(m, "GlobalOptions") + .def_static("dataDirectory", [](const std::string& datadir) { + Mutation::GlobalOptions::dataDirectory(datadir); + }) + .def_static("dataDirectory", []() { + return Mutation::GlobalOptions::dataDirectory(); + }) + .def_static("workingDirectory", [](const std::string& workdir) { + Mutation::GlobalOptions::workingDirectory(workdir); + }) + .def_static("workingDirectory", []() { + return Mutation::GlobalOptions::workingDirectory(); + }) + .def_static("reset", []() { + Mutation::GlobalOptions::reset(); + }) + ; +} diff --git a/src/general/GlobalOptions.h b/src/general/GlobalOptions.h index 02f5fcc3..fb9f80be 100644 --- a/src/general/GlobalOptions.h +++ b/src/general/GlobalOptions.h @@ -22,6 +22,9 @@ #ifndef GLOBAL_OPTIONS_H #define GLOBAL_OPTIONS_H +#include +#include + namespace Mutation { /// Singleton class providing access to global options. diff --git a/tests/python/test_options.py b/tests/python/test_options.py new file mode 100644 index 00000000..474751ad --- /dev/null +++ b/tests/python/test_options.py @@ -0,0 +1,67 @@ +import importlib.resources +import os +import pytest +import unittest.mock as mock +import mutationpp as mpp + +INITIAL_DATADIR = os.environ.get("MPP_DATA_DIRECTORY", "") + + +def test_options_datadir_initial(): + datadir = mpp.GlobalOptions.dataDirectory() + assert datadir == INITIAL_DATADIR + + +def test_options_workdir_initial(): + workdir = mpp.GlobalOptions.workingDirectory() + assert workdir == "" + + +@pytest.fixture +def options(): + datadir = mpp.GlobalOptions.dataDirectory() + workdir = mpp.GlobalOptions.workingDirectory() + yield {"datadir": datadir, "workdir": workdir} + mpp.GlobalOptions.dataDirectory(datadir) + mpp.GlobalOptions.workingDirectory(workdir) + + +@pytest.fixture +def datadir(options): + return options["datadir"] + + +@pytest.fixture +def workdir(options): + return options["workdir"] + + +def test_options_datadir_getset(datadir): + new_datadir = os.path.join(datadir, "subdir") + mpp.GlobalOptions.dataDirectory(new_datadir) + set_datadir = mpp.GlobalOptions.dataDirectory() + assert set_datadir == new_datadir + + +def test_options_workdir_getset(workdir): + new_workdir = os.path.join(workdir, "subdir") + mpp.GlobalOptions.workingDirectory(new_workdir) + set_workdir = mpp.GlobalOptions.workingDirectory() + assert set_workdir == new_workdir + + +def test_options_reset(datadir): + new_datadir = os.path.join(datadir, "subdir") + environment = {"MPP_DATA_DIRECTORY": new_datadir} + with mock.patch.dict(os.environ, environment): + mpp.GlobalOptions.reset() + set_datadir = mpp.GlobalOptions.dataDirectory() + assert set_datadir == new_datadir + set_workdir = mpp.GlobalOptions.workingDirectory() + assert set_workdir == "" + with mock.patch.dict(os.environ, clear=True): + mpp.GlobalOptions.reset() + set_datadir = mpp.GlobalOptions.dataDirectory() + assert set_datadir == "" + set_workdir = mpp.GlobalOptions.workingDirectory() + assert set_workdir == "" From d53701e4d76ae32f51d6ef3f236782db71f84e3d Mon Sep 17 00:00:00 2001 From: Lisandro Dalcin Date: Fri, 5 Dec 2025 11:47:27 +0300 Subject: [PATCH 3/3] python: Install data files as package data --- .github/workflows/test_python.yml | 5 +---- .github/workflows/wheels.yml | 10 ++-------- interface/python/CMakeLists.txt | 2 ++ interface/python/src/pyGlobalOptions.cpp | 21 ++++++++++++++++++++- tests/python/test_options.py | 5 +++-- 5 files changed, 28 insertions(+), 15 deletions(-) diff --git a/.github/workflows/test_python.yml b/.github/workflows/test_python.yml index a970397f..a91fb553 100644 --- a/.github/workflows/test_python.yml +++ b/.github/workflows/test_python.yml @@ -27,7 +27,4 @@ jobs: run: python -m pip install ".[test]" - name: Run tests - run: > - export MPP_DIRECTORY=$(pwd) && - export MPP_DATA_DIRECTORY=$MPP_DIRECTORY/data && - pytest + run: pytest diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index ae1d3b34..0113e745 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -20,10 +20,7 @@ jobs: run: python -m pip install ".[test]" - name: Run tests - run: > - export MPP_DIRECTORY=$(pwd) && - export MPP_DATA_DIRECTORY=$MPP_DIRECTORY/data && - pytest + run: pytest test_mac: runs-on: macos-latest @@ -40,10 +37,7 @@ jobs: run: python -m pip install ".[test]" - name: Run tests - run: > - export MPP_DIRECTORY=$(pwd) && - export MPP_DATA_DIRECTORY=$MPP_DIRECTORY/data && - pytest + run: pytest linux_wheels: strategy: diff --git a/interface/python/CMakeLists.txt b/interface/python/CMakeLists.txt index 52a7011b..803aa71c 100644 --- a/interface/python/CMakeLists.txt +++ b/interface/python/CMakeLists.txt @@ -14,3 +14,5 @@ target_link_libraries(_mutationpp PRIVATE mutation++) install(DIRECTORY mutationpp DESTINATION .) install(TARGETS _mutationpp DESTINATION mutationpp) + +install(DIRECTORY "${CMAKE_SOURCE_DIR}/data" DESTINATION mutationpp) diff --git a/interface/python/src/pyGlobalOptions.cpp b/interface/python/src/pyGlobalOptions.cpp index 2cd9fdac..dfba9f14 100644 --- a/interface/python/src/pyGlobalOptions.cpp +++ b/interface/python/src/pyGlobalOptions.cpp @@ -7,8 +7,26 @@ namespace py = pybind11; * Python wrapper definition for the GlobalOptions class. */ +namespace { + + inline void setDefaultDataDirectory(const std::string& datadir) { + if (std::getenv("MPP_DATA_DIRECTORY") == nullptr) { + Mutation::GlobalOptions::dataDirectory(datadir); + } + }; + +} + void py_export_GlobalOptions(py::module &m) { + const py::object resources = py::module_::import("importlib.resources"); + const py::object pkgname = m.attr("__spec__").attr("parent"); + const py::object rootdir = resources.attr("files")(pkgname); + const py::object datadir = rootdir / py::str("data"); + const std::string data_directory = py::str(datadir).cast(); + + setDefaultDataDirectory(data_directory); + /** * Overloaded member functions wrappers */ @@ -29,8 +47,9 @@ void py_export_GlobalOptions(py::module &m) { .def_static("workingDirectory", []() { return Mutation::GlobalOptions::workingDirectory(); }) - .def_static("reset", []() { + .def_static("reset", [data_directory]() { Mutation::GlobalOptions::reset(); + setDefaultDataDirectory(data_directory); }) ; } diff --git a/tests/python/test_options.py b/tests/python/test_options.py index 474751ad..466f8648 100644 --- a/tests/python/test_options.py +++ b/tests/python/test_options.py @@ -4,7 +4,8 @@ import unittest.mock as mock import mutationpp as mpp -INITIAL_DATADIR = os.environ.get("MPP_DATA_DIRECTORY", "") +PACKAGE_DATADIR = os.fspath(importlib.resources.files(mpp) / "data") +INITIAL_DATADIR = os.environ.get("MPP_DATA_DIRECTORY", PACKAGE_DATADIR) def test_options_datadir_initial(): @@ -62,6 +63,6 @@ def test_options_reset(datadir): with mock.patch.dict(os.environ, clear=True): mpp.GlobalOptions.reset() set_datadir = mpp.GlobalOptions.dataDirectory() - assert set_datadir == "" + assert set_datadir == PACKAGE_DATADIR set_workdir = mpp.GlobalOptions.workingDirectory() assert set_workdir == ""