diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 76859d0..d7c2c3a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -44,7 +44,7 @@ jobs: export libcarna_version=$(conda list --json |jq -rj '[ .[] | select( .name == "libcarna" ) ][0].version') echo "libcarna_version=$libcarna_version" >> "$GITHUB_OUTPUT" - - name: Build dist + - name: Build wheel shell: bash run: ./linux_build.bash @@ -52,8 +52,7 @@ jobs: uses: actions/upload-artifact@v4 with: name: dist-${{ inputs.python-version }} - path: | - dist + path: build/dist/libcarna_python-*.whl outputs: libcarna_version: ${{ steps.meta.outputs.libcarna_version }} @@ -117,9 +116,6 @@ jobs: run: | eval "$(conda shell.bash hook)" conda activate ./.env - cd test - mkdir test - mv results test/results python -m unittest -vv env: LIBCARNA_PYTHON_LOGGING: true diff --git a/.gitignore b/.gitignore index 20dccfb..7fcbcfe 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ /dist /LibCarna_Python.egg-info /.libcarna-dev +/test/results/actual .ipynb_checkpoints *.swp *.pyc diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 63787ce..09ace70 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -20,7 +20,7 @@ build: jobs: install: - bash ./linux_build.bash - - pip install dist/libcarna_python-*.whl + - pip install build/dist/libcarna_python-*.whl pre_build: - pip install -r docs/requirements.txt build: diff --git a/CMakeLists.txt b/CMakeLists.txt index ea0bb89..1b8415d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,6 +3,10 @@ project( LibCarna-Python ) set( PYTHON_MODULE_NAME "libcarna" ) include( FindPackageHandleStandardArgs ) +set( MAJOR_VERSION 0 ) +set( MINOR_VERSION 2 ) +set( PATCH_VERSION 0 ) + set( CMAKE_CXX_STANDARD 14 ) set( CMAKE_CXX_STANDARD_REQUIRED ON ) set( CMAKE_INTERPROCEDURAL_OPTIMIZATION FALSE ) @@ -73,20 +77,31 @@ find_package( Eigen3 REQUIRED ) include_directories( ${EIGEN3_INCLUDE_DIR} ) # LibCarna -find_package( LibCarna ${REQUIRED_VERSION_LIBCARNA} REQUIRED COMPONENTS release ) +find_package( LibCarna "3.4.0" REQUIRED COMPONENTS release ) include_directories( ${LibCarna_INCLUDE_DIR} ) set( LIBCARNA_VERSION ${FOUND_VERSION} ) ############################################ -configure_file( ${CMAKE_CURRENT_SOURCE_DIR}/misc/__init__.py.in - ${CMAKE_CURRENT_BINARY_DIR}/${PYTHON_MODULE_NAME}/__init__.py @ONLY ) +configure_file( + ${CMAKE_CURRENT_SOURCE_DIR}/misc/__init__.py.in + ${CMAKE_CURRENT_BINARY_DIR}/${PYTHON_MODULE_NAME}/__init__.py + @ONLY +) + +configure_file( + ${CMAKE_CURRENT_SOURCE_DIR}/setup.py.in + ${CMAKE_CURRENT_BINARY_DIR}/setup.py + @ONLY +) file( GLOB PYTHON_AUX_FILES "${CMAKE_CURRENT_SOURCE_DIR}/misc/libcarna/*.py" ) file( COPY ${PYTHON_AUX_FILES} DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/${PYTHON_MODULE_NAME}" ) file( GLOB LICENSES "${LibCarna_LICENSE_DIR}/LICENSE*" ) -file( COPY ${LICENSES} DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/licenses/" ) +file( COPY ${LICENSES} DESTINATION "${CMAKE_CURRENT_BINARY_DIR}" ) +file( COPY ${CMAKE_CURRENT_SOURCE_DIR}/README.md DESTINATION "${CMAKE_CURRENT_BINARY_DIR}" ) +file( COPY ${CMAKE_CURRENT_SOURCE_DIR}/LICENSE DESTINATION "${CMAKE_CURRENT_BINARY_DIR}" ) ############################################ # Project @@ -165,11 +180,3 @@ install( FILES ${CMAKE_CURRENT_BINARY_DIR}/${PYTHON_MODULE_NAME}/__init__.py DESTINATION ${INSTALL_LIBRARY_DIR} ) - -############################################ -# Process unit tests -############################################ - -if( BUILD_TEST ) - add_subdirectory( test ) -endif() diff --git a/LICENSE b/LICENSE index e5f02d9..e4e9ab4 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2025 Leonid Kostrykin +Copyright (c) 2021-2025 Leonid Kostrykin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/environment.yml b/environment.yml index ad6eafd..78929e0 100644 --- a/environment.yml +++ b/environment.yml @@ -18,8 +18,9 @@ dependencies: - cmake - eigen >=3.0.5 - libxcrypt # requied for Python 3.10 - - pyyaml - setuptools + - python-build + - pip # --------------------------------------------------------------------------- # Runtime dependencies (general) diff --git a/linux_build.bash b/linux_build.bash index 30244e2..29f4df2 100755 --- a/linux_build.bash +++ b/linux_build.bash @@ -3,36 +3,51 @@ set -ex # Create or update conda environment export ROOT="$PWD"/$(dirname "$0") -if [ ! -d "$ROOT/.env" ]; then - conda env create -f "$ROOT/environment.yml" --prefix "$ROOT/.env" +if [ ! -d "$ROOT"/.env ]; then + conda env create -f "$ROOT"/environment.yml --prefix "$ROOT"/.env else - conda env update -f "$ROOT/environment.yml" --prefix "$ROOT/.env" --prune + conda env update -f "$ROOT"/environment.yml --prefix "$ROOT"/.env --prune fi # Activate conda environment eval "$(conda shell.bash hook)" -conda activate "$ROOT/.env" +conda activate "$ROOT"/.env -# Setup and check dependencies -export PYBIND11_PREFIX="$CONDA_PREFIX/share/cmake/pybind11" -export CMAKE_MODULE_PATH="$CONDA_PREFIX/share/cmake/Modules" +# Create build directory +mkdir -p "$ROOT"/build -# Default to not building the test suite -if [ -z "$LIBCARNA_PYTHON_BUILD_TEST" ]; then - export LIBCARNA_PYTHON_BUILD_TEST="OFF" -else - pip install -r test/requirements.txt +# Build native extension +cd "$ROOT"/build +cmake -DCMAKE_BUILD_TYPE=Release \ + -DPYTHON_EXECUTABLE="$(which python)" \ + -Dpybind11_DIR="$CONDA_PREFIX/share/cmake/pybind11" \ + -DCMAKE_MODULE_PATH="$CONDA_PREFIX/share/cmake/Modules" \ + "$ROOT" +if [ -z "$LIBCARNA_SKIP_NATIVE" ]; then + make VERBOSE=1 fi -# Build wheel and test -cd "$ROOT" -python setup.py bdist_wheel +# Build wheel +python -m build --no-isolation + +# Install wheel +rm -rf venv +python -m venv venv --system-site-package +source venv/bin/activate +pip install --no-deps dist/*.whl + +# Optionally, run the test suite +if [ -v LIBCARNA_PYTHON_BUILD_TEST ]; then + cd "$ROOT" + pip install -r test/requirements.txt + python -m unittest +fi # Optionally, build the documentation if [ -v LIBCARNA_PYTHON_BUILD_DOCS ]; then + cd "$ROOT" pip install -r docs/requirements.txt - export LIBCARNA_PYTHON_PATH="$ROOT/build/make_release" - rm -rf $ROOT/docs/build + rm -rf docs/build sphinx-build -M html docs docs/build - cp $ROOT/docs/build/html/examples/*.ipynb $ROOT/examples/ + cp docs/build/html/examples/*.ipynb examples/ fi \ No newline at end of file diff --git a/misc/__init__.py.in b/misc/__init__.py.in index 987663f..5a65f70 100644 --- a/misc/__init__.py.in +++ b/misc/__init__.py.in @@ -1,4 +1,4 @@ -version = '@MAJOR_VERSION@.@MINOR_VERSION@.@PATCH_VERSION@' +version = '@FULL_VERSION@' libcarna_version = '@LIBCARNA_VERSION@' diff --git a/setup.py b/setup.py deleted file mode 100644 index 22804a7..0000000 --- a/setup.py +++ /dev/null @@ -1,124 +0,0 @@ -import os -import sys -from pathlib import Path - -VERSION_LIBCARNA_PYTHON = '0.2.0' -VERSION_LIBCARNA = '3.4.0' - -root_dir = Path(os.path.abspath(os.path.dirname(__file__))) - -build_dirs = dict( - debug = root_dir / 'build' / 'make_debug', - release = root_dir / 'build' / 'make_release', -) - -if __name__ == '__main__': - - (build_dirs['debug'] / 'libcarna').mkdir(parents=True, exist_ok=True) - (build_dirs['release'] / 'libcarna').mkdir(parents=True, exist_ok=True) - - from setuptools import setup, Extension - from setuptools.command.build_ext import build_ext as _build_ext - from setuptools.command.build_py import build_py as _build_py - - with open(root_dir / 'README.md', encoding='utf-8') as io: - long_description = io.read() - - class CMakeExtension(Extension): - - def __init__(self): - super().__init__('CMake', sources=[]) - - def build(self, build_ext): - version_major, version_minor, version_patch = [int(val) for val in VERSION_LIBCARNA_PYTHON.split('.')] - build_test = os.environ.get('LIBCARNA_PYTHON_BUILD_TEST', 'ON') - assert build_test in ('ON', 'OFF') - build_type = os.environ.get('CMAKE_BUILD_TYPE', 'Release') - cmake_args = [ - f'-DCMAKE_BUILD_TYPE={build_type}', - f'-DBUILD_TEST={build_test}', - f'-DMAJOR_VERSION={version_major}', - f'-DMINOR_VERSION={version_minor}', - f'-DPATCH_VERSION={version_patch}', - f'-DREQUIRED_VERSION_LIBCARNA={VERSION_LIBCARNA}', - f'-DPYTHON_EXECUTABLE={sys.executable}', - f'-Dpybind11_DIR={os.environ["PYBIND11_PREFIX"]}', - f'-DCMAKE_MODULE_PATH={os.environ.get("CMAKE_MODULE_PATH")}', - f'../..', - ] - - if not build_ext.dry_run: - os.chdir(str(build_dirs[build_type.lower()])) - build_ext.spawn(['cmake'] + cmake_args) - build_ext.spawn(['make', 'VERBOSE=1']) - if build_test == 'ON': - build_ext.spawn(['make', 'RUN_TESTSUITE']) - - os.chdir(str(root_dir)) - - class build_ext(_build_ext): - - def run(self): - for ext in self.extensions: - ext.build(self) - - class build_py(_build_py): - - def run(self): - self.run_command('build_ext') # ensure `build_ext` runs before `build_py` - super().run() - - setup( - name = 'LibCarna-Python', - version = VERSION_LIBCARNA_PYTHON, - description = 'General-purpose real-time 3D visualization', - long_description = long_description, - long_description_content_type = 'text/markdown', - author = 'Leonid Kostrykin', - author_email = 'leonid.kostrykin@bioquant.uni-heidelberg.de', - url = 'https://github.com/kostrykin/LibCarna-Python', - include_package_data = True, - license = 'MIT', - license_files = [ - 'LICENSE', - 'build/make_release/licenses/LICENSE-Carna', - 'build/make_release/licenses/LICENSE-LibCarna', - 'build/make_release/licenses/LICENSE-Eigen', - 'build/make_release/licenses/LICENSE-GLEW', - ], - package_dir = { - 'libcarna': 'build/make_release/libcarna', - }, - packages = ['libcarna'], - package_data = { - 'libcarna': ['*.so'], - }, - ext_modules = [CMakeExtension()], - cmdclass={ - 'build_ext': build_ext, - 'build_py': build_py, - }, - classifiers = [ - 'Development Status :: 4 - Beta', - 'Environment :: GPU', - 'Operating System :: POSIX :: Linux', - 'Programming Language :: C++', - 'Programming Language :: Python', - 'Topic :: Education', - 'Topic :: Multimedia :: Graphics :: 3D Rendering', - 'Topic :: Scientific/Engineering :: Visualization', - 'Topic :: Software Development :: User Interfaces', - ], - install_requires = [ - 'numpy', - 'numpngw >=0.1.4, <0.2', - 'scikit-video >=1.1.11, <1.2', - 'scipy', - 'scikit-image', - 'tifffile', - 'pooch', - 'matplotlib', - 'typing_extensions', # required for Python 3.10 - ], - ) - diff --git a/setup.py.in b/setup.py.in new file mode 100644 index 0000000..db3a5a9 --- /dev/null +++ b/setup.py.in @@ -0,0 +1,55 @@ +from setuptools import setup + + +if __name__ == '__main__': + with open('README.md', encoding='utf-8') as io: + long_description = io.read() + + setup( + name = 'LibCarna-Python', + version = '@FULL_VERSION@', + description = 'General-purpose real-time 3D visualization', + long_description = long_description, + long_description_content_type = 'text/markdown', + author = 'Leonid Kostrykin', + author_email = 'leonid.kostrykin@bioquant.uni-heidelberg.de', + url = 'https://github.com/kostrykin/LibCarna-Python', + include_package_data = True, + license = 'MIT', + license_files = [ + 'LICENSE', + 'LICENSE-Carna', + 'LICENSE-LibCarna', + 'LICENSE-Eigen', + 'LICENSE-GLEW', + ], + package_dir = { + 'libcarna': 'libcarna', + }, + packages = ['libcarna'], + package_data = { + 'libcarna': ['*.so'], + }, + classifiers = [ + 'Development Status :: 4 - Beta', + 'Environment :: GPU', + 'Operating System :: POSIX :: Linux', + 'Programming Language :: C++', + 'Programming Language :: Python', + 'Topic :: Education', + 'Topic :: Multimedia :: Graphics :: 3D Rendering', + 'Topic :: Scientific/Engineering :: Visualization', + ], + install_requires = [ + 'numpy', + 'numpngw >=0.1.4, <0.2', + 'scikit-video >=1.1.11, <1.2', + 'scipy', + 'scikit-image', + 'tifffile', + 'pooch', + 'matplotlib', + 'typing_extensions', # required for Python 3.10 + ], + ) + diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt deleted file mode 100644 index 03900f9..0000000 --- a/test/CMakeLists.txt +++ /dev/null @@ -1,51 +0,0 @@ -cmake_minimum_required( VERSION 3.5 ) - -############################################ -# Run the tests -############################################ - -set( TEST_FILES - test_base - test_cutting_planes - test_data - test_drr - test_dvr - test_egl - test_helpers - test_integration - test_mask_renderer - test_material - test_mip - test_opaque_renderer - test_presets - test_spatial -) - -configure_file( - ${CMAKE_CURRENT_SOURCE_DIR}/testsuite.py - ${CMAKE_CURRENT_BINARY_DIR}/../testsuite.py COPYONLY ) - -execute_process( - COMMAND ${CMAKE_COMMAND} -E copy_directory - ${CMAKE_CURRENT_SOURCE_DIR}/results - ${CMAKE_CURRENT_BINARY_DIR}/../test/results ) - -foreach( TEST_FILE ${TEST_FILES} ) - configure_file( - ${CMAKE_CURRENT_SOURCE_DIR}/${TEST_FILE}.py - ${CMAKE_CURRENT_BINARY_DIR}/../${TEST_FILE}.py COPYONLY ) - - add_custom_target( - ${TEST_FILE} - COMMAND ${PYTHON_EXECUTABLE} -Xfaulthandler -m unittest ${TEST_FILE} - WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/.." - DEPENDS ${MODULES} - COMMENT "Running ${TEST_FILE}..." - ) -endforeach( TEST_FILE ) - -add_custom_target( - RUN_TESTSUITE - DEPENDS ${TEST_FILES} - COMMENT "Running test suite..." -) \ No newline at end of file diff --git a/test/test_base.py b/test/test_base.py index e06182e..1e99054 100644 --- a/test/test_base.py +++ b/test/test_base.py @@ -1,7 +1,7 @@ import numpy as np import libcarna.base -import testsuite +from . import testsuite class SpatialMixin: diff --git a/test/test_cutting_planes.py b/test/test_cutting_planes.py index baf0575..e97714a 100644 --- a/test/test_cutting_planes.py +++ b/test/test_cutting_planes.py @@ -1,7 +1,7 @@ import numpy as np import libcarna -import testsuite +from . import testsuite class cutting_planes(testsuite.LibCarnaTestCase): diff --git a/test/test_data.py b/test/test_data.py index cbe8a6a..a844138 100644 --- a/test/test_data.py +++ b/test/test_data.py @@ -1,7 +1,7 @@ import numpy as np import libcarna -import testsuite +from . import testsuite class drr(testsuite.LibCarnaTestCase): diff --git a/test/test_drr.py b/test/test_drr.py index 131f4a9..17f7fc2 100644 --- a/test/test_drr.py +++ b/test/test_drr.py @@ -1,5 +1,5 @@ import libcarna -import testsuite +from . import testsuite class drr(testsuite.LibCarnaTestCase): diff --git a/test/test_dvr.py b/test/test_dvr.py index b4aeb14..8512ee9 100644 --- a/test/test_dvr.py +++ b/test/test_dvr.py @@ -1,5 +1,5 @@ import libcarna -import testsuite +from . import testsuite class dvr(testsuite.LibCarnaTestCase): diff --git a/test/test_egl.py b/test/test_egl.py index 46fab4b..338503e 100644 --- a/test/test_egl.py +++ b/test/test_egl.py @@ -2,7 +2,7 @@ import libcarna.egl -import testsuite +from . import testsuite class EGLContext(testsuite.LibCarnaTestCase): diff --git a/test/test_helpers.py b/test/test_helpers.py index 806540a..6fda591 100644 --- a/test/test_helpers.py +++ b/test/test_helpers.py @@ -1,7 +1,7 @@ import libcarna.helpers import numpy as np -import testsuite +from . import testsuite class VolumeGridHelper: diff --git a/test/test_integration.py b/test/test_integration.py index 0d81bb2..4d38412 100644 --- a/test/test_integration.py +++ b/test/test_integration.py @@ -1,7 +1,7 @@ import numpy as np import libcarna -import testsuite +from . import testsuite class VolumeGridHelper_IntensityVolumeUInt16(testsuite.LibCarnaTestCase): diff --git a/test/test_mask_renderer.py b/test/test_mask_renderer.py index 88e7913..9eeb22e 100644 --- a/test/test_mask_renderer.py +++ b/test/test_mask_renderer.py @@ -1,5 +1,5 @@ import libcarna -import testsuite +from . import testsuite class mask_renderer(testsuite.LibCarnaTestCase): diff --git a/test/test_material.py b/test/test_material.py index 1b57721..2d72723 100644 --- a/test/test_material.py +++ b/test/test_material.py @@ -1,6 +1,6 @@ import libcarna -import testsuite +from . import testsuite class material(testsuite.LibCarnaTestCase): diff --git a/test/test_mip.py b/test/test_mip.py index 8adbedf..651b293 100644 --- a/test/test_mip.py +++ b/test/test_mip.py @@ -1,5 +1,5 @@ import libcarna -import testsuite +from . import testsuite class mip(testsuite.LibCarnaTestCase): diff --git a/test/test_opaque_renderer.py b/test/test_opaque_renderer.py index f951037..0971106 100644 --- a/test/test_opaque_renderer.py +++ b/test/test_opaque_renderer.py @@ -1,5 +1,5 @@ import libcarna -import testsuite +from . import testsuite class opaque_renderer(testsuite.LibCarnaTestCase): diff --git a/test/test_presets.py b/test/test_presets.py index 6817b07..05cc781 100644 --- a/test/test_presets.py +++ b/test/test_presets.py @@ -1,7 +1,7 @@ import libcarna.presets import numpy as np -import testsuite +from . import testsuite class VolumeRenderingStage(testsuite.LibCarnaTestCase): diff --git a/test/test_spatial.py b/test/test_spatial.py index be81c04..a312e5e 100644 --- a/test/test_spatial.py +++ b/test/test_spatial.py @@ -2,7 +2,7 @@ import libcarna -import testsuite +from . import testsuite class node(testsuite.LibCarnaTestCase): diff --git a/test/testsuite.py b/test/testsuite.py index 1004843..1f17144 100644 --- a/test/testsuite.py +++ b/test/testsuite.py @@ -120,7 +120,7 @@ def verify_frame(actual, expected): raise ValueError(f'Unsupported array shape: {actual.shape}') def assert_image_almost_expected(self, actual, **kwargs): - expected = f'{self.id()}.png' + expected = f'{self.id()}.png'.removeprefix('test.') try: self.assert_image_almost_equal(actual, expected, **kwargs) except: