diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index bda4a6c..d467935 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -11,41 +11,69 @@ on: jobs: build: - + name: Python ${{ matrix.python-version }} (${{ matrix.backend }}) runs-on: ubuntu-latest strategy: fail-fast: false matrix: - python-version: ["3.7", "3.8", "3.9", "3.10"] - cython: ['python -m pip install -q cython', 'echo "No Cython"'] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + # PySpike treats Cython as an optional accelerator. Both install paths + # need to work and produce a passing test suite: + # + # cython -- `pip install .` uses pip's isolated build env, which + # installs Cython per pyproject.toml's build-system + # requires. setup.py compiles the .pyx sources into + # .so modules and the fast path is used at runtime. + # + # no-cython -- `pip install --no-build-isolation .` with Cython + # deliberately absent. setup.py's `try: import Cython` + # raises ImportError, no extensions are built, and + # each pyspike module falls back to python_backend.py + # via its own `try/except ImportError`. + backend: [cython, no-cython] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - - name: Install dependencies + + - name: Install system dependencies run: | sudo apt-get update - sudo apt-get install libblas-dev - sudo apt-get install liblapack-dev - sudo apt-get install gfortran - python -m pip install --upgrade pip - python -m pip install flake8 pytest nose numpy scipy - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - - name: Install Cython - run: | - ${{ matrix.cython }} - - name: Install package + sudo apt-get install -y libblas-dev liblapack-dev gfortran + + - name: Upgrade pip + run: python -m pip install --upgrade pip + + - name: Install PySpike — with Cython (build isolation) + if: matrix.backend == 'cython' + run: pip install . + + - name: Install PySpike — without Cython (pure-Python fallback) + if: matrix.backend == 'no-cython' run: | - python SetupNoPrompt.py build_ext --inplace - # - name: Lint with flake8 - # run: | - # # stop the build if there are Python syntax errors or undefined names - # flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - # flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + # Provide only the bare build prereqs Cython is not among them, so + # setup.py's `try: from Cython.Distutils import build_ext` fails and + # no C extensions are produced. --no-build-isolation makes pip use + # this env instead of provisioning a fresh one (which would install + # Cython per pyproject.toml). + pip install "setuptools>=77" wheel "numpy>=1.25" + pip install --no-build-isolation . + # Sanity check: the compiled extension must NOT be importable. + python -c " + try: + from pyspike.cython import cython_distances + except ImportError: + print('OK: cython_distances absent, runtime will use python_backend') + else: + raise SystemExit('FAIL: cython_distances was built despite no-cython matrix') + " + + - name: Install test dependencies + run: pip install pytest scipy + - name: Test with PyTest - run: | - python -m pytest + run: python -m pytest diff --git a/MANIFEST.in b/MANIFEST.in index aed0ae0..91b877a 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,7 +1,13 @@ include *.rst include *.txt -include pyspike/cython/*.c -include directionality/cython/*.c +include License.txt +include Changelog +include pyproject.toml + +# Cython sources — required so `pip install` from sdist can regenerate +# the .c files and build the extension modules. +recursive-include pyspike/cython *.pyx *.pxd + recursive-include examples *.py *.txt recursive-include test *.py *.txt recursive-include doc * diff --git a/SetupNoPrompt.py b/SetupNoPrompt.py deleted file mode 100644 index 90c3270..0000000 --- a/SetupNoPrompt.py +++ /dev/null @@ -1,5 +0,0 @@ -## interlude to force answer to input('Abort?'): - -import io, sys -sys.stdin = io.StringIO('N\n') -import setup diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..fbcf413 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,53 @@ +[build-system] +# setuptools >=77 supports the SPDX-string form `license = "BSD-2-Clause"` +# in [project] (PEP 639). numpy and Cython are required because setup.py +# calls numpy.get_include() and compiles .pyx sources for the C extensions. +requires = [ + "setuptools>=77", + "wheel", + "Cython>=3.0", + "numpy>=1.25", +] +build-backend = "setuptools.build_meta" + +[project] +name = "pyspike" +version = "0.8.1" +description = "A Python library for the numerical analysis of spike train similarity" +readme = "Readme.rst" +requires-python = ">=3.9" +license = "BSD-2-Clause" +license-files = ["License.txt"] +authors = [ + { name = "Mario Mulansky", email = "mario.mulansky@gmx.net" }, +] +keywords = ["data analysis", "spike", "neuroscience"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Science/Research", + "Topic :: Scientific/Engineering", + "Topic :: Scientific/Engineering :: Information Analysis", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", +] +dependencies = [ + "numpy", +] + +[project.urls] +Homepage = "https://github.com/mariomulansky/PySpike" +Repository = "https://github.com/mariomulansky/PySpike" +Issues = "https://github.com/mariomulansky/PySpike/issues" + +[tool.setuptools] +include-package-data = true + +[tool.setuptools.packages.find] +exclude = ["doc*", "test*", "examples*"] +# Cython .c files are generated at build time (.gitignore excludes them) and +# the resulting .so files are placed automatically. No package_data needed +# for the wheel; MANIFEST.in handles inclusion of .pyx sources in the sdist. diff --git a/pyspike/__init__.py b/pyspike/__init__.py index 4e31120..1003bbc 100644 --- a/pyspike/__init__.py +++ b/pyspike/__init__.py @@ -35,23 +35,17 @@ spike_train_order_bi, spike_train_order_multi, \ optimal_spike_train_sorting, permutate_matrix -# define the __version__ following -# http://stackoverflow.com/questions/17583443 -from pkg_resources import get_distribution, DistributionNotFound -import os.path +# Expose the installed version via importlib.metadata (stdlib since 3.8). +# Previously this used pkg_resources, which depends on setuptools — and from +# Python 3.12 onwards venvs no longer ship setuptools by default, so the old +# import would fail at runtime in fresh 3.12+ environments. +from importlib.metadata import version as _get_version, PackageNotFoundError try: - _dist = get_distribution('pyspike') - # Normalize case for Windows systems - dist_loc = os.path.normcase(_dist.location) - here = os.path.normcase(__file__) - if not here.startswith(os.path.join(dist_loc, 'pyspike')): - # not installed, but there is another version that *is* - raise DistributionNotFound -except DistributionNotFound: - __version__ = 'Please install this project with setup.py' -else: - __version__ = _dist.version + __version__ = _get_version("pyspike") +except PackageNotFoundError: + # Running from a source checkout that hasn't been installed. + __version__ = "0.0.0+unknown" disable_backend_warning = False diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index c855aaa..0000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[metadata] -description_file = Readme.rst diff --git a/setup.py b/setup.py index b52cf8b..3a55f1a 100644 --- a/setup.py +++ b/setup.py @@ -1,18 +1,24 @@ """ setup.py -to compile cython files: -python setup.py build_ext --inplace +Compile the Cython extensions for PySpike. +All packaging metadata (name, version, dependencies, classifiers, ...) lives in +pyproject.toml. This file only declares the C extension modules, because that +still needs imperative setup() configuration. -Copyright 2014-2017, Mario Mulansky +To compile cython files in-place: + python setup.py build_ext --inplace + + +Copyright 2014-2026, Mario Mulansky Distributed under the BSD License """ -from setuptools import setup, find_packages -from distutils.extension import Extension import os.path +from setuptools import Extension, setup + try: from Cython.Distutils import build_ext except ImportError: @@ -22,110 +28,62 @@ class numpy_include(os.PathLike): - """Defers import of numpy until install_requires is through""" - def __str__(self): - import numpy - return numpy.get_include() - - def __fspath__(self): - return str(self) - - -if os.path.isfile("pyspike/cython/cython_add.c") and \ - os.path.isfile("pyspike/cython/cython_get_tau.c") and \ - os.path.isfile("pyspike/cython/cython_profiles.c") and \ - os.path.isfile("pyspike/cython/cython_distances.c") and \ - os.path.isfile("pyspike/cython/cython_directionality.c") and \ - os.path.isfile("pyspike/cython/cython_simulated_annealing.c"): - use_c = True -else: - use_c = False + """Defers import of numpy until the build environment is in place. + + pyproject.toml lists numpy as a build-system requirement, so by the time + setup.py actually runs build_ext, numpy is importable. We can't import it + at module top level, though, because setuptools imports setup.py before + build-system requires are installed. + """ + + def __str__(self): + import numpy + return numpy.get_include() + + def __fspath__(self): + return str(self) + + +_CYTHON_MODULES = ( + "cython_add", + "cython_get_tau", + "cython_profiles", + "cython_distances", + "cython_directionality", + "cython_simulated_annealing", +) + + +def _all_c_sources_present(): + return all( + os.path.isfile(f"pyspike/cython/{name}.c") for name in _CYTHON_MODULES + ) + + +use_c = _all_c_sources_present() if not use_cython and not use_c: - print('Cython not installed. Programs will be slow.') - # Ans = input('Abort? (Y/N)\n') - # if len(Ans)>0 and (Ans[0]=='Y' or Ans[0]=='y'): - # print("\nAborting\n") - # raise RuntimeError('User termination') + print("Cython not installed and no pre-generated .c files found. " + "PySpike will fall back to the pure-Python backend (slow).") cmdclass = {} ext_modules = [] -if use_cython: # Cython is available, compile .pyx -> .c - ext_modules += [ - Extension("pyspike.cython.cython_add", - ["pyspike/cython/cython_add.pyx"]), - Extension("pyspike.cython.cython_get_tau", - ["pyspike/cython/cython_get_tau.pyx"]), - Extension("pyspike.cython.cython_profiles", - ["pyspike/cython/cython_profiles.pyx"]), - Extension("pyspike.cython.cython_distances", - ["pyspike/cython/cython_distances.pyx"]), - Extension("pyspike.cython.cython_directionality", - ["pyspike/cython/cython_directionality.pyx"]), - Extension("pyspike.cython.cython_simulated_annealing", - ["pyspike/cython/cython_simulated_annealing.pyx"]) +if use_cython: # Cython is available, compile .pyx -> .c -> binary + ext_modules = [ + Extension(f"pyspike.cython.{name}", [f"pyspike/cython/{name}.pyx"]) + for name in _CYTHON_MODULES ] - cmdclass.update({'build_ext': build_ext}) -elif use_c: # c files are there, compile to binaries - ext_modules += [ - Extension("pyspike.cython.cython_add", - ["pyspike/cython/cython_add.c"]), - Extension("pyspike.cython.cython_get_tau", - ["pyspike/cython/cython_get_tau.c"]), - Extension("pyspike.cython.cython_profiles", - ["pyspike/cython/cython_profiles.c"]), - Extension("pyspike.cython.cython_distances", - ["pyspike/cython/cython_distances.c"]), - Extension("pyspike.cython.cython_directionality", - ["pyspike/cython/cython_directionality.c"]), - Extension("pyspike.cython.cython_simulated_annealing", - ["pyspike/cython/cython_simulated_annealing.c"]) + cmdclass["build_ext"] = build_ext +elif use_c: # No Cython, but pre-generated .c files are present + ext_modules = [ + Extension(f"pyspike.cython.{name}", [f"pyspike/cython/{name}.c"]) + for name in _CYTHON_MODULES ] -# neither cython nor c files available -> automatic fall-back to python backend +# else: neither Cython nor .c files — fall through to pure-Python backend. setup( - name='pyspike', - packages=find_packages(exclude=['doc', 'test*']), - version='0.8.0', cmdclass=cmdclass, ext_modules=ext_modules, include_dirs=[numpy_include()], - description='A Python library for the numerical analysis of spike\ -train similarity', - author='Mario Mulansky', - author_email='mario.mulansky@gmx.net', - license='BSD', - url='https://github.com/mariomulansky/PySpike', - install_requires=['numpy'], - keywords=['data analysis', 'spike', 'neuroscience'], # arbitrary keywords - classifiers=[ - # How mature is this project? Common values are - # 3 - Alpha - # 4 - Beta - # 5 - Production/Stable - 'Development Status :: 4 - Beta', - - # Indicate who your project is intended for - 'Intended Audience :: Science/Research', - 'Topic :: Scientific/Engineering', - 'Topic :: Scientific/Engineering :: Information Analysis', - - 'License :: OSI Approved :: BSD License', - - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - ], - package_data={ - 'pyspike': ['cython/cython_add.c', - 'cython/cython_profiles.c', - 'cython/cython_get_tau.c', - 'cython/cython_distances.c', - 'cython/cython_directionality.c', - 'cython/cython_simulated_annealing.c'], - 'test': ['Spike_testdata.txt'] - } )