diff --git a/.idea/pytools.iml b/.idea/pytools.iml
index f5c7f766..a8c82518 100644
--- a/.idea/pytools.iml
+++ b/.idea/pytools.iml
@@ -16,7 +16,7 @@
-
+
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index cfb4ef8b..75e7c27c 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -50,6 +50,6 @@ repos:
language_version: python310
pass_filenames: false
additional_dependencies:
- - numpy~=1.24
+ - numpy~=2.0
- pytest
- packaging
diff --git a/RELEASE_NOTES.rst b/RELEASE_NOTES.rst
index b487cdf3..de391330 100644
--- a/RELEASE_NOTES.rst
+++ b/RELEASE_NOTES.rst
@@ -11,6 +11,14 @@ Release Notes
*pytools* 3.0 adds support for language features introduced up to and including
Python 3.10, and drops support for Python versions.
+*pytools* 3.0.2
+~~~~~~~~~~~~~~~
+
+- BUILD: :mod:`numpy` |nbsp| 2 is now supported
+- FIX: :func:`.issubclass_generic` now supports unions, tuples of types, and ``None``,
+ and uses clearer error messages if called with invalid arguments
+
+
*pytools* 3.0.1
~~~~~~~~~~~~~~~
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index e1b6aaa8..939c73cf 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -89,7 +89,7 @@ stages:
- script: |
# package dependencies for mypy
dependencies=(
- numpy~=1.24
+ numpy~=2.0
packaging
pytest
)
diff --git a/environment.yml b/environment.yml
index ae2a1280..317b5466 100644
--- a/environment.yml
+++ b/environment.yml
@@ -5,7 +5,7 @@ dependencies:
# run
- joblib ~= 1.2
- matplotlib ~= 3.6
- - numpy ~= 1.24
+ - numpy ~= 2.0
- pandas ~= 2.0
- python ~= 3.10.14
- scipy ~= 1.10
diff --git a/pyproject.toml b/pyproject.toml
index 8e7ad8b5..7dd5ad8d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -16,7 +16,7 @@ license = "Apache Software License v2.0"
requires = [
"joblib ~=1.0",
"matplotlib ~=3.6",
- "numpy >=1.23,<2a", # cannot use ~= due to conda bug
+ "numpy >=1.23,<3a", # cannot use ~= due to conda bug
"pandas >=1.5",
"scipy ~=1.9",
"typing_inspect ~=0.7",
@@ -80,7 +80,7 @@ typing_extensions = "~=4.0.0"
# maximum requirements of gamma-pytools
joblib = "~=1.3"
matplotlib = "~=3.8"
-numpy = ">=1.26,<2a" # cannot use ~= due to conda bug
+numpy = ">=2,<3a" # cannot use ~= due to conda bug
pandas = "~=2.0"
python = ">=3.12,<4a" # cannot use ~= due to conda bug
scipy = "~=1.12"
diff --git a/src/pytools/__init__.py b/src/pytools/__init__.py
index 819356e4..b96059f0 100644
--- a/src/pytools/__init__.py
+++ b/src/pytools/__init__.py
@@ -2,4 +2,4 @@
A collection of Python extensions and tools used in BCG GAMMA's open-source libraries.
"""
-__version__ = "3.0.1"
+__version__ = "3.0.2"
diff --git a/src/pytools/data/_linkage.py b/src/pytools/data/_linkage.py
index eca40d96..163ed7e5 100644
--- a/src/pytools/data/_linkage.py
+++ b/src/pytools/data/_linkage.py
@@ -32,7 +32,7 @@
# Type variables
#
-LinkageMatrix = npt.NDArray[np.float_]
+LinkageMatrix = npt.NDArray[np.float64]
#
diff --git a/src/pytools/data/_matrix.py b/src/pytools/data/_matrix.py
index c01c1b21..ff5250bf 100644
--- a/src/pytools/data/_matrix.py
+++ b/src/pytools/data/_matrix.py
@@ -32,7 +32,7 @@
#
T_Matrix = TypeVar("T_Matrix", bound="Matrix[Any]")
-T_Number = TypeVar("T_Number", bound="np.number[npt.NBitBase]")
+T_Number = TypeVar("T_Number", bound="np.number[Any]")
#
# Ensure all symbols introduced below are included in __all__
@@ -59,7 +59,7 @@ class Matrix(HasExpressionRepr, Generic[T_Number]):
names: tuple[npt.NDArray[Any] | None, npt.NDArray[Any] | None]
#: the weights of the rows and columns
- weights: tuple[npt.NDArray[np.float_] | None, npt.NDArray[np.float_] | None]
+ weights: tuple[npt.NDArray[np.float64] | None, npt.NDArray[np.float64] | None]
#: the labels for the row and column axes
name_labels: tuple[str | None, str | None]
@@ -155,8 +155,8 @@ def _arg_to_array(
else:
def _ensure_positive(
- w: npt.NDArray[np.float_] | None, axis: int
- ) -> npt.NDArray[np.float_] | None:
+ w: npt.NDArray[np.float64] | None, axis: int
+ ) -> npt.NDArray[np.float64] | None:
if w is not None and (w < 0).any():
raise ValueError(
f"arg weights[{axis}] should be all positive, "
@@ -352,7 +352,7 @@ def _message(error: str) -> str:
def _top_items_mask(
- weights: npt.NDArray[np.float_] | None,
+ weights: npt.NDArray[np.float64] | None,
current_size: int,
target_size: tuple[int | None, float | None],
) -> npt.NDArray[np.bool_]:
@@ -385,7 +385,7 @@ def _top_items_mask(
# THe target weight is expressed as a ratio of total weight
# (0 < target_ratio <= 1).
- weights_sorted_cumsum: npt.NDArray[np.float_] = weights[
+ weights_sorted_cumsum: npt.NDArray[np.float64] = weights[
ix_weights_descending_stable
].cumsum()
mask[
@@ -401,12 +401,12 @@ def _top_items_mask(
def _resize_rows(
values: npt.NDArray[T_Number],
- weights: npt.NDArray[np.float_] | None,
+ weights: npt.NDArray[np.float64] | None,
names: npt.NDArray[Any] | None,
current_size: int,
target_size: tuple[int | None, float | None],
) -> tuple[
- npt.NDArray[T_Number], npt.NDArray[np.float_] | None, npt.NDArray[Any] | None
+ npt.NDArray[T_Number], npt.NDArray[np.float64] | None, npt.NDArray[Any] | None
]:
mask = _top_items_mask(
weights=weights, current_size=current_size, target_size=target_size
diff --git a/src/pytools/typing/_typing.py b/src/pytools/typing/_typing.py
index f989289b..479a153c 100644
--- a/src/pytools/typing/_typing.py
+++ b/src/pytools/typing/_typing.py
@@ -26,7 +26,7 @@
Sequence,
ValuesView,
)
-from types import GenericAlias
+from types import GenericAlias, NoneType, UnionType
from typing import (
AbstractSet,
Any,
@@ -373,13 +373,13 @@ def get_type_arguments(obj: Any, base: type) -> list[tuple[type, ...]]:
return list(map(get_args, get_generic_instance(ti.get_generic_type(obj), base)))
-def issubclass_generic(subclass: type | Never, base: type | Never) -> bool:
+def issubclass_generic(subclass: Any, base: Any) -> bool:
"""
Check if a class is a subclass of a generic instance, i.e., it is a subclass of the
generic class, and has compatible type arguments.
- :param subclass: the class to check
- :param base: the generic class to check against
+ :param subclass: the (potentially generic) subclass to check
+ :param base: the (potentially generic) base class to check against
:return: ``True`` if the class is a subclass of the generic instance, ``False``
otherwise
"""
@@ -396,16 +396,56 @@ def issubclass_generic(subclass: type | Never, base: type | Never) -> bool:
elif base is Never:
return False
+ # Special case: if the subclass is a union type, check if all types in the union are
+ # subclasses of the base class
+ if get_origin(subclass) in (typing.Union, UnionType):
+ return all(issubclass_generic(arg, base) for arg in get_args(subclass))
+
+ # Special case: if the base class is a union type, check if the subclass is a
+ # subclass of at least one of the types in the union
+ if get_origin(base) in (typing.Union, UnionType):
+ return any(issubclass_generic(subclass, arg) for arg in get_args(base))
+
+ # Special case: if the base class is a tuple, check if the subclass is a subclass of
+ # at least one type in the tuple
+ if isinstance(base, tuple):
+ try:
+ return any(issubclass_generic(subclass, arg) for arg in base)
+ except TypeError as e:
+ raise TypeError(
+ f"isinstance_generic() arg 2 must be a type, type-like, or tuple of "
+ f"types or type-likes, but got {base!r}"
+ ) from e
+
+ # Typehints can contain `None` as a shorthand for `NoneType`; replace it with the
+ # actual type
+ if subclass is None:
+ subclass = NoneType
+ if base is None:
+ base = NoneType
+
+ # Replace deprecated types in typing with their canonical replacements in
+ # collections.abc
subclass = _replace_deprecated_type(subclass)
base = _replace_deprecated_type(base)
# Get the non-generic origin of the base class
base_origin = get_origin(base) or base
+ if not isinstance(base_origin, type):
+ raise TypeError(
+ f"isinstance_generic() arg 2 must be a type, type-like, or tuple of types "
+ f"or type-likes, but got {base!r}"
+ )
# If the non-generic origin of the subclass is not a subclass of the non-generic
# origin of the base class, the subclass cannot be a subclass of the base class
subclass_origin = get_origin(subclass) or subclass
- if not issubclass(subclass_origin, base_origin):
+ if not isinstance(subclass_origin, type):
+ raise TypeError(
+ f"isinstance_generic() arg 1 must be a type or type-like, but got "
+ f"{subclass!r}"
+ )
+ elif not issubclass(subclass_origin, base_origin):
return False
# If the base class is not a generic class, there are no type arguments to check
@@ -567,7 +607,7 @@ def _get_origin_parameters(
)
-def _replace_deprecated_type(tp: type) -> type:
+def _replace_deprecated_type(tp: T) -> T:
"""
Replace deprecated types in :mod:`typing` with their canonical replacements in
:mod:`collections.abc`.
@@ -577,18 +617,23 @@ def _replace_deprecated_type(tp: type) -> type:
deprecated
"""
- if tp.__module__ == "typing":
+ origin: type | None = get_origin(tp)
+ if (
# Check if the same type is defined in collections.abc
- origin: type | None = get_origin(tp)
- if origin is not None and origin.__module__ == "collections.abc":
- log.warning(
- f"Type typing.{tp.__name__} is deprecated; "
- f"please use {origin.__module__}.{origin.__name__} instead"
- )
- args: tuple[type, ...] = get_args(tp)
- if args:
- # If the type has arguments, apply the same arguments to the replacement
- return cast(type, origin[args]) # type: ignore[index]
- else:
- return origin
+ origin is not None
+ and tp.__module__ == "typing"
+ and origin.__module__ == "collections.abc"
+ ):
+ log.warning(
+ "Type typing.%s is deprecated; please use %s.%s instead",
+ tp.__name__, # type: ignore[attr-defined]
+ origin.__module__,
+ origin.__name__,
+ )
+ args: tuple[type, ...] = get_args(tp)
+ if args:
+ # If the type has arguments, apply the same arguments to the replacement
+ return cast(T, origin[args]) # type: ignore[index]
+ else:
+ return cast(T, origin)
return tp
diff --git a/src/pytools/viz/_matplot.py b/src/pytools/viz/_matplot.py
index 9b14ccbe..8cefa669 100644
--- a/src/pytools/viz/_matplot.py
+++ b/src/pytools/viz/_matplot.py
@@ -304,13 +304,13 @@ def color_for_value(self, z: int | float) -> RgbaColor:
pass
@overload
- def color_for_value(self, z: npt.NDArray[np.float_]) -> npt.NDArray[np.float_]:
+ def color_for_value(self, z: npt.NDArray[np.float64]) -> npt.NDArray[np.float64]:
"""[overload]"""
pass
def color_for_value(
- self, z: int | float | npt.NDArray[np.float_]
- ) -> RgbaColor | npt.NDArray[np.float_]:
+ self, z: int | float | npt.NDArray[np.float64]
+ ) -> RgbaColor | npt.NDArray[np.float64]:
"""
Get the color(s) associated with the given value(s), based on the color map and
normalization defined for this style.
diff --git a/src/pytools/viz/dendrogram/_style.py b/src/pytools/viz/dendrogram/_style.py
index 6e053bc7..a031ac61 100644
--- a/src/pytools/viz/dendrogram/_style.py
+++ b/src/pytools/viz/dendrogram/_style.py
@@ -222,7 +222,7 @@ def draw_leaf_labels(
def _get_ytick_locations(
self, *, weights: Sequence[float]
- ) -> npt.NDArray[np.float_]:
+ ) -> npt.NDArray[np.float64]:
"""
Get the tick locations for the y axis.
@@ -231,7 +231,7 @@ def _get_ytick_locations(
"""
weights_array = np.array(weights)
# noinspection PyTypeChecker
- ytick_locations: npt.NDArray[np.float_] = -(
+ ytick_locations: npt.NDArray[np.float64] = -(
np.arange(len(weights)) * self.padding
+ weights_array.cumsum()
- weights_array / 2
diff --git a/src/pytools/viz/matrix/_matrix.py b/src/pytools/viz/matrix/_matrix.py
index bc1e02b9..308230d9 100644
--- a/src/pytools/viz/matrix/_matrix.py
+++ b/src/pytools/viz/matrix/_matrix.py
@@ -155,16 +155,16 @@ def draw_matrix(
npt.NDArray[Any] | None,
],
weights: tuple[
- npt.NDArray[np.float_] | None,
- npt.NDArray[np.float_] | None,
+ npt.NDArray[np.float64] | None,
+ npt.NDArray[np.float64] | None,
],
) -> None:
"""[see superclass]"""
ax: Axes = self.ax
colors = self.colors
- weights_rows: npt.NDArray[np.float_]
- weights_columns: npt.NDArray[np.float_]
+ weights_rows: npt.NDArray[np.float64]
+ weights_columns: npt.NDArray[np.float64]
# replace undefined weights with all ones
weights_rows, weights_columns = tuple(
np.ones(n) if w is None else w for w, n in zip(weights, data.shape)
@@ -172,8 +172,8 @@ def draw_matrix(
# calculate the horizontal and vertical matrix cell bounds based on the
# cumulative sums of the axis weights; default all weights to 1 if not defined
- column_bounds: npt.NDArray[np.float_]
- row_bounds: npt.NDArray[np.float_]
+ column_bounds: npt.NDArray[np.float64]
+ row_bounds: npt.NDArray[np.float64]
row_bounds = -np.array([0, *weights_rows]).cumsum()
column_bounds = np.array([0, *weights_columns]).cumsum()
@@ -200,7 +200,7 @@ def draw_matrix(
# draw the matrix cells
for c, (x0, x1) in enumerate(zip(column_bounds, column_bounds[1:])):
for r, (y1, y0) in enumerate(zip(row_bounds, row_bounds[1:])):
- color: npt.NDArray[np.float_] = cell_colors[r, c]
+ color: npt.NDArray[np.float64] = cell_colors[r, c]
ax.add_patch(
Rectangle(
(
@@ -224,7 +224,7 @@ def draw_matrix(
y_tick_locations = (row_bounds[:-1] + row_bounds[1:]) / 2
def _set_ticks(
- tick_locations: npt.NDArray[np.float_],
+ tick_locations: npt.NDArray[np.float64],
tick_labels: npt.NDArray[Any],
axis: Axis,
tick_params: dict[str, Any],
@@ -461,15 +461,15 @@ def draw_matrix(
npt.NDArray[Any] | None,
],
weights: tuple[
- npt.NDArray[np.float_] | None,
- npt.NDArray[np.float_] | None,
+ npt.NDArray[np.float64] | None,
+ npt.NDArray[np.float64] | None,
],
) -> None:
"""[see superclass]"""
def _axis_marks(
axis_names: npt.NDArray[Any] | None,
- axis_weights: npt.NDArray[np.float_] | None,
+ axis_weights: npt.NDArray[np.float64] | None,
) -> Iterable[str] | None:
axis_names_iter: Iterable[Any]
diff --git a/src/pytools/viz/matrix/base/_base.py b/src/pytools/viz/matrix/base/_base.py
index 5b4dc4c8..d0c52ac0 100644
--- a/src/pytools/viz/matrix/base/_base.py
+++ b/src/pytools/viz/matrix/base/_base.py
@@ -67,8 +67,8 @@ def draw_matrix(
npt.NDArray[Any] | None,
],
weights: tuple[
- npt.NDArray[np.float_] | None,
- npt.NDArray[np.float_] | None,
+ npt.NDArray[np.float64] | None,
+ npt.NDArray[np.float64] | None,
],
) -> None:
"""
diff --git a/test/test/pytools/test_typing.py b/test/test/pytools/test_typing.py
index 8788cd02..e9f29661 100644
--- a/test/test/pytools/test_typing.py
+++ b/test/test/pytools/test_typing.py
@@ -11,6 +11,7 @@
from typing import Any, Generic, TypeVar
import pytest
+from typing_extensions import Never
from pytools.typing import get_generic_instance, issubclass_generic
@@ -106,6 +107,43 @@ def test_issubclass_generic() -> None:
AsyncIterator_typing[Mapping[str, int]], AsyncIterable[Mapping[str, Any]]
)
+ assert issubclass_generic(int, float | int)
+ assert issubclass_generic(float, float | int)
+ assert issubclass_generic(int | float, float | int)
+ assert issubclass_generic(int | float, typing.Union[float | int])
+
+ assert issubclass_generic(int, (float, int))
+
+ assert issubclass_generic(Never, None)
+ assert issubclass_generic(None, None)
+ assert not issubclass_generic(None, int)
+
+ with pytest.raises(
+ TypeError,
+ match=(
+ r"^isinstance_generic\(\) arg 2 must be a type, type-like, or tuple of "
+ r"types or type-likes, but got 3$"
+ ),
+ ):
+ issubclass_generic(int, 3)
+
+ with pytest.raises(
+ TypeError,
+ match=(
+ r"^isinstance_generic\(\) arg 2 must be a type, type-like, or tuple of "
+ r"types or type-likes, but got \(3, \)$"
+ ),
+ ):
+ issubclass_generic(int, (3, int))
+
+ with pytest.raises(
+ TypeError,
+ match=(
+ r"^isinstance_generic\(\) arg 1 must be a type or type-like, but got 3$"
+ ),
+ ):
+ issubclass_generic(3, int)
+
def test_get_generic_instance() -> None:
diff --git a/test/test/pytools/viz/dendrogram/test_dendrogram.py b/test/test/pytools/viz/dendrogram/test_dendrogram.py
index 476c4144..b9b09ba0 100644
--- a/test/test/pytools/viz/dendrogram/test_dendrogram.py
+++ b/test/test/pytools/viz/dendrogram/test_dendrogram.py
@@ -25,7 +25,7 @@ def linkage_matrix() -> npt.NDArray[np.int_]:
@pytest.fixture
-def linkage_tree(linkage_matrix: npt.NDArray[np.float_]) -> LinkageTree:
+def linkage_tree(linkage_matrix: npt.NDArray[np.float64]) -> LinkageTree:
"""Create a linkage tree for drawing tests."""
return LinkageTree(
scipy_linkage_matrix=linkage_matrix,
@@ -34,7 +34,7 @@ def linkage_tree(linkage_matrix: npt.NDArray[np.float_]) -> LinkageTree:
)
-def test_dendrogram_drawer_text(linkage_matrix: npt.NDArray[np.float_]) -> None:
+def test_dendrogram_drawer_text(linkage_matrix: npt.NDArray[np.float64]) -> None:
leaf_names = list("ABCDEFGH")
leaf_weights = [(w + 1) / 36 for w in range(8)]