diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 48f9f14d7e..991e8f92b2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,7 +5,30 @@ on: ["push", "pull_request"] permissions: {} jobs: + test-build-helpers: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + python-version: + - "3.10" + - "3.14" + os: + - ubuntu-latest + - macos-latest + - windows-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 + with: + python-version: ${{ matrix.python-version }} + no-project: true + - run: uvx pytest build_helpers + test-linux-macos: + needs: [test-build-helpers] runs-on: ${{ matrix.os }} strategy: fail-fast: false @@ -25,6 +48,7 @@ jobs: - uses: ./.github/bottleneck-action test-windows: + needs: [test-build-helpers] runs-on: ${{ matrix.os }} strategy: fail-fast: false @@ -39,16 +63,16 @@ jobs: os: - windows-latest - windows-2022 - include: - - os: windows-11-arm - architecture: arm64 - python-version: "3.11" - - os: windows-11-arm - architecture: arm64 - python-version: "3.14" - - os: windows-11-arm - architecture: arm64 - python-version: "3.14t" + # include: + # - os: windows-11-arm + # architecture: arm64 + # python-version: "3.11" + # - os: windows-11-arm + # architecture: arm64 + # python-version: "3.14" + # - os: windows-11-arm + # architecture: arm64 + # python-version: "3.14t" steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: @@ -56,6 +80,7 @@ jobs: - uses: ./.github/bottleneck-action test-pyversions: + needs: [test-build-helpers] runs-on: ubuntu-latest strategy: fail-fast: false @@ -92,6 +117,7 @@ jobs: - test-windows runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: include: - os: ubuntu-latest @@ -127,6 +153,12 @@ jobs: fetch-depth: 0 persist-credentials: false + - name: Setup MSVC (32-bit) + if: ${{ startsWith(runner.os, 'windows') && matrix.archs == 'x86' }} + uses: bus1/cabuild/action/msdevshell@e22aba57d6e74891d059d66501b6b5aed8123c4d # v1 + with: + architecture: 'x86' + - name: Build wheels uses: pypa/cibuildwheel@8d2b08b68458a16aeb24b64e68a09ab1c8e82084 # v3.4.1 env: diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index e083e6ace5..0000000000 --- a/MANIFEST.in +++ /dev/null @@ -1,20 +0,0 @@ -include RELEASE.rst -include Makefile -include pyproject.toml - -recursive-include bottleneck/src *.c *.h -exclude bottleneck/src/reduce.c -exclude bottleneck/src/move.c -exclude bottleneck/src/nonreduce.c -exclude bottleneck/src/nonreduce_axis.c -exclude bottleneck/src/bn_config.h - -recursive-include doc * -recursive-exclude doc/build * -include versioneer.py -include bottleneck/_version.py -include bottleneck/conftest.py - -global-exclude __pycache__ -global-exclude *.pyc -global-exclude *~ diff --git a/Makefile b/Makefile deleted file mode 100644 index 79e4efe6db..0000000000 --- a/Makefile +++ /dev/null @@ -1,65 +0,0 @@ -# Bottleneck Makefile - -PYTHON=python - -srcdir := bottleneck - -help: - @echo "Available tasks:" - @echo "help --> This help page" - @echo "all --> clean, build, flake8, test" - @echo "build --> Build the Python C extensions" - @echo "clean --> Remove all the build files for a fresh start" - @echo "test --> Run unit tests" - @echo "flake8 --> Check for pep8 errors" - @echo "readme --> Update benchmark results in README.rst" - @echo "bench --> Run performance benchmark" - @echo "detail --> Detailed benchmarks for all functions" - @echo "sdist --> Make source distribution" - @echo "doc --> Build Sphinx manual" - @echo "pypi --> Upload to pypi" - -all: clean build test flake8 - -build: - ${PYTHON} setup.py build_ext --inplace - -test: - ${PYTHON} -c "import bottleneck;bottleneck.test()" - -lint: - ruff check - -format: - ruff format . --exclude "(build/|dist/|\.git/|\.mypy_cache/|\.tox/|\.venv/\.asv/|env|\.eggs)" - -readme: - PYTHONPATH=`pwd`:PYTHONPATH ${PYTHON} tools/update_readme.py - -bench: - ${PYTHON} -c "import bottleneck; bottleneck.bench()" - -detail: - ${PYTHON} -c "import bottleneck; bottleneck.bench_detailed('all')" - -sdist: clean - ${PYTHON} setup.py sdist - git status - -pypi: clean - ${PYTHON} setup.py sdist upload -r pypi - -# doc directory exists so use phony -.PHONY: doc -doc: clean build - rm -rf build/sphinx - ${PYTHON} setup.py build_sphinx - -clean: - rm -rf build dist Bottleneck.egg-info - find . -name \*.pyc -delete - rm -f MANIFEST - rm -rf ${srcdir}/*.html ${srcdir}/build - rm -rf ${srcdir}/*.c - rm -rf ${srcdir}/*.so - rm -rf ${srcdir}/bn_config.h diff --git a/README.rst b/README.rst index fe3ee99a01..ad40aa2f58 100644 --- a/README.rst +++ b/README.rst @@ -125,18 +125,12 @@ Unit tests pytest Documentation sphinx, numpydoc ======================== ============================================================================ -To install Bottleneck on Linux, Mac OS X, et al.: +To install Bottleneck: .. code-block:: console $ pip install . -To install bottleneck on Windows, first install MinGW and add it to your -system path. Then install Bottleneck with the command: - -.. code-block:: console - - $ python setup.py install --compiler=mingw32 Unit tests ========== diff --git a/bottleneck/__init__.py b/bottleneck/__init__.py index 801967e9fd..e4501cb8d2 100644 --- a/bottleneck/__init__.py +++ b/bottleneck/__init__.py @@ -37,11 +37,10 @@ test = PytestTester(__name__) del PytestTester -from ._version import get_versions # noqa: E402 -__version__ = get_versions()["version"] -del get_versions +def __getitem__(key: str): + if key == "__version__": + from importlib.metadata import version -from . import _version # noqa: E402 - -__version__ = _version.get_versions()["version"] + return version("bottleneck") + raise AttributeError diff --git a/bottleneck/src/bottleneck.h b/bottleneck/src/bottleneck.h index 371fefe99a..cab3326058 100644 --- a/bottleneck/src/bottleneck.h +++ b/bottleneck/src/bottleneck.h @@ -7,6 +7,11 @@ #include #include +#ifdef Py_LIMITED_API + #define PyTuple_GET_ITEM PyTuple_GetItem + #define PyTuple_GET_SIZE PyTuple_Size +#endif + /* THREADS=1 releases the GIL but increases function call * overhead. THREADS=0 does not release the GIL but keeps * function call overhead low. Curly brackets are for C89 diff --git a/bottleneck/src/move_template.c b/bottleneck/src/move.c.template similarity index 100% rename from bottleneck/src/move_template.c rename to bottleneck/src/move.c.template diff --git a/bottleneck/src/nonreduce_template.c b/bottleneck/src/nonreduce.c.template similarity index 100% rename from bottleneck/src/nonreduce_template.c rename to bottleneck/src/nonreduce.c.template diff --git a/bottleneck/src/nonreduce_axis_template.c b/bottleneck/src/nonreduce_axis.c.template similarity index 100% rename from bottleneck/src/nonreduce_axis_template.c rename to bottleneck/src/nonreduce_axis.c.template diff --git a/bottleneck/src/reduce_template.c b/bottleneck/src/reduce.c.template similarity index 100% rename from bottleneck/src/reduce_template.c rename to bottleneck/src/reduce.c.template diff --git a/bottleneck/tests/test_template.py b/bottleneck/tests/test_template.py deleted file mode 100644 index aab60865cc..0000000000 --- a/bottleneck/tests/test_template.py +++ /dev/null @@ -1,28 +0,0 @@ -import os -import posixpath as path - -import pytest - -from ..src.bn_template import make_c_files - - -@pytest.mark.thread_unsafe -def test_make_c_files() -> None: - dirpath = os.path.join(os.path.dirname(__file__), "data/template_test/") - modules = ["test"] - test_input = os.path.join(dirpath, "test.c") - if os.path.exists(test_input): - os.remove(test_input) - - make_c_files(dirpath=dirpath, modules=modules) - - with open(os.path.join(dirpath, "truth.c")) as f: - truth = f.read() - - with open(os.path.join(dirpath, "test.c")) as f: - test = f.read() - test = test.replace(path.relpath(dirpath), "{DIRPATH}") - - assert truth == test - - os.remove(test_input) diff --git a/build_helpers/__init__.py b/build_helpers/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/bottleneck/src/bn_config.py b/build_helpers/config.py similarity index 69% rename from bottleneck/src/bn_config.py rename to build_helpers/config.py index 77507a2e6d..b28f93d064 100644 --- a/bottleneck/src/bn_config.py +++ b/build_helpers/config.py @@ -1,9 +1,10 @@ +#!/usr/bin/env python3 """Based on numpy's approach to exposing compiler features via a config header. Unfortunately that file is not exposed, so re-implement the portions we need. """ -import os import textwrap +from pathlib import Path OPTIONAL_FUNCTION_ATTRIBUTES = [ ("HAVE_ATTRIBUTE_OPTIMIZE_OPT_3", '__attribute__((optimize("O3")))') @@ -90,25 +91,27 @@ def check_gcc_function_attribute(cmd, attribute, name): return cmd.try_compile(body, None, None) != 0 -def create_config_h(config): - dirname = os.path.dirname(__file__) - config_h = os.path.join(dirname, "bn_config.h") - - if ( - os.path.exists(config_h) - and os.stat(__file__).st_mtime < os.stat(config_h).st_mtime - ): - return +def create_config_h(config, output_dir: Path): + config_h = output_dir / "bn_config.h" + # this_script = Path(__file__) + # if ( + # config_h.exists() + # and this_script.stat().st_mtime < config_h.stat().st_mtime + # ): + # return output = [] - for config_attr, func_attr in OPTIONAL_FUNCTION_ATTRIBUTES: - if check_gcc_function_attribute(config, func_attr, config_attr.lower()): - output.append((config_attr, "1")) - else: - output.append((config_attr, "0")) + if config is not None: + for config_attr, func_attr in OPTIONAL_FUNCTION_ATTRIBUTES: + if check_gcc_function_attribute(config, func_attr, config_attr.lower()): + output.append((config_attr, "1")) + else: + output.append((config_attr, "0")) - inline_alias = check_inline(config) + inline_alias = check_inline(config) + else: + inline_alias = "" with open(config_h, "w") as f: for setting in output: @@ -118,3 +121,25 @@ def create_config_h(config): f.write("/* undef inline */\n") else: f.write(f"#define inline {inline_alias}\n") + print(f"wrote {config_h}") + + +def main(argv: list[str] | None = None) -> int: + from argparse import ArgumentParser + + parser = ArgumentParser() + parser.add_argument( + "-o", + dest="output_dir", + help="output directory", + ) + args = parser.parse_args(argv) + create_config_h( + config=None, + output_dir=Path(args.output_dir), + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/build_helpers/get_numpy_include_dir.py b/build_helpers/get_numpy_include_dir.py new file mode 100644 index 0000000000..5bf083a34f --- /dev/null +++ b/build_helpers/get_numpy_include_dir.py @@ -0,0 +1,21 @@ +# adapted from scipy's meson.build +import os +from contextlib import suppress + +import numpy as np + + +def main(argv: list[str] | None = None) -> int: + assert not argv + incdir = np.get_include() + + with suppress(Exception): + # when things are split across drives on Windows, + # there is no relative path and an exception gets raised. + incdir = os.path.relpath(np.get_include()) + print(incdir) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/bottleneck/src/bn_template.py b/build_helpers/template.py similarity index 81% rename from bottleneck/src/bn_template.py rename to build_helpers/template.py index 9166a57049..7dbb63b7f2 100644 --- a/bottleneck/src/bn_template.py +++ b/build_helpers/template.py @@ -1,33 +1,30 @@ +#!/usr/bin/env python3 import ast -import os -import posixpath as path import re +from pathlib import Path from re import Pattern -def make_c_files(dirpath: str | None = None, modules: list[str] | None = None) -> None: - if modules is None: - modules = ["reduce", "move", "nonreduce", "nonreduce_axis"] - if dirpath is None: - dirpath = os.path.dirname(__file__) - for module in modules: - template_file = os.path.join(dirpath, module + "_template.c") - posix_template = path.relpath(path.join(dirpath, module + "_template.c")) - target_file = os.path.join(dirpath, module + ".c") +def make_c_file( + template_file: Path, + output_file: Path, +) -> None: + assert template_file.suffixes == [".c", ".template"] + target_name = template_file.stem # trim the last suffix ('.template') + assert target_name.endswith(".c"), template_file + assert target_name == output_file.name, output_file - if ( - os.path.exists(target_file) - and os.stat(template_file).st_mtime < os.stat(target_file).st_mtime - ): - continue + # if ( + # target_file.exists() + # and template_file.stat().st_mtime < target_file.stat().st_mtime + # ): + # continue - with open(template_file) as f: - src_str = f.read() - src_str = f'#line 1 "{posix_template}"\n' + template(src_str) - if len(src_str) and src_str[-1] != "\n": - src_str += "\n" - with open(target_file, "w") as f: - f.write(src_str) + src_str = f'#line 1 "{template_file.name}"\n' + template(template_file.read_text()) + if len(src_str) and src_str[-1] != "\n": + src_str += "\n" + output_file.write_text(src_str) + print(f"wrote {output_file}") def template(src_str: str) -> str: @@ -209,3 +206,30 @@ def next_block( raise ValueError("found end of function before beginning") return idx, i return None, None + + +def main(argv: list[str] | None = None) -> int: + from argparse import ArgumentParser + + parser = ArgumentParser() + parser.add_argument( + "template_file", + type=Path, + help="path to template file", + ) + parser.add_argument( + "-o", + dest="output_file", + type=Path, + help="path to output file", + ) + args = parser.parse_args(argv) + make_c_file( + template_file=args.template_file, + output_file=args.output_file, + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/build_helpers/tests/__init__.py b/build_helpers/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/bottleneck/tests/data/template_test/test_template.c b/build_helpers/tests/data/template_test/test.c.template similarity index 100% rename from bottleneck/tests/data/template_test/test_template.c rename to build_helpers/tests/data/template_test/test.c.template diff --git a/bottleneck/tests/data/template_test/truth.c b/build_helpers/tests/data/template_test/truth.c similarity index 97% rename from bottleneck/tests/data/template_test/truth.c rename to build_helpers/tests/data/template_test/truth.c index 80a0badc4a..4ade306d23 100644 --- a/bottleneck/tests/data/template_test/truth.c +++ b/build_helpers/tests/data/template_test/truth.c @@ -1,4 +1,4 @@ -#line 1 "{DIRPATH}/test_template.c" +#line 1 "test.c.template" // Copyright 2010-2019 Keith Goodman // Copyright 2019 Bottleneck Developers #include "bottleneck.h" diff --git a/build_helpers/tests/test_template.py b/build_helpers/tests/test_template.py new file mode 100644 index 0000000000..79c300e5e2 --- /dev/null +++ b/build_helpers/tests/test_template.py @@ -0,0 +1,30 @@ +from pathlib import Path + +import pytest + +from ..template import main + +SRC_DIR = Path(__file__).parents[2] / "bottleneck" / "src" + + +@pytest.fixture(params=sorted(SRC_DIR.glob("*.c.template"))) +def template_file(request): + return request.param + + +def test_make_c_file(template_file, tmp_path) -> None: + target = tmp_path / template_file.stem + retcode = main([str(template_file), "-o", str(target)]) + assert retcode == 0 + assert target.is_file() + + +DATA_DIR = Path(__file__).parent / "data" / "template_test" + + +def test_known_result(tmp_path) -> None: + ref = DATA_DIR / "truth.c" + target = tmp_path / "test.c" + retcode = main([str(DATA_DIR / "test.c.template"), "-o", str(target)]) + assert retcode == 0 + assert target.read_text() == ref.read_text() diff --git a/meson.build b/meson.build new file mode 100644 index 0000000000..fbce3fb512 --- /dev/null +++ b/meson.build @@ -0,0 +1,141 @@ +project( + 'bottleneck', + 'c', + # https://mesonbuild.com/meson-python/reference/meson-compatibility.html#cmdoption-arg-1.6.0 + meson_version: '>=1.6.0', +) + +# detect missing dependencies early +py = import('python').find_installation(pure: false) + +helpers = 'build_helpers' + +npy_inc_str = run_command(py, + [helpers / 'get_numpy_include_dir.py'], + check: true, +).stdout().strip() +npy_inc = include_directories(npy_inc_str) + +npy_nodeprapi = [ + # keep in sync with pyproject.toml + '-DNPY_NO_DEPRECATED_API=NPY_1_21_API_VERSION', +] + +npy_dep = declare_dependency( + include_directories: npy_inc, + compile_args: npy_nodeprapi, +) + +bn_config_h = custom_target( + 'bn_config', + output: 'bn_config.h', + command: [find_program(helpers / 'config.py'), '-o', '@OUTDIR@'], +) + +cgen = generator( + find_program(helpers / 'template.py'), + arguments: ['@INPUT@', '-o', '@OUTPUT@'], + output: '@BASENAME@', +) + +bn = 'bottleneck' +src = bn / 'src' +bn_dep = declare_dependency( + dependencies: npy_dep, + compile_args: npy_nodeprapi, + include_directories: [src, npy_inc], +) + +py.extension_module( + 'reduce', + [ + bn_config_h, + cgen.process(src / 'reduce.c.template'), + ], + subdir: 'bottleneck', + install: true, + dependencies : [npy_dep, bn_dep], + c_args: npy_nodeprapi, + limited_api: py.language_version(), +) +py.extension_module( + 'move', + [ + bn_config_h, + cgen.process(src / 'move.c.template'), + src / 'move_median/move_median.c', + ], + subdir: 'bottleneck', + install: true, + dependencies : [npy_dep, bn_dep], + c_args: npy_nodeprapi, + limited_api: py.language_version(), +) +py.extension_module( + 'nonreduce', + [ + bn_config_h, + cgen.process(src / 'nonreduce.c.template'), + ], + subdir: 'bottleneck', + install: true, + dependencies : [npy_dep, bn_dep], + c_args: npy_nodeprapi, + limited_api: py.language_version(), +) +py.extension_module( + 'nonreduce_axis', + [ + bn_config_h, + cgen.process(src / 'nonreduce_axis.c.template'), + ], + subdir: 'bottleneck', + install: true, + dependencies : [npy_dep, bn_dep], + c_args: npy_nodeprapi, + limited_api: py.language_version(), +) + + +py.install_sources( + bn / '__init__.py', + bn / '_pytesttester.py', + bn / 'conftest.py', + # bn / '_version.py', + subdir: 'bottleneck', +) + +benchmark = bn / 'benchmark' +py.install_sources( + benchmark / '__init__.py', + benchmark / 'autotimeit.py', + benchmark / 'bench_detailed.py', + benchmark / 'bench.py', + subdir: 'bottleneck/benchmark', +) + +slow = bn / 'slow' +py.install_sources( + slow / '__init__.py', + slow / 'move.py', + slow / 'nonreduce_axis.py', + slow / 'nonreduce.py', + slow / 'reduce.py', + subdir: 'bottleneck/slow', +) + +tests = bn / 'tests' +py.install_sources( + tests / '__init__.py', + tests / 'common.py', + tests / 'input_modification_test.py', + tests / 'list_input_test.py', + tests / 'memory_test.py', + tests / 'move_test.py', + tests / 'nonreduce_axis_test.py', + tests / 'nonreduce_test.py', + tests / 'reduce_test.py', + tests / 'scalar_input_test.py', + tests / 'util.py', + subdir: 'bottleneck/tests', +) diff --git a/pyproject.toml b/pyproject.toml index 4578f6f338..c15e08cec4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,8 +1,7 @@ [build-system] +# keep in sync with dependency-groups requires = [ - "setuptools>=77.0.1", - "versioneer>=0.29", - "versioneer[toml] ; python_version < '3.11'", + "meson-python>=0.18.0", # Comments on numpy build requirement range: # # 1. >=2.0.x is the numpy requirement for wheel builds for distribution @@ -15,10 +14,11 @@ requires = [ # it should not be loosened more than that. "numpy>=2,<2.6" ] -build-backend = "setuptools.build_meta" +build-backend = "mesonpy" [project] name = "Bottleneck" +version = "1.7.0.dev0" description = "Fast NumPy array functions written in C" maintainers = [ { name = "Christopher Whelan" , email = "bottle-neck@googlegroups.com" }, @@ -51,16 +51,36 @@ classifiers = [ ] requires-python = ">=3.10" dependencies = [ - # keep in sync with NPY_* macros (setup.py) + # keep in sync with NPY_* macros (meson.build) "numpy>=1.21.3", ] -dynamic = ["version"] [project.urls] Repository = "https://github.com/pydata/bottleneck" Changelog = "https://github.com/pydata/bottleneck/blob/master/RELEASE.rst" [dependency-groups] +# For convenience, the dev group contains build time requirements because +# - meson-python supports editable installs or build isolation, but not both +# - uv special cases this group name and includes it by default via uv sync +# +# Combined with no-build-isolation set in [tool.uv], +# this allows to bootstrap an editable in just two simple steps: +# uv sync --only-dev && uv sync +# +# This could be simplified/reunifed depending on what happens on the following +# upstream issue +# https://github.com/astral-sh/uv/issues/7052 +# +# ninja is a system level requirement to meson-python +# it is automatically obtained from PyPI if lacking at the system level +# when building a package in isolation, which is why it's not listed +# under build-system requires. +dev = [ + "meson-python>=0.18.0", + "ninja>=1.11.1.3", + "numpy>=2.0.0, <2.6", +] doc = [ "numpydoc", "sphinx", @@ -74,12 +94,12 @@ concurrency = [ "pytest-run-parallel>=0.9.0", ] -[tool.setuptools] -include-package-data = false -zip-safe = false - -[tool.setuptools.packages.find] -namespaces = false +[tool.meson-python] +# disallow limited-api builds by default +# - users building from source generally do not need portable binaries +# - this is overridden in the cibuildwheel section +limited-api = true +args.setup = ['-Dpython.allow_limited_api=false'] [tool.versioneer] VCS = "git" @@ -95,11 +115,29 @@ filterwarnings = ["error"] [tool.cibuildwheel] build-frontend = "uv; args: --no-config" +config-settings = { setup-args = ["-Dpython.allow_limited_api=true"] } skip = [ "*_i686", "cp310-win_arm64", # no numpy wheels for this target ] +[[tool.cibuildwheel.overrides]] +# CPython 3.14 predates PEP 803 +select = "cp314t-*" +config-settings = { setup-args = ["-Dpython.allow_limited_api=false"] } + +[[tool.cibuildwheel.overrides]] +# adapted from numpy +select = "*win_arm64*" +config-settings = { build-dir = "build" , setup-args = ["--vsenv", "-Dpython.allow_limited_api=true"] } + +[[tool.cibuildwheel.overrides]] +# cibw's override system is not flexible enough to inherit *nested* +# config items, so instead we need to repeat every other overlapping +# overrides here. +select = "cp314t-*win_arm64*" +config-settings = { build-dir = "build" , setup-args = ["--vsenv", "-Dpython.allow_limited_api=false"] } + [tool.ruff] exclude = [ "doc", diff --git a/setup.py b/setup.py deleted file mode 100644 index fd43a3cc21..0000000000 --- a/setup.py +++ /dev/null @@ -1,161 +0,0 @@ -#!/usr/bin/env python - -import os -import shutil -import sys -from distutils.command.config import config as _config - -from setuptools import Command, setup -from setuptools.command.build_ext import build_ext as _build_ext -from setuptools.extension import Extension - -import versioneer - -define_macros = [ - # keep in sync with runtime requirements (pyproject.toml) - ("NPY_NO_DEPRECATED_API", "NPY_1_21_API_VERSION"), -] - - -class config(_config): - def run(self): - from bn_config import create_config_h - - create_config_h(self) - - -class clean(Command): - user_options = [("all", "a", "")] - - def initialize_options(self): - self.all = True - self.delete_dirs = [] - self.delete_files = [] - - for root, dirs, files in os.walk("bottleneck"): - for d in dirs: - if d == "__pycache__": - self.delete_dirs.append(os.path.join(root, d)) - - if "__pycache__" in root: - continue - - for f in files: - if f.endswith(".pyc") or f.endswith(".so"): - self.delete_files.append(os.path.join(root, f)) - - if f.endswith(".c") and "template" in f: - generated_file = os.path.join(root, f.replace("_template", "")) - if os.path.exists(generated_file): - self.delete_files.append(generated_file) - - config_h = "bottleneck/src/bn_config.h" - if os.path.exists(config_h): - self.delete_files.append(config_h) - - if os.path.exists("build"): - self.delete_dirs.append("build") - - def finalize_options(self): - pass - - def run(self): - for delete_dir in self.delete_dirs: - shutil.rmtree(delete_dir) - for delete_file in self.delete_files: - os.unlink(delete_file) - - -# workaround for installing bottleneck when numpy is not present -class build_ext(_build_ext): - # taken from: stackoverflow.com/questions/19919905/ - # how-to-bootstrap-numpy-installation-in-setup-py#21621689 - def finalize_options(self): - _build_ext.finalize_options(self) - # prevent numpy from thinking it is still in its setup process - import builtins - - builtins.__NUMPY_SETUP__ = False - import numpy - - # place numpy includes first, see gh #156 - self.include_dirs.insert(0, numpy.get_include()) - self.include_dirs.append("bottleneck/src") - - def build_extensions(self): - from bn_template import make_c_files - - self.run_command("config") - dirpath = "bottleneck/src" - modules = ["reduce", "move", "nonreduce", "nonreduce_axis"] - make_c_files(dirpath, modules) - - _build_ext.build_extensions(self) - - -cmdclass = versioneer.get_cmdclass() -cmdclass["build_ext"] = build_ext -cmdclass["clean"] = clean -cmdclass["config"] = config - -# Add our template path to the path so that we don't have a circular reference -# of working install to be able to re-compile -sys.path.append(os.path.join(os.path.dirname(__file__), "bottleneck/src")) - - -def prepare_modules(): - base_includes = [ - "bottleneck/src/bottleneck.h", - "bottleneck/src/bn_config.h", - "bottleneck/src/iterators.h", - ] - ext = [ - Extension( - "bottleneck.reduce", - sources=["bottleneck/src/reduce.c"], - depends=base_includes, - define_macros=define_macros, - extra_compile_args=["-O2"], - ) - ] - ext += [ - Extension( - "bottleneck.move", - sources=[ - "bottleneck/src/move.c", - "bottleneck/src/move_median/move_median.c", - ], - depends=base_includes + ["bottleneck/src/move_median/move_median.h"], - define_macros=define_macros, - extra_compile_args=["-O2"], - ) - ] - ext += [ - Extension( - "bottleneck.nonreduce", - sources=["bottleneck/src/nonreduce.c"], - depends=base_includes, - define_macros=define_macros, - extra_compile_args=["-O2"], - ) - ] - ext += [ - Extension( - "bottleneck.nonreduce_axis", - sources=["bottleneck/src/nonreduce_axis.c"], - depends=base_includes, - define_macros=define_macros, - extra_compile_args=["-O2"], - ) - ] - return ext - - -setup( - version=versioneer.get_version(), - package_data={ - "bottleneck.tests": ["data/*/*"], - }, - cmdclass=cmdclass, - ext_modules=prepare_modules(), -) diff --git a/tools/travis/bn_setup.sh b/tools/travis/bn_setup.sh deleted file mode 100644 index 29eaab9796..0000000000 --- a/tools/travis/bn_setup.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env bash - -set -ev # exit on first error, print commands - -if [ "${TEST_RUN}" = "style" ]; then - ruff check - ruff format . --check --exclude "(build/|dist/|\.git/|\.mypy_cache/|\.tox/|\.venv/\.asv/|env|\.eggs)" -else - if [ "${TEST_RUN}" = "sdist" ]; then - python setup.py sdist - ARCHIVE=`ls dist/*.tar.gz` - pip install "${ARCHIVE[0]}" - else - pip install "." - fi - python setup.py build_ext --inplace - set +e - if [ "${TEST_RUN}" = "doc" ]; then - make doc - else - # Workaround for https://github.com/travis-ci/travis-ci/issues/6522 - python "tools/test-installed-bottleneck.py" - fi -fi diff --git a/uv.lock b/uv.lock index 1726a96683..e79601a011 100644 --- a/uv.lock +++ b/uv.lock @@ -2,9 +2,12 @@ version = 1 revision = 3 requires-python = ">=3.10" resolution-markers = [ - "python_full_version >= '3.12'", - "python_full_version == '3.11.*'", - "python_full_version < '3.11'", + "python_full_version >= '3.12' and sys_platform != 'ios'", + "python_full_version >= '3.12' and sys_platform == 'ios'", + "python_full_version == '3.11.*' and sys_platform != 'ios'", + "python_full_version == '3.11.*' and sys_platform == 'ios'", + "python_full_version < '3.11' and sys_platform != 'ios'", + "python_full_version < '3.11' and sys_platform == 'ios'", ] [[package]] @@ -27,6 +30,7 @@ wheels = [ [[package]] name = "bottleneck" +version = "1.7.0.dev0" source = { editable = "." } dependencies = [ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, @@ -38,6 +42,12 @@ concurrency = [ { name = "pytest" }, { name = "pytest-run-parallel" }, ] +dev = [ + { name = "meson-python" }, + { name = "ninja" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] doc = [ { name = "gitpython" }, { name = "numpydoc" }, @@ -57,6 +67,11 @@ concurrency = [ { name = "pytest", specifier = ">=8.0.0" }, { name = "pytest-run-parallel", specifier = ">=0.9.0" }, ] +dev = [ + { name = "meson-python", specifier = ">=0.18.0" }, + { name = "ninja", specifier = ">=1.11.1.3" }, + { name = "numpy", specifier = ">=2.0.0,<2.6" }, +] doc = [ { name = "gitpython" }, { name = "numpydoc" }, @@ -176,7 +191,8 @@ name = "docutils" version = "0.21.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.11'", + "python_full_version < '3.11' and sys_platform != 'ios'", + "python_full_version < '3.11' and sys_platform == 'ios'", ] sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444, upload-time = "2024-04-23T18:57:18.24Z" } wheels = [ @@ -188,8 +204,10 @@ name = "docutils" version = "0.22.4" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.12'", - "python_full_version == '3.11.*'", + "python_full_version >= '3.12' and sys_platform != 'ios'", + "python_full_version >= '3.12' and sys_platform == 'ios'", + "python_full_version == '3.11.*' and sys_platform != 'ios'", + "python_full_version == '3.11.*' and sys_platform == 'ios'", ] sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } wheels = [ @@ -356,12 +374,62 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] +[[package]] +name = "meson" +version = "1.11.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/cd/f3a881ff5e601d6bbeff63b38ee2362e1167c47d9cde03eddf8d71a4ffb0/meson-1.11.1-py3-none-any.whl", hash = "sha256:9b3a023657e393dbc5335b95c561337d49b7a458f5541e47ec44f2cc566e0d80", size = 1078534, upload-time = "2026-04-21T03:15:31.611Z" }, +] + +[[package]] +name = "meson-python" +version = "0.19.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "meson" }, + { name = "packaging" }, + { name = "pyproject-metadata" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/32/98/7fe5d1bf741c03c6eea04b6245737dbd79657d4f9200e82fcbb4cc12637b/meson_python-0.19.0.tar.gz", hash = "sha256:9959d198aa69b57fcfd354a34518c6f795b781a73ed0656f4d01660160cc2553", size = 101504, upload-time = "2026-01-15T13:52:44.368Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/7f/d1b0c65b267a1463d752b324f11d3470e30889daefc4b9ec83029bfa30b5/meson_python-0.19.0-py3-none-any.whl", hash = "sha256:67b5906c37404396d23c195e12c8825506074460d4a2e7083266b845d14f0298", size = 28946, upload-time = "2026-01-15T13:52:43.107Z" }, +] + +[[package]] +name = "ninja" +version = "1.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/73/79a0b22fc731989c708068427579e840a6cf4e937fe7ae5c5d0b7356ac22/ninja-1.13.0.tar.gz", hash = "sha256:4a40ce995ded54d9dc24f8ea37ff3bf62ad192b547f6c7126e7e25045e76f978", size = 242558, upload-time = "2025-08-11T15:10:19.421Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/74/d02409ed2aa865e051b7edda22ad416a39d81a84980f544f8de717cab133/ninja-1.13.0-py3-none-macosx_10_9_universal2.whl", hash = "sha256:fa2a8bfc62e31b08f83127d1613d10821775a0eb334197154c4d6067b7068ff1", size = 310125, upload-time = "2025-08-11T15:09:50.971Z" }, + { url = "https://files.pythonhosted.org/packages/8e/de/6e1cd6b84b412ac1ef327b76f0641aeb5dcc01e9d3f9eee0286d0c34fd93/ninja-1.13.0-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3d00c692fb717fd511abeb44b8c5d00340c36938c12d6538ba989fe764e79630", size = 177467, upload-time = "2025-08-11T15:09:52.767Z" }, + { url = "https://files.pythonhosted.org/packages/c8/83/49320fb6e58ae3c079381e333575fdbcf1cca3506ee160a2dcce775046fa/ninja-1.13.0-py3-none-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:be7f478ff9f96a128b599a964fc60a6a87b9fa332ee1bd44fa243ac88d50291c", size = 187834, upload-time = "2025-08-11T15:09:54.115Z" }, + { url = "https://files.pythonhosted.org/packages/56/c7/ba22748fb59f7f896b609cd3e568d28a0a367a6d953c24c461fe04fc4433/ninja-1.13.0-py3-none-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:60056592cf495e9a6a4bea3cd178903056ecb0943e4de45a2ea825edb6dc8d3e", size = 202736, upload-time = "2025-08-11T15:09:55.745Z" }, + { url = "https://files.pythonhosted.org/packages/79/22/d1de07632b78ac8e6b785f41fa9aad7a978ec8c0a1bf15772def36d77aac/ninja-1.13.0-py3-none-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:1c97223cdda0417f414bf864cfb73b72d8777e57ebb279c5f6de368de0062988", size = 179034, upload-time = "2025-08-11T15:09:57.394Z" }, + { url = "https://files.pythonhosted.org/packages/ed/de/0e6edf44d6a04dabd0318a519125ed0415ce437ad5a1ec9b9be03d9048cf/ninja-1.13.0-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fb46acf6b93b8dd0322adc3a4945452a4e774b75b91293bafcc7b7f8e6517dfa", size = 180716, upload-time = "2025-08-11T15:09:58.696Z" }, + { url = "https://files.pythonhosted.org/packages/54/28/938b562f9057aaa4d6bfbeaa05e81899a47aebb3ba6751e36c027a7f5ff7/ninja-1.13.0-py3-none-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4be9c1b082d244b1ad7ef41eb8ab088aae8c109a9f3f0b3e56a252d3e00f42c1", size = 146843, upload-time = "2025-08-11T15:10:00.046Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fb/d06a3838de4f8ab866e44ee52a797b5491df823901c54943b2adb0389fbb/ninja-1.13.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:6739d3352073341ad284246f81339a384eec091d9851a886dfa5b00a6d48b3e2", size = 154402, upload-time = "2025-08-11T15:10:01.657Z" }, + { url = "https://files.pythonhosted.org/packages/31/bf/0d7808af695ceddc763cf251b84a9892cd7f51622dc8b4c89d5012779f06/ninja-1.13.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:11be2d22027bde06f14c343f01d31446747dbb51e72d00decca2eb99be911e2f", size = 552388, upload-time = "2025-08-11T15:10:03.349Z" }, + { url = "https://files.pythonhosted.org/packages/9d/70/c99d0c2c809f992752453cce312848abb3b1607e56d4cd1b6cded317351a/ninja-1.13.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:aa45b4037b313c2f698bc13306239b8b93b4680eb47e287773156ac9e9304714", size = 472501, upload-time = "2025-08-11T15:10:04.735Z" }, + { url = "https://files.pythonhosted.org/packages/9f/43/c217b1153f0e499652f5e0766da8523ce3480f0a951039c7af115e224d55/ninja-1.13.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5f8e1e8a1a30835eeb51db05cf5a67151ad37542f5a4af2a438e9490915e5b72", size = 638280, upload-time = "2025-08-11T15:10:06.512Z" }, + { url = "https://files.pythonhosted.org/packages/8c/45/9151bba2c8d0ae2b6260f71696330590de5850e5574b7b5694dce6023e20/ninja-1.13.0-py3-none-musllinux_1_2_ppc64le.whl", hash = "sha256:3d7d7779d12cb20c6d054c61b702139fd23a7a964ec8f2c823f1ab1b084150db", size = 642420, upload-time = "2025-08-11T15:10:08.35Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/95752eb635bb8ad27d101d71bef15bc63049de23f299e312878fc21cb2da/ninja-1.13.0-py3-none-musllinux_1_2_riscv64.whl", hash = "sha256:d741a5e6754e0bda767e3274a0f0deeef4807f1fec6c0d7921a0244018926ae5", size = 585106, upload-time = "2025-08-11T15:10:09.818Z" }, + { url = "https://files.pythonhosted.org/packages/c1/31/aa56a1a286703800c0cbe39fb4e82811c277772dc8cd084f442dd8e2938a/ninja-1.13.0-py3-none-musllinux_1_2_s390x.whl", hash = "sha256:e8bad11f8a00b64137e9b315b137d8bb6cbf3086fbdc43bf1f90fd33324d2e96", size = 707138, upload-time = "2025-08-11T15:10:11.366Z" }, + { url = "https://files.pythonhosted.org/packages/34/6f/5f5a54a1041af945130abdb2b8529cbef0cdcbbf9bcf3f4195378319d29a/ninja-1.13.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b4f2a072db3c0f944c32793e91532d8948d20d9ab83da9c0c7c15b5768072200", size = 581758, upload-time = "2025-08-11T15:10:13.295Z" }, + { url = "https://files.pythonhosted.org/packages/95/97/51359c77527d45943fe7a94d00a3843b81162e6c4244b3579fe8fc54cb9c/ninja-1.13.0-py3-none-win32.whl", hash = "sha256:8cfbb80b4a53456ae8a39f90ae3d7a2129f45ea164f43fadfa15dc38c4aef1c9", size = 267201, upload-time = "2025-08-11T15:10:15.158Z" }, + { url = "https://files.pythonhosted.org/packages/29/45/c0adfbfb0b5895aa18cec400c535b4f7ff3e52536e0403602fc1a23f7de9/ninja-1.13.0-py3-none-win_amd64.whl", hash = "sha256:fb8ee8719f8af47fed145cced4a85f0755dd55d45b2bddaf7431fa89803c5f3e", size = 309975, upload-time = "2025-08-11T15:10:16.697Z" }, + { url = "https://files.pythonhosted.org/packages/df/93/a7b983643d1253bb223234b5b226e69de6cda02b76cdca7770f684b795f5/ninja-1.13.0-py3-none-win_arm64.whl", hash = "sha256:3c0b40b1f0bba764644385319028650087b4c1b18cdfa6f45cb39a3669b81aa9", size = 290806, upload-time = "2025-08-11T15:10:18.018Z" }, +] + [[package]] name = "numpy" version = "2.2.6" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.11'", + "python_full_version < '3.11' and sys_platform != 'ios'", + "python_full_version < '3.11' and sys_platform == 'ios'", ] sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } wheels = [ @@ -426,8 +494,10 @@ name = "numpy" version = "2.4.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.12'", - "python_full_version == '3.11.*'", + "python_full_version >= '3.12' and sys_platform != 'ios'", + "python_full_version >= '3.12' and sys_platform == 'ios'", + "python_full_version == '3.11.*' and sys_platform != 'ios'", + "python_full_version == '3.11.*' and sys_platform == 'ios'", ] sdist = { url = "https://files.pythonhosted.org/packages/57/fd/0005efbd0af48e55eb3c7208af93f2862d4b1a56cd78e84309a2d959208d/numpy-2.4.2.tar.gz", hash = "sha256:659a6107e31a83c4e33f763942275fd278b21d095094044eb35569e86a21ddae", size = 20723651, upload-time = "2026-01-31T23:13:10.135Z" } wheels = [ @@ -546,6 +616,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] +[[package]] +name = "pyproject-metadata" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/fa/8bf4fa41adfebd95dce360afe3f5fca243a17932089d3d5486e95ca44c57/pyproject_metadata-0.11.0.tar.gz", hash = "sha256:c72fa49418bb7c5a10f25e050c418009898d1c051721d19f98a6fb6da59a66cf", size = 43799, upload-time = "2026-02-09T19:12:50.578Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/0b/da4851b1e2d9c40c9bd74c0abd94510a7d797da9ccde0a90e8953751ed4a/pyproject_metadata-0.11.0-py3-none-any.whl", hash = "sha256:85bbecca8694e2c00f63b492c96921d6c228454057c88e7c352b2077fcaa4096", size = 22040, upload-time = "2026-02-09T19:12:49.184Z" }, +] + [[package]] name = "pytest" version = "9.0.3" @@ -623,7 +705,8 @@ name = "sphinx" version = "8.1.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.11'", + "python_full_version < '3.11' and sys_platform != 'ios'", + "python_full_version < '3.11' and sys_platform == 'ios'", ] dependencies = [ { name = "alabaster", marker = "python_full_version < '3.11'" }, @@ -654,7 +737,8 @@ name = "sphinx" version = "9.0.4" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version == '3.11.*'", + "python_full_version == '3.11.*' and sys_platform != 'ios'", + "python_full_version == '3.11.*' and sys_platform == 'ios'", ] dependencies = [ { name = "alabaster", marker = "python_full_version == '3.11.*'" }, @@ -685,7 +769,8 @@ name = "sphinx" version = "9.1.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.12'", + "python_full_version >= '3.12' and sys_platform != 'ios'", + "python_full_version >= '3.12' and sys_platform == 'ios'", ] dependencies = [ { name = "alabaster", marker = "python_full_version >= '3.12'" },