diff --git a/.github/workflows/quality-checks.yaml b/.github/workflows/quality-checks.yaml
index c1053b44..7c787550 100644
--- a/.github/workflows/quality-checks.yaml
+++ b/.github/workflows/quality-checks.yaml
@@ -16,7 +16,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
- python-version: ["3.9", "3.10", "3.11", "3.12"]
+ python-version: ["3.10", "3.11", "3.12"]
fail-fast: false
runs-on: ${{ matrix.os }}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 399859e3..cd176bbb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
# CHANGELOG
+## v0.4.2 - 11.02.2026
+Maintance release (https://github.com/geometric-kernels/GeometricKernels/pull/173).
+* Fix compatibility with modern `plum-dispatch`/`numpy`.
+* Replace legacy `plum` typing aliases with modern Python type syntax.
+* Drop Python 3.9 support.
+* Discontinue testing for `gpflow` due to incompatibility with the latest `setuptools`.
+
## v0.4.1 - 15.12.2025
* HammingGraph space by @colmont in https://github.com/geometric-kernels/GeometricKernels/pull/170
* Added a link to a pull request example in README by @vabor112 in https://github.com/geometric-kernels/GeometricKernels/pull/171
diff --git a/Makefile b/Makefile
index d1a21268..6c3c2f5e 100644
--- a/Makefile
+++ b/Makefile
@@ -43,10 +43,6 @@ lint: sync
test: sync ## Run the tests, start with the failing ones and break on first fail.
@$(UV_RUN) pytest -v -x --ff -rN -Wignore -s --tb=short --durations=0 --cov --cov-report=xml --cov-report=html:coverage_html tests
- @$(UV_RUN) pytest --nbmake --nbmake-kernel=python3 --durations=0 --nbmake-timeout=1000 --ignore=notebooks/frontends/GPJax.ipynb notebooks/
- @if [ "$(UV_PYTHON)" = "python3.9" ]; then \
- echo "Skipping GPJax notebook on python3.9"; \
- else \
- $(UV_RUN) pytest --nbmake --nbmake-kernel=python3 --durations=0 --nbmake-timeout=1000 notebooks/frontends/GPJax.ipynb; \
- fi;
+ # gpflow is ignored due to incompatibility with the recent setuptools
+ @$(UV_RUN) pytest --nbmake --nbmake-kernel=python3 --durations=0 --nbmake-timeout=1000 --ignore=notebooks/frontends/GPflow.ipynb notebooks/
@echo -e "$(SUCCESS)Tests done$(RESET)"
diff --git a/docs/index.rst b/docs/index.rst
index d5698e32..3cb2824c 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -33,7 +33,7 @@ Before doing anything, you might want to create and activate a new virtual envir
uv venv --python python[version] [venv_dir]
-where [env_dir] is the directory (default is `.venv`) of the environment and [version] is the version of Python you want to use, we currently support 3.9, 3.10, 3.11, 3.12.
+where [env_dir] is the directory (default is `.venv`) of the environment and [version] is the version of Python you want to use, we currently support 3.10, 3.11, 3.12.
[Optional] activate the environment. However, this is not strictly necessary for `uv`. Instead, use tools like `uv run python` to run Python inside the environment. See `uv documentation ` for more details.
@@ -65,7 +65,7 @@ where [env_dir] is the directory (default is `.venv`) of the environment and [ve
conda create -n [env_name] python=[version]
conda activate [env_name]
-where [env_name] is the name of the environment and [version] is the version of Python you want to use, we currently support 3.9, 3.10, 3.11, 3.12.
+where [env_name] is the name of the environment and [version] is the version of Python you want to use, we currently support 3.10, 3.11, 3.12.
.. raw:: html
@@ -449,4 +449,3 @@ Please also consider citing the theoretical papers the library is based on. You
API reference
Bibliography
GitHub
-
diff --git a/geometric_kernels/frontends/gpflow.py b/geometric_kernels/frontends/gpflow.py
index 172f483d..87fadea4 100644
--- a/geometric_kernels/frontends/gpflow.py
+++ b/geometric_kernels/frontends/gpflow.py
@@ -6,13 +6,40 @@
:doc:`frontends/GPflow.ipynb ` notebook.
"""
-import gpflow
import numpy as np
import tensorflow as tf
from beartype.typing import List, Optional, Union
-from gpflow.base import TensorType
-from gpflow.kernels.base import ActiveDims
-from gpflow.utilities import positive
+
+try:
+ import gpflow
+ from gpflow.base import TensorType
+ from gpflow.kernels.base import ActiveDims
+ from gpflow.utilities import positive
+except ImportError as error:
+ seen = set()
+ current: BaseException | None = error
+ missing_pkg_resources = False
+ while current is not None and id(current) not in seen:
+ seen.add(id(current))
+ if getattr(current, "name", None) == "pkg_resources":
+ missing_pkg_resources = True
+ break
+ text = str(current)
+ if "pkg_resources" in text and (
+ "No module named" in text or "cannot import name" in text
+ ):
+ missing_pkg_resources = True
+ break
+ current = current.__cause__ or current.__context__
+
+ if missing_pkg_resources:
+ raise ImportError(
+ "Importing `gpflow` failed because it depends on `pkg_resources`. "
+ "`pkg_resources` was removed from `setuptools==82.0.0`. "
+ "You can try pinning an older `setuptools` version, but this setup "
+ "is no longer tested by GeometricKernels."
+ ) from error
+ raise
from geometric_kernels.kernels import BaseGeometricKernel
from geometric_kernels.spaces import Space
diff --git a/geometric_kernels/lab_extras/extras.py b/geometric_kernels/lab_extras/extras.py
index 8d75a4fd..15f673f7 100644
--- a/geometric_kernels/lab_extras/extras.py
+++ b/geometric_kernels/lab_extras/extras.py
@@ -1,8 +1,6 @@
import lab as B
-from beartype.typing import List
from lab import dispatch
from lab.util import abstract
-from plum import Union
from scipy.sparse import spmatrix
@@ -23,7 +21,7 @@ def take_along_axis(a: B.Numeric, index: B.Numeric, axis: int = 0):
@dispatch
@abstract()
-def from_numpy(_: B.Numeric, b: Union[List, B.Numeric]):
+def from_numpy(_: B.Numeric, b: list | B.Numeric):
"""
Converts the array `b` to a tensor of the same backend as `_`.
@@ -290,7 +288,7 @@ def eigvalsh(x: B.Numeric):
@dispatch
@abstract()
-def reciprocal_no_nan(x: Union[B.Numeric, spmatrix]):
+def reciprocal_no_nan(x: B.Numeric | spmatrix):
"""
Return element-wise reciprocal (1/x). Whenever x = 0 puts 1/x = 0.
@@ -357,9 +355,7 @@ def bool_like(reference: B.Numeric):
"""
-def smart_cast(
- dtype: Union[B.Bool, B.Int, B.Float, B.Complex, B.Numeric], x: B.Numeric
-):
+def smart_cast(dtype: B.Bool | B.Int | B.Float | B.Complex | B.Numeric, x: B.Numeric):
"""
Return `x` cast to the `dtype` abstract data type.
diff --git a/geometric_kernels/lab_extras/jax/extras.py b/geometric_kernels/lab_extras/jax/extras.py
index 974fae99..6455325d 100644
--- a/geometric_kernels/lab_extras/jax/extras.py
+++ b/geometric_kernels/lab_extras/jax/extras.py
@@ -1,14 +1,15 @@
+from typing import TypeAlias
+
import jax.numpy as jnp
import lab as B
-from beartype.typing import List
from lab import dispatch
-from plum import Union, convert
+from plum import convert
-_Numeric = Union[B.Number, B.JAXNumeric]
+_Numeric: TypeAlias = B.Number | B.JAXNumeric
@dispatch
-def take_along_axis(a: Union[_Numeric, B.Numeric], index: _Numeric, axis: int = 0) -> _Numeric: # type: ignore
+def take_along_axis(a: _Numeric | B.Numeric, index: _Numeric, axis: int = 0) -> _Numeric: # type: ignore
"""
Gathers elements of `a` along `axis` at `index` locations.
"""
@@ -18,7 +19,7 @@ def take_along_axis(a: Union[_Numeric, B.Numeric], index: _Numeric, axis: int =
@dispatch
-def from_numpy(_: B.JAXNumeric, b: Union[List, B.NPNumeric, B.Number, B.JAXNumeric]): # type: ignore
+def from_numpy(_: B.JAXNumeric, b: list | B.NPNumeric | B.Number | B.JAXNumeric): # type: ignore
"""
Converts the array `b` to a tensor of the same backend as `a`
"""
diff --git a/geometric_kernels/lab_extras/numpy/extras.py b/geometric_kernels/lab_extras/numpy/extras.py
index 01c69168..5167823c 100644
--- a/geometric_kernels/lab_extras/numpy/extras.py
+++ b/geometric_kernels/lab_extras/numpy/extras.py
@@ -1,11 +1,12 @@
+from typing import TypeAlias
+
import lab as B
import numpy as np
-from beartype.typing import Any, List, Optional
+from beartype.typing import Any
from lab import dispatch
-from plum import Union
from scipy.sparse import spmatrix
-_Numeric = Union[B.Number, B.NPNumeric]
+_Numeric: TypeAlias = B.Number | B.NPNumeric
@dispatch
@@ -17,7 +18,7 @@ def take_along_axis(a: _Numeric, index: _Numeric, axis: int = 0) -> _Numeric: #
@dispatch
-def from_numpy(_: B.NPNumeric, b: Union[List, B.NPNumeric, B.Number]): # type: ignore
+def from_numpy(_: B.NPNumeric, b: list | B.NPNumeric | B.Number): # type: ignore
"""
Converts the array `b` to a tensor of the same backend as `a`
"""
@@ -33,7 +34,7 @@ def trapz(y: _Numeric, x: _Numeric, dx: _Numeric = 1.0, axis: int = -1): # type
@dispatch
-def norm(x: _Numeric, ord: Optional[Any] = None, axis: Optional[int] = None): # type: ignore
+def norm(x: _Numeric, ord: Any | None = None, axis: int | None = None): # type: ignore
"""
Matrix or vector norm.
"""
diff --git a/geometric_kernels/lab_extras/tensorflow/extras.py b/geometric_kernels/lab_extras/tensorflow/extras.py
index 594be7f9..90606723 100644
--- a/geometric_kernels/lab_extras/tensorflow/extras.py
+++ b/geometric_kernels/lab_extras/tensorflow/extras.py
@@ -1,13 +1,12 @@
-import sys
+from typing import TypeAlias
import lab as B
import tensorflow as tf
import tensorflow_probability as tfp
-from beartype.typing import Any, List, Optional
+from beartype.typing import Any
from lab import dispatch
-from plum import Union
-_Numeric = Union[B.Number, B.TFNumeric, B.NPNumeric]
+_Numeric: TypeAlias = B.Number | B.TFNumeric | B.NPNumeric
@dispatch
@@ -15,15 +14,11 @@ def take_along_axis(a: _Numeric, index: _Numeric, axis: int = 0) -> _Numeric: #
"""
Gathers elements of `a` along `axis` at `index` locations.
"""
- if sys.version_info[:2] <= (3, 9):
- index = tf.cast(index, tf.int32)
- return tf.experimental.numpy.take_along_axis(
- a, index, axis=axis
- ) # the absence of explicit cast to int64 causes an error for Python 3.9 and below
+ return tf.experimental.numpy.take_along_axis(a, index, axis=axis)
@dispatch
-def from_numpy(_: B.TFNumeric, b: Union[List, B.Numeric, B.NPNumeric, B.TFNumeric]): # type: ignore
+def from_numpy(_: B.TFNumeric, b: list | B.Numeric | B.NPNumeric | B.TFNumeric): # type: ignore
"""
Converts the array `b` to a tensor of the same backend as `a`
"""
@@ -39,7 +34,7 @@ def trapz(y: _Numeric, x: _Numeric, dx=None, axis=-1): # type: ignore
@dispatch
-def norm(x: _Numeric, ord: Optional[Any] = None, axis: Optional[int] = None): # type: ignore
+def norm(x: _Numeric, ord: Any | None = None, axis: int | None = None): # type: ignore
"""
Matrix or vector norm.
"""
diff --git a/geometric_kernels/lab_extras/torch/extras.py b/geometric_kernels/lab_extras/torch/extras.py
index 2f80d42c..771aa205 100644
--- a/geometric_kernels/lab_extras/torch/extras.py
+++ b/geometric_kernels/lab_extras/torch/extras.py
@@ -1,14 +1,15 @@
+from typing import TypeAlias
+
import lab as B
import torch
-from beartype.typing import Any, List, Optional
+from beartype.typing import Any
from lab import dispatch
-from plum import Union
-_Numeric = Union[B.Number, B.TorchNumeric]
+_Numeric: TypeAlias = B.Number | B.TorchNumeric
@dispatch
-def take_along_axis(a: Union[_Numeric, B.Numeric], index: _Numeric, axis: int = 0) -> _Numeric: # type: ignore
+def take_along_axis(a: _Numeric | B.Numeric, index: B.TorchNumeric, axis: int = 0) -> _Numeric: # type: ignore
"""
Gathers elements of `a` along `axis` at `index` locations.
"""
@@ -19,7 +20,7 @@ def take_along_axis(a: Union[_Numeric, B.Numeric], index: _Numeric, axis: int =
@dispatch
def from_numpy(
- a: B.TorchNumeric, b: Union[List, B.Number, B.NPNumeric, B.TorchNumeric]
+ a: B.TorchNumeric, b: list | B.Number | B.NPNumeric | B.TorchNumeric
): # type: ignore
"""
Converts the array `b` to a tensor of the same backend as `a`
@@ -38,7 +39,7 @@ def trapz(y: B.TorchNumeric, x: _Numeric, dx: _Numeric = 1.0, axis: int = -1):
@dispatch
-def norm(x: _Numeric, ord: Optional[Any] = None, axis: Optional[int] = None): # type: ignore
+def norm(x: _Numeric, ord: Any | None = None, axis: int | None = None): # type: ignore
"""
Matrix or vector norm.
"""
diff --git a/pyproject.toml b/pyproject.toml
index 9bfe07ed..a8432136 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -11,7 +11,6 @@ description="A Python Package offering geometric kernels in NumPy, TensorFlow, P
readme = "README.md"
classifiers = [
"License :: OSI Approved :: Apache Software License",
- "Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
@@ -21,21 +20,21 @@ classifiers = [
keywords=[
"geometric-kernels",
]
-requires-python = ">=3.9"
+requires-python = ">=3.10"
dependencies = [
- "backends", # >=1.7",
+ "backends>=1.8.0",
"einops",
"geomstats",
- "numpy>=2.0",
+ "numpy>=2.0,<2.4", # constraining from above due to geomstats==2.8.0 incompatibility
"opt-einsum",
- "plum-dispatch",
+ "plum-dispatch>=2.6.0",
"potpourri3d",
"robust_laplacian",
"scipy>=1.3",
"spherical-harmonics-basis",
"sympy~=1.13",
]
-version="0.4.1"
+version="0.4.2"
[project.urls]
Documentation = "https://geometric-kernels.github.io/"
@@ -58,7 +57,7 @@ allow_redefinition = true
[tool.black]
line-length = 88
-target-version = ['py39', 'py310', 'py311', 'py312']
+target-version = ['py310', 'py311', 'py312']
[tool.uv]
@@ -106,5 +105,5 @@ dev = [
'jaxlib',
'jaxtyping',
'optax',
- 'gpjax>=0.12.2; python_version >= "3.10"', # gpjax is not supported on python-3.9 or older.
+ 'gpjax>=0.12.2; python_version >= "3.10"', # gpjax requires python >= 3.10.
]
diff --git a/tests/spaces/test_hamming_graph.py b/tests/spaces/test_hamming_graph.py
index 55cefa34..b78bbfd9 100644
--- a/tests/spaces/test_hamming_graph.py
+++ b/tests/spaces/test_hamming_graph.py
@@ -1,7 +1,6 @@
import lab as B
import numpy as np
import pytest
-from plum import Tuple
from geometric_kernels.kernels import MaternGeometricKernel
from geometric_kernels.spaces import HammingGraph, HypercubeGraph
@@ -11,7 +10,7 @@
@pytest.fixture(params=[(1, 2), (2, 2), (5, 2), (10, 2), (10, 4)])
-def inputs(request) -> Tuple[B.Numeric]:
+def inputs(request) -> tuple[B.Numeric]:
"""
Returns a tuple (space, eigenfunctions, X, X2, weights) where:
- space is a HammingGraph object with (dim, n_cat) equal to request.param,
diff --git a/tests/spaces/test_hypercube_graph.py b/tests/spaces/test_hypercube_graph.py
index cb5ac9c9..fa820137 100644
--- a/tests/spaces/test_hypercube_graph.py
+++ b/tests/spaces/test_hypercube_graph.py
@@ -1,7 +1,6 @@
import lab as B
import numpy as np
import pytest
-from plum import Tuple
from geometric_kernels.kernels import MaternGeometricKernel
from geometric_kernels.spaces import HypercubeGraph
@@ -11,7 +10,7 @@
@pytest.fixture(params=[1, 2, 3, 5, 10])
-def inputs(request) -> Tuple[B.Numeric]:
+def inputs(request) -> tuple[B.Numeric]:
"""
Returns a tuple (space, eigenfunctions, X, X2) where:
- space is a HypercubeGraph object with dimension equal to request.param,