From a8af1c1d2821b68b2e3c48e2bcb933c1f0f8f77f Mon Sep 17 00:00:00 2001 From: Mario Mulansky Date: Mon, 11 May 2026 10:29:05 -0700 Subject: [PATCH 1/3] Modernize packaging; fix pip install with build isolation (#84) Issue #84: 'pip install pyspike' fails on modern pip/setuptools with 'ModuleNotFoundError: No module named numpy' because pip builds in an isolated environment that doesn't see numpy from the user's venv, but setup.py calls numpy.get_include() for the Cython extensions. - Add pyproject.toml with full [project] metadata moved from setup.py - Declare numpy, Cython, setuptools>=77 and wheel in build-system.requires - Replace deprecated BSD License classifier with SPDX 'BSD-2-Clause' - Remove setup.cfg (description-file deprecation), metadata in pyproject.toml - Update supported Python versions (drop 3.7/3.8 EOL, add 3.11/3.12/3.13) - Refresh CI to use pip install . so the matrix actually tests the build-isolation path that broke - Slim setup.py to only the Cython/ext_modules logic - Update MANIFEST.in to ship .pyx/.pxd sources in sdist - Remove SetupNoPrompt.py (workaround for a long-disabled input() prompt) Closes #84. --- .github/workflows/python-package.yml | 31 ++---- MANIFEST.in | 10 +- SetupNoPrompt.py | 5 - pyproject.toml | 53 +++++++++ setup.cfg | 2 - setup.py | 154 ++++++++++----------------- 6 files changed, 128 insertions(+), 127 deletions(-) delete mode 100644 SetupNoPrompt.py create mode 100644 pyproject.toml delete mode 100644 setup.cfg diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index bda4a6c..067622a 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -16,36 +16,27 @@ jobs: 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"] 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 + sudo apt-get install -y libblas-dev liblapack-dev gfortran + - name: Upgrade pip + run: | 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 + - name: Install PySpike (uses build isolation; reproduces the pip-install path) run: | - ${{ matrix.cython }} - - name: Install package + pip install . + - name: Install test dependencies 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 + pip install pytest scipy - name: Test with 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/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'] - } ) From e850b28b53e07b464f68b91fad5bc3ab0b21b2b9 Mon Sep 17 00:00:00 2001 From: Mario Mulansky Date: Mon, 11 May 2026 10:45:25 -0700 Subject: [PATCH 2/3] Re-enable no-cython test cases --- .github/workflows/python-package.yml | 55 +++++++++++++++++++++++----- 1 file changed, 46 insertions(+), 9 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 067622a..d467935 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -11,32 +11,69 @@ on: jobs: build: - + name: Python ${{ matrix.python-version }} (${{ matrix.backend }}) runs-on: ubuntu-latest strategy: fail-fast: false matrix: 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@v4 + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} + - name: Install system dependencies run: | sudo apt-get update 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 -m pip install --upgrade pip - - name: Install PySpike (uses build isolation; reproduces the pip-install path) - run: | - pip install . + # 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 + run: pip install pytest scipy + - name: Test with PyTest - run: | - python -m pytest + run: python -m pytest From faf13fc2c6946bc7d38c48d2fd50079e732c51b1 Mon Sep 17 00:00:00 2001 From: Mario Mulansky Date: Mon, 11 May 2026 10:51:59 -0700 Subject: [PATCH 3/3] Use importlib.metadata for __version__ (fix Python 3.12+) pkg_resources is provided by setuptools, and starting with Python 3.12 venv no longer installs setuptools by default. After 'pip install .' in a fresh 3.12+ environment the runtime venv contains only pyspike and numpy, so 'import pyspike' raised ModuleNotFoundError: No module named 'pkg_resources' and every test file failed to collect (pytest exit code 2). Switch to importlib.metadata.version, which has been in the standard library since Python 3.8 and works regardless of whether setuptools is present at runtime. --- pyspike/__init__.py | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) 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