diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml index 24700a5..2389faa 100644 --- a/.github/workflows/github-actions.yml +++ b/.github/workflows/github-actions.yml @@ -1,5 +1,5 @@ -name: build -on: [push, pull_request] +name: build-and-test-with-wheels +on: [pull_request, workflow_dispatch] jobs: test: name: ${{ matrix.name }} @@ -19,103 +19,139 @@ jobs: toxpython: 'python3.11' tox_env: 'docs' os: 'ubuntu-latest' - - name: 'py39 (ubuntu)' + - name: 'py39 (ubuntu/x86_64)' python: '3.9' toxpython: 'python3.9' python_arch: 'x64' tox_env: 'py39' + cibw_arch: 'x86_64' + cibw_build: false os: 'ubuntu-latest' - - name: 'py39 (windows)' + - name: 'py39 (windows/AMD64)' python: '3.9' toxpython: 'python3.9' python_arch: 'x64' tox_env: 'py39' + cibw_arch: 'AMD64' + cibw_build: false os: 'windows-latest' - - name: 'py39 (macos x64)' + - name: 'py39 (macos/x86_64)' python: '3.9' toxpython: 'python3.9' python_arch: 'x64' tox_env: 'py39' + cibw_arch: 'x86_64' + cibw_build: false os: 'macos-13' - - name: 'py310 (ubuntu)' + - name: 'py39 (macos/arm64)' + python: '3.9' + toxpython: 'python3.9' + python_arch: 'arm64' + tox_env: 'py39' + cibw_arch: 'arm64' + cibw_build: false + os: 'macos-latest' + - name: 'py310 (ubuntu/x86_64)' python: '3.10' toxpython: 'python3.10' python_arch: 'x64' tox_env: 'py310' + cibw_arch: 'x86_64' + cibw_build: false os: 'ubuntu-latest' - - name: 'py310 (windows)' + - name: 'py310 (windows/AMD64)' python: '3.10' toxpython: 'python3.10' python_arch: 'x64' tox_env: 'py310' + cibw_arch: 'AMD64' + cibw_build: false os: 'windows-latest' - - name: 'py310 (macos x64)' + - name: 'py310 (macos/x86_64)' python: '3.10' toxpython: 'python3.10' python_arch: 'x64' tox_env: 'py310' + cibw_arch: 'x86_64' + cibw_build: false os: 'macos-13' - - name: 'py311 (ubuntu)' + - name: 'py310 (macos/arm64)' + python: '3.10' + toxpython: 'python3.10' + python_arch: 'arm64' + tox_env: 'py310' + cibw_arch: 'arm64' + cibw_build: false + os: 'macos-latest' + - name: 'py311 (ubuntu/x86_64)' python: '3.11' toxpython: 'python3.11' python_arch: 'x64' tox_env: 'py311' + cibw_arch: 'x86_64' + cibw_build: 'cp39-manylinux_x86_64' os: 'ubuntu-latest' - - name: 'py311 (windows)' + - name: 'py311 (windows/AMD64)' python: '3.11' toxpython: 'python3.11' python_arch: 'x64' tox_env: 'py311' + cibw_arch: 'AMD64' + cibw_build: 'cp39-win_amd64' os: 'windows-latest' - - name: 'py311 (macos x64 rosetta)' + - name: 'py311 (macos/x86_64)' python: '3.11' toxpython: 'python3.11' python_arch: 'x64' tox_env: 'py311' + cibw_arch: 'x86_64' + cibw_build: 'cp39-macosx_x86_64' + os: 'macos-13' + - name: 'py311 (macos/arm64)' + python: '3.11' + toxpython: 'python3.11' + python_arch: 'arm64' + tox_env: 'py311' + cibw_arch: 'arm64' + cibw_build: 'cp39-macosx_arm64' os: 'macos-latest' - - name: 'py312 (ubuntu)' + - name: 'py312 (ubuntu/x86_64)' python: '3.12' toxpython: 'python3.12' python_arch: 'x64' tox_env: 'py312' + cibw_arch: 'x86_64' + cibw_build: false os: 'ubuntu-latest' - - name: 'py312 (windows)' + - name: 'py312 (windows/AMD64)' python: '3.12' toxpython: 'python3.12' python_arch: 'x64' tox_env: 'py312' + cibw_arch: 'AMD64' + cibw_build: false os: 'windows-latest' - - name: 'py312 (macos x64 rosetta)' + - name: 'py312 (macos/x86_64)' python: '3.12' toxpython: 'python3.12' python_arch: 'x64' tox_env: 'py312' - os: 'macos-latest' - - name: 'py39 (macos arm64 apple silicon)' - python: '3.9' - toxpython: 'python3.9' - python_arch: 'arm64' - tox_env: 'py39' - os: 'macos-latest' - - name: 'py310 (macos arm64 apple silicon)' - python: '3.10' - toxpython: 'python3.10' - python_arch: 'arm64' - tox_env: 'py310' - os: 'macos-latest' - - name: 'py311 (macos arm64 apple silicon)' - python: '3.11' - toxpython: 'python3.11' - python_arch: 'arm64' - tox_env: 'py311' - os: 'macos-latest' - - name: 'py312 (macos arm64 apple silicon)' + cibw_arch: 'x86_64' + cibw_build: false + os: 'macos-13' + - name: 'py312 (macos/arm64)' python: '3.12' toxpython: 'python3.12' python_arch: 'arm64' tox_env: 'py312' + cibw_arch: 'arm64' + cibw_build: false os: 'macos-latest' steps: + - uses: docker/setup-qemu-action@v3 + if: matrix.cibw_arch == 'aarch64' + with: + platforms: arm64 - uses: actions/checkout@v4 with: fetch-depth: 0 @@ -125,13 +161,54 @@ jobs: architecture: ${{ matrix.python_arch }} - name: install dependencies run: | - python -mpip install --progress-bar=off -r ci/requirements.txt + python -mpip install --progress-bar=off cibuildwheel -r ci/requirements.txt virtualenv --version pip --version tox --version pip list --format=freeze - - name: test + - name: cibw build and test + if: matrix.cibw_build + run: cibuildwheel + env: + TOXPYTHON: '${{ matrix.toxpython }}' + CIBW_ARCHS: '${{ matrix.cibw_arch }}' + CIBW_BUILD: '${{ matrix.cibw_build }}' + CIBW_BUILD_VERBOSITY: '3' + CIBW_TEST_REQUIRES: > + tox + tox-direct + CIBW_TEST_COMMAND: > + cd {project} && + tox --skip-pkg-install --direct-yolo -e ${{ matrix.tox_env }} -v + CIBW_TEST_COMMAND_WINDOWS: > + cd /d {project} && + tox --skip-pkg-install --direct-yolo -e ${{ matrix.tox_env }} -v + - name: regular build and test env: TOXPYTHON: '${{ matrix.toxpython }}' + if: > + !matrix.cibw_build run: > tox -e ${{ matrix.tox_env }} -v + - uses: codecov/codecov-action@v3 + if: matrix.cover + with: + verbose: true + flags: ${{ matrix.tox_env }} + - name: check wheel + if: matrix.cibw_build + run: twine check wheelhouse/*.whl + - name: upload wheel + uses: actions/upload-artifact@v4 + if: matrix.cibw_build + with: + name: ${{ matrix.cibw_build }} + path: wheelhouse/*.whl + finish: + needs: test + if: ${{ false && always() }} + runs-on: ubuntu-latest + steps: + - uses: codecov/codecov-action@v5 + with: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 54ec7a4..7268d42 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: hooks: - id: black - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v5.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer diff --git a/docs/source/laser_core.rst b/docs/source/laser_core.rst index 0f89d57..9a90f37 100644 --- a/docs/source/laser_core.rst +++ b/docs/source/laser_core.rst @@ -20,6 +20,14 @@ laser\_core.cli module :undoc-members: :show-inheritance: +laser\_core.extension module +---------------------------- + +.. automodule:: laser_core.extension + :members: + :undoc-members: + :show-inheritance: + laser\_core.laserframe module ----------------------------- diff --git a/pyproject.toml b/pyproject.toml index ee4a977..6441168 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -122,6 +122,9 @@ exclude_also = [ [tool.setuptools] packages = ["laser_core"] +ext-modules = [ + {name = "_extension", sources = ["src/laser_core/_extension.c"]} +] [tool.setuptools.package-data] "tests" = ["data/*.csv"] diff --git a/src/laser_core/__init__.py b/src/laser_core/__init__.py index 1431d57..31712db 100644 --- a/src/laser_core/__init__.py +++ b/src/laser_core/__init__.py @@ -1,5 +1,6 @@ __version__ = "0.4.0" +from .extension import compiled from .laserframe import LaserFrame from .propertyset import PropertySet from .sortedqueue import SortedQueue @@ -9,4 +10,5 @@ "PropertySet", "SortedQueue", "__version__", + "compiled", ] diff --git a/src/laser_core/_extension.c b/src/laser_core/_extension.c new file mode 100644 index 0000000..92a2d7c --- /dev/null +++ b/src/laser_core/_extension.c @@ -0,0 +1,82 @@ +#include "Python.h" + +static PyObject* compiled(PyObject *self, PyObject *value) { + PyObject *module; + PyObject *module_dict; + PyObject *len; + PyObject *max; + PyObject *args; + PyObject *kwargs; + PyObject *result; + module = PyImport_ImportModule("builtins"); + if (!module) + return NULL; + + module_dict = PyModule_GetDict(module); + len = PyDict_GetItemString(module_dict, "len"); + if (!len) { + Py_DECREF(module); + return NULL; + } + max = PyDict_GetItemString(module_dict, "max"); + if (!max) { + Py_DECREF(module); + return NULL; + } + Py_DECREF(module); + + args = PyTuple_New(1); + if (!args) { + return NULL; + } + Py_INCREF(value); + PyTuple_SetItem(args, 0, value); + + kwargs = PyDict_New(); + if (!kwargs) { + Py_DECREF(args); + return NULL; + } + PyDict_SetItemString(kwargs, "key", len); + + result = PyObject_Call(max, args, kwargs); + + Py_DECREF(args); + Py_DECREF(kwargs); + + return result; +} + +PyDoc_STRVAR(compiled_doc, "Docstring for compiled function."); + +static struct PyMethodDef module_functions[] = { + {"compiled", compiled, METH_O, compiled_doc}, + {NULL, NULL} +}; + +static struct PyModuleDef moduledef = { + PyModuleDef_HEAD_INIT, + "laser_generic._core", /* m_name */ + NULL, /* m_doc */ + -1, /* m_size */ + module_functions, /* m_methods */ + NULL, /* m_reload */ + NULL, /* m_traverse */ + NULL, /* m_clear */ + NULL, /* m_free */ +}; + +static PyObject* moduleinit(void) { + PyObject *module; + + module = PyModule_Create(&moduledef); + + if (module == NULL) + return NULL; + + return module; +} + +PyMODINIT_FUNC PyInit__extension(void) { + return moduleinit(); +} diff --git a/src/laser_core/extension.py b/src/laser_core/extension.py new file mode 100644 index 0000000..1e82f5c --- /dev/null +++ b/src/laser_core/extension.py @@ -0,0 +1,7 @@ +try: + from ._extension import compiled +except ImportError: + + def compiled(args): + print("Warning: LASER extension not compiled, falling back to Python implementation.") + return max(args, key=len) diff --git a/tox.ini b/tox.ini index 9d4da03..1bfe4f0 100644 --- a/tox.ini +++ b/tox.ini @@ -35,6 +35,7 @@ extras = test deps = pytest pytest-cov + scipy commands = {posargs:pytest --cov --cov-report=term-missing --cov-report=xml -vv -s tests}