From 3efa0a4de786eab4b092a44b522e589afb8fd74e Mon Sep 17 00:00:00 2001 From: Niels Bassler Date: Sun, 29 Mar 2026 00:56:11 +0100 Subject: [PATCH 1/9] new python wrapper --- .github/workflows/ci.yml | 27 +++++++ .github/workflows/python.yml | 21 +++++ .gitignore | 5 ++ .vscode/extensions.json | 3 +- .vscode/settings.json | 8 ++ libdedx/CMakeLists.txt | 44 ++++++---- python/CMakeLists.txt | 65 --------------- python/README | 17 ---- python/libdedx.c | 141 --------------------------------- python/libdedx.py | 119 ++++++++++++++++++++++++++++ python/libdedx.pyi | 4 + python/pyproject.toml | 21 +++++ python/test.py | 4 - python/tests/conftest.py | 22 +++++ python/tests/test_libdedx.py | 44 ++++++++++ tests/install_smoke_test.cmake | 5 +- 16 files changed, 304 insertions(+), 246 deletions(-) create mode 100644 .github/workflows/python.yml delete mode 100644 python/CMakeLists.txt delete mode 100644 python/README delete mode 100644 python/libdedx.c create mode 100644 python/libdedx.py create mode 100644 python/libdedx.pyi create mode 100644 python/pyproject.toml delete mode 100644 python/test.py create mode 100644 python/tests/conftest.py create mode 100644 python/tests/test_libdedx.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6e8cfa7..57a6f4d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,3 +60,30 @@ jobs: run: | valgrind --leak-check=full --track-origins=yes --error-exitcode=1 \ ./build/tests/test_bethe_ext00 + + python_tests: + runs-on: ubuntu-latest + needs: build_and_test + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Configure + run: cmake -S . -B build -DBUILD_SHARED_LIBS=ON + + - name: Build + run: cmake --build build --parallel + + - name: Install pytest + run: python -m pip install pytest + + - name: Run Python tests + env: + LIBDEDX_SO: ${{ github.workspace }}/build/libdedx/libdedx.so + PYTHONPATH: ${{ github.workspace }}/python + run: pytest python/tests -q diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml new file mode 100644 index 0000000..48e66a1 --- /dev/null +++ b/.github/workflows/python.yml @@ -0,0 +1,21 @@ +name: Python + +on: [push, pull_request] + +jobs: + python: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Configure and build (shared library) + run: | + cmake -S . -B build + cmake --build build --parallel + + - name: Install Python binding and test dependencies + run: pip install --break-system-packages pytest python/ + + - name: Run tests + run: | + LIBDEDX_SO=build/libdedx/libdedx.so pytest python/tests/ -v diff --git a/.gitignore b/.gitignore index 6c67661..6760954 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,11 @@ build-release/ build-coverage/ docs/doxygen/html/ .codex +python/.venv/ +python/__pycache__/ +python/.pytest_cache/ +python/*.egg-info/ +*.pyc # Generated by CMake at configure time - do not commit libdedx/dedx_config.h diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 4c901c8..015d8a6 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,7 +1,6 @@ { "recommendations": [ - "llvm-vs-code-extensions.vscode-clangd", "ms-vscode.cmake-tools", - "twxs.cmake" + "ms-python.python" ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index fcd5de8..d9dd77f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -18,6 +18,14 @@ "editor.formatOnSave": true }, + // Python — add python/ to the analysis path so Pylance finds libdedx.py and libdedx.pyi + "python.defaultInterpreterPath": "${workspaceFolder}/python/.venv/bin/python", + "python.analysis.extraPaths": ["python"], + "python.testing.pytestEnabled": true, + "python.testing.unittestEnabled": false, + "python.testing.cwd": "${workspaceFolder}/python", + "python.testing.pytestArgs": ["tests"], + // Editor "editor.rulers": [120], "files.trimTrailingWhitespace": true, diff --git a/libdedx/CMakeLists.txt b/libdedx/CMakeLists.txt index dcab99f..53afa84 100644 --- a/libdedx/CMakeLists.txt +++ b/libdedx/CMakeLists.txt @@ -3,35 +3,47 @@ set(dedx_data data/ASTAR.bin data/astarEng.dat data/mstarEng.dat data/pstarEng.b set(dedx_data_bin data/ASTAR.bin data/astarEng.bin data/ICRU73.bin data/icru73Eng.bin data/ICRU73_NEW.bin data/icru73_newEng.bin data/MSTAR.bin data/mstarEng.bin data/PSTAR.bin data/ICRU_PSTAR.bin data/ICRU_ASTAR.bin data/pstarEng.bin data/betheEng.bin data/icru_astarEng.bin data/icru_pstarEng.bin data/compos.txt data/effective_charge.dat data/short_names data/composition data/gas_states.dat data/atima_compos) -add_library(dedx STATIC dedx_tools.c dedx_bethe.c dedx.c dedx_wrappers.c dedx_file_access.c dedx_file.c dedx_mpaul.c dedx_mstar.c dedx_spline.c dedx_split.c dedx_periodic_table.c tools/dedx_math.c dedx_validate.c) - -target_include_directories(dedx PUBLIC +# Object library: compiled once with -fPIC so the objects feed both the +# static and the shared library without a second compilation pass. +add_library(dedx_objects OBJECT + dedx_tools.c dedx_bethe.c dedx.c dedx_wrappers.c dedx_file_access.c + dedx_file.c dedx_mpaul.c dedx_mstar.c dedx_spline.c dedx_split.c + dedx_periodic_table.c tools/dedx_math.c dedx_validate.c +) +set_property(TARGET dedx_objects PROPERTY POSITION_INDEPENDENT_CODE ON) +target_include_directories(dedx_objects PUBLIC "${PROJECT_SOURCE_DIR}/libdedx" "${PROJECT_BINARY_DIR}" ) -# removed math_dedx.h -# removed dedx_atima.c - # Version passed as compile definitions, derived from git tag at configure time -target_compile_definitions(dedx PRIVATE +target_compile_definitions(dedx_objects PRIVATE DEDX_VERSION_MAJOR=${DEDX_VERSION_MAJOR} DEDX_VERSION_MINOR=${DEDX_VERSION_MINOR} DEDX_VERSION_PATCH=${DEDX_VERSION_PATCH} ) -target_link_libraries(dedx) - +# Static library — for C/CMake consumers; PUBLIC so include dirs propagate. +add_library(dedx STATIC) +target_link_libraries(dedx PUBLIC dedx_objects) # Visual Studio does not need or want you to explicitly request linking the math library. # You must avoid adding it as a link library when building for Windows. # see https://stackoverflow.com/questions/54935559/linking-math-library-in-cmake-file-on-windows-and-linux -IF (NOT WIN32) - target_link_libraries (dedx m) -ENDIF() +if(NOT WIN32) + target_link_libraries(dedx PUBLIC m) +endif() + +# Shared library — for Python ctypes and other dynamic consumers. +add_library(dedx_shared SHARED) +target_link_libraries(dedx_shared PRIVATE dedx_objects) +set_target_properties(dedx_shared PROPERTIES OUTPUT_NAME dedx) +if(NOT WIN32) + target_link_libraries(dedx_shared PRIVATE m) +endif() -install (FILES dedx.h dedx_tools.h dedx_wrappers.h DESTINATION include) -install (TARGETS dedx DESTINATION lib) +install(FILES dedx.h dedx_tools.h dedx_wrappers.h DESTINATION include) +install(TARGETS dedx dedx_shared DESTINATION lib) if(WIN32) - install (FILES ${dedx_data_bin} DESTINATION data) + install(FILES ${dedx_data_bin} DESTINATION data) else() - install (FILES ${dedx_data_bin} DESTINATION share/libdedx) + install(FILES ${dedx_data_bin} DESTINATION share/libdedx) endif() diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt deleted file mode 100644 index 032f928..0000000 --- a/python/CMakeLists.txt +++ /dev/null @@ -1,65 +0,0 @@ -cmake_minimum_required(VERSION 2.6.0) - -find_package(PythonLibs) -if (NOT PYTHONLIBS_FOUND) - message(STATUS "Python libraries NOT found") -endif (NOT PYTHONLIBS_FOUND) - -find_package(PythonInterp) -if (NOT PYTHONINTERP_FOUND) - message(STATUS "Python interpreter NOT found") -endif (NOT PYTHONINTERP_FOUND) - -if(PYTHONLIBS_FOUND AND PYTHONINTERP_FOUND) - set(DO_PYTHON_BINDINGS ON BOOL) - message(STATUS "Python bindings will be compiled") -endif(PYTHONLIBS_FOUND AND PYTHONINTERP_FOUND) - -if(DO_PYTHON_BINDINGS) - add_custom_target( python_bindings ALL ) - - add_custom_command( - TARGET python_bindings - POST_BUILD - - COMMAND echo "\\#!/usr/bin/python" > setup.py - COMMAND echo "from distutils.core import setup, Extension" >> setup.py - COMMAND echo "" >> setup.py - COMMAND echo "module1 = Extension\\(\\'libdedx\\'," >> setup.py - COMMAND echo " define_macros = [\\(\\'MAJOR_VERSION\\', \\'${DEDX_VERSION_MAJOR}\\'\\)," >> setup.py - COMMAND echo " \\(\\'MINOR_VERSION\\', \\'${DEDX_VERSION_MINOR}\\'\\)]," >> setup.py - COMMAND echo " include_dirs = [\\'${PROJECT_SOURCE_DIR}/libdedx\\']," >> setup.py - COMMAND echo " libraries = [\\'dedx\\']," >> setup.py - COMMAND echo " library_dirs = [\\'${PROJECT_BINARY_DIR}/libdedx\\']," >> setup.py - COMMAND echo " sources = [\\'libdedx.c\\']\\)" >> setup.py - COMMAND echo "setup \\(name = \\'libdedx\\'," >> setup.py - COMMAND echo " version = \\'${DEDX_VERSION_MAJOR}.${DEDX_VERSION_MINOR}.${DEDX_VERSION_PATCH}\\'," >> setup.py - COMMAND echo " description = \\'Wrapper for stopping power library libdedx\\'," >> setup.py - COMMAND echo " author = \\'Jakob Toftegaard\\'," >> setup.py - COMMAND echo " author_email = \\'jakob.toftegaard@gmail.com\\'," >> setup.py - COMMAND echo " url = \\'https://github.com/APTG/libdedx\\'," >> setup.py - COMMAND echo " long_description = \\'\\'," >> setup.py - COMMAND echo " ext_modules = [module1]\\)" >> setup.py - COMMAND echo "" >> setup.py - - COMMAND ${PYTHON_EXECUTABLE} setup.py build - COMMENT "Build python bindings..." - WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/python - DEPENDS dedx - ) -endif() - -#add_custom_target(python_bindings_install -# COMMAND echo Nb test2 -# COMMAND ${PYTHON_EXECUTABLE} setup.py install -# COMMENT "Install python bindings..." -# WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/python -# DEPENDS dedx -#) - -# TODO: this simply runs python ./setup.py install after libdEdx was installed. -# This is not the proper way of doing it. -if(DO_PYTHON_BINDINGS) - install(CODE "execute_process(COMMAND echo Install python bindings...)") - install(CODE "execute_process(COMMAND ${PYTHON_EXECUTABLE} setup.py install WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/python)") -endif() \ No newline at end of file diff --git a/python/README b/python/README deleted file mode 100644 index 053ac98..0000000 --- a/python/README +++ /dev/null @@ -1,17 +0,0 @@ -#install - -run -python setup.py build -as su -python setup.py install - -#module name -libdedx - -#functions -get_simple_dedx(ion,target,energy) - -# or - -a = libdedx.Libdedx(2,1,276) -a.get_stp(200) diff --git a/python/libdedx.c b/python/libdedx.c deleted file mode 100644 index ff0e720..0000000 --- a/python/libdedx.c +++ /dev/null @@ -1,141 +0,0 @@ -#include -#include -#include - -#include "structmember.h" - -typedef struct { - PyObject_HEAD dedx_config *config; - dedx_workspace *ws; - int err; - -} Libdedx; - -static void Libdedx_dealloc(Libdedx *self) { - int err = 0; - dedx_free_workspace(self->ws, &err); - dedx_free_config(self->config, &err); - self->ob_type->tp_free((PyObject *) self); -} - -static PyObject *Libdedx_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { - - Libdedx *self; - int program, ion, target; - static char *kwlist[] = { - "program", - "ion", - "target", - }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|lll", kwlist, &program, &ion, &target)) - return NULL; - self = (Libdedx *) type->tp_alloc(type, 0); - self->ws = dedx_allocate_workspace(1, &(self->err)); - if (self->err != 0) - return NULL; - self->config = (dedx_config *) calloc(1, sizeof(dedx_config)); - dedx_config *config = self->config; - config->program = program; - config->ion = ion; - config->target = target; - dedx_load_config(self->ws, config, &(self->err)); - if (self->err != 0) - return NULL; - return (PyObject *) self; -} - -static int Libdedx_init(Libdedx *self, PyObject *args, PyObject *kwds) { - return 0; -} -static PyObject *Libdedx_get_stp(Libdedx *self, PyObject *args) { - float energy, stp; - PyArg_ParseTuple(args, "f", &energy); - stp = dedx_get_stp(self->ws, self->config, energy, &(self->err)); - if (self->err != 0) - return NULL; - return PyFloat_FromDouble((double) stp); -} -static PyObject *Libdedx_get_error(Libdedx *self) { - char *err = calloc(100, sizeof(char)); - PyObject *out; - if (self->err == 0) - err = "No error"; - else { - dedx_get_error_code(err, self->err); - } - out = PyString_FromString(err); - free(err); - return out; -} -static PyMemberDef Libdedx_members[] = { - {NULL} /* Sentinel */ -}; - -static PyMethodDef Libdedx_methods[] = { - {"get_stp", (PyCFunction) Libdedx_get_stp, METH_VARARGS, "Return stp"}, - {"get_error", (PyCFunction) Libdedx_get_error, METH_NOARGS, "Return error string"}, - {NULL} /* Sentinel */ -}; - -static PyTypeObject LibdedxType = { - PyObject_HEAD_INIT(NULL) 0, /*ob_size*/ - "libdedx.Libdedx", /*tp_name*/ - sizeof(Libdedx), /*tp_basicsize*/ - 0, /*tp_itemsize*/ - (destructor) Libdedx_dealloc, /*tp_dealloc*/ - 0, /*tp_print*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_compare*/ - 0, /*tp_repr*/ - 0, /*tp_as_number*/ - 0, /*tp_as_sequence*/ - 0, /*tp_as_mapping*/ - 0, /*tp_hash */ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/ - "Libdedx objects", /* tp_doc */ - 0, /* tp_traverse */ - 0, /* tp_clear */ - 0, /* tp_richcompare */ - 0, /* tp_weaklistoffset */ - 0, /* tp_iter */ - 0, /* tp_iternext */ - Libdedx_methods, /* tp_methods */ - Libdedx_members, /* tp_members */ - 0, /* tp_getset */ - 0, /* tp_base */ - 0, /* tp_dict */ - 0, /* tp_descr_get */ - 0, /* tp_descr_set */ - 0, /* tp_dictoffset */ - (initproc) Libdedx_init, /* tp_init */ - 0, /* tp_alloc */ - Libdedx_new, /* tp_new */ -}; - -static PyMethodDef module_methods[] = { - {NULL} /* Sentinel */ -}; - -#ifndef PyMODINIT_FUNC /* declarations for DLL import/export */ -#define PyMODINIT_FUNC void -#endif -PyMODINIT_FUNC initlibdedx(void) { - PyObject *m; - - if (PyType_Ready(&LibdedxType) < 0) - return; - - m = Py_InitModule3("libdedx", module_methods, "Example module that creates an extension type."); - - if (m == NULL) - return; - - Py_INCREF(&LibdedxType); - PyModule_AddObject(m, "Libdedx", (PyObject *) &LibdedxType); -} diff --git a/python/libdedx.py b/python/libdedx.py new file mode 100644 index 0000000..7a0ec37 --- /dev/null +++ b/python/libdedx.py @@ -0,0 +1,119 @@ +""" +libdedx — ctypes binding for the libdedx stopping-power library. + +The shared library (libdedx.so / libdedx.dylib / dedx.dll) must be built +and locatable before importing this module. Set the LIBDEDX_SO environment +variable to the full path of the shared library to override library search, +which is useful during development or CI before the library is installed. + +Example usage:: + + import libdedx + # PSTAR program (2), Hydrogen ion (1), Liquid water target (276) + stp = libdedx.get_stp(2, 1, 276, 10.0) # MeV cm²/g at 10 MeV/u +""" + +import ctypes +import ctypes.util +import os + + +def _load_library(): + path = os.environ.get("LIBDEDX_SO") + if path: + return ctypes.CDLL(path) + name = ctypes.util.find_library("dedx") + if name is None: + raise OSError( + "libdedx shared library not found. " + "Install libdedx or set LIBDEDX_SO to the path of the shared library." + ) + return ctypes.CDLL(name) + + +_lib = _load_library() + +_c_int_p = ctypes.POINTER(ctypes.c_int) +_c_float_p = ctypes.POINTER(ctypes.c_float) +_c_double_p = ctypes.POINTER(ctypes.c_double) + +_lib.dedx_get_simple_stp_for_program.argtypes = [ + ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_float, _c_int_p, +] +_lib.dedx_get_simple_stp_for_program.restype = ctypes.c_float + +_lib.dedx_get_stp_table_size.argtypes = [ctypes.c_int, ctypes.c_int, ctypes.c_int] +_lib.dedx_get_stp_table_size.restype = ctypes.c_int + +_lib.dedx_get_stp_table.argtypes = [ + ctypes.c_int, ctypes.c_int, ctypes.c_int, + ctypes.c_int, _c_float_p, _c_float_p, +] +_lib.dedx_get_stp_table.restype = ctypes.c_int + +_lib.dedx_fill_default_energy_stp_table.argtypes = [ + ctypes.c_int, ctypes.c_int, ctypes.c_int, _c_float_p, _c_float_p, +] +_lib.dedx_fill_default_energy_stp_table.restype = ctypes.c_int + +_lib.dedx_get_csda_range_table.argtypes = [ + ctypes.c_int, ctypes.c_int, ctypes.c_int, + ctypes.c_int, _c_float_p, _c_double_p, +] +_lib.dedx_get_csda_range_table.restype = ctypes.c_int + + +def _check(err): + if err.value != 0: + raise RuntimeError(f"libdedx error code {err.value}") + + +def get_stp(program, ion, target, energy): + """Return mass stopping power in MeV cm²/g for a single energy in MeV/u.""" + err = ctypes.c_int(0) + result = _lib.dedx_get_simple_stp_for_program( + program, ion, target, float(energy), ctypes.byref(err) + ) + _check(err) + return float(result) + + +def get_stp_table(program, ion, target, energies): + """Return stopping powers (MeV cm²/g) for a list of energies (MeV/u).""" + n = len(energies) + e_arr = (ctypes.c_float * n)(*energies) + s_arr = (ctypes.c_float * n)() + ret = _lib.dedx_get_stp_table(program, ion, target, n, e_arr, s_arr) + if ret != 0: + raise RuntimeError(f"libdedx error code {ret}") + return list(s_arr) + + +def get_default_table(program, ion, target): + """Return (energies, stps) for the built-in tabulated data points. + + energies — list of energies in MeV/u + stps — list of stopping powers in MeV cm²/g + """ + n = _lib.dedx_get_stp_table_size(program, ion, target) + if n <= 0: + raise RuntimeError( + f"No tabulated data for program={program}, ion={ion}, target={target}" + ) + e_arr = (ctypes.c_float * n)() + s_arr = (ctypes.c_float * n)() + ret = _lib.dedx_fill_default_energy_stp_table(program, ion, target, e_arr, s_arr) + if ret < 0: + raise RuntimeError(f"libdedx error code {ret}") + return list(e_arr), list(s_arr) + + +def get_csda_table(program, ion, target, energies): + """Return CSDA ranges (g/cm²) for a list of energies (MeV/u).""" + n = len(energies) + e_arr = (ctypes.c_float * n)(*energies) + r_arr = (ctypes.c_double * n)() + ret = _lib.dedx_get_csda_range_table(program, ion, target, n, e_arr, r_arr) + if ret != 0: + raise RuntimeError(f"libdedx error code {ret}") + return list(r_arr) diff --git a/python/libdedx.pyi b/python/libdedx.pyi new file mode 100644 index 0000000..ef12062 --- /dev/null +++ b/python/libdedx.pyi @@ -0,0 +1,4 @@ +def get_stp(program: int, ion: int, target: int, energy: float) -> float: ... +def get_stp_table(program: int, ion: int, target: int, energies: list[float]) -> list[float]: ... +def get_default_table(program: int, ion: int, target: int) -> tuple[list[float], list[float]]: ... +def get_csda_table(program: int, ion: int, target: int, energies: list[float]) -> list[float]: ... diff --git a/python/pyproject.toml b/python/pyproject.toml new file mode 100644 index 0000000..7f92ef8 --- /dev/null +++ b/python/pyproject.toml @@ -0,0 +1,21 @@ +[build-system] +requires = ["setuptools>=68"] +build-backend = "setuptools.build_meta" + +[project] +name = "libdedx" +version = "0.1.0" +description = "Python ctypes binding for the libdedx stopping-power library" +readme = "README.md" +requires-python = ">=3.8" +license = {text = "LGPL-2.0-or-later"} + +[project.optional-dependencies] +test = ["pytest"] + +[tool.setuptools] +py-modules = ["libdedx"] +include-package-data = true + +[tool.setuptools.data-files] +"" = ["libdedx.pyi"] diff --git a/python/test.py b/python/test.py deleted file mode 100644 index f5f0e4e..0000000 --- a/python/test.py +++ /dev/null @@ -1,4 +0,0 @@ -from libdedx import Libdedx -a = Libdedx(program=2,ion=1,target=276) -for i in range(10000000): - a.get_stp(10) diff --git a/python/tests/conftest.py b/python/tests/conftest.py new file mode 100644 index 0000000..ae695b7 --- /dev/null +++ b/python/tests/conftest.py @@ -0,0 +1,22 @@ +import os +from pathlib import Path + + +def _candidate_library_paths(repo_root: Path) -> list[Path]: + return [ + repo_root / "build" / "libdedx" / "libdedx.so", + repo_root / "build-debug" / "libdedx" / "libdedx.so", + repo_root / "build-release" / "libdedx" / "libdedx.so", + repo_root / "build-coverage" / "libdedx" / "libdedx.so", + ] + + +def pytest_configure() -> None: + if os.environ.get("LIBDEDX_SO"): + return + + repo_root = Path(__file__).resolve().parents[2] + for candidate in _candidate_library_paths(repo_root): + if candidate.exists(): + os.environ["LIBDEDX_SO"] = str(candidate) + return diff --git a/python/tests/test_libdedx.py b/python/tests/test_libdedx.py new file mode 100644 index 0000000..bdd4780 --- /dev/null +++ b/python/tests/test_libdedx.py @@ -0,0 +1,44 @@ +""" +Basic smoke tests for the libdedx Python binding. + +Uses PSTAR (program=2), Hydrogen (ion=1), Liquid water (target=276) +as a reference combination with well-known stopping power values. +""" + +import libdedx + +PROGRAM = 2 # PSTAR +ION = 1 # Hydrogen (Z=1) +TARGET = 276 # Liquid water + + +def test_get_stp_returns_positive(): + stp = libdedx.get_stp(PROGRAM, ION, TARGET, 10.0) + assert stp > 0.0 + + +def test_get_stp_table(): + energies = [1.0, 10.0, 100.0] + stps = libdedx.get_stp_table(PROGRAM, ION, TARGET, energies) + assert len(stps) == len(energies) + assert all(s > 0.0 for s in stps) + + +def test_get_default_table_nonempty(): + energies, stps = libdedx.get_default_table(PROGRAM, ION, TARGET) + assert len(energies) > 0 + assert len(energies) == len(stps) + assert all(e > 0.0 for e in energies) + assert all(s > 0.0 for s in stps) + + +def test_get_csda_table(): + energies = [10.0, 100.0] + ranges = libdedx.get_csda_table(PROGRAM, ION, TARGET, energies) + assert len(ranges) == len(energies) + assert all(r > 0.0 for r in ranges) + + +# TODO: passing an invalid program number currently causes the library to +# segfault rather than returning an error code. A proper test can be added +# once input validation is implemented in the C library. diff --git a/tests/install_smoke_test.cmake b/tests/install_smoke_test.cmake index 143bdd8..acb60af 100644 --- a/tests/install_smoke_test.cmake +++ b/tests/install_smoke_test.cmake @@ -49,7 +49,10 @@ if(NOT rc EQUAL 0) message(FATAL_ERROR "install smoke consumer compile failed: ${rc}") endif() -execute_process(COMMAND "${smoke_consumer}" +execute_process( + COMMAND "${CMAKE_COMMAND}" -E env + "LD_LIBRARY_PATH=${smoke_prefix}/lib" + "${smoke_consumer}" RESULT_VARIABLE rc) if(NOT rc EQUAL 0) message(FATAL_ERROR "install smoke consumer run failed: ${rc}") From f6c6b9ee7f854de568c8264ea6c58ba5dc70b5f9 Mon Sep 17 00:00:00 2001 From: Niels Bassler Date: Sun, 29 Mar 2026 01:09:04 +0100 Subject: [PATCH 2/9] add version retrieval, update CI workflow, and refine dependencies --- .github/workflows/python.yml | 14 +++++++++++--- .gitignore | 7 ++++++- python/libdedx.py | 12 ++++++++++++ python/libdedx.pyi | 1 + python/pyproject.toml | 24 ++++++++++++++++++++---- python/tests/test_libdedx.py | 8 ++++++++ tests/test_version.c | 13 +++++++++++++ 7 files changed, 71 insertions(+), 8 deletions(-) create mode 100644 tests/test_version.c diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 48e66a1..fd6f333 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -10,11 +10,19 @@ jobs: - name: Configure and build (shared library) run: | - cmake -S . -B build + cmake -S . -B build -DBUILD_SHARED_LIBS=ON cmake --build build --parallel - - name: Install Python binding and test dependencies - run: pip install --break-system-packages pytest python/ + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install Python package and dev dependencies + run: python -m pip install -e "python[dev]" + + - name: Run ruff + run: python -m ruff check python - name: Run tests run: | diff --git a/.gitignore b/.gitignore index 6760954..15c9b23 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,15 @@ # Custom ignores +.codex *.bin + build/ build-release/ build-coverage/ + +Testing/ + docs/doxygen/html/ -.codex + python/.venv/ python/__pycache__/ python/.pytest_cache/ diff --git a/python/libdedx.py b/python/libdedx.py index 7a0ec37..bacec45 100644 --- a/python/libdedx.py +++ b/python/libdedx.py @@ -62,12 +62,24 @@ def _load_library(): ] _lib.dedx_get_csda_range_table.restype = ctypes.c_int +_lib.dedx_get_version.argtypes = [_c_int_p, _c_int_p, _c_int_p] +_lib.dedx_get_version.restype = None + def _check(err): if err.value != 0: raise RuntimeError(f"libdedx error code {err.value}") +def get_version(): + """Return the libdedx version as a ``major.minor.patch`` string.""" + major = ctypes.c_int(0) + minor = ctypes.c_int(0) + patch = ctypes.c_int(0) + _lib.dedx_get_version(ctypes.byref(major), ctypes.byref(minor), ctypes.byref(patch)) + return f"{major.value}.{minor.value}.{patch.value}" + + def get_stp(program, ion, target, energy): """Return mass stopping power in MeV cm²/g for a single energy in MeV/u.""" err = ctypes.c_int(0) diff --git a/python/libdedx.pyi b/python/libdedx.pyi index ef12062..4ae03c9 100644 --- a/python/libdedx.pyi +++ b/python/libdedx.pyi @@ -1,3 +1,4 @@ +def get_version() -> str: ... def get_stp(program: int, ion: int, target: int, energy: float) -> float: ... def get_stp_table(program: int, ion: int, target: int, energies: list[float]) -> list[float]: ... def get_default_table(program: int, ion: int, target: int) -> tuple[list[float], list[float]]: ... diff --git a/python/pyproject.toml b/python/pyproject.toml index 7f92ef8..12405d3 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -1,17 +1,18 @@ [build-system] -requires = ["setuptools>=68"] +requires = ["setuptools>=68", "wheel", "setuptools-scm>=8"] build-backend = "setuptools.build_meta" [project] name = "libdedx" -version = "0.1.0" description = "Python ctypes binding for the libdedx stopping-power library" -readme = "README.md" +readme = "../README.md" requires-python = ">=3.8" license = {text = "LGPL-2.0-or-later"} +dynamic = ["version"] [project.optional-dependencies] -test = ["pytest"] +test = ["pytest>=8"] +dev = ["ruff>=0.5.0", "libdedx[test]"] [tool.setuptools] py-modules = ["libdedx"] @@ -19,3 +20,18 @@ include-package-data = true [tool.setuptools.data-files] "" = ["libdedx.pyi"] + +[tool.pytest.ini_options] +testpaths = ["tests"] + +[tool.ruff] +line-length = 120 +target-version = "py38" + +[tool.ruff.lint] +select = ["E", "F", "W"] + +[tool.setuptools_scm] +root = ".." +version_scheme = "post-release" +local_scheme = "node-and-date" diff --git a/python/tests/test_libdedx.py b/python/tests/test_libdedx.py index bdd4780..bb6399a 100644 --- a/python/tests/test_libdedx.py +++ b/python/tests/test_libdedx.py @@ -12,6 +12,14 @@ TARGET = 276 # Liquid water +def test_get_version(capsys): + version = libdedx.get_version() + print(version) + captured = capsys.readouterr() + assert version.count(".") == 2 + assert captured.out.strip() == version + + def test_get_stp_returns_positive(): stp = libdedx.get_stp(PROGRAM, ION, TARGET, 10.0) assert stp > 0.0 diff --git a/tests/test_version.c b/tests/test_version.c new file mode 100644 index 0000000..48b8d49 --- /dev/null +++ b/tests/test_version.c @@ -0,0 +1,13 @@ +#include + +#include + +int main(void) { + int major = 0; + int minor = 0; + int patch = 0; + + dedx_get_version(&major, &minor, &patch); + printf("%d.%d.%d\n", major, minor, patch); + return 0; +} From d7a5006c6e21312180f834915fcfcf24936b6df7 Mon Sep 17 00:00:00 2001 From: Niels Bassler Date: Sun, 29 Mar 2026 01:28:30 +0100 Subject: [PATCH 3/9] copilot fixes --- .github/workflows/ci.yml | 18 ++++++++++++------ .vscode/settings.json | 1 - libdedx/CMakeLists.txt | 21 +++++++++++++++++---- python/libdedx.py | 4 +++- python/pyproject.toml | 4 ++-- python/tests/conftest.py | 21 +++++++++++++++------ tests/install_smoke_test.cmake | 13 ++++++++++++- tests/test_version.c | 3 +-- 8 files changed, 62 insertions(+), 23 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 57a6f4d..6cad063 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,6 +64,9 @@ jobs: python_tests: runs-on: ubuntu-latest needs: build_and_test + strategy: + matrix: + python-version: ["3.9", "3.13"] steps: - name: Checkout uses: actions/checkout@v4 @@ -71,19 +74,22 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: "3.12" + python-version: ${{ matrix.python-version }} - name: Configure - run: cmake -S . -B build -DBUILD_SHARED_LIBS=ON + run: cmake -S . -B build - name: Build run: cmake --build build --parallel - - name: Install pytest - run: python -m pip install pytest + - name: Install Python package and dev dependencies + run: python -m pip install -e "python[dev]" + + - name: Run ruff + if: ${{ matrix.python-version == '3.13' }} + run: python -m ruff check python - name: Run Python tests env: LIBDEDX_SO: ${{ github.workspace }}/build/libdedx/libdedx.so - PYTHONPATH: ${{ github.workspace }}/python - run: pytest python/tests -q + run: python -m pytest python/tests -q diff --git a/.vscode/settings.json b/.vscode/settings.json index d9dd77f..f71804b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -19,7 +19,6 @@ }, // Python — add python/ to the analysis path so Pylance finds libdedx.py and libdedx.pyi - "python.defaultInterpreterPath": "${workspaceFolder}/python/.venv/bin/python", "python.analysis.extraPaths": ["python"], "python.testing.pytestEnabled": true, "python.testing.unittestEnabled": false, diff --git a/libdedx/CMakeLists.txt b/libdedx/CMakeLists.txt index 53afa84..deeeac8 100644 --- a/libdedx/CMakeLists.txt +++ b/libdedx/CMakeLists.txt @@ -34,14 +34,27 @@ endif() # Shared library — for Python ctypes and other dynamic consumers. add_library(dedx_shared SHARED) -target_link_libraries(dedx_shared PRIVATE dedx_objects) -set_target_properties(dedx_shared PROPERTIES OUTPUT_NAME dedx) +target_link_libraries(dedx_shared PUBLIC dedx_objects) +if(WIN32) + set_target_properties(dedx_shared PROPERTIES + OUTPUT_NAME dedx + ARCHIVE_OUTPUT_NAME dedx_shared + ) +else() + set_target_properties(dedx_shared PROPERTIES OUTPUT_NAME dedx) +endif() if(NOT WIN32) - target_link_libraries(dedx_shared PRIVATE m) + target_link_libraries(dedx_shared PUBLIC m) endif() install(FILES dedx.h dedx_tools.h dedx_wrappers.h DESTINATION include) -install(TARGETS dedx dedx_shared DESTINATION lib) + +install(TARGETS dedx dedx_shared + RUNTIME DESTINATION bin + LIBRARY DESTINATION lib + ARCHIVE DESTINATION lib +) + if(WIN32) install(FILES ${dedx_data_bin} DESTINATION data) else() diff --git a/python/libdedx.py b/python/libdedx.py index bacec45..af9f9a8 100644 --- a/python/libdedx.py +++ b/python/libdedx.py @@ -108,7 +108,9 @@ def get_default_table(program, ion, target): stps — list of stopping powers in MeV cm²/g """ n = _lib.dedx_get_stp_table_size(program, ion, target) - if n <= 0: + if n < 0: + raise RuntimeError(f"libdedx error code {n}") + if n == 0: raise RuntimeError( f"No tabulated data for program={program}, ion={ion}, target={target}" ) diff --git a/python/pyproject.toml b/python/pyproject.toml index 12405d3..18e982c 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta" name = "libdedx" description = "Python ctypes binding for the libdedx stopping-power library" readme = "../README.md" -requires-python = ">=3.8" +requires-python = ">=3.9" license = {text = "LGPL-2.0-or-later"} dynamic = ["version"] @@ -26,7 +26,7 @@ testpaths = ["tests"] [tool.ruff] line-length = 120 -target-version = "py38" +target-version = "py39" [tool.ruff.lint] select = ["E", "F", "W"] diff --git a/python/tests/conftest.py b/python/tests/conftest.py index ae695b7..c99c009 100644 --- a/python/tests/conftest.py +++ b/python/tests/conftest.py @@ -1,14 +1,23 @@ import os +import sys from pathlib import Path def _candidate_library_paths(repo_root: Path) -> list[Path]: - return [ - repo_root / "build" / "libdedx" / "libdedx.so", - repo_root / "build-debug" / "libdedx" / "libdedx.so", - repo_root / "build-release" / "libdedx" / "libdedx.so", - repo_root / "build-coverage" / "libdedx" / "libdedx.so", - ] + if sys.platform.startswith("linux"): + lib_names = ["libdedx.so"] + elif sys.platform == "darwin": + lib_names = ["libdedx.dylib"] + elif os.name == "nt": + lib_names = ["dedx.dll", "libdedx.dll"] + else: + lib_names = ["libdedx.so"] + + candidates = [] + for build_dir in ("build", "build-debug", "build-release", "build-coverage"): + for lib_name in lib_names: + candidates.append(repo_root / build_dir / "libdedx" / lib_name) + return candidates def pytest_configure() -> None: diff --git a/tests/install_smoke_test.cmake b/tests/install_smoke_test.cmake index acb60af..7f8eaa8 100644 --- a/tests/install_smoke_test.cmake +++ b/tests/install_smoke_test.cmake @@ -49,9 +49,20 @@ if(NOT rc EQUAL 0) message(FATAL_ERROR "install smoke consumer compile failed: ${rc}") endif() +set(runtime_env) +if(APPLE) + list(APPEND runtime_env + "DYLD_LIBRARY_PATH=${smoke_prefix}/lib:$ENV{DYLD_LIBRARY_PATH}" + ) +else() + list(APPEND runtime_env + "LD_LIBRARY_PATH=${smoke_prefix}/lib:$ENV{LD_LIBRARY_PATH}" + ) +endif() + execute_process( COMMAND "${CMAKE_COMMAND}" -E env - "LD_LIBRARY_PATH=${smoke_prefix}/lib" + ${runtime_env} "${smoke_consumer}" RESULT_VARIABLE rc) if(NOT rc EQUAL 0) diff --git a/tests/test_version.c b/tests/test_version.c index 48b8d49..c867ec3 100644 --- a/tests/test_version.c +++ b/tests/test_version.c @@ -1,6 +1,5 @@ -#include - #include +#include int main(void) { int major = 0; From daea7323b6ab8a9d8a7283a8d582be214112f4f0 Mon Sep 17 00:00:00 2001 From: Niels Bassler Date: Sun, 29 Mar 2026 01:29:56 +0100 Subject: [PATCH 4/9] fix python readme --- python/README.md | 7 +++++++ python/pyproject.toml | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 python/README.md diff --git a/python/README.md b/python/README.md new file mode 100644 index 0000000..5bcb3fb --- /dev/null +++ b/python/README.md @@ -0,0 +1,7 @@ +# libdedx Python binding + +This directory contains the `ctypes`-based Python binding for `libdedx`. + +The Python module loads the native `libdedx` shared library at runtime. +Build and install the C library first, or set `LIBDEDX_SO` to the path of +the built shared library during development and testing. diff --git a/python/pyproject.toml b/python/pyproject.toml index 18e982c..03c17f9 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [project] name = "libdedx" description = "Python ctypes binding for the libdedx stopping-power library" -readme = "../README.md" +readme = "README.md" requires-python = ">=3.9" license = {text = "LGPL-2.0-or-later"} dynamic = ["version"] From e6b9ee3811396f3a66da936899b3271d9f5a963d Mon Sep 17 00:00:00 2001 From: Niels Bassler Date: Sun, 29 Mar 2026 01:32:23 +0100 Subject: [PATCH 5/9] fix python test --- .github/workflows/ci.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6cad063..3b27b0d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,9 +87,11 @@ jobs: - name: Run ruff if: ${{ matrix.python-version == '3.13' }} - run: python -m ruff check python + working-directory: python + run: python -m ruff check . - name: Run Python tests + working-directory: python env: LIBDEDX_SO: ${{ github.workspace }}/build/libdedx/libdedx.so - run: python -m pytest python/tests -q + run: python -m pytest tests -q From 25dd59b8214ed5a0ddbeb53c707614521f130160 Mon Sep 17 00:00:00 2001 From: Niels Bassler Date: Sun, 29 Mar 2026 03:02:54 +0200 Subject: [PATCH 6/9] remove obsolete CI workflow and type stubs; add new API implementation and package configuration --- .github/workflows/python.yml | 29 -------------------------- python/libdedx.pyi | 5 ----- python/libdedx/__init__.py | 11 ++++++++++ python/libdedx/__init__.pyi | 3 +++ python/{libdedx.py => libdedx/_api.py} | 21 ++++--------------- python/libdedx/py.typed | 0 python/pyproject.toml | 9 +++++--- 7 files changed, 24 insertions(+), 54 deletions(-) delete mode 100644 .github/workflows/python.yml delete mode 100644 python/libdedx.pyi create mode 100644 python/libdedx/__init__.py create mode 100644 python/libdedx/__init__.pyi rename python/{libdedx.py => libdedx/_api.py} (83%) create mode 100644 python/libdedx/py.typed diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml deleted file mode 100644 index fd6f333..0000000 --- a/.github/workflows/python.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: Python - -on: [push, pull_request] - -jobs: - python: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Configure and build (shared library) - run: | - cmake -S . -B build -DBUILD_SHARED_LIBS=ON - cmake --build build --parallel - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - - name: Install Python package and dev dependencies - run: python -m pip install -e "python[dev]" - - - name: Run ruff - run: python -m ruff check python - - - name: Run tests - run: | - LIBDEDX_SO=build/libdedx/libdedx.so pytest python/tests/ -v diff --git a/python/libdedx.pyi b/python/libdedx.pyi deleted file mode 100644 index 4ae03c9..0000000 --- a/python/libdedx.pyi +++ /dev/null @@ -1,5 +0,0 @@ -def get_version() -> str: ... -def get_stp(program: int, ion: int, target: int, energy: float) -> float: ... -def get_stp_table(program: int, ion: int, target: int, energies: list[float]) -> list[float]: ... -def get_default_table(program: int, ion: int, target: int) -> tuple[list[float], list[float]]: ... -def get_csda_table(program: int, ion: int, target: int, energies: list[float]) -> list[float]: ... diff --git a/python/libdedx/__init__.py b/python/libdedx/__init__.py new file mode 100644 index 0000000..2f7f17a --- /dev/null +++ b/python/libdedx/__init__.py @@ -0,0 +1,11 @@ +"""Python package for the libdedx ctypes binding.""" + +from ._api import get_csda_table, get_default_table, get_stp, get_stp_table, get_version + +__all__ = [ + "get_version", + "get_stp", + "get_stp_table", + "get_default_table", + "get_csda_table", +] diff --git a/python/libdedx/__init__.pyi b/python/libdedx/__init__.pyi new file mode 100644 index 0000000..7acee04 --- /dev/null +++ b/python/libdedx/__init__.pyi @@ -0,0 +1,3 @@ +from ._api import get_csda_table, get_default_table, get_stp, get_stp_table, get_version + +__all__: list[str] diff --git a/python/libdedx.py b/python/libdedx/_api.py similarity index 83% rename from python/libdedx.py rename to python/libdedx/_api.py index af9f9a8..0b6856d 100644 --- a/python/libdedx.py +++ b/python/libdedx/_api.py @@ -1,17 +1,4 @@ -""" -libdedx — ctypes binding for the libdedx stopping-power library. - -The shared library (libdedx.so / libdedx.dylib / dedx.dll) must be built -and locatable before importing this module. Set the LIBDEDX_SO environment -variable to the full path of the shared library to override library search, -which is useful during development or CI before the library is installed. - -Example usage:: - - import libdedx - # PSTAR program (2), Hydrogen ion (1), Liquid water target (276) - stp = libdedx.get_stp(2, 1, 276, 10.0) # MeV cm²/g at 10 MeV/u -""" +"""Public ctypes-backed API for libdedx.""" import ctypes import ctypes.util @@ -104,8 +91,8 @@ def get_stp_table(program, ion, target, energies): def get_default_table(program, ion, target): """Return (energies, stps) for the built-in tabulated data points. - energies — list of energies in MeV/u - stps — list of stopping powers in MeV cm²/g + energies -- list of energies in MeV/u + stps -- list of stopping powers in MeV cm^2/g """ n = _lib.dedx_get_stp_table_size(program, ion, target) if n < 0: @@ -123,7 +110,7 @@ def get_default_table(program, ion, target): def get_csda_table(program, ion, target, energies): - """Return CSDA ranges (g/cm²) for a list of energies (MeV/u).""" + """Return CSDA ranges (g/cm^2) for a list of energies (MeV/u).""" n = len(energies) e_arr = (ctypes.c_float * n)(*energies) r_arr = (ctypes.c_double * n)() diff --git a/python/libdedx/py.typed b/python/libdedx/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/python/pyproject.toml b/python/pyproject.toml index 03c17f9..9289513 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -15,11 +15,14 @@ test = ["pytest>=8"] dev = ["ruff>=0.5.0", "libdedx[test]"] [tool.setuptools] -py-modules = ["libdedx"] include-package-data = true -[tool.setuptools.data-files] -"" = ["libdedx.pyi"] +[tool.setuptools.packages.find] +where = ["."] +include = ["libdedx"] + +[tool.setuptools.package-data] +libdedx = ["__init__.pyi", "py.typed"] [tool.pytest.ini_options] testpaths = ["tests"] From 1124f8364990fec23346fe19055d407d7af762ed Mon Sep 17 00:00:00 2001 From: Niels Bassler Date: Sun, 29 Mar 2026 03:04:37 +0200 Subject: [PATCH 7/9] fix ruff --- python/libdedx/__init__.pyi | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/python/libdedx/__init__.pyi b/python/libdedx/__init__.pyi index 7acee04..2635f7e 100644 --- a/python/libdedx/__init__.pyi +++ b/python/libdedx/__init__.pyi @@ -1,3 +1,7 @@ -from ._api import get_csda_table, get_default_table, get_stp, get_stp_table, get_version +def get_version() -> str: ... +def get_stp(program: int, ion: int, target: int, energy: float) -> float: ... +def get_stp_table(program: int, ion: int, target: int, energies: list[float]) -> list[float]: ... +def get_default_table(program: int, ion: int, target: int) -> tuple[list[float], list[float]]: ... +def get_csda_table(program: int, ion: int, target: int, energies: list[float]) -> list[float]: ... __all__: list[str] From c0db18baf7f1f1e0cdcd16a58658286d054351d6 Mon Sep 17 00:00:00 2001 From: Niels Bassler <7140398+nbassler@users.noreply.github.com> Date: Sun, 29 Mar 2026 03:22:29 +0200 Subject: [PATCH 8/9] Update .vscode/settings.json Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .vscode/settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index f71804b..2a02297 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -18,7 +18,7 @@ "editor.formatOnSave": true }, - // Python — add python/ to the analysis path so Pylance finds libdedx.py and libdedx.pyi + // Python — add python/ to the analysis path so Pylance finds the libdedx package (__init__.py / __init__.pyi) "python.analysis.extraPaths": ["python"], "python.testing.pytestEnabled": true, "python.testing.unittestEnabled": false, From 052ead4092ca8616c84f0d68c992a48b74156ee9 Mon Sep 17 00:00:00 2001 From: Niels Bassler Date: Sun, 29 Mar 2026 03:24:29 +0200 Subject: [PATCH 9/9] copilot fix --- python/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/pyproject.toml b/python/pyproject.toml index 9289513..f8da0c0 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -12,7 +12,7 @@ dynamic = ["version"] [project.optional-dependencies] test = ["pytest>=8"] -dev = ["ruff>=0.5.0", "libdedx[test]"] +dev = ["ruff>=0.5.0", "pytest>=8"] [tool.setuptools] include-package-data = true