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)]