From 997dfc676d70e67c00e25fcc8e33ab8f043fc1b3 Mon Sep 17 00:00:00 2001 From: Hans Dembinski Date: Thu, 10 Oct 2024 14:37:12 +0200 Subject: [PATCH] Fixes for numpy-2.0 (#81) This patch fixes a couple of deprecations/dependency changes. * Resolved issues with numpy-2.0: replaced numpy.VisibleDeprecationWarning, upgraded pybind11 to 2.13.2 * Fixed issues with clang-format, which destroyed the jupyter notebooks * Updated CI scripts analog to iminuit * Dropped support for python-3.8 --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .clang-format-ignore | 2 ++ .github/workflows/docs.yml | 40 +++++++++++++-------------- .github/workflows/test.yml | 52 +++++++++++++++++++----------------- .github/workflows/wheels.yml | 52 +++++++++++++++++++----------------- .gitignore | 4 ++- .pre-commit-config.yaml | 10 +++---- docs/conf.py | 9 +------ extern/pybind11 | 2 +- pyproject.toml | 16 +++++------ src/equal.cpp | 11 ++++++++ src/io.cpp | 2 +- src/pyhepmc/__init__.py | 3 +-- src/pyhepmc/_deprecated.py | 34 +++++++++++++---------- src/pyhepmc/_graphviz.py | 7 +++-- src/pyhepmc/view.py | 14 +++++----- tests/test_deprecated.py | 15 ++++++++--- tests/test_io.py | 3 +-- tests/test_view.py | 6 ++++- 18 files changed, 159 insertions(+), 123 deletions(-) create mode 100644 .clang-format-ignore diff --git a/.clang-format-ignore b/.clang-format-ignore new file mode 100644 index 0000000..d229323 --- /dev/null +++ b/.clang-format-ignore @@ -0,0 +1,2 @@ +docs/examples/* +extern/* diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 635dfcc..1d01a53 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -14,24 +14,24 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - with: - submodules: true - fetch-depth: 0 # needed for setuptools_scm - # must come after checkout - - uses: hendrikmuhs/ccache-action@v1.2 - with: - key: ${{ github.job }}-${{ matrix.os }}-${{ matrix.python-version }} - - uses: actions/setup-python@v4 - with: - python-version: "3.9" - - run: sudo apt-get install pandoc - - run: python -m pip install --prefer-binary -v .[doc] - - run: python -m ipykernel install --user --name python3 - - run: python docs/build.py - - uses: actions/upload-pages-artifact@v1 - with: - path: 'docs/_build/html' + - uses: actions/checkout@v3 + with: + submodules: true + fetch-depth: 0 # needed for setuptools_scm + # must come after checkout + - uses: hendrikmuhs/ccache-action@v1.2 + with: + key: ${{ github.job }}-${{ matrix.os }}-${{ matrix.python-version }} + - uses: actions/setup-python@v4 + with: + python-version: "3.11" + - run: sudo apt-get install pandoc + - run: python -m pip install --prefer-binary -v .[doc] + - run: python -m ipykernel install --user --name python3 + - run: python docs/build.py + - uses: actions/upload-pages-artifact@v1 + with: + path: "docs/_build/html" deploy: if: github.ref_type == 'tag' || github.ref_name == 'main' @@ -49,5 +49,5 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/configure-pages@v2 - - uses: actions/deploy-pages@v1 + - uses: actions/configure-pages@v2 + - uses: actions/deploy-pages@v1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 44384dd..5030e1b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,9 +3,9 @@ name: Test on: pull_request: paths-ignore: - - 'docs/**' - - '*.rst' - - '*.md' + - "docs/**" + - "*.rst" + - "*.md" workflow_dispatch: concurrency: @@ -22,27 +22,31 @@ jobs: # version number must be string, otherwise 3.10 becomes 3.1 - os: windows-latest python-version: "3.11" - - os: macos-13 - python-version: "3.8" + installs: "numpy>=2" + - os: macos-14 + python-version: "3.9" + installs: "numpy==1.21.0" - os: ubuntu-latest - python-version: "pypy-3.8" + python-version: "3.13" + installs: "'numpy>=2' scipy matplotlib" fail-fast: false steps: - - uses: actions/checkout@v3 - with: - submodules: true - fetch-depth: 3 - # must come after checkout - - uses: hendrikmuhs/ccache-action@v1.2 - with: - key: ${{ github.job }}-${{ matrix.os }}-${{ matrix.python-version }} - - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - run: python -m pip install --upgrade pip - - run: python -m pip install --prefer-binary -v .[test] - env: - DEBUG: 1 - - uses: ts-graphviz/setup-graphviz@v1 - if: ${{ matrix.os != 'macos-13' }} - - run: python -m pytest + - uses: actions/checkout@v4 + with: + submodules: true + fetch-depth: 3 + # must come after checkout + - uses: hendrikmuhs/ccache-action@v1.2 + with: + key: ${{ github.job }}-${{ matrix.os }}-${{ matrix.python-version }} + - uses: astral-sh/setup-uv@v3 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + allow-prereleases: true + - uses: ts-graphviz/setup-graphviz@v1 + if: ${{ matrix.os != 'macos-13' }} + - run: uv pip install --system .[test] ${{ matrix.installs }} + env: + DEBUG: 1 + - run: python -m pytest diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index e0d852c..72623fe 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -3,7 +3,7 @@ name: Wheels on: push: tags: - - '**' + - "**" workflow_dispatch: concurrency: @@ -22,40 +22,33 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, windows-latest, macos-13] - arch: [auto, aarch64, universal2] - py: [cp38, cp39, cp310, cp311, cp312] + os: [ubuntu-latest, windows-latest, macos-13, macos-14] + arch: [auto, aarch64] + py: [cp39, cp310, cp311, cp312, cp313] exclude: - os: windows-latest arch: aarch64 - - os: windows-latest - arch: universal2 - os: macos-13 arch: aarch64 - - os: ubuntu-latest - arch: universal2 - # some unrelated error with installing pillow - - os: ubuntu-latest - py: cp38 - env: - CIBW_BUILD: ${{ matrix.py }}-* - CIBW_ARCHS_LINUX: ${{ matrix.arch }} + - os: macos-14 + arch: aarch64 steps: - uses: actions/checkout@v4 with: submodules: true - fetch-depth: 0 # needed by setuptools_scm + fetch-depth: 0 # needed by setuptools_scm - if: ${{ matrix.arch == 'aarch64' }} - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - - uses: pypa/cibuildwheel@v2.16.5 + - uses: pypa/cibuildwheel@v2.21 env: CIBW_BUILD: ${{ matrix.py }}-* CIBW_ARCHS: ${{ matrix.arch }} - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: + name: wheel-${{ matrix.py }}-${{ matrix.os }}-${{ matrix.arch }} path: ./wheelhouse/*.whl sdist: @@ -65,28 +58,39 @@ jobs: - uses: actions/checkout@v4 with: submodules: true - fetch-depth: 0 # needed by setuptools_scm + fetch-depth: 0 # needed by setuptools_scm - uses: actions/setup-python@v4 with: - python-version: '3.11' + python-version: "3.11" - - run: python -m pip install --upgrade pip setuptools wheel - - run: python setup.py sdist + - run: pipx run build --sdist + - run: python -m pip install --upgrade pip setuptools - run: python -m pip install -v $(echo dist/*)'[test]' - run: python -m pytest - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: + name: sdist path: dist/*.tar.gz upload: + if: github.event_name == 'push' && contains(github.event.ref, '/tags/') needs: [wheels, sdist] runs-on: ubuntu-latest - if: github.event_name == 'push' && contains(github.event.ref, '/tags/') + + environment: + name: pypi + url: https://pypi.org/project/pyhepmc/ + + permissions: + id-token: write + attestations: write + steps: - uses: actions/download-artifact@v4.1.7 with: + pattern: "*" name: artifact path: dist diff --git a/.gitignore b/.gitignore index 9c39faf..def7a53 100644 --- a/.gitignore +++ b/.gitignore @@ -4,9 +4,10 @@ var build dist *.pyc +*.pyd *.so *.egg-info -tests/__pycache__ +__pycache__ .eggs .cache .compiler_support_cache @@ -17,6 +18,7 @@ py?? py??? .ci_bak venv +.venv .coverage htmlcov src/pyhepmc/_version.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 34c7624..bbdd6af 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: # Standard hooks - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v5.0.0 hooks: - id: check-case-conflict - id: check-docstring-first @@ -35,20 +35,20 @@ repos: # Python formatting - repo: https://github.com/psf/black-pre-commit-mirror - rev: 24.4.2 + rev: 24.10.0 hooks: - id: black # Ruff linter, replacement for flake8, pydocstyle, isort - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.4.5' + rev: 'v0.6.9' hooks: - id: ruff args: [--fix, --show-fixes] # C++ formatting - repo: https://github.com/pre-commit/mirrors-clang-format - rev: v18.1.5 + rev: v19.1.1 hooks: - id: clang-format @@ -63,7 +63,7 @@ repos: # Python type checking - repo: https://github.com/pre-commit/mirrors-mypy - rev: 'v1.10.0' + rev: 'v1.11.2' hooks: - id: mypy # additional_dependencies: [numpy] diff --git a/docs/conf.py b/docs/conf.py index 5f8378c..8a79ca3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -6,7 +6,6 @@ # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information import pyhepmc -import os project = "pyhepmc" copyright = "2022, Hans Dembinski" @@ -30,13 +29,7 @@ html_static_path = ["_static"] -on_rtd = os.environ.get("READTHEDOCS", None) == "True" -if not on_rtd: - # Import and set the theme if we're building docs locally - import sphinx_rtd_theme - - html_theme = "sphinx_rtd_theme" - html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] +html_theme = "sphinx_rtd_theme" # Autodoc options autodoc_member_order = "groupwise" diff --git a/extern/pybind11 b/extern/pybind11 index 914c06f..a2e59f0 160000 --- a/extern/pybind11 +++ b/extern/pybind11 @@ -1 +1 @@ -Subproject commit 914c06fb252b6cc3727d0eedab6736e88a3fcb01 +Subproject commit a2e59f0e7065404b44dfe92a28aca47ba1378dc4 diff --git a/pyproject.toml b/pyproject.toml index 07dee11..c12e57e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,12 +10,9 @@ build-backend = "setuptools.build_meta" [project] name = "pyhepmc" description = "Pythonic interface to the HepMC3 C++ library licensed under LGPL-v3." -maintainers = [ - { name = "Hans Dembinski" }, - { email = "hans.dembinski@gmail.com" }, -] +maintainers = [{ name = "Hans Dembinski", email = "hans.dembinski@gmail.com" }] readme = "README.rst" -requires-python = ">=3.8" +requires-python = ">=3.9" license = { text = "BSD 3-Clause License" } classifiers = [ "Development Status :: 5 - Production/Stable", @@ -27,7 +24,7 @@ classifiers = [ "Topic :: Scientific/Engineering", "Intended Audience :: Developers", ] -dependencies = ["numpy"] +dependencies = ["numpy>=1.21", "packaging"] dynamic = ["version"] [project.urls] @@ -54,8 +51,9 @@ testpaths = ["tests"] log_cli_level = "INFO" xfail_strict = true filterwarnings = [ + "error::PendingDeprecationWarning", "error::DeprecationWarning", - "error::numpy.VisibleDeprecationWarning", + "error::FutureWarning", ] [tool.coverage.run] @@ -66,10 +64,10 @@ source = ["pyhepmc"] exclude_lines = ["pragma: no cover"] [tool.ruff] -src = ["src"] +exclude = ["docs/examples/*"] [tool.ruff.lint] -select = [ +extend-select = [ "E", "F", # flake8 "D", # pydocstyle diff --git a/src/equal.cpp b/src/equal.cpp index cd7767a..cd67790 100644 --- a/src/equal.cpp +++ b/src/equal.cpp @@ -9,10 +9,21 @@ #include #include #include +#include namespace HepMC3 { +template +using void_t = void; + +template +struct has_unequal_op : std::false_type {}; + template +struct has_unequal_op() != std::declval())>> + : std::true_type {}; + +template ::value>> bool operator!=(const T& a, const T& b) { return !operator==(a, b); } diff --git a/src/io.cpp b/src/io.cpp index 2da7c9e..38f907e 100644 --- a/src/io.cpp +++ b/src/io.cpp @@ -70,7 +70,7 @@ void register_io(py::module& m) { .def(py::init<>()) .def(py::init()) .def("__str__", - (std::string(std::stringstream::*)() const) & std::stringstream::str); + (std::string(std::stringstream::*)() const)&std::stringstream::str); py::class_(m, "Reader") // clang-format off diff --git a/src/pyhepmc/__init__.py b/src/pyhepmc/__init__.py index 3aa7131..0de189b 100644 --- a/src/pyhepmc/__init__.py +++ b/src/pyhepmc/__init__.py @@ -109,13 +109,12 @@ def __getattr__(name: str) -> Any: from . import io import warnings - from numpy import VisibleDeprecationWarning if name in dir(io): warnings.warn( f"importing {name} from pyhepmc is deprecated, " "please import from pyhepmc.io", - category=VisibleDeprecationWarning, + category=DeprecationWarning, stacklevel=2, ) return getattr(io, name) diff --git a/src/pyhepmc/_deprecated.py b/src/pyhepmc/_deprecated.py index 917ed6c..3f3809f 100644 --- a/src/pyhepmc/_deprecated.py +++ b/src/pyhepmc/_deprecated.py @@ -1,23 +1,29 @@ import warnings -from numpy import VisibleDeprecationWarning -import typing as _tp +from typing import Callable, Any +from importlib.metadata import version +from packaging.version import Version + + +CURRENT_VERSION = Version(version("pyhepmc")) class deprecated: - def __init__(self, reason: str): - if not isinstance(reason, str): - raise ValueError("argument `reason: str` is required") - self._reason = reason + def __init__(self, reason: str, removal: str = ""): + assert isinstance(reason, str) + self.reason = reason + self.removal = Version(removal) if removal else None + + def __call__(self, func: Callable[..., Any]) -> Callable[..., Any]: + category: Any = DeprecationWarning + extra = "" + if self.removal and CURRENT_VERSION < self.removal: + extra = f" and will be removed in version {self.removal}" + msg = f"{func.__name__} is deprecated{extra}: {self.reason}" - def __call__(self, func: _tp.Callable[..., _tp.Any]) -> _tp.Callable[..., _tp.Any]: - def decorated_func(*args: _tp.Any, **kwargs: _tp.Any) -> _tp.Any: - warnings.warn( - f"{func.__name__} is deprecated: {self._reason}", - category=VisibleDeprecationWarning, - stacklevel=2, - ) + def decorated_func(*args: Any, **kwargs: Any) -> Any: + warnings.warn(msg, category=category, stacklevel=2) return func(*args, **kwargs) decorated_func.__name__ = func.__name__ - decorated_func.__doc__ = "deprecated: " + self._reason + decorated_func.__doc__ = msg return decorated_func diff --git a/src/pyhepmc/_graphviz.py b/src/pyhepmc/_graphviz.py index a9d7111..c92d544 100644 --- a/src/pyhepmc/_graphviz.py +++ b/src/pyhepmc/_graphviz.py @@ -125,8 +125,11 @@ def pipe(self, *, format: str, encoding: str = None) -> Union[str, bytes]: return r.stdout def _repr_svg_(self) -> str: - s = self.pipe(format="svg", encoding="utf-8") - assert isinstance(s, str) + try: + s = self.pipe(format="svg", encoding="utf-8") + assert isinstance(s, str) + except FileNotFoundError as e: + return f"SVG view not available: {e.args[0]}" return s def _repr_png_(self) -> bytes: diff --git a/src/pyhepmc/view.py b/src/pyhepmc/view.py index dfad4c9..57ec2c8 100644 --- a/src/pyhepmc/view.py +++ b/src/pyhepmc/view.py @@ -6,7 +6,7 @@ from pyhepmc import Units, GenEvent import numpy as np import os -from pathlib import PurePath +from pathlib import Path from typing import BinaryIO, Union, Set, Any, Optional, Tuple @@ -245,15 +245,17 @@ def savefig( Other arguments are forwarded to pyhepmc.view.to_dot. """ if isinstance(fname, (str, os.PathLike)): - p = PurePath(fname) + p = Path(fname) if format is None: format = "".join(p.suffixes)[1:] - if format in SUPPORTED_FORMATS: + try: + # unknown format raises exception in nested call with open(p, "wb") as fo: savefig(event, fo, format=format, **kwargs) - # unknown format raises exception in nested call - with open(os.devnull, "wb") as fo: - savefig(event, fo, format=format, **kwargs) + except ValueError: + if p.exists(): + p.unlink() + raise return # if we arrive here, fname is a file-like object diff --git a/tests/test_deprecated.py b/tests/test_deprecated.py index 3345449..bf0d171 100644 --- a/tests/test_deprecated.py +++ b/tests/test_deprecated.py @@ -1,6 +1,5 @@ from pyhepmc._deprecated import deprecated import pytest -import numpy as np class Foo: @@ -8,15 +7,25 @@ class Foo: def bar(self): return 0 + @deprecated("use baz", removal="1000.0.0") + def foo(self): + return 0 + def test_deprecated(): foo = Foo() - with pytest.raises(ValueError, match="reason.*required"): + with pytest.raises(AssertionError): @deprecated def bad(self): return 0 - with pytest.warns(np.VisibleDeprecationWarning, match="bar is deprecated: use baz"): + with pytest.warns(DeprecationWarning, match="bar is deprecated: use baz"): foo.bar() + + with pytest.warns( + DeprecationWarning, + match="foo is deprecated and will be removed in version 1000.0.0: use baz", + ): + foo.foo() diff --git a/tests/test_io.py b/tests/test_io.py index 31d6ec8..66f5aaa 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -6,7 +6,6 @@ from pyhepmc._core import stringstream, pyiostream from io import BytesIO from pathlib import Path -import numpy as np import typing import gzip from sys import version_info @@ -476,7 +475,7 @@ def test_open_standalone(evt): # noqa def test_deprecated_import(): - with pytest.warns(np.VisibleDeprecationWarning): + with pytest.warns(DeprecationWarning): from pyhepmc import ReaderAscii # noqa F401 diff --git a/tests/test_view.py b/tests/test_view.py index ae6580d..e8c2186 100644 --- a/tests/test_view.py +++ b/tests/test_view.py @@ -121,13 +121,17 @@ def test_savefig_1(evt, ext): assert np.sum(a1[:size] != a2[:size]) < 150 -def test_savefig_2(evt): +def test_savefig_2a(evt): with pytest.raises(ValueError): view.savefig(evt, "foo") + +def test_savefig_2b(evt): with pytest.raises(ValueError): view.savefig(evt, "foo.foo") + +def test_savefig_2c(evt): with pytest.raises(ValueError): with io.BytesIO() as f: view.savefig(evt, f)